羊をめぐるブログ

趣味の色々について書きます

Cloud Run におけるGitHub flowでのCDフロー構築

はじめに

これは2021年振り返りカレンダー及びついに卒業かぁカレンダーの1日目の記事です.

昨年はスタートアップにて新規サービス開発におけるCDフローの構築を複数回行ったので,そこから得られた知見をまず書いていこうと思います.前提として,チームは数人,トラフィックもそんなに大きくないという想定での設計です.

まずはCloud RunでのGitHub flow の構築についてです. 最近見る新規サービスはこれに乗ってるのをよく見る気がします.

engineering.mercari.com

(今回作るのはこんな強そうなのではないですが...)

お金になるかわからない初期サービスにおいてCloud Runを用いるメリットは以下があると思っています.

  • リクエストがない時は一定時間でインスタンスが落ち,課金もされない
  • オートスケール
  • revision機能があり,新規バージョンでバグが出た時はすぐに前のバージョンに戻せる(DBの依存なければ).

また,GitHub flowの解説については,大まかに要点をまとめると,

  • mainブランチは常にデプロイ可能
  • 新規の変更はmainからブランチを切りPRを出しmainにマージ.

みたいな感じです.

仕様

今回は,フロントエンド(SSR),バックエンドの両方をCloud Run上に乗せている形を想定します. また,環境は一旦stg環境とprd環境の2つとします(何個でも良い).

そして,仕様は次のような形です.

  1. mainブランチがプロダクトのデプロイ可能な最新コードになっているよう管理.
  2. 新機能は feature ブランチ,fix ブランチなどとして,mainから切り,レビュー後に main にマージ.
  3. きりの良いタイミングで,stg環境へのデプロイを行う.バージョンタグとともにコマンドを以下の形で打つと,リリースノートの生成と,stg環境へのデプロイがされる. sh release.sh release v0.0.0

  4. 動作確認ができたタイミングで,migarationなどを行ったのち,prd環境をデプロイ.

  5. prdでバグが起きたら,修正 or 前のバージョンにロールバック

実装

ディレクトリ構成は以下のような感じです(モノレポ). CDフローに必要なファイルなどは,release下に,環境ごとに用意します.

root/
├── .github
├── api
├── ui
├── release
│  ├── dev
│  └── prd
└── release.sh

リリース

リリースでは以下を行います.

  • mainの最新にバージョンタグを振る(githubのtag機能を用いる)
  • 前回のリリースからの差分をまとめたリリースノートを生成
  • stg環境へのデプロイ

リリースタグを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された時に,リリースノート生成が行われます.

リリースノートの生成は,もっと新しい方法が色々ありそうです.

www.publickey1.jp

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の連携とトリガーの作成を行います. 詳細は以下.

cloud.google.com

これで,リリースから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編で書く予定です.