GitHub Flow with GitOpsの導入

ogp

はじめに

こんにちは、計測プラットフォーム開発本部SREブロックの近藤です。普段はZOZOMATやZOZOGLASS、ZOZOFITなどの計測技術に関わるシステムの開発、運用に携わっています。

計測プラットフォーム開発本部では、複数のプロダクトを開発運用していますが、リリース作業はプロダクト単位で行っています。プロダクトによってローンチから数年経過し安定傾向のものもあれば、ローンチしたばかりで機能開発が盛んなものもある状態です。

複数のプロダクトを管理する上では当然の状況ですが、プロダクト単位でリリース作業手順が異なり、手順そのものにも課題がある状態でした。

本記事では、リリース作業で課題となっていた部分の紹介と、それぞれの課題に対する対応策についてご紹介します。

目次

現状

これまであった課題をお話しする前に、現状のプロダクト毎のデプロイ方法、ブランチ戦略、リリース頻度をご紹介します。

プロダクト名 デプロイ方法 ブランチ戦略 リリース頻度
ZOZOMAT(2020/02ローンチ) ArgoCD 1Git Flow 隔週に一度の定期リリース
ZOZOGLASS(2021/03ローンチ) ArgoCD Git Flow 隔週に一度の定期リリース
ZOZOFIT(2022/08ローンチ) ArgoCD 2GitHub Flow mainが更新されたらリリース

上記の3つのプロダクトで、デプロイ方法は統一されていますが、ブランチ戦略が異なるため、リリース手順が微妙に異なる状態でした。

ブランチ戦略は昨年から新しいプロダクトはGitHub Flow、それ以前はGit Flowを採用していました。

Git Flowを採用していた背景として、昔は動作確認を手動で行っており、都度のPR単位でリリースすると動作確認の工数がとても高くなる状況でした。このため、リリースを定期作業とすることで作業工数を抑えていた経緯があります。

昨年からデプロイパイプラインのリアーキテクトやArgo Rolloutsの導入なども進み、現状は動作確認やロールバックが自動化されている状態でした。このため、新しいプロダクトではGitHub Flowを採用しています。リリース手順に関しては、GitHubのIssue Templateで管理しており、リリースのタイミングで担当者がIssueを起票していました。

Git FlowとGitHub Flowのブランチ戦略で大きな違いは、GitHub Flowはmainが常にリリース可能な状態であることです。

Git Flow GitHub Flow
git_flow github_flow

弊チームで採用していたGit Flowですが、アプリケーションリポジトリではいくつかオリジナルのものに変更を加えてあります。特徴はmainブランチとreleaseブランチという2つのプライマリブランチを保持している点です。developブランチはありません。通常の開発は、releaseブランチからfeatureブランチを作成して行います。リリース作業時はreleaseブランチをmainブランチにmergeし、mainブランチをリリースします。

課題と対応方針

このような状況の中で、課題は大きく3つありました。なるべくシンプルに、なるべく楽にしたい、という理想ベースでそれぞれの課題への対応方針を定めました。

No. 課題 方針
1 手動によるリリースのため、人の工数が取られる リリース作業の自動化
2 複数の変更が一度にリリースされるため、パフォーマンスの変更要因の特定が困難 リリース粒度のミニマム化
3 リリース手順やブランチ戦略がプロダクト毎に異なり、認知負荷が高い リリース手順、ブランチ戦略の統一

リリース作業の自動化

リリース作業の自動化については、導入する上での必須条件を整理し、対応が必要な箇所の洗い出しを行いました。

リリース作業の自動化をする上での必須条件の確認

条件 対応済み
PRのマージが自動化されていること
動作確認が自動化されていること
問題が発生した場合、自動でロールバックが行われること

補足すると、動作確認はスクリプトで自動化されており、ロールバックはArgo Rolloutsによって自動化されていました。Argo Rolloutsに関しては、カナリアリリースについてのブログ記事が公開されているので、気になる方はこちらの記事をご参照ください。

techblog.zozo.com

自動化が必要な箇所の洗い出し

リリース作業の流れを簡略化して図にすると以下のようになります。自動化前の状態で人が行っていた作業は、図のオレンジ色の部分になります。

自動化前 自動化後
manually_deploy_step automation_deploy_step

人が行っていた作業は、PRのマージと、Slackへのアナウンス、負荷試験の結果確認のみでした。なお、ここでいうPRのマージは、releaseブランチをmainブランチにマージするという作業です。個別の修正はreleaseブランチにマージする時点でレビューを受けており、実質CIが通過していることを確認だけしていた状態です。

自動化対応

自動化が必要な作業の洗い出しが終わったので、次は各作業の自動化を行いました。

リリースアナウンス

Argo Rolloutsでは、ロールアウトの開始終了を通知できます。各Subscribe可能なイベントについては公式サイトを参照ください。この仕組みを利用することで、Kubernetesマニフェストを数行変更するだけで、通知の自動化ができました。

もともと開発チーム向けに通知はしていたのですが、ロールアウトの開始と終了をこれまでリリースアナウンスを行っていたSlackチャンネルにも通知するようにしました。通知は複数のチャンネルに飛ばすことができ、その場合は ; で繋ぎます。

# ZOZOGLASSのAPIサーバー用のRolloutの設定
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api-server-rollout
  annotations:
    notifications.argoproj.io/subscribe.on-rollout-aborted.slack: rollout_notification
    notifications.argoproj.io/subscribe.on-rollout-completed.slack: rollout_notification;zozoglass_release
    notifications.argoproj.io/subscribe.on-rollout-step-completed.slack: rollout_notification
    notifications.argoproj.io/subscribe.on-rollout-updated.slack: zozoglass_release
    notifications.argoproj.io/subscribe.on-analysis-run-failed.slack: rollout_notification

負荷試験の結果確認

リリース時にパフォーマンス上問題がないか確認するため、負荷試験を実行し、Gatlingが生成したレポートを人が目視で確認していました。

Gatlingでは、負荷試験の結果を評価するAssertionsが提供されています。Assertionsについての詳しい設定方法については公式サイトを参照ください。Assertionsが提供するresponseTime.percentile(99)を利用することで、レスポンスタイムの99パーセンタイルの期待値を設定できます。同様にfailedRequests.percentを利用することで、エラー率の期待値を設定できます。期待値を満たさなかった場合、Gatlingのテストは失敗となります。

導入は既に目標値が定まっていたので、既存のGatlingのコードを数行変更することで、結果確認の自動化ができました。

setUp(
  scenarioMeasure
    .inject(rampUsers(LoadTestMeasureUsers.toInt).during(LoadTestSeconds.toInt)),
  scenarioBrowseCosmeticsRecommendations
    .inject(constantUsersPerSec(GetFaceColorRps.toInt).during(LoadTestSeconds.toInt).randomized)
).protocols(httpProtocol) // Then
  // 今回追加したAssertions部分
  .assertions(
    forAll.responseTime.percentile(99).lt(ResponceTimeThreshold.toInt),
    forAll.failedRequests.percent.lte(FailedRateThreshold.toDouble)
  )

CIが通過した場合、PRのマージ

PRの自動マージに関しては、自動でマージすること自体は簡単でしたが、いくつか例外を考慮する必要がありました。今回PRの自動マージを行いたいリポジトリは、アプリケーションのコードを管理するリポジトリとKubernetesマニフェストを管理するリポジトリの2つがありました。

計測プラットフォームでは、ArgoCDを利用することでGitOpsに準拠した形でのデプロイパイプラインを採用しています。ArgoCDを利用したデプロイパイプラインについては、ArgoCDの導入についてのブログ記事が公開されているので、気になる方はこちらの記事をご参照ください。

techblog.zozo.com

それぞれのリポジトリで作成されるPR、自動マージをブロックしたい条件を整理した結果を以下の表にまとめます。

リポジトリ PRの種別 自動マージをブロックしたい条件
アプリケーションリポジトリ ・BotUserが作るライブラリ更新のPR
・人が作る機能追加/BugFixのPR
・releaseブランチからmainブランチへのPR
・CIが失敗したPR
・BotUser以外が作ったPR
Kubernetesマニフェストリポジトリ ・ImageUpdaterが作るアプリケーションImage更新のPR
・人が作るKubernetesマニフェストに対する更新PR
・mainブランチからreleaseブランチへのPR
・CIが失敗したPR
・人のコミットが入ったPR
・負荷試験が失敗した場合のreleaseブランチへのPR

BotUserが作成するPRとImageUpdaterが作成するPRに関しては、自動マージするために以下のようなGitHub Actionsを追加し、CIが通過した場合は自動マージするようにしました。なお、自動マージはGitHubから提供されている自動マージ機能を利用しています。この機能により、ブランチプロテクションルールを守りつつ自動マージを容易に実装できました。自動マージの利用にはリポジトリ側で設定を有効化する必要があるので、設定時には公式の設定手順を参照ください。

BotUserが作成するPRの自動マージを有効化

name: BotUser Auto Merge
on:
  pull_request:
    branches:
      - 'main'

permissions:
  pull-requests: write
  contents: write

jobs:
  auto-merge:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'bot-user' }}
    environment:
      name: ${{ inputs.env }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Approve PR
        shell: bash
        run: gh pr review "$PR_URL" --approve
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN  }} # BotUserが作るPRにapproveするので、GitHubActionのTOKENを指定している
      - name: Auto Merge PR
        shell: bash
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

ImageUpdaterが作成するPRの自動マージを有効化

name: Create Image Updater PR and Auto Merge

inputs:
  argocd-application:
    description: ArgoCD Application Name
    required: true
  application-repo:
    description: Application Repository of GitHub
    required: true
  source-image:
    description: image watched by Image Updater. e.x. api-server
    required: true
  target-images-to-duplicate-image-tag:
    description: images apart from api-server. The format of item is `imageA,imageB,imageC`
    required: true
  bot-user-pat:
    required: true

runs:
  using: composite
  steps:
    - name: Update production file
      id: create-commit
      shell: bash
      run: |
        git config --global user.email "action@github.com"
        git config --global user.name "GitHub Action"
        STG_FILE="kubernetes/overlays/staging/.argocd-source-${{ inputs.argocd-application }}.yaml"
        PRD_FILE="kubernetes/overlays/production/.argocd-source-${{ inputs.argocd-application }}.yaml"
        # 次のステップで変数を利用する
        COMMIT_HASH=$(grep -oE "${{ inputs.source-image }}:[0-9a-z\-]+$" $STG_FILE | sed "s/${{ inputs.source-image }}://g")
        echo "COMMIT_HASH=${COMMIT_HASH}" >> $GITHUB_OUTPUT
        # 更新されたapi-serverのタグを全イメージに反映する。
        IMAGES=${{ inputs.target-images-to-duplicate-image-tag }}
        for image in ${IMAGES//,/ };
        do
          grep -oE ".+${{ inputs.source-image }}:[0-9a-z\-]+$" $STG_FILE | sed "s/${{ inputs.source-image }}/$image/g" >> $STG_FILE
        done
        # ステージング用のイメージタグの変更を本番用のファイルにも反映させる
        sed -e 's/${STG_AWS_ACCOUNT_ID}/${PRD_AWS_ACCOUNT_ID}/g' $STG_FILE > $PRD_FILE
        git add $STG_FILE
        git commit -m 'update image tags on staging'
        git add $PRD_FILE
        git commit -m 'update image tags on production'
        git status
        git push origin HEAD
    - name: Create PR to main branch
      id: create-pr
      uses: actions/github-script@v6
      with:
        script: |
          const { COMMIT_HASH } = process.env
          const { repo, owner } = context.repo;
          const result = await github.rest.pulls.create({
            title: '[Image Updater] イメージタグの更新',
            owner,
            repo,
            head: '${{ github.ref_name }}',
            base: 'main',
            body: `## アプリケーションの変更内容\nhttps://github.com/st-tech/${{ inputs.application-repo }}/commit/${COMMIT_HASH}\n## 反映方法\n - staging リリース: main ブランチにマージすると自動で staging 環境の Pod が入れ替わる。\n - production リリース: main から release ブランチの PR をマージすると自動で production 環境の Pod が入れ替わる。`
          });
          process.env.PR_URL = result.data.html_url
      env:
        COMMIT_HASH: ${{ steps.create-commit.outputs.COMMIT_HASH }}
    #[NOTE] GH_TOKENでBotUserのpatを指定している理由はsecrets.GITHUB_TOKENだとPR作成者とapprove者が同一になってしまい、Approveできないため
    - name: Approve PR
      shell: bash
      run: gh pr review "$PR_URL" --approve
      env:
        PR_URL: ${{ steps.create-pr.outputs.PR_URL }}
        GH_TOKEN: ${{ inputs.bot-user-pat }}
    - name: Auto Merge PR
      shell: bash
      run: gh pr merge --auto --merge "$PR_URL"
      env:
        PR_URL: ${{ steps.create-pr.outputs.PR_URL }}
        GH_TOKEN: ${{ inputs.bot-user-pat }}

例外として、Kubernetesマニフェストのreleaseブランチに対する自動マージは人のコミットが含まれない、かつ、負荷試験が成功した場合のみ有効化する必要がありました。このケースだけGitHub Actionsではなく、負荷試験の成功後にスクリプトで自動マージするようにしました。

負荷試験が成功した際に自動マージを有効化する

#!/usr/bin/env bash

# GH_TOKENはJobの環境変数で設定、BASE(release)とHEAD(main)は動作確認を手軽に行うために環境変数から取得する
pr_url=`gh search prs --state open --repo st-tech/${GIT_REPOSITORY} --base ${GIT_BASE_BRANCH} --head ${GIT_HEAD_BRANCH} --sort created --limit 1 --json url|jq -r .[].url`

# check only argocd-image-updater or GitHub Action and BotUser
gh pr view --repo st-tech/${GIT_REPOSITORY} $pr_url --json commits --jq '.commits[].authors[] |select(.name|test("(argocd-image-updater|GitHub Action|bot-user)")|not)'|grep email
contain_human_commit=$?

if [ $contain_human_commit == 0 ]; then
  message="<!here>["${PRODUCT_NAME}"] Stopped Auto Merge ${pr_url}"
  echo $message | ./slack.sh
  exit 0
fi

# approved & auto-merge enabled
gh pr review $pr_url --approve --repo st-tech/${GIT_REPOSITORY}
approved=$?

gh pr merge --auto --merge $pr_url --repo st-tech/${GIT_REPOSITORY}
auto_merge=$?

if [ $approved == 0 ] && [ $auto_merge == 0 ]; then
  message="<!here>["${PRODUCT_NAME}"] Merged Release PR ${pr_url}"
else
  message="<!here>["${PRODUCT_NAME}"] Failed Auto Merge ${pr_url}"
fi

リリース粒度のミニマム化

リリース作業の自動化対応が完了した後、アプリケーションリポジトリのブランチ戦略をGitHub Flowに統一しました。リリース作業の自動化とブランチ戦略の変更により、1PR毎にリリースが行われる様になりました。

リリース手順、ブランチ戦略の統一

リリース作業を自動化したことで手順は統一され、ブランチ戦略もGitHub Flowに統一されました。

振り返り

リリース作業の自動化対応とブランチ戦略を変更することで、最終的には以下のような状態になりました。

プロダクト名 デプロイ方法 ブランチ戦略 リリース頻度
ZOZOMAT ArgoCD GitHub Flow 随時
ZOZOGLASS ArgoCD GitHub Flow 随時
ZOZOFIT ArgoCD GitHub Flow 随時

結果、GitHub Flow with GitOpsとタイトルで挙げた状態に辿り着きました。

導入効果

導入後に元々の課題が解決されたか確認してみたところ、以下のような結果になりました。

リリース粒度 1リリースに要する作業時間 リリース頻度
変更前 5-10のPRをまとめてリリース 2時間 1-2/月
変更後 PR単位でリリース 5分 15-20/月

まず、リリース粒度に関しては、PR単位でのリリースとなりました。課題だった複数の変更が同時にリリースされ、パフォーマンスの変更要因の特定が困難な状況は解消されました。

1リリースに要する作業時間は、これまで2時間かかっていたものがPRのマージだけになったので5分に短縮されました(なお、PRのレビュー時間はリリース作業とは別として扱っています)。リリース頻度は課題ではなかったですが、こちらも大きな変化があったので記載しておきます。これまで1-2/月だったものが15-20/月になっており、7倍以上に増えました。

導入後に顕在化した課題

  • 負荷試験の失敗

    導入直後にストックされていたPRが短時間に連続でマージされ、負荷試験を立て続けて行う状態が発生しました。これにより想定以上の負荷がかかる状態となり、負荷試験の結果が目標値を下回りました。結果として、自動リリースが失敗しました。

    この課題に関しては、切り替え直後の一時的な問題だと判断し、恒久対応は行っていない状態です。開発ペースが上がると顕在化する問題のため、負荷試験の排他制御などの対応を将来的には入れる可能性があります。

  • 祝祭日にも自動リリースされてしまう

    ライブラリ更新系のPRは自動作成の曜日は平日のみとすることで、極力人がいる時間帯にリリースが発生するようにしていました。 ただ、祝祭日は考慮していなかったので、どうしようかという議論が導入後の祝日を迎えてされました。

    結果として、問題があった場合は検知されリリースが止まる、万が一リリースされてしまっても自動でロールバックされるため、祝祭日は考慮しない形となりました。

終わりに

今回のご紹介させていただいたGitHub Flow with GitOpsの導入は、これまでの改善があってこそできた形です。日々の改善に助けられた形で、リリース作業の自動化に踏み切れたので、継続して改善を続けてくれたチームメンバーへの感謝が凄まじかったです。

GitHub Flowはより早く価値を届ける事が強く求められる、更新が活発なプロダクトこそ恩恵を得られる印象が強いです。 今回の改善では、小さい粒度でリリースすることやリリース速度の高速化は、プロダクトのフェーズに関わらず得られる恩恵があると再確認できました。

計測プラットフォーム開発本部では、今回紹介させていただいたように、新規サービスの開発だけでなく既存サービスの改善も日々行っています。このような環境を楽しみ、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。

corp.zozo.com


  1. Git Flowは、Gitのブランチ戦略の1つです。mainブランチとは別にプライマリブランチを保持することで、リリースタイミングを柔軟にコントロール可能とします。Git Flowに関しての詳細は、原著であるA successful Git branching modelを参照ください。
  2. GitHub Flowは、GitHub社が採用しているGitのブランチ戦略です。GitHub Flowに関しての詳細は、公式のガイドを参照ください。
カテゴリー