はじめに
これは2021年振り返りカレンダー及びついに卒業かぁカレンダーの1日目の記事です.
昨年はスタートアップにて新規サービス開発におけるCDフローの構築を複数回行ったので,そこから得られた知見をまず書いていこうと思います.前提として,チームは数人,トラフィックもそんなに大きくないという想定での設計です.
まずはCloud RunでのGitHub flow の構築についてです. 最近見る新規サービスはこれに乗ってるのをよく見る気がします.
(今回作るのはこんな強そうなのではないですが...)
お金になるかわからない初期サービスにおいてCloud Runを用いるメリットは以下があると思っています.
- リクエストがない時は一定時間でインスタンスが落ち,課金もされない
- 常時インスタンスを立てることももちろんできる
- オートスケール
- revision機能があり,新規バージョンでバグが出た時はすぐに前のバージョンに戻せる(DBの依存なければ).
また,GitHub flowの解説については,大まかに要点をまとめると,
- mainブランチは常にデプロイ可能
- 新規の変更はmainからブランチを切りPRを出しmainにマージ.
みたいな感じです.
仕様
今回は,フロントエンド(SSR),バックエンドの両方をCloud Run上に乗せている形を想定します. また,環境は一旦stg環境とprd環境の2つとします(何個でも良い).
そして,仕様は次のような形です.
- mainブランチがプロダクトのデプロイ可能な最新コードになっているよう管理.
- 新機能は feature ブランチ,fix ブランチなどとして,mainから切り,レビュー後に main にマージ.
きりの良いタイミングで,stg環境へのデプロイを行う.バージョンタグとともにコマンドを以下の形で打つと,リリースノートの生成と,stg環境へのデプロイがされる.
sh release.sh release v0.0.0
動作確認ができたタイミングで,migarationなどを行ったのち,prd環境をデプロイ.
- prdでバグが起きたら,修正 or 前のバージョンにロールバック
実装
ディレクトリ構成は以下のような感じです(モノレポ). CDフローに必要なファイルなどは,release下に,環境ごとに用意します.
root/ ├── .github ├── api ├── ui ├── release │ ├── dev │ └── prd └── release.sh
リリース
リリースでは以下を行います.
リリースタグをpush
以下のようなスクリプトを用意しておくと,tag pushがしやすいです.
#!/bin/sh function release() { git checkout main git pull origin main git tag -a $2 -m "release $2" git push origin $2 } $1 $@
以下のような形で使います.
sh release.sh release v0.0.0
リリースノート生成
各バージョンごとに,前回のリリースからどのような変更があったのかを見れるようにしておくのは,
などのために有用です.
今回は,前回のリリースからの差分コミットをリリースノートにのせるように, 以下のようなGitHub Actionsのワークフローを作りました.
この記事の内容をそのまま使わせていただきました. zenn.dev
バージョンタグを表す v から始まるtagがpushされた時に,リリースノート生成が行われます.
リリースノートの生成は,もっと新しい方法が色々ありそうです.
stg環境へのデプロイ
stg環境へのデプロイは,cloud buildをgithubと連携させることで行います.
デプロイ用のスクリプトなどは,release/stg下で,以下のような構成で管理します.
release/stg ├── .env ├── .env.secret ├── deploy.sh ├── deploy_api.yaml └── deploy_ui.yaml
deploy.shはapi及びuiをデプロイするためのコマンドを,各yamlファイルはcloud buildの構成を, .envは環境変数を,.env.secretはGCPのsecret managerに保存したシークレットの名前を管理します.
まずdeploy.sh,.env,.env.secretの内容について解説します.
deploy.shのdeploy_apiでは,バージョンタグ,.env,.env.secretを引数として受け取り, apiのコンテナをcloud runにデプロイします.
sh deploy.sh deploy_api v0.0.0 .env .env.secret
deploy_uiもだいたい同じです.
このように.envや.env.secretを別個に用意する構成にすることで, メンバーの全員が環境変数の追加を簡単にできる などのメリットがあります(特に開発初期は追加が多いので).
#!/usr/loca/bin/bash # deploy.sh function deploy_api() { # 固定変数の定義 [[ -n "$PROJECT_ID" ]] || PROJECT_ID="service-stg" [[ -n "$IMAGE_ID" ]] || IMAGE_ID="service-api" [[ -n "$SERVICE_NAME" ]] || SERVICE_NAME="service-stg-api" [[ -n "$SERVICE_ACCOUNT" ]] || SERVICE_ACCOUNT="service-stg-api" [[ -n "$REGION" ]] || REGION="asia-northeast1" [[ -n "$INSTANCE_CONNECTION_NAME" ]] || INSTANCE_CONNECTION_NAME="service-stg:asia-northeast1:service-stg" [[ -n "$CLOUD_RUN_MIN_INSTANCES" ]] || CLOUD_RUN_MIN_INSTANCES="1" [[ -n "$CLOUD_RUN_MAX_INSTANCES" ]] || CLOUD_RUN_MAX_INSTANCES="5" [[ -n "$CLOUD_RUN_MEMORY" ]] || CLOUD_RUN_MEMORY="2Gi" [[ -n "$CLOUD_RUN_CPU" ]] || CLOUD_RUN_CPU="2" # バージョンタグを引数として受け取る TAG_NAME=$4 # cloud runのサービス名のsuffixにバージョンを付ける BASE_COMMAND=" gcloud beta run deploy $SERVICE_NAME \ --revision-suffix $(echo $TAG_NAME | sed -e s/\\./-/g) \ --image gcr.io/$PROJECT_ID/$IMAGE_ID:$TAG_NAME \ --region $REGION \ --platform managed \ --min-instances $CLOUD_RUN_MIN_INSTANCES \ --max-instances $CLOUD_RUN_MAX_INSTANCES \ --memory $CLOUD_RUN_MEMORY \ --cpu $CLOUD_RUN_CPU \ --service-account $SERVICE_ACCOUNT \ --add-cloudsql-instances $INSTANCE_CONNECTION_NAME \ " # .envから環境変数を読み込み command=$BASE_COMMAND while read line do IFS='=' read -r -a array <<< $line env_name=${array[0]} env_value=${array[1]} command="$command --update-env-vars $env_name=$env_value" done < $2 # secretを読み込み secret_options="--set-secrets " while read line do IFS='=' read -r -a array <<< $line secret_name=${array[0]} secret_value=${array[1]} secret_options="$secret_options$secret_name=$secret_value:latest," done < $3 command="$command $secret_options" $($command) } $1 $@
#.env APP_PORT=8080 APP_CLIENT_HOST=http://service-stg.com ...
#.env.secret DB_PASSWORD=service-stg-mysql-password ...
cloud buildの構成ファイルでは,コンテナのビルドを行った後,上記のスクリプトを使ってデプロイを行います. デプロイスクリプトを構成ファイルと分離したことで内容がかなりスッキリし,記法が独特でわかりにくい構成ファイルを 見る人の数も減らせます.
# deploy_api.yaml steps: - id: build_api_image name: gcr.io/cloud-builders/docker dir: api args: ["build", "-t", "gcr.io/$_PROJECT_ID/$_IMAGE_ID:$TAG_NAME", "."] - id: push_api_image name: gcr.io/cloud-builders/docker args: ["push", "gcr.io/$_PROJECT_ID/$_IMAGE_ID:$TAG_NAME"] - id: deploy_api name: gcr.io/google.com/cloudsdktool/cloud-sdk dir: release/dev entrypoint: bash args: ["deploy.sh", "deploy_api", ".env", ".env.secret", "$TAG_NAME"] images: - gcr.io/$_PROJECT_ID/$_IMAGE_ID:$TAG_NAME substitutions: _PROJECT_ID: service-stg _IMAGE_ID: service-api
最後に,バージョンタグをプッシュした際にデプロイが自動で行われるよう, cloud buildとgithubの連携とトリガーの作成を行います. 詳細は以下.
これで,リリースからstgへのデプロイまでの自動化が完了です.
prd環境へのデプロイ
prd環境へのデプロイも,stgへのデプロイと内容はほぼ一緒です. 異なる点は,リリースの際に自動デプロイされるのではなく, 開発者の任意のタイミングでデプロイを行うという部分です. そのため,cloud buildのトリガーを,tag pushではなく,manualに設定します. 以下のようなスクリプトをprd下に用意しておくことで,簡単に作業が行えます.
# deploy.sh function deploy() { gcloud beta builds triggers run --tag $2 service-prd-api-trigger gcloud beta builds triggers run --tag $2 service-prd-ui-trigger }
今回はprdのデプロイ時にコンテナのビルドとcloud runへのデプロイを行う形にしましたが, リリースの際にprdコンテナのビルドとcloud runへのトラフィック無しでのデプロイを 行い,prdデプロイの際はトラフィックを切り替えるだけ,という方式にすると,prdデプロイが 速くなります.その場合のデプロイコマンドは以下のようになります.
# deploy.sh function deploy() { gcloud beta run services update-traffic service-prd-api --to-revisions service-prd-api-$(echo $2 | sed -e s/\\./-/g)=100 --region asia-northeast1 --platform managed gcloud beta run services update-traffic service-prd-client --to-revisions service-prd-client-$(echo $2 | sed -e s/\\./-/g)=100 --region asia-northeast1 --platform managed }
以上が,構築したcloud run上でのgithub flowでのCDフローの全体になります.
その他細かい知見など
- クラウド上のリソースにプロジェクトIDを入れるようにしておくと,プロジェクト切り替えを忘れてた時なども事故らなくて済む(cloud buildのトリガーなど,開発者が叩く部分は特に).
- ローカルmacでbuildしたコンテナをcloud runにあげるとマシンの違いでアプリケーションがバグることがあるのでビルドはcloud build上で行うようにする
- cloud runのリビジョンをUIから弄りまくったりすると,たまに切り替えられなくなるバグが起きる
最後に
昨年度の最初に作ったCDフローの解説を行いました. 単純な構成ですが,初期においてはメンバー全員が設定(環境変数)の追加をしやすい,高速でデプロイサイクルを回せるなどの利点がありました. リリース生成部分や,環境変数管理の部分などは新しいやり方がもっとありそうですし,チームの規模やサービスの性質によって内容も変わると思います. 実際,ネイティブアプリのようなリリースタイミングをこちらで制御できないケースにおいては,緊急修正(hotfix)がすごく面倒でした. アプリの場合については,次回以降のGitlab flow編で書く予定です.