リリースすべき開発内容、「すべて入っています」と誓えますか?

リリースすべき開発内容、「すべて入っています」と誓えますか?

エンペイ 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回の本番リリースを行うように開発を進めています。 大雑把にリリース作業をするまでは下記の流れで動いています。

  1. 各開発者がdevelopブランチに向けて開発を行う
  2. 本番環境のリリース前日にstagingブランチにdevelopブランチの開発物を取り込む
    1. stagingブランチ向けのPRがmergeされる際に、自動でimageがbuildされます
  3. staging環境でスルーテストを行う
    1. スルーテストであらかじめ用意したテストケースを基にデグレ(意図せず生まれた不具合)がないか網羅的に確認しています
  4. スルーテストでバグがなければ本番リリース
    1. Slackから手動で行います
    2. 実態としては、Slackからリリース用のプログラムが書いてあるAWS Lambdaを動かしています

今回の事件の話として、3のスルーテストの最中に不具合が発見されて、その修正が入らずに4の本番リリースが行われました。 ブランチでの流れも交えて言い換えると、3のスルーテストで見つかった不具合だったのでstagingブランチからブランチを切って修正を行いましたが、修正が含まれたimageがbuildされる前に4の本番リリースが行われたと言えます。

※Git-flowにおいては release ブランチと呼称されますが、ややこしくなるので staging ブランチと表現しています

どう防止するか

それでは、どのようにして「本番環境に必要な修正が含まれているか」を確認すれば良いでしょうか?

まず、どういう状態なら必要な修正が含まれているといえるかを考えてみましょう。

我々は以下の2点が満たされていれば必要な修正が含まれていると言えると考えました。

  1. stagingブランチに向けたPRが存在していない
  2. staging環境のimageの最新コミットと、リモートのstagingブランチの最新コミットが同じ

staging環境に向けたPRが存在していない

まず、1の「stagingブランチに向けたPRが存在していない」についてですが、現在のenpayの開発の流れとして(Git-flow的にも)stagingブランチからブランチを切って開発を進める場合は基本的にはありません。

stagingからブランチを切ることがあるのは、今回の事例のようにリリース予定の開発物から生じたデグレをはじめとする「必ずリリースに含めないといけない修正が発生した」時です。

言い換えるなら、stagingブランチに向けたPRが残っているうちはリリースに含めないといけない開発物がまだ残っている状態と言えます。

ℹ️
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環境とリモートブランチの最新コミットが同じ = 本番環境に必要な開発物が入った 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のタグ付けでこちらのコードから出力されるコミットハッシュを入れているので、それと同じ値をパラメーターストアに記録するためです。

さいごに

この記事をご覧になっている皆様のリリースの仕方はそれぞれ異なるものだと思いますが、参考になりそうな箇所はあったでしょうか?

ヒューマンエラーは必ず起き、また、それが本番環境で起きたというなら肝が冷えるものです。

この記事で参考になりそうなものが見つかり、見ていただいた皆様が少しでも心安らかにリリース作業をできるようになると幸いです 🙏