こんにちは。SRE部の巣立(@ksudate)です。
我々のチームでは、AWS上で多数のマイクロサービスを構築・運用しています。マイクロサービスが増えるにつれて、CI/CDの長期化やリリース手法の分散など様々な課題に直面しました。
本記事では、それらの課題をどのように解決したのかを紹介します。
目次
はじめに
我々のチームが管理するCI/CDでは、ZOZOTOWNマイクロサービス基盤の全てのインフラリソースを対象にリリースまで行います。
インフラリソースごとに管理ツールが異なっており、全てGitHubにコードとして管理されています。
CI/CDで実行される処理は以下のようになっています。
インフラリソース | 管理ツール | CI | CD |
---|---|---|---|
AWS | CloudFormation | aws cloudformation create-change-set | aws cloudformation execute-change-set |
Kubernetes | Fluxcd | kubectl diff | kubectl apply or flux push artifact |
Datadog Sentry PagerDuty |
Terraform | terraform plan | terraform apply |
CI/CDのこれまで
これまでに2つのリリース手法を利用していました。
- Release PRによるリリース
- ドメイン単位の並行リリース
Release PRによるリリース
マイクロサービス基盤の構築当初は、release
ブランチを利用してリリースしていました。
このリリース手法ではGitHubのPull Request(以下、PR)の作成・更新によってCIパイプラインが動作し、マージによってCDパイプラインが動作します。
開発用ブランチ(ここでは、featureブランチとする)からmasterブランチ宛のPRを作成すると、DEVELOP・STAGING環境のCIパイプラインが動作します。PRをmergeするとCDパイプラインによってDEVELOP環境・STAGING環境へデプロイされます。
masterブランチからreleaseブランチ宛のPRを作成すると、PRODUCTION環境のCIパイプラインが動作します。PRをmergeするとCDパイプラインによってPRODCUTION環境へデプロイされます。
PR | CI/CDパイプラインの対象環境 |
---|---|
feature -> master | DEVELOP・STAGING |
master -> release | PRODUCTION |
masterブランチからreleaseブランチへのPR(以下、Release PR)は自動で作成されます。既にRelease PRが存在する場合は、そのPRの変更内容に追加されます。
この手法のメリットは、リリース前の動作確認が可能でリリース手順も簡単という点です。
しかし、マイクロサービスが増えると以下の課題が生まれました。
- CI/CD実行時間の長期化
- マイクロサービスごとのリリースが難しい
- リリーサーの制限ができない
CI/CD実行時間の長期化
CI/CDパイプラインでは、マイクロサービスの数だけ直列に処理を実行していました。
そのため、マイクロサービスが増えるにつれて、実行時間も増加していきました。
マイクロサービスごとのリリースが難しい
前述の通りreleaseブランチやRelease PRを全てのマイクロサービスで共有するため、マイクロサービスごとにリリースするのが難しいという問題がありました。
ロールバックする可能性のある変更は、他の変更と一緒にリリースしたくないことがあります。それを実現するには次の手順が必要です。
- Release PRが存在するか確認。存在する場合、mergeする。
- Release PRが存在するとその変更内容と同時にリリースされます。
- 他メンバーにPRのmergeを停止するように連絡する。
- Release PRをmergeするまでに他メンバーがmasterブランチ宛に別のPRをmergeするとその変更も含まれてしまいます。
この調整が大きな負担で、リリースサイクルの低下を引き起こしていました。
リリーサーの制限ができない
Release PRをmergeすることでCI/CDパイプラインによってPRODUCTION環境へリリースされます。
Release PRをmergeするには、1名以上のSREのレビューが必要です。
そのため、リリース可能なメンバー(以下、リリーサー)もSREに制限するのが理想です。
しかし、レビュー済みであればリポジトリへアクセスできる人は誰でもmergeできてしまいます。
上記の課題から新たなリリース手法を導入しました。
ドメイン単位の並行リリース
この手法ではマイクロサービスを大まかな機能や担当チームごとにグループ(以下、ドメイン)に分けます。そしてそのドメインごとにmain
ブランチとrelease
ブランチを使ってリリースします。
以下は検索ドメインの例を示しています。
検索ドメインでは、Search APIとSuggest APIの2つのマイクロサービスを持ちます。また、利用するブランチはzozo-search-main
とzozo-search-release
とします。
この手法のメリットは、ドメイン単位で並行にリリースできることです。
以前は複数チーム間でリリースの調整が必要でしたが、ドメイン内のマイクロサービスは1つのチームが管理しているので、調整なしでリリースできるようになりました。
また、CI/CDパイプラインではドメイン内のマイクロサービスに対してのみ処理が実行されます。そのため、以前の方法に比べて大幅な高速化を実現しています。
しかし、この手法でもいくつかの問題を抱えていました。
- リリース手法が分散する
- ブランチ間の同期が必要
- パイプラインの増加
- CI/CD実行時間の長期化
- リリーサーを制限できない
リリース手法が分散する
この手法ではマイクロサービスで利用するリソースのみをリリースの対象としました。
その他のリソース(ex. Cluster Autoscaler)は従来通りrelease
ブランチを利用していました。
そのため、リリース手法が2つ存在することになり新規利用者を困惑させる原因となっていました。
また、変更内容によってはブランチを切り替えながら作業する必要がありました。
ブランチ間の同期が必要
複数のブランチから参照されるリソース、例えば作業用のスクリプトなどは、これまで通りmaster
ブランチやrelease
ブランチで管理していました。
各ブランチで最新の内容を参照するために定期的に各ブランチを同期していました。この作業は毎週SREが実施していました。
しかし、各ブランチを同期するには複雑な手順が必要になります。この作業手順を間違えるとブランチの変更内容が消えたり、最新の状態と異なるものをリリースする可能性があります。
そのため、SREの大きなトイルとなっていました。
パイプラインの増加
この手法では専用のブランチごとにパイプラインが実行されます。そのため、ブランチごとにパイプラインを作成する必要があります。
パイプラインの大部分は同じ内容になっています。しかし、全てのパイプラインで利用しているカスタムアクションのアップグレードにも複数のファイルを修正する必要がありました。
またパイプラインが増加したことでCI/CDの渋滞が発生するようになりました。一度に大量のパイプラインが起動すると新しいパイプラインは他のパイプラインが一定数に落ち着くまで待機状態となります。その結果、CI/CD完了までの時間も大幅に増加していました。
CI/CD実行時間の長期化
Release PRによるリリースに比べて実行時間の高速化は達成しました。
しかし、ドメイン内のマイクロサービスが増えるにつれ実行時間が増加するため根本的な問題解決には至っていませんでした。
また、release
ブランチで稼働するCI/CDパイプラインは高速化できていませんでした。
リリーサーを制限できない
Release PRによるリリース同様にこの問題は解決していません。
CI/CDの刷新
これらの問題を全て解決するために、CI/CD基盤を新たにデザインすることにしました。
高速かつシンプルなCIパイプライン
まずは、CIパイプラインについて説明します。
変更差分を利用したCIパイプラインの実行
新しいCIパイプラインでは、PRに変更のあるインフラリソースに対してのみ処理が実行されます。
directory-changes
Jobでは、変更差分のあるインフラリソースのディレクトリ名を取得します。
変更差分の検知には、changed-filesを利用しました。このGitHubアクションを使うとPull Requestに変更のあったファイルやディレクトリを取得できます。
以下は、cloudformation
ディレクトリ配下に変更があった場合にそのディレクトリ名を返します。
cfn-directory-changes: outputs: cfn_changed_files: ${{ steps.directory_changes.outputs.cfn_all_changed_files }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: List modified directories id: directory_changes uses: tj-actions/changed-files@v40 with: json: true dir_names: true escape_json: false files_yaml: | cfn: - cloudformation/** dir_names_max_depth: 2 base_sha: ${{ github.event.pull_request.base.sha }} - name: Echo modified directories run: | echo "${{ steps.directory_changes.outputs.cfn_all_changed_files }}"
ここで取得したディレクトリを後続のJobへ渡します。結果、Pull Requestに変更のあるインフラリソースに対してのみ処理が実行されます。
directory-changes
Jobの導入によってマイクロサービスがどれだけ増えようとも実行時間が長期化することはなくなりました。
承認機能付きのCDパイプライン
続いて、CDパイプラインです。
CDパイプラインでもCIパイプライン同様にdirectory-changes
を活用しています。
また、新たにrelease-approval
とconfirm-management-team
の2つのJobを追加しました。
2つのJobによる新しい機能について説明します。
GitHub Environmentsによるリリース制御
これまで利用していたrelease
やzozo-search-release
などのブランチを廃止しました。新しいリリース手法では、master
ブランチのみを利用します。
しかし、リリース用のブランチが無くなるとリリース前の動作確認やリリースタイミングの制御ができません。
そこで、GitHub Environmentsを使うことにしました。
GitHub EnvironmentsはGitHub ActionsのJobに設定できます。
release-approval: runs-on: ubuntu-latest environment: name: <ENVIRONMENT NAME>
GitHub EnvironmentsにはいくつかのProtection Ruleが存在します。
今回は、Required reviewersを付与しました。Required reviewersを設定すると指定のレビュアーからレビューがあるまでJobは実行されません。
以下の例では、sre
Environmentsの必須レビュアーにksudate
が設定されています。
上記のsre
Environmentsを使用した例がこちらです。
ksudate
から承認があるまで、release-approval
のJobは実行されません。また、needsにrelease-approval
を指定しているk8s-apply
も実行されません。
name: Release Gate on: push: branches: - main jobs: release-approval: runs-on: ubuntu-latest environment: name: sre steps: - run: | echo "release approved" # CI STEP k8s-diff: runs-on: ubuntu-latest steps: - run: | echo "kubectl diff -f xxx" # CD STEP k8s-apply: runs-on: ubuntu-latest if: github.event.pull_request.merged == true needs: release-approval steps: - run: | echo "kubectl apply -f xxx"
この機能によってPRODUCTION環境のリリース前にレビューを追加できました。
その結果、リリース前の動作確認が可能になり、リリースタイミングを指定のレビュアーが制御できます。
しかし、この方法はGitOpsを実現するKubernetesクラスタで問題があります。
マイクロサービス基盤のKubernetesクラスタはFluxcdでGitHubを参照してアプリケーションのデプロイを行なっています。
そのため、master
へmergeしたタイミングでクラスタへ同期されます。
この対策として、FluxcdのOCIRepositoryを利用しました。OCIRepositoryを利用することで、FluxcdはGitHubではなく任意のOCI Repositoryからマニフェストを取得します。
OCI RepositoryにはAmazon ECRを利用しています。このAmazon ECRにマニフェストが格納されるとFluxcdはその情報を元に同期を行います。
そこで、release-approval
の実行後にAmazon ECRへマニフェストを格納することでリリースのタイミングを制御できました。
他にもOCI Repositoryを利用するメリットはあります。
詳しくは「Kubernetes Meetup Tokyo #58」で発表した資料をご覧ください。
release-approval
によって、以下の課題を解決しました。
- マイクロサービスごとのリリースが難しい
release-approval
によってPRごとにリリースすることが可能になりました。
- リリース手法が分散する
- 全てのインフラリソースを
master
ブランチを使ってリリース可能になりました。
- 全てのインフラリソースを
- ブランチ間の同期が必要
release
ブランチは廃止されました。ドメインごとにrelease
ブランチ、main
ブランチを管理する必要もありません。
- パイプラインの増加
- パイプラインのトリガーは
master
ブランチのみで今後増えることもありません。
- パイプラインのトリガーは
GitHub Environmentsによるリリーサーの制限
残る課題は、リリーサーの制限についてです。
リリーサーはどのファイルを変更したかによって変わります。例えば、Search API変更時のリリーサーはSearch Teamになります。Cart API変更時のリリーサーはCart Teamになります。
そこで、ファイルごとに管理するチームを設定しました。
これには、paths-filterを利用しました。
paths-filterを利用すると、事前に定義されたファイルの変更があったかどうかを知ることができます。
以下に例を示します。
confirm-management-team
Jobはfiltersに定義された情報を元に変更を検知します。例えば、cloudformation/search-api
に変更があれば、${{ steps.filter.outputs.search-team }}
がtrueを返します。
この情報をrelease-approval
Jobに渡すことで特定のチームのリリースを必須にできます。
今回の例では、release-approval
はチームごとに作成しています。こうすることで、Search Teamが管理するファイルに変更があった場合にsearch-team-release-approval
を起動して、Search Teamのレビューを必須にできます。
name: Release Gate on: push: branches: - main jobs: confirm-management-team: runs-on: ubuntu-latest outputs: search-team: ${{ steps.filter.outputs.search-team }} cart-team: ${{ steps.filter.outputs.cart-team }} steps: - uses: actions/checkout@v3 - uses: dorny/paths-filter@v2 id: filter with: filters: | search-team: - 'cloudformation/search-api' - 'kubernetes/search-api' - 'terraform/datadog/search-api' cart-team: - 'cloudformation/cart-api' - 'kubernetes/cart-api' - 'terraform/datadog/cart-api' search-team-release-approval: runs-on: ubuntu-latest if: ${{ needs.confirm-management-team.outputs.search-team == 'true' }} needs: confirm-management-team environment: name: search-team steps: - run: | echo "search-team release approved" cart-team-release-approval: runs-on: ubuntu-latest if: ${{ needs.confirm-management-team.outputs.cart-team == 'true' }} needs: confirm-management-team environment: name: cart-team steps: - run: | echo "cart-team release approved" # CI STEP k8s-diff: runs-on: ubuntu-latest steps: - run: | echo "kubectl diff -f xxx" # CD STEP k8s-apply: runs-on: ubuntu-latest if: ${{ ! ( failure() || cancelled() ) }} needs: - search-team-release-approval - cart-team-release-approval steps: - run: | echo "kubectl apply -f xxx"
結果
実行時間は、導入前に比べると大幅な削減を実現しました。
既存のワークフローから段階的に移行しているため、正確な比較は難しいです。しかし、導入前後で1か月間の平均などを見ると以下のようになっていました。
Before (min) | After (min) | |
---|---|---|
Avg | 9.45 | 2.00 |
Max | 23.5 | 6.38 |
Min | 0.37 | 0.28 |
Sum | 1146 | 126 |
1か月のトータル実行時間は1/10程度に減少しており、平均も7分近く削減できました。
加えて、これまで抱えていた課題も全て解決できました。
さいごに
この記事では、拡大し続けるマイクロサービス基盤で直面したCI/CDの課題をどのように改善したのかを説明しました。
現在、新しいモノレポCI/CDは問題なく稼働しています。引き続き、より良い開発体験を提供できるよう改善を進めていきます。
ZOZOTOWNでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!