Cloud Run FunctionsからAWSのリソース(S3)をアクセスキー払い出しなしで操作する

Cloud Run FunctionsからAWSのリソース(S3)をアクセスキー払い出しなしで操作する

Clock Icon2025.02.13

Cloud Run FunctionsなどGoogle CloudからAWSのリソース(S3やEC2など)を操作したい場合、AWS側でアクセスキーを払出してそのキーをSecret Managerや環境変数などに保存して使用することあるかもしれません。しかしながら当然アクセスキーの払い出し・運用はリスクがあり推奨されるものではありません。

そこで今回はCloud Run FunctionsからAWSリソース(S3)をアクセスキーを払い出すことなく操作する方法を記事にしてみました。

概要

イメージとしては以下の通りです。

スクリーンショット 2025-02-13 16.05.25.png

  1. Cloud Run FunctionsがIDトークンを取得

    • Cloud Run Functionsに割り当てられたサービスアカウントが、Google CloudのメタデータサーバーからIDトークンを取得
  2. ID トークンを AWS STS に送信

    • 取得したIDトークンを使って、AWS の**STS (Security Token Service)**に対して AssumeRoleWithWebIdentityAPIを呼び出し、一時的な認証情報を取得
  3. AWS STS が認証情報を発行

    • AWS STS は、ID トークンを検証し、対応するIAM ロールに基づいた一時的な認証情報(アクセスキー・シックレットアクセスキーなど)を発行
  4. 取得した認証情報を使用

    • Cloud Run Functionsは、取得した一時的な認証情報を使用して、AWSリソース(例: EC2やS3バケット)にアクセス

Cloud Shellなどからアクセスキー情報なしでAWS CLIを用いるために用いた方法のCloud Run Functions版のようなものです。

やりかた

準備するもの

リソース名 準備内容
サービスアカウント AWSを操作するだけなら特に権限はアタッチ不要
AWS IAMロール 信頼ポリシー Google Cloudのサービスアカウントの一意なIDをsubに設定
AWS IAMロール 許可ポリシー 対象リソースの許可ポリシーを設定
Cloud Run Functions AWSを操作するスクリプト

サービスアカウント

まずはサービスアカウントを作成します。

# サービスアカウント作成コマンド
gcloud iam service-accounts create サービスアカウント名 \
--description="AWS操作用" \
--display-name="AWS Access"

作成したら一意なID(uniqueId)を取得します。

# uniqueId取得コマンド
gcloud iam service-accounts describe サービスアカウント名@プロジェクト名.iam.gserviceaccount.com --format="json" | jq .uniqueId

取得した一意なIDは後続のAWS IAMロール信頼ポリシーで使用するので控えておきます。

AWS IAMロールの作成

AWSマネジメントコンソールでIAM > ロール > ロールを作成からカスタム信頼ポリシーを選択して"accounts.google.com:sub":に先ほど作成したサービスアカウントの一意なIDを設定します。

スクリーンショット 2025-02-13 17.21.49.png

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "accounts.google.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "accounts.google.com:oaud": "適当な値",
                    "accounts.google.com:sub": "一意なID"
                }
            }
        }
    ]
}
  • Principal.Federatedaccounts.google.com を指定することで、Google の ID トークンを受け入れます
  • Condition.StringEquals で、指定したAudience値とサービスアカウントのID(一意なID)の場合のみ許可します

許可ポリシーは必要に応じて設定してください。私はS3の操作権限をアタッチしています。
作成完了後はIAMロールのARNを控えておきます。

Cloud Run Functionsの作成

# S3の指定パス配下のparquetファイルを読み込んでサマリを返すスクリプト
import functions_framework
import boto3
import awswrangler as wr
import google.auth
import google.auth.transport.requests
from google.auth import compute_engine

# AWS IAM ロールの ARN
AWS_ROLE_ARN = "IAMロールのARN"
CLOUD_RUN_URL = "Cloud Run FunctionsのURL"

def get_google_identity_token():
    """メタデータサーバーを使用して ID トークンを取得"""
    request = google.auth.transport.requests.Request()

    credentials = compute_engine.IDTokenCredentials(
        request=request, target_audience=CLOUD_RUN_URL, use_metadata_identity_endpoint=True
    )

    credentials.refresh(request)
    return credentials.token

def get_aws_credentials():
    """Google Cloud の ID トークンを使用して AWS の一時認証情報を取得"""
    identity_token = get_google_identity_token()

    sts_client = boto3.client("sts")
    response = sts_client.assume_role_with_web_identity(
        RoleArn=AWS_ROLE_ARN,
        RoleSessionName="cloud-run-session",
        WebIdentityToken=identity_token
    )

    return response["Credentials"]

def fetch_s3_parquet_data(bucket: str, prefix: str):
    """指定した S3 パスの Parquet ファイルを一括で読み込み、Pandas DataFrame に変換"""
    aws_creds = get_aws_credentials()

    session = boto3.Session(
        aws_access_key_id=aws_creds["AccessKeyId"],
        aws_secret_access_key=aws_creds["SecretAccessKey"],
        aws_session_token=aws_creds["SessionToken"]
    )

    s3_path = f"s3://{bucket}/{prefix}*"
    print(f"Fetching Parquet files from: {s3_path}")

    # Parquet ファイルを一括で読み込む
    df = wr.s3.read_parquet(path=s3_path, boto3_session=session)
    return df

@functions_framework.http
def main(request):
    bucket = "S3バケット名" 
    prefix = "ファイルが保存されているパス"

    try:
        df = fetch_s3_parquet_data(bucket, prefix) 
        return {"rows": df.shape[0], "columns": df.shape[1]}, 200
    except Exception as e:
        return {"error": str(e)}, 500
requirements.txt
google-auth
google-auth-oauthlib
google-auth-httplib2
boto3
awswrangler
functions-framework==3.*

以下のステップごとにコードを簡潔に解説します。

ステップ 処理概要
1.ID トークンを取得 メタデータサーバーから ID トークンを取得
2.AWS STS で認証情報を取得 assume_role_with_web_identity() を使用し、AWS の一時認証情報を取得
3. S3のParquet データを取得 取得した AWS 認証情報を使い、S3のparquetファイルのデータをDataFrameに変換
def get_google_identity_token():
    """メタデータサーバーを使用して ID トークンを取得"""
    request = google.auth.transport.requests.Request()

    credentials = compute_engine.IDTokenCredentials(
        request=request,  target_audience=CLOUD_RUN_URL, use_metadata_identity_endpoint=True
    )

    credentials.refresh(request)
    return credentials.token

1. ID トークンを取得

  • get_google_identity_token()

  • メタデータサーバーからIDトークンを取得する。

  • compute_engine.IDTokenCredentials を使用し、Cloud Run Functionsに割り当てたサービスアカウントに紐づいたIDトークンを取得

  • ポイント

    • compute_engine.IDTokenCredentials を使用してメタデータサーバーからIDトークンを取得
    • target_audienceにはCloud Run FunctionsのURLを指定
    • credentials.refresh(request) を実行して最新のIDトークンを取得

def get_aws_credentials():
    """Google Cloud の ID トークンを使用して AWS の一時認証情報を取得"""
    identity_token = get_google_identity_token()  # ID トークンを取得

    sts_client = boto3.client("sts")
    response = sts_client.assume_role_with_web_identity(
        RoleArn=AWS_ROLE_ARN,
        RoleSessionName="cloud-run-session",
        WebIdentityToken=identity_token  # ID トークンを渡す
    )

    return response["Credentials"]

2. STSを使用して一時的な認証情報を取得

  • get_aws_credentials()

  • 取得したIDトークンをAWS STS(AssumeRoleWithWebIdentity)に渡し、一時的な AWS 認証情報を取得する

  • ポイント

    • boto3.client("sts")を使用してAWS STSクライアントを作成
    • assume_role_with_web_identity() を実行し、サービスアカウントのIDトークンをAWSに渡して認証
    • 一時的なAWS認証情報(AccessKeyId, SecretAccessKey, SessionToken)を取得
    • AWS 側で、Google Cloud の ID トークンを受け入れるために、IAM ロールの 信頼ポリシー (Trust Policy) を設定しておく必要あり

3. S3 の Parquet データを取得

  • fetch_s3_parquet_data(bucket, prefix)
  • 取得した AWS 認証情報を使用して、S3 の Parquet データを Pandas DataFrame に変換する。
def fetch_s3_parquet_data(bucket: str, prefix: str):
    """指定した S3 パスの Parquet ファイルを一括で読み込み、Pandas DataFrame に変換"""
    aws_creds = get_aws_credentials()

    session = boto3.Session(
        aws_access_key_id=aws_creds["AccessKeyId"],
        aws_secret_access_key=aws_creds["SecretAccessKey"],
        aws_session_token=aws_creds["SessionToken"]
    )

    s3_path = f"s3://{bucket}/{prefix}*"  # 指定パス内のすべての Parquet ファイルを対象
    print(f"Fetching Parquet files from: {s3_path}")  # デバッグ用ログ

    # Parquet ファイルを一括で読み込む
    df = wr.s3.read_parquet(path=s3_path, boto3_session=session)

    print(f"DataFrame shape: {df.shape}")  # データの行数・列数を確認
    print(df.head())  # 最初の数行を表示

    return df
  • ポイント
    • boto3.Session() を作成し、取得した AWS 認証情報を使用
    • awswrangler.s3.read_parquet() を使用して、S3から取得したparquetファイルのデータをDataFrameに変換

実行してみる

S3に以下の4行3列の内容を持ったparquetファイルを保存して実行します。

data = {
    "id": [1, 2, 3],
    "name": ["Alice", "Bob", "Charlie"],
    "age": [25, 30, 35],
    "score": [85.5, 90.0, 78.5]
}
# 実行コマンド
curl -m 370 -X POST "Cloud Run FunctionsのURL" -H "Authorization: bearer $(gcloud auth print-identity-token)"

レスポンスは以下となっており、問題なくS3からデータを読み取れていました。

{"columns":4,"rows":3}

例示したのはS3のデータを読み取るスクリプトですが、認証部分以外を変更すればあとはAWS側のIAMポリシーで付与されている権限のことは一通りできます。

まとめ

一時的な認証情報、と書いていますが実態はアクセスキー・シークレットアクセスキーです。ただしプログラム中やSecret Managerなど外部に保存する必要は一切ない使い捨てのものとなっておりCloud Run Functions内にしか存在しないので流出の危険性はまずないものとなっております(取得した一時認証情報をログに書き込むなどしてしまった場合はこの限りではありません)。
AWSリソースをCloud Run Functionsから操作する場合は、本記事でご紹介したような方法を使うことも検討してみても良いかもしれません。

それではまた。ナマステー

参考

https://cloud.google.com/functions/docs/securing/function-identity?hl=ja#metadata-server
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_temp.html

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.