🎉

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に関連付けるためのリソースです。

処理の流れ

  1. LambdaがS3のファイルを取得する。
  2. Lambdaに紐づくIAMロールがWorkload IdentityによってGoogle Cloudのサービスアカウントの権限を借用する。
  3. 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のコマンドからダウンロードしてください。
https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds?hl=ja#console_3

※ワークスペース間をまたぐと、構成ファイルのダウンロードは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 Cloud コンソールでは行えません。gcloud コマンドを使用してダウンロードする必要があります。

感想

Google Cloudが初めてということもあり、概念の理解にかなり苦労しました。
今まではサービスアカウントでキーを発行する方法でやっていたのですが、今回のWorkload Idenitytによる認証でよりセキュアにすることができました。
不明点や間違い、感想ありましたらコメントいただけると幸いです。

参考記事

https://laboratory.kiyono-co.jp/1638/gcp/

https://zenn.dev/nextbeat/articles/google-workload-identity

https://blog.g-gen.co.jp/entry/using-workload-identity-federation-with-aws

Discussion