How to create polymorphic infrastructure objects using CDK for Terraform
By: Torsten Stiller
Tags: Cdk, Iac, Terraform, Oop, Azure

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?!