Overview On AWS CloudFormation Custom Resources
AWS CloudFormation lets you describe most AWS cloud environment resources in a neat and tidy way. Developing a product which is described completely by CloudFormation is easy to create, duplicate, maintain and delete.
But sometimes it’s not enough.
What if you want to extend AWS CloudFormation to support more resources, such as non-supported AWS services, or even to support a non-AWS service. This is where the interesting part begins.
With custom resources, you can bring your own logic into a CloudFormation template. You can handle the resource logic for its creation, update and deletion.
One option to write this logic is using Lambda functions. Lambda functions are great. But as we will see, using them in a custom resource might be tricky. Let me explain why.
If you ever used custom resources and wondered why your stack got stuck when trying to delete a custom resource, or even wondered why your Lambda function behind the resource was called more than once, this blog post is exactly for you.
Even if you haven’t ever used custom resource but are looking to extend your CloudFormation, here you will find a complete explanation to avoiding mistakes that can cost you in time and sometimes even loss of important data.
Building an authentication system
We will describe a basic yet complete authentication system using Auth0. I find Auth0 to be a very flexible and easy to use authentication service. Let’s dive deeper.
We will describe a microservice using an AWS CloudFormation template. Throughout the building process, I will emphasize some best practices I have learnt over an extensive use of custom resources.
Describing the Microservice
The microservice will include the following services:
- Login web page hosted on S3
- Custom resources to represent Auth0 resources
I’ll skip the details of the S3 resource, so we can concentrate on writing our custom resource. I recommend using Auth0 Lock to make things easy.
The flow is as follows:
- User opens a webpage, and logs in with Auth0
- After successful authentication, user will be redirected to another page
This flow is very basic, and now I want to focus on our custom resources and best practices.
How to Design a Custom Resource?
It’s important to design your custom resource well. Partial or bad design could lead to endless debugging and even to loss of resources.
First you have to break your resources into the smallest pieces you can imagine. It is much easier to maintain and think of a resource when it only has a single purpose. In addition, it will make your life easier in choosing the physical resource id. But what is a physical resource id?
Each resource has a physical resource id which identifies the resource. Try to think of it as the minimum required to describe, update and delete your resource. For example, with Auth0 client id and the proper API call, we can update the client information or even delete the client. So choosing the right physical resource id is a crucial part.
Custom Resources For Authentication
We will use two custom resources:
- Resource to represent the Auth0 client
- Resource to represent the Auth0 connection (i.e. user database)
In our case, the physical resource id would be the client id and connection id respectively. I chose them because they are the only thing we need to describe, update and delete through the API later on.
If you can think of more than one parameter that you need to update or delete your resource, then you should split your resources into even smaller pieces.
Custom Resource Lifecycle
In a Lambda backed custom resource, you implement your logic to support creation, update and deletion of the resource. These indications are sent from CloudFormation via the event and give you information about the stack process.
In addition, every resource has properties. In a custom resource you can write whatever properties you want, and it will be passed to the function within the event. There is only one required property: ServiceToken, which in our case is the function's ARN.
Let’s take a closer look at our CloudFormation resources:
It’s important to understand the custom resource life cycle, to prevent your data from being deleted. We will now take a closer look at each part of the life cycle.
Create, Delete and Update
Create - that’s easy, when a resource is being created an event with request type Create is sent to your function.
Delete - this one is more tricky. When a resource is being deleted a Delete request type is sent. But there are more scenarios other than resource Delete. We will have to explain Update first.
Update - gets called if any of your custom resource properties were changed. For example, in our app we can modify the allowed callback urls, which will trigger the function with an Update request type.
More about Update
A very interesting and important thing to know is that CloudFormation compares the physical resource id you returned by your Lambda function to the one you returned previously. If the IDs are different, CloudFormation assumes the resource has been replaced with a new resource. Then something interesting happens.
When the resource update logic completes successfully, a Delete request is sent with the old physical resource id. If the stack update fails and a rollback occurs, the new physical resource id is sent in the Delete event.
Auth0 Custom Resources Logic
Now that you know about the lifecycle, the resources logic should be simple. By using the right Api calls you can easily create, update and delete client/connection in the appropriate function.
Send Your Response Always (really, ALWAYS)
CloudFormation expects to get a response from your Lambda function after you're done with your logic. It will not continue with the deployment process if it doesn’t get a response, or at least until a 1 hour(!) timeout is reached. It can cost you a lot of time and frustration.
Believe me, I know what a CloudFormation console full of stuck stacks looks like. Therefore, make sure you always return an answer. What are the situations that we should pay attention to? Let's take a closer look:
- It’s important to wrap with try...catch the import of libraries that happens before the handler is being called. Otherwise your function might crash and you will not be able to return an answer
- Return an answer if you see that your function is about to reach timeout (e.g. you can use context.getRemainingTimeInMillis() )
Handle all possible code exceptions. Use finally to avoid paths in your code which can lead to a situation where you don’t send a response
Timeout and Re-invoke
Make sure that your functions are designed with idempotency in mind. An idempotent function can be repeated any number of times with the same inputs, and the result will be the same as if it had been done only once. Let’s see why is it important.
Idempotency is valuable when working with CloudFormation to ensure that retries, updates, and rollbacks don't cause creation of duplicate resources, errors on rollback or delete, or other unintended effects.
In our example, you don’t want to finish with multiple client/connections being created.
This example here can be used as the basis for more general and complicated applications using CloudFormation custom resources. Although AWS is working hard and adding new services to the CloudFormation template, custom resources are still useful and vital to integrating external services. Using custom resources properly gives you the power to make diverse and rich products.