AWS CDK: 6 Things You Need To Know
Steven Smiley
Posted on January 27, 2022
The AWS Cloud Development Kit (CDK) has quickly become my preferred tool for defining AWS infrastructure. I built a project recently that would have been significantly more difficult with any other tool because it needed to span multiple AWS accounts, multiple AWS regions, and deploy several stacks of resources across them.
While the CDK docs are pretty good, and the CDK Book is a great way to learn the basics, here are some important features I have learned along the way.
Direct CloudFormation, a.k.a. Escape Hatches
CDK includes several 'level 2' constructs which are excellent: they provide sane defaults, improved autocompletion, boilerplate, and glue logic built-in. However, most AWS services off-the-beaten path don't have them yet, so you need to be prepared to effectively use 'level 1' constructs. These translate directly to CloudFormation resources, and include the full capability that CloudFormation does. This is huge, as you can confidently start a CDK project knowing that, at minimum, you'll have level 1 constructs for everything supported by CloudFormation.
Level 1 constructs will begin with Cfn
, and you should refer to the CloudFormation resource reference when providing parameters, as there is no meaningful autocomplete support.
To pass references between these resources, use .ref
to pass the ARN that will be generated at deploy-time.
Here's an example, creating a Systems Manager Maintenance Window to run a Command Document on a schedule:
with open('example/example_ssm_document.json', 'r') as document_content:
document_content = json.load(document_content)
document = ssm.CfnDocument(self, "ExampleCommandDocument",
content=document_content,
document_format="JSON",
document_type="Command",
)
window = ssm.CfnMaintenanceWindow(self, "ExampleWindow",
allow_unassociated_targets=True,
cutoff=0,
name="ExampleWindow",
duration=1,
schedule='rate(1 hour)'
)
task = ssm.CfnMaintenanceWindowTask(self, "ExampleTask",
window_id=window.ref,
max_concurrency='1',
max_errors='1',
priority=1,
targets=[{
'key': 'InstanceIds',
'values': ['i-xxxxxxxxxxxxxx']
}],
task_arn=document.ref,
task_type='RUN_COMMAND',
)
Custom Resources
In the rare case you need to define a resource that isn't supported by CloudFormation, you can still automate it using Custom Resources. Most of the time, these will just be single AWS API calls, and CDK has an awesome construct just for that: AwsCustomResource
. You provide the service, API action, and parameters, and CDK will create and invoke a Lambda function accordingly. But here are some tricks that aren't immediately obvious:
-
AwsCustomResource
uses the JavaScript SDK, so you have to provide the services, actions, and parameters using that specification for spelling/casing/etc. Refer to the JavaScript SDK for those. - The
physical_resource_id
parameter needs a verification token from the API response, which is easy to retrieve but wasn't intuitive:PhysicalResourceId.from_response("VerificationToken")
- You can pass parameters directly, but you can't dynamically retrieve them, for example a secret from Secrets Manager. This can be a complete killer! I tried to create an AD Connector, and of course don't want to embed a password into the code, but attempting to retrieve it as below does not work.
AwsCustomResource(self, id='ADConnector',
policy=AwsCustomResourcePolicy.from_sdk_calls(
resources=AwsCustomResourcePolicy.ANY_RESOURCE),
on_create=AwsSdkCall(
service="DirectoryService",
action="connectDirectory",
parameters={
"Name": ad_fqdn,
"Password": cdk.SecretValue.secrets_manager(secret_id=ad_service_account_secret_arn, json_field="password").to_string(),
"ConnectSettings": {
"VpcId": vpc_id,
"SubnetIds": subnet_ids,
"CustomerDnsIps": customer_dns_ips,
"CustomerUserName": cdk.SecretValue.secrets_manager(secret_id=ad_service_account_secret_arn, json_field="username").to_string(),
},
"Size": "Small",
},
physical_resource_id=PhysicalResourceId.from_response(
"VerificationToken")
))
Something on my #awswishlist would be custom resources backed by the new Step Functions SDK support, instead of a Lambda function. This could include a series of API calls that pass values between them.
Import Existing Resources
In many cases, you'll need to get information about resources defined outside your application, and you can import them quite easily. A common one is where networks have been created by some other mechanism, but you need to deploy into them. Importing a VPC is straightforward using ec2.Vpc.from_lookup
, but importing subnets by id wasn't immediately obvious. The answer is to use ec2.SubnetSelection
as easy as below:
SubnetSelection(subnet_filters=[SubnetFilter.by_ids(subnet_ids)])
Create and Use a Secrets from Secrets Manager
When handling secrets, you have to be extra careful to avoid printing them into the template where they could be inadvertently accessed. To generate a secret securely, use SecretStringGenerator
from Secrets Manager, and to pass that into a resource, refer to the secret using SecretValue.secrets_manager
. For example, the below sample creates a Directory Service Microsoft AD with a generated admin password:
ad_admin_password = secretsmanager.Secret(
self, "ADAdminPassword",
secret_name="ad-admin-password",
generate_secret_string=secretsmanager.SecretStringGenerator(),
removal_policy=cdk.RemovalPolicy.RETAIN)
managed_ad = directoryservice.CfnMicrosoftAD(self, "ManagedAD",
name=name,
password=cdk.SecretValue.secrets_manager(
ad_admin_password.secret_arn).to_string(),
vpc_settings=directoryservice.CfnMicrosoftAD.VpcSettingsProperty(
subnet_ids=subnet_ids,
vpc_id=vpc_id
),
create_alias=False,
edition=edition,
enable_sso=False,
)
Pass Values Between Stacks
One of the biggest reasons to use CDK is the ability to handle multiple CloudFormation stacks in one application and pass values between them.
To do this, define a CfnOutput
with an export name that you will reference in another Stack. CDK is smart enough to identify this dependency and order stack deployment accordingly, awesome!
For example, let's say we want to be able to access the alias or DNS addresses generated by the above Managed AD:
cdk.CfnOutput(self, "ManagedADId",
value=managed_ad.attr_alias,
export_name="ManagedADId")
cdk.CfnOutput(self, "ManagedADDnsIpAddresses",
value=cdk.Fn.join(',', managed_ad.attr_dns_ip_addresses),
export_name="ManagedADDnsIpAddresses")
Notice that we use cdk.Fn.join
to combine multiple DNS addresses into a single value.
Create Multiple Similar Resources with Unique IDs
This is a dangerous one, but powerful if used carefully. You always want infrastructure definition to be declarative, and using loops risks breaking that if the inputs are dynamic.
That said, let's say you want a pipeline to deploy a set of stacks into multiple regions. You can define a stage with those stacks, and put multiple stages into a single wave to be deployed in parallel:
for region in constants.REGIONS:
pipeline_wave.add_stage(
ExampleStage(self,
id="ExampleStage-{}".format(
region.region),
env=cdk.Environment(
account=constants.EXAMPLE_ACCOUNT_ID,
region=region.region),
)
We've defined a python dataclass with values needed in each region, and we can loop through it during the CDK Pipeline definition. Notice that the id
must be unique, so it includes the region name.
Conclusion
I am optimistic about the future of AWS CDK, and I'll be using it for new projects until someone convinces me otherwise. At minimum, it's a better way to write CloudFormation. But it also empowers you well beyond that, and is just a pleasure to use.
Posted on January 27, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.