Workload Identity連携を使ってAWS LambdaからCloud Spannerにパスワードレスでアクセスしてみた
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_PROJECT
とGOOGLE_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.txt
にgoogle-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リソースへアクセスできるようになりました。これでマルチクラウド構成のハードルが少し下がるので、両サービスのメリットを活かした構成を検討してみると面白そうですね。