はじめに
こんにちは、EC基盤開発本部SRE部カート決済SREブロックの金田・小松です。普段はSREとしてZOZOTOWNのカート決済機能のリプレイスや運用を担当し、AWSやAkamaiの管理者としても活動しています。
本記事では、前編と後編に分けて、Classic ASPの手動リリースをGitHub ActionsとAWX Operatorを活用して自動化したプロジェクトについてご紹介します。手動で行っていたリリース手順を自動化することで、効率化と安定性をどのように実現したか、そのアプローチをお伝えします。
前編では、Classic ASPの手動リリース作業が抱える課題を解決するためにGitHub Actionsを活用したリリースプロセス自動化の概要について解説します。
後編では、GitHub Actionsと連携してリリース作業を具体的に遂行するためのツールであるAWX Operatorについて、導入の背景や構成、実装の詳細に焦点を当ててご紹介します。
目次
- はじめに
- 目次
- リプレイス前の環境とリプレイスプロジェクトについて
- 手動リリースの課題
- リリース自動化へのアプローチ
- アーキテクチャ全体図
- リリース方式: 25%ずつのカナリアリリース
- GitHub Actions
- さいごに
リプレイス前の環境とリプレイスプロジェクトについて
ZOZOTOWNは、2004年12月にサービスを開始して以降、基本的なアーキテクチャは変えずにオンプレミス環境で動くモノリシックなシステムとして運用されてきました。リプレイス前のアプリケーションは、Classic ASPで構築されオンプレミス環境のWindowsサーバー上で動作しています。
しかし、サービスの拡大に伴い、モノリシックなシステムの運用や開発に課題が生じるようになりました。そこで2017年から、レガシー化したシステムの課題を解決するためにZOZOTOWNのマイクロサービス化をするためのリプレイスプロジェクトを進めています。リプレイス後のマイクロサービスでは、IaCやCI/CDの導入により、開発効率や運用効率の向上を図っています。
詳しくは以下のテックブログをご覧ください。
手動リリースの課題
ZOZOTOWNのリリース作業は、リプレイスされマイクロサービス化したシステムを除き、以下の手順を手作業で実施していました。
- リリース対象ファイルの準備
- バックアップの取得
- リリース手順の準備
- ファイルをどの順番でリリースするかの検討
- WebDAVを利用したファイルのアップロードによるリリース
この手作業のプロセスにより、リリースごとに多くの工数がかかり、作業負担が増大する課題を抱えていました。
また、以下のような問題も発生していました。
Git管理とリリース手順の不一致
リリース後に対象ファイルをmasterブランチにマージする運用のため、同時期に複数案件のリリースをする際、共通ファイルのコンフリクトが発生する場合もありました。この場合、リリース担当者が最新ファイルを手元でマージし直す必要があり、リリース担当者間の調整やコミュニケーションが求められ、工数増加やコミュニケーションミスが障害リスクを生む要因となっていました。
リリース作業の信頼性と一貫性の不足
手作業によるリリースは操作ミスが発生しやすく、リリースミスや障害リスクの温床となり、リリース担当者の精神的な負担を増大させていました。
これらの課題を解消するためには、リリースプロセスの自動化が不可欠です。自動化により作業負担が軽減され、ミスのリスクが抑えられることで、リリース作業の信頼性が向上し、開発および運用チームの負担も軽減されます。
さらに、Git管理状態とサーバーにリリースされている状態を常に一致させるため、GitOpsの概念を取り入れたリリース自動化を進めました。これにより、リリースの一貫性が担保され、ミスの削減とプロセスの効率化が期待できます。
リリース自動化へのアプローチ
アーキテクチャ概要
これらの課題を解消するため、リリースプロセスの自動化にGitHub ActionsとAWX Operatorを採用しました。本節では、それぞれのツールが果たす役割について説明します。
GitHub Actionsの役割
GitHub Actionsは、リリースプロセス全体を制御する役割を担っています。主に以下の3つのタスクを実行します。
変更検知
- GitHubリポジトリにコード変更(マージ)があると、リリースプロセスをトリガーして開始します。
ジョブ制御
- リリースの進行状況に応じて、AWX Operatorへジョブ実行リクエストを送信し、次のステップを制御します。
結果収集と通知
- AWX Operatorから返却された実行結果を確認し、Slackを通じてリリース進行状況を通知します。
GitHub Actionsは、リポジトリとリリース管理のハブとして機能し、全体のプロセスを統括します。
AWX Operatorの役割
AWX Operatorは、GitHub ActionsとAPIを介して連携し、リリース作業の具体的な実行を担当します。以下が主な役割です。
- サーバー操作
- 対象サーバーに対して、必要なファイル配置やIISリサイクル、ロードバランサーを操作します。
- リクエスト処理
- GitHub Actionsから送信されたリリース条件やジョブパラメータをもとに、安全かつ正確に作業を実施します。
- 実行結果の返却
- ジョブの実行結果や状況をGitHub Actionsに返却し、次のステップに移行するための判断材料を提供します。
アーキテクチャ全体図
以下の図は、GitHub ActionsとAWX Operatorを連携させたリリースフローを示しています。この仕組みにより、リポジトリの変更を起点としてリリース作業を自動化し、一貫性と効率性を実現しています。
- GitHub Actionsがリポジトリの変更を検知してトリガーを発火。
- リポジトリの差分情報やリリース条件をもとに、AWX Operatorへジョブ実行リクエストを送信。
- AWX Operatorがサーバーグループごとに作業を実施。
- 実行結果をGitHub Actionsに返却し、Slackを通じて通知。
リリース方式: 25%ずつのカナリアリリース
リリースを自動化するにあたり、すべてのWebサーバーを4つのグループに分割し、25%ずつロードバランサーから切り離した状態でファイルを更新する方法を採用しました。リリースが完了したサーバーは順次ロードバランサーに復帰させることで、段階的にサービスに影響を与えないリリースを実現しています。
この方式により、従来のリリースで必要だったファイルのリリース順を考慮する手間が不要になり、安全かつ効率的なリリースが可能です。また、25%ずつのカナリアリリースをすることで、万が一問題が発生した場合の影響範囲を限定でき、リスクを最小限に抑えられます。
具体的には、サーバーを4つのグループに分け、各グループを順番にリリース対象とした上で、以下の手順を実施します。
- STEP1:リリース対象の1グループのサーバーをロードバランサーから切り離し、外部トラフィックの影響を遮断します。
- STEP2:切り離したサーバーでリリース作業を実施し、IISリサイクルを行います。
- STEP3:作業が完了したサーバーをロードバランサーに再接続し、トラフィックを復旧させます。
この手順を各グループで順番に繰り返します。こうすることで、リリース中のリスクとダウンタイムを最小限に抑えつつ、全体の安定性を維持できます。以降で、リリース自動化の具体的な仕組みについて、GitHub ActionsやAWX Operatorの役割を中心に詳しく説明していきます。
GitHub Actions
本記事で紹介するリリースの自動化では、masterブランチへのマージをトリガーにGitHub Actionsのワークフローを起動して本番のサーバーにファイルをリリースしています。
また25%ずつのカナリアリリースを実現するため、GitHub Actionsでは様々な工夫をしています。
対象サーバー群の管理方法
リリース対象となるサーバー群はyamlファイルで管理しています。
下記はサーバー群の管理ファイルのフォーマットです。
<env>: - type: サーバーグループのタイプ servergroups: - <server role>: lb_info: - ロードバランサーを操作するための情報 . . . limitations: - 後述するAWXに渡すhost情報
項目 | 説明 |
---|---|
env | 環境を指定します |
type | サーバーグループのタイプを指定します。 ZOZOTOWNではonpremissのサーバーに加えてEC2のサーバーを増設することがあり、そのための設定です。 |
servergroups | サーバーグループの設定をリストとして保持します。 servergroupsリストの1要素ごとに直列でReleaseReview → LB Down → デプロイ → LB Up 操作を行います |
server role | サーバーの役割を任意の文字列で指定します。 同じservergroup内のserver role分、並列でデプロイ処理が実行されます |
lb_info | ロードバランサー情報の設定を含むリスト |
limitations | 対象サーバーの制限条件を指定します。 AWX用のサーバーパラメータをリスト形式で指定 |
上記のyamlファイルではリリース対象のサーバー情報の他にロードバランサーを操作するための情報も記載しています。このyamlファイルをworkflowで参照し、servergroups毎にデプロイ処理を行っています。
ReleaseReview
25%ずつのカナリアリリースを実施するにあたり、1つのグループのリリース完了後、即座に次のグループのリリースを進めてしまうとエラー発生の有無を確認する間がありません。各リリースの合間に確認時間を設けないと問題が発生した際にリリースを中断するのも難しくなってしまいます。
そこで本記事の自動リリースではグループごとのリリース処理を実行する前に、GitHub Environmentsを利用した承認stepを取り入れています。
GitHub EnvironmentsはGitHub ActionsのJobに設定でき、Protection Ruleを設定できます。設定できるProtection Ruleにはいくつか種類がありますが、Required reviewersを設定すると必須レビュアーからの承認があるまでジョブの実行を止めることができます。
Required reviewersの承認はリリースの担当者が承認できるように設定しています。リリースの担当者は1グループのリリースが完了したら、リリースしたグループにエラーがないことを確認後、次のグループのリリース承認を行います。
ReleaseReviewについてはマイクロサービスのCI/CDを紹介したブログ記事でも詳しく説明していますので、ぜひご参照ください。
リリース時のサービス監視
カナリアリリースによるリリースでは、リリース起因でのエラーが発生していないかを確認することが重要です。本記事での自動リリースではサーバー群を4つのグループに分割して順番にリリースしています。そこでグループ別にエラー発生の有無、エラー内容の確認、CPU負荷の確認ができるダッシュボードを用意して、リリースで問題が発生してないかの確認ができるようにしています。
リリースプロセスをよりよく把握するため、Splunkのダッシュボードを導入しています。リリース担当者は、ダッシュボードを使って、リリースされる各サーバーグループの状況をリアルタイムでチェックできます。段階的リリース(N%リリース)の際には、特にエラーやCPU使用率などのシステム負荷を詳細に監視できるようにしています。
さらに、500エラーが発生した場合、ダッシュボードから直接Gitの該当コード行へリンクできる機能を備えています。これにより、エラーの原因を素早く特定し、迅速な対応を可能にしています。
このダッシュボードはIaC化されており、SplunkのIaC化については、以前のブログ記事で詳しく紹介していますので、そちらもぜひご参照ください。
ワークフローの分割
GitHub Actionsでは、柔軟なループ構文(例えば、forループ)のネイティブサポートがありません。特に複数回実行の必要なジョブやステップがある場合、同一ワークフロー内でループを実行できないことが大きな制約となります。本記事の自動化においても、対象サーバー群を設定ファイルで管理できるようにしたものの、GitHub Actionsでは繰り返し処理を使えないため工夫が必要でした。
この制約を解決する方法として、ワークフローの分割とBashスクリプトの組み合わせで柔軟な繰り返し処理をGitHub Actionsでも実現できるようにしました。
この手法では、まず親となるワークフローからBashスクリプトでforループを構築し、gh workflow runコマンドで分割した子ワークフローを繰り返し実行します。子ワークフローとして実行したいワークフローは、gh workflow run
で実行できるようにworkflow_dispatch
をトリガーとして設定しておきます。
gh workflow run <実行したいワークフローファイルのpath> -f hoge=fuga --repo <対象リポジトリ> --ref github.base_ref
さらに、実行したワークフローのRunIDを追跡するため、gh run listコマンドで取得したURLから実行したワークフローのRunIDを抽出します。
gh run list --workflow=<ワークフローファイル名> --limit 1 --json url --repo <対象リポジトリ名> | jq -r '.[0].url'
取得したRunIDを使用し、gh run watchコマンドで指定したワークフローの終了を待つことも可能です。例えば、以下のようにして指定のRunIDの実行が終了するまで待機し、その結果を取得できます。
gh run watch <runid> -i 30 --exit-status --repo <対象リポジトリ名>
これらのコマンドをforループで繰り返し実行することで、柔軟な繰り返し処理を実装しています。
for i in <繰り返す回数>; do gh workflow run <実行したいワークフローファイルのpath> -f hoge=fuga --repo <対象リポジトリ> --ref github.base_ref # ワークフローが実行中になるのを待つ sleep 10 # RunIDの取得 runurl=$(gh run list --workflow=<ワークフローファイル名> --limit 1 --json url --repo <対象リポジトリ名> | jq -r '.[0].url') runId=$(basename "${runUrl}") # gh run watchで終了待機と結果取得を行う gh run watch "${runid}" -i 30 --exit-status --repo <対象リポジトリ名> done
排他制御
本記事の自動リリースでは、ワークフローが並列で実行されないように独自の排他制御を実装しています。排他制御が必要な理由としては以下が挙げられます。
- 複数のグループに分割してグループ毎にロードバランサーから下げてファイルを更新する方式のため、並列実行してしまうと複数のグループを本番から下げた状況になり、本番のサーバー数不足が想定される
GitHub Actionsにはconcurrencyというワークフローを直列で実行させるための設定がありますが、以下の理由で本記事の自動リリースでは採用できないため独自の排他制御を実装しています。
- concurrencyは、
最大 1 つの実行ジョブと 1 つの保留中のジョブが存在できる
とドキュメントに明記されており、3つ以上実行されると待機中のワークフローが古いものから順にキャンセルされる。 - 本記事の自動リリースでは、Mergeコミットの差分のみを展開する方式(※ 詳細は後述)としているため、ワークフローの実行がキャンセルされると展開されない差分ができてしまう。
lock処理の実装方式
GitHub Actionsで排他制御を実現するには、どのようにlockオブジェクトを実装するかが重要です。通常のシステムのようにデータベースやファイルシステムを利用できないため、独自の手法が必要です。
本記事で紹介する排他制御では、特定名称のGitブランチをlockオブジェクトとして扱い、そのブランチがGitHub上のワークフロー導入リポジトリに存在するかどうかで排他状態を判定しています。
具体的には、lockを取得したいワークフローがGitHubへブランチのPushを試み、Pushが成功すればlock獲得、失敗すれば他のワークフローがlockを保持していると判断します。これは、GitHub上でのブランチPushがアトミック操作であることを利用した方法です。
また、lockの解放は、lockブランチの削除によって行います。このシンプルな方式により、複雑な管理リソースを必要とせず、GitHub Actions内で効率的かつ確実な排他制御を実現できます。
lock処理の高速化
自動リリースを導入するリポジトリでは、長い履歴と多数の管理ファイルが存在するため、lock管理用のブランチ作成やPushに時間を要することが予想されます。そこで、効率的なリリースフローを実現するため、lock用ブランチはリポジトリの既存ブランチから派生させず、新たに空のリポジトリをgit init
でワークフロー上に作成する方法を採用しました。この空リポジトリ上で、lock状態を管理するためのファイルのみをcommitし、lock用ブランチとしてPushすることで処理の高速化を図っています。この手法により、リポジトリ全体をcloneする手間を省き、lock状態管理に必要な最小限のリソースのみを効率的に操作できるため、時間短縮とパフォーマンスの向上を同時に実現できます。
下記はワークフローで実行しているgit init
コマンドを使って作成した空リポジトリから作成したブランチを導入リポジトリにpushするためのコマンド例です。
git init git remote add origin "https://x-access-token:${{ github.token }}@github.com/<Repository名>.git" git config --global user.email "github-actions[bot]" git config --global user.name "github-actions[bot]@users.noreply.github.com" echo <lock情報> > lock.txt git add . git commit -m "add lock files" git push origin HEAD:<lockブランチ名>
各サーバー上での処理について
本記事で紹介する自動リリースの仕組みでは、GitHub Actionsから後述するAWX OperatorのAPIを介して各サーバー上でPowerShellスクリプトを実行し、デプロイ処理を行います。
サーバーからGitHubにアクセスする認証手段としては、一般的にデプロイキーやマシンユーザが利用されます。しかしこれらの認証方法はサーバーごとに個別のSSHキーを生成・設定する必要があるため、複数のサーバーでGitHubへのアクセスが必要な場合には設定の負担が増大し、採用が現実的ではありません。
そこで本記事の自動リリースでは、GitHub Appsで一時的なアクセストークンを生成し、各サーバーに配布する方式を採用しました。
GitHub Apps経由のトークン取得
GitHub Appsについての詳細は以下のドキュメントをご参照ください。
GitHub Appsで取得できるトークンには以下の2種類があります。
- インストールアクセストークン
- ユーザアクセストークン
インストールアクセストークン
GitHub AppsがリポジトリやOrgnizationのリソースへアクセスするために発行するトークンです。Botとして個別のリポジトリや組織へアクセスし、リポジトリ単位の自動化タスクを行う場合に使用します。アクセス権限はGitHub Appsの設定画面で細かく設定できます。トークンの有効期限もデフォルトでは1時間になっており、一時的な利用に適しています。
ユーザアクセストークン
OAuthトークンの一種で、認証されたユーザの権限とリポジトリへのアクセス範囲を持ちます。ユーザーが持つ全ての権限に基づいた広範囲なアクセスが許可されるため、アプリがユーザーとしての行動をシミュレートするような用途に適しています。有効期限はデフォルトでは8時間で更新トークンを利用してトークンの再生成が可能です。
本記事の自動リリースでは、各サーバーからGitHubにアクセスするためのトークンが必要です。そのため、一時的な利用に適したインストールアクセストークンを発行して使用しています。このトークンは、デプロイ処理を実行するたびに新規発行するよう設定しており、セキュリティリスクを低減しつつ、最新のアクセス情報で安全にリリース操作を行うことが可能です。
GitHub Actionsでのインストールアクセストークン発行は、公式のトークン発行Actionであるactions/create-github-app-tokenを使用しています。
このアクションの入力値として必要なApp IDとPrivateKeyは作成したGitHub Appsの設定画面で確認・作成できます。
上記の値は、それぞれリポジトリのSecretsに登録してGitHub Actionsから参照しています。
actions/create-github-app-token
で取得したインストールアクセストークンは、AWXのAPIを介して各サーバーのPowerShellにパラメータとして渡すようにしています。
デプロイ処理
PowerShellスクリプトでは以下の処理を実行しています。
- トークンを使ったremote url設定
- マージコミットにswitchしての差分取得とコミットされた状態に応じた処理
トークンを使ったremote url設定
トークンを使用してGitコマンドからGitHubにアクセスするためにはremote urlの設定が必要です。下記が設定例になります。
git remote set-url origin https://x-access-token:<トークン>@github.com/<owner>/<リポジトリ名>.git
この設定をすることで、origin指定のGitコマンドがトークンを使用したアクセスになります。
マージコミットにswitchしての差分取得とコミットされた状態に応じた処理
Gitリポジトリで最新のファイルを取得してリリースするのに一番シンプルな方法はディレクトリごと上書きする方式です。しかし自動リリースを導入するリポジトリはファイル数が大量にあり、毎リリースで全ファイルを上書きするのはパフォーマンス上の懸念がありました。またサーバー上のディレクトリ構成とリポジトリ上のディレクトリ構成が一致していないという課題もありました。
そこでマージされたpull requestの差分のみを取得・リリースする方式としました。
具体的には、GitHub ActionsからマージコミットのHashをパラメータとしてPowerShellスクリプトに渡します。PowerShellスクリプトは受け取ったHashにswitchして、git log
コマンドでマージコミットの親となるHashを取得します。マージコミットの親となるHashはPullRequestがマージされたbaseブランチとfeatureブランチ、それぞれのHashになります。
マージコミットの親となるHashを取得するコマンド例です。
# マージコミットにswitch git switch -f --detach <マージコミットのHash> git log -n 1 --pretty=format:%P
--pretty=format:%P
で親となるHashだけの出力になります。
詳細は公式ドキュメントをご参照ください。
下記はマージコミットのgit log
コマンドの出力例です。--pretty=format:%P
オプションを付けて実行することでMerge: e1108e69e07 83daabd093a
のe1108e69e07 83daabd093a
のみ出力されます。
$ git log --merges -n 1 commit ab4474df796f1dd4c8c188f3d88e3366de79be12 (HEAD -> master, origin/master, origin/HEAD) Merge: e1108e69e07 83daabd093a Author: kane8n <[email protected]> Date: Fri Nov 8 19:01:02 2024 +0900 Merge pull request #317 from st-tech/fe-deploy-test Fe deploy test $ $ git log --merges -n 1 --pretty=format:%P e1108e69e07fa210b4a5584d0a724b9f7ebf160a 83daabd093a2bf7826a458bb9d356f861478fd0b $
上記出力のスペースを...
に置換し、git diff
コマンドの引数とすることでマージコミットの差分が取得できます。git diff
コマンドにはファイルがどのような操作でコミットされたのか(追加されたのか・削除されたのかなど)の情報も取得するため、--name-status
オプションも付けて実行しています。
$ git diff --name-status e1108e69e07fa210b4a5584d0a724b9f7ebf160a...83daabd093a2bf7826a458bb9d356f861478fd0b A pc/wwwroot/FeDeployTest/brand/default.html D pc/wwwroot/FeDeployTest/brand/include/default_2021.html D pc/wwwroot/FeDeployTest/brand/include/default_2022.html A pc/wwwroot/FeDeployTest/css/common.css A pc/wwwroot/FeDeployTest/css/common.v2.css A pc/wwwroot/FeDeployTest/css/common.v3.css A pc/wwwroot/FeDeployTest/css/interview.css A pc/wwwroot/FeDeployTest/css/mv.css A pc/wwwroot/FeDeployTest/css/mv.v2.css A pc/wwwroot/FeDeployTest/default.html $
上記出力をPowerShellスクリプトでパースして、追加(A)や修正(M)であればコピー、削除(D)ならサーバー上のファイルも削除というように処理しています。
さいごに
前編では、Classic ASPの手動リリース作業が抱える課題を解決するためにGitHub Actionsを活用したリリースプロセス自動化の概要について解説しました。後編では、リリース自動化の中核となるAWX Operatorについて解説します。こちらもぜひご覧ください。
https://techblog.zozo.com/entry/asp-auto-deploy-implementation-second-parttechblog.zozo.com
ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。