How to create polymorphic infrastructure objects using CDK for Terraform

They are one of a kind

TL;DR

We are going to create a couple of Azure infrastructure components like a Storage Account, an AKS cluster, a Redis Cluster and a KeyVault using CDK for Terraform. But instead of just deploying those services into an environment with CDKTF we are going to make use of CDK Aspects which allows us to leverage the Visitor Pattern.
With that we can adjust the behavior of an object - in this case a TerraformResource - without changing the class itself. In our example we are going to apply a dev-prod environment switch that can be used to specify certain settings (SKUs, soft-delete enabled, etc.) that will be used when deploying the resources, but also later as a dynamic switch that can be applied to the resources after they have been created. With that we can simply switch our cloud environment from dev to production without recreating any of the resources.

Check out my Github repo if you want skip all the details and explanations.

If you want to see a hands-on demo on this topic check out this recording of a session I did together with Sebastian Korfmann (CDKTF core contributor) on Youtube as part of the HashiTalks DACH, yes sorry it’s in German.

What is the CDK?

The Cloud Development Kit was initially created by a team at Amazon Web Services, but it was open-source from the very beginning. It received a lot of attention from the community when it went GA in July 2019 after it has been in developer preview for about a year.

The initial idea behind the CDK was to enable developers to use their favorite programming language instead of writing plain Cloudformation templates. Furthermore it supports the concept of creating high-level, reusable component building blocks named Constructs. One could for example use those constructs to define a piece of enterprise hardened cloud infrastructure with governance and security measures built in.

One of the major benefits that comes with the ability to use a programming language to define your infrastructure instead of writing templates in yaml, json or hcl is that you can use all the power of object oriented programming like encapsulating data and operations in classes, using inheritance and polymorphism. And this is what this article is about. But we are going to use the CDK for Terraform and not for AWS.

Right now AWS CDK supports:

If you want to know more about the AWS CDK take a look at the offical product site or visit the Github repo.

What is the CDK for Terraform?

In a TV series world CDKTF would probably by called a spinoff.
It is also an opensource community project. But it is also strongly backed by Hashicorp itself. You can guess the main differentiator is that Terraform is used deploy your cloud resources instead of AWS Cloudformation. You might even use CDKTF to generate Terraform templates and continue to use your existing Terraform infrastructure deployment pipeline.
In general the same benefits that come with AWS CDK all apply for CDKTF. But of course with CDKTF you are no longer limited to write infrastructure as code only for AWS but also for all 200+ providers that Terraform supports.

To complete the story, besides CDK and CDKTF there is also CDK8s which can be used to generate Kubernetes yaml.

For all the Azure folks out there, let me tell you that there is also a prototype in the make. So stay tuned.

CDK ecosystem

CDKTF comes with a powerful cli tool which can be installed with npm.
To get started quickly you might follow the steps in the “getting started” section in my cdktf-demo repo.

Right now it supports Javascript/Typescript, Python, Java with C# coming soon.

What is the Visitor pattern all about?

Okay, after the basic introduction let us focus on what is possible with CDKTF besides implementing Terraform resources as objects in code and potentially deploying them. For that we are going to create a CDKTF demo application in Typescript that makes use of the visitor pattern.

The visitor pattern is one of the twenty-three well-known GoF design pattern that are supposed to overcome recurring SW development problems. The idea behind it is basically extending functionalities of classes without directly adding function members to it or adjusting types on which they operate. It is classified as a behavioral pattern. So if you want to add a certain kind of behavior to different but similar classes, the visitor pattern is your best friend. In our case we are going to add the “switch environment” functionality to some Terraform resources using this pattern.

What are Aspects in CDK?

The visitor pattern is built into CDK with concept of Aspects. And since CDKTF uses the same underlying components we can also use aspects there.

Using aspects you can apply certain functionality to all constructs in a TerraformStack, which is a construct itself, or even only to constructs in the same scope. You could for example add tags to all resources or apply some kind of checks on certain resources for example. All you need to do is to enable the aspect using one line of code:

Node.of(construct).applyAspect(aspect);

The aspect object needs to implement the visit function in the IAspect interface. The construct that is passed to the applyAspect function defines the scope. During runtime the tree of constructs will be traversed and the visit function will be invoked for each of them.

What is Polymorphism in OOP?

Back to some object-oriented programming fundamentals.

Polymorphism is one of the major OOP-concepts that allows us to use different classes with the same interface. You can use it to make sure that all classes that implement the same interface provide a common kind of functionality with some type-specific implementation details.

Image an interface of type IVehicle with a single function definition drive(). Two classes should inherit this interface, Car and Bike. You might guess that both of them implement the drive() function in a different way.

Another class VehicleDriver maintains a vehicle list of type IVehicle. We can iterate over this list and call the drive() function in one go. Each call to drive() invokes the specialized function of either Car or Bike. That is polymorphism in action.

Why are we going to use Polymorphism and CDK Aspects together?

We are using an interface of type SwitchableTerraformResource that defines only one function:

switchEnvironment(env: Env):void

Env is an enum:

enum Env { DEFAULT, DEV, PROD }

Then we create four classes like this:

class SwitchableKubernetesCluster extends KubernetesCluster implements SwitchableTerraformResource...class SwitchableKeyVault extends KeyVault implements SwitchableTerraformResource...class SwitchableRedisCache extends RedisCache implements SwitchableTerraformResource...class SwitchableStorageAccount extends StorageAccount implements SwitchableTerraformResource

All four classes need to implement the switchEnvironment function. Since we are going to apply certain adjustments to those TerraformResource objects in a similar yet not equal way, we need to invoke the switchEnvironment function from within the visit() function. The aspect we have applied to the whole TerraformStack makes sure the visit() function is invoked for every construct in the tree, so for all TerraformResource objects. That means we only need to implement one visit() function like this:

public visit(node: IConstruct): void {      
if (instanceOfSwitchableTerraformResource(node)) {
node.switchEnvironment(this._env)
}
}

And now we are going to implement the different switchEnvironment() functions for all the classes that implement the interface of SwitchableTerraformResource. Here are two examples. As you can see we are going to apply service specific adjustments.

This is switchEnvironment() in the SwitchableKubernetesCluster class:

public switchEnvironment(env: Env): void {
switch (env) {
case Env.DEV:
this.skuTier = "Free"
break
case Env.PROD:
this.skuTier = "Paid"
break
case Env.DEFAULT:
}
}

This is switchEnvironment() in the SwitchableStorageAccount class:

public switchEnvironment(env: Env): void {        
switch (env) {
case Env.DEV:
this.accountReplicationType = "LRS"
this.accessTier = "Cool"
break
case Env.PROD:
this.accountReplicationType = "RAGZRS"
this.accessTier = "Hot"
break
case Env.DEFAULT:
this.accountReplicationType = "ZRS"
this.accessTier = "Hot"
}
}

What is the end result?

In the end we have a CDKTF application with some TerraformRessources:

class DemoStack extends TerraformStack {    
constructor(scope: Construct, name: string) {
super(scope, name);
new AzurermProvider(this, 'AzureRm', {
features: [{}]
})
const LOCATION = 'northeurope'
const RG_NAME = 'hdemo'
const AKS_NAME = 'hdemo'
const AKS_DNS_PREFIX = 'hdemo'

// Resource Group ---------------------------------------------
const rgConfig: ResourceGroupConfig = {
location: LOCATION,
name: RG_NAME
}
const rg = new ResourceGroup(this, 'rg', rgConfig)
// Azure Redis ------------------------------------------------
const rcConfig: RedisCacheConfig = {
location: LOCATION,
name: 'hdemoredisCC',
resourceGroupName: RG_NAME,
capacity: 2,
family: 'C',
skuName: 'Standard',
dependsOn: [rg],
}
new SwitchableRedisCache(this, 'redis', rcConfig)

// Key Vault --------------------------------------------------
const kvConfig: KeyVaultConfig = {
location: LOCATION,
name: 'keyVaulthdemo',
resourceGroupName: RG_NAME,
skuName: 'standard',
tenantId: '72f988bf-86f1-41af-91ab-2d7cd011db47',
dependsOn: [rg]
}
new SwitchableKeyVault(this, 'keyvault', kvConfig)

// AKS --------------------------------------------------------
const pool: KubernetesClusterDefaultNodePool = {
name: 'default',
vmSize: 'Standard_D2_v2',
nodeCount: 1
}
const ident: KubernetesClusterServicePrincipal = {
clientId: process.env.AZ_SP_CLIENT_ID as string,
clientSecret: process.env.AZ_SP_CLIENT_SECRET as string
}
const k8sconfig: KubernetesClusterConfig = {
dnsPrefix: AKS_DNS_PREFIX,
location: "westeurope",
name: AKS_NAME,
resourceGroupName: rg.name,
servicePrincipal: [ident],
defaultNodePool: [pool],
dependsOn: [rg]
};
new SwitchableKubernetesCluster(this, 'k8scluster', k8sconfig)
}
}
const app = new App();
new DemoStack(app, 'typescript-azurerm-k8s')
EnvironmentSwitch.enable(app, Env.DEV)
app.synth();

The second last line is where all the magic starts. Depending on the Env parameter we are switching all the parameters that we have specified in the different switchEnvironment() functions above. And the cool thing is that after we have deployed all those resources to Azure with the first run of

cdktf deploy

we can change the environment to Env.PROD and re-run the same command. Now all those changes are applied to the already deployed resources without the need for recreating them. Dynamically, without any downtime.
How cool is that?!

Next steps

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store