ECSでBlueGreen DeployをCDKで試す記事 part1

記事タイトルとURLをコピーする

CS1の石井です。

タイトルの通りECSでBlueGreenDeployをCDKで記載しました。

単純にECSのBlueGreenデプロイを行うpart1の前編とパイプラインを通じて行うpart2の後編で分けます。

対象者

  • CDKのワークショップを終わらせている人
  • ECSのBlueGreenデプロイをやってみたい人
  • BlueGreenデプロイのチュートリアルを昔やったが、今更同じ手順をやるのが面倒な人

初めに

本記事は半自動です。コードだけ欲しい人は「CDKでBLUEのコンテナをデプロイ」のコードから自由に改造してください。

part1は単純なBlueGreenデプロイ(以下B/Gデプロイ)を試してみたい方向けであり、内容も公式のチュートリアルをCDKで表現しただけの内容となります。

チュートリアルベースのため、コードをデプロイして動きを確認するだけであれば、10分程度で読み終わると思います。

本記事にてB/Gデプロイの全体の流れを記載し、詳細なコードの内容はpart2で記載します。

用語の整理

登場人物となるリソースを先に軽く整理しておきます。

今回作成するリソースは以下です。

リソース名 用途
ALB ロードバランサー、web用コンテナのエンドポイントであり
B/Gデプロイのコンテナの通信を制御する
ECS AWSのコンテナ管理サービスの名前
クラスターという単位でコンテナを稼働させる
タスク定義ファイル ECSタスクがどのように動作するかを指示する「設計図」的要素
環境変数や稼働させるコンテナイメージ、コンテナのスペックを定義
サービス 長期的に実行されるタスクの集合、または
特定の数のタスクのインスタンスを維持するための定義。
コンテナがストップしてもサービスで定義した内容に自動で修復する
ECR コンテナイメージを保管するリポジトリ
CodeDeploy アプリケーションのリリースを自動化するサービス
B/Gデプロイ デプロイメント戦略の一つ
新しいバージョンのアプリケーション (Green) を既存のバージョン (Blue) と並行して起動し
トラフィックを新しいバージョンに切り替える方法

作成するリソースの構成図

作業の段取り

CDKで全て表現したいところですが、ECRへのDocker pushとCodeDeployを用いたデプロイ作業は手動でやります。 全て自動で行う場合PipelineやCodebuildの要素が必要となり、その点はpart2でまとめます。

今回はシングルアカウントに以下の作業段取りで進めます。

  1. ECRをマネジメントコンソールで作成
  2. dockerfileを作成し、imageをpushする
  3. CDKのコードを記載してBLUEバージョンのページを公開する
  4. CDKのコードを修正し、タスク定義ファイルのバージョンを一つ上げる
  5. CodeDeployを手動で実行しB/Gデプロイの動きを確認する

本記事の作業段取りは以上となります。次の項目から早速作業に入ります。

ECR作成

まずはAWSアカウントにコンテナイメージの格納先となるECRを作成します。

マネジメントコンソールにログインしてCloudShellから実行するか、自身の端末のaws cliで以下のコマンドを実行します。

aws ecr create-repository --repository-name ecs-tutorial

なお、普通にGUIで「ecs-tutorial」という名前のECRを作成いただいても構いません。

dockerfileを作成

ECRにpushするdockerfileを作成します。

適当なディレクトリで以下コマンドでDockerfileを作成してください。

touch Dockerfile && echo "FROM httpd:2.4" > Dockerfile

dockerイメージをpush

Dockerfileの用意は自分で行う必要がありますが、pushはecrの手順に従えば問題ありません。

ECRにマネジメントコンソールから接続して表示されるコマンドを実行するだけでECRにDockerfileで作成したdocker imageがpushされます。

補足の用語整理

Dockerfile

Dockerイメージを構築するための設定ファイルです。 このファイルには、ベースとなるOS、アプリケーションのインストール、設定の変更、スクリプトの実行など、 イメージを作成するための手順が記述されています。

docker build

docker buildは、Dockerfileを元にDockerイメージを構築するためのコマンドです。このコマンドを実行すると、DockerはDockerfileの指示に従って新しいイメージを作成します。

docker image

Dockerイメージは、コンテナを実行するためのテンプレートです。これはアプリケーション、依存関係、ライブラリ、ベースのOSなど、コンテナ化されたアプリケーションを実行するためのすべてのものを含む静的なスナップショットです。イメージは変更不可(イミュータブル)で、これをベースにして動的なインスタンス、つまりコンテナを起動します。

CDKでBLUEのコンテナをデプロイ

本項にてblueのコンテナをデプロイを行うCDKを書き始めます。

まずはcdk initを行います。

プロジェクトのinit

下記のコマンドでinitを行います。

mkdir ecs-single ; cd $_
npx cdk init --language typescript

ecs-single-stackの編集

lib/ecs-single-stack.tsというファイルがあるはずなので、そのファイルを以下のように編集します。

import { Stack, StackProps } from "aws-cdk-lib";
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as codedeploy from "aws-cdk-lib/aws-codedeploy";
import * as iam from 'aws-cdk-lib/aws-iam'; 


export class EcsSingleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // VPCの作成
    const vpc = new ec2.Vpc(this, 'MyVPC', {
      maxAzs: 2,
      subnetConfiguration: [
        {
          subnetType: ec2.SubnetType.PUBLIC,
          name: 'MyPublicSubnet',
          cidrMask: 24
        },
        {
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          name: 'MyPrivateSubnet',
          cidrMask: 24
        }
      ]
    });
    

    // セキュリティグループの作成
    const securityGroup = new ec2.SecurityGroup(this, 'MySecurityGroup', {
      vpc: vpc,
      description: 'Allow all outbound traffic by default',
      allowAllOutbound: true // すべてのアウトバウンドトラフィックを許可(デフォルト)
    });

    // インバウンドルールの追加例 (80番ポートを許可する)
    securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'allow SSH access from the world');
    securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(8080), 'allow access to test listener');



    // Application Load Balancerの作成
    const loadBalancer = new elbv2.ApplicationLoadBalancer(this, 'BlueGreenALB', {
      vpc: vpc,
      internetFacing: true, // 公開向け
      loadBalancerName: 'bluegreen-alb',
      securityGroup: securityGroup, // 既に作成したセキュリティグループを使用
    });


    // Target Groupの作成
    const targetGroupBlue = new elbv2.ApplicationTargetGroup(this, 'BlueTarget', {
      vpc: vpc,
      protocol: elbv2.ApplicationProtocol.HTTP,
      port: 80,
      targetType: elbv2.TargetType.IP,
      targetGroupName: "BlueTarget",
    });
    const targetGroupGreen = new elbv2.ApplicationTargetGroup(this, 'GreenTarget', {
      vpc: vpc,
      protocol: elbv2.ApplicationProtocol.HTTP,
      port: 80,
      targetType: elbv2.TargetType.IP,
      targetGroupName: "GreenTarget",
    });
    
    // ALBにHTTPリスナーを追加して、トラフィックをTarget Groupに転送する
    const bglistener = loadBalancer.addListener('ListenerGreen', {
      port: 80,
      protocol: elbv2.ApplicationProtocol.HTTP,
      defaultAction: elbv2.ListenerAction.forward([targetGroupBlue]),
    });

    // テスト用のリスナーを追加する
    const testListener = loadBalancer.addListener('TestListener', {
      port: 8080, // 8080というテストポートを使用します。必要に応じて変更できます。
      protocol: elbv2.ApplicationProtocol.HTTP,
      defaultAction: elbv2.ListenerAction.forward([targetGroupGreen]), // Greenのターゲットグループに転送
    });


    // ECSクラスタの作成
    const cluster = new ecs.Cluster(this, 'BlueGreenCluster', {
      clusterName: 'tutorial-bluegreen-cluster',
      vpc: vpc,
    });

    // ECSタスク定義の作成
    const taskDefinition = new ecs.FargateTaskDefinition(this, 'TutorialTaskDef', {
      memoryLimitMiB: 512, // 必要なメモリを指定してください
      cpu: 256,            // 必要なCPUを指定してください
      
    });


    // ECRリポジトリからコンテナイメージを取得。ecs-tutorialという名前のリポジトリで指定
    const containerImage = ecs.ContainerImage.fromEcrRepository(ecr.Repository.fromRepositoryName(this, 'Repo', 'ecs-tutorial'));

    const container = taskDefinition.addContainer('sample-app', {
      image: containerImage,
      memoryLimitMiB: 512,
      command: [
        "/bin/sh",
        "-c",
        "echo '<html> <head> <title>Amazon ECS Sample App</title> <style>body {margin-top: 40px; background-color: #00FFFF;} </style> </head><body> <div style=color:white;text-align:center> <h1>Amazon ECS Sample App</h1> <h2>Congratulations!</h2> <p>Your application is now running on a container in Amazon ECS.</p> </div></body></html>' > /usr/local/apache2/htdocs/index.html && httpd-foreground",
        // "echo '<html> <head> <title>Amazon ECS Sample App</title> <style>body {margin-top: 40px; background-color: #097969;} </style> </head><body> <div style=color:white;text-align:center> <h1>Amazon ECS Sample App</h1> <h2>Congratulations!</h2> <p>Your application is now running on a container in Amazon ECS.</p> </div></body></html>' > /usr/local/apache2/htdocs/index.html && httpd-foreground"
      ]
    });

    container.addPortMappings({
      containerPort: 80
    });

    // ECSサービスの作成(Fargateサービス)
    const ecsFargateService = new ecs.FargateService(this, 'FargateService', {
      cluster: cluster,
      taskDefinition: taskDefinition,
      desiredCount: 1,
      assignPublicIp: true,
      securityGroups: [securityGroup],
      vpcSubnets: {
        // 必要なサブネットを指定してください
        subnetType: ec2.SubnetType.PUBLIC,
      },
      deploymentController: {
        type: ecs.DeploymentControllerType.CODE_DEPLOY,
      },
      platformVersion: ecs.FargatePlatformVersion.LATEST,
    });

    // タスクをTarget Groupに関連付け
    targetGroupBlue.addTarget(ecsFargateService);


    // CodeDeployの定義
    const application = new codedeploy.EcsApplication(this, 'BlueGreenApp', {
      applicationName: 'tutorial-bluegreen-app',
    });

    // IAM Roleの作成
    const ecsCodeDeployRole = new iam.Role(this, 'EcsCodeDeployRole', {
      assumedBy: new iam.ServicePrincipal('codedeploy.amazonaws.com'),
    });

    // デプロイグループの作成
    new codedeploy.EcsDeploymentGroup(this, 'BlueGreenDG', {
      application: application,
      deploymentGroupName: "tutorial-bluegreen-dg",
      service: ecsFargateService,  
      blueGreenDeploymentConfig: {
        blueTargetGroup: targetGroupBlue,
        greenTargetGroup: targetGroupGreen,
        listener: bglistener,
        testListener: testListener, 
        deploymentApprovalWaitTime: cdk.Duration.minutes(30),  // Greenへの切り替わりを30分待機
        terminationWaitTime: cdk.Duration.minutes(10) // 新しいタスクの開始後、古いタスクの停止までの待機時間
      },
      role: ecsCodeDeployRole,
    });
    
    // CodeDeployのアプリケーションとデプロイグループの参照
    const app = codedeploy.ServerApplication.fromServerApplicationName(this, 'ExistingApp', 'tutorial-bluegreen-app');
    const deploymentGroup = codedeploy.ServerDeploymentGroup.fromServerDeploymentGroupAttributes(this, 'ExistingDG', {
      application: app,
      deploymentGroupName: 'tutorial-bluegreen-dg',
    });

  }
}

Blue版をデプロイ

以下のコマンドで早速CDKをデプロイしてみます。

npx cdk deploy

デプロイが完了したらALBが作成されているので、DNS名でアクセスします。

目がチカチカしますが、Blueのコンテナは正常にデプロイできているようです。

CDKでGREENのコンテナをデプロイ

先ほど編集したecs-single-stack.tsを以下のように編集します。

    const container = taskDefinition.addContainer('sample-app', {
      image: containerImage,
      memoryLimitMiB: 512,
      command: [
        "/bin/sh",
        "-c",
        // "echo '<html> <head> <title>Amazon ECS Sample App</title> <style>body {margin-top: 40px; background-color: #00FFFF;} </style> </head><body> <div style=color:white;text-align:center> <h1>Amazon ECS Sample App</h1> <h2>Congratulations!</h2> <p>Your application is now running on a container in Amazon ECS.</p> </div></body></html>' > /usr/local/apache2/htdocs/index.html && httpd-foreground",
        "echo '<html> <head> <title>Amazon ECS Sample App</title> <style>body {margin-top: 40px; background-color: #097969;} </style> </head><body> <div style=color:white;text-align:center> <h1>Amazon ECS Sample App</h1> <h2>Congratulations!</h2> <p>Your application is now running on a container in Amazon ECS.</p> </div></body></html>' > /usr/local/apache2/htdocs/index.html && httpd-foreground"
      ]
    });

単純にechoコマンドで記載しているhtmlファイルをgreen版に変えただけですが、これでタスク定義ファイルのリビジョンが一つ上がります。

下記コマンドで修正したタスク定義をデプロイします。

npx cdk deploy

cdk deployが完了しても、この時点ではタスク定義ファイルのリビジョンが上がっただけで、ECSのサービスで稼働しているコンテナには影響を及ぼしません。

実際ALBにアクセスしても先ほど表示したBlueの画面が表示されます。

コンテナの振る舞いは、「ECSのサービス」がタスク定義ファイルを元に稼働させています。 「ECSのサービス」は現在リビジョン1で稼働しているため、「ECSのサービス」が参照するタスク定義ファイルをリビジョン2に書き換える必要があります。

これは手動で実行してもいいのですが、CodeDeployを使えば自動でコンテナの置き換え作業を実行します。

CodeDeployからB/Gデプロイを実行する

まずはマネジメントコンソールのCodeDeployの画面から「tutorial-bluegreen-app」を選択します。

アプリケーションの画面から「デプロイ」→「デプロイの作成」を選択します。

デプロイの作成画面から下記のappspec.ymlを記載します。

version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXXXX:タスク定義の名前:リビジョン番号"
        LoadBalancerInfo:
          ContainerName: "sample-app"
          ContainerPort: 80
        PlatformVersion: "LATEST"

デプロイの作成ボタンを押した後、デプロイ画面でしばらく待つと、以下の画像のようになります。

ECSのサービスの中でblueとgreenのコンテナが両方起動している状態となり、テスト用のリスナーでgreenのコンテナにアクセスが可能となっています。

ALBのDNS名:8080でアクセスすると以下の画像が出力されます。

テストリスナーでアクセスしても問題なくページが表示できました。

設定では30分のテスト時間を設けていますが、単純なサイトなので「トラフィックの再ルーティング」ボタンで一気にデプロイを完了させます。

また、ステップ5の10分の待機時間もGreen切り替え後にBlueのコンテナを残しておく時間なので、こちらもデプロイを完了させてしまいます。

再度80ポートでアクセスすればGreenの画面が表示されます。 これで80での通常公開用のアドレスもGreenのコンテナに接続できるようになったため、B/Gデプロイは無事完了しました。

補足の用語整理

BlueGreenデプロイについて

Blue/Greenデプロイメントとは、ソフトウェアリリースの戦略の1つで、新旧のバージョンを同時に運用することで、ダウンタイムなく、またリスクを最小化して新しいバージョンのソフトウェアをデプロイする手法です。

今回のデプロイを図に表すと以下のような流れとなり、ALBの背後にいるコンテナは入れ替わっていますが、ダウンタイムは発生しません。

※テストリスナーの8080はALBのSGに自端末のIPを設定しておき、一般ユーザーからはSGで拒否するように設定する想定です。

具体的な手順としては以下のようになります:

準備段階:

現行の稼働中の環境を「Blue」と呼びます。 新しいバージョンのソフトウェアをデプロイするための新しい環境を「Green」と呼び、これを準備します。

デプロイ:

新しいバージョンのソフトウェアをGreen環境にデプロイします。 このとき、Blue環境は引き続き通常通り動作しています。

テスト:

Green環境にデプロイした新しいバージョンのソフトウェアをテストします。 このときのテストは、実際の運用環境と同じ条件下で行われるため、本番での動作を非常に高い確度で確認できます。

トラフィックの切り替え:

Green環境が正常に動作していることを確認した後、ユーザーのトラフィックをBlue環境からGreen環境に切り替えます。 これはロードバランサの設定を変更することで行います。 トラフィックの切り替え自体は非常に高速に行われ、ユーザーはダウンタイムを感じることなく新しいバージョンのソフトウェアを利用することができます。 ロールバックの可能性:

何らかの問題がGreen環境で発生した場合、トラフィックを直ちにBlue環境に戻すことで、迅速に前のバージョンにロールバックすることができます。

完了:

Green環境が問題なく動作していることが確認できれば、Blue環境を解放または再利用することでデプロイメントは完了となります。

Blue/Greenデプロイメントの利点:

ダウンタイムがない。 本番環境と同じ条件下でのテストが可能。 問題が発生した場合のロールバックが迅速。

欠点:

2つの環境を同時に管理・運用する必要があるため、リソースやコストが増加する可能性がある。

まとめ

ECSはやる前はなんだかよくわからないイメージのサービスでしたが、やってみるとFargateはコンテナを稼働させるミニVMとか、タスク定義はEC2で言い換えればスペックのパラメーターシート、という感じでEC2でコンテナ稼働させるイメージと置き換えていったら理解が進みました。

その過程でB/Gデプロイも勉強したのですが、たまに復習したくなった時にチュートリアルを再度やるのが面倒で「CDKでパパッと環境作れてすぐ試せる」という記事を書きたいなぁと前から思っていました。

ただ、パイプラインがないと大きな意味は持たないかなぁと思っています。そのためpart2でパイプラインを用いた構成で再度記事を上げたいと思います。