AWS can be overwhelming. Such a vast ecosystem, so many options, and so many configuration dialogs. But at least there is a UI giving you a search function and some guidance. It's just that nobody wants to use the UI in the age of DevOps, and it's out of fashion for a good reason. Modern programmers embrace the infrastructure as a code pattern, using nice and clean source code written in a contemporary programming language like Cloudformation or Terraform. Wait, what? These are configuration files, not code! Enter AWS CDK. It's a proper implementation of the IaC pattern. I was electrified when I first heard about "infrastructure as code." Using a programming language sounds like being productive in virtually no time. Not to mention the flexibility. Do you need three EC2 instances that are almost but not quite identical? Just configure the difference in an array and create the instances in a for loop. Easy as pie. Then I looked at the presenter's slides. They didn't show code. They showed a YAML file. Oops. That's not code. It's a static configuration file. Bye, bye, for loop!

Concepts borrowed from programming languages

Actually, Terraform does offer for loops, conditional execution, variables, and much more. They've even managed to create a nice syntax. That's no small achievement. The underlying syntax is YAML, optimized for static configurations. If you're using Cloudformation, you're worse off. It borrows some concepts from programming languages - variables and references - but generally speaking, the resulting syntax is arcane.

Programmers love programming languages

Programming languages can do this in a much cleaner way. Maybe I feel so because I'm a programmer. I've optimized my entire toolchain for programming languages. Programming is what I do day by day. I've got my IDE and debugger, and I'm familiar with design patterns and refactoring. That's pretty cool, but it also means I have a hard time following instructions for the operations department. They are so clumsy and verbose! Here's an example. Some time ago, I moved my blog to AWS. The corresponding Cloudformation YAML file weighs in at more than 1800 lines. If I were to optimize it manually, I could cut it in half, maybe even more. Make that 500 lines. The CDK code has roughly 300 lines, and it doesn't bear the weight of all that boilerplate code. What is much more important is that a glance at the root file tells you what the entire stack does. You don't have to read 300 lines. These eight lines give you a quick overview: [sourcecode typescript] const blogBucket = createBlogBucket(this); const commentBucket = createCommentBucket(this); const readCommentsLambda = createLambdaReadingComments(this); commentBucket.grantRead(readCommentsLambda); const writeCommentsLambda = createLambdaWritingComments(this); commentBucket.grantReadWrite(writeCommentsLambda); const [apiKey, gatewayId] = createGateway(this, readCommentsLambda, writeCommentsLambda); createCloudfrontDistribution(this, blogBucket, gatewayId, apiKey); [/sourcecode] Compare this to the first seventeen lines of the corresponding Cloudformation stack description: [sourcecode typescript] Resources: cdkblogbucketB729DF96: Type: AWS::S3::Bucket Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 BucketName: cdk-blog-bucket CorsConfiguration: CorsRules: - AllowedHeaders: - "*" AllowedMethods: - GET - POST - PUT AllowedOrigins: - http://localhost:4200 [/sourcecode] You get the idea: the YAML file contains a lot of boilerplate code. You can't hide the boilerplate code. Your options to refactor the code are limited. For example, you can't easily move the CORS configuration to another file and reference it via a simple reference. Programming languages, in contrast, are optimized to support this kind of the. Functions have specifically been invented for that. They hide complexity you don't want to deal with just now. The complexity is still there, but refactoring moves it to a file where it doesn't hurt.

Next stop: simplicity

That's the promise of AWS CDK. You can describe a complex stack so that new project members get to speed in no time. Using common sense and their IDE, they can navigate the stack description quickly. Cloudformation, in contrast, has a nasty tendency to drown you in a lot of detail. Adding insult to injury, the YAML files describe the stack bottom-up. First, they describe the basic building blocks, and later they tell you how these building blocks play together. The CDK enables you to define the stack top-down, which is more agreeable in a culture reading from top to bottom.

A two-sided sword

Of course, there's also the lesson Gradle told us. Comparing Cloudformation to CDK is pretty much like comparing Maven to Gradle. In theory, Gradle is the superior technology, but experience tells us good old Maven is often the better choice. Fewer options are a virtue. An option that's not there is an option that won't confuse you. You can write messy stuff with the CDK. Time and again, I've observed teams trapping into this trap. A programmer refactors a piece of code, and it turns out to be a bad idea in the long run. It doesn't have to be that way. Aim for simplicity, and keep asking your team if their idea of simplicity matches your definition of simplicity. That's important: every developer prefers a different level of abstraction.

Intelligent defaults

CDK scripts tend to be short because CDK uses sensible default values. If the default value matches your requirements - and it often does - you're rewarded with a remarkably concise script: [tabs] [tab title="CDK"] [sourcecode TypeScript] new s3.Bucket(stack, 'example-bucket', { bucketName: 'example-bucket', blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, encryption: s3.BucketEncryption.S3_MANAGED, }) [/sourcecode] [/tab] [tab title="CloudFormation"] [sourcecode yaml] Resources: examplebucketC9DFA43E: Type: AWS::S3::Bucket Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 BucketName: example-bucket PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true UpdateReplacePolicy: Retain DeletionPolicy: Retain [/sourcecode] [/tab] [/tabs]

Splitting your stack into multiple files

Every programming language allows you to spread your application across multiple files. You can do this with Cloudformation, too, but it shows that it's a feature added as an afterthought. Here's an example from the AWS documentation: [sourcecode yaml] FirehoseDestination: Type: MyCompany::S3::Bucket::MODULE Properties: KMSKeyAlias: !Sub "${AWS::StackName}" ReadWriteArn: !GetAtt FirehoseRole.Arn ReadOnlyArn: !Sub 'arn:aws:iam::${AWS::AccountId}:root' [/sourcecode] This code snippet uses an S3 bucket stored in a dedicated module file. I deliberately omit the module file, because it weighs in at roughly 300 lines. Follow the link to see it in all its glory. In TypeScript, you simply use a constant imported from a different source code file: [tabs] [tab title="using the S3 bucket"] [sourcecode TypeScript] import { auditLogBucket } from "@aws-cdk/aws-kinesisfirehose"; return new CfnDeliveryStream(root, "FirehoseStreamToS3", { s3DestinationConfiguration: { bucketArn: auditLogBucket.bucketArn, ... // other properties omitted for brevity }, }); [/sourcecode] [/tab] [tab title="declaring the S3 bucket"] [sourcecode TypeScript] export const auditLogBucket = new s3.Bucket(root, "test-firehose-s3-bucket", { removalPolicy:RemovalPolicy.DESTROY, // REMOVE FOR PRODUCTION autoDeleteObjects: true, // REMOVE FOR PRODUCTION, bucketName: 'test-firehose-s3', publicReadAccess: true, }); [/sourcecode] [/tab] [/tabs] You get the point: using variables and functions feels natural in any programming language. Static DSLs like Cloudformation have been designed with other goals in mind, and while they can emulate cross-references, it always feels a bit clumsy. If you're interested in the complete Firehose CDK example, read the full story here.

Reading parameters from arbitrary sources

Here's another example. How do you parameterize your Cloudformation stack? There's only a limited range of options. You can use parameters stored in SSM or the secrets manager. Or you can append parameters to the aws cloudformation create-stack CLI command. As a result, many Cloudformation stacks rely heavily on shell scripts. That's particularly true for older Cloudformation stacks. Over time, Cloudformation adopted quite a few useful features. The CDK is even more flexible. It's a program. In other words, you can do anything. You can read parameters from files, a database, or a web service, whatever you need. And it takes the sting out of using parameters. In the case of command-line parameters, you just have to read process.env.myParameter. Reading a parameter from SSM is a one-liner, too, as this example shows. Well, it's a one-liner in your IDE, but two lines in this blog: [tabs] [tab title="CDK"] [sourcecode typescript] const instanceType = ssm.StringParameter.valueForStringParameter( this, 'InstanceTypeParameter'); const ec2Instance = new ec2.Instance(this, 'ec2-instance', { instanceType: new InstanceType.of(instanceType), machineImage: ec2.MachineImage.latestAmazonLinux() }); [/sourcecode] [/tab] [tab title="CloudFormation"] [sourcecode yaml] Parameters: LatestAmiId: Type: 'AWS::SSM::Parameter::Value' Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' Resources: BasicParameter: Type: AWS::SSM::Parameter Properties: Name: InstanceTypeParameter Type: String Value: t2.micro Description: SSM Parameter for running date command. Description: Enter t2.micro, m1.small, or m1.large. Default is t2.micro. Ec2Instance: Type: AWS::EC2::Instance Properties: InstanceType: Ref: InstanceTypeParameter ImageId: !Ref LatestAmiId [/sourcecode] [/tab] [/tabs] As a bonus, the CDK version uses the latest Linux version by default. That's a simple method call: ec2.MachineImage.latestAmazonLinux(). The Cloudformation counterpart is slightly more convoluted. I didn't test it, but I hope I put all the pieces correctly. If not, read this article for a more expert tutorial.

IDE support and documentation

You can write CDK scripts in TypeScript and half a dozen other languages, some of them strongly typed. Strong typing usually means good IDE support. Visual Studio Code offers decent code completions for my TypeScript files. Plus, browsing the type definition files of the CDK gives you an idea of which attributes you can use, which values are allowed, and you even get a glimpse at the documentation. Due to its popularity, there are also plugins for Cloudformation for the most popular IDEs, which is great. Here's the catch: the CDK doesn't need a dedicated plugin. Code completion works out of the box. When AWS introduces a new feature, code completion is there from day one. However, that's not entirely true. CDK is an abstraction layer on top of Cloudformation, so it's always a step behind. The new feature is available from day one of the release of the CDK type definition. It's still good news. You don't have to wait until a third-party plugin developer detects and supports the feature.

Escape hatches

I've mentioned the CDK is always a step behind the latest AWS features. That doesn't need to be an obstacle. If you need an AWS feature that's ill-supported by the higher-level constructs (or not supported at all), you can use an escape hatch. That's writing the YAML script in TypeScript, so that's only a last resort. Talking about the documentation: that's something that has improved a lot during the last couple of years. There's a lot of documentation on the AWS website, and it's always been there, but it's often difficult to read because it covers everything in depth. There are also tutorials in the official documentation, but I'm usually better off with third-party blogs for some reason unknown. Nowadays, they are plenty of them, covering the most popular use-cases. I happened to need some not-so-popular use-cases, and they drove me nuts. The documentation is there. You have to find and understand it. It helps to have a good understanding of the Cloudformation basics. I tried to skip that. You can do that, but judging by my humble experience, that's not what I recommend. BTW, I'm going to fill the gaps I've run into in a series of articles soon. Stay tuned!

Breaking changes

When I began using CDK one and a half years ago, my team and I frequently ran into breaking changes. I'm not sure how many were classical breaking changes because our build often broke after adding a CDK module. We learned the hard way that every AWS dependency needs to have precisely the same version number. On average, there's a new minor version every week, so keeping the version numbers in sync can be exhausting. For the last twelve months, I have been busy doing other things, so I don't know about the current state of the art. Since I began migrating my blog to the CDK, I didn't run into breaking changes. I tend to believe it's stable now. At least version 1 of the CDK. CDK v2 went public in December. It promises to follow the idea of semantic versioning, which is to say, there are no breaking changes. But of course, updating to the second iteration of the CDK is a breaking change. At the time of writing, googling inevitably sends you to the documentation and tutorials of CDK v1, so embracing v2 may be difficult at the moment. On the other hand, the announcement claims that most teams only have to replace the import statements. That sounds like there are a few breaking changes. I didn't try that yet, so that I wouldn't know.

Deployment speed

The weak point of the CDK is deployment speed. Deploying your modified stack to AWS is usually a matter of a couple of minutes. Once you've deployed your stack, subsequent deployments only deploy what's changed. Unfortunately, even this incremental deployment usually takes a minute or two. Adam Ruka has good news for you. Since September 2021, the CDK supports a hot-swap mode. It's not perfect. Some operations can't be sped up. But if it works - and it works most of the time - the effect is dramatic. You're down to a few seconds. You can do even better.

Testing and debugging

I recommend using LocalStack for testing your deployment. LocalStack is AWS running in a Docker container. The emulation is not perfect. It doesn't cover every service offered by AWS. In particular, you don't get every AWS service for free. But chances are you get everything you need. In any case, it allows you to test most of your stack locally. And it shines for deployment times. LocalStack is optimized for running in a CI/CD pipeline.

Wrapping it up

I admit it: I'm a programmer. To me, using a programming language is the natural approach to the infrastructure as code pattern. Every other approach feels weird. Bear with me! If you're coming from an operations background, you may feel more at home with the YAML files. Even so, I hope I've managed to show you why using a formal programming language is attractive to developers. It's always easier if you're allowed to use your native language. Programming languages, in particular, have many advantages over static DSL like Cloudformation or even Terraform, although the latter does an excellent job at simplifying things.