Overcoming shortcomings in CloudFormation, using AWS CDK

aws
awscdk
cloudformation
typescript
infrastructure-as-code
Author

Erik Lundevall-Zara

Published

May 23, 2023

Modified

February 19, 2024

Have you sometimes been frustrated over some feature which you can do via AWS Console, or perhaps via AWS CLI or one of the AWS SDKs (Software development Kits), but not via AWS CDK or CloudFormation?

Then this little tip may be of use to you! There is a relatively simple way to add functionality to AWS CDK (and indirectly, CloudFormation). This works if that functionality corresponds to a specific AWS API call only.

Let us use an example to show how it can be done. The example here is written in Typescript. The principles are the same for any AWS CDK-supported language, though. It is using a convenience feature of AWS CDK to more easily define custom CloudFormation resources.

A background for the example

The background for this example is that you cannot tag CloudWatch alarms via CloudFormation, or AWS CDK, but it is available through other means. In particular, when you have many alarms and the generated names are not crystal clear to read, you may want tags to make better sense of the alarms.

We used this approach in a setup where we have 100+ groups of AWS Lambda functions, ECS Fargate containers, load balancers, API Gateways, etc with associated alarms. These are deployed automatically through deployment pipelines, driven from a set of configuration files for each group of resources.

Alarms may trigger actions in monitoring systems, and/or notifications through Slack channels. Finding the right resources when human action is needed is helped a lot with tags for identification on the alarms.

Design of alarm tag workaround

The approach here is then simply to define a custom resource which will add provided tags to the CloudWatch alarm. You can add or update the tags with a single API call, which makes it suitable for this approach.

Whenever we create a CloudWatch Alarm using AWS CDK, we also create a CloudFormation custom resource which represents adding the tags to that resource. Under the hood, custom resources are implemented using AWS Lambda functions. These functions perform actions on events which represent create, update, delete actions for that resource.

Defining an AWS Lambda function for a single API call is a relatively large chunk of boilerplate. Fortunately, AWS CDK will handle a lot of that boiler plating for you.

The AWS CDK sub-module aws-cdk-lib/custom-resources will handle a large portion of that boiler plating.

Implementing the alarm tag workaround

From the aws-cdk-lib/custom-resources sub-module, we will use three constructs and classes: AwsCustomResource, AwsCustomResourcePolicy, and PhysicalResourceId.

import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from 'aws-cdk-lib/custom-resources';

The AwsCustomResource construct is where most of the action happens. The AwsCustomResourcePolicy class will set the proper IAM permissions for the underlying AWS Lambda function to make the appropriate API calls, and the PhysicalResourceId class is something we will use to tell CloudFormation if it should consider the resource to be a new one or the same one as before.

Below is an extract of functions and type definitions for implementing this custom resource, and to add it to the AWS CDK solution.

type StringMap = { [key: string]: string };
type AwsTag = { Key: string; Value: string };
type AwsTags = AwsTag[];

const awsifyTags = function(tags: StringMap) : AwsTags {
  return Object.keys(tags).map((key) => {
    return {
      Key: key,
      Value: tags[key],
    };
  });
};

const addAlarmTagsResource = function(scope: Construct, id: string, alarm: Alarm, tags: StringMap): AwsCustomResource {
  const resource = new AwsCustomResource(scope, id, {
    onUpdate: {
      service: 'CloudWatch',
      action: 'tagResource',
      parameters: {
        ResourceARN: alarm.alarmArn,
        Tags: awsifyTags(tags),
      },
      physicalResourceId: PhysicalResourceId.of(Date.now().toString()),
    },
    policy: AwsCustomResourcePolicy.fromStatements([
      new PolicyStatement({
        effect: Effect.ALLOW,
        actions: ['cloudwatch:TagResource'],
        resources: [alarm.alarmArn],
      }),
    ]),
  });

  return resource;
};

The bulk of the work is in the addAlarmTagResource function. The property onUpdate is the key part here. It specifies the information needed to make an AWS API call:

  • service - the AWS service to make an API call to
  • action - the actual API/function to call in that service. This is based on the function name in the AWS SDK for JavaScript, version 2.x.
  • parameters - the actual parameters to include in the function call. Again, this is based on AWS SDK for JavaScript, version 2.x.
  • physicalResourceId - the id of this custom resource.

In our use case, we only need to specify onUpdate, since creating an updating the custom resource is the same really for us. We also do not care about specific deletion logic, since the resource will never exist by itself, and only updates an associated resource, the CloudWatch Alarm. So we can leave out the corresponding onCreate and onDelete properties. If we do not specify an onCreate property, it will use onUpdate at creation time as well.

The physicalResourceId property will get a new value each time. This is done to ensure that the tagResource API call is always done on an update, and trick CloudFormation into believing there is replaced resource on update, so it would make the API call. We could build something with logic that actually checks whether the tags have changed, but in that case we need to build more complex logic, which is something we aim to avoid here and keep it simple.

An IAM policy that will allow the underlying AWS Lambda that the AWS CDK generates to make the defined API call is also needed.

There is a helper function AwsCustomResourcePolicy.fromSdkCalls() which can generate an IAM policy based on the configured API calls for the custom resource. That does not always work, though, which was the case here. So we need to define the policy ourselves in that case.

Tag format conversion

In the code that calls the addAlarmTagResource function to add tags, the tags are represented as a key-value structure (map, dictionary, etc - name depends on your flavour of language). However, the AWS API represents tags as an array/list of structures with Key and Value fields. So we have included some logic to convert the tag data from one format to the other.

Rounding up

This was a relatively simple example of addressing a shortcoming in CloudFormation, for which AWS CDK had not directly addressed themselves. The constructs and classes in AWS CDK help to make these kinds of workarounds relatively straightforward to implement. It is not ideal, but can be useful and can be kept simple, so that it also stays manageable and maintainable.

I hope this example may be useful for you.

Back to top