author title date
haimtran
developing on amazon ecs
20/08/2023

Introduction

GitHub this project shows how to build applications on Amazon ECS with Blue/Green deployment.

Codepipeline

ecs-codepipeline

Blue/Green deployment

ecs-blue-green-deployment

Warning

  • Tested with "aws-cdk-lib": "2.93.0"
  • Need to use taskdef.json, appspec.yaml and iamgeDetail.json
  • Pull image from docker hub might experience rate limit

Network Stack

Let create a VPC with three subnets and security groups for ALB and ECS service

import { Stack, StackProps, aws_ec2 } from "aws-cdk-lib";
import { Construct } from "constructs";

interface VpcProps extends StackProps {
  cidr: string;
}

export class NetworkStack extends Stack {
  public readonly vpc: aws_ec2.Vpc;
  public readonly sgForAlb: aws_ec2.SecurityGroup;
  public readonly sgForGoWebapp: aws_ec2.SecurityGroup;
  public readonly sgForCvSummaryService: aws_ec2.SecurityGroup;

  constructor(scope: Construct, id: string, props: VpcProps) {
    super(scope, id, props);

    const vpc = new aws_ec2.Vpc(this, "VPC", {
      maxAzs: 3,
      enableDnsHostnames: true,
      enableDnsSupport: true,
      ipAddresses: aws_ec2.IpAddresses.cidr(props.cidr),
      natGatewayProvider: aws_ec2.NatProvider.gateway(),
      natGateways: 1,
      subnetConfiguration: [
        {
          // cdk add igw and route tables
          name: "PublicSubnet",
          cidrMask: 24,
          subnetType: aws_ec2.SubnetType.PUBLIC,
        },
        {
          // cdk add nat and route tables
          name: "PrivateSubnetNat",
          cidrMask: 24,
          subnetType: aws_ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
    });

    // security group for load balancer
    const sgForAlb = new aws_ec2.SecurityGroup(this, "SGForAlb", {
      vpc,
      allowAllOutbound: true,
    });

    // open http for the world
    sgForAlb.addIngressRule(aws_ec2.Peer.anyIpv4(), aws_ec2.Port.tcp(80));

    // security group for ecs go webapp service
    const sgForGoWebapp = new aws_ec2.SecurityGroup(this, "SGForGoWebapp", {
      vpc,
      allowAllOutbound: true,
    });

    // open port 3000 for alb
    sgForGoWebapp.addIngressRule(sgForAlb, aws_ec2.Port.tcp(3000));

    // security group for ecs cv summary service
    const sgForCvSummaryService = new aws_ec2.SecurityGroup(
      this,
      "SGForCvSummaryService",
      {
        vpc,
        allowAllOutbound: true,
      }
    );

    // open port 8080 for alb
    sgForCvSummaryService.addIngressRule(sgForAlb, aws_ec2.Port.tcp(8080));

    // export output
    this.vpc = vpc;
    this.sgForAlb = sgForAlb;
    this.sgForGoWebapp = sgForGoWebapp;
    this.sgForCvSummaryService = sgForCvSummaryService;
  }
}

Load Balancer Stack

Let create an ALB with two targe groups (blue and green), and two listeners (production and test).

import {
  aws_ec2,
  aws_elasticloadbalancingv2,
  Duration,
  Stack,
  StackProps,
} from "aws-cdk-lib";

import { Construct } from "constructs";

interface AlbProps extends StackProps {
  vpc: aws_ec2.Vpc;
}

export class AlbStack extends Stack {
  public readonly alb: aws_elasticloadbalancingv2.ApplicationLoadBalancer;
  public readonly blueTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup;
  public readonly greenTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup;
  public readonly prodListener: aws_elasticloadbalancingv2.ApplicationListener;

  constructor(scope: Construct, id: string, props: AlbProps) {
    super(scope, id, props);

    // application load balancer
    const alb = new aws_elasticloadbalancingv2.ApplicationLoadBalancer(
      this,
      "AlbForEcs",
      {
        vpc: props.vpc,
        internetFacing: true,
      }
    );

    // add product listener
    const prodListener = alb.addListener("ProdListener", {
      port: 80,
      open: true,
      protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,
    });

    // add test listener
    const testListener = alb.addListener("TestListener", {
      port: 8080,
      open: true,
      protocol: aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,
    });

    prodListener.connections.allowDefaultPortFromAnyIpv4("");
    testListener.connections.allowDefaultPortFromAnyIpv4("");

    // blue target group
    const blueTargetGroup =
      new aws_elasticloadbalancingv2.ApplicationTargetGroup(
        this,
        "GlueTargetGroup",
        {
          targetType: aws_elasticloadbalancingv2.TargetType.IP,
          port: 80,
          healthCheck: {
            timeout: Duration.seconds(20),
            interval: Duration.seconds(35),
            path: "/",
            protocol: aws_elasticloadbalancingv2.Protocol.HTTP,
          },
          vpc: props.vpc,
        }
      );

    // green target group
    const greenTargetGroup =
      new aws_elasticloadbalancingv2.ApplicationTargetGroup(
        this,
        "GreenTargetGroup",
        {
          targetType: aws_elasticloadbalancingv2.TargetType.IP,
          healthCheck: {
            timeout: Duration.seconds(20),
            interval: Duration.seconds(35),
            path: "/",
            protocol: aws_elasticloadbalancingv2.Protocol.HTTP,
          },
          port: 80,
          vpc: props.vpc,
        }
      );

    prodListener.addTargetGroups("GlueTargetGroup", {
      targetGroups: [blueTargetGroup],
    });

    testListener.addTargetGroups("GreenTargetGroup", {
      targetGroups: [greenTargetGroup],
    });

    // export output
    this.alb = alb;
    this.blueTargetGroup = blueTargetGroup;
    this.greenTargetGroup = greenTargetGroup;
    this.prodListener = prodListener;
  }
}

ECS Cluster Stack

Let create a ECS cluster

import {
  aws_ec2,
  aws_ecs,
  Stack,
  StackProps,
  IAspect,
  Aspects,
} from "aws-cdk-lib";
import { Construct, IConstruct } from "constructs";

interface EcsClusterProps extends StackProps {
  vpc: aws_ec2.Vpc;
}

export class EcsClusterStack extends Stack {
  public readonly cluster: aws_ecs.Cluster;

  constructor(scope: Construct, id: string, props: EcsClusterProps) {
    super(scope, id, props);

    Aspects.of(this).add(new CapacityProviderDependencyAspect());

    // ecs cluster
    this.cluster = new aws_ecs.Cluster(this, "EcsClusterBlueGreen", {
      vpc: props.vpc,
      containerInsights: true,
      enableFargateCapacityProviders: true,
    });
  }
}

/**
 * Add a dependency from capacity provider association to the cluster
 * and from each service to the capacity provider association.
 */
class CapacityProviderDependencyAspect implements IAspect {
  public visit(node: IConstruct): void {
    if (node instanceof aws_ecs.CfnClusterCapacityProviderAssociations) {
      // IMPORTANT: The id supplied here must be the same as the id of your cluster. Don't worry, you won't remove the cluster.
      node.node.scope?.node.tryRemoveChild("EcsClusterBlueGreen");
    }

    if (node instanceof aws_ecs.Ec2Service) {
      const children = node.cluster.node.findAll();
      for (const child of children) {
        if (child instanceof aws_ecs.CfnClusterCapacityProviderAssociations) {
          child.node.addDependency(node.cluster);
          node.node.addDependency(child);
        }
      }
    }
  }
}

ECS Service Stack

Let create an ECS service stack

import {
  aws_codedeploy,
  aws_ec2,
  aws_ecr,
  aws_ecs,
  aws_elasticloadbalancingv2,
  aws_iam,
  Duration,
  Stack,
  StackProps,
} from "aws-cdk-lib";

import { FargatePlatformVersion } from "aws-cdk-lib/aws-ecs";
import { Effect } from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";

interface EcsServiceProps extends StackProps {
  ecrRepoName: string;
  cluster: aws_ecs.Cluster;
  alb: aws_elasticloadbalancingv2.ApplicationLoadBalancer;
  blueTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup;
}

export class EcsServiceStack extends Stack {
  public readonly service: aws_ecs.FargateService;

  constructor(scope: Construct, id: string, props: EcsServiceProps) {
    super(scope, id, props);

    // task role
    const executionRole = new aws_iam.Role(
      this,
      "RoleForEcsTaskToPullEcrChatbotImage",
      {
        assumedBy: new aws_iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      }
    );

    // execution role
    executionRole.addToPolicy(
      new aws_iam.PolicyStatement({
        effect: Effect.ALLOW,
        actions: ["ecr:*"],
        resources: ["*"],
      })
    );

    // ecs task definition
    const task = new aws_ecs.FargateTaskDefinition(
      this,
      "TaskDefinitionForWeb",
      {
        family: "latest",
        cpu: 2048,
        memoryLimitMiB: 4096,
        runtimePlatform: {
          operatingSystemFamily: aws_ecs.OperatingSystemFamily.LINUX,
          cpuArchitecture: aws_ecs.CpuArchitecture.X86_64,
        },
        // taskRole: "",
        // retrieve container images from ECR
        executionRole: executionRole,
      }
    );

    // taask add container
    task.addContainer("GoWebAppContainer", {
      containerName: props.ecrRepoName,
      memoryLimitMiB: 4096,
      memoryReservationMiB: 4096,
      stopTimeout: Duration.seconds(120),
      startTimeout: Duration.seconds(120),
      // image: aws_ecs.ContainerImage.fromRegistry(
      //   "public.ecr.aws/b5v7e4v7/entest-chatbot-app:latest"
      // ),
      image: aws_ecs.ContainerImage.fromEcrRepository(
        aws_ecr.Repository.fromRepositoryName(
          this,
          props.ecrRepoName,
          props.ecrRepoName
        )
      ),
      portMappings: [{ containerPort: 3000 }],
    });

    // service
    const service = new aws_ecs.FargateService(this, "ChatbotService", {
      vpcSubnets: {
        subnetType: aws_ec2.SubnetType.PUBLIC,
      },
      assignPublicIp: true,
      cluster: props.cluster,
      taskDefinition: task,
      desiredCount: 2,
      deploymentController: {
        type: aws_ecs.DeploymentControllerType.CODE_DEPLOY,
      },
      capacityProviderStrategies: [
        {
          capacityProvider: "FARGATE",
          weight: 1,
        },
        {
          capacityProvider: "FARGATE_SPOT",
          weight: 0,
        },
      ],
      platformVersion: FargatePlatformVersion.LATEST,
    });

    // attach service to target group
    service.connections.allowFrom(props.alb, aws_ec2.Port.tcp(80));
    service.connections.allowFrom(props.alb, aws_ec2.Port.tcp(8080));
    service.attachToApplicationTargetGroup(props.blueTargetGroup);

    // exported
    this.service = service;
  }
}

interface EcsDeploymentProps extends StackProps {
  service: aws_ecs.FargateService;
  blueTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup;
  greenTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup;
  listener: aws_elasticloadbalancingv2.ApplicationListener;
}

export class EcsDeploymentGroup extends Stack {
  public readonly deploymentGroup: aws_codedeploy.EcsDeploymentGroup;

  constructor(scope: Construct, id: string, props: EcsDeploymentProps) {
    super(scope, id, props);

    const service = props.service;
    const blueTargetGroup = props.blueTargetGroup;
    const greenTargetGroup = props.greenTargetGroup;
    const listener = props.listener;

    this.deploymentGroup = new aws_codedeploy.EcsDeploymentGroup(
      this,
      "BlueGreenDeploymentGroup",
      {
        service: service,
        blueGreenDeploymentConfig: {
          blueTargetGroup,
          greenTargetGroup,
          listener,
        },
        deploymentConfig: aws_codedeploy.EcsDeploymentConfig.ALL_AT_ONCE,
      }
    );
  }
}

Deployment Group

The deployment group from CodeDeploy will handle the Blue/Green deployment with configuration and strategry for routing traffice such as ALL_AT_ONCE, CANARY.

interface EcsDeploymentProps extends StackProps {
  service: aws_ecs.FargateService;
  blueTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup;
  greenTargetGroup: aws_elasticloadbalancingv2.ApplicationTargetGroup;
  listener: aws_elasticloadbalancingv2.ApplicationListener;
}

export class EcsDeploymentGroup extends Stack {
  public readonly deploymentGroup: aws_codedeploy.EcsDeploymentGroup;

  constructor(scope: Construct, id: string, props: EcsDeploymentProps) {
    super(scope, id, props);

    const service = props.service;
    const blueTargetGroup = props.blueTargetGroup;
    const greenTargetGroup = props.greenTargetGroup;
    const listener = props.listener;

    this.deploymentGroup = new aws_codedeploy.EcsDeploymentGroup(
      this,
      "BlueGreenDeploymentGroup",
      {
        service: service,
        blueGreenDeploymentConfig: {
          blueTargetGroup,
          greenTargetGroup,
          listener,
        },
        deploymentConfig: aws_codedeploy.EcsDeploymentConfig.ALL_AT_ONCE,
      }
    );
  }
}

CI/CD Pipeline Stack

Important

Please pay attention to taskdef.json, appspec.yaml and imageDetail.json

Let create a CI/CD pipeline for deploying the chatbot app continuously as the following

import {
  aws_codedeploy,
  aws_ecr,
  aws_ecs,
  aws_iam,
  aws_codebuild,
  aws_codecommit,
  aws_codepipeline,
  aws_codepipeline_actions,
  Stack,
  StackProps,
} from "aws-cdk-lib";

import * as path from "path";
import { Construct } from "constructs";

interface CodePipelineProps extends StackProps {
  codecommitRepoName: string;
  repoBranch: string;
  ecrRepoName: string;
  appDir: string;
  service: aws_ecs.FargateService;
  deploymentGroup: aws_codedeploy.EcsDeploymentGroup;
}

export class CodePipelineStack extends Stack {
  constructor(scope: Construct, id: string, props: CodePipelineProps) {
    super(scope, id, props);

    // create codecommit repo
    const codecommitRepository = new aws_codecommit.Repository(
      this,
      "CodeCommitChatbot",
      {
        repositoryName: props.codecommitRepoName,
      }
    );

    // lookup ecr repo
    const ecrRepository = aws_ecr.Repository.fromRepositoryName(
      this,
      "EcrRepositoryForChatbot",
      props.ecrRepoName
    );

    // artifact - source code
    const sourceOutput = new aws_codepipeline.Artifact("SourceOutput");

    // artifact - codebuild output
    const codeBuildOutput = new aws_codepipeline.Artifact("CodeBuildOutput");

    // codebuild role push ecr image
    const codebuildRole = new aws_iam.Role(this, "RoleForCodeBuildChatbotApp", {
      assumedBy: new aws_iam.ServicePrincipal("codebuild.amazonaws.com"),
    });

    ecrRepository.grantPullPush(codebuildRole);

    // codebuild - build ecr image
    const ecrBuild = new aws_codebuild.PipelineProject(
      this,
      "BuildChatbotEcrImage",
      {
        projectName: "BuildChatbotEcrImage",
        role: codebuildRole,
        environment: {
          privileged: true,
          buildImage: aws_codebuild.LinuxBuildImage.STANDARD_5_0,
          computeType: aws_codebuild.ComputeType.MEDIUM,
          environmentVariables: {
            ACCOUNT_ID: {
              value: this.account,
              type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            },
            REGION: {
              value: this.region,
              type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            },
            REPO_NAME: {
              value: props.ecrRepoName,
              type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            },
            APP_DIR: {
              value: props.appDir,
              type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            },
            TAG: {
              value: "demo",
              type: aws_codebuild.BuildEnvironmentVariableType.PLAINTEXT,
            },
          },
        },

        // cdk upload build_spec.yaml to s3
        buildSpec: aws_codebuild.BuildSpec.fromAsset(
          path.join(__dirname, "./../buildspec/build_spec.yaml")
        ),
      }
    );

    // code pipeline
    new aws_codepipeline.Pipeline(this, "CodePipelineChatbot", {
      pipelineName: "CodePipelineChatbot",
      // cdk automatically creates role for codepipeline
      // role: pipelineRole,
      stages: [
        // source
        {
          stageName: "SourceCode",
          actions: [
            new aws_codepipeline_actions.CodeCommitSourceAction({
              actionName: "CodeCommitChatbot",
              repository: codecommitRepository,
              branch: props.repoBranch,
              output: sourceOutput,
            }),
          ],
        },

        // build docker image and push to ecr
        {
          stageName: "BuildChatbotEcrImageStage",
          actions: [
            new aws_codepipeline_actions.CodeBuildAction({
              actionName: "BuildChatbotEcrImage",
              project: ecrBuild,
              input: sourceOutput,
              outputs: [codeBuildOutput],
            }),
          ],
        },
        // deploy new tag image to ecs service
        // {
        //   stageName: "EcsCodeDeploy",
        //   actions: [
        //     new aws_codepipeline_actions.EcsDeployAction({
        //       // role: pipelineRole,
        //       actionName: "Deploy",
        //       service: props.service,
        //       input: codeBuildOutput,
        //       // imageFile: codeBuildOutput.atPath(""),
        //       deploymentTimeout: Duration.minutes(10),
        //     }),
        //   ],
        // },
        {
          stageName: "EcsCodeDeployBlueGreen",
          actions: [
            new aws_codepipeline_actions.CodeDeployEcsDeployAction({
              actionName: "EcsDeployGlueGreen",
              deploymentGroup: props.deploymentGroup,
              // file name shoulde be appspec.yaml
              appSpecTemplateInput: sourceOutput,
              // update task definition
              containerImageInputs: [
                {
                  // should contain imageDetail.json
                  input: codeBuildOutput,
                  taskDefinitionPlaceholder: "IMAGE1_NAME",
                },
              ],
              // should be taskdef.json
              taskDefinitionTemplateInput: sourceOutput,
              // variablesNamespace: ''
            }),
          ],
        },
      ],
    });
  }
}

Important

CDK automatically create role for codebuild, codedeploy, and codepipeline. Below is the content of the iam policy generated for codepipeline role. The codepline role will assume on of three different role for codebuild action, ecsdeploy action, and source action.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "s3:Abort*",
        "s3:DeleteObject*",
        "s3:GetBucket*",
        "s3:GetObject*",
        "s3:List*",
        "s3:PutObject",
        "s3:PutObjectLegalHold",
        "s3:PutObjectRetention",
        "s3:PutObjectTagging",
        "s3:PutObjectVersionTagging"
      ],
      "Resource": [
        "arn:aws:s3:::artifact-bucket-name",
        "arn:aws:s3:::artifact-bucket-name/*"
      ],
      "Effect": "Allow"
    },
    {
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey",
        "kms:Encrypt",
        "kms:GenerateDataKey*",
        "kms:ReEncrypt*"
      ],
      "Resource": "arn:aws:kms:ap-southeast-1:$ACCOUNT_ID:key/$KEY_ID",
      "Effect": "Allow"
    },
    {
      "Action": "sts:AssumeRole",
      "Resource": [
        "arn:aws:iam::$ACCOUNT_ID:role/CodePipelineChatbotBuildC-9DSS5JG1VE7T",
        "arn:aws:iam::$ACCOUNT_ID:role/CodePipelineChatbotEcsCod-AO6ZDE82ELPC",
        "arn:aws:iam::$ACCOUNT_ID:role/CodePipelineChatbotSource-1SZLHE9CFAAXO"
      ],
      "Effect": "Allow"
    }
  ]
}

CDK Deploy

Let create a CDK app in bin/ecs-blue-green-app.ts as below.

import * as cdk from "aws-cdk-lib";
import { EcrStack } from "../lib/ecr-stack";
import { AlbStack } from "../lib/alb-stack";
import { EcsClusterStack } from "../lib/ecs-cluster-stack";
import { EcsDeploymentGroup, EcsServiceStack } from "../lib/ecs-service-stack";
import { CodePipelineStack } from "../lib/codepipeline-stack";
import { NetworkStack } from "../lib/network-stack";

const app = new cdk.App();

// parameters
const REGION = process.env.CDK_DEFAULT_REGION;
const ACCOUNT = process.env.CDK_DEFAULT_ACCOUNT;
const CIDR = "10.0.0.0/16";
const ECR_REPO_NAME = "go-blue-green-app";
const CODE_COMMIT_REPO_NAME = "go-blue-green-app";
const REPO_BRANCH = "main";
const APP_DIR = "go-web-app";

// create an ecr repository
const ecr = new EcrStack(app, "EcrStack", {
  repoName: ECR_REPO_NAME,
  env: {
    region: REGION,
    account: ACCOUNT,
  },
});

// create vpc
const network = new NetworkStack(app, "NetworkStackBlue", {
  cidr: CIDR,
  env: {
    region: REGION,
    account: ACCOUNT,
  },
});

// create application load balancer
const alb = new AlbStack(app, "AlbStackBlue", {
  vpc: network.vpc,
  env: {
    region: REGION,
    account: ACCOUNT,
  },
});

// create ecs cluster
const cluster = new EcsClusterStack(app, "EcsClusterStackBlue", {
  vpc: network.vpc,
  env: {
    region: REGION,
    account: ACCOUNT,
  },
});

// create ecs service
const service = new EcsServiceStack(app, "EcsServiceStackBlue", {
  cluster: cluster.cluster,
  ecrRepoName: ECR_REPO_NAME,
  alb: alb.alb,
  blueTargetGroup: alb.blueTargetGroup,
  env: {
    region: REGION,
    account: ACCOUNT,
  },
});

// deployment group
const deploymentGroup = new EcsDeploymentGroup(app, "DeploymentGroupStack", {
  service: service.service,
  blueTargetGroup: alb.blueTargetGroup,
  greenTargetGroup: alb.greenTargetGroup,
  listener: alb.prodListener,
  env: {
    region: REGION,
    account: ACCOUNT,
  },
});

// codpipeline blue green deployment
new CodePipelineStack(app, "CodePipelineStack", {
  codecommitRepoName: CODE_COMMIT_REPO_NAME,
  repoBranch: REPO_BRANCH,
  ecrRepoName: ECR_REPO_NAME,
  appDir: APP_DIR,
  service: service.service,
  deploymentGroup: deploymentGroup.deploymentGroup,
  env: {
    region: REGION,
    account: ACCOUNT,
  },
});

Script to deploy infrastructure using CDK

cdk bootstrap aws://<ACCOUNT_ID>/<REGION>
cdk --app 'npx ts-node --prefer-ts-exts bin/ecs-blue-green-app.ts' synth
cdk --app 'npx ts-node --prefer-ts-exts bin/ecs-blue-green-app.ts' deploy EcrStack
cdk --app 'npx ts-node --prefer-ts-exts bin/ecs-blue-green-app.ts' deploy NetworkStack
cdk --app 'npx ts-node --prefer-ts-exts bin/ecs-blue-green-app.ts' deploy AlbStack
cdk --app 'npx ts-node --prefer-ts-exts bin/ecs-blue-green-app.ts' deploy EcsClusterStack
cdk --app 'npx ts-node --prefer-ts-exts bin/ecs-blue-green-app.ts' deploy DeploymentGroupStack
cdk --app 'npx ts-node --prefer-ts-exts bin/ecs-blue-green-app.ts' deploy EcsServiceStack
cdk --app 'npx ts-node --prefer-ts-exts bin/ecs-blue-green-app.ts' deploy CodePipelineStack

CI/CD Blue/Green

After created a repository named go-blue-green-app with project structure as below

|--Dockerfile
|--go.mod
|--go.sum
|--main.go
|--index.html
|--taskdef.json
|--appspec.yaml

Let's update content of the taskdef.json

{
  "containerDefinitions": [
    {
      "name": "go-blue-green-app",
      "image": "<IMAGE1_NAME>",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "essential": true,
      "environment": [
        {
          "name": "ENV",
          "value": "DEPLOY"
        }
      ]
    }
  ],
  "family": "latest",
  "taskRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/<TASK_ROLE_NAME>",
  "executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/<EXECUTION_ROLE_NAME>",
  "networkMode": "awsvpc",
  "placementConstraints": [],
  "compatibilities": ["EC2", "FARGATE"],
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "2048",
  "memory": "4096",
  "runtimePlatform": {
    "cpuArchitecture": "X86_64",
    "operatingSystemFamily": "LINUX"
  }
}

and appspec.yaml

version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "go-blue-green-app"
          ContainerPort: 3000
        PlatformVersion: "LATEST"

Application

There is a build.py script to build Docker image and push to ecr repository

import os

# parameters
REGION = "us-west-2"
ACCOUNT = os.popen("aws sts get-caller-identity | jq -r '.Account'").read().strip()
APP_NAME = "go-blue-green-app"

# delete all docker images
os.system("sudo docker system prune -a")

# build go-blog-app image
os.system(f"sudo docker build -t {APP_NAME} . ")

#  aws ecr login
os.system(
    f"aws ecr get-login-password --region {REGION} | sudo docker login --username AWS --password-stdin {ACCOUNT}.dkr.ecr.{REGION}.amazonaws.com"
)

# get image id
IMAGE_ID = os.popen(f"sudo docker images -q {APP_NAME}:latest").read()

# tag {APP_NAME} image
os.system(
    f"sudo docker tag {IMAGE_ID.strip()} {ACCOUNT}.dkr.ecr.{REGION}.amazonaws.com/{APP_NAME}:latest"
)

# create ecr repository
os.system(
    f"aws ecr create-repository --registry-id {ACCOUNT} --repository-name {APP_NAME} --region {REGION}"
)

# push image to ecr
os.system(
    f"sudo docker push {ACCOUNT}.dkr.ecr.{REGION}.amazonaws.com/{APP_NAME}:latest"
)

# run locally to test
# os.system(f"sudo docker run -d -p 3001:3000 {APP_NAME}:latest")

ECS Exec

  • Launch task which support /bin/sh
  • Run task from CLI and enable exec command
  • Not supported from the console
  • Update task role

Here is task definition for busybox which support /bin/sh

taskdef.json
{
  "taskDefinitionArn": "arn:aws:ecs:<REGION>:<ACCOUNT_ID>:task-definition/busybox:2",
  "containerDefinitions": [
    {
      "name": "busybox",
      "image": "public.ecr.aws/docker/library/busybox:uclibc",
      "cpu": 0,
      "portMappings": [],
      "essential": true,
      "entryPoint": ["sh", "-c"],
      "command": [
        "/bin/sh -c \"while true; do /bin/date > /var/www/my-vol/date; sleep 1; done\""
      ],
      "environment": [],
      "environmentFiles": [],
      "mountPoints": [],
      "volumesFrom": [],
      "ulimits": [],
      "systemControls": []
    }
  ],
  "family": "busybox",
  "taskRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/EcsServiceStack-TaskDefinitionForWebTaskRole9A993B6-8N9ZDRJr2oa7",
  "executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/EcsServiceStack-RoleForEcsTaskToPullEcrChatbotImage-gV9XivFSTObj",
  "networkMode": "awsvpc",
  "revision": 2,
  "volumes": [],
  "status": "ACTIVE",
  "requiresAttributes": [
    {
      "name": "com.amazonaws.ecs.capability.task-iam-role"
    },
    {
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
    },
    {
      "name": "ecs.capability.task-eni"
    }
  ],
  "placementConstraints": [],
  "compatibilities": ["EC2", "FARGATE"],
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "3072",
  "runtimePlatform": {
    "cpuArchitecture": "X86_64",
    "operatingSystemFamily": "LINUX"
  },
  "registeredAt": "2024-04-24T05:25:31.112Z",
  "registeredBy": "arn:aws:sts::<ACCOUNT_ID>:assumed-role/WSParticipantRole/Participant",
  "tags": []
}

Let's update IAM policy for the task role so we can access via SSM (ECS Exec)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel"
      ],
      "Resource": "*"
    }
  ]
}

Exec into the container via CLI or AWS Toolkit Explorer (VSCode).

aws ecs describe-tasks \
    --cluster ecs-exec-demo-cluster \
    --region $AWS_REGION \
    --tasks ef6260ed8aab49cf926667ab0c52c313
aws ecs execute-command  \
    --region us-west-2\
    --cluster  EcsClusterStack-EcsClusterBlueGreen1FCAD080-GEfiXDOireob\
    --task a307d3eaca3e470db6f65807a32354d5\
    --container demo\
    --command "/bin/sh" \
    --interactive
aws ecs execute-command\
    --region us-west-2\
    --cluster  EcsClusterStack-EcsClusterBlueGreen1FCAD080-GEfiXDOireob\
    --task 0b48038d1d8f4d6589dee73bda8fd9d6\
    --container busybox\
    --command "/bin/sh" \
    --interactive
aws ecs run-task\
  --region us-west-2\
  --cluster EcsClusterStack-EcsClusterBlueGreen1FCAD080-GEfiXDOireob\
  --count 1\
  --enable-execute-command\
  --launch-type FARGATE\
  --network-configuration "awsvpcConfiguration={subnets=[subnet-09a914fbdf1120262],securityGroups=[	sg-04f2cbafa41127bb1],assignPublicIp=ENABLED}" \
  --task-definition "busybox:2"

then exec into the task

aws ecs execute-command\
    --region us-west-2\
    --cluster  EcsClusterStack-EcsClusterBlueGreen1FCAD080-GEfiXDOireob\
    --task 0b48038d1d8f4d6589dee73bda8fd9d6\
    --container busybox\
    --command "/bin/sh" \
    --interactive

Referece