e-dash における ECS の Blue/Green デプロイの実現
はじめに
こちらはe-dash advent calendar 2024の12日目の記事です。
e-dash プロダクト開発部 SREチーム 伊藤です。
リリース作業における課題
e-dash プロダクトのアプリケーションの多くは ECS 上で動いており、これまで ECS の標準機能であるローリングアップデートによりアプリケーションの新バージョンをリリースしていました。この運用では、以下のような課題がありました。
- 本番環境でリリース事前確認ができない
- 本番環境でユーザ開放前の動作確認ができないため、環境起因の障害が発生するリスクがあります。
- リリース作業の長時間化
- 全アプリケーションのローリングアップデートが正常終了するまで確認する必要があるため、リリース担当の拘束時間が長時間化していました。
とあるリリースで実際に環境起因のユーザ影響が出てしまう本番障害が発生してしまったことを受け、リリース運用改善を行うこととなりました。細かい作業内容の見直しや運用ルールの整備以外にも、仕組みによる根本的な改善として Blue/Green デプロイの導入を検討することとなりました。
ECS の Blue/Green デプロイの方式検討
e-dash はすでに多くのユーザが利用しており運用も作り込んでいる環境であるため、Kubernetes 化などアーキテクチャの変更をせず ECS 上での Blue/Green デプロイを実現することとしました。
ECS 標準機能としての Blue/Green デプロイ
ECS には、標準機能として Blue/Green デプロイ機能を提供しています。この機能では、AWS CodeDeploy の導入が不可欠です。現状 e-dash では CodeDeploy を利用していないため、学習・導入コストや運用負荷を鑑み、こちらは利用しないこととしました。
ECS 標準機能でない Blue/Green デプロイ
今回 ECS の Blue/Green デプロイを実現する上で登場するコンポーネントは以下になります。
今回の方式における基本的な考え方は、Listener Rule に紐づく Target Group を変更することで Blue と Green を切り替える です。Target Group に紐づく Blue 及び Green のリソースは事前に準備しておく必要があります。この前提の元で、標準機能に頼らず自分たちで ECS の Blue/Green デプロイを実現するために、まずは ECS の どのレイヤで Blue/Green デプロイを実現するか を考えます。
ECS タスクレイヤのBGデプロイ
ECS タスクセット を利用する方法です。
同一の ECS サービス内に複数のECSタスク(タスクセットv1 と タスクセットv2)を用意し、リリース時に v1 から v2 のタスクセットに切り替えます。
ECS タスクセットを利用するには、ECS サービスのパラメータ DeploymentController
の設定を EXTERNAL
に変更する必要があります。DeploymentController
は以下のオプションがあります。
-
ECS
- デフォルト値。ローリングアップデートを行います。
-
CODE_DEPLOY
- CodeDeploy を利用して Blue/Green デプロイを行います。
- 標準機能の Blue/Green デプロイを利用する場合はこちらを設定します。
-
EXTERNAL
- 外部のデプロイメントコントローラーを利用してデプロイを行います。
EXTERNAL
にすると、ECS タスクセットを活用した柔軟な切り替え方式が実現できるようになります。一方で、ローリングアップデートができなくなるのでリリースの仕組みを実現するための追加実装が必要になります。ちなみに、ECS
の場合タスクセットを作成することができません。
ECS クラスタレイヤのBGデプロイ
ECS クラスタごと切り替える方法です。
ECSタスクの方式に比べると管理対象リソースが多くなりますが、こちらの方式はクラスタ全体で切り替えることになるため クラスタ内でサービス間依存関係がある場合により安全に切り替えができる可能性があります。
また、ECSサービスのレイヤで Blue と Green を切り替えるという選択肢もありますが、ECSタスクの方式に比べて複雑になってしまう割にあまりメリットはなさそうと判断したため、今回の選択肢からは除外しました。
クラスタとタスク方式のどちらを選択するか?
メリデメ表に整理しました。
Type | Pros | Cons |
---|---|---|
タスク | ・CodeDeploy でもECSタスクセットをベースにBGデプロイを実施しており、実績もある無難な方法である。 ・切り替えの単位が小さいため、効率的に運用できる可能性がある。 |
・Blue/Green 切り替え時に依存関係のあるサービス間の不整合が発生するリスクがある。 ・ローリングアップデートができなくなる。 ・ECS Service Connect がサポートされない。 |
クラスタ | ・クラスタ全体で Blue/Green 切り替えが完全に同一タイミングにできる。 ・Deployment Controller は ECS のまま運用できるため、従来のローリングアップデートの実行も可能。 |
・ECSクラスタ全体を切り替えるため、切り替えのために必要な時間が増える可能性がある。 ・よりAWSリソースのコストが掛かる。 |
e-dash プロダクトは、フロントエンド・バックエンドを含むいくつかのECSサービスで一つの Webサービス を構成しています。今回は、それらの依存関係をもつ ECS サービスが同じタイミングで切り替えられる方が望ましい ということがポイントとなり、クラスタ方式を選択しました。
また、ECSサービスの Deployment Controller を EXTERNAL
にすると ECS Service Connect がサポートされない という仕様が存在します。現状コンテナ間の名前解決に ECS Service Connect を使っており、この点からもタスク方式の採用は難しいと判断しました。
Amazon ECS Anywhere の External コンテナインスタンスは、Service Connect ではサポートされていません。
上記の引用元は Anywhere についての記述ですが、Deployment Controller を EXTERNAL
にすると Service Connect がサポートされないという仕様は Anywhere に限らず適用されるようです。
Blue/Green デプロイ方式に切り替える際に発生した問題・課題とその対応
切り替え時に発生した問題・課題とその対応について紹介します。
1. ECS Service Connect による名前解決が失敗する
先述の通り、弊社では ECSサービス間の名前解決に ECS Service Connect を利用しています。Blue/Green デプロイのために構築した新しいクラスタ上で初めて既存サービスを動かす際に、名前解決が失敗し正常にコンテナが稼働しない問題が発生しました。
ECS Service Connect は コンテナ起動時点で作成されていないECSサービスは、そのサービス起動後も名前解決ができない ことが原因でした。一般的な DNS とは違い、対象のサービスが起動した後も名前解決できるようになりません。
アプリケーションが新しいエンドポイントを解決できるようにするには、既存のサービスを再デプロイする必要があります。最新のデプロイ後に名前空間に追加された新しいエンドポイントは、タスク構成には追加されません。詳細については、「Amazon ECS Service Connect コンポーネント」を参照してください。
これは基本的には ECSサービス を新規作成する際にしか発生しない問題ですし、上記の通り再デプロイ(例えば以下の Force new deploy を実行すればOK)すれば解決します。しかし、初期構築時の余計なトラブルを最小化するために ECS Service Connect を使う場合は Terraform の depends_on
句などで適切にサービス間の依存関係を設定して構築する方がよいでしょう。
サービスで新しいデプロイを開始するには、[Force new deployment] (新しいデプロイの強制) を選択します。
2. ALB ターゲットグループの切り替えが失敗する
Blue/Green デプロイの切り替えを実現するためには、ALB のターゲットグループを切り替える必要があります。Github Actions から ターゲットグループを切り替えるために、modify-rules コマンドを利用しています。
弊社が運用している ALB のリスナールールは以下のような形式になっています。Actions
配列にリスナールール動作が定義されており、forward
における TargetGroupArn
を変更することでターゲットグループの切り替えが実現できます。
{
"RuleArn": "[omit]",
"Priority": "100",
"Conditions": [
{
[omit]
}
],
"Actions": [
{
"Type": "authenticate-oidc",
"AuthenticateOidcConfig": [omit],
"Order": 1
},
{
"Type": "forward",
"TargetGroupArn": [omit],
"Order": 2,
}
],
"IsDefault": false
}
ここでやりたいのはターゲットグループの変更だけです。しかし、残念ながら modify-rules には 既存の他の設定を残しつつターゲットグループの変更だけを行うオプションがない ため、ターゲットグループ以外の設定が存在する場合はその設定もの含めて入力する必要があります。ターゲットグループの変更だけを入力として modify-rules を実行すると、Type: authenticate-oidc
の設定が消えてしまい、ログインができなくなってしまいます。
この仕様があるため、今回リスナールールの切り替えは以下のステップで実現しています。
- describe-rules によって既存のリスナールールを json 形式で取得
- jq コマンドによってターゲットグループ等の書き換えを実施
- modify-rules によって書き換えた設定を適用
このステップでも、さらに jqコマンドによるリスナールールの書き換えがうまくいかなかった 問題が発生しました。
当初は、ステップ2 で実行している jq コマンドを以下のように実行していました。
jq 'del(.Conditions[0].PathPatternConfig, .Conditions[0].HostHeaderConfig, .IsDefault, .Priority, .Actions[1].ForwardConfig)' | \
jq '.Actions[0].AuthenticateOidcConfig |= .+ {"UseExistingClientSecret": true}' | \
jq '.Actions[1].TargetGroupArn|="'${TARGET_GROUP_ARN}'"'
json 内の配列のインデックスをハードコーディングしていました。
検証環境では問題なく動いたのですが、本番環境では配列の順序が異なっており、上記の書き換え処理がうまく動きませんでした。そのため、Actions 内の配列の順序に依存しないように jq コマンドを修正しました。
jq 'del(.Conditions[].PathPatternConfig, .Conditions[].HostHeaderConfig, .IsDefault, .Priority, .Actions[].ForwardConfig)' | \
jq '(.Actions[] | select(.Type=="authenticate-oidc").AuthenticateOidcConfig) |= .+ {"UseExistingClientSecret": true}' | \
jq '(.Actions[] | select(.Type=="forward").TargetGroupArn) |= "'${TARGET_GROUP_ARN}'"'
一旦は上記のように対応しましたが、このようにシェル芸的に作り込むアプローチはあまりメリットがないと考えています。Future work として、シェルで実現している切り替え処理を Go などの別の言語でツールを開発する予定です。
まとめ
ECS はとても柔軟でかつ高品質なサービスだと思います。一方、Blue/Green デプロイやカナリアリリースなどの仕組みを実現する場合は、Argo CD のような優れた OSS が利用可能という観点で EKS のような Kubernetes ベースのサービスの方がもう少し楽に実現できるのかもしれません。といっても、Kubernetes は ECS に比べて学習・運用コストが高い可能性があるため、ユースケースに応じて慎重に検討することが重要です。
今回構築したこの仕組みを利用して、リリースのさらなる自動化やDBレイヤの Blue/Green デプロイなどの運用改善に取り組んでいきたいと思います。また、今回の設計・実装を通じて得た知見を活用して、プレビュー機能(作業ブランチを独立した環境で一時的にプレビューできる機能)を併せて構築しました。こちらは別の記事で紹介したいと思います。
採用情報
e-dash開発チームは現在一緒にはたらく仲間を募集中です!
同じ夢について語り合える仲間と一緒に、環境問題を解決するプロダクトを作りませんか?
Discussion