in src/main/java/com/ecs/cicd/EcsWindowsBlueGreenStack.java [110:464]
public EcsWindowsBlueGreenStack(final Construct scope, final String id, final StackProps props) throws IOException {
super(scope, id, props);
Vpc vpc = new Vpc(this, "ECSWindowsVpc", VpcProps.builder()
.maxAzs(2)
.build());
Cluster cluster = new Cluster(this, "WindowsCluster", ClusterProps.builder()
.vpc(vpc)
.containerInsights(true)
.build());
String clusterUniqueId = Names.uniqueId(cluster);
String clusterSubUniqueId = clusterUniqueId.substring(clusterUniqueId.length() - 4);
SecurityGroup asgSecurityGroup = new SecurityGroup(this, "AsgSecurityGroup", SecurityGroupProps.builder()
.vpc(vpc)
.description("Security group for ASG launched instances")
.build());
asgSecurityGroup.getConnections().allowFromAnyIpv4(Port.tcp(8080));
AutoScalingGroup windowsEcsAutoScalingGroup = new AutoScalingGroup(this, "AutoScalingGroupWindowsCluster", AutoScalingGroupProps.builder()
.healthCheck(HealthCheck.ec2())
.blockDevices(singletonList(
BlockDevice.builder()
.volume(BlockDeviceVolume.ebs(50, EbsDeviceOptions.builder()
.volumeType(EbsDeviceVolumeType.GP2)
.build()))
.deviceName("/dev/sda1")
.build()
))
.maxCapacity(6)
.vpc(vpc)
.securityGroup(asgSecurityGroup)
.instanceType(InstanceType.of(InstanceClass.BURSTABLE3, InstanceSize.LARGE))
.machineImage(EcsOptimizedImage.windows(WindowsOptimizedVersion.SERVER_2019))
.build());
String capacityProviderName = "WindowsClusterCapacity-" + clusterSubUniqueId;
AsgCapacityProvider windowsClusterCapacityProvider = new AsgCapacityProvider(this, "WindowsClusterAsg", AsgCapacityProviderProps.builder()
.autoScalingGroup(windowsEcsAutoScalingGroup)
.enableManagedScaling(true)
.capacityProviderName(capacityProviderName)
.build());
cluster.addAsgCapacityProvider(windowsClusterCapacityProvider);
Role taskRole = new Role(this, "ecs-taskRole" + clusterSubUniqueId, RoleProps.builder()
.assumedBy(new ServicePrincipal("ecs-tasks.amazonaws.com"))
.build());
PolicyStatement executionRolePolicy = new PolicyStatement(PolicyStatementProps.builder()
.effect(Effect.ALLOW)
.resources(singletonList("*"))
.actions(asList(
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents"
))
.build());
AwsLogDriver awsLogDriver = new AwsLogDriver(AwsLogDriverProps.builder()
.streamPrefix("ecs-windows-logs")
.build());
Ec2TaskDefinition windowsContainerTaskDef = new Ec2TaskDefinition(this, "WindowsContainerTaskDef", Ec2TaskDefinitionProps.builder()
.taskRole(taskRole)
.networkMode(null)
.family("windows-simple-iis")
.build());
//NOTE: You must explicitly change the task as the cdk does not support <default> mode
if (windowsContainerTaskDef.getNode().getDefaultChild() instanceof CfnTaskDefinition) {
CfnTaskDefinition defaultChild = (CfnTaskDefinition) windowsContainerTaskDef.getNode().getDefaultChild();
defaultChild.setNetworkMode(null);
}
windowsContainerTaskDef.addToExecutionRolePolicy(executionRolePolicy);
String containerName = "windows_sample_app";
int containerPort = 80;
windowsContainerTaskDef.addContainer("WindowsContainer", ContainerDefinitionOptions.builder()
.entryPoint(asList(
"powershell",
"-Command"
))
.command(singletonList(
"New-Item -Path C:\\inetpub\\wwwroot\\index.html -ItemType file -Value '<html> <head> <title>Amazon ECS Sample App</title> <style>body {margin-top: 40px; background-color: #333;} </style> </head><body> <div style=color:white;text-align:center> <h1>Amazon ECS Sample App</h1> <h2>Congratulations!</h2> <p>Your application is now running on a container in Amazon ECS.</p>' -Force ; C:\\ServiceMonitor.exe w3svc"
))
.containerName(containerName)
.image(RepositoryImage.fromRegistry("microsoft/iis"))
.cpu(512)
.memoryLimitMiB(1024)
.essential(true)
.portMappings(singletonList(
PortMapping.builder()
.containerPort(containerPort)
.hostPort(8080)
.protocol(Protocol.TCP)
.build()
))
.logging(awsLogDriver)
.build());
NetworkLoadBalancer serviceNlb = new NetworkLoadBalancer(this, "BgServiceNLB", NetworkLoadBalancerProps.builder()
.internetFacing(true)
.vpc(vpc)
.vpcSubnets(SubnetSelection.builder()
.subnetType(SubnetType.PUBLIC)
.build())
.build());
String nlbUniqueId = Names.uniqueId(serviceNlb);
String nlbSubUniqueId = nlbUniqueId.substring(nlbUniqueId.length() - 4);
String blueTarget = "BlueTarget" + nlbSubUniqueId;
String targetGreen = "GreenTarget" + nlbSubUniqueId;
NetworkWeightedTargetGroup greenTarget = NetworkWeightedTargetGroup.builder()
.targetGroup(new NetworkTargetGroup(this, targetGreen, NetworkTargetGroupProps.builder()
.port(80)
.targetGroupName(targetGreen)
.targetType(TargetType.INSTANCE)
.protocol(software.amazon.awscdk.services.elasticloadbalancingv2.Protocol.TCP)
.vpc(vpc)
.build()))
.build();
NetworkListener productionListener = serviceNlb.addListener("ProductionListener", BaseNetworkListenerProps.builder()
.port(80)
.build());
NetworkListener greenListener = serviceNlb.addListener("GreenListener", BaseNetworkListenerProps.builder()
.port(9000)
.defaultAction(NetworkListenerAction.weightedForward(singletonList(
greenTarget)))
.build());
Ec2Service service = new Ec2Service(this, "bg-Service", Ec2ServiceProps.builder()
.cluster(cluster)
.daemon(false)
.taskDefinition(windowsContainerTaskDef)
.deploymentController(DeploymentController.builder()
.type(DeploymentControllerType.CODE_DEPLOY)
.build())
.placementConstraints(singletonList(PlacementConstraint.distinctInstances()))
.desiredCount(2)
.capacityProviderStrategies(singletonList(CapacityProviderStrategy.builder()
.capacityProvider(windowsClusterCapacityProvider.getCapacityProviderName())
.weight(1)
.build()))
.build());
service.registerLoadBalancerTargets(EcsTarget.builder()
.listener(ListenerConfig.networkListener(productionListener, AddNetworkTargetsProps.builder()
.targetGroupName(blueTarget)
.port(80)
.build()))
.containerName(containerName)
.containerPort(80)
.newTargetGroupId(blueTarget)
.build());
// Create code deploy application with a deployment group
EcsApplication ecsApplication = new EcsApplication(this, "BgDeploy", EcsApplicationProps.builder()
.applicationName("BgDeploy-" + clusterSubUniqueId)
.build());
Role serviceRole = new Role(this, "AWSCodeDeployRoleForECS", RoleProps.builder()
.assumedBy(new ServicePrincipal("codedeploy.amazonaws.com"))
.managedPolicies(singletonList(ManagedPolicy.fromAwsManagedPolicyName("AWSCodeDeployRoleForECS")))
.build());
String deploymentGroupName = "BgDeploymentGroup";
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ECS.html#describeTaskDefinition-property
AwsSdkCall createDgCall = AwsSdkCall.builder()
.service("CodeDeploy")
.action("createDeploymentGroup")
.apiVersion("2014-10-06")
.parameters(createDeploymentGroupParam(ecsApplication,
serviceRole,
productionListener,
greenListener,
blueTarget,
targetGreen,
cluster.getClusterName(),
service.getServiceName(),
deploymentGroupName))
.physicalResourceId(PhysicalResourceId.of("CreateDeploymentGroup"))
.build();
AwsSdkCall updateDgCall = AwsSdkCall.builder()
.service("CodeDeploy")
.action("updateDeploymentGroup")
.apiVersion("2014-10-06")
.parameters(updateDeploymentGroup(ecsApplication,
serviceRole,
productionListener,
greenListener,
blueTarget,
targetGreen,
cluster.getClusterName(),
service.getServiceName(),
deploymentGroupName))
.physicalResourceId(PhysicalResourceId.of("UpdateDeploymentGroup"))
.build();
AwsCustomResource.Builder.create(this, "CreateDeploymentGroup")
.onCreate(createDgCall)
.onUpdate(updateDgCall)
.policy(AwsCustomResourcePolicy.fromStatements(Arrays.asList(
PolicyStatement.Builder.create()
.actions(Arrays.asList(
"codedeploy:CreateDeploymentGroup",
"codedeploy:UpdateDeploymentGroup",
"codedeploy:DeleteDeploymentGroup"
))
.effect(Effect.ALLOW)
.resources(singletonList(
"*"
))
.build(),
PolicyStatement.Builder.create()
.effect(Effect.ALLOW)
.actions(singletonList(
"iam:PassRole"
))
.resources(singletonList(
serviceRole.getRoleArn()
))
.build()
)))
.logRetention(RetentionDays.FIVE_DAYS)
.build();
Bucket bgServiceCiCdBucket = new Bucket(this, "bg-service-cicd-bucket", BucketProps.builder()
.versioned(true)
.removalPolicy(RemovalPolicy.DESTROY)
.build());
Path path = Paths.get("asset/appspec_template.yaml");
String appspec = Files.readString(path).replace("<CONTAINER_NAME>", containerName)
.replace("<CONTAINER_PORT>", String.valueOf(containerPort))
.replace("<CAPACITY_PROVIDER>", capacityProviderName);
writeFileAsZipIfChanged(appspec);
new BucketDeployment(this, "CopyAppSecFile", BucketDeploymentProps.builder()
.destinationBucket(bgServiceCiCdBucket)
.prune(false)
.sources(singletonList(Source.asset("asset", AssetOptions.builder()
.exclude(asList("**", "!appspec.zip"))
.build())))
.build());
// Task definition(taskdef.json) and Appspec(appspec_template.yaml) copying
// https://github.com/aws/aws-cdk/issues/8304 until cdk provides this natively
Map<String, Object> properties = new HashMap<>();
properties.put("s3Bucket", bgServiceCiCdBucket.getBucketName());
properties.put("s3Key", "taskdef.zip");
properties.put("imagePlaceHolder", "<IMAGE1_NAME>");
properties.put("InstallLatestAwsSdk", true);
properties.put("TaskDefUniqueId", Names.uniqueId(windowsContainerTaskDef));
properties.put("ServiceName", Names.uniqueId(service));
HashMap<String, String> param = new HashMap<>();
param.put("taskDefinition", windowsContainerTaskDef.getTaskDefinitionArn());
AwsSdkCall getTaskDef = AwsSdkCall.builder()
.service("ECS")
.action("describeTaskDefinition")
.apiVersion("2014-11-13")
.parameters(param)
.physicalResourceId(PhysicalResourceId.of("FetchTaskDef"))
.build();
String propAsMap = mapper().writeValueAsString(getTaskDef);
properties.put("Create", propAsMap);
properties.put("Update", propAsMap);
properties.put("Delete", "");
PolicyStatement policyStatement = new PolicyStatement(PolicyStatementProps.builder()
.resources(singletonList("*"))
.actions(singletonList("ecs:DescribeTaskDefinition"))
.effect(Effect.ALLOW)
.build());
Role fetchTaskDefProviderFunctionRole = new Role(this, "FetchTaskDefProviderFunctionRole", RoleProps.builder()
.assumedBy(new ServicePrincipal("lambda.amazonaws.com"))
.build());
fetchTaskDefProviderFunctionRole.addToPolicy(policyStatement);
bgServiceCiCdBucket.grantReadWrite(fetchTaskDefProviderFunctionRole);
CustomResourceProvider fetchTaskDefProvider = CustomResourceProvider.getOrCreateProvider(this,
"FetchTaskDefProvider",
CustomResourceProviderProps.builder()
.codeDirectory("lambda")
.description("Function to fetch task def")
.runtime(CustomResourceProviderRuntime.NODEJS_14_X)
.timeout(Duration.minutes(1))
.policyStatements(singletonList(policyStatement.toJSON()))
.memorySize(Size.mebibytes(512))
.build());
CustomResource fetchTaskDefJson = new CustomResource(this, "FetchTaskDefJson", CustomResourceProps.builder()
.serviceToken(fetchTaskDefProvider.getServiceToken())
.properties(properties)
.build());
fetchTaskDefJson.getNode().addDependency(service);
IRole taskDefLambdaRole = Role.fromRoleArn(this, "TaskDefLambdaRole", fetchTaskDefProvider.getRoleArn());
bgServiceCiCdBucket.grantReadWrite(taskDefLambdaRole);
new CfnOutput(this, "TaskDefArn", CfnOutputProps.builder()
.value(windowsContainerTaskDef.getTaskDefinitionArn())
.description("TaskDefinition Arn")
.build());
new CfnOutput(this, "LoadBalancerDnsName", CfnOutputProps.builder()
.value(serviceNlb.getLoadBalancerDnsName())
.description("Application URL")
.build());
new CfnOutput(this, "ArtifactSourceBucketArn", CfnOutputProps.builder()
.value(bgServiceCiCdBucket.getBucketArn())
.exportName("ArtifactSourceBucketArn")
.description("ArtifactSourceBucketArn")
.build());
new CfnOutput(this, "CodeDeployDeploymentGroupName", CfnOutputProps.builder()
.value(deploymentGroupName)
.exportName("CodeDeployDeploymentGroupName")
.description("CodeDeployDeploymentGroupName")
.build());
new CfnOutput(this, "CodeDeployApplicationName", CfnOutputProps.builder()
.value(ecsApplication.getApplicationName())
.exportName("CodeDeployApplicationName")
.description("CodeDeployApplicationName")
.build());
}