Network Stack#

Let's create a network stack:

  • VPC with 2 public and 2 private subnets
  • Security groups for alb and fargate
  • NAT gateway
AWSTemplateFormatVersion: '2010-09-09'
#------------------------------------------------------
# Mappings
#------------------------------------------------------
Mappings:
CidrMappings:
public-subnet-1:
CIDR: 10.0.0.0/24
public-subnet-2:
CIDR: 10.0.2.0/24
private-subnet-1:
CIDR: 10.0.1.0/24
private-subnet-2:
CIDR: 10.0.3.0/24
#------------------------------------------------------
# Parameters
#------------------------------------------------------
Parameters:
CidrBlock:
Type: String
Description: CidrBlock
Default: 10.0.0.0/16
InternetCidrBlock:
Type: String
Description: UserCidrBlock
Default: 0.0.0.0/0
#------------------------------------------------------
# Resources: VPC, Subnets, NAT, Routes
#------------------------------------------------------
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref CidrBlock
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-vpc
#------------------------------------------------------
# Resources: internet gateway
#------------------------------------------------------
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-ig
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
#------------------------------------------------------
# Resources: public and private subnets
#------------------------------------------------------
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
MapPublicIpOnLaunch: false
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs:
Ref: AWS::Region
VpcId: !Ref VPC
CidrBlock:
Fn::FindInMap:
- CidrMappings
- public-subnet-1
- CIDR
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet-1
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
MapPublicIpOnLaunch: false
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs:
Ref: AWS::Region
VpcId: !Ref VPC
CidrBlock:
Fn::FindInMap:
- CidrMappings
- public-subnet-2
- CIDR
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-subnet-2
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
MapPublicIpOnLaunch: false
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs:
Ref: AWS::Region
VpcId: !Ref VPC
CidrBlock:
Fn::FindInMap:
- CidrMappings
- private-subnet-1
- CIDR
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-subnet-1
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
MapPublicIpOnLaunch: false
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs:
Ref: AWS::Region
VpcId: !Ref VPC
CidrBlock:
Fn::FindInMap:
- CidrMappings
- private-subnet-2
- CIDR
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-subnet-2
#------------------------------------------------------
# Resources: nat gateway
#------------------------------------------------------
NatGatewayEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt: [NatGatewayEIP, AllocationId]
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-nat-gateway
#------------------------------------------------------
# Resources: public route table
#------------------------------------------------------
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-public-rt
RouteInternetGateway:
Type: AWS::EC2::Route
DependsOn: AttachGateway
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: !Ref InternetCidrBlock
GatewayId: !Ref InternetGateway
#------------------------------------------------------
# Resources: private route table
#------------------------------------------------------
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: !Sub ${AWS::StackName}-private-rt
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: !Ref InternetCidrBlock
NatGatewayId: !Ref NatGateway
#------------------------------------------------------
# Resources: routeable subnet associations
#------------------------------------------------------
PublicSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
PrivateSubnet1RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable
PrivateSubnet2RouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable
#------------------------------------------------------
# Security Group:
#------------------------------------------------------
ECSFargateSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Communication between the control plane and worker nodegroups
VpcId: !Ref VPC
GroupName: !Sub ${AWS::StackName}-ecs-fargate-sg
ECSFargateSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref ECSFargateSecurityGroup
IpProtocol: -1
SourceSecurityGroupId: !Ref ECSFargateSecurityGroup
SourceSecurityGroupOwnerId: !Ref AWS::AccountId
ECSFargateSecurityGroupIngressALB:
Type: AWS::EC2::SecurityGroupIngress
Properties:
IpProtocol: tcp
FromPort: 3000
ToPort: 3000
SourceSecurityGroupId: !Ref ALBSecurityGroup
GroupId: !Ref ECSFargateSecurityGroup
ALBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ALB Security Group
VpcId: !Ref VPC
GroupName: !Sub ${AWS::StackName}-alb-sg
ALBSecurityGroupIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: !Ref InternetCidrBlock
GroupId: !Ref ALBSecurityGroup
#------------------------------------------------------
# Export:
#------------------------------------------------------
Outputs:
VPC:
Value: !Ref VPC
Export:
Name: !Sub ${AWS::StackName}-vpc
PublicSubnet1:
Value: !Ref PublicSubnet1
Export:
Name: !Sub ${AWS::StackName}-public-subnet-1
PublicSubnet2:
Value: !Ref PublicSubnet2
Export:
Name: !Sub ${AWS::StackName}-public-subnet-2
PrivateSubnet1:
Value: !Ref PrivateSubnet2
Export:
Name: !Sub ${AWS::StackName}-private-subnet-1
PrivateSubnet2:
Value: !Ref PrivateSubnet2
Export:
Name: !Sub ${AWS::StackName}-private-subnet-2
PrivateRouteTable:
Value: !Ref PrivateRouteTable
Export:
Name: !Sub ${AWS::StackName}-private-route-table
InternetGateway:
Value: !Ref InternetGateway
Export:
Name: !Sub ${AWS::StackName}-igw
ECSFargateSecurityGroup:
Value: !Ref ECSFargateSecurityGroup
Export:
Name: !Sub ${AWS::StackName}-ecs-fargate-sg
ALBSecurityGroup:
Value: !Ref ALBSecurityGroup
Export:
Name: !Sub ${AWS::StackName}-alb-sg

Load Balancer#

Let's create a load balancer stack:

  • Application load balancer
  • Blue and green target group
  • Listener on port 80
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
NetworkStackName:
Description: Stack name of the network stack
Type: String
Default: cfn-network
Resources:
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Subnets:
- Fn::ImportValue: !Sub ${NetworkStackName}-public-subnet-1
- Fn::ImportValue: !Sub ${NetworkStackName}-public-subnet-2
SecurityGroups:
- Fn::ImportValue: !Sub ${NetworkStackName}-alb-sg
TargetGroupBlue:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId:
Fn::ImportValue: !Sub ${NetworkStackName}-vpc
Port: 80
Protocol: HTTP
TargetType: ip
Matcher:
HttpCode: 200-299
HealthCheckIntervalSeconds: 10
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
TargetGroupGreen:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId:
Fn::ImportValue: !Sub ${NetworkStackName}-vpc
Port: 80
Protocol: HTTP
TargetType: ip
Matcher:
HttpCode: 200-299
HealthCheckIntervalSeconds: 10
HealthCheckPath: /
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 2
Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 81
Protocol: HTTP
DefaultActions:
- Type: forward
TargetGroupArn: !Ref TargetGroupBlue
# Export output
Outputs:
LoadBalancer:
Description: Load balancer
Value: !Ref LoadBalancer
Export:
Name: !Sub ${AWS::StackName}-load-balancer
TargetGroupBlue:
Description: Target group blue
Value: !Ref TargetGroupBlue
Export:
Name: !Sub ${AWS::StackName}-target-group-blue
TargetGroupGreen:
Description: Target group green
Value: !Ref TargetGroupGreen
Export:
Name: !Sub ${AWS::StackName}-target-group-green
TargetGroupBlueName:
Description: Target group name blue
Value: !GetAtt TargetGroupBlue.TargetGroupName
Export:
Name: !Sub ${AWS::StackName}-target-group-blue-name
TargetGroupGreenName:
Description: Target group name green
Value: !GetAtt TargetGroupGreen.TargetGroupName
Export:
Name: !Sub ${AWS::StackName}-target-group-green-name
ListenerArns:
Description: Listener ARNs
Value: !GetAtt Listener.ListenerArn
Export:
Name: !Sub ${AWS::StackName}-alb-listener-arn

CodeCommit and ECR#

Let's create a CodeCommit repository and a ECR repository. We need to build a container image from a local machine and push to this ECR repository before using a CI/CD piepline.

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
CodeCommitRepoName:
Type: String
Description: The name of the CodeCommit repository to create.
Default: ecs-blue-green
ECRRepoName:
Type: String
Description: The name of the Elastic Container Registry to create.
Default: go-app
CodePipelineS3Bucket:
Type: String
Description: The name of the S3 bucket to store the deployment code in
Default: codepipeline-us-west-2-27062024
Resources:
# A CodeCommit repository to store your code
CodeCommitRepo:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: !Ref CodeCommitRepoName
# An Elastic Container Registry to store Docker images
ECRRepo:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Ref ECRRepoName
# S3 bucket for storing templates and pipeline artifacts
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref CodePipelineS3Bucket

ECS Cluster#

Let's create an ecs cluster:

  • Fargate capacity provider
  • Enable logs
AWSTemplateFormatVersion: '2010-09-09'
# Parameters
Parameters:
ClusterName:
Type: String
Description: 'ImageId to be used to create an EC2 instance.'
Default: 'demo'
# Create an ecs cluster
Resources:
ecscluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Ref ClusterName
ClusterSettings:
- Name: containerInsights
Value: enabled
CapacityProviders:
- FARGATE
- FARGATE_SPOT
DefaultCapacityProviderStrategy:
- CapacityProvider: FARGATE
Weight: 4
Base: 1
- CapacityProvider: FARGATE_SPOT
Weight: 1
Base: 0
# Export
Outputs:
ecscluster:
Value: !Ref ecscluster
Export:
Name: !Sub '${AWS::StackName}-ecscluster'

ECS Task#

Let's create a task definition:

  • Allocate cpu and memory
  • Specify container definition
AWSTemplateFormatVersion: '2010-09-09'
Description: ECS Task Definition
Parameters:
EcrRepoName:
Type: String
Description: Name of the ecr repo
Default: go-app
Resources:
TaskRoleBook:
Type: AWS::IAM::Role
Properties:
RoleName: 'ECSTaskRoleForBook'
AssumeRolePolicyDocument:
Statement:
- Action:
- 'sts:AssumeRole'
Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
Policies:
- PolicyName: root
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'ssmmessages:CreateControlChannel'
- 'ssmmessages:CreateDataChannel'
- 'ssmmessages:OpenControlChannel'
- 'ssmmessages:OpenDataChannel'
Resource: '*'
ManagedPolicyArns:
- 'arn:aws:iam::aws:policy/AmazonS3FullAccess'
TaskExecutionRoleBook:
Type: AWS::IAM::Role
Properties:
RoleName: 'ECSTaskExecutionRoleForBook'
AssumeRolePolicyDocument:
Statement:
- Action:
- 'sts:AssumeRole'
Effect: Allow
Principal:
Service:
- ecs-tasks.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
BookTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref EcrRepoName
TaskRoleArn: !GetAtt TaskRoleBook.Arn
ExecutionRoleArn: !GetAtt TaskExecutionRoleBook.Arn
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
Cpu: 256
Memory: 512
ContainerDefinitions:
- Name: book-container
Image: !Sub
- '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepoName}:latest'
- EcrRepoName: !Ref EcrRepoName
PortMappings:
- ContainerPort: 3000
Privileged: false
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref EcrRepoName
LogGroup:
Type: AWS::Logs::LogGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
LogGroupName: !Sub
- '/ecs/${EcrRepoName}'
- EcrRepoName: !Ref EcrRepoName
RetentionInDays: 7
# Export output
Outputs:
taskDefinition:
Description: Task Definition
Value: !Ref BookTaskDefinition
Export:
Name: !Sub '${AWS::StackName}-book-task-def'

ECS Service#

Let's create a service and expose it via the load balancer created above.

AWSTemplateFormatVersion: '2010-09-09'
Description: ECS Service
Parameters:
NetworkStackName:
Type: String
Description: Name of the network stack
Default: 'cfn-network'
ClusterStackName:
Type: String
Description: Name of the ECS cluster stack to create the service
Default: 'cfn-ecs-cluster'
ALBStackName:
Type: String
Description: Name of the ALB stack to associate with the service
Default: 'cfn-alb-blue-green'
TaskDefinitionStackName:
Type: String
Description: Name of the task definition stack
Default: 'cfn-task-def-book'
DesiredCount:
Type: Number
Description: Desired number of tasks
Default: 1
ContainerName:
Type: String
Description: Name of the container
Default: 'book-container'
ServiceName:
Type: String
Description: Name of the service
Default: 'book-service-blue-green'
Resources:
ECSService:
Type: AWS::ECS::Service
Properties:
ServiceName: !Ref ServiceName
TaskDefinition:
Fn::ImportValue: !Sub ${TaskDefinitionStackName}-book-task-def
Cluster:
Fn::ImportValue: !Sub ${ClusterStackName}-ecscluster
LaunchType: FARGATE
DesiredCount: !Ref DesiredCount
DeploymentController:
Type: CODE_DEPLOY
NetworkConfiguration:
AwsvpcConfiguration:
# AssignPublicIp: ENABLED
SecurityGroups:
- Fn::ImportValue: !Sub ${NetworkStackName}-ecs-fargate-sg
Subnets:
- Fn::ImportValue: !Sub ${NetworkStackName}-private-subnet-1
- Fn::ImportValue: !Sub ${NetworkStackName}-private-subnet-2
LoadBalancers:
- ContainerName: !Ref ContainerName
ContainerPort: 3000
TargetGroupArn:
Fn::ImportValue: !Sub ${ALBStackName}-target-group-blue
HealthCheckGracePeriodSeconds: 60
ServiceScalingTarget:
Type: AWS::ApplicationAutoScaling::ScalableTarget
Properties:
MaxCapacity: 4
MinCapacity: 1
ResourceId:
Fn::Join:
- /
- - service
- Fn::ImportValue: !Sub ${ClusterStackName}-ecscluster
- !Ref ServiceName
RoleARN: !GetAtt AutoScalingRole.Arn
ScalableDimension: ecs:service:DesiredCount
ServiceNamespace: ecs
ServiceScalingPolicy:
Type: AWS::ApplicationAutoScaling::ScalingPolicy
Properties:
PolicyName: AverageCPUUtilizationPolicy
PolicyType: TargetTrackingScaling
ScalingTargetId: !Ref ServiceScalingTarget
TargetTrackingScalingPolicyConfiguration:
TargetValue: 80.0
ScaleInCooldown: 60
ScaleOutCooldown: 60
PredefinedMetricSpecification:
PredefinedMetricType: ECSServiceAverageCPUUtilization
AutoScalingRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs.application-autoscaling.amazonaws.com
Action: 'sts:AssumeRole'
Policies:
- PolicyName: MyAWSApplicationAutoscalingECSServicePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ecs:DescribeServices
- ecs:UpdateService
- cloudwatch:PutMetricAlarm
- cloudwatch:PutMetricAlarm
- cloudwatch:PutMetricAlarm
Resource: '*'

CI/CD Pipeline#

Let's create a CI/CD pipeline:

  • CodeCommit source
  • ECR respository
  • CodeBuild project
  • CodeDeploy application and deployment
  • S3 artifact bucket already created from the codecommit stack above
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
AlbStackName:
Type: String
Description: The name of the ALB stack
Default: cfn-alb-blue-green
CodeBuildProjectName:
Type: String
Description: CodeBuild project name
Default: code-build-blue-green
CodePipelineS3Bucket:
Type: String
Description: The name of the S3 bucket to store the deployment code in
Default: codepipeline-us-west-2-27062024
EcrRepoName:
Type: String
Description: The name of the ECR repository to store the deployment image in
Default: go-app
EcrArtifactOutput:
Type: String
Description: The name of the ECR artifact output
Default: ecr-artifact
CodeCommitArtifactOutput:
Type: String
Description: The name of the CodeBuild artifact output
Default: codecommit-artifact
CodeBuildArtifactOutput:
Type: String
Description: The name of the CodeBuild artifact output
Default: codebuild-artifact
CodeDeployAppName:
Type: String
Description: The name of the CodeDeploy application
Default: blue-green-deploy-app
DeploymentGroupName:
Type: String
Description: The name of the Deployment Group
Default: blue-green-deploy-group
EcsClusterName:
Type: String
Description: The name of the ECS cluster
Default: demo
ServiceName:
Type: String
Description: The name of the ECS service
Default: book-service-blue-green
CodeCommitRepoName:
Type: String
Description: The name of the CodeCommit repository to use
Default: ecs-blue-green
CodePipelineName:
Type: String
Description: The name of the CodePipeline
Default: pipeline-blue-green
Resources:
# IAM role for codepipeline
RoleForCodePipeline:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Policies:
- PolicyName: !Sub ${AWS::StackName}-CodePipelinePolicyBlueGreen
PolicyDocument: |
{
"Statement": [
{
"Action": [
"iam:PassRole"
],
"Resource": "*",
"Effect": "Allow",
"Condition": {
"StringEqualsIfExists": {
"iam:PassedToService": [
"cloudformation.amazonaws.com",
"elasticbeanstalk.amazonaws.com",
"ec2.amazonaws.com",
"ecs-tasks.amazonaws.com"
]
}
}
},
{
"Action": [
"codecommit:CancelUploadArchive",
"codecommit:GetBranch",
"codecommit:GetCommit",
"codecommit:GetRepository",
"codecommit:GetUploadArchiveStatus",
"codecommit:UploadArchive"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": ["ecr:*"],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"codedeploy:CreateDeployment",
"codedeploy:GetApplication",
"codedeploy:GetApplicationRevision",
"codedeploy:GetDeployment",
"codedeploy:GetDeploymentConfig",
"codedeploy:RegisterApplicationRevision"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"codestar-connections:UseConnection"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"elasticbeanstalk:*",
"ec2:*",
"elasticloadbalancing:*",
"autoscaling:*",
"cloudwatch:*",
"s3:*",
"sns:*",
"cloudformation:*",
"rds:*",
"sqs:*",
"ecs:*"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"lambda:InvokeFunction",
"lambda:ListFunctions"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"opsworks:CreateDeployment",
"opsworks:DescribeApps",
"opsworks:DescribeCommands",
"opsworks:DescribeDeployments",
"opsworks:DescribeInstances",
"opsworks:DescribeStacks",
"opsworks:UpdateApp",
"opsworks:UpdateStack"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"cloudformation:CreateStack",
"cloudformation:DeleteStack",
"cloudformation:DescribeStacks",
"cloudformation:UpdateStack",
"cloudformation:CreateChangeSet",
"cloudformation:DeleteChangeSet",
"cloudformation:DescribeChangeSet",
"cloudformation:ExecuteChangeSet",
"cloudformation:SetStackPolicy",
"cloudformation:ValidateTemplate"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"codebuild:BatchGetBuilds",
"codebuild:StartBuild",
"codebuild:BatchGetBuildBatches",
"codebuild:StartBuildBatch"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Effect": "Allow",
"Action": [
"devicefarm:ListProjects",
"devicefarm:ListDevicePools",
"devicefarm:GetRun",
"devicefarm:GetUpload",
"devicefarm:CreateUpload",
"devicefarm:ScheduleRun"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"servicecatalog:ListProvisioningArtifacts",
"servicecatalog:CreateProvisioningArtifact",
"servicecatalog:DescribeProvisioningArtifact",
"servicecatalog:DeleteProvisioningArtifact",
"servicecatalog:UpdateProduct"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"cloudformation:ValidateTemplate"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"ecr:DescribeImages"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"states:DescribeExecution",
"states:DescribeStateMachine",
"states:StartExecution"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"appconfig:StartDeployment",
"appconfig:StopDeployment",
"appconfig:GetDeployment"
],
"Resource": "*"
}
],
"Version": "2012-10-17"
}
# IAM role for codebuild
RoleForCodeBuild:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Policies:
- PolicyName: !Sub ${AWS::StackName}-CodeBuildPolicyBlueGreen
PolicyDocument: !Sub
- |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Resource": [
"arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/debug",
"arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/debug:*"
],
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
},
{
"Effect": "Allow",
"Resource": [
"arn:${AWS::Partition}:s3:::${CodePipelineS3Bucket}/*"
],
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetBucketAcl",
"s3:GetBucketLocation"
]
},
{
"Effect": "Allow",
"Resource": [
"arn:${AWS::Partition}:s3:::${CodePipelineS3Bucket}/*"
],
"Action": [
"s3:*"
]
},
{
"Effect": "Allow",
"Action": [
"codebuild:CreateReportGroup",
"codebuild:CreateReport",
"codebuild:UpdateReport",
"codebuild:BatchPutTestCases",
"codebuild:BatchPutCodeCoverages"
],
"Resource": [
"arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/debug-*"
]
},
{
"Action": ["ecr:*"],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"codecommit:CancelUploadArchive",
"codecommit:GetBranch",
"codecommit:GetCommit",
"codecommit:GetRepository",
"codecommit:GetUploadArchiveStatus",
"codecommit:UploadArchive"
],
"Resource": "*",
"Effect": "Allow"
}
]
}
- CodePipelineS3Bucket: !Ref CodePipelineS3Bucket
# IAM role for codedeploy
RoleForCodeDeploy:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codedeploy.amazonaws.com
Policies:
- PolicyName: !Sub ${AWS::StackName}-CodeDeployPolicyBlueGreen
PolicyDocument: |
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"ecs:DescribeServices",
"ecs:CreateTaskSet",
"ecs:UpdateServicePrimaryTaskSet",
"ecs:DeleteTaskSet",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:ModifyListener",
"elasticloadbalancing:DescribeRules",
"elasticloadbalancing:ModifyRule",
"lambda:InvokeFunction",
"cloudwatch:DescribeAlarms",
"sns:Publish",
"s3:GetObject",
"s3:GetObjectVersion"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"iam:PassRole"
],
"Effect": "Allow",
"Resource": "*",
"Condition": {
"StringLike": {
"iam:PassedToService": [
"ecs-tasks.amazonaws.com"
]
}
}
}
]
}
# Codebuild project
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Ref CodeBuildProjectName
ServiceRole: !Ref RoleForCodeBuild
Artifacts:
Type: S3
Packaging: NONE
Location: !Ref CodePipelineS3Bucket
Environment:
Type: LINUX_CONTAINER
Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
ComputeType: BUILD_GENERAL1_SMALL
PrivilegedMode: false
LogsConfig:
CloudWatchLogs:
Status: ENABLED
GroupName: /aws/codebuild/debug
Source:
Type: CODECOMMIT
Location: !Sub
- https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/${EcrRepoName}
- EcrRepoName: !Ref EcrRepoName
BuildSpec: !Sub
- |
version: 0.2
env:
shell: bash
variables:
REPO_NAME: ${EcrRepoName}
CODEBUILD_RESOLVED_SOURCE_VERSION: $CODEBUILD_RESOLVED_SOURCE_VERSION
TAG_NAME: "latest"
phases:
pre_build:
commands:
- export TAG_NAME=$(date +%s)
build:
commands:
- docker build -t $REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION-$TAG_NAME .
- docker tag $REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION-$TAG_NAME ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/$REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION-$TAG_NAME
- printf '{"ImageURI":"%s"}' ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepoName}:$REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION-$TAG_NAME > imageDetail.json
- cat imageDetail.json
- cat taskdef.json
- cat appspec.yaml
post_build:
commands:
- aws ecr get-login-password --region ${AWS::Region} | docker login --username AWS --password-stdin ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com
- docker push ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/$REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION-$TAG_NAME
artifacts:
files:
- imageDetail.json
- taskdef.json
- appspec.yaml
name: ${CodeBuildArtifactOutput}
- CodeBuildArtifactOutput: !Ref CodeBuildArtifactOutput
EcrRepoName: !Ref EcrRepoName
Cache:
Type: NO_CACHE
Tags:
- Key: name
Value: !Ref CodeBuildProjectName
# CodeDeploy Application
CodeDeployApplication:
Type: AWS::CodeDeploy::Application
Properties:
ApplicationName: !Ref CodeDeployAppName
ComputePlatform: ECS
# Deploy DeploymentGroup
Deploy:
Type: AWS::CodeDeploy::DeploymentGroup
Properties:
ApplicationName: !Ref CodeDeployAppName
ServiceRoleArn: !GetAtt [RoleForCodeDeploy, Arn]
AlarmConfiguration:
Enabled: false
AutoRollbackConfiguration:
Enabled: false
DeploymentConfigName: CodeDeployDefault.ECSAllAtOnce
DeploymentGroupName: !Ref DeploymentGroupName
DeploymentStyle:
DeploymentOption: WITH_TRAFFIC_CONTROL
DeploymentType: BLUE_GREEN
BlueGreenDeploymentConfiguration:
DeploymentReadyOption:
ActionOnTimeout: CONTINUE_DEPLOYMENT
WaitTimeInMinutes: 0
TerminateBlueInstancesOnDeploymentSuccess:
Action: TERMINATE
TerminationWaitTimeInMinutes: 5
ECSServices:
- ServiceName: !Ref ServiceName
ClusterName: !Ref EcsClusterName
LoadBalancerInfo:
TargetGroupPairInfoList:
- ProdTrafficRoute:
ListenerArns:
- !ImportValue
Fn::Sub: ${AlbStackName}-alb-listener-arn
TargetGroups:
- Name: !ImportValue
Fn::Sub: ${AlbStackName}-target-group-blue-name
- Name: !ImportValue
Fn::Sub: ${AlbStackName}-target-group-green-name
# CodePipeline
CodePipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Ref CodePipelineName
# ExecutionMode: SUPERSEDED
RoleArn:
Fn::GetAtt:
- RoleForCodePipeline
- Arn
ArtifactStore:
Type: S3
Location: !Ref CodePipelineS3Bucket
Stages:
- Name: Source
Actions:
- Name: ECRSource
ActionTypeId:
Owner: AWS
Category: Source
Version: '1'
Provider: ECR
Configuration:
RepositoryName: !Ref EcrRepoName
ImageTag: latest
OutputArtifacts:
- Name: !Ref EcrArtifactOutput
- Name: CodeCommitSource
ActionTypeId:
Owner: AWS
Category: Source
Version: '1'
Provider: CodeCommit
Configuration:
RepositoryName: !Ref CodeCommitRepoName
BranchName: main
OutputArtifacts:
- Name: !Ref CodeCommitArtifactOutput
- Name: Build
Actions:
- Name: CodeBuild
ActionTypeId:
Owner: AWS
Category: Build
Version: '1'
Provider: CodeBuild
RunOrder: 1
Configuration:
ProjectName: !Ref CodeBuildProject
PrimarySource: !Ref CodeCommitArtifactOutput
InputArtifacts:
- Name: !Ref CodeCommitArtifactOutput
OutputArtifacts:
- Name: !Ref CodeBuildArtifactOutput
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Owner: AWS
Category: Deploy
Version: '1'
Provider: CodeDeployToECS
Configuration:
AppSpecTemplateArtifact: !Ref CodeCommitArtifactOutput
ApplicationName: !Ref CodeDeployAppName
DeploymentGroupName: !Ref DeploymentGroupName
Image1ArtifactName: !Ref EcrArtifactOutput
Image1ContainerName: IMAGE1_NAME
TaskDefinitionTemplatePath: taskdef.json
AppSpecTemplatePath: appspec.yaml
TaskDefinitionTemplateArtifact: !Ref CodeCommitArtifactOutput
InputArtifacts:
- Name: !Ref CodeCommitArtifactOutput
- Name: !Ref EcrArtifactOutput

Blue Green Deployment#

We need to provide some additional files.

taskdef.json

{
"containerDefinitions": [
{
"name": "book-container",
"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/ECSTaskRoleForBook",
"executionRoleArn": "arn:aws:iam::<ACCOUNT_ID>:role/ECSTaskExecutionRoleForBook",
"networkMode": "awsvpc",
"placementConstraints": [],
"compatibilities": ["EC2", "FARGATE"],
"requiresCompatibilities": ["FARGATE"],
"cpu": "2048",
"memory": "4096",
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
}
}

appspec.yaml

version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: 'book-container'
ContainerPort: 3000
PlatformVersion: 'LATEST'

imageDetail.json

{
"ImageURI": "<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/dk-image-repo@sha256:example3"
}

imagedefinitions.json

[
{
"name": "book-container",
"imageUri": "<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/go-app:latest"
}
]

References#