Cloud Deploy で Cloud Run functions に継続的デリバリーする
Cloud Deploy は継続的デリバリーを行うための Google Cloud のフルマネージドサービスです。
標準では Google Kubernetes Engine と Cloud Run (service と job) へのデプロイをサポートしていますが、カスタムターゲットを定義することでそれ以外の対象にもデプロイすることができます。
今回はカスタムターゲットを利用して Cloud Run functions へのデプロイを自動化してみます。
本記事では Cloud Deploy の基本的な概念(ターゲット、リリース、デプロイパイプラインなど)については説明しません。
これらについては以下の記事が分かりやすかったのでご覧下さい。
本記事のコードは以下のリポジトリで公開しています。
カスタムターゲットについて
実は Cloud Deploy にはカスタムターゲットというリソースがあるわけではありません。
実際にはカスタムターゲットは以下の 3 種類のリソースで構成されます。
- カスタムアクション
- カスタムターゲットタイプの定義
- ターゲット定義
カスタムアクションではデプロイ対象についてのレンダリングアクションとデプロイアクションを定義します。
そして上記で定義したアクションを使ってカスタムターゲットタイプの定義を作成します。
最後に上記の定義を使ってターゲット定義を作成します。
実際のコードは後の章でお見せしますが、カスタムターゲットタイプの定義とターゲット定義はそれより前に作成したものを参照するだけでそんなに難しくありません。
カスタムアクションについては少しややこしいのでもう少し詳しく説明します。
レンダリングアクションの入力
レンダリングアクションでは環境変数として様々な値を受け取ります。
ここでは特に重要な以下の 3 つの変数について説明します。
CLOUD_DEPLOY_FEATURES
CLOUD_DEPLOY_PERCENTAGE_DEPLOY
CLOUD_DEPLOY_OUTPUT_GCS_PATH
CLOUD_DEPLOY_FEATURES
ではカスタムアクションがサポートしなくてはいけない Cloud Deploy の機能が与えられます。
実際にはこの値は空文字列か CANARY
のどちらかです(標準デプロイの場合は空文字列、カナリアデプロイの場合は CANARY
)。
カスタムアクションがカナリアデプロイをサポートしておらず、CLOUD_DEPLOY_FEATURES
が CANARY
だった場合、アクションは失敗することが推奨されています。
CLOUD_DEPLOY_PERCENTAGE_DEPLOY
はデプロイの割合です。
標準デプロイとカナリアデプロイの stable
フェーズの場合は 100
になります。
CLOUD_DEPLOY_OUTPUT_GCS_PATH
はレンダリングアクションが結果を出力するべき Cloud Storage のパスです。
どのようなファイルを出力する必要があるかは次の節で説明します。
レンダリングアクションの出力
レンダリングアクションでは以下の 2 種類のファイルを CLOUD_DEPLOY_OUTPUT_GCS_PATH
で指定されたパスに作成する必要があります。
- レンダリングされた構成ファイル
results.json
構成ファイルとして何を出力するべきかは、そのカスタムアクションでどこにデプロイしようとしているかによって変わります。
例えばデプロイアクションで Terraform を利用するなら tf ファイルですし、K8s 関連なら YAML 形式の何かになるでしょう。
results.json
に含めるべき情報はいくつかありますが、必須なのは以下の 2 つです。
resultStatus
manifestFile
resultStatus
はそのレンダリングアクションの成功/失敗を表し、値は SUCCEEDED
か FAILED
のどちらかです。
manifestFile
は構成ファイルのパスです。
result.json
の例を以下に示します。
{
"resultStatus": "SUCCEEDED",
"manifestFile": "gs://bucket/my-pipeline/release-001/rollout-a/01234/custom-output/manifest.yaml"
}
デプロイアクションの入力
レンダリングアクションと同じく、デプロイアクションでも環境変数として様々な値を受け取ります。
デプロイアクションの場合は以下の 4 つが重要です。
CLOUD_DEPLOY_FEATURES
CLOUD_DEPLOY_PERCENTAGE_DEPLOY
CLOUD_DEPLOY_MANIFEST_GCS_PATH
CLOUD_DEPLOY_OUTPUT_GCS_PATH
このうち、CLOUD_DEPLOY_FEATURES
, CLOUD_DEPLOY_PERCENTAGE_DEPLOY
, CLOUD_DEPLOY_OUTPUT_GCS_PATH
についてはレンダリングアクションと同じような意味を持ちます。
CLOUD_DEPLOY_MANIFEST_GCS_PATH
はレンダリングアクションで出力した構成ファイルの Cloud Storage パスになります。
このファイルを取得し、デプロイ操作をしていくことになります。
デプロイアクションの出力
デプロイアクションでは results.json
を CLOUD_DEPLOY_OUTPUT_GCS_PATH
で指定されたパスに作成する必要があります。
デプロイアクションでも results.json
に含めるべき情報はいくつかありますが、必須なのは resultStatus
のみです。
デプロイアクションの場合、resultStatus
の値は SUCCEEDED
, FAILED
, SKIPPED
のいずれかです。
SKIPPED
については、対象のアクションがカナリアフェーズで実行されており(つまり CLOUD_DEPLOY_PERCENTAGE_DEPLOY
が 100
でなく)、対象の環境にまだ一度もデプロイされていない場合に出力します。
これはこれまでに一度もデプロイされていない環境に対してはカナリアデプロイが意味を持たず、stable
フェーズが実行されるべきなためです。
デプロイパラメータ
カスタムアクションでは上記で説明したような Cloud Deploy が提供する値以外にも動的な値が欲しくなります。
Cloud Deploy ではリリース作成時に動的な値をデプロイパラメータとして渡すことができます。
カスタムターゲットの場合は customTarget/
という prefix をデプロイパラメータにつけることで、レンダリングアクションとデプロイアクションの両方で環境変数として参照できるようになります。
例えば customTarget/vertexAIModel
というデプロイパラメータを渡した場合、アクションでは CLOUD_DEPLOY_customTarget_vertexAIModel
で参照できます。
標準デプロイ
この章では Cloud Run functions に対して標準デプロイ、すなわち常に 100% の割合でデプロイするパイプラインを実装します。
実装 (skaffold.yaml)
まずはカスタムアクションを作成します。
雛形は以下のようになります。
apiVersion: skaffold/v4beta7
kind: Config
customActions:
- name: cloud-run-functions-renderer
containers:
- name: render
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:503.0.0-stable
command: ["/bin/bash"]
args:
- "-c"
- |-
...
- name: cloud-run-functions-deployer
containers:
- name: deploy
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:503.0.0-stable
command: ["/bin/bash"]
args:
- "-c"
- |-
...
まずレンダリングアクションについてですが、レンダリングアクションは構成ファイルと result.json
を Cloud Storage に作成する必要がありました。
Cloud Run functions では構成ファイルはありませんから、ここではそれっぽい内容を出力すれば良いです。
今回はデプロイパラメータで関数のコミットハッシュを与えることにして、それを出力しておくことにします。
echo {\"commit\": \"$CLOUD_DEPLOY_customTarget_commit\"} > manifest.json
gcloud storage cp manifest.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json
result.json
についてですが、この章ではカナリアデプロイはサポートしないため、CLOUD_DEPLOY_FEATURES
が CANARY
なら失敗、そうでなければ成功のステータスで作成します。
if [ \"$CLOUD_DEPLOY_FEATURES\" = \"CANARY\" ]; then
echo {\"resultStatus\": \"FAILED\", \"manifestFile\": \"$CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json\"} > results.json
else
echo {\"resultStatus\": \"SUCCEEDED\", \"manifestFile\": \"$CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json\"} > results.json
fi
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
次にデプロイアクションについてです。
デプロイアクションでは result.json
だけ出力すれば良いので、CLOUD_DEPLOY_FEATURES
が CANARY
の場合の早期リターンしてしまいます。
if [ \"$CLOUD_DEPLOY_FEATURES\" = \"CANARY\" ]; then
echo {\"resultStatus\": \"FAILED\"} > results.json
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
exit 0
fi
実際のデプロイについてですが、デプロイパラメータで指定されたコミットハッシュでリポジトリをチェックアウトし、gcloud functions deploy
コマンドでデプロイするようにします。
apt-get update
apt-get install -y git
git clone https://github.com/YunosukeY/cloud-deploy-cloud-run-functions-sample.git
cd cloud-deploy-cloud-run-functions-sample
git checkout $CLOUD_DEPLOY_customTarget_commit
gcloud functions deploy go-http-function \
--gen2 \
--runtime=go123 \
--region=asia-northeast1 \
--source=. \
--entry-point=HelloGet \
--trigger-http \
--no-allow-unauthenticated
最後に成功の場合の result.json
を出力します。
echo {\"resultStatus\": \"SUCCEEDED\"} > results.json
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
skaffold.yaml
については以上で完成です。
skaffold.yaml の全体
apiVersion: skaffold/v4beta7
kind: Config
customActions:
- name: cloud-run-functions-renderer
containers:
- name: render
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:503.0.0-stable
command: ["/bin/bash"]
args:
- "-c"
- |-
echo {\"commit\": \"$CLOUD_DEPLOY_customTarget_commit\"} > manifest.json
gcloud storage cp manifest.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json
if [ \"$CLOUD_DEPLOY_FEATURES\" = \"CANARY\" ]; then
echo {\"resultStatus\": \"FAILED\", \"manifestFile\": \"$CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json\"} > results.json
else
echo {\"resultStatus\": \"SUCCEEDED\", \"manifestFile\": \"$CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json\"} > results.json
fi
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
- name: cloud-run-functions-deployer
containers:
- name: deploy
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:503.0.0-stable
command: ["/bin/bash"]
args:
- "-c"
- |-
if [ \"$CLOUD_DEPLOY_FEATURES\" = \"CANARY\" ]; then
echo {\"resultStatus\": \"FAILED\"} > results.json
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
exit 0
fi
apt-get update
apt-get install -y git
git clone https://github.com/YunosukeY/cloud-deploy-cloud-run-functions-sample.git
cd cloud-deploy-cloud-run-functions-sample
git checkout $CLOUD_DEPLOY_customTarget_commit
gcloud functions deploy go-http-function \
--gen2 \
--runtime=go123 \
--region=asia-northeast1 \
--source=. \
--entry-point=HelloGet \
--trigger-http \
--no-allow-unauthenticated
echo {\"resultStatus\": \"SUCCEEDED\"} > results.json
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
実装 (clouddeploy.yaml)
次にカスタムターゲットタイプ、ターゲット、デプロイパイプラインをそれぞれ定義します。
とは言ってもここはそれより前に参照していくだけです。
カスタムターゲットタイプ
apiVersion: deploy.cloud.google.com/v1
kind: CustomTargetType
metadata:
name: cloud-run-functions
customActions:
renderAction: cloud-run-functions-renderer
deployAction: cloud-run-functions-deployer
ターゲット
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: sample-env
customTarget:
customTargetType: cloud-run-functions
デプロイパイプライン
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: cloud-run-functions-pipeline
serialPipeline:
stages:
- targetId: sample-env
clouddeploy.yaml
については以上で完成です。
clouddeploy.yaml の全体
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: cloud-run-functions-pipeline
serialPipeline:
stages:
- targetId: sample-env
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: sample-env
customTarget:
customTargetType: cloud-run-functions
---
apiVersion: deploy.cloud.google.com/v1
kind: CustomTargetType
metadata:
name: cloud-run-functions
customActions:
renderAction: cloud-run-functions-renderer
deployAction: cloud-run-functions-deployer
動作確認
パイプライン作成
gcloud deploy apply \
--file=clouddeploy.yaml \
--region=asia-northeast1
リリース作成
デプロイパラメータで最初のコミットを指定します。
gcloud deploy releases create test-release-001 \
--region=asia-northeast1 \
--delivery-pipeline=cloud-run-functions-pipeline \
--deploy-parameters="customTarget/commit=0cbb1d72abe7d5fac69d6c9e9fbb5816b587d612"
レンダリングが完了すると Cloud Storage にコミットハッシュの書かれた manifest.json
が作成されます。
{
"commit": "0cbb1d72abe7d5fac69d6c9e9fbb5816b587d612"
}
デプロイ完了後、ログに表示される関数の URL に対してリクエストを送ると Hello, World!
が返ります。
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World!%
カナリアデプロイ
この章では、前の章で作成した skaffold.yaml
と clouddeploy.yaml
を修正し、Cloud Run functions に対してカナリアデプロイするパイプラインを実装します。
修正 (skaffold.yaml)
まずレンダリングアクションについてですが、カナリアデプロイをサポートするため、分岐は削除し必ず成功するように修正します。
+echo {\"resultStatus\": \"SUCCEEDED\", \"manifestFile\": \"$CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json\"} > results.json
-if [ \"$CLOUD_DEPLOY_FEATURES\" = \"CANARY\" ]; then
- echo {\"resultStatus\": \"FAILED\", \"manifestFile\": \"$CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json\"} > results.json
-else
- echo {\"resultStatus\": \"SUCCEEDED\", \"manifestFile\": \"$CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json\"} > results.json
-fi
次にデプロイアクションについてです。
デプロイアクションでは、現在デプロイのリビジョンが分かると便利なので初めに取得します。
+LATEST_REVISION=$(gcloud run revisions list --region=asia-northeast1 --service=go-http-function --format="value(REVISION)" --limit=1)
次にレンダリングアクションと同じく、カナリアデプロイをサポートするため、分岐は削除します。
その代わりにカナリアフェーズでこれまで一度もデプロイされていない場合にスキップする分岐を追加します。
+if [ \"$CLOUD_DEPLOY_FEATURES\" = \"CANARY\" ] \
+ && [ \"$CLOUD_DEPLOY_PERCENTAGE_DEPLOY\" -ne 100 ] \
+ && [ -z \"$LATEST_REVISION\" ]; then
+ echo {\"resultStatus\": \"SKIPPED\"} > results.json
-if [ \"$CLOUD_DEPLOY_FEATURES\" = \"CANARY\" ]; then
- echo {\"resultStatus\": \"FAILED\"} > results.json
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
exit 0
fi
実際のデプロイについてですが、gcloud beta run deploy
コマンドを用いてトラフィックなしでデプロイし、その後 gcloud run services update-traffic
コマンドを用いて割合を変更します。
注意としては、gcloud beta run deploy
コマンドについては現在のコミットハッシュの関数がデプロイされていない場合のみ行う必要があります。
そこでリビジョンの suffix をコミットハッシュにするようにしています。
+if [ \"$LATEST_REVISION\" != \"go-http-function-$CLOUD_DEPLOY_customTarget_commit\" ]; then
...
+ gcloud beta run deploy go-http-function \
+ --base-image go122 \
+ --region asia-northeast1 \
+ --source . \
+ --function HelloGet \
+ --no-allow-unauthenticated \
+ --no-traffic \
+ --revision-suffix $CLOUD_DEPLOY_customTarget_commit
-gcloud functions deploy go-http-function \
- --gen2 \
- --runtime=go123 \
- --region=asia-northeast1 \
- --source=. \
- --entry-point=HelloGet \
- --trigger-http \
- --no-allow-unauthenticated
+fi
+gcloud run services update-traffic go-http-function \
+ --region asia-northeast1 \
+ --to-revisions=LATEST=$CLOUD_DEPLOY_PERCENTAGE_DEPLOY
skaffold.yaml
については以上で完了です。
skaffold.yaml の全体
apiVersion: skaffold/v4beta7
kind: Config
customActions:
- name: cloud-run-functions-renderer
containers:
- name: render
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:503.0.0-stable
command: ["/bin/bash"]
args:
- "-c"
- |-
echo {\"commit\": \"$CLOUD_DEPLOY_customTarget_commit\"} > manifest.json
gcloud storage cp manifest.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json
echo {\"resultStatus\": \"SUCCEEDED\", \"manifestFile\": \"$CLOUD_DEPLOY_OUTPUT_GCS_PATH/manifest.json\"} > results.json
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
- name: cloud-run-functions-deployer
containers:
- name: deploy
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:503.0.0-stable
command: ["/bin/bash"]
args:
- "-c"
- |-
LATEST_REVISION=$(gcloud run revisions list --region=asia-northeast1 --service=go-http-function --format="value(REVISION)" --limit=1)
if [ \"$CLOUD_DEPLOY_FEATURES\" = \"CANARY\" ] \
&& [ \"$CLOUD_DEPLOY_PERCENTAGE_DEPLOY\" -ne 100 ] \
&& [ -z \"$LATEST_REVISION\" ]; then
echo {\"resultStatus\": \"SKIPPED\"} > results.json
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
exit 0
fi
if [ \"$LATEST_REVISION\" != \"go-http-function-$CLOUD_DEPLOY_customTarget_commit\" ]; then
apt-get update
apt-get install -y git
git clone https://github.com/YunosukeY/cloud-deploy-cloud-run-functions-sample.git
cd cloud-deploy-cloud-run-functions-sample
git checkout $CLOUD_DEPLOY_customTarget_commit
gcloud beta run deploy go-http-function \
--base-image go122 \
--region asia-northeast1 \
--source . \
--function HelloGet \
--no-allow-unauthenticated \
--no-traffic \
--revision-suffix $CLOUD_DEPLOY_customTarget_commit
fi
gcloud run services update-traffic go-http-function \
--region asia-northeast1 \
--to-revisions=LATEST=$CLOUD_DEPLOY_PERCENTAGE_DEPLOY
echo {\"resultStatus\": \"SUCCEEDED\"} > results.json
gcloud storage cp results.json $CLOUD_DEPLOY_OUTPUT_GCS_PATH/results.json
修正 (clouddeploy.yaml)
clouddeploy.yaml
についてですが、カスタムターゲットタイプとターゲットは修正不要です。
デプロイパイプラインについてはカナリアデプロイについての定義を追加します。
カスタムターゲットの場合、カナリアデプロイはカスタムカナリアとして定義します。
serialPipeline:
stages:
- targetId: sample-env
+ strategy:
+ canary:
+ customCanaryDeployment:
+ phaseConfigs:
+ - phaseId: canary-25
+ percentage: 25
+ - phaseId: canary-50
+ percentage: 50
+ - phaseId: canary-75
+ percentage: 75
+ - phaseId: stable
+ percentage: 100
clouddeploy.yaml
については以上で完了です。
clouddeploy.yaml の全体
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
name: cloud-run-functions-pipeline
serialPipeline:
stages:
- targetId: sample-env
strategy:
canary:
customCanaryDeployment:
phaseConfigs:
- phaseId: canary-25
percentage: 25
- phaseId: canary-50
percentage: 50
- phaseId: canary-75
percentage: 75
- phaseId: stable
percentage: 100
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
name: sample-env
customTarget:
customTargetType: cloud-run-functions
---
apiVersion: deploy.cloud.google.com/v1
kind: CustomTargetType
metadata:
name: cloud-run-functions
customActions:
renderAction: cloud-run-functions-renderer
deployAction: cloud-run-functions-deployer
動作確認
パイプラインの更新
gcloud deploy apply \
--file=clouddeploy.yaml \
--region=asia-northeast1
リリース作成
デプロイパラメータで 2 つ目のコミットを指定します。
gcloud deploy releases create test-release-002 \
--region=asia-northeast1 \
--delivery-pipeline=cloud-run-functions-pipeline \
--deploy-parameters="customTarget/commit=7252129b3f438a2fe0f61d49b54d0f9bb811636d"
デプロイが完了すると Cloud Run の画面でトラフィックが分割されていることを確認できます。
関数の URL に対してリクエストを送ると数回に 1 回 Hello, World2!
が返るようになっています。
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World!%
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World2!%
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World!%
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World!%
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World!%
カナリアを進めます。
gcloud deploy rollouts advance test-release-002-to-sample-env-0001 \
--release=test-release-002 \
--delivery-pipeline=cloud-run-functions-pipeline \
--region=asia-northeast1 \
--quiet
デプロイが完了すると、トラフィックの割合が更新されています。
関数の URL に対してリクエストを送ると半分は Hello, World2!
が返るようになっています。
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World2!%
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World!%
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World!%
curl {関数のURL} -H "Authorization: Bearer $(gcloud auth print-identity-token)"
Hello, World2!%
謝辞
本記事は、以下の記事で同僚の方が書いてくれた疑問が元ネタとなっています。
Cloud Functions などの Cloud Deploy に対応してないリソースがある場合はどうするんだろうと思った。
私自身 Cloud Deploy を触ったことがなかったため、とても良いきっかけになりました。
ありがとうございます。
参考
Discussion