DEV Community

Steven Smiley
Steven Smiley

Posted on • Updated on

AWS CDK: 6 Things You Need To Know

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',
                                    )
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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.
  2. 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")
  3. 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")
            ))
Enter fullscreen mode Exit fullscreen mode

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)])
Enter fullscreen mode Exit fullscreen mode

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,
    )
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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),
        )
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
mdtr profile image
Maciej Drozdzik

Is it possible to see the dataclass example?