Amazon Web Services ブログ
ECS 外部デプロイコントローラーを使用したブルー/グリーンデプロイ
はじめに
継続的インテグレーション (CI) と継続的デリバリー (CD) は、現代のソフトウェア開発における重要なプラクティスです。それによりソフトウェアの配信を合理化して、ビジネス価値を迅速に提供できます。迅速な配信に加えて、現在のビジネス環境では、アプリのダウンタイムもほぼゼロであることが求められます。ブルー/グリーンデプロイが提供するソリューションは、チームが迅速に提供できるようにするだけでなく、機能のリリースに不可欠と考えられていた「メンテナンスウィンドウ」を排除するのにも役立ちます。大まかに言うと、このアプローチでは、新しいアプリケーションスタック (グリーン) を本番バージョン (ブルー) と並行してデプロイする必要があります。最初に、このグリーンスタックは生産負荷から分離され、安定性を確保するためにさまざまなテストが行われます。安定したと見なされると、本番トラフィック全体がブルースタックからグリーンスタックに移ります。Canary リリースは、時間の経過とともにトラフィックがゆっくりとグリーンバージョンに移行する手法のバリエーションです。この手法については、今後の記事で取り上げます。
Amazon ECS の場合、エンジニアリングチームが AWS CodeDeploy をデプロイに使用すると、ブルー/グリーンデプロイで必要なカスタム作業が軽減されます。ただし、CD のニーズに合わせて他のツールを利用するチームにとっては、ブルー/グリーンデプロイは課題のまま残ります。このようなチームが使用するソリューションの 1 つでは、アプリのブルーとグリーンバージョン用の個別のサービスを、ロードバランサーの背後にある個別のターゲットグループにデプロイする必要があります。ブルーからグリーンに切り替えるには、リスナールールでターゲットグループをスワップする必要があります。この方法は実用的ではありますが、ブルーバージョンとグリーンバージョンに個別の ECS サービスを作成する必要があるため、理想的ではありませんでした。ECS タスクセット、外部コントローラ、および ALB 向けの強化されたリクエストルーティング が導入されたことで、このブログは、外部ツールの Jenkins をどのように用いて、ブルー/グリーンおよび Canary パターンを実装する CD パイプラインを構築するかをご紹介します。
まず、このソリューションの主要コンポーネントを定義しましょう。
ソリューションコンポーネント
タスクセット
タスクセットには、関連付けられたタスク定義のバリエーションを介して、ECS サービスがアプリケーションの複数のバージョンを実行できるようにする情報が含まれています。論理的には、これはタスクカウント、ネットワーク設定、LB 設定などのパラメータを含むデプロイの単位を表します。これにより、基本的に、重要な設定を変更し、本番バージョンに影響を与えることなく、タスクの新しいバージョンの動作に影響を与えることができます。status
パラメータは、現在本番稼働中のタスクセットであるプライマリはどれで、「Active」ではないタスクセットはどれかを表します。AWS CodeDeploy の場合、タスクセットはデプロイと 1:1 で関連付けられ、externalId
フィールドにデプロイ ID があります。いったん定義されると、タスクセットはスケールパラメータの変更のみを許可します。この更新制約は、実際には、タスクセットの意図を分離境界として中継します。他のパラメータのいずれかを変更すると、混乱が生じる可能性があるため、最初にプロビジョニングされたときに「Active」とマークされる新しいタスクセットを作成する必要があります。
外部コントローラー
ECS が提供するさまざまなデプロイタイプのうち、外部デプロイにより、デプロイプロセスを完全に制御できます。このデプロイプロセスは、選択したサードパーティーのデプロイコントローラー (この記事の場合は Jenkins) によって実行されます。カスタムデプロイプロセスとサードパーティーのコントローラーの使用は、サービス定義の deploymentController
パラメータに「EXTERNAL」の値を使用して指定されます。
Jenkins
Jenkins は、業界で広く採用されている非常に人気のある CI/CD サーバーです。プラグインと拡張性モデルによりその用途が本当に広がり、さらに OSS の性質から、あらゆるさまざまなタイプの言語とフレームワーク向けのプラグインが使えます。
エンジニアリングチームは、システムの配信ユニットを構築するためのパイプラインを作成します。パイプラインは、依存関係の解決、コンパイル、リンク、バージョン管理など、システムを構築するための小規模で細かいステップを実行するステージで構成されています。構築が完了すると、同じパイプライン (または異なるパイプライン) の異なるステージのセットが、アーティファクトを適切な環境にデプロイします。
このブログでは、ECS サービスのデプロイをオーケストレーションする「外部コントローラー」として Jenkins を使用します。Groovy スクリプトのサポートにより、パイプラインのステージは、カスタムロジックを実装します。これにより、新しいタスク定義、タスクセット、その他のステップを作成して、ブルー/グリーンまたは Canary 戦略を使用する新しいバージョンをデプロイします。
サンプルパイプラインを使用する
前提条件
パイプラインを実行するには、次の依存関係を持つ Jenkins エージェントが必要です。
- Docker
- GNU Make
- Python
- Pip
- AWS CLI
- Jenkins プラグイン
- Git プラグイン
- パイプラインユーティリティのステップ
さらに、AWS CLI には適切な認証情報が設定されている必要があります。AWS CLI 設定に割り当てられたプロファイル名は、Jenkins パイプラインのパラメータとして提供されます。
サンプルリポジトリの詳細
この例では、GitHub で利用できるシンプルな flask ベースの API を使用します。リポジトリには、アプリケーションコード、コードアセットとしてのインフラストラクチャ (AWS CloudFormation)、ビルドパイプライン (Jenkinsfile) が含まれています。アプリケーションの構築では、Makefile
を使用して、一般的な業界の慣習を反映します。Makefile.env
ファイルには、パイプラインステップに必要なすべての関連する変数が含まれています。必要に応じて、ALB ARN の値などのファイル内の値を、1-InfraStack.yaml
および 2-AppStack.yaml
を使用してデプロイされた CloudFormation スタックの出力値に更新する必要があります。
独自のセットアップを使用してこのサンプルを実行するには、次の手順に従ってください。
- GitHub リポジトリをフォークします。
- 独自の AWS アカウントを使用して、
infrastructure
フォルダにある1-InfraStack.yaml
と2-AppStack.yaml
スタックをデプロイします。 - ステップ 2 で作成した 2 つのスタックの出力セクションから、
Makefile.env
の次の値を更新します。- REGION
- SERVICE_ARN
- CLUSTER_ARN
- TASK_ROLE_ARN
- EXECUTION_ROLE_ARN
- BLUE_TARGET_GROUP_ARN
- GREEN_TARGET_GROUP_ARN
- LIVE_LISTENER_ARN
- TEST_LISTENER_ARN
- LOG_GROUP
- TASK_SECURITY_GROUPS
- TASK_SUBNETS
- 変更をコミットして、フォークしたリポジトリにプッシュします。
パイプラインのセットアップ
Jenkins エージェントに適切な依存関係を設定したら、次の手順を使用して Jenkins にパイプラインを設定します。
パイプラインの作成
1.ダッシュボードで [New Pipeline] ボタンをクリックします。
2.[Where do you store your code] で [Git] をクリックします。
3.[Connect to Git repository] に、フォークのリポジトリ URL を入力します。
4.[Create Pipeline] をクリックします。
Jenkins はパイプラインを作成し、すべてのブランチをスキャンして Jenkinsfile
を見つけます。この場合、マスターブランチにある Jenkinsfile が見つかり、構築を開始します。残念ながら、初回の実行では awsAccountNumber
および awsProfile
パラメータの値が指定されていなかったため、最初の構築は失敗します。
パイプライン実装の詳細
パイプラインがサンプルアプリケーションのブルー/グリーンデプロイを実現する方法を見てみましょう。
デプロイ段階
初期スタックプロビジョニングの一部として作成された既存の「ブルー」デプロイの場合、新しい「グリーン」バージョンをデプロイするには、次の手順を実行します。
アプリケーションコードの更新
まず、必要な変更を加えてアプリケーションコードを更新します。次に、環境変数 NEXT_ENV
がグリーンに設定されます。次にこれらの変更をコミットし、リポジトリにプッシュします。新しい構築を開始するには、パイプラインをクリックし、[Branches] タブを選択します。[Run] ボタンをクリックして構築をトリガーします。
「awsAccountNumber」と「awsProfile」のパラメータを要求するポップアップが開きます。 これらの値を入力して、[Run] をクリックします。
構築が開始し、次の段階を経て、「テスト」リスナーを通じて新しいバージョン (グリーン) を公開します。
イメージの構築
最初の段階は、コンテナイメージを構築することです。パイプラインは、GNU Make に依存して、Makefile の仕様を用いて構築を実行します。サンプルアプリは単純ですが、このメカニズムを使用して、一般的な実際の構築システムを示しています。イメージが構築され、現在のコミットの Git ハッシュでタグ付けされます。このタグ付けにより、「最新」のタグではなく、特定のタグを使用してコンテナイメージを参照できるため、ベストプラクティスです。
build-image:
docker build -t $(APP_NAME):$(GIT_COMMIT) $(APP_DIR)
docker tag $(APP_NAME):$(GIT_COMMIT) $(AWS_ACCOUNT_NUMBER).dkr.ecr.$(REGION).amazonaws.com/$(REPO_NAME):$(GIT_COMMIT)
Amazon ECR へのプッシュ
次に、新しい画像が ECR にプッシュされます。最新の Git ハッシュのみでタグ付けされた最新のイメージでは、「latest」タグを使用することはできません。
push-image:
@docker push $(AWS_ACCOUNT_NUMBER).dkr.ecr.$(REGION).amazonaws.com/$(REPO_NAME):$(GIT_COMMIT)
タスク定義の登録
ECR で利用できる新しいアプリケーションイメージを使用して、最新のイメージを参照する新しいタスク定義が登録されます。このタスク定義により、ECS は最新のアプリケーションコードを使用してタスクを起動できます。構築ステップはタスク定義テンプレートを使用し、テンプレートの値を Makefile.env
によって指定されるものに設定します。次に、AWS CLI を使用してタスク定義を登録します。
// Read template and update values
def taskDefinitionTemplate = readJSON(file: templateFile)
taskDefinitionTemplate.family = taskFamily
taskDefinitionTemplate.taskRoleArn = env.TASK_ROLE_ARN
taskDefinitionTemplate.executionRoleArn = env.EXECUTION_ROLE_ARN
…
// Uses the updated template as input to register the TaskDefinition.
def registerTaskDefinitionOutput = sh (
script: "aws ecs register-task-definition --cli-input-json file://${taskDefFile}",
returnStdout: true
).trim()
タスクセットの作成
このステップでは、前のステップで作成したタスク定義を参照して、新しいタスクセット、デプロイを作成します。この手順では、基本タスクセットテンプレートも使用し、新しいデプロイを反映する値を提供します。このタスクセットが作成された後、ECS は指定された数のタスクをプロビジョニングし、テンプレートで指定されたターゲットグループに登録します。
def taskSetTemplateFile = env.TEMPLATE_BASE_PATH + '/' + env.TASK_SET_TEMPLATE_FILE
def taskSetTemplateJson = readJSON(file: taskSetTemplateFile)
taskSetTemplateJson.taskDefinition = registerTaskDefinitionOutput.taskDefinition.taskDefinitionArn
…
def createTaskSetOutput = sh (
script: "aws ecs create-task-set --service $SERVICE_ARN --cluster $CLUSTER_ARN --cli-input-json file://${taskSetFile}",
returnStdout: true
).trim()
テストリスナーの有効化
タスクセットが作成されたところで、「テスト」リスナーを調整して、トラフィックをグリーンターゲットグループに転送します。「テスト」リスナーの重みを 100 に設定することでこれを実現します。この変更により、リスナーは受信したトラフィックの 100% をグリーンターゲットグループに転送します。
greenTG = ["Weight": 100, "TargetGroupArn": env.GREEN_TARGET_GROUP_ARN]
…
def modifyTestListenerResult = sh (
script: "aws elbv2 modify-listener --listener-arn $GREEN_LISTENER_ARN --cli-input-json file://${testDefaultActionsFile}",
returnStdout: true
).trim()
ALB とリスナーの観点から、次の図はこの時点でのセットアップを示しています。
この時点で、パイプラインは続行する前にユーザー入力のために一時停止します。
検証
「テスト」リスナーを介してアプリケーションの新しいバージョンが利用できるようになったら、パイプラインは停止し、ユーザーの入力を待って続行します。このステップは、デプロイワークフローの検証および/または承認ステップを表します。新しいバージョンが検証され、適切であると見なされた場合、パイプラインは「ライブリスナー」を入れ替えてこのバージョンを指すようになります。
ライブリスナーの入れ替え
検証と承認の後、パイプラインは「ライブ」リスナーを調整して、トラフィックをグリーンターゲットグループに送信するようになります。この手順により、実際にグリーンデプロイが達成され、新しいバージョンのアプリケーションがライブトラフィックで利用できるようになりました。この手順では、前のデプロイ (ブルー) も「テスト」リスナーに移ります。ECS の観点から見ると、この段階では 2 つのデプロイがあり、基本的に、実行中のタスクの数が 2 倍になります。
論理的には、ALB とリスナーからのビューは次のようになります。
ビルドパイプラインは、ユーザー入力のために再び一時停止します。次の段階で以前のデプロイが削除されるため、新しいバージョンが完全に安定していると見なされることが不可欠です。現在のデプロイで問題が検出された場合、ライブリスナーの重みをブルーターゲットグループに戻すように設定するだけで、以前のバージョンに戻すことができます。
クリーンアップ
ユーザー入力に基づいて、パイプラインは最後のステージを実行して以前のデプロイを削除し、タスク定義を非アクティブとしてマークします。これにより、タスクの数も意図したレベルに下がります。
- 前のタスクセットを削除します。
- 前のタスク定義を非アクティブとしてマークします。
まとめ
AWS CodeBuild により、チームはカスタム作業をほとんど行うことなく、ECS サービスにブルー/グリーンデプロイを採用できるようになりました。「外部コントローラー」を導入する前は、他の CI/CD ツールを使用するチームは、同じ目標を達成するために ECS サービスを追加で導入する必要がありました。このブログ記事では、Jenkins を使用して「外部コントローラー」を構築する方法を取り上げました。これにより、チームは既存の CI/CD ツールを柔軟に使用できるようになります。