AWS Custom Resource using CDK
Tags: aws, tutorial, cloud, typescript
Published: 2023-10-04 08:16:14

If you are new to AWS' Cloud Development Kit (CDK), here's a quick explanation of what exactly it is:

> The AWS Cloud Development Kit (CDK) is a software development framework that allows you to define and provision cloud infrastructure using familiar programming languages such as TypeScript, Python, and Java.

> Traditionally, infrastructure provisioning has been done using templates or scripts that are difficult to read and understand. CDK simplifies this process by allowing you to define your infrastructure in code, using the same programming constructs that you use to build applications.

> With CDK, you can define your infrastructure as a series of reusable components called "constructs." These constructs can be shared across your organization and easily reused in multiple projects.

> Overall, AWS CDK makes it easier and faster to build and manage cloud infrastructure, with less room for error and greater potential for reuse.

Thanks ChatGPT for that great explanation! 😂

So, now we have the basics of what CDK is and what it does for us, I want to look at building up a Custom Resource.

## Why build a Custom Resource? 

Custom Resources is a very useful feature of CDK that allows you to define and manage resources that are not available as a [resource type in AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html).

There are a few different use cases for such a thing, these include:

1. Provisioning resources not supported by CDK
2. Implementing custom logic/configuration
3. Integrating with third-party services

> Using resource types for third-party resources provides you a way to reliably manage these resources using a single tool, without having to resort to time-consuming and error-prone methods like manual configuration or custom scripts.

[AWS Documentation](https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-types.html)

Using a Custom Resource allows us to automate the creation, deletion and updating of these resources across multiple environments as and when we need.

## How to build a Custom Resource? 

To build a Custom Resource we need 3 things:

1. A Lambda function for `onEvent` handling `create`, `delete` and `update` events.
2. A `Provider` which points the Custom Resource to the lambda.
3. The `CustomResource` itself with any props you need for your third party resource or otherwise.

You can also provide an additional Lambda function to handle `isComplete`. 
This is used when the lifecycle operation cannot be completed immediately. 
The `isComplete` handler will be retried asynchronously after `onEvent` until it returns `{ IsComplete: true }`, or until it times out.

### onEvent Lambda

This function will be invoked for all resource lifecycle operations (Create/Update/Delete). 

Here is how this handler might look:

```
import {
  CloudFormationCustomResourceCreateEvent,
  CloudFormationCustomResourceDeleteEvent,
  CloudFormationCustomResourceEvent,
  CloudFormationCustomResourceResponse,
  CloudFormationCustomResourceUpdateEvent,
} from "aws-lambda";

export const handler = async (event: CloudFormationCustomResourceEvent): Promise<CloudFormationCustomResourceResponse> => {
  switch (event.RequestType) {
    case "Create":
      return await createSomeResource(event as CloudFormationCustomResourceCreateEvent);
    case "Update":
      return await updateSomeResource(event as CloudFormationCustomResourceUpdateEvent);
    case "Delete":
      return await deleteSomeResource(event as CloudFormationCustomResourceDeleteEvent);
  }
};
```

For each of these cases, we want to respond according.

In other words, for a `Create` event we would want to, well, create something. `Update` we might want to delete something and create a new item if our use case doesn't offer a way to directly update the resource. For `Delete`, we should be deleting our resource. Pretty straight forward.

If you were, for example, using this Custom Resource to integrate with a third party service, you may want to make some specific API calls for creating, updating and deleting.

#### Understanding Lifecycle Events

It is important to understand how these events will be handled by CloudFormation.

If `onEvent` returns successfully, CloudFormation will show you the nice green tick of `CREATE_COMPLETE` for the `CustomResource`.

However, if `onEvent` throws an error, CloudFormation will let you know something went wrong and the CDK deploy will fail.

There are some important cases to think about when errors occur:

- Do you need to tidy up some other resources if some step fails in your create flow?
- What happens if you hit the delete event but there is nothing to delete?

Should be noted, if a `Delete` event happens to fail, CloudFormation will just abandon that resource moving forward.

You can find more detail on [these cases here](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.custom_resources-readme.html#important-cases-to-handle).

#### Lambda in CDK

A super simple way to declare this Lambda in CDK is:

```  
readonly onEventHandlerFunction = new NodejsFunction(this, "CustomResourceOnEventHandlerFunction", {
  timeout: Duration.seconds(30),
  runtime: Runtime.NODEJS_18_X,
  entry: "/path/to/CustomResourceOnEventHandler.ts"
});
```

[NodejsFunction](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-lambda-nodejs.NodejsFunction.html) creates a Node.js Lambda function bundled using `esbuild`. This means you can directly pass in your TypeScript file. Cool, right?

### Provider

We just need to tell the `Provider` (from `aws-cdk-lib/custom-resources`) to point the custom resource to the above function.

```
readonly customResourceProvider = new Provider(this, "CustomResourceProvider", {
  onEventHandler: this.onEventHandlerFunction,
  logRetention: RetentionDays.ONE_DAY
});
```

### Custom Resource

Finally, we just need to tell the Custom Resource who its provider is and give it a type starting with `Custom::` :

```
readonly resource = new CustomResource(this, "YourCustomResource", {
  serviceToken: this.customResourceProvider.serviceToken,
  properties: {...this.props, id: this.id},
  resourceType: "Custom::YourCustomResource",
});
```

## Result

Bringing these 3 things together, you will create a class similar to:

```
import {Construct} from "constructs";
import {Provider} from "aws-cdk-lib/custom-resources";
import {RetentionDays} from "aws-cdk-lib/aws-logs";
import {CustomResource, Duration} from "aws-cdk-lib";
import {NodejsFunction} from "aws-cdk-lib/aws-lambda-nodejs";
import {SomeProps} from "../models/SomeProps";
import {Runtime} from "aws-cdk-lib/aws-lambda";

export class YourCustomResource extends Construct {
  constructor(private scope: Construct, private id: string, private props: Omit<SomeProps, "id">) {
    super(scope, id);
  };

  readonly onEventHandlerFunction = new NodejsFunction(this, "CustomResourceOnEventHandlerFunction", {
    timeout: Duration.seconds(30),
    runtime: Runtime.NODEJS_18_X,
    entry: "/path/to/CustomResourceOnEventHandler.ts"
  });

  readonly customResourceProvider = new Provider(this, "CustomResourceProvider", {
    onEventHandler: this.onEventHandlerFunction,
    logRetention: RetentionDays.ONE_DAY
  });

  readonly resource = new CustomResource(this, "YourCustomResource", {
    serviceToken: this.customResourceProvider.serviceToken,
    properties: {...this.props, id: this.id},
    resourceType: "Custom::YourCustomResource",
  });
}
```

You can see here we are passing through all the props from the `YourCustomResource` into the Custom Resource.

In the example of making API calls using the Custom Resource we might need something like an API Key to be passed through from our CDK stack into the resource.

Using this `YourCustomResource` class you can now build up something like this in one of your CDK Stacks:

```    
readonly exampleCustomResource = new YourCustomResource(this, "YourCustomResourceExample", {
    enabled: true,
    apiKey: "api-key", // This prop could differ per environment
    name: "Example Custom Resource"
});
```

Your CDK diff for this stack would look something like this:

![CDK diff for Custom Resource](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qmb78zdwaxy41vf7c8oi.png)

Now we have an understanding of how to build up the infrastructure for this Custom Resource, we now just need to determine how the `onEvent` Lambda will handle each of the lifecycle events. 

This depends on your use case but the possibilities are endless really! 

This part I will leave up to you as it is very specific per scenario, but I hope this has helped you on your journey of getting a `CustomResource` up and running.