はじめまして、SRE チームの佐藤です。
SREチームの業務として、サービス信頼性向上のためのインフラの構築/改善や保守業務や、開発業務効率向上のためのCI/CD整備等に携わっています。
先日、社内サーバー上のJenkinsジョブで実装されていたビルド/デプロイの仕組みをAWS Code サービス群やAWS Step Functionsを利用した新構成へ移行しました。
今回はその対応や検討の経緯を紹介させていただきたいと思います。
移行の背景
既存構成
シンクロ・フードで提供しているサービスは基本的にRuby on Railsで構築されているのですが、昔から使っているシステムではJava (Seasar2)が利用されています。
Seasar2 から Railsへのリプレースを進めているためかなりの部分がRailsに移行されていますが、完全には移行完了できておらず、現時点でSeasar2側の開発もそれなりの頻度で発生している状況でした。
CI/CDの構成もそれぞれ異なっており、Rails側はGitHub Actionsを使ったテストやAWS Code サービス群を使ってビルド/デプロイを実施していますが、一方のSeasar2側は社内サーバー上のJenkinsジョブを利用してテストからデプロイまでを実施していました。
既存構成の課題
Seasar2向けのJenkinsジョブによるCI/CDは長らく運用されていたこともあり、処理自体は非常に安定していましたが、以下の課題感も持っていました。
課題1: 設備/HW面の懸念
開発業務を継続する上で重要な役割をもったサーバーですが可用性の観点に懸念がある状態でした。
社内サーバーは文字通り社内(オフィス)に置かれているサーバーです。データセンタのように電源系統冗長化はされておらず予備の電源装置などもないため、設備メンテナンスなどでオフィスが停電する際に必ず停止期間が発生していました。
また、HW保守契約なども特に結んでおらず、機器に障害が起きた場合は交換部材を探すところから対応する必要があり万が一の際の復旧時間にも懸念がありました。
課題2: 維持管理の手間
こちらはサーバー運用でよくある課題かと思いますが、OSやソフトウェア(Jenkins自体やJenkinsのplugin)のバージョンアップが後回しになっており、本サーバーについては塩漬け状態でした。
いつかは対応が必要と思いつつも、サービス提供側の基盤構築/運用をどうしても優先してしまう状況が続いていたため、そもそも維持管理が不要な状態としたいと感じていました。
移行方針
前述の課題に対する対応方針を以下のように整理し、詳細な検討を進めていきました。
"課題1: 設備/HW面の懸念"への対応方針
弊社ではデータセンタは利用しておらず、クラウドサービス活用が第一の選択肢となりそうです。
複数のクラウドサービスを利用している状況ですが、AWSを主なクラウド基盤として利用しているため、AWSへ移行することで設備/HW面の課題については対応する方針としました。
"課題2: 維持管理の手間"への対応方針
色々な選択肢がありそうですが、Rails側では既にGitHub ActionsやAWS Codeサービス群を活用したCI/CDが実装されているので、Seasar2側もこのあたりを軸に構成決めていくことで維持管理の手間は最小化する方針としました。
移行対応の詳細
ここからは、どのように移行を進めていったかの詳細を記載します。
構成概要
細かな検討経緯などは一旦後回しにして、対応の前後でどのように構成変化したかを記載します。
着手前の構成
CI/CDに係る主な要素
- AWS上
- ソースコード管理: AWS上にGitHub Enterprise Server(以下GHES)
- 社内サーバー上
- CI: Jenkinsジョブでテストや成果物作成を実行していました
- CD: JenkinsジョブでTomcatへのデプロイ処理を実施していました
- AWS上
デプロイ処理の大まかな流れは以下の通りです
- 1: Jenkinsジョブ実行時のパラメータ指定でデプロイ対象等挙動を調整する
- 2: デプロイ対象サーバーをLBから切り離す
- 3: デプロイ対象サーバーのTomcatを停止する
- 4: デプロイ対象サーバーにビルド成果物を配置する
- 5: デプロイ対象サーバーのTomcatを起動する
- 6: デプロイ対象サーバーをLBに再接続する
- 7: 冗長構成のため次のデプロイ対象サーバー向けに同様の処理を行う
- デプロイ実装方法については本番/非本番環境間の構成差異や実装タイミングの違いに起因して若干の差異がある状況でした
- シェルスクリプトによる実装
- Fabric(デプロイに活用できるPythonのライブラリ)による実装
移行後の構成
- ビルド部分はGitHub Actionsに移行
- ビルド成果物はS3に配置する
- デプロイ処理は以下の組み合わせで実装
- AWS CodePipeline(以下CodePipeline): パラメタ指定で処理を起動
- AWS Step Functions(以下Step Functions): パラメタに応じて処理調整
- AWS CodeBuild(以下CodeBuild): Step Functionsから呼び出され、各State向けに用意されたAnsible Playbookを実行することでデプロイを行う
- その他、本記事の記載対象とはしていませんが、社内ライブラリを管理するために利用しているMavenリポジトリをAWS CodeArtifactへ移行しています
検討経緯
テスト/ビルド部分の移行先として、組織としてCIにGitHub Actionsを利用する方針だったのでGitHub Actionsにすんなり決まりました。
一方でデプロイ箇所については結構悩むことになりました。
もともとは CodePipeline & Deployを軸としたシンプルな構成で実装したいと思っていましたが、AWS CodeDeploy(以下CodeDeploy) への移行に課題があったのです。
障壁1: 複雑な条件分岐や順序制御が難しい
- 詳細は本記事では割愛するのですが、セッションレプリケーションの仕様上、同一サーバー上のTomcatを連続して停止してしまうとセッションが巻き戻る問題がありました
- 詳細についてご興味あれば弊社の過去ブログも是非ご参照ください
- こちらについてはCodeDeployのデプロイグループをサーバー単位で作成し、順次呼び出しを行うことで解決できそうです
- 既存環境ではJenkinsジョブ実行時のパラメタで挙動を制御することで、デプロイ対象を制御するなどを行っており、移行後も引き続き同機能を提供したいと考えていました
- 実現方法検討してみたのですが、CodeDeployだけでこのような制御を実装することは難しそうに思えました
- 詳細は本記事では割愛するのですが、セッションレプリケーションの仕様上、同一サーバー上のTomcatを連続して停止してしまうとセッションが巻き戻る問題がありました
障壁2: CodeDeploy Agentを導入できない問題
- そもそものデプロイ対象サーバーのOSが残念ながらCodeDeploy Agentに対応していませんでした
- Apache License 2.0で公開されているCodeDeploy Agentの導入も検討したのですが、こちらもRubyのバージョンが条件を満たしていない状況でした
- 前述の障壁のこともあり、Rubyのバージョンアップ検証を実施するくらいならいっそ代替案を検討してみようと考えました
代替案
当初考えていたCodeDeployによる実装が難しいため、次の選択肢としてAWS上のなんらかのコンピュートリソース上でデプロイ処理を実行するという方式を検討しました。
まず、検討したことはデプロイ処理の実装方法です。
既存ではシェルスクリプトとFabricを利用していたため、比較的複雑な処理を実装していたFabric側に併せて処理統一することも検討したのですが、ここでも懸念がありました。
- Fabricのv1を利用しているが、現在Fabric v2以上の利用が推奨されている
- Fabricのv1から v2移行に際しては一定コード書き換えが必要そう
現在SREチームではサーバーのOS以上のレイヤの管理は原則Ansibleを利用して管理する運用となっていたこともあり、どうせ書き換えが必要ならいっそのことすべてAnsibleで実装してしまう。という方針にしました。
もう一つの選定理由としてはAnsibleは普段から書き慣れており、かつ処理ロジックも既存処理から流用できるので、移行コストがさほどかからないのではないか?という今振り返るとやや楽観的な見積もりもありました(実際はサーバー間の実行順序制御などツール間の違いを埋める検討が必要で片手間で対応できるほど容易ではありませんでした。)
次にAnsibleを実行するコンピュートリソースの選定です。
極力管理対象OSは増やしたくないという思いがあり、EC2利用以外で検討しました。
ECSタスクとして実行する方法や、CodeBuildから呼び出す方法を検討しましたが、既にRails側でCI/CDで利用しておりNW環境が整っていたこともありCodeBuildを採用する方針としました。
あとはCodeBuildをどのように呼び出すかを検討します。
移行前のJenkinsではパラメタを使って柔軟にデプロイ処理対象を制御していたので、移行後も極力同等の機能は提供したいと思いました。
こちらについては、CodePipeline のv2タイプ パイプラインでは実行時に変数が指定できるようになっているので問題はなさそうです。
(余談ですが、Jenkinsの場合はパラメタに設定する値をドロップダウンメニューから選べたりして操作が一目瞭然だったのですが、CodePipeline側では現状パラメタ指定は文字入力のみのようです。手順書を用意したり、パラメタ説明に期待する入力値を記載するなどでお茶を濁しています)
次に受け取ったパラメタを使ってどのように処理を調整するかを検討しました。
AnsibleのPlaybook内でパラメタを使って制御するという方法は以下の理由で検討外としました。
- Playbookが複雑になりすぎてしまう懸念がある
- デプロイ処理の各所でWEBページの応答確認を行っている都合上、単一Playbook内で冪等性を保証することが難しい
その他にも、なんらかの理由でPlaybookが失敗した場合、問題を取り除いた後に気軽に処理を再スタートしたいという思いもありました。
(既存のJenkins上の処理は単一のジョブとして構成されており、かつ処理に冪等性がない状態でした。稀にジョブが失敗した場面ではジョブ再実行ができないためあたふたしながら手動で後続の作業を実施してたりしたので、この機会に改善したかったという思いもありました)
今回は以下の理由からStep Functionsで処理の分岐を行い、Step FunctionsからCodeBuildを呼び出す構成を取る方針としました。
- Step Functions は社内で活用されており、一定知見が溜まっていた
- Step Functionsにはredrive機能で失敗したStateから再実行が可能で活用できそう
実装
AWSの管理はTerraformを利用しているため、Step Functionsの実装もTerraformから実施しました。
直接 Terraform のコードを新規に作成するのではなくAWS管理コンソールから処理を組み立てた後、JSON ベースの構造化言語(Amazon States Language)をTerraform に取り込み、これをベースに適宜修正を行うことで比較的短期間で実装できました。
前述の通りCodePipelineやCodeBuild周りの環境整備についても、既に利用している環境だったので追加で検討した箇所はIAMやSGくらいで、概ねスムーズに実装ができました。
一点実装段階で検討不足が露呈したのは、GHESとの接続部分です。
設計当初はCode Connections(旧CodeStar Connections)をCodePipelineのソースに指定することでStepFuntions/CodeBuildにGHESリポジトリのデータを渡せると思っていました。
ただ、Step FunctionsへアーティファクトのS3パスをパラメタの一部として渡す方法はなさそうだったためこの方法が利用できないことが分かりました。
こちらの対応策としては、GHES側でトークンを用意しCodeBuildでこのトークンを登録することでアクセス許可させる方式としました。
導入/切り替え
今回の移行対象ジョブは複数あったのですが順次導入していくと、デプロイ処理を行う開発者の方がどのジョブがJenkinsにまだ残置していてどのジョブがAWSに移行されたかわからなくなる恐れがありました。
なので、導入自体は裏で粛々と進めておき移行対象すべてのテストが完了した段階で切り替え宣言/運用ドキュメント差し替えを行うという方針を取りました。
この方針自体は問題なかったと思っていますが、並行稼働期間が生じることで顕在化した問題もありました。
- 既存ジョブ側で権限不足でデプロイ処理に失敗する
- こちらは単純なミスではありましたが、移行後のデプロイ処理でファイル配置時に指定した所有者に誤りがあったため、既存ジョブが実行された際に権限不足でファイルの置き換えができない問題でした
まとめ
JenkinsにはわかりやすいWEB管理画面があり、私を含め愛着を持っている開発者も多かったのですが、今回は基盤運用の手間を削減するために思い切ってAWSのマネージドサービスを活用したリファクタリングを行う選択をしました。
移行後に若干のバグ修正などが発覚したものの現状は安定して稼働してくれています。今回移行を行ったことでSeasar2からRailsへの移行が完全に完了するその日まではHW障害に怯えることなく、CI/CDを安定して提供できるようになったのではないかと思っています。
かなりニッチな内容になってしまった気もしますが、本取り組みの内容がどなたかの役に立てれば幸いです。