AWS Custom Resource using CDK AwsCustomResource construct

AWS Custom Resource using CDK AwsCustomResource construct

Implementing AWS custom resources with "AwsCustomResource" and how it is different from "Provider" CDK construct

Prerequisite

  • Understanding of AWS CDK (Cloud development kit)
  • Understanding of AWS Custom resources

INTRODUCTION

All through the blog I will be mentioning terms custom resource lambda and custom resource. Both terms look similar but actually are different. Custom resource lambda refers to the lambda that is responsible for implementing the logic. Custom resource is the resource itself that is created by invoking the custom resource lambda.

In this blog I am covering a niche scenario related to AWS custom resources when all your custom resource lambda needs is to call an AWS API. Usual use case of doing so would be when cloudformation does not support a resource and it is only available with an API.

For all other use cases, please refer to my earlier blog Custom Resources with AWS CDK.

CDK Construct AwsCustomResource helps us achieve that. It takes care of creating custom resource lambda and permissions needed under the hood. As a custom resource author, all you need to do is pass the AWS Service and API action.

Why should you use yet another way of creating custom resource?

Although you can achieve the same results using Provider cdk construct described in my previous blog, the reasons why you might choose this method are:

You will see all these points in action in the example below. So revisit these points again after going through the example for better understanding)

  • You do not need to even create a Custom resource lambda explicitly
  • You do not need to manage the permissions needed by custom resource lambda anymore. CDK construct takes care of it. Also it follows the least privilege principle by design.
  • It seamlessly blends with your CDK code
  • CDK interfaces and enums definitions can be leveraged since all you code is part of CDK application and not a separate lambda
  • You need lesser lines of code to achieve the same results
  • Code readability is better

EXAMPLE

Below is an example of a use case not possible to achieve with Cloudformation.

If you want to create a subscription when the SNS topic and the endpoint are in different regions, it is not possible with Cloudformation at the moment.

Let us try and create a custom resource for it using CDK construct AwsCustomResource. This construct will be responsible for calling AWS subscribe and unsubscribe API's.

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

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

    const topicArnInAnotherRegion = '<ARN_TOPIC_IN_DIFFERENT_REGION>';

    // Just a dummy lambda to act as an endpoint for SNS. Don't confuse it as lambda for CDK resource
    const dummyLambdaEndpoint = new lambda.Function(this, 'Function', {
      code: lambda.Code.fromInline('exports.handler = async (event) => {console.log(event)};'),
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_12_X,
    });

    // Construct that takes care of creating the custom resource
    new cr.AwsCustomResource(this, 'SNSCustomResource', {
      policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
        resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
      }),
      onCreate: {
        action: 'subscribe',
        service: 'SNS',
        physicalResourceId: cr.PhysicalResourceId.fromResponse('SubscriptionArn'),
        region: '<TOPIC_REGION>',
        parameters: {
          Protocol: sns.SubscriptionProtocol.LAMBDA,
          TopicArn: topicArnInAnotherRegion,
          Endpoint: dummyLambdaEndpoint.functionArn,
        },
      },
      onUpdate: {
        action: 'subscribe',
        service: 'SNS',
        physicalResourceId: cr.PhysicalResourceId.fromResponse('SubscriptionArn'),
        region: '<TOPIC_REGION>',
        parameters: {
          Protocol: sns.SubscriptionProtocol.LAMBDA,
          TopicArn: topicArnInAnotherRegion,
          Endpoint: dummyLambdaEndpoint.functionArn,
        },
      },
      onDelete: {
        action: 'unsubscribe',
        service: 'SNS',
        region: '<TOPIC_REGION>',
        parameters: {
          SubscriptionArn: new cr.PhysicalResourceIdReference(), // Passes the physical resource ID set during CREATE/UPDATE
        },
      },
    });
  }
}

Understanding the code

You might have already noticed some differences with the Provider CDK construct.

The Provider CDK construct just like Cloudformation custom resources first creates a custom resource lambda, which you later reference using serviceToken property in your custom resource. The semantics of AwsCustomResource work a little differently. Here everything is done in 1 shot. You do not create a custom resource lambda separately and then reference it in your custom resource. Both the custom resource lambda and custom resource itself are created by the same AwsCustomResource construct.

As understandable by their names, onCreate, onUpdate and onDelete properties represent the lifecycle events of the custom resource and are invoked when the custom resource is created, updated or deleted respectively.

Let us go through the interesting parts in the code one by one:

    ...
      policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
        resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
      }),
    ...

Property policy configures the IAM permissions for your custom resource lambda role. Method fromSdkCalls adds actions configured in onCreate, onUpdate and onDelete properties, to the custom resource lambda IAM policy. Based on the above example, custom resource lambda IAM policy will have sns:subscribe and sns:unsubscribe permissions.

Property resources controls which resources the custom resource lambda role will be given access to. AwsCustomResourcePolicy.ANY_RESOURCE will simply pass "*" wildcard as resource in the custom resource lambda IAM policy. However you should always aim to explicitly pass the resources so as to follow the least privilege principle. In our example we could have used it like:

resources: [topicArnInAnotherRegion],

    ...
      onCreate: {
        action: 'subscribe',
        service: 'SNS',
        physicalResourceId: cr.PhysicalResourceId.fromResponse('SubscriptionArn'),
        region: '<TOPIC_REGION>',
        parameters: {
          Protocol: sns.SubscriptionProtocol.LAMBDA,
          TopicArn: topicArnInAnotherRegion,
          Endpoint: dummyLambdaEndpoint.functionArn,
        },
      },
    ...

This block is configuring the various parameters that we intend to pass to the AWS SUBSCRIBE API.

physicalResourceId: PhysicalResourceId.fromResponse('SubscriptionArn') This piece of code tells lambda to fetch SubscriptionArn from the response of sns subscribe api call and set it as physical resource id for the custom resource created.

The onUpdate block also works similarly.


    ...
      onDelete: {
        action: 'unsubscribe',
        service: 'SNS',
        region: '<TOPIC_REGION>',
        parameters: {
          SubscriptionArn: new cr.PhysicalResourceIdReference(),
        },
      },
    ...

in the onDelete block the only interesting part of code is SubscriptionArn: new cr.PhysicalResourceIdReference(). Class PhysicalResourceIdReference is responsible here to return the current physical resource id set for the custom resource.

CONCLUSION

In conclusion, the aim of the construct AwsCustomResource is to abstract away all the infrastructure parts and let you as a custom resource author just care about the parameters needed to be passed to the underlying AWS API. It creates a lambda for you and also manages the permissions needed by lambda.

Although as mentioned at the beginning of the article also, this method serves a very specific use case. You might not use it that often. Do check out my other post on Custom Resources with AWS CDK. That is how you will most likely be using the custom resources.