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.
There are a few different use cases for such a thing, these include:
- Provisioning resources not supported by CDK
- Implementing custom logic/configuration
- 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.
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:
- A Lambda function for
onEvent
handlingcreate
,delete
andupdate
events. - A
Provider
which points the Custom Resource to the lambda. - 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.
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 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:
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.
Top comments (0)