import * as cdk from '@aws-cdk/core'; import ecs = require("@aws-cdk/aws-ecs"); import elbv2 = require("@aws-cdk/aws-elasticloadbalancingv2"); import log = require("@aws-cdk/aws-logs"); import iam = require("@aws-cdk/aws-iam"); import { Vpc, SubnetType, SecurityGroup, Port } from "@aws-cdk/aws-ec2"; import { Repository } from '@aws-cdk/aws-ecr'; import { ManagedPolicy, ServicePrincipal, Role} from "@aws-cdk/aws-iam"; import {ContainerImage, Cluster, DeploymentControllerType, FargateTaskDefinition, CfnService, FargateService, AwsLogDriver, FargatePlatformVersion, CfnTaskSet, LaunchType, CfnPrimaryTaskSet, CfnTaskDefinition, PropagatedTagSource} from "@aws-cdk/aws-ecs"; import {ApplicationProtocol, ApplicationLoadBalancer, ApplicationTargetGroup, ApplicationListenerRule, TargetType, CfnListener, CfnTargetGroup, Protocol, ListenerAction, ListenerCondition} from "@aws-cdk/aws-elasticloadbalancingv2"; export interface ECSStackProps extends cdk.StackProps { clusterName: string; loadBalancerName: string; repositoryArn: string; repository: string, imageTag: string; bizUnitTag: string; envTag: string; containerPort: number; containerName: string, desiredContainersCount: number; containerMemoryLimit: number; containerCpuLimit: number; CFCodeDeployBGDeploymentRole: string; } export class BlueGreenUsingEcsStack extends cdk.Stack { readonly service: ecs.FargateService; static readonly ECS_TASK_FAMILY_NAME = "HOV-ECS-blue-green-deployment"; static readonly ECS_LOG_GROUP_NAME = "HOV-ECS-blue-green-LogGroup" static readonly ECS_LOG_STREAM_NAME = "blue_green_ecs_app"; static readonly ECS_SERVICE_NAME = "HOV-ECS-blue-green-service" constructor(scope: cdk.Construct, id: string, props: ECSStackProps) { super(scope, id, props); // export the existing VPC by tags const vpc = Vpc.fromLookup(this, "vpc", { tags: { BusinessUnit: props.bizUnitTag, Environment: props.envTag, }, }); const selection = vpc.selectSubnets({ subnetType: SubnetType.PRIVATE, onePerAz: true }); // Creating a ECS cluster const clusterNameParam = new cdk.CfnParameter(this, "ClusterName", { type: "String", default: props.clusterName, }); const cluster = new Cluster(this, "ECSCluster", { vpc: vpc, clusterName: clusterNameParam.valueAsString, }); //Creating SecurityGroup for ECSService const serviceSG = new SecurityGroup(this, "ECSServiceSG", { vpc: vpc, }); // Creating an application load balancer, listener , listener Rule and two target groups for Blue/Green deployment const loadBalancerNameParam = new cdk.CfnParameter(this, "loadBalancerName", { type: "String", default: props.loadBalancerName, }); const alb = new ApplicationLoadBalancer(this, "ApplicationLB", { vpc: vpc, internetFacing: false, loadBalancerName: loadBalancerNameParam.valueAsString }); serviceSG.connections.allowFrom(alb, Port.tcp(80)); serviceSG.connections.allowFrom(alb, Port.tcp(8080)); // Creating Target groups, CodeDeploy will shift traffic between these two target groups. // Target group 1 const blueGroup = new ApplicationTargetGroup(this, "blueTargetGroup", { vpc: vpc, protocol: ApplicationProtocol.HTTP, port: 80, targetType: TargetType.IP, deregistrationDelay: cdk.Duration.seconds(300), targetGroupName: "BlueTargetGroup", healthCheck: { interval: cdk.Duration.seconds(10), path: "/", protocol: Protocol.HTTP, healthyHttpCodes: "200-299", healthyThresholdCount: 2, unhealthyThresholdCount: 3, timeout: cdk.Duration.seconds(3) } }); // Target group 2 const greenGroup = new ApplicationTargetGroup(this, "greenTargetGroup", { vpc: vpc, protocol: ApplicationProtocol.HTTP, port: 80, targetType: TargetType.IP, deregistrationDelay: cdk.Duration.seconds(300), targetGroupName: "GreenTargetGroup", healthCheck: { interval: cdk.Duration.seconds(10), path: "/", protocol: Protocol.HTTP, healthyHttpCodes: "200-299", healthyThresholdCount: 2, unhealthyThresholdCount: 3, timeout: cdk.Duration.seconds(3) } }); const albProdListener = alb.addListener('albProdListener', { port: 80, protocol: ApplicationProtocol.HTTP, defaultAction: ListenerAction.weightedForward([{ targetGroup: blueGroup, weight: 100 }]) }); let albTestListener = alb.addListener('albTestListener', { port: 8080, // test traffic port protocol: ApplicationProtocol.HTTP, defaultAction: ListenerAction.weightedForward([{ targetGroup: blueGroup, weight: 100 }]) }); albProdListener.connections.allowDefaultPortFromAnyIpv4('Allow traffic from everywhere'); albTestListener.connections.allowDefaultPortFromAnyIpv4('Allow traffic from everywhere'); const albListenerRule = new ApplicationListenerRule(this, "albListenerRule",{ priority: 1, listener: albProdListener, conditions: [ ListenerCondition.pathPatterns(["*"]) ], action: ListenerAction.forward([blueGroup]) }); // Creating ECS TaskDefination Role const ecsTaskRole = new Role(this, "ECSTaskExecutionRole", { assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com') }); ecsTaskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")); // Creating an ECS task definition using ECR image , LogGroup and Service const taskDefinition = new FargateTaskDefinition(this, "TaskDefinition", { family: "HOV-ECS-blue-green-deployment", cpu: props.containerCpuLimit, memoryLimitMiB: props.containerMemoryLimit, taskRole: ecsTaskRole, executionRole: ecsTaskRole }); const repo = Repository.fromRepositoryArn(this,"ecrRepository",props.repositoryArn); const containerDefinition = taskDefinition.addContainer(props.containerName, { image: ContainerImage.fromEcrRepository(repo, props.imageTag), logging: new AwsLogDriver({ logGroup: new log.LogGroup(this, "BlueGreenLogGroup", { logGroupName: BlueGreenUsingEcsStack.ECS_LOG_GROUP_NAME, retention: log.RetentionDays.ONE_MONTH // removalPolicy: RemovalPolicy.DESTROY }), streamPrefix: BlueGreenUsingEcsStack.ECS_LOG_STREAM_NAME }), environment: { ENVIRONMENT: props.envTag, }, }); containerDefinition.addPortMappings({ containerPort: props.containerPort, }); //Implementation 1: const Service = new FargateService(this, "Service", { cluster: cluster, taskDefinition: taskDefinition, healthCheckGracePeriod: cdk.Duration.seconds(10), minHealthyPercent: 50, maxHealthyPercent: 200, desiredCount: props.desiredContainersCount, deploymentController: { type: DeploymentControllerType.EXTERNAL }, }); //Implementation 2: // const Service = new CfnService(this, 'Service', { // cluster: cluster.clusterName, // desiredCount: 3, // launchType: "FARGATE", // deploymentController: { type: DeploymentControllerType.EXTERNAL }, // propagateTags: PropagatedTagSource.SERVICE, // }); // enable EnableExecuteCommand for the service // https://github.com/pahud/ecs-exec-cdk-demo const cfnService = Service.node.findChild('Service') as CfnService; cfnService.addPropertyOverride('EnableExecuteCommand', true); Service.node.addDependency(blueGroup); Service.node.addDependency(greenGroup); Service.node.addDependency(albProdListener); Service.node.addDependency(albTestListener); // Creating ECS TaskSet const taskSet = new CfnTaskSet(this, 'TaskSet', { cluster: cluster.clusterName, // service: Service.attrName, service: Service.serviceArn, scale: { unit: 'PERCENT', value: 100 }, taskDefinition: taskDefinition.taskDefinitionArn, launchType: LaunchType.FARGATE, loadBalancers: [ { containerName: props.containerName, containerPort: 80, targetGroupArn: blueGroup.targetGroupArn, } ], networkConfiguration: { awsVpcConfiguration: { assignPublicIp: 'DISABLED', securityGroups: [ serviceSG.securityGroupId ], subnets: vpc.selectSubnets({ subnetType: SubnetType.PRIVATE }).subnetIds, } }, }); // Creating ECS Primary TaskSet new CfnPrimaryTaskSet(this, 'PrimaryTaskSet', { // cluster: cluster.clusterName, // service: Service.attrName, cluster: props.clusterName, service: BlueGreenUsingEcsStack.ECS_SERVICE_NAME, taskSetId: taskSet.attrId, }); // transform to configure the blue-green deployments this.addTransform('AWS::CodeDeployBlueGreen'); //Code Deploy hook and transform to configure the blue-green deployments. const taskDefLogicalId = this.getLogicalId(taskDefinition.node.defaultChild as CfnTaskDefinition) const taskSetLogicalId = this.getLogicalId(taskSet) new cdk.CfnCodeDeployBlueGreenHook(this, 'CodeDeployBlueGreenHook', { trafficRoutingConfig: { type: cdk.CfnTrafficRoutingType.TIME_BASED_CANARY, timeBasedCanary: { // Shift 20% of prod traffic, then wait 15 minutes stepPercentage: 20, bakeTimeMins: 15 } }, additionalOptions: { // After canary period, shift 100% of prod traffic, then wait 30 minutes terminationWaitTimeInMinutes: 30 }, serviceRole: props.CFCodeDeployBGDeploymentRole, applications: [{ target: { type: "AWS::ECS::Service", // logicalId: this.getLogicalId(Service) logicalId: BlueGreenUsingEcsStack.ECS_SERVICE_NAME, }, ecsAttributes: { taskDefinitions: [ taskDefLogicalId, taskDefLogicalId + 'Green' ], taskSets: [ taskSetLogicalId, taskSetLogicalId + 'Green' ], trafficRouting: { prodTrafficRoute: { type: CfnListener.CFN_RESOURCE_TYPE_NAME, logicalId: this.getLogicalId(albProdListener.node.defaultChild as CfnListener) }, testTrafficRoute: { type: CfnListener.CFN_RESOURCE_TYPE_NAME, logicalId: this.getLogicalId(albTestListener.node.defaultChild as CfnListener) }, targetGroups: [ this.getLogicalId(blueGroup.node.defaultChild as CfnTargetGroup), this.getLogicalId(greenGroup.node.defaultChild as CfnTargetGroup) ] } } }] }); } }