Network Stack#
Let's create: vpc, private subnets, public subnets, nat, security group for alb and for ecs fargate.
AWSTemplateFormatVersion: '2010-09-09'#------------------------------------------------------# Mappings#------------------------------------------------------Mappings:CidrMappings:public-subnet-1:CIDR: 10.0.0.0/24public-subnet-2:CIDR: 10.0.2.0/24private-subnet-1:CIDR: 10.0.1.0/24private-subnet-2:CIDR: 10.0.3.0/24#------------------------------------------------------# Parameters#------------------------------------------------------Parameters:CidrBlock:Type: StringDescription: CidrBlockDefault: 10.0.0.0/16InternetCidrBlock:Type: StringDescription: UserCidrBlockDefault: 0.0.0.0/0#------------------------------------------------------# Resources: VPC, Subnets, NAT, Routes#------------------------------------------------------Resources:VPC:Type: AWS::EC2::VPCProperties:CidrBlock: !Ref CidrBlockEnableDnsSupport: trueEnableDnsHostnames: trueTags:- Key: NameValue: !Sub ${AWS::StackName}-vpc#------------------------------------------------------# Resources: internet gateway#------------------------------------------------------InternetGateway:Type: AWS::EC2::InternetGatewayProperties:Tags:- Key: NameValue: !Sub ${AWS::StackName}-igAttachGateway:Type: AWS::EC2::VPCGatewayAttachmentProperties:VpcId: !Ref VPCInternetGatewayId: !Ref InternetGateway#------------------------------------------------------# Resources: public and private subnets#------------------------------------------------------PublicSubnet1:Type: AWS::EC2::SubnetProperties:MapPublicIpOnLaunch: falseAvailabilityZone:Fn::Select:- 0- Fn::GetAZs:Ref: AWS::RegionVpcId: !Ref VPCCidrBlock:Fn::FindInMap:- CidrMappings- public-subnet-1- CIDRTags:- Key: NameValue: !Sub ${AWS::StackName}-public-subnet-1PublicSubnet2:Type: AWS::EC2::SubnetProperties:MapPublicIpOnLaunch: falseAvailabilityZone:Fn::Select:- 1- Fn::GetAZs:Ref: AWS::RegionVpcId: !Ref VPCCidrBlock:Fn::FindInMap:- CidrMappings- public-subnet-2- CIDRTags:- Key: NameValue: !Sub ${AWS::StackName}-public-subnet-2PrivateSubnet1:Type: AWS::EC2::SubnetProperties:MapPublicIpOnLaunch: falseAvailabilityZone:Fn::Select:- 0- Fn::GetAZs:Ref: AWS::RegionVpcId: !Ref VPCCidrBlock:Fn::FindInMap:- CidrMappings- private-subnet-1- CIDRTags:- Key: NameValue: !Sub ${AWS::StackName}-private-subnet-1PrivateSubnet2:Type: AWS::EC2::SubnetProperties:MapPublicIpOnLaunch: falseAvailabilityZone:Fn::Select:- 1- Fn::GetAZs:Ref: AWS::RegionVpcId: !Ref VPCCidrBlock:Fn::FindInMap:- CidrMappings- private-subnet-2- CIDRTags:- Key: NameValue: !Sub ${AWS::StackName}-private-subnet-2#------------------------------------------------------# Resources: nat gateway#------------------------------------------------------NatGatewayEIP:Type: AWS::EC2::EIPProperties:Domain: vpcNatGateway:Type: AWS::EC2::NatGatewayProperties:AllocationId:Fn::GetAtt: [NatGatewayEIP, AllocationId]SubnetId: !Ref PublicSubnet1Tags:- Key: NameValue: !Sub ${AWS::StackName}-nat-gateway#------------------------------------------------------# Resources: public route table#------------------------------------------------------PublicRouteTable:Type: AWS::EC2::RouteTableProperties:VpcId: !Ref VPCTags:- Key: NameValue: !Sub ${AWS::StackName}-public-rtRouteInternetGateway:Type: AWS::EC2::RouteDependsOn: AttachGatewayProperties:RouteTableId: !Ref PublicRouteTableDestinationCidrBlock: !Ref InternetCidrBlockGatewayId: !Ref InternetGateway#------------------------------------------------------# Resources: private route table#------------------------------------------------------PrivateRouteTable:Type: AWS::EC2::RouteTableProperties:VpcId: !Ref VPCTags:- Key: NameValue: !Sub ${AWS::StackName}-private-rtPrivateRoute:Type: AWS::EC2::RouteProperties:RouteTableId: !Ref PrivateRouteTableDestinationCidrBlock: !Ref InternetCidrBlockNatGatewayId: !Ref NatGateway#------------------------------------------------------# Resources: routeable subnet associations#------------------------------------------------------PublicSubnet1RouteTableAssociation:Type: AWS::EC2::SubnetRouteTableAssociationProperties:SubnetId: !Ref PublicSubnet1RouteTableId: !Ref PublicRouteTablePublicSubnet2RouteTableAssociation:Type: AWS::EC2::SubnetRouteTableAssociationProperties:SubnetId: !Ref PublicSubnet2RouteTableId: !Ref PublicRouteTablePrivateSubnet1RouteTableAssociation:Type: AWS::EC2::SubnetRouteTableAssociationProperties:SubnetId: !Ref PrivateSubnet1RouteTableId: !Ref PrivateRouteTablePrivateSubnet2RouteTableAssociation:Type: AWS::EC2::SubnetRouteTableAssociationProperties:SubnetId: !Ref PrivateSubnet2RouteTableId: !Ref PrivateRouteTable#------------------------------------------------------# Security Group:#------------------------------------------------------ECSFargateSecurityGroup:Type: AWS::EC2::SecurityGroupProperties:GroupDescription: Communication between the control plane and worker nodegroupsVpcId: !Ref VPCGroupName: !Sub ${AWS::StackName}-ecs-fargate-sgECSFargateSecurityGroupIngress:Type: AWS::EC2::SecurityGroupIngressProperties:GroupId: !Ref ECSFargateSecurityGroupIpProtocol: -1SourceSecurityGroupId: !Ref ECSFargateSecurityGroupSourceSecurityGroupOwnerId: !Ref AWS::AccountIdECSFargateSecurityGroupIngressALB:Type: AWS::EC2::SecurityGroupIngressProperties:IpProtocol: tcpFromPort: 3000ToPort: 3000SourceSecurityGroupId: !Ref ALBSecurityGroupGroupId: !Ref ECSFargateSecurityGroupALBSecurityGroup:Type: AWS::EC2::SecurityGroupProperties:GroupDescription: ALB Security GroupVpcId: !Ref VPCGroupName: !Sub ${AWS::StackName}-alb-sgALBSecurityGroupIngress:Type: AWS::EC2::SecurityGroupIngressProperties:IpProtocol: tcpFromPort: 80ToPort: 80CidrIp: !Ref InternetCidrBlockGroupId: !Ref ALBSecurityGroup#------------------------------------------------------# Export:#------------------------------------------------------Outputs:VPC:Value: !Ref VPCExport:Name: !Sub ${AWS::StackName}-vpcPublicSubnet1:Value: !Ref PublicSubnet1Export:Name: !Sub ${AWS::StackName}-public-subnet-1PublicSubnet2:Value: !Ref PublicSubnet2Export:Name: !Sub ${AWS::StackName}-public-subnet-2PrivateSubnet1:Value: !Ref PrivateSubnet2Export:Name: !Sub ${AWS::StackName}-private-subnet-1PrivateSubnet2:Value: !Ref PrivateSubnet2Export:Name: !Sub ${AWS::StackName}-private-subnet-2PrivateRouteTable:Value: !Ref PrivateRouteTableExport:Name: !Sub ${AWS::StackName}-private-route-tableInternetGateway:Value: !Ref InternetGatewayExport:Name: !Sub ${AWS::StackName}-igwECSFargateSecurityGroup:Value: !Ref ECSFargateSecurityGroupExport:Name: !Sub ${AWS::StackName}-ecs-fargate-sgALBSecurityGroup:Value: !Ref ALBSecurityGroupExport:Name: !Sub ${AWS::StackName}-alb-sg
Load Balancer#
Next let's create a ALB with a listener on port 80 and a target group.
AWSTemplateFormatVersion: '2010-09-09'Parameters:NetworkStackName:Description: Stack name of the network stackType: StringDefault: cfn-networkResources:LoadBalancer:Type: AWS::ElasticLoadBalancingV2::LoadBalancerProperties:Subnets:- Fn::ImportValue: !Sub ${NetworkStackName}-public-subnet-1- Fn::ImportValue: !Sub ${NetworkStackName}-public-subnet-2SecurityGroups:- Fn::ImportValue: !Sub ${NetworkStackName}-alb-sgTargetGroup:Type: AWS::ElasticLoadBalancingV2::TargetGroupProperties:VpcId:Fn::ImportValue: !Sub ${NetworkStackName}-vpcPort: 80Protocol: HTTPTargetType: ipMatcher:HttpCode: 200-299HealthCheckIntervalSeconds: 10HealthCheckPath: /HealthCheckProtocol: HTTPHealthCheckTimeoutSeconds: 5HealthyThresholdCount: 2Listener:Type: AWS::ElasticLoadBalancingV2::ListenerProperties:LoadBalancerArn: !Ref LoadBalancerPort: 80Protocol: HTTPDefaultActions:- Type: forwardTargetGroupArn: !Ref TargetGroup# Export outputOutputs:LoadBalancer:Description: Load balancerValue: !Ref LoadBalancerExport:Name: !Sub ${AWS::StackName}-load-balancerTargetGroup:Description: Target groupValue: !Ref TargetGroupExport:Name: !Sub ${AWS::StackName}-target-group
ECS Cluster#
Now let's create a ecs cluster.
AWSTemplateFormatVersion: '2010-09-09'# ParametersParameters:ClusterName:Type: StringDescription: 'ImageId to be used to create an EC2 instance.'Default: 'demo'# Create an ecs clusterResources:ecscluster:Type: AWS::ECS::ClusterProperties:ClusterName: !Ref ClusterNameClusterSettings:- Name: containerInsightsValue: enabledCapacityProviders:- FARGATE- FARGATE_SPOTDefaultCapacityProviderStrategy:- CapacityProvider: FARGATEWeight: 4Base: 1- CapacityProvider: FARGATE_SPOTWeight: 1Base: 0# ExportOutputs:ecscluster:Value: !Ref ecsclusterExport:Name: !Sub '${AWS::StackName}-ecscluster'
Task Definition#
Let's create a task definition with task role and task execution role, container definition, log group.
AWSTemplateFormatVersion: '2010-09-09'Description: ECS Task DefinitionParameters:ImageUri:Type: StringDescription: ecr image uriDefault: <ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/go-appResources:TaskRoleBook:Type: AWS::IAM::RoleProperties:RoleName: 'ECSTaskRoleForBook'AssumeRolePolicyDocument:Statement:- Action:- 'sts:AssumeRole'Effect: AllowPrincipal:Service:- ecs-tasks.amazonaws.comPolicies:- PolicyName: rootPolicyDocument:Version: '2012-10-17'Statement:- Effect: AllowAction:- 'ssmmessages:CreateControlChannel'- 'ssmmessages:CreateDataChannel'- 'ssmmessages:OpenControlChannel'- 'ssmmessages:OpenDataChannel'Resource: '*'ManagedPolicyArns:- 'arn:aws:iam::aws:policy/AmazonS3FullAccess'TaskExecutionRoleBook:Type: AWS::IAM::RoleProperties:RoleName: 'ECSTaskExecutionRoleForBook'AssumeRolePolicyDocument:Statement:- Action:- 'sts:AssumeRole'Effect: AllowPrincipal:Service:- ecs-tasks.amazonaws.comManagedPolicyArns:- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicyBookTaskDefinition:Type: AWS::ECS::TaskDefinitionProperties:Family: book-serviceTaskRoleArn: !GetAtt TaskRoleBook.ArnExecutionRoleArn: !GetAtt TaskExecutionRoleBook.ArnNetworkMode: awsvpcRequiresCompatibilities:- FARGATECpu: 256Memory: 512ContainerDefinitions:- Name: book-containerImage: !Ref ImageUriPortMappings:- ContainerPort: 3000Privileged: falseLogConfiguration:LogDriver: awslogsOptions:awslogs-group: !Ref LogGroupawslogs-region: !Ref AWS::Regionawslogs-stream-prefix: book-serviceLogGroup:Type: AWS::Logs::LogGroupDeletionPolicy: DeleteUpdateReplacePolicy: DeleteProperties:LogGroupName: /ecs/book-serviceRetentionInDays: 7# Export outputOutputs:taskDefinition:Description: Task DefinitionValue: !Ref BookTaskDefinitionExport:Name: !Sub '${AWS::StackName}-book-task-def'
Book Service#
Let's create a book service with auto scaling policy, and integrate with the ALB via the aboved created target group.
AWSTemplateFormatVersion: '2010-09-09'Description: ECS ServiceParameters:NetworkStackName:Type: StringDescription: Name of the network stackDefault: 'cfn-network'ClusterStackName:Type: StringDescription: Name of the ECS cluster stack to create the serviceDefault: 'cfn-ecs-cluster'ALBStackName:Type: StringDescription: Name of the ALB stack to associate with the serviceDefault: 'cfn-load-balancer'TaskDefinitionStackName:Type: StringDescription: Name of the task definition stackDefault: 'cfn-task-def-book'DesiredCount:Type: NumberDescription: Desired number of tasksDefault: 1ContainerName:Type: StringDescription: Name of the containerDefault: 'book-container'ServiceName:Type: StringDescription: Name of the serviceDefault: 'book-service'Resources:ECSService:Type: AWS::ECS::ServiceProperties:ServiceName: !Ref ServiceNameTaskDefinition:Fn::ImportValue: !Sub ${TaskDefinitionStackName}-book-task-defCluster:Fn::ImportValue: !Sub ${ClusterStackName}-ecsclusterLaunchType: FARGATEDesiredCount: !Ref DesiredCountDeploymentController:Type: ECSNetworkConfiguration:AwsvpcConfiguration:# AssignPublicIp: ENABLEDSecurityGroups:- Fn::ImportValue: !Sub ${NetworkStackName}-ecs-fargate-sgSubnets:- Fn::ImportValue: !Sub ${NetworkStackName}-private-subnet-1- Fn::ImportValue: !Sub ${NetworkStackName}-private-subnet-2LoadBalancers:- ContainerName: !Ref ContainerNameContainerPort: 3000TargetGroupArn:Fn::ImportValue: !Sub ${ALBStackName}-target-groupHealthCheckGracePeriodSeconds: 60ServiceScalingTarget:Type: AWS::ApplicationAutoScaling::ScalableTargetProperties:MaxCapacity: 4MinCapacity: 1ResourceId:Fn::Join:- /- - service- Fn::ImportValue: !Sub ${ClusterStackName}-ecscluster- !Ref ServiceNameRoleARN: !GetAtt AutoScalingRole.ArnScalableDimension: ecs:service:DesiredCountServiceNamespace: ecsServiceScalingPolicy:Type: AWS::ApplicationAutoScaling::ScalingPolicyProperties:PolicyName: AverageCPUUtilizationPolicyPolicyType: TargetTrackingScalingScalingTargetId: !Ref ServiceScalingTargetTargetTrackingScalingPolicyConfiguration:TargetValue: 80.0ScaleInCooldown: 60ScaleOutCooldown: 60PredefinedMetricSpecification:PredefinedMetricType: ECSServiceAverageCPUUtilizationAutoScalingRole:Type: AWS::IAM::RoleProperties:AssumeRolePolicyDocument:Version: '2012-10-17'Statement:- Effect: AllowPrincipal:Service: ecs.application-autoscaling.amazonaws.comAction: 'sts:AssumeRole'Policies:- PolicyName: MyAWSApplicationAutoscalingECSServicePolicyPolicyDocument:Version: '2012-10-17'Statement:- Effect: AllowAction:- ecs:DescribeServices- ecs:UpdateService- cloudwatch:PutMetricAlarm- cloudwatch:PutMetricAlarm- cloudwatch:PutMetricAlarmResource: '*'
Code Pipeline#
Finally let's create a code pipeline with standard ECS deployment which is simple rolling update. It consists of codebuild, ecr, and codedeploy. Please take note the imagedefinitions.json
AWSTemplateFormatVersion: '2010-09-09'Resources:# IAM role for codepipelineRoleForCodePipeline:Type: AWS::IAM::RoleProperties:AssumeRolePolicyDocument:Statement:- Action: sts:AssumeRoleEffect: AllowPrincipal:Service: codepipeline.amazonaws.comPolicies:- PolicyName: CodePipelinePolicyDemoPolicyDocument: |{"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": ["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 codebuildRoleForCodeBuild:Type: AWS::IAM::RoleProperties:AssumeRolePolicyDocument:Statement:- Action: sts:AssumeRoleEffect: AllowPrincipal:Service: codebuild.amazonaws.comPolicies:- PolicyName: CodeBuildPolicyDemoPolicyDocument: |{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Resource": ["arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/codebuild/debug","arn:aws:logs:<REGION>:<ACCOUNT_ID>:log-group:/aws/codebuild/debug:*"],"Action": ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"]},{"Effect": "Allow","Resource": ["arn:aws:s3:::codepipeline-<REGION>-*"],"Action": ["s3:PutObject","s3:GetObject","s3:GetObjectVersion","s3:GetBucketAcl","s3:GetBucketLocation"]},{"Effect": "Allow","Resource": ["arn:aws:s3:::codebuild-demo-23062023","arn:aws:s3:::codebuild-demo-23062023/*","arn:aws:s3:::codepipeline-<REGION>-499144044954/*"],"Action": ["s3:PutObject","s3:GetBucketAcl","s3:GetBucketLocation"]},{"Effect": "Allow","Action": ["codebuild:CreateReportGroup","codebuild:CreateReport","codebuild:UpdateReport","codebuild:BatchPutTestCases","codebuild:BatchPutCodeCoverages"],"Resource": ["arn:aws:codebuild:<REGION>:<ACCOUNT_ID>:report-group/debug-*"]}]}# Codebuild projectCodeBuildProject:Type: AWS::CodeBuild::ProjectProperties:Name: demoServiceRole: !Ref RoleForCodeBuildArtifacts:Type: S3Location: codebuild-demo-23062023Packaging: NONEEnvironment:Type: LINUX_CONTAINERImage: aws/codebuild/amazonlinux2-x86_64-standard:5.0ComputeType: BUILD_GENERAL1_SMALLPrivilegedMode: falseLogsConfig:CloudWatchLogs:Status: ENABLEDGroupName: /aws/codebuild/debugSource:Type: NO_SOURCEBuildSpec: |version: 0.2phases:build:commands:- printf '[{"name":"%s","imageUri":"%s"}]' book-container <ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/go-app:latest > imagedefinitions.json- cat imagedefinitions.jsonartifacts:files:- imagedefinitions.jsonname: demoCache:Type: NO_CACHETags:- Key: nameValue: demo# CodePipelineCodePipeline:Type: AWS::CodePipeline::PipelineProperties:Name: demo-pipelineRoleArn:Fn::GetAtt:- RoleForCodePipeline- ArnArtifactStore:Type: S3Location: codepipeline-<REGION>-499144044954Stages:- Name: SourceActions:- Name: ECRSourceActionTypeId:Owner: AWSCategory: SourceVersion: '1'Provider: ECRConfiguration:RepositoryName: go-appImageTag: latestOutputArtifacts:- Name: ecr-demo- Name: BuildActions:- Name: CodeBuildActionTypeId:Owner: AWSCategory: BuildVersion: '1'Provider: CodeBuildRunOrder: 1Configuration:ProjectName: !Ref CodeBuildProjectInputArtifacts:- Name: ecr-demoOutputArtifacts:- Name: demo- Name: DeployActions:- Name: DeployActionTypeId:Owner: AWSCategory: DeployVersion: '1'Provider: ECSConfiguration:ClusterName: demoServiceName: book-serviceFileName: imagedefinitions.jsonInputArtifacts:- Name: demo
Here is the imagedefinitions.json
[{"name": "book-container","imageUri": "<ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/go-app:latest"}]
And here is the build_spec.yaml
artifacts:files:- imagedefinitions.json- imageDetail.jsonversion: '0.2'phases:install:commands:- export ACCOUNT_ID=<ACCOUNT_ID>- export REGION=<REGION>- export REPO_NAME=go-app- echo ${ACCOUNT_ID} ${REGION} ${REPO_NAME}pre_build:commands:- export TAG_NAME=$(date +%s)build:commands:post_build:commands:- printf '[{"name":"%s","imageUri":"%s"}]' book-container ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO_NAME}:latest > imagedefinitions.json- printf '{"ImageURI":"%s"}' ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPO_NAME}:latest > imageDetail.json- cat imagedefinitions.json- cat imageDetail.json