ぷらすのブログ

Vercel・Cloud Run間の通信をIAMで認証する

#開発#Vercel#Google Cloud#Cloud Run

こんにちは、@p1assです。 Google Cloud の IAM を使ったテクニックを紹介します。

モチベーション

とある趣味のプロジェクトで、Vercel に Node.js のフロントエンドサーバー、Cloud Run にバックエンド API をホスティングするアーキテクチャを設計しました。 Cloud Run にホスティングしている API は、フロントエンドサーバーが SSR するときに呼び出されます。 一方で、ブラウザから直接 API を叩くことはありません。

アーキテクチャ 簡略化したアーキテクチャ

このようなアーキテクチャでは、Vercel から Cloud Run にアクセスできるようにするために、Cloud Run のエンドポイントをインターネットに公開する必要があります。 しかし、何も対策をせずインターネットに公開してしまうと、URL が露出してしまった際に第三者から API を叩かれてしまう危険性がありました。

そこで、IAM の仕組みを使うことで、エンドポイントをインターネットに公開しつつも、Vercel からしか Cloud Run の API を叩けないようにする仕組みを導入したいです。

IAM を使って認証する

IAM による Cloud Run のサービス間認証の仕組み

まずは、Vercel ではなく Cloud Run 同士の通信を IAM で認証する仕組みを知る必要があります。

Cloud Run のサービス間認証では IAM が使われます。 ここでは、ClientServer という 2 つの Cloud Run サービスがあると仮定して説明します。

まず、Client の Cloud Run サービスにアタッチするサービスアカウント用意します。 ここでは、service-account-client とします。

gcloud iam service-accounts create "service-account-client" \
    --display-name="service-account-client"

作成したサービスアカウントは Cloud Run で使うように設定しておきましょう。 (コマンドは省略)

次に、Cloud Run サービスを呼び出すために必要なロールである roles/run.invoker を持つ IAM ポリシーを作成します。 このとき、IAM ポリシーのプリンシパルはロールを使いたい方、つまり呼び出し側の service-account-client になります。

最後に、この IAM ポリシーを Server の Cloud Run サービスにバインドします。 なお、gcloud CLI でバインドする場合は、明示的に IAM ポリシーを作成せずに 1 コマンドで実行できます。 Terraform を使う場合は、これらの違いを意識して resource を書く必要があります。

# memberがプリンシパルにあたる
gcloud run services add-iam-policy-binding "Server" \
  --member='serviceAccount:service-account-client@[PROJECT_ID].iam.gserviceaccount.com' \
  --role='roles/run.invoker'

以上の設定により、Server の Cloud Run を叩ける権限を持つのは、service-account-client のみとなり、Client の Cloud Run からしかリクエストを送れなくなります。

この設定を行うことで、開発者は特に意識せずとも裏側で認証を行ってくれます。

サービス間認証の仕組みを Vercel 上に構築する

Vercel から Cloud Run へのリクエストの認証を行うためには、認証の裏側のロジックも知っておく必要があります。

サービス間認証では、OpenID Connect に準拠したIDTokenを発行し、受け取り側が IDToken を検証することで認証が行われます。 IDToken は Google の管理する秘密鍵によって署名されているので、受信側(Server)は公開鍵で署名を検証することで、リクエスト元の身元を認証できます。

先程のドキュメントによれば、IDToken は次の 2 つのいずれかのヘッダーによって送信されるようです。

  • Authorization: Bearer ID_TOKEN ヘッダー。- X-Serverless-Authorization: Bearer ID_TOKEN ヘッダー。アプリケーションがすでにカスタム承認に Authorization ヘッダーを使用している場合は、このヘッダーを使用できます。これにより、トークンがユーザー コンテナに渡される前に署名が削除されます。

Cloud Run 同士の通信の場合は IDToken は自動的に発行されますが、Vercel の場合は手動で IDToken を発行する必要があります。

ここでは、Node.js で IDToken を取得・送信するコードを見ていきます。 まず、事前準備としてサービスアカウントの JSON を発行し、環境変数に登録します。 本来は Vercel 上で登録しますが、ローカルで試すなら以下のようなコマンドになるでしょう。

export SERVICE_ACCOUNT_JSON={....}

IDToken は npm に公開されている google-auth-library を使うことで生成できます。

import { GoogleAuth } from "google-auth-library";

// aud="https://my-cloud-run-service.run.app"
async function getAuthorizationHeaderWithIdToken(aud: string) {
  const serviceAccountJsonString = process.env.SERVICE_ACCOUNT_JSON;
  if (!serviceAccountJsonString) {
    throw new Error(
      "The $SERVICE_ACCOUNT_JSON environment variable was not found"
    );
  }

  const googleAuth = new GoogleAuth({
    credentials: JSON.parse(serviceAccountJsonString),
  });
  const client = await googleAuth.getIdTokenClient(aud);

  // clientを使ってリクエストを送っても良いが、IDTokenだけ欲しい場合はヘッダーから抜き出す
  const clientHeaders = await client.getRequestHeaders();
  const authorizationHeaderWithIdToken = clientHeaders["Authorization"];
  return authorizationHeaderWithIdToken; // "Bearer eyJ...." の文字列
}

まず、サービスアカウントを読み込み、credentials として渡します。 その後、googleAuth.getIdTokenClient(aud) で HTTP Client を作成します。

aud は OpenID Connect の文脈での Audience です。 Cloud Run では、Cloud Run サービスの URL を指定します。 なお、注意ですがカスタムドメインを aud として指定することは出来ないようです。 run.app の URL を指定する必要があります。

終わりに

この記事では、Vercel・Cloud Run 間の通信を IAM で認証認可する仕組みを紹介しました。

あくまでサンプルなので、IDToken の使いまわしや有効期限チェックなどのロジックを作り込む必要がありますが、便利な仕組みなので有効活用していきたいです。

← 有機ELテレビを買った 【SONY BRAVIA XRJ-48A90K】Google DomainsからCloudflareにドメインを移管した →
Topへ戻る