diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/aws-ecs-blue-green-deployment.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/aws-ecs-blue-green-deployment.assets.json index 6fdc76f55f9bb..694301ea99736 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/aws-ecs-blue-green-deployment.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/aws-ecs-blue-green-deployment.assets.json @@ -1,16 +1,16 @@ { "version": "48.0.0", "files": { - "fd615fb1e7e79dc8dbd0171fa9da178930b6f83121b4aab3b516529352a074f3": { + "3f2de3687de753e01df8af1f3f5db3418f528a660a93dfdd7cb90e7ed75c213f": { "displayName": "aws-ecs-blue-green-deployment Template", "source": { "path": "aws-ecs-blue-green-deployment.template.json", "packaging": "file" }, "destinations": { - "current_account-current_region-40aafb80": { + "current_account-current_region-289748b6": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "fd615fb1e7e79dc8dbd0171fa9da178930b6f83121b4aab3b516529352a074f3.json", + "objectKey": "3f2de3687de753e01df8af1f3f5db3418f528a660a93dfdd7cb90e7ed75c213f.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/aws-ecs-blue-green-deployment.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/aws-ecs-blue-green-deployment.template.json index 76e870cbef2ed..cc03e2ba30041 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/aws-ecs-blue-green-deployment.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/aws-ecs-blue-green-deployment.template.json @@ -614,143 +614,6 @@ "Priority": 1 } }, - "EcsTaskExecutionRoleC0B6A84D": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "ecs-tasks.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" - ] - ] - } - ] - } - }, - "ServiceRole4288B192": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "ecs.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonEC2ContainerServiceRole" - ] - ] - } - ], - "Policies": [ - { - "PolicyDocument": { - "Statement": [ - { - "Action": "lambda:InvokeFunction", - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "LambdaInvokePolicy" - }, - { - "PolicyDocument": { - "Statement": [ - { - "Action": [ - "elasticloadbalancing:ModifyListener", - "elasticloadbalancing:ModifyRule" - ], - "Effect": "Allow", - "Resource": { - "Ref": "ALBProductionListenerRule243D0687" - } - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "ELBPolicy" - } - ] - } - }, - "ServiceRoleDefaultPolicy94CF55F6": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "lambda:InvokeFunction", - "Effect": "Allow", - "Resource": [ - { - "Fn::GetAtt": [ - "LambdaHookBF1BC8B4", - "Arn" - ] - }, - { - "Fn::Join": [ - "", - [ - { - "Fn::GetAtt": [ - "LambdaHookBF1BC8B4", - "Arn" - ] - }, - ":*" - ] - ] - } - ] - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "ServiceRoleDefaultPolicy94CF55F6", - "Roles": [ - { - "Ref": "ServiceRole4288B192" - } - ] - } - }, "LambdaHookServiceRole9AAAD33B": { "Type": "AWS::IAM::Role", "Properties": { @@ -856,12 +719,6 @@ } ], "Cpu": "256", - "ExecutionRoleArn": { - "Fn::GetAtt": [ - "EcsTaskExecutionRoleC0B6A84D", - "Arn" - ] - }, "Family": "awsecsbluegreendeploymentTaskDef51D80572", "Memory": "512", "NetworkMode": "awsvpc", @@ -883,11 +740,6 @@ "Ref": "FargateCluster7CCD5F93" }, "DeploymentConfiguration": { - "BakeTimeInMinutes": 0, - "DeploymentCircuitBreaker": { - "Enable": false, - "Rollback": false - }, "LifecycleHooks": [ { "HookTargetArn": { @@ -896,24 +748,21 @@ "Arn" ] }, + "LifecycleStages": [ + "PRE_SCALE_UP" + ], "RoleArn": { "Fn::GetAtt": [ - "ServiceRole4288B192", + "ServicePreScaleUpRoleD002A553", "Arn" ] - }, - "LifecycleStages": [ - "POST_TEST_TRAFFIC_SHIFT" - ] + } } ], "MaximumPercent": 200, - "MinimumHealthyPercent": 100, + "MinimumHealthyPercent": 50, "Strategy": "BLUE_GREEN" }, - "DeploymentController": { - "Type": "ECS" - }, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -923,14 +772,14 @@ "AlternateTargetGroupArn": { "Ref": "GreenTG71A27F2F" }, + "ProductionListenerRule": { + "Ref": "ALBProductionListenerRule243D0687" + }, "RoleArn": { "Fn::GetAtt": [ - "ServiceRole4288B192", + "ServiceLBAlternateOptionsRole06C91D94", "Arn" ] - }, - "ProductionListenerRule": { - "Ref": "ALBProductionListenerRule243D0687" } }, "ContainerName": "nginx", @@ -969,6 +818,99 @@ "ALBProductionListenerRule243D0687", "TaskDefTaskRole1EDB4A67" ] + }, + "ServiceLBAlternateOptionsRole06C91D94": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers" + ] + ] + } + ] + }, + "DependsOn": [ + "TaskDefTaskRole1EDB4A67" + ] + }, + "ServicePreScaleUpRoleD002A553": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ServicePreScaleUpRoleDefaultPolicyF1032E86": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "LambdaHookBF1BC8B4", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LambdaHookBF1BC8B4", + "Arn" + ] + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ServicePreScaleUpRoleDefaultPolicyF1032E86", + "Roles": [ + { + "Ref": "ServicePreScaleUpRoleD002A553" + } + ] + } } }, "Parameters": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/manifest.json index 6c2b13cd57b6f..8addde70457d7 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/manifest.json @@ -18,7 +18,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/fd615fb1e7e79dc8dbd0171fa9da178930b6f83121b4aab3b516529352a074f3.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3f2de3687de753e01df8af1f3f5db3418f528a660a93dfdd7cb90e7ed75c213f.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -498,123 +498,6 @@ "data": "ALBProductionListenerRule243D0687" } ], - "/aws-ecs-blue-green-deployment/EcsTaskExecutionRole": [ - { - "type": "aws:cdk:analytics:construct", - "data": { - "assumedBy": { - "principalAccount": "*", - "assumeRoleAction": "*" - }, - "managedPolicies": [ - { - "managedPolicyArn": "*" - } - ] - } - } - ], - "/aws-ecs-blue-green-deployment/EcsTaskExecutionRole/ImportEcsTaskExecutionRole": [ - { - "type": "aws:cdk:analytics:construct", - "data": "*" - } - ], - "/aws-ecs-blue-green-deployment/EcsTaskExecutionRole/Resource": [ - { - "type": "aws:cdk:logicalId", - "data": "EcsTaskExecutionRoleC0B6A84D" - } - ], - "/aws-ecs-blue-green-deployment/ServiceRole": [ - { - "type": "aws:cdk:analytics:construct", - "data": { - "assumedBy": { - "principalAccount": "*", - "assumeRoleAction": "*" - }, - "managedPolicies": [ - { - "managedPolicyArn": "*" - } - ], - "inlinePolicies": "*" - } - }, - { - "type": "aws:cdk:analytics:method", - "data": { - "addToPrincipalPolicy": [ - {} - ] - } - }, - { - "type": "aws:cdk:analytics:method", - "data": { - "attachInlinePolicy": [ - "*" - ] - } - }, - { - "type": "aws:cdk:analytics:method", - "data": { - "attachInlinePolicy": [ - "*" - ] - } - } - ], - "/aws-ecs-blue-green-deployment/ServiceRole/ImportServiceRole": [ - { - "type": "aws:cdk:analytics:construct", - "data": "*" - } - ], - "/aws-ecs-blue-green-deployment/ServiceRole/Resource": [ - { - "type": "aws:cdk:logicalId", - "data": "ServiceRole4288B192" - } - ], - "/aws-ecs-blue-green-deployment/ServiceRole/DefaultPolicy": [ - { - "type": "aws:cdk:analytics:construct", - "data": "*" - }, - { - "type": "aws:cdk:analytics:method", - "data": { - "attachToRole": [ - "*" - ] - } - }, - { - "type": "aws:cdk:analytics:method", - "data": { - "attachToRole": [ - "*" - ] - } - }, - { - "type": "aws:cdk:analytics:method", - "data": { - "addStatements": [ - {} - ] - } - } - ], - "/aws-ecs-blue-green-deployment/ServiceRole/DefaultPolicy/Resource": [ - { - "type": "aws:cdk:logicalId", - "data": "ServiceRoleDefaultPolicy94CF55F6" - } - ], "/aws-ecs-blue-green-deployment/LambdaHook": [ { "type": "aws:cdk:analytics:construct", @@ -742,6 +625,117 @@ ] } ], + "/aws-ecs-blue-green-deployment/Service/LBAlternateOptionsRole": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "assumedBy": { + "principalAccount": "*", + "assumeRoleAction": "*" + }, + "managedPolicies": [ + { + "managedPolicyArn": "*" + } + ] + } + } + ], + "/aws-ecs-blue-green-deployment/Service/LBAlternateOptionsRole/ImportLBAlternateOptionsRole": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/aws-ecs-blue-green-deployment/Service/LBAlternateOptionsRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ServiceLBAlternateOptionsRole06C91D94" + } + ], + "/aws-ecs-blue-green-deployment/Service/PreScaleUpRole": [ + { + "type": "aws:cdk:analytics:construct", + "data": { + "assumedBy": { + "principalAccount": "*", + "assumeRoleAction": "*" + } + } + }, + { + "type": "aws:cdk:analytics:method", + "data": { + "addToPrincipalPolicy": [ + {} + ] + } + }, + { + "type": "aws:cdk:analytics:method", + "data": { + "attachInlinePolicy": [ + "*" + ] + } + }, + { + "type": "aws:cdk:analytics:method", + "data": { + "attachInlinePolicy": [ + "*" + ] + } + } + ], + "/aws-ecs-blue-green-deployment/Service/PreScaleUpRole/ImportPreScaleUpRole": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + } + ], + "/aws-ecs-blue-green-deployment/Service/PreScaleUpRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ServicePreScaleUpRoleD002A553" + } + ], + "/aws-ecs-blue-green-deployment/Service/PreScaleUpRole/DefaultPolicy": [ + { + "type": "aws:cdk:analytics:construct", + "data": "*" + }, + { + "type": "aws:cdk:analytics:method", + "data": { + "attachToRole": [ + "*" + ] + } + }, + { + "type": "aws:cdk:analytics:method", + "data": { + "attachToRole": [ + "*" + ] + } + }, + { + "type": "aws:cdk:analytics:method", + "data": { + "addStatements": [ + {} + ] + } + } + ], + "/aws-ecs-blue-green-deployment/Service/PreScaleUpRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ServicePreScaleUpRoleDefaultPolicyF1032E86" + } + ], "/aws-ecs-blue-green-deployment/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/tree.json index 707ef48bf14cc..c6da4e094e80f 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.js.snapshot/tree.json @@ -1 +1 @@ -{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"0.0.0"},"children":{"aws-ecs-blue-green-deployment":{"id":"aws-ecs-blue-green-deployment","path":"aws-ecs-blue-green-deployment","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"Vpc":{"id":"Vpc","path":"aws-ecs-blue-green-deployment/Vpc","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.Vpc","version":"0.0.0","metadata":[{"maxAzs":"*","restrictDefaultSecurityGroup":false}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/Vpc/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnVPC","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::VPC","aws:cdk:cloudformation:props":{"cidrBlock":"10.0.0.0/16","enableDnsHostnames":true,"enableDnsSupport":true,"instanceTenancy":"default","tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc"}]}}},"PublicSubnet1":{"id":"PublicSubnet1","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.PublicSubnet","version":"0.0.0","metadata":[{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":true,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":true,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{},{"addNatGateway":["*"]}]},"children":{"Subnet":{"id":"Subnet","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/Subnet","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnet","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Subnet","aws:cdk:cloudformation:props":{"availabilityZone":{"Fn::Select":[0,{"Fn::GetAZs":""}]},"cidrBlock":"10.0.0.0/18","mapPublicIpOnLaunch":true,"tags":[{"key":"aws-cdk:subnet-name","value":"Public"},{"key":"aws-cdk:subnet-type","value":"Public"},{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"Acl":{"id":"Acl","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/Acl","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":[]}},"RouteTable":{"id":"RouteTable","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/RouteTable","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRouteTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::RouteTable","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"RouteTableAssociation":{"id":"RouteTableAssociation","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/RouteTableAssociation","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SubnetRouteTableAssociation","aws:cdk:cloudformation:props":{"routeTableId":{"Ref":"VpcPublicSubnet1RouteTable6C95E38E"},"subnetId":{"Ref":"VpcPublicSubnet1Subnet5C2D37C4"}}}},"DefaultRoute":{"id":"DefaultRoute","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/DefaultRoute","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRoute","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Route","aws:cdk:cloudformation:props":{"destinationCidrBlock":"0.0.0.0/0","gatewayId":{"Ref":"VpcIGWD7BA715C"},"routeTableId":{"Ref":"VpcPublicSubnet1RouteTable6C95E38E"}}}},"EIP":{"id":"EIP","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/EIP","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnEIP","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::EIP","aws:cdk:cloudformation:props":{"domain":"vpc","tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1"}]}}},"NATGateway":{"id":"NATGateway","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/NATGateway","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnNatGateway","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::NatGateway","aws:cdk:cloudformation:props":{"allocationId":{"Fn::GetAtt":["VpcPublicSubnet1EIPD7E02669","AllocationId"]},"subnetId":{"Ref":"VpcPublicSubnet1Subnet5C2D37C4"},"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1"}]}}}}},"PublicSubnet2":{"id":"PublicSubnet2","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.PublicSubnet","version":"0.0.0","metadata":[{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":true,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":true,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{},{"addNatGateway":["*"]}]},"children":{"Subnet":{"id":"Subnet","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/Subnet","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnet","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Subnet","aws:cdk:cloudformation:props":{"availabilityZone":{"Fn::Select":[1,{"Fn::GetAZs":""}]},"cidrBlock":"10.0.64.0/18","mapPublicIpOnLaunch":true,"tags":[{"key":"aws-cdk:subnet-name","value":"Public"},{"key":"aws-cdk:subnet-type","value":"Public"},{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"Acl":{"id":"Acl","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/Acl","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":[]}},"RouteTable":{"id":"RouteTable","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/RouteTable","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRouteTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::RouteTable","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"RouteTableAssociation":{"id":"RouteTableAssociation","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/RouteTableAssociation","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SubnetRouteTableAssociation","aws:cdk:cloudformation:props":{"routeTableId":{"Ref":"VpcPublicSubnet2RouteTable94F7E489"},"subnetId":{"Ref":"VpcPublicSubnet2Subnet691E08A3"}}}},"DefaultRoute":{"id":"DefaultRoute","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/DefaultRoute","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRoute","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Route","aws:cdk:cloudformation:props":{"destinationCidrBlock":"0.0.0.0/0","gatewayId":{"Ref":"VpcIGWD7BA715C"},"routeTableId":{"Ref":"VpcPublicSubnet2RouteTable94F7E489"}}}},"EIP":{"id":"EIP","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/EIP","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnEIP","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::EIP","aws:cdk:cloudformation:props":{"domain":"vpc","tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2"}]}}},"NATGateway":{"id":"NATGateway","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/NATGateway","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnNatGateway","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::NatGateway","aws:cdk:cloudformation:props":{"allocationId":{"Fn::GetAtt":["VpcPublicSubnet2EIP3C605A87","AllocationId"]},"subnetId":{"Ref":"VpcPublicSubnet2Subnet691E08A3"},"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2"}]}}}}},"PrivateSubnet1":{"id":"PrivateSubnet1","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.PrivateSubnet","version":"0.0.0","metadata":[{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":false,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":false,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{}]},"children":{"Subnet":{"id":"Subnet","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/Subnet","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnet","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Subnet","aws:cdk:cloudformation:props":{"availabilityZone":{"Fn::Select":[0,{"Fn::GetAZs":""}]},"cidrBlock":"10.0.128.0/18","mapPublicIpOnLaunch":false,"tags":[{"key":"aws-cdk:subnet-name","value":"Private"},{"key":"aws-cdk:subnet-type","value":"Private"},{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"Acl":{"id":"Acl","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/Acl","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":[]}},"RouteTable":{"id":"RouteTable","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/RouteTable","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRouteTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::RouteTable","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"RouteTableAssociation":{"id":"RouteTableAssociation","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/RouteTableAssociation","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SubnetRouteTableAssociation","aws:cdk:cloudformation:props":{"routeTableId":{"Ref":"VpcPrivateSubnet1RouteTableB2C5B500"},"subnetId":{"Ref":"VpcPrivateSubnet1Subnet536B997A"}}}},"DefaultRoute":{"id":"DefaultRoute","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/DefaultRoute","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRoute","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Route","aws:cdk:cloudformation:props":{"destinationCidrBlock":"0.0.0.0/0","natGatewayId":{"Ref":"VpcPublicSubnet1NATGateway4D7517AA"},"routeTableId":{"Ref":"VpcPrivateSubnet1RouteTableB2C5B500"}}}}}},"PrivateSubnet2":{"id":"PrivateSubnet2","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.PrivateSubnet","version":"0.0.0","metadata":[{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":false,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":false,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{}]},"children":{"Subnet":{"id":"Subnet","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/Subnet","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnet","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Subnet","aws:cdk:cloudformation:props":{"availabilityZone":{"Fn::Select":[1,{"Fn::GetAZs":""}]},"cidrBlock":"10.0.192.0/18","mapPublicIpOnLaunch":false,"tags":[{"key":"aws-cdk:subnet-name","value":"Private"},{"key":"aws-cdk:subnet-type","value":"Private"},{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"Acl":{"id":"Acl","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/Acl","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":[]}},"RouteTable":{"id":"RouteTable","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/RouteTable","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRouteTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::RouteTable","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"RouteTableAssociation":{"id":"RouteTableAssociation","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/RouteTableAssociation","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SubnetRouteTableAssociation","aws:cdk:cloudformation:props":{"routeTableId":{"Ref":"VpcPrivateSubnet2RouteTableA678073B"},"subnetId":{"Ref":"VpcPrivateSubnet2Subnet3788AAA1"}}}},"DefaultRoute":{"id":"DefaultRoute","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/DefaultRoute","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRoute","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Route","aws:cdk:cloudformation:props":{"destinationCidrBlock":"0.0.0.0/0","natGatewayId":{"Ref":"VpcPublicSubnet2NATGateway9182C01D"},"routeTableId":{"Ref":"VpcPrivateSubnet2RouteTableA678073B"}}}}}},"IGW":{"id":"IGW","path":"aws-ecs-blue-green-deployment/Vpc/IGW","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnInternetGateway","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::InternetGateway","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc"}]}}},"VPCGW":{"id":"VPCGW","path":"aws-ecs-blue-green-deployment/Vpc/VPCGW","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnVPCGatewayAttachment","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::VPCGatewayAttachment","aws:cdk:cloudformation:props":{"internetGatewayId":{"Ref":"VpcIGWD7BA715C"},"vpcId":{"Ref":"Vpc8378EB38"}}}}}},"FargateCluster":{"id":"FargateCluster","path":"aws-ecs-blue-green-deployment/FargateCluster","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.Cluster","version":"0.0.0","metadata":[{"vpc":"*","defaultCloudMapNamespace":{"name":"*","useForServiceConnect":true}},{"addDefaultCloudMapNamespace":[{"name":"*","useForServiceConnect":true}]}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/FargateCluster/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.CfnCluster","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ECS::Cluster","aws:cdk:cloudformation:props":{"serviceConnectDefaults":{"namespace":{"Fn::GetAtt":["FargateClusterDefaultServiceDiscoveryNamespace04381E1E","Arn"]}}}}},"DefaultServiceDiscoveryNamespace":{"id":"DefaultServiceDiscoveryNamespace","path":"aws-ecs-blue-green-deployment/FargateCluster/DefaultServiceDiscoveryNamespace","constructInfo":{"fqn":"aws-cdk-lib.aws_servicediscovery.PrivateDnsNamespace","version":"0.0.0","metadata":[{"name":"*","vpc":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/FargateCluster/DefaultServiceDiscoveryNamespace/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_servicediscovery.CfnPrivateDnsNamespace","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ServiceDiscovery::PrivateDnsNamespace","aws:cdk:cloudformation:props":{"name":"bluegreendeployment.com","vpc":{"Ref":"Vpc8378EB38"}}}}}}}},"BlueTG":{"id":"BlueTG","path":"aws-ecs-blue-green-deployment/BlueTG","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationTargetGroup","version":"0.0.0"},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/BlueTG/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnTargetGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::TargetGroup","aws:cdk:cloudformation:props":{"healthCheckPath":"/","matcher":{"httpCode":"200"},"port":80,"protocol":"HTTP","targetGroupAttributes":[{"key":"stickiness.enabled","value":"false"}],"targetType":"ip","vpcId":{"Ref":"Vpc8378EB38"}}}}}},"GreenTG":{"id":"GreenTG","path":"aws-ecs-blue-green-deployment/GreenTG","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationTargetGroup","version":"0.0.0"},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/GreenTG/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnTargetGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::TargetGroup","aws:cdk:cloudformation:props":{"healthCheckPath":"/","matcher":{"httpCode":"200"},"port":80,"protocol":"HTTP","targetGroupAttributes":[{"key":"stickiness.enabled","value":"false"}],"targetType":"ip","vpcId":{"Ref":"Vpc8378EB38"}}}}}},"LBSecurityGroup":{"id":"LBSecurityGroup","path":"aws-ecs-blue-green-deployment/LBSecurityGroup","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.SecurityGroup","version":"0.0.0","metadata":[{"vpc":"*","allowAllOutbound":true},{"addIngressRule":[{"canInlineRule":true,"connections":"*","uniqueId":"*"},{}]},{"addIngressRule":[{"canInlineRule":true,"connections":"*","uniqueId":"*"},{},"*",false]},{"addEgressRule":["*",{},"*",true]}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/LBSecurityGroup/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSecurityGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SecurityGroup","aws:cdk:cloudformation:props":{"groupDescription":"aws-ecs-blue-green-deployment/LBSecurityGroup","securityGroupEgress":[{"cidrIp":"0.0.0.0/0","description":"Allow all outbound traffic by default","ipProtocol":"-1"}],"securityGroupIngress":[{"cidrIp":"0.0.0.0/0","ipProtocol":"tcp","fromPort":80,"toPort":80,"description":"from 0.0.0.0/0:80"}],"vpcId":{"Ref":"Vpc8378EB38"}}}}}},"ECSSecurityGroup":{"id":"ECSSecurityGroup","path":"aws-ecs-blue-green-deployment/ECSSecurityGroup","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.SecurityGroup","version":"0.0.0","metadata":[{"vpc":"*","allowAllOutbound":true},{"addIngressRule":["*",{}]},{"addIngressRule":["*",{},"*",false]}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ECSSecurityGroup/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSecurityGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SecurityGroup","aws:cdk:cloudformation:props":{"groupDescription":"aws-ecs-blue-green-deployment/ECSSecurityGroup","securityGroupEgress":[{"cidrIp":"0.0.0.0/0","description":"Allow all outbound traffic by default","ipProtocol":"-1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"from awsecsbluegreendeploymentLBSecurityGroup67E4A783:80":{"id":"from awsecsbluegreendeploymentLBSecurityGroup67E4A783:80","path":"aws-ecs-blue-green-deployment/ECSSecurityGroup/from awsecsbluegreendeploymentLBSecurityGroup67E4A783:80","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSecurityGroupIngress","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SecurityGroupIngress","aws:cdk:cloudformation:props":{"description":"from awsecsbluegreendeploymentLBSecurityGroup67E4A783:80","fromPort":80,"groupId":{"Fn::GetAtt":["ECSSecurityGroupA14DBE7D","GroupId"]},"ipProtocol":"tcp","sourceSecurityGroupId":{"Fn::GetAtt":["LBSecurityGroup4464B654","GroupId"]},"toPort":80}}}}},"ALB":{"id":"ALB","path":"aws-ecs-blue-green-deployment/ALB","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer","version":"0.0.0","metadata":["*","*"]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ALB/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnLoadBalancer","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::LoadBalancer","aws:cdk:cloudformation:props":{"loadBalancerAttributes":[{"key":"deletion_protection.enabled","value":"false"},{"key":"idle_timeout.timeout_seconds","value":"60"}],"scheme":"internet-facing","securityGroups":[{"Fn::GetAtt":["LBSecurityGroup4464B654","GroupId"]}],"subnets":[{"Ref":"VpcPublicSubnet1Subnet5C2D37C4"},{"Ref":"VpcPublicSubnet2Subnet691E08A3"}],"type":"application"}}},"ALBListenerHTTP":{"id":"ALBListenerHTTP","path":"aws-ecs-blue-green-deployment/ALB/ALBListenerHTTP","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationListener","version":"0.0.0","metadata":["*","*"]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ALB/ALBListenerHTTP/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnListener","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::Listener","aws:cdk:cloudformation:props":{"defaultActions":[{"type":"fixed-response","fixedResponseConfig":{"statusCode":"404"}}],"loadBalancerArn":{"Ref":"ALBAEE750D2"},"port":80,"protocol":"HTTP"}}}}}}},"ALBProductionListenerRule":{"id":"ALBProductionListenerRule","path":"aws-ecs-blue-green-deployment/ALBProductionListenerRule","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationListenerRule","version":"0.0.0"},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ALBProductionListenerRule/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnListenerRule","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::ListenerRule","aws:cdk:cloudformation:props":{"actions":[{"type":"forward","forwardConfig":{"targetGroups":[{"targetGroupArn":{"Ref":"BlueTG7C058B2C"},"weight":100},{"targetGroupArn":{"Ref":"GreenTG71A27F2F"},"weight":0}]}}],"conditions":[{"field":"path-pattern","pathPatternConfig":{"values":["/*"]}}],"listenerArn":{"Ref":"ALBALBListenerHTTP413250C3"},"priority":1}}}}},"EcsTaskExecutionRole":{"id":"EcsTaskExecutionRole","path":"aws-ecs-blue-green-deployment/EcsTaskExecutionRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"},"managedPolicies":[{"managedPolicyArn":"*"}]}]},"children":{"ImportEcsTaskExecutionRole":{"id":"ImportEcsTaskExecutionRole","path":"aws-ecs-blue-green-deployment/EcsTaskExecutionRole/ImportEcsTaskExecutionRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/EcsTaskExecutionRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"]]}]}}}}},"ServiceRole":{"id":"ServiceRole","path":"aws-ecs-blue-green-deployment/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"},"managedPolicies":[{"managedPolicyArn":"*"}],"inlinePolicies":"*"},{"addToPrincipalPolicy":[{}]},{"attachInlinePolicy":["*"]},{"attachInlinePolicy":["*"]}]},"children":{"ImportServiceRole":{"id":"ImportServiceRole","path":"aws-ecs-blue-green-deployment/ServiceRole/ImportServiceRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"ecs.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AmazonEC2ContainerServiceRole"]]}],"policies":[{"policyName":"LambdaInvokePolicy","policyDocument":{"Statement":[{"Action":"lambda:InvokeFunction","Effect":"Allow","Resource":"*"}],"Version":"2012-10-17"}},{"policyName":"ELBPolicy","policyDocument":{"Statement":[{"Action":["elasticloadbalancing:ModifyListener","elasticloadbalancing:ModifyRule"],"Effect":"Allow","Resource":{"Ref":"ALBProductionListenerRule243D0687"}}],"Version":"2012-10-17"}}]}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"aws-ecs-blue-green-deployment/ServiceRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"0.0.0","metadata":["*",{"attachToRole":["*"]},{"attachToRole":["*"]},{"addStatements":[{}]}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ServiceRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":"lambda:InvokeFunction","Effect":"Allow","Resource":[{"Fn::GetAtt":["LambdaHookBF1BC8B4","Arn"]},{"Fn::Join":["",[{"Fn::GetAtt":["LambdaHookBF1BC8B4","Arn"]},":*"]]}]}],"Version":"2012-10-17"},"policyName":"ServiceRoleDefaultPolicy94CF55F6","roles":[{"Ref":"ServiceRole4288B192"}]}}}}}}},"LambdaHook":{"id":"LambdaHook","path":"aws-ecs-blue-green-deployment/LambdaHook","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"0.0.0","metadata":[{"handler":"*","runtime":"*","code":"*"}]},"children":{"ServiceRole":{"id":"ServiceRole","path":"aws-ecs-blue-green-deployment/LambdaHook/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"},"managedPolicies":[{"managedPolicyArn":"*"}]}]},"children":{"ImportServiceRole":{"id":"ImportServiceRole","path":"aws-ecs-blue-green-deployment/LambdaHook/ServiceRole/ImportServiceRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/LambdaHook/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}}}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/LambdaHook/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:props":{"code":{"zipFile":"exports.handler = async (event, context) => {\n console.log('Event received:', JSON.stringify(event));\n return { hookStatus: 'SUCCEEDED' }; \n };"},"handler":"index.handler","role":{"Fn::GetAtt":["LambdaHookServiceRole9AAAD33B","Arn"]},"runtime":"nodejs22.x"}}},"LogGroup":{"id":"LogGroup","path":"aws-ecs-blue-green-deployment/LambdaHook/LogGroup","constructInfo":{"fqn":"aws-cdk-lib.aws_logs.LogGroup","version":"0.0.0","metadata":[{"logGroupName":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/LambdaHook/LogGroup/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_logs.CfnLogGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Logs::LogGroup","aws:cdk:cloudformation:props":{"logGroupName":{"Fn::Join":["",["/aws/lambda/",{"Ref":"LambdaHookBF1BC8B4"}]]},"retentionInDays":731}}}}}}},"TaskDef":{"id":"TaskDef","path":"aws-ecs-blue-green-deployment/TaskDef","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.FargateTaskDefinition","version":"0.0.0","metadata":["*","*","*","*"]},"children":{"TaskRole":{"id":"TaskRole","path":"aws-ecs-blue-green-deployment/TaskDef/TaskRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"}}]},"children":{"ImportTaskRole":{"id":"ImportTaskRole","path":"aws-ecs-blue-green-deployment/TaskDef/TaskRole/ImportTaskRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/TaskDef/TaskRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"}}],"Version":"2012-10-17"}}}}}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/TaskDef/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.CfnTaskDefinition","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ECS::TaskDefinition","aws:cdk:cloudformation:props":{"containerDefinitions":[{"essential":true,"image":"public.ecr.aws/nginx/nginx:latest","name":"nginx","portMappings":[{"containerPort":80,"protocol":"tcp","appProtocol":"http","name":"api"}]}],"cpu":"256","executionRoleArn":{"Fn::GetAtt":["EcsTaskExecutionRoleC0B6A84D","Arn"]},"family":"awsecsbluegreendeploymentTaskDef51D80572","memory":"512","networkMode":"awsvpc","requiresCompatibilities":["FARGATE"],"taskRoleArn":{"Fn::GetAtt":["TaskDefTaskRole1EDB4A67","Arn"]}}}},"container":{"id":"container","path":"aws-ecs-blue-green-deployment/TaskDef/container","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.ContainerDefinition","version":"0.0.0"}}}},"Service":{"id":"Service","path":"aws-ecs-blue-green-deployment/Service","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.FargateService","version":"0.0.0","metadata":["*"]},"children":{"Service":{"id":"Service","path":"aws-ecs-blue-green-deployment/Service/Service","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.CfnService","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ECS::Service","aws:cdk:cloudformation:props":{"cluster":{"Ref":"FargateCluster7CCD5F93"},"deploymentConfiguration":{"maximumPercent":200,"minimumHealthyPercent":50},"enableEcsManagedTags":false,"healthCheckGracePeriodSeconds":60,"launchType":"FARGATE","loadBalancers":[{"targetGroupArn":{"Ref":"BlueTG7C058B2C"},"containerName":"nginx","containerPort":80}],"networkConfiguration":{"awsvpcConfiguration":{"assignPublicIp":"DISABLED","subnets":[{"Ref":"VpcPrivateSubnet1Subnet536B997A"},{"Ref":"VpcPrivateSubnet2Subnet3788AAA1"}],"securityGroups":[{"Fn::GetAtt":["ECSSecurityGroupA14DBE7D","GroupId"]}]}},"taskDefinition":{"Ref":"TaskDef54694570"}}}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"aws-ecs-blue-green-deployment/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"aws-ecs-blue-green-deployment/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}},"aws-ecs-blue-green":{"id":"aws-ecs-blue-green","path":"aws-ecs-blue-green","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTest","version":"0.0.0"},"children":{"DefaultTest":{"id":"DefaultTest","path":"aws-ecs-blue-green/DefaultTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTestCase","version":"0.0.0"},"children":{"Default":{"id":"Default","path":"aws-ecs-blue-green/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"DeployAssert":{"id":"DeployAssert","path":"aws-ecs-blue-green/DefaultTest/DeployAssert","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"aws-ecs-blue-green/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"aws-ecs-blue-green/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}}}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}}} \ No newline at end of file +{"version":"tree-0.1","tree":{"id":"App","path":"","constructInfo":{"fqn":"aws-cdk-lib.App","version":"0.0.0"},"children":{"aws-ecs-blue-green-deployment":{"id":"aws-ecs-blue-green-deployment","path":"aws-ecs-blue-green-deployment","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"Vpc":{"id":"Vpc","path":"aws-ecs-blue-green-deployment/Vpc","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.Vpc","version":"0.0.0","metadata":[{"maxAzs":"*","restrictDefaultSecurityGroup":false}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/Vpc/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnVPC","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::VPC","aws:cdk:cloudformation:props":{"cidrBlock":"10.0.0.0/16","enableDnsHostnames":true,"enableDnsSupport":true,"instanceTenancy":"default","tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc"}]}}},"PublicSubnet1":{"id":"PublicSubnet1","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.PublicSubnet","version":"0.0.0","metadata":[{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":true,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":true,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{},{"addNatGateway":["*"]}]},"children":{"Subnet":{"id":"Subnet","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/Subnet","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnet","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Subnet","aws:cdk:cloudformation:props":{"availabilityZone":{"Fn::Select":[0,{"Fn::GetAZs":""}]},"cidrBlock":"10.0.0.0/18","mapPublicIpOnLaunch":true,"tags":[{"key":"aws-cdk:subnet-name","value":"Public"},{"key":"aws-cdk:subnet-type","value":"Public"},{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"Acl":{"id":"Acl","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/Acl","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":[]}},"RouteTable":{"id":"RouteTable","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/RouteTable","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRouteTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::RouteTable","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"RouteTableAssociation":{"id":"RouteTableAssociation","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/RouteTableAssociation","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SubnetRouteTableAssociation","aws:cdk:cloudformation:props":{"routeTableId":{"Ref":"VpcPublicSubnet1RouteTable6C95E38E"},"subnetId":{"Ref":"VpcPublicSubnet1Subnet5C2D37C4"}}}},"DefaultRoute":{"id":"DefaultRoute","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/DefaultRoute","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRoute","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Route","aws:cdk:cloudformation:props":{"destinationCidrBlock":"0.0.0.0/0","gatewayId":{"Ref":"VpcIGWD7BA715C"},"routeTableId":{"Ref":"VpcPublicSubnet1RouteTable6C95E38E"}}}},"EIP":{"id":"EIP","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/EIP","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnEIP","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::EIP","aws:cdk:cloudformation:props":{"domain":"vpc","tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1"}]}}},"NATGateway":{"id":"NATGateway","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1/NATGateway","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnNatGateway","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::NatGateway","aws:cdk:cloudformation:props":{"allocationId":{"Fn::GetAtt":["VpcPublicSubnet1EIPD7E02669","AllocationId"]},"subnetId":{"Ref":"VpcPublicSubnet1Subnet5C2D37C4"},"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet1"}]}}}}},"PublicSubnet2":{"id":"PublicSubnet2","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.PublicSubnet","version":"0.0.0","metadata":[{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":true,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":true,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{},{"addNatGateway":["*"]}]},"children":{"Subnet":{"id":"Subnet","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/Subnet","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnet","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Subnet","aws:cdk:cloudformation:props":{"availabilityZone":{"Fn::Select":[1,{"Fn::GetAZs":""}]},"cidrBlock":"10.0.64.0/18","mapPublicIpOnLaunch":true,"tags":[{"key":"aws-cdk:subnet-name","value":"Public"},{"key":"aws-cdk:subnet-type","value":"Public"},{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"Acl":{"id":"Acl","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/Acl","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":[]}},"RouteTable":{"id":"RouteTable","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/RouteTable","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRouteTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::RouteTable","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"RouteTableAssociation":{"id":"RouteTableAssociation","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/RouteTableAssociation","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SubnetRouteTableAssociation","aws:cdk:cloudformation:props":{"routeTableId":{"Ref":"VpcPublicSubnet2RouteTable94F7E489"},"subnetId":{"Ref":"VpcPublicSubnet2Subnet691E08A3"}}}},"DefaultRoute":{"id":"DefaultRoute","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/DefaultRoute","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRoute","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Route","aws:cdk:cloudformation:props":{"destinationCidrBlock":"0.0.0.0/0","gatewayId":{"Ref":"VpcIGWD7BA715C"},"routeTableId":{"Ref":"VpcPublicSubnet2RouteTable94F7E489"}}}},"EIP":{"id":"EIP","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/EIP","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnEIP","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::EIP","aws:cdk:cloudformation:props":{"domain":"vpc","tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2"}]}}},"NATGateway":{"id":"NATGateway","path":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2/NATGateway","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnNatGateway","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::NatGateway","aws:cdk:cloudformation:props":{"allocationId":{"Fn::GetAtt":["VpcPublicSubnet2EIP3C605A87","AllocationId"]},"subnetId":{"Ref":"VpcPublicSubnet2Subnet691E08A3"},"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PublicSubnet2"}]}}}}},"PrivateSubnet1":{"id":"PrivateSubnet1","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.PrivateSubnet","version":"0.0.0","metadata":[{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":false,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":false,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{}]},"children":{"Subnet":{"id":"Subnet","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/Subnet","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnet","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Subnet","aws:cdk:cloudformation:props":{"availabilityZone":{"Fn::Select":[0,{"Fn::GetAZs":""}]},"cidrBlock":"10.0.128.0/18","mapPublicIpOnLaunch":false,"tags":[{"key":"aws-cdk:subnet-name","value":"Private"},{"key":"aws-cdk:subnet-type","value":"Private"},{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"Acl":{"id":"Acl","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/Acl","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":[]}},"RouteTable":{"id":"RouteTable","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/RouteTable","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRouteTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::RouteTable","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"RouteTableAssociation":{"id":"RouteTableAssociation","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/RouteTableAssociation","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SubnetRouteTableAssociation","aws:cdk:cloudformation:props":{"routeTableId":{"Ref":"VpcPrivateSubnet1RouteTableB2C5B500"},"subnetId":{"Ref":"VpcPrivateSubnet1Subnet536B997A"}}}},"DefaultRoute":{"id":"DefaultRoute","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet1/DefaultRoute","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRoute","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Route","aws:cdk:cloudformation:props":{"destinationCidrBlock":"0.0.0.0/0","natGatewayId":{"Ref":"VpcPublicSubnet1NATGateway4D7517AA"},"routeTableId":{"Ref":"VpcPrivateSubnet1RouteTableB2C5B500"}}}}}},"PrivateSubnet2":{"id":"PrivateSubnet2","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.PrivateSubnet","version":"0.0.0","metadata":[{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":false,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{"availabilityZone":"*","vpcId":"*","cidrBlock":"*","mapPublicIpOnLaunch":false,"ipv6CidrBlock":"*","assignIpv6AddressOnCreation":"*"},{}]},"children":{"Subnet":{"id":"Subnet","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/Subnet","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnet","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Subnet","aws:cdk:cloudformation:props":{"availabilityZone":{"Fn::Select":[1,{"Fn::GetAZs":""}]},"cidrBlock":"10.0.192.0/18","mapPublicIpOnLaunch":false,"tags":[{"key":"aws-cdk:subnet-name","value":"Private"},{"key":"aws-cdk:subnet-type","value":"Private"},{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"Acl":{"id":"Acl","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/Acl","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":[]}},"RouteTable":{"id":"RouteTable","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/RouteTable","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRouteTable","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::RouteTable","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"RouteTableAssociation":{"id":"RouteTableAssociation","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/RouteTableAssociation","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SubnetRouteTableAssociation","aws:cdk:cloudformation:props":{"routeTableId":{"Ref":"VpcPrivateSubnet2RouteTableA678073B"},"subnetId":{"Ref":"VpcPrivateSubnet2Subnet3788AAA1"}}}},"DefaultRoute":{"id":"DefaultRoute","path":"aws-ecs-blue-green-deployment/Vpc/PrivateSubnet2/DefaultRoute","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnRoute","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::Route","aws:cdk:cloudformation:props":{"destinationCidrBlock":"0.0.0.0/0","natGatewayId":{"Ref":"VpcPublicSubnet2NATGateway9182C01D"},"routeTableId":{"Ref":"VpcPrivateSubnet2RouteTableA678073B"}}}}}},"IGW":{"id":"IGW","path":"aws-ecs-blue-green-deployment/Vpc/IGW","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnInternetGateway","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::InternetGateway","aws:cdk:cloudformation:props":{"tags":[{"key":"Name","value":"aws-ecs-blue-green-deployment/Vpc"}]}}},"VPCGW":{"id":"VPCGW","path":"aws-ecs-blue-green-deployment/Vpc/VPCGW","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnVPCGatewayAttachment","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::VPCGatewayAttachment","aws:cdk:cloudformation:props":{"internetGatewayId":{"Ref":"VpcIGWD7BA715C"},"vpcId":{"Ref":"Vpc8378EB38"}}}}}},"FargateCluster":{"id":"FargateCluster","path":"aws-ecs-blue-green-deployment/FargateCluster","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.Cluster","version":"0.0.0","metadata":[{"vpc":"*","defaultCloudMapNamespace":{"name":"*","useForServiceConnect":true}},{"addDefaultCloudMapNamespace":[{"name":"*","useForServiceConnect":true}]}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/FargateCluster/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.CfnCluster","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ECS::Cluster","aws:cdk:cloudformation:props":{"serviceConnectDefaults":{"namespace":{"Fn::GetAtt":["FargateClusterDefaultServiceDiscoveryNamespace04381E1E","Arn"]}}}}},"DefaultServiceDiscoveryNamespace":{"id":"DefaultServiceDiscoveryNamespace","path":"aws-ecs-blue-green-deployment/FargateCluster/DefaultServiceDiscoveryNamespace","constructInfo":{"fqn":"aws-cdk-lib.aws_servicediscovery.PrivateDnsNamespace","version":"0.0.0","metadata":[{"name":"*","vpc":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/FargateCluster/DefaultServiceDiscoveryNamespace/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_servicediscovery.CfnPrivateDnsNamespace","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ServiceDiscovery::PrivateDnsNamespace","aws:cdk:cloudformation:props":{"name":"bluegreendeployment.com","vpc":{"Ref":"Vpc8378EB38"}}}}}}}},"BlueTG":{"id":"BlueTG","path":"aws-ecs-blue-green-deployment/BlueTG","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationTargetGroup","version":"0.0.0"},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/BlueTG/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnTargetGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::TargetGroup","aws:cdk:cloudformation:props":{"healthCheckPath":"/","matcher":{"httpCode":"200"},"port":80,"protocol":"HTTP","targetGroupAttributes":[{"key":"stickiness.enabled","value":"false"}],"targetType":"ip","vpcId":{"Ref":"Vpc8378EB38"}}}}}},"GreenTG":{"id":"GreenTG","path":"aws-ecs-blue-green-deployment/GreenTG","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationTargetGroup","version":"0.0.0"},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/GreenTG/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnTargetGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::TargetGroup","aws:cdk:cloudformation:props":{"healthCheckPath":"/","matcher":{"httpCode":"200"},"port":80,"protocol":"HTTP","targetGroupAttributes":[{"key":"stickiness.enabled","value":"false"}],"targetType":"ip","vpcId":{"Ref":"Vpc8378EB38"}}}}}},"LBSecurityGroup":{"id":"LBSecurityGroup","path":"aws-ecs-blue-green-deployment/LBSecurityGroup","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.SecurityGroup","version":"0.0.0","metadata":[{"vpc":"*","allowAllOutbound":true},{"addIngressRule":[{"canInlineRule":true,"connections":"*","uniqueId":"*"},{}]},{"addIngressRule":[{"canInlineRule":true,"connections":"*","uniqueId":"*"},{},"*",false]},{"addEgressRule":["*",{},"*",true]}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/LBSecurityGroup/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSecurityGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SecurityGroup","aws:cdk:cloudformation:props":{"groupDescription":"aws-ecs-blue-green-deployment/LBSecurityGroup","securityGroupEgress":[{"cidrIp":"0.0.0.0/0","description":"Allow all outbound traffic by default","ipProtocol":"-1"}],"securityGroupIngress":[{"cidrIp":"0.0.0.0/0","ipProtocol":"tcp","fromPort":80,"toPort":80,"description":"from 0.0.0.0/0:80"}],"vpcId":{"Ref":"Vpc8378EB38"}}}}}},"ECSSecurityGroup":{"id":"ECSSecurityGroup","path":"aws-ecs-blue-green-deployment/ECSSecurityGroup","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.SecurityGroup","version":"0.0.0","metadata":[{"vpc":"*","allowAllOutbound":true},{"addIngressRule":["*",{}]},{"addIngressRule":["*",{},"*",false]}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ECSSecurityGroup/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSecurityGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SecurityGroup","aws:cdk:cloudformation:props":{"groupDescription":"aws-ecs-blue-green-deployment/ECSSecurityGroup","securityGroupEgress":[{"cidrIp":"0.0.0.0/0","description":"Allow all outbound traffic by default","ipProtocol":"-1"}],"vpcId":{"Ref":"Vpc8378EB38"}}}},"from awsecsbluegreendeploymentLBSecurityGroup67E4A783:80":{"id":"from awsecsbluegreendeploymentLBSecurityGroup67E4A783:80","path":"aws-ecs-blue-green-deployment/ECSSecurityGroup/from awsecsbluegreendeploymentLBSecurityGroup67E4A783:80","constructInfo":{"fqn":"aws-cdk-lib.aws_ec2.CfnSecurityGroupIngress","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::EC2::SecurityGroupIngress","aws:cdk:cloudformation:props":{"description":"from awsecsbluegreendeploymentLBSecurityGroup67E4A783:80","fromPort":80,"groupId":{"Fn::GetAtt":["ECSSecurityGroupA14DBE7D","GroupId"]},"ipProtocol":"tcp","sourceSecurityGroupId":{"Fn::GetAtt":["LBSecurityGroup4464B654","GroupId"]},"toPort":80}}}}},"ALB":{"id":"ALB","path":"aws-ecs-blue-green-deployment/ALB","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer","version":"0.0.0","metadata":["*","*"]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ALB/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnLoadBalancer","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::LoadBalancer","aws:cdk:cloudformation:props":{"loadBalancerAttributes":[{"key":"deletion_protection.enabled","value":"false"},{"key":"idle_timeout.timeout_seconds","value":"60"}],"scheme":"internet-facing","securityGroups":[{"Fn::GetAtt":["LBSecurityGroup4464B654","GroupId"]}],"subnets":[{"Ref":"VpcPublicSubnet1Subnet5C2D37C4"},{"Ref":"VpcPublicSubnet2Subnet691E08A3"}],"type":"application"}}},"ALBListenerHTTP":{"id":"ALBListenerHTTP","path":"aws-ecs-blue-green-deployment/ALB/ALBListenerHTTP","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationListener","version":"0.0.0","metadata":["*","*"]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ALB/ALBListenerHTTP/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnListener","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::Listener","aws:cdk:cloudformation:props":{"defaultActions":[{"type":"fixed-response","fixedResponseConfig":{"statusCode":"404"}}],"loadBalancerArn":{"Ref":"ALBAEE750D2"},"port":80,"protocol":"HTTP"}}}}}}},"ALBProductionListenerRule":{"id":"ALBProductionListenerRule","path":"aws-ecs-blue-green-deployment/ALBProductionListenerRule","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationListenerRule","version":"0.0.0"},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/ALBProductionListenerRule/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_elasticloadbalancingv2.CfnListenerRule","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ElasticLoadBalancingV2::ListenerRule","aws:cdk:cloudformation:props":{"actions":[{"type":"forward","forwardConfig":{"targetGroups":[{"targetGroupArn":{"Ref":"BlueTG7C058B2C"},"weight":100},{"targetGroupArn":{"Ref":"GreenTG71A27F2F"},"weight":0}]}}],"conditions":[{"field":"path-pattern","pathPatternConfig":{"values":["/*"]}}],"listenerArn":{"Ref":"ALBALBListenerHTTP413250C3"},"priority":1}}}}},"LambdaHook":{"id":"LambdaHook","path":"aws-ecs-blue-green-deployment/LambdaHook","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.Function","version":"0.0.0","metadata":[{"handler":"*","runtime":"*","code":"*"}]},"children":{"ServiceRole":{"id":"ServiceRole","path":"aws-ecs-blue-green-deployment/LambdaHook/ServiceRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"},"managedPolicies":[{"managedPolicyArn":"*"}]}]},"children":{"ImportServiceRole":{"id":"ImportServiceRole","path":"aws-ecs-blue-green-deployment/LambdaHook/ServiceRole/ImportServiceRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/LambdaHook/ServiceRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]]}]}}}}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/LambdaHook/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_lambda.CfnFunction","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Lambda::Function","aws:cdk:cloudformation:props":{"code":{"zipFile":"exports.handler = async (event, context) => {\n console.log('Event received:', JSON.stringify(event));\n return { hookStatus: 'SUCCEEDED' }; \n };"},"handler":"index.handler","role":{"Fn::GetAtt":["LambdaHookServiceRole9AAAD33B","Arn"]},"runtime":"nodejs22.x"}}},"LogGroup":{"id":"LogGroup","path":"aws-ecs-blue-green-deployment/LambdaHook/LogGroup","constructInfo":{"fqn":"aws-cdk-lib.aws_logs.LogGroup","version":"0.0.0","metadata":[{"logGroupName":"*"}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/LambdaHook/LogGroup/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_logs.CfnLogGroup","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::Logs::LogGroup","aws:cdk:cloudformation:props":{"logGroupName":{"Fn::Join":["",["/aws/lambda/",{"Ref":"LambdaHookBF1BC8B4"}]]},"retentionInDays":731}}}}}}},"TaskDef":{"id":"TaskDef","path":"aws-ecs-blue-green-deployment/TaskDef","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.FargateTaskDefinition","version":"0.0.0","metadata":["*","*","*","*"]},"children":{"TaskRole":{"id":"TaskRole","path":"aws-ecs-blue-green-deployment/TaskDef/TaskRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"}}]},"children":{"ImportTaskRole":{"id":"ImportTaskRole","path":"aws-ecs-blue-green-deployment/TaskDef/TaskRole/ImportTaskRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/TaskDef/TaskRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"ecs-tasks.amazonaws.com"}}],"Version":"2012-10-17"}}}}}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/TaskDef/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.CfnTaskDefinition","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ECS::TaskDefinition","aws:cdk:cloudformation:props":{"containerDefinitions":[{"essential":true,"image":"public.ecr.aws/nginx/nginx:latest","name":"nginx","portMappings":[{"containerPort":80,"protocol":"tcp","appProtocol":"http","name":"api"}]}],"cpu":"256","family":"awsecsbluegreendeploymentTaskDef51D80572","memory":"512","networkMode":"awsvpc","requiresCompatibilities":["FARGATE"],"taskRoleArn":{"Fn::GetAtt":["TaskDefTaskRole1EDB4A67","Arn"]}}}},"container":{"id":"container","path":"aws-ecs-blue-green-deployment/TaskDef/container","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.ContainerDefinition","version":"0.0.0"}}}},"Service":{"id":"Service","path":"aws-ecs-blue-green-deployment/Service","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.FargateService","version":"0.0.0","metadata":["*"]},"children":{"Service":{"id":"Service","path":"aws-ecs-blue-green-deployment/Service/Service","constructInfo":{"fqn":"aws-cdk-lib.aws_ecs.CfnService","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::ECS::Service","aws:cdk:cloudformation:props":{"cluster":{"Ref":"FargateCluster7CCD5F93"},"deploymentConfiguration":{"maximumPercent":200,"minimumHealthyPercent":50,"strategy":"BLUE_GREEN","lifecycleHooks":[{"hookTargetArn":{"Fn::GetAtt":["LambdaHookBF1BC8B4","Arn"]},"roleArn":{"Fn::GetAtt":["ServicePreScaleUpRoleD002A553","Arn"]},"lifecycleStages":["PRE_SCALE_UP"]}]},"enableEcsManagedTags":false,"healthCheckGracePeriodSeconds":60,"launchType":"FARGATE","loadBalancers":[{"targetGroupArn":{"Ref":"BlueTG7C058B2C"},"containerName":"nginx","containerPort":80,"advancedConfiguration":{"alternateTargetGroupArn":{"Ref":"GreenTG71A27F2F"},"roleArn":{"Fn::GetAtt":["ServiceLBAlternateOptionsRole06C91D94","Arn"]},"productionListenerRule":{"Ref":"ALBProductionListenerRule243D0687"}}}],"networkConfiguration":{"awsvpcConfiguration":{"assignPublicIp":"DISABLED","subnets":[{"Ref":"VpcPrivateSubnet1Subnet536B997A"},{"Ref":"VpcPrivateSubnet2Subnet3788AAA1"}],"securityGroups":[{"Fn::GetAtt":["ECSSecurityGroupA14DBE7D","GroupId"]}]}},"taskDefinition":{"Ref":"TaskDef54694570"}}}},"LBAlternateOptionsRole":{"id":"LBAlternateOptionsRole","path":"aws-ecs-blue-green-deployment/Service/LBAlternateOptionsRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"},"managedPolicies":[{"managedPolicyArn":"*"}]}]},"children":{"ImportLBAlternateOptionsRole":{"id":"ImportLBAlternateOptionsRole","path":"aws-ecs-blue-green-deployment/Service/LBAlternateOptionsRole/ImportLBAlternateOptionsRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/Service/LBAlternateOptionsRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"ecs.amazonaws.com"}}],"Version":"2012-10-17"},"managedPolicyArns":[{"Fn::Join":["",["arn:",{"Ref":"AWS::Partition"},":iam::aws:policy/AmazonECSInfrastructureRolePolicyForLoadBalancers"]]}]}}}}},"PreScaleUpRole":{"id":"PreScaleUpRole","path":"aws-ecs-blue-green-deployment/Service/PreScaleUpRole","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Role","version":"0.0.0","metadata":[{"assumedBy":{"principalAccount":"*","assumeRoleAction":"*"}},{"addToPrincipalPolicy":[{}]},{"attachInlinePolicy":["*"]},{"attachInlinePolicy":["*"]}]},"children":{"ImportPreScaleUpRole":{"id":"ImportPreScaleUpRole","path":"aws-ecs-blue-green-deployment/Service/PreScaleUpRole/ImportPreScaleUpRole","constructInfo":{"fqn":"aws-cdk-lib.Resource","version":"0.0.0","metadata":["*"]}},"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/Service/PreScaleUpRole/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnRole","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Role","aws:cdk:cloudformation:props":{"assumeRolePolicyDocument":{"Statement":[{"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"Service":"ecs.amazonaws.com"}}],"Version":"2012-10-17"}}}},"DefaultPolicy":{"id":"DefaultPolicy","path":"aws-ecs-blue-green-deployment/Service/PreScaleUpRole/DefaultPolicy","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.Policy","version":"0.0.0","metadata":["*",{"attachToRole":["*"]},{"attachToRole":["*"]},{"addStatements":[{}]}]},"children":{"Resource":{"id":"Resource","path":"aws-ecs-blue-green-deployment/Service/PreScaleUpRole/DefaultPolicy/Resource","constructInfo":{"fqn":"aws-cdk-lib.aws_iam.CfnPolicy","version":"0.0.0"},"attributes":{"aws:cdk:cloudformation:type":"AWS::IAM::Policy","aws:cdk:cloudformation:props":{"policyDocument":{"Statement":[{"Action":"lambda:InvokeFunction","Effect":"Allow","Resource":[{"Fn::GetAtt":["LambdaHookBF1BC8B4","Arn"]},{"Fn::Join":["",[{"Fn::GetAtt":["LambdaHookBF1BC8B4","Arn"]},":*"]]}]}],"Version":"2012-10-17"},"policyName":"ServicePreScaleUpRoleDefaultPolicyF1032E86","roles":[{"Ref":"ServicePreScaleUpRoleD002A553"}]}}}}}}}}},"BootstrapVersion":{"id":"BootstrapVersion","path":"aws-ecs-blue-green-deployment/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"aws-ecs-blue-green-deployment/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}},"aws-ecs-blue-green":{"id":"aws-ecs-blue-green","path":"aws-ecs-blue-green","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTest","version":"0.0.0"},"children":{"DefaultTest":{"id":"DefaultTest","path":"aws-ecs-blue-green/DefaultTest","constructInfo":{"fqn":"@aws-cdk/integ-tests-alpha.IntegTestCase","version":"0.0.0"},"children":{"Default":{"id":"Default","path":"aws-ecs-blue-green/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"DeployAssert":{"id":"DeployAssert","path":"aws-ecs-blue-green/DefaultTest/DeployAssert","constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"},"children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"aws-ecs-blue-green/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"aws-ecs-blue-green/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}}}}}}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}}}} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.ts index a7c20dc113b60..c9888ee7d1c00 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.blue-green-deployment-strategy.ts @@ -1,6 +1,5 @@ import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import * as iam from 'aws-cdk-lib/aws-iam'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as cdk from 'aws-cdk-lib'; import * as ecs from 'aws-cdk-lib/aws-ecs'; @@ -89,45 +88,6 @@ const prodListenerRule = new elbv2.ApplicationListenerRule(stack, 'ALBProduction ]), }); -// Create granular IAM roles -const ecsTaskExecutionRole = new iam.Role(stack, 'EcsTaskExecutionRole', { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'), - ], -}); - -// Create Blue/Green deployment service role -const ecsServiceRole = new iam.Role(stack, 'ServiceRole', { - assumedBy: new iam.CompositePrincipal( - new iam.ServicePrincipal('ecs.amazonaws.com'), - ), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonEC2ContainerServiceRole'), - ], - inlinePolicies: { - LambdaInvokePolicy: new iam.PolicyDocument({ - statements: [ - new iam.PolicyStatement({ - actions: ['lambda:InvokeFunction'], - resources: ['*'], - }), - ], - }), - ELBPolicy: new iam.PolicyDocument({ - statements: [ - new iam.PolicyStatement({ - actions: [ - 'elasticloadbalancing:ModifyRule', - 'elasticloadbalancing:ModifyListener', - ], - resources: [prodListenerRule.listenerRuleArn], - }), - ], - }), - }, -}); - // Create Lambda hook const lambdaHook = new lambda.Function(stack, 'LambdaHook', { handler: 'index.handler', @@ -138,13 +98,10 @@ const lambdaHook = new lambda.Function(stack, 'LambdaHook', { };`), }); -lambdaHook.grantInvoke(ecsServiceRole); - // Create task definition const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { memoryLimitMiB: 512, cpu: 256, - executionRole: ecsTaskExecutionRole, }); // Add container to task definition @@ -163,39 +120,24 @@ const service = new ecs.FargateService(stack, 'Service', { cluster, taskDefinition, securityGroups: [ecsSecurityGroup], + deploymentStrategy: ecs.DeploymentStrategy.BLUE_GREEN, }); -service.attachToApplicationTargetGroup(blueTargetGroup); +service.addLifecycleHook(new ecs.DeploymentLifecycleLambdaTarget(lambdaHook, 'PreScaleUp', { + lifecycleStages: [ecs.DeploymentLifecycleStage.PRE_SCALE_UP], +})); -// Use escape hatching to set B/G deployment properties -const cfnService = service.node.defaultChild as ecs.CfnService; -cfnService.addPropertyOverride('DeploymentController', { - Type: 'ECS', -}); - -cfnService.addPropertyOverride('DeploymentConfiguration', { - DeploymentCircuitBreaker: { - Enable: false, - Rollback: false, - }, - MaximumPercent: 200, - MinimumHealthyPercent: 100, - Strategy: 'BLUE_GREEN', - BakeTimeInMinutes: 0, - LifecycleHooks: [{ - HookTargetArn: lambdaHook.functionArn, - RoleArn: ecsServiceRole.roleArn, - LifecycleStages: ['POST_TEST_TRAFFIC_SHIFT'], - }], +const target = service.loadBalancerTarget({ + containerName: 'nginx', + containerPort: 80, + protocol: ecs.Protocol.TCP, + alternateTarget: new ecs.AlternateTarget('LBAlternateOptions', { + alternateTargetGroup: greenTargetGroup, + productionListener: ecs.ListenerRuleConfiguration.applicationListenerRule(prodListenerRule), + }), }); -cfnService.addPropertyOverride('LoadBalancers.0', { - AdvancedConfiguration: { - AlternateTargetGroupArn: greenTargetGroup.targetGroupArn, - RoleArn: ecsServiceRole.roleArn, - ProductionListenerRule: prodListenerRule.listenerRuleArn, - }, -}); +target.attachToApplicationTargetGroup(blueTargetGroup); // Create integration test new integ.IntegTest(app, 'aws-ecs-blue-green', { diff --git a/packages/aws-cdk-lib/aws-ecs/README.md b/packages/aws-cdk-lib/aws-ecs/README.md index 375aa395cea26..8a346ae0c936a 100644 --- a/packages/aws-cdk-lib/aws-ecs/README.md +++ b/packages/aws-cdk-lib/aws-ecs/README.md @@ -2076,6 +2076,41 @@ Amazon ECS supports native blue/green deployments that allow you to deploy new v [Amazon ECS blue/green deployments](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-blue-green.html) +### Using Fargate L2 constructs for Blue/Green Feature + +```ts +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +declare const cluster: ecs.Cluster; +declare const taskDefinition: ecs.TaskDefinition; +declare const lambdaHook: lambda.Function; +declare const blueTargetGroup: elbv2.ApplicationTargetGroup; +declare const greenTargetGroup: elbv2.ApplicationTargetGroup; +declare const prodListenerRule: elbv2.ApplicationListenerRule; + +const service = new ecs.FargateService(this, 'Service', { + cluster, + taskDefinition, + deploymentStrategy: ecs.DeploymentStrategy.BLUE_GREEN, +}); + +service.addLifecycleHook(new ecs.DeploymentLifecycleLambdaTarget(lambdaHook, 'PreScaleHook', { + lifecycleStages: [ecs.DeploymentLifecycleStage.PRE_SCALE_UP], +})); + +const target = service.loadBalancerTarget({ + containerName: 'nginx', + containerPort: 80, + protocol: ecs.Protocol.TCP, + alternateTarget: new ecs.AlternateTarget('AlternateTarget', { + alternateTargetGroup: greenTargetGroup, + productionListener: ecs.ListenerRuleConfiguration.applicationListenerRule(prodListenerRule), + }), +}); + +target.attachToApplicationTargetGroup(blueTargetGroup); +``` + ### Using Escape Hatches for Blue/Green Features The new blue/green deployment features are added to CloudFormation but not yet available in the CDK L2 constructs, you can use escape hatches to access them through the L1 (CfnService) construct. diff --git a/packages/aws-cdk-lib/aws-ecs/lib/alternate-target-configuration.ts b/packages/aws-cdk-lib/aws-ecs/lib/alternate-target-configuration.ts new file mode 100644 index 0000000000000..c87a760bc4586 --- /dev/null +++ b/packages/aws-cdk-lib/aws-ecs/lib/alternate-target-configuration.ts @@ -0,0 +1,149 @@ +import { IConstruct } from 'constructs'; +import * as elbv2 from '../../aws-elasticloadbalancingv2'; +import * as iam from '../../aws-iam'; + +/** + * Represents a listener configuration for advanced load balancer settings + */ +export abstract class ListenerRuleConfiguration { + /** + * Use an Application Load Balancer listener rule + */ + public static applicationListenerRule(rule: elbv2.ApplicationListenerRule): ListenerRuleConfiguration { + return new ApplicationListenerRuleConfiguration(rule); + } + + /** + * Use a Network Load Balancer listener + */ + public static networkListener(listener: elbv2.NetworkListener): ListenerRuleConfiguration { + return new NetworkListenerConfiguration(listener); + } + + /** + * @internal + */ + public abstract readonly _listenerArn: string; +} + +class ApplicationListenerRuleConfiguration extends ListenerRuleConfiguration { + constructor(private readonly rule: elbv2.ApplicationListenerRule) { + super(); + } + + public get _listenerArn(): string { + return this.rule.listenerRuleArn; + } +} + +class NetworkListenerConfiguration extends ListenerRuleConfiguration { + constructor(private readonly listener: elbv2.NetworkListener) { + super(); + } + + public get _listenerArn(): string { + return this.listener.listenerArn; + } +} + +/** + * Configuration returned by AlternateTargetConfiguration.bind() + */ +export interface AlternateTargetConfig { + /** + * The ARN of the alternate target group + */ + readonly alternateTargetGroupArn: string; + + /** + * The IAM role ARN for the configuration + * @default - a new role will be created + */ + readonly roleArn: string; + + /** + * The production listener rule ARN (ALB) or listener ARN (NLB) + * @default - none + */ + readonly productionListenerRule?: string; + + /** + * The test listener rule ARN (ALB) or listener ARN (NLB) + * @default - none + */ + readonly testListenerRule?: string; +} + +/** + * Interface for configuring alternate target groups for blue/green deployments + */ +export interface IAlternateTarget { + /** + * Bind this configuration to a service + * + * @param scope The construct scope + * @returns The configuration to apply to the service + */ + bind(scope: IConstruct): AlternateTargetConfig; +} + +/** + * Options for AlternateTarget configuration + */ +export interface AlternateTargetOptions { + /** + * The IAM role for the configuration + * @default - a new role will be created + */ + readonly role?: iam.IRole; + + /** + * The test listener configuration + * @default - none + */ + readonly testListener?: ListenerRuleConfiguration; +} + +/** + * Properties for AlternateTarget configuration + */ +export interface AlternateTargetProps extends AlternateTargetOptions { + /** + * The alternate target group + */ + readonly alternateTargetGroup: elbv2.ITargetGroup; + + /** + * The production listener rule ARN (ALB) or listener ARN (NLB) + */ + readonly productionListener: ListenerRuleConfiguration; +} + +/** + * Configuration for alternate target groups used in blue/green deployments with load balancers + */ +export class AlternateTarget implements IAlternateTarget { + constructor(private readonly id: string, private readonly props: AlternateTargetProps) { } + + /** + * Bind this configuration to a service + */ + public bind(scope: IConstruct): AlternateTargetConfig { + const roleId = `${this.id}Role`; + const role = this.props.role ?? new iam.Role(scope, roleId, { + assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonECSInfrastructureRolePolicyForLoadBalancers'), + ], + }); + + const config: AlternateTargetConfig = { + alternateTargetGroupArn: this.props.alternateTargetGroup.targetGroupArn, + roleArn: role.roleArn, + productionListenerRule: this.props.productionListener._listenerArn, + testListenerRule: this.props.testListener?._listenerArn, + }; + + return config; + } +} diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts index f2a73a0745274..32f0cf6274bd2 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts @@ -26,6 +26,7 @@ import { } from '../../../core'; import * as cxapi from '../../../cx-api'; import { RegionInfo } from '../../../region-info'; +import { IAlternateTarget } from '../alternate-target-configuration'; import { LoadBalancerTargetOptions, NetworkMode, @@ -34,6 +35,7 @@ import { } from '../base/task-definition'; import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging, Cluster } from '../cluster'; import { ContainerDefinition, Protocol } from '../container-definition'; +import { IDeploymentLifecycleHookTarget } from '../deployment-lifecycle-hook-target'; import { CfnService } from '../ecs.generated'; import { LogDriver, LogDriverConfig } from '../log-drivers/log-driver'; @@ -435,6 +437,24 @@ export interface BaseServiceOptions { * @default - undefined */ readonly volumeConfigurations?: ServiceManagedVolume[]; + + /** + * The deployment strategy to use for the service. + * @default ROLLING + */ + readonly deploymentStrategy?: DeploymentStrategy; + + /** + * bake time minutes for service. + * @default - none + */ + readonly bakeTime?: Duration; + + /** + * The lifecycle hooks to execute during deployment stages + * @default - none; + */ + readonly lifecycleHooks?: IDeploymentLifecycleHookTarget[]; } /** @@ -494,7 +514,7 @@ class ApplicationListenerConfig extends ListenerConfig { const protocol = props.protocol; const port = props.port ?? (protocol === elbv2.ApplicationProtocol.HTTPS ? 443 : 80); this.listener.addTargets(id, { - ... props, + ...props, targets: [ service.loadBalancerTarget({ ...target, @@ -655,6 +675,12 @@ export abstract class BaseService extends Resource */ protected _serviceConnectConfig?: CfnService.ServiceConnectConfigurationProperty; + /** + * Whether this service is using the ECS deployment controller. + * @internal + */ + private readonly isEcsDeploymentController: boolean; + private readonly resource: CfnService; private scalableTaskCount?: ScalableTaskCount; @@ -663,6 +689,12 @@ export abstract class BaseService extends Resource */ private readonly volumes: ServiceManagedVolume[] = []; + /** + * A deployment lifecycle hook runs custom logic at specific stages of the deployment process. + * @default - none + */ + private readonly lifecycleHooks: IDeploymentLifecycleHookTarget[] = []; + /** * Constructs a new instance of the BaseService class. */ @@ -689,6 +721,9 @@ export abstract class BaseService extends Resource const propagateTagsFromSource = props.propagateTaskTagsFrom ?? props.propagateTags ?? PropagatedTagSource.NONE; const deploymentController = this.getDeploymentController(props); + + // Determine if this service is using the ECS deployment controller + this.isEcsDeploymentController = !deploymentController || deploymentController.type === DeploymentControllerType.ECS; this.resource = new CfnService(this, 'Service', { desiredCount: props.desiredCount, serviceName: this.physicalName, @@ -701,6 +736,9 @@ export abstract class BaseService extends Resource rollback: props.circuitBreaker.rollback ?? false, } : undefined, alarms: Lazy.any({ produce: () => this.deploymentAlarms }, { omitEmptyArray: true }), + strategy: props.deploymentStrategy, + bakeTimeInMinutes: props.bakeTime?.toMinutes(), + lifecycleHooks: Lazy.any({ produce: () => this.renderLifecycleHooks() }, { omitEmptyArray: true }), }, propagateTags: propagateTagsFromSource === PropagatedTagSource.NONE ? undefined : props.propagateTags, enableEcsManagedTags: props.enableECSManagedTags ?? false, @@ -723,15 +761,11 @@ export abstract class BaseService extends Resource Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs:externalDeploymentController', 'taskDefinition and launchType are blanked out when using external deployment controller.'); } - if (props.circuitBreaker - && deploymentController - && deploymentController.type !== DeploymentControllerType.ECS) { + if (props.circuitBreaker && !this.isEcsDeploymentController) { Annotations.of(this).addError('Deployment circuit breaker requires the ECS deployment controller.'); } - if (props.deploymentAlarms - && deploymentController - && deploymentController.type !== DeploymentControllerType.ECS) { + if (props.deploymentAlarms && !this.isEcsDeploymentController) { throw new ValidationError('Deployment alarms requires the ECS deployment controller.', this); } @@ -804,9 +838,8 @@ export abstract class BaseService extends Resource enable: true, rollback: props.deploymentAlarms.behavior !== AlarmBehavior.FAIL_ON_ALARM, }; - // CloudWatch alarms is only supported for Amazon ECS services that use the rolling update (ECS) deployment controller. - } else if ((!props.deploymentController || - props.deploymentController?.type === DeploymentControllerType.ECS) && this.deploymentAlarmsAvailableInRegion()) { + // CloudWatch alarms is only supported for Amazon ECS services that use the rolling update (ECS) deployment controller. + } else if (this.isEcsDeploymentController && this.deploymentAlarmsAvailableInRegion()) { // Only set default deployment alarms settings when feature flag is not enabled. if (!FeatureFlags.of(this).isEnabled(cxapi.ECS_REMOVE_DEFAULT_DEPLOYMENT_ALARM)) { this.deploymentAlarms = { @@ -817,9 +850,39 @@ export abstract class BaseService extends Resource } } + if (props.lifecycleHooks) { + if (this.isEcsDeploymentController) { + props.lifecycleHooks.forEach(target => this.addLifecycleHook(target)); + } else { + throw new ValidationError('Deployment lifecycle hooks requires the ECS deployment controller.', this); + } + } + this.node.defaultChild = this.resource; } + /** + * Add a deployment lifecycle hook target + * @param target The lifecycle hook target to add + */ + public addLifecycleHook(target: IDeploymentLifecycleHookTarget) { + if (!this.isEcsDeploymentController) { + throw new ValidationError('Deployment lifecycle hooks requires the ECS deployment controller.', this); + } + this.lifecycleHooks.push(target); + } + + private renderLifecycleHooks(): CfnService.DeploymentLifecycleHookProperty[] { + return this.lifecycleHooks.map((target) => { + const config = target.bind(this); + return { + hookTargetArn: config.targetArn, + roleArn: config.role!.roleArn, + lifecycleStages: config.lifecycleStages.map(stage => stage.toString()), + }; + }); + } + /** * Adds a volume to the Service. */ @@ -879,7 +942,7 @@ export abstract class BaseService extends Resource * */ public enableDeploymentAlarms(alarmNames: string[], options?: DeploymentAlarmOptions) { - if (alarmNames.length === 0 ) { + if (alarmNames.length === 0) { throw new ValidationError('at least one alarm name is required when calling enableDeploymentAlarms(), received empty array', this); } @@ -1072,7 +1135,7 @@ export abstract class BaseService extends Resource return props.deploymentController; } const disableCircuitBreakerEcsDeploymentControllerFeatureFlag = - FeatureFlags.of(this).isEnabled(cxapi.ECS_DISABLE_EXPLICIT_DEPLOYMENT_CONTROLLER_FOR_CIRCUIT_BREAKER); + FeatureFlags.of(this).isEnabled(cxapi.ECS_DISABLE_EXPLICIT_DEPLOYMENT_CONTROLLER_FOR_CIRCUIT_BREAKER); if (!disableCircuitBreakerEcsDeploymentControllerFeatureFlag && props.circuitBreaker) { // This is undesirable behavior (the controller is implicitly ECS anyway when left @@ -1215,16 +1278,20 @@ export abstract class BaseService extends Resource * }); */ public loadBalancerTarget(options: LoadBalancerTargetOptions): IEcsLoadBalancerTarget { + if (options.alternateTarget && !this.isEcsDeploymentController) { + throw new ValidationError('Deployment lifecycle hooks requires the ECS deployment controller.', this); + } + const self = this; const target = this.taskDefinition._validateTarget(options); const connections = self.connections; return { attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { targetGroup.registerConnectable(self, self.taskDefinition._portRangeFromPortMapping(target.portMapping)); - return self.attachToELBv2(targetGroup, target.containerName, target.portMapping.containerPort!); + return self.attachToELBv2(targetGroup, target.containerName, target.portMapping.containerPort!, options.alternateTarget); }, attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { - return self.attachToELBv2(targetGroup, target.containerName, target.portMapping.containerPort!); + return self.attachToELBv2(targetGroup, target.containerName, target.portMapping.containerPort!, options.alternateTarget); }, connections, attachToClassicLB(loadBalancer: elb.LoadBalancer): void { @@ -1486,15 +1553,21 @@ export abstract class BaseService extends Resource /** * Shared logic for attaching to an ELBv2 */ - private attachToELBv2(targetGroup: elbv2.ITargetGroup, containerName: string, containerPort: number): elbv2.LoadBalancerTargetProps { + private attachToELBv2( + targetGroup: elbv2.ITargetGroup, + containerName: string, + containerPort: number, + alternateTarget?: IAlternateTarget): elbv2.LoadBalancerTargetProps { if (this.taskDefinition.networkMode === NetworkMode.NONE) { throw new ValidationError('Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead.', this); } + const advancedConfiguration = alternateTarget?.bind(this); this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, containerName, containerPort, + advancedConfiguration, }); // Service creation can only happen after the load balancer has @@ -1580,6 +1653,14 @@ export abstract class BaseService extends Resource perRequestTimeoutSeconds: perRequestTimeout?.toSeconds(), }; } + + /** + * Checks if the service is using the ECS deployment controller. + * @returns true if the service is using the ECS deployment controller or if no deployment controller is specified (defaults to ECS) + */ + public isUsingECSDeploymentController(): boolean { + return this.isEcsDeploymentController; + } } /** @@ -1726,6 +1807,20 @@ export enum DeploymentControllerType { EXTERNAL = 'EXTERNAL', } +/** + * The deployment stratergy to use for ECS controller + */ +export enum DeploymentStrategy { + /** + * Rolling update deployment + */ + ROLLING = 'ROLLING', + /** + * Blue/green deployment + */ + BLUE_GREEN = 'BLUE_GREEN', +} + /** * Propagate tags from either service or task definition */ diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts index 9b708a65be4f2..6cbe39091ae10 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts @@ -5,6 +5,7 @@ import * as iam from '../../../aws-iam'; import { IResource, Lazy, Names, PhysicalName, Resource, UnscopedValidationError, ValidationError } from '../../../core'; import { addConstructMetadata, MethodMetadata } from '../../../core/lib/metadata-resource'; import { propertyInjectable } from '../../../core/lib/prop-injectable'; +import { IAlternateTarget } from '../alternate-target-configuration'; import { ContainerDefinition, ContainerDefinitionOptions, PortMapping, Protocol } from '../container-definition'; import { CfnTaskDefinition } from '../ecs.generated'; import { FirelensLogRouter, FirelensLogRouterDefinitionOptions, FirelensLogRouterType, obtainDefaultFluentBitECRImage } from '../firelens-log-router'; @@ -1143,6 +1144,13 @@ export interface LoadBalancerTargetOptions { * @default Protocol.TCP */ readonly protocol?: Protocol; + + /** + * Alternate target configuration for blue/green deployments. + * + * @default - No alternate target configuration + */ + readonly alternateTarget?: IAlternateTarget; } /** diff --git a/packages/aws-cdk-lib/aws-ecs/lib/deployment-lifecycle-hook-target.ts b/packages/aws-cdk-lib/aws-ecs/lib/deployment-lifecycle-hook-target.ts new file mode 100644 index 0000000000000..87c736fbe554e --- /dev/null +++ b/packages/aws-cdk-lib/aws-ecs/lib/deployment-lifecycle-hook-target.ts @@ -0,0 +1,127 @@ +import { IConstruct } from 'constructs'; +import * as iam from '../../aws-iam'; +import * as lambda from '../../aws-lambda'; + +/** + * Deployment lifecycle stages where hooks can be executed + */ +export enum DeploymentLifecycleStage { + /** + * Execute during service reconciliation + */ + RECONCILE_SERVICE = 'RECONCILE_SERVICE', + /** + * Execute before scaling up tasks + */ + PRE_SCALE_UP = 'PRE_SCALE_UP', + /** + * Execute after scaling up tasks + */ + POST_SCALE_UP = 'POST_SCALE_UP', + /** + * Execute during test traffic shift + */ + TEST_TRAFFIC_SHIFT = 'TEST_TRAFFIC_SHIFT', + /** + * Execute after test traffic shift + */ + POST_TEST_TRAFFIC_SHIFT = 'POST_TEST_TRAFFIC_SHIFT', + /** + * Execute during production traffic shift + */ + PRODUCTION_TRAFFIC_SHIFT = 'PRODUCTION_TRAFFIC_SHIFT', + /** + * Execute after production traffic shift + */ + POST_PRODUCTION_TRAFFIC_SHIFT = 'POST_PRODUCTION_TRAFFIC_SHIFT', +} + +/** + * Configuration for a deployment lifecycle hook target + */ +export interface DeploymentLifecycleHookTargetConfig { + /** + * The ARN of the target resource + */ + readonly targetArn: string; + + /** + * The IAM role that grants permissions to invoke the target + * @default - a role will be created automatically + */ + readonly role?: iam.IRole; + + /** + * The lifecycle stages when this hook should be executed + */ + readonly lifecycleStages: DeploymentLifecycleStage[]; +} + +/** + * Interface for deployment lifecycle hook targets + */ +export interface IDeploymentLifecycleHookTarget { + /** + * Bind this target to a deployment lifecycle hook + * + * @param scope The construct scope + * @param id A unique identifier for this binding + */ + bind(scope: IConstruct): DeploymentLifecycleHookTargetConfig; +} + +/** + * Configuration for a lambda deployment lifecycle hook + */ +export interface DeploymentLifecycleLambdaTargetProps { + /** + * The IAM role that grants permissions to invoke the lambda target + * @default - A unique role will be generated for this lambda function. + */ + readonly role?: iam.IRole; + + /** + * The lifecycle stages when this hook should be executed + */ + readonly lifecycleStages: DeploymentLifecycleStage[]; +} + +/** + * Use an AWS Lambda function as a deployment lifecycle hook target + */ +export class DeploymentLifecycleLambdaTarget implements IDeploymentLifecycleHookTarget { + private _role?: iam.IRole; + + constructor( + private readonly handler: lambda.IFunction, + private readonly id: string, + private readonly props: DeploymentLifecycleLambdaTargetProps, + ) { } + + /** + * The IAM role for the deployment lifecycle hook target + */ + public get role(): iam.IRole { + return this._role!; + } + + /** + * Bind this target to a deployment lifecycle hook + */ + public bind(scope: IConstruct): DeploymentLifecycleHookTargetConfig { + // Create role if not provided + this._role = this.props.role; + if (!this._role) { + this._role = new iam.Role(scope, `${this.id}Role`, { + assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'), + }); + this.handler.grantInvoke(this._role); + } + + return { + targetArn: this.handler.functionArn, + role: this._role, + lifecycleStages: this.props.lifecycleStages, + }; + } +} diff --git a/packages/aws-cdk-lib/aws-ecs/lib/index.ts b/packages/aws-cdk-lib/aws-ecs/lib/index.ts index c60b22e95cdb5..44f77052f914f 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/index.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/index.ts @@ -47,6 +47,9 @@ export * from './proxy-configuration/proxy-configuration'; export * from './proxy-configuration/proxy-configurations'; export * from './runtime-platform'; +export * from './deployment-lifecycle-hook-target'; +export * from './alternate-target-configuration'; + // AWS::ECS CloudFormation Resources: // export * from './ecs.generated'; diff --git a/packages/aws-cdk-lib/aws-ecs/test/alternate-target-configuration.test.ts b/packages/aws-cdk-lib/aws-ecs/test/alternate-target-configuration.test.ts new file mode 100644 index 0000000000000..ea807f95bb4a0 --- /dev/null +++ b/packages/aws-cdk-lib/aws-ecs/test/alternate-target-configuration.test.ts @@ -0,0 +1,334 @@ +import { Template, Match } from '../../assertions'; +import * as ec2 from '../../aws-ec2'; +import * as elbv2 from '../../aws-elasticloadbalancingv2'; +import * as iam from '../../aws-iam'; +import * as cdk from '../../core'; +import { App, Stack } from '../../core'; +import * as ecs from '../lib'; + +describe('AlternateTarget', () => { + let stack: cdk.Stack; + let vpc: ec2.Vpc; + let cluster: ecs.Cluster; + let taskDefinition: ecs.FargateTaskDefinition; + let blueTargetGroup: elbv2.ApplicationTargetGroup; + let greenTargetGroup: elbv2.ApplicationTargetGroup; + let alb: elbv2.ApplicationLoadBalancer; + let listener: elbv2.ApplicationListener; + let prodRule: elbv2.ApplicationListenerRule; + let testRule: elbv2.ApplicationListenerRule; + + beforeEach(() => { + stack = new cdk.Stack(); + vpc = new ec2.Vpc(stack, 'Vpc'); + cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + portMappings: [{ containerPort: 80 }], + }); + + blueTargetGroup = new elbv2.ApplicationTargetGroup(stack, 'BlueTG', { + vpc, + port: 80, + targetType: elbv2.TargetType.IP, + }); + + greenTargetGroup = new elbv2.ApplicationTargetGroup(stack, 'GreenTG', { + vpc, + port: 80, + targetType: elbv2.TargetType.IP, + }); + + alb = new elbv2.ApplicationLoadBalancer(stack, 'ALB', { vpc }); + listener = alb.addListener('Listener', { + port: 80, + defaultAction: elbv2.ListenerAction.fixedResponse(200), + }); + + prodRule = new elbv2.ApplicationListenerRule(stack, 'ProdRule', { + listener, + priority: 1, + conditions: [elbv2.ListenerCondition.pathPatterns(['/prod'])], + action: elbv2.ListenerAction.forward([blueTargetGroup]), + }); + + testRule = new elbv2.ApplicationListenerRule(stack, 'TestRule', { + listener, + priority: 2, + conditions: [elbv2.ListenerCondition.pathPatterns(['/test'])], + action: elbv2.ListenerAction.forward([blueTargetGroup]), + }); + }); + + test('AlternateTarget creates correct configuration with production listener only', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // WHEN + const alternateTarget = new ecs.AlternateTarget('GreenTG', { + alternateTargetGroup: greenTargetGroup, + productionListener: ecs.ListenerRuleConfiguration.applicationListenerRule(prodRule), + }); + + const target = service.loadBalancerTarget({ + containerName: 'web', + containerPort: 80, + alternateTarget, + }); + target.attachToApplicationTargetGroup(blueTargetGroup); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: 'web', + ContainerPort: 80, + TargetGroupArn: { + Ref: Match.stringLikeRegexp('BlueTG'), + }, + AdvancedConfiguration: { + AlternateTargetGroupArn: { + Ref: Match.stringLikeRegexp('GreenTG'), + }, + ProductionListenerRule: { + Ref: Match.stringLikeRegexp('ProdRule'), + }, + TestListenerRule: Match.absent(), + }, + }, + ], + }); + }); + + test('AlternateTarget creates correct configuration with both production and test listeners', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // WHEN + const alternateTarget = new ecs.AlternateTarget('GreenTG', { + alternateTargetGroup: greenTargetGroup, + productionListener: ecs.ListenerRuleConfiguration.applicationListenerRule(prodRule), + testListener: ecs.ListenerRuleConfiguration.applicationListenerRule(testRule), + }); + + const target = service.loadBalancerTarget({ + containerName: 'web', + containerPort: 80, + alternateTarget, + }); + target.attachToApplicationTargetGroup(blueTargetGroup); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: 'web', + ContainerPort: 80, + TargetGroupArn: { + Ref: Match.stringLikeRegexp('BlueTG'), + }, + AdvancedConfiguration: { + AlternateTargetGroupArn: { + Ref: Match.stringLikeRegexp('GreenTG'), + }, + ProductionListenerRule: { + Ref: Match.stringLikeRegexp('ProdRule'), + }, + TestListenerRule: { + Ref: Match.stringLikeRegexp('TestRule'), + }, + }, + }, + ], + }); + }); + + test('AlternateTarget creates correct configuration with custom role', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + const customRole = new iam.Role(stack, 'CustomRole', { + assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonECSInfrastructureRolePolicyForLoadBalancers'), + ], + }); + + // WHEN + const alternateTarget = new ecs.AlternateTarget('GreenTG', { + alternateTargetGroup: greenTargetGroup, + productionListener: ecs.ListenerRuleConfiguration.applicationListenerRule(prodRule), + role: customRole, + }); + + const target = service.loadBalancerTarget({ + containerName: 'web', + containerPort: 80, + alternateTarget, + }); + target.attachToApplicationTargetGroup(blueTargetGroup); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: 'web', + ContainerPort: 80, + TargetGroupArn: { + Ref: Match.stringLikeRegexp('BlueTG'), + }, + AdvancedConfiguration: { + AlternateTargetGroupArn: { + Ref: Match.stringLikeRegexp('GreenTG'), + }, + ProductionListenerRule: { + Ref: Match.stringLikeRegexp('ProdRule'), + }, + RoleArn: { + 'Fn::GetAtt': [ + 'CustomRole6D8E6809', + 'Arn', + ], + }, + }, + }, + ], + }); + }); + + test('NetworkListenerConfiguration works with NLB listeners', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + const nlb = new elbv2.NetworkLoadBalancer(stack, 'NLB', { vpc }); + const nlbListener = nlb.addListener('NlbListener', { port: 80 }); + + const nlbBlueTargetGroup = new elbv2.NetworkTargetGroup(stack, 'NlbBlueTG', { + vpc, + port: 80, + targetType: elbv2.TargetType.IP, + }); + + const nlbGreenTargetGroup = new elbv2.NetworkTargetGroup(stack, 'NlbGreenTG', { + vpc, + port: 80, + targetType: elbv2.TargetType.IP, + }); + + nlbListener.addAction('DefaultAction', { + action: elbv2.NetworkListenerAction.forward([nlbBlueTargetGroup]), + }); + + // WHEN + const alternateTarget = new ecs.AlternateTarget('GreenTG', { + alternateTargetGroup: nlbGreenTargetGroup, + productionListener: ecs.ListenerRuleConfiguration.networkListener(nlbListener), + }); + + const target = service.loadBalancerTarget({ + containerName: 'web', + containerPort: 80, + alternateTarget, + }); + target.attachToNetworkTargetGroup(nlbBlueTargetGroup); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: 'web', + ContainerPort: 80, + TargetGroupArn: { + Ref: Match.stringLikeRegexp('NlbBlueTG'), + }, + AdvancedConfiguration: { + AlternateTargetGroupArn: { + Ref: Match.stringLikeRegexp('NlbGreenTG'), + }, + ProductionListenerRule: { + Ref: Match.stringLikeRegexp('NlbListener'), + }, + }, + }, + ], + }); + }); + + test('Service without alternate target works correctly (regression test)', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // WHEN - No alternate target provided + const target = service.loadBalancerTarget({ + containerName: 'web', + containerPort: 80, + }); + target.attachToApplicationTargetGroup(blueTargetGroup); + + // THEN - Should not have AdvancedConfiguration + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: 'web', + ContainerPort: 80, + TargetGroupArn: { + Ref: Match.stringLikeRegexp('BlueTG'), + }, + AdvancedConfiguration: Match.absent(), + }, + ], + }); + }); + + test('Service without alternate target works with NLB (regression test)', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + const nlb = new elbv2.NetworkLoadBalancer(stack, 'NLB', { vpc }); + const nlbBlueTargetGroup = new elbv2.NetworkTargetGroup(stack, 'NlbBlueTG', { + vpc, + port: 80, + targetType: elbv2.TargetType.IP, + }); + + // WHEN - No alternate target provided + const target = service.loadBalancerTarget({ + containerName: 'web', + containerPort: 80, + }); + target.attachToNetworkTargetGroup(nlbBlueTargetGroup); + + // THEN - Should not have AdvancedConfiguration + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + LoadBalancers: [ + { + ContainerName: 'web', + ContainerPort: 80, + TargetGroupArn: { + Ref: Match.stringLikeRegexp('NlbBlueTG'), + }, + AdvancedConfiguration: Match.absent(), + }, + ], + }); + }); +}); diff --git a/packages/aws-cdk-lib/aws-ecs/test/base-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/base-service.test.ts index ab2c65a7df87b..d2b332c95dd52 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/base-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/base-service.test.ts @@ -1,7 +1,9 @@ import { Template, Match } from '../../assertions'; import * as ec2 from '../../aws-ec2'; +import * as elbv2 from '../../aws-elasticloadbalancingv2'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; +import * as lambda from '../../aws-lambda'; import * as cdk from '../../core'; import { App, Stack } from '../../core'; import * as cxapi from '../../cx-api'; @@ -398,7 +400,7 @@ test.each([ new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, - circuitBreaker: circuitBreaker ? { } : undefined, + circuitBreaker: circuitBreaker ? {} : undefined, }); // THEN @@ -444,3 +446,74 @@ test.each([ }, }); }); + +describe('Blue/Green Deployment', () => { + let stack: cdk.Stack; + let vpc: ec2.Vpc; + let cluster: ecs.Cluster; + let taskDefinition: ecs.FargateTaskDefinition; + + beforeEach(() => { + stack = new cdk.Stack(); + vpc = new ec2.Vpc(stack, 'Vpc'); + cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + portMappings: [{ containerPort: 80 }], + }); + }); + + test('isUsingECSDeploymentController returns true when no deployment controller is specified', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // THEN + expect(service.isUsingECSDeploymentController()).toBe(true); + }); + + test('isUsingECSDeploymentController returns true when ECS deployment controller is specified', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + deploymentController: { + type: ecs.DeploymentControllerType.ECS, + }, + }); + + // THEN + expect(service.isUsingECSDeploymentController()).toBe(true); + }); + + test('isUsingECSDeploymentController returns false when CODE_DEPLOY deployment controller is specified', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + + // THEN + expect(service.isUsingECSDeploymentController()).toBe(false); + }); + + test('isUsingECSDeploymentController returns false when EXTERNAL deployment controller is specified', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + deploymentController: { + type: ecs.DeploymentControllerType.EXTERNAL, + }, + }); + + // THEN + expect(service.isUsingECSDeploymentController()).toBe(false); + }); +}); diff --git a/packages/aws-cdk-lib/aws-ecs/test/deployment-lifecycle-hook-target.test.ts b/packages/aws-cdk-lib/aws-ecs/test/deployment-lifecycle-hook-target.test.ts new file mode 100644 index 0000000000000..0ca099da28e69 --- /dev/null +++ b/packages/aws-cdk-lib/aws-ecs/test/deployment-lifecycle-hook-target.test.ts @@ -0,0 +1,285 @@ +import { Template, Match } from '../../assertions'; +import * as ec2 from '../../aws-ec2'; +import * as iam from '../../aws-iam'; +import * as lambda from '../../aws-lambda'; +import * as cdk from '../../core'; +import { App, Stack } from '../../core'; +import * as ecs from '../lib'; + +describe('DeploymentLifecycleHookTarget', () => { + let stack: cdk.Stack; + let vpc: ec2.Vpc; + let cluster: ecs.Cluster; + let taskDefinition: ecs.FargateTaskDefinition; + let lambdaFunction: lambda.Function; + + beforeEach(() => { + stack = new cdk.Stack(); + vpc = new ec2.Vpc(stack, 'Vpc'); + cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + portMappings: [{ containerPort: 80 }], + }); + + lambdaFunction = new lambda.Function(stack, 'TestFunction', { + runtime: lambda.Runtime.NODEJS_18_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = async () => { return { hookStatus: "SUCCEEDED" }; }'), + }); + }); + + test('DeploymentLifecycleLambdaTarget creates default role when none provided', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // WHEN + const hookTarget = new ecs.DeploymentLifecycleLambdaTarget(lambdaFunction, 'PreScaleUpHook', { + lifecycleStages: [ecs.DeploymentLifecycleStage.PRE_SCALE_UP], + }); + service.addLifecycleHook(hookTarget); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'ecs.amazonaws.com', + }, + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('TestFunction'), + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('TestFunction'), + 'Arn', + ], + }, + ':*', + ], + ], + }, + ], + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DeploymentConfiguration: { + LifecycleHooks: [ + { + LifecycleStages: ['PRE_SCALE_UP'], + HookTargetArn: { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('TestFunction'), + 'Arn', + ], + }, + RoleArn: { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('Role'), + 'Arn', + ], + }, + }, + ], + }, + }); + }); + + test('DeploymentLifecycleLambdaTarget uses provided role', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + const customRole = new iam.Role(stack, 'CustomRole', { + assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'), + }); + lambdaFunction.grantInvoke(customRole); + + // WHEN + const hookTarget = new ecs.DeploymentLifecycleLambdaTarget(lambdaFunction, 'PreScaleUpHook', { + lifecycleStages: [ecs.DeploymentLifecycleStage.PRE_SCALE_UP], + role: customRole, + }); + service.addLifecycleHook(hookTarget); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DeploymentConfiguration: { + LifecycleHooks: [ + { + LifecycleStages: ['PRE_SCALE_UP'], + HookTargetArn: { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('TestFunction'), + 'Arn', + ], + }, + RoleArn: { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('CustomRole'), + 'Arn', + ], + }, + }, + ], + }, + }); + }); + + test('DeploymentLifecycleLambdaTarget supports multiple lifecycle stages', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // WHEN + const hookTarget = new ecs.DeploymentLifecycleLambdaTarget(lambdaFunction, 'PreScaleUpHook', { + lifecycleStages: [ + ecs.DeploymentLifecycleStage.PRE_SCALE_UP, + ecs.DeploymentLifecycleStage.POST_SCALE_UP, + ecs.DeploymentLifecycleStage.TEST_TRAFFIC_SHIFT, + ], + }); + service.addLifecycleHook(hookTarget); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DeploymentConfiguration: { + LifecycleHooks: [ + { + LifecycleStages: ['PRE_SCALE_UP', 'POST_SCALE_UP', 'TEST_TRAFFIC_SHIFT'], + HookTargetArn: { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('TestFunction'), + 'Arn', + ], + }, + }, + ], + }, + }); + }); + + test('addLifecycleHook throws when not using ECS deployment controller', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + + const hookTarget = new ecs.DeploymentLifecycleLambdaTarget(lambdaFunction, 'PreScaleUpHook', { + lifecycleStages: [ecs.DeploymentLifecycleStage.PRE_SCALE_UP], + }); + + // THEN + expect(() => { + service.addLifecycleHook(hookTarget); + }).toThrow(/Deployment lifecycle hooks requires the ECS deployment controller/); + }); + + test('multiple lifecycle hooks can be added to a service', () => { + // GIVEN + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + const secondLambda = new lambda.Function(stack, 'SecondFunction', { + runtime: lambda.Runtime.NODEJS_18_X, + handler: 'index.handler', + code: lambda.Code.fromInline('exports.handler = async () => { return { hookStatus: "SUCCEEDED" }; }'), + }); + + // WHEN + const firstHook = new ecs.DeploymentLifecycleLambdaTarget(lambdaFunction, 'PreScaleUpHook', { + lifecycleStages: [ecs.DeploymentLifecycleStage.PRE_SCALE_UP], + }); + + const secondHook = new ecs.DeploymentLifecycleLambdaTarget(secondLambda, 'PostScaleUpHook', { + lifecycleStages: [ecs.DeploymentLifecycleStage.POST_SCALE_UP], + }); + + service.addLifecycleHook(firstHook); + service.addLifecycleHook(secondHook); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + DeploymentConfiguration: { + LifecycleHooks: Match.arrayWith([ + Match.objectLike({ + LifecycleStages: ['PRE_SCALE_UP'], + HookTargetArn: { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('TestFunction'), + 'Arn', + ], + }, + }), + Match.objectLike({ + LifecycleStages: ['POST_SCALE_UP'], + HookTargetArn: { + 'Fn::GetAtt': [ + Match.stringLikeRegexp('SecondFunction'), + 'Arn', + ], + }, + }), + ]), + }, + }); + }); + + test('lifecycle hooks cannot be added during service creation with non-ECS deployment controller', () => { + // GIVEN + const hookTarget = new ecs.DeploymentLifecycleLambdaTarget(lambdaFunction, 'PreScaleUpHook', { + lifecycleStages: [ecs.DeploymentLifecycleStage.PRE_SCALE_UP], + }); + + // THEN + expect(() => { + const service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + service.addLifecycleHook(hookTarget); + }).toThrow(/Deployment lifecycle hooks requires the ECS deployment controller/); + }); +});