本稿はJCB Advent Calendar 2022の12/7の記事です。
JCBデジタルソリューション開発部の四方です。 本記事では、Google Cloud Platform(以下、GCP) のリソースとSpreadsheetで作成した管理台帳のデータを比較するプログラムをリファクタリングした話について記述させていただきます。
背景について
弊社では以下のPCIDSSの要件を満たすため、変更検出メカニズム(以下、監査ツール)をGoogle Apps Script(以下、GAS)を用いて実装、運用しております。
重要なシステムファイル、構成ファイル、またはコンテンツファイルの不正な変更(変更、追加、および削除を含む)を担当者に警告し、重要なファイルの比較を少なくとも週に一度実行するようにソフトウェアを構成する
監査ツールではSpreadsheetによる管理台帳を設計ドキュメントとし、これと実際のプラットフォーム構成が一致しているか確認をしています。 管理台帳と実際の構成に差分がある場合、管理台帳の更新漏れ、もしくは悪意あるユーザによってリソースを変更されているリスクを有するため差分に気付ける仕組みが必要となります。 JDEPではコミュニケーションツールにGoogle Chatを用いているため、以下の図のようにチャットに通知がくるようにしています。
監査ツールが実装された当初はJCB Digital Enablement Platform (JDEP)が現在よりも小規模であり、GCP上のリソース総数も少ないものでした。 しかし、日々開発が進むことによりGCP上のリソースが増加し、ある日を境に監査ツールが正常に動作しなくなるという問題が起こりました。
問題の原因は以下の2つでした。
- GCP上のリソース増加に伴い、一度のAPIリクエストでは全データを取得できなくなったこと
- GCP上のリソース増加に伴い、処理時間が長くなりGASの実行時間制限(6分間)を超えてしまったこと
PCIDSS要件遵守のため、問題の解決が不可欠でした。 そのため、監査ツールのリファクタリングに着手することとなりました。
監査ツールについて
監査ツールでは主にGCPのIAMの差分監査を実施してます。 以下が監査対象にしているリソースの一部です。
- 組織
- フォルダ
- プロジェクト
- バケット
- サブネット
- PubSub
- 請求先アカウント
- サービスアカウント
- Cloud Spannerデータベース
- Artifact Registry
- 等
これらのリソースに対して付与されているIAMについて以下のフローで差分監査を実施します。
- GCP上のリソース一覧の取得
- 各リソースのIAM権限を取得
- 2で取得したIAM権限の情報とSpreadsheet上の記載を比較
- 3の結果をGoogle Chatへ通知
既存のツールではこのフローが1つの関数内で処理される実装になっており、1 ~ 4の処理がGASの実行時間制限に引っかかっていました。 そこでまず、これらの処理を分割しました。
処理の分割
今回のリファクタリングでは、最初に「1. GCP上のリソース一覧の取得」を他の処理と分割しました。 理由としては処理の実行時間の大部分を、このGCP上のリソース一覧の取得が占めていたためです。
GCP上のリソースをREST APIで取得後、中間ファイルとしてSpreadsheet上に出力する処理を実装しました。
後続の処理では、このSpreadsheet上のデータを読み取ることで毎回APIリクエストを投げる必要がなくなるため処理時間の短縮が見込めます。
2 ~ 4の処理についても各リソースごとに処理を実行するように変更しました。
上記に記載した監査対象のリソースを、組織 -> フォルダ -> プロジェクト... -> Artifact Registry
という順番で処理します。
これらの処理はリソースが異なるのみで、差分比較処理自体はほぼ共通の処理となります。そのため、class化を行い実装を簡潔にしました。
このように処理の分割を実施したことで、GASの実行時間制限を越えてしまうという問題は解消することが出来ました。
処理の見直し
残る問題である「一度のAPIリクエストでは全データを取得できなくなったこと」について処理の見直しを行いました。
こちらについては、APIのレスポンス内に含まれるnextPageTokenを用いて解消しました。
実際にGCPプロジェクト内のサービスアカウント一覧を取得する簡易的なコードを例に説明します。 また、使用しているAPIについては公式のリファレンスを参照してください。
// API Endpoint const IAM_BASE_URL = "https://iam.googleapis.com/v1"; // nextPageToken 用の変数を宣言 let pageToken: string; do { let url = `${IAM_BASE_URL}/projects/${projectId}/serviceAccounts`; // もしpageToken がnull ではない場合、url にパラメータとして付与する if (pageToken) { url += `&pageToken=${pageToken}`; } let response: GoogleAppsScript.URL_Fetch.HTTPResponse; try { response = UrlFetchApp.fetch(url, requestOption("get")); } catch (e) { // エラーハンドリングについては割愛します } // ステータスコードを確認し、処理の失敗を検知 const statusCode = response.getResponseCode(); if (statusCode < 200 || statusCode >= 300) { throw new Error( `Failed with Http Status: ${statusCode}.` ); } // API レスポンスから情報を取得 // 本来はas を用いて型を保証してあげるなどするが今回は割愛 const content = JSON.parse(response.getContentText()); // レスポンス内にaccount の情報がなければreturn if (!content.accounts) { return; } // サービスアカウントは配列で取得できるので1件ずつ情報を処理 content.accounts.forEach(serviceAccount => { allServiceAccounts.push([ { // 欲しい情報を取得 id: serviceAccount.name, email: serviceAccount.email, }, ]); }); // レスポンス内にnextPageToken が含まれている場合、変数に格納 pageToken = content.nextPageToken; } while (pageToken);
上記のようにdo while構文を用いてAPIリクエストを実装し、nextPageTokenが取得できる場合にループさせます。 ループ時にはnextPageTokenをパラメータにし、再度APIを叩く処理を記述します。 こうすることで、1回のAPIリクエストで取得できる件数の上限に達する場合でも続けてリソースを取得することが可能となります。
今回のように修正を加えたことで今後GCP上のリソース数が増えても問題なくリソースを取得することが可能となりました。
まとめ
監査ツールが抱えていたリソース数および処理時間の増加に伴う問題は上記の取り組みで解消することが出来ました。 今回のリファクタリングを通して、GAS/Typescriptの実装のスキルが身についたと共に、将来を見据えた実装の重要性を改めて認識いたしました。
本記事では概要程度となったリファクタリングの話ですが、より詳細な実装等についてまた別の記事でも記載できればと考えております。
最後になりますが、JCBでは我々と一緒に働きたいという人材を募集しています。 詳しい募集要項等については採用ページをご覧下さい。
本文および図表中では、「™」、「®」を明記しておりません。 Google Cloud, GCPならびにすべてのGoogleの商標およびロゴはGoogle LLCの商標または登録商標です。 記載されているロゴ、システム名、製品名は各社及び商標権者の登録商標あるいは商標です。