Buildへ切り替えました。結論から言うと、切り替えにあたってパイプラインの中身とプロセスを今一度見直したところ、以下のように改善しました。Buildの設定、注意点について書きます。開発環境用のプロジェクト本番環境用のプロジェクト注意点cloudbuild.yamlの多段構成は避ける自前のタグをつけたリビジョンはこの世に生きているおわりにタフな仕事始めきっかけは2023年はじめのCircleCIのセキュリティインシデントでした。詳細は省略しますが、不正な第三者がCircleCIに保存された環境変数やトークン、キーなどのシークレット情報へアクセス可能な状態だったようで、CircleCIから提示された対応策はシークレット情報をすぐにローテーションしてくださいというものでした。コミューンはビルド・デプロイの仕組み上CircleCIへシークレットを保存していました。Cloudに対する操作が可能な権限を付与したサービスアカウントのキーなどです。応急処置としてまずCircleCIからシークレットを削除、プロダクトのリポジトリとCircleCIのプロジェクトを切り離しました。不正アクセスがなかったかログなどから確認しました。(幸い不正アクセスはありませんでした)その場限りの対応ではなくあるべきを考えるCircleCIからリポジトリを切り離したのでリリースを停止せざるを得ませんでした。応急処置が済んだことや不正アクセスがないことを確認できたので、わたしは再発行したシークレットをCircleCIへ登録して使用を再開しようと思ってました。Buildへ乗り換えることをインシデント対応の真っ只中に決めました。その場限りの対応にせずあるべきを考えること、意思決定の速さがコミューンらしくて良いです。あるべきを設計する移行を決めたとはいえリリースを止めるわけにはいかないので、手動でもリリースできるようまずは簡単なシェルスクリプトを作ってから移行を進めました。Buildへ移行するだけならやる意義がないので、CI/CD周りであがっていた課題をこのタイミングで解消することにしました。課題の1つにパイプラインの効率の悪さがありました。従来は、developブランチへマージすると開発環境へビルド・デプロイし、本番環境へリリースする時はdevelopブランチをmasterブランチへマージしてビルド・デプロイしていました。developとmasterのコードは基本同じなので、同じコードでビルド・デプロイするというステップが2回ありました。installをしている箇所もありました。このようにプロセスを見直した結果、最終的なパイプラインは以下のようになりました。パイプラインのステップ大きく2つのステップに分けました。Runのリビジョンへエンドユーザーからのトラフィックをルーティングする(=リリース)これにより、ビルド回数を2回から1回へ減らすことができ、本番環境のリリースはビルドとデプロイが不要でトラフィックを切り替えるだけで良くなりました。Buildで実装します。Buildの設定コミューンのアーキテクチャを見ていただくとよりご理解いただけると思いますので構成図を貼ります。構成を詳しく知りたい方はこちらの記事がおすすめです。Buildの構成ファイル(yaml)をビルド、デプロイ、リリースの各ステップ分作成しました。ファイル構成は正確には少し異なるのですが以下のイメージです。Dockerfile保守のしやすさを考え、ビルド、デプロイなど各ステップのyamlとフロントエンドなど各サービスのyamlを分けて作りました。cloudbuild/build.cloudbuild.yamlへフロントエンド、バックエンド、公開APIサーバのビルド方法を定義します。以下はバックエンドの定義箇所の抜粋です。'N1_HIGHCPU_32'--configオプションでバックエンド用のyamlを指定しています。バックエンドのビルド手順をcloudbuild/backend/cloudbuild.yamlで定義します。'N1_HIGHCPU_32'ビルド時間を短くするようkanikoキャッシュを使い、machineTypeでハイスペックなマシンを使っています。Storageへ保存するようにしています。--substitutionsを使ってビルド時に変数を置き換えることができます。Registryへ保存するDockerイメージへタグをつけるため_IMAGE_TAGへ${SHORT_SHA}(ショートコミットハッシュの値)を渡しています。変数は自前で定義することも、定義せずともデフォルトで利用できるものもあります。上のyamlでいうと_IMAGE_TAGは自前で定義、${SHORT_SHA}や${PROJECT_ID}はデフォルトで利用できる変数です。この辺りはこちらのドキュメントを見ると良いです。デフォルトで利用できる変数だけで足りなければ自前で定義すると良いでしょう。この他にデプロイ用とリリース用のyamlをそれぞれ作成しました。update-trafficと使っているコマンドやオプションが違うくらいです。Build用のサービスアカウントへ以下の権限を付与しました。Storageを参照するコミューンは環境ごとにGCPのプロジェクトを分けているので、プロジェクトをまたいだ権限設定が必要です。ここは根気良くトライアンドエラーを繰り返してミニマムな権限を設定しました。次はトリガーを使って先ほどの2ステップを実装しました。BuildのPub/Subトリガーを使いました。トリガーが成功するとPub/Subのトピックへメッセージをパブリッシュします。別のトリガーはメッセージの内容次第でトリガーを発動するか定義できます。フィールドで確認できます。とあるためこれを使ってトリガーの発動条件を設定します。ビルドのステータスはbody.message.attributes.statusなので、以下のようにこれを_BUILD_STATUSという変数へ代入して使いました。フィルターを使って前のトリガーのIDかつ結果が成功であれば発動するという条件を設定しました。_BUILD_TRIGGER_IDの正規表現値には例えばデプロイ用のトリガーであればその前のステップのビルドのトリガーIDを入力します。リリースのステップを実装しました。URLへリクエストして発動するようにしています。Cloudのプロジェクトごとに以下のトリガーを作りました。URLへリクエストが飛ぶとコンテナへトラフィックをルーティングする新CI/CDパイプラインが完成しました。二人のSREチームのメンバーがおもに手を動かし、他のタスクもやりつつリリースまで約1ヶ月かかりました。Actionsでさくっと実装してくれたりと開発チームのメンバーにも感謝です。Buildを使う際の注意点、ハマったポイントを書きます。Engine用のyamlという3段構成にしていたのですが、トリガーの終了時間がかなりかかったため2階層にすることで時間を短縮しました。多段にすればするほどタスクを実行するマシンの起動回数が増えるので階層は少なくした方がよいです。Runのリビジョンがどのリリースを含むのか判別しやすくすることを目的として、リリース当初はリビジョンへショートコミットハッシュ(コミットハッシュの頭7桁)を含むタグをつけていました。Runのコストが急増しているという報告があり調べたところ、CPUを常に割り当てている自前のタグをつけたリビジョンはトラフィックがルーティングされていなくても課金されることがわかりました。タグの利用をやめてリビジョン名へショートコミットハッシュをつけることでこの問題を回避しました。CPUを常に割り当てるコンテナで以下の画像のようにルーティングしていないリビジョンに緑のチェックアイコンがついていると課金対象になるので注意してください。画像はドキュメントから引用おわりにいかがでしたでしょうか。Buildを設定する際の参考になれば幸いです。今回の移行に限ったことではないですが、プロセスの中に無駄を見つけ改善すると開発チームの生産性を向上することができ、より多くの価値をユーザーへ提供することができます。その結果がビジネス上の成果に繋がるので、何かトラブルがあっても暫定対応で終わらせることなく本質を考えて仕事をしていきたいです。