AWS CDK + ecspresso + CodeDeployを使用してECSをデプロイする

2024.12.03
AWS CDK + ecspresso + CodeDeployを使用してECSをデプロイする
この記事をシェアする

こんにちは、クラウドビルダーズのきむです。

業務でECSを利用する機会があったのでECSのデプロイツールとしてecspressoを採用してみました。

今回はAWS CDK + ecspresso + CodeDeployを利用して環境を構築する方法を紹介します。

ecspressoとは

ecspressoはECSサービスのデプロイに特化したツールであり、ECSサービスとタスクに関わるリソースをコードとして管理します。

exspressoは下記の3ファイルで構成されています。

  • ecs-service-def: ECSサービス定義
  • ecs-task-def: ECSタスク定義
  • ecspresso: ecspresso全体の動作定義

ecspressoでは下記の形式がサポートされています。

  • yaml
  • json
  • jsonnet

ecspressoのファイルではSecrets ManagerやParameter Storeに格納された値を参照可能です。

各値の参照方法は公式リポジトリのREADMEに記載されています。

ソースコード

今回使用するソースコードは下記に置いています。

今回は下記のような構成を作成します。

ECSサービスで実行されるアプリはGoで実装されています。’/’にアクセスすると文字列を返すだけの簡易的なものです。

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()

	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
		// return c.String(http.StatusOK, "Welcome to ecspresso-demo!")
	})

	e.Logger.Fatal(e.Start(":8080"))
}

サービスのデプロイにはCodeDeployを使用してB/Gデプロイ方式を採用します。

VPC, IAM, ALBなどはもちろんですが、ECSサービスに関連するAutoScaling設定やCodeDeploy(デプロイコントローラーで’CODE_DEPLOY’を指定した場合)等はecspressoでは管理されないのでこれらのリソースはAWS CDKで作成します

AWS CDKのデプロイ(1回目)

ECSサービに設定するAutoScalingやCodeDeployのデプロイグループはECSサービスをデプロイした後でなければデプロイができないため、AWS CDKは2回に分けてデプロイします。

上記のリソースの作成についてはAWS CDKに渡すフラグで制御を行っています。

...省略
    // 初回時は作成しない
    if (props.isFirst === 'n') {
      this.createAutoScaling();
    }
...省略
    // 初回時は作成しない
    if (props.isFirst === 'n') {
      new codedeploy.EcsDeploymentGroup(this, 'EcsCodeDeployGroup', {
        application: this.ecsDeployApp,
        deploymentGroupName: `${this.prefix}-ecs-deploy-group`,
        service: ecs.BaseService.fromServiceArnWithCluster(
          this,
          'EcsService',
          `arn:aws:ecs:${cdk.Stack.of(this).region}:${
            cdk.Stack.of(this).account
          }:service/${props.cluster.clusterName}/${this.prefix}-service`,
        ),
        deploymentConfig: codedeploy.EcsDeploymentConfig.ALL_AT_ONCE,
        role: this.codeDeployRole,
        blueGreenDeploymentConfig: {
          blueTargetGroup: props.blueTargetGroup,
          greenTargetGroup: props.greenTargetGroup,
          listener: props.listener,
          testListener: props.testListener,
        },
        autoRollback: {
          failedDeployment: true,
          stoppedDeployment: true,
        },
      });
    }

AWS CDKに必要なパッケージをインストールします。

npm install --prefix infra

AWS CDKに関するコマンドは全てcdk.shにまとめているので実行権限を与えてデプロイを行います。

chmod +x infra/cdk.sh
infra/cdk.sh deploy

上記のコマンドを実行するとAutoScaling, CodeDeployデプロイグループを除いたリソースが作成されます。

ecspressoデプロイ(1回目)

今回使用するecspressoのコードはjsonnetで記載しています。

タスク定義やサービス定義で必要なAWSリソース情報(ネットワークリソースやIAMロールなど)はParameter Storeから参照しています。

local must_env = std.native('must_env');

{
  region: 'ap-northeast-1',
  cluster: must_env('SYS_NAME') + '-' + must_env('CLUSTER'),
  service: must_env('SYS_NAME') + '-' + must_env('SERVICE'),
  service_definition: 'ecs-service-def.jsonnet',
  task_definition: 'ecs-task-def.jsonnet',
  timeout: '10m0s',
  codedeploy: {
    application_name: must_env('SYS_NAME') + '-' + must_env('APPLICATION_NAME'),
    deployment_group_name: must_env('SYS_NAME') + '-' + must_env('DEPLOYMENT_GROUP_NAME'),
  },
}
local must_env = std.native('must_env');
local ssm = std.native('ssm');

{
  family: must_env('SYS_NAME'),
  containerDefinitions: [
    {
      name: must_env('SYS_NAME') + '-container',
      image: must_env('REPO') + ':' + must_env('TAG'),
      cpu: 0,
      portMappings: [
        {
          containerPort: 8080,
          hostPort: 8080,
          protocol: 'tcp',
          appProtocol: 'http',
        },
      ],
      essential: true,
      environment: [],
      secrets: [],
      mountPoints: [],
      volumesFrom: [],
      logConfiguration: {
        logDriver: 'awslogs',
        options: {
          'awslogs-group': '/aws/' + must_env('SYS_NAME') + '/logs',
          mode: 'non-blocking',
          'max-buffer-size': '25m',
          'awslogs-region': 'ap-northeast-1',
          'awslogs-stream-prefix': '${prefix-name}/${container-name}/${ecs-task-id}',
          'awslogs-create-group': 'true',
        },
        secretOptions: [],
      },
      systemControls: [],
      dependsOn: [],
    },
  ],
  taskRoleArn: ssm('/' + must_env('SYS_NAME') + '/task-role-arn'),
  executionRoleArn: ssm('/' + must_env('SYS_NAME') + '/execution-role-arn'),
  networkMode: 'awsvpc',
  requiresCompatibilities: ['FARGATE'],
  cpu: '1024',
  memory: '2048',
  runtimePlatform: {
    cpuArchitecture: 'ARM64',
    operatingSystemFamily: 'LINUX',
  },
}
local must_env = std.native('must_env');
local ssm = std.native('ssm');

{
  capacityProviderStrategy: [
    {
      base: 0,
      capacityProvider: 'FARGATE',
      weight: 1,
    },
  ],
  deploymentConfiguration: {
    maximumPercent: 200,
    minimumHealthyPercent: 100,
  },
  deploymentController: {
    type: 'CODE_DEPLOY',
  },
  desiredCount: 1,
  enableECSManagedTags: true,
  enableExecuteCommand: false,
  healthCheckGracePeriodSeconds: 0,
  launchType: '',
  loadBalancers: [
    {
      containerName: must_env('SYS_NAME') + '-container',
      containerPort: 8080,
      targetGroupArn: ssm('/' + must_env('SYS_NAME') + '/target-group-arn'),
    },
  ],
  networkConfiguration: {
    awsvpcConfiguration: {
      assignPublicIp: 'DISABLED',
      securityGroups: [ssm('/' + must_env('SYS_NAME') + '/security-group-id')],
      subnets: [
        ssm('/' + must_env('SYS_NAME') + '/subnet-id-1'),
        ssm('/' + must_env('SYS_NAME') + '/subnet-id-2'),
      ],
    },
  },
  platformFamily: 'Linux',
  platformVersion: 'LATEST',
  propagateTags: 'NONE',
  schedulingStrategy: 'REPLICA',
}

ecspressoに関するコマンドは全てecspresso.shにまとめているので実行権限を与えてコマンドを実行していきます。

chmod +x app/ecspresso.sh

アプリをビルドして作成したECRリポジトリにプッシュします。

// ECRへログイン
app/ecspresso.sh login

// アプリのビルド & ECRへのプッシュ
app/ecspresso.sh push

ECRへのプッシュが完了したらecspressoファイルの検証を行います。

全てのチェックで問題がなければVerify OKが出力されます。

app/ecspresso.sh verify

2024/12/02 15:39:27 [INFO] ecspresso version: v2.4.1
2024/12/02 15:39:28 ecspresso-demo-service/ecspresso-demo-cluster Starting verify
  TaskDefinition
    ExecutionRole[arn:aws:iam::xxxxxxxxxxxx:role/ecspresso-demo-execution-role]
    --> [OK]
    TaskRole[arn:aws:iam::xxxxxxxxxxxx:role/ecspresso-demo-task-role]
    --> [OK]
    ContainerDefinition[ecspresso-demo-container]
      Image[xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ecspresso-demo:latest]
      --> [OK]
      LogConfiguration[awslogs-create-group=true,awslogs-group=/aws/ecspresso-demo/logs,awslogs-region=ap-northeast-1,awslogs-stream-prefix=${prefix-name}/${container-name}/${ecs-task-id},max-buffer-size=25m,mode=non-blocking]
      --> [OK]
    --> [OK]
  --> [OK]
  ServiceDefinition
    LoadBalancer[0]
    --> [OK]
  --> [OK]
  Cluster
  --> [OK]
2024/12/02 15:39:30 ecspresso-demo-service/ecspresso-demo-cluster Verify OK!

ファイルの検証に失敗すると下記のようなエラーが出力されます。

app/ecspresso.sh verify

2024/12/02 16:45:51 [INFO] ecspresso version: v2.4.1
2024/12/02 16:45:52 [ERROR] FAILED. failed to load task definition ecspresso/ecs-task-def.jsonnet: RUNTIME ERROR: something went wrong calling get-parameter API: operation error SSM: GetParameter, https response error StatusCode: 400, RequestID: 3f2f6240-9f2a-40e7-ba05-53b994c6a958, ParameterNotFound: 
        ecspresso/ecs-task-def.jsonnet:41:21-76 object <anonymous>
        Field "executionRoleArn"
        During manifestation

ファイルの検証に成功したのでデプロイを行います。

Service is stable now. Completed!が出力されればデプロイは完了です。

app/ecspresso.sh deploy

2024/12/02 15:37:31 [INFO] ecspresso version: v2.4.1
2024/12/02 15:37:31 ecspresso-demo-service/ecspresso-demo-cluster Starting deploy 
2024/12/02 15:37:32 ecspresso-demo-service/ecspresso-demo-cluster Service ecspresso-demo-service not found. Creating a new service 
2024/12/02 15:37:32 ecspresso-demo-service/ecspresso-demo-cluster Starting create service 
2024/12/02 15:37:32 ecspresso-demo-service/ecspresso-demo-cluster Registering a new task definition...
2024/12/02 15:37:32 ecspresso-demo-service/ecspresso-demo-cluster Task definition is registered ecspresso-demo:1
2024/12/02 15:37:37 ecspresso-demo-service/ecspresso-demo-cluster Service is created
2024/12/02 15:37:40 ecspresso-demo-service/ecspresso-demo-cluster [INFO] failed to find CodeDeploy Application/DeploymentGroup for ECS service ecspresso-demo-service on cluster ecspresso-demo-cluster
2024/12/02 15:37:40 ecspresso-demo-service/ecspresso-demo-cluster Waiting a task set PRIMARY stable: STABILIZING
2024/12/02 15:39:02 ecspresso-demo-service/ecspresso-demo-cluster Waiting a task set PRIMARY stable: STEADY_STATE
2024/12/02 15:39:02 ecspresso-demo-service/ecspresso-demo-cluster Service is stable now. Completed!

上記の実行ログでCodeDeployのデプロイグループが見つからない旨のメッセージが出力しています。

2024/12/02 15:37:40 ecspresso-demo-service/ecspresso-demo-cluster [INFO] failed to find CodeDeploy Application/DeploymentGroup for ECS service ecspresso-demo-service on cluster ecspresso-demo-cluster

CodeDeployのデプロイグループが未作成なので想定内のメッセージです。

初回のデプロイには影響がありませんが、未作成の状態で2回目のデプロイを行おうとするとデプロイが失敗するので注意してください。

2024/12/02 15:39:53 [INFO] ecspresso version: v2.4.1
2024/12/02 15:39:53 ecspresso-demo-service/ecspresso-demo-cluster Starting deploy 
Service: ecspresso-demo-service
Cluster: ecspresso-demo-cluster
TaskDefinition: ecspresso-demo:1
TaskSets:
   PRIMARY ecspresso-demo:1 desired:1 pending:0 running:1 STEADY_STATE
Events:
2024/12/02 15:39:54 ecspresso-demo-service/ecspresso-demo-cluster Registering a new task definition...
2024/12/02 15:39:54 ecspresso-demo-service/ecspresso-demo-cluster Task definition is registered ecspresso-demo:2
2024/12/02 15:39:54 ecspresso-demo-service/ecspresso-demo-cluster [INFO] deployment by CodeDeploy
2024/12/02 15:39:54 ecspresso-demo-service/ecspresso-demo-cluster Updating service attributes...
2024/12/02 15:39:57 ecspresso-demo-service/ecspresso-demo-cluster desired count: 1
2024/12/02 15:39:57 ecspresso-demo-service/ecspresso-demo-cluster updating desired count to 1
2024/12/02 15:39:58 [ERROR] FAILED. failed to find CodeDeploy Application/DeploymentGroup for ECS service ecspresso-demo-service on cluster ecspresso-demo-cluster

作成されたサービスを確認していきます。

サービスは無事作成されました。初回のデプロイはCodeDeployによるデプロイが行われなかったのでデプロイのステータスが’-‘となっています。また、AutoScalingについても未作成なので設定はされいません。

動作確認のためALBのエンドポイントにアクセスしてみます。

curl -i http://ecspresso-demo-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com

HTTP/1.1 200 OK
Date: Mon, 02 Dec 2024 06:39:13 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 13
Connection: close

Hello, World!%   

AWS CDKのデプロイ(2回目)

AutoScalingとCodeDeployのデプロイグループを作成します。

infra/cdk.sh deploy:n

デプロイが完了するとecspressoでデプロイしたサービスにAutoScalingが設定されいるのとCodeDeployのデプロイグループが作成されているのを確認できます。

ecspressoデプロイ(2回目)

CodeDeployのデプロイグループが作成されたのでB/Gデプロイを試してみます。

まずはアプリを修正して返却される文字列を変更します。

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()

	e.GET("/", func(c echo.Context) error {
		// return c.String(http.StatusOK, "Hello, World!") コメントアウト
		return c.String(http.StatusOK, "Welcome to ecspresso-demo!") // コメントアウト解除
	})

	e.Logger.Fatal(e.Start(":8080"))
}

変更が完了したらもう一度ビルド & プッシュを行います。

app/ecspresso.sh push

ECRへのプッシュが完了したらデプロイを行います。

デプロイコマンドを実行するとCodeDeployのデプロイ画面が開かれます。

Service is stable now. Completed!が出力されればデプロイは完了です。

app/ecspresso.sh deploy

2024/12/02 15:46:30 [INFO] ecspresso version: v2.4.1
2024/12/02 15:46:30 ecspresso-demo-service/ecspresso-demo-cluster Starting deploy 
Service: ecspresso-demo-service
Cluster: ecspresso-demo-cluster
TaskDefinition: ecspresso-demo:1
TaskSets:
   PRIMARY ecspresso-demo:1 desired:1 pending:0 running:1 STEADY_STATE
AutoScaling:
  Capacity min:1 max:2
  Suspended in:false out:false scheduled:false
  Policy name:ecspressodemostackEcsApiServiceAutoScaleApiServiceCPUUtilizationACFEBD3D type:TargetTrackingScaling
  Policy name:ecspressodemostackEcsApiServiceAutoScaleApiServiceMemoryUtilization886BF5C6 type:TargetTrackingScaling
Events:
2024/12/02 15:46:32 ecspresso-demo-service/ecspresso-demo-cluster Registering a new task definition...
2024/12/02 15:46:32 ecspresso-demo-service/ecspresso-demo-cluster Task definition is registered ecspresso-demo:3
2024/12/02 15:46:32 ecspresso-demo-service/ecspresso-demo-cluster [INFO] deployment by CodeDeploy
2024/12/02 15:46:32 ecspresso-demo-service/ecspresso-demo-cluster Updating service attributes...
2024/12/02 15:46:35 ecspresso-demo-service/ecspresso-demo-cluster desired count: 1
2024/12/02 15:46:35 ecspresso-demo-service/ecspresso-demo-cluster updating desired count to 1
2024/12/02 15:46:36 ecspresso-demo-service/ecspresso-demo-cluster Deployment d-WTAOIT3F9 is created on CodeDeploy:
2024/12/02 15:46:36 ecspresso-demo-service/ecspresso-demo-cluster https://ap-northeast-1.console.aws.amazon.com/codesuite/codedeploy/deployments/d-WTAOIT3F9?region=ap-northeast-1
2024/12/02 15:46:36 ecspresso-demo-service/ecspresso-demo-cluster Waiting for a deployment successful ID: d-WTAOIT3F9
2024/12/02 15:46:46 ecspresso-demo-service/ecspresso-demo-cluster BeforeInstall: Succeeded
2024/12/02 15:46:46 ecspresso-demo-service/ecspresso-demo-cluster Install: InProgress
2024/12/02 15:49:06 ecspresso-demo-service/ecspresso-demo-cluster Install: Succeeded
2024/12/02 15:49:06 ecspresso-demo-service/ecspresso-demo-cluster AfterInstall: Succeeded
2024/12/02 15:49:06 ecspresso-demo-service/ecspresso-demo-cluster AllowTestTraffic: Succeeded
2024/12/02 15:49:06 ecspresso-demo-service/ecspresso-demo-cluster AfterAllowTestTraffic: Succeeded
2024/12/02 15:49:06 ecspresso-demo-service/ecspresso-demo-cluster BeforeAllowTraffic: InProgress
Traffic shifted 100% |████████████████████| 
2024/12/02 15:49:22 ecspresso-demo-service/ecspresso-demo-cluster Service is stable now. Completed!

デプロイ完了後にALBへアクセスするとアプリが更新されていることがわかります。

curl -i http://ecspresso-demo-alb-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com

HTTP/1.1 200 OK
Date: Mon, 02 Dec 2024 06:39:13 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 13
Connection: close

Welcome to ecspresso-demo!%   

ECSサービスのターゲットグループが切り替わり、デプロイステータスも成功へと変化しています。

これでAWS CDK + ecspresso + CodeDeployを利用した環境の構築が完了しました。

環境の削除

最後に下記のコマンドを使用して今回作成した環境の削除を行います。

// ECSサービスのスケールを0にする
app/ecspresso.sh scale0

2024/12/02 17:15:17 [INFO] ecspresso version: v2.4.1
2024/12/02 17:15:17 ecspresso-demo-service/ecspresso-demo-cluster Starting deploy 
Service: ecspresso-demo-service
Cluster: ecspresso-demo-cluster
TaskDefinition: ecspresso-demo:3
TaskSets:
   PRIMARY ecspresso-demo:3 desired:1 pending:0 running:1 STEADY_STATE
AutoScaling:
  Capacity min:1 max:2
  Suspended in:false out:false scheduled:false
  Policy name:ecspressodemostackEcsApiServiceAutoScaleApiServiceCPUUtilizationACFEBD3D type:TargetTrackingScaling
  Policy name:ecspressodemostackEcsApiServiceAutoScaleApiServiceMemoryUtilization886BF5C6 type:TargetTrackingScaling
Events:
2024/12/02 17:15:18 ecspresso-demo-service/ecspresso-demo-cluster desired count: 0
2024/12/02 17:15:18 ecspresso-demo-service/ecspresso-demo-cluster updating desired count to 0
2024/12/02 17:15:19 ecspresso-demo-service/ecspresso-demo-cluster [INFO] No deployments found in progress on CodeDeploy
kmrysk@kmrysknoMac-mini ecspresso-demo % app/ecspresso.sh delete

// ECSサービスの削除
app/ecspresso.sh delete

2024/12/02 17:15:21 [INFO] ecspresso version: v2.4.1
2024/12/02 17:15:21 ecspresso-demo-service/ecspresso-demo-cluster Deleting service 
Service: ecspresso-demo-service
Cluster: ecspresso-demo-cluster
TaskDefinition: ecspresso-demo:3
TaskSets:
   PRIMARY ecspresso-demo:3 desired:1 pending:0 running:1 STEADY_STATE
AutoScaling:
  Capacity min:1 max:2
  Suspended in:false out:false scheduled:false
  Policy name:ecspressodemostackEcsApiServiceAutoScaleApiServiceCPUUtilizationACFEBD3D type:TargetTrackingScaling
  Policy name:ecspressodemostackEcsApiServiceAutoScaleApiServiceMemoryUtilization886BF5C6 type:TargetTrackingScaling
Events:
2024/12/02 15:49:25 (service ecspresso-demo-service, taskSet ecs-svc/4445189337499124883) has begun draining connections on 1 tasks.
2024/12/02 15:49:25 (service ecspresso-demo-service, taskSet ecs-svc/4445189337499124883) updated state to STEADY_STATE.
2024/12/02 15:50:32 (service ecspresso-demo-service) has reached a steady state.
Enter the service name to DELETE: ecspresso-demo-service
2024/12/02 17:15:44 ecspresso-demo-service/ecspresso-demo-cluster Service is deleted

// AWS CDKの削除
infra/cdk.sh destroy

さいごに

ECSを運用する上でインフラ側とアプリ側のライフサイクルの違いがあり、AWS CDKだけでは管理が難しかったのですが、ecspressoを採用したことで境界が明確になり抱えていた課題も解決できました。

ECSのサービスとタスクのみを管理するシンプルなツールなので学習コストも高くありません。

非常に使いやすいツールですのでぜひ活用してみてください。

この記事をシェアする
著者:きむ
フェスとポケカに夢中です