author | title | date |
---|---|---|
haimtran |
developing on amazon ecs |
20/08/2023 |
GitHub this project shows how to build applications on Amazon ECS with Blue/Green deployment.
Codepipeline
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
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;
}
}
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;
}
}
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);
}
}
}
}
}
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,
}
);
}
}
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,
}
);
}
}
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"
}
]
}
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
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"
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")
- 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