- エンペイ Advent Calender 2023 Day 5
- 背景
- リリースフローについて
- ブランチ戦略とリリース
- どう防止するか
- staging環境に向けたPRが存在していない
- staging環境のimageの最新コミットと、リモートのstagingブランチの最新コミットが同じ
- CIの実行コード
- PRの存在確認と最新コミットの記録での共通事項
- stagingブランチに向けのPRの存在確認
- 最新コミットの記録
- さいごに
エンペイ Advent Calender 2023 Day 5
タイトルの通り、この記事はエンペイ Advent Calenderの5日目の記事です。 担当は最近バックエンドからインフラに染み出し始めた堀(@ngine_engineer)がお送りします! 👨
背景
タイトルにある通り、本番環境のリリース作業のミスを無くすための機構を入れたわけですが、これは実際に下記のような事件が起きたので導入の運びとなりました...
A衛門: 本番環境のリリース前のスルーテストを行っている最中にバグが見つかりました!! 修正おなしゃす!!
B之丞: うっすうっす!直ちに修正します!!
うぉっしゃーー、修正おわたぁぁぁ!
本番環境リリースしまぁぁぁぁす!! デプロイ!!ターン!
A衛門: リリースありが...、あれエラーまだ起きてるぞ? ( ^ω^)おっ
B之丞: これ、"修正したimageが作られる前に"デプロイしちゃってますね ( ^ω^)あっ
弊社では、本番環境で使用するDockerのimageはstaging環境でテストをした、動作が保証されているimageを使い回すようにしています。
そのため、いくら修正が終わっていようがstaging環境用のimageが修正が入ったバージョンで構築されないと本番環境には入りません。
今回の事件は修正が取り込まれた直後、staging環境のimageが構築される前に爆速リリースを行なったために発生しました...
この記事では、「仕組み」でstaging環境の修正が反映される前にリリースされるのを防ぐ、つまり、リリースに必要な開発物が全て含まれているかを確認する機構を導入した旨について書かせていただきます 💁♂️
リリースフローについて
一口に「リリース」といっても、そのフローはプロダクト単位で千差万別になっているものかと思います。 リリースフローが違えばどんな仕組みを入れるかも変わってくると思うので、まずは今回のきっかけとなったプロダクト enpay のリリースフロー(+軽くブランチ戦略)についての説明をさせていただきます!
ブランチ戦略とリリース
まず、enpayの開発チームではGit-flowに従ったブランチ戦略の基、週1回の本番リリースを行うように開発を進めています。 大雑把にリリース作業をするまでは下記の流れで動いています。
- 各開発者がdevelopブランチに向けて開発を行う
- 本番環境のリリース前日にstagingブランチにdevelopブランチの開発物を取り込む
- stagingブランチ向けのPRがmergeされる際に、自動でimageがbuildされます
- staging環境でスルーテストを行う
- スルーテストであらかじめ用意したテストケースを基にデグレ(意図せず生まれた不具合)がないか網羅的に確認しています
- スルーテストでバグがなければ本番リリース
- Slackから手動で行います
- 実態としては、Slackからリリース用のプログラムが書いてあるAWS Lambdaを動かしています
今回の事件の話として、3のスルーテストの最中に不具合が発見されて、その修正が入らずに4の本番リリースが行われました。 ブランチでの流れも交えて言い換えると、3のスルーテストで見つかった不具合だったのでstagingブランチからブランチを切って修正を行いましたが、修正が含まれたimageがbuildされる前に4の本番リリースが行われたと言えます。
※Git-flowにおいては release
ブランチと呼称されますが、ややこしくなるので staging
ブランチと表現しています
どう防止するか
それでは、どのようにして「本番環境に必要な修正が含まれているか」を確認すれば良いでしょうか?
まず、どういう状態なら必要な修正が含まれているといえるかを考えてみましょう。
我々は以下の2点が満たされていれば必要な修正が含まれていると言えると考えました。
- stagingブランチに向けたPRが存在していない
- staging環境のimageの最新コミットと、リモートのstagingブランチの最新コミットが同じ
staging環境に向けたPRが存在していない
まず、1の「stagingブランチに向けたPRが存在していない」についてですが、現在のenpayの開発の流れとして(Git-flow的にも)stagingブランチからブランチを切って開発を進める場合は基本的にはありません。
stagingからブランチを切ることがあるのは、今回の事例のようにリリース予定の開発物から生じたデグレをはじめとする「必ずリリースに含めないといけない修正が発生した」時です。
言い換えるなら、stagingブランチに向けたPRが残っているうちはリリースに含めないといけない開発物がまだ残っている状態と言えます。
staging環境のimageの最新コミットと、リモートのstagingブランチの最新コミットが同じ
続いて2ですが、こちらは背景で紹介したstaging環境のimageの構築前にリリースをした際にどのような状態になるかを想像していただけると良いかと思います。
修正ブランチ
↓
↓ merge
↓
リモートブランチ[456cdf789]
↓
↓ 自動image更新
↓
staging環境[123abc456] → docker build → staging環境[456cdf789]
このようにdocker build
で image が構築される前にリリースをすると、リモートの staging ブランチの最新コミットが456cdf789
であるにも関わらず、本番環境で使い回されるテスト済みのstaging環境の image のコミットは123abc456
で不一致が生じてしまいます。
であるなら、リモートのstagingブランチとstaging環境のimageの最新コミットが同じでさえすれば最新のimageが使われていると言えそうです。
まとめると、
- ① stagingブランチに向けたPRが無いなら、必要な開発物は全て取り込まれており
- ② staging環境のimageとリモートのstagingブランチの最新のコミットが同じなら、開発物が入ったimageが作られていると言える
- つまり、現在のstaging環境のimageには全ての必要な修正が含まれている状態である と考えました。
続いては、実際のコード部分を見ていきます。
CIの実行コード
stagingブランチのPRに動きがあった時を見張っておけば良いので、GitHub Actionsを使って実装することとなりました。
コードの解説に伴い追加で共有させて頂くと、enpayではimageが構築された時に最新のコミットハッシュでタグを付けており、また、各環境が現在どのimageタグを使用しているかはAWS SSMのパラメーターストアに値を持っています。
これら既存環境で参照する値がパラメーターストアで作られていること、本記事で書いた修正が含まれているかの判定はリリースのプログラムの中(AWS Lambda)で行いたいことから、判定結果の値もパラメーターストアに保存するようにしました。
開発物が取り込めているかのチェックの部分は、パラメーターストアとの文字列一致判定しかしていないので、以下ではパラメーターストアにどうやって保存しているかのコードを書いています。
まずは全体のコードをご覧ください。
on:
pull_request:
types: [closed, opened]
branches:
- staging
- hotfix
jobs:
check-exist-open-pr:
runs-on: ubuntu-latest
permissions:
# 一部省略
pull-requests: write # gh pr コマンドで必要
steps:
# 一部省略
- name: Check if there is an open pull request for the release branch
shell: bash -x {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ghコマンドを使用するのに必要
# GitHub CLIはターミナルで見える情報と、実際のデータが異なるので一度変数に入れる
# grepはマッチしなかった場合に終了ステータスが1になるので、それを利用して条件分岐
run: |
ENV=${{ github.base_ref }}
OPEN_PR=$(gh pr list -B $ENV -s open)
echo $OPEN_PR | grep -E -o '^\\d+\\s+'
if [ $? -eq 0 ]; then
echo "$ENV has open pull request"
aws ssm put-parameter --name "/$ENV/has_open_pr" --value "true" --type "String" --overwrite
else
echo "Ready for production deployment"
aws ssm put-parameter --name "/$ENV/has_open_pr" --value "false" --type "String" --overwrite
fi
store-current-commit:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Store current commit hash
run: |
ENV=${{ github.base_ref }}
COMMIT_HASH=$(git rev-parse --short=9 HEAD)
aws ssm put-parameter --name "/enpay/$ENV/current_commit" --value "$COMMIT_HASH" --type "String" --overwrite
そこまで複雑なscriptでは無いのでごく一部のみ解説します 💪
PRの存在確認と最新コミットの記録での共通事項
pull_request:
types: [closed, opened]
branches:
- staging
- hotfix
起動のタイミングはstagingブランチとhotfixブランチをbaseブランチにしたPRが作られた時と、closeされた時にしています。
openとclose両方のタイミングなのは、最新のコミット値のパラメーターストアの更新はcloseだけで良いですが、openなPRの存在確認のパラメーターストアの更新は修正でPRが増えるたびに行いたいためです。
※hotfixブランチに関しては、stagingと同様にテストされたimageを使い回すことから対象にしています。
stagingブランチに向けのPRの存在確認
OPEN_PR=$(gh pr list -B $ENV -s open)
echo $OPEN_PR | grep -E -o '^\\d+\\s+'
上記の全体のコードでもコメントアウトしていますが、下記の通りgh pr
コマンドの出力が画面に表示されるものと、実際のデータで構造が異なるので一度変数化しています。
# gh pr 実行時に画面に表示されるもの
ID TITLE BRANCH CREATED AT
#1234 バックエンドの修正 fix-backend about 2 days ago
#1235 フロントエンドの修正 fix-frontend about 2 days ago
# 変数化した時のデータ(echo $OPEN_PR)
1234 バックエンドの修正 fix-backend OPEN 2023-12-01T04:17:51Z
1235 フロントエンドの修正 fix-frontend OPEN 2023-12-01T04:17:51Z
PRが存在していなければ grep -E -o '^\\d+\\s+'
でエラーが発生するので、それを基に条件分岐をしてパラメーターストアに値を入れています。
最新コミットの記録
store-current-commit:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
steps:
- name: Store current commit hash
run: |
COMMIT_HASH=$(git rev-parse --short=9 HEAD)
aws ssm put-parameter --name "/enpay/$ENV/current_commit" --value "$COMMIT_HASH" --type "String" --overwrite
最新コミットの記録に関しては、PRがcloseした時だけで良いので if
で起動条件を絞り込んでいます。
git rev-parse --short=9 HEAD
は、imageのタグ付けでこちらのコードから出力されるコミットハッシュを入れているので、それと同じ値をパラメーターストアに記録するためです。
さいごに
この記事をご覧になっている皆様のリリースの仕方はそれぞれ異なるものだと思いますが、参考になりそうな箇所はあったでしょうか?
ヒューマンエラーは必ず起き、また、それが本番環境で起きたというなら肝が冷えるものです。
この記事で参考になりそうなものが見つかり、見ていただいた皆様が少しでも心安らかにリリース作業をできるようになると幸いです 🙏