RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

AWS Lambda から GCP CloudFunctions/Cloud Run を呼び出す (REST API 編)

はじめに

こんにちは、DevOps グループの中島です。
最近タイトルのような Lambda から、認証が必要な CloudFunction エンドポイントを呼び出すアーキテクチャを実装する機会がありました。 認証を行うには、クライアントライブラリを利用する方法 (推奨) と REST API を叩いて自力でやる方法がありますが、 内部の動作を理解してこそライブラリを使う権利が与えられると思いますので、今回は REST API を叩く方法を紹介します。

概要

以下のような流れで認証を行った上で、最終的に Cloud Functions を呼び出します。

引用元: https://cloud.google.com/iam/docs/tutorial-cloud-run-workload-id-federation

  1. Lambda の認証情報を用いて、IdP Token を生成する
  2. IdP Token を用いて、GCP STS から Federated Token を取得する
  3. Federated Token を用いて GCP IAM Credentials から ID Token を取得する
  4. ID Token を用いて、Cloud Functions を呼び出す
用語 説明
IdP Token Workload Identity 連携を使用すると、事前に GCP に Lambda を実行するロールの ARN 等が登録されているため、Lambda の認証情報を用いて GCP STS トークン (Federated Token) と交換できます。 この交換に必要となるトークンです。(参考)
(Workload Identity 連携の詳しい説明や必要なリソース、設定等については他所に譲ります)
Federated Token サービスアカウントの権限を借用 (Impersonation)するためのトークンです。この権限を用いて OAuth 2.0 アクセストークン (ID Token) を取得できます。(参考)
ID Token 認証を要求する Cloud Functions の呼び出しに必要なトークンです。(参考)

1. Lambda の認証情報を用いて、 IdP Token を生成する

IdP Token のフォーマットは こちら の subjectToken に記載があります。

If subjectToken is for AWS, it must be a serialized GetCallerIdentity token. This token contains the same information as a request to the AWS GetCallerIdentity() method, as well as the AWS signature for the request information. Use Signature Version 4. Format the request as URL-encoded JSON, and ...

つまり、GetCallerIdentity の呼び出し情報に SigV4 署名を付与したものが、IdP Token となります。

IdP Token は 仕様 に記載の通り以下の情報を含める必要があります。

項目名 設定値
url AWS STS のエンドポイント: https://sts.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15
method POST
headers リクエストヘッダ
authorization SigV4 署名
host sts.amazonaws.com
x-amz-date ISO 8601 形式の現在日時
x-goog-cloud-target-resource //iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/<pool-id>/providers/<provider-id>
x-amz-security-token Lambda の実行環境で取得可能な AWS_SESSION_TOKEN

上記を参考に IdP トークンを生成するコードは以下のようになります。

def create_aws_token(project_number, pool_id, provider_id):
    # Request を利用して SigV4 で署名
    request = AWSRequest(
        method="POST",
        url="https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15",
        headers={
            "Host": "sts.amazonaws.com",
            "x-goog-cloud-target-resource": f"//iam.googleapis.com/projects/{project_number}/locations/global/workloadIdentityPools/{pool_id}/providers/{provider_id}",
        },
    )
    SigV4Auth(boto3.Session().get_credentials(), "sts", "us-east-1").add_auth(request)

    # Request の情報をもとに Token を生成
    token = {"url": request.url, "method": request.method, "headers": []}
    for key, value in request.headers.items():
        token["headers"].append({"key": key, "value": value})

    print(json.dumps(token, indent=2, sort_keys=True))

    return urllib.parse.quote(json.dumps(token))

print の出力は以下のようになり、仕様通りに作れていることが分かると思います。

{
  "headers": [
    {
      "key": "Host",
      "value": "sts.amazonaws.com"
    },
    {
      "key": "x-goog-cloud-target-resource",
      "value": "//iam.googleapis.com/projects/.../locations/global/workloadIdentityPools/.../providers/..."
    },
    {
      "key": "X-Amz-Date",
      "value": "20240410T064224Z"
    },
    {
      "key": "X-Amz-Security-Token",
      "value": "..."
    },
    {
      "key": "Authorization",
      "value": "AWS4-HMAC-SHA256 Credential=.../20240410/us-east-1/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token;x-goog-cloud-target-resource, Signature=..."
    }
  ],
  "method": "POST",
  "url": "https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15"
}

SigV4 署名の詳細は以下の画像のようになるのですが、こちら に詳しい説明がありますので、今回はライブラリの力を借りて詳細は割愛します。

https://docs.aws.amazon.com/images/IAM/latest/UserGuide/images/sigV4-using-auth-header.png

2. IdP Token を用いて、GCP STS から Federated Token を取得する

Federated Token は GCP STS の token エンドポイントから取得できます。API 仕様は こちら です。 audience に Workload Identity の情報を入力し、subject_token に 1. で作成したトークンを使用します。それ以外は規定の値です。

def get_sts_token(aws_token, project_number, pool_id, provider_id):
    url = "https://sts.googleapis.com/v1/token"
    data = {
        "audience": f"//iam.googleapis.com/projects/{project_number}/locations/global/workloadIdentityPools/{pool_id}/providers/{provider_id}",
        "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
        "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
        "scope": "https://www.googleapis.com/auth/cloud-platform",
        "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
        "subject_token": aws_token
    }
    response = requests.post(url, data=data)

    return response.json()['access_token']

3. Federated Token を用いて GCP IAM Credentials から ID Token を取得する

ID Token は GCP IAM Credentials, projects.serviceAccounts リソースの generateIdToken エンドポイントから取得できます。API 仕様は こちら です。 Authentication ヘッダに 2. で取得した Federated Token を認証情報として設定し、audience に呼び出したい CloudFunctions のエンドポイントを指定します。

def get_id_token(sts_token, service_account):
    url = f"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{service_account}:generateIdToken"
    headers = {
        "Authorization": f"Bearer {sts_token}",
        "Content-Type": "application/json; charset=utf-8"
    }
    data = {
        "audience": "https://asia-northeast1-....cloudfunctions.net/...",
        "includeEmail": "true"
    }
    response = requests.post(url, headers=headers, json=data)
    id_token = response.json()['token']
    
    return id_token

4. ID Token を用いて、Cloud Functions を呼び出す

あとは Authorization ヘッダに ID Token を設定して呼び出すだけです。

def call_cloud_functions(id_token):
    url = "https://asia-northeast1-....cloudfunctions.net/..."
    data = {
        "name": "Hello World"
    }
    headers = {
        "Authorization": f"Bearer {id_token}",
        "Content-Type": "application/json"
    }
    response = requests.post(url, headers=headers, json=data)

おわりに

各所に散らばるドキュメントの情報をつなぎ合わせる過程で、認証方式の理解が進んだように思います。 次回はクライアントライブラリ編を執筆する予定です。お楽しみに。

We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp