本記事はGCP(Google Cloud Platform) Advent Calendar 2022 3日目の記事です。2日目はGoogle Cloud Storage(GCS)上の画像やJSにIP制限をかけるでした。
Overview
エムスリーエンジニアリンググループ AI・機械学習チームでソフトウェアエンジニアをしている中村(po3rin) です。検索とGoが好きです。
今回は社内のとあるプロジェクトでIdentity-Aware Platform * Identity Platformによるメアド/パスワード認証を採用しました。この記事では技術的な紹介と採用した所感をお伝えします。
- Overview
- 技術的な要請
- 採用した構成
- ログインページのカスタマイズ
- 署名ヘッダーによるアプリケーションの保護
- ユーザー情報管理
- ログアウト処理
- Amazon CloudWatch Synthetics での外形監視
- 現状のめんどいポイント
- 所感とまとめ
技術的な要請
とあるアプリケーションのリリースにあたり、下記の要請がありました。
- GKE上にリリースしたい
- 面倒な認証を一瞬でつけたい
- 認証画面なんて作りたくない
1点目に関してはAI・機械学習チームではほとんど全てのアプリケーション、ジョブなどをGKEにデプロイしており、デプロイのための高速道路が既に整備されているので、今回もGKEにデプロイしたいと考えました。
2,3点目に関しては早くリリースしてユーザーの反応を見て開発を進めたいので、できれば認証も楽で最速な手段でつけたいという想いがありました。
採用した構成
そこでIdentity-Aware Proxy × Identity Platformの構成を採用しました。
APIへのアクセスがあると、IngressでBackendConfigとして指定したIdentity-Aware Proxyがプロキシし、認証画面を返します。Identity Platformによる認証が成功したら、認証ヘッダーをつけてAPIへリクエストを通します。API内部では署名ヘッダーによるアプリケーションの保護を行なっています。
それぞれの技術について簡単に紹介して行きます。
Identity-Aware Proxy
Identity-Aware Proxyを使用するとアプリケーションの一元的な承認レイヤを確立でき、アプリケーションレベルのアクセス制御モデルを使用できます。例えばIdentity-Aware Proxyによって保護されているアプリケーションに適切なIAMを持つユーザーがプロキシ経由でのみアクセスできるようにするなどの構成が可能です。
GKEで利用したい場合はIngressのBackendConfigとして指定できるのですぐに利用を始めることができます。下記はIngressに紐付けるBackendConfigの一例です。
apiVersion: cloud.google.com/v1 kind: BackendConfig metadata: name: sample-api spec: iap: enabled: true oauthclientCredentials: secretName: oauth-secret
詳細な連携方法はドキュメントをご覧ください。
Identity Platform
Identity Platformは顧客IDおよびアクセス管理(CIAM)プラットフォームであり、メールアドレス/パスワードの認証の他にも、連携IDプロバイダと統合や電話番号による認証なども可能です。Identity Platformで構成したテナントをIdentity-Aware Proxyで利用することによってIdentity Platformの認証をプロキシとして立てることが可能です。
Identity-Aware Proxy × Identity Platformの特徴として、下記のようなデフォルトの認証画面がすぐに利用できるのも魅力的です。
この画面自体はCloud Runでリリースされます。後で説明しますがデザインの簡単なカスタマイズなら環境変数でも可能です。
ログインページのカスタマイズ
デフォルトのログインページのカスタマイズをする方法はざっとあげると2通りあります。
- Cloud Runの環境変数で設定をJSONとして渡してあげる
- Googleアカウントログインを有効にして管理画面で設定JSON編集
2つ目の方法は運用中のアプリケーションだと認証方法を一回変えなくてはいけないので、私は環境変数によるカスタマイズを行なっています。
環境変数でカスタマイズする際には環境変数UI_CONFIG
にUI設定のJSONを渡してあげます。UI_CONFIG
は下記のFirebase UIの設定と同じでタイトルや、アイコン、ボタンの色なども変更できます。
署名ヘッダーによるアプリケーションの保護
IAPが手違いで無効になるなどの障害があるとアプリケーションの保護が外れてしまうのは問題です。 更にアプリケーションを適切に保護するには署名付きヘッダーを使用する必要があります。 署名済みIAPヘッダーをプロキシが付与するのでアプリケーション側でもIAP JWTの検証が可能です。
例えばGoのアプリケーションであれば下記のコードで検証ができます。
import "google.golang.org/api/idtoken" // validateJWTFromComputeEngine validates a JWT found in the // "x-goog-iap-jwt-assertion" header. func validateJWTFromComputeEngine(w io.Writer, iapJWT, projectNumber, backendServiceID string) error { // iapJWT := "YmFzZQ==.ZW5jb2RlZA==.and0" // req.Header.Get("X-Goog-IAP-JWT-Assertion") // projectNumber := "123456789" // backendServiceID := "backend-service-id" ctx := context.Background() aud := fmt.Sprintf("/projects/%s/global/backendServices/%s", projectNumber, backendServiceID) payload, err := idtoken.Validate(ctx, iapJWT, aud) if err != nil { return fmt.Errorf("idtoken.Validate: %v", err) } // payload contains the JWT claims for further inspection or validation fmt.Fprintf(w, "payload: %v", payload) return nil }
詳細はこちらのドキュメントをご覧ください。
ユーザー情報管理
ユーザー情報はコードで管理可能で、ユーザーの作成、検索、変更などが行えます。我々の用途では権限を与えたいユーザーが限られていたので、GoのAdmin SDKでユーザーを生成しました。下記はGoでユーザーを生成する例です。
params := (&auth.UserToCreate{}). Email("user@example.com"). EmailVerified(false). PhoneNumber("+15555550100"). Password("secretPassword"). DisplayName("John Doe"). PhotoURL("http://www.example.com/12345678/photo.png"). Disabled(false) u, err := client.CreateUser(ctx, params) if err != nil { log.Fatalf("error creating user: %v\n", err) } log.Printf("Successfully created user: %v\n", u)
ログアウト処理
ログアウトをする際には下記のようなオプションを持つリンクを持つボタンを用意します。
https://<何かしらのpath>?gcp-iap-mode=GCIP_SIGNOUT
ドキュメントには「App Engine アプリの場合」とありますが、GKE上でも上記のオプションを付与するだけでログアウト処理が出来ました。
Amazon CloudWatch Synthetics での外形監視
ログインが正しく機能しているかの監視は重要です。そのため我々はAmazon CloudWatch Syntheticsによる外形監視を用意しました。 Amazon CloudWatch Syntheticsを採用した理由は、AIチームの外形監視に元々使われていて、既に内部Terraform moduleも用意されていたからです。 Identity-Aware Proxyのメアド/パスワード認証画面であれば下記のJavaScript Canary ScriptをリリースするだけでOKです。ログイン処理を行なった後に、ログイン後にあるclassが存在するかをチェックし、失敗したらスクリーンショットを撮って終了します。
var synthetics = require('Synthetics'); const log = require('SyntheticsLogger'); const puppeteer = require('puppeteer-core'); const pageLoadBlueprint = async function () { // INSERT URL here const URL = process.env.TARGET_URL; let page = await synthetics.getPage(); const response = await page.goto(URL, {waitUntil: 'domcontentloaded', timeout: 5000}); if (!response) { throw "Failed to load page!"; } await page.waitForSelector('input[name="email"]', {timeout: 5000}); await page.type('input[name="email"]', process.env.LOGIN_EMAIL); const clickButton = await page.$('.firebaseui-id-submit'); await clickButton.click(); await page.waitForSelector('input[name="password"]', {timeout: 3000}); await page.type('input[name="password"]', process.env.LOGIN_PASS); const loginButton = await page.$('.firebaseui-id-submit'); await loginButton.click(); await page.waitForSelector(`.${process.env.EXPECTED_CLASS_NAME}`, {timeout: 5000}).then(() => { log.info(`Expected class name ${process.env.EXPECTED_CLASS_NAME} found`); }).catch(e => { const msg = `Failed IAP Login Check. Expected class name ${process.env.EXPECTED_CLASS_NAME} not found` log.error(msg); throw msg }); }; exports.handler = async () => { return await pageLoadBlueprint(); };
Canary Scriptの作成に関しては下記のドキュメントをご覧ください。 docs.aws.amazon.com
現状のめんどいポイント
この構成は非常に良さそうですが弱点もあります。実は外部ID認証を使う際にはTerraformだけだと設定できません。
Terraformのドキュメントにも記載があります。
Google翻訳
宣言型ツールを使用して作成できるのは、内部組織クライアントのみです。外部クライアントは、GCP コンソールを介して手動で作成する必要があります。この制限は、既存の API によるものであり、このツールでのサポート不足ではありません。
そのため、この構成はコンソールをポチポチして作る必要があります。この辺の楽な運用方法については調査中です。
所感とまとめ
上記のコンソールぽちぽちの辛い管理を除けば、Kubernetes上のAPIに楽に認証を挟むことができ非常に便利です。今回のように新しく小さなアプリケーションでサクッと認証をつけたい場合などには非常に便利でした。
更に運用を楽にするためにログイン画面をリリースしているCloud Runの管理をTerraformに移すなどのNext Stepが考えられます。
We're hiring !!!
エムスリーではクラウド技術で日本の医療を前進させたいメンバーを募集中です。
「ちょっと話を聞いてみたいかも」という人はこちらから! jobs.m3.com