GoogleCloudでカスタムオーディエンスを使用してCloudRunのマルチリージョン環境を作ってみた
はじめに
CloudRunでマルチリージョンの構成をとりたい場合、どのように作成するでしょうか。
CloudRunサービスを複数立ちあげて、サービスごとに別々のリージョンを指定することでマルチリージョンの構成をとるのが一般的かと思います。
CloudRunのSLAは本記事を投稿した2023年11月時点で99.95%となっており、実はそこまで高くありません。(年間6.18時間のダウンタイムが許容されていることになります。)
マルチリージョンの構成をとることで予期せぬダウンタイムへの対応が可能になります。
ではマルチリージョン構成に加えて、サービス間認証をしようとした場合どのような作りになるでしょうか。
例えばCloudRunにデプロイされたアプリへのアクセスを特定のアプリケーションからのみ受け付ける制限をしたい場合、一般的には呼び出し元のアプリ側でアクセストークンを発行し、トークンをヘッダーに付与したうえでリクエストを行います。
しかし呼び出し元でトークンを発行するには呼び出し先のCloudRunサービスのURLが必要となるため、マルチリージョン構成にしているとどのサービスのURLを指定したらよいか判断できません。
そのため私が携わっていたプロジェクトでは泣く泣く単一リージョンにしたのですが、この度カスタムオーディエンスという機能が正式にGAされ、マルチリージョン+サービス間認証の構成を簡単に構築できるようになったためサンプルを交えて設定方法を共有いたします。
サンプルの概要
サンプルではカスタムオーディエンスを指定することによってリクエスト元と同じリージョンのCloudRunサービスにリクエストされることを確認したいと思います。
2つのGoogleCloudプロジェクト(プロジェクトAとプロジェクトB)を用意し、各プロジェクトにリージョンを別にしてCloudRunサービスを2つ用意します。(サンプルでは東京と大阪を指定しました。)
プロジェクトAのtokyo-serviceとosaka-serviceはリージョンが異なるだけで処理はプロジェクトBにリクエストを送っているだけです。
プロジェクトBのサービスはリクエストを受け付けたら、サービス名を表示します。
以下はプロジェクトAのサービスにデプロイしたソース
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"os"
"golang.org/x/oauth2/google"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
)
func main() {
audience := os.Getenv("AUDIENCE")
if audience == "" {
log.Fatal("AUDIENCE environment variable not set")
}
url := os.Getenv("URL")
if url == "" {
log.Fatal("URL environment variable not set")
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
handleRequest(w, r, audience, url)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleRequest(w http.ResponseWriter, r *http.Request, audience, url string) {
//IDトークンを発行する
token, err := generateIDToken(audience)
if err != nil {
http.Error(w, "Error generating ID token", http.StatusInternalServerError)
log.Println(err)
return
}
//リクエストを送信する
resp, err := sendRequest(url, token)
if err != nil {
http.Error(w, "Error sending request", http.StatusInternalServerError)
log.Println(err)
return
}
defer resp.Body.Close()
//レスポンスを取得する
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Error reading response body", http.StatusInternalServerError)
log.Println(err)
return
}
fmt.Fprintf(w, "Response from App B: %s", string(body))
}
func generateIDToken(audience string) (string, error) {
ctx := context.Background()
creds, err := google.FindDefaultCredentials(ctx, audience)
if err != nil {
return "", err
}
idTokenSource, err := idtoken.NewTokenSource(ctx, audience, option.WithCredentials(creds))
if err != nil {
return "", err
}
token, err := idTokenSource.Token()
if err != nil {
return "", err
}
return token.AccessToken, nil
}
func sendRequest(url, token string) (*http.Response, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", "Bearer "+token)
return client.Do(req)
}
以下はプロジェクトBのサービスにデプロイしたソース
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
// どのサービスにルーティングされたかわかりやすくするために環境変数SERVICE_NAMEを取得
serviceName := os.Getenv("SERVICE_NAME")
if serviceName == "" {
serviceName = "Unknown Service"
}
fmt.Fprintf(w, "Hello from %s!", serviceName)
}
CloudRunにデプロイする際はDockerイメージにする必要があるためDockerfileも作成する
# 使用するGoのバージョンを指定
FROM golang:1.21 as builder
# 作業ディレクトリを設定
WORKDIR /app
# ソースコードをコンテナにコピー
COPY . .
# Goアプリケーションをビルド
RUN CGO_ENABLED=0 GOOS=linux go build -v -o app
# 実行ステージ
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# ビルドしたバイナリをコピー
COPY --from=builder /app/app .
# コンテナがリッスンするポートを指定
EXPOSE 8080
# 実行コマンド
CMD ["./app"]
ローカルでdocker buildを行いイメージを作成したら各GCPプロジェクトのArtifactRegistoryにpushしてください。
プロジェクトBのCloudRunの設定
まずはプロジェクトBのCloudRunサービスを作成します。
項目 | 設定内容 |
---|---|
コンテナイメージのURL | 先ほどArtifactRegistryにpushしたイメージ |
リージョン | asia-northeast-1(東京)、asia-northeast-2(大阪) |
環境変数 | SERVICE_NAME(Valueにはレスポンスに表示したいサービス名を指定する) |
認証 | 認証が必要 |
Ingressの制御 | すべて |
カスタムオーディエンスの設定
以下のドキュメントを参考にプロジェクトBのCloudRunサービスに対してカスタムオーディエンスを作成します。
gcloud run services update サービス名 --add-custom-audiences=オーディエンス名
上記コマンドをサービス分実行しますが、オーディエンス名は同じものを指定してください。
コマンド実行後はリージョンを選択するよう促されるのでサービスのリージョンを指定してください。
またロードバランサーを作成してプロジェクトBのCloudRunサービスをバックエンドサービスとして指定してください。
プロジェクトAのCloudRunの設定
次にプロジェクトAのCloudRunサービスを作成します。
項目 | 設定内容 |
---|---|
コンテナイメージのURL | 先ほどArtifactRegistryにpushしたイメージ |
リージョン | asia-northeast-1(東京)、asia-northeast-2(大阪) |
環境変数 | AUDIENCE(Valueにはカスタムオーディエンスのオーディエンス名を指定する) |
環境変数 | URL(ValueにはロードバランサーのグローバルIPを指定する) |
認証 | 未認証の呼び出しを許可 |
Ingressの制御 | すべて |
権限の設定
プロジェクトBのCloudRunサービスにプリンシパルを追加して、プロジェクトAのCloudRunサービスのデフォルトアカウントに「CloudRun起動元」ロールを付与してください。
動作確認
プロジェクトAのサービスに対してリクエストを送信してみましょう。
プロジェクトBのサービスに対してリクエストを送信してみましょう。
これでカスタムオーディエンスを指定することで簡単にマルチリージョン+サービス間認証の構成をとることができました。
ちなみにサービスの詳細画面からカスタムオーディエンスを設定しているかを確認できます。
参考文献
- Google Cloud Runのサービスレベル契約(SLA)に関する公式ドキュメント
https://cloud.google.com/run/sla - Google Cloud Runでのカスタムオーディエンスの設定に関する公式ドキュメント
https://cloud.google.com/run/docs/configuring/custom-audiences?hl=ja#command-line - Google Cloud Artifact RegistryでのDockerイメージのプッシュとプルに関する公式ドキュメント
https://cloud.google.com/artifact-registry/docs/docker/pushing-and-pulling?hl=ja
Discussion