障害のとき本番に何が出ていたのか誰も知らなかった
目標
固定環境とブランチごとのレビュー環境、環境を閉じるジョブ、手動の本番デプロイを設定し、コミットSHAで残るデプロイ記録と、その記録から選ぶロールバック、デプロイ凍結のルールを、gitlab-ci-localで実際に動かします。
なぜ重要なのか
デプロイは、元に戻せて初めて安全です。何がどこにいつ出たかがenvironmentで残り、出たものがコミットSHAで識別できてはじめて、事故のとき「そのときのもの」に数分で戻れます。レビュー環境は便利ですが、閉じる相方がないと積み上がって、コストと露出になり、本番デプロイの承認は、allow_failureの値1つでゲートにも飾りにもなります。凍結期間のように「今は出さない」という約束も、設定に刻んでこそ守られます。
ステップ
/root/glci-deployをgitリポジトリにし(.gitignoreに.gitlab-ci-local/を置く)、.gitlab-ci.ymlにstages[deploy, cleanup]、グローバル変数DEPLOY_LOG: /root/glci-deploy-history/deploys.log、そしてmainでだけ作られるジョブdeploy-staging(deploy)を置いてください。environmentは名前staging、URLhttps://staging.example.comで、スクリプトはecho "env=$CI_ENVIRONMENT_NAME url=$CI_ENVIRONMENT_URL"です。mainにコミットして実行してください。- main以外のブランチでだけ作られるジョブ
review-app(deploy)を追加してください。environmentの名前はreview/$CI_COMMIT_REF_SLUG、URLはhttps://$CI_COMMIT_REF_SLUG.review.example.comで、スクリプトはステップ1と同じechoです。コミットしてください。採点ツールは、コピーでFeature/Login-Pageブランチに移って、名前がreview/feature-login-page、URLがhttps://feature-login-page.review.example.comかを確認します。 - review-appのenvironmentに
on_stop: stop-reviewとauto_stop_in: 1 dayを追加し、ジョブstop-review(cleanup)を作ってください。同じ環境名でaction: stop、main以外のブランチでwhen: manual、スクリプトはecho "stopping $CI_ENVIRONMENT_NAME"です。コミットしてください。featureブランチで、stop-reviewが一覧にmanualとしてあり、--manual stop-reviewで動かすとstopping review/<슬러그>が出力されるはずです(プレースホルダーはスラッグです)。 - mainで
when: manual、allow_failure: falseとして作られるジョブdeploy-prod(deploy)を追加してください。environmentはproduction、URLはhttps://www.example.com、スクリプトはecho "env=$CI_ENVIRONMENT_NAME"です。コミットしてください。そのまま実行するとdeploy-prodは動かず、--manual deploy-prodを与えると動くはずです。 /root/glci-deploy/scripts/deploy.sh <환경>を作ってください。ROLLBACK_TOが空なら、タグを$CI_COMMIT_SHORT_SHAにして、$DEPLOY_LOGに<환경> registry.example.com/app:<태그> releaseを1行追記し、deployed ...を出力します(プレースホルダーは、順に環境とタグです)。deploy-staging・deploy-prodのスクリプトの最後にbash scripts/deploy.sh staging・bash scripts/deploy.sh productionを追加して、コミットしてください。採点ツールは、コピーで2つのコミットを順にデプロイして、記録がコミットごとに異なるタグで残るかを確認します。- deploy.shが
ROLLBACK_TOを受け取ったら、新しいタグを作らず、そのタグを出し直すようにしてください。その環境の履歴にregistry.example.com/app:<ROLLBACK_TO>として出た記録がなければ、이력에 없는 태그입니다を出力して失敗します(韓国語の文は「履歴にないタグです」という意味です)。記録の行の最後の語はrollbackです。採点ツールは、コピーで2回デプロイしたあと、最初のタグに戻す実行と、存在しないタグに戻そうとする実行を行います。 - deploy-prodのrulesの一番前に、
if: $CI_DEPLOY_FREEZEのときwhen: neverを置いてください。コミットしてください。--variable CI_DEPLOY_FREEZE=1で一覧を見るとdeploy-prodがなく、変数がなければmanualとしてあるはずです。deploy-stagingは、凍結と関係なくあるはずです。
参考
- このVMにはGitLabサーバーもランナーもなく、gitlab-ci-local 4.75.1が.gitlab-ci.ymlをGitLabと同じルールで解釈し、シェルでジョブを実行します。
image:を書くとDockerで動かそうとするので使いません。保護変数・マスキング・CI_JOB_TOKEN・ランナーのタグ・マージリクエストパイプラインの作成はサーバーの機能なので、ここでは再現されません。 - 実行はリポジトリのルートで
gitlab-ci-local --shell-isolation --no-artifacts-to-source(ジョブごとに別の作業ディレクトリを使い、アーティファクトをリポジトリに書き戻さない)、ジョブ一覧はgitlab-ci-local --list-csv-all、解釈された設定はgitlab-ci-local --previewです。gitlab-ci-localはgitが追跡しているファイルだけをジョブに渡すので、ファイルを作ったらgit addしてください。採点ツールはリポジトリをコピーし、すべてのファイルをコミットしたあとで、同じツールでもう一度実行します。 - gitlab-ci-localの違い(実測): resource_groupで同じ環境のデプロイを順番に並べません。manualジョブは--manualで指定しないと動かず、後ろのステージを止めません。環境画面・デプロイ履歴・保護環境の承認・凍結期間の設定はサーバーの機能です。
- デプロイ記録
/root/glci-deploy-history/deploys.logは、リポジトリの外にあります。採点ツールは、コピーを動かすとき--variable DEPLOY_LOG=<임시 경로>で別に書きます(プレースホルダーは一時的なパスです)。 - Environments・Predefined CI/CD variables(CI_COMMIT_REF_SLUG・CI_DEPLOY_FREEZE)・Resource groups・CI/CD YAML syntax reference
デプロイジョブにラベルを付ける
/root/glci-deployをgitリポジトリにし(.gitignoreに.gitlab-ci-local/を置く)、.gitlab-ci.ymlにstages[deploy, cleanup]、グローバル変数DEPLOY_LOG: /root/glci-deploy-history/deploys.log、そしてmainでだけ作られるジョブdeploy-staging(deploy)を置いてください。environmentは名前staging、URLhttps://staging.example.comで、スクリプトはecho "env=$CI_ENVIRONMENT_NAME url=$CI_ENVIRONMENT_URL"です。mainにコミットして実行してください。
environmentを付けたジョブは、GitLabでデプロイとして記録され、環境画面に、何がいつ出たかが残ります。ジョブの中では、CI_ENVIRONMENT_NAMEとCI_ENVIRONMENT_URLで、自分がどこに向かうのかがわかります。
ブランチごとにできるレビュー環境
main以外のブランチでだけ作られるジョブreview-app(deploy)を追加してください。environmentの名前はreview/$CI_COMMIT_REF_SLUG、URLはhttps://$CI_COMMIT_REF_SLUG.review.example.comで、スクリプトはステップ1と同じechoです。コミットしてください。採点ツールは、コピーでFeature/Login-Pageブランチに移って、名前がreview/feature-login-page、URLがhttps://feature-login-page.review.example.comかを確認します。
ブランチ名には、大文字やスラッシュのように、ホスト名に使えない文字が混ざります。CI_COMMIT_REF_SLUGは、小文字に変えて、許可されない文字をハイフンに変えた値なので、URLや環境名にそのまま使えます。
レビュー環境は閉じるジョブと対になる
review-appのenvironmentにon_stop: stop-reviewとauto_stop_in: 1 dayを追加し、ジョブstop-review(cleanup)を作ってください。同じ環境名でaction: stop、main以外のブランチでwhen: manual、スクリプトはecho "stopping $CI_ENVIRONMENT_NAME"です。コミットしてください。featureブランチで、stop-reviewが一覧にmanualとしてあり、--manual stop-reviewで動かすとstopping review/<슬러그>が出力されるはずです(プレースホルダーはスラッグです)。
レビュー環境は、ブランチの数だけ積み上がります。on_stopは「この環境を閉じるジョブはあれ」という結び付けで、ブランチを削除したりマージしたりすると、GitLabがそのジョブを呼び出します。auto_stop_inは、忘れられた環境を、時間が経つと閉じさせます。
本番デプロイは人が押す
mainでwhen: manual、allow_failure: falseとして作られるジョブdeploy-prod(deploy)を追加してください。environmentはproduction、URLはhttps://www.example.com、スクリプトはecho "env=$CI_ENVIRONMENT_NAME"です。コミットしてください。そのまま実行するとdeploy-prodは動かず、--manual deploy-prodを与えると動くはずです。
allow_failure: falseの手動ジョブは、GitLabでは押すまでパイプラインを「ブロック」状態にします。trueだと押さなくてもパイプラインが成功で終わるので、承認ゲートになれません。gitlab-ci-localはこのブロックを再現しないので、一覧のallowFailureの列で確認します。
何が出たかをコミットSHAで残す
/root/glci-deploy/scripts/deploy.sh <환경>を作ってください。ROLLBACK_TOが空なら、タグを$CI_COMMIT_SHORT_SHAにして、$DEPLOY_LOGに<환경> registry.example.com/app:<태그> releaseを1行追記し、deployed ...を出力します(プレースホルダーは、順に環境とタグです)。deploy-staging・deploy-prodのスクリプトの最後にbash scripts/deploy.sh staging・bash scripts/deploy.sh productionを追加して、コミットしてください。採点ツールは、コピーで2つのコミットを順にデプロイして、記録がコミットごとに異なるタグで残るかを確認します。
latestのように動くタグでデプロイすると、事故のとき「そのとき出たもの」を誰も知りません。コミットSHAは、ソース・ビルド・デプロイを1行でつなぐ識別子です。デプロイ記録のパスは変数で受け取り、実行時に変えられるようにしておきます。
ロールバックは履歴にあるタグを出し直すこと
deploy.shがROLLBACK_TOを受け取ったら、新しいタグを作らず、そのタグを出し直すようにしてください。その環境の履歴にregistry.example.com/app:<ROLLBACK_TO>として出た記録がなければ、이력에 없는 태그입니다を出力して失敗します(韓国語の文は「履歴にないタグです」という意味です)。記録の行の最後の語はrollbackです。採点ツールは、コピーで2回デプロイしたあと、最初のタグに戻す実行と、存在しないタグに戻そうとする実行を行います。
ロールバックは、「古いソースでビルドし直すこと」ではなく、「すでに検証されて出たアーティファクトを出し直すこと」です。履歴にないタグを止めておけば、1文字の打ち間違いで、検証されていないイメージが本番に出ることがありません。実行するとき--variable ROLLBACK_TO=<태그>で渡します(プレースホルダーはタグです)。
デプロイ凍結期間には本番デプロイを作らない
deploy-prodのrulesの一番前に、if: $CI_DEPLOY_FREEZEのときwhen: neverを置いてください。コミットしてください。--variable CI_DEPLOY_FREEZE=1で一覧を見るとdeploy-prodがなく、変数がなければmanualとしてあるはずです。deploy-stagingは、凍結と関係なくあるはずです。
GitLabは、プロジェクトに凍結期間を設定しておくと、その時間の間CI_DEPLOY_FREEZEを埋めてくれます。ルールは最初のマッチだけを使うので、凍結のルールがmainのルールより前にある必要があります。凍結を人の記憶に任せず、パイプラインが拒否するようにする仕組みです。