Custom Resources with AWS CDK

Writing Custom resources with AWS CDK lib @aws-cdk/custom-resources

Custom Resources with AWS CDK

Prerequisite

  • Understanding of AWS CDK (Cloud development kit)

  • Understanding of AWS Custom resources

Custom resource with lib @aws-cdk/custom-resources

CDK provides a library for conveniently writing custom resources. As a custom resource author how does it make your life any easier?

  • As a custom resource author, you can focus on the actual logic for the custom resource and let CDK take care of other boilerplate stuff. Eg- you do not need to worry about sending the response back to the pre-signed S3 URL. Just return PhysicalResourceId using the return statement and you are good to go. Consequently it also makes your code easier to read and maintain.

  • Doubling down on the point above, in case your Custom resource lambda needs to run for more than the lambda timeout, this cdk construct can also take care of polling and retrying by using an additional handler isComplete. An example where this might come in handy would be if you are provisioning some service on EC2 which takes 20~30 mins to stabilize (consider that the max time a lambda can run is 15 minutes at the moment)

How does it do all these things?

As you might have already guessed, the answer is using a wrapper lambda which takes care of the boilerplate stuff, so that you as a custom resource author just concentrate on the logic needed for creating the actual resource.

Lambda needs to return PhysicalResourceId and if needed Data. Here is an example custom resource lambda which just reads the latest ami-id and returns it after reading from SSM parameter /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2. This example is written in typescript but you can easily achieve the same in the language of your choice (should be supported by AWS LAMBDA ofcourse).

Note that this example is just to demonstrate how to use custom resources with CDK. It has no real use case, since you can always directly use this SSM parameter in your CDK app / Cloudformation stack to get the AMI.

import { CloudFormationCustomResourceEvent } from 'aws-lambda';
import * as AWS from 'aws-sdk';

export const handler = async (event: CloudFormationCustomResourceEvent) => {
  const ssm = new AWS.SSM();
  switch (event.RequestType) {
    case 'Update':
    case 'Create': {
      const ami = await ssm.getParameter({
        Name: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2',
      }).promise();
      return {
        PhysicalResourceId: ami.Parameter?.Value,
      };
    }
    case 'Delete': {
      return;
    }
    default: {
      throw new Error('Unknown request type');
    }
  }
};

Stitching it with CDK

Now that we have a lambda which does the actual job of fetching the AMI ID from the SSM parameter, we will see how to use it inside a CDK application. Here is an example:

import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import * as cr from '@aws-cdk/custom-resources';

export class CustomResourceWithCDK extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    // Points to the lambda we saw above
    const customResourceLambda = new lambda.Function(this, 'CRLambda', {
      code: lambda.Code.fromAsset(`${__dirname}/<LOCAL_PATH_TO_LAMBDA>`),
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_12_X,
    });

    // Allow lambda to read SSM parameter
    const ssmPolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ['ssm:*'],
      resources: ['arn:aws:ssm:eu-west-1::parameter/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'],
    });
    customResourceLambda.addToRolePolicy(ssmPolicy);

    // Create a custom resource provider which wraps around the lambda above
    const customResourceProvider = new cr.Provider(this, 'CRProvider', {
      onEventHandler: customResourceLambda,
    });

    // Create a new custom resource consumer
    new cdk.CustomResource(this, 'CustomResourceAMIConsume', {
      serviceToken: customResourceProvider.serviceToken,
    });
  }
}

Understanding this CDK code

const customResourceLambda = new lambda.Function(
    ...
)

This part deploys the actual lambda responsible for the Custom resource logic (The first lambda example we saw in this example)

const customResourceProvider = new cr.Provider(
    ...
)

This part is responsible for creating a wrapper around your lambda. This wrapper takes care of sending the response back to s3 Pre signed URL in case of SUCCESS/Failure.

new cdk.CustomResource(
    ...
)

In this part we are just consuming the Custom resource created above.

There is an even easier way of writing custom resources with CDK if the custom resource just needs to call an AWS API call. I have covered it under this post.