Writing your CDK in Java

Melina Schweizer
My Local Farmer Engineering
12 min readJun 22, 2021

A quirky journey into cloud IaC, Java Dev’ style.

At this point, we’ve been a Java shop for over a decade, so here’s our conundrum:

.. we want to migrate to the cloud (yay!)
.. we want to do serverless (awesome!!)
…….. but Java’s tuning favors long-running tasks (shoot)
.. we want to do IaC (good for us!!)
.…. but we’re coming up short on Java examples (seriously?)

So what are the options?

Disclaimer
I Love My Local Farmer is a fictional company inspired by customer interactions with AWS Solutions Architects. Any stories told in this blog are not related to a specific customer. Similarities with any real companies, people, or situations are purely coincidental. Stories in this blog represent the views of the authors and are not endorsed by AWS.

The decision to stick to Java was, quite frankly, an easy one:

  • we didn’t want to have our entire staff switch from being an expert in their language to being novices in a new one
  • there wasn’t enough time to commit resources for training while under intense pressure to deliver a new project
  • ..and to be honest… no one wanted to switch. And why should they? Especially given the timeframe…

So, buckle up, it’s been quite a tumultuous ride!
We’ll talk about our experience using Java for IaC (CDK) to build a brand new MySql database and serverless environment with AWS Java Lambdas.

We’ll cover tips & tricks to get this to work, hurdles that needed to be overcome, some basics, and some not so basics that we wish someone had explained to us so that we could’ve saved time!

Writing CDK in Java

Yes.. I’ll admit it… We’ve never done IaC.😬

So far, whenever we set up a new virtual server we’d cut a ticket and someone in the Linux team would set it up and install Apache and Tomcat on it. I’m not even sure who set up the networking piece for it. It just got done… probably a couple of days (and sometimes weeks!) after asking for it, as long as we had capacity, and often times with the same connectivity issues that older requests had.

Now, we know we’re behind in the times, but we’ve always had so many competing priorities that we never had the time to look into it. Now that our new Delivery project has got all the attention (and resources) from upper management, we finally have our chance to get on that bandwagon.

As discussed in our Serverless Solution post, we decided to use CDK in order to deploy our infrastructure onto AWS. What the CDK does is basically enable us to use a programming language to define the resources (e.g. databases, servers, networking, lambdas) we need. The CDK will then convert that code into a CloudFormation template, and deploy the resources into our AWS accounts.

During subsequent deployments, it’s also smart enough to do a comparison to see what are the changes, and add/delete resources or update changes to existing ones. Additionally, another team has already used Java CDK to deliver a Site-to-Site VPN solution to facilitate our employees to work from home, so we know that this works well and is already understood by some of our folks.

Since the CDK can be used with several languages, we obviously favor using the Java version of it. However, we’ve been looking for Java examples and not finding many. This obviously put a hamper on our plans… no one has time to learn another language right now. We did find a ton of Typescript examples though and so attempted to translate a few into Java. Obviously, this is not ideal… but thankfully the syntax is very, very similar..

Translating Typescript CDK into Java CDK

Take a look at this Typescript example for creating an AWS Secrets Manager secret:

const dbUserSecret = new Secret(this, 'DbUserSecret', {
description: 'Db Username and password',
secretName: 'DbUserSecret',
generateSecretString: {
secretStringTemplate: JSON.stringify({username: dbUser}),
generateStringKey: 'THEPASSWORD',
passwordLength: 16,
excludePunctuation: true,
}
});

and here’s its equivalent Java:

this.userSecret = new Secret(this, "DbUserSecret",
SecretProps.builder()
.description("Db Username and password")
.secretName("DbUserSecret")
.generateSecretString(
SecretStringGenerator.builder()
.secretStringTemplate("{\"username\": \"" + user + "\"}")
.generateStringKey("THEPASSWORD")
.passwordLength(16)
.excludePunctuation(true)
.build())
.build());

The main difference between the 2 languages is that when properties are passed in as a JSON argument to a function in Typescript:

new Secret(this, 'DbUserSecret', { THEPROPERTIES }

its equivalent in JAVA will likely use the Builder Pattern instead:

new Secret(this,"DbUserSecret", 
SecretProps.builder()
THEPROPERTIES
.build();

I’ve seen a ton of JAVA examples in the AWS docs that use this pattern to construct their objects, so if you’re a constructor-kinda-guy or gal like me, it’s probably time to become familiar with it (sigh).

Some classes also offer a shorter option, which is to use the class’s Builder directly. For our example, we could also write:

this.userSecret = Secret.Builder.create(this, “DbUserSecret”)
.description(“Db Username and password”)
.secretName(“DbUsersecret”)
.generateSecretString(....
.build();

In any case, the good news is that being able to convert from one to the other helped us keep Java as our main language, save training time and keep plenty of people happy.

CDK, Java & OpenAPI

To define our REST APIs we’re using OpenAPI , a standard we’ve been using for years now.
To do this, we declare a schema file apiSchema.json with our REST API paths, and add some extra instructions relevant to the AWS API gateway integration:

...
"paths": {
"/farm/{farm-id}/slots": {
"get": {
...
"x-amazon-apigateway-integration": {
"type": "AWS_PROXY",
"httpMethod": "POST",
"uri": "{{GetSlots}}",
"payloadFormatVersion": 1.0,
"credentials": "{{ApiRole}}"
}
...

In this example we have a couple of placeholders {{GetSlots}} and {{ApiRole}}, which we’re going to string-replace later with the actual URI and credentials created by CDK during deployment.

To do this replacement, we first declare a Map with the key value pairs in ApiStack.java:

Map<String, Object> variables = new HashMap<>();
variables.put(
"GetSlots",
String.format(
"arn:aws:apigateway:%s:lambda:path/"
+ "2015-03-31/functions/%s/invocations",
Stack.of(this).getRegion(),
getSlotsHandler.getFunctionArn()));

And then we use Mustache to do the replacement:

try (Reader reader =
new InputStreamReader(
getClass().getClassLoader()
.getResourceAsStream("apiSchema.json"))) {
Mustache mustache = mf.compile(reader, "OAS");
mustache.execute(writer, variables);
writer.flush();

ObjectMapper JSONMapper = new ObjectMapper(new JsonFactory());
openapiSpecAsObject =
JSONMapper.readValue(writer.toString(), Object.class);
}

Now that we have a JSON object with the OpenAPI spec, we can use it to declare our ApiGateway to handle the REST requests.

Our example uses CfnApi, a Level 1 Construct to create the ApiGateway.
Level 1 constructs are basically CDK classes which map directly to CloudFormation resources. That is, they’ll omit some of the boilerplate that CDK Level 2 constructs generate for us. Sometimes, Level 2 constructs are a bit behind on adapting to all the features that CloudFormation provides, and so you might have to resort to using Level 1 constructs. In our case, we just used one because we adapted a Typescript example off the internet.

CfnApi apiGw = new CfnApi(this, "ILMLFDelivery",
CfnApiProps.builder()
.stageName("ILMLFDeliveryStage")
.definitionBody(openapiSpecAsObject)

As you can see, the OpenAPI JSON object gets passed into the definitionBody property. You can see the full example in ApiStack.java.

It’s the little things…

Sometimes, the smallest things wind up taking huge amounts of time.

I noticed that on one occasion, I made a Map with a bunch of items and for some reason, CDK thought I had changed them when I deployed the 2nd time, and triggered a change of the resource using the Map.

I hadn’t in fact, changed anything,.. but apparently I used an unordered Map which resulted in the items being retrieved sometimes in a different order. Usually, this is not a problem.. CloudFormation is smart enough to see that the items are the same, just that the order is different. But in this particular case I was converting the Map into String format. The change in order therefore resulted in a different String and that was viewed as a change in the properties of the resource by CloudFormation, which ultimately changed the formatting of our logs. This was fixed by declaring a LinkedHashMap so that the same order is always kept.

Map logFormat = new LinkedHashMap();
logFormat.put("status", "$context.status");
logFormat.put("profile", "$context.authorizer.claims.profile");

CfnApi.AccessLogSettingProperty.builder()
.destinationArn(accessLogGroup.getLogGroupArn())
.format(logFormat.toString())
.build())

Needless to say, this was a pain to figure out, so if you have CloudFormation picking up changes you are not expecting, one of the ways to mitigate this is by including snapshot testing in your unit tests. This basically detects changes from the last CFN template to the current one. We will cover this topic in a subsequent post!

Common issues and troubleshooting

There are several common issues that most of our team ran into when starting to work with the CDK. If you run into an unknown error, you might want to follow this playbook:

Pitfall #1: your modules don’t play nice with each other
Inconsistent versioning among modules in your build.gradle (or pom.xml) and CDK CLI might cause problems.

CDK constructs are separated into many modules, usually by service. For example, we use the lambda and api-gateway modules to create our API stack. If these two modules in your dependency have different versions, you might get a compilation error (e.g. if a method isn’t available in another version) or a “synthesize” error.

The same goes for your CDK CLI. Try running cdk --version to check that it is the same as your module version in your build.gradle file. You can also use npx to run a specific CDK version without installing it (npx cdk@1.107.0 --version) . This could be helpful if you have multiple projects with different version.

Pitfall #2: did you forget bootstraping?
For the CDK to be able to deploy to your AWS account on your behalf, you’ll need to bootstrap your AWS account.

Make sure to run the command cdk bootstrap for every AWS account and region you will work with. Yes, this means your team members will also have to do this if they work on their own accounts.

This is required because CDK needs some cloud resources to operate. For example, a bucket to store compiled (synthesized) artifacts before deployment. If not, you will receive an error that some expected AWS resource is missing.

When you run this command, it will create a CloudFormation stack named “CDKToolKit” with required resources for your project:

Screenshot showing CloudFormation stack created by CDK for bootstrapping

These issues are the most common ones we found. For other issues, our approach is to first identify where the issue really is. If the error happens during compilation or synthesizing, it’s definitely on CDK. Sometimes, the error happens during deployment (e.g. deployment fails and gets rolled back). This is usually from missing permissions to deploy certain resources or a malformed CloudFormation template.

Use the generated CloudFormation template to save time
In this case, take a look at the cdk.out folder generated by the cdk synth command. There’s a *.template.json file for each CDK stack, which holds the CloudFormation template depicting all the resources in your stack. Sometimes, I’ll resort to modifying the code and running cdk synth until I get the template I want, instead of doing the full deploy and waiting several minutes just to discover the change is not what I wanted.

... and sometimes, just try it in the AWS Console
Sometimes the deployment was successful but the created resource does not work in the way we expected. I’ve run into that several times and when I just couldn’t get it to work through the code, I wound up trying the change out in the console just to see what happens.

Furthermore, sometimes doing the change in the console actually applies other changes (IAM policies and other configurations) which I didn’t know that I needed... and then I’d go back and see what those were and apply them to the code. This happened for example, while setting up our Lambda to RDS Proxy connection.. a quick trial run in the AWS Console produced the right IAM policies that I was lacking in my code.

...where is my change?
Another issue we ran into a few times that was really, .. really annoying was changing something in the CDK on an existing resource that was already propped up in our AWS account, only for that change to be ignored 😑
And of course, we didn’t notice it until much later 😠

This happened to us on changing the “iam auth” property of our RDS Proxy.... it just flat out ignored it (?). And it gets worse.. sometimes we changed what seemed like an unimportant attribute and all of a sudden our entire database got deleted and recreated (another use case for snapshot testing!).

Long story short, what happens on a change is determined by the generated CloudFormation template. I recommend the explanation of this post on how it determines what to do.

KEEP OUT.... or proceed at your own risk

The AWS Console, looking sweet & innocent.. luring you in

This one we basically learned the hard way.

We stood up some resources and got lazy, so instead of changing a resource through the CDK, we went directly into the resource in the AWS console, and made the mods there. From there on, the CDK ignored any subsequent changes. On a (stupid) attempt to coerce it to stand up the resource again, I deleted the resource directly through the console…. yeah, you heard me. Not through the CloudFormation stack nor through the CDK… through the console.

Maybe it was an hour, maybe it was two when I finally managed to dig myself out of that hole. I got into a whole tiff of trying to revive the resource, the stack, Elvis.. you name it, I tried it. In the end, I deleted the entire stack and started again from scratch.

Long story short… anything stood up via CloudFormation or CDK, you might wanna not modify in the console until you’re reaaaalllll comfortable with what you’re doing…. or you’re prepared to delete the entire CloudFormation stack in the console and recreate everything.

Directories and files of interest

For the impatient like me who don’t want to spend the time guessing, here’s a cheat sheet on what goes where:

  • The README.md file explains the basics of the project, including instructions on how to set it up and run it.
  • The /ApiHandlers directory holds the Java Lambda code and the Gradle setup used to compile & package the classes. This is where the code executed during API calls lives, i.e. our CreateSlots, GetSlots, etc API calls, along with any resources they need for execution.
  • /ApiHandlers/build/distributions will hold the lambda.zip file that ultimately gets copied to the Lambdas (with the Java code for the APIs). Once you generate a lambda.zip, go ahead and look inside it. You will find the contents of the resources and scripts folder directly at the root (everything else will be in their expected relative folders). When you need to access those resources, keep in mind they will be accessible in the current directory where your java code is (e.g. ./theresourcefile.xml ).
  • The /cdk directory holds the Java CDK code. This is the code that defines how to configure the resources such as the database, the Lambdas, the networking constructs where the former live, etc.
  • The cdk.json file specifies how the CDK application will be executed (in our case, using Gradle).
  • The gradle.build file declares dependencies and other configurations needed for the CDK app.
  • /cdk/src/main/java/com/ilmlf/delivery holds the DeliveryApp.java file, which is basically the entrypoint for the CDK (i.e. with a public static void main() function). The entrypoint is called an “App”, and an App declares one or more “Stacks”. In our case, we have a stack for standing up our database resources, and another one for declaring the API call constructs. Note: we’re using different stacks because we want to keep a separation of duties. While the db Stack will be quite stable, the API stack will be continuously worked on. Another difference is that potentially different teams might be in charge of each Stack.
  • The /cdk/cdk.out directory holds the outputs generated by the CDK, e.g. the CloudFormation template generated by a “cdk deploy” command.

So.. if you’re a Java developer embarking on the journey of using CDK in Java, I hope this was useful! Feel free to comment on the post to give us feedback (anything missing? any other gotchas?)

Our next post (in a couple of weeks) will cover Java Lambdas, and how to tie them into this Java CDK construct. Stay tuned!

Useful Links

The source code for this blog post:

CloudFormation templates
CDK Level x Constructs
CDK Bootstrapping
AWS Secrets Manager
CloudFormation changes
Mustache
OpenAPI spec
CDK & JAVA

--

--

Melina Schweizer
My Local Farmer Engineering

Melina Schweizer is a Sr. Partner Solutions Architect at AWS and an avid do-it-yourselfer.