Workload Identity連携で認証情報の構成ファイルを読み込まずに実行する

Google Cloud

2025.12.17

Topics

この記事はNHN テコラス Advent Calendar 2025 の 17 日目の記事です。

はじめに

Workload Identity連携を構築した際、提供される認証情報の構成ファイルをそのまま読み込んで、実行しているケースが多いと思います。この場合実装は楽ですが、Lambdaであれば1つのサービスアカウントにしかアクセスできなくなるため拡張性が低くなります。

また構成ファイルをParameter Storeに置いたとしても、複数のサービスアカウントにアクセスしたい場合、この構成ファイルの管理が手間になります。

これを解決する方法として、認証情報の構成ファイルの内容をLambdaの中で作成する方法があります。これによって1つのLambdaから複数のWorkload Identity Poolにアクセスできるようになります。

そのため、今回は最初にWorkload Identity連携の認証情報の構成ファイルの定義を確認します。その後Lambdaの中で構成ファイルを作成して、これを使ってWorkload Identity連携を行い、Google CloudのCloud Storageにアクセスする方法を紹介します。

Workload Identity連携自体の説明は省略しています。こちらを確認したい人は別の記事を参考にしてください。

関連記事
【やってみた】AWS LambdaからセキュアにGoogle CloudのVertex AIを使ってみた

関連記事
Workload Identityを用いたGoogle CloudとAWSの連携

Workload Identity連携の認証情報の構成ファイルを確認する

AWSをプロバイダとした場合、Workload Identityを構築した後に発行できる認証情報の構成ファイルは以下になっています。

{
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
  "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL: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",
    "imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
  }
}

参考: https://google.aip.dev/auth/4117#determining-the-subject-token-in-aws

こちらを確認すると、ほとんど固定の値であることがわかります。
つまり以下が分かれば構成ファイルは作成できることになります。

変数 説明
$PROJECT_NUMBER プロジェクトナンバーのこと、プロジェクトIDではないので注意
$POOL_ID Workload IdentityのPool IDのこと
$PROVIDER_ID Workload Identityのプロパイダー IDのこと
$EMAIL サービスアカウントのID={ユニーク名}@{project名}.iam.gserviceaccount.comのこと

こちらの情報を元にLambdaで実装をしていきます。

実装の前提

今回はSAMを使ってAWS Lambdaを構築します。言語はJava 17を使用します。またWorkload Identity Pool、プロバイダー、サービスアカウント、Role、Cloud Storageを作成します。

リソースを作成したら、LambdaからWorkload Identity連携を実行して、Cloud Storageにあるオブジェクトの一覧を取得します。

Workload Identity連携の検証リソースの構築

Workload Identityをはじめとする検証リソースを作成します。今回はCLIで行います。

今回重要なのはこの後のLambdaの実装になります。リソースの構築の説明は長いためLambdaの実装を確認したい場合は、こちらを飛ばして次の項目を確認してください。

変数準備

export PROJECT_ID="your-project-id"
export PROJECT_NUMBER="your-project-number"
export POOL_ID="aws-lambda-pool"
export SERVICE_ACCOUNT_NAME="lambda-gcs-access"
export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
export GCS_BUCKET_NAME="lambda-gcs-access-bucket"
export AWS_ACCOUNT_ID="your-aws-account-id"
export AWS_ROLE_NAME="lambda-sample-role" 

Pool 作成

$ gcloud iam workload-identity-pools create $POOL_ID \
  --project=$PROJECT_ID \
  --location="global" \
  --display-name="aws-lambda-pool" \
  --description="aws lambda pool"

Workload Identityでは削除時に同じIDが30日使用不可能になるため、削除した場合はIDを変更する必要があります。この点は注意してください。

プロバイダー作成

$ gcloud iam workload-identity-pools providers create-aws aws-provider \
  --project=${PROJECT_ID} \
  --location="global" \
  --workload-identity-pool="${POOL_ID}" \
  --display-name="aws-lambda-pool" \
  --description="aws provider" \
  --attribute-mapping="google.subject=assertion.arn,attribute.aws_account=assertion.account,attribute.aws_role=assertion.arn.contains('assumed-role') ? assertion.arn.extract('{account_arn}assumed-role/') + 'assumed-role/' + assertion.arn.extract('assumed-role/{role_name}/') : assertion.arn" \
  --account-id=$AWS_ACCOUNT_ID

Service Account作成

次にGoogle Cloud側にLambdaが使うService Accountを作成します。

$ gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \
    --project=$PROJECT_ID \
    --display-name="Lambda GCS Access"

Cloud Storage作成

$ gcloud storage buckets create gs://$GCS_BUCKET_NAME  \
  --project=$PROJECT_ID \
  --location=asia-northeast1 \
  --default-storage-class=STANDARD \
  --uniform-bucket-level-access

権限作成

Roleを作成します。

$ gcloud iam roles create LambdaGcsAccess \
  --project=$PROJECT_ID \
  --title=lambda-gcs-access \
  --permissions=storage.buckets.list,storage.objects.get,storage.objects.list \
  --stage=GA

Roleをサービスアカウントに紐づけます。

$ gcloud projects add-iam-policy-binding $PROJECT_ID \
  --project=$PROJECT_ID \
  --member="serviceAccount:$SERVICE_ACCOUNT_EMAIL" \
  --role="projects/${PROJECT_ID}/roles/LambdaGcsAccess" \
  --condition="None"

その後作成したサービスアカウントにWorkload Identityの権限を付与します。

$ gcloud iam service-accounts add-iam-policy-binding $SERVICE_ACCOUNT_EMAIL \
  --project=$PROJECT_ID \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/attribute.aws_role/arn:aws:sts::${AWS_ACCOUNT_ID}:assumed-role/${AWS_ROLE_NAME}"

Lambdaを作成する

次にSAMを使ってLambdaを作成します。

App.java

package helloworld;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.google.api.gax.paging.Page;
import com.google.auth.oauth2.AwsCredentialSource;
import com.google.auth.oauth2.AwsCredentials;
import com.google.auth.oauth2.ExternalAccountCredentials;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.BlobInfo;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.Storage.BlobListOption;
import com.google.cloud.storage.StorageOptions;
import java.text.MessageFormat;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

public class App implements RequestHandler<Map<String, Object>, Map<String, Object>> {

    private static final Logger logger = LoggerFactory.getLogger(App.class);

    // 固定値
    private static final String SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request";
    private static final String TOKEN_URL = "https://sts.googleapis.com/v1/token";
    private static final String FORMAT_AUDIENCE = "//iam.googleapis.com/projects/{0}/locations/global/workloadIdentityPools/{1}/providers/{2}";
    private static final String FORMAT_SERVICE_ACCOUNT_IMPERSONATION_URL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{0}:generateAccessToken";
    private static final String ENVIRONMENT_ID = "aws1";
    private static final String REGION_URL =  "http://169.254.169.254/latest/meta-data/placement/availability-zone";
    private static final String URL =  "http://169.254.169.254/latest/meta-data/iam/security-credentials";
    private static final String REGIONAL_CRED_VERIFICATION_URL = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15";

    @Override
    public Map<String, Object> handleRequest(Map<String, Object> event, Context context) {
        // パラメータを取得する
        String projectNumber = (String) event.get("project_number");
        String projectId = (String) event.get("project_id");
        String poolId = (String) event.get("pool_id");
        String providerId = (String) event.get("provider_id");
        String serviceAccountEmail = (String) event.get("service_account_email");
        String gcsBucketName = (String) event.get("gcs_bucket_name");

        // credential 作成
        ExternalAccountCredentials credentials = createCredentials(
            projectNumber,
            poolId,
            providerId,
            serviceAccountEmail
        );

        // credentialを使ってClientを作成
        Storage gcsClient = StorageOptions.newBuilder()
            .setCredentials(credentials)
            .setProjectId(projectId)
            .build()
            .getService();

        // CloudStorageにアクセス
        BlobListOption blobListOption = BlobListOption.prefix("");
        Page<Blob> blobs = gcsClient.list(gcsBucketName, blobListOption);

        // 確認
        List<String> resultList = blobs.streamValues().map(BlobInfo::getName).toList();

        return createReturnItem(resultList.toString());
    }

    private ExternalAccountCredentials createCredentials(
        String projectNumber,
        String poolId,
        String providerId,
        String serviceAccountEmail
    ) {
        // audience
        String audience = MessageFormat.format(FORMAT_AUDIENCE, projectNumber, poolId, providerId);
        logger.info("audience: {}", audience);

        // serviceAccountImpersonationUrl
        String serviceAccountImpersonationUrl = MessageFormat.format(FORMAT_SERVICE_ACCOUNT_IMPERSONATION_URL, serviceAccountEmail);
        logger.info("serviceAccountImpersonationUrl: {}", serviceAccountImpersonationUrl);

        // awsCredentialSource関連
        Map<String, Object> credentialSourceMap = Map.of(
            "environment_id", ENVIRONMENT_ID,
            "region_url", REGION_URL,
            "url", URL,
            "regional_cred_verification_url", REGIONAL_CRED_VERIFICATION_URL
        );
        AwsCredentialSource awsCredentialSource = new AwsCredentialSource(credentialSourceMap);

        // AwsCredentials.Builder
        ExternalAccountCredentials credentials = AwsCredentials.newBuilder()
            .setAudience(audience)
            .setSubjectTokenType(SUBJECT_TOKEN_TYPE)
            .setTokenUrl(TOKEN_URL)
            .setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
            .setCredentialSource(awsCredentialSource)
            .build();

        return credentials;
    }

    private Map<String, Object> createReturnItem(String result) {
        Map<String, Object> item = Map.of("result", result);
        logger.info("return = {}", item);

        return item;
    }
}

build.gradle

plugins {
    id 'java'
}

group = 'helloworld'
version = '1.0'

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}

repositories {
    mavenCentral()
}

dependencies {
    // AWS
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.3'
    implementation 'com.amazonaws:aws-lambda-java-events:3.11.4'
    implementation 'org.slf4j:slf4j-api:2.0.9'

    // Google Cloud
    implementation platform('com.google.cloud:libraries-bom:26.28.0')
    implementation 'com.google.cloud:google-cloud-storage'
    implementation 'com.google.auth:google-auth-library-credentials'
}

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Sample SAM Lambda application
Resources:
  SampleRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: lambda-sample-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: "/"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AmazonSSMFullAccess
  SampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: Google Cloud Access Test
      FunctionName: googlecloud-access-test
      CodeUri: HelloWorldFunction
      Handler: helloworld.App::handleRequest
      Runtime: java17
      Architectures:
        - x86_64
      MemorySize: 512
      Timeout: 300
      Environment:
        Variables:
          ENV_TYPE: dev
      Role: !GetAtt SampleRole.Arn

準備ができたらデプロイします

$ sam build
$ sam deploy \
  --stack-name sample-lambda \
  --s3-bucket your-s3-bucket-name  \
  --region ap-northeast-1 \
  --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
  --profile your-profile-name

Lambdaを実行する

次にCloud Storageに適当なファイルをアップロードします。今回はsample.jsonを作ってアップロードしています。

$ touch sample.json
$ gcloud storage cp sample.json gs://lambda-gcs-access-bucket/sample.json

Lambdaのパラメータを準備します。このパラメータを変更することによって、利用するWorkload Identityを切り替えることができます。

{
    "project_number": "your-project-number",
    "project_id": "your-project-id",
    "pool_id": "your-pool-id",
    "provider_id": "aws-provider",
    "gcs_bucket_name": "lambda-gcs-access-bucket",
    "service_account_email": "lambda-gcs-access@{your-project-id}.iam.gserviceaccount.com"
}

上記のパラメータを使ってLambdaを実行します。

$ aws lambda invoke \
--function-name googlecloud-access-test \
--cli-binary-format raw-in-base64-out \
--profile your-profile-name \
--payload '{
  "project_number": "your-project-number",
  "project_id": "your-project-id", 
  "pool_id": "your-pool-id",
  "provider_id": "aws-provider",
  "gcs_bucket_name": "lambda-gcs-access-bucket",
  "service_account_email": "lambda-gcs-access@{your-project-id}.iam.gserviceaccount.com"
}' \
response.json

レスポンスを確認して、sample.jsonがあればアクセス成功です。

$ cat response.json
{"result":"[sample.json]"}

まとめ

今回は認証情報の構成ファイルの内容をLambdaの中で作成してからWorkload Identity連携を使ってGoogle CloudのCloud Storageにアクセスする方法を紹介しました。構成ファイルを読み込むのは楽ですが、認証先が増えた場合構成ファイルの管理の手間が生まれます。

変数で渡せるようにした方がより多くのサービスアカウントにアクセスできるようになるため、複数の認証をする場合は今回の実装がおすすめです。

NHN テコラスの採用情報はこちら

テックブログ新着情報のほか、AWSやGoogle Cloudに関するお役立ち情報を配信中!

nemod

2018年新卒入社。エンジニア。フロントエンド&サーバサイドを担当。 Vue.js, Ruby on Rails, Ruby, Javaを主に使用する。 会社では全力で働き、家では全力で遊ぶ。

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら