RareJob Tech Blog

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

AWS Lambda から GCP CloudFunctions/Cloud Run を呼び出す (クライアントライブラリ編)

はじめに

こんにちは、DevOps グループの中島です。
前回 は REST API を自力で叩いて認証を行い、取得した ID Token で Cloud Functions のエンドポイントを呼び出しました。 今回はクライアントライブラリを使用する方法を見ていきたいと思います。 といってもコード自体は数行しかないので、そのコードにたどり着くまでの過程も書いてみます。

google.oauth2.id_token.fetch_id_token を使う

Cloud Functions を呼び出すためには ID Token が必要となるので、まずはこの ID Token を一発で取得できる方法を探していきます。 gcp id token python などで検索すると google-auth というライブラリの ユーザガイドがヒットし以下のような記述があります。

If your application runs on App Engine, Cloud Run, Compute Engine, or has application default credentials set via GOOGLE_APPLICATION_CREDENTIALS environment variable, you can also use google.oauth2.id_token.fetch_id_token to obtain an ID token from your current running environment. The following is an example

import google.oauth2.id_token
import google.auth.transport.requests

request = google.auth.transport.requests.Request()
target_audience = "https://pubsub.googleapis.com"

id_token = google.oauth2.id_token.fetch_id_token(request, target_audience)

書いてある内容を見ると上記のコードでいけそうな気がするので GOOGLE_APPLICATION_CREDENTIALS に、サービスアカウントの 認証情報構成ファイル のファイルパスを指定して試してみます。 補足ですが、Lambda にデプロイしたファイルは /var/task/ に配備されます。

// 実行結果
{
  "errorMessage": "Neither metadata server or valid service account credentials are found.",
  "errorType": "DefaultCredentialsError",
  "requestId": "e01c15bd-50e6-4c5c-913b-02e09d2aada0",
  "stackTrace": [
    ...
    "  File \"/var/task/google/oauth2/id_token.py\", line 295, in fetch_id_token_credentials\n    raise exceptions.DefaultCredentialsError(\n"
  ]
}

Function Logs
START RequestId: e01c15bd-50e6-4c5c-913b-02e09d2aada0 Version: $LATEST
[WARNING]   2024-04-11T06:19:36.615Z    e01c15bd-50e6-4c5c-913b-02e09d2aada0    Compute Engine Metadata server unavailable on attempt 1 of 3. Reason: HTTPConnectionPool(host='169.254.169.254', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f5294d9bd50>: Failed to establish a new connection: [Errno 111] Connection refused'))
...

しかし、これはうまくいきません。 なぜか Lambda の実行環境には存在しない Metadata server (169.254.169.254) につなぎに行こうとしています。

Lambda で動作していることを認識できていないのかと思い、以下のように aws.Credentials.from_file を使って明示的に AWS の認証情報を与えてもうまくいきません。

from google.auth import aws
from google.auth.transport.requests import AuthorizedSession
import google.auth.transport.requests
import google.oauth2.credentials
import google.oauth2.id_token

def get_id_token(service_account, audience):
    credentials = aws.Credentials.from_file(
        os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
    )
    session = AuthorizedSession(credentials)
    request = google.auth.transport.requests.Request(session)
    id_token = google.oauth2.id_token.fetch_id_token(request, audience)

    return  id_token

それもそのはずで、実装 を見に行くと以下のように書かれており、

  1. If the application is running in Compute Engine, App Engine or Cloud Run, then the ID token are obtained from the metadata server.
  2. If the environment variable GOOGLE_APPLICATION_CREDENTIALS is set to the path of a valid service account JSON file, then ID token is acquired using this service account credentials.
  3. If metadata server doesn't exist and no valid service account credentials are found, :class:~google.auth.exceptions.DefaultCredentialsError will be raised.
  1. は GCP 上で動作する場合のみ対象で、頼みの 2. は以下のように読み込んだサービスアカウントの構成ファイルが type = service_account の場合しか対応していません。
with open(credentials_filename, "r") as f:
    info = json.load(f)
    credentials_content = (
        (info.get("type") == "service_account") and info or None
    )

    from google.oauth2 import service_account

    credentials = service_account.IDTokenCredentials.from_service_account_info(
        credentials_content, target_audience=audience
    )

今回ダウンロードした認証情報構成ファイルは type = external_account です。

{
    "type": "external_account",
    "audience": "//iam.googleapis.com/projects/.../locations/global/workloadIdentityPools/.../providers/...",
    "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
    "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/...@....iam.gserviceaccount.com: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"
    }
}

このため、google.oauth2.id_token.fetch_id_tokenを使うのは無理筋です。

IAM credentials の ID Token を取得する API を呼ぶ

google-auth 単体ではダメなのかもしれないと思い、公式の API クライアントを使用してみます。 以下のライブラリを使えるようにした上でコードを実行してみます。

  • google-api-python-client
  • google-cloud-iam
from google.auth import aws
from google.cloud import iam_credentials_v1

def get_id_token(service_account, audience):
    credentials = aws.Credentials.from_file(
        os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
    )
    client = iam_credentials_v1.IAMCredentialsClient(credentials=credentials)
    request = iam_credentials_v1.GenerateIdTokenRequest(
        name="projects/-/serviceAccounts/{}".format(service_account),
        audience=audience,
    )
    response = client.generate_id_token(request=request)
    return response.token

これはうまく動作します。ただし、こちら にある通り、このライブラリはメンテナンスモードとなっており、代わりに google-cloud-python を使うように書かれています。 しかし、リファレンス を見ると ID Token を取得する API は実装されていません! メンテナンスモードになったライブラリを使うのは気が引けるのと、余計な依存ライブラリが増えてサイズも大きくなってしまうので、また別の方法を探すことにします。

google-auth 再訪

どうにかして google-auth 単体でできないかと思い、ID Token 取得エンドポイントの generateIdToken で google-auth のコードを grep していると、 このような実装 があるのを見つけました。

_IAM_ENDPOINT = (
    "https://iamcredentials.googleapis.com/v1/projects/-"
    + "/serviceAccounts/{}:generateAccessToken"
)

エンドポイントの URL が書いてあるということは、それを呼んで ID Token を取得しているはずです。 この実装のあるファイル名は impersonated_credentials.py で、どこかで聞き覚えがある単語だと思ったら Workload Identity 連携を調べていたときに 偶然英語で開かれたドキュメントでみたものでした。 GCP ドキュメント上での日本語訳は"借用"です。サービスアカウントを借用して ID Token を生成する の"借用"です!

たぶんこれだということで、うまくいくことを祈りながら ドキュメント を参考に実装してみます。

import google.auth.transport.requests
from google.auth import aws
from google.auth import impersonated_credentials

def get_id_token(service_account, audience):

    # 実行環境 (Lambda) の認証情報を取得
    credentials = aws.Credentials.from_file(
        os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
    )

    # サービスアカウントを借用
    target_credentials = impersonated_credentials.Credentials(
      source_credentials=credentials,
      target_principal=service_account,
      target_scopes = ["https://www.googleapis.com/auth/cloud-platform"],
      lifetime=500)

    # ID トークンを取得
    imp_cred = impersonated_credentials.IDTokenCredentials(target_credentials, target_audience=audience)
    imp_cred.refresh(google.auth.transport.requests.Request())

    return imp_cred.token

これで無事 ID Token が取得できました。

ところで弊社には ChatGPT, Gemini, Bedrock を使用できる環境があるので、以下のようなお願いを形を変えて何度かしてみましたが、架空の API を作り上げてしまったりしてうまくいきませんでした。いつか上手に使えるようになりたいものです。

AWS Lambda で動作する Python アプリケーションにおいて、認証が必要な Cloud Functions を呼び出すコードを書いてください。Cloud Functions を呼び出す際のサービスアカウント認証情報は Workload Identity 連携を使用して取得するものとします。

おわりに

今回紹介したコードは、認証が必要な Cloud Functions を Lambda から呼び出す構成において AWS と GCP の設定が問題ないかを検証するために書いたものです。 実際のアプリケーションは Go で実装されこのコードは使われなかったので、ここで供養することにしました。
この記事がどなたかのお役に立てば幸いです。