Workload Identity連携でAWSからGoogle Cloudにセキュアにアクセスする
はじめに
GoogleCloudのWorkload Identityでの連携をAWSのLambdaを使用し、S3のファイルをGoogle CloudのCloud Storageにアップロードする方法を解説します。本記事では、Workload Identityを利用して、IAMロールとGoogle Cloudのサービスアカウントを紐付けることで、安全にアクセスできる環境を構築します。
lambdaではPythonを使用します。また、Terraformを使用します。
Workload Identityとは
Workload Identityとは、AWSや他のクラウドプロバイダーのIAMロールとGoogle Cloudのサービスアカウントを紐付けることで、シークレットキーを使わずにGoogle Cloudのリソースにアクセスできる仕組みです。
Workload Identity Poolとは
Workload Identity Poolは、外部のクラウドプロバイダーやIDプロバイダーからの認証情報を受け入れるためのリソースです。
Workload Identity Providerとは
Workload Identity Providerは、特定のクラウドプロバイダーのIDをWorkload Identity Poolに関連付けるためのリソースです。
処理の流れ
- LambdaがS3のファイルを取得する。
 - Lambdaに紐づくIAMロールがWorkload IdentityによってGoogle Cloudのサービスアカウントの権限を借用する。
 - LambdaがCloud Storageにファイルをアップロードする。
 
Workload Identityでは、AWSのIAMロールがGoogle Cloudのサービスアカウントの権限を借用することで、Google Cloudのサービスにアクセスできるようになります。
Terraformで構築していく
IAMロールとS3バケットの作成
LambdaにアタッチするIAMロールとS3バケットを作成します。
Lambdaの構築にはGoogle Cloudの設定が必要となるため、後で行います。
# IAM Role の作成
resource "aws_iam_role" "lambda_role" {
  name = "lambda_execution_role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}
# IAMポリシー(S3読み取り権限)
resource "aws_iam_policy" "s3_read_policy" {
  name        = "s3_read_policy"
  description = "Allow Lambda to read from S3"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action   = ["s3:GetObject", "s3:ListBucket"]
      Effect   = "Allow"
      Resource = [
        aws_s3_bucket.lambda_bucket.arn,
        "${aws_s3_bucket.lambda_bucket.arn}/*"
      ]
    }]
  })
}
# IAMポリシーをIAMロールにアタッチ
resource "aws_iam_role_policy_attachment" "lambda_s3_read" {
  policy_arn = aws_iam_policy.s3_read_policy.arn
  role       = aws_iam_role.lambda_role.name
}
# Lambda 用のS3バケット作成
resource "aws_s3_bucket" "lambda_bucket" {
  bucket = "test-bucket"
}
サービスアカウントとCloud Storageの作成
Google Cloud上でサービスアカウントとCloud Storageを作成し、サービスアカウントにCloud Storageへのアクセス権を付与します。
resource "google_service_account" "main" {
  account_id   = "gc-test-sa"
  display_name = "gc-test-sa"
}
resource "google_storage_bucket" "main" {
  name                     = "gc-test-storage"
  location                 = "ASIA-NORTHEAST1"
  force_destroy            = true
  public_access_prevention = "enforced"
  storage_class            = "REGIONAL"
}
resource "google_storage_bucket_iam_member" "main_viewer" {
  bucket = google_storage_bucket.receiver.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:${google_service_account.main.email}"
}
resource "google_storage_bucket_iam_member" "main_creator" {
  bucket = google_storage_bucket.receiver.name
  role   = "roles/storage.objectCreator"
  member = "serviceAccount:${google_service_account.main.email}"
}
Workload Identityの作成
Workload IdentityプールとWorkload Identityプロバイダを作成します。
resource "google_iam_workload_identity_pool" "aws_pool" {
  project                   = local.project_id
  workload_identity_pool_id = "test-aws-pool"
  display_name              = "test-aws-pool"
  description               = "aws pool for test"
}
resource "google_iam_workload_identity_pool_provider" "aws_provider" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.aws_pool.workload_identity_pool_id
  workload_identity_pool_provider_id = "test-aws-provider"
  display_name                       = "test-aws-provider"
  description                        = "test-aws provider"
  aws {
    account_id = local.aws_id #AWSアカウントのID
  }
}
Workload IdentityでサービスアカウントとIAMロールの紐付け
Workload IdentityでサービスアカウントとIAMロールの紐付けをします。
principalSetの設定を書くときには、以下のような形である必要があります。
principalSet://iam.googleapis.com/${WorkloadIdentity Poolの名前}/attribute.aws_role/arn:aws:sts::${AWSのアカウントID}:assumed-role/${IAMロール名}
以下の部分はIAMロールのARNではないことに注意してください。
arn:aws:sts::${AWSのアカウントID}:assumed-role/${IAMロール名}
resource "google_service_account_iam_binding" "aws" {
  service_account_id = google_service_account.main.id
  role               = "roles/iam.workloadIdentityUser"
  members = [    "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.aws_pool.name}/attribute.aws_role/arn:aws:sts::${local.aws_id}:assumed-role/${local.aws_role_name}"
  ]
}
構成ファイルのダウンロード
以下の記事を参考に、Google Cloudのコンソール、またはgcloudのコマンドからダウンロードしてください。
※ワークスペース間をまたぐと、構成ファイルのダウンロードはGoogle Cloudのコンソールではできなくて、gcloudのコマンドを使用してダウンロードする必要がある。
Lambdaの作成
フォルダー構成は以下のようになります。
root
  ┣━ lambda
  ┃     ┗━ src
  ┃        ┣━ test.py
  ┃        ┣━ GoogleCloudの構成ファイル.json
  ┃        ┗━ lambda.zip          
  ┣━ main.tf
  ┣━ variables.tf
今回はPythonで実装しています。
lambda/src/test.py がソースコードとなり、archive_file リソースが自動でZIP化してくれます。
環境変数 GOOGLE_APPLICATION_CREDENTIALS に Google Cloud の構成ファイルのパスを設定するのを忘れないでください。
# `lambda_function.py` をZIP化
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "lambda/src" # ローカルの `lambda/src` をZIP化
  output_path = "lambda/src/lambda.zip"
}
# Lambda関数の作成
resource "aws_lambda_function" "my_lambda" {
  function_name    = "MyPythonLambda"
  role            = aws_iam_role.lambda_role.arn
  runtime         = "python3.9"
  handler         = "test.lambda_handler"
  filename        = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
  environment {
    variables = {
      GOOGLE_APPLICATION_CREDENTIALS = "構成ファイルのパス/構成ファイル名"
    }
  }
}
 test.py
import boto3
from google.cloud import storage
def get_from_s3(s3_bucket_name, s3_object_name):
    # S3クライアントの作成
    s3 = boto3.client('s3')
    s3_object_path = f"{s3_bucket_name}/{s3_object_name}"
    tmp_file_path = f"/tmp/{s3_object_name}"
    
    # ファイルを Lambda の一時領域にダウンロード
    s3.download_file(s3_bucket_name, s3_object_name, tmp_file_path)
    print(f"{s3_object_path} was downloaded to {tmp_file_path}.")
    
    return tmp_file_path
def upload_to_gcs(tmp_file_path, gcs_bucket_name):
    # Cloud Storage クライアントの作成
    gcs = storage.Client()
    file_name = tmp_file_path.split('/')[-1]
    gcs_object_path = f"my_gcs_path/{file_name}"
    
    bucket = gcs.bucket(gcs_bucket_name)
    blob = bucket.blob(gcs_object_path)
    
    # オブジェクトを Cloud Storage バケットにアップロード
    blob.upload_from_filename(tmp_file_path)
    print(f"{tmp_file_path} was uploaded to {gcs_bucket_name}/{gcs_object_path}.")
    
    return None
def lambda_handler(event, context):
    # event から各種情報を取得
    s3_bucket_name = event['s3_bucket_name']
    s3_object_name = event['s3_object_name']
    gcs_bucket_name = event['gcs_bucket_name']
    
    # オブジェクトを S3 から取得
    tmp_file_path = get_from_s3(
        s3_bucket_name=s3_bucket_name,
        s3_object_name=s3_object_name
    )
    
    # オブジェクトを Cloud Storage にアップロード
    upload_to_gcs(
        tmp_file_path=tmp_file_path,
        gcs_bucket_name=gcs_bucket_name
    )
    
    return {'statusCode': 200}
注意点
- Workload Identity を利用して AWS IAM ロールと Google Cloud のサービスアカウントを紐付ける際、プリンシパルセットの設定が必要です。その際、AWS の IAM ロールの ARN はそのまま使用できないため、適切な形式に変換する必要があります。
 - Lambda の環境変数に 
GOOGLE_APPLICATION_CREDENTIALSを設定する必要があります。- 
GOOGLE_APPLICATION_CREDENTIALSは Google Cloud のデフォルトの認証 (ADC) で必要になります。 - 参考: Google Cloud Application Default Credentials
 
 - 
 - ワークスペースをまたぐ場合、構成ファイルのダウンロードは Google Cloud コンソールでは行えません。
gcloudコマンドを使用してダウンロードする必要があります。 
感想
Google Cloudが初めてということもあり、概念の理解にかなり苦労しました。
今まではサービスアカウントでキーを発行する方法でやっていたのですが、今回のWorkload Idenitytによる認証でよりセキュアにすることができました。
不明点や間違い、感想ありましたらコメントいただけると幸いです。
参考記事
Discussion