Testing Infrastructure Code vs. Getting Constantly Nagged About It

I recently found a nice tool that may make your life easier when writing infrastructure as code using CDK.

I Wanted Tests

I noticed that the cdk init command creates a default test folder, and started reading about how to write effective tests for your IaC.

It’s quite easy actually, and after you get the hang of it, it can be very entertaining. I really like doing TDD when working on my projects, so it seemed like a good option for the infrastructure code I’ve been writing lately.

Everything was good, until I started wondering…

Maybe I Don’t Need Tests?

I couldn’t shake the feeling that the tests I was writing were a bit redundant. I don’t know if there’s a better word for it, but I felt I was writing a spec for a spec.

Your CDK is a document that specifies the structure and state you want for your system: which resources, their configuration, and how they are all wired together. The stack itself is the spec, so why am I writing a spec for code that is already quite declarative?

Make no mistake, we are using TypeScript (or Python, or C#, or any of the supported CDK languages), but writing IaC using CDK is still completely declarative: We are defining the structure and state we want for our system, and CloudFormation will be in charge of the steps needed to reach it—we just don’t care how it happens.

So, what are the alternatives?

Enter cdk-nag.

Setting Up cdk-nag

cdk-nag is an awesome project with a simple premise: You install it, configure it, and then it starts nagging nonstop about your CDK projects!

The process is quite simple—the first thing you need to do is add the dependency to your project’s package.json:

{
  ...
  "devDependencies": {
    ...
  },
  "dependencies": {
    ...
    "cdk-nag": "^2.36.54",
  }
}

At the time of writing, that was the latest release—make sure to check the most current version and use that one instead. Then just run npm install.

After this, you need to set it up on your project. Go to the file in your project’s bin folder—mine are usually called main.ts. In the latest cloud experiment we worked on it looked like this:

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { InventoryStockAlarmStack } from '../lib/inventory_stock_alarm-stack';

const app = new cdk.App();
new InventoryStockAlarmStack(app, 'InventoryStockAlarmStack', {
  targetEmail: 'jl.orozco.villa@gmail.com',
  productUrl: 'https://www.mediamarkt.hu/hu/product/_nintendo-switch-2-mario-kart-world-1482762.html',
  availabilityString: 'Online elérhető',
});

Now you just need to import the list of checks you need (we will use AwsSolutionsChecks) and the Aspects class. The last step involves adding the solutions checks as a new aspect to your app, just like this:

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { InventoryStockAlarmStack } from '../lib/inventory_stock_alarm-stack';
import { AwsSolutionsChecks } from 'cdk-nag'; // <- New Stuff
import { Aspects } from 'aws-cdk-lib'; // <- New Stuff

const app = new cdk.App();
new InventoryStockAlarmStack(app, 'InventoryStockAlarmStack', {
  targetEmail: 'jl.orozco.villa@gmail.com',
  productUrl: 'https://www.mediamarkt.hu/hu/product/_nintendo-switch-2-mario-kart-world-1482762.html',
  availabilityString: 'Online elérhető',
});

Aspects.of(app).add(new AwsSolutionsChecks()) // <- New Stuff

You can use different pre-made rule packs on your app, or even create your own.

The wonderful thing is that now, every time you synthesize or try to deploy your app, you will get a series of notices telling you how your stack deviates from the applied ruleset:

Nag Output Example

It’s relentless, and makes me feel like an absolute dumb-ass, which is probably the main reason I love it. Learning and growth usually happens outside of your comfort zone, so getting a constant reminder of the things that still need work in your project is a great way to push you towards getting better at writing infra code.

Fine, What About the Tests Then?

I think they still have a place in your stacks. I thought about it, and read a bunch of different opinions online about what needs to be tested, and reached the conclusion that you should write tests for:

  • Any logic or imperative behavior you have in your code. Not everything in you stack is truly declarative, and if you are processing some form of data to alter the structure of your stack—for example, receiving stack props that are somehow processed or dictate the number of any resource—it’s a good idea to write tests to verify your code behaves as it should.
  • Any important configuration that is critical for the proper behavior of the system (route tables, Route53 Hosted Zones, VPC peering)—is a good candidate for tests. They are much more about safeguarding against future changes with unintended side effects than about verifying current behavior. Think of them as a safety harness against breaking changes made in the future.

So yeah, I was just writing the wrong type of test—they are still quite useful when focused on the things that actually need testing.

Wrapping Up

cdk-nag is a very useful tool for making sure your stacks follow best practices, and it can be a great tool to help you write more secure and better architected solutions, especially when you are starting out!

Give it a try, but don’t follow it blindly. Especially in the beginning, it can be easy to feel overwhelmed with the number of complaints it can output, but part of growing as a designer and engineer comes from knowing which principles apply to what you are trying to do. Remember it’s just meant to be a guide for building a better solution, not an entity with the final say.

I hope you find this useful!

Juan Luis Orozco Villalobos

Hey there, I'm Juan. A software engineer and consultant currently living in Budapest.