aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

GCP のサービスアカウントキーを使用せず AWS Lambda から Google Cloud Storage へアクセスする

こんにちは。ソリューションプロフェッショナルグループの新崎です。

最近「intdash で収集したデータをユーザー所有のクラウドストレージに転送する」という機能開発を担当しました。ユーザー所有のクラウドストレージにアクセスするためにはユーザー環境の認証情報が必要になります。しかし、認証情報をそのまま受け取ってしまうとセキュリティ対策として管理コストが大きくなったり、ユーザー自身で頻繁に認証情報のローテーションをしていただかなければならなくなったりと、サービスとしてのユーザビリティにも影響が出てしまいます。

今回の開発ではワークロードを AWS Lambda 上に構築していましたので、「ユーザー所有のクラウドストレージ」が Amazon S3 であれば AWS Security Token Service の AssumeRole を利用して一時トークンを発行する方式を採用することで、これらの課題を解決できます。本記事ではさらに一歩踏み込んで「ユーザー所有のクラウドストレージ」が AWS の外、具体的には GCP の Cloud Storage だった場合についても同様の一時トークン方式で実現させる方法をご紹介したいと思います。

Workload Identity 連携

サービスアカウントキーを使用せずに GCP のリソースへのアクセス権を、オンプレミスまたはマルチクラウドのワークロードに付与するためのサービスとして Workload Identity が活用できます。

Google Cloud のブログ記事 に記載のある以下の手順で設定をしていきます。

  1. GCP プロジェクトで、Workload Identity プールのリソース オブジェクトを作成します。Workload Identity プールは、キーを必要としない連携メカニズムを容易にするために構築された新しいコンポーネントです。このプールは、外部 ID のコレクション用のコンテナとして機能します。
  2. 1 つ以上の IdP を Workload Identity プールに接続します。IdP は、OIDC プロトコルをサポートする AWS や Azure のアカウントまたはプロバイダのいずれかになります(SAML もまもなくサポート)。
  3. 次の 2 つの IAM ポリシーを定義して、プールにリソースへのアクセス権を付与します。
    1. サービス アカウントに、目的のリソースへのアクセスを許可するポリシー。新しいサービス アカウントを作成することも、既存のサービス アカウントを再利用することもできます。
    2. プールの ID が、サービス アカウントになりすますことを許可するポリシー。これらのポリシーの作成に関する詳しい情報については、ドキュメントをご覧ください。
  4. ワークロードを STS エンドポイントに対して認証し、サービス アカウントになりすまして、目的の GCP API を呼び出します。

ユーザー環境での準備

ユーザー環境(GCP)では以下の手順で設定をしていただく必要があります。

事前準備

事前準備として、GCP のコンソールやコマンドラインツール等を使って こちらの公式ドキュメント を参考に Workload Identity プールとサービス側の AWS アカウントと紐付けたプロバイダを作成しておきます。また、アクセス許可するリソースに対するアクセス権を持ったサービスアカウントも作成しておきます。

サービス アカウントの権限借用を許可する権限を外部 ID に付与する

GCP のコンソールで Workload Identity プールのページより事前準備で作成した Workload Identity プールを見つけて開きます。

IAMと管理のスクリーンショット
Workload Identity プールを開いたところ

画面上部の アクセスを許可 をクリックし、開いたナビゲーションに従って必要な項目を入力していきます。

サービスアカウントにアクセス権を付与するのスクリーンショット
アクセス許可を設定

サービスアカウントには事前準備で作成したサービスアカウントを選択します。また、必要に応じて AWS ロールでのフィルタ条件を設定します。 保存 をクリックすると次のような画面がポップアップします。

アプリケーションの構成のスクリーンショット
構成ファイルをダウンロード

プロバイダを選択し 構成をダウンロード をクリックすると構成ファイルがダウンロードされます。このファイルの中身は以下のような JSON になっています。

{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/************/locations/global/workloadIdentityPools/sample/providers/sample-aws",
  "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/workloadidsample@**************.iam.gserviceaccount.com:generateAccessToken",
  "token_url": "https://sts.googleapis.com/v1/token",
  "credential_source": {
    "environment_id": "aws1",
    "region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
    "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
    "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
  }
}

サービスアカウントキーのような認証情報が含まれていないことがご確認いただけると思います。このファイルをサービス側に連携していただき、ユーザー環境での準備は完了です。

サービスワークロードでの処理

サービス側の AWS Lambda では次のようなコードを実行しています。

package main

import (
  "context"

    "gocloud.dev/blob"
    "gocloud.dev/blob/gcsblob"
    "gocloud.dev/gcp"
    "golang.org/x/oauth2/google"
)

func WriteGCS(ctx context.Context, cred, bucketName, objectName string, content []byte) error {
    // 一時トークンを発行して GCP クライアントを生成
    creds, err := google.CredentialsFromJSON(ctx, []byte(cred), "https://www.googleapis.com/auth/devstorage.read_write")
    if err != nil {
        return err
    }
    client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), gcp.CredentialsTokenSource(creds))
    if err != nil {
        return err
    }

    // 以下は通常通り Bucket オブジェクトを生成して書き込み処理を実行
    bucket, err = gcsblob.OpenBucket(ctx, client, bucketName, nil)
    if err != nil {
        return err
    }
    if err := bucket.WriteAll(ctx, key, bt, nil); err != nil {
        return err
    }

    return nil
}

前提として Go CDK を利用しています。このモジュールはクラウドサービスへのアクセスを抽象化してくれているので、今回のように複数のクラウドサービスを跨いだ処理が必要な際にとても便利です。

WriteGCS 関数の引数には

  • cred: ユーザー環境で取得していただいた構成ファイルの JSON 文字列
  • bucketName: 対象のバケット名
  • objectName: 対象のオブジェクト名
  • content: 対象のオブジェクトのコンテンツ

がそれぞれ渡される想定です。

   creds, err := google.CredentialsFromJSON(ctx, []byte(cred), "https://www.googleapis.com/auth/devstorage.read_write")

ここの3番目の引数はスコープを指定するのですが Google API の OAuth 2.0 スコープ というドキュメントがあるのでここから必要なスコープを指定します。ドキュメント内でも記載がありますが、できるだけ機密性の低いスコープを使用することが推奨されています。今回は Cloud Storage への読み込み/書き込みを可能にしたいので https://www.googleapis.com/auth/devstorage.read_write を指定しています。 以降、クライアントを生成してバケットオブジェクトの読み書きメソッドを呼ぶ流れはサービスアカウントキーを使った場合と同じです。実装時に認証方式をそれほど意識せずに済むようになっています。

まとめ

Workload Identity 連携を利用することで、セキュリティリスクを最小限に抑えたシステム構成が実現できました。クラウドを活用したマイクロサービス化が進むとサービス間認証は課題となってくるので、今回のような仕組みを活用できる機会が今後増えてきそうです。