Cloud RunからSecret Managerのシークレットにアクセスする

記事タイトルとURLをコピーする

G-gen の佐々木です。当記事では Cloud Run から Secret Manager に格納したシークレットを利用する方法を解説します。

事前知識

Cloud Run

Cloud Run は Google Cloud のマネージドなコンテナ実行環境でアプリケーションを実行することができる、サーバレス コンテナコンピューティング サービスです。

Cloud Run には Cloud Run services、Cloud Run jobs、Cloud Run functions(旧称:Cloud Functions)の3種類がありますが、当記事の内容はこれら3種類で共通のため、記事内では区別せずに Cloud Run と表記しています。

当記事では、Cloud Run services を例として手順を説明していきます。

それぞれの詳細については以下の記事を参照してください。

Secret Manager

Secret Manager は、API キーやパスワード、証明書といった機密情報(シークレット)を安全に格納するためのサービスです。Secret Manager に格納されたシークレットはバージョン管理され、IAM を用いてアクセス制御を行うことができます。

Secret Manager に格納されたすべてのシークレットはデフォルトで AES256ビット暗号化が施され、読み取りアクセスはすべて TLS で暗号化されます。

Cloud Run で Secret Manager を利用する

ユースケース

Cloud Run 上のアプリケーションからデータベースにアクセスする場合や、外部 API を利用する場合などの認証情報は、Cloud Run の環境変数に格納することができます。この場合、Cloud Run の設定を閲覧できるユーザーであれば、誰でも認証情報にアクセスすることができてしまいます。

環境変数の代わりに Secret Manager のシークレットとして認証情報を管理することで、そのシークレットに対する IAM 権限を持つプリンシパル(ユーザー、アプリケーションなど)のみにアクセスを制限することができます。また、Cloud Run 側の設定を変えることなく、シークレットのバージョニングやローテーションを容易に行うことができます。

2つの方法

Cloud Run で Secret Manager を使用する場合、以下に示す2種類の方法のいずれかを用いてシークレットを読み込むことができます。

シークレットの読み込み方法 特徴
Cloud Run の環境変数として読み込む プログラムから環境変数を読み取ることでシークレットの値にアクセスできる。
シークレットの値はコンテナインスタンス起動時に環境変数として読み込まれるため、シークレットの更新は起動中のコンテナインスタンスに反映されない点に注意が必要。
シークレットのバージョンを latest ではなく特定のバージョンで固定することで、コンテナインスタンスごとに異なる値が参照されないようにすることが推奨される。
Cloud Run のボリュームとしてマウント シークレットをコンテナインスタンスにボリュームとしてマウントし、ファイルシステム経由で値を読み込む。
シークレットの値は、起動中のインスタンスであっても常に最新のものが読み込まれる。

シークレットを環境変数として読み込む場合、シークレットの値を更新したときにすぐに反映されず、データベース接続不可などの障害を引き起こしてしまう可能性があります。そのため、Cloud Run 上のアプリケーションが頻繁に呼び出されるユースケースではボリュームマウントの使用が推奨されます。

バッチ処理など Cloud Run を定期的に実行するようなユースケースでは、都度コンテナインスタンスが起動されるため、シークレットを環境変数として読み込む方法を使用しても更新時の影響は起こりにくいでしょう。

当記事では、それぞれの設定方法を紹介し、シークレット更新時の動作についても実際に確認してみます。

事前準備

シェル変数の設定

当記事では gcloud CLI を使用して各種リソースの作成を行っていきます。gcloud CLI のインストールについてはドキュメントを参照してください。

まず、実行するコマンドで何度か使用する値をシェル変数として設定しておきます。

PROJECT_ID={プロジェクトID} # リソースを作成するプロジェクトの ID
SERVICE_ACCOUNT_NAME={サービスアカウントの名前} # 任意の文字列
REPO_NAME={Artifact Registry リポジトリの名前} # 使用するリポジトリの名前
REGION=asia-northeast1 # 東京リージョン(別のリージョンを使いたい場合は変更する)

Cloud Run のコンテナイメージを格納する Artifact Registry リポジトリがない場合は事前に作成してください。

当記事では、リポジトリが東京リージョン(asia-northeast1)に存在する前提で進めていきます。

シークレットの準備

シークレット作成時に読み込むファイルを作成する

まず、シークレットを作成する際に読み込むテキストファイルを作成します。このファイルには、シークレットの値を記述しておきます。

# シークレットの値を記述したファイルを作成
$ echo "hello" > secret.txt

シークレットの値はシークレット作成時に直接指定することもできますが、入力した値はプロセスのリストに平文で表示され、コマンドの実行履歴としても残ってしまいます。そのため、ファイルから値を読み込む方法が推奨されています。

シークレットを作成する

作成したテキストファイルを使用して、Cloud Run からアクセスする Secret Manager のシークレットを作成します。

当記事では mysecret という名前でシークレットを作成します。このシークレットの名前は、値を参照する際にキーとして使用されます。

# ファイルを指定してシークレットを作成する
$ gcloud secrets create mysecret \
--project=${PROJECT_ID} \
--data-file=secret.txt

サービスアカウントの準備

サービスアカウントを作成する

Cloud Run に設定するサービスアカウントを作成します。

# サービスアカウントを作成する
$ gcloud iam service-accounts create ${SERVICE_ACCOUNT_NAME} \
--project=${PROJECT_ID}

サービスアカウントに権限を付与する

作成したサービスアカウントに対して以下の IAM ロールを紐付け、シークレットのアクセス権限を付与します。

  • Secret Manager のシークレット アクセサーroles/secretmanager.secretAccessor
# サービスアカウントに対してシークレットへのアクセス権を付与する
$ gcloud secrets add-iam-policy-binding mysecret \
--member="serviceAccount:${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"

環境変数を使用したアクセス

サンプルコード(Go)

まずは、シークレットを Cloud Run の環境変数として読み込む方法を試してみます。

以下のサンプルコード(main.go)は、環境変数 SECRET から読み込んだ値を表示するだけのウェブサーバーを実行します。

// main.go
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Read the secret from the environment variable SECRET
secretVal := os.Getenv("SECRET")
fmt.Fprintf(w, "Environment Secret: %s", secretVal)
})
http.ListenAndServe(":8080", nil)
}

コンテナイメージのビルド

Cloud Build を使用してコンテナイメージをビルドし、Artifact Registry のリポジトリにプッシュします。

先ほどの main.go ファイルがあるディレクトリで、以下のコマンドを実行します。コンテナイメージ名は env-secret とします。

# Cloud Build で Buildpack を使用してコンテナイメージをビルドする
$ gcloud builds submit --pack image=${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/env-secret

このコマンドでは Buildpack を使用することで、Dockerfile を用意することなく、ソースコードからコンテナイメージをビルドしています。

Cloud Run のデプロイ

Artifact Registry にプッシュしたコンテナイメージを使用して、Cloud Run サービスをデプロイします。サービス名はコンテナイメージと同じ env-secret とします。

# Secret を環境変数として読み込む Cloud Run サービスをデプロイする
$ gcloud run deploy env-secret \
--image=${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/env-secret \
--project=${PROJECT_ID} \
--region=${REGION} \
--service-account=${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \
--allow-unauthenticated \
--set-secrets=SECRET=mysecret:latest

シークレットは --set-secrets フラグを使用して、以下の形式で設定することができます。

--set-secrets={任意の環境変数名}={シークレットの名前}:{シークレットのバージョン}

Cloud Run の環境変数を確認すると、値としてシークレットが設定されていることがわかります。この環境変数のキー(SECRET)を使用してシークレットの値を読み込むことができます。

シークレットを環境変数として読み込む場合

動作確認

curl コマンドやブラウザを使用して、env-secret サービスにアクセスしてみます。

# env-secret の URL にアクセスする
$ curl $(gcloud run services describe env-secret \
--project=${PROJECT_ID} \
--region=${REGION} \
--format='value(status.url)')

環境変数から読み込まれたシークレットの値を含む、以下のレスポンスが返ってきます。

Environment Secret: hello

ボリュームマウントを使用したアクセス

サンプルコード(Go)

次に、シークレットを Cloud Run のコンテナインスタンスにボリュームマウントし、ファイルシステムからシークレットの値を読み込む方法を試してみます。

以下のコード(main.go)は、/mnt ディレクトリにマウントしたシークレットの値を読み込み、表示するだけのウェブサーバーを実行します。

// main.go
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Read the secret from the file /mnt/secret
secretVal, err := os.ReadFile("/mnt/secret")
if err != nil {
fmt.Fprintf(w, "Error reading secret: %v", err)
return
}
fmt.Fprintf(w, "Mount Secret: %s", secretVal)
})
http.ListenAndServe(":8080", nil)
}

コンテナイメージのビルド

main.go があるディレクトリで以下のコマンドを実行し、コンテナイメージをビルドします。イメージ名は mnt-secret とします。

# Cloud Build で Buildpack を使用してコンテナイメージをビルドする
$ gcloud builds submit --pack image=asia-northeast1-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/mnt-secret

Cloud Run のデプロイ

サービス名をコンテナイメージと同じ mnt-secret として、新たな Cloud Run サービスをデプロイします。

# Secret をボリュームマウントする Cloud Run サービスをデプロイする
$ gcloud run deploy mnt-secret \
--image=${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPO_NAME}/mnt-secret \
--project=${PROJECT_ID} \
--region=${REGION} \
--service-account=${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \
--allow-unauthenticated \
--set-secrets=/mnt/secret=mysecret:latest

シークレットをボリュームマウントする場合、--set-secrets フラグを使用して、以下の形式で設定することができます。

--set-secrets={マウントするパス}={シークレットの名前}:{シークレットのバージョン}

シークレットは Cloud Run の環境変数ではなく、ボリュームとしてマウントされています。当記事の設定の場合、マウントパス /mnt/secret からシークレットの値を読み込むことができます。

シークレットをボリュームマウントする場合

動作確認

curl コマンドやブラウザを使用して、mnt-secret サービスにアクセスしてみます。

# mnt-secret の URL にアクセスする
$ curl $(gcloud run services describe mnt-secret \
--project=${PROJECT_ID} \
--region=${REGION} \
--format='value(status.url)')

ボリュームマウントされたシークレットの値を含む、以下のレスポンスが返ってきます。

Mount Secret: hello

シークレット更新時の動作検証

シークレット更新前の確認

シークレットの読み込み方法ごとの、シークレット更新時の動作を確認してみます。

まず、シークレット更新前にそれぞれのサービスにアクセスし、コンテナインスタンスを起動状態にします。

# env-secret の URL にアクセスする
$ curl $(gcloud run services describe env-secret \
--project=${PROJECT_ID} \
--region=${REGION} \
--format='value(status.url)')
# mnt-secret の URL にアクセスする
$ curl $(gcloud run services describe mnt-secret \
--project=${PROJECT_ID} \
--region=${REGION} \
--format='value(status.url)')

シークレットの更新

シークレットの新しい値を記述したファイルを用意します。

# シークレットの値を記述したファイルを作成する
$ echo "world" > newsecret.txt

以下のコマンドで、シークレットに新しいバージョンを追加します。

# シークレットを更新する
$ gcloud secrets versions add mysecret \
--project=${PROJECT_ID} \
--data-file=newsecret.txt

Cloud Run からはシークレットの latest バージョンを参照するようにしているため、サービスを編集することなくシークレットの更新が自動で反映されます。

動作確認

シークレットをボリュームマウントしたサービスの動作

先に、mnt-secret の動作確認を行います。以下のコマンドでサービスにアクセスします。

# mnt-secret の URL にアクセスする
$ curl $(gcloud run services describe mnt-secret \
--project=${PROJECT_ID} \
--region=${REGION} \
--format='value(status.url)')

レスポンスを確認すると、以下のように更新後のシークレットの値が表示されます。このように、シークレットをボリュームマウントした場合はシークレットの値は常に最新のものが読み込まれます。

Mount Secret: world

シークレットを環境変数として読み込むサービスの動作

env-secret の動作確認を行います。以下のコマンドでサービスにアクセスします。

# env-secret の URL にアクセスする
$ curl $(gcloud run services describe env-secret \
--project=${PROJECT_ID} \
--region=${REGION} \
--format='value(status.url)')

レスポンスを確認すると、以下のように更新前のシークレットの値が表示されます。シークレットの値はコンテナインスタンス起動時に環境変数として読み込まれるため、起動中のコンテナインスタンスは更新前の値を使用してしまいます。

Environment Secret: hello

しばらく待機し、コンテナインスタンスがすべて停止したあと改めてサービスにアクセスすると、更新後のシークレットが読み込まれることがわかります。

Environment Secret: world

参考リンク

佐々木 駿太 (記事一覧)

G-gen最北端、北海道在住のクラウドソリューション部エンジニア

2022年6月にG-genにジョイン。Google Cloud Partner Top Engineer 2025 Fellowに選出。好きなGoogle CloudプロダクトはCloud Run。

趣味はコーヒー、小説(SF、ミステリ)、カラオケなど。