Workload Identity連携を使ってAWS LambdaからCloud Spannerにパスワードレスでアクセスしてみた

Workload Identity連携を使うとAWSのクレデンシャル情報だけでGoogle Cloudリソースにアクセスできて便利です
2022.09.05

Workload Identity連携を使うとGoogle Cloud外部のIDに対してサービスアカウントになりすます機能を含むIAMロールを付与できます。この機能を使うとGoogle Cloud外部のワークロードがサービスアカウントキー無しでGoogle Cloudのリソースにアクセスできるようになります。Workload Identity連携はAWSを外部 ID プロバイダとして利用する構成にも対応しており、この構成を用いることでAWSのクレデンシャル情報を用いてGoogle Cloudのリソースにアクセスすることが可能です。

このブログではWorkload Identity連携を使ってAWS LambdaからGoogle CloudのSpannerにアクセスする構成を試してみます。

環境

今回利用した環境です

  • Python3.9
  • SAM CLI, version 1.53.0
  • google_cloud_spanner 3.19.0

Spannerの準備

まずLambdaからアクセスするSpannerのインスタンス&データベースを作成します。こちらのチュートリアルに従ってSpannerのインスタンス&データベースを作成し、テスト用にデータを投入しておいて下さい。詳細な手順は割愛します。

gcloud CLI を使用してデータベースを作成し、クエリを実行する

サービスアカウントの作成

続いてLambdaから権限を借用するためのサービスアカウントを作成します。gcloud CLI で以下のコマンドを実行しましょう。

$ gcloud iam service-accounts create <サービスアカウント名> \
    --description="<適当な説明>" \
    --display-name="<表示名>"

作成したサービスアカウントがSpannerにアクセスできるようにspanner.databaseUserのロールを割り当てます

$ gcloud projects add-iam-policy-binding <プロジェクト名> \
    --member=serviceAccount:<サービスアカウント名>@<プロジェクト名>.iam.gserviceaccount.com --role=roles/spanner.databaseUser

Workload Identity 関連の設定

ここからWorkload Identity関連の設定を進めていきます。ここはマネコンからやってみました。

まずはIDプールの名前を設定

続いてプールにプロバイダを追加。ここはAWSを選択して進めます。

プロバイダの属性はデフォルトのまま進めます。

続いてサービス アカウントの権限借用を許可する権限を外部 ID に付与します。「アクセスを許可」をクリックし、後ほどLambdaから権限借用するサービスアカウントを選択、プリンシパルは「プール内のすべてのID」を選択して「保存」を押下します。

構成ファイルをDLするためのダイアログが表示されるので、プロバイダに先程作成したAWSのプロバイダを選択し、「構成をダウンロード」をクリックします。ここでダウンロードできるファイルには機密情報は含まれません。

こんな感じのJSONファイルがダウンロードできます。

{
    "type": "external_account",
    "audience": "//iam.googleapis.com/projects/<プロジェクトNO>/locations/global/workloadIdentityPools/<IDプール名>/providers/<プロバイダ名>",
    "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
    "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/<サービスアカウント名>@<プロジェクト名>.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"
    }
}

Lambdaの作成

一通り準備ができたので、SpannerにアクセスするLambdaを準備します。今回はSAMを使ってデプロイしたいと思います。

まずはsam init

$ sam init --name lambda-to-spanner -r python3.9

template.yamlを以下のように変更します

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  SpannerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_spanner/
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - x86_64
      Environment:
        Variables:
          GOOGLE_CLOUD_PROJECT: iwata-tomoya
          GOOGLE_APPLICATION_CREDENTIALS: /var/task/config.json
      Timeout: 10
      Role: !GetAtt SpannerFuncRole.Arn
  SpannerFuncRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${AWS::StackName}-spanner-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: spanner-func-policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"

ポイントをいくつか説明していきます。

まず環境変数GOOGLE_CLOUD_PROJECTGOOGLE_APPLICATION_CREDENTIALSです。GOOGLE_CLOUD_PROJECTにはGoogleクラウドのプロジェクト名を、GOOGLE_APPLICATION_CREDENTIALSは先程Workload Identity 関連の設定を行った際にDLした構成ファイルの名前を指定します。

次にタイムアウト値もデフォルトから変更しています。サービス アカウントの権限を借用するためにGoogle Cloudと何度か通信を行う必要があり、デフォルト値のままだとタイムアウトする可能性があるので少し長めに10秒としています。

IAMロールもSAM任せではなく明示的に作成しています。SAM任せにしてしまうとロール名が長くなりすぎて

The size of mapped attribute google.subject exceeds the 127 bytes limit. Either modify your attribute mapping or the incoming assertion to produce a mapped attribute that is less than 127 bytes.

というエラーが発生するためです。※上記テンプレートの記載でもスタック名が長くなり過ぎるとエラーになるためスタック名を長くし過ぎないよう注意して下さい。

次にLambda Functionのコードを準備していきます。まずsam initで作成されたディレクトリをリネーム

$ mv hello_world hello_spanner

先程DLした構成ファイルを環境変数GOOGLE_APPLICATION_CREDENTIALSで指定したファイル名に合わせて配置します。今回はconfig.jsonです。続いてrequirements.txtgoogle-cloud-spannerを追記します。

$ echo google-cloud-spanner > hello_spanner/requirements.txt

このライブラリが依存するgoogle-authがWorkload Identity 連携をサポートしているため、環境変数GOOGLE_APPLICATION_CREDENTIALSを設定するだけでよしなにサービスアカウントの権限借用を実行してくれます。

最終的なディレクトリ構成です

$ tree hello_spanner/
hello_spanner/
├── app.py
├── config.json
└── requirements.txt

最後にLambdaのコードを記述します

import json
from google.cloud import spanner

spanner_instance_name = '<Spannerのインスタンス名>'
spanner_db_name = '<Spannerのデータベース名>'

spanner_client = spanner.Client()
instance = spanner_client.instance(spanner_instance_name)
database = instance.database(spanner_db_name)


def lambda_handler(event, context):

    with database.snapshot() as snapshot:
        singers = snapshot.execute_sql('SELECT SingerId, FirstName, LastName FROM Singers')

    return {
        'statusCode': 200,
        'body': json.dumps({
            'singers': [{
                'singer_id': row[0],
                'first_name': row[1],
                'last_name': row[2],
            } for row in singers]
        }),
    }

コードが準備できたのでビルド&デプロイします。

$ sam build
$ sam deploy

デプロイできたらマネコンからLambdaをテストしてみましょう

無事にLambdaからSpannerのデータを取得できました!Secrets Managerも使っていないのでラクチンです。

まとめ

Workload Identity連携を使うことでサービスアカウントキー無しでAWSからGoogle Cloudリソースへアクセスできるようになりました。これでマルチクラウド構成のハードルが少し下がるので、両サービスのメリットを活かした構成を検討してみると面白そうですね。

参考