import * as cdk from '@aws-cdk/core'; import {CfnOutput, CustomResource, Duration, RemovalPolicy} from '@aws-cdk/core'; import {ApplicationProtocol, TargetType} from "@aws-cdk/aws-elasticloadbalancingv2"; import {ContainerImage, DeploymentControllerType, Protocol} from "@aws-cdk/aws-ecs"; import {AnyPrincipal, Effect, ManagedPolicy, ServicePrincipal} from "@aws-cdk/aws-iam"; import {BuildEnvironmentVariableType, ComputeType} from "@aws-cdk/aws-codebuild"; import * as path from "path"; import {Port} from "@aws-cdk/aws-ec2"; import {BlockPublicAccess, BucketEncryption} from "@aws-cdk/aws-s3"; import {EcsDeploymentConfig} from "@aws-cdk/aws-codedeploy"; import codeCommit = require('@aws-cdk/aws-codecommit'); import codeBuild = require("@aws-cdk/aws-codebuild"); import codeDeploy = require("@aws-cdk/aws-codedeploy"); import iam = require("@aws-cdk/aws-iam"); import ec2 = require('@aws-cdk/aws-ec2'); import ecs = require('@aws-cdk/aws-ecs'); import elb = require("@aws-cdk/aws-elasticloadbalancingv2"); import log = require("@aws-cdk/aws-logs"); import ecr = require("@aws-cdk/aws-ecr"); import s3 = require("@aws-cdk/aws-s3"); import cloudWatch = require('@aws-cdk/aws-cloudwatch'); import codePipelineActions = require("@aws-cdk/aws-codepipeline-actions"); import lambda = require('@aws-cdk/aws-lambda'); import codePipeline = require("@aws-cdk/aws-codepipeline"); export class BlueGreenUsingEcsStack extends cdk.Stack { static readonly ECS_DEPLOYMENT_GROUP_NAME = "DemoAppECSBlueGreen"; static readonly ECS_DEPLOYMENT_CONFIG_NAME = "CodeDeployDefault.ECSLinear10PercentEvery1Minutes"; static readonly ECS_TASKSET_TERMINATION_WAIT_TIME = 10; static readonly ECS_TASK_FAMILY_NAME = "demo-app"; static readonly ECS_APP_NAME = "demo-app"; static readonly ECS_APP_LOG_GROUP_NAME = "/ecs/demo-app"; static readonly DUMMY_TASK_FAMILY_NAME = "sample-app"; static readonly DUMMY_APP_NAME = "sample-app"; static readonly DUMMY_APP_LOG_GROUP_NAME = "/ecs/sample-app"; static readonly DUMMY_CONTAINER_IMAGE = "smuralee/nginx"; constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // ============================================================================= // ECR and CodeCommit repositories for the Blue/ Green deployment // ============================================================================= // ECR repository for the docker images const ecrRepo = new ecr.Repository(this, 'demoAppEcrRepo', { imageScanOnPush: true }); // CodeCommit repository for storing the source code const codeRepo = new codeCommit.Repository(this, "demoAppCodeRepo", { repositoryName: BlueGreenUsingEcsStack.ECS_APP_NAME, description: "Demo application hosted on NGINX" }); // ============================================================================= // CODE BUILD and ECS TASK ROLES for the Blue/ Green deployment // ============================================================================= // IAM role for the Code Build project const codeBuildServiceRole = new iam.Role(this, "codeBuildServiceRole", { assumedBy: new ServicePrincipal('codebuild.amazonaws.com') }); const inlinePolicyForCodeBuild = new iam.PolicyStatement({ effect: Effect.ALLOW, actions: [ "ecr:GetAuthorizationToken", "ecr:BatchCheckLayerAvailability", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload", "ecr:PutImage" ], resources: ["*"] }); codeBuildServiceRole.addToPolicy(inlinePolicyForCodeBuild); // ECS task role const ecsTaskRole = new iam.Role(this, "ecsTaskRoleForWorkshop", { assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com') }); ecsTaskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")); // ============================================================================= // CODE DEPLOY APPLICATION for the Blue/ Green deployment // ============================================================================= // Creating the code deploy application const codeDeployApplication = new codeDeploy.EcsApplication(this, "demoAppCodeDeploy"); // Creating the code deploy service role const codeDeployServiceRole = new iam.Role(this, "codeDeployServiceRole", { assumedBy: new ServicePrincipal('codedeploy.amazonaws.com') }); codeDeployServiceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AWSCodeDeployRoleForECS")); // IAM role for custom lambda function const customLambdaServiceRole = new iam.Role(this, "codeDeployCustomLambda", { assumedBy: new ServicePrincipal('lambda.amazonaws.com') }); const inlinePolicyForLambda = new iam.PolicyStatement({ effect: Effect.ALLOW, actions: [ "iam:PassRole", "sts:AssumeRole", "codedeploy:List*", "codedeploy:Get*", "codedeploy:UpdateDeploymentGroup", "codedeploy:CreateDeploymentGroup", "codedeploy:DeleteDeploymentGroup" ], resources: ["*"] }); customLambdaServiceRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')) customLambdaServiceRole.addToPolicy(inlinePolicyForLambda); // Custom resource to create the deployment group const createDeploymentGroupLambda = new lambda.Function(this, 'createDeploymentGroupLambda', { code: lambda.Code.fromAsset( path.join(__dirname, 'custom_resources'), { exclude: ["**", "!create_deployment_group.py"] }), runtime: lambda.Runtime.PYTHON_3_8, handler: 'create_deployment_group.handler', role: customLambdaServiceRole, description: "Custom resource to create deployment group", memorySize: 128, timeout: cdk.Duration.seconds(60) }); // ============================================================================= // VPC, ECS Cluster, ELBs and Target groups for the Blue/ Green deployment // ============================================================================= // Creating the VPC const vpc = new ec2.Vpc(this, 'vpcForECSCluster'); // Creating a ECS cluster const cluster = new ecs.Cluster(this, 'ecsClusterForWorkshop', {vpc}); // Creating an application load balancer, listener and two target groups for Blue/Green deployment const alb = new elb.ApplicationLoadBalancer(this, "alb", { vpc: vpc, internetFacing: true }); const albProdListener = alb.addListener('albProdListener', { port: 80 }); const albTestListener = alb.addListener('albTestListener', { port: 8080 }); albProdListener.connections.allowDefaultPortFromAnyIpv4('Allow traffic from everywhere'); albTestListener.connections.allowDefaultPortFromAnyIpv4('Allow traffic from everywhere'); // Target group 1 const blueGroup = new elb.ApplicationTargetGroup(this, "blueGroup", { vpc: vpc, protocol: ApplicationProtocol.HTTP, port: 80, targetType: TargetType.IP, healthCheck: { path: "/", timeout: Duration.seconds(10), interval: Duration.seconds(15), healthyHttpCodes: "200,404" } }); // Target group 2 const greenGroup = new elb.ApplicationTargetGroup(this, "greenGroup", { vpc: vpc, protocol: ApplicationProtocol.HTTP, port: 80, targetType: TargetType.IP, healthCheck: { path: "/", timeout: Duration.seconds(10), interval: Duration.seconds(15), healthyHttpCodes: "200,404" } }); // Registering the blue target group with the production listener of load balancer albProdListener.addTargetGroups("blueTarget", { targetGroups: [blueGroup] }); // Registering the green target group with the test listener of load balancer albTestListener.addTargetGroups("greenTarget", { targetGroups: [greenGroup] }); // ================================================================================================ // CloudWatch Alarms for 5XX errors const blue4xxMetric = new cloudWatch.Metric({ namespace: 'AWS/ApplicationELB', metricName: 'HTTPCode_Target_4XX_Count', dimensions: { TargetGroup: blueGroup.targetGroupFullName, LoadBalancer: alb.loadBalancerFullName }, statistic: cloudWatch.Statistic.SUM, period: Duration.minutes(1) }); const blueGroupAlarm = new cloudWatch.Alarm(this, "blue5xxErrors", { alarmName: "Blue_5xx_Alarm", alarmDescription: "CloudWatch Alarm for the 4xx errors of Blue target group", metric: blue4xxMetric, threshold: 1, evaluationPeriods: 1 }); const green4xxMetric = new cloudWatch.Metric({ namespace: 'AWS/ApplicationELB', metricName: 'HTTPCode_Target_4XX_Count', dimensions: { TargetGroup: greenGroup.targetGroupFullName, LoadBalancer: alb.loadBalancerFullName }, statistic: cloudWatch.Statistic.SUM, period: Duration.minutes(1) }); const greenGroupAlarm = new cloudWatch.Alarm(this, "green4xxErrors", { alarmName: "Green_4xx_Alarm", alarmDescription: "CloudWatch Alarm for the 4xx errors of Green target group", metric: green4xxMetric, threshold: 1, evaluationPeriods: 1 }); // ================================================================================================ // ================================================================================================ // DUMMY TASK DEFINITION for the initial service creation // This is required for the service being made available to create the CodeDeploy Deployment Group // ================================================================================================ const sampleTaskDefinition = new ecs.FargateTaskDefinition(this, "sampleTaskDefn", { family: BlueGreenUsingEcsStack.DUMMY_TASK_FAMILY_NAME, cpu: 256, memoryLimitMiB: 1024, taskRole: ecsTaskRole, executionRole: ecsTaskRole }); const sampleContainerDefn = sampleTaskDefinition.addContainer("sampleAppContainer", { image: ecs.ContainerImage.fromRegistry(BlueGreenUsingEcsStack.DUMMY_CONTAINER_IMAGE), logging: new ecs.AwsLogDriver({ logGroup: new log.LogGroup(this, "sampleAppLogGroup", { logGroupName: BlueGreenUsingEcsStack.DUMMY_APP_LOG_GROUP_NAME, removalPolicy: RemovalPolicy.DESTROY }), streamPrefix: BlueGreenUsingEcsStack.DUMMY_APP_NAME }), dockerLabels: { name: BlueGreenUsingEcsStack.DUMMY_APP_NAME } }); sampleContainerDefn.addPortMappings({ containerPort: 80, protocol: Protocol.TCP }); // ================================================================================================ // ECS task definition using ECR image // Will be used by the CODE DEPLOY for Blue/Green deployment // ================================================================================================ const taskDefinition = new ecs.FargateTaskDefinition(this, "appTaskDefn", { family: BlueGreenUsingEcsStack.ECS_TASK_FAMILY_NAME, cpu: 256, memoryLimitMiB: 1024, taskRole: ecsTaskRole, executionRole: ecsTaskRole }); const containerDefinition = taskDefinition.addContainer("demoAppContainer", { image: ContainerImage.fromEcrRepository(ecrRepo, "latest"), logging: new ecs.AwsLogDriver({ logGroup: new log.LogGroup(this, "demoAppLogGroup", { logGroupName: BlueGreenUsingEcsStack.ECS_APP_LOG_GROUP_NAME, removalPolicy: RemovalPolicy.DESTROY }), streamPrefix: BlueGreenUsingEcsStack.ECS_APP_NAME }), dockerLabels: { name: BlueGreenUsingEcsStack.ECS_APP_NAME } }); containerDefinition.addPortMappings({ containerPort: 80, protocol: Protocol.TCP }); // ============================================================================= // ECS SERVICE for the Blue/ Green deployment // ============================================================================= const demoAppService = new ecs.FargateService(this, "demoAppService", { cluster: cluster, taskDefinition: sampleTaskDefinition, healthCheckGracePeriod: Duration.seconds(10), desiredCount: 3, deploymentController: { type: DeploymentControllerType.CODE_DEPLOY }, serviceName: BlueGreenUsingEcsStack.ECS_APP_NAME }); demoAppService.connections.allowFrom(alb, Port.tcp(80)) demoAppService.connections.allowFrom(alb, Port.tcp(8080)) demoAppService.attachToApplicationTargetGroup(blueGroup); // ============================================================================= // CODE DEPLOY - Deployment Group CUSTOM RESOURCE for the Blue/ Green deployment // ============================================================================= new CustomResource(this, 'customEcsDeploymentGroup', { serviceToken: createDeploymentGroupLambda.functionArn, properties: { ApplicationName: codeDeployApplication.applicationName, DeploymentGroupName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_GROUP_NAME, DeploymentConfigName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_CONFIG_NAME, ServiceRoleArn: codeDeployServiceRole.roleArn, BlueTargetGroup: blueGroup.targetGroupName, GreenTargetGroup: greenGroup.targetGroupName, ProdListenerArn: albProdListener.listenerArn, TestListenerArn: albTestListener.listenerArn, EcsClusterName: cluster.clusterName, EcsServiceName: demoAppService.serviceName, TerminationWaitTime: BlueGreenUsingEcsStack.ECS_TASKSET_TERMINATION_WAIT_TIME, BlueGroupAlarm: blueGroupAlarm.alarmName, GreenGroupAlarm: greenGroupAlarm.alarmName, } }); const ecsDeploymentGroup = codeDeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(this, "ecsDeploymentGroup", { application: codeDeployApplication, deploymentGroupName: BlueGreenUsingEcsStack.ECS_DEPLOYMENT_GROUP_NAME, deploymentConfig: EcsDeploymentConfig.fromEcsDeploymentConfigName(this, "ecsDeploymentConfig", BlueGreenUsingEcsStack.ECS_DEPLOYMENT_CONFIG_NAME) }); // ============================================================================= // CODE BUILD PROJECT for the Blue/ Green deployment // ============================================================================= // Creating the code build project const demoAppCodeBuild = new codeBuild.Project(this, "demoAppCodeBuild", { role: codeBuildServiceRole, description: "Code build project for the demo application", environment: { buildImage: codeBuild.LinuxBuildImage.STANDARD_4_0, computeType: ComputeType.SMALL, privileged: true, environmentVariables: { REPOSITORY_URI: { value: ecrRepo.repositoryUri, type: BuildEnvironmentVariableType.PLAINTEXT }, TASK_EXECUTION_ARN: { value: ecsTaskRole.roleArn, type: BuildEnvironmentVariableType.PLAINTEXT }, TASK_FAMILY: { value: BlueGreenUsingEcsStack.ECS_TASK_FAMILY_NAME, type: BuildEnvironmentVariableType.PLAINTEXT } } }, source: codeBuild.Source.codeCommit({ repository: codeRepo }) }); // ============================================================================= // CODE PIPELINE for Blue/Green ECS deployment // ============================================================================= const codePipelineServiceRole = new iam.Role(this, "codePipelineServiceRole", { assumedBy: new ServicePrincipal('codepipeline.amazonaws.com') }); const inlinePolicyForCodePipeline = new iam.PolicyStatement({ effect: Effect.ALLOW, actions: [ "iam:PassRole", "sts:AssumeRole", "codecommit:Get*", "codecommit:List*", "codecommit:GitPull", "codecommit:UploadArchive", "codecommit:CancelUploadArchive", "codebuild:BatchGetBuilds", "codebuild:StartBuild", "codedeploy:CreateDeployment", "codedeploy:Get*", "codedeploy:RegisterApplicationRevision", "s3:Get*", "s3:List*", "s3:PutObject" ], resources: ["*"] }); codePipelineServiceRole.addToPolicy(inlinePolicyForCodePipeline); const sourceArtifact = new codePipeline.Artifact('sourceArtifact'); const buildArtifact = new codePipeline.Artifact('buildArtifact'); // S3 bucket for storing the code pipeline artifacts const demoAppArtifactsBucket = new s3.Bucket(this, "demoAppArtifactsBucket", { encryption: BucketEncryption.S3_MANAGED, blockPublicAccess: BlockPublicAccess.BLOCK_ALL }); // S3 bucket policy for the code pipeline artifacts const denyUnEncryptedObjectUploads = new iam.PolicyStatement({ effect: Effect.DENY, actions: ["s3:PutObject"], principals: [new AnyPrincipal()], resources: [demoAppArtifactsBucket.bucketArn.concat("/*")], conditions: { StringNotEquals: { "s3:x-amz-server-side-encryption": "aws:kms" } } }); const denyInsecureConnections = new iam.PolicyStatement({ effect: Effect.DENY, actions: ["s3:*"], principals: [new AnyPrincipal()], resources: [demoAppArtifactsBucket.bucketArn.concat("/*")], conditions: { Bool: { "aws:SecureTransport": "false" } } }); demoAppArtifactsBucket.addToResourcePolicy(denyUnEncryptedObjectUploads); demoAppArtifactsBucket.addToResourcePolicy(denyInsecureConnections); // Code Pipeline - CloudWatch trigger event is created by CDK new codePipeline.Pipeline(this, "ecsBlueGreen", { role: codePipelineServiceRole, artifactBucket: demoAppArtifactsBucket, stages: [ { stageName: 'Source', actions: [ new codePipelineActions.CodeCommitSourceAction({ actionName: 'Source', repository: codeRepo, output: sourceArtifact, }), ] }, { stageName: 'Build', actions: [ new codePipelineActions.CodeBuildAction({ actionName: 'Build', project: demoAppCodeBuild, input: sourceArtifact, outputs: [buildArtifact] }) ] }, { stageName: 'Deploy', actions: [ new codePipelineActions.CodeDeployEcsDeployAction({ actionName: 'Deploy', deploymentGroup: ecsDeploymentGroup, appSpecTemplateInput: buildArtifact, taskDefinitionTemplateInput: buildArtifact, }) ] } ] }); // ============================================================================= // Export the outputs // ============================================================================= new CfnOutput(this, "ecsBlueGreenCodeRepo", { description: "Demo app code commit repository", exportName: "ecsBlueGreenDemoAppRepo", value: codeRepo.repositoryCloneUrlHttp }); new CfnOutput(this, "ecsBlueGreenLBDns", { description: "Load balancer DNS", exportName: "ecsBlueGreenLBDns", value: alb.loadBalancerDnsName }); } }