From fc0a89a392209324a4834389722656da8fe5e0d4 Mon Sep 17 00:00:00 2001 From: Adithya Kolla <71282729+KollaAdithya@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:52:34 -0800 Subject: [PATCH 1/4] feat(ecs): L2 for ebs task attach (#28691) To simplify ECS customer experience, customers need the ability to utilize highly available (HA) durable, high-performance, cost effective block storage for their workloads. To address these needs, ECS will provide support for Elastic Block Storage (EBS) task attachments. This PR adds the L2 constructs for EBS task attach. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- ...efaultTestDeployAssertF52EF4F9.assets.json | 19 + ...aultTestDeployAssertF52EF4F9.template.json | 36 + .../integ.ebs-taskattach.js.snapshot/cdk.out | 1 + .../integ-aws-ecs-ebs-task-attach.assets.json | 19 + ...nteg-aws-ecs-ebs-task-attach.template.json | 451 ++++++++++ .../integ.json | 12 + .../manifest.json | 221 +++++ .../tree.json | 794 ++++++++++++++++++ .../test/fargate/integ.ebs-taskattach.ts | 77 ++ packages/aws-cdk-lib/aws-ecs/README.md | 83 ++ .../aws-ecs/lib/base/base-service.ts | 61 ++ .../lib/base/service-managed-volume.ts | 310 +++++++ .../aws-ecs/lib/base/task-definition.ts | 38 + .../aws-ecs/lib/container-definition.ts | 10 +- packages/aws-cdk-lib/aws-ecs/lib/index.ts | 1 + .../test/fargate/fargate-service.test.ts | 549 ++++++++++++ .../fargate/fargate-task-definition.test.ts | 39 + .../aws-ecs/test/task-definition.test.ts | 142 ++++ 18 files changed, 2861 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/EBSTaskAttachDefaultTestDeployAssertF52EF4F9.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/EBSTaskAttachDefaultTestDeployAssertF52EF4F9.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ-aws-ecs-ebs-task-attach.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ-aws-ecs-ebs-task-attach.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.ts create mode 100644 packages/aws-cdk-lib/aws-ecs/lib/base/service-managed-volume.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/EBSTaskAttachDefaultTestDeployAssertF52EF4F9.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/EBSTaskAttachDefaultTestDeployAssertF52EF4F9.assets.json new file mode 100644 index 0000000000000..7478188235117 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/EBSTaskAttachDefaultTestDeployAssertF52EF4F9.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "EBSTaskAttachDefaultTestDeployAssertF52EF4F9.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/EBSTaskAttachDefaultTestDeployAssertF52EF4F9.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/EBSTaskAttachDefaultTestDeployAssertF52EF4F9.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/EBSTaskAttachDefaultTestDeployAssertF52EF4F9.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ-aws-ecs-ebs-task-attach.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ-aws-ecs-ebs-task-attach.assets.json new file mode 100644 index 0000000000000..95bfe458f82d1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ-aws-ecs-ebs-task-attach.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "114105e2d29626a5303e48c5ab4b3be1059592f803b1d317c99665664cd954a9": { + "source": { + "path": "integ-aws-ecs-ebs-task-attach.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "114105e2d29626a5303e48c5ab4b3be1059592f803b1d317c99665664cd954a9.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ-aws-ecs-ebs-task-attach.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ-aws-ecs-ebs-task-attach.template.json new file mode 100644 index 0000000000000..bac09e2c34fec --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ-aws-ecs-ebs-task-attach.template.json @@ -0,0 +1,451 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "integ-aws-ecs-ebs-task-attach/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.0.0/17", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + }, + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1" + } + ] + }, + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet1RouteTableAssociation97140677" + ] + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "AvailabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "CidrBlock": "10.0.128.0/17", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + }, + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-aws-ecs-ebs-task-attach/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + }, + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "MountPoints": [ + { + "ContainerPath": "/var/lib", + "ReadOnly": false, + "SourceVolume": "ebs1" + } + ], + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "Family": "integawsecsebstaskattachTaskDefB8F13A4F", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "Volumes": [ + { + "ConfiguredAtLaunch": true, + "Name": "ebs1" + } + ] + } + }, + "EBSVolumeEBSRoleC27DD941": { + "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/AmazonECSInfrastructureRolePolicyForVolumes" + ] + ] + } + ] + } + }, + "FargateServiceAC2B3B85": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "Alarms": { + "AlarmNames": [], + "Enable": false, + "Rollback": false + }, + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup0A0E79CB", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + ] + } + }, + "TaskDefinition": { + "Ref": "TaskDef54694570" + }, + "VolumeConfigurations": [ + { + "ManagedEBSVolume": { + "Encrypted": true, + "FilesystemType": "ext4", + "Iops": 4000, + "RoleArn": { + "Fn::GetAtt": [ + "EBSVolumeEBSRoleC27DD941", + "Arn" + ] + }, + "SizeInGiB": 15, + "TagSpecifications": [ + { + "PropagateTags": "SERVICE", + "ResourceType": "volume", + "Tags": [ + { + "Key": "purpose", + "Value": "production" + } + ] + }, + { + "PropagateTags": "TASK_DEFINITION", + "ResourceType": "volume", + "Tags": [ + { + "Key": "purpose", + "Value": "development" + } + ] + } + ], + "Throughput": 500, + "VolumeType": "gp3" + }, + "Name": "ebs1" + } + ] + }, + "DependsOn": [ + "TaskDefTaskRole1EDB4A67" + ] + }, + "FargateServiceSecurityGroup0A0E79CB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "integ-aws-ecs-ebs-task-attach/FargateService/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + }, + "DependsOn": [ + "TaskDefTaskRole1EDB4A67" + ] + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ.json new file mode 100644 index 0000000000000..7e4cd54d7f54c --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "EBSTaskAttach/DefaultTest": { + "stacks": [ + "integ-aws-ecs-ebs-task-attach" + ], + "assertionStack": "EBSTaskAttach/DefaultTest/DeployAssert", + "assertionStackName": "EBSTaskAttachDefaultTestDeployAssertF52EF4F9" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/manifest.json new file mode 100644 index 0000000000000..5c7609dc81843 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/manifest.json @@ -0,0 +1,221 @@ +{ + "version": "36.0.0", + "artifacts": { + "integ-aws-ecs-ebs-task-attach.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integ-aws-ecs-ebs-task-attach.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integ-aws-ecs-ebs-task-attach": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integ-aws-ecs-ebs-task-attach.template.json", + "terminationProtection": false, + "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}/114105e2d29626a5303e48c5ab4b3be1059592f803b1d317c99665664cd954a9.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "integ-aws-ecs-ebs-task-attach.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "integ-aws-ecs-ebs-task-attach.assets" + ], + "metadata": { + "/integ-aws-ecs-ebs-task-attach/Vpc/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Vpc8378EB38" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPublicSubnet1Subnet5C2D37C4" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPublicSubnet1RouteTable6C95E38E" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPublicSubnet1RouteTableAssociation97140677" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPublicSubnet1DefaultRoute3DA9E72A" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/EIP": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPublicSubnet1EIPD7E02669" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/NATGateway": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPublicSubnet1NATGateway4D7517AA" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/Subnet": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPrivateSubnet1Subnet536B997A" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/RouteTable": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPrivateSubnet1RouteTableB2C5B500" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/RouteTableAssociation": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPrivateSubnet1RouteTableAssociation70C59FA6" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/DefaultRoute": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcPrivateSubnet1DefaultRouteBE02A9ED" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/IGW": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcIGWD7BA715C" + } + ], + "/integ-aws-ecs-ebs-task-attach/Vpc/VPCGW": [ + { + "type": "aws:cdk:logicalId", + "data": "VpcVPCGWBF912B6E" + } + ], + "/integ-aws-ecs-ebs-task-attach/FargateCluster/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FargateCluster7CCD5F93" + } + ], + "/integ-aws-ecs-ebs-task-attach/TaskDef/TaskRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TaskDefTaskRole1EDB4A67" + } + ], + "/integ-aws-ecs-ebs-task-attach/TaskDef/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TaskDef54694570" + } + ], + "/integ-aws-ecs-ebs-task-attach/EBSVolume/EBSRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "EBSVolumeEBSRoleC27DD941" + } + ], + "/integ-aws-ecs-ebs-task-attach/FargateService/Service": [ + { + "type": "aws:cdk:logicalId", + "data": "FargateServiceAC2B3B85" + } + ], + "/integ-aws-ecs-ebs-task-attach/FargateService/SecurityGroup/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FargateServiceSecurityGroup0A0E79CB" + } + ], + "/integ-aws-ecs-ebs-task-attach/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-aws-ecs-ebs-task-attach/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-aws-ecs-ebs-task-attach" + }, + "EBSTaskAttachDefaultTestDeployAssertF52EF4F9.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "EBSTaskAttachDefaultTestDeployAssertF52EF4F9.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "EBSTaskAttachDefaultTestDeployAssertF52EF4F9": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "EBSTaskAttachDefaultTestDeployAssertF52EF4F9.template.json", + "terminationProtection": false, + "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}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "EBSTaskAttachDefaultTestDeployAssertF52EF4F9.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "EBSTaskAttachDefaultTestDeployAssertF52EF4F9.assets" + ], + "metadata": { + "/EBSTaskAttach/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/EBSTaskAttach/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "EBSTaskAttach/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/tree.json new file mode 100644 index 0000000000000..896bcffef46a0 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.js.snapshot/tree.json @@ -0,0 +1,794 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "integ-aws-ecs-ebs-task-attach": { + "id": "integ-aws-ecs-ebs-task-attach", + "path": "integ-aws-ecs-ebs-task-attach", + "children": { + "Vpc": { + "id": "Vpc", + "path": "integ-aws-ecs-ebs-task-attach/Vpc", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/Resource", + "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": "integ-aws-ecs-ebs-task-attach/Vpc" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnVPC", + "version": "0.0.0" + } + }, + "PublicSubnet1": { + "id": "PublicSubnet1", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1", + "children": { + "Subnet": { + "id": "Subnet", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "availabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "cidrBlock": "10.0.0.0/17", + "mapPublicIpOnLaunch": true, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Public" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Public" + }, + { + "key": "Name", + "value": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1" + } + ], + "vpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/Acl", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "Name", + "value": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1" + } + ], + "vpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "subnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "destinationCidrBlock": "0.0.0.0/0", + "gatewayId": { + "Ref": "VpcIGWD7BA715C" + }, + "routeTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnRoute", + "version": "0.0.0" + } + }, + "EIP": { + "id": "EIP", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/EIP", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::EIP", + "aws:cdk:cloudformation:props": { + "domain": "vpc", + "tags": [ + { + "key": "Name", + "value": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnEIP", + "version": "0.0.0" + } + }, + "NATGateway": { + "id": "NATGateway", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1/NATGateway", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::NatGateway", + "aws:cdk:cloudformation:props": { + "allocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "subnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "tags": [ + { + "key": "Name", + "value": "integ-aws-ecs-ebs-task-attach/Vpc/PublicSubnet1" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnNatGateway", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.PublicSubnet", + "version": "0.0.0" + } + }, + "PrivateSubnet1": { + "id": "PrivateSubnet1", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1", + "children": { + "Subnet": { + "id": "Subnet", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/Subnet", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Subnet", + "aws:cdk:cloudformation:props": { + "availabilityZone": { + "Fn::Select": [ + 0, + { + "Fn::GetAZs": "" + } + ] + }, + "cidrBlock": "10.0.128.0/17", + "mapPublicIpOnLaunch": false, + "tags": [ + { + "key": "aws-cdk:subnet-name", + "value": "Private" + }, + { + "key": "aws-cdk:subnet-type", + "value": "Private" + }, + { + "key": "Name", + "value": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1" + } + ], + "vpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnSubnet", + "version": "0.0.0" + } + }, + "Acl": { + "id": "Acl", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/Acl", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "RouteTable": { + "id": "RouteTable", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/RouteTable", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::RouteTable", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "Name", + "value": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1" + } + ], + "vpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnRouteTable", + "version": "0.0.0" + } + }, + "RouteTableAssociation": { + "id": "RouteTableAssociation", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/RouteTableAssociation", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SubnetRouteTableAssociation", + "aws:cdk:cloudformation:props": { + "routeTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "subnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnSubnetRouteTableAssociation", + "version": "0.0.0" + } + }, + "DefaultRoute": { + "id": "DefaultRoute", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/PrivateSubnet1/DefaultRoute", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::Route", + "aws:cdk:cloudformation:props": { + "destinationCidrBlock": "0.0.0.0/0", + "natGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + }, + "routeTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnRoute", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.PrivateSubnet", + "version": "0.0.0" + } + }, + "IGW": { + "id": "IGW", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/IGW", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::InternetGateway", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "Name", + "value": "integ-aws-ecs-ebs-task-attach/Vpc" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnInternetGateway", + "version": "0.0.0" + } + }, + "VPCGW": { + "id": "VPCGW", + "path": "integ-aws-ecs-ebs-task-attach/Vpc/VPCGW", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::VPCGatewayAttachment", + "aws:cdk:cloudformation:props": { + "internetGatewayId": { + "Ref": "VpcIGWD7BA715C" + }, + "vpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnVPCGatewayAttachment", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.Vpc", + "version": "0.0.0" + } + }, + "FargateCluster": { + "id": "FargateCluster", + "path": "integ-aws-ecs-ebs-task-attach/FargateCluster", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-ecs-ebs-task-attach/FargateCluster/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECS::Cluster", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.CfnCluster", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.Cluster", + "version": "0.0.0" + } + }, + "TaskDef": { + "id": "TaskDef", + "path": "integ-aws-ecs-ebs-task-attach/TaskDef", + "children": { + "TaskRole": { + "id": "TaskRole", + "path": "integ-aws-ecs-ebs-task-attach/TaskDef/TaskRole", + "children": { + "ImportTaskRole": { + "id": "ImportTaskRole", + "path": "integ-aws-ecs-ebs-task-attach/TaskDef/TaskRole/ImportTaskRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-aws-ecs-ebs-task-attach/TaskDef/TaskRole/Resource", + "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" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-aws-ecs-ebs-task-attach/TaskDef/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECS::TaskDefinition", + "aws:cdk:cloudformation:props": { + "containerDefinitions": [ + { + "essential": true, + "image": "amazon/amazon-ecs-sample", + "mountPoints": [ + { + "containerPath": "/var/lib", + "readOnly": false, + "sourceVolume": "ebs1" + } + ], + "name": "web", + "portMappings": [ + { + "containerPort": 80, + "protocol": "tcp" + } + ] + } + ], + "cpu": "256", + "family": "integawsecsebstaskattachTaskDefB8F13A4F", + "memory": "512", + "networkMode": "awsvpc", + "requiresCompatibilities": [ + "FARGATE" + ], + "taskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + }, + "volumes": [ + { + "name": "ebs1", + "configuredAtLaunch": true + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.CfnTaskDefinition", + "version": "0.0.0" + } + }, + "web": { + "id": "web", + "path": "integ-aws-ecs-ebs-task-attach/TaskDef/web", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.ContainerDefinition", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.FargateTaskDefinition", + "version": "0.0.0" + } + }, + "EBSVolume": { + "id": "EBSVolume", + "path": "integ-aws-ecs-ebs-task-attach/EBSVolume", + "children": { + "EBSRole": { + "id": "EBSRole", + "path": "integ-aws-ecs-ebs-task-attach/EBSVolume/EBSRole", + "children": { + "ImportEBSRole": { + "id": "ImportEBSRole", + "path": "integ-aws-ecs-ebs-task-attach/EBSVolume/EBSRole/ImportEBSRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "integ-aws-ecs-ebs-task-attach/EBSVolume/EBSRole/Resource", + "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/AmazonECSInfrastructureRolePolicyForVolumes" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.ServiceManagedVolume", + "version": "0.0.0" + } + }, + "FargateService": { + "id": "FargateService", + "path": "integ-aws-ecs-ebs-task-attach/FargateService", + "children": { + "Service": { + "id": "Service", + "path": "integ-aws-ecs-ebs-task-attach/FargateService/Service", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECS::Service", + "aws:cdk:cloudformation:props": { + "cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "deploymentConfiguration": { + "maximumPercent": 200, + "minimumHealthyPercent": 50, + "alarms": { + "alarmNames": [], + "enable": false, + "rollback": false + } + }, + "desiredCount": 1, + "enableEcsManagedTags": false, + "launchType": "FARGATE", + "networkConfiguration": { + "awsvpcConfiguration": { + "assignPublicIp": "DISABLED", + "subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + ], + "securityGroups": [ + { + "Fn::GetAtt": [ + "FargateServiceSecurityGroup0A0E79CB", + "GroupId" + ] + } + ] + } + }, + "taskDefinition": { + "Ref": "TaskDef54694570" + }, + "volumeConfigurations": [ + { + "name": "ebs1", + "managedEbsVolume": { + "roleArn": { + "Fn::GetAtt": [ + "EBSVolumeEBSRoleC27DD941", + "Arn" + ] + }, + "encrypted": true, + "filesystemType": "ext4", + "iops": 4000, + "throughput": 500, + "volumeType": "gp3", + "sizeInGiB": 15, + "tagSpecifications": [ + { + "resourceType": "volume", + "propagateTags": "SERVICE", + "tags": [ + { + "key": "purpose", + "value": "production" + } + ] + }, + { + "resourceType": "volume", + "propagateTags": "TASK_DEFINITION", + "tags": [ + { + "key": "purpose", + "value": "development" + } + ] + } + ] + } + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.CfnService", + "version": "0.0.0" + } + }, + "SecurityGroup": { + "id": "SecurityGroup", + "path": "integ-aws-ecs-ebs-task-attach/FargateService/SecurityGroup", + "children": { + "Resource": { + "id": "Resource", + "path": "integ-aws-ecs-ebs-task-attach/FargateService/SecurityGroup/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::EC2::SecurityGroup", + "aws:cdk:cloudformation:props": { + "groupDescription": "integ-aws-ecs-ebs-task-attach/FargateService/SecurityGroup", + "securityGroupEgress": [ + { + "cidrIp": "0.0.0.0/0", + "description": "Allow all outbound traffic by default", + "ipProtocol": "-1" + } + ], + "vpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.CfnSecurityGroup", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ec2.SecurityGroup", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecs.FargateService", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-aws-ecs-ebs-task-attach/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-aws-ecs-ebs-task-attach/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "EBSTaskAttach": { + "id": "EBSTaskAttach", + "path": "EBSTaskAttach", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "EBSTaskAttach/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "EBSTaskAttach/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "EBSTaskAttach/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "EBSTaskAttach/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "EBSTaskAttach/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.ts new file mode 100644 index 0000000000000..d178806e48136 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/fargate/integ.ebs-taskattach.ts @@ -0,0 +1,77 @@ +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as cdk from 'aws-cdk-lib'; +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import * as integ from '@aws-cdk/integ-tests-alpha'; +import { Construct } from 'constructs'; + +class TestStack extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'Vpc', { + maxAzs: 1, + restrictDefaultSecurityGroup: false, + }); + + const cluster = new ecs.Cluster(this, 'FargateCluster', { + vpc, + }); + + const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef'); + + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + portMappings: [{ + containerPort: 80, + protocol: ecs.Protocol.TCP, + }], + }); + + const volume = new ecs.ServiceManagedVolume(this, 'EBSVolume', { + name: 'ebs1', + managedEBSVolume: { + encrypted: true, + volumeType: ec2.EbsDeviceVolumeType.GP3, + size: cdk.Size.gibibytes(15), + iops: 4000, + throughput: 500, + fileSystemType: ecs.FileSystemType.EXT4, + tagSpecifications: [{ + tags: { + purpose: 'production', + }, + propagateTags: ecs.EbsPropagatedTagSource.SERVICE, + }, + { + tags: { + purpose: 'development', + }, + propagateTags: ecs.EbsPropagatedTagSource.TASK_DEFINITION, + }], + }, + }); + + volume.mountIn(container, { + containerPath: '/var/lib', + readOnly: false, + }); + + taskDefinition.addVolume(volume); + + const service = new ecs.FargateService(this, 'FargateService', { + cluster, + taskDefinition, + desiredCount: 1, + }); + + service.addVolume(volume); + } +} + +const app = new cdk.App(); +const stack = new TestStack(app, 'integ-aws-ecs-ebs-task-attach'); + +new integ.IntegTest(app, 'EBSTaskAttach', { + testCases: [stack], +}); +app.synth(); diff --git a/packages/aws-cdk-lib/aws-ecs/README.md b/packages/aws-cdk-lib/aws-ecs/README.md index b120df0bf0049..3b0db4577e94c 100644 --- a/packages/aws-cdk-lib/aws-ecs/README.md +++ b/packages/aws-cdk-lib/aws-ecs/README.md @@ -1631,6 +1631,89 @@ const customService = new ecs.FargateService(this, 'CustomizedService', { }); ``` +## ServiceManagedVolume + +Amazon ECS now supports the attachment of Amazon Elastic Block Store (EBS) volumes to ECS tasks, +allowing you to utilize persistent, high-performance block storage with your ECS services. +This feature supports various use cases, such as using EBS volumes as extended ephemeral storage or +loading data from EBS snapshots. +You can also specify `encrypted: true` so that ECS will manage the KMS key. If you want to use your own KMS key, you may do so by providing both `encrypted: true` and `kmsKeyId`. + +You can only attach a single volume for each task in the ECS Service. + +To add an empty EBS Volume to an ECS Service, call service.addVolume(). + +```ts +declare const cluster: ecs.Cluster; +const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef'); + +const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + portMappings: [{ + containerPort: 80, + protocol: ecs.Protocol.TCP, + }], +}); + +const volume = new ecs.ServiceManagedVolume(this, 'EBSVolume', { + name: 'ebs1', + managedEBSVolume: { + size: Size.gibibytes(15), + volumeType: ec2.EbsDeviceVolumeType.GP3, + fileSystemType: ecs.FileSystemType.XFS, + tagSpecifications: [{ + tags: { + purpose: 'production', + }, + propagateTags: ecs.EbsPropagatedTagSource.SERVICE, + }], + }, +}); + +volume.mountIn(container, { + containerPath: '/var/lib', + readOnly: false, +}); + +taskDefinition.addVolume(volume); + +const service = new ecs.FargateService(this, 'FargateService', { + cluster, + taskDefinition, +}); + +service.addVolume(volume); +``` + +To create an EBS volume from an existing snapshot by specifying the `snapShotId` while adding a volume to the service. + +```ts +declare const container: ecs.ContainerDefinition; +declare const cluster: ecs.Cluster; +declare const taskDefinition: ecs.TaskDefinition; + +const volumeFromSnapshot = new ecs.ServiceManagedVolume(this, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + snapShotId: 'snap-066877671789bd71b', + volumeType: ec2.EbsDeviceVolumeType.GP3, + fileSystemType: ecs.FileSystemType.XFS, + }, +}); + +volumeFromSnapshot.mountIn(container, { + containerPath: '/var/lib', + readOnly: false, +}); +taskDefinition.addVolume(volumeFromSnapshot); +const service = new ecs.FargateService(this, 'FargateService', { + cluster, + taskDefinition, +}); + +service.addVolume(volumeFromSnapshot); +``` + ## Enable pseudo-terminal (TTY) allocation You can allocate a pseudo-terminal (TTY) for a container passing `pseudoTerminal` option while adding the container 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 9726ef21526ce..00c0941db1022 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 @@ -1,5 +1,6 @@ import { Construct } from 'constructs'; import { ScalableTaskCount } from './scalable-task-count'; +import { ServiceManagedVolume } from './service-managed-volume'; import * as appscaling from '../../../aws-applicationautoscaling'; import * as cloudwatch from '../../../aws-cloudwatch'; import * as ec2 from '../../../aws-ec2'; @@ -357,6 +358,14 @@ export interface BaseServiceOptions { * @default - Uses the revision of the passed task definition deployed by CloudFormation */ readonly taskDefinitionRevision?: TaskDefinitionRevision; + + /** + * Configuration details for a volume used by the service. This allows you to specify + * details about the EBS volume that can be attched to ECS tasks. + * + * @default - undefined + */ + readonly volumeConfigurations?: ServiceManagedVolume[]; } /** @@ -576,6 +585,11 @@ export abstract class BaseService extends Resource private readonly resource: CfnService; private scalableTaskCount?: ScalableTaskCount; + /** + * All volumes + */ + private readonly volumes: ServiceManagedVolume[] = []; + /** * Constructs a new instance of the BaseService class. */ @@ -626,6 +640,7 @@ export abstract class BaseService extends Resource networkConfiguration: Lazy.any({ produce: () => this.networkConfiguration }, { omitEmptyArray: true }), serviceRegistries: Lazy.any({ produce: () => this.serviceRegistries }, { omitEmptyArray: true }), serviceConnectConfiguration: Lazy.any({ produce: () => this._serviceConnectConfig }, { omitEmptyArray: true }), + volumeConfigurations: Lazy.any({ produce: () => this.renderVolumes() }, { omitEmptyArray: true }), ...additionalProps, }); @@ -686,6 +701,10 @@ export abstract class BaseService extends Resource this.enableServiceConnect(props.serviceConnectConfiguration); } + if (props.volumeConfigurations) { + props.volumeConfigurations.forEach(v => this.addVolume(v)); + } + if (props.enableExecuteCommand) { this.enableExecuteCommand(); @@ -721,6 +740,48 @@ export abstract class BaseService extends Resource this.node.defaultChild = this.resource; } + /** + * Adds a volume to the Service. + */ + public addVolume(volume: ServiceManagedVolume) { + this.volumes.push(volume); + } + + private renderVolumes(): CfnService.ServiceVolumeConfigurationProperty[] { + if (this.volumes.length > 1) { + throw new Error(`Only one EBS volume can be specified for 'volumeConfigurations', got: ${this.volumes.length}`); + } + return this.volumes.map(renderVolume); + function renderVolume(spec: ServiceManagedVolume): CfnService.ServiceVolumeConfigurationProperty { + const tagSpecifications = spec.config?.tagSpecifications?.map(ebsTagSpec => { + return { + resourceType: 'volume', + propagateTags: ebsTagSpec.propagateTags, + tags: ebsTagSpec.tags ? Object.entries(ebsTagSpec.tags).map(([key, value]) => ({ + key: key, + value: value, + })) : undefined, + } as CfnService.EBSTagSpecificationProperty; + }); + + return { + name: spec.name, + managedEbsVolume: spec.config && { + roleArn: spec.role.roleArn, + encrypted: spec.config.encrypted, + filesystemType: spec.config.fileSystemType, + iops: spec.config.iops, + kmsKeyId: spec.config.kmsKeyId?.keyId, + throughput: spec.config.throughput, + volumeType: spec.config.volumeType, + snapshotId: spec.config.snapShotId, + sizeInGiB: spec.config.size?.toGibibytes(), + tagSpecifications: tagSpecifications, + }, + }; + } + } + /** * Enable Deployment Alarms which take advantage of arbitrary alarms and configure them after service initialization. * If you have already enabled deployment alarms, this function can be used to tell ECS about additional alarms that diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/service-managed-volume.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/service-managed-volume.ts new file mode 100644 index 0000000000000..6cea5e124224a --- /dev/null +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/service-managed-volume.ts @@ -0,0 +1,310 @@ +import { Construct } from 'constructs'; +import * as ec2 from '../../../aws-ec2'; +import * as iam from '../../../aws-iam'; +import * as kms from '../../../aws-kms'; +import { Size, Token } from '../../../core'; +import { BaseMountPoint, ContainerDefinition } from '../container-definition'; + +/** +* Represents the Volume configuration for an ECS service. +*/ +export interface ServiceManagedVolumeProps { + /** + * The name of the volume. This corresponds to the name provided in the ECS TaskDefinition. + */ + readonly name: string; + + /** + * Configuration for an Amazon Elastic Block Store (EBS) volume managed by ECS. + * + * @default - undefined + */ + readonly managedEBSVolume?: ServiceManagedEBSVolumeConfiguration +} + +/** +* Represents the configuration for an ECS Service managed EBS volume. +*/ +export interface ServiceManagedEBSVolumeConfiguration { + /** + * An IAM role that allows ECS to make calls to EBS APIs on your behalf. + * This role is required to create and manage the Amazon EBS volume. + * + * @default - automatically generated role. + */ + readonly role?: iam.IRole; + + /** + * Indicates whether the volume should be encrypted. + * + * @default - Default Amazon EBS encryption. + */ + readonly encrypted?: boolean; + + /** + * AWS Key Management Service key to use for Amazon EBS encryption. + * + * @default - When `encryption` is turned on and no `kmsKey` is specified, + * the default AWS managed key for Amazon EBS volumes is used. + */ + readonly kmsKeyId?: kms.IKey; + + /** + * The volume type. + * + * @default - ec2.EbsDeviceVolumeType.GP2 + */ + readonly volumeType?: ec2.EbsDeviceVolumeType; + + /** + * The size of the volume in GiB. + * + * You must specify either `size` or `snapshotId`. + * You can optionally specify a volume size greater than or equal to the snapshot size. + * + * The following are the supported volume size values for each volume type. + * - gp2 and gp3: 1-16,384 + * - io1 and io2: 4-16,384 + * - st1 and sc1: 125-16,384 + * - standard: 1-1,024 + * + * @default - The snapshot size is used for the volume size if you specify `snapshotId`, + * otherwise this parameter is required. + */ + readonly size?: Size; + + /** + * The snapshot that Amazon ECS uses to create the volume. + * + * You must specify either `size` or `snapshotId`. + * + * @default - No snapshot. + */ + readonly snapShotId?: string; + + /** + * The number of I/O operations per second (IOPS). + * + * For gp3, io1, and io2 volumes, this represents the number of IOPS that are provisioned + * for the volume. For gp2 volumes, this represents the baseline performance of the volume + * and the rate at which the volume accumulates I/O credits for bursting. + * + * The following are the supported values for each volume type. + * - gp3: 3,000 - 16,000 IOPS + * - io1: 100 - 64,000 IOPS + * - io2: 100 - 256,000 IOPS + * + * This parameter is required for io1 and io2 volume types. The default for gp3 volumes is + * 3,000 IOPS. This parameter is not supported for st1, sc1, or standard volume types. + * + * @default - undefined + */ + readonly iops?: number; + + /** + * The throughput to provision for a volume, in MiB/s, with a maximum of 1,000 MiB/s. + * + * This parameter is only supported for the gp3 volume type. + * + * @default - No throughput. + */ + readonly throughput?: number; + + /** + * The Linux filesystem type for the volume. + * + * For volumes created from a snapshot, you must specify the same filesystem type that + * the volume was using when the snapshot was created. + * The available filesystem types are ext3, ext4, and xfs. + * + * @default - FileSystemType.XFS + */ + readonly fileSystemType?: FileSystemType; + + /** + * Specifies the tags to apply to the volume and whether to propagate those tags to the volume. + * + * @default - No tags are specified. + */ + readonly tagSpecifications?: EBSTagSpecification[]; +} + +/** + * Tag Specification for EBS volume. + */ +export interface EBSTagSpecification { + /** + * The tags to apply to the volume. + * + * @default - No tags + */ + readonly tags?: {[key: string]: string}; + + /** + * Specifies whether to propagate the tags from the task definition or the service to the task. + * Valid values are: PropagatedTagSource.SERVICE, PropagatedTagSource.TASK_DEFINITION + * + * @default - undefined + */ + readonly propagateTags?: EbsPropagatedTagSource; +} + +/** + * FileSystemType for Service Managed EBS Volume Configuration. + */ +export enum FileSystemType { + /** + * ext3 type + */ + EXT3 = 'ext3', + /** + * ext4 type + */ + EXT4 = 'ext4', + /** + * xfs type + */ + XFS = 'xfs', +} + +/** + * Propagate tags for EBS Volume Configuration from either service or task definition. + */ +export enum EbsPropagatedTagSource { + /** + * SERVICE + */ + SERVICE = 'SERVICE', + /** + * TASK_DEFINITION + */ + TASK_DEFINITION = 'TASK_DEFINITION', +} + +/** + * Defines the mount point details for attaching a volume to a container. + */ +export interface ContainerMountPoint extends BaseMountPoint { +} + +/** + * Represents a service-managed volume and always configured at launch. + */ +export class ServiceManagedVolume extends Construct { + /** + * Name of the volume, referenced by taskdefintion and mount point. + */ + public readonly name: string; + + /** + * Volume configuration + */ + public readonly config?: ServiceManagedEBSVolumeConfiguration; + + /** + * configuredAtLaunch indicates volume at launch time, referenced by taskdefinition volume. + */ + public readonly configuredAtLaunch: boolean = true; + + /** + * An IAM role that allows ECS to make calls to EBS APIs. + * If not provided, a new role with appropriate permissions will be created by default. + */ + public readonly role: iam.IRole; + + constructor(scope: Construct, id: string, props: ServiceManagedVolumeProps) { + super(scope, id); + this.validateEbsVolumeConfiguration(props.managedEBSVolume); + this.name = props.name; + this.role = props.managedEBSVolume?.role ?? new iam.Role(this, 'EBSRole', { + assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSInfrastructureRolePolicyForVolumes'), + ], + }); + this.config = { + ...props.managedEBSVolume, + role: this.role, + }; + } + + /** + * Mounts the service managed volume to a specified container at a defined mount point. + * @param container The container to mount the volume on. + * @param mountPoint The mounting point details within the container. + */ + public mountIn(container: ContainerDefinition, mountPoint: ContainerMountPoint) { + container.addMountPoints({ + sourceVolume: this.name, + ...mountPoint, + }); + } + + private validateEbsVolumeConfiguration(volumeConfig?: ServiceManagedEBSVolumeConfiguration) { + if (!volumeConfig) return; + + const { volumeType = ec2.EbsDeviceVolumeType.GP2, iops, size, throughput, snapShotId } = volumeConfig; + + // Validate if both size and snapShotId are not specified. + if (size === undefined && snapShotId === undefined) { + throw new Error('\'size\' or \'snapShotId\' must be specified'); + } + + if (snapShotId && !Token.isUnresolved(snapShotId) && !/^snap-[0-9a-fA-F]+$/.test(snapShotId)) { + throw new Error(`'snapshotId' does match expected pattern. Expected 'snap-' (ex: 'snap-05abe246af') or Token, got: ${snapShotId}`); + } + + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-servicemanagedebsvolumeconfiguration.html#cfn-ecs-service-servicemanagedebsvolumeconfiguration-sizeingib + const sizeInGiBRanges = { + [ec2.EbsDeviceVolumeType.GP2]: { minSize: 1, maxSize: 16384 }, + [ec2.EbsDeviceVolumeType.GP3]: { minSize: 1, maxSize: 16384 }, + [ec2.EbsDeviceVolumeType.IO1]: { minSize: 4, maxSize: 16384 }, + [ec2.EbsDeviceVolumeType.IO2]: { minSize: 4, maxSize: 16384 }, + [ec2.EbsDeviceVolumeType.SC1]: { minSize: 125, maxSize: 16384 }, + [ec2.EbsDeviceVolumeType.ST1]: { minSize: 125, maxSize: 16384 }, + [ec2.EbsDeviceVolumeType.STANDARD]: { minSize: 1, maxSize: 1024 }, + }; + + // Validate volume size ranges. + if (size !== undefined) { + const { minSize, maxSize } = sizeInGiBRanges[volumeType]; + if (size.toGibibytes() < minSize || size.toGibibytes() > maxSize) { + throw new Error(`'${volumeType}' volumes must have a size between ${minSize} and ${maxSize} GiB, got ${size.toGibibytes()} GiB`); + } + } + + // Validate throughput. + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-servicemanagedebsvolumeconfiguration.html#cfn-ecs-service-servicemanagedebsvolumeconfiguration-throughput + if (throughput !== undefined) { + if (volumeType !== ec2.EbsDeviceVolumeType.GP3) { + throw new Error(`'throughput' can only be configured with gp3 volume type, got ${volumeType}`); + } else if (!Token.isUnresolved(throughput) && throughput > 1000) { + throw new Error(`'throughput' must be less than or equal to 1000 MiB/s, got ${throughput} MiB/s`); + } + } + + // Check if IOPS is not supported for the volume type. + // https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateVolume.html + if ([ec2.EbsDeviceVolumeType.SC1, ec2.EbsDeviceVolumeType.ST1, ec2.EbsDeviceVolumeType.STANDARD, + ec2.EbsDeviceVolumeType.GP2].includes(volumeType) && iops !== undefined) { + throw new Error(`'iops' cannot be specified with sc1, st1, gp2 and standard volume types, got ${volumeType}`); + } + + // Check if IOPS is required but not provided. + if ([ec2.EbsDeviceVolumeType.IO1, ec2.EbsDeviceVolumeType.IO2].includes(volumeType) && iops === undefined) { + throw new Error(`'iops' must be specified with io1 or io2 volume types, got ${volumeType}`); + } + + // Validate IOPS range if specified. + const iopsRanges: { [key: string]: { min: number, max: number } } = {}; + iopsRanges[ec2.EbsDeviceVolumeType.GP3]= { min: 3000, max: 16000 }; + iopsRanges[ec2.EbsDeviceVolumeType.IO1]= { min: 100, max: 64000 }; + iopsRanges[ec2.EbsDeviceVolumeType.IO2]= { min: 100, max: 256000 }; + if (iops !== undefined && !Token.isUnresolved(iops)) { + const { min, max } = iopsRanges[volumeType]; + if ((iops < min || iops > max)) { + throw new Error(`'${volumeType}' volumes must have 'iops' between ${min} and ${max}, got ${iops}`); + } + } + } +} 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 34793208d28d5..cb2823ffb24ba 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 @@ -525,6 +525,7 @@ export class TaskDefinition extends TaskDefinitionBase { return { host: spec.host, name: spec.name, + configuredAtLaunch: spec.configuredAtLaunch, dockerVolumeConfiguration: spec.dockerVolumeConfiguration && { autoprovision: spec.dockerVolumeConfiguration.autoprovision, driver: spec.dockerVolumeConfiguration.driver, @@ -653,9 +654,21 @@ export class TaskDefinition extends TaskDefinitionBase { * Adds a volume to the task definition. */ public addVolume(volume: Volume) { + this.validateVolume(volume); this.volumes.push(volume); } + private validateVolume(volume: Volume):void { + if (volume.configuredAtLaunch !== true) { + return; + } + + // Other volume configurations must not be specified. + if (volume.host || volume.dockerVolumeConfiguration || volume.efsVolumeConfiguration) { + throw new Error(`Volume Configurations must not be specified for '${volume.name}' when 'configuredAtLaunch' is set to true`); + } + } + /** * Adds the specified placement constraint to the task definition. */ @@ -764,7 +777,25 @@ export class TaskDefinition extends TaskDefinitionBase { } } }); + // Validate if multiple volumes configured with configuredAtLaunch. + const runtimeVolumes = this.volumes.filter(vol => vol.configuredAtLaunch); + if (runtimeVolumes.length > 1) { + const volumeNames = runtimeVolumes.map(vol => vol.name).join(','); + ret.push(`More than one volume is configured at launch: [${volumeNames}]`); + } + + // Validate that volume with configuredAtLaunch set to true is mounted by at least one container. + for (const volume of this.volumes) { + if (volume.configuredAtLaunch) { + const isVolumeMounted = this.containers.some(container => { + return container.mountPoints.some(mp => mp.sourceVolume === volume.name); + }); + if (!isVolumeMounted) { + ret.push(`Volume '${volume.name}' should be mounted by at least one container when 'configuredAtLaunch' is true`); + } + } + } return ret; } @@ -978,6 +1009,13 @@ export interface Volume { */ readonly name: string; + /** + * Indicates if the volume should be configured at launch. + * + * @default false + */ + readonly configuredAtLaunch ?: boolean; + /** * This property is specified when you are using Docker volumes. * diff --git a/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts index 0478a059df68d..1d4e3d995dc76 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts @@ -1364,9 +1364,9 @@ export interface ScratchSpace { } /** - * The details of data volume mount points for a container. + * The base details of where a volume will be mounted within a container */ -export interface MountPoint { +export interface BaseMountPoint { /** * The path on the container to mount the host volume at. */ @@ -1378,6 +1378,12 @@ export interface MountPoint { * If this value is false, then the container can write to the volume. */ readonly readOnly: boolean, +} + +/** + * The details of data volume mount points for a container. + */ +export interface MountPoint extends BaseMountPoint { /** * The name of the volume to mount. * diff --git a/packages/aws-cdk-lib/aws-ecs/lib/index.ts b/packages/aws-cdk-lib/aws-ecs/lib/index.ts index 498e8a0db5081..e3e34f236dd4d 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/index.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/index.ts @@ -1,6 +1,7 @@ export * from './base/base-service'; export * from './base/scalable-task-count'; export * from './base/task-definition'; +export * from './base/service-managed-volume'; export * from './container-definition'; export * from './container-image'; diff --git a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts index aec9f09bff889..791c913f261b4 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-service.test.ts @@ -4,6 +4,7 @@ import * as appscaling from '../../../aws-applicationautoscaling'; import * as cloudwatch from '../../../aws-cloudwatch'; 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 logs from '../../../aws-logs'; import * as s3 from '../../../aws-s3'; @@ -15,6 +16,7 @@ import * as cxapi from '../../../cx-api'; import { ECS_ARN_FORMAT_INCLUDES_CLUSTER_NAME } from '../../../cx-api'; import * as ecs from '../../lib'; import { DeploymentControllerType, LaunchType, PropagatedTagSource, ServiceConnectProps } from '../../lib/base/base-service'; +import { ServiceManagedVolume } from '../../lib/base/service-managed-volume'; import { addDefaultCapacityProvider } from '../util'; describe('fargate service', () => { @@ -1453,6 +1455,553 @@ describe('fargate service', () => { }); }); + describe('When setting up a service volume configurations', ()=>{ + let service: ecs.FargateService; + let stack: cdk.Stack; + let cluster: ecs.Cluster; + let taskDefinition: ecs.TaskDefinition; + let container: ecs.ContainerDefinition; + let role: iam.IRole; + let app: cdk.App; + + beforeEach(() => { + // GIVEN + app = new cdk.App(); + stack = new cdk.Stack(app); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'), + }); + container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + service = new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + }); + test('success when adding a service volume', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + role: role, + size: cdk.Size.gibibytes(20), + fileSystemType: ecs.FileSystemType.XFS, + tagSpecifications: [{ + tags: { + purpose: 'production', + }, + propagateTags: ecs.EbsPropagatedTagSource.SERVICE, + }], + }, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + VolumeConfigurations: [ + { + ManagedEBSVolume: { + RoleArn: { 'Fn::GetAtt': ['Role1ABCC5F0', 'Arn'] }, + SizeInGiB: 20, + FilesystemType: 'xfs', + TagSpecifications: [ + { + PropagateTags: 'SERVICE', + ResourceType: 'volume', + Tags: [ + { + Key: 'purpose', + Value: 'production', + }, + ], + }, + ], + }, + Name: 'nginx-vol', + }, + ], + }); + }); + + test('success when mounting via ServiceManagedVolume', () => { + // WHEN + const volume = new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + role: role, + size: cdk.Size.gibibytes(20), + tagSpecifications: [{ + tags: { + purpose: 'production', + }, + propagateTags: ecs.EbsPropagatedTagSource.SERVICE, + }], + }, + }); + taskDefinition.addVolume(volume); + service.addVolume(volume); + volume.mountIn(container, { + containerPath: '/var/lib', + readOnly: false, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + VolumeConfigurations: [ + { + ManagedEBSVolume: { + RoleArn: { 'Fn::GetAtt': ['Role1ABCC5F0', 'Arn'] }, + SizeInGiB: 20, + TagSpecifications: [ + { + PropagateTags: 'SERVICE', + ResourceType: 'volume', + Tags: [ + { + Key: 'purpose', + Value: 'production', + }, + ], + }, + ], + }, + Name: 'nginx-vol', + }, + ], + }); + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + MountPoints: [ + { + ContainerPath: '/var/lib', + ReadOnly: false, + SourceVolume: 'nginx-vol', + }, + ], + }, + ], + Volumes: [ + { + Name: 'nginx-vol', + ConfiguredAtLaunch: true, + }, + ], + }); + }); + + test('throw an error when multiple volume configurations are added to ECS service', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + const vol1 = new ServiceManagedVolume(stack, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + size: cdk.Size.gibibytes(15), + }, + }); + const vol2 = new ServiceManagedVolume(stack, 'ebs1', { + name: 'ebs1', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + size: cdk.Size.gibibytes(15), + }, + }); + service.addVolume(vol1); + service.addVolume(vol2); + expect(() => { + app.synth(); + }).toThrow(/Only one EBS volume can be specified for 'volumeConfigurations', got: 2/); + }); + + test('create a default ebsrole when not provided', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + size: cdk.Size.gibibytes(20), + fileSystemType: ecs.FileSystemType.XFS, + tagSpecifications: [{ + tags: { + purpose: 'production', + }, + propagateTags: ecs.EbsPropagatedTagSource.SERVICE, + }], + }, + })); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + VolumeConfigurations: [ + { + ManagedEBSVolume: { + RoleArn: { 'Fn::GetAtt': ['EBSVolumeEBSRoleD38B9F31', 'Arn'] }, + SizeInGiB: 20, + FilesystemType: 'xfs', + TagSpecifications: [ + { + PropagateTags: 'SERVICE', + ResourceType: 'volume', + Tags: [ + { + Key: 'purpose', + Value: 'production', + }, + ], + }, + ], + }, + Name: 'nginx-vol', + }, + ], + }); + }); + + test('throw an error when both size and snapshotId are not provided', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + }, + })); + }).toThrow("'size' or 'snapShotId' must be specified"); + }); + + test('throw an error snapshot does not match pattern', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + snapShotId: 'snap-0d48decab5c493eee_', + }, + })); + }).toThrow("'snapshotId' does match expected pattern. Expected 'snap-' (ex: 'snap-05abe246af') or Token, got: snap-0d48decab5c493eee_"); + }); + + test('success when snapshotId matches the pattern', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + const vol = new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + snapShotId: 'snap-0d48decab5c493eee', + }, + }); + service.addVolume(vol); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + VolumeConfigurations: [ + { + ManagedEBSVolume: { + RoleArn: { 'Fn::GetAtt': ['EBSVolumeEBSRoleD38B9F31', 'Arn'] }, + SnapshotId: 'snap-0d48decab5c493eee', + FilesystemType: 'xfs', + }, + Name: 'nginx-vol', + }, + ], + }); + }); + + test('throw an error when size is greater than 16384 for gp2', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + size: cdk.Size.gibibytes(16390), + }, + })); + }).toThrow(/'gp2' volumes must have a size between 1 and 16384 GiB, got 16390 GiB/); + }); + + test('throw an error when size is less than 4 for volume type io1', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.IO1, + size: cdk.Size.gibibytes(0), + }, + })); + }).toThrow(/'io1' volumes must have a size between 4 and 16384 GiB, got 0 GiB/); + }); + test('throw an error when size is greater than 1024 for volume type standard', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.STANDARD, + size: cdk.Size.gibibytes(1500), + }, + })); + }).toThrow(/'standard' volumes must have a size between 1 and 1024 GiB, got 1500 GiB/); + }); + + test('throw an error if throughput is configured for volumetype gp2', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + size: cdk.Size.gibibytes(10), + throughput: 0, + }, + })); + }).toThrow(/'throughput' can only be configured with gp3 volume type, got gp2/); + }); + + test('throw an error if throughput is greater tahn 1000 for volume type gp3', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.GP3, + size: cdk.Size.gibibytes(10), + throughput: 10001, + }, + })); + }).toThrow("'throughput' must be less than or equal to 1000 MiB/s, got 10001 MiB/s"); + }); + + test('throw an error if throughput is greater tahn 1000 for volume type gp3', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.GP3, + size: cdk.Size.gibibytes(10), + throughput: 10001, + }, + })); + }).toThrow("'throughput' must be less than or equal to 1000 MiB/s, got 10001 MiB/s"); + }); + + test('throw an error if iops is not supported for volume type sc1', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.SC1, + size: cdk.Size.gibibytes(125), + iops: 0, + }, + })); + }).toThrow(/'iops' cannot be specified with sc1, st1, gp2 and standard volume types, got sc1/); + }); + + test('throw an error if iops is not supported for volume type sc1', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + size: cdk.Size.gibibytes(125), + iops: 0, + }, + })); + }).toThrow(/'iops' cannot be specified with sc1, st1, gp2 and standard volume types, got gp2/); + }); + + test('throw an error if if iops is required but not provided for volume type io2', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.IO2, + size: cdk.Size.gibibytes(125), + }, + })); + }).toThrow(/'iops' must be specified with io1 or io2 volume types, got io2/); + }); + + test('throw an error if if iops is less than 100 for volume type io2', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.IO2, + size: cdk.Size.gibibytes(125), + iops: 0, + }, + })); + }).toThrow("io2' volumes must have 'iops' between 100 and 256000, got 0"); + }); + + test('throw an error if if iops is greater than 256000 for volume type io2', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + expect(() => { + service.addVolume(new ServiceManagedVolume(stack, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.IO2, + size: cdk.Size.gibibytes(125), + iops: 256001, + }, + })); + }).toThrow("io2' volumes must have 'iops' between 100 and 256000, got 256001"); + }); + + test('success adding gp3 volume with throughput 0', ()=> { + // WHEN + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + + service.addVolume(new ServiceManagedVolume(stack, 'EBSVolume', { + name: 'nginx-vol', + managedEBSVolume: { + fileSystemType: ecs.FileSystemType.XFS, + volumeType: ec2.EbsDeviceVolumeType.GP3, + size: cdk.Size.gibibytes(15), + throughput: 0, + }, + })); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::Service', { + VolumeConfigurations: [ + { + ManagedEBSVolume: { + RoleArn: { 'Fn::GetAtt': ['EBSVolumeEBSRoleC27DD941', 'Arn'] }, + SizeInGiB: 15, + FilesystemType: 'xfs', + VolumeType: 'gp3', + Throughput: 0, + }, + Name: 'nginx-vol', + }, + ], + }); + }); + }); + describe('When setting up a health check', () => { test('grace period is respected', () => { // GIVEN diff --git a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-task-definition.test.ts b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-task-definition.test.ts index eb1c280ec69c8..df7beb1b3fec8 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-task-definition.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/fargate/fargate-task-definition.test.ts @@ -162,6 +162,45 @@ describe('fargate task definition', () => { // THEN }); }); + describe('When configuredAtLaunch in the Volume', ()=> { + test('do not throw when configuredAtLaunch is false', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + const taskDefinition =new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + taskDefinition.addVolume({ + name: 'nginx-vol', + efsVolumeConfiguration: { + fileSystemId: 'fs-1234', + }, + }); + taskDefinition.addVolume({ + name: 'nginx-vol1', + efsVolumeConfiguration: { + fileSystemId: 'fs-456', + }, + }); + }); + }); + test('throws when other volume configuration set with configuredAtLaunch', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + const taskDefinition =new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + taskDefinition.addVolume({ + name: 'nginx-vol', + configuredAtLaunch: true, + efsVolumeConfiguration: { + fileSystemId: 'fs-1234', + }, + }); + }).toThrow(/Volume Configurations must not be specified for 'nginx-vol' when 'configuredAtLaunch' is set to true/); + }); + }); describe('When importing from an existing Fargate TaskDefinition', () => { test('can succeed using TaskDefinition Arn', () => { diff --git a/packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts b/packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts index 2b4bad0b5d5a1..f098b3e89afcb 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts @@ -1,8 +1,10 @@ import { Template } from '../../assertions'; +import { EbsDeviceVolumeType } from '../../aws-ec2'; import * as ecr from '../../aws-ecr'; import * as iam from '../../aws-iam'; import * as cdk from '../../core'; import * as ecs from '../lib'; +import { ServiceManagedVolume } from '../lib/base/service-managed-volume'; describe('task definition', () => { describe('When creating a new TaskDefinition', () => { @@ -230,6 +232,146 @@ describe('task definition', () => { }).toThrow("Port mapping name 'api' cannot appear in both 'Container2' and 'Container'"); }); + test('throws when multiple runtime volumes are set', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition =new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const container = taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + container.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol', + }); + const container1 = taskDefinition.addContainer('front', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + container1.addMountPoints({ + containerPath: '/var/lib', + readOnly: false, + sourceVolume: 'nginx-vol1', + }); + taskDefinition.addVolume({ + name: 'nginx-vol', + configuredAtLaunch: true, + }); + taskDefinition.addVolume({ + name: 'nginx-vol1', + configuredAtLaunch: true, + }); + + // THEN + expect(() => { + Template.fromStack(stack); + }).toThrow('More than one volume is configured at launch: [nginx-vol,nginx-vol1]'); + }); + + test('throws when none of the container mounts the volume', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition =new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + taskDefinition.addVolume({ + name: 'nginx-vol', + configuredAtLaunch: true, + }); + + // THEN + expect(() => { + Template.fromStack(stack); + }).toThrow(/Volume 'nginx-vol' should be mounted by at least one container when 'configuredAtLaunch' is true/); + }); + + test('throws when none of the container mount the volume using ServiceManagedVolume', () => { + // GIVEN + const stack = new cdk.Stack(); + const ebsRole = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'), + }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + taskDefinition.addContainer('db', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + const serviceManagedVolume = new ServiceManagedVolume(stack, 'EBS Volume', { + name: 'nginx-vol', + managedEBSVolume: { + role: ebsRole, + size: cdk.Size.gibibytes(3), + volumeType: EbsDeviceVolumeType.GP3, + fileSystemType: ecs.FileSystemType.XFS, + tagSpecifications: [{ + tags: { + purpose: 'production', + }, + propagateTags: ecs.EbsPropagatedTagSource.SERVICE, + }], + }, + }); + taskDefinition.addVolume(serviceManagedVolume); + + // THEN + expect(() => { + Template.fromStack(stack); + }).toThrow(/Volume 'nginx-vol' should be mounted by at least one container when 'configuredAtLaunch' is true/); + }); + + test('throws when multiple runtime volumes are set using ServiceManagedVolume', () => { + // GIVEN + const stack = new cdk.Stack(); + const ebsRole = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('ecs.amazonaws.com'), + }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const containerDef = taskDefinition.addContainer('db', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + const volume1 = new ServiceManagedVolume(stack, 'EBS Volume1', { + name: 'nginx-vol', + managedEBSVolume: { + role: ebsRole, + size: cdk.Size.gibibytes(3), + volumeType: EbsDeviceVolumeType.GP3, + fileSystemType: ecs.FileSystemType.XFS, + tagSpecifications: [{ + tags: { + purpose: 'production', + }, + propagateTags: ecs.EbsPropagatedTagSource.SERVICE, + }], + }, + }); + volume1.mountIn(containerDef, { + readOnly: false, + containerPath: 'var/lib', + }); + taskDefinition.addVolume(volume1); + const volume2 = new ServiceManagedVolume(stack, 'EBS Volume2', { + name: 'nginx-vol1', + managedEBSVolume: { + role: ebsRole, + size: cdk.Size.gibibytes(3), + volumeType: EbsDeviceVolumeType.GP3, + fileSystemType: ecs.FileSystemType.XFS, + tagSpecifications: [{ + tags: { + purpose: 'production', + }, + propagateTags: ecs.EbsPropagatedTagSource.SERVICE, + }], + }, + }); + volume2.mountIn(containerDef, { + readOnly: false, + containerPath: 'var/lib', + }); + taskDefinition.addVolume(volume2); + + // THEN + expect(() => { + Template.fromStack(stack); + }).toThrow('More than one volume is configured at launch: [nginx-vol,nginx-vol1]'); + }); + test('You can specify a container ulimits using the dedicated property in ContainerDefinitionOptions', () => { // GIVEN const stack = new cdk.Stack(); From 396072025ea1282dd28e14158afe339c393bf0d5 Mon Sep 17 00:00:00 2001 From: Jane Chen <125300057+chenjane-dev@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:01:36 -0500 Subject: [PATCH 2/4] fix(appconfig): prefix names with resource name (#28742) Linter rules caught that CDK standardizes resource name prop as `[resource]Name`. Previously this module only used `name` for the prop. Follow up from #28671. BREAKING CHANGE: `ApplicationProps.name` renamed to `ApplicationProps.applicationName` - **appconfig**: `EnvironmentProps.name` renamed to `EnvironmentProps.environmentName` - **appconfig**: `DeploymentStrategyProps.name` renamed to `DeploymentStrategyProps.deploymentStrategyName` - **appconfig**: `ExtensionProps.name` renamed to `ExtensionProps.extensionName` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-appconfig-alpha/README.md | 2 +- .../@aws-cdk/aws-appconfig-alpha/awslint.json | 4 ---- .../aws-appconfig-alpha/lib/application.ts | 4 ++-- .../lib/deployment-strategy.ts | 6 +++--- .../aws-appconfig-alpha/lib/environment.ts | 6 +++--- .../aws-appconfig-alpha/lib/extension.ts | 10 +++++----- .../test/application.test.ts | 4 ++-- .../test/configuration.test.ts | 14 +++++++------- .../test/deployment-strategy.test.ts | 12 ++++++------ .../test/environment.test.ts | 18 +++++++++--------- .../aws-appconfig-alpha/test/extension.test.ts | 4 ++-- .../test/integ.configuration.ts | 2 +- .../test/integ.environment.ts | 2 +- .../test/integ.extension.ts | 2 +- 14 files changed, 43 insertions(+), 47 deletions(-) diff --git a/packages/@aws-cdk/aws-appconfig-alpha/README.md b/packages/@aws-cdk/aws-appconfig-alpha/README.md index b29fa5707a26d..d96b8dfc6a7f6 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/README.md +++ b/packages/@aws-cdk/aws-appconfig-alpha/README.md @@ -39,7 +39,7 @@ Create an application with a name and description: ```ts new appconfig.Application(this, 'MyApplication', { - name: 'App1', + applicationName: 'App1', description: 'This is my application created through CDK.', }); ``` diff --git a/packages/@aws-cdk/aws-appconfig-alpha/awslint.json b/packages/@aws-cdk/aws-appconfig-alpha/awslint.json index d5b37fa1cc5b6..4355a6e6a41b1 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/awslint.json +++ b/packages/@aws-cdk/aws-appconfig-alpha/awslint.json @@ -27,10 +27,6 @@ "no-unused-type:@aws-cdk/aws-appconfig-alpha.PredefinedDeploymentStrategyId", "ref-via-interface:@aws-cdk/aws-appconfig-alpha.Application.addAgentToEcs.taskDef", - "props-physical-name:@aws-cdk/aws-appconfig-alpha.ApplicationProps", - "props-physical-name:@aws-cdk/aws-appconfig-alpha.DeploymentStrategyProps", - "props-physical-name:@aws-cdk/aws-appconfig-alpha.EnvironmentProps", - "props-physical-name:@aws-cdk/aws-appconfig-alpha.ExtensionProps", "events-in-interface", "events-method-signature", "events-generic" diff --git a/packages/@aws-cdk/aws-appconfig-alpha/lib/application.ts b/packages/@aws-cdk/aws-appconfig-alpha/lib/application.ts index 27b38f506b571..71197edca52b1 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/lib/application.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/lib/application.ts @@ -84,7 +84,7 @@ export interface ApplicationProps { * * @default - A name is generated. */ - readonly name?: string; + readonly applicationName?: string; /** * The description for the application. @@ -336,7 +336,7 @@ export class Application extends ApplicationBase { super(scope, id); this.description = props.description; - this.name = props.name || Names.uniqueResourceName(this, { + this.name = props.applicationName || Names.uniqueResourceName(this, { maxLength: 64, separator: '-', }); diff --git a/packages/@aws-cdk/aws-appconfig-alpha/lib/deployment-strategy.ts b/packages/@aws-cdk/aws-appconfig-alpha/lib/deployment-strategy.ts index a97c6ff02d038..0ebb40b59ac1a 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/lib/deployment-strategy.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/lib/deployment-strategy.ts @@ -18,7 +18,7 @@ export interface DeploymentStrategyProps { * * @default - A name is generated. */ - readonly name?: string; + readonly deploymentStrategyName?: string; /** * A description of the deployment strategy. @@ -130,7 +130,7 @@ export class DeploymentStrategy extends Resource implements IDeploymentStrategy constructor(scope: Construct, id: string, props: DeploymentStrategyProps) { super(scope, id, { - physicalName: props.name, + physicalName: props.deploymentStrategyName, }); this.deploymentDurationInMinutes = props.rolloutStrategy.deploymentDuration.toMinutes(); @@ -138,7 +138,7 @@ export class DeploymentStrategy extends Resource implements IDeploymentStrategy this.description = props.description; this.finalBakeTimeInMinutes = props.rolloutStrategy.finalBakeTime?.toMinutes(); this.growthType = props.rolloutStrategy.growthType; - this.name = props.name || Names.uniqueResourceName(this, { + this.name = props.deploymentStrategyName || Names.uniqueResourceName(this, { maxLength: 64, separator: '-', }); diff --git a/packages/@aws-cdk/aws-appconfig-alpha/lib/environment.ts b/packages/@aws-cdk/aws-appconfig-alpha/lib/environment.ts index e4216a569ab87..767373f92da81 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/lib/environment.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/lib/environment.ts @@ -94,7 +94,7 @@ export interface EnvironmentOptions { * * @default - A name is generated. */ - readonly name?: string; + readonly environmentName?: string; /** * The description of the environment. @@ -236,10 +236,10 @@ export class Environment extends EnvironmentBase { constructor(scope: Construct, id: string, props: EnvironmentProps) { super(scope, id, { - physicalName: props.name, + physicalName: props.environmentName, }); - this.name = props.name || Names.uniqueResourceName(this, { + this.name = props.environmentName || Names.uniqueResourceName(this, { maxLength: 64, separator: '-', }); diff --git a/packages/@aws-cdk/aws-appconfig-alpha/lib/extension.ts b/packages/@aws-cdk/aws-appconfig-alpha/lib/extension.ts index 72f6d1bc9c739..5d9f1164d3ac2 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/lib/extension.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/lib/extension.ts @@ -338,7 +338,7 @@ export interface ExtensionOptions { * * @default - A name is generated. */ - readonly name?: string; + readonly extensionName?: string; /** * A description of the extension @@ -493,11 +493,11 @@ export class Extension extends Resource implements IExtension { constructor(scope: Construct, id: string, props: ExtensionProps) { super(scope, id, { - physicalName: props.name, + physicalName: props.extensionName, }); this.actions = props.actions; - this.name = props.name || Names.uniqueResourceName(this, { + this.name = props.extensionName || Names.uniqueResourceName(this, { maxLength: 64, separator: '-', }); @@ -662,7 +662,7 @@ export class ExtensibleBase implements IExtensible { } private getExtensionForActionPoint(eventDestination: IEventDestination, actionPoint: ActionPoint, options?: ExtensionOptions) { - const name = options?.name || this.getExtensionDefaultName(); + const name = options?.extensionName || this.getExtensionDefaultName(); const versionNumber = options?.latestVersionNumber ? options?.latestVersionNumber + 1 : 1; const extension = new Extension(this.scope, `Extension${this.getExtensionHash(name, versionNumber)}`, { actions: [ @@ -673,7 +673,7 @@ export class ExtensibleBase implements IExtensible { ], }), ], - name, + extensionName: name, ...(options?.description ? { description: options.description } : {}), ...(options?.latestVersionNumber ? { latestVersionNumber: options.latestVersionNumber } : {}), ...(options?.parameters ? { parameters: options.parameters } : {}), diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/application.test.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/application.test.ts index a5c85dad77d95..deda4253bcf4d 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/application.test.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/application.test.ts @@ -23,7 +23,7 @@ describe('appconfig', () => { test('appconfig with name', () => { const stack = new cdk.Stack(); new Application(stack, 'MyAppConfig', { - name: 'TestApp', + applicationName: 'TestApp', }); Template.fromStack(stack).hasResourceProperties('AWS::AppConfig::Application', { @@ -121,7 +121,7 @@ describe('appconfig', () => { }); appconfig.preCreateHostedConfigurationVersion(new LambdaDestination(func), { description: 'This is my description', - name: 'MyExtension', + extensionName: 'MyExtension', latestVersionNumber: 1, parameters: [ Parameter.required('myparam', 'val'), diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/configuration.test.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/configuration.test.ts index 489c95dfd818e..f6564708642f1 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/configuration.test.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/configuration.test.ts @@ -58,7 +58,7 @@ describe('configuration', () => { test('configuration with environments and no deployTo prop', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig', { - name: 'MyApplication', + applicationName: 'MyApplication', }); app.addEnvironment('MyEnv1'); app.addEnvironment('MyEnv2'); @@ -96,7 +96,7 @@ describe('configuration', () => { test('configuration with environments and deployTo prop', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig', { - name: 'MyApplication', + applicationName: 'MyApplication', }); app.addEnvironment('MyEnv1'); const env = app.addEnvironment('MyEnv2'); @@ -152,7 +152,7 @@ describe('configuration', () => { test('configuration using deploy method and no environment associated', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig', { - name: 'MyApplication', + applicationName: 'MyApplication', }); app.addEnvironment('MyEnv1'); const env = app.addEnvironment('MyEnv2'); @@ -191,7 +191,7 @@ describe('configuration', () => { test('configuration using deploy method with environment associated', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig', { - name: 'MyApplication', + applicationName: 'MyApplication', }); const env1 = app.addEnvironment('MyEnv1'); const env2 = app.addEnvironment('MyEnv2'); @@ -248,7 +248,7 @@ describe('configuration', () => { test('configuration with no environment associated and no deploy method used', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig', { - name: 'MyApplication', + applicationName: 'MyApplication', }); new HostedConfiguration(stack, 'MyHostedConfig', { content: ConfigurationContent.fromInlineText('This is my content'), @@ -267,7 +267,7 @@ describe('configuration', () => { test('configuration with two configurations specified', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig', { - name: 'MyApplication', + applicationName: 'MyApplication', }); const env1 = app.addEnvironment('MyEnv1'); const env2 = app.addEnvironment('MyEnv2'); @@ -382,7 +382,7 @@ describe('configuration', () => { test('configuration with two configurations and no deployment strategy specified', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig', { - name: 'MyApplication', + applicationName: 'MyApplication', }); const bucket = new Bucket(stack, 'MyBucket'); new HostedConfiguration(stack, 'MyHostedConfig', { diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/deployment-strategy.test.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/deployment-strategy.test.ts index 14e4e1875af25..d4076f587b697 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/deployment-strategy.test.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/deployment-strategy.test.ts @@ -25,7 +25,7 @@ describe('deployment strategy', () => { test('deployment strategy with name', () => { const stack = new cdk.Stack(); new DeploymentStrategy(stack, 'MyDeploymentStrategy', { - name: 'TestDeploymentStrategy', + deploymentStrategyName: 'TestDeploymentStrategy', rolloutStrategy: RolloutStrategy.linear({ growthFactor: 10, deploymentDuration: cdk.Duration.minutes(10), @@ -44,7 +44,7 @@ describe('deployment strategy', () => { test('deployment strategy duration in seconds', () => { const stack = new cdk.Stack(); new DeploymentStrategy(stack, 'MyDeploymentStrategy', { - name: 'TestDeploymentStrategy', + deploymentStrategyName: 'TestDeploymentStrategy', rolloutStrategy: RolloutStrategy.linear({ growthFactor: 10, deploymentDuration: cdk.Duration.seconds(120), @@ -63,7 +63,7 @@ describe('deployment strategy', () => { test('deployment strategy with description', () => { const stack = new cdk.Stack(); new DeploymentStrategy(stack, 'MyDeploymentStrategy', { - name: 'TestDeploymentStrategy', + deploymentStrategyName: 'TestDeploymentStrategy', rolloutStrategy: RolloutStrategy.linear({ growthFactor: 10, deploymentDuration: cdk.Duration.minutes(10), @@ -84,7 +84,7 @@ describe('deployment strategy', () => { test('deployment strategy with final bake time', () => { const stack = new cdk.Stack(); new DeploymentStrategy(stack, 'MyDeploymentStrategy', { - name: 'TestDeploymentStrategy', + deploymentStrategyName: 'TestDeploymentStrategy', rolloutStrategy: RolloutStrategy.linear({ growthFactor: 10, deploymentDuration: cdk.Duration.minutes(10), @@ -105,7 +105,7 @@ describe('deployment strategy', () => { test('deployment strategy with growth type', () => { const stack = new cdk.Stack(); new DeploymentStrategy(stack, 'MyDeploymentStrategy', { - name: 'TestDeploymentStrategy', + deploymentStrategyName: 'TestDeploymentStrategy', rolloutStrategy: RolloutStrategy.exponential({ growthFactor: 10, deploymentDuration: cdk.Duration.minutes(10), @@ -124,7 +124,7 @@ describe('deployment strategy', () => { test('deployment strategy with replicate to', () => { const stack = new cdk.Stack(); new DeploymentStrategy(stack, 'MyDeploymentStrategy', { - name: 'TestDeploymentStrategy', + deploymentStrategyName: 'TestDeploymentStrategy', rolloutStrategy: RolloutStrategy.linear({ growthFactor: 10, deploymentDuration: cdk.Duration.minutes(10), diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/environment.test.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/environment.test.ts index 9c6dd7c7f3e4b..261d113728dae 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/environment.test.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/environment.test.ts @@ -25,7 +25,7 @@ describe('environment', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig'); new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, }); @@ -41,7 +41,7 @@ describe('environment', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig'); new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, description: 'This is my description', }); @@ -72,7 +72,7 @@ describe('environment', () => { assumedBy: new iam.ServicePrincipal('appconfig.amazonaws.com'), }); const env = new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, monitors: [Monitor.fromCloudWatchAlarm(alarm, alarmRole)], }); @@ -118,7 +118,7 @@ describe('environment', () => { }); const app = new Application(stack, 'MyAppConfig'); const env = new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, monitors: [Monitor.fromCloudWatchAlarm(alarm)], }); @@ -174,7 +174,7 @@ describe('environment', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig'); const env = new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, monitors: [ Monitor.fromCfnMonitorsProperty({ @@ -203,7 +203,7 @@ describe('environment', () => { const stack = new cdk.Stack(); const app = new Application(stack, 'MyAppConfig'); const env = new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, monitors: [ Monitor.fromCfnMonitorsProperty({ @@ -247,7 +247,7 @@ describe('environment', () => { alarmRule: alarm, }); const env = new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, monitors: [ Monitor.fromCloudWatchAlarm(compositeAlarm), @@ -330,7 +330,7 @@ describe('environment', () => { alarmRule: alarm, }); const env = new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, monitors: [ Monitor.fromCloudWatchAlarm(compositeAlarm1), @@ -433,7 +433,7 @@ describe('environment', () => { ), }); new Environment(stack, 'MyEnvironment', { - name: 'TestEnv', + environmentName: 'TestEnv', application: app, monitors: [ Monitor.fromCloudWatchAlarm(alarm1), diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/extension.test.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/extension.test.ts index 4bef613850bbf..9109975c31cad 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/extension.test.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/extension.test.ts @@ -185,7 +185,7 @@ describe('extension', () => { value: 'arn:lambda:us-east-1:123456789012:function:my-function', }); const appconfig = new Application(stack, 'MyApplication', { - name: 'MyApplication', + applicationName: 'MyApplication', }); const ext = new Extension(stack, 'MyExtension', { actions: [ @@ -196,7 +196,7 @@ describe('extension', () => { eventDestination: new LambdaDestination(func), }), ], - name: 'TestExtension', + extensionName: 'TestExtension', description: 'This is my extension', parameters: [ Parameter.required('testVariable', 'hello'), diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/integ.configuration.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/integ.configuration.ts index bf6e396bc8f51..d893c415ec5b0 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/integ.configuration.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/integ.configuration.ts @@ -38,7 +38,7 @@ const stack = new Stack(app, 'aws-appconfig-configuration'); // create application for config profile const appConfigApp = new Application(stack, 'MyAppConfig', { - name: 'AppForConfigTest', + applicationName: 'AppForConfigTest', }); const deploymentStrategy = new DeploymentStrategy(stack, 'MyDeployStrategy', { diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/integ.environment.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/integ.environment.ts index bce1591e70ba8..6681c66597bd4 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/integ.environment.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/integ.environment.ts @@ -10,7 +10,7 @@ const stack = new Stack(app, 'aws-appconfig-environment'); // create resources needed for environment const appForEnv = new Application(stack, 'MyApplicationForEnv', { - name: 'AppForEnvTest', + applicationName: 'AppForEnvTest', }); const alarm = new Alarm(stack, 'MyAlarm', { metric: new Metric({ diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/integ.extension.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/integ.extension.ts index 94b3c38897037..0fdfd86d92e80 100755 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/integ.extension.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/integ.extension.ts @@ -31,7 +31,7 @@ const lambda = new Function(stack, 'MyFunction', { code: Code.fromInline('def handler(event, context):\n\tprint(\'The function has been invoked.\')'), }); const app = new Application(stack, 'MyApplication', { - name: 'AppForExtensionTest', + applicationName: 'AppForExtensionTest', }); const lambdaExtension = new Extension(stack, 'MyLambdaExtension', { actions: [ From 2b59ed1b54b5b83f22020ed5f2c4b77c6a1292f8 Mon Sep 17 00:00:00 2001 From: Jane Chen <125300057+chenjane-dev@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:35:44 -0500 Subject: [PATCH 3/4] fix(appconfig): fromDeploymentStrategyId takes an enum-like class rather than a string (#28743) Previously, we were typing this as a `string` and providing an enum for `PredefinedDeploymentStrategyId`s. This is a CDK anti-pattern because this makes the enum undiscoverable, since users see that it is typed only as a `string`. It also may not work in non-TS languages. Instead, we are moving the type to explicitly be an enum-like class. Follow up from #28671. BREAKING CHANGE: `deploymentStrategyId` prop in `fromDeploymentStrategyId` now takes a `DeploymentStrategyId` rather than a `string`. To import a predefined deployment strategy id, use `DeploymentStrategyId.CANARY_10_PERCENT_20_MINUTES`. Otherwise, use `DeploymentStrategyId.fromString('abc123')`. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-appconfig-alpha/README.md | 16 +++++++++ .../@aws-cdk/aws-appconfig-alpha/awslint.json | 4 +-- .../lib/deployment-strategy.ts | 34 ++++++++++++++----- .../test/deployment-strategy.test.ts | 10 +++--- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-appconfig-alpha/README.md b/packages/@aws-cdk/aws-appconfig-alpha/README.md index d96b8dfc6a7f6..f7a658fdc5ab8 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/README.md +++ b/packages/@aws-cdk/aws-appconfig-alpha/README.md @@ -70,6 +70,22 @@ new appconfig.DeploymentStrategy(this, 'MyDeploymentStrategy', { }); ``` +Importing a deployment strategy by ID: + +```ts +appconfig.DeploymentStrategy.fromDeploymentStrategyId(this, 'MyImportedDeploymentStrategy', appconfig.DeploymentStrategyId.fromString('abc123')); +``` + +Importing an AWS AppConfig predefined deployment strategy by ID: + +```ts +appconfig.DeploymentStrategy.fromDeploymentStrategyId( + this, + 'MyImportedPredefinedDeploymentStrategy', + appconfig.DeploymentStrategyId.CANARY_10_PERCENT_20_MINUTES, +); +``` + ## Configuration A configuration is a higher-level construct that can either be a `HostedConfiguration` (stored internally through AWS diff --git a/packages/@aws-cdk/aws-appconfig-alpha/awslint.json b/packages/@aws-cdk/aws-appconfig-alpha/awslint.json index 4355a6e6a41b1..74fa11fd7c925 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/awslint.json +++ b/packages/@aws-cdk/aws-appconfig-alpha/awslint.json @@ -25,10 +25,10 @@ "docs-public-apis:@aws-cdk/aws-appconfig-alpha.IConfiguration", "docs-public-apis:@aws-cdk/aws-appconfig-alpha.IApplication", - "no-unused-type:@aws-cdk/aws-appconfig-alpha.PredefinedDeploymentStrategyId", "ref-via-interface:@aws-cdk/aws-appconfig-alpha.Application.addAgentToEcs.taskDef", "events-in-interface", "events-method-signature", - "events-generic" + "events-generic", + "from-signature:@aws-cdk/aws-appconfig-alpha.DeploymentStrategy.fromDeploymentStrategyId.params[2]" ] } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appconfig-alpha/lib/deployment-strategy.ts b/packages/@aws-cdk/aws-appconfig-alpha/lib/deployment-strategy.ts index 0ebb40b59ac1a..83b3e9c2a88bd 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/lib/deployment-strategy.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/lib/deployment-strategy.ts @@ -66,16 +66,16 @@ export class DeploymentStrategy extends Resource implements IDeploymentStrategy * @param id The name of the deployment strategy construct * @param deploymentStrategyId The ID of the deployment strategy */ - public static fromDeploymentStrategyId(scope: Construct, id: string, deploymentStrategyId: string): IDeploymentStrategy { + public static fromDeploymentStrategyId(scope: Construct, id: string, deploymentStrategyId: DeploymentStrategyId): IDeploymentStrategy { const stack = Stack.of(scope); const deploymentStrategyArn = stack.formatArn({ service: 'appconfig', resource: 'deploymentstrategy', - resourceName: deploymentStrategyId, + resourceName: deploymentStrategyId.id, }); class Import extends Resource implements IDeploymentStrategy { - public readonly deploymentStrategyId = deploymentStrategyId; + public readonly deploymentStrategyId = deploymentStrategyId.id; public readonly deploymentStrategyArn = deploymentStrategyArn; } @@ -182,36 +182,52 @@ export enum GrowthType { } /** - * Defines the deployment strategy ID's of AWS AppConfig predefined strategies. + * Defines the deployment strategy ID's of AWS AppConfig deployment strategies. * * @see https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-creating-deployment-strategy.html */ -export enum PredefinedDeploymentStrategyId { +export abstract class DeploymentStrategyId { /** * **AWS Recommended**. This strategy processes the deployment exponentially using a 10% growth factor over 20 minutes. * AWS AppConfig recommends using this strategy for production deployments because it aligns with AWS best practices * for configuration deployments. */ - CANARY_10_PERCENT_20_MINUTES = 'AppConfig.Canary10Percent20Minutes', + public static readonly CANARY_10_PERCENT_20_MINUTES = DeploymentStrategyId.fromString('AppConfig.Canary10Percent20Minutes'); /** * **Testing/Demonstration**. This strategy deploys the configuration to half of all targets every 30 seconds for a * one-minute deployment. AWS AppConfig recommends using this strategy only for testing or demonstration purposes because * it has a short duration and bake time. */ - LINEAR_50_PERCENT_EVERY_30_SECONDS = 'AppConfig.Linear50PercentEvery30Seconds', + public static readonly LINEAR_50_PERCENT_EVERY_30_SECONDS = DeploymentStrategyId.fromString('AppConfig.Linear50PercentEvery30Seconds'); /** * **AWS Recommended**. This strategy deploys the configuration to 20% of all targets every six minutes for a 30 minute deployment. * AWS AppConfig recommends using this strategy for production deployments because it aligns with AWS best practices * for configuration deployments. */ - LINEAR_20_PERCENT_EVERY_6_MINUTES = 'AppConfig.Linear20PercentEvery6Minutes', + public static readonly LINEAR_20_PERCENT_EVERY_6_MINUTES = DeploymentStrategyId.fromString('AppConfig.Linear20PercentEvery6Minutes'); /** * **Quick**. This strategy deploys the configuration to all targets immediately. */ - ALL_AT_ONCE = 'AppConfig.AllAtOnce', + public static readonly ALL_AT_ONCE = DeploymentStrategyId.fromString('AppConfig.AllAtOnce'); + + /** + * Builds a deployment strategy ID from a string. + * + * @param deploymentStrategyId The deployment strategy ID. + */ + public static fromString(deploymentStrategyId: string): DeploymentStrategyId { + return { + id: deploymentStrategyId, + }; + } + + /** + * The deployment strategy ID. + */ + public abstract readonly id: string; } /** diff --git a/packages/@aws-cdk/aws-appconfig-alpha/test/deployment-strategy.test.ts b/packages/@aws-cdk/aws-appconfig-alpha/test/deployment-strategy.test.ts index d4076f587b697..2e0d51e1d966f 100644 --- a/packages/@aws-cdk/aws-appconfig-alpha/test/deployment-strategy.test.ts +++ b/packages/@aws-cdk/aws-appconfig-alpha/test/deployment-strategy.test.ts @@ -1,7 +1,7 @@ import * as cdk from 'aws-cdk-lib'; import { App } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; -import { DeploymentStrategy, PredefinedDeploymentStrategyId, RolloutStrategy } from '../lib'; +import { DeploymentStrategy, DeploymentStrategyId, RolloutStrategy } from '../lib'; describe('deployment strategy', () => { test('default deployment strategy', () => { @@ -166,7 +166,7 @@ describe('deployment strategy', () => { account: '123456789012', }, }); - const deploymentStrategy = DeploymentStrategy.fromDeploymentStrategyId(stack, 'MyDeploymentStrategy', 'abc123'); + const deploymentStrategy = DeploymentStrategy.fromDeploymentStrategyId(stack, 'MyDeploymentStrategy', DeploymentStrategyId.fromString('abc123')); expect(deploymentStrategy.deploymentStrategyId).toEqual('abc123'); expect(deploymentStrategy.env.account).toEqual('123456789012'); @@ -181,7 +181,7 @@ describe('deployment strategy', () => { account: '123456789012', }, }); - const deploymentStrategy = DeploymentStrategy.fromDeploymentStrategyId(stack, 'MyDeploymentStrategy', PredefinedDeploymentStrategyId.ALL_AT_ONCE); + const deploymentStrategy = DeploymentStrategy.fromDeploymentStrategyId(stack, 'MyDeploymentStrategy', DeploymentStrategyId.ALL_AT_ONCE); expect(deploymentStrategy.deploymentStrategyId).toEqual('AppConfig.AllAtOnce'); expect(deploymentStrategy.env.account).toEqual('123456789012'); @@ -196,7 +196,7 @@ describe('deployment strategy', () => { account: '123456789012', }, }); - const deploymentStrategy = DeploymentStrategy.fromDeploymentStrategyId(stack, 'MyDeploymentStrategy', PredefinedDeploymentStrategyId.CANARY_10_PERCENT_20_MINUTES); + const deploymentStrategy = DeploymentStrategy.fromDeploymentStrategyId(stack, 'MyDeploymentStrategy', DeploymentStrategyId.CANARY_10_PERCENT_20_MINUTES); expect(deploymentStrategy.deploymentStrategyId).toEqual('AppConfig.Canary10Percent20Minutes'); expect(deploymentStrategy.env.account).toEqual('123456789012'); @@ -211,7 +211,7 @@ describe('deployment strategy', () => { account: '123456789012', }, }); - const deploymentStrategy = DeploymentStrategy.fromDeploymentStrategyId(stack, 'MyDeploymentStrategy', PredefinedDeploymentStrategyId.LINEAR_50_PERCENT_EVERY_30_SECONDS); + const deploymentStrategy = DeploymentStrategy.fromDeploymentStrategyId(stack, 'MyDeploymentStrategy', DeploymentStrategyId.LINEAR_50_PERCENT_EVERY_30_SECONDS); expect(deploymentStrategy.deploymentStrategyId).toEqual('AppConfig.Linear50PercentEvery30Seconds'); expect(deploymentStrategy.env.account).toEqual('123456789012'); From a30a2058de1af3201e3316ce73ee8ad5f2907ec6 Mon Sep 17 00:00:00 2001 From: "k.goto" <24818752+go-to-k@users.noreply.github.com> Date: Thu, 18 Jan 2024 08:05:59 +0900 Subject: [PATCH 4/4] fix(ec2): max iops value for io2 EBS volume is wrong (#28695) The max value of `iops` for `io2` EBS volume is wrong. And I fixed the reference URL. - 64000 -> 256000 https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volume.html#cfn-ec2-volume-iops ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk-lib/aws-ec2/lib/volume.ts | 12 ++++++------ packages/aws-cdk-lib/aws-ec2/test/volume.test.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ec2/lib/volume.ts b/packages/aws-cdk-lib/aws-ec2/lib/volume.ts index 2c29b6e6dbbd9..5b349c2ecd02e 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/volume.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/volume.ts @@ -344,7 +344,7 @@ export interface VolumeProps { /** * The size of the volume, in GiBs. You must specify either a snapshot ID or a volume size. - * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html + * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volume.html * for details on the allowable size for each type of volume. * * @default If you're creating the volume from a snapshot and don't specify a volume size, the default is the snapshot size. @@ -427,7 +427,7 @@ export interface VolumeProps { /** * The number of I/O operations per second (IOPS) to provision for the volume. The maximum ratio is 50 IOPS/GiB for PROVISIONED_IOPS_SSD, * and 500 IOPS/GiB for both PROVISIONED_IOPS_SSD_IO2 and GENERAL_PURPOSE_SSD_GP3. - * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html + * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volume.html * for more information. * * This parameter is valid only for PROVISIONED_IOPS_SSD, PROVISIONED_IOPS_SSD_IO2 and GENERAL_PURPOSE_SSD_GP3 volumes. @@ -446,7 +446,7 @@ export interface VolumeProps { /** * The throughput that the volume supports, in MiB/s * Takes a minimum of 125 and maximum of 1000. - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html#cfn-ec2-ebs-volume-throughput + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volume.html#cfn-ec2-volume-throughput * @default - 125 MiB/s. Only valid on gp3 volumes. */ readonly throughput?: number; @@ -691,11 +691,11 @@ export class Volume extends VolumeBase { ); } // Enforce minimum & maximum IOPS: - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volume.html const iopsRanges: { [key: string]: { Min: number, Max: number } } = {}; iopsRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = { Min: 3000, Max: 16000 }; iopsRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = { Min: 100, Max: 64000 }; - iopsRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = { Min: 100, Max: 64000 }; + iopsRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = { Min: 100, Max: 256000 }; const { Min, Max } = iopsRanges[volumeType]; if (props.iops < Min || props.iops > Max) { throw new Error(`\`${volumeType}\` volumes iops must be between ${Min} and ${Max}.`); @@ -739,7 +739,7 @@ export class Volume extends VolumeBase { if (props.size) { const size = props.size.toGibibytes({ rounding: SizeRoundingBehavior.FAIL }); // Enforce minimum & maximum volume size: - // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-volume.html const sizeRanges: { [key: string]: { Min: number, Max: number } } = {}; sizeRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD] = { Min: 1, Max: 16384 }; sizeRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = { Min: 1, Max: 16384 }; diff --git a/packages/aws-cdk-lib/aws-ec2/test/volume.test.ts b/packages/aws-cdk-lib/aws-ec2/test/volume.test.ts index ffe1ef38a9c17..cce55f2497bc8 100644 --- a/packages/aws-cdk-lib/aws-ec2/test/volume.test.ts +++ b/packages/aws-cdk-lib/aws-ec2/test/volume.test.ts @@ -1279,7 +1279,7 @@ describe('volume', () => { for (const testData of [ [EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, 3000, 16000], [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, 100, 64000], - [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, 100, 64000], + [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, 100, 256000], ]) { const volumeType = testData[0] as EbsDeviceVolumeType; const min = testData[1] as number;