diff --git a/.github/semantic.yaml b/.github/semantic.yml similarity index 100% rename from .github/semantic.yaml rename to .github/semantic.yml diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 00e2ee1d2ecaf..8c077aa836951 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -22,7 +22,7 @@ jobs: {"keywords":["(@aws-cdk/app-delivery)","(app-delivery)","(app delivery)"],"labels":["@aws-cdk/app-delivery"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/assert)","(assert)"],"labels":["@aws-cdk/assert"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/assets)","(assets)"],"labels":["@aws-cdk/assets"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-accessanalyzer)","(aws-accessanalyzer)","(accessanalyzer)","(access analyzer)"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-accessanalyzer)","(aws-accessanalyzer)","(accessanalyzer)","(access analyzer)"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-acmpca)","(aws-acmpca)","(acmpca)"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-amazonmq)","(aws-amazonmq)","(amazonmq)","(amazon mq)","(amazon-mq)"],"labels":["@aws-cdk/aws-amazonmq"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-amplify)","(aws-amplify)","(amplify)"],"labels":["@aws-cdk/aws-amplify"],"assignees":["MrArnoldPalmer"]}, @@ -53,7 +53,7 @@ jobs: {"keywords":["(@aws-cdk/aws-cloudformation)","(aws-cloudformation)","(cloudformation)","(cloud formation)"],"labels":["@aws-cdk/aws-cloudformation"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-cloudfront)","(aws-cloudfront)","(cloudfront)","(cloud front)"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudfront-origins)","(aws-cloudfront-origins)","(cloudfront-origins)","(cloudfront origins)"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-cloudtrail)","(aws-cloudtrail)","(cloudtrail)","(cloud trail)"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-cloudtrail)","(aws-cloudtrail)","(cloudtrail)","(cloud trail)"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["NetaNir"]}, {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-codeartifact)","(aws-codeartifact)","(codeartifact)","(code artifact)","(code-artifact)"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["njlynch"]}, @@ -61,7 +61,7 @@ jobs: {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-codedeploy)","(aws-codedeploy)","(codedeploy)","(code deploy)","(code-deploy)"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-codeguruprofiler)","(aws-codeguruprofiler)","(codeguruprofiler)","(codeguru profiler)","(codeguru-profiler)"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codegurureviewer)","(aws-codegurureviewer)","(codegurureviewer)","(codeguru reviewer)","(codeguru-reviewer)"],"labels":["@aws-cdk/aws-codegurureviewer"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-codegurureviewer)","(aws-codegurureviewer)","(codegurureviewer)","(codeguru reviewer)","(codeguru-reviewer)"],"labels":["@aws-cdk/aws-codegurureviewer"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-codepipeline)","(aws-codepipeline)","(codepipeline)","(code pipeline)","(code-pipeline)"],"labels":["@aws-cdk/aws-codepipeline"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-codepipeline-actions)","(aws-codepipeline-actions)","(codepipeline-actions)","(codepipeline actions)"],"labels":["@aws-cdk/aws-codepipeline-actions"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-codestar)","(aws-codestar)","(codestar)"],"labels":["@aws-cdk/aws-codestar"],"assignees":["skinny85"]}, @@ -71,7 +71,7 @@ jobs: {"keywords":["(@aws-cdk/aws-config)","(aws-config)","(config)"],"labels":["@aws-cdk/aws-config"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-datapipeline)","(aws-datapipeline)","(datapipeline)","(data pipeline)","(data-pipeline)"],"labels":["@aws-cdk/aws-datapipeline"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-dax)","(aws-dax)","(dax)"],"labels":["@aws-cdk/aws-dax"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-directoryservice)","(aws-directoryservice)","(directoryservice)","(directory service)","(directory-service)"],"labels":["@aws-cdk/aws-directoryservice"],"assignees":["NetaNir"]}, {"keywords":["(@aws-cdk/aws-dlm)","(aws-dlm)","(dlm)"],"labels":["@aws-cdk/aws-dlm"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-dms)","(aws-dms)","(dms)"],"labels":["@aws-cdk/aws-dms"],"assignees":["njlynch"]}, @@ -81,7 +81,7 @@ jobs: {"keywords":["(@aws-cdk/aws-ec2)","(aws-ec2)","(ec2)","(vpc)"],"labels":["@aws-cdk/aws-ec2"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-ecr)","(aws-ecr)","(ecr)"],"labels":["@aws-cdk/aws-ecr"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-ecr-assets)","(aws-ecr-assets)","(ecr-assets)","(ecr assets)","(ecrassets)"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-efs)","(aws-efs)","(efs)"],"labels":["@aws-cdk/aws-efs"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-efs)","(aws-efs)","(efs)"],"labels":["@aws-cdk/aws-efs"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-eks)","(aws-eks)","(eks)"],"labels":["@aws-cdk/aws-eks"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-elasticache)","(aws-elasticache)","(elasticache)","(elastic cache)","(elastic-cache)"],"labels":["@aws-cdk/aws-elasticache"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-elasticbeanstalk)","(aws-elasticbeanstalk)","(elasticbeanstalk)","(elastic beanstalk)","(elastic-beanstalk)"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["skinny85"]}, @@ -90,27 +90,27 @@ jobs: {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2-targets)","(aws-elasticloadbalancingv2-targets)","(elasticloadbalancingv2-targets)","(elasticloadbalancingv2 targets)","(elbv2 targets)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-targets"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-elasticsearch)","(aws-elasticsearch)","(elasticsearch)","(elastic search)","(elastic-search)"],"labels":["@aws-cdk/aws-elasticsearch"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-emr)","(aws-emr)","(emr)"],"labels":["@aws-cdk/aws-emr"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-events)","(aws-events)","(events)","(eventbridge)","event-bridge)"],"labels":["@aws-cdk/aws-events"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-events-targets)","(aws-events-targets)","(events-targets)","(events targets)"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-eventschemas)","(aws-eventschemas)","(eventschemas)","(event schemas)"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-events)","(aws-events)","(events)","(eventbridge)","event-bridge)"],"labels":["@aws-cdk/aws-events"],"assignees":["rix0rrr"]}, + {"keywords":["(@aws-cdk/aws-events-targets)","(aws-events-targets)","(events-targets)","(events targets)"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["rix0rrr"]}, + {"keywords":["(@aws-cdk/aws-eventschemas)","(aws-eventschemas)","(eventschemas)","(event schemas)"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-fms)","(aws-fms)","(fms)"],"labels":["@aws-cdk/aws-fms"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-fsx)","(aws-fsx)","(fsx)"],"labels":["@aws-cdk/aws-fsx"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-gamelift)","(aws-gamelift)","(gamelift)","(game lift)"],"labels":["@aws-cdk/aws-gamelift"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-globalaccelerator)","(aws-globalaccelerator)","(globalaccelerator)","(global accelerator)","(global-accelerator)"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-glue)","(aws-glue)","(glue)"],"labels":["@aws-cdk/aws-glue"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)","(green-grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)","(green-grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-guardduty)","(aws-guardduty)","(guardduty)","(guard duty)","(guard-duty)"],"labels":["@aws-cdk/aws-guardduty"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-iam)","(aws-iam)","(iam)"],"labels":["@aws-cdk/aws-iam"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-imagebuilder)","(aws-imagebuilder)","(imagebuilder)","(image builder)","(image-builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)","(iot-analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)","(iot-events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iotsitewise)","(aws-iotsitewise)","(iotsitewise)","(iot sitewise)","(iot-sitewise)","(iot-site-wise)","(iot site wise)"],"labels":["@aws-cdk/aws-iotsitewise"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)","(iot-things-graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-ivs)","(aws-ivs)","(Interactive Video Service)","(ivs)"],"labels":["@aws-cdk/aws-ivs"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-kendra)","(aws-kendra)","(kendra)"],"labels":["@aws-cdk/aws-kendra"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-imagebuilder)","(aws-imagebuilder)","(imagebuilder)","(image builder)","(image-builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)","(iot-analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)","(iot-events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotsitewise)","(aws-iotsitewise)","(iotsitewise)","(iot sitewise)","(iot-sitewise)","(iot-site-wise)","(iot site wise)"],"labels":["@aws-cdk/aws-iotsitewise"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)","(iot-things-graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-ivs)","(aws-ivs)","(Interactive Video Service)","(ivs)"],"labels":["@aws-cdk/aws-ivs"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-kendra)","(aws-kendra)","(kendra)"],"labels":["@aws-cdk/aws-kendra"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-kinesis)","(aws-kinesis)","(kinesis)"],"labels":["@aws-cdk/aws-kinesis"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-kinesisanalytics)","(aws-kinesisanalytics)","(kinesisanalytics)","(kinesis analytics)","(kinesis-analytics)"],"labels":["@aws-cdk/aws-kinesisanalytics"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-kinesisfirehose)","(aws-kinesisfirehose)","(kinesisfirehose)","(kinesis firehose)","(kinesis-firehose)"],"labels":["@aws-cdk/aws-kinesisfirehose"],"assignees":["iliapolo"]}, @@ -120,32 +120,32 @@ jobs: {"keywords":["(@aws-cdk/aws-lambda-event-sources)","(aws-lambda-event-sources)","(lambda-event-sources)","(lambda event sources)"],"labels":["@aws-cdk/aws-lambda-event-sources"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-lambda-nodejs)","(aws-lambda-nodejs)","(lambda-nodejs)","(lambda nodejs)"],"labels":["@aws-cdk/aws-lambda-nodejs"],"assignees":["eladb"]}, {"keywords":["(@aws-cdk/aws-lambda-python)","(aws-lambda-python)","(lambda-python)","(lambda python)"],"labels":["@aws-cdk/aws-lambda-python"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-logs)","(aws-logs)","(logs)"],"labels":["@aws-cdk/aws-logs"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-logs-destinations)","(aws-logs-destinations)","(logs-destinations)","(logs destinations)"],"labels":["@aws-cdk/aws-logs-destinations"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-logs)","(aws-logs)","(logs)"],"labels":["@aws-cdk/aws-logs"],"assignees":["rix0rrr"]}, + {"keywords":["(@aws-cdk/aws-logs-destinations)","(aws-logs-destinations)","(logs-destinations)","(logs destinations)"],"labels":["@aws-cdk/aws-logs-destinations"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-macie)","(aws-macie)","(macie)"],"labels":["@aws-cdk/aws-macie"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)","(managed-blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)","(media-convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)","(media-live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)","(media-store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-mediapackage)","(aws-mediapackage)","(mediapackage)","(media package)","(media-package)"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)","(managed-blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)","(media-convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)","(media-live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)","(media-store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediapackage)","(aws-mediapackage)","(mediapackage)","(media package)","(media-package)"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-msk)","(aws-msk)","(msk)"],"labels":["@aws-cdk/aws-msk"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-neptune)","(aws-neptune)","(neptune)"],"labels":["@aws-cdk/aws-neptune"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)","(network-manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)","(network-manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-opsworks)","(aws-opsworks)","(opsworks)","(ops works)","(ops-works)"],"labels":["@aws-cdk/aws-opsworks"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-opsworkscm)","(aws-opsworkscm)","(opsworkscm)","(opsworks cm)","(opsworks-cm)"],"labels":["@aws-cdk/aws-opsworkscm"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-personalize)","(aws-personalize)","(personalize)"],"labels":["@aws-cdk/aws-personalize"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-pinpoint)","(aws-pinpoint)","(pinpoint)"],"labels":["@aws-cdk/aws-pinpoint"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-pinpointemail)","(aws-pinpointemail)","(pinpointemail)","(pinpoint email)","(pinpoint-email)"],"labels":["@aws-cdk/aws-pinpointemail"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-ram)","(aws-ram)","(ram)"],"labels":["@aws-cdk/aws-ram"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-rds)","(aws-rds)","(rds)"],"labels":["@aws-cdk/aws-rds"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-redshift)","(aws-redshift)","(redshift)","(red shift)","(red-shift)"],"labels":["@aws-cdk/aws-redshift"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-resourcegroups)","(aws-resourcegroups)","(resourcegroups)","(resource groups)","(resource-groups)"],"labels":["@aws-cdk/aws-resourcegroups"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-robomaker)","(aws-robomaker)","(robomaker)","(robo maker)","(robo-maker)"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-route53)","(aws-route53)","(route53)","(route 53)","(route-53)"],"labels":["@aws-cdk/aws-route53"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-route53-patterns)","(aws-route53-patterns)","(route53-patterns)","(route53 patterns)","(route-53-patterns)"],"labels":["@aws-cdk/aws-route53-patterns"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-route53-targets)","(aws-route53-targets)","(route53-targets)","(route53 targets)","(route-53-targets)"],"labels":["@aws-cdk/aws-route53-targets"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-route53resolver)","(aws-route53resolver)","(route53resolver)","(route53 resolver)","(route-53-resolver)"],"labels":["@aws-cdk/aws-route53resolver"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-route53)","(aws-route53)","(route53)","(route 53)","(route-53)"],"labels":["@aws-cdk/aws-route53"],"assignees":["njlynch"]}, + {"keywords":["(@aws-cdk/aws-route53-patterns)","(aws-route53-patterns)","(route53-patterns)","(route53 patterns)","(route-53-patterns)"],"labels":["@aws-cdk/aws-route53-patterns"],"assignees":["njlynch"]}, + {"keywords":["(@aws-cdk/aws-route53-targets)","(aws-route53-targets)","(route53-targets)","(route53 targets)","(route-53-targets)"],"labels":["@aws-cdk/aws-route53-targets"],"assignees":["njlynch"]}, + {"keywords":["(@aws-cdk/aws-route53resolver)","(aws-route53resolver)","(route53resolver)","(route53 resolver)","(route-53-resolver)"],"labels":["@aws-cdk/aws-route53resolver"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-s3)","(aws-s3)","(s3)"],"labels":["@aws-cdk/aws-s3"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-s3-assets)","(aws-s3-assets)","(s3-assets)","(s3 assets)"],"labels":["@aws-cdk/aws-s3-assets"],"assignees":["iliapolo"]}, {"keywords":["(@aws-cdk/aws-s3-deployment)","(aws-s3-deployment)","(s3-deployment)","(s3 deployment)"],"labels":["@aws-cdk/aws-s3-deployment"],"assignees":["iliapolo"]}, @@ -154,7 +154,7 @@ jobs: {"keywords":["(@aws-cdk/aws-sam)","(aws-sam)","(sam)"],"labels":["@aws-cdk/aws-sam"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-sdb)","(aws-sdb)","(sdb)"],"labels":["@aws-cdk/aws-sdb"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-secretsmanager)","(aws-secretsmanager)","(secretsmanager)","(secrets manager)","(secrets-manager)"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)","(security-hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)","(security-hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-servicecatalog)","(aws-servicecatalog)","(servicecatalog)","(service catalog)","(service-catalog)"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-servicediscovery)","(aws-servicediscovery)","(servicediscovery)","(service discovery)","(service-discovery)"],"labels":["@aws-cdk/aws-servicediscovery"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-ses)","(aws-ses)","(ses)"],"labels":["@aws-cdk/aws-ses"],"assignees":["iliapolo"]}, @@ -169,13 +169,13 @@ jobs: {"keywords":["(@aws-cdk/aws-synthetics)","(aws-synthetics)","(synthetics)"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["NetaNir"]}, {"keywords":["(@aws-cdk/aws-timestream)","(aws-timestream)","(timestream)"],"labels":["@aws-cdk/aws-timestream"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-transfer)","(aws-transfer)","(transfer)"],"labels":["@aws-cdk/aws-transfer"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-waf)","(aws-waf)","(waf)"],"labels":["@aws-cdk/aws-waf"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-wafregional)","(aws-wafregional)","(wafregional)","(waf regional)","(waf-regional)"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-wafv2)","(aws-wafv2)","(wafv2)","(waf v2)","(waf-v2)"],"labels":["@aws-cdk/aws-wafv2"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/aws-waf)","(aws-waf)","(waf)"],"labels":["@aws-cdk/aws-waf"],"assignees":["njlynch"]}, + {"keywords":["(@aws-cdk/aws-wafregional)","(aws-wafregional)","(wafregional)","(waf regional)","(waf-regional)"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["njlynch"]}, + {"keywords":["(@aws-cdk/aws-wafv2)","(aws-wafv2)","(wafv2)","(waf v2)","(waf-v2)"],"labels":["@aws-cdk/aws-wafv2"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-workspaces)","(aws-workspaces)","(workspaces)"],"labels":["@aws-cdk/aws-workspaces"],"assignees":["NetaNir"]}, {"keywords":["(@aws-cdk/cfnspec)","(cfnspec)","(cfn spec)","(cfn-spec)"],"labels":["@aws-cdk/cfnspec"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/cloud-assembly-schema)","(cloud-assembly-schema)","(cloud assembly schema)"],"labels":["@aws-cdk/cloud-assembly-schema"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["shivlaks"]}, + {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["NetaNir"]}, {"keywords":["(@aws-cdk/cloudformation-include)","(cloudformation-include)","(cloudformation include)","(cfn include)","(cfn-include)"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/core)","(core)"],"labels":["@aws-cdk/core"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/custom-resources)","(custom-resources)","(custom resources)"],"labels":["@aws-cdk/custom-resources"],"assignees":["rix0rrr"]}, diff --git a/CHANGELOG.md b/CHANGELOG.md index 02bad01ae7f3f..19b456a9e607b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,70 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.80.0](https://github.com/aws/aws-cdk/compare/v1.79.0...v1.80.0) (2020-12-22) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **eks:** `LegacyCluster` was removed since it existed only for a transition period to allow gradual migration to the current cluster class. +* **eks:** `kubectlEnabled` property was removed, all clusters now support `kubectl`. +* **core:** Creation stack traces for `Lazy` values are no longer +captured by default in order to speed up tests. Run with +`CDK_DEBUG=true` (or `cdk --debug`) to capture stack traces. + +### Features + +* **ec2:** Add VPC endpoints for Athena and Glue ([#12073](https://github.com/aws/aws-cdk/issues/12073)) ([73ef6b1](https://github.com/aws/aws-cdk/commit/73ef6b180c8a7c3d8e984b308149eeb9eb78b40b)), closes [#12072](https://github.com/aws/aws-cdk/issues/12072) +* **ecs-patterns:** add ruleName optional parameter for ScheduledTask constructs ([#12190](https://github.com/aws/aws-cdk/issues/12190)) ([b1318bd](https://github.com/aws/aws-cdk/commit/b1318bda54d1c0955a371eccce76b748d312b570)) +* **eks:** connect all custom resources to the cluster VPC ([#10200](https://github.com/aws/aws-cdk/issues/10200)) ([eaa8222](https://github.com/aws/aws-cdk/commit/eaa82222349fcce1ef4b80e873a35002d6f036e5)) +* **lambda-nodejs:** Expose optional props for advanced usage of esbuild ([#12123](https://github.com/aws/aws-cdk/issues/12123)) ([ecc98ac](https://github.com/aws/aws-cdk/commit/ecc98ac75acb1adbb4f5e66f853dc3226e490c98)) + + +### Bug Fixes + +* **core:** capturing stack traces still takes a long time ([#12180](https://github.com/aws/aws-cdk/issues/12180)) ([71cd38c](https://github.com/aws/aws-cdk/commit/71cd38c8fac276e34b79ad416305b214a57af25a)), closes [#11170](https://github.com/aws/aws-cdk/issues/11170) +* **dynamodb:** allow global replicas with Provisioned billing mode ([#12159](https://github.com/aws/aws-cdk/issues/12159)) ([ab5a383](https://github.com/aws/aws-cdk/commit/ab5a38379999bb57f28bbf22ec09d315df6b358a)), closes [#11346](https://github.com/aws/aws-cdk/issues/11346) +* **lambda-nodejs:** local bundling fails with relative depsLockFilePath ([#12125](https://github.com/aws/aws-cdk/issues/12125)) ([d5afb55](https://github.com/aws/aws-cdk/commit/d5afb555b983c8c034f63dd58d1fa24b82b6e9fe)), closes [#12115](https://github.com/aws/aws-cdk/issues/12115) +* **eks:** Remove legacy and deprecated code ([#12189](https://github.com/aws/aws-cdk/issues/12189)) ([6a20e61](https://github.com/aws/aws-cdk/commit/6a20e61dd2ed8366cbff1451c943a02b79380de2)) + +## [1.79.0](https://github.com/aws/aws-cdk/compare/v1.78.0...v1.79.0) (2020-12-17) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **apigatewayv2:** `HttpApi.fromApiId()` has been replaced with +`HttpApi.fromHttpApiAttributes()`. +* **elasticsearch:** ES Domain LogGroup LogicalId will change, which will trigger new log group resources to be created + +### Features + +* **appmesh:** add timeout support to Routes ([#11973](https://github.com/aws/aws-cdk/issues/11973)) ([78c185d](https://github.com/aws/aws-cdk/commit/78c185d15e64e81ee86ee71cd6430cd80fdbb8fe)) +* **core:** expose custom resource provider's role ([#11923](https://github.com/aws/aws-cdk/issues/11923)) ([06f26d3](https://github.com/aws/aws-cdk/commit/06f26d390707b0e2a4e05e36405a4751c907a234)), closes [/github.com/aws/aws-cdk/pull/9751#issuecomment-723554595](https://github.com/aws//github.com/aws/aws-cdk/pull/9751/issues/issuecomment-723554595) +* **ec2:** add r5b instance type to instance class ([#12027](https://github.com/aws/aws-cdk/issues/12027)) ([d276b02](https://github.com/aws/aws-cdk/commit/d276b020e61ee4455c7ed9f093436d1aab319e76)), closes [#12025](https://github.com/aws/aws-cdk/issues/12025) +* **ecs-patterns:** Add DeploymentController option to Fargate services ([#10452](https://github.com/aws/aws-cdk/issues/10452)) ([2cd233a](https://github.com/aws/aws-cdk/commit/2cd233a94fc2f3cb06211157738e59e8c7ee85e5)), closes [aws/containers-roadmap#130](https://github.com/aws/containers-roadmap/issues/130) [#10971](https://github.com/aws/aws-cdk/issues/10971) +* **eks:** attach cluster security group to self-managed nodes ([#12042](https://github.com/aws/aws-cdk/issues/12042)) ([1078bea](https://github.com/aws/aws-cdk/commit/1078bea4c90afaac76a5e81328a9d6ec44a79e9a)) +* **elasticsearch:** support audit logs ([#12106](https://github.com/aws/aws-cdk/issues/12106)) ([d10ea63](https://github.com/aws/aws-cdk/commit/d10ea631f8699385cadf61d6e0a067b68da37df6)), closes [#12105](https://github.com/aws/aws-cdk/issues/12105) +* **ivs:** add IVS L2 Constructs ([#11454](https://github.com/aws/aws-cdk/issues/11454)) ([f813bff](https://github.com/aws/aws-cdk/commit/f813bff2da4792cfa7bfce6f572a7d2bb5c4759d)) +* **lambda:** encryption key for environment variables ([#11893](https://github.com/aws/aws-cdk/issues/11893)) ([ccbaf83](https://github.com/aws/aws-cdk/commit/ccbaf8399c3a9f3ff6e60758e0b713d82f37420b)), closes [#10837](https://github.com/aws/aws-cdk/issues/10837) +* **lambda-nodejs:** expose more esbuild options ([#12063](https://github.com/aws/aws-cdk/issues/12063)) ([bab21b3](https://github.com/aws/aws-cdk/commit/bab21b377593b7475b047d05a54914344352c054)), closes [#12046](https://github.com/aws/aws-cdk/issues/12046) +* **route53:** Vpc endpoint service private dns ([#10780](https://github.com/aws/aws-cdk/issues/10780)) ([8f6f9a8](https://github.com/aws/aws-cdk/commit/8f6f9a8678496e131a43ca4c76e561d50a0a0de8)) +* **s3-deployment:** support vpc in BucketDeploymentProps ([#12035](https://github.com/aws/aws-cdk/issues/12035)) ([6caf72f](https://github.com/aws/aws-cdk/commit/6caf72f67d6d3373186e57f32671369c2cc8b56e)), closes [#11734](https://github.com/aws/aws-cdk/issues/11734) +* **stepfunctions-tasks:** add support for ModelClientConfig to SageMakerCreateTransformJob ([#11892](https://github.com/aws/aws-cdk/issues/11892)) ([bf05092](https://github.com/aws/aws-cdk/commit/bf050928c033328b259746c0a7f33038aadc4c17)) + + +### Bug Fixes + +* **ec2:** 'encoded list token' error using Vpc imported from deploy-time lists ([#12040](https://github.com/aws/aws-cdk/issues/12040)) ([0690da9](https://github.com/aws/aws-cdk/commit/0690da925144c821a73bfab4ae8d678a8c074357)) +* **ec2:** fromInterfaceVpcEndpointAttributes: Security Groups should not be required ([#11857](https://github.com/aws/aws-cdk/issues/11857)) ([86ae5d6](https://github.com/aws/aws-cdk/commit/86ae5d6ec5291f7a8da37bbf021c31f88e66d283)), closes [#11050](https://github.com/aws/aws-cdk/issues/11050) +* **eks:** failure to deploy cluster since aws-auth configmap exists ([#12068](https://github.com/aws/aws-cdk/issues/12068)) ([dc8a98a](https://github.com/aws/aws-cdk/commit/dc8a98a5436a7a2347fa9676d84f73a8cf00cd49)), closes [#12053](https://github.com/aws/aws-cdk/issues/12053) +* **eks:** k8s resources accidentally deleted due to logical ID change ([#12053](https://github.com/aws/aws-cdk/issues/12053)) ([019852e](https://github.com/aws/aws-cdk/commit/019852e4834327d848c9fe8dc271f1d4d5117fb8)), closes [#10397](https://github.com/aws/aws-cdk/issues/10397) [#10397](https://github.com/aws/aws-cdk/issues/10397) +* **elasticsearch:** Defining 2 domains with logging enabled in the same stack fails on construct id conflict ([#12055](https://github.com/aws/aws-cdk/issues/12055)) ([ec3ce19](https://github.com/aws/aws-cdk/commit/ec3ce19bc8203703cb1abcecdb2afc674c2013f6)), closes [#12017](https://github.com/aws/aws-cdk/issues/12017) +* **elasticsearch:** log policies are overwritten when creating 2 domains which also results in a failure while destroying the stack ([#12056](https://github.com/aws/aws-cdk/issues/12056)) ([889d089](https://github.com/aws/aws-cdk/commit/889d0892bae10243e03900f0ae6db078fc7eb320)), closes [#12016](https://github.com/aws/aws-cdk/issues/12016) +* **stepfunctions-tasks:** policies created for EMR tasks have ARNs that are not partition-aware ([#11553](https://github.com/aws/aws-cdk/issues/11553)) ([1cf6713](https://github.com/aws/aws-cdk/commit/1cf6713b778c789af7a420ad890910a9516473f0)), closes [#11503](https://github.com/aws/aws-cdk/issues/11503) + + +* **apigatewayv2:** apiEndpoint is elevated to the IHttpApi interface ([#11988](https://github.com/aws/aws-cdk/issues/11988)) ([bc5b9b6](https://github.com/aws/aws-cdk/commit/bc5b9b659444bfbef9cfc3c8666fce7e6f45465a)) + ## [1.78.0](https://github.com/aws/aws-cdk/compare/v1.77.0...v1.78.0) (2020-12-11) diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index f3a042f06512f..cc2e646443f0e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -18,6 +18,12 @@ export interface IHttpApi extends IResource { */ readonly httpApiId: string; + /** + * The default endpoint for an API + * @attribute + */ + readonly apiEndpoint: string; + /** * The default stage */ @@ -184,6 +190,7 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions { abstract class HttpApiBase extends Resource implements IHttpApi { // note that this is not exported public abstract readonly httpApiId: string; + public abstract readonly apiEndpoint: string; private vpcLinks: Record = {}; public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { @@ -233,6 +240,21 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t } } +/** + * Attributes for importing an HttpApi into the CDK + */ +export interface HttpApiAttributes { + /** + * The identifier of the HttpApi + */ + readonly httpApiId: string; + /** + * The endpoint URL of the HttpApi + * @default - throws an error if apiEndpoint is accessed. + */ + readonly apiEndpoint?: string; +} + /** * Create a new API Gateway HTTP API endpoint. * @resource AWS::ApiGatewayV2::Api @@ -241,9 +263,17 @@ export class HttpApi extends HttpApiBase { /** * Import an existing HTTP API into this CDK app. */ - public static fromApiId(scope: Construct, id: string, httpApiId: string): IHttpApi { + public static fromHttpApiAttributes(scope: Construct, id: string, attrs: HttpApiAttributes): IHttpApi { class Import extends HttpApiBase { - public readonly httpApiId = httpApiId; + public readonly httpApiId = attrs.httpApiId; + private readonly _apiEndpoint = attrs.apiEndpoint; + + public get apiEndpoint(): string { + if (!this._apiEndpoint) { + throw new Error('apiEndpoint is not configured on the imported HttpApi.'); + } + return this._apiEndpoint; + } } return new Import(scope, id); } @@ -252,13 +282,7 @@ export class HttpApi extends HttpApiBase { * A human friendly name for this HTTP API. Note that this is different from `httpApiId`. */ public readonly httpApiName?: string; - public readonly httpApiId: string; - - /** - * The default endpoint for an API - * @attribute - */ public readonly apiEndpoint: string; /** diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index fb91ec588be56..b061f613f4ca3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -29,11 +29,10 @@ describe('HttpApi', () => { test('import', () => { const stack = new Stack(); - const api = new HttpApi(stack, 'api', { apiName: 'customName' }); - const imported = HttpApi.fromApiId(stack, 'imported', api.httpApiId ); - - expect(imported.httpApiId).toEqual(api.httpApiId); + const imported = HttpApi.fromHttpApiAttributes(stack, 'imported', { httpApiId: 'http-1234', apiEndpoint: 'api-endpoint' }); + expect(imported.httpApiId).toEqual('http-1234'); + expect(imported.apiEndpoint).toEqual('api-endpoint'); }); test('unsetting createDefaultStage', () => { @@ -188,7 +187,7 @@ describe('HttpApi', () => { // GIVEN const stack = new Stack(); const apiId = 'importedId'; - const api = HttpApi.fromApiId(stack, 'test-api', apiId); + const api = HttpApi.fromHttpApiAttributes(stack, 'test-api', { httpApiId: apiId }); const metricName = '4xxError'; const statistic = 'Sum'; @@ -261,6 +260,13 @@ describe('HttpApi', () => { expect(api.apiEndpoint).toBeDefined(); }); + + test('apiEndpoint for imported', () => { + const stack = new Stack(); + const api = HttpApi.fromHttpApiAttributes(stack, 'imported', { httpApiId: 'api-1234' }); + + expect(() => api.apiEndpoint).toThrow(/apiEndpoint is not configured/); + }); }); class DummyRouteIntegration implements IHttpRouteIntegration { diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 0b736380de817..9a4fcddf386cc 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -289,6 +289,7 @@ The `tcp()`, `http()` and `http2()` methods provide the spec necessary to define For HTTP based routes, the match field can be used to match on a route prefix. By default, an HTTP based route will match on `/`. All matches must start with a leading `/`. +The timeout field can also be specified for `idle` and `perRequest` timeouts. ```ts router.addRoute('route-http', { @@ -301,6 +302,10 @@ router.addRoute('route-http', { match: { serviceName: 'my-service.default.svc.cluster.local', }, + timeout: { + idle : Duration.seconds(2), + perRequest: Duration.seconds(1), + }, }), }); ``` diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index b015bd4580171..b3ce09f0c0031 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -1,6 +1,6 @@ import * as cdk from '@aws-cdk/core'; import { CfnRoute } from './appmesh.generated'; -import { Protocol } from './shared-interfaces'; +import { Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces'; import { IVirtualNode } from './virtual-node'; /** @@ -58,6 +58,13 @@ export interface HttpRouteSpecOptions { * List of targets that traffic is routed to when a request matches the route */ readonly weightedTargets: WeightedTarget[]; + + /** + * An object that represents a http timeout + * + * @default - None + */ + readonly timeout?: HttpTimeout; } /** @@ -68,6 +75,13 @@ export interface TcpRouteSpecOptions { * List of targets that traffic is routed to when a request matches the route */ readonly weightedTargets: WeightedTarget[]; + + /** + * An object that represents a tcp timeout + * + * @default - None + */ + readonly timeout?: TcpTimeout; } /** @@ -79,6 +93,13 @@ export interface GrpcRouteSpecOptions { */ readonly match: GrpcRouteMatch; + /** + * An object that represents a grpc timeout + * + * @default - None + */ + readonly timeout?: GrpcTimeout; + /** * List of targets that traffic is routed to when a request matches the route */ @@ -169,6 +190,11 @@ class HttpRouteSpec extends RouteSpec { */ public readonly match?: HttpRouteMatch; + /** + * The criteria for determining a timeout configuration + */ + public readonly timeout?: HttpTimeout; + /** * List of targets that traffic is routed to when a request matches the route */ @@ -179,6 +205,7 @@ class HttpRouteSpec extends RouteSpec { this.protocol = protocol; this.match = props.match; this.weightedTargets = props.weightedTargets; + this.timeout = props.timeout; } public bind(_scope: cdk.Construct): RouteSpecConfig { @@ -193,6 +220,7 @@ class HttpRouteSpec extends RouteSpec { match: { prefix: prefixPath, }, + timeout: renderTimeout(this.timeout), }; return { httpRouteSpec: this.protocol === Protocol.HTTP ? httpConfig : undefined, @@ -207,9 +235,15 @@ class TcpRouteSpec extends RouteSpec { */ public readonly weightedTargets: WeightedTarget[]; + /** + * The criteria for determining a timeout configuration + */ + public readonly timeout?: TcpTimeout; + constructor(props: TcpRouteSpecOptions) { super(); this.weightedTargets = props.weightedTargets; + this.timeout = props.timeout; } public bind(_scope: cdk.Construct): RouteSpecConfig { @@ -218,6 +252,7 @@ class TcpRouteSpec extends RouteSpec { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), }, + timeout: renderTimeout(this.timeout), }, }; } @@ -226,11 +261,13 @@ class TcpRouteSpec extends RouteSpec { class GrpcRouteSpec extends RouteSpec { public readonly weightedTargets: WeightedTarget[]; public readonly match: GrpcRouteMatch; + public readonly timeout?: GrpcTimeout; constructor(props: GrpcRouteSpecOptions) { super(); this.weightedTargets = props.weightedTargets; this.match = props.match; + this.timeout = props.timeout; } public bind(_scope: cdk.Construct): RouteSpecConfig { @@ -242,6 +279,7 @@ class GrpcRouteSpec extends RouteSpec { match: { serviceName: this.match.serviceName, }, + timeout: renderTimeout(this.timeout), }, }; } @@ -260,3 +298,25 @@ function renderWeightedTargets(weightedTargets: WeightedTarget[]): CfnRoute.Weig } return renderedTargets; } + +/** + * Utility method to construct a route timeout object + */ +function renderTimeout(timeout?: HttpTimeout): CfnRoute.HttpTimeoutProperty | undefined { + return timeout + ? { + idle: timeout?.idle !== undefined + ? { + unit: 'ms', + value: timeout?.idle.toMilliseconds(), + } + : undefined, + perRequest: timeout?.perRequest !== undefined + ? { + unit: 'ms', + value: timeout?.perRequest.toMilliseconds(), + } + : undefined, + } + : undefined; +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index c8b3ba88f6378..cbd574489f9c4 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -1,6 +1,56 @@ import * as cdk from '@aws-cdk/core'; import { CfnVirtualGateway, CfnVirtualNode } from './appmesh.generated'; +/** + * Represents timeouts for HTTP protocols. + */ +export interface HttpTimeout { + /** + * Represents an idle timeout. The amount of time that a connection may be idle. + * + * @default - none + */ + readonly idle?: cdk.Duration; + + /** + * Represents per request timeout. + * + * @default - 15 s + */ + readonly perRequest?: cdk.Duration; +} + +/** + * Represents timeouts for GRPC protocols. + */ +export interface GrpcTimeout { + /** + * Represents an idle timeout. The amount of time that a connection may be idle. + * + * @default - none + */ + readonly idle?: cdk.Duration; + + /** + * Represents per request timeout. + * + * @default - 15 s + */ + readonly perRequest?: cdk.Duration; +} + +/** + * Represents timeouts for TCP protocols. + */ +export interface TcpTimeout { + /** + * Represents an idle timeout. The amount of time that a connection may be idle. + * + * @default - none + */ + readonly idle?: cdk.Duration; +} + /** * Enum of supported AppMesh protocols */ diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts index e3e433d8e25d8..5793a746ee968 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node-listener.ts @@ -1,7 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { CfnVirtualNode } from './appmesh.generated'; import { validateHealthChecks } from './private/utils'; -import { HealthCheck, Protocol } from './shared-interfaces'; +import { HealthCheck, Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces'; /** * Properties for a VirtualNode listener @@ -68,56 +68,6 @@ export interface TcpVirtualNodeListenerOptions extends VirtualNodeListenerCommon readonly timeout?: TcpTimeout; } -/** - * Represents timeouts for HTTP protocols. - */ -export interface HttpTimeout { - /** - * Represents an idle timeout. The amount of time that a connection may be idle. - * - * @default - none - */ - readonly idle?: cdk.Duration; - - /** - * Represents per request timeout. - * - * @default - 15 s - */ - readonly perRequest?: cdk.Duration; -} - -/** - * Represents timeouts for GRPC protocols. - */ -export interface GrpcTimeout { - /** - * Represents an idle timeout. The amount of time that a connection may be idle. - * - * @default - none - */ - readonly idle?: cdk.Duration; - - /** - * Represents per request timeout. - * - * @default - 15 s - */ - readonly perRequest?: cdk.Duration; -} - -/** - * Represents timeouts for TCP protocols. - */ -export interface TcpTimeout { - /** - * Represents an idle timeout. The amount of time that a connection may be idle. - * - * @default - none - */ - readonly idle?: cdk.Duration; -} - /** * Defines listener for a VirtualNode */ diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index c956c2e77c844..fad86447a4fe3 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -180,7 +180,10 @@ "duration-prop-type:@aws-cdk/aws-appmesh.GrpcVirtualNodeListenerOptions.timeout", "duration-prop-type:@aws-cdk/aws-appmesh.HttpVirtualNodeListenerOptions.timeout", "duration-prop-type:@aws-cdk/aws-appmesh.Http2VirtualNodeListenerOptions.timeout", - "duration-prop-type:@aws-cdk/aws-appmesh.TcpVirtualNodeListenerOptions.timeout" + "duration-prop-type:@aws-cdk/aws-appmesh.TcpVirtualNodeListenerOptions.timeout", + "duration-prop-type:@aws-cdk/aws-appmesh.GrpcRouteSpecOptions.timeout", + "duration-prop-type:@aws-cdk/aws-appmesh.HttpRouteSpecOptions.timeout", + "duration-prop-type:@aws-cdk/aws-appmesh.TcpRouteSpecOptions.timeout" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index e3a9f98c5a016..e642633387206 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -515,6 +515,16 @@ }, "Match": { "Prefix": "/" + }, + "Timeout": { + "Idle": { + "Unit": "ms", + "Value": 10000 + }, + "PerRequest": { + "Unit": "ms", + "Value": 10000 + } } } }, @@ -553,6 +563,16 @@ }, "Match": { "Prefix": "/path2" + }, + "Timeout": { + "Idle": { + "Unit": "ms", + "Value": 11000 + }, + "PerRequest": { + "Unit": "ms", + "Value": 11000 + } } } }, @@ -588,6 +608,12 @@ "Weight": 20 } ] + }, + "Timeout": { + "Idle": { + "Unit": "ms", + "Value": 12000 + } } } }, diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 6790f4c749316..2f1884b3f7e63 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -59,6 +59,10 @@ router.addRoute('route-1', { match: { prefixPath: '/', }, + timeout: { + idle: cdk.Duration.seconds(10), + perRequest: cdk.Duration.seconds(10), + }, }), }); @@ -118,6 +122,10 @@ router.addRoute('route-2', { match: { prefixPath: '/path2', }, + timeout: { + idle: cdk.Duration.seconds(11), + perRequest: cdk.Duration.seconds(11), + }, }), }); @@ -129,6 +137,9 @@ router.addRoute('route-3', { weight: 20, }, ], + timeout: { + idle: cdk.Duration.seconds(12), + }, }), }); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index 2fd03f907f236..cb5c92e6464cf 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -29,6 +29,10 @@ export = { virtualNode: node, }, ], + timeout: { + idle: cdk.Duration.seconds(10), + perRequest: cdk.Duration.seconds(11), + }, }), }); @@ -39,6 +43,10 @@ export = { virtualNode: node, }, ], + timeout: { + idle: cdk.Duration.seconds(12), + perRequest: cdk.Duration.seconds(13), + }, }), }); @@ -49,6 +57,9 @@ export = { virtualNode: node, }, ], + timeout: { + idle: cdk.Duration.seconds(14), + }, }), }); @@ -62,6 +73,10 @@ export = { match: { serviceName: 'test.svc.local', }, + timeout: { + idle: cdk.Duration.seconds(15), + perRequest: cdk.Duration.seconds(16), + }, }), }); @@ -85,6 +100,16 @@ export = { Match: { Prefix: '/', }, + Timeout: { + Idle: { + Value: 10000, + Unit: 'ms', + }, + PerRequest: { + Value: 11000, + Unit: 'ms', + }, + }, }, }, RouteName: 'test-http-route', @@ -109,6 +134,16 @@ export = { Match: { Prefix: '/', }, + Timeout: { + Idle: { + Value: 12000, + Unit: 'ms', + }, + PerRequest: { + Value: 13000, + Unit: 'ms', + }, + }, }, }, RouteName: 'test-http2-route', @@ -130,6 +165,12 @@ export = { }, ], }, + Timeout: { + Idle: { + Value: 14000, + Unit: 'ms', + }, + }, }, }, RouteName: 'test-tcp-route', @@ -154,6 +195,16 @@ export = { Match: { ServiceName: 'test.svc.local', }, + Timeout: { + Idle: { + Value: 15000, + Unit: 'ms', + }, + PerRequest: { + Value: 16000, + Unit: 'ms', + }, + }, }, }, RouteName: 'test-grpc-route', @@ -188,6 +239,10 @@ export = { match: { prefixPath: '/node', }, + timeout: { + idle: cdk.Duration.seconds(10), + perRequest: cdk.Duration.seconds(11), + }, }), }); @@ -211,6 +266,16 @@ export = { Match: { Prefix: '/node', }, + Timeout: { + Idle: { + Value: 10000, + Unit: 'ms', + }, + PerRequest: { + Value: 11000, + Unit: 'ms', + }, + }, }, }, RouteName: 'test-http-route', diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index 83b1977014074..7eff891a9ca59 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -92,6 +92,7 @@ const gitHubSource = codebuild.Source.gitHub({ owner: 'awslabs', repo: 'aws-cdk', webhook: true, // optional, default: true if `webhookFilters` were provided, false otherwise + webhookTriggersBatchBuild: true, // optional, default is false webhookFilters: [ codebuild.FilterGroup .inEventOf(codebuild.EventAction.PUSH) diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 597a816ca8fc1..461bb8fd5e233 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -7,7 +7,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Aws, Duration, IResource, Lazy, Names, PhysicalName, Resource, Stack } from '@aws-cdk/core'; +import { Aws, Duration, IResource, Lazy, Names, PhysicalName, Resource, SecretValue, Stack, Tokenization } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IArtifacts } from './artifacts'; import { BuildSpec } from './build-spec'; @@ -465,6 +465,17 @@ export interface CommonProjectProps { */ readonly environmentVariables?: { [name: string]: BuildEnvironmentVariable }; + /** + * Whether to check for the presence of any secrets in the environment variables of the default type, BuildEnvironmentVariableType.PLAINTEXT. + * Since using a secret for the value of that kind of variable would result in it being displayed in plain text in the AWS Console, + * the construct will throw an exception if it detects a secret was passed there. + * Pass this property as false if you want to skip this validation, + * and keep using a secret in a plain text environment variable. + * + * @default true + */ + readonly checkSecretsInPlainTextEnvVariables?: boolean; + /** * The physical, human-readable name of the CodeBuild Project. * @@ -659,15 +670,39 @@ export class Project extends ProjectBase { * which is the representation of environment variables in CloudFormation. * * @param environmentVariables the map of string to environment variables + * @param validateNoPlainTextSecrets whether to throw an exception + * if any of the plain text environment variables contain secrets, defaults to 'false' * @returns an array of {@link CfnProject.EnvironmentVariableProperty} instances */ - public static serializeEnvVariables(environmentVariables: { [name: string]: BuildEnvironmentVariable }): - CfnProject.EnvironmentVariableProperty[] { - return Object.keys(environmentVariables).map(name => ({ - name, - type: environmentVariables[name].type || BuildEnvironmentVariableType.PLAINTEXT, - value: environmentVariables[name].value, - })); + public static serializeEnvVariables(environmentVariables: { [name: string]: BuildEnvironmentVariable }, + validateNoPlainTextSecrets: boolean = false): CfnProject.EnvironmentVariableProperty[] { + + const ret = new Array(); + + for (const [name, envVariable] of Object.entries(environmentVariables)) { + const cfnEnvVariable: CfnProject.EnvironmentVariableProperty = { + name, + type: envVariable.type || BuildEnvironmentVariableType.PLAINTEXT, + value: envVariable.value?.toString(), + }; + ret.push(cfnEnvVariable); + + // validate that the plain-text environment variables don't contain any secrets in them + if (validateNoPlainTextSecrets && cfnEnvVariable.type === BuildEnvironmentVariableType.PLAINTEXT) { + const fragments = Tokenization.reverseString(cfnEnvVariable.value); + for (const token of fragments.tokens) { + if (token instanceof SecretValue) { + throw new Error(`Plaintext environment variable '${name}' contains a secret value! ` + + 'This means the value of this variable will be visible in plain text in the AWS Console. ' + + "Please consider using CodeBuild's SecretsManager environment variables feature instead. " + + "If you'd like to continue with having this secret in the plaintext environment variables, " + + 'please set the checkSecretsInPlainTextEnvVariables property to false'); + } + } + } + } + + return ret; } public readonly grantPrincipal: iam.IPrincipal; @@ -761,7 +796,7 @@ export class Project extends ProjectBase { }, artifacts: artifactsConfig.artifactsProperty, serviceRole: this.role.roleArn, - environment: this.renderEnvironment(props.environment, environmentVariables), + environment: this.renderEnvironment(props, environmentVariables), fileSystemLocations: Lazy.any({ produce: () => this.renderFileSystemLocations() }), // lazy, because we have a setter for it in setEncryptionKey // The 'alias/aws/s3' default is necessary because leaving the `encryptionKey` field @@ -790,7 +825,7 @@ export class Project extends ProjectBase { this.projectName = this.getResourceNameAttribute(resource.ref); this.addToRolePolicy(this.createLoggingPermission()); - this.addParameterStorePermission(props); + this.addEnvVariablesPermissions(props.environmentVariables); // add permissions to create and use test report groups // with names starting with the project's name, // unless the customer explicitly opts out of it @@ -922,12 +957,13 @@ export class Project extends ProjectBase { }); } - private addParameterStorePermission(props: ProjectProps) { - if (!props.environmentVariables) { - return; - } + private addEnvVariablesPermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { + this.addParameterStorePermissions(environmentVariables); + this.addSecretsManagerPermissions(environmentVariables); + } - const resources = Object.values(props.environmentVariables) + private addParameterStorePermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { + const resources = Object.values(environmentVariables || {}) .filter(envVariable => envVariable.type === BuildEnvironmentVariableType.PARAMETER_STORE) .map(envVariable => // If the parameter name starts with / the resource name is not separated with a double '/' @@ -951,9 +987,32 @@ export class Project extends ProjectBase { })); } + private addSecretsManagerPermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { + const resources = Object.values(environmentVariables || {}) + .filter(envVariable => envVariable.type === BuildEnvironmentVariableType.SECRETS_MANAGER) + .map(envVariable => Stack.of(this).formatArn({ + service: 'secretsmanager', + resource: 'secret', + // we don't know the exact ARN of the Secret just from its name, but we can get close + resourceName: `${envVariable.value}-??????`, + sep: ':', + })); + + if (resources.length === 0) { + return; + } + + this.addToRolePolicy(new iam.PolicyStatement({ + actions: ['secretsmanager:GetSecretValue'], + resources, + })); + } + private renderEnvironment( - env: BuildEnvironment = {}, + props: ProjectProps, projectVars: { [name: string]: BuildEnvironmentVariable } = {}): CfnProject.EnvironmentProperty { + + const env = props.environment ?? {}; const vars: { [name: string]: BuildEnvironmentVariable } = {}; const containerVars = env.environmentVariables || {}; @@ -1008,7 +1067,9 @@ export class Project extends ProjectBase { : undefined, privilegedMode: env.privileged || false, computeType: env.computeType || this.buildImage.defaultComputeType, - environmentVariables: hasEnvironmentVars ? Project.serializeEnvVariables(vars) : undefined, + environmentVariables: hasEnvironmentVars + ? Project.serializeEnvVariables(vars, props.checkSecretsInPlainTextEnvVariables ?? true) + : undefined, }; } @@ -1076,7 +1137,7 @@ export class Project extends ProjectBase { private renderLoggingConfiguration(props: LoggingOptions | undefined): CfnProject.LogsConfigProperty | undefined { if (props === undefined) { return undefined; - }; + } let s3Config: CfnProject.S3LogsConfigProperty|undefined = undefined; let cloudwatchConfig: CfnProject.CloudWatchLogsConfigProperty|undefined = undefined; @@ -1085,9 +1146,10 @@ export class Project extends ProjectBase { const s3Logs = props.s3; s3Config = { status: (s3Logs.enabled ?? true) ? 'ENABLED' : 'DISABLED', - location: `${s3Logs.bucket.bucketName}/${s3Logs.prefix}`, + location: `${s3Logs.bucket.bucketName}` + (s3Logs.prefix ? `/${s3Logs.prefix}` : ''), encryptionDisabled: s3Logs.encrypted, }; + s3Logs.bucket?.grantWrite(this); } if (props.cloudWatch) { @@ -1097,6 +1159,7 @@ export class Project extends ProjectBase { if (status === 'ENABLED' && !(cloudWatchLogs.logGroup)) { throw new Error('Specifying a LogGroup is required if CloudWatch logging for CodeBuild is enabled'); } + cloudWatchLogs.logGroup?.grantWrite(this); cloudwatchConfig = { status, diff --git a/packages/@aws-cdk/aws-codebuild/lib/source.ts b/packages/@aws-cdk/aws-codebuild/lib/source.ts index f0a61373c0fbd..b8002f2db9e36 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/source.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/source.ts @@ -482,6 +482,13 @@ interface ThirdPartyGitSourceProps extends GitSourceProps { */ readonly webhook?: boolean; + /** + * Trigger a batch build from a webhook instead of a standard one. + * + * @default false + */ + readonly webhookTriggersBatchBuild?: boolean; + /** * A list of webhook filters that can constraint what events in the repository will trigger a build. * A build is triggered if any of the provided filter groups match. @@ -500,6 +507,7 @@ abstract class ThirdPartyGitSource extends GitSource { protected readonly webhookFilters: FilterGroup[]; private readonly reportBuildStatus: boolean; private readonly webhook?: boolean; + private readonly webhookTriggersBatchBuild?: boolean; protected constructor(props: ThirdPartyGitSourceProps) { super(props); @@ -507,12 +515,21 @@ abstract class ThirdPartyGitSource extends GitSource { this.webhook = props.webhook; this.reportBuildStatus = props.reportBuildStatus === undefined ? true : props.reportBuildStatus; this.webhookFilters = props.webhookFilters || []; + this.webhookTriggersBatchBuild = props.webhookTriggersBatchBuild; } public bind(_scope: CoreConstruct, _project: IProject): SourceConfig { const anyFilterGroupsProvided = this.webhookFilters.length > 0; const webhook = this.webhook === undefined ? (anyFilterGroupsProvided ? true : undefined) : this.webhook; + if (!webhook && anyFilterGroupsProvided) { + throw new Error('`webhookFilters` cannot be used when `webhook` is `false`'); + } + + if (!webhook && this.webhookTriggersBatchBuild) { + throw new Error('`webhookTriggersBatchBuild` cannot be used when `webhook` is `false`'); + } + const superConfig = super.bind(_scope, _project); return { sourceProperty: { @@ -522,6 +539,7 @@ abstract class ThirdPartyGitSource extends GitSource { sourceVersion: superConfig.sourceVersion, buildTriggers: webhook === undefined ? undefined : { webhook, + buildType: this.webhookTriggersBatchBuild ? 'BUILD_BATCH' : undefined, filterGroups: anyFilterGroupsProvided ? this.webhookFilters.map(fg => fg._toJson()) : undefined, }, }; diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.expected.json new file mode 100644 index 0000000000000..e68bc6b47d2b4 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.expected.json @@ -0,0 +1,223 @@ +{ + "Resources": { + "LogingGroupE599B53B": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LoggingBucket1E5A6F3B": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ProjectRole4CCB274E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ProjectRoleDefaultPolicy7F29461B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "LoggingBucket1E5A6F3B", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "LoggingBucket1E5A6F3B", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LogingGroupE599B53B", + "Arn" + ] + } + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "ProjectC78D97AD" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "ProjectC78D97AD" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "codebuild:CreateReportGroup", + "codebuild:CreateReport", + "codebuild:UpdateReport", + "codebuild:BatchPutTestCases", + "codebuild:BatchPutCodeCoverages" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codebuild:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":report-group/", + { + "Ref": "ProjectC78D97AD" + }, + "-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ProjectRoleDefaultPolicy7F29461B", + "Roles": [ + { + "Ref": "ProjectRole4CCB274E" + } + ] + } + }, + "ProjectC78D97AD": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "ProjectRole4CCB274E", + "Arn" + ] + }, + "Source": { + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3", + "LogsConfig": { + "CloudWatchLogs": { + "GroupName": { + "Ref": "LogingGroupE599B53B" + }, + "Status": "ENABLED" + }, + "S3Logs": { + "Location": { + "Ref": "LoggingBucket1E5A6F3B" + }, + "Status": "ENABLED" + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.ts b/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.ts new file mode 100644 index 0000000000000..b0134178bac30 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-logging.ts @@ -0,0 +1,25 @@ +import * as logs from '@aws-cdk/aws-logs'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as codebuild from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codebuild-logging'); + +new codebuild.PipelineProject(stack, 'Project', { + logging: { + cloudWatch: { + logGroup: new logs.LogGroup(stack, 'LogingGroup', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }), + }, + s3: { + bucket: new s3.Bucket(stack, 'LoggingBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }), + }, + }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index 66a4ccd09e8db..5a7766918c67b 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -694,6 +694,48 @@ export = { test.done(); }, + + 'with webhookTriggersBatchBuild option'(test: Test) { + const stack = new cdk.Stack(); + + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'testowner', + repo: 'testrepo', + webhook: true, + webhookTriggersBatchBuild: true, + }), + }); + + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + Triggers: { + Webhook: true, + BuildType: 'BUILD_BATCH', + }, + })); + + test.done(); + }, + + 'fail creating a Project when webhook false and webhookTriggersBatchBuild option'(test: Test) { + [false, undefined].forEach((webhook) => { + const stack = new cdk.Stack(); + + test.throws(() => { + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'testowner', + repo: 'testrepo', + webhook, + webhookTriggersBatchBuild: true, + }), + }); + }, /`webhookTriggersBatchBuild` cannot be used when `webhook` is `false`/); + }); + + test.done(); + }, + 'fail creating a Project when no build spec is given'(test: Test) { const stack = new cdk.Stack(); @@ -1670,6 +1712,25 @@ export = { test.done(); }, + 'cannot be used when webhook is false'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => { + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.bitBucket({ + owner: 'owner', + repo: 'repo', + webhook: false, + webhookFilters: [ + codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH), + ], + }), + }); + }, /`webhookFilters` cannot be used when `webhook` is `false`/); + + test.done(); + }, + 'can have FILE_PATH filters if the Group contains PUSH and PR_CREATED events'(test: Test) { codebuild.FilterGroup.inEventOf( codebuild.EventAction.PULL_REQUEST_CREATED, diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts index 81a09bd3b96f8..2912d832bd2c0 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts @@ -711,11 +711,9 @@ export = { logging: { cloudWatch: { logGroup, - prefix: '/my-logs', }, s3: { bucket, - prefix: 'my-logs', }, }, }); @@ -726,10 +724,9 @@ export = { CloudWatchLogs: { GroupName: 'MyLogGroupName', Status: 'ENABLED', - StreamName: '/my-logs', }, S3Logs: { - Location: 'MyBucketName/my-logs', + Location: 'MyBucketName', Status: 'ENABLED', }, }), @@ -889,5 +886,78 @@ export = { test.done(); }, + + "grants the Project's Role read permissions to the SecretsManager environment variables"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.PipelineProject(stack, 'Project', { + environmentVariables: { + 'ENV_VAR1': { + type: codebuild.BuildEnvironmentVariableType.SECRETS_MANAGER, + value: 'my-secret', + }, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': arrayWith({ + 'Action': 'secretsmanager:GetSecretValue', + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':secretsmanager:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':secret:my-secret-??????', + ]], + }, + }), + }, + })); + + test.done(); + }, + + 'should fail creating when using a secret value in a plaintext variable'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + test.throws(() => { + new codebuild.PipelineProject(stack, 'Project', { + environmentVariables: { + 'a': { + value: `a_${cdk.SecretValue.secretsManager('my-secret')}_b`, + }, + }, + }); + }, /Plaintext environment variable 'a' contains a secret value!/); + + test.done(); + }, + + "should allow opting out of the 'secret value in a plaintext variable' validation"(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + new codebuild.PipelineProject(stack, 'Project', { + environmentVariables: { + 'b': { + value: cdk.SecretValue.secretsManager('my-secret'), + }, + }, + checkSecretsInPlainTextEnvVariables: false, + }); + + test.done(); + }, }, }; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index e81095b746eee..aa2d3be190029 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -78,6 +78,17 @@ export interface CodeBuildActionProps extends codepipeline.CommonAwsActionProps */ readonly environmentVariables?: { [name: string]: codebuild.BuildEnvironmentVariable }; + /** + * Whether to check for the presence of any secrets in the environment variables of the default type, BuildEnvironmentVariableType.PLAINTEXT. + * Since using a secret for the value of that kind of variable would result in it being displayed in plain text in the AWS Console, + * the construct will throw an exception if it detects a secret was passed there. + * Pass this property as false if you want to skip this validation, + * and keep using a secret in a plain text environment variable. + * + * @default true + */ + readonly checkSecretsInPlainTextEnvVariables?: boolean; + /** * Trigger a batch build. * @@ -139,8 +150,8 @@ export class CodeBuildAction extends Action { resources: [this.props.project.projectArn], actions: [ 'codebuild:BatchGetBuilds', - 'codebuild:StartBuild', - 'codebuild:StopBuild', + `codebuild:${this.props.executeBatchBuild ? 'StartBuildBatch' : 'StartBuild'}`, + `codebuild:${this.props.executeBatchBuild ? 'StopBuildBatch' : 'StopBuild'}`, ], })); @@ -177,7 +188,8 @@ export class CodeBuildAction extends Action { const configuration: any = { ProjectName: this.props.project.projectName, EnvironmentVariables: this.props.environmentVariables && - cdk.Stack.of(scope).toJsonString(codebuild.Project.serializeEnvVariables(this.props.environmentVariables)), + cdk.Stack.of(scope).toJsonString(codebuild.Project.serializeEnvVariables(this.props.environmentVariables, + this.props.checkSecretsInPlainTextEnvVariables ?? true)), }; if ((this.actionProperties.inputs || []).length > 1) { // lazy, because the Artifact name might be generated lazily diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts index f5d5abaeb4934..68e5acaa849ca 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts @@ -4,7 +4,7 @@ import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; -import { App, Stack } from '@aws-cdk/core'; +import { App, SecretValue, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as cpactions from '../../lib'; @@ -257,5 +257,84 @@ export = { test.done(); }, + + 'environment variables': { + 'should fail by default when added to a Pipeline while using a secret value in a plaintext variable'(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new cpactions.CodeCommitSourceAction({ + actionName: 'source', + repository: new codecommit.Repository(stack, 'CodeCommitRepo', { + repositoryName: 'my-repo', + }), + output: sourceOutput, + })], + }, + ], + }); + + const buildStage = pipeline.addStage({ + stageName: 'Build', + }); + const codeBuildProject = new codebuild.PipelineProject(stack, 'CodeBuild'); + const buildAction = new cpactions.CodeBuildAction({ + actionName: 'Build', + project: codeBuildProject, + input: sourceOutput, + environmentVariables: { + 'X': { + value: SecretValue.secretsManager('my-secret'), + }, + }, + }); + + test.throws(() => { + buildStage.addAction(buildAction); + }, /Plaintext environment variable 'X' contains a secret value!/); + + test.done(); + }, + + "should allow opting out of the 'secret value in a plaintext variable' validation"(test: Test) { + const stack = new Stack(); + + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new cpactions.CodeCommitSourceAction({ + actionName: 'source', + repository: new codecommit.Repository(stack, 'CodeCommitRepo', { + repositoryName: 'my-repo', + }), + output: sourceOutput, + })], + }, + { + stageName: 'Build', + actions: [new cpactions.CodeBuildAction({ + actionName: 'build', + project: new codebuild.PipelineProject(stack, 'CodeBuild'), + input: sourceOutput, + environmentVariables: { + 'X': { + value: SecretValue.secretsManager('my-secret'), + }, + }, + checkSecretsInPlainTextEnvVariables: false, + })], + }, + ], + }); + + test.done(); + }, + }, }, }; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json new file mode 100644 index 0000000000000..d489a5712eeba --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json @@ -0,0 +1,487 @@ +{ + "Resources": { + "MyRepoF4F48043": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "MyIntegTestTempRepo" + } + }, + "MyRepoawscdkcodepipelinecodebuildbatchPipeline674F06D4EventRuleD3DE52E7": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.codecommit" + ], + "resources": [ + { + "Fn::GetAtt": [ + "MyRepoF4F48043", + "Arn" + ] + } + ], + "detail-type": [ + "CodeCommit Repository State Change" + ], + "detail": { + "event": [ + "referenceCreated", + "referenceUpdated" + ], + "referenceName": [ + "master" + ] + } + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineEventsRole46BEEA7C", + "Arn" + ] + } + } + ] + } + }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*", + "s3:PutObject*", + "s3:Abort*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + } + }, + { + "Action": [ + "codecommit:GetBranch", + "codecommit:GetCommit", + "codecommit:UploadArchive", + "codecommit:GetUploadArchiveStatus", + "codecommit:CancelUploadArchive" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyRepoF4F48043", + "Arn" + ] + } + }, + { + "Action": [ + "codebuild:BatchGetBuilds", + "codebuild:StartBuildBatch", + "codebuild:StopBuildBatch" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyBuildProject30DB9D6E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "CodeCommit", + "Version": "1" + }, + "Configuration": { + "RepositoryName": { + "Fn::GetAtt": [ + "MyRepoF4F48043", + "Name" + ] + }, + "BranchName": "master", + "PollForSourceChanges": false + }, + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "Artifact_Source_Source" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Build", + "Owner": "AWS", + "Provider": "CodeBuild", + "Version": "1" + }, + "Configuration": { + "ProjectName": { + "Ref": "MyBuildProject30DB9D6E" + }, + "BatchEnabled": "true" + }, + "InputArtifacts": [ + { + "Name": "Artifact_Source_Source" + } + ], + "Name": "Build", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Build" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "MyBucketF68F3FF0" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "PipelineRoleDefaultPolicyC7A05455", + "PipelineRoleD68726F7" + ] + }, + "PipelineEventsRole46BEEA7C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineEventsRoleDefaultPolicyFF4FCCE0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "codepipeline:StartPipelineExecution", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":codepipeline:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "PipelineC660917D" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineEventsRoleDefaultPolicyFF4FCCE0", + "Roles": [ + { + "Ref": "PipelineEventsRole46BEEA7C" + } + ] + } + }, + "MyBuildProjectRole6B7E2258": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyBuildProjectRoleDefaultPolicy5604AA87": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyBuildProject30DB9D6E" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyBuildProject30DB9D6E" + }, + ":*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyBuildProjectRoleDefaultPolicy5604AA87", + "Roles": [ + { + "Ref": "MyBuildProjectRole6B7E2258" + } + ] + } + }, + "MyBuildProject30DB9D6E": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "CODEPIPELINE" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyBuildProjectRole6B7E2258", + "Arn" + ] + }, + "Source": { + "Type": "CODEPIPELINE" + }, + "EncryptionKey": "alias/aws/s3" + } + } + } +} diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.ts new file mode 100644 index 0000000000000..d5fe1b2fb9b2f --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.ts @@ -0,0 +1,56 @@ +import * as codebuild from '@aws-cdk/aws-codebuild'; +import * as codecommit from '@aws-cdk/aws-codecommit'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as cpactions from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-codebuild-batch'); + +const repository = new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'MyIntegTestTempRepo', +}); +const bucket = new s3.Bucket(stack, 'MyBucket', { + versioned: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, +}); + +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + artifactBucket: bucket, +}); +const pipelineRole = pipeline.role; + +const sourceOutput = new codepipeline.Artifact(); +const sourceAction = new cpactions.CodeCommitSourceAction({ + actionName: 'Source', + repository, + output: sourceOutput, + role: pipelineRole, +}); +pipeline.addStage({ + stageName: 'Source', + actions: [ + sourceAction, + ], +}); + +const project = new codebuild.PipelineProject(stack, 'MyBuildProject', { + grantReportGroupPermissions: false, +}); +const buildAction = new cpactions.CodeBuildAction({ + actionName: 'Build', + project, + executeBatchBuild: true, + input: sourceOutput, + role: pipelineRole, +}); +pipeline.addStage({ + stageName: 'Build', + actions: [ + buildAction, + ], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index 72d10f35c1c15..acd3f1820c156 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -92,6 +92,23 @@ const globalTable = new dynamodb.Table(this, 'Table', { When doing so, a CloudFormation Custom Resource will be added to the stack in order to create the replica tables in the selected regions. +The default billing mode for Global Tables is `PAY_PER_REQUEST`. +If you want to use `PROVISIONED`, +you have to make sure write auto-scaling is enabled for that Table: + +```ts +const globalTable = new dynamodb.Table(this, 'Table', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + replicationRegions: ['us-east-1', 'us-east-2', 'us-west-2'], + billingMode: BillingMode.PROVISIONED, +}); + +globalTable.autoScaleWriteCapacity({ + minCapacity: 1, + maxCapacity: 10, +}).scaleOnUtilization({ targetUtilizationPercent: 75 }); +``` + ## Encryption All user data stored in Amazon DynamoDB is fully encrypted at rest. When creating a new table, you can choose to encrypt using the following customer master keys (CMK) to encrypt your table: diff --git a/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts b/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts index 4ec8e1365773f..fc2a538e2545d 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/scalable-table-attribute.ts @@ -5,10 +5,13 @@ import { UtilizationScalingProps } from './scalable-attribute-api'; * A scalable table attribute */ export class ScalableTableAttribute extends appscaling.BaseScalableAttribute { + private scalingPolicyCreated = false; + /** * Scale out or in based on time */ public scaleOnSchedule(id: string, action: appscaling.ScalingSchedule) { + this.scalingPolicyCreated = true; super.doScaleOnSchedule(id, action); } @@ -20,6 +23,7 @@ export class ScalableTableAttribute extends appscaling.BaseScalableAttribute { // eslint-disable-next-line max-len throw new RangeError(`targetUtilizationPercent for DynamoDB scaling must be between 10 and 90 percent, got: ${props.targetUtilizationPercent}`); } + this.scalingPolicyCreated = true; const predefinedMetric = this.props.dimension.indexOf('ReadCapacity') === -1 ? appscaling.PredefinedMetric.DYANMODB_WRITE_CAPACITY_UTILIZATION : appscaling.PredefinedMetric.DYNAMODB_READ_CAPACITY_UTILIZATION; @@ -33,6 +37,11 @@ export class ScalableTableAttribute extends appscaling.BaseScalableAttribute { predefinedMetric, }); } + + /** @internal */ + public get _scalingPolicyCreated(): boolean { + return this.scalingPolicyCreated; + } } /** diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index dfcae3146ad56..792f68f4cca3e 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1064,6 +1064,8 @@ export class Table extends TableBase { private readonly indexScaling = new Map(); private readonly scalingRole: iam.IRole; + private readonly globalReplicaCustomResources = new Array(); + constructor(scope: Construct, id: string, props: TableProps) { super(scope, id, { physicalName: props.tableName, @@ -1071,9 +1073,6 @@ export class Table extends TableBase { const { sseSpecification, encryptionKey } = this.parseEncryption(props); - this.billingMode = props.billingMode || BillingMode.PROVISIONED; - this.validateProvisioning(props); - let streamSpecification: CfnTable.StreamSpecificationProperty | undefined; if (props.replicationRegions) { if (props.stream && props.stream !== StreamViewType.NEW_AND_OLD_IMAGES) { @@ -1081,13 +1080,14 @@ export class Table extends TableBase { } streamSpecification = { streamViewType: StreamViewType.NEW_AND_OLD_IMAGES }; - if (props.billingMode && props.billingMode !== BillingMode.PAY_PER_REQUEST) { - throw new Error('The `PAY_PER_REQUEST` billing mode must be used when specifying `replicationRegions`'); + this.billingMode = props.billingMode ?? BillingMode.PAY_PER_REQUEST; + } else { + this.billingMode = props.billingMode ?? BillingMode.PROVISIONED; + if (props.stream) { + streamSpecification = { streamViewType: props.stream }; } - this.billingMode = BillingMode.PAY_PER_REQUEST; - } else if (props.stream) { - streamSpecification = { streamViewType: props.stream }; } + this.validateProvisioning(props); this.table = new CfnTable(this, 'Resource', { tableName: this.physicalName, @@ -1222,13 +1222,17 @@ export class Table extends TableBase { throw new Error('AutoScaling is not available for tables with PAY_PER_REQUEST billing mode'); } - return this.tableScaling.scalableWriteAttribute = new ScalableTableAttribute(this, 'WriteScaling', { + this.tableScaling.scalableWriteAttribute = new ScalableTableAttribute(this, 'WriteScaling', { serviceNamespace: appscaling.ServiceNamespace.DYNAMODB, resourceId: `table/${this.tableName}`, dimension: 'dynamodb:table:WriteCapacityUnits', role: this.scalingRole, ...props, }); + for (const globalReplicaCustomResource of this.globalReplicaCustomResources) { + globalReplicaCustomResource.node.addDependency(this.tableScaling.scalableWriteAttribute); + } + return this.tableScaling.scalableWriteAttribute; } /** @@ -1298,6 +1302,17 @@ export class Table extends TableBase { errors.push('a sort key of the table must be specified to add local secondary indexes'); } + if (this.globalReplicaCustomResources.length > 0 && this.billingMode === BillingMode.PROVISIONED) { + const writeAutoScaleAttribute = this.tableScaling.scalableWriteAttribute; + if (!writeAutoScaleAttribute) { + errors.push('A global Table that uses PROVISIONED as the billing mode needs auto-scaled write capacity. ' + + 'Use the autoScaleWriteCapacity() method to enable it.'); + } else if (!writeAutoScaleAttribute._scalingPolicyCreated) { + errors.push('A global Table that uses PROVISIONED as the billing mode needs auto-scaled write capacity with a policy. ' + + 'Call one of the scaleOn*() methods of the object returned from autoScaleWriteCapacity()'); + } + } + return errors; } @@ -1467,6 +1482,7 @@ export class Table extends TableBase { onEventHandlerPolicy.policy, isCompleteHandlerPolicy.policy, ); + this.globalReplicaCustomResources.push(currentRegion); // Deploy time check to prevent from creating a replica in the region // where this stack is deployed. Only needed for environment agnostic @@ -1681,4 +1697,4 @@ class SourceTableAttachedPrincipal extends iam.PrincipalBase { statementAdded: true, }; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 550a47f81ec1d..119043b54464a 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -836,27 +836,40 @@ test('when specifying PAY_PER_REQUEST billing mode', () => { ); }); -test('error when specifying read or write capacity with a PAY_PER_REQUEST billing mode', () => { - const stack = new Stack(); - expect(() => new Table(stack, 'Table A', { - tableName: TABLE_NAME, - billingMode: BillingMode.PAY_PER_REQUEST, - partitionKey: TABLE_PARTITION_KEY, - readCapacity: 1, - })).toThrow(/PAY_PER_REQUEST/); - expect(() => new Table(stack, 'Table B', { - tableName: TABLE_NAME, - billingMode: BillingMode.PAY_PER_REQUEST, - partitionKey: TABLE_PARTITION_KEY, - writeCapacity: 1, - })).toThrow(/PAY_PER_REQUEST/); - expect(() => new Table(stack, 'Table C', { - tableName: TABLE_NAME, - billingMode: BillingMode.PAY_PER_REQUEST, - partitionKey: TABLE_PARTITION_KEY, - readCapacity: 1, - writeCapacity: 1, - })).toThrow(/PAY_PER_REQUEST/); +describe('when billing mode is PAY_PER_REQUEST', () => { + let stack: Stack; + + beforeEach(() => { + stack = new Stack(); + }); + + test('creating the Table fails when readCapacity is specified', () => { + expect(() => new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + billingMode: BillingMode.PAY_PER_REQUEST, + readCapacity: 1, + })).toThrow(/PAY_PER_REQUEST/); + }); + + test('creating the Table fails when writeCapacity is specified', () => { + expect(() => new Table(stack, 'Table B', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + billingMode: BillingMode.PAY_PER_REQUEST, + writeCapacity: 1, + })).toThrow(/PAY_PER_REQUEST/); + }); + + test('creating the Table fails when both readCapacity and writeCapacity are specified', () => { + expect(() => new Table(stack, 'Table C', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + billingMode: BillingMode.PAY_PER_REQUEST, + readCapacity: 1, + writeCapacity: 1, + })).toThrow(/PAY_PER_REQUEST/); + }); }); test('when adding a global secondary index with hash key only', () => { @@ -2766,12 +2779,62 @@ describe('global', () => { }); }); - test('throws with PROVISIONED billing mode', () => { + test('throws when PROVISIONED billing mode is used without auto-scaled writes', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new Table(stack, 'Table', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + replicationRegions: [ + 'eu-west-2', + 'eu-central-1', + ], + billingMode: BillingMode.PROVISIONED, + }); + + // THEN + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/A global Table that uses PROVISIONED as the billing mode needs auto-scaled write capacity/); + }); + + test('throws when PROVISIONED billing mode is used with auto-scaled writes, but without a policy', () => { // GIVEN const stack = new Stack(); + // WHEN + const table = new Table(stack, 'Table', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + replicationRegions: [ + 'eu-west-2', + 'eu-central-1', + ], + billingMode: BillingMode.PROVISIONED, + }); + table.autoScaleWriteCapacity({ + minCapacity: 1, + maxCapacity: 10, + }); + // THEN - expect(() => new Table(stack, 'Table', { + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/A global Table that uses PROVISIONED as the billing mode needs auto-scaled write capacity with a policy/); + }); + + test('allows PROVISIONED billing mode when auto-scaled writes with a policy are specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const table = new Table(stack, 'Table', { partitionKey: { name: 'id', type: AttributeType.STRING, @@ -2781,7 +2844,15 @@ describe('global', () => { 'eu-central-1', ], billingMode: BillingMode.PROVISIONED, - })).toThrow(/`PAY_PER_REQUEST`/); + }); + table.autoScaleWriteCapacity({ + minCapacity: 1, + maxCapacity: 10, + }).scaleOnUtilization({ targetUtilizationPercent: 75 }); + + expect(stack).toHaveResourceLike('AWS::DynamoDB::Table', { + BillingMode: ABSENT, // PROVISIONED is the default + }); }); test('throws when stream is set and not set to NEW_AND_OLD_IMAGES', () => { diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json new file mode 100644 index 0000000000000..7bfa6b9637bcd --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json @@ -0,0 +1,372 @@ +{ + "Resources": { + "TableCD117FA1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "StreamSpecification": { + "StreamViewType": "NEW_AND_OLD_IMAGES" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:us-east-2:", + { + "Ref": "AWS::AccountId" + }, + ":table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:eu-west-3:", + { + "Ref": "AWS::AccountId" + }, + ":table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "leAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "Roles": [ + { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.awscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole348A0C9ARef" + ] + } + ] + } + }, + "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:DescribeTable", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", + "Roles": [ + { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.awscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole750F1EE9Ref" + ] + } + ] + } + }, + "TableReplicauseast28A15C236": { + "Type": "Custom::DynamoDBReplica", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.awscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderframeworkonEventACC2C387Arn" + ] + }, + "TableName": { + "Ref": "TableCD117FA1" + }, + "Region": "us-east-2" + }, + "DependsOn": [ + "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", + "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "TableWriteScalingTargetE5669214", + "TableWriteScalingTargetTrackingD78DCCD8" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Condition": "TableStackRegionNotEqualsuseast2D20A1E77" + }, + "TableReplicaeuwest314C3E552": { + "Type": "Custom::DynamoDBReplica", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.awscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderframeworkonEventACC2C387Arn" + ] + }, + "TableName": { + "Ref": "TableCD117FA1" + }, + "Region": "eu-west-3" + }, + "DependsOn": [ + "TableReplicauseast28A15C236", + "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", + "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "TableWriteScalingTargetE5669214", + "TableWriteScalingTargetTrackingD78DCCD8" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete", + "Condition": "TableStackRegionNotEqualseuwest302B3591C" + }, + "TableWriteScalingTargetE5669214": { + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + "Properties": { + "MaxCapacity": 10, + "MinCapacity": 5, + "ResourceId": { + "Fn::Join": [ + "", + [ + "table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_DynamoDBTable" + ] + ] + }, + "ScalableDimension": "dynamodb:table:WriteCapacityUnits", + "ServiceNamespace": "dynamodb" + } + }, + "TableWriteScalingTargetTrackingD78DCCD8": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "awscdkdynamodbglobalreplicasprovisionedTableWriteScalingTargetTrackingD631E2EC", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "TableWriteScalingTargetE5669214" + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "DynamoDBWriteCapacityUtilization" + }, + "TargetValue": 75 + } + } + }, + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3Bucket150DFE4F" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3VersionKey304CC738" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3VersionKey304CC738" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket50997EC4Ref": { + "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0" + }, + "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey0F47C425Ref": { + "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275" + }, + "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket6C51C355Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" + }, + "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKey84AB7371Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" + } + } + } + } + }, + "Conditions": { + "TableStackRegionNotEqualsuseast2D20A1E77": { + "Fn::Not": [ + { + "Fn::Equals": [ + "us-east-2", + { + "Ref": "AWS::Region" + } + ] + } + ] + }, + "TableStackRegionNotEqualseuwest302B3591C": { + "Fn::Not": [ + { + "Fn::Equals": [ + "eu-west-3", + { + "Ref": "AWS::Region" + } + ] + } + ] + } + }, + "Parameters": { + "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0": { + "Type": "String", + "Description": "S3 bucket for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + }, + "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275": { + "Type": "String", + "Description": "S3 key for asset version \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + }, + "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714ArtifactHash477AAEA7": { + "Type": "String", + "Description": "Artifact hash for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { + "Type": "String", + "Description": "S3 bucket for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F": { + "Type": "String", + "Description": "S3 key for asset version \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1ArtifactHashA521A16F": { + "Type": "String", + "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3Bucket150DFE4F": { + "Type": "String", + "Description": "S3 bucket for asset \"a8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefc\"" + }, + "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcS3VersionKey304CC738": { + "Type": "String", + "Description": "S3 key for asset version \"a8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefc\"" + }, + "AssetParametersa8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefcArtifactHash6868817F": { + "Type": "String", + "Description": "Artifact hash for asset \"a8adade98e50c02310e9b63ef8d0d201926ed756455b604d8eb5d1bc4f5deefc\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.ts b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.ts new file mode 100644 index 0000000000000..8403538c2bd50 --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.ts @@ -0,0 +1,19 @@ +import * as cdk from '@aws-cdk/core'; +import * as dynamodb from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-dynamodb-global-replicas-provisioned'); + +const table = new dynamodb.Table(stack, 'Table', { + partitionKey: { name: 'hashKey', type: dynamodb.AttributeType.STRING }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + replicationRegions: ['us-east-2', 'eu-west-3'], + billingMode: dynamodb.BillingMode.PROVISIONED, +}); + +table.autoScaleWriteCapacity({ + minCapacity: 5, + maxCapacity: 10, +}).scaleOnUtilization({ targetUtilizationPercent: 75 }); + +app.synth(); diff --git a/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts b/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts index cddd4c258689b..3a1d97bd4b345 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts @@ -3,6 +3,17 @@ import * as AWS from 'aws-sdk-mock'; import * as sinon from 'sinon'; import { isCompleteHandler, onEventHandler } from '../lib/replica-handler'; +let oldConsoleLog: any; + +beforeAll(() => { + oldConsoleLog = global.console.log; + global.console.log = jest.fn(); +}); + +afterAll(() => { + global.console.log = oldConsoleLog; +}); + AWS.setSDK(require.resolve('aws-sdk')); const createEvent: OnEventRequest = { diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 664f3c2b2eff2..b8a583d1a2ea5 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -623,6 +623,23 @@ new VpcEndpointService(this, 'EndpointService', { }); ``` +Endpoint services support private DNS, which makes it easier for clients to connect to your service by automatically setting up DNS in their VPC. +You can enable private DNS on an endpoint service like so: + +```ts +import { VpcEndpointServiceDomainName } from '@aws-cdk/aws-route53'; + +new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff.aws-cdk.dev', + publicHostedZone: zone, +}); +``` + +Note: The domain name must be owned (registered through Route53) by the account the endpoint service is in, or delegated to the account. +The VpcEndpointServiceDomainName will handle the AWS side of domain verification, the process for which can be found +[here](https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-dns-validation.html) + ## Instances You can use the `Instance` class to start up a single EC2 instance. For production setups, we recommend diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts index d5d043eef0dc0..d17e56aab202c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts @@ -27,6 +27,14 @@ export interface IVpcEndpointService extends IResource { * @attribute */ readonly vpcEndpointServiceName: string; + + /** + * The id of the VPC Endpoint Service that clients use to connect to, + * like vpce-svc-xxxxxxxxxxxxxxxx + * + * @attribute + */ + readonly vpcEndpointServiceId: string; } /** diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 2efcfbc69b68a..83ef63d885b57 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -257,6 +257,7 @@ export class InterfaceVpcEndpointService implements IInterfaceVpcEndpointService */ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointService { public static readonly SAGEMAKER_NOTEBOOK = new InterfaceVpcEndpointAwsService('notebook', 'aws.sagemaker'); + public static readonly ATHENA = new InterfaceVpcEndpointAwsService('athena'); public static readonly CLOUDFORMATION = new InterfaceVpcEndpointAwsService('cloudformation'); public static readonly CLOUDTRAIL = new InterfaceVpcEndpointAwsService('cloudtrail'); public static readonly CODEBUILD = new InterfaceVpcEndpointAwsService('codebuild'); @@ -280,6 +281,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly APIGATEWAY = new InterfaceVpcEndpointAwsService('execute-api'); public static readonly CODECOMMIT_GIT = new InterfaceVpcEndpointAwsService('git-codecommit'); public static readonly CODECOMMIT_GIT_FIPS = new InterfaceVpcEndpointAwsService('git-codecommit-fips'); + public static readonly GLUE = new InterfaceVpcEndpointAwsService('glue'); public static readonly KINESIS_STREAMS = new InterfaceVpcEndpointAwsService('kinesis-streams'); public static readonly KINESIS_FIREHOSE = new InterfaceVpcEndpointAwsService('kinesis-firehose'); public static readonly KMS = new InterfaceVpcEndpointAwsService('kms'); diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index a875368de30d9..bb73e8b467e48 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -206,6 +206,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.GatewayVpcEndpointAwsService.DYNAMODB", "docs-public-apis:@aws-cdk/aws-ec2.GatewayVpcEndpointAwsService.S3", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.APIGATEWAY", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.ATHENA", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.CLOUDFORMATION", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.CLOUDTRAIL", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH", @@ -230,6 +231,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM_FIPS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.ELASTIC_INFERENCE_RUNTIME", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.ELASTIC_LOAD_BALANCING", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.GLUE", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_STREAMS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_FIREHOSE", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KMS", diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 33fef16ac9865..7a80d93aad346 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -264,7 +264,8 @@ const queueProcessingEc2Service = new QueueProcessingEc2Service(stack, 'Service' TEST_ENVIRONMENT_VARIABLE2: "test environment variable 2 value" }, queue, - maxScalingCapacity: 5 + maxScalingCapacity: 5, + containerName: 'test', }); ``` @@ -283,7 +284,8 @@ const queueProcessingFargateService = new QueueProcessingFargateService(stack, ' TEST_ENVIRONMENT_VARIABLE2: "test environment variable 2 value" }, queue, - maxScalingCapacity: 5 + maxScalingCapacity: 5, + containerName: 'test', }); ``` @@ -302,7 +304,8 @@ const ecsScheduledTask = new ScheduledEc2Task(stack, 'ScheduledTask', { memoryLimitMiB: 256, environment: { name: 'TRIGGER', value: 'CloudWatch Events' }, }, - schedule: events.Schedule.expression('rate(1 minute)') + schedule: events.Schedule.expression('rate(1 minute)'), + ruleName: 'sample-scheduled-task-rule' }); ``` @@ -371,6 +374,25 @@ scalableTarget.scaleOnMemoryUtilization('MemoryScaling', { }); ``` +### Change the default Deployment Controller + +```ts +import { ApplicationLoadBalancedFargateService } from './application-load-balanced-fargate-service'; + +const loadBalancedFargateService = new ApplicationLoadBalancedFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + desiredCount: 1, + cpu: 512, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + }, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, +}); +``` + ### Set deployment configuration on QueueProcessingService ```ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index e9dd04e494b82..3bcade2f49a73 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -1,6 +1,6 @@ import { Certificate, CertificateValidation, ICertificate } from '@aws-cdk/aws-certificatemanager'; import { IVpc } from '@aws-cdk/aws-ec2'; -import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; +import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, IApplicationLoadBalancer, ListenerCertificate, ListenerAction, AddApplicationTargetsProps, @@ -212,6 +212,14 @@ export interface ApplicationLoadBalancedServiceBaseProps { * @default ApplicationLoadBalancedServiceRecordType.ALIAS */ readonly recordType?: ApplicationLoadBalancedServiceRecordType; + + /** + * Specifies which deployment controller to use for the service. For more information, see + * [Amazon ECS Deployment Types](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-types.html) + * + * @default - Rolling update (ECS) + */ + readonly deploymentController?: DeploymentController; } export interface ApplicationLoadBalancedTaskImageOptions { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index 5143cf18525fe..8329db85f2792 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -1,5 +1,5 @@ import { IVpc } from '@aws-cdk/aws-ec2'; -import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; +import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; import { INetworkLoadBalancer, NetworkListener, NetworkLoadBalancer, NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, CnameRecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; @@ -162,6 +162,14 @@ export interface NetworkLoadBalancedServiceBaseProps { * @default NetworkLoadBalancedServiceRecordType.ALIAS */ readonly recordType?: NetworkLoadBalancedServiceRecordType; + + /** + * Specifies which deployment controller to use for the service. For more information, see + * [Amazon ECS Deployment Types](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-types.html) + * + * @default - Rolling update (ECS) + */ + readonly deploymentController?: DeploymentController; } export interface NetworkLoadBalancedTaskImageOptions { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index 600432466fde1..66bdea9619629 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -1,6 +1,6 @@ import { ScalingInterval } from '@aws-cdk/aws-applicationautoscaling'; import { IVpc } from '@aws-cdk/aws-ec2'; -import { AwsLogDriver, BaseService, Cluster, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; +import { AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; import { IQueue, Queue } from '@aws-cdk/aws-sqs'; import { CfnOutput, Duration, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -170,6 +170,14 @@ export interface QueueProcessingServiceBaseProps { * @default - default from underlying service. */ readonly minHealthyPercent?: number; + + /** + * Specifies which deployment controller to use for the service. For more information, see + * [Amazon ECS Deployment Types](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-types.html) + * + * @default - Rolling update (ECS) + */ + readonly deploymentController?: DeploymentController; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts index 899ded10cbc41..259e375b1973c 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts @@ -38,6 +38,14 @@ export interface ScheduledTaskBaseProps { */ readonly schedule: Schedule; + /** + * A name for the rule. + * + * @default - AWS CloudFormation generates a unique physical ID and uses that ID + * for the rule name. For more information, see [Name Type](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html). + */ + readonly ruleName?: string; + /** * The desired number of instantiations of the task definition to keep running on the service. * @@ -139,6 +147,7 @@ export abstract class ScheduledTaskBase extends CoreConstruct { // An EventRule that describes the event trigger (in this case a scheduled run) this.eventRule = new Rule(this, 'ScheduledEventRule', { schedule: props.schedule, + ruleName: props.ruleName, }); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts index 142443de7cce2..9cec9e1d9e083 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts @@ -131,6 +131,7 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, cloudMapOptions: props.cloudMapOptions, + deploymentController: props.deploymentController, }); this.addServiceAsTarget(this.service); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts index 64c72e0e03c94..2d2c00fcf345e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts @@ -129,6 +129,7 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, cloudMapOptions: props.cloudMapOptions, + deploymentController: props.deploymentController, }); this.addServiceAsTarget(this.service); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index 0814ac40ba206..f9d9b98810aa0 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -52,6 +52,13 @@ export interface QueueProcessingEc2ServiceProps extends QueueProcessingServiceBa * @default - No memory reserved. */ readonly memoryReservationMiB?: number; + + /** + * Optional name for the container added + * + * @default - QueueProcessingContainer + */ + readonly containerName?: string; } /** @@ -74,11 +81,13 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { constructor(scope: Construct, id: string, props: QueueProcessingEc2ServiceProps) { super(scope, id, props); + const containerName = props.containerName ?? 'QueueProcessingContainer'; + // Create a Task Definition for the container to start this.taskDefinition = new Ec2TaskDefinition(this, 'QueueProcessingTaskDef', { family: props.family, }); - this.taskDefinition.addContainer('QueueProcessingContainer', { + this.taskDefinition.addContainer(containerName, { image: props.image, memoryLimitMiB: props.memoryLimitMiB, memoryReservationMiB: props.memoryReservationMiB, @@ -100,6 +109,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { maxHealthyPercent: props.maxHealthyPercent, propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, + deploymentController: props.deploymentController, }); this.configureAutoscalingForService(this.service); this.grantPermissionsToService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts index a8a43a6736326..00049e662ee0f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts @@ -166,6 +166,7 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc enableECSManagedTags: props.enableECSManagedTags, cloudMapOptions: props.cloudMapOptions, platformVersion: props.platformVersion, + deploymentController: props.deploymentController, securityGroups: props.securityGroups, vpcSubnets: props.taskSubnets, }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts index 6246cc3d23c45..10dcfebdc2f80 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts @@ -155,6 +155,7 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic enableECSManagedTags: props.enableECSManagedTags, cloudMapOptions: props.cloudMapOptions, platformVersion: props.platformVersion, + deploymentController: props.deploymentController, vpcSubnets: props.taskSubnets, }); this.addServiceAsTarget(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index 931fe00d9985b..68d0278c3e203 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -59,6 +59,13 @@ export interface QueueProcessingFargateServiceProps extends QueueProcessingServi * @default Latest */ readonly platformVersion?: FargatePlatformVersion; + + /** + * Optional name for the container added + * + * @default - QueueProcessingContainer + */ + readonly containerName?: string; } /** @@ -86,7 +93,10 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { cpu: props.cpu || 256, family: props.family, }); - this.taskDefinition.addContainer('QueueProcessingContainer', { + + const containerName = props.containerName ?? 'QueueProcessingContainer'; + + this.taskDefinition.addContainer(containerName, { image: props.image, command: props.command, environment: this.environment, @@ -106,6 +116,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, platformVersion: props.platformVersion, + deploymentController: props.deploymentController, }); this.configureAutoscalingForService(this.service); this.grantPermissionsToService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 16ace6a20c4a0..959a6348fde11 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -943,6 +943,64 @@ export = { test.done(); }, + 'ALB - having deployment controller'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + DeploymentController: { + Type: 'CODE_DEPLOY', + }, + })); + + test.done(); + }, + + 'NLB - having deployment controller'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + DeploymentController: { + Type: 'CODE_DEPLOY', + }, + })); + + test.done(); + }, + 'NetworkLoadbalancedEC2Service accepts previously created load balancer'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index 4bfa0732591cb..d449cc27db2c4 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts @@ -183,6 +183,9 @@ export = { maxHealthyPercent: 150, serviceName: 'ecs-test-service', family: 'ecs-task-family', + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, }); // THEN - QueueWorker is of EC2 launch type, an SQS queue is created and all optional properties are set. @@ -194,6 +197,9 @@ export = { }, LaunchType: 'EC2', ServiceName: 'ecs-test-service', + DeploymentController: { + Type: 'CODE_DEPLOY', + }, })); expect(stack).to(haveResource('AWS::SQS::Queue', { @@ -281,4 +287,31 @@ export = { test.done(); }, + + 'can set custom containerName'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.QueueProcessingEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + containerName: 'my-container', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Name: 'my-container', + }, + ], + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.scheduled-ecs-task.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.scheduled-ecs-task.ts index f4a0abcf1e410..d96c8cd2edc7d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.scheduled-ecs-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.scheduled-ecs-task.ts @@ -86,10 +86,12 @@ export = { }, desiredTaskCount: 2, schedule: events.Schedule.expression('rate(1 minute)'), + ruleName: 'sample-scheduled-task-rule', }); // THEN expect(stack).to(haveResource('AWS::Events::Rule', { + Name: 'sample-scheduled-task-rule', Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts index 84ee226b7e805..1feffcb2e70fd 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts @@ -383,6 +383,50 @@ export = { test.done(); }, + 'setting ALB deployment controller'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', { + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + }, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + DeploymentController: { + Type: 'CODE_DEPLOY', + }, + })); + test.done(); + }, + + 'setting NLB deployment controller'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecsPatterns.NetworkLoadBalancedFargateService(stack, 'Service', { + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + }, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + DeploymentController: { + Type: 'CODE_DEPLOY', + }, + })); + test.done(); + }, + 'setting NLB special listener port to create the listener'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts index 27cb8a08ed14a..a0c09d572343f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts @@ -228,6 +228,9 @@ export = { serviceName: 'fargate-test-service', family: 'fargate-task-family', platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, }); // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all optional properties are set. @@ -240,6 +243,9 @@ export = { LaunchType: 'FARGATE', ServiceName: 'fargate-test-service', PlatformVersion: ecs.FargatePlatformVersion.VERSION1_4, + DeploymentController: { + Type: 'CODE_DEPLOY', + }, })); expect(stack).to(haveResource('AWS::SQS::Queue', { QueueName: 'fargate-test-sqs-queue' })); @@ -279,4 +285,29 @@ export = { test.done(); }, + + 'can set custom containerName'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.QueueProcessingFargateService(stack, 'Service', { + cluster, + containerName: 'my-container', + image: ecs.ContainerImage.fromRegistry('test'), + }); + + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Name: 'my-container', + }, + ], + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts index dea31584246c8..27367249ee3cb 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts @@ -98,10 +98,12 @@ export = { }, desiredTaskCount: 2, schedule: events.Schedule.expression('rate(1 minute)'), + ruleName: 'sample-scheduled-task-rule', }); // THEN expect(stack).to(haveResource('AWS::Events::Rule', { + Name: 'sample-scheduled-task-rule', Targets: [ { Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index a5f2b09e8e92b..f2056229c6bf5 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -335,6 +335,21 @@ const service = new ecs.FargateService(this, 'Service', { `Services` by default will create a security group if not provided. If you'd like to specify which security groups to use you can override the `securityGroups` property. +### Deployment circuit breaker and rollback + +Amazon ECS [deployment circuit breaker](https://aws.amazon.com/tw/blogs/containers/announcing-amazon-ecs-deployment-circuit-breaker/) +automatically rolls back unhealthy service deployments without the need for manual intervention. Use `circuitBreaker` to enable +deployment circuit breaker and optionally enable `rollback` for automatic rollback. See [Using the deployment circuit breaker](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html) +for more details. + +```ts +const service = new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition, + circuitBreaker: { rollback: true }, +}); +``` + ### Include an application/network load balancer `Services` are load balancing targets and can be added to a target group, which will be attached to an application/network load balancers: diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 535fb240217ec..f6f725756167d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -44,6 +44,18 @@ export interface DeploymentController { readonly type?: DeploymentControllerType; } +/** + * The deployment circuit breaker to use for the service + */ +export interface DeploymentCircuitBreaker { + /** + * Whether to enable rollback on deployment failure + * @default false + */ + readonly rollback?: boolean; + +} + export interface EcsTarget { /** * The name of the container. @@ -161,6 +173,13 @@ export interface BaseServiceOptions { * @default - Rolling update (ECS) */ readonly deploymentController?: DeploymentController; + + /** + * Whether to enable the deployment circuit breaker. If this property is defined, circuit breaker will be implicitly + * enabled. + * @default - disabled + */ + readonly circuitBreaker?: DeploymentCircuitBreaker; } /** @@ -356,6 +375,16 @@ export abstract class BaseService extends Resource ...additionalProps, }); + if (props.circuitBreaker) { + const deploymentConfiguration = { + DeploymentCircuitBreaker: { + Enable: true, + Rollback: props.circuitBreaker.rollback ?? false, + }, + }; + // TODO: fix this when this property is available in CfnService + this.resource.addPropertyOverride('DeploymentConfiguration', deploymentConfiguration); + }; if (props.deploymentController?.type === DeploymentControllerType.EXTERNAL) { Annotations.of(this).addWarning('taskDefinition and launchType are blanked out when using external deployment controller.'); } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json index 24784b1521a45..0c64ae63155b9 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json @@ -437,8 +437,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -448,7 +446,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -636,8 +636,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -647,7 +645,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -874,8 +874,8 @@ ], "Family": "awsecsintegTaskDef6FDFB69A", "IpcMode": "host", - "PidMode": "task", "NetworkMode": "awsvpc", + "PidMode": "task", "ProxyConfiguration": { "ContainerName": "envoy", "ProxyConfigurationProperties": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json index 80111c4451470..62d48e3bced1e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json @@ -458,8 +458,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -469,7 +467,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -657,8 +657,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -668,7 +666,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -895,8 +895,8 @@ ], "Family": "awsecsintegTaskDef6FDFB69A", "IpcMode": "host", - "PidMode": "task", "NetworkMode": "host", + "PidMode": "task", "RequiresCompatibilities": [ "EC2" ], diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json index 0c611b4879869..c2a555e9c9a71 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.environment-file.expected.json @@ -991,6 +991,50 @@ } } }, + "EnvFileDeploymentAwsCliLayerA8FC897D": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "/opt/awscli/aws" + } + }, "EnvFileDeploymentCustomResourceDBE78DE4": { "Type": "Custom::CDKBucketDeployment", "Properties": { @@ -1175,7 +1219,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3Bucket77147E20" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3Bucket28CE5152" }, "S3Key": { "Fn::Join": [ @@ -1188,7 +1232,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED" } ] } @@ -1201,7 +1245,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED" } ] } @@ -1219,6 +1263,11 @@ ] }, "Runtime": "python3.6", + "Layers": [ + { + "Ref": "EnvFileDeploymentAwsCliLayerA8FC897D" + } + ], "Timeout": 900 }, "DependsOn": [ @@ -1288,17 +1337,29 @@ "Type": "AWS::SSM::Parameter::Value", "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" }, - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3Bucket77147E20": { + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": { + "Type": "String", + "Description": "S3 bucket for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F": { + "Type": "String", + "Description": "S3 key for asset version \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68ArtifactHashD9A515C3": { + "Type": "String", + "Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3Bucket28CE5152": { "Type": "String", - "Description": "S3 bucket for asset \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "S3 bucket for asset \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F": { + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED": { "Type": "String", - "Description": "S3 key for asset version \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "S3 key for asset version \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176ArtifactHash4E343C6C": { + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7ArtifactHash8926088E": { "Type": "String", - "Description": "Artifact hash for asset \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "Artifact hash for asset \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, "AssetParameters972240f9dd6e036a93d5f081af9a24315b2053828ac049b3b19b2fa12d7ae64aS3Bucket1F1A8472": { "Type": "String", diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json index ba3354e59a7ef..bd368de2a0f11 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json @@ -437,8 +437,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -448,7 +446,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -636,8 +636,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -647,7 +645,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json index 743c08d9f537a..0d3144db3f8f1 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json @@ -437,8 +437,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -448,7 +446,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -636,8 +636,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -647,7 +645,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index 3cfe5df21405a..a66e3674dd919 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -458,8 +458,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -469,7 +467,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -657,8 +657,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -668,7 +666,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json index 66d961bceb04b..e8638efad5f38 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json @@ -437,8 +437,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -448,7 +446,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -636,8 +636,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -647,7 +645,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json index 2214c69632d98..7d0f3770d69ce 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json @@ -437,8 +437,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -448,7 +446,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -636,8 +636,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -647,7 +645,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json index f214a22fea2cb..6fe13e7abc27e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json @@ -116,4 +116,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json index 3c78f85f86425..aaa968ada846f 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json @@ -437,8 +437,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -448,7 +446,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -638,8 +638,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -649,7 +647,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -919,8 +919,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -930,7 +928,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -1119,8 +1119,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -1130,7 +1128,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 7b8dc6975bae1..546844c604eac 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -212,6 +212,7 @@ export = { deploymentController: { type: ecs.DeploymentControllerType.CODE_DEPLOY, }, + circuitBreaker: { rollback: true }, securityGroup: new ec2.SecurityGroup(stack, 'SecurityGroup1', { allowAllOutbound: true, description: 'Example', @@ -235,6 +236,10 @@ export = { DeploymentConfiguration: { MaximumPercent: 150, MinimumHealthyPercent: 55, + DeploymentCircuitBreaker: { + Enable: true, + Rollback: true, + }, }, DeploymentController: { Type: ecs.DeploymentControllerType.CODE_DEPLOY, @@ -1786,6 +1791,38 @@ export = { test.done(); }, + 'with circuit breaker'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const cluster = new ecs.Cluster(stack, 'EcsCluster'); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + taskDefinition.addContainer('Container', { + image: ecs.ContainerImage.fromRegistry('hello'), + }); + + // WHEN + new ecs.FargateService(stack, 'EcsService', { + cluster, + taskDefinition, + circuitBreaker: { rollback: true }, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + DeploymentConfiguration: { + MaximumPercent: 200, + MinimumHealthyPercent: 50, + DeploymentCircuitBreaker: { + Enable: true, + Rollback: true, + }, + }, + })); + + test.done(); + }, + 'throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 903636c5d4e89..0b92c183fb786 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -201,7 +201,7 @@ const cluster = new eks.Cluster(this, 'HelloEKS', { }); cluster.addNodegroupCapacity('custom-node-group', { - instanceType: new ec2.InstanceType('m5.large'), + instanceTypes: [new ec2.InstanceType('m5.large')], minSize: 4, diskSize: 100, amiType: eks.NodegroupAmiType.AL2_X86_64_GPU, @@ -209,6 +209,27 @@ cluster.addNodegroupCapacity('custom-node-group', { }); ``` +#### Spot Instances Support + +Use `capacityType` to create managed node groups comprised of spot instances. To maximize the availability of your applications while using +Spot Instances, we recommend that you configure a Spot managed node group to use multiple instance types with the `instanceTypes` property. + +> For more details visit [Managed node group capacity types](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html#managed-node-group-capacity-types). + + +```ts +cluster.addNodegroupCapacity('extra-ng-spot', { + instanceTypes: [ + new ec2.InstanceType('c5.large'), + new ec2.InstanceType('c5a.large'), + new ec2.InstanceType('c5d.large'), + ], + minSize: 3, + capacityType: eks.CapacityType.SPOT, +}); + +``` + #### Launch Template Support You can specify a launch template that the node group will use. Note that when using a custom AMI, Amazon EKS doesn't merge any user data. @@ -236,7 +257,9 @@ cluster.addNodegroupCapacity('extra-ng', { }); ``` -> For more details visit [Launch Template Support](https://docs.aws.amazon.com/en_ca/eks/latest/userguide/launch-templates.html). +You may specify one or instance types in either the `instanceTypes` property of `NodeGroup` or in the launch template, **but not both**. + +> For more details visit [Launch Template Support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html). Graviton 2 instance types are supported including `c6g`, `m6g`, `r6g` and `t4g`. @@ -304,6 +327,19 @@ cluster.addAutoScalingGroupCapacity('frontend-nodes', { }); ``` +To connect an already initialized auto-scaling group, use the `cluster.connectAutoScalingGroupCapacity()` method: + +```ts +const asg = new ec2.AutoScalingGroup(...); +cluster.connectAutoScalingGroupCapacity(asg); +``` + +In both cases, the [cluster security group](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html#cluster-sg) will be autoamtically attached to +the auto-scaling group, allowing for traffic to flow freely between managed and self-managed nodes. + +> **Note:** The default `updateType` for auto-scaling groups does not replace existing nodes. Since security groups are determined at launch time, self-managed nodes that were provisioned with version `1.78.0` or lower, will not be updated. +> To apply the new configuration on all your self-managed nodes, you'll need to replace the nodes using the `UpdateType.REPLACING_UPDATE` policy for the [`updateType`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-autoscaling.AutoScalingGroup.html#updatetypespan-classapi-icon-api-icon-deprecated-titlethis-api-element-is-deprecated-its-use-is-not-recommended%EF%B8%8Fspan) property. + You can customize the [/etc/eks/boostrap.sh](https://github.com/awslabs/amazon-eks-ami/blob/master/files/bootstrap.sh) script, which is responsible for bootstrapping the node to the EKS cluster. For example, you can use `kubeletExtraArgs` to add custom node labels or taints. @@ -420,6 +456,8 @@ new eks.Cluster(this, 'HelloEKS', { }); ``` +> Note: Isolated VPCs (i.e with no internet access) are not currently supported. See https://github.com/aws/aws-cdk/issues/12171 + If you do not specify a VPC, one will be created on your behalf, which you can then access via `cluster.vpc`. The cluster VPC will be associated to any EKS managed capacity (i.e Managed Node Groups and Fargate Profiles). If you allocate self managed capacity, you can specify which subnets should the auto-scaling group use: @@ -431,8 +469,7 @@ cluster.addAutoScalingGroupCapacity('nodes', { }); ``` -In addition to the cluster and the capacity, there are two additional components you might want to -provision within a VPC. +There are two additional components you might want to provision within the VPC. #### Kubectl Handler @@ -446,7 +483,18 @@ If the endpoint does not expose private access (via `EndpointAccess.PUBLIC`) **o #### Cluster Handler -The `ClusterHandler` is a Lambda function responsible to interact the EKS API in order to control the cluster lifecycle. At the moment, this function cannot be provisioned inside the VPC. See [Attach all Lambda Function to a VPC](https://github.com/aws/aws-cdk/issues/9509) for more details. +The `ClusterHandler` is a Lambda function responsible to interact with the EKS API in order to control the cluster lifecycle. To provision this function inside the VPC, set the `placeClusterHandlerInVpc` property to `true`. This will place the function inside the private subnets of the VPC based on the selection strategy specified in the [`vpcSubnets`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-eks.Cluster.html#vpcsubnetsspan-classapi-icon-api-icon-experimental-titlethis-api-element-is-experimental-it-may-change-without-noticespan) property. + +You can configure the environment of this function by specifying it at cluster instantiation. For example, this can be useful in order to configure an http proxy: + +```ts +const cluster = new eks.Cluster(this, 'hello-eks', { + version: eks.KubernetesVersion.V1_18, + clusterHandlerEnvironment: { + 'http_proxy': 'http://proxy.myproxy.com' + } +}); +``` ### Kubectl Support @@ -467,26 +515,32 @@ const cluster = new eks.Cluster(this, 'hello-eks', { #### Runtime -By default, the `kubectl`, `helm` and `aws` commands used to operate the cluster are provided by an AWS Lambda Layer from the AWS Serverless Application in [aws-lambda-layer-kubectl](https://github.com/aws-samples/aws-lambda-layer-kubectl). In most cases this should be sufficient. +The kubectl handler uses `kubectl`, `helm` and the `aws` CLI in order to +interact with the cluster. These are bundled into AWS Lambda layers included in +the `@aws-cdk/lambda-layer-awscli` and `@aws-cdk/lambda-layer-kubectl` modules. + +You can specify a custom `lambda.LayerVersion` if you wish to use a different +version of these tools. The handler expects the layer to include the following +three executables: + +```text +helm/helm +kubectl/kubectl +awscli/aws +``` -You can provide a custom layer in case the default layer does not meet your -needs or if the SAR app is not available in your region. +See more information in the +[Dockerfile](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/lambda-layer-awscli/layer) for @aws-cdk/lambda-layer-awscli +and the +[Dockerfile](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/lambda-layer-kubectl/layer) for @aws-cdk/lambda-layer-kubectl. ```ts -// custom build: const layer = new lambda.LayerVersion(this, 'KubectlLayer', { - code: lambda.Code.fromAsset(`${__dirname}/layer.zip`)), - compatibleRuntimes: [lambda.Runtime.PROVIDED] -}); - -// or, a specific version or appid of aws-lambda-layer-kubectl: -const layer = new eks.KubectlLayer(this, 'KubectlLayer', { - version: '2.0.0', // optional - applicationId: '...' // optional + code: lambda.Code.fromAsset('layer.zip'), }); ``` -Pass it to `kubectlLayer` when you create or import a cluster: +Now specify when the cluster is defined: ```ts const cluster = new eks.Cluster(this, 'MyCluster', { @@ -499,9 +553,6 @@ const cluster = eks.Cluster.fromClusterAttributes(this, 'MyCluster', { }); ``` -> Instructions on how to build `layer.zip` can be found -> [here](https://github.com/aws-samples/aws-lambda-layer-kubectl/blob/master/cdk/README.md). - #### Memory By default, the kubectl provider is configured with 1024MiB of memory. You can use the `kubectlMemory` option to specify the memory size for the AWS Lambda function: @@ -527,7 +578,7 @@ Amazon Linux 2 AMI for ARM64 will be automatically selected. ```ts // add a managed ARM64 nodegroup cluster.addNodegroupCapacity('extra-ng-arm', { - instanceType: new ec2.InstanceType('m6g.medium'), + instanceTypes: [new ec2.InstanceType('m6g.medium')], minSize: 2, }); @@ -831,6 +882,19 @@ new Cluster(this, 'MyCluster', { }); ``` +#### Manifests Validation + +The `kubectl` CLI supports applying a manifest by skipping the validation. +This can be accomplished by setting the `skipValidation` flag to `true` in the `KubernetesManifest` props. + +```ts +new eks.KubernetesManifest(this, 'HelloAppWithoutValidation', { + cluster: this.cluster, + manifest: [ deployment, service ], + skipValidation: true, +}); +``` + ### Helm Charts The `HelmChart` construct or `cluster.addHelmChart` method can be used @@ -1109,6 +1173,5 @@ Kubernetes [endpoint access](#endpoint-access), you must also specify: ## Known Issues and Limitations * [One cluster per stack](https://github.com/aws/aws-cdk/issues/10073) -* [Object pruning](https://github.com/aws/aws-cdk/issues/10495) * [Service Account dependencies](https://github.com/aws/aws-cdk/issues/9910) -* [Attach all Lambda Functions to VPC](https://github.com/aws/aws-cdk/issues/9509) +* [Support isolated VPCs](https://github.com/aws/aws-cdk/issues/12171) diff --git a/packages/@aws-cdk/aws-eks/lib/aws-auth.ts b/packages/@aws-cdk/aws-eks/lib/aws-auth.ts index 02f2b321933f7..c1c405bf2dc29 100644 --- a/packages/@aws-cdk/aws-eks/lib/aws-auth.ts +++ b/packages/@aws-cdk/aws-eks/lib/aws-auth.ts @@ -39,6 +39,7 @@ export class AwsAuth extends CoreConstruct { new KubernetesManifest(this, 'manifest', { cluster: props.cluster, + overwrite: true, // this config map is auto-created by the cluster manifest: [ { apiVersion: 'v1', diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-provider.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-provider.ts index fa5da2d80807e..12839a3ee6044 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-provider.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-provider.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, NestedStack, Stack } from '@aws-cdk/core'; @@ -17,6 +18,21 @@ export interface ClusterResourceProviderProps { * The IAM role to assume in order to interact with the cluster. */ readonly adminRole: iam.IRole; + + /** + * The VPC to provision the functions in. + */ + readonly vpc?: ec2.IVpc; + + /** + * The subnets to place the functions in. + */ + readonly subnets?: ec2.ISubnet[]; + + /** + * Environment to add to the handler. + */ + readonly environment?: { [key: string]: string }; } /** @@ -46,8 +62,11 @@ export class ClusterResourceProvider extends NestedStack { code: lambda.Code.fromAsset(HANDLER_DIR), description: 'onEvent handler for EKS cluster resource provider', runtime: HANDLER_RUNTIME, + environment: props.environment, handler: 'index.onEvent', timeout: Duration.minutes(1), + vpc: props.subnets ? props.vpc : undefined, + vpcSubnets: props.subnets ? { subnets: props.subnets } : undefined, }); const isComplete = new lambda.Function(this, 'IsCompleteHandler', { @@ -56,6 +75,8 @@ export class ClusterResourceProvider extends NestedStack { runtime: HANDLER_RUNTIME, handler: 'index.isComplete', timeout: Duration.minutes(1), + vpc: props.subnets ? props.vpc : undefined, + vpcSubnets: props.subnets ? { subnets: props.subnets } : undefined, }); this.provider = new cr.Provider(this, 'Provider', { @@ -63,6 +84,8 @@ export class ClusterResourceProvider extends NestedStack { isCompleteHandler: isComplete, totalTimeout: Duration.hours(1), queryInterval: Duration.minutes(1), + vpc: props.subnets ? props.vpc : undefined, + vpcSubnets: props.subnets ? { subnets: props.subnets } : undefined, }); props.adminRole.grant(onEvent.role!, 'sts:AssumeRole'); diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index 788210c987dbe..5d89741026aa9 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -21,6 +21,8 @@ export interface ClusterResourceProps { readonly endpointPublicAccess: boolean; readonly publicAccessCidrs?: string[]; readonly vpc: ec2.IVpc; + readonly environment?: { [key: string]: string }; + readonly subnets?: ec2.ISubnet[]; readonly secretsEncryptionKey?: kms.IKey; } @@ -57,6 +59,9 @@ export class ClusterResource extends CoreConstruct { const provider = ClusterResourceProvider.getOrCreate(this, { adminRole: this.adminRole, + subnets: props.subnets, + vpc: props.vpc, + environment: props.environment, }); const resource = new CustomResource(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index fcbc2aaffdf10..07ed5aa60ce32 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -67,11 +67,17 @@ export interface ICluster extends IResource, ec2.IConnectable { readonly clusterCertificateAuthorityData: string; /** - * The cluster security group that was created by Amazon EKS for the cluster. + * The id of the cluster security group that was created by Amazon EKS for the cluster. * @attribute */ readonly clusterSecurityGroupId: string; + /** + * The cluster security group that was created by Amazon EKS for the cluster. + * @attribute + */ + readonly clusterSecurityGroup: ec2.ISecurityGroup; + /** * Amazon Resource Name (ARN) or alias of the customer master key (CMK). * @attribute @@ -255,32 +261,16 @@ export interface ClusterAttributes { readonly openIdConnectProvider?: iam.IOpenIdConnectProvider; /** - * An AWS Lambda Layer which includes `kubectl`, Helm and the AWS CLI. + * An AWS Lambda Layer which includes `kubectl`, Helm and the AWS CLI. This layer + * is used by the kubectl handler to apply manifests and install helm charts. * - * By default, the provider will use the layer included in the - * "aws-lambda-layer-kubectl" SAR application which is available in all - * commercial regions. + * The handler expects the layer to include the following executables: * - * To deploy the layer locally, visit - * https://github.com/aws-samples/aws-lambda-layer-kubectl/blob/master/cdk/README.md - * for instructions on how to prepare the .zip file and then define it in your - * app as follows: + * helm/helm + * kubectl/kubectl + * awscli/aws * - * ```ts - * const layer = new lambda.LayerVersion(this, 'kubectl-layer', { - * code: lambda.Code.fromAsset(`${__dirname}/layer.zip`)), - * compatibleRuntimes: [lambda.Runtime.PROVIDED] - * }); - * - * Or you can use the standard layer like this (with options - * to customize the version and SAR application ID): - * - * ```ts - * const layer = new eks.KubectlLayer(this, 'KubectlLayer'); - * ``` - * - * @default - the layer provided by the `aws-lambda-layer-kubectl` SAR app. - * @see https://github.com/aws-samples/aws-lambda-layer-kubectl + * @default - a layer bundled with this module. */ readonly kubectlLayer?: lambda.ILayerVersion; @@ -422,6 +412,13 @@ export interface ClusterOptions extends CommonClusterOptions { */ readonly kubectlEnvironment?: { [key: string]: string }; + /** + * Custom environment variables when interacting with the EKS endpoint to manage the cluster lifecycle. + * + * @default - No environment variables. + */ + readonly clusterHandlerEnvironment?: { [key: string]: string }; + /** * An AWS Lambda Layer which includes `kubectl`, Helm and the AWS CLI. * @@ -462,6 +459,14 @@ export interface ClusterOptions extends CommonClusterOptions { * @default true */ readonly prune?: boolean; + + /** + * If set to true, the cluster handler functions will be placed in the private subnets + * of the cluster vpc, subject to the `vpcSubnets` selection strategy. + * + * @default false + */ + readonly placeClusterHandlerInVpc?: boolean; } /** @@ -562,28 +567,6 @@ export class EndpointAccess { * Common configuration props for EKS clusters. */ export interface ClusterProps extends ClusterOptions { - /** - * NOT SUPPORTED: We no longer allow disabling kubectl-support. Setting this - * option to `false` will throw an error. - * - * To temporary allow you to retain existing clusters created with - * `kubectlEnabled: false`, you can use `eks.LegacyCluster` class, which is a - * drop-in replacement for `eks.Cluster` with `kubectlEnabled: false`. - * - * Bear in mind that this is a temporary workaround. We have plans to remove - * `eks.LegacyCluster`. If you have a use case for using `eks.LegacyCluster`, - * please add a comment here https://github.com/aws/aws-cdk/issues/9332 and - * let us know so we can make sure to continue to support your use case with - * `eks.Cluster`. This issue also includes additional context into why this - * class is being removed. - * - * @deprecated `eks.LegacyCluster` is __temporarily__ provided as a drop-in - * replacement until you are able to migrate to `eks.Cluster`. - * - * @see https://github.com/aws/aws-cdk/issues/9332 - * @default true - */ - readonly kubectlEnabled?: boolean; /** * Number of instances to allocate as an initial capacity for this cluster. @@ -671,6 +654,7 @@ abstract class ClusterBase extends Resource implements ICluster { public abstract readonly clusterEndpoint: string; public abstract readonly clusterCertificateAuthorityData: string; public abstract readonly clusterSecurityGroupId: string; + public abstract readonly clusterSecurityGroup: ec2.ISecurityGroup; public abstract readonly clusterEncryptionConfigKeyArn: string; public abstract readonly kubectlRole?: iam.IRole; public abstract readonly kubectlEnvironment?: { [key: string]: string }; @@ -802,10 +786,15 @@ export class Cluster extends ClusterBase { public readonly clusterCertificateAuthorityData: string; /** - * The cluster security group that was created by Amazon EKS for the cluster. + * The id of the cluster security group that was created by Amazon EKS for the cluster. */ public readonly clusterSecurityGroupId: string; + /** + * The cluster security group that was created by Amazon EKS for the cluster. + */ + public readonly clusterSecurityGroup: ec2.ISecurityGroup; + /** * Amazon Resource Name (ARN) or alias of the customer master key (CMK). */ @@ -847,7 +836,6 @@ export class Cluster extends ClusterBase { /** * Custom environment variables when running `kubectl` against this cluster. - * @default - no additional environment variables */ public readonly kubectlEnvironment?: { [key: string]: string }; @@ -950,14 +938,6 @@ export class Cluster extends ClusterBase { physicalName: props.clusterName, }); - if (props.kubectlEnabled === false) { - throw new Error( - 'The "eks.Cluster" class no longer allows disabling kubectl support. ' + - 'As a temporary workaround, you can use the drop-in replacement class `eks.LegacyCluster`, ' + - 'but bear in mind that this class will soon be removed and will no longer receive additional ' + - 'features or bugfixes. See https://github.com/aws/aws-cdk/issues/9332 for more details'); - } - const stack = Stack.of(this); this.prune = props.prune ?? true; @@ -1008,8 +988,15 @@ export class Cluster extends ClusterBase { throw new Error('Vpc must contain private subnets when public endpoint access is restricted'); } + const placeClusterHandlerInVpc = props.placeClusterHandlerInVpc ?? false; + + if (placeClusterHandlerInVpc && privateSubents.length === 0) { + throw new Error('Cannot place cluster handler in the VPC since no private subnets could be selected'); + } + const resource = this._clusterResource = new ClusterResource(this, 'Resource', { name: this.physicalName, + environment: props.clusterHandlerEnvironment, roleArn: this.role.roleArn, version: props.version.version, resourcesVpcConfig: { @@ -1029,6 +1016,7 @@ export class Cluster extends ClusterBase { publicAccessCidrs: this.endpointAccess._config.publicCidrs, secretsEncryptionKey: props.secretsEncryptionKey, vpc: this.vpc, + subnets: placeClusterHandlerInVpc ? privateSubents : undefined, }); if (this.endpointAccess._config.privateAccess && privateSubents.length !== 0) { @@ -1070,16 +1058,16 @@ export class Cluster extends ClusterBase { this.clusterSecurityGroupId = resource.attrClusterSecurityGroupId; this.clusterEncryptionConfigKeyArn = resource.attrEncryptionConfigKeyArn; - const clusterSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(this, 'ClusterSecurityGroup', this.clusterSecurityGroupId); + this.clusterSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(this, 'ClusterSecurityGroup', this.clusterSecurityGroupId); this.connections = new ec2.Connections({ - securityGroups: [clusterSecurityGroup, securityGroup], + securityGroups: [this.clusterSecurityGroup, securityGroup], defaultPort: ec2.Port.tcp(443), // Control Plane has an HTTPS API }); // we can use the cluster security group since its already attached to the cluster // and configured to allow connections from itself. - this.kubectlSecurityGroup = clusterSecurityGroup; + this.kubectlSecurityGroup = this.clusterSecurityGroup; // use the cluster creation role to issue kubectl commands against the cluster because when the // cluster is first created, that's the only role that has "system:masters" permissions @@ -1119,7 +1107,7 @@ export class Cluster extends ClusterBase { this.addAutoScalingGroupCapacity('DefaultCapacity', { instanceType, minCapacity }) : undefined; this.defaultNodegroup = props.defaultCapacityType !== DefaultCapacityType.EC2 ? - this.addNodegroupCapacity('DefaultCapacity', { instanceType, minSize: minCapacity }) : undefined; + this.addNodegroupCapacity('DefaultCapacity', { instanceTypes: [instanceType], minSize: minCapacity }) : undefined; } const outputConfigCommand = props.outputConfigCommand === undefined ? true : props.outputConfigCommand; @@ -1254,6 +1242,9 @@ export class Cluster extends ClusterBase { autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allUdp()); autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allIcmp()); + // allow traffic to/from managed node groups (eks attaches this security group to the managed nodes) + autoScalingGroup.addSecurityGroup(this.clusterSecurityGroup); + const bootstrapEnabled = options.bootstrapEnabled !== undefined ? options.bootstrapEnabled : true; if (options.bootstrapOptions && !bootstrapEnabled) { throw new Error('Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false'); @@ -1449,7 +1440,7 @@ export class Cluster extends ClusterBase { if (!this._spotInterruptHandler) { this._spotInterruptHandler = this.addHelmChart('spot-interrupt-handler', { chart: 'aws-node-termination-handler', - version: '0.9.5', + version: '0.13.2', repository: 'https://aws.github.io/eks-charts', namespace: 'kube-system', values: { @@ -1693,6 +1684,10 @@ class ImportedCluster extends ClusterBase { public readonly kubectlMemory?: Size; public readonly prune: boolean; + // so that `clusterSecurityGroup` on `ICluster` can be configured without optionality, avoiding users from having + // to null check on an instance of `Cluster`, which will always have this configured. + private readonly _clusterSecurityGroup?: ec2.ISecurityGroup; + constructor(scope: Construct, id: string, private readonly props: ClusterAttributes) { super(scope, id); @@ -1713,7 +1708,8 @@ class ImportedCluster extends ClusterBase { } if (props.clusterSecurityGroupId) { - this.connections.addSecurityGroup(ec2.SecurityGroup.fromSecurityGroupId(this, 'ClusterSecurityGroup', props.clusterSecurityGroupId)); + this._clusterSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(this, 'ClusterSecurityGroup', this.clusterSecurityGroupId); + this.connections.addSecurityGroup(this._clusterSecurityGroup); } } @@ -1724,6 +1720,13 @@ class ImportedCluster extends ClusterBase { return this.props.vpc; } + public get clusterSecurityGroup(): ec2.ISecurityGroup { + if (!this._clusterSecurityGroup) { + throw new Error('"clusterSecurityGroup" is not defined for this imported cluster'); + } + return this._clusterSecurityGroup; + } + public get clusterSecurityGroupId(): string { if (!this.props.clusterSecurityGroupId) { throw new Error('"clusterSecurityGroupId" is not defined for this imported cluster'); @@ -1917,4 +1920,3 @@ function cpuArchForInstanceType(instanceType: ec2.InstanceType) { INSTANCE_TYPES.graviton.includes(instanceType.toString().substring(0, 2)) ? CpuArch.ARM_64 : CpuArch.X86_64; } - diff --git a/packages/@aws-cdk/aws-eks/lib/fargate-cluster.ts b/packages/@aws-cdk/aws-eks/lib/fargate-cluster.ts index 60265a67c0eee..11036875a9d30 100644 --- a/packages/@aws-cdk/aws-eks/lib/fargate-cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/fargate-cluster.ts @@ -27,7 +27,6 @@ export class FargateCluster extends Cluster { super(scope, id, { ...props, defaultCapacity: 0, - kubectlEnabled: true, coreDnsComputeType: props.coreDnsComputeType ?? CoreDnsComputeType.FARGATE, version: props.version, }); diff --git a/packages/@aws-cdk/aws-eks/lib/index.ts b/packages/@aws-cdk/aws-eks/lib/index.ts index 633b51cc9ca30..454242cf5259f 100644 --- a/packages/@aws-cdk/aws-eks/lib/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/index.ts @@ -1,7 +1,6 @@ export * from './aws-auth'; export * from './aws-auth-mapping'; export * from './cluster'; -export * from './legacy-cluster'; export * from './eks.generated'; export * from './fargate-profile'; export * from './helm-chart'; @@ -11,5 +10,4 @@ export * from './k8s-object-value'; export * from './fargate-cluster'; export * from './service-account'; export * from './managed-nodegroup'; -export * from './kubectl-layer'; export * from './oidc-provider'; diff --git a/packages/@aws-cdk/aws-eks/lib/k8s-manifest.ts b/packages/@aws-cdk/aws-eks/lib/k8s-manifest.ts index 09bd952f2e64a..205c28a9ee647 100644 --- a/packages/@aws-cdk/aws-eks/lib/k8s-manifest.ts +++ b/packages/@aws-cdk/aws-eks/lib/k8s-manifest.ts @@ -37,6 +37,13 @@ export interface KubernetesManifestOptions { * otherwise specified. */ readonly prune?: boolean; + + /** + * A flag to signify if the manifest validation should be skipped + * + * @default false + */ + readonly skipValidation?: boolean; } /** @@ -72,6 +79,17 @@ export interface KubernetesManifestProps extends KubernetesManifestOptions { * */ readonly manifest: Record[]; + + /** + * Overwrite any existing resources. + * + * If this is set, we will use `kubectl apply` instead of `kubectl create` + * when the resource is created. Otherwise, if there is already a resource + * in the cluster with the same name, the operation will fail. + * + * @default false + */ + readonly overwrite?: boolean; } /** @@ -110,6 +128,8 @@ export class KubernetesManifest extends CoreConstruct { ClusterName: props.cluster.clusterName, RoleArn: provider.roleArn, // TODO: bake into provider's environment PruneLabel: pruneLabel, + Overwrite: props.overwrite, + SkipValidation: props.skipValidation, }, }); } diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/apply/__init__.py b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/apply/__init__.py index 6b69534d0e363..80e9a7891481e 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-handler/apply/__init__.py +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-handler/apply/__init__.py @@ -23,13 +23,17 @@ def apply_handler(event, context): manifest_text = props['Manifest'] role_arn = props['RoleArn'] prune_label = props.get('PruneLabel', None) + overwrite = props.get('Overwrite', 'false').lower() == 'true' + skip_validation = props.get('SkipValidation', 'false').lower() == 'true' # "log in" to the cluster - subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig', + cmd = [ 'aws', 'eks', 'update-kubeconfig', '--role-arn', role_arn, '--name', cluster_name, '--kubeconfig', kubeconfig - ]) + ] + logger.info(f'Running command: {cmd}') + subprocess.check_call(cmd) # write resource manifests in sequence: { r1 }{ r2 }{ r3 } (this is how # a stream of JSON objects can be included in a k8s manifest). @@ -40,14 +44,25 @@ def apply_handler(event, context): logger.info("manifest written to: %s" % manifest_file) + kubectl_opts = [] + if skip_validation: + kubectl_opts.extend(['--validate=false']) + if request_type == 'Create': - # --save-config will allow us to use "apply" later - kubectl('create', manifest_file, '--save-config') + # if "overwrite" is enabled, then we use "apply" for CREATE operations + # which technically means we can determine the desired state of an + # existing resource. + if overwrite: + kubectl('apply', manifest_file, *kubectl_opts) + else: + # --save-config will allow us to use "apply" later + kubectl_opts.extend(['--save-config']) + kubectl('create', manifest_file, *kubectl_opts) elif request_type == 'Update': - opts = [] if prune_label is not None: - opts = ['--prune', '-l', prune_label] - kubectl('apply', manifest_file, *opts) + kubectl_opts.extend(['--prune', '-l', prune_label]) + + kubectl('apply', manifest_file, *kubectl_opts) elif request_type == "Delete": try: kubectl('delete', manifest_file) diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-layer.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-layer.ts deleted file mode 100644 index cf57b3189b1e5..0000000000000 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-layer.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as crypto from 'crypto'; -import * as lambda from '@aws-cdk/aws-lambda'; -import { CfnResource, Token, Stack, ResourceEnvironment } from '@aws-cdk/core'; -import { Construct } from 'constructs'; - -const KUBECTL_APP_ARN = 'arn:aws:serverlessrepo:us-east-1:903779448426:applications/lambda-layer-kubectl'; -const KUBECTL_APP_CN_ARN = 'arn:aws-cn:serverlessrepo:cn-north-1:487369736442:applications/lambda-layer-kubectl'; -const KUBECTL_APP_VERSION = '2.0.0'; - -// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. -// eslint-disable-next-line -import { Construct as CoreConstruct } from '@aws-cdk/core'; - -/** - * Properties for KubectlLayer. - */ -export interface KubectlLayerProps { - /** - * The semantic version of the kubectl AWS Lambda Layer SAR app to use. - * - * @default '2.0.0' - */ - readonly version?: string; - - /** - * The Serverless Application Repository application ID which contains the kubectl layer. - * @default - The ARN for the `lambda-layer-kubectl` SAR app. - * @see https://github.com/aws-samples/aws-lambda-layer-kubectl - */ - readonly applicationId?: string; -} - -/** - * An AWS Lambda layer that includes kubectl and the AWS CLI. - * - * @see https://github.com/aws-samples/aws-lambda-layer-kubectl - */ -export class KubectlLayer extends CoreConstruct implements lambda.ILayerVersion { - /** - * The ARN of the AWS Lambda layer version. - */ - public readonly layerVersionArn: string; - - public readonly stack: Stack; - public readonly env: ResourceEnvironment; - - /** - * All runtimes are compatible. - */ - public readonly compatibleRuntimes?: lambda.Runtime[] = undefined; - - constructor(scope: Construct, id: string, props: KubectlLayerProps = {}) { - super(scope, id); - - this.stack = Stack.of(this); - this.env = { - account: this.stack.account, - region: this.stack.region, - }; - - const uniqueId = crypto.createHash('md5').update(this.node.path).digest('hex'); - const version = props.version ?? KUBECTL_APP_VERSION; - const applictionId = props.applicationId ?? (this.isChina() ? KUBECTL_APP_CN_ARN : KUBECTL_APP_ARN); - - this.stack.templateOptions.transforms = ['AWS::Serverless-2016-10-31']; // required for AWS::Serverless - const resource = new CfnResource(this, 'Resource', { - type: 'AWS::Serverless::Application', - properties: { - Location: { - ApplicationId: applictionId, - SemanticVersion: version, - }, - Parameters: { - LayerName: `kubectl-${uniqueId}`, - }, - }, - }); - - this.layerVersionArn = Token.asString(resource.getAtt('Outputs.LayerVersionArn')); - } - - public addPermission(_id: string, _permission: lambda.LayerVersionPermission): void { - return; - } - - private isChina(): boolean { - const region = this.stack.region; - return !Token.isUnresolved(region) && region.startsWith('cn-'); - } -} diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts index 4cf2d254099f6..e4c71cfda381c 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts @@ -3,9 +3,10 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, Stack, NestedStack, Names } from '@aws-cdk/core'; import * as cr from '@aws-cdk/custom-resources'; +import { AwsCliLayer } from '@aws-cdk/lambda-layer-awscli'; +import { KubectlLayer } from '@aws-cdk/lambda-layer-kubectl'; import { Construct } from 'constructs'; import { ICluster, Cluster } from './cluster'; -import { KubectlLayer, KubectlLayerProps } from './kubectl-layer'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line @@ -66,7 +67,6 @@ export class KubectlProvider extends NestedStack { throw new Error('"kubectlSecurityGroup" is required if "kubectlSubnets" is specified'); } - const layer = cluster.kubectlLayer ?? getOrCreateKubectlLayer(this); const memorySize = cluster.kubectlMemory ? cluster.kubectlMemory.toMebibytes() : 1024; const handler = new lambda.Function(this, 'Handler', { @@ -75,7 +75,6 @@ export class KubectlProvider extends NestedStack { handler: 'index.handler', timeout: Duration.minutes(15), description: 'onEvent handler for EKS kubectl resource provider', - layers: [layer], memorySize, environment: cluster.kubectlEnvironment, @@ -85,9 +84,17 @@ export class KubectlProvider extends NestedStack { vpcSubnets: cluster.kubectlPrivateSubnets ? { subnets: cluster.kubectlPrivateSubnets } : undefined, }); + // allow user to customize the layer + if (!props.cluster.kubectlLayer) { + handler.addLayers(new AwsCliLayer(this, 'AwsCliLayer')); + handler.addLayers(new KubectlLayer(this, 'KubectlLayer')); + } else { + handler.addLayers(props.cluster.kubectlLayer); + } + this.handlerRole = handler.role!; - this.handlerRole.addToPolicy(new iam.PolicyStatement({ + this.handlerRole.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['eks:DescribeCluster'], resources: [cluster.clusterArn], })); @@ -97,6 +104,8 @@ export class KubectlProvider extends NestedStack { const provider = new cr.Provider(this, 'Provider', { onEventHandler: handler, + vpc: cluster.kubectlPrivateSubnets ? cluster.vpc : undefined, + vpcSubnets: cluster.kubectlPrivateSubnets ? { subnets: cluster.kubectlPrivateSubnets } : undefined, }); this.serviceToken = provider.serviceToken; @@ -104,19 +113,3 @@ export class KubectlProvider extends NestedStack { } } - -/** - * Gets or create a singleton instance of KubectlLayer. - * - * (exported for unit tests). - */ -export function getOrCreateKubectlLayer(scope: Construct, props: KubectlLayerProps = {}): KubectlLayer { - const stack = Stack.of(scope); - const id = 'kubectl-layer-' + (props.version ? props.version : '8C2542BC-BF2B-4DFE-B765-E181FD30A9A0'); - const exists = stack.node.tryFindChild(id) as KubectlLayer; - if (exists) { - return exists; - } - - return new KubectlLayer(stack, id, props); -} diff --git a/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts b/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts deleted file mode 100644 index 2193ec301de3c..0000000000000 --- a/packages/@aws-cdk/aws-eks/lib/legacy-cluster.ts +++ /dev/null @@ -1,540 +0,0 @@ -import * as autoscaling from '@aws-cdk/aws-autoscaling'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as iam from '@aws-cdk/aws-iam'; -import * as kms from '@aws-cdk/aws-kms'; -import * as ssm from '@aws-cdk/aws-ssm'; -import { Annotations, CfnOutput, Resource, Stack, Token, Tags } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import { ICluster, ClusterAttributes, KubernetesVersion, NodeType, DefaultCapacityType, EksOptimizedImage, AutoScalingGroupCapacityOptions, MachineImageType, AutoScalingGroupOptions, CommonClusterOptions } from './cluster'; -import { clusterArnComponents } from './cluster-resource'; -import { CfnCluster, CfnClusterProps } from './eks.generated'; -import { HelmChartOptions, HelmChart } from './helm-chart'; -import { KubernetesManifest } from './k8s-manifest'; -import { Nodegroup, NodegroupOptions } from './managed-nodegroup'; -import { ServiceAccount, ServiceAccountOptions } from './service-account'; -import { renderAmazonLinuxUserData, renderBottlerocketUserData } from './user-data'; - -// defaults are based on https://eksctl.io -const DEFAULT_CAPACITY_COUNT = 2; -const DEFAULT_CAPACITY_TYPE = ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE); - -/** - * Common configuration props for EKS clusters. - */ -export interface LegacyClusterProps extends CommonClusterOptions { - /** - * Number of instances to allocate as an initial capacity for this cluster. - * Instance type can be configured through `defaultCapacityInstanceType`, - * which defaults to `m5.large`. - * - * Use `cluster.addCapacity` to add additional customized capacity. Set this - * to `0` is you wish to avoid the initial capacity allocation. - * - * @default 2 - */ - readonly defaultCapacity?: number; - - /** - * The instance type to use for the default capacity. This will only be taken - * into account if `defaultCapacity` is > 0. - * - * @default m5.large - */ - readonly defaultCapacityInstance?: ec2.InstanceType; - - /** - * The default capacity type for the cluster. - * - * @default NODEGROUP - */ - readonly defaultCapacityType?: DefaultCapacityType; - - /** - * KMS secret for envelope encryption for Kubernetes secrets. - * - * @default - By default, Kubernetes stores all secret object data within etcd and - * all etcd volumes used by Amazon EKS are encrypted at the disk-level - * using AWS-Managed encryption keys. - */ - readonly secretsEncryptionKey?: kms.IKey; -} - -/** - * A Cluster represents a managed Kubernetes Service (EKS) - * - * This is a fully managed cluster of API Servers (control-plane) - * The user is still required to create the worker nodes. - * - * @resource AWS::EKS::Cluster - */ -export class LegacyCluster extends Resource implements ICluster { - /** - * Import an existing cluster - * - * @param scope the construct scope, in most cases 'this' - * @param id the id or name to import as - * @param attrs the cluster properties to use for importing information - */ - public static fromClusterAttributes(scope: Construct, id: string, attrs: ClusterAttributes): ICluster { - return new ImportedCluster(scope, id, attrs); - } - - /** - * The VPC in which this Cluster was created - */ - public readonly vpc: ec2.IVpc; - - /** - * The Name of the created EKS Cluster - */ - public readonly clusterName: string; - - /** - * The AWS generated ARN for the Cluster resource - * - * @example arn:aws:eks:us-west-2:666666666666:cluster/prod - */ - public readonly clusterArn: string; - - /** - * The endpoint URL for the Cluster - * - * This is the URL inside the kubeconfig file to use with kubectl - * - * @example https://5E1D0CEXAMPLEA591B746AFC5AB30262.yl4.us-west-2.eks.amazonaws.com - */ - public readonly clusterEndpoint: string; - - /** - * The certificate-authority-data for your cluster. - */ - public readonly clusterCertificateAuthorityData: string; - - /** - * The cluster security group that was created by Amazon EKS for the cluster. - */ - public readonly clusterSecurityGroupId: string; - - /** - * Amazon Resource Name (ARN) or alias of the customer master key (CMK). - */ - public readonly clusterEncryptionConfigKeyArn: string; - - /** - * Manages connection rules (Security Group Rules) for the cluster - * - * @type {ec2.Connections} - * @memberof Cluster - */ - public readonly connections: ec2.Connections; - - /** - * IAM role assumed by the EKS Control Plane - */ - public readonly role: iam.IRole; - - /** - * The auto scaling group that hosts the default capacity for this cluster. - * This will be `undefined` if the `defaultCapacityType` is not `EC2` or - * `defaultCapacityType` is `EC2` but default capacity is set to 0. - */ - public readonly defaultCapacity?: autoscaling.AutoScalingGroup; - - /** - * The node group that hosts the default capacity for this cluster. - * This will be `undefined` if the `defaultCapacityType` is `EC2` or - * `defaultCapacityType` is `NODEGROUP` but default capacity is set to 0. - */ - public readonly defaultNodegroup?: Nodegroup; - - public readonly prune: boolean = false; - - private readonly version: KubernetesVersion; - - /** - * Initiates an EKS Cluster with the supplied arguments - * - * @param scope a Construct, most likely a cdk.Stack created - * @param name the name of the Construct to create - * @param props properties in the IClusterProps interface - */ - constructor(scope: Construct, id: string, props: LegacyClusterProps) { - super(scope, id, { - physicalName: props.clusterName, - }); - - const stack = Stack.of(this); - - this.vpc = props.vpc || new ec2.Vpc(this, 'DefaultVpc'); - this.version = props.version; - - this.tagSubnets(); - - // this is the role used by EKS when interacting with AWS resources - this.role = props.role || new iam.Role(this, 'Role', { - assumedBy: new iam.ServicePrincipal('eks.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSClusterPolicy'), - ], - }); - - const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'ControlPlaneSecurityGroup', { - vpc: this.vpc, - description: 'EKS Control Plane Security Group', - }); - - this.connections = new ec2.Connections({ - securityGroups: [securityGroup], - defaultPort: ec2.Port.tcp(443), // Control Plane has an HTTPS API - }); - - // Get subnetIds for all selected subnets - const placements = props.vpcSubnets || [{ subnetType: ec2.SubnetType.PUBLIC }, { subnetType: ec2.SubnetType.PRIVATE }]; - const subnetIds = [...new Set(Array().concat(...placements.map(s => this.vpc.selectSubnets(s).subnetIds)))]; - - const clusterProps: CfnClusterProps = { - name: this.physicalName, - roleArn: this.role.roleArn, - version: props.version.version, - resourcesVpcConfig: { - securityGroupIds: [securityGroup.securityGroupId], - subnetIds, - }, - ...(props.secretsEncryptionKey ? { - encryptionConfig: [{ - provider: { - keyArn: props.secretsEncryptionKey.keyArn, - }, - resources: ['secrets'], - }], - } : {} ), - }; - - const resource = new CfnCluster(this, 'Resource', clusterProps); - - this.clusterName = this.getResourceNameAttribute(resource.ref); - this.clusterArn = this.getResourceArnAttribute(resource.attrArn, clusterArnComponents(this.physicalName)); - - this.clusterEndpoint = resource.attrEndpoint; - this.clusterCertificateAuthorityData = resource.attrCertificateAuthorityData; - this.clusterSecurityGroupId = resource.attrClusterSecurityGroupId; - this.clusterEncryptionConfigKeyArn = resource.attrEncryptionConfigKeyArn; - - const updateConfigCommandPrefix = `aws eks update-kubeconfig --name ${this.clusterName}`; - const getTokenCommandPrefix = `aws eks get-token --cluster-name ${this.clusterName}`; - const commonCommandOptions = [`--region ${stack.region}`]; - - if (props.outputClusterName) { - new CfnOutput(this, 'ClusterName', { value: this.clusterName }); - } - - // allocate default capacity if non-zero (or default). - const minCapacity = props.defaultCapacity === undefined ? DEFAULT_CAPACITY_COUNT : props.defaultCapacity; - if (minCapacity > 0) { - const instanceType = props.defaultCapacityInstance || DEFAULT_CAPACITY_TYPE; - this.defaultCapacity = props.defaultCapacityType === DefaultCapacityType.EC2 ? - this.addCapacity('DefaultCapacity', { instanceType, minCapacity }) : undefined; - - this.defaultNodegroup = props.defaultCapacityType !== DefaultCapacityType.EC2 ? - this.addNodegroup('DefaultCapacity', { instanceType, minSize: minCapacity }) : undefined; - } - - const outputConfigCommand = props.outputConfigCommand === undefined ? true : props.outputConfigCommand; - if (outputConfigCommand) { - const postfix = commonCommandOptions.join(' '); - new CfnOutput(this, 'ConfigCommand', { value: `${updateConfigCommandPrefix} ${postfix}` }); - new CfnOutput(this, 'GetTokenCommand', { value: `${getTokenCommandPrefix} ${postfix}` }); - } - } - - public addServiceAccount(_id: string, _options?: ServiceAccountOptions): ServiceAccount { - throw new Error('legacy cluster does not support adding service accounts'); - } - - /** - * Since we dont really want to make it required on the top-level ICluster - * we do this trick here in return type to match interface type - */ - public get openIdConnectProvider(): iam.IOpenIdConnectProvider { - throw new Error('legacy cluster does not support open id connect providers'); - } - - /** - * Add nodes to this EKS cluster - * - * The nodes will automatically be configured with the right VPC and AMI - * for the instance type and Kubernetes version. - * - * Spot instances will be labeled `lifecycle=Ec2Spot` and tainted with `PreferNoSchedule`. - */ - public addCapacity(id: string, options: AutoScalingGroupCapacityOptions): autoscaling.AutoScalingGroup { - if (options.machineImageType === MachineImageType.BOTTLEROCKET && options.bootstrapOptions !== undefined ) { - throw new Error('bootstrapOptions is not supported for Bottlerocket'); - } - const asg = new autoscaling.AutoScalingGroup(this, id, { - ...options, - vpc: this.vpc, - machineImage: options.machineImageType === MachineImageType.BOTTLEROCKET ? - new BottleRocketImage() : - new EksOptimizedImage({ - nodeType: nodeTypeForInstanceType(options.instanceType), - kubernetesVersion: this.version.version, - }), - updateType: options.updateType || autoscaling.UpdateType.ROLLING_UPDATE, - instanceType: options.instanceType, - }); - - this.addAutoScalingGroup(asg, { - mapRole: options.mapRole, - bootstrapOptions: options.bootstrapOptions, - bootstrapEnabled: options.bootstrapEnabled, - machineImageType: options.machineImageType, - }); - - return asg; - } - - /** - * Add managed nodegroup to this Amazon EKS cluster - * - * This method will create a new managed nodegroup and add into the capacity. - * - * @see https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html - * @param id The ID of the nodegroup - * @param options options for creating a new nodegroup - */ - public addNodegroup(id: string, options?: NodegroupOptions): Nodegroup { - return new Nodegroup(this, `Nodegroup${id}`, { - cluster: this, - ...options, - }); - } - - /** - * Add compute capacity to this EKS cluster in the form of an AutoScalingGroup - * - * The AutoScalingGroup must be running an EKS-optimized AMI containing the - * /etc/eks/bootstrap.sh script. This method will configure Security Groups, - * add the right policies to the instance role, apply the right tags, and add - * the required user data to the instance's launch configuration. - * - * Spot instances will be labeled `lifecycle=Ec2Spot` and tainted with `PreferNoSchedule`. - * If kubectl is enabled, the - * [spot interrupt handler](https://github.com/awslabs/ec2-spot-labs/tree/master/ec2-spot-eks-solution/spot-termination-handler) - * daemon will be installed on all spot instances to handle - * [EC2 Spot Instance Termination Notices](https://aws.amazon.com/blogs/aws/new-ec2-spot-instance-termination-notices/). - * - * Prefer to use `addCapacity` if possible. - * - * @see https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html - * @param autoScalingGroup [disable-awslint:ref-via-interface] - * @param options options for adding auto scaling groups, like customizing the bootstrap script - */ - public addAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AutoScalingGroupOptions) { - // self rules - autoScalingGroup.connections.allowInternally(ec2.Port.allTraffic()); - - // Cluster to:nodes rules - autoScalingGroup.connections.allowFrom(this, ec2.Port.tcp(443)); - autoScalingGroup.connections.allowFrom(this, ec2.Port.tcpRange(1025, 65535)); - - // Allow HTTPS from Nodes to Cluster - autoScalingGroup.connections.allowTo(this, ec2.Port.tcp(443)); - - // Allow all node outbound traffic - autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allTcp()); - autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allUdp()); - autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allIcmp()); - - const bootstrapEnabled = options.bootstrapEnabled !== undefined ? options.bootstrapEnabled : true; - if (options.bootstrapOptions && !bootstrapEnabled) { - throw new Error('Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false'); - } - - if (bootstrapEnabled) { - const userData = options.machineImageType === MachineImageType.BOTTLEROCKET ? - renderBottlerocketUserData(this) : - renderAmazonLinuxUserData(this.clusterName, autoScalingGroup, options.bootstrapOptions); - autoScalingGroup.addUserData(...userData); - } - - autoScalingGroup.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSWorkerNodePolicy')); - autoScalingGroup.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEKS_CNI_Policy')); - autoScalingGroup.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly')); - - // EKS Required Tags - Tags.of(autoScalingGroup).add(`kubernetes.io/cluster/${this.clusterName}`, 'owned', { - applyToLaunchedInstances: true, - }); - - if (options.mapRole) { - throw new Error('Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster'); - } - - // since we are not mapping the instance role to RBAC, synthesize an - // output so it can be pasted into `aws-auth-cm.yaml` - new CfnOutput(autoScalingGroup, 'InstanceRoleARN', { - value: autoScalingGroup.role.roleArn, - }); - } - - public addManifest(_id: string, ..._manifest: Record[]): KubernetesManifest { - throw new Error('legacy cluster does not support adding kubernetes manifests'); - } - - public addHelmChart(_id: string, _options: HelmChartOptions): HelmChart { - throw new Error('legacy cluster does not support adding helm charts'); - } - - public addCdk8sChart(_id: string, _chart: Construct): KubernetesManifest { - throw new Error('legacy cluster does not support adding cdk8s charts'); - } - - /** - * Opportunistically tag subnets with the required tags. - * - * If no subnets could be found (because this is an imported VPC), add a warning. - * - * @see https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html - */ - private tagSubnets() { - const tagAllSubnets = (type: string, subnets: ec2.ISubnet[], tag: string) => { - for (const subnet of subnets) { - // if this is not a concrete subnet, attach a construct warning - if (!ec2.Subnet.isVpcSubnet(subnet)) { - // message (if token): "could not auto-tag public/private subnet with tag..." - // message (if not token): "count not auto-tag public/private subnet xxxxx with tag..." - const subnetID = Token.isUnresolved(subnet.subnetId) ? '' : ` ${subnet.subnetId}`; - Annotations.of(this).addWarning(`Could not auto-tag ${type} subnet${subnetID} with "${tag}=1", please remember to do this manually`); - continue; - } - - Tags.of(subnet).add(tag, '1'); - } - }; - - // https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html - tagAllSubnets('private', this.vpc.privateSubnets, 'kubernetes.io/role/internal-elb'); - tagAllSubnets('public', this.vpc.publicSubnets, 'kubernetes.io/role/elb'); - } -} - -/** - * Import a cluster to use in another stack - */ -class ImportedCluster extends Resource implements ICluster { - public readonly clusterName: string; - public readonly clusterArn: string; - public readonly connections = new ec2.Connections(); - public readonly prune: boolean = false; - - constructor(scope: Construct, id: string, private readonly props: ClusterAttributes) { - super(scope, id); - - this.clusterName = props.clusterName; - this.clusterArn = this.stack.formatArn(clusterArnComponents(props.clusterName)); - - let i = 1; - for (const sgid of props.securityGroupIds ?? []) { - this.connections.addSecurityGroup(ec2.SecurityGroup.fromSecurityGroupId(this, `SecurityGroup${i}`, sgid)); - i++; - } - } - - public addManifest(_id: string, ..._manifest: Record[]): KubernetesManifest { - throw new Error('legacy cluster does not support adding kubernetes manifests'); - } - - public addHelmChart(_id: string, _options: HelmChartOptions): HelmChart { - throw new Error('legacy cluster does not support adding helm charts'); - } - - public addCdk8sChart(_id: string, _chart: Construct): KubernetesManifest { - throw new Error('legacy cluster does not support adding cdk8s charts'); - } - - public addServiceAccount(_id: string, _options?: ServiceAccountOptions): ServiceAccount { - throw new Error('legacy cluster does not support adding service accounts'); - } - - public get openIdConnectProvider(): iam.IOpenIdConnectProvider { - throw new Error('legacy cluster does not support open id connect providers'); - } - - public get vpc() { - if (!this.props.vpc) { - throw new Error('"vpc" is not defined for this imported cluster'); - } - return this.props.vpc; - } - - public get clusterSecurityGroupId(): string { - if (!this.props.clusterSecurityGroupId) { - throw new Error('"clusterSecurityGroupId" is not defined for this imported cluster'); - } - return this.props.clusterSecurityGroupId; - } - - public get clusterEndpoint(): string { - if (!this.props.clusterEndpoint) { - throw new Error('"clusterEndpoint" is not defined for this imported cluster'); - } - return this.props.clusterEndpoint; - } - - public get clusterCertificateAuthorityData(): string { - if (!this.props.clusterCertificateAuthorityData) { - throw new Error('"clusterCertificateAuthorityData" is not defined for this imported cluster'); - } - return this.props.clusterCertificateAuthorityData; - } - - public get clusterEncryptionConfigKeyArn(): string { - if (!this.props.clusterEncryptionConfigKeyArn) { - throw new Error('"clusterEncryptionConfigKeyArn" is not defined for this imported cluster'); - } - return this.props.clusterEncryptionConfigKeyArn; - } - -} - -/** - * Construct an Bottlerocket image from the latest AMI published in SSM - */ -class BottleRocketImage implements ec2.IMachineImage { - private readonly kubernetesVersion?: string; - - private readonly amiParameterName: string; - - /** - * Constructs a new instance of the BottleRocketImage class. - */ - public constructor() { - // only 1.15 is currently available - this.kubernetesVersion = '1.15'; - - // set the SSM parameter name - this.amiParameterName = `/aws/service/bottlerocket/aws-k8s-${this.kubernetesVersion}/x86_64/latest/image_id`; - } - - /** - * Return the correct image - */ - public getImage(scope: Construct): ec2.MachineImageConfig { - const ami = ssm.StringParameter.valueForStringParameter(scope, this.amiParameterName); - return { - imageId: ami, - osType: ec2.OperatingSystemType.LINUX, - userData: ec2.UserData.custom(''), - }; - } -} - -const GPU_INSTANCETYPES = ['p2', 'p3', 'g4']; -const INFERENTIA_INSTANCETYPES = ['inf1']; - -function nodeTypeForInstanceType(instanceType: ec2.InstanceType) { - return GPU_INSTANCETYPES.includes(instanceType.toString().substring(0, 2)) ? NodeType.GPU : - INFERENTIA_INSTANCETYPES.includes(instanceType.toString().substring(0, 4)) ? NodeType.INFERENTIA : - NodeType.STANDARD; -} diff --git a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts index ea7e7a448e4f8..d475e69574fb3 100644 --- a/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts @@ -1,6 +1,6 @@ import { InstanceType, ISecurityGroup, SubnetSelection } from '@aws-cdk/aws-ec2'; import { IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; -import { IResource, Resource } from '@aws-cdk/core'; +import { IResource, Resource, Annotations } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Cluster, ICluster } from './cluster'; import { CfnNodegroup } from './eks.generated'; @@ -37,6 +37,20 @@ export enum NodegroupAmiType { AL2_ARM_64 = 'AL2_ARM_64' } +/** + * Capacity type of the managed node group + */ +export enum CapacityType { + /** + * spot instances + */ + SPOT = 'SPOT', + /** + * on-demand instances + */ + ON_DEMAND = 'ON_DEMAND' +} + /** * The remote access (SSH) configuration to use with your node group. * @@ -95,7 +109,7 @@ export interface NodegroupOptions { /** * The AMI type for your node group. * - * @default - auto-determined from the instanceType property. + * @default - auto-determined from the instanceTypes property. */ readonly amiType?: NodegroupAmiType; /** @@ -138,8 +152,15 @@ export interface NodegroupOptions { * `AL2_x86_64_GPU` with the amiType parameter. * * @default t3.medium + * @deprecated Use `instanceTypes` instead. */ readonly instanceType?: InstanceType; + /** + * The instance types to use for your node group. + * @default t3.medium will be used according to the cloudformation document. + * @see - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-instancetypes + */ + readonly instanceTypes?: InstanceType[]; /** * The Kubernetes labels to be applied to the nodes in the node group when they are created. * @@ -183,6 +204,12 @@ export interface NodegroupOptions { * @default - no launch template */ readonly launchTemplateSpec?: LaunchTemplateSpec; + /** + * The capacity type of the nodegroup. + * + * @default - ON_DEMAND + */ + readonly capacityType?: CapacityType; } /** @@ -199,6 +226,10 @@ export interface NodegroupProps extends NodegroupOptions { * The Nodegroup resource class */ export class Nodegroup extends Resource implements INodegroup { + /** + * Default instanceTypes + */ + public static readonly DEFAULT_INSTANCE_TYPES = [new InstanceType('t3.medium')]; /** * Import the Nodegroup from attributes */ @@ -253,6 +284,25 @@ export class Nodegroup extends Resource implements INodegroup { throw new Error(`Minimum capacity ${this.minSize} can't be greater than desired size ${this.desiredSize}`); } + if (props.instanceType && props.instanceTypes) { + throw new Error('"instanceType is deprecated, please use "instanceTypes" only.'); + } + + if (props.instanceType) { + Annotations.of(this).addWarning('"instanceType" is deprecated and will be removed in the next major version. please use "instanceTypes" instead'); + } + const instanceTypes = props.instanceTypes ?? (props.instanceType ? [props.instanceType] : Nodegroup.DEFAULT_INSTANCE_TYPES); + // get unique AMI types from instanceTypes + const uniqAmiTypes = getAmiTypes(instanceTypes); + // uniqAmiTypes.length should be at least 1 + if (uniqAmiTypes.length > 1) { + throw new Error('instanceTypes of different CPU architectures is not allowed'); + } + const determinedAmiType = uniqAmiTypes[0]; + if (props.amiType && props.amiType !== determinedAmiType) { + throw new Error(`The specified AMI does not match the instance types architecture, either specify ${determinedAmiType} or dont specify any`); + } + if (!props.nodeRole) { const ngRole = new Role(this, 'NodeGroupRole', { assumedBy: new ServicePrincipal('ec2.amazonaws.com'), @@ -271,11 +321,12 @@ export class Nodegroup extends Resource implements INodegroup { nodegroupName: props.nodegroupName, nodeRole: this.role.roleArn, subnets: this.cluster.vpc.selectSubnets(props.subnets).subnetIds, - amiType: props.amiType ?? (props.instanceType ? getAmiTypeForInstanceType(props.instanceType).toString() : - undefined), + // AmyType is not allowed by CFN when specifying an image id in your launch template. + amiType: props.launchTemplateSpec === undefined ? determinedAmiType : undefined, diskSize: props.diskSize, forceUpdateEnabled: props.forceUpdate ?? true, - instanceTypes: props.instanceType ? [props.instanceType.toString()] : undefined, + instanceTypes: props.instanceTypes ? props.instanceTypes.map(t => t.toString()) : + props.instanceType ? [props.instanceType.toString()] : undefined, labels: props.labels, releaseVersion: props.releaseVersion, remoteAccess: props.remoteAccess ? { @@ -291,17 +342,21 @@ export class Nodegroup extends Resource implements INodegroup { tags: props.tags, }); + if (props.capacityType) { + resource.addPropertyOverride('CapacityType', props.capacityType.valueOf()); + } + if (props.launchTemplateSpec) { if (props.diskSize) { // see - https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html // and https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-disksize throw new Error('diskSize must be specified within the launch template'); } - if (props.instanceType) { - // see - https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html - // and https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-disksize - throw new Error('Instance types must be specified within the launch template'); - } + /** + * Instance types can be specified either in `instanceType` or launch template but not both. AS we can not check the content of + * the provided launch template and the `instanceType` property is preferrable. We allow users to define `instanceType` property here. + * see - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-instancetypes + */ // TODO: update this when the L1 resource spec is updated. resource.addPropertyOverride('LaunchTemplate', { Id: props.launchTemplateSpec.id, @@ -340,3 +395,8 @@ function getAmiTypeForInstanceType(instanceType: InstanceType) { NodegroupAmiType.AL2_X86_64; } +function getAmiTypes(instanceType: InstanceType[]) { + const amiTypes = instanceType.map(i =>getAmiTypeForInstanceType(i)); + // retuen unique AMI types + return [...new Set(amiTypes)]; +} diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index a57aaa253db2f..658a302f3d697 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -94,6 +94,8 @@ "@aws-cdk/aws-ssm": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", + "@aws-cdk/lambda-layer-awscli": "0.0.0", + "@aws-cdk/lambda-layer-kubectl": "0.0.0", "constructs": "^3.2.0", "yaml": "1.10.0" }, @@ -110,7 +112,9 @@ "@aws-cdk/aws-ssm": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", - "constructs": "^3.2.0" + "constructs": "^3.2.0", + "@aws-cdk/lambda-layer-awscli": "0.0.0", + "@aws-cdk/lambda-layer-kubectl": "0.0.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.expected.json new file mode 100644 index 0000000000000..82488919bedb9 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.expected.json @@ -0,0 +1,1430 @@ +{ + "Resources": { + "EksAllHandlersInVpcStackDefaultVpcBE11D4AE": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1SubnetEA05A5C7": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet1" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTable183714C5": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet1" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTableAssociation1012ACB8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTable183714C5" + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1SubnetEA05A5C7" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1DefaultRoute8E294BC5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTable183714C5" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcIGW916D42F1" + } + }, + "DependsOn": [ + "EksAllHandlersInVpcStackDefaultVpcVPCGW5DC3BDB4" + ] + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1EIP9380B54C": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet1" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1NATGatewayFD57AC6C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1EIP9380B54C", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1SubnetEA05A5C7" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet1" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2Subnet8A9F7D50": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet2" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableE4762B74": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet2" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableAssociation5DFA3BFD": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableE4762B74" + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2Subnet8A9F7D50" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2DefaultRouteC7B27F81": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableE4762B74" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcIGW916D42F1" + } + }, + "DependsOn": [ + "EksAllHandlersInVpcStackDefaultVpcVPCGW5DC3BDB4" + ] + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2EIP9186922F": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet2" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2NATGatewayEC0B8252": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2EIP9186922F", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2Subnet8A9F7D50" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet2" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3SubnetB436275F": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet3" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTable85E4266C": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet3" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTableAssociationEA306E19": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTable85E4266C" + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3SubnetB436275F" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3DefaultRoute965D74B7": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTable85E4266C" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcIGW916D42F1" + } + }, + "DependsOn": [ + "EksAllHandlersInVpcStackDefaultVpcVPCGW5DC3BDB4" + ] + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3EIPBF5ED908": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet3" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3NATGateway7AE6F6B3": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3EIPBF5ED908", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3SubnetB436275F" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PublicSubnet3" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PrivateSubnet1" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableF214D04E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PrivateSubnet1" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableAssociationC09E4B48": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableF214D04E" + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1DefaultRoute27B45BF6": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableF214D04E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1NATGatewayFD57AC6C" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PrivateSubnet2" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTable22627B70": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PrivateSubnet2" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTableAssociation475205D6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTable22627B70" + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2DefaultRoute8A741F7F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTable22627B70" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2NATGatewayEC0B8252" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PrivateSubnet3" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTable19D4047C": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "Tags": [ + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc/PrivateSubnet3" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTableAssociationC07A6A83": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTable19D4047C" + }, + "SubnetId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3DefaultRoute203EAFA4": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTable19D4047C" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3NATGateway7AE6F6B3" + } + } + }, + "EksAllHandlersInVpcStackDefaultVpcIGW916D42F1": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-eks-handlers-in-vpc-test/EksAllHandlersInVpcStack/DefaultVpc" + } + ] + } + }, + "EksAllHandlersInVpcStackDefaultVpcVPCGW5DC3BDB4": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "InternetGatewayId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcIGW916D42F1" + } + } + }, + "EksAllHandlersInVpcStackRoleC36F09F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "eks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSClusterPolicy" + ] + ] + } + ] + } + }, + "EksAllHandlersInVpcStackControlPlaneSecurityGroup10B6E594": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "EKS Control Plane Security Group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + } + } + }, + "EksAllHandlersInVpcStackCreationRole0BAA4CDC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + }, + "DependsOn": [ + "EksAllHandlersInVpcStackDefaultVpcIGW916D42F1", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1DefaultRoute27B45BF6", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableF214D04E", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableAssociationC09E4B48", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2DefaultRoute8A741F7F", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTable22627B70", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTableAssociation475205D6", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3DefaultRoute203EAFA4", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTable19D4047C", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTableAssociationC07A6A83", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1DefaultRoute8E294BC5", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1EIP9380B54C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1NATGatewayFD57AC6C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTable183714C5", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTableAssociation1012ACB8", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1SubnetEA05A5C7", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2DefaultRouteC7B27F81", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2EIP9186922F", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2NATGatewayEC0B8252", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableE4762B74", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableAssociation5DFA3BFD", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2Subnet8A9F7D50", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3DefaultRoute965D74B7", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3EIPBF5ED908", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3NATGateway7AE6F6B3", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTable85E4266C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTableAssociationEA306E19", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3SubnetB436275F", + "EksAllHandlersInVpcStackDefaultVpcBE11D4AE", + "EksAllHandlersInVpcStackDefaultVpcVPCGW5DC3BDB4" + ] + }, + "EksAllHandlersInVpcStackCreationRoleDefaultPolicy783D59F3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iam:PassRole", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackRoleC36F09F0", + "Arn" + ] + } + }, + { + "Action": [ + "ec2:DescribeSubnets", + "ec2:DescribeRouteTables" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "eks:CreateCluster", + "eks:DescribeCluster", + "eks:DescribeUpdate", + "eks:DeleteCluster", + "eks:UpdateClusterVersion", + "eks:UpdateClusterConfig", + "eks:CreateFargateProfile", + "eks:TagResource", + "eks:UntagResource" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Action": [ + "eks:DescribeFargateProfile", + "eks:DeleteFargateProfile" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "iam:GetRole", + "iam:listAttachedRolePolicies" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "iam:CreateServiceLinkedRole", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "ec2:DescribeVpcs", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ec2:test-region:12345678:vpc/", + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "EksAllHandlersInVpcStackCreationRoleDefaultPolicy783D59F3", + "Roles": [ + { + "Ref": "EksAllHandlersInVpcStackCreationRole0BAA4CDC" + } + ] + }, + "DependsOn": [ + "EksAllHandlersInVpcStackDefaultVpcIGW916D42F1", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1DefaultRoute27B45BF6", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableF214D04E", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableAssociationC09E4B48", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2DefaultRoute8A741F7F", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTable22627B70", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTableAssociation475205D6", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3DefaultRoute203EAFA4", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTable19D4047C", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTableAssociationC07A6A83", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1DefaultRoute8E294BC5", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1EIP9380B54C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1NATGatewayFD57AC6C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTable183714C5", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTableAssociation1012ACB8", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1SubnetEA05A5C7", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2DefaultRouteC7B27F81", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2EIP9186922F", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2NATGatewayEC0B8252", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableE4762B74", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableAssociation5DFA3BFD", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2Subnet8A9F7D50", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3DefaultRoute965D74B7", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3EIPBF5ED908", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3NATGateway7AE6F6B3", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTable85E4266C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTableAssociationEA306E19", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3SubnetB436275F", + "EksAllHandlersInVpcStackDefaultVpcBE11D4AE", + "EksAllHandlersInVpcStackDefaultVpcVPCGW5DC3BDB4" + ] + }, + "EksAllHandlersInVpcStack9ED695D7": { + "Type": "Custom::AWSCDK-EKS-Cluster", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454", + "Outputs.awscdkekshandlersinvpctestawscdkawseksClusterResourceProviderframeworkonEvent5C6C2463Arn" + ] + }, + "Config": { + "version": "1.18", + "roleArn": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackRoleC36F09F0", + "Arn" + ] + }, + "resourcesVpcConfig": { + "subnetIds": [ + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1SubnetEA05A5C7" + }, + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2Subnet8A9F7D50" + }, + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3SubnetB436275F" + }, + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978" + }, + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3" + }, + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9" + } + ], + "securityGroupIds": [ + { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackControlPlaneSecurityGroup10B6E594", + "GroupId" + ] + } + ], + "endpointPublicAccess": true, + "endpointPrivateAccess": true + } + }, + "AssumeRoleArn": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackCreationRole0BAA4CDC", + "Arn" + ] + }, + "AttributesRevision": 2 + }, + "DependsOn": [ + "EksAllHandlersInVpcStackDefaultVpcIGW916D42F1", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1DefaultRoute27B45BF6", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableF214D04E", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1RouteTableAssociationC09E4B48", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2DefaultRoute8A741F7F", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTable22627B70", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2RouteTableAssociation475205D6", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3DefaultRoute203EAFA4", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTable19D4047C", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3RouteTableAssociationC07A6A83", + "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1DefaultRoute8E294BC5", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1EIP9380B54C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1NATGatewayFD57AC6C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTable183714C5", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1RouteTableAssociation1012ACB8", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet1SubnetEA05A5C7", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2DefaultRouteC7B27F81", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2EIP9186922F", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2NATGatewayEC0B8252", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableE4762B74", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2RouteTableAssociation5DFA3BFD", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet2Subnet8A9F7D50", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3DefaultRoute965D74B7", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3EIPBF5ED908", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3NATGateway7AE6F6B3", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTable85E4266C", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3RouteTableAssociationEA306E19", + "EksAllHandlersInVpcStackDefaultVpcPublicSubnet3SubnetB436275F", + "EksAllHandlersInVpcStackDefaultVpcBE11D4AE", + "EksAllHandlersInVpcStackDefaultVpcVPCGW5DC3BDB4", + "EksAllHandlersInVpcStackCreationRoleDefaultPolicy783D59F3", + "EksAllHandlersInVpcStackCreationRole0BAA4CDC" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "EksAllHandlersInVpcStackKubectlReadyBarrier8687350F": { + "Type": "AWS::SSM::Parameter", + "Properties": { + "Type": "String", + "Value": "aws:cdk:eks:kubectl-ready" + }, + "DependsOn": [ + "EksAllHandlersInVpcStackCreationRoleDefaultPolicy783D59F3", + "EksAllHandlersInVpcStackCreationRole0BAA4CDC", + "EksAllHandlersInVpcStack9ED695D7" + ] + }, + "EksAllHandlersInVpcStackMastersRole825EE5E6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::12345678:root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "EksAllHandlersInVpcStackAwsAuthmanifest66335CD9": { + "Type": "Custom::AWSCDK-EKS-KubernetesResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkekshandlersinvpctestawscdkawseksKubectlProviderframeworkonEventB8D0A5E7Arn" + ] + }, + "Manifest": { + "Fn::Join": [ + "", + [ + "[{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"aws-auth\",\"namespace\":\"kube-system\",\"labels\":{\"aws.cdk.eks/prune-c8fa2698c0d935568a51a7732ad19350286b302ae8\":\"\"}},\"data\":{\"mapRoles\":\"[{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackMastersRole825EE5E6", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"", + { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackMastersRole825EE5E6", + "Arn" + ] + }, + "\\\",\\\"groups\\\":[\\\"system:masters\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackNodegroupDefaultCapacityNodeGroupRoleFFBF949C", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]}]\",\"mapUsers\":\"[]\",\"mapAccounts\":\"[]\"}}]" + ] + ] + }, + "ClusterName": { + "Ref": "EksAllHandlersInVpcStack9ED695D7" + }, + "RoleArn": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackCreationRole0BAA4CDC", + "Arn" + ] + }, + "PruneLabel": "aws.cdk.eks/prune-c8fa2698c0d935568a51a7732ad19350286b302ae8", + "Overwrite": true + }, + "DependsOn": [ + "EksAllHandlersInVpcStackKubectlReadyBarrier8687350F" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "EksAllHandlersInVpcStackNodegroupDefaultCapacityNodeGroupRoleFFBF949C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSWorkerNodePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKS_CNI_Policy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ] + ] + } + ] + } + }, + "EksAllHandlersInVpcStackNodegroupDefaultCapacityD8DD5ECF": { + "Type": "AWS::EKS::Nodegroup", + "Properties": { + "ClusterName": { + "Ref": "EksAllHandlersInVpcStack9ED695D7" + }, + "NodeRole": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackNodegroupDefaultCapacityNodeGroupRoleFFBF949C", + "Arn" + ] + }, + "Subnets": [ + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978" + }, + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3" + }, + { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9" + } + ], + "AmiType": "AL2_x86_64", + "ForceUpdateEnabled": true, + "InstanceTypes": [ + "m5.large" + ], + "ScalingConfig": { + "DesiredSize": 2, + "MaxSize": 2, + "MinSize": 2 + } + } + }, + "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.test-region.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172cS3Bucket151BE34C" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172cS3VersionKey89E7CC67" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172cS3VersionKey89E7CC67" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackCreationRoleADAAC7FDArn": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackCreationRole0BAA4CDC", + "Arn" + ] + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackDefaultVpcE40EA7ACRef": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "referencetoawscdkekshandlersinvpctestAssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3Bucket020723FERef": { + "Ref": "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3Bucket14D204F9" + }, + "referencetoawscdkekshandlersinvpctestAssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3VersionKeyEC505E3ARef": { + "Ref": "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3VersionKeyDE8A2F1F" + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackDefaultVpcPrivateSubnet1Subnet9479BAA8Ref": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978" + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackDefaultVpcPrivateSubnet2Subnet9480A740Ref": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3" + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackDefaultVpcPrivateSubnet3Subnet1B127970Ref": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9" + }, + "referencetoawscdkekshandlersinvpctestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket9D7E9998Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" + }, + "referencetoawscdkekshandlersinvpctestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyE6908FD8Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" + } + } + } + }, + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.test-region.", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParameters11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7S3BucketE510C342" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7S3VersionKeyD31A83B6" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7S3VersionKeyD31A83B6" + } + ] + } + ] + } + ] + ] + }, + "Parameters": { + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStack429D29C0Arn": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStack9ED695D7", + "Arn" + ] + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackCreationRoleADAAC7FDArn": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackCreationRole0BAA4CDC", + "Arn" + ] + }, + "referencetoawscdkekshandlersinvpctestAssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3BucketF3527C76Ref": { + "Ref": "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3Bucket008DBB35" + }, + "referencetoawscdkekshandlersinvpctestAssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKeyE9C79D35Ref": { + "Ref": "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKey97C3E1A0" + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackDefaultVpcPrivateSubnet1Subnet9479BAA8Ref": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet1SubnetE2B86978" + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackDefaultVpcPrivateSubnet2Subnet9480A740Ref": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet2SubnetFBAAF3E3" + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackDefaultVpcPrivateSubnet3Subnet1B127970Ref": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcPrivateSubnet3SubnetA75A8BA9" + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStack429D29C0ClusterSecurityGroupId": { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStack9ED695D7", + "ClusterSecurityGroupId" + ] + }, + "referencetoawscdkekshandlersinvpctestAssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3BucketC0281AE8Ref": { + "Ref": "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket6DACDE73" + }, + "referencetoawscdkekshandlersinvpctestAssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKeyD6BA7117Ref": { + "Ref": "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey015AEA61" + }, + "referencetoawscdkekshandlersinvpctestAssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket88622CD5Ref": { + "Ref": "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket7EE7EA15" + }, + "referencetoawscdkekshandlersinvpctestAssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey1C342D31Ref": { + "Ref": "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C948E78" + }, + "referencetoawscdkekshandlersinvpctestEksAllHandlersInVpcStackDefaultVpcE40EA7ACRef": { + "Ref": "EksAllHandlersInVpcStackDefaultVpcBE11D4AE" + }, + "referencetoawscdkekshandlersinvpctestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket9D7E9998Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" + }, + "referencetoawscdkekshandlersinvpctestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyE6908FD8Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" + } + } + } + } + }, + "Outputs": { + "EksAllHandlersInVpcStackConfigCommandE25F67E8": { + "Value": { + "Fn::Join": [ + "", + [ + "aws eks update-kubeconfig --name ", + { + "Ref": "EksAllHandlersInVpcStack9ED695D7" + }, + " --region test-region --role-arn ", + { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackMastersRole825EE5E6", + "Arn" + ] + } + ] + ] + } + }, + "EksAllHandlersInVpcStackGetTokenCommand5EB9ED5B": { + "Value": { + "Fn::Join": [ + "", + [ + "aws eks get-token --cluster-name ", + { + "Ref": "EksAllHandlersInVpcStack9ED695D7" + }, + " --region test-region --role-arn ", + { + "Fn::GetAtt": [ + "EksAllHandlersInVpcStackMastersRole825EE5E6", + "Arn" + ] + } + ] + ] + } + } + }, + "Parameters": { + "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3Bucket14D204F9": { + "Type": "String", + "Description": "S3 bucket for asset \"87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dba\"" + }, + "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3VersionKeyDE8A2F1F": { + "Type": "String", + "Description": "S3 key for asset version \"87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dba\"" + }, + "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaArtifactHash54822A43": { + "Type": "String", + "Description": "Artifact hash for asset \"87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dba\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { + "Type": "String", + "Description": "S3 bucket for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F": { + "Type": "String", + "Description": "S3 key for asset version \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1ArtifactHashA521A16F": { + "Type": "String", + "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3Bucket008DBB35": { + "Type": "String", + "Description": "S3 bucket for asset \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" + }, + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKey97C3E1A0": { + "Type": "String", + "Description": "S3 key for asset version \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" + }, + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757ArtifactHashF584A7D8": { + "Type": "String", + "Description": "Artifact hash for asset \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" + }, + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket6DACDE73": { + "Type": "String", + "Description": "S3 bucket for asset \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" + }, + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey015AEA61": { + "Type": "String", + "Description": "S3 key for asset version \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" + }, + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1ArtifactHashC9FD06BA": { + "Type": "String", + "Description": "Artifact hash for asset \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" + }, + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket7EE7EA15": { + "Type": "String", + "Description": "S3 bucket for asset \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" + }, + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C948E78": { + "Type": "String", + "Description": "S3 key for asset version \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" + }, + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fArtifactHash7E705796": { + "Type": "String", + "Description": "Artifact hash for asset \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" + }, + "AssetParameters1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172cS3Bucket151BE34C": { + "Type": "String", + "Description": "S3 bucket for asset \"1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172c\"" + }, + "AssetParameters1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172cS3VersionKey89E7CC67": { + "Type": "String", + "Description": "S3 key for asset version \"1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172c\"" + }, + "AssetParameters1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172cArtifactHashAEE8C2AB": { + "Type": "String", + "Description": "Artifact hash for asset \"1a2bf12b9f0cf5ab2c838e7dd9be4d485bbf32056d6d5333bce57e49d12a172c\"" + }, + "AssetParameters11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7S3BucketE510C342": { + "Type": "String", + "Description": "S3 bucket for asset \"11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7\"" + }, + "AssetParameters11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7S3VersionKeyD31A83B6": { + "Type": "String", + "Description": "S3 key for asset version \"11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7\"" + }, + "AssetParameters11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7ArtifactHash1C37295C": { + "Type": "String", + "Description": "Artifact hash for asset \"11ba420a0c99f0c77f563fb974e76d6110b4445114137af1fe1b69b0d366d2d7\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.ts new file mode 100644 index 0000000000000..ec9fe15d081da --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.ts @@ -0,0 +1,25 @@ +/// !cdk-integ pragma:ignore-assets +import { App } from '@aws-cdk/core'; +import * as eks from '../lib'; +import { TestStack } from './util'; + +const CLUSTER_VERSION = eks.KubernetesVersion.V1_18; + + +class EksAllHandlersInVpcStack extends TestStack { + + constructor(scope: App, id: string) { + super(scope, id); + + new eks.Cluster(this, 'EksAllHandlersInVpcStack', { + version: CLUSTER_VERSION, + placeClusterHandlerInVpc: true, + }); + } +} + +const app = new App(); + +new EksAllHandlersInVpcStack(app, 'aws-cdk-eks-handlers-in-vpc-test'); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json index bf6763aa4ece1..f76e62c73ee73 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json @@ -914,7 +914,8 @@ "ClusterCreationRole360249B6", "Arn" ] - } + }, + "Overwrite": true }, "DependsOn": [ "ClusterKubectlReadyBarrier200052AF" @@ -1061,7 +1062,7 @@ }, "/", { - "Ref": "AssetParameters570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51S3Bucket9A573B14" + "Ref": "AssetParameters84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08beS3Bucket9E737267" }, "/", { @@ -1071,7 +1072,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51S3VersionKeyE2BADBC0" + "Ref": "AssetParameters84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08beS3VersionKeyD5E002BC" } ] } @@ -1084,7 +1085,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51S3VersionKeyE2BADBC0" + "Ref": "AssetParameters84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08beS3VersionKeyD5E002BC" } ] } @@ -1100,17 +1101,17 @@ "Arn" ] }, - "referencetoawscdkeksclusterprivateendpointtestAssetParameters7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4S3Bucket4C62B914Ref": { - "Ref": "AssetParameters7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4S3Bucket086F94BB" + "referencetoawscdkeksclusterprivateendpointtestAssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3Bucket0D497746Ref": { + "Ref": "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3Bucket14D204F9" }, - "referencetoawscdkeksclusterprivateendpointtestAssetParameters7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4S3VersionKey8874BF8DRef": { - "Ref": "AssetParameters7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4S3VersionKeyA4B5C598" + "referencetoawscdkeksclusterprivateendpointtestAssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3VersionKeyC516A514Ref": { + "Ref": "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3VersionKeyDE8A2F1F" }, - "referencetoawscdkeksclusterprivateendpointtestAssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3Bucket41FE7429Ref": { - "Ref": "AssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3BucketD25BCC90" + "referencetoawscdkeksclusterprivateendpointtestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket7DDAFC04Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" }, - "referencetoawscdkeksclusterprivateendpointtestAssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3VersionKeyE935A11ARef": { - "Ref": "AssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3VersionKey72DFE7A5" + "referencetoawscdkeksclusterprivateendpointtestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKey69BACD98Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } } } @@ -1128,7 +1129,7 @@ }, "/", { - "Ref": "AssetParameterse843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796S3Bucket39E2BF35" + "Ref": "AssetParameters2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0S3Bucket8FBFE327" }, "/", { @@ -1138,7 +1139,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796S3VersionKey0218A255" + "Ref": "AssetParameters2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0S3VersionKeyF5A05918" } ] } @@ -1151,7 +1152,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796S3VersionKey0218A255" + "Ref": "AssetParameters2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0S3VersionKeyF5A05918" } ] } @@ -1173,11 +1174,11 @@ "Arn" ] }, - "referencetoawscdkeksclusterprivateendpointtestAssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3Bucket59F91893Ref": { - "Ref": "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3Bucket9ABBD5A2" + "referencetoawscdkeksclusterprivateendpointtestAssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3BucketD11B5EC1Ref": { + "Ref": "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3Bucket008DBB35" }, - "referencetoawscdkeksclusterprivateendpointtestAssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3VersionKey4D2CDF61Ref": { - "Ref": "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3VersionKey40FF2C4A" + "referencetoawscdkeksclusterprivateendpointtestAssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKey8375F1D2Ref": { + "Ref": "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKey97C3E1A0" }, "referencetoawscdkeksclusterprivateendpointtestVpcPrivateSubnet1Subnet94DAD769Ref": { "Ref": "VpcPrivateSubnet1Subnet536B997A" @@ -1194,11 +1195,26 @@ "ClusterSecurityGroupId" ] }, - "referencetoawscdkeksclusterprivateendpointtestAssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3Bucket41FE7429Ref": { - "Ref": "AssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3BucketD25BCC90" + "referencetoawscdkeksclusterprivateendpointtestAssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket4A0D6BE2Ref": { + "Ref": "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket6DACDE73" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey6D9B8A02Ref": { + "Ref": "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey015AEA61" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3BucketD44FB215Ref": { + "Ref": "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket7EE7EA15" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C30661CRef": { + "Ref": "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C948E78" }, - "referencetoawscdkeksclusterprivateendpointtestAssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3VersionKeyE935A11ARef": { - "Ref": "AssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3VersionKey72DFE7A5" + "referencetoawscdkeksclusterprivateendpointtestVpcFCD064BFRef": { + "Ref": "Vpc8378EB38" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket7DDAFC04Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" + }, + "referencetoawscdkeksclusterprivateendpointtestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKey69BACD98Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } } } @@ -1247,65 +1263,89 @@ } }, "Parameters": { - "AssetParameters7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4S3Bucket086F94BB": { + "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3Bucket14D204F9": { + "Type": "String", + "Description": "S3 bucket for asset \"87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dba\"" + }, + "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3VersionKeyDE8A2F1F": { + "Type": "String", + "Description": "S3 key for asset version \"87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dba\"" + }, + "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaArtifactHash54822A43": { + "Type": "String", + "Description": "Artifact hash for asset \"87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dba\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { + "Type": "String", + "Description": "S3 bucket for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F": { + "Type": "String", + "Description": "S3 key for asset version \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1ArtifactHashA521A16F": { + "Type": "String", + "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3Bucket008DBB35": { "Type": "String", - "Description": "S3 bucket for asset \"7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4\"" + "Description": "S3 bucket for asset \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" }, - "AssetParameters7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4S3VersionKeyA4B5C598": { + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKey97C3E1A0": { "Type": "String", - "Description": "S3 key for asset version \"7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4\"" + "Description": "S3 key for asset version \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" }, - "AssetParameters7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4ArtifactHash9B26D532": { + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757ArtifactHashF584A7D8": { "Type": "String", - "Description": "Artifact hash for asset \"7997347617940455774a736af2df2e6238c13b755ad25353a3d081446cfc80a4\"" + "Description": "Artifact hash for asset \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" }, - "AssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3BucketD25BCC90": { + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket6DACDE73": { "Type": "String", - "Description": "S3 bucket for asset \"34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1\"" + "Description": "S3 bucket for asset \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" }, - "AssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1S3VersionKey72DFE7A5": { + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey015AEA61": { "Type": "String", - "Description": "S3 key for asset version \"34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1\"" + "Description": "S3 key for asset version \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" }, - "AssetParameters34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1ArtifactHashAA0236EE": { + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1ArtifactHashC9FD06BA": { "Type": "String", - "Description": "Artifact hash for asset \"34131c2e554ab57ad3a47fc0a13173a5c2a4b65a7582fe9622277b3d04c8e1e1\"" + "Description": "Artifact hash for asset \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" }, - "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3Bucket9ABBD5A2": { + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket7EE7EA15": { "Type": "String", - "Description": "S3 bucket for asset \"b7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2\"" + "Description": "S3 bucket for asset \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, - "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3VersionKey40FF2C4A": { + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C948E78": { "Type": "String", - "Description": "S3 key for asset version \"b7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2\"" + "Description": "S3 key for asset version \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, - "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2ArtifactHashE86B38C7": { + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fArtifactHash7E705796": { "Type": "String", - "Description": "Artifact hash for asset \"b7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2\"" + "Description": "Artifact hash for asset \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, - "AssetParameters570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51S3Bucket9A573B14": { + "AssetParameters84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08beS3Bucket9E737267": { "Type": "String", - "Description": "S3 bucket for asset \"570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51\"" + "Description": "S3 bucket for asset \"84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08be\"" }, - "AssetParameters570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51S3VersionKeyE2BADBC0": { + "AssetParameters84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08beS3VersionKeyD5E002BC": { "Type": "String", - "Description": "S3 key for asset version \"570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51\"" + "Description": "S3 key for asset version \"84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08be\"" }, - "AssetParameters570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51ArtifactHash754674EC": { + "AssetParameters84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08beArtifactHashDF0A0444": { "Type": "String", - "Description": "Artifact hash for asset \"570f91ed45d0c45e8ff145969f7499419312e806c83f009b76539ce989960e51\"" + "Description": "Artifact hash for asset \"84ba29b05aaf6a233dbb97b37e48eb1300f9d014f270252e29a8b2c22d6a08be\"" }, - "AssetParameterse843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796S3Bucket39E2BF35": { + "AssetParameters2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0S3Bucket8FBFE327": { "Type": "String", - "Description": "S3 bucket for asset \"e843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796\"" + "Description": "S3 bucket for asset \"2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0\"" }, - "AssetParameterse843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796S3VersionKey0218A255": { + "AssetParameters2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0S3VersionKeyF5A05918": { "Type": "String", - "Description": "S3 key for asset version \"e843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796\"" + "Description": "S3 key for asset version \"2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0\"" }, - "AssetParameterse843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796ArtifactHash0AFD7EAC": { + "AssetParameters2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0ArtifactHashDFBC9DE7": { "Type": "String", - "Description": "Artifact hash for asset \"e843c57c7bcb07856b1680280dc9387725661764509856e890ae6e18a5e40796\"" + "Description": "Artifact hash for asset \"2e2ec0fae5975d4ee5f3580e522c46615c1bd344e0302bc5d2df7501b7bb1ad0\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index d271b7029e942..f62d6a1fa0d9a 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -1251,6 +1251,13 @@ ] }, "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterNodegroupextrangspotNodeGroupRoleB53B4857", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]},{\\\"rolearn\\\":\\\"", { "Fn::GetAtt": [ "ClusterNodegroupextrangarmNodeGroupRoleADF5749F", @@ -1277,7 +1284,8 @@ "Arn" ] }, - "PruneLabel": "aws.cdk.eks/prune-c842be348c45337cd97b8759de76d5a68b4910d487" + "PruneLabel": "aws.cdk.eks/prune-c842be348c45337cd97b8759de76d5a68b4910d487", + "Overwrite": true }, "DependsOn": [ "ClusterKubectlReadyBarrier200052AF" @@ -1701,6 +1709,12 @@ "ClusterNodesInstanceSecurityGroup899246BD", "GroupId" ] + }, + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] } ], "UserData": { @@ -2020,6 +2034,12 @@ "ClusterNodesArmInstanceSecurityGroup599F388B", "GroupId" ] + }, + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] } ], "UserData": { @@ -2339,6 +2359,12 @@ "ClusterBottlerocketNodesInstanceSecurityGroup3794A94B", "GroupId" ] + }, + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] } ], "UserData": { @@ -2672,6 +2698,12 @@ "ClusterspotInstanceSecurityGroup01F7B1CE", "GroupId" ] + }, + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] } ], "SpotPrice": "0.1094", @@ -2762,7 +2794,7 @@ }, "Release": "ksclustertestclusterchartspotinterrupthandlerf41ba997", "Chart": "aws-node-termination-handler", - "Version": "0.9.5", + "Version": "0.13.2", "Values": "{\"nodeSelector.lifecycle\":\"Ec2Spot\"}", "Namespace": "kube-system", "Repository": "https://aws.github.io/eks-charts", @@ -3024,6 +3056,12 @@ "ClusterInferenceInstancesInstanceSecurityGroupECB3FC45", "GroupId" ] + }, + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] } ], "UserData": { @@ -3220,6 +3258,109 @@ } } }, + "ClusterNodegroupextrangspotNodeGroupRoleB53B4857": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSWorkerNodePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKS_CNI_Policy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ] + ] + } + ] + } + }, + "ClusterNodegroupextrangspotB327AE6B": { + "Type": "AWS::EKS::Nodegroup", + "Properties": { + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "NodeRole": { + "Fn::GetAtt": [ + "ClusterNodegroupextrangspotNodeGroupRoleB53B4857", + "Arn" + ] + }, + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ], + "AmiType": "AL2_x86_64", + "ForceUpdateEnabled": true, + "InstanceTypes": [ + "c5.large", + "c5a.large", + "c5d.large" + ], + "ScalingConfig": { + "DesiredSize": 3, + "MaxSize": 3, + "MinSize": 3 + }, + "CapacityType": "SPOT" + } + }, "ClusterNodegroupextrangarmNodeGroupRoleADF5749F": { "Type": "AWS::IAM::Role", "Properties": { @@ -3836,7 +3977,7 @@ }, "/", { - "Ref": "AssetParametersfc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384aS3BucketCF0594BD" + "Ref": "AssetParameters5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853S3Bucket3EB15EF2" }, "/", { @@ -3846,7 +3987,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersfc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384aS3VersionKey1CB38323" + "Ref": "AssetParameters5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853S3VersionKeyD6A244FC" } ] } @@ -3859,7 +4000,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersfc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384aS3VersionKey1CB38323" + "Ref": "AssetParameters5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853S3VersionKeyD6A244FC" } ] } @@ -3881,11 +4022,11 @@ "Arn" ] }, - "referencetoawscdkeksclustertestAssetParameters81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74S3Bucket4AFE6229Ref": { - "Ref": "AssetParameters81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74S3BucketFC3C258F" + "referencetoawscdkeksclustertestAssetParametersd01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34fS3Bucket3AA74A74Ref": { + "Ref": "AssetParametersd01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34fS3Bucket81EA5F11" }, - "referencetoawscdkeksclustertestAssetParameters81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74S3VersionKey49D5E273Ref": { - "Ref": "AssetParameters81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74S3VersionKey6820D47C" + "referencetoawscdkeksclustertestAssetParametersd01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34fS3VersionKey2EF124C2Ref": { + "Ref": "AssetParametersd01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34fS3VersionKey32DED07C" }, "referencetoawscdkeksclustertestVpcPrivateSubnet1Subnet32A4EC2ARef": { "Ref": "VpcPrivateSubnet1Subnet536B997A" @@ -3902,6 +4043,21 @@ "ClusterSecurityGroupId" ] }, + "referencetoawscdkeksclustertestAssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket69155862Ref": { + "Ref": "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket6DACDE73" + }, + "referencetoawscdkeksclustertestAssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey0A6CC98ARef": { + "Ref": "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey015AEA61" + }, + "referencetoawscdkeksclustertestAssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3BucketDD492793Ref": { + "Ref": "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket7EE7EA15" + }, + "referencetoawscdkeksclustertestAssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKeyD869415CRef": { + "Ref": "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C948E78" + }, + "referencetoawscdkeksclustertestVpc9A302ADDRef": { + "Ref": "Vpc8378EB38" + }, "referencetoawscdkeksclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket0815E7B5Ref": { "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" }, @@ -3935,6 +4091,34 @@ } } }, + "HelloAppWithoutValidation7C638ACB": { + "Type": "Custom::AWSCDK-EKS-KubernetesResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkeksclustertestawscdkawseksKubectlProviderframeworkonEventC681B49AArn" + ] + }, + "Manifest": "[{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"data\":{\"hello\":\"world\"},\"metadata\":{\"name\":\"config-map\",\"labels\":{\"aws.cdk.eks/prune-c89cbcc5d9bdd35cfc69c0334c0a9af21d1e0e372e\":\"\"}},\"unknown\":{\"key\":\"value\"}}]", + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "RoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + }, + "PruneLabel": "aws.cdk.eks/prune-c89cbcc5d9bdd35cfc69c0334c0a9af21d1e0e372e", + "SkipValidation": true + }, + "DependsOn": [ + "ClusterKubectlReadyBarrier200052AF" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65": { "Type": "AWS::IAM::Role", "Properties": { @@ -4541,17 +4725,41 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParameters81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74S3BucketFC3C258F": { + "AssetParametersd01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34fS3Bucket81EA5F11": { + "Type": "String", + "Description": "S3 bucket for asset \"d01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34f\"" + }, + "AssetParametersd01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34fS3VersionKey32DED07C": { + "Type": "String", + "Description": "S3 key for asset version \"d01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34f\"" + }, + "AssetParametersd01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34fArtifactHashE68669BA": { + "Type": "String", + "Description": "Artifact hash for asset \"d01b2d8959358117de0017e6f18135905e5680cfc8a83e406229c02671c2b34f\"" + }, + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket6DACDE73": { + "Type": "String", + "Description": "S3 bucket for asset \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" + }, + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey015AEA61": { + "Type": "String", + "Description": "S3 key for asset version \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" + }, + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1ArtifactHashC9FD06BA": { + "Type": "String", + "Description": "Artifact hash for asset \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" + }, + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket7EE7EA15": { "Type": "String", - "Description": "S3 bucket for asset \"81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74\"" + "Description": "S3 bucket for asset \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, - "AssetParameters81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74S3VersionKey6820D47C": { + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C948E78": { "Type": "String", - "Description": "S3 key for asset version \"81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74\"" + "Description": "S3 key for asset version \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, - "AssetParameters81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74ArtifactHash1DF738E9": { + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fArtifactHash7E705796": { "Type": "String", - "Description": "Artifact hash for asset \"81ef9ae09d999514914c8c39a2f87e135a40bd56cd33f2fad771824d5072fd74\"" + "Description": "Artifact hash for asset \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3Bucket14156880": { "Type": "String", @@ -4601,17 +4809,17 @@ "Type": "String", "Description": "Artifact hash for asset \"a69aadbed84d554dd9f2eb7987ffe5d8f76b53a86f1909059df07050e57bef0c\"" }, - "AssetParametersfc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384aS3BucketCF0594BD": { + "AssetParameters5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853S3Bucket3EB15EF2": { "Type": "String", - "Description": "S3 bucket for asset \"fc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384a\"" + "Description": "S3 bucket for asset \"5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853\"" }, - "AssetParametersfc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384aS3VersionKey1CB38323": { + "AssetParameters5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853S3VersionKeyD6A244FC": { "Type": "String", - "Description": "S3 key for asset version \"fc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384a\"" + "Description": "S3 key for asset version \"5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853\"" }, - "AssetParametersfc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384aArtifactHash25E56295": { + "AssetParameters5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853ArtifactHashD763BE57": { "Type": "String", - "Description": "Artifact hash for asset \"fc3e757b1108608694a6c0e92715f352dc4c7ca05a94991b95050f3cbe71384a\"" + "Description": "Artifact hash for asset \"5b4a9f125b1d010c96760d55e0fc56362a73e6ca6da3af20a4d13ea27e369853\"" }, "SsmParameterValueawsserviceeksoptimizedami118amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.ts deleted file mode 100644 index 6bf5fd6f7dad0..0000000000000 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as kms from '@aws-cdk/aws-kms'; -import * as cdk from '@aws-cdk/core'; -import * as eks from '../lib'; -import { TestStack } from './util'; - -const CLUSTER_VERSION = eks.KubernetesVersion.V1_18; - -class EksClusterStack extends TestStack { - constructor(scope: cdk.App, id: string) { - super(scope, id); - - const vpc = new ec2.Vpc(this, 'VPC'); - - const secretsEncryptionKey = new kms.Key(this, 'SecretsKey'); - - const cluster = new eks.LegacyCluster(this, 'EKSCluster', { - vpc, - defaultCapacity: 0, - version: CLUSTER_VERSION, - secretsEncryptionKey, - }); - - cluster.addCapacity('Nodes', { - instanceType: new ec2.InstanceType('t2.medium'), - minCapacity: 1, // Raise this number to add more nodes - }); - } -} - -const app = new cdk.App(); - -// since the EKS optimized AMI is hard-coded here based on the region, -// we need to actually pass in a specific region. -new EksClusterStack(app, 'eks-integ-kubectl-disabled'); - -app.synth(); diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts index d8522eb543619..12c7334bd96a5 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts @@ -53,12 +53,16 @@ class EksClusterStack extends TestStack { this.assertNodeGroupX86(); + this.assertNodeGroupSpot(); + this.assertNodeGroupArm(); this.assertNodeGroupCustomAmi(); this.assertSimpleManifest(); + this.assertManifestWithoutValidation(); + this.assertSimpleHelmChart(); this.assertSimpleCdk8sChart(); @@ -137,6 +141,20 @@ class EksClusterStack extends TestStack { // apply a kubernetes manifest this.cluster.addManifest('HelloApp', ...hello.resources); } + private assertManifestWithoutValidation() { + // apply a kubernetes manifest + new eks.KubernetesManifest(this, 'HelloAppWithoutValidation', { + cluster: this.cluster, + manifest: [{ + apiVersion: 'v1', + kind: 'ConfigMap', + data: { hello: 'world' }, + metadata: { name: 'config-map' }, + unknown: { key: 'value' }, + }], + skipValidation: true, + }); + } private assertNodeGroupX86() { // add a extra nodegroup this.cluster.addNodegroupCapacity('extra-ng', { @@ -146,6 +164,20 @@ class EksClusterStack extends TestStack { nodeRole: this.cluster.defaultCapacity ? this.cluster.defaultCapacity.role : undefined, }); } + private assertNodeGroupSpot() { + // add a extra nodegroup + this.cluster.addNodegroupCapacity('extra-ng-spot', { + instanceTypes: [ + new ec2.InstanceType('c5.large'), + new ec2.InstanceType('c5a.large'), + new ec2.InstanceType('c5d.large'), + ], + minSize: 3, + // reusing the default capacity nodegroup instance role when available + nodeRole: this.cluster.defaultCapacity ? this.cluster.defaultCapacity.role : undefined, + capacityType: eks.CapacityType.SPOT, + }); + } private assertNodeGroupCustomAmi() { // add a extra nodegroup const userData = ec2.UserData.forLinux(); diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-oidc-provider.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-oidc-provider.expected.json index 615f6f5d97d00..b6a2cad449f9c 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-oidc-provider.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-oidc-provider.expected.json @@ -9,12 +9,12 @@ "Arn" ] }, - "ThumbprintList": [ - "9e99a48a9960b14926bb7f3b02e22da2b0ab7280" - ], "ClientIDList": [ "sts.amazonaws.com" ], + "ThumbprintList": [ + "9e99a48a9960b14926bb7f3b02e22da2b0ab7280" + ], "Url": { "Fn::Join": [ "", @@ -145,4 +145,4 @@ "Description": "Artifact hash for asset \"b075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0de\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.fargate-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.fargate-cluster.expected.json index b8f3d7dad9a74..5d6efed26b63f 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.fargate-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.fargate-cluster.expected.json @@ -1018,7 +1018,8 @@ "FargateClusterCreationRole8C524AD8", "Arn" ] - } + }, + "Overwrite": true }, "DependsOn": [ "FargateClusterKubectlReadyBarrier93746934" @@ -1138,7 +1139,7 @@ }, "/", { - "Ref": "AssetParametersbedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89S3Bucket60C2BF28" + "Ref": "AssetParametersae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48S3BucketDA5FB24D" }, "/", { @@ -1148,7 +1149,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersbedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89S3VersionKey81C20166" + "Ref": "AssetParametersae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48S3VersionKey798A3941" } ] } @@ -1161,7 +1162,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersbedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89S3VersionKey81C20166" + "Ref": "AssetParametersae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48S3VersionKey798A3941" } ] } @@ -1183,11 +1184,11 @@ "referencetoawscdkeksfargateclustertestAssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3VersionKey56570425Ref": { "Ref": "AssetParameters87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dbaS3VersionKeyDE8A2F1F" }, - "referencetoawscdkeksfargateclustertestAssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3Bucket7ABEDF68Ref": { - "Ref": "AssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3Bucket8132A6E0" + "referencetoawscdkeksfargateclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket8EEF0922Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" }, - "referencetoawscdkeksfargateclustertestAssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3VersionKey810DC943Ref": { - "Ref": "AssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3VersionKey722E831A" + "referencetoawscdkeksfargateclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKey47333356Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } } } @@ -1205,7 +1206,7 @@ }, "/", { - "Ref": "AssetParametersc3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7cS3Bucket1E579A0A" + "Ref": "AssetParameters7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98dS3BucketF39EF776" }, "/", { @@ -1215,7 +1216,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7cS3VersionKey91701E13" + "Ref": "AssetParameters7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98dS3VersionKeyE6E734A4" } ] } @@ -1228,7 +1229,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7cS3VersionKey91701E13" + "Ref": "AssetParameters7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98dS3VersionKeyE6E734A4" } ] } @@ -1250,11 +1251,11 @@ "Arn" ] }, - "referencetoawscdkeksfargateclustertestAssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3BucketF3D15942Ref": { - "Ref": "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3Bucket9ABBD5A2" + "referencetoawscdkeksfargateclustertestAssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3Bucket78A48DE4Ref": { + "Ref": "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3Bucket008DBB35" }, - "referencetoawscdkeksfargateclustertestAssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3VersionKey362BF04DRef": { - "Ref": "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3VersionKey40FF2C4A" + "referencetoawscdkeksfargateclustertestAssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKey69DB854ARef": { + "Ref": "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKey97C3E1A0" }, "referencetoawscdkeksfargateclustertestFargateClusterDefaultVpcPrivateSubnet1Subnet0278E6BCRef": { "Ref": "FargateClusterDefaultVpcPrivateSubnet1Subnet50EA43AA" @@ -1271,11 +1272,26 @@ "ClusterSecurityGroupId" ] }, - "referencetoawscdkeksfargateclustertestAssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3Bucket7ABEDF68Ref": { - "Ref": "AssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3Bucket8132A6E0" + "referencetoawscdkeksfargateclustertestAssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket9D3BB190Ref": { + "Ref": "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket6DACDE73" + }, + "referencetoawscdkeksfargateclustertestAssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey3BB3C6F5Ref": { + "Ref": "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey015AEA61" + }, + "referencetoawscdkeksfargateclustertestAssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket99BFDD36Ref": { + "Ref": "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket7EE7EA15" + }, + "referencetoawscdkeksfargateclustertestAssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKeyEEC9E8C1Ref": { + "Ref": "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C948E78" }, - "referencetoawscdkeksfargateclustertestAssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3VersionKey810DC943Ref": { - "Ref": "AssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3VersionKey722E831A" + "referencetoawscdkeksfargateclustertestFargateClusterDefaultVpcBD3C976FRef": { + "Ref": "FargateClusterDefaultVpcE69D3A13" + }, + "referencetoawscdkeksfargateclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket8EEF0922Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" + }, + "referencetoawscdkeksfargateclustertestAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKey47333356Ref": { + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } } } @@ -1336,53 +1352,77 @@ "Type": "String", "Description": "Artifact hash for asset \"87b1e2c41f84590d14f7ab8cb0f338c51d6fa3efe78943867af07fa959593dba\"" }, - "AssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3Bucket8132A6E0": { + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { + "Type": "String", + "Description": "S3 bucket for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F": { + "Type": "String", + "Description": "S3 key for asset version \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1ArtifactHashA521A16F": { + "Type": "String", + "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" + }, + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3Bucket008DBB35": { + "Type": "String", + "Description": "S3 bucket for asset \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" + }, + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757S3VersionKey97C3E1A0": { + "Type": "String", + "Description": "S3 key for asset version \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" + }, + "AssetParametersbafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757ArtifactHashF584A7D8": { + "Type": "String", + "Description": "Artifact hash for asset \"bafd50ae9f214e496ff8c72c6425f93dca3ccd590e20963706d5d610d9c75757\"" + }, + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3Bucket6DACDE73": { "Type": "String", - "Description": "S3 bucket for asset \"5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0c\"" + "Description": "S3 bucket for asset \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" }, - "AssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cS3VersionKey722E831A": { + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1S3VersionKey015AEA61": { "Type": "String", - "Description": "S3 key for asset version \"5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0c\"" + "Description": "S3 key for asset version \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" }, - "AssetParameters5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0cArtifactHash67988836": { + "AssetParametersefd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1ArtifactHashC9FD06BA": { "Type": "String", - "Description": "Artifact hash for asset \"5db52e19f1f79cac27e817fa59d0b1f73d524301b679e2e7354122e474fcba0c\"" + "Description": "Artifact hash for asset \"efd72738f046105c96299fb31b3da40320e71ee9cf74bc37720042898403e2a1\"" }, - "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3Bucket9ABBD5A2": { + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3Bucket7EE7EA15": { "Type": "String", - "Description": "S3 bucket for asset \"b7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2\"" + "Description": "S3 bucket for asset \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, - "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2S3VersionKey40FF2C4A": { + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fS3VersionKey6C948E78": { "Type": "String", - "Description": "S3 key for asset version \"b7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2\"" + "Description": "S3 key for asset version \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, - "AssetParametersb7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2ArtifactHashE86B38C7": { + "AssetParametersb61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449fArtifactHash7E705796": { "Type": "String", - "Description": "Artifact hash for asset \"b7d8a9750f8bfded8ac76be100e3bee1c3d4824df006766110d023f42952f5c2\"" + "Description": "Artifact hash for asset \"b61858bbf1a0be803552e3efa9647befd728156696dff1b413b7b2fd4da1449f\"" }, - "AssetParametersbedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89S3Bucket60C2BF28": { + "AssetParametersae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48S3BucketDA5FB24D": { "Type": "String", - "Description": "S3 bucket for asset \"bedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89\"" + "Description": "S3 bucket for asset \"ae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48\"" }, - "AssetParametersbedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89S3VersionKey81C20166": { + "AssetParametersae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48S3VersionKey798A3941": { "Type": "String", - "Description": "S3 key for asset version \"bedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89\"" + "Description": "S3 key for asset version \"ae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48\"" }, - "AssetParametersbedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89ArtifactHashC2E43922": { + "AssetParametersae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48ArtifactHash865DB842": { "Type": "String", - "Description": "Artifact hash for asset \"bedcab6ba6cdb530ab9574b630651abdee4c67fb22c69ad17ab3b4369d3b7c89\"" + "Description": "Artifact hash for asset \"ae946640aaf0743990584e4a1cf45ddebbaddcaf60611f572e80100a02162f48\"" }, - "AssetParametersc3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7cS3Bucket1E579A0A": { + "AssetParameters7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98dS3BucketF39EF776": { "Type": "String", - "Description": "S3 bucket for asset \"c3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7c\"" + "Description": "S3 bucket for asset \"7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98d\"" }, - "AssetParametersc3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7cS3VersionKey91701E13": { + "AssetParameters7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98dS3VersionKeyE6E734A4": { "Type": "String", - "Description": "S3 key for asset version \"c3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7c\"" + "Description": "S3 key for asset version \"7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98d\"" }, - "AssetParametersc3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7cArtifactHash4D9F989B": { + "AssetParameters7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98dArtifactHash93FFAA4A": { "Type": "String", - "Description": "Artifact hash for asset \"c3e7ad226d0efc3c9ecf3b4c84ea434556c67008349c6cc43c1cdb58323ddf7c\"" + "Description": "Artifact hash for asset \"7449e3c4cf21a811d1d6612d2f1a806025f018320ebc8c1d8037eb34f0d0e98d\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index d57c18561c2b4..fa63e9edc5112 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -12,7 +12,6 @@ import * as constructs from 'constructs'; import { Test } from 'nodeunit'; import * as YAML from 'yaml'; import * as eks from '../lib'; -import * as kubectl from '../lib/kubectl-provider'; import { BottleRocketImage } from '../lib/private/bottlerocket'; import { testFixture, testFixtureNoVpc } from './util'; @@ -22,6 +21,151 @@ const CLUSTER_VERSION = eks.KubernetesVersion.V1_18; export = { + 'can specify custom environment to cluster resource handler'(test: Test) { + + const { stack } = testFixture(); + + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + clusterHandlerEnvironment: { + foo: 'bar', + }, + }); + + const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.ClusterResourceProvider') as cdk.NestedStack; + + test.deepEqual(expect(nested).value.Resources.OnEventHandler42BEBAE0.Properties.Environment, { Variables: { foo: 'bar' } }); + test.done(); + + }, + + 'throws when trying to place cluster handlers in a vpc with no private subnets'(test: Test) { + + const { stack } = testFixture(); + + const vpc = new ec2.Vpc(stack, 'Vpc'); + + test.throws(() => { + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + placeClusterHandlerInVpc: true, + vpc: vpc, + vpcSubnets: [{ subnetType: ec2.SubnetType.PUBLIC }], + }); + }, /Cannot place cluster handler in the VPC since no private subnets could be selected/); + + test.done(); + }, + + 'throws when accessing cluster security group for imported cluster without cluster security group id'(test: Test) { + + const { stack } = testFixture(); + + const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { + clusterName: 'cluster', + }); + + test.throws(() => cluster.clusterSecurityGroup, /"clusterSecurityGroup" is not defined for this imported cluster/); + test.done(); + + }, + + 'can place cluster handlers in the cluster vpc'(test: Test) { + + const { stack } = testFixture(); + + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + placeClusterHandlerInVpc: true, + }); + + const nested = stack.node.tryFindChild('@aws-cdk/aws-eks.ClusterResourceProvider') as cdk.NestedStack; + + function assertFunctionPlacedInVpc(id: string) { + test.deepEqual(expect(nested).value.Resources[id].Properties.VpcConfig.SubnetIds, [ + { Ref: 'referencetoStackClusterDefaultVpcPrivateSubnet1SubnetA64D1BF0Ref' }, + { Ref: 'referencetoStackClusterDefaultVpcPrivateSubnet2Subnet32D85AB8Ref' }, + ]); + } + + assertFunctionPlacedInVpc('OnEventHandler42BEBAE0'); + assertFunctionPlacedInVpc('IsCompleteHandler7073F4DA'); + assertFunctionPlacedInVpc('ProviderframeworkonEvent83C1D0A7'); + assertFunctionPlacedInVpc('ProviderframeworkisComplete26D7B0CB'); + assertFunctionPlacedInVpc('ProviderframeworkonTimeout0B47CA38'); + + test.done(); + }, + + 'can access cluster security group for imported cluster with cluster security group id'(test: Test) { + + const { stack } = testFixture(); + + const clusterSgId = 'cluster-sg-id'; + + const cluster = eks.Cluster.fromClusterAttributes(stack, 'Cluster', { + clusterName: 'cluster', + clusterSecurityGroupId: clusterSgId, + }); + + const clusterSg = cluster.clusterSecurityGroup; + + test.equal(clusterSg.securityGroupId, clusterSgId); + test.done(); + }, + + 'cluster security group is attached when adding self-managed nodes'(test: Test) { + + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + prune: false, + }); + + // WHEN + cluster.addAutoScalingGroupCapacity('self-managed', { + instanceType: new ec2.InstanceType('t2.medium'), + }); + + test.deepEqual(expect(stack).value.Resources.ClusterselfmanagedLaunchConfigA5B57EF6.Properties.SecurityGroups, [ + { 'Fn::GetAtt': ['ClusterselfmanagedInstanceSecurityGroup64468C3A', 'GroupId'] }, + { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, + ]); + test.done(); + + }, + + 'cluster security group is attached when connecting self-managed nodes'(test: Test) { + + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + prune: false, + }); + + const selfManaged = new asg.AutoScalingGroup(stack, 'self-managed', { + instanceType: new ec2.InstanceType('t2.medium'), + vpc: vpc, + machineImage: new ec2.AmazonLinuxImage(), + }); + + // WHEN + cluster.connectAutoScalingGroupCapacity(selfManaged, {}); + + test.deepEqual(expect(stack).value.Resources.selfmanagedLaunchConfigD41289EB.Properties.SecurityGroups, [ + { 'Fn::GetAtt': ['selfmanagedInstanceSecurityGroupEA6D80C9', 'GroupId'] }, + { 'Fn::GetAtt': ['Cluster9EE0221C', 'ClusterSecurityGroupId'] }, + ]); + test.done(); + + }, + 'throws when a non cdk8s chart construct is added as cdk8s chart'(test: Test) { const { stack } = testFixture(); @@ -392,46 +536,6 @@ export = { test.done(); }, - 'create custom cluster correctly in any aws region'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'stack', { env: { region: 'us-east-1' } }); - - // WHEN - const vpc = new ec2.Vpc(stack, 'VPC'); - new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); - kubectl.getOrCreateKubectlLayer(stack); - - // THEN - expect(stack).to(haveResource('Custom::AWSCDK-EKS-Cluster')); - expect(stack).to(haveResourceLike('AWS::Serverless::Application', { - Location: { - ApplicationId: 'arn:aws:serverlessrepo:us-east-1:903779448426:applications/lambda-layer-kubectl', - }, - })); - test.done(); - }, - - 'create custom cluster correctly in any aws region in china'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'stack', { env: { region: 'cn-north-1' } }); - - // WHEN - const vpc = new ec2.Vpc(stack, 'VPC'); - new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION, prune: false }); - kubectl.getOrCreateKubectlLayer(stack); - - // THEN - expect(stack).to(haveResource('Custom::AWSCDK-EKS-Cluster')); - expect(stack).to(haveResourceLike('AWS::Serverless::Application', { - Location: { - ApplicationId: 'arn:aws-cn:serverlessrepo:cn-north-1:487369736442:applications/lambda-layer-kubectl', - }, - })); - test.done(); - }, - 'if "vpc" is not specified, vpc with default configuration will be created'(test: Test) { // GIVEN const { stack } = testFixtureNoVpc(); @@ -2575,38 +2679,6 @@ export = { test.done(); }, - 'SAR-based kubectl layer can be customized'(test: Test) { - // GIVEN - const { stack } = testFixture(); - - // WHEN - const layer = new eks.KubectlLayer(stack, 'Kubectl', { - applicationId: 'custom:app:id', - version: '2.3.4', - }); - - new eks.Cluster(stack, 'Cluster1', { - version: CLUSTER_VERSION, - prune: false, - kubectlLayer: layer, - }); - - // THEN - const providerStack = stack.node.tryFindChild('@aws-cdk/aws-eks.KubectlProvider') as cdk.NestedStack; - expect(providerStack).to(haveResource('AWS::Lambda::Function', { - Layers: [{ Ref: 'referencetoStackKubectl7F29063EOutputsLayerVersionArn' }], - })); - - expect(stack).to(haveResource('AWS::Serverless::Application', { - Location: { - ApplicationId: 'custom:app:id', - SemanticVersion: '2.3.4', - }, - })); - - test.done(); - }, - 'create a cluster using custom resource with secrets encryption using KMS CMK'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); diff --git a/packages/@aws-cdk/aws-eks/test/test.legacy-cluster.ts b/packages/@aws-cdk/aws-eks/test/test.legacy-cluster.ts deleted file mode 100644 index 6395990fb909d..0000000000000 --- a/packages/@aws-cdk/aws-eks/test/test.legacy-cluster.ts +++ /dev/null @@ -1,635 +0,0 @@ -import { expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as iam from '@aws-cdk/aws-iam'; -import * as kms from '@aws-cdk/aws-kms'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as eks from '../lib'; -import { testFixture, testFixtureNoVpc } from './util'; - -/* eslint-disable max-len */ - -const CLUSTER_VERSION = eks.KubernetesVersion.V1_16; - -export = { - 'a default cluster spans all subnets'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - - // WHEN - new eks.LegacyCluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); - - // THEN - expect(stack).to(haveResourceLike('AWS::EKS::Cluster', { - ResourcesVpcConfig: { - SubnetIds: [ - { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, - { Ref: 'VPCPublicSubnet2Subnet74179F39' }, - { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, - { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, - ], - }, - })); - - test.done(); - }, - - 'if "vpc" is not specified, vpc with default configuration will be created'(test: Test) { - // GIVEN - const { stack } = testFixtureNoVpc(); - - // WHEN - new eks.LegacyCluster(stack, 'cluster', { version: CLUSTER_VERSION }) ; - - // THEN - expect(stack).to(haveResource('AWS::EC2::VPC')); - test.done(); - }, - - 'default capacity': { - - 'x2 m5.large by default'(test: Test) { - // GIVEN - const { stack } = testFixtureNoVpc(); - - // WHEN - const cluster = new eks.LegacyCluster(stack, 'cluster', { version: CLUSTER_VERSION }); - - // THEN - test.ok(cluster.defaultNodegroup); - expect(stack).to(haveResource('AWS::EKS::Nodegroup', { - InstanceTypes: [ - 'm5.large', - ], - ScalingConfig: { - DesiredSize: 2, - MaxSize: 2, - MinSize: 2, - }, - })); - test.done(); - }, - - 'quantity and type can be customized'(test: Test) { - // GIVEN - const { stack } = testFixtureNoVpc(); - - // WHEN - const cluster = new eks.LegacyCluster(stack, 'cluster', { - defaultCapacity: 10, - defaultCapacityInstance: new ec2.InstanceType('m2.xlarge'), - version: CLUSTER_VERSION, - }); - - // THEN - test.ok(cluster.defaultNodegroup); - expect(stack).to(haveResource('AWS::EKS::Nodegroup', { - ScalingConfig: { - DesiredSize: 10, - MaxSize: 10, - MinSize: 10, - }, - })); - // expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' })); - test.done(); - }, - - 'defaultCapacity=0 will not allocate at all'(test: Test) { - // GIVEN - const { stack } = testFixtureNoVpc(); - - // WHEN - const cluster = new eks.LegacyCluster(stack, 'cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); - - // THEN - test.ok(!cluster.defaultCapacity); - expect(stack).notTo(haveResource('AWS::AutoScaling::AutoScalingGroup')); - expect(stack).notTo(haveResource('AWS::AutoScaling::LaunchConfiguration')); - test.done(); - }, - }, - - 'creating a cluster tags the private VPC subnets'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - - // WHEN - new eks.LegacyCluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); - - // THEN - expect(stack).to(haveResource('AWS::EC2::Subnet', { - Tags: [ - { Key: 'aws-cdk:subnet-name', Value: 'Private' }, - { Key: 'aws-cdk:subnet-type', Value: 'Private' }, - { Key: 'kubernetes.io/role/internal-elb', Value: '1' }, - { Key: 'Name', Value: 'Stack/VPC/PrivateSubnet1' }, - ], - })); - - test.done(); - }, - - 'creating a cluster tags the public VPC subnets'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - - // WHEN - new eks.LegacyCluster(stack, 'Cluster', { vpc, defaultCapacity: 0, version: CLUSTER_VERSION }); - - // THEN - expect(stack).to(haveResource('AWS::EC2::Subnet', { - MapPublicIpOnLaunch: true, - Tags: [ - { Key: 'aws-cdk:subnet-name', Value: 'Public' }, - { Key: 'aws-cdk:subnet-type', Value: 'Public' }, - { Key: 'kubernetes.io/role/elb', Value: '1' }, - { Key: 'Name', Value: 'Stack/VPC/PublicSubnet1' }, - ], - })); - - test.done(); - }, - - 'adding capacity creates an ASG with tags'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { - vpc, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - // WHEN - cluster.addCapacity('Default', { - instanceType: new ec2.InstanceType('t2.medium'), - }); - - // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { - Tags: [ - { - Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, - PropagateAtLaunch: true, - Value: 'owned', - }, - { - Key: 'Name', - PropagateAtLaunch: true, - Value: 'Stack/Cluster/Default', - }, - ], - })); - - test.done(); - }, - - 'create nodegroup with existing role'(test: Test) { - // GIVEN - const { stack } = testFixtureNoVpc(); - - // WHEN - const cluster = new eks.LegacyCluster(stack, 'cluster', { - defaultCapacity: 10, - defaultCapacityInstance: new ec2.InstanceType('m2.xlarge'), - version: CLUSTER_VERSION, - }); - - const existingRole = new iam.Role(stack, 'ExistingRole', { - assumedBy: new iam.AccountRootPrincipal(), - }); - - new eks.Nodegroup(stack, 'Nodegroup', { - cluster, - nodeRole: existingRole, - }); - - // THEN - test.ok(cluster.defaultNodegroup); - expect(stack).to(haveResource('AWS::EKS::Nodegroup', { - ScalingConfig: { - DesiredSize: 10, - MaxSize: 10, - MinSize: 10, - }, - })); - test.done(); - }, - - 'adding bottlerocket capacity creates an ASG with tags'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { - vpc, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - // WHEN - cluster.addCapacity('Bottlerocket', { - instanceType: new ec2.InstanceType('t2.medium'), - machineImageType: eks.MachineImageType.BOTTLEROCKET, - }); - - // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { - Tags: [ - { - Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, - PropagateAtLaunch: true, - Value: 'owned', - }, - { - Key: 'Name', - PropagateAtLaunch: true, - Value: 'Stack/Cluster/Bottlerocket', - }, - ], - })); - test.done(); - }, - - 'adding bottlerocket capacity with bootstrapOptions throws error'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { - vpc, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - test.throws(() => cluster.addCapacity('Bottlerocket', { - instanceType: new ec2.InstanceType('t2.medium'), - machineImageType: eks.MachineImageType.BOTTLEROCKET, - bootstrapOptions: {}, - }), /bootstrapOptions is not supported for Bottlerocket/); - test.done(); - }, - - 'exercise export/import'(test: Test) { - // GIVEN - const { stack: stack1, vpc, app } = testFixture(); - const stack2 = new cdk.Stack(app, 'stack2', { env: { region: 'us-east-1' } }); - const cluster = new eks.LegacyCluster(stack1, 'Cluster', { - vpc, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - // WHEN - const imported = eks.LegacyCluster.fromClusterAttributes(stack2, 'Imported', { - vpc: cluster.vpc, - clusterEndpoint: cluster.clusterEndpoint, - clusterName: cluster.clusterName, - securityGroupIds: cluster.connections.securityGroups.map(s => s.securityGroupId), - clusterCertificateAuthorityData: cluster.clusterCertificateAuthorityData, - clusterSecurityGroupId: cluster.clusterSecurityGroupId, - clusterEncryptionConfigKeyArn: cluster.clusterEncryptionConfigKeyArn, - }); - - // this should cause an export/import - new cdk.CfnOutput(stack2, 'ClusterARN', { value: imported.clusterArn }); - - // THEN - expect(stack2).toMatch({ - Outputs: { - ClusterARN: { - Value: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':eks:us-east-1:', - { - Ref: 'AWS::AccountId', - }, - ':cluster/', - { - 'Fn::ImportValue': 'Stack:ExportsOutputRefClusterEB0386A796A0E3FE', - }, - ], - ], - }, - }, - }, - }); - test.done(); - }, - - 'disabled features when kubectl is disabled'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { - vpc, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - test.throws(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true }), - /Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster/); - test.done(); - }, - - 'addCapacity will *not* map the IAM role if mapRole is false'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { - vpc, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - // WHEN - cluster.addCapacity('default', { - instanceType: new ec2.InstanceType('t2.nano'), - mapRole: false, - }); - - // THEN - expect(stack).to(not(haveResource(eks.KubernetesManifest.RESOURCE_TYPE))); - test.done(); - }, - - 'addCapacity will *not* map the IAM role if kubectl is disabled'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { - vpc, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - // WHEN - cluster.addCapacity('default', { - instanceType: new ec2.InstanceType('t2.nano'), - }); - - // THEN - expect(stack).to(not(haveResource(eks.KubernetesManifest.RESOURCE_TYPE))); - test.done(); - }, - - 'outputs': { - 'aws eks update-kubeconfig is the only output synthesized by default'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - - // WHEN - new eks.LegacyCluster(stack, 'Cluster', { version: CLUSTER_VERSION }); - - // THEN - const assembly = app.synth(); - const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { - ClusterConfigCommand43AAE40F: { Value: { 'Fn::Join': ['', ['aws eks update-kubeconfig --name ', { Ref: 'ClusterEB0386A7' }, ' --region us-east-1']] } }, - ClusterGetTokenCommand06AE992E: { Value: { 'Fn::Join': ['', ['aws eks get-token --cluster-name ', { Ref: 'ClusterEB0386A7' }, ' --region us-east-1']] } }, - }); - test.done(); - }, - - 'if `outputConfigCommand=false` will disabled the output'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - - // WHEN - new eks.LegacyCluster(stack, 'Cluster', { - outputConfigCommand: false, - version: CLUSTER_VERSION, - }); - - // THEN - const assembly = app.synth(); - const template = assembly.getStackByName(stack.stackName).template; - test.ok(!template.Outputs); // no outputs - test.done(); - }, - - '`outputClusterName` can be used to synthesize an output with the cluster name'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - - // WHEN - new eks.LegacyCluster(stack, 'Cluster', { - outputConfigCommand: false, - outputClusterName: true, - version: CLUSTER_VERSION, - }); - - // THEN - const assembly = app.synth(); - const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { - ClusterClusterNameEB26049E: { Value: { Ref: 'ClusterEB0386A7' } }, - }); - test.done(); - }, - - 'boostrap user-data': { - - 'rendered by default for ASGs'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); - - // WHEN - cluster.addCapacity('MyCapcity', { instanceType: new ec2.InstanceType('m3.xlargs') }); - - // THEN - const template = app.synth().getStackByName(stack.stackName).template; - const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'ClusterEB0386A7' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, - - 'not rendered if bootstrap is disabled'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); - - // WHEN - cluster.addCapacity('MyCapcity', { - instanceType: new ec2.InstanceType('m3.xlargs'), - bootstrapEnabled: false, - }); - - // THEN - const template = app.synth().getStackByName(stack.stackName).template; - const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': '#!/bin/bash' }); - test.done(); - }, - - // cursory test for options: see test.user-data.ts for full suite - 'bootstrap options'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); - - // WHEN - cluster.addCapacity('MyCapcity', { - instanceType: new ec2.InstanceType('m3.xlargs'), - bootstrapOptions: { - kubeletExtraArgs: '--node-labels FOO=42', - }, - }); - - // THEN - const template = app.synth().getStackByName(stack.stackName).template; - const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'ClusterEB0386A7' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand --node-labels FOO=42" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, - - 'spot instances': { - - 'nodes labeled an tainted accordingly'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); - - // WHEN - cluster.addCapacity('MyCapcity', { - instanceType: new ec2.InstanceType('m3.xlargs'), - spotPrice: '0.01', - }); - - // THEN - const template = app.synth().getStackByName(stack.stackName).template; - const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'ClusterEB0386A7' }, ' --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, - - 'if kubectl is disabled, interrupt handler is not added'(test: Test) { - // GIVEN - const { stack } = testFixtureNoVpc(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - - // WHEN - cluster.addCapacity('MyCapcity', { - instanceType: new ec2.InstanceType('m3.xlargs'), - spotPrice: '0.01', - }); - - // THEN - expect(stack).notTo(haveResource(eks.KubernetesManifest.RESOURCE_TYPE)); - test.done(); - }, - - }, - - }, - - 'if bootstrap is disabled cannot specify options'(test: Test) { - // GIVEN - const { stack } = testFixtureNoVpc(); - const cluster = new eks.LegacyCluster(stack, 'Cluster', { defaultCapacity: 0, version: CLUSTER_VERSION }); - - // THEN - test.throws(() => cluster.addCapacity('MyCapcity', { - instanceType: new ec2.InstanceType('m3.xlargs'), - bootstrapEnabled: false, - bootstrapOptions: { awsApiRetryAttempts: 10 }, - }), /Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false/); - test.done(); - }, - - 'EksOptimizedImage() with no nodeType always uses STANDARD with LATEST_KUBERNETES_VERSION'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - const LATEST_KUBERNETES_VERSION = '1.14'; - - // WHEN - new eks.EksOptimizedImage().getImage(stack); - - // THEN - const assembly = app.synth(); - const parameters = assembly.getStackByName(stack.stackName).template.Parameters; - test.ok(Object.entries(parameters).some( - ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && - (v as any).Default.includes('/amazon-linux-2/'), - ), 'EKS STANDARD AMI should be in ssm parameters'); - test.ok(Object.entries(parameters).some( - ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && - (v as any).Default.includes(LATEST_KUBERNETES_VERSION), - ), 'LATEST_KUBERNETES_VERSION should be in ssm parameters'); - test.done(); - }, - - 'EksOptimizedImage() with specific kubernetesVersion return correct AMI'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - - // WHEN - new eks.EksOptimizedImage({ kubernetesVersion: '1.15' }).getImage(stack); - - // THEN - const assembly = app.synth(); - const parameters = assembly.getStackByName(stack.stackName).template.Parameters; - test.ok(Object.entries(parameters).some( - ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && - (v as any).Default.includes('/amazon-linux-2/'), - ), 'EKS STANDARD AMI should be in ssm parameters'); - test.ok(Object.entries(parameters).some( - ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && - (v as any).Default.includes('/1.15/'), - ), 'kubernetesVersion should be in ssm parameters'); - test.done(); - }, - - 'EKS-Optimized AMI with GPU support when addCapacity'(test: Test) { - // GIVEN - const { app, stack } = testFixtureNoVpc(); - - // WHEN - new eks.LegacyCluster(stack, 'cluster', { - defaultCapacity: 0, - version: CLUSTER_VERSION, - }).addCapacity('GPUCapacity', { - instanceType: new ec2.InstanceType('g4dn.xlarge'), - }); - - // THEN - const assembly = app.synth(); - const parameters = assembly.getStackByName(stack.stackName).template.Parameters; - test.ok(Object.entries(parameters).some( - ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && (v as any).Default.includes('amazon-linux-2-gpu'), - ), 'EKS AMI with GPU should be in ssm parameters'); - test.done(); - }, - }, - - 'create a cluster with secrets encryption using KMS CMK'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - - // WHEN - new eks.LegacyCluster(stack, 'Cluster', { - vpc, - version: CLUSTER_VERSION, - secretsEncryptionKey: new kms.Key(stack, 'Key'), - }); - - // THEN - expect(stack).to(haveResourceLike('AWS::EKS::Cluster', { - EncryptionConfig: [{ - Provider: { - KeyArn: { - 'Fn::GetAtt': [ - 'Key961B73FD', - 'Arn', - ], - }, - }, - Resources: ['secrets'], - }], - })); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts index 29d683d2c0d09..ff962572a6f92 100644 --- a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -17,7 +17,6 @@ export = { // WHEN const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -59,7 +58,6 @@ export = { // WHEN const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -117,7 +115,6 @@ export = { // WHEN const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -152,7 +149,6 @@ export = { // WHEN const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -165,47 +161,201 @@ export = { )); test.done(); }, - 'create nodegroups with kubectlEnabled is false'(test: Test) { + 'create nodegroup with instanceType provided'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); // WHEN - const cluster = new eks.LegacyCluster(stack, 'Cluster', { + const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - defaultCapacity: 2, + defaultCapacity: 0, version: CLUSTER_VERSION, }); - // add a extra nodegroup - cluster.addNodegroup('extra-ng'); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + instanceType: new ec2.InstanceType('m5.large'), + }); + // THEN - expect(stack).to(countResources('AWS::EKS::Nodegroup', 2)); + expect(stack).to(haveResourceLike('AWS::EKS::Nodegroup', { + InstanceTypes: [ + 'm5.large', + ], + }, + )); test.done(); }, - 'create nodegroup with instanceType provided'(test: Test) { + 'create nodegroup with on-demand capacity type'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); // WHEN const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); new eks.Nodegroup(stack, 'Nodegroup', { cluster, instanceType: new ec2.InstanceType('m5.large'), + capacityType: eks.CapacityType.ON_DEMAND, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::EKS::Nodegroup', { + InstanceTypes: [ + 'm5.large', + ], + CapacityType: 'ON_DEMAND', + }, + )); + test.done(); + }, + 'create nodegroup with spot capacity type'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + instanceTypes: [ + new ec2.InstanceType('m5.large'), + new ec2.InstanceType('t3.large'), + new ec2.InstanceType('c5.large'), + ], + capacityType: eks.CapacityType.SPOT, }); + // THEN + expect(stack).to(haveResourceLike('AWS::EKS::Nodegroup', { + InstanceTypes: [ + 'm5.large', + 't3.large', + 'c5.large', + ], + CapacityType: 'SPOT', + }, + )); + test.done(); + }, + 'create nodegroup with on-demand capacity type and multiple instance types'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + instanceTypes: [ + new ec2.InstanceType('m5.large'), + new ec2.InstanceType('t3.large'), + new ec2.InstanceType('c5.large'), + ], + capacityType: eks.CapacityType.ON_DEMAND, + }); // THEN expect(stack).to(haveResourceLike('AWS::EKS::Nodegroup', { InstanceTypes: [ 'm5.large', + 't3.large', + 'c5.large', ], + CapacityType: 'ON_DEMAND', }, )); test.done(); }, + 'throws when both instanceTypes and instanceType defined'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + // THEN + test.throws(() => cluster.addNodegroupCapacity('ng', { + instanceType: new ec2.InstanceType('m5.large'), + instanceTypes: [ + new ec2.InstanceType('m5.large'), + new ec2.InstanceType('t3.large'), + new ec2.InstanceType('c5.large'), + ], + capacityType: eks.CapacityType.SPOT, + }), /"instanceType is deprecated, please use "instanceTypes" only/); + test.done(); + }, + 'create nodegroup with neither instanceTypes nor instanceType defined'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + version: CLUSTER_VERSION, + }); + new eks.Nodegroup(stack, 'Nodegroup', { + cluster, + capacityType: eks.CapacityType.SPOT, + }); + // THEN + expect(stack).to(haveResourceLike('AWS::EKS::Nodegroup', { + CapacityType: 'SPOT', + }, + )); + test.done(); + }, + 'throws when instanceTypes provided with different CPU architrcture'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + // THEN + test.throws(() => cluster.addNodegroupCapacity('ng', { + instanceTypes: [ + // X86 + new ec2.InstanceType('c5.large'), + new ec2.InstanceType('c5a.large'), + // ARM64 + new ec2.InstanceType('m6g.large'), + ], + }), /instanceTypes of different CPU architectures is not allowed/); + test.done(); + }, + 'throws when amiType provided is incorrect'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + const cluster = new eks.Cluster(stack, 'Cluster', { + vpc, + defaultCapacity: 0, + version: CLUSTER_VERSION, + }); + // THEN + test.throws(() => cluster.addNodegroupCapacity('ng', { + instanceTypes: [ + new ec2.InstanceType('c5.large'), + new ec2.InstanceType('c5a.large'), + new ec2.InstanceType('c5d.large'), + ], + // incorrect amiType + amiType: eks.NodegroupAmiType.AL2_ARM_64, + }), /The specified AMI does not match the instance types architecture/); + test.done(); + }, + 'remoteAccess without security group provided'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); @@ -266,7 +416,6 @@ export = { const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -308,7 +457,6 @@ export = { const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -321,7 +469,6 @@ export = { const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -336,7 +483,6 @@ export = { // WHEN const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -383,7 +529,6 @@ export = { // WHEN const cluster = new eks.Cluster(stack, 'Cluster', { vpc, - kubectlEnabled: true, defaultCapacity: 0, version: CLUSTER_VERSION, }); @@ -410,38 +555,4 @@ export = { }), /diskSize must be specified within the launch template/); test.done(); }, - 'throws when both instanceType and launch template specified'(test: Test) { - // GIVEN - const { stack, vpc } = testFixture(); - - // WHEN - const cluster = new eks.Cluster(stack, 'Cluster', { - vpc, - kubectlEnabled: true, - defaultCapacity: 0, - version: CLUSTER_VERSION, - }); - const userData = ec2.UserData.forLinux(); - userData.addCommands( - 'set -o xtrace', - `/etc/eks/bootstrap.sh ${cluster.clusterName}`, - ); - const lt = new ec2.CfnLaunchTemplate(stack, 'LaunchTemplate', { - launchTemplateData: { - imageId: new eks.EksOptimizedImage().getImage(stack).imageId, - instanceType: new ec2.InstanceType('t3.small').toString(), - userData: cdk.Fn.base64(userData.render()), - }, - }); - // THEN - test.throws(() => - cluster.addNodegroupCapacity('ng-lt', { - instanceType: new ec2.InstanceType('c5.large'), - launchTemplateSpec: { - id: lt.ref, - version: lt.attrDefaultVersionNumber, - }, - }), /Instance types must be specified within the launch template/); - test.done(); - }, }; diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 502085c85d9c5..f6e94c7c62c49 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -37,6 +37,17 @@ const devDomain = new es.Domain(this, 'Domain', { }); ``` +To perform version upgrades without replacing the entire domain, specify the `enableVersionUpgrade` property. + +```ts +import * as es from '@aws-cdk/aws-elasticsearch'; + +const devDomain = new es.Domain(this, 'Domain', { + version: es.ElasticsearchVersion.V7_9, + enableVersionUpgrade: true // defaults to false +}); +``` + Create a production grade cluster by also specifying things like capacity and az distribution ```ts @@ -173,3 +184,29 @@ const domain = new es.Domain(this, 'Domain', { const masterUserPassword = domain.masterUserPassword; ``` + + + +## Audit logs + +Audit logs can be enabled for a domain, but only when fine grained access control is enabled. + +```ts +const domain = new es.Domain(this, 'Domain', { + version: es.ElasticsearchVersion.V7_1, + enforceHttps: true, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + fineGrainedAccessControl: { + masterUserName: 'master-user', + }, + logging: { + auditLogEnabled: true, + slowSearchLogEnabled: true, + appLogEnabled: true, + slowIndexLogEnabled: true, + }, +}); +``` diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 3e5e3ef50c344..477edb5106a79 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -66,6 +66,12 @@ export class ElasticsearchVersion { /** AWS Elasticsearch 7.7 */ public static readonly V7_7 = ElasticsearchVersion.of('7.7'); + /** AWS Elasticsearch 7.8 */ + public static readonly V7_8 = ElasticsearchVersion.of('7.8'); + + /** AWS Elasticsearch 7.9 */ + public static readonly V7_9 = ElasticsearchVersion.of('7.9'); + /** * Custom Elasticsearch version * @param version custom version number @@ -244,6 +250,21 @@ export interface LoggingOptions { * @default - a new log group is created if app logging is enabled */ readonly appLogGroup?: logs.ILogGroup; + + /** + * Specify if Elasticsearch audit logging should be set up. + * Requires Elasticsearch version 6.7 or later and fine grained access control to be enabled. + * + * @default - false + */ + readonly auditLogEnabled?: boolean; + + /** + * Log Elasticsearch audit logs to this log group. + * + * @default - a new log group is created if audit logging is enabled + */ + readonly auditLogGroup?: logs.ILogGroup; } /** @@ -496,6 +517,16 @@ export interface DomainProps { * @default - false */ readonly useUnsignedBasicAuth?: boolean; + + /** + * To upgrade an Amazon ES domain to a new version of Elasticsearch rather than replacing the entire + * domain resource, use the EnableVersionUpgrade update policy. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatepolicy.html#cfn-attributes-updatepolicy-upgradeelasticsearchdomain + * @default - false + */ + readonly enableVersionUpgrade?: boolean; + } /** @@ -1162,6 +1193,13 @@ export class Domain extends DomainBase implements IDomain { */ public readonly appLogGroup?: logs.ILogGroup; + /** + * Log group that audit logs are logged to. + * + * @attribute + */ + public readonly auditLogGroup?: logs.ILogGroup; + /** * Master user password if fine grained access control is configured. */ @@ -1365,6 +1403,12 @@ export class Domain extends DomainBase implements IDomain { } } + // Validate fine grained access control enabled for audit logs, per + // https://aws.amazon.com/about-aws/whats-new/2020/09/elasticsearch-audit-logs-now-available-on-amazon-elasticsearch-service/ + if (props.logging?.auditLogEnabled && !advancedSecurityEnabled) { + throw new Error('Fine-grained access control is required when audit logs publishing is enabled.'); + } + let cfnVpcOptions: CfnDomain.VPCOptionsProperty | undefined; if (props.vpcOptions) { cfnVpcOptions = { @@ -1403,6 +1447,15 @@ export class Domain extends DomainBase implements IDomain { logGroups.push(this.appLogGroup); }; + if (props.logging?.auditLogEnabled) { + this.auditLogGroup = props.logging.auditLogGroup ?? + new logs.LogGroup(this, 'AuditLogs', { + retention: logs.RetentionDays.ONE_MONTH, + }); + + logGroups.push(this.auditLogGroup); + }; + let logGroupResourcePolicy: LogGroupResourcePolicy | null = null; if (logGroups.length > 0) { const logPolicyStatement = new iam.PolicyStatement({ @@ -1414,8 +1467,9 @@ export class Domain extends DomainBase implements IDomain { // Use a custom resource to set the log group resource policy since it is not supported by CDK and cfn. // https://github.com/aws/aws-cdk/issues/5343 - logGroupResourcePolicy = new LogGroupResourcePolicy(this, 'ESLogGroupPolicy', { - policyName: 'ESLogPolicy', + logGroupResourcePolicy = new LogGroupResourcePolicy(this, `ESLogGroupPolicy${this.node.addr}`, { + // create a cloudwatch logs resource policy name that is unique to this domain instance + policyName: `ESLogPolicy${this.node.addr}`, policyStatements: [logPolicyStatement], }); } @@ -1453,6 +1507,10 @@ export class Domain extends DomainBase implements IDomain { }, nodeToNodeEncryptionOptions: { enabled: nodeToNodeEncryptionEnabled }, logPublishingOptions: { + AUDIT_LOGS: { + enabled: this.auditLogGroup != null, + cloudWatchLogsLogGroupArn: this.auditLogGroup?.logGroupArn, + }, ES_APPLICATION_LOGS: { enabled: this.appLogGroup != null, cloudWatchLogsLogGroupArn: this.appLogGroup?.logGroupArn, @@ -1493,6 +1551,13 @@ export class Domain extends DomainBase implements IDomain { : undefined, }); + if (props.enableVersionUpgrade) { + this.domain.cfnOptions.updatePolicy = { + ...this.domain.cfnOptions.updatePolicy, + enableVersionUpgrade: props.enableVersionUpgrade, + }; + } + if (logGroupResourcePolicy) { this.domain.node.addDependency(logGroupResourcePolicy); } if (props.domainName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.domainName); } diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index 668bd76a04aa2..ccce4e0860c5c 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -1,7 +1,10 @@ +/* eslint-disable jest/expect-expect */ import '@aws-cdk/assert/jest'; +import { ResourcePart } from '@aws-cdk/assert'; import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch'; import { Subnet, Vpc, EbsDeviceVolumeType } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; import { App, Stack, Duration, SecretValue } from '@aws-cdk/core'; import { Domain, ElasticsearchVersion } from '../lib'; @@ -47,6 +50,9 @@ test('minimal example renders correctly', () => { Enabled: false, }, LogPublishingOptions: { + AUDIT_LOGS: { + Enabled: false, + }, ES_APPLICATION_LOGS: { Enabled: false, }, @@ -63,6 +69,19 @@ test('minimal example renders correctly', () => { }); }); +test('can enable version upgrade update policy', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + enableVersionUpgrade: true, + }); + + expect(stack).toHaveResource('AWS::Elasticsearch::Domain', { + UpdatePolicy: { + EnableVersionUpgrade: true, + }, + }, ResourcePart.CompleteDefinition); +}); + describe('log groups', () => { test('slowSearchLogEnabled should create a custom log group', () => { @@ -152,6 +171,46 @@ describe('log groups', () => { }); }); + test('auditLogEnabled should create a custom log group', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, + logging: { + auditLogEnabled: true, + }, + fineGrainedAccessControl: { + masterUserName: 'username', + }, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + enforceHttps: true, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + LogPublishingOptions: { + AUDIT_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'DomainAuditLogs608E0FA6', + 'Arn', + ], + }, + Enabled: true, + }, + ES_APPLICATION_LOGS: { + Enabled: false, + }, + SEARCH_SLOW_LOGS: { + Enabled: false, + }, + INDEX_SLOW_LOGS: { + Enabled: false, + }, + }, + }); + }); + test('two domains with logging enabled can be created in same stack', () => { new Domain(stack, 'Domain1', { version: ElasticsearchVersion.V7_7, @@ -233,6 +292,195 @@ describe('log groups', () => { }); }); + test('log group policy is uniquely named for each domain', () => { + new Domain(stack, 'Domain1', { + version: ElasticsearchVersion.V7_4, + logging: { + appLogEnabled: true, + }, + }); + new Domain(stack, 'Domain2', { + version: ElasticsearchVersion.V7_4, + logging: { + appLogEnabled: true, + }, + }); + + // Domain1 + expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { + Create: { + parameters: { + policyName: 'ESLogPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391', + }, + }, + }); + // Domain2 + expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { + Create: { + parameters: { + policyName: 'ESLogPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2', + }, + }, + }); + }); + + test('enabling audit logs throws without fine grained access control enabled', () => { + expect(() => new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V6_7, + logging: { + auditLogEnabled: true, + }, + })).toThrow(/Fine-grained access control is required when audit logs publishing is enabled\./); + }); + + test('slowSearchLogGroup should use a custom log group', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, + logging: { + slowSearchLogEnabled: true, + slowSearchLogGroup: new logs.LogGroup(stack, 'SlowSearchLogs', { + retention: logs.RetentionDays.THREE_MONTHS, + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + LogPublishingOptions: { + AUDIT_LOGS: { + Enabled: false, + }, + ES_APPLICATION_LOGS: { + Enabled: false, + }, + SEARCH_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'SlowSearchLogsE00DC2E7', + 'Arn', + ], + }, + Enabled: true, + }, + INDEX_SLOW_LOGS: { + Enabled: false, + }, + }, + }); + }); + + test('slowIndexLogEnabled should use a custom log group', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, + logging: { + slowIndexLogEnabled: true, + slowIndexLogGroup: new logs.LogGroup(stack, 'SlowIndexLogs', { + retention: logs.RetentionDays.THREE_MONTHS, + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + LogPublishingOptions: { + AUDIT_LOGS: { + Enabled: false, + }, + ES_APPLICATION_LOGS: { + Enabled: false, + }, + SEARCH_SLOW_LOGS: { + Enabled: false, + }, + INDEX_SLOW_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'SlowIndexLogsAD49DED0', + 'Arn', + ], + }, + Enabled: true, + }, + }, + }); + }); + + test('appLogGroup should use a custom log group', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, + logging: { + appLogEnabled: true, + appLogGroup: new logs.LogGroup(stack, 'AppLogs', { + retention: logs.RetentionDays.THREE_MONTHS, + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + LogPublishingOptions: { + AUDIT_LOGS: { + Enabled: false, + }, + ES_APPLICATION_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'AppLogsC5DF83A6', + 'Arn', + ], + }, + Enabled: true, + }, + SEARCH_SLOW_LOGS: { + Enabled: false, + }, + INDEX_SLOW_LOGS: { + Enabled: false, + }, + }, + }); + }); + + test('auditLOgGroup should use a custom log group', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_4, + fineGrainedAccessControl: { + masterUserName: 'username', + }, + nodeToNodeEncryption: true, + encryptionAtRest: { + enabled: true, + }, + enforceHttps: true, + logging: { + auditLogEnabled: true, + auditLogGroup: new logs.LogGroup(stack, 'AuditLogs', { + retention: logs.RetentionDays.THREE_MONTHS, + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + LogPublishingOptions: { + AUDIT_LOGS: { + CloudWatchLogsLogGroupArn: { + 'Fn::GetAtt': [ + 'AuditLogsB945E340', + 'Arn', + ], + }, + Enabled: true, + }, + ES_APPLICATION_LOGS: { + Enabled: false, + }, + SEARCH_SLOW_LOGS: { + Enabled: false, + }, + INDEX_SLOW_LOGS: { + Enabled: false, + }, + }, + }); + }); + }); describe('grants', () => { diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json index 5a5275e4c52a9..a4ec48af68521 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.advancedsecurity.expected.json @@ -41,6 +41,9 @@ "Enabled": true }, "LogPublishingOptions": { + "AUDIT_LOGS": { + "Enabled": false + }, "ES_APPLICATION_LOGS": { "Enabled": false }, @@ -57,4 +60,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index 6cdefe4f285d9..7af9c57f73337 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -1,6 +1,6 @@ { "Resources": { - "DomainSlowSearchLogs5B35A97A": { + "Domain1SlowSearchLogs8F3B0506": { "Type": "AWS::Logs::LogGroup", "Properties": { "RetentionInDays": 30 @@ -8,7 +8,7 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "DomainAppLogs21698C1B": { + "Domain1AppLogs6E8D1D67": { "Type": "AWS::Logs::LogGroup", "Properties": { "RetentionInDays": 30 @@ -16,7 +16,7 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41": { + "Domain1ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6CustomResourcePolicy24436E05": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -34,7 +34,7 @@ ], "Version": "2012-10-17" }, - "PolicyName": "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41", + "PolicyName": "Domain1ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6CustomResourcePolicy24436E05", "Roles": [ { "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" @@ -42,7 +42,7 @@ ] } }, - "DomainESLogGroupPolicy5373A2E8": { + "Domain1ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6D8BDCF36": { "Type": "Custom::CloudwatchLogResourcePolicy", "Properties": { "ServiceToken": { @@ -55,7 +55,7 @@ "service": "CloudWatchLogs", "action": "putResourcePolicy", "parameters": { - "policyName": "ESLogPolicy", + "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6", "policyDocument": { "Fn::Join": [ "", @@ -63,14 +63,14 @@ "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", { "Fn::GetAtt": [ - "DomainSlowSearchLogs5B35A97A", + "Domain1SlowSearchLogs8F3B0506", "Arn" ] }, "\",\"", { "Fn::GetAtt": [ - "DomainAppLogs21698C1B", + "Domain1AppLogs6E8D1D67", "Arn" ] }, @@ -80,14 +80,14 @@ } }, "physicalResourceId": { - "id": "ESLogGroupPolicy" + "id": "ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" } }, "Update": { "service": "CloudWatchLogs", "action": "putResourcePolicy", "parameters": { - "policyName": "ESLogPolicy", + "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6", "policyDocument": { "Fn::Join": [ "", @@ -95,14 +95,14 @@ "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", { "Fn::GetAtt": [ - "DomainSlowSearchLogs5B35A97A", + "Domain1SlowSearchLogs8F3B0506", "Arn" ] }, "\",\"", { "Fn::GetAtt": [ - "DomainAppLogs21698C1B", + "Domain1AppLogs6E8D1D67", "Arn" ] }, @@ -112,26 +112,26 @@ } }, "physicalResourceId": { - "id": "ESLogGroupPolicy" + "id": "ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" } }, "Delete": { "service": "CloudWatchLogs", "action": "deleteResourcePolicy", "parameters": { - "policyName": "ESLogPolicy" + "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" }, "ignoreErrorCodesMatching": "400" }, "InstallLatestAwsSdk": true }, "DependsOn": [ - "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41" + "Domain1ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6CustomResourcePolicy24436E05" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "Domain66AC69E0": { + "Domain19FCBCB91": { "Type": "AWS::Elasticsearch::Domain", "Properties": { "CognitoOptions": { @@ -157,10 +157,13 @@ "Enabled": true }, "LogPublishingOptions": { + "AUDIT_LOGS": { + "Enabled": false + }, "ES_APPLICATION_LOGS": { "CloudWatchLogsLogGroupArn": { "Fn::GetAtt": [ - "DomainAppLogs21698C1B", + "Domain1AppLogs6E8D1D67", "Arn" ] }, @@ -169,7 +172,7 @@ "SEARCH_SLOW_LOGS": { "CloudWatchLogsLogGroupArn": { "Fn::GetAtt": [ - "DomainSlowSearchLogs5B35A97A", + "Domain1SlowSearchLogs8F3B0506", "Arn" ] }, @@ -184,8 +187,8 @@ } }, "DependsOn": [ - "DomainESLogGroupPolicyCustomResourcePolicyB35C8E41", - "DomainESLogGroupPolicy5373A2E8" + "Domain1ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6CustomResourcePolicy24436E05", + "Domain1ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6D8BDCF36" ] }, "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { @@ -273,6 +276,197 @@ "DependsOn": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" ] + }, + "Domain2SlowSearchLogs0C75F64B": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Domain2AppLogs810876E2": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 30 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Domain2ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71CustomResourcePolicy77691A33": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "logs:PutResourcePolicy", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "logs:DeleteResourcePolicy", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "Domain2ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71CustomResourcePolicy77691A33", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, + "Domain2ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71F1DE93A1": { + "Type": "Custom::CloudwatchLogResourcePolicy", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" + ] + }, + "Create": { + "service": "CloudWatchLogs", + "action": "putResourcePolicy", + "parameters": { + "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71", + "policyDocument": { + "Fn::Join": [ + "", + [ + "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", + { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" + ] + }, + "\",\"", + { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "\"]}],\"Version\":\"2012-10-17\"}" + ] + ] + } + }, + "physicalResourceId": { + "id": "ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" + } + }, + "Update": { + "service": "CloudWatchLogs", + "action": "putResourcePolicy", + "parameters": { + "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71", + "policyDocument": { + "Fn::Join": [ + "", + [ + "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", + { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" + ] + }, + "\",\"", + { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "\"]}],\"Version\":\"2012-10-17\"}" + ] + ] + } + }, + "physicalResourceId": { + "id": "ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" + } + }, + "Delete": { + "service": "CloudWatchLogs", + "action": "deleteResourcePolicy", + "parameters": { + "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" + }, + "ignoreErrorCodesMatching": "400" + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "Domain2ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71CustomResourcePolicy77691A33" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Domain2644FE48C": { + "Type": "AWS::Elasticsearch::Domain", + "Properties": { + "CognitoOptions": { + "Enabled": false + }, + "DomainEndpointOptions": { + "EnforceHTTPS": false, + "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07" + }, + "EBSOptions": { + "EBSEnabled": true, + "VolumeSize": 10, + "VolumeType": "gp2" + }, + "ElasticsearchClusterConfig": { + "DedicatedMasterEnabled": false, + "InstanceCount": 1, + "InstanceType": "r5.large.elasticsearch", + "ZoneAwarenessEnabled": false + }, + "ElasticsearchVersion": "7.1", + "EncryptionAtRestOptions": { + "Enabled": true + }, + "LogPublishingOptions": { + "AUDIT_LOGS": { + "Enabled": false + }, + "ES_APPLICATION_LOGS": { + "CloudWatchLogsLogGroupArn": { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "Enabled": true + }, + "SEARCH_SLOW_LOGS": { + "CloudWatchLogsLogGroupArn": { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" + ] + }, + "Enabled": true + }, + "INDEX_SLOW_LOGS": { + "Enabled": false + } + }, + "NodeToNodeEncryptionOptions": { + "Enabled": true + } + }, + "DependsOn": [ + "Domain2ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71CustomResourcePolicy77691A33", + "Domain2ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71F1DE93A1" + ] } }, "Parameters": { diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts index cb4937b1d5c29..52856982e97fc 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.ts @@ -7,7 +7,7 @@ class TestStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); - new es.Domain(this, 'Domain', { + const domainProps: es.DomainProps = { version: es.ElasticsearchVersion.V7_1, ebs: { volumeSize: 10, @@ -21,7 +21,11 @@ class TestStack extends Stack { encryptionAtRest: { enabled: true, }, - }); + }; + + // create 2 elasticsearch domains to ensure that Cloudwatch Log Group policy names dont conflict + new es.Domain(this, 'Domain1', domainProps); + new es.Domain(this, 'Domain2', domainProps); } } diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json index 3791b985eddbe..99ca282a3469a 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json @@ -55,6 +55,9 @@ "Enabled": true }, "LogPublishingOptions": { + "AUDIT_LOGS": { + "Enabled": false + }, "ES_APPLICATION_LOGS": { "Enabled": false }, diff --git a/packages/@aws-cdk/aws-iam/lib/policy.ts b/packages/@aws-cdk/aws-iam/lib/policy.ts index 6c006c6576120..795049a1cc163 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy.ts @@ -78,7 +78,7 @@ export interface PolicyProps { * creating invalid--and hence undeployable--CloudFormation templates. * * In cases where you know the policy must be created and it is actually - * an error if no statements have been added to it, you can se this to `true`. + * an error if no statements have been added to it, you can set this to `true`. * * @default false */ diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index 1a84a58440f6c..723ce157c6fbb 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -132,7 +132,7 @@ Docker container even if `esbuild` is available in your environment. This can be ## Configuring `esbuild` The `NodejsFunction` construct exposes some [esbuild options](https://esbuild.github.io/api/#build-api) -via properties under `bundling`: `minify`, `sourceMap`, `target` and `loader`. +via properties under `bundling`: ```ts new lambda.NodejsFunction(this, 'my-handler', { @@ -143,6 +143,12 @@ new lambda.NodejsFunction(this, 'my-handler', { loader: { // Use the 'dataurl' loader for '.png' files '.png': 'dataurl', }, + logLevel: LogLevel.SILENT, // defaults to LogLevel.WARNING + keepNames: true, // defaults to false + tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, + metafile: true, // include meta file, defaults to false + banner : '/* comments */', // by default no comments are passed + footer : '/* comments */', // by default no comments are passed }, }); ``` diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index 30d59ea82f7a1..32c9336d96b5a 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -55,6 +55,7 @@ export class Bundling implements cdk.BundlingOptions { public readonly local?: cdk.ILocalBundling; private readonly relativeEntryPath: string; + private readonly relativeTsconfigPath?: string; private readonly externals: string[]; constructor(private readonly props: BundlingProps) { @@ -65,6 +66,10 @@ export class Bundling implements cdk.BundlingOptions { const projectRoot = path.dirname(props.depsLockFilePath); this.relativeEntryPath = path.relative(projectRoot, path.resolve(props.entry)); + if (props.tsconfig) { + this.relativeTsconfigPath = path.relative(projectRoot, path.resolve(props.tsconfig)); + } + this.externals = [ ...props.externalModules ?? ['aws-sdk'], // Mark aws-sdk as external by default (available in the runtime) ...props.nodeModules ?? [], // Mark the modules that we are going to install as externals also @@ -142,6 +147,12 @@ export class Bundling implements cdk.BundlingOptions { ...this.props.sourceMap ? ['--sourcemap'] : [], ...this.externals.map(external => `--external:${external}`), ...loaders.map(([ext, name]) => `--loader:${ext}=${name}`), + ...this.props.logLevel ? [`--log-level=${this.props.logLevel}`] : [], + ...this.props.keepNames ? ['--keep-names'] : [], + ...this.relativeTsconfigPath ? [`--tsconfig=${pathJoin(inputDir, this.relativeTsconfigPath)}`] : [], + ...this.props.metafile ? [`--metafile=${pathJoin(outputDir, 'index.meta.json')}`] : [], + ...this.props.banner ? [`--banner='${this.props.banner}'`] : [], + ...this.props.footer ? [`--footer='${this.props.footer}'`] : [], ].join(' '); let depsCommand = ''; diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts index a1c7611e64825..5e4e160eaa717 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts @@ -87,7 +87,10 @@ export class NodejsFunction extends lambda.Function { if (!fs.existsSync(props.depsLockFilePath)) { throw new Error(`Lock file at ${props.depsLockFilePath} doesn't exist`); } - depsLockFilePath = props.depsLockFilePath; + if (!fs.statSync(props.depsLockFilePath).isFile()) { + throw new Error('`depsLockFilePath` should point to a file'); + } + depsLockFilePath = path.resolve(props.depsLockFilePath); } else { const lockFile = findUp(LockFile.YARN) ?? findUp(LockFile.NPM); if (!lockFile) { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts index 4f7b6505facfa..f1008dc7b0368 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/types.ts @@ -41,6 +41,96 @@ export interface BundlingOptions { */ readonly loader?: { [ext: string]: string }; + /** + * Log level for esbuild + * + * @default LogLevel.WARNING + */ + readonly logLevel?: LogLevel; + + /** + * Whether to preserve the original `name` values even in minified code. + * + * In JavaScript the `name` property on functions and classes defaults to a + * nearby identifier in the source code. + * + * However, minification renames symbols to reduce code size and bundling + * sometimes need to rename symbols to avoid collisions. That changes value of + * the `name` property for many of these cases. This is usually fine because + * the `name` property is normally only used for debugging. However, some + * frameworks rely on the `name` property for registration and binding purposes. + * If this is the case, you can enable this option to preserve the original + * `name` values even in minified code. + * + * @default false + */ + readonly keepNames?: boolean; + + /** + * Normally the esbuild automatically discovers `tsconfig.json` files and reads their contents during a build. + * + * However, you can also configure a custom `tsconfig.json` file to use instead. + * + * This is similar to entry path, you need to provide path to your custom `tsconfig.json`. + * + * This can be useful if you need to do multiple builds of the same code with different settings. + * + * @example { 'tsconfig': 'path/custom.tsconfig.json' } + * + * @default - automatically discovered by `esbuild` + */ + readonly tsconfig? : string + + /** + * This option tells esbuild to write out a JSON file relative to output directory with metadata about the build. + * + * The metadata in this JSON file follows this schema (specified using TypeScript syntax): + * + * ```typescript + * { + * outputs: { + * [path: string]: { + * bytes: number + * inputs: { + * [path: string]: { bytesInOutput: number } + * } + * imports: { path: string }[] + * exports: string[] + * } + * } + * } + * } + * ``` + * This data can then be analyzed by other tools. For example, + * bundle buddy can consume esbuild's metadata format and generates a treemap visualization + * of the modules in your bundle and how much space each one takes up. + * @see https://esbuild.github.io/api/#metafile + * @default - false + */ + readonly metafile?: boolean + + /** + * Use this to insert an arbitrary string at the beginning of generated JavaScript files. + * + * This is similar to footer which inserts at the end instead of the beginning. + * + * This is commonly used to insert comments: + * + * @default - no comments are passed + */ + readonly banner? : string + + /** + * Use this to insert an arbitrary string at the end of generated JavaScript files. + * + * This is similar to banner which inserts at the beginning instead of the end. + * + * This is commonly used to insert comments + * + * @default - no comments are passed + */ + readonly footer? : string + /** * Environment variables defined when bundling runs. * @@ -58,7 +148,7 @@ export interface BundlingOptions { /** * A list of modules that should be installed instead of bundled. Modules are - * installed in a Lambda compatible environnment only when bundling runs in + * installed in a Lambda compatible environment only when bundling runs in * Docker. * * @default - all modules are bundled @@ -152,3 +242,17 @@ export interface ICommandHooks { */ afterBundling(inputDir: string, outputDir: string): string[]; } + +/** + * Log level for esbuild + */ +export enum LogLevel { + /** Show everything */ + INFO = 'info', + /** Show warnings and errors */ + WARNING = 'warning', + /** Show errors only */ + ERROR = 'error', + /** Show nothing */ + SILENT = 'silent', +} diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index d71a8c719639a..c665b862ad93f 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -5,6 +5,7 @@ import { Code, Runtime } from '@aws-cdk/aws-lambda'; import { AssetHashType, BundlingDockerImage } from '@aws-cdk/core'; import { version as delayVersion } from 'delay/package.json'; import { Bundling } from '../lib/bundling'; +import { LogLevel } from '../lib/types'; import * as util from '../lib/util'; jest.mock('@aws-cdk/aws-lambda'); @@ -27,6 +28,7 @@ beforeEach(() => { let depsLockFilePath = '/project/yarn.lock'; let entry = '/project/lib/handler.ts'; +let tsconfig = '/project/lib/custom-tsconfig.ts'; test('esbuild bundling in Docker', () => { Bundling.bundle({ @@ -148,6 +150,44 @@ test('esbuild bundling with externals and dependencies', () => { }); }); +test('esbuild bundling with esbuild options', () => { + Bundling.bundle({ + entry, + depsLockFilePath, + runtime: Runtime.NODEJS_12_X, + minify: true, + sourceMap: true, + target: 'es2020', + loader: { + '.png': 'dataurl', + }, + logLevel: LogLevel.SILENT, + keepNames: true, + tsconfig, + metafile: true, + banner: '/* comments */', + footer: '/* comments */', + forceDockerBundling: true, + }); + + // Correctly bundles with esbuild + expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(depsLockFilePath), { + assetHashType: AssetHashType.OUTPUT, + bundling: expect.objectContaining({ + command: [ + 'bash', '-c', + [ + 'npx esbuild --bundle /asset-input/lib/handler.ts', + '--target=es2020 --platform=node --outfile=/asset-output/index.js', + '--minify --sourcemap --external:aws-sdk --loader:.png=dataurl', + '--log-level=silent --keep-names --tsconfig=/asset-input/lib/custom-tsconfig.ts', + '--metafile=/asset-output/index.meta.json --banner=\'/* comments */\' --footer=\'/* comments */\'', + ].join(' '), + ], + }), + }); +}); + test('Detects yarn.lock', () => { const yarnLock = path.join(__dirname, '..', 'yarn.lock'); Bundling.bundle({ diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts index 743526d0e3899..d898d312be92c 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts @@ -107,6 +107,22 @@ test('throws with non existing lock file', () => { })).toThrow(/Lock file at \/does\/not\/exist.lock doesn't exist/); }); +test('throws when depsLockFilePath is not a file', () => { + expect(() => new NodejsFunction(stack, 'handler1', { + depsLockFilePath: __dirname, + })).toThrow(/\`depsLockFilePath\` should point to a file/); +}); + +test('resolves depsLockFilePath to an absolute path', () => { + new NodejsFunction(stack, 'handler1', { + depsLockFilePath: './package.json', + }); + + expect(Bundling.bundle).toHaveBeenCalledWith(expect.objectContaining({ + depsLockFilePath: expect.stringMatching(/@aws-cdk\/aws-lambda-nodejs\/package.json$/), + })); +}); + test('resolves entry to an absolute path', () => { // WHEN new NodejsFunction(stack, 'fn', { diff --git a/packages/@aws-cdk/aws-logs/lib/log-group.ts b/packages/@aws-cdk/aws-logs/lib/log-group.ts index 83c4a8e170b89..8b24506f11ba0 100644 --- a/packages/@aws-cdk/aws-logs/lib/log-group.ts +++ b/packages/@aws-cdk/aws-logs/lib/log-group.ts @@ -154,7 +154,7 @@ abstract class LogGroupBase extends Resource implements ILogGroup { } /** - * Give permissions to write to create and write to streams in this log group + * Give permissions to create and write to streams in this log group */ public grantWrite(grantee: iam.IGrantable) { return this.grant(grantee, 'logs:CreateLogStream', 'logs:PutLogEvents'); diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index d83ba3a547915..116dd217748cd 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -129,3 +129,51 @@ const zone = HostedZone.fromHostedZoneId(this, 'MyZone', { hostedZoneId: 'ZOJJZC49E0EPZ', }); ``` + +## VPC Endpoint Service Private DNS + +When you create a VPC endpoint service, AWS generates endpoint-specific DNS hostnames that consumers use to communicate with the service. +For example, vpce-1234-abcdev-us-east-1.vpce-svc-123345.us-east-1.vpce.amazonaws.com. +By default, your consumers access the service with that DNS name. +This can cause problems with HTTPS traffic because the DNS will not match the backend certificate: + +```console +curl: (60) SSL: no alternative certificate subject name matches target host name 'vpce-abcdefghijklmnopq-rstuvwx.vpce-svc-abcdefghijklmnopq.us-east-1.vpce.amazonaws.com' +``` + +Effectively, the endpoint appears untrustworthy. To mitigate this, clients have to create an alias for this DNS name in Route53. + +Private DNS for an endpoint service lets you configure a private DNS name so consumers can +access the service using an existing DNS name without creating this Route53 DNS alias +This DNS name can also be guaranteed to match up with the backend certificate. + +Before consumers can use the private DNS name, you must verify that you have control of the domain/subdomain. + +Assuming your account has ownership of the particlar domain/subdomain, +this construct sets up the private DNS configuration on the endpoint service, +creates all the necessary Route53 entries, and verifies domain ownership. + +```ts +import { Stack } from '@aws-cdk/core'; +import { Vpc, VpcEndpointService } from '@aws-cdk/aws-ec2'; +import { NetworkLoadBalancer } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { PublicHostedZone } from '@aws-cdk/aws-route53'; + +stack = new Stack(); +vpc = new Vpc(stack, 'VPC'); +nlb = new NetworkLoadBalancer(stack, 'NLB', { + vpc, +}); +vpces = new VpcEndpointService(stack, 'VPCES', { + vpcEndpointServiceLoadBalancers: [nlb], +}); +// You must use a public hosted zone so domain ownership can be verified +zone = new PublicHostedZone(stack, 'PHZ', { + zoneName: 'aws-cdk.dev', +}); +new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff.aws-cdk.dev', + publicHostedZone: zone, +}); +``` diff --git a/packages/@aws-cdk/aws-route53/lib/index.ts b/packages/@aws-cdk/aws-route53/lib/index.ts index 9a6ed0a853679..c35c57025fc26 100644 --- a/packages/@aws-cdk/aws-route53/lib/index.ts +++ b/packages/@aws-cdk/aws-route53/lib/index.ts @@ -1,8 +1,9 @@ +export * from './alias-record-target'; export * from './hosted-zone'; export * from './hosted-zone-provider'; export * from './hosted-zone-ref'; export * from './record-set'; -export * from './alias-record-target'; +export * from './vpc-endpoint-service-domain-name'; // AWS::Route53 CloudFormation Resources: export * from './route53.generated'; diff --git a/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts b/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts new file mode 100644 index 0000000000000..2af889661098a --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts @@ -0,0 +1,230 @@ +import * as crypto from 'crypto'; +import { IVpcEndpointService } from '@aws-cdk/aws-ec2'; +import { Fn, Names, Stack } from '@aws-cdk/core'; +import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '@aws-cdk/custom-resources'; +import { Construct } from 'constructs'; +import { IPublicHostedZone, TxtRecord } from '../lib'; + +// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. +// eslint-disable-next-line +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Properties to configure a VPC Endpoint Service domain name + */ +export interface VpcEndpointServiceDomainNameProps { + + /** + * The VPC Endpoint Service to configure Private DNS for + */ + readonly endpointService: IVpcEndpointService; + + /** + * The domain name to use. + * + * This domain name must be owned by this account (registered through Route53), + * or delegated to this account. Domain ownership will be verified by AWS before + * private DNS can be used. + * @see https://docs.aws.amazon.com/vpc/latest/userguide/endpoint-services-dns-validation.html + */ + readonly domainName: string; + + /** + * The public hosted zone to use for the domain. + */ + readonly publicHostedZone: IPublicHostedZone; +} + +/** + * A Private DNS configuration for a VPC endpoint service. + */ +export class VpcEndpointServiceDomainName extends CoreConstruct { + + // Track all domain names created, so someone doesn't accidentally associate two domains with a single service + private static readonly endpointServices: IVpcEndpointService[] = []; + + // Track all domain names created, so someone doesn't accidentally associate two domains with a single service + private static readonly endpointServicesMap: { [endpointService: string]: string} = {}; + + // The way this class works is by using three custom resources and a TxtRecord in conjunction + // The first custom resource tells the VPC endpoint service to use the given DNS name + // The VPC endpoint service will then say: + // "ok, create a TXT record using these two values to prove you own the domain" + // The second custom resource retrieves these two values from the service + // The TxtRecord is created from these two values + // The third custom resource tells the VPC Endpoint Service to verify the domain ownership + constructor(scope: Construct, id: string, props: VpcEndpointServiceDomainNameProps) { + super(scope, id); + + const serviceUniqueId = Names.nodeUniqueId(props.endpointService.node); + const serviceId = props.endpointService.vpcEndpointServiceId; + const privateDnsName = props.domainName; + + // Make sure a user doesn't accidentally add multiple domains + this.validateProps(props); + + VpcEndpointServiceDomainName.endpointServicesMap[serviceUniqueId] = privateDnsName; + VpcEndpointServiceDomainName.endpointServices.push(props.endpointService); + + // Enable Private DNS on the endpoint service and retrieve the AWS-generated configuration + const privateDnsConfiguration = this.getPrivateDnsConfiguration(serviceUniqueId, serviceId, privateDnsName); + + // Tell AWS to verify that this account owns the domain attached to the service + this.verifyPrivateDnsConfiguration(privateDnsConfiguration, props.publicHostedZone); + + // Finally, don't do any of the above before the endpoint service is created + this.node.addDependency(props.endpointService); + } + + private validateProps(props: VpcEndpointServiceDomainNameProps): void { + const serviceUniqueId = Names.nodeUniqueId(props.endpointService.node); + if (serviceUniqueId in VpcEndpointServiceDomainName.endpointServicesMap) { + const endpoint = VpcEndpointServiceDomainName.endpointServicesMap[serviceUniqueId]; + throw new Error( + `Cannot create a VpcEndpointServiceDomainName for service ${serviceUniqueId}, another VpcEndpointServiceDomainName (${endpoint}) is already associated with it`); + } + } + + /** + * Sets up Custom Resources to make AWS calls to set up Private DNS on an endpoint service, + * returning the values to use in a TxtRecord, which AWS uses to verify domain ownership. + */ + private getPrivateDnsConfiguration(serviceUniqueId: string, serviceId: string, privateDnsName: string): PrivateDnsConfiguration { + + // The custom resource which tells AWS to enable Private DNS on the given service, using the given domain name + // AWS will generate a name/value pair for use in a TxtRecord, which is used to verify domain ownership. + const enablePrivateDnsAction = { + service: 'EC2', + action: 'modifyVpcEndpointServiceConfiguration', + parameters: { + ServiceId: serviceId, + PrivateDnsName: privateDnsName, + }, + physicalResourceId: PhysicalResourceId.of(serviceUniqueId), + }; + const removePrivateDnsAction = { + service: 'EC2', + action: 'modifyVpcEndpointServiceConfiguration', + parameters: { + ServiceId: serviceId, + RemovePrivateDnsName: true, + }, + }; + const enable = new AwsCustomResource(this, 'EnableDns', { + onCreate: enablePrivateDnsAction, + onUpdate: enablePrivateDnsAction, + onDelete: removePrivateDnsAction, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [ + Fn.join(':', [ + 'arn', + Stack.of(this).partition, + 'ec2', + Stack.of(this).region, + Stack.of(this).account, + Fn.join('/', [ + 'vpc-endpoint-service', + serviceId, + ]), + ]), + ], + }), + }); + + // Look up the name/value pair if the domain changes, or the service changes, + // which would cause the values to be different. If the unique ID changes, + // the resource may be entirely recreated, so we will need to look it up again. + const lookup = hashcode(Names.uniqueId(this) + serviceUniqueId + privateDnsName); + + // Create the custom resource to look up the name/value pair generated by AWS + // after the previous API call + const retriveNameValuePairAction = { + service: 'EC2', + action: 'describeVpcEndpointServiceConfigurations', + parameters: { + ServiceIds: [serviceId], + }, + physicalResourceId: PhysicalResourceId.of(lookup), + }; + const getNames = new AwsCustomResource(this, 'GetNames', { + onCreate: retriveNameValuePairAction, + onUpdate: retriveNameValuePairAction, + // describeVpcEndpointServiceConfigurations can't take an ARN for granular permissions + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: AwsCustomResourcePolicy.ANY_RESOURCE, + }), + }); + + // We only want to call and get the name/value pair after we've told AWS to enable Private DNS + // If we call before then, we'll get an empty pair of values. + getNames.node.addDependency(enable); + + // Get the references to the name/value pair associated with the endpoint service + const name = getNames.getResponseField('ServiceConfigurations.0.PrivateDnsNameConfiguration.Name'); + const value = getNames.getResponseField('ServiceConfigurations.0.PrivateDnsNameConfiguration.Value'); + + return { name, value, serviceId }; + } + + /** + * Creates a Route53 entry and a Custom Resource which explicitly tells AWS to verify ownership + * of the domain name attached to an endpoint service. + */ + private verifyPrivateDnsConfiguration(config: PrivateDnsConfiguration, publicHostedZone: IPublicHostedZone) { + // Create the TXT record in the provided hosted zone + const verificationRecord = new TxtRecord(this, 'DnsVerificationRecord', { + recordName: config.name, + values: [config.value], + zone: publicHostedZone, + }); + + // Tell the endpoint service to verify the domain ownership + const startVerificationAction = { + service: 'EC2', + action: 'startVpcEndpointServicePrivateDnsVerification', + parameters: { + ServiceId: config.serviceId, + }, + physicalResourceId: PhysicalResourceId.of(Fn.join(':', [config.name, config.value])), + }; + const startVerification = new AwsCustomResource(this, 'StartVerification', { + onCreate: startVerificationAction, + onUpdate: startVerificationAction, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [ + Fn.join(':', [ + 'arn', + Stack.of(this).partition, + 'ec2', + Stack.of(this).region, + Stack.of(this).account, + Fn.join('/', [ + 'vpc-endpoint-service', + config.serviceId, + ]), + ]), + ], + }), + }); + // Only verify after the record has been created + startVerification.node.addDependency(verificationRecord); + } +} + +/** + * Represent the name/value pair associated with a Private DNS enabled endpoint service + */ +interface PrivateDnsConfiguration { + readonly name: string; + readonly value: string; + readonly serviceId: string; +} + +/** + * Hash a string + */ +function hashcode(s: string): string { + const hash = crypto.createHash('md5'); + hash.update(s); + return hash.digest('hex'); +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index c7cdda9c49f38..2145126c0e42d 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -78,6 +78,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^26.6.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, @@ -86,6 +87,7 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.2.0" }, "homepage": "https://github.com/aws/aws-cdk", @@ -94,6 +96,7 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.2.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json similarity index 51% rename from packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json rename to packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json index bf7ee135b983d..df7ab45491d39 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json @@ -10,7 +10,7 @@ "Tags": [ { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC" } ] } @@ -33,13 +33,9 @@ "Key": "aws-cdk:subnet-type", "Value": "Public" }, - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet1" } ] } @@ -51,13 +47,9 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet1" } ] } @@ -93,13 +85,9 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet1" } ] } @@ -117,13 +105,9 @@ "Ref": "VPCPublicSubnet1SubnetB4246D30" }, "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet1" } ] } @@ -146,13 +130,9 @@ "Key": "aws-cdk:subnet-type", "Value": "Public" }, - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet2" } ] } @@ -164,13 +144,9 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet2" } ] } @@ -206,13 +182,9 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet2" } ] } @@ -230,13 +202,9 @@ "Ref": "VPCPublicSubnet2Subnet74179F39" }, "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet2" } ] } @@ -259,13 +227,9 @@ "Key": "aws-cdk:subnet-type", "Value": "Public" }, - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet3" } ] } @@ -277,13 +241,9 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet3" } ] } @@ -319,13 +279,9 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet3" } ] } @@ -343,13 +299,9 @@ "Ref": "VPCPublicSubnet3Subnet631C5E25" }, "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PublicSubnet3" } ] } @@ -372,13 +324,9 @@ "Key": "aws-cdk:subnet-type", "Value": "Private" }, - { - "Key": "kubernetes.io/role/internal-elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet1" } ] } @@ -390,13 +338,9 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "kubernetes.io/role/internal-elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet1" } ] } @@ -442,13 +386,9 @@ "Key": "aws-cdk:subnet-type", "Value": "Private" }, - { - "Key": "kubernetes.io/role/internal-elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet2" } ] } @@ -460,13 +400,9 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "kubernetes.io/role/internal-elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet2" } ] } @@ -512,13 +448,9 @@ "Key": "aws-cdk:subnet-type", "Value": "Private" }, - { - "Key": "kubernetes.io/role/internal-elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet3" } ] } @@ -530,13 +462,9 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "kubernetes.io/role/internal-elb", - "Value": "1" - }, { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC/PrivateSubnet3" } ] } @@ -570,7 +498,7 @@ "Tags": [ { "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC" + "Value": "aws-cdk-vpc-endpoint-dns-integ/VPC" } ] } @@ -586,276 +514,393 @@ } } }, - "SecretsKey317DCF94": { - "Type": "AWS::KMS::Key", + "mylb": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", "Properties": { - "KeyPolicy": { - "Statement": [ - { - "Action": [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion", - "kms:GenerateDataKey", - "kms:TagResource", - "kms:UntagResource" - ], - "Effect": "Allow", - "Principal": { - "AWS": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::12345678:root" - ] - ] - } - }, - "Resource": "*" - } - ], - "Version": "2012-10-17" - } - }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "Type": "network", + "Name": "mylb", + "Scheme": "internal", + "Subnets": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + ] + } }, - "EKSClusterRoleC0AEAC3D": { - "Type": "AWS::IAM::Role", + "VPCES3AE7D565": { + "Type": "AWS::EC2::VPCEndpointService", "Properties": { - "AssumeRolePolicyDocument": { + "AcceptanceRequired": true, + "NetworkLoadBalancerArns": [ + { + "Ref": "mylb" + } + ] + } + }, + "PHZ45BE903D": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "aws-cdk.dev." + } + }, + "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { "Statement": [ { - "Action": "sts:AssumeRole", + "Action": "ec2:ModifyVpcEndpointServiceConfiguration", "Effect": "Allow", - "Principal": { - "Service": "eks.amazonaws.com" + "Resource": { + "Fn::Join": [ + ":", + [ + "arn", + { + "Ref": "AWS::Partition" + }, + "ec2", + { + "Ref": "AWS::Region" + }, + { + "Ref": "AWS::AccountId" + }, + { + "Fn::Join": [ + "/", + [ + "vpc-endpoint-service", + { + "Ref": "VPCES3AE7D565" + } + ] + ] + } + ] + ] } } ], "Version": "2012-10-17" }, - "ManagedPolicyArns": [ + "PolicyName": "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB", + "Roles": [ { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/AmazonEKSClusterPolicy" - ] - ] + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" } ] - } - }, - "EKSClusterControlPlaneSecurityGroup580AD1FE": { - "Type": "AWS::EC2::SecurityGroup", - "Properties": { - "GroupDescription": "EKS Control Plane Security Group", - "SecurityGroupEgress": [ - { - "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" - } - ], - "VpcId": { - "Ref": "VPCB9E5F0B4" - } - } + }, + "DependsOn": [ + "VPCES3AE7D565" + ] }, - "EKSClusterControlPlaneSecurityGroupfromeksintegkubectldisabledEKSClusterNodesInstanceSecurityGroup1E8EEB07443828A1FF0": { - "Type": "AWS::EC2::SecurityGroupIngress", + "EndpointDomainEnableDnsDACBF5A6": { + "Type": "Custom::AWS", "Properties": { - "IpProtocol": "tcp", - "Description": "from eksintegkubectldisabledEKSClusterNodesInstanceSecurityGroup1E8EEB07:443", - "FromPort": 443, - "GroupId": { + "ServiceToken": { "Fn::GetAtt": [ - "EKSClusterControlPlaneSecurityGroup580AD1FE", - "GroupId" + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" ] }, - "SourceSecurityGroupId": { - "Fn::GetAtt": [ - "EKSClusterNodesInstanceSecurityGroup460A275E", - "GroupId" - ] + "Create": { + "service": "EC2", + "action": "modifyVpcEndpointServiceConfiguration", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + }, + "PrivateDnsName": "my-stuff.aws-cdk.dev" + }, + "physicalResourceId": { + "id": "awscdkvpcendpointdnsintegVPCES2D7BC258" + } }, - "ToPort": 443 - } + "Update": { + "service": "EC2", + "action": "modifyVpcEndpointServiceConfiguration", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + }, + "PrivateDnsName": "my-stuff.aws-cdk.dev" + }, + "physicalResourceId": { + "id": "awscdkvpcendpointdnsintegVPCES2D7BC258" + } + }, + "Delete": { + "service": "EC2", + "action": "modifyVpcEndpointServiceConfiguration", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + }, + "RemovePrivateDnsName": "TRUE:BOOLEAN" + } + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB", + "VPCES3AE7D565" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" }, - "EKSClusterBA6ECF8F": { - "Type": "AWS::EKS::Cluster", + "EndpointDomainGetNamesCustomResourcePolicy141775B1": { + "Type": "AWS::IAM::Policy", "Properties": { - "ResourcesVpcConfig": { - "SecurityGroupIds": [ + "PolicyDocument": { + "Statement": [ { - "Fn::GetAtt": [ - "EKSClusterControlPlaneSecurityGroup580AD1FE", - "GroupId" - ] + "Action": "ec2:DescribeVpcEndpointServiceConfigurations", + "Effect": "Allow", + "Resource": "*" } ], - "SubnetIds": [ - { - "Ref": "VPCPublicSubnet1SubnetB4246D30" - }, - { - "Ref": "VPCPublicSubnet2Subnet74179F39" - }, - { - "Ref": "VPCPublicSubnet3Subnet631C5E25" - }, - { - "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" - }, - { - "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" - }, - { - "Ref": "VPCPrivateSubnet3Subnet3EDCD457" - } - ] + "Version": "2012-10-17" }, - "RoleArn": { + "PolicyName": "EndpointDomainGetNamesCustomResourcePolicy141775B1", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB", + "EndpointDomainEnableDnsDACBF5A6", + "VPCES3AE7D565" + ] + }, + "EndpointDomainGetNames9E697ED2": { + "Type": "Custom::AWS", + "Properties": { + "ServiceToken": { "Fn::GetAtt": [ - "EKSClusterRoleC0AEAC3D", + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", "Arn" ] }, - "EncryptionConfig": [ - { - "Provider": { - "KeyArn": { - "Fn::GetAtt": [ - "SecretsKey317DCF94", - "Arn" - ] + "Create": { + "service": "EC2", + "action": "describeVpcEndpointServiceConfigurations", + "parameters": { + "ServiceIds": [ + { + "Ref": "VPCES3AE7D565" } - }, - "Resources": [ - "secrets" ] + }, + "physicalResourceId": { + "id": "0b26ca4969ad06c279e229b1b55b9bc2" } - ], - "Version": "1.18" - } - }, - "EKSClusterNodesInstanceSecurityGroup460A275E": { - "Type": "AWS::EC2::SecurityGroup", - "Properties": { - "GroupDescription": "eks-integ-kubectl-disabled/EKSCluster/Nodes/InstanceSecurityGroup", - "SecurityGroupEgress": [ - { - "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" + }, + "Update": { + "service": "EC2", + "action": "describeVpcEndpointServiceConfigurations", + "parameters": { + "ServiceIds": [ + { + "Ref": "VPCES3AE7D565" + } + ] + }, + "physicalResourceId": { + "id": "0b26ca4969ad06c279e229b1b55b9bc2" } - ], - "Tags": [ - { - "Key": { - "Fn::Join": [ - "", - [ - "kubernetes.io/cluster/", - { - "Ref": "EKSClusterBA6ECF8F" - } + }, + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB", + "EndpointDomainEnableDnsDACBF5A6", + "EndpointDomainGetNamesCustomResourcePolicy141775B1", + "VPCES3AE7D565" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "EndpointDomainDnsVerificationRecord66623BDA": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" ] - ] - }, - "Value": "owned" - }, + }, + ".aws-cdk.dev." + ] + ] + }, + "Type": "TXT", + "HostedZoneId": { + "Ref": "PHZ45BE903D" + }, + "ResourceRecords": [ { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" + "Fn::Join": [ + "", + [ + "\"", + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + }, + "\"" + ] + ] } ], - "VpcId": { - "Ref": "VPCB9E5F0B4" - } - } + "TTL": "1800" + }, + "DependsOn": [ + "VPCES3AE7D565" + ] }, - "EKSClusterNodesInstanceSecurityGroupfromeksintegkubectldisabledEKSClusterNodesInstanceSecurityGroup1E8EEB07ALLTRAFFIC813BA9BB": { - "Type": "AWS::EC2::SecurityGroupIngress", + "EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6": { + "Type": "AWS::IAM::Policy", "Properties": { - "IpProtocol": "-1", - "Description": "from eksintegkubectldisabledEKSClusterNodesInstanceSecurityGroup1E8EEB07:ALL TRAFFIC", - "GroupId": { - "Fn::GetAtt": [ - "EKSClusterNodesInstanceSecurityGroup460A275E", - "GroupId" - ] + "PolicyDocument": { + "Statement": [ + { + "Action": "ec2:StartVpcEndpointServicePrivateDnsVerification", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + ":", + [ + "arn", + { + "Ref": "AWS::Partition" + }, + "ec2", + { + "Ref": "AWS::Region" + }, + { + "Ref": "AWS::AccountId" + }, + { + "Fn::Join": [ + "/", + [ + "vpc-endpoint-service", + { + "Ref": "VPCES3AE7D565" + } + ] + ] + } + ] + ] + } + } + ], + "Version": "2012-10-17" }, - "SourceSecurityGroupId": { - "Fn::GetAtt": [ - "EKSClusterNodesInstanceSecurityGroup460A275E", - "GroupId" - ] - } - } + "PolicyName": "EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "EndpointDomainDnsVerificationRecord66623BDA", + "VPCES3AE7D565" + ] }, - "EKSClusterNodesInstanceSecurityGroupfromeksintegkubectldisabledEKSClusterControlPlaneSecurityGroupA8D847C7443405A887C": { - "Type": "AWS::EC2::SecurityGroupIngress", + "EndpointDomainStartVerification05E2F7A3": { + "Type": "Custom::AWS", "Properties": { - "IpProtocol": "tcp", - "Description": "from eksintegkubectldisabledEKSClusterControlPlaneSecurityGroupA8D847C7:443", - "FromPort": 443, - "GroupId": { + "ServiceToken": { "Fn::GetAtt": [ - "EKSClusterNodesInstanceSecurityGroup460A275E", - "GroupId" - ] - }, - "SourceSecurityGroupId": { - "Fn::GetAtt": [ - "EKSClusterControlPlaneSecurityGroup580AD1FE", - "GroupId" + "AWS679f53fac002430cb0da5b7982bd22872D164C4C", + "Arn" ] }, - "ToPort": 443 - } - }, - "EKSClusterNodesInstanceSecurityGroupfromeksintegkubectldisabledEKSClusterControlPlaneSecurityGroupA8D847C71025655350C1AD63E": { - "Type": "AWS::EC2::SecurityGroupIngress", - "Properties": { - "IpProtocol": "tcp", - "Description": "from eksintegkubectldisabledEKSClusterControlPlaneSecurityGroupA8D847C7:1025-65535", - "FromPort": 1025, - "GroupId": { - "Fn::GetAtt": [ - "EKSClusterNodesInstanceSecurityGroup460A275E", - "GroupId" - ] + "Create": { + "service": "EC2", + "action": "startVpcEndpointServicePrivateDnsVerification", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + } + }, + "physicalResourceId": { + "id": { + "Fn::Join": [ + ":", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + } + ] + ] + } + } }, - "SourceSecurityGroupId": { - "Fn::GetAtt": [ - "EKSClusterControlPlaneSecurityGroup580AD1FE", - "GroupId" - ] + "Update": { + "service": "EC2", + "action": "startVpcEndpointServicePrivateDnsVerification", + "parameters": { + "ServiceId": { + "Ref": "VPCES3AE7D565" + } + }, + "physicalResourceId": { + "id": { + "Fn::Join": [ + ":", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + } + ] + ] + } + } }, - "ToPort": 65535 - } - }, - "EKSClusterNodesInstanceRoleEE5595D6": { + "InstallLatestAwsSdk": true + }, + "DependsOn": [ + "EndpointDomainDnsVerificationRecord66623BDA", + "EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6", + "VPCES3AE7D565" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -864,17 +909,7 @@ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { - "Service": { - "Fn::Join": [ - "", - [ - "ec2.", - { - "Ref": "AWS::URLSuffix" - } - ] - ] - } + "Service": "lambda.amazonaws.com" } } ], @@ -889,206 +924,81 @@ { "Ref": "AWS::Partition" }, - ":iam::aws:policy/AmazonEKSWorkerNodePolicy" - ] - ] - }, - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/AmazonEKS_CNI_Policy" - ] - ] - }, - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ] ] } - ], - "Tags": [ - { - "Key": { - "Fn::Join": [ - "", - [ - "kubernetes.io/cluster/", - { - "Ref": "EKSClusterBA6ECF8F" - } - ] - ] - }, - "Value": "owned" - }, - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" - } ] } }, - "EKSClusterNodesInstanceProfile0F2DB3B9": { - "Type": "AWS::IAM::InstanceProfile", + "AWS679f53fac002430cb0da5b7982bd22872D164C4C": { + "Type": "AWS::Lambda::Function", "Properties": { - "Roles": [ - { - "Ref": "EKSClusterNodesInstanceRoleEE5595D6" - } - ] - } - }, - "EKSClusterNodesLaunchConfig921F1106": { - "Type": "AWS::AutoScaling::LaunchConfiguration", - "Properties": { - "ImageId": { - "Ref": "SsmParameterValueawsserviceeksoptimizedami118amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" - }, - "InstanceType": "t2.medium", - "IamInstanceProfile": { - "Ref": "EKSClusterNodesInstanceProfile0F2DB3B9" - }, - "SecurityGroups": [ - { - "Fn::GetAtt": [ - "EKSClusterNodesInstanceSecurityGroup460A275E", - "GroupId" - ] - } - ], - "UserData": { - "Fn::Base64": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + }, + "S3Key": { "Fn::Join": [ "", [ - "#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ", { - "Ref": "EKSClusterBA6ECF8F" + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + } + ] + } + ] }, - " --kubelet-extra-args \"--node-labels lifecycle=OnDemand\" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack eks-integ-kubectl-disabled --resource EKSClusterNodesASGC2597E34 --region test-region" + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + } + ] + } + ] + } ] ] } - } - }, - "DependsOn": [ - "EKSClusterNodesInstanceRoleEE5595D6" - ] - }, - "EKSClusterNodesASGC2597E34": { - "Type": "AWS::AutoScaling::AutoScalingGroup", - "Properties": { - "MaxSize": "1", - "MinSize": "1", - "LaunchConfigurationName": { - "Ref": "EKSClusterNodesLaunchConfig921F1106" }, - "Tags": [ - { - "Key": { - "Fn::Join": [ - "", - [ - "kubernetes.io/cluster/", - { - "Ref": "EKSClusterBA6ECF8F" - } - ] - ] - }, - "PropagateAtLaunch": true, - "Value": "owned" - }, - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" - } - ], - "VPCZoneIdentifier": [ - { - "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" - }, - { - "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" - }, - { - "Ref": "VPCPrivateSubnet3Subnet3EDCD457" - } - ] - }, - "UpdatePolicy": { - "AutoScalingRollingUpdate": { - "WaitOnResourceSignals": false, - "PauseTime": "PT0S", - "SuspendProcesses": [ - "HealthCheck", - "ReplaceUnhealthy", - "AZRebalance", - "AlarmNotification", - "ScheduledActions" + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", + "Arn" ] }, - "AutoScalingScheduledAction": { - "IgnoreUnmodifiedGroupSizeProperties": true - } - } + "Runtime": "nodejs12.x", + "Timeout": 120 + }, + "DependsOn": [ + "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + ] } }, - "Outputs": { - "EKSClusterConfigCommand3809C9C9": { - "Value": { - "Fn::Join": [ - "", - [ - "aws eks update-kubeconfig --name ", - { - "Ref": "EKSClusterBA6ECF8F" - }, - " --region test-region" - ] - ] - } + "Parameters": { + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "Type": "String", + "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" }, - "EKSClusterGetTokenCommand10DBF41A": { - "Value": { - "Fn::Join": [ - "", - [ - "aws eks get-token --cluster-name ", - { - "Ref": "EKSClusterBA6ECF8F" - }, - " --region test-region" - ] - ] - } + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "Type": "String", + "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" }, - "EKSClusterNodesInstanceRoleARN10992C84": { - "Value": { - "Fn::GetAtt": [ - "EKSClusterNodesInstanceRoleEE5595D6", - "Arn" - ] - } - } - }, - "Parameters": { - "SsmParameterValueawsserviceeksoptimizedami118amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { - "Type": "AWS::SSM::Parameter::Value", - "Default": "/aws/service/eks/optimized-ami/1.18/amazon-linux-2/recommended/image_id" + "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "Type": "String", + "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.ts b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.ts new file mode 100644 index 0000000000000..672b2133f077b --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.ts @@ -0,0 +1,48 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { PublicHostedZone, VpcEndpointServiceDomainName } from '../lib'; + +/** + * A load balancer that can host a VPC Endpoint Service. + * + * Why do this instead of using the NetworkLoadBalancer construct? aws-route53 + * cannot depend on aws-elasticloadbalancingv2 because aws-elasticloadbalancingv2 + * already takes a dependency on aws-route53. + */ +class DummyEndpointLoadBalancer implements ec2.IVpcEndpointServiceLoadBalancer { + /** + * The ARN of the load balancer that hosts the VPC Endpoint Service + */ + public readonly loadBalancerArn: string; + constructor(scope: Construct, id: string, vpc: ec2.Vpc) { + const lb = new cdk.CfnResource(scope, id, { + type: 'AWS::ElasticLoadBalancingV2::LoadBalancer', + properties: { + Type: 'network', + Name: 'mylb', + Scheme: 'internal', + Subnets: [vpc.privateSubnets[0].subnetId], + }, + }); + this.loadBalancerArn = lb.ref; + } +} + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-vpc-endpoint-dns-integ'); +const vpc = new ec2.Vpc(stack, 'VPC'); +const nlb = new DummyEndpointLoadBalancer(stack, 'mylb', vpc); +const vpces = new ec2.VpcEndpointService(stack, 'VPCES', { + vpcEndpointServiceLoadBalancers: [nlb], +}); +const zone = new PublicHostedZone(stack, 'PHZ', { + zoneName: 'aws-cdk.dev', +}); +new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff.aws-cdk.dev', + publicHostedZone: zone, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts new file mode 100644 index 0000000000000..70ba07c201d5f --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts @@ -0,0 +1,254 @@ +import { expect as cdkExpect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { IVpcEndpointServiceLoadBalancer, VpcEndpointService } from '@aws-cdk/aws-ec2'; +import { Stack } from '@aws-cdk/core'; +import { PublicHostedZone, VpcEndpointServiceDomainName } from '../lib'; + +let stack: Stack; +let nlb: IVpcEndpointServiceLoadBalancer; +let vpces: VpcEndpointService; +let zone: PublicHostedZone; + +/** + * A load balancer that can host a VPC Endpoint Service + */ +class DummyEndpointLoadBalancer implements IVpcEndpointServiceLoadBalancer { + /** + * The ARN of the load balancer that hosts the VPC Endpoint Service + */ + public readonly loadBalancerArn: string; + constructor(arn: string) { + this.loadBalancerArn = arn; + } +} + +beforeEach(() => { + stack = new Stack(); + nlb = new DummyEndpointLoadBalancer('arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/net/Test/9bn6qkf4e9jrw77a'); + vpces = new VpcEndpointService(stack, 'VPCES', { + vpcEndpointServiceLoadBalancers: [nlb], + }); + zone = new PublicHostedZone(stack, 'PHZ', { + zoneName: 'aws-cdk.dev', + }); +}); + +test('create domain name resource', () => { + // GIVEN + + // WHEN + new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff.aws-cdk.dev', + publicHostedZone: zone, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Properties: { + Create: { + action: 'modifyVpcEndpointServiceConfiguration', + service: 'EC2', + parameters: { + PrivateDnsName: 'my-stuff.aws-cdk.dev', + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + physicalResourceId: { + id: 'EndpointDomain', + }, + }, + Update: { + action: 'modifyVpcEndpointServiceConfiguration', + service: 'EC2', + parameters: { + PrivateDnsName: 'my-stuff.aws-cdk.dev', + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + physicalResourceId: { + id: 'EndpointDomain', + }, + }, + Delete: { + action: 'modifyVpcEndpointServiceConfiguration', + service: 'EC2', + parameters: { + RemovePrivateDnsName: 'TRUE:BOOLEAN', + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + }, + }, + DependsOn: [ + 'EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB', + 'VPCES3AE7D565', + ], + }, ResourcePart.CompleteDefinition)); + + // Have to use `haveResourceLike` because there is a property that, by design, changes on every build + cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Properties: { + Create: { + action: 'describeVpcEndpointServiceConfigurations', + service: 'EC2', + parameters: { + ServiceIds: [{ + Ref: 'VPCES3AE7D565', + }], + }, + }, + Update: { + action: 'describeVpcEndpointServiceConfigurations', + service: 'EC2', + parameters: { + ServiceIds: [{ + Ref: 'VPCES3AE7D565', + }], + }, + }, + }, + DependsOn: [ + 'EndpointDomainEnableDnsCustomResourcePolicy5E6DE7EB', + 'EndpointDomainEnableDnsDACBF5A6', + 'EndpointDomainGetNamesCustomResourcePolicy141775B1', + 'VPCES3AE7D565', + ], + }, ResourcePart.CompleteDefinition)); + + cdkExpect(stack).to(haveResource('AWS::Route53::RecordSet', { + Properties: { + Name: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + '.aws-cdk.dev.', + ], + ], + }, + Type: 'TXT', + HostedZoneId: { + Ref: 'PHZ45BE903D', + }, + ResourceRecords: [ + { + 'Fn::Join': [ + '', + [ + '\"', + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + '\"', + ], + ], + }, + ], + TTL: '1800', + }, + DependsOn: [ + 'VPCES3AE7D565', + ], + }, ResourcePart.CompleteDefinition)); + + cdkExpect(stack).to(haveResourceLike('Custom::AWS', { + Properties: { + Create: { + action: 'startVpcEndpointServicePrivateDnsVerification', + service: 'EC2', + parameters: { + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + physicalResourceId: { + id: { + 'Fn::Join': [ + ':', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + ], + ], + }, + }, + }, + Update: { + action: 'startVpcEndpointServicePrivateDnsVerification', + service: 'EC2', + parameters: { + ServiceId: { + Ref: 'VPCES3AE7D565', + }, + }, + physicalResourceId: { + id: { + 'Fn::Join': [ + ':', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + ], + ], + }, + }, + }, + }, + DependsOn: [ + 'EndpointDomainDnsVerificationRecord66623BDA', + 'EndpointDomainStartVerificationCustomResourcePolicyD2BAC9A6', + 'VPCES3AE7D565', + ], + }, ResourcePart.CompleteDefinition)); +}); + +test('throws if creating multiple domains for a single service', () => { + // GIVEN + + new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { + endpointService: vpces, + domainName: 'my-stuff-1.aws-cdk.dev', + publicHostedZone: zone, + }); + + // WHEN / THEN + expect(() => { + new VpcEndpointServiceDomainName(stack, 'EndpointDomain2', { + endpointService: vpces, + domainName: 'my-stuff-2.aws-cdk.dev', + publicHostedZone: zone, + }); + }).toThrow(); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh b/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh deleted file mode 100755 index cc9df52d130df..0000000000000 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/build.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -#------------------------------------------------------------------------------- -# builds a python-lambda deployment package -# -set -euo pipefail -set -x - -# make sure we run from the script directory -cd $(dirname $0) - -# clean up old bundle -bundle_out="$PWD/bundle.zip" -rm -f ${bundle_out} - -# prepare staging -staging="$(mktemp -d)" -piptemp="$(mktemp -d)" -trap "rm -rf ${staging} ${piptemp}" EXIT - -echo "staging lambda bundle at ${staging}..." - -# copy sources -rsync -av src/ "${staging}" - -cd ${staging} - -# install python requirements -# Must use --prefix to because --target cannot be used on -# platforms that have a default --prefix set. -pip3 install --ignore-installed --prefix ${piptemp} --no-user -r ${staging}/requirements.txt -mv ${piptemp}/lib/python*/*-packages/* . -[ -d ${piptemp}/lib64 ] && mv ${piptemp}/lib64/python*/*-packages/* . -rm -fr ./awscli/examples - - -# create archive -zip -qr ${bundle_out} . - -echo "bundle: ${bundle_out}" diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/src/aws b/packages/@aws-cdk/aws-s3-deployment/lambda/src/aws deleted file mode 100644 index 15ee2d7093f85..0000000000000 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/src/aws +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -# Licensed under the Apache License, Version 2.0 (the "License"). You -# may not use this file except in compliance with the License. A copy of -# the License is located at - -# http://aws.amazon.com/apache2.0/ - -# or in the "license" file accompanying this file. This file is -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF -# ANY KIND, either express or implied. See the License for the specific -# language governing permissions and limitations under the License. -import sys -import os - -if os.environ.get('LC_CTYPE', '') == 'UTF-8': - os.environ['LC_CTYPE'] = 'en_US.UTF-8' -import awscli.clidriver - - -def main(): - return awscli.clidriver.main() - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt b/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt deleted file mode 100644 index 9522336d851c8..0000000000000 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/src/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -awscli==1.16.218 -boto3==1.9.208 diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/src/setup.cfg b/packages/@aws-cdk/aws-s3-deployment/lambda/src/setup.cfg deleted file mode 100644 index eee4ab11a70e6..0000000000000 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/src/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[install] -prefix= \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts index 9cb91bffc4f16..352f350b91600 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts @@ -1,5 +1,3 @@ -import * as crypto from 'crypto'; -import * as fs from 'fs'; import * as path from 'path'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -7,12 +5,10 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import { AwsCliLayer } from '@aws-cdk/lambda-layer-awscli'; import { Construct } from 'constructs'; import { ISource, SourceConfig } from './source'; -const handlerCodeBundle = path.join(__dirname, '..', 'lambda', 'bundle.zip'); -const handlerSourceDirectory = path.join(__dirname, '..', 'lambda', 'src'); - export interface BucketDeploymentProps { /** * The sources from which to deploy the contents of this bucket. @@ -187,11 +183,10 @@ export class BucketDeployment extends cdk.Construct { throw new Error('Distribution must be specified if distribution paths are specified'); } - const assetHash = calcSourceHash(handlerSourceDirectory); - const handler = new lambda.SingletonFunction(this, 'CustomResourceHandler', { uuid: this.renderSingletonUuid(props.memoryLimit), - code: lambda.Code.fromAsset(handlerCodeBundle, { assetHash }), + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda')), + layers: [new AwsCliLayer(this, 'AwsCliLayer')], runtime: lambda.Runtime.PYTHON_3_6, handler: 'index.handler', lambdaPurpose: 'Custom::CDKBucketDeployment', @@ -253,25 +248,6 @@ export class BucketDeployment extends cdk.Construct { } } -/** - * We need a custom source hash calculation since the bundle.zip file - * contains python dependencies installed during build and results in a - * non-deterministic behavior. - * - * So we just take the `src/` directory of our custom resoruce code. - */ -function calcSourceHash(srcDir: string): string { - const sha = crypto.createHash('sha256'); - for (const file of fs.readdirSync(srcDir)) { - const data = fs.readFileSync(path.join(srcDir, file)); - sha.update(``); - sha.update(data); - sha.update(''); - } - - return sha.digest('hex'); -} - /** * Metadata */ diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/src/index.py b/packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py similarity index 98% rename from packages/@aws-cdk/aws-s3-deployment/lambda/src/index.py rename to packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py index 300e8d0772bf0..34a2da1681f4d 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/src/index.py +++ b/packages/@aws-cdk/aws-s3-deployment/lib/lambda/index.py @@ -182,9 +182,9 @@ def create_metadata_args(raw_user_metadata, raw_system_metadata): #--------------------------------------------------------------------------------------------------- # executes an "aws" cli command def aws_command(*args): - aws=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'aws') + aws="/opt/awscli/aws" # from AwsCliLayer logger.info("| aws %s" % ' '.join(args)) - subprocess.check_call(["python3", aws] + list(args)) + subprocess.check_call([aws] + list(args)) #--------------------------------------------------------------------------------------------------- # sends a response to cloudformation diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index df5c84cf3283b..fc802538bb33f 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -49,13 +49,9 @@ "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat", - "gen": "/bin/bash lambda/build.sh", "rosetta:extract": "yarn --silent jsii-rosetta extract" }, "cdk-build": { - "test": [ - "/bin/bash lambda/test.sh" - ], "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true @@ -73,8 +69,7 @@ "test/**", "examples/**", "lib/*.generated.js", - "build-tools/**", - "lambda/**" + "build-tools/**" ] }, "author": { @@ -98,6 +93,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/lambda-layer-awscli": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, @@ -109,6 +105,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/lambda-layer-awscli": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json index 3f025bb6bf6a9..aa8c7fddd60c7 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment-cloudfront.expected.json @@ -53,6 +53,50 @@ } } }, + "DeployWithInvalidationAwsCliLayerDEDD5787": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "/opt/awscli/aws" + } + }, "DeployWithInvalidationCustomResourceE3FF7455": { "Type": "Custom::CDKBucketDeployment", "Properties": { @@ -251,7 +295,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3Bucket77147E20" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3Bucket28CE5152" }, "S3Key": { "Fn::Join": [ @@ -264,7 +308,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED" } ] } @@ -277,7 +321,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED" } ] } @@ -295,6 +339,11 @@ ] }, "Runtime": "python3.6", + "Layers": [ + { + "Ref": "DeployWithInvalidationAwsCliLayerDEDD5787" + } + ], "Timeout": 900 }, "DependsOn": [ @@ -304,17 +353,29 @@ } }, "Parameters": { - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3Bucket77147E20": { + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": { + "Type": "String", + "Description": "S3 bucket for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F": { + "Type": "String", + "Description": "S3 key for asset version \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68ArtifactHashD9A515C3": { + "Type": "String", + "Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3Bucket28CE5152": { "Type": "String", - "Description": "S3 bucket for asset \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "S3 bucket for asset \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F": { + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED": { "Type": "String", - "Description": "S3 key for asset version \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "S3 key for asset version \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176ArtifactHash4E343C6C": { + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7ArtifactHash8926088E": { "Type": "String", - "Description": "Artifact hash for asset \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "Artifact hash for asset \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json index ff9a3162fbae8..267c6eaa23476 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json +++ b/packages/@aws-cdk/aws-s3-deployment/test/integ.bucket-deployment.expected.json @@ -10,6 +10,50 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "DeployMeAwsCliLayer5F9219E9": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "/opt/awscli/aws" + } + }, "DeployMeCustomResource4455EE35": { "Type": "Custom::CDKBucketDeployment", "Properties": { @@ -260,7 +304,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3Bucket77147E20" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3Bucket28CE5152" }, "S3Key": { "Fn::Join": [ @@ -273,7 +317,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED" } ] } @@ -286,7 +330,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F" + "Ref": "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED" } ] } @@ -304,6 +348,11 @@ ] }, "Runtime": "python3.6", + "Layers": [ + { + "Ref": "DeployMeAwsCliLayer5F9219E9" + } + ], "Timeout": 900 }, "DependsOn": [ @@ -316,6 +365,50 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "DeployWithPrefixAwsCliLayerC9DDB597": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "/opt/awscli/aws" + } + }, "DeployWithPrefixCustomResource9CF7C694": { "Type": "Custom::CDKBucketDeployment", "Properties": { @@ -380,6 +473,50 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, + "DeployWithMetadataAwsCliLayer2C774B41": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "/opt/awscli/aws" + } + }, "DeployWithMetadataCustomResourceA73C95DC": { "Type": "Custom::CDKBucketDeployment", "Properties": { @@ -447,6 +584,50 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "DeployMeWithoutDeletingFilesOnDestinationAwsCliLayer4D54C41C": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F" + } + ] + } + ] + } + ] + ] + } + }, + "Description": "/opt/awscli/aws" + } + }, "DeployMeWithoutDeletingFilesOnDestinationCustomResourceA390B02B": { "Type": "Custom::CDKBucketDeployment", "Properties": { @@ -507,17 +688,29 @@ } }, "Parameters": { - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3Bucket77147E20": { + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3BucketAEADE8C7": { + "Type": "String", + "Description": "S3 bucket for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68S3VersionKeyE415415F": { + "Type": "String", + "Description": "S3 key for asset version \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameterse9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68ArtifactHashD9A515C3": { + "Type": "String", + "Description": "Artifact hash for asset \"e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68\"" + }, + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3Bucket28CE5152": { "Type": "String", - "Description": "S3 bucket for asset \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "S3 bucket for asset \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176S3VersionKey4253216F": { + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7S3VersionKeyAF6E05ED": { "Type": "String", - "Description": "S3 key for asset version \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "S3 key for asset version \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, - "AssetParametersc9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176ArtifactHash4E343C6C": { + "AssetParameters3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7ArtifactHash8926088E": { "Type": "String", - "Description": "Artifact hash for asset \"c9ac4b3b65f3510a2088b7fd003de23d2aefac424025eb168725ce6769e3c176\"" + "Description": "Artifact hash for asset \"3c3ed777478fe845fb5950df5e26461242b39cf220f00e0683aab244d9d7c0f7\"" }, "AssetParametersfc4481abf279255619ff7418faa5d24456fef3432ea0da59c95542578ff0222eS3Bucket9CD8B20A": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda.test.ts b/packages/@aws-cdk/aws-s3-deployment/test/lambda.test.ts new file mode 100644 index 0000000000000..830938b3811b3 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda.test.ts @@ -0,0 +1,7 @@ +import { spawnSync } from 'child_process'; +import * as path from 'path'; + +test('lambda python pytest', () => { + const result = spawnSync(path.join(__dirname, 'lambda', 'test.sh'), { stdio: 'inherit' }); + expect(result.status).toBe(0); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/test/lambda/Dockerfile b/packages/@aws-cdk/aws-s3-deployment/test/lambda/Dockerfile new file mode 100644 index 0000000000000..8e3c05334efac --- /dev/null +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/Dockerfile @@ -0,0 +1,13 @@ +FROM public.ecr.aws/lambda/python:latest + +# add everything to /opt/awscli (this is where `aws` is executed from) +ADD . /opt/awscli + +# install boto3, which is available on Lambda +RUN pip3 install boto3 + +# run tests +WORKDIR /opt/awscli +RUN ["python3", "./test.py"] + +ENTRYPOINT [ "/bin/bash" ] \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/test/aws b/packages/@aws-cdk/aws-s3-deployment/test/lambda/aws old mode 100644 new mode 100755 similarity index 100% rename from packages/@aws-cdk/aws-s3-deployment/lambda/test/aws rename to packages/@aws-cdk/aws-s3-deployment/test/lambda/aws diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/test/test.py b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py similarity index 99% rename from packages/@aws-cdk/aws-s3-deployment/lambda/test/test.py rename to packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py index df48d6cdf38ff..cd88eaf6a5269 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/test/test.py +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.py @@ -12,6 +12,9 @@ from unittest.mock import MagicMock from unittest.mock import patch +# set TEST_AWSCLI_PATH to point to the "aws" stub we have here +scriptdir=os.path.dirname(os.path.realpath(__file__)) +os.environ['TEST_AWSCLI_PATH'] = os.path.join(scriptdir, 'aws') class TestHandler(unittest.TestCase): def setUp(self): diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh similarity index 67% rename from packages/@aws-cdk/aws-s3-deployment/lambda/test.sh rename to packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh index e87f8dfc2492b..a094c8ae16cfa 100755 --- a/packages/@aws-cdk/aws-s3-deployment/lambda/test.sh +++ b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.sh @@ -4,7 +4,6 @@ # # prepares a staging directory with the requirements set -e -set -x scriptdir=$(cd $(dirname $0) && pwd) # prepare staging directory @@ -13,11 +12,8 @@ mkdir -p ${staging} cd ${staging} # copy src and overlay with test -cp -f ${scriptdir}/src/* $PWD -cp -f ${scriptdir}/test/* $PWD +cp -f ${scriptdir}/../../lib/lambda/* $PWD +cp -f ${scriptdir}/* $PWD -# install deps -pip3 install --no-user -r requirements.txt -t . - -# run our tests -exec python3 test.py $@ +# this will run our tests inside the right environment +docker build . diff --git a/packages/@aws-cdk/aws-s3-deployment/lambda/test/test.zip b/packages/@aws-cdk/aws-s3-deployment/test/lambda/test.zip similarity index 100% rename from packages/@aws-cdk/aws-s3-deployment/lambda/test/test.zip rename to packages/@aws-cdk/aws-s3-deployment/test/lambda/test.zip diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 84e176f62a6a5..a81b67e2b7bb4 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -349,7 +349,7 @@ bucket.virtualHostedUrlForObject('objectname', { regional: false }); // Virtual ### Object Ownership -You can use the two following properties to specify the bucket [object Ownership]. +You can use the two following properties to specify the bucket [object Ownership]. [object Ownership]: https://docs.aws.amazon.com/AmazonS3/latest/dev/about-object-ownership.html @@ -365,10 +365,28 @@ new s3.Bucket(this, 'MyBucket', { #### Bucket owner preferred -The bucket owner will own the object if the object is uploaded with the bucket-owner-full-control canned ACL. Without this setting and canned ACL, the object is uploaded and remains owned by the uploading account. +The bucket owner will own the object if the object is uploaded with the bucket-owner-full-control canned ACL. Without this setting and canned ACL, the object is uploaded and remains owned by the uploading account. ```ts new s3.Bucket(this, 'MyBucket', { objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, }); ``` + +### Bucket deletion + +When a bucket is removed from a stack (or the stack is deleted), the S3 +bucket will be removed according to its removal policy (which by default will +simply orphan the bucket and leave it in your AWS account). If the removal +policy is set to `RemovalPolicy.DESTROY`, the bucket will be deleted as long +as it does not contain any objects. + +To override this and force all objects to get deleted during bucket deletion, +enable the`autoDeleteObjects` option. + +```ts +const bucket = new Bucket(this, 'MyTempFileBucket', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, +}); +``` diff --git a/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts b/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts new file mode 100644 index 0000000000000..5dd144b446e8e --- /dev/null +++ b/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts @@ -0,0 +1,42 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { S3 } from 'aws-sdk'; + +const s3 = new S3(); + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + switch (event.RequestType) { + case 'Create': + case 'Update': + return; + case 'Delete': + return onDelete(event); + } +} + +/** + * Recursively delete all items in the bucket + * + * @param bucketName the bucket name + */ +async function emptyBucket(bucketName: string) { + const listedObjects = await s3.listObjectVersions({ Bucket: bucketName }).promise(); + const contents = [...listedObjects.Versions ?? [], ...listedObjects.DeleteMarkers ?? []]; + if (contents.length === 0) { + return; + }; + + const records = contents.map((record: any) => ({ Key: record.Key, VersionId: record.VersionId })); + await s3.deleteObjects({ Bucket: bucketName, Delete: { Objects: records } }).promise(); + + if (listedObjects?.IsTruncated) { + await emptyBucket(bucketName); + } +} + +async function onDelete(deleteEvent: AWSLambda.CloudFormationCustomResourceDeleteEvent) { + const bucketName = deleteEvent.ResourceProperties?.BucketName; + if (!bucketName) { + throw new Error('No BucketName was provided.'); + } + await emptyBucket(bucketName); +} diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 30a6acf40d13c..51f4a30800cad 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1,8 +1,12 @@ import { EOL } from 'os'; +import * as path from 'path'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Fn, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { + Fn, IResource, Lazy, RemovalPolicy, Resource, Stack, Token, + CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, +} from '@aws-cdk/core'; import { Construct } from 'constructs'; import { BucketPolicy } from './bucket-policy'; import { IBucketNotificationDestination } from './destination'; @@ -12,6 +16,8 @@ import { LifecycleRule } from './rule'; import { CfnBucket } from './s3.generated'; import { parseBucketArn, parseBucketName } from './util'; +const AUTO_DELETE_OBJECTS_RESOURCE_TYPE = 'Custom::S3AutoDeleteObjects'; + export interface IBucket extends IResource { /** * The ARN of the bucket. @@ -1041,6 +1047,16 @@ export interface BucketProps { */ readonly removalPolicy?: RemovalPolicy; + /** + * Whether all objects should be automatically deleted when the bucket is + * removed from the stack or when the stack is deleted. + * + * Requires the `removalPolicy` to be set to `RemovalPolicy.DESTROY`. + * + * @default false + */ + readonly autoDeleteObjects?: boolean; + /** * Whether this bucket should have versioning turned on or not. * @@ -1326,6 +1342,14 @@ export class Bucket extends BucketBase { if (props.publicReadAccess) { this.grantPublicAccess(); } + + if (props.autoDeleteObjects) { + if (props.removalPolicy !== RemovalPolicy.DESTROY) { + throw new Error('Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'.'); + } + + this.enableAutoDeleteObjects(); + } } /** @@ -1728,6 +1752,42 @@ export class Bucket extends BucketBase { }; }); } + + private enableAutoDeleteObjects() { + const provider = CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_OBJECTS_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'auto-delete-objects-handler'), + runtime: CustomResourceProviderRuntime.NODEJS_12, + }); + + // Use a bucket policy to allow the custom resource to delete + // objects in the bucket + this.addToResourcePolicy(new iam.PolicyStatement({ + actions: [ + ...perms.BUCKET_READ_ACTIONS, // list objects + ...perms.BUCKET_DELETE_ACTIONS, // and then delete them + ], + resources: [ + this.bucketArn, + this.arnForObjects('*'), + ], + principals: [new iam.ArnPrincipal(provider.roleArn)], + })); + + const customResource = new CustomResource(this, 'AutoDeleteObjectsCustomResource', { + resourceType: AUTO_DELETE_OBJECTS_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + BucketName: this.bucketName, + }, + }); + + // Ensure bucket policy is deleted AFTER the custom resource otherwise + // we don't have permissions to list and delete in the bucket. + // (add a `if` to make TS happy) + if (this.policy) { + customResource.node.addDependency(this.policy); + } + } } /** diff --git a/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts b/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts new file mode 100644 index 0000000000000..e5a7072441974 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts @@ -0,0 +1,168 @@ +const mockS3Client = { + listObjectVersions: jest.fn().mockReturnThis(), + deleteObjects: jest.fn().mockReturnThis(), + promise: jest.fn(), +}; + +import { handler } from '../lib/auto-delete-objects-handler'; + +jest.mock('aws-sdk', () => { + return { S3: jest.fn(() => mockS3Client) }; +}); + +beforeEach(() => { + mockS3Client.listObjectVersions.mockReturnThis(); + mockS3Client.deleteObjects.mockReturnThis(); +}); + +afterEach(() => { + jest.resetAllMocks(); +}); + +test('does nothing on create event', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('deletes no objects on delete event when bucket has no objects', async () => { + // GIVEN + mockS3Client.promise.mockResolvedValue({ Versions: [] }); // listObjectVersions() call + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(1); + expect(mockS3Client.listObjectVersions).toHaveBeenCalledWith({ Bucket: 'MyBucket' }); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('deletes all objects on delete event', async () => { + // GIVEN + mockS3Client.promise.mockResolvedValue({ // listObjectVersions() call + Versions: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(1); + expect(mockS3Client.listObjectVersions).toHaveBeenCalledWith({ Bucket: 'MyBucket' }); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(1); + expect(mockS3Client.deleteObjects).toHaveBeenCalledWith({ + Bucket: 'MyBucket', + Delete: { + Objects: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }, + }); +}); + +test('delete event where bucket has many objects does recurse appropriately', async () => { + // GIVEN + mockS3Client.promise // listObjectVersions() call + .mockResolvedValueOnce({ + Versions: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + IsTruncated: true, + }) + .mockResolvedValueOnce(undefined) // deleteObjects() call + .mockResolvedValueOnce({ // listObjectVersions() call + Versions: [ + { Key: 'Key3', VersionId: 'VersionId3' }, + { Key: 'Key4', VersionId: 'VersionId4' }, + ], + }); + + // WHEN + const event: Partial = { + RequestType: 'Delete', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(2); + expect(mockS3Client.listObjectVersions).toHaveBeenCalledWith({ Bucket: 'MyBucket' }); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(2); + expect(mockS3Client.deleteObjects).toHaveBeenNthCalledWith(1, { + Bucket: 'MyBucket', + Delete: { + Objects: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }, + }); + expect(mockS3Client.deleteObjects).toHaveBeenNthCalledWith(2, { + Bucket: 'MyBucket', + Delete: { + Objects: [ + { Key: 'Key3', VersionId: 'VersionId3' }, + { Key: 'Key4', VersionId: 'VersionId4' }, + ], + }, + }); +}); + +// helper function to get around TypeScript expecting a complete event object, +// even though our tests only need some of the fields +async function invokeHandler(event: Partial) { + return handler(event as AWSLambda.CloudFormationCustomResourceEvent); +} diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index c9ea670dbb126..997a3abd4e56f 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -1,5 +1,5 @@ import { EOL } from 'os'; -import { expect, haveResource, haveResourceLike, SynthUtils, arrayWith, objectLike } from '@aws-cdk/assert'; +import { countResources, expect, haveResource, haveResourceLike, ResourcePart, SynthUtils, arrayWith, objectLike } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; @@ -2316,4 +2316,115 @@ nodeunitShim({ }); test.done(); }, + + 'with autoDeleteObjects'(test: Test) { + const stack = new cdk.Stack(); + + new s3.Bucket(stack, 'MyBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + }); + + expect(stack).to(haveResource('AWS::S3::Bucket', { + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + }, ResourcePart.CompleteDefinition)); + + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + Bucket: { + Ref: 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + 's3:DeleteObject*', + ], + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::GetAtt': [ + 'CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092', + 'Arn', + ], + }, + }, + 'Resource': [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + '/*', + ], + ], + }, + ], + }, + ], + 'Version': '2012-10-17', + }, + })); + + expect(stack).to(haveResource('Custom::S3AutoDeleteObjects', { + 'Properties': { + 'ServiceToken': { + 'Fn::GetAtt': [ + 'CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F', + 'Arn', + ], + }, + 'BucketName': { + 'Ref': 'MyBucketF68F3FF0', + }, + }, + 'DependsOn': [ + 'MyBucketPolicyE7FBAC7B', + ], + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + 'with autoDeleteObjects on multiple buckets'(test: Test) { + const stack = new cdk.Stack(); + + new s3.Bucket(stack, 'Bucket1', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + }); + + new s3.Bucket(stack, 'Bucket2', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + }); + + expect(stack).to(countResources('AWS::Lambda::Function', 1)); + + test.done(); + }, + + 'autoDeleteObjects throws if RemovalPolicy is not DESTROY'(test: Test) { + const stack = new cdk.Stack(); + + test.throws(() => new s3.Bucket(stack, 'MyBucket', { + autoDeleteObjects: true, + }), /Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'/); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json new file mode 100644 index 0000000000000..d9f263a8d840d --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json @@ -0,0 +1,306 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + "s3:DeleteObject*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "BucketAutoDeleteObjectsCustomResourceBAFD23C2": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "Bucket83908E77" + } + }, + "DependsOn": [ + "BucketPolicyE9A3008A" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "CustomS3PutObjectsCustomResourceProviderRole40D98C91": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:PutObject", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ] + } + } + ] + } + }, + "CustomS3PutObjectsCustomResourceProviderHandler1D33F0A9": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653S3BucketDB5FAF47" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653S3VersionKey9809F0E6" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653S3VersionKey9809F0E6" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3PutObjectsCustomResourceProviderRole40D98C91", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "CustomS3PutObjectsCustomResourceProviderRole40D98C91" + ] + }, + "PutObjectsCustomResource": { + "Type": "Custom::S3PutObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3PutObjectsCustomResourceProviderHandler1D33F0A9", + "Arn" + ] + }, + "BucketName": { + "Ref": "Bucket83908E77" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B": { + "Type": "String", + "Description": "S3 bucket for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + }, + "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51": { + "Type": "String", + "Description": "S3 key for asset version \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + }, + "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1ArtifactHash9ECACDFD": { + "Type": "String", + "Description": "Artifact hash for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + }, + "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653S3BucketDB5FAF47": { + "Type": "String", + "Description": "S3 bucket for asset \"f7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653\"" + }, + "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653S3VersionKey9809F0E6": { + "Type": "String", + "Description": "S3 key for asset version \"f7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653\"" + }, + "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653ArtifactHash7CDE16B1": { + "Type": "String", + "Description": "Artifact hash for asset \"f7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts new file mode 100644 index 0000000000000..83243212409d7 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts @@ -0,0 +1,39 @@ +import * as path from 'path'; +import { App, CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, RemovalPolicy, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as s3 from '../lib'; + +const PUT_OBJECTS_RESOURCE_TYPE = 'Custom::S3PutObjects'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const bucket = new s3.Bucket(this, 'Bucket', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, + }); + + // Put objects in the bucket to ensure auto delete works as expected + const serviceToken = CustomResourceProvider.getOrCreate(this, PUT_OBJECTS_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'put-objects-handler'), + runtime: CustomResourceProviderRuntime.NODEJS_12, + policyStatements: [{ + Effect: 'Allow', + Action: 's3:PutObject', + Resource: bucket.arnForObjects('*'), + }], + }); + new CustomResource(this, 'PutObjectsCustomResource', { + resourceType: PUT_OBJECTS_RESOURCE_TYPE, + serviceToken, + properties: { + BucketName: bucket.bucketName, + }, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-s3-bucket-auto-delete-objects'); +app.synth(); diff --git a/packages/@aws-cdk/aws-s3/test/put-objects-handler/index.ts b/packages/@aws-cdk/aws-s3/test/put-objects-handler/index.ts new file mode 100644 index 0000000000000..d4137760eb591 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/put-objects-handler/index.ts @@ -0,0 +1,28 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { S3 } from 'aws-sdk'; + +const s3 = new S3(); + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent): Promise { + switch (event.RequestType) { + case 'Create': + const bucketName = event.ResourceProperties.BucketName; + if (!bucketName) { + throw new Error('Missing BucketName'); + } + return putObjects(bucketName); + case 'Update': + case 'Delete': + return; + } +} + +async function putObjects(bucketName: string, n = 5) { + // Put n objects in parallel + await Promise.all([...Array(n).keys()] + .map(key => s3.putObject({ + Bucket: bucketName, + Key: `Key${key}`, + Body: `Body${key}`, + }).promise())); +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index eaa556e10882c..32044b4a23649 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -832,6 +832,10 @@ You can call the [`CreateTransformJob`](https://docs.aws.amazon.com/sagemaker/la new sfn.SagemakerTransformTask(this, 'Batch Inference', { transformJobName: 'MyTransformJob', modelName: 'MyModelName', + modelClientOptions: { + invocationMaxRetries: 3, // default is 0 + invocationTimeout: cdk.Duration.minutes(5), // default is 60 seconds + } role, transformInput: { transformDataSource: { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts index a9dc4ffbd6fd4..c46c6a098f816 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-ec2-task.ts @@ -42,7 +42,7 @@ export interface RunEcsEc2TaskProps extends CommonEcsRunTaskProps { /** * Run an ECS/EC2 Task in a StepFunctions workflow * - * @deprecated - replaced by `EcsEc2RunTask` + * @deprecated - replaced by `EcsRunTask` */ export class RunEcsEc2Task extends EcsRunTaskBase { constructor(props: RunEcsEc2TaskProps) { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts index 2910613c4d2c5..a9cf73b616351 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-fargate-task.ts @@ -41,7 +41,7 @@ export interface RunEcsFargateTaskProps extends CommonEcsRunTaskProps { /** * Start a service on an ECS cluster * - * @deprecated - replaced by `EcsFargateRunTask` + * @deprecated - replaced by `EcsRunTask` */ export class RunEcsFargateTask extends EcsRunTaskBase { constructor(props: RunEcsFargateTaskProps) { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts index 2e8e598d57747..6f35a7e45a858 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts @@ -500,6 +500,28 @@ export enum CompressionType { // Create Transform Job types // +/** + * Configures the timeout and maximum number of retries for processing a transform job invocation. + * + * @experimental + */ +export interface ModelClientOptions { + + /** + * The maximum number of retries when invocation requests are failing. + * + * @default 0 + */ + readonly invocationsMaxRetries?: number; + + /** + * The timeout duration for an invocation request. + * + * @default Duration.minutes(1) + */ + readonly invocationsTimeout?: Duration; +} + /** * Dataset to be transformed and the Amazon S3 location where it is stored. * diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts index ecc8845304686..4525ef489e3f2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts @@ -1,25 +1,25 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Size, Stack } from '@aws-cdk/core'; +import { Size, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; -import { BatchStrategy, S3DataType, TransformInput, TransformOutput, TransformResources } from './base-types'; +import { BatchStrategy, ModelClientOptions, S3DataType, TransformInput, TransformOutput, TransformResources } from './base-types'; import { renderTags } from './private/utils'; /** - * Properties for creating an Amazon SageMaker training job task + * Properties for creating an Amazon SageMaker transform job task * * @experimental */ export interface SageMakerCreateTransformJobProps extends sfn.TaskStateBaseProps { /** - * Training Job Name. + * Transform Job Name. */ readonly transformJobName: string; /** - * Role for the Training Job. + * Role for the Transform Job. * * @default - A role is created with `AmazonSageMakerFullAccess` managed policy */ @@ -59,6 +59,13 @@ export interface SageMakerCreateTransformJobProps extends sfn.TaskStateBaseProps */ readonly modelName: string; + /** + * Configures the timeout and maximum number of retries for processing a transform job invocation. + * + * @default - 0 retries and 60 seconds of timeout + */ + readonly modelClientOptions?: ModelClientOptions; + /** * Tags to be applied to the train job. * @@ -85,7 +92,7 @@ export interface SageMakerCreateTransformJobProps extends sfn.TaskStateBaseProps } /** - * Class representing the SageMaker Create Training Job task. + * Class representing the SageMaker Create Transform Job task. * * @experimental */ @@ -147,7 +154,7 @@ export class SageMakerCreateTransformJob extends sfn.TaskStateBase { } /** - * The execution role for the Sagemaker training job. + * The execution role for the Sagemaker transform job. * * Only available after task has been added to a state machine. */ @@ -164,6 +171,7 @@ export class SageMakerCreateTransformJob extends sfn.TaskStateBase { ...this.renderEnvironment(this.props.environment), ...(this.props.maxConcurrentTransforms ? { MaxConcurrentTransforms: this.props.maxConcurrentTransforms } : {}), ...(this.props.maxPayload ? { MaxPayloadInMB: this.props.maxPayload.toMebibytes() } : {}), + ...this.props.modelClientOptions ? this.renderModelClientOptions(this.props.modelClientOptions) : {}, ModelName: this.props.modelName, ...renderTags(this.props.tags), ...this.renderTransformInput(this.transformInput), @@ -173,6 +181,23 @@ export class SageMakerCreateTransformJob extends sfn.TaskStateBase { }; } + private renderModelClientOptions(options: ModelClientOptions): { [key: string]: any } { + const retries = options.invocationsMaxRetries; + if (!Token.isUnresolved(retries) && retries? (retries < 0 || retries > 3): false) { + throw new Error(`invocationsMaxRetries should be between 0 and 3. Received: ${retries}.`); + } + const timeout = options.invocationsTimeout?.toSeconds(); + if (!Token.isUnresolved(timeout) && timeout? (timeout < 1 || timeout > 3600): false) { + throw new Error(`invocationsTimeout should be between 1 and 3600 seconds. Received: ${timeout}.`); + } + return { + ModelClientConfig: { + InvocationsMaxRetries: retries ?? 0, + InvocationsTimeoutInSeconds: timeout ?? 60, + }, + }; + } + private renderTransformInput(input: TransformInput): { [key: string]: any } { return { TransformInput: { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts index b80529977cd00..978d5bfa4ab37 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts @@ -102,6 +102,10 @@ test('create complex transform job', () => { const task = new SageMakerCreateTransformJob(stack, 'TransformTask', { transformJobName: 'MyTransformJob', modelName: 'MyModelName', + modelClientOptions: { + invocationsMaxRetries: 1, + invocationsTimeout: cdk.Duration.minutes(20), + }, integrationPattern: sfn.IntegrationPattern.RUN_JOB, role, transformInput: { @@ -151,6 +155,10 @@ test('create complex transform job', () => { Parameters: { TransformJobName: 'MyTransformJob', ModelName: 'MyModelName', + ModelClientConfig: { + InvocationsMaxRetries: 1, + InvocationsTimeoutInSeconds: 1200, + }, TransformInput: { DataSource: { S3DataSource: { diff --git a/packages/@aws-cdk/core/lib/stack-trace.ts b/packages/@aws-cdk/core/lib/stack-trace.ts index 67e47dea5010a..2782799c75453 100644 --- a/packages/@aws-cdk/core/lib/stack-trace.ts +++ b/packages/@aws-cdk/core/lib/stack-trace.ts @@ -8,11 +8,8 @@ import { debugModeEnabled } from './debug'; * large. Consequently, users are stronly advised to condition capturing stack * traces to specific user opt-in. * - * If the `CDK_DISABLE_STACK_TRACE` environment variable is set (to any value, - * except for an empty string), no stack traces will be captured, and instead - * the literal value `['stack traces disabled']` will be returned instead. This - * is only true if the `CDK_DEBUG` environment variable is not set to `'true'` - * or '1', in which case stack traces are *always* captured. + * Stack traces will only be captured if the `CDK_DEBUG` environment variable + * is set to `'true'` or `1`. * * @param below an optional function starting from which stack frames will be * ignored. Defaults to the `captureStackTrace` function itself. @@ -26,7 +23,7 @@ export function captureStackTrace( below: Function = captureStackTrace, limit = Number.MAX_SAFE_INTEGER, ): string[] { - if (process.env.CDK_DISABLE_STACK_TRACE && !debugModeEnabled()) { + if (!debugModeEnabled()) { return ['stack traces disabled']; } diff --git a/packages/@aws-cdk/core/test/tokens.test.ts b/packages/@aws-cdk/core/test/tokens.test.ts index 72b37d0881f73..eba1e75686048 100644 --- a/packages/@aws-cdk/core/test/tokens.test.ts +++ b/packages/@aws-cdk/core/test/tokens.test.ts @@ -656,6 +656,7 @@ nodeunitShim({ } const previousValue = process.env.CDK_DEBUG; + process.env.CDK_DEBUG = 'true'; const x = showMeInTheStackTrace(); let message; diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts index a61e7ab475939..3ae10fdf2e560 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import * as cfn from '@aws-cdk/aws-cloudformation'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import { Construct as CoreConstruct, Duration } from '@aws-cdk/core'; @@ -70,6 +71,24 @@ export interface ProviderProps { * @default logs.RetentionDays.INFINITE */ readonly logRetention?: logs.RetentionDays; + + /** + * The vpc to provision the lambda functions in. + * + * @default - functions are not provisioned inside a vpc. + */ + readonly vpc?: ec2.IVpc; + + /** + * Which subnets from the VPC to place the lambda functions in. + * + * Only used if 'vpc' is supplied. Note: internet access for Lambdas + * requires a NAT gateway, so picking Public subnets is not allowed. + * + * @default - the Vpc default strategy if not specified + */ + readonly vpcSubnets?: ec2.SubnetSelection; + } /** @@ -97,6 +116,8 @@ export class Provider extends CoreConstruct implements cfn.ICustomResourceProvid private readonly entrypoint: lambda.Function; private readonly logRetention?: logs.RetentionDays; + private readonly vpc?: ec2.IVpc; + private readonly vpcSubnets?: ec2.SubnetSelection; constructor(scope: Construct, id: string, props: ProviderProps) { super(scope, id); @@ -110,6 +131,8 @@ export class Provider extends CoreConstruct implements cfn.ICustomResourceProvid this.isCompleteHandler = props.isCompleteHandler; this.logRetention = props.logRetention; + this.vpc = props.vpc; + this.vpcSubnets = props.vpcSubnets; const onEventFunction = this.createFunction(consts.FRAMEWORK_ON_EVENT_HANDLER_NAME); @@ -153,6 +176,8 @@ export class Provider extends CoreConstruct implements cfn.ICustomResourceProvid handler: `framework.${entrypoint}`, timeout: FRAMEWORK_HANDLER_TIMEOUT, logRetention: this.logRetention, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, }); fn.addEnvironment(consts.USER_ON_EVENT_FUNCTION_ARN_ENV, this.onEventHandler.functionArn); diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 61908e19814c2..d291d6b66815b 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -96,6 +96,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, @@ -106,6 +107,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts b/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts index 0d0f98f634d07..deed031d8909f 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/provider.test.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import * as ec2 from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import { Duration, Stack } from '@aws-cdk/core'; @@ -7,6 +8,61 @@ import * as util from '../../lib/provider-framework/util'; import '@aws-cdk/assert/jest'; +test('vpc is applied to all framework functions', () => { + + // GIVEN + const stack = new Stack(); + + const vpc = new ec2.Vpc(stack, 'Vpc'); + + // WHEN + new cr.Provider(stack, 'MyProvider', { + onEventHandler: new lambda.Function(stack, 'OnEvent', { + code: lambda.Code.fromInline('foo'), + handler: 'index.onEvent', + runtime: lambda.Runtime.NODEJS_10_X, + }), + isCompleteHandler: new lambda.Function(stack, 'IsComplete', { + code: lambda.Code.fromInline('foo'), + handler: 'index.isComplete', + runtime: lambda.Runtime.NODEJS_10_X, + }), + vpc: vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Handler: 'framework.onEvent', + VpcConfig: { + SubnetIds: [ + { Ref: 'VpcPrivateSubnet1Subnet536B997A' }, + { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }, + ], + }, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Handler: 'framework.isComplete', + VpcConfig: { + SubnetIds: [ + { Ref: 'VpcPrivateSubnet1Subnet536B997A' }, + { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }, + ], + }, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::Function', { + Handler: 'framework.onTimeout', + VpcConfig: { + SubnetIds: [ + { Ref: 'VpcPrivateSubnet1Subnet536B997A' }, + { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }, + ], + }, + }); + +}); + test('minimal setup', () => { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/lambda-layer-awscli/.eslintrc.js b/packages/@aws-cdk/lambda-layer-awscli/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/lambda-layer-awscli/.gitignore b/packages/@aws-cdk/lambda-layer-awscli/.gitignore new file mode 100644 index 0000000000000..a86aa21cbaad0 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/.gitignore @@ -0,0 +1,20 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml +lib/*.zip diff --git a/packages/@aws-cdk/lambda-layer-awscli/.npmignore b/packages/@aws-cdk/lambda-layer-awscli/.npmignore new file mode 100644 index 0000000000000..a7cfe248b3336 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/.npmignore @@ -0,0 +1,30 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml + +layer/ + +test/ \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-awscli/LICENSE b/packages/@aws-cdk/lambda-layer-awscli/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/lambda-layer-awscli/NOTICE b/packages/@aws-cdk/lambda-layer-awscli/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/lambda-layer-awscli/README.md b/packages/@aws-cdk/lambda-layer-awscli/README.md new file mode 100644 index 0000000000000..ae6460fae09d3 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/README.md @@ -0,0 +1,28 @@ +# AWS Lambda Layer with AWS CLI + + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + +--- + + + + +This module exports a single class called `AwsCliLayer` which is a `lambda.Layer` that bundles the AWS CLI. + +Usage: + +```ts +const fn = new lambda.Function(...); +fn.addLayers(new AwsCliLayer(stack, 'AwsCliLayer')); +``` + +The CLI will be installed under `/opt/awscli/aws`. diff --git a/packages/@aws-cdk/lambda-layer-awscli/jest.config.js b/packages/@aws-cdk/lambda-layer-awscli/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/lambda-layer-awscli/layer/.dockerignore b/packages/@aws-cdk/lambda-layer-awscli/layer/.dockerignore new file mode 100644 index 0000000000000..88a84e55aa43b --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/layer/.dockerignore @@ -0,0 +1 @@ +build.sh diff --git a/packages/@aws-cdk/lambda-layer-awscli/layer/Dockerfile b/packages/@aws-cdk/lambda-layer-awscli/layer/Dockerfile new file mode 100644 index 0000000000000..7cf1287e2023c --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/layer/Dockerfile @@ -0,0 +1,51 @@ +FROM public.ecr.aws/lambda/provided:latest + +# +# versions +# + +ARG AWSCLI_VERSION=1.18.198 + +USER root +RUN mkdir -p /opt +WORKDIR /tmp + +# +# tools +# + +RUN yum update -y \ + && yum install -y zip unzip wget tar gzip + +# +# aws cli +# + +RUN curl https://s3.amazonaws.com/aws-cli/awscli-bundle-${AWSCLI_VERSION}.zip -o awscli-bundle.zip +RUN unzip awscli-bundle.zip +RUN ./awscli-bundle/install -i /opt/awscli -b /opt/awscli/aws + +# organize for self-contained usage +RUN mv /opt/awscli /opt/awscli.tmp +RUN mv /opt/awscli.tmp/lib/python2.7/site-packages /opt/awscli +RUN mv /opt/awscli.tmp/bin /opt/awscli/bin +RUN mv /opt/awscli/bin/aws /opt/awscli + +# cleanup +RUN rm -fr /opt/awscli.tmp +RUN rm -rf \ + /opt/awscli/pip* \ + /opt/awscli/setuptools* \ + /opt/awscli/awscli/examples + +# +# create the bundle +# + +RUN cd /opt \ + && zip --symlinks -r ../layer.zip * \ + && echo "/layer.zip is ready" \ + && ls -alh /layer.zip; + +WORKDIR / +ENTRYPOINT [ "/bin/bash" ] \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-awscli/layer/build.sh b/packages/@aws-cdk/lambda-layer-awscli/layer/build.sh new file mode 100755 index 0000000000000..a7c13263ebdce --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/layer/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +cd $(dirname $0) + +echo ">> Building AWS Lambda layer inside a docker image..." + +TAG='aws-lambda-layer' + +docker build -t ${TAG} . + +echo ">> Extrating layer.zip from the build container..." +CONTAINER=$(docker run -d ${TAG} false) +docker cp ${CONTAINER}:/layer.zip ../lib/layer.zip + +echo ">> Stopping container..." +docker rm -f ${CONTAINER} +echo ">> lib/layer.zip is ready" diff --git a/packages/@aws-cdk/lambda-layer-awscli/lib/awscli-layer.ts b/packages/@aws-cdk/lambda-layer-awscli/lib/awscli-layer.ts new file mode 100644 index 0000000000000..525babcac82f3 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/lib/awscli-layer.ts @@ -0,0 +1,27 @@ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Construct } from 'constructs'; + +/** + * An AWS Lambda layer that includes the AWS CLI. + */ +export class AwsCliLayer extends lambda.LayerVersion { + constructor(scope: Construct, id: string) { + super(scope, id, { + code: lambda.Code.fromAsset(path.join(__dirname, 'layer.zip'), { + // we hash the Dockerfile (it contains the tools versions) because hashing the zip is non-deterministic + assetHash: hashFile(path.join(__dirname, '..', 'layer', 'Dockerfile')), + }), + description: '/opt/awscli/aws', + }); + } +} + +function hashFile(fileName: string) { + return crypto + .createHash('sha256') + .update(fs.readFileSync(fileName)) + .digest('hex'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-awscli/lib/index.ts b/packages/@aws-cdk/lambda-layer-awscli/lib/index.ts new file mode 100644 index 0000000000000..8bf4706a24cc4 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/lib/index.ts @@ -0,0 +1 @@ +export * from './awscli-layer'; diff --git a/packages/@aws-cdk/lambda-layer-awscli/package.json b/packages/@aws-cdk/lambda-layer-awscli/package.json new file mode 100644 index 0000000000000..344ffc2e43521 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/package.json @@ -0,0 +1,106 @@ +{ + "name": "@aws-cdk/lambda-layer-awscli", + "private": false, + "version": "0.0.0", + "description": "An AWS Lambda layer that contains the AWS CLI", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.lambdalayer.awscli", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cdk-lambda-layer-awscli" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.LambdaLayer.AwsCli", + "packageId": "Amazon.CDK.LambdaLayer.AwsCli", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.lambda-layer-awscli", + "module": "aws_cdk.lambda_layer_awscli", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/lambda-layer-awscli" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test": "npm run build && npm test", + "build+test+package": "npm run build+test && npm run package", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract" + }, + "keywords": [ + "aws", + "cdk", + "example", + "construct", + "library" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "jest": "^26.6.3", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "cdk-build": { + "pre": [ + "layer/build.sh" + ], + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "ubergen": { + "exclude": false + } +} diff --git a/packages/@aws-cdk/lambda-layer-awscli/test/awscli-layer.test.ts b/packages/@aws-cdk/lambda-layer-awscli/test/awscli-layer.test.ts new file mode 100644 index 0000000000000..923d4f3ceaf64 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-awscli/test/awscli-layer.test.ts @@ -0,0 +1,16 @@ +import { Stack } from '@aws-cdk/core'; +import { AwsCliLayer } from '../lib'; +import '@aws-cdk/assert/jest'; + +test('synthesized to a layer version', () => { + //GIVEN + const stack = new Stack(); + + // WHEN + new AwsCliLayer(stack, 'MyLayer'); + + // THEN + expect(stack).toHaveResource('AWS::Lambda::LayerVersion', { + Description: '/opt/awscli/aws', + }); +}); diff --git a/packages/@aws-cdk/lambda-layer-kubectl/.eslintrc.js b/packages/@aws-cdk/lambda-layer-kubectl/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/lambda-layer-kubectl/.gitignore b/packages/@aws-cdk/lambda-layer-kubectl/.gitignore new file mode 100644 index 0000000000000..a86aa21cbaad0 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/.gitignore @@ -0,0 +1,20 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml +lib/*.zip diff --git a/packages/@aws-cdk/lambda-layer-kubectl/.npmignore b/packages/@aws-cdk/lambda-layer-kubectl/.npmignore new file mode 100644 index 0000000000000..a7cfe248b3336 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/.npmignore @@ -0,0 +1,30 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml + +layer/ + +test/ \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-kubectl/LICENSE b/packages/@aws-cdk/lambda-layer-kubectl/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/lambda-layer-kubectl/NOTICE b/packages/@aws-cdk/lambda-layer-kubectl/NOTICE new file mode 100644 index 0000000000000..ebe97bffffac1 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/NOTICE @@ -0,0 +1,37 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +---------------- + +** kubectl - https://github.com/kubernetes/kubectl +Copyright 2017 The Kubernetes Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +---------------- + +** helm - https://github.com/helm/helm +Copyright 2016 The Kubernetes Authors All Rights Reserved + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/packages/@aws-cdk/lambda-layer-kubectl/README.md b/packages/@aws-cdk/lambda-layer-kubectl/README.md new file mode 100644 index 0000000000000..ae6460fae09d3 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/README.md @@ -0,0 +1,28 @@ +# AWS Lambda Layer with AWS CLI + + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + +--- + + + + +This module exports a single class called `AwsCliLayer` which is a `lambda.Layer` that bundles the AWS CLI. + +Usage: + +```ts +const fn = new lambda.Function(...); +fn.addLayers(new AwsCliLayer(stack, 'AwsCliLayer')); +``` + +The CLI will be installed under `/opt/awscli/aws`. diff --git a/packages/@aws-cdk/lambda-layer-kubectl/jest.config.js b/packages/@aws-cdk/lambda-layer-kubectl/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/lambda-layer-kubectl/layer/.dockerignore b/packages/@aws-cdk/lambda-layer-kubectl/layer/.dockerignore new file mode 100644 index 0000000000000..88a84e55aa43b --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/layer/.dockerignore @@ -0,0 +1 @@ +build.sh diff --git a/packages/@aws-cdk/lambda-layer-kubectl/layer/Dockerfile b/packages/@aws-cdk/lambda-layer-kubectl/layer/Dockerfile new file mode 100644 index 0000000000000..c04c0cc9bfbea --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/layer/Dockerfile @@ -0,0 +1,47 @@ +# base lambda image +FROM public.ecr.aws/lambda/provided:latest + +# +# versions +# + +ARG KUBECTL_VERSION=1.20.0 +ARG HELM_VERSION=3.4.2 + +USER root +RUN mkdir -p /opt +WORKDIR /tmp + +# +# tools +# + +RUN yum update -y \ + && yum install -y zip unzip wget tar gzip + +# +# kubectl +# + +RUN mkdir -p /opt/kubectl +RUN cd /opt/kubectl && curl -LO "https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl" +RUN chmod +x /opt/kubectl/kubectl + +# +# helm +# + +RUN mkdir -p /tmp/helm && wget -qO- https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz | tar -xvz -C /tmp/helm +RUN mkdir -p /opt/helm && cp /tmp/helm/linux-amd64/helm /opt/helm/helm + +# +# create the bundle +# + +RUN cd /opt \ + && zip --symlinks -r ../layer.zip * \ + && echo "/layer.zip is ready" \ + && ls -alh /layer.zip; + +WORKDIR / +ENTRYPOINT [ "/bin/bash" ] \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-kubectl/layer/build.sh b/packages/@aws-cdk/lambda-layer-kubectl/layer/build.sh new file mode 100755 index 0000000000000..a7c13263ebdce --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/layer/build.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +cd $(dirname $0) + +echo ">> Building AWS Lambda layer inside a docker image..." + +TAG='aws-lambda-layer' + +docker build -t ${TAG} . + +echo ">> Extrating layer.zip from the build container..." +CONTAINER=$(docker run -d ${TAG} false) +docker cp ${CONTAINER}:/layer.zip ../lib/layer.zip + +echo ">> Stopping container..." +docker rm -f ${CONTAINER} +echo ">> lib/layer.zip is ready" diff --git a/packages/@aws-cdk/lambda-layer-kubectl/lib/index.ts b/packages/@aws-cdk/lambda-layer-kubectl/lib/index.ts new file mode 100644 index 0000000000000..1967f462ac1dc --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/lib/index.ts @@ -0,0 +1 @@ +export * from './kubectl-layer'; diff --git a/packages/@aws-cdk/lambda-layer-kubectl/lib/kubectl-layer.ts b/packages/@aws-cdk/lambda-layer-kubectl/lib/kubectl-layer.ts new file mode 100644 index 0000000000000..91c8b3e9cc1de --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/lib/kubectl-layer.ts @@ -0,0 +1,27 @@ +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Construct } from 'constructs'; + +/** + * An AWS Lambda layer that includes `kubectl` and `helm`. + */ +export class KubectlLayer extends lambda.LayerVersion { + constructor(scope: Construct, id: string) { + super(scope, id, { + code: lambda.Code.fromAsset(path.join(__dirname, 'layer.zip'), { + // we hash the Dockerfile (it contains the tools versions) because hashing the zip is non-deterministic + assetHash: hashFile(path.join(__dirname, '..', 'layer', 'Dockerfile')), + }), + description: '/opt/kubectl/kubectl and /opt/helm/helm', + }); + } +} + +function hashFile(fileName: string) { + return crypto + .createHash('sha256') + .update(fs.readFileSync(fileName)) + .digest('hex'); +} \ No newline at end of file diff --git a/packages/@aws-cdk/lambda-layer-kubectl/package.json b/packages/@aws-cdk/lambda-layer-kubectl/package.json new file mode 100644 index 0000000000000..8a6c18ac0e9c4 --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/package.json @@ -0,0 +1,112 @@ +{ + "name": "@aws-cdk/lambda-layer-kubectl", + "private": false, + "version": "0.0.0", + "description": "An AWS Lambda layer that contains the `kubectl` and `helm`", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.lambdalayer.kubectl", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cdk-lambda-layer-kubectl" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.LambdaLayer.Kubectl", + "packageId": "Amazon.CDK.LambdaLayer.Kubectl", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.lambda-layer-kubectl", + "module": "aws_cdk.lambda_layer_kubectl", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/lambda-layer-kubectl" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test": "npm run build && npm test", + "build+test+package": "npm run build+test && npm run package", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract" + }, + "keywords": [ + "aws", + "cdk", + "example", + "construct", + "library" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "jest": "^26.6.3", + "pkglint": "0.0.0" + }, + "pkglint": { + "attribution": [ + "kubectl", + "helm" + ] + }, + "dependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "cdk-build": { + "pre": [ + "layer/build.sh" + ], + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "ubergen": { + "exclude": false + } +} diff --git a/packages/@aws-cdk/lambda-layer-kubectl/test/kubectl-layer.test.ts b/packages/@aws-cdk/lambda-layer-kubectl/test/kubectl-layer.test.ts new file mode 100644 index 0000000000000..d73303dd5e32d --- /dev/null +++ b/packages/@aws-cdk/lambda-layer-kubectl/test/kubectl-layer.test.ts @@ -0,0 +1,16 @@ +import { Stack } from '@aws-cdk/core'; +import { KubectlLayer } from '../lib'; +import '@aws-cdk/assert/jest'; + +test('synthesized to a layer version', () => { + //GIVEN + const stack = new Stack(); + + // WHEN + new KubectlLayer(stack, 'MyLayer'); + + // THEN + expect(stack).toHaveResource('AWS::Lambda::LayerVersion', { + Description: '/opt/kubectl/kubectl and /opt/helm/helm', + }); +}); diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 58064177abf74..718ba9b503690 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -648,6 +648,14 @@ These command lines explained: > account only to bootstrap it and provision the initial pipeline. Otherwise, > access to administrative credentials should be dropped as soon as possible. +
+ +> **On the use of AdministratorAccess**: The use of the `AdministratorAccess` policy +> ensures that your pipeline can deploy every type of AWS resource to your account. +> Make sure you trust all the code and dependencies that make up your CDK app. +> Check with the appropriate department within your organization to decide on the +> proper policy to use. + ### Migrating from old bootstrap stack The bootstrap stack is a CloudFormation stack in your account named diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 3e637757b47de..e311335fa1b04 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -260,6 +260,8 @@ "@aws-cdk/aws-wafregional": "0.0.0", "@aws-cdk/aws-wafv2": "0.0.0", "@aws-cdk/aws-workspaces": "0.0.0", + "@aws-cdk/lambda-layer-awscli": "0.0.0", + "@aws-cdk/lambda-layer-kubectl": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cloudformation-include": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index f32367e7265ea..188ceb25e8c26 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -194,6 +194,8 @@ "@aws-cdk/pipelines": "0.0.0", "@aws-cdk/region-info": "0.0.0", "@aws-cdk/yaml-cfn": "0.0.0", + "@aws-cdk/lambda-layer-awscli": "0.0.0", + "@aws-cdk/lambda-layer-kubectl": "0.0.0", "constructs": "^3.2.0", "fs-extra": "^9.0.1", "jsii-reflect": "^1.15.0", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index b78a8c6adcc9c..d3d36459b0941 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -264,9 +264,13 @@ "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", "@aws-cdk/cx-api": "0.0.0", + "@aws-cdk/lambda-layer-awscli": "0.0.0", + "@aws-cdk/lambda-layer-kubectl": "0.0.0", "@aws-cdk/pipelines": "0.0.0", "@aws-cdk/region-info": "0.0.0", "@aws-cdk/yaml-cfn": "0.0.0", + "@aws-cdk/lambda-layer-awscli": "0.0.0", + "@aws-cdk/lambda-layer-kubectl": "0.0.0", "@types/fs-extra": "^8.1.1", "@types/node": "^10.17.48", "cdk-build-tools": "0.0.0", diff --git a/scripts/check-api-compatibility.sh b/scripts/check-api-compatibility.sh index ca15553418f72..e49d684e8b98c 100755 --- a/scripts/check-api-compatibility.sh +++ b/scripts/check-api-compatibility.sh @@ -68,7 +68,8 @@ if ! ${SKIP_DOWNLOAD:-false}; then mkdir -p $tmpdir echo "Installing from NPM..." >&2 - (cd $tmpdir && npm install --prefix $tmpdir $existing_names) + # use npm7 to automatically install peer dependencies + (cd $tmpdir && npx npm@^7.0.0 install --prefix $tmpdir $existing_names) fi #---------------------------------------------------------------------- diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index 1729eaef0a23d..88688249729a6 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -147,6 +147,7 @@ export class ThirdPartyAttributions extends ValidationRule { return; } const bundled = pkg.getAllBundledDependencies().filter(dep => !dep.startsWith('@aws-cdk')); + const attribution = pkg.json.pkglint?.attribution ?? []; const noticePath = path.join(pkg.packageRoot, 'NOTICE'); const lines = fs.existsSync(noticePath) ? fs.readFileSync(noticePath, { encoding: 'utf8' }).split('\n') @@ -164,9 +165,9 @@ export class ThirdPartyAttributions extends ValidationRule { } } for (const attr of attributions) { - if (!bundled.includes(attr)) { + if (!bundled.includes(attr) && !attribution.includes(attr)) { pkg.report({ - message: `Unnecessary attribution found for dependency '${attr}' in NOTICE file.`, + message: `Unnecessary attribution found for dependency '${attr}' in NOTICE file. Attribution is determined from package.json (all "bundledDependencies" or the list in "pkglint.attribution")`, ruleName: this.name, }); } diff --git a/version.v1.json b/version.v1.json index 0e9c47b816903..bfcd3a035b942 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.78.0" + "version": "1.80.0" } diff --git a/yarn.lock b/yarn.lock index b45567e77170e..bfbd12e27e0ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6216,7 +6216,7 @@ jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^26.6.3: +jest@^26.6.0, jest@^26.6.3: version "26.6.3" resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== @@ -7370,9 +7370,9 @@ node-modules-regexp@^1.0.0: integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= node-notifier@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.0.tgz#a7eee2d51da6d0f7ff5094bc7108c911240c1620" - integrity sha512-46z7DUmcjoYdaWyXouuFNNfUo6eFa94t23c53c+lG/9Cvauk4a98rAUp9672X5dxGdQmLpPzTxzu8f/OeEPaFA== + version "8.0.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.1.tgz#f86e89bbc925f2b068784b31f382afdc6ca56be1" + integrity sha512-BvEXF+UmsnAfYfoapKM9nGxnP+Wn7P91YfXmrKnfcYCx6VBeoN5Ez5Ogck6I8Bi5k4RlpqRYaw75pAwzX9OphA== dependencies: growly "^1.3.0" is-wsl "^2.2.0"