Effective AWS CDK for AWS CloudFormation

Infrastructure as Code is the trend to manage the resources of application. AWS CloudFormation is the managed service offering the IaC capability on AWS since 2011. CloudFormation uses the declarative language to manage your AWS resources with the style what you get is what you declare.

However there are cons of CloudFormation as a declarative language,

  • the readability and maintenance for applications involving lots of resources
  • the reuseable of code, CloudFormation modules released in re:Invent 2020 might help mitigate it

AWS CDK provides the programming way to define the infra in code by your preferred programming languages, such as Typescript, Javascript, Python, Java and C#. AWS CDK will synthesis the code to CloudFormation template, then deploying the stack via AWS CloudFormation service. It benefits the Devops engineers manage the infra on AWS as programming application, having version control, code review, unit testing, integration testing and CI/CD pipelines, the deployment still depends on the mature CloudFormation service to rolling update the resources and rollback when failing.

For solution development, using CDK indeed improves the productivity then publish the deployment assets as CloudFormation templates.

Though CDK application can be synthesized to CloudFormation template, there are still some differences blocking the synthesized templates to be deployed across multiple AWS regions.

This post will share the tips on how effectively writing AWS CDK application then deploying the application by CloudFormation across multiple regions.

General

Environment-agnostic stack

Don’t specify env with account and region like below that will generate account/region hardcode in CloudFormation template.

1new MyStack(app, 'Stack1', {
2    env: {
3      account: '123456789012',
4      region: 'us-east-1'
5    },
6});

use CfnMapping/CfnCondition instead of if-else clause

CloudFormation does not have logistic processing like programming language. Use CfnMapping or CfnCondition instead.

Note: the CfnMapping does not support default value, you have to list all supported regions like below code snippet,

 1getAwsLoadBalancerControllerRepo() {
 2    const albImageMapping = new cdk.CfnMapping(this, 'ALBImageMapping', {
 3      mapping: {
 4        'me-south-1': {
 5          2: '558608220178',
 6        },
 7        'eu-south-1': {
 8          2: '590381155156',
 9        },
10        'ap-northeast-1': {
11          2: '602401143452',
12        },
13        'ap-northeast-2': {
14          2: '602401143452',
15        },
16        ...        
17        'ap-east-1': {
18          2: '800184023465',
19        },
20        'af-south-1': {
21          2: '877085696533',
22        },
23        'cn-north-1': {
24          2: '918309763551',
25        },
26        'cn-northwest-1': {
27          2: '961992271922',
28        },
29      }
30    }); 
31    return `${albImageMapping.findInMap(cdk.Aws.REGION, '2')}.dkr.ecr.${cdk.Aws.REGION}.${cdk.Aws.URL_SUFFIX}/amazon/aws-load-balancer-controller`;
32  }

never use Stack.region

Don’t rely on stack.region to do the logistic for China regions. Use additional context parameter or CfnMapping like below snippet,

 1const partitionMapping = new cdk.CfnMapping(this, 'PartitionMapping', {
 2    mapping: {
 3      aws: {
 4        nexus: 'quay.io/travelaudience/docker-nexus',
 5        nexusProxy: 'quay.io/travelaudience/docker-nexus-proxy',
 6      },
 7      'aws-cn': {
 8        nexus: '048912060910.dkr.ecr.cn-northwest-1.amazonaws.com.cn/quay/travelaudience/docker-nexus',
 9        nexusProxy: '048912060910.dkr.ecr.cn-northwest-1.amazonaws.com.cn/quay/travelaudience/docker-nexus-proxy',
10      },
11    }
12  });
13partitionMapping.findInMap(cdk.Aws.PARTITION, 'nexus');

Use core.Aws.region token referred to the region which region of the stack is deployed.

explicitly add dependencies on resources to control the creation/deletion order of resources

For example, when deploying a solution with creating a new VPC with NAT gateway, then deploying EMR cluster in private subnets of VPC. The EMR cluster might fail on creation due to network issue. It’s caused by the NAT gateway is not ready when initializing the EMR cluster, you have to manually create the dependencies among EMR cluster and NAT gateway.

Always override the logical ID of CloudFormation resource when creating AWS resources with unique name

UPDATED: 2023/10/24

When creating an AWS resource via CDK with a friendly name, for example, you create a Glue Table named my-table in CDK. The logical ID will be generated by CDK constructs' name inheritance, however, you might refactor your constructs in the system design level. The default logical ID of the resource will be changed after your refactor or renaming the ID of construct. After the logical ID changes, the resource will be replaced when updating the CloudFormation stack to a new template. In the updating process of CloudFormation stack, a new resource will be created firstly, however, the resource creation will fail due to the conflict resource name. Currently, the workaround is that explicitly overriding the logical ID of the AWS resource created by CDK to avoid the replacement in stack updating. Explicitly override the logical ID will maintain the code readability and avoid the unintended the failure of stack update.

1(table.node.defaultChild as CfnResource).overrideLogicalId(tableLogicId);

EKS module(@aws-cdk/aws-eks)

specify kubectl layer when creating EKS cluster

NOTE: This tricky only applies for AWS CDK prior to 1.81.0. CDK will bundle kubectl, helm and awscli as lambda layer instead of SAR application since 1.81.0, it resolves below limitation.

EKS uses a lambda layer to run kubectl/helm cli as custom resource, the @aws-cdk/aws-eks module depends on the Stack.region to check the region to be deployed in synthesizing phase. It violates the principle of Environment-agnostic stack! Use below workaround to create the EKS cluster,

 1const partitionMapping = new cdk.CfnMapping(this, 'PartitionMapping', {
 2  mapping: {
 3    aws: {
 4      // see https://github.com/aws/aws-cdk/blob/60c782fe173449ebf912f509de7db6df89985915/packages/%40aws-cdk/aws-eks/lib/kubectl-layer.ts#L6
 5      kubectlLayerAppid: 'arn:aws:serverlessrepo:us-east-1:903779448426:applications/lambda-layer-kubectl',
 6    },
 7    'aws-cn': {
 8      kubectlLayerAppid: 'arn:aws-cn:serverlessrepo:cn-north-1:487369736442:applications/lambda-layer-kubectl',
 9    },
10  }
11});
12
13const kubectlLayer = new eks.KubectlLayer(this, 'KubeLayer', {
14  applicationId: partitionMapping.findInMap(cdk.Aws.PARTITION, 'kubectlLayerAppid'),
15});
16const cluster = new eks.Cluster(this, 'MyK8SCluster', {
17  vpc,
18  defaultCapacity: 0,
19  kubectlEnabled: true,
20  mastersRole: clusterAdmin,
21  version: eks.KubernetesVersion.V1_16,
22  coreDnsComputeType: eks.CoreDnsComputeType.EC2,
23  kubectlLayer,
24});

If you're interested on this issue, see cdk issue for detail.

manage the lifecycle of helm chart deployment

The k8s helm chart might create AWS resources out of CloudFormation scope. You have to manage the lifecycle of those resources by yourself.

For example, there is an EKS cluster with AWS load balancer controller, then you deploy a helm chart with ingress that will create ALB/NLB by the chart, you must clean those load balancers in deletion of the chart. Also the uninstall of Helm chart is asynchronous, you have to watch the deletion of resource completing before continuing to clean other resources.

THE END

The tips will be updated when something new is found or the one is deprecated after CDK is updated.

HAPPY CDK 😆

Posts in this Series