動機
コンテナ化されている常時起動サーバをGCP上で「簡単に」「安く」立ち上げたい。
そのサーバが利用するAPI KEYなどの秘密情報の生の値をGCPサービスの設定に直接書くのではなく、Secret Manager で管理したい。
解決:
- GCEでContainer-Optimized-OS イメージのe2-microを常時起動
- 起動時に secret を gcloud なしで取得する
- docker コマンドに --env-file 取得した secret の内容をで渡す
以下は詳細です。
アプローチと課題
- Cloud Run?
- secret を環境変数に設定する機能はある
- 常時起動させる場合の最小コンテナが n1-standard からで、e2-micro など節約インスタンスが選択できない
- GKE (k8s) ?
- secret を環境変数に設定する機能はある(かなり yaml を書く必要はありそう)
- クラスタがある程度のノードプールを必要とするので、やはり e2-micro x 1 などの節約インスタンスには向かない
- GCE でコンテナ付きVMを使う?
- secret からとってきた値を設定する機能は提供されていない
- 環境変数を直書きすることはできるが、これを避けたい
- コンテナ付きVMではなく Container Optimized OS を使い、自分で secret を取得してその結果をもとに
dcoker run -eKEY1=VALUE1 IMAGE
する?- gcloud コマンドが使えないので secret を取得することができない
- gcloud コマンドをインストールしようとすると権限周りの問題が発生してすぐにはできなさそう
- 標準の各種Linux VMイメージを使う?
- gcloud は入っている
- が、今度は逆に docker コマンドが使えない
- docker コマンドをインストールすることはできそう
- こちらを掘っても良かったが、Cotainer Optimized OS を使いたい気がしてくる
解決1:※アプリケーションの改修が必要なので解決2がおすすめ
「GCE でコンテナ付きVMを使う」アプローチになりました。
- gcloud が入っていなくても curl で直接 Secret Manager のAPIを叩けばよい
- 適切な権限をもった access token が必要だが、VMの中からそれを取得する方法がある
- VM の startup script で secret を取得してホストVMの /tmp/.env などに書き込んでおく
- この /tmp/.env をコンテナから参照できるようにする
- コンテナ起動時に /tmp を /mnt などにマウントしておく
- ただし、このマウント設定はコマンドラインではVMインスタンス作成時にしか指定できない
- 一度 GCEインスタンスを作ってしまったら GCPコンソールから修正するか、非公開の仕様になっている metadata を修正するしかない
- コンテナ起動時に /tmp を /mnt などにマウントしておく
- アプリケーション側で
/mnt/.env
を参照して環境変数を設定するような修正をいれる- アプリケーションに手を入れなければならないところが非常にいけていないので、もっと良い方法があれば探したい→解決2ができたのでそちらを推奨
作成方法
secret の登録
設定したい環境変数の内容を.env
というファイル名(何でもよい)で作成しておきます
KEY1=VALUE1
KEY2=VALUE2
...
これを Secret Manager に登録します。以降、[YOUR_*]
とあるところは適切な値に置き換えてください。
gcloud compute secrets create [YOUR_SECRET_NAME] \
--data-file=.env
startup script の作成
VM用の startup script を startup.sh
というファイル名で作っておきます
#!/bin/sh
# this script is an equivalent to:
# `gcloud secrets versions access latest --secret=$SECRET_NAME > .env`
SECRET_NAME=[YOUR_SECRET_NAME]
PROJECT_ID=$(curl -s -H 'Metadata-Flavor: Google' 'http://metadata.google.internal/computeMetadata/v1/project/project-id')
ACCESS_TOKEN="$(curl -s -H 'Metadata-Flavor: Google' \
'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token' \
| jq -r '.access_token')"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/$PROJECT_ID/secrets/$SECRET_NAME/versions/latest:access" \
| jq -r '.payload.data' | base64 --decode \
> /tmp/.env
アプリケーションの修正
アプリケーションを、/mnt/.env
から環境変数を取得するように改修します。
Python だと dotenv-python を使うなど
VMインスタンスの作成
まず、VM用のサービスアカウントを作成します。
必要な権限は以下のようですが、もう少し絞れるかもしれません。Compute Engine のデフォルトのサービスアカウントを使ってもよいのですが、roles/secretmanager.secretAccessor
は追加でつける必要があります。
- roles/secretmanager.secretAccessor
- roles/containerregistry.ServiceAgent
- roles/logging.bucketWriter
- roles/logging.logWriter
SERVICE_ACCOUNT=[YOUR_SERVICE_ACCOUNT]
PROJECT_ID=[YOUR_PROJECT_ID]
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name=$SERVICE_ACCOUNT
for role in secretmanager.secretAccessor containerregistry.ServiceAgent logging.bucketWriter logging.logWriter; do
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role=roles/$role
done
前述の startup script を使って コンテナ付きVM を作成します。
- ホストの
/tmp
を コンテナの/mnt
にマウント -
startup.sh
を起動時に実行して、Secret Manager から取得した secret の値を/tmp/.env
に書き出す → コンテナから/mnt/.env
として見える
INSTANCE=[YOUR-INSTANCE_NAME]
ZONE=[YOUR_ZONE]
IMAGE=[YOUR_IMAGE]
MACHINE_TYPE=[YOUR_MACHINE_TYPE]
SERVICE_ACCOUNT=[YOUR_SERVICE_ACCOUNT]
PROJECT_ID=[YOUR_PROJECT_ID]
gcloud compute instances create-with-container \
$INSTANCE \
--project_id=$PROJECT_ID \
--zone=$ZONE \
--container-image=$IMAGE \
--machine-type=$MACHINE_TYPE \
--service-account=$SERVICE_ACCOUNT \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--container-mount-host-path=mode=ro,host-path=/tmp,mount-path=/mnt \
--metadata-from-file startup-script=./startup.sh
これで完成!
もっと良い方法がありましたらアイデア歓迎です!マサカリも歓迎です!
解決2:アプリケーションの改修不要
解決1 ではアプリケーションに /tmp/.env を読ませるための改修が必要でしたが、docker の起動を自前でやることでこの対応が不要になりました。
Container-Optimized-OS を利用しつつ、startup-script を用いて docker を起動させます。
secret の設定とサービスアカウントの作成は解決1と同じです。startup-script に、自力で ECRにログインして docker を起動する記述を追加しました。
コンテナイメージの指定 IMAGE=[YOUR_IMAGE]
はこのスクリプトにハードコードしていますが、.env の中に書いておいてそれを参照するようにしてしまえばこのスクリプトを修正せずに使いまわせるので、その方が良いかもしれません。
startup-script
SECRET_NAME=[YOUR_SECRET_NAME]
IMAGE=[YOUR_IMAGE]
# this script is an equivalent to :
# gcloud secrets versions access latest --secret=$SECRET_NAME > .env
function meta {
curl -s -H 'Metadata-Flavor: Google' 'http://metadata.google.internal/computeMetadata'$1
}
PROJECT_ID=$(meta '/v1/project/project-id')
ACCESS_TOKEN="$(meta '/v1/instance/service-accounts/default/token' | jq -r '.access_token')"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://secretmanager.googleapis.com/v1/projects/${PROJECT_ID}/secrets/${SECRET_NAME}/versions/latest:access" \
| jq -r '.payload.data' \
| base64 --decode \
> /tmp/.env
HOME=/tmp docker-credential-gcr configure-docker
HOME=/tmp docker run --name=mycloudservice --restart always --env-file=/tmp/.env $IMAGE &
起動後も /tmp/.env が残るのが少しいけていない感はあります。
GCEインスタンスの作成
GCEインスタンスの作成は create-with-container
ではなく普通の create
になります。
gcloud compute instances create \
$INSTANCE_NAME \
--zone $ZONE \
--machine-type=$MACHINE_TYPE \
--service-account $SERVICE_ACCOUNT \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--image-family cos-stable \
--image-project cos-cloud \
--metadata-from-file startup-script=./startup-script
補足
解決1での コンテナ設定部分の VM instance metadata は現時点では以下のようになっていました。
value: |-
spec:
restartPolicy: Always
containers:
- name: [YOUR_INSTANCE_NAME]
image: '[YOUR_IMAGE]'
volumeMounts:
- name: host-path-0
mountPath: /mnt
readOnly: true
volumes:
- name: host-path-0
hostPath:
path: /tmp
# This container declaration format is not public API and may change without notice. Please
# use gcloud command-line tool or Google Cloud Console to run Containers on Google Compute Engine.