AWS Bites Podcast

121. 5 Ways to extend CloudFormation

In this episode, we discuss 5 different ways to extend CloudFormation capabilities beyond what it natively supports. We started with a quick recap of what CloudFormation is and why we might need to extend it. We then covered using custom scripts and templating engines, which can be effective but require extra maintenance. We recommended relying instead on tools like Serverless Framework, SAM, and CDK which generate CloudFormation templates but provide abstractions and syntax improvements. When you need custom resources, CloudFormation macros allow pre-processing templates, while custom resources and the CloudFormation registry allow defining new resource types. We summarized recommendations for when to use each approach based on our experience. Overall, we covered multiple options for extending CloudFormation to support more complex infrastructure needs.

AWS Bites is brought to you by fourTheorem, an AWS Partner that specialises in modern application architecture and migration. If you are curious to find out more and to work with us, check us out on fourtheorem.com!

In this episode, we mentioned the following resources.

Let's talk!

Do you agree with our opinions? Do you have interesting AWS questions you'd like us to chat about? Leave a comment on YouTube or connect with us on:

Eoin
00:01:45

Back in episode 31, we did a battle episode talking about CloudFormation versus Terraform. It's worth going back to a look at that one. It's one of the most popular episodes even still. And in that, we talked a little bit about CloudFormation and how you might extend it, but we're going to dive a little bit deeper today. So CloudFormation is the native infrastructure as cloud solution from AWS. Like you said, Luciano, you get templates that allow you to define stacks, which are just a collection of resources. You use YAML or JSON, and then you can deploy them to AWS. AWS's responsibility then is to manage the state of those resources while they're provisioning the dependency between them and to detect failures, retry failures, and then roll back if anything goes wrong and try to keep your cloud in a consistent state. That's really the goal. And it does its job pretty well. There are some cases, of course, where it might be a bit limited or it can be inconvenient. So you do need to build from time to time alternative solutions on top of it. I think we've been in this case quite a lot. We can let you know in a bit what we tend to go for. So CloudFormation does do its job pretty well, but there are some cases where you might need to customize it or extend it just because there are different situations you find yourselves in. So one such thing is you just find that the syntax might be very verbose. So you might want to write something a bit more high level, higher level component, if you will, CloudFormation that allows you to abstract that and have a modular reusable component. Then again, you might also just want to provision resources that aren't even supported yet by AWS. This happens less these days, but it still does happen. One example I know you've come across recently, Luciana, is Amazon Q Business. It's in preview, but there is no CloudFormation support yet. And then there are resources outside the realm of AWS, like you might be using a third party vendor like Auth0 or SupaBase or something else. And you want to make sure that all your resources are part of the same stack for consistent updates and deployments and for making it easy to reference resources from one stack to another. Now you might need to do things before or after CloudFormation runs. So that's another use case for customizing it. So you might want to build a front end application and copy all of the assets into an S3 bucket, or you might want to pre-process assets like optimizing pictures, building container images, something that normally falls outside the realm of provisioning infrastructure, but it does become part of your deployment. Or you might just want to fetch configuration that you might need as a parameter for your stacks. So how do we get started? What's the first thing you came across as a solution to this, Luciana?

00:05:43

And maybe you can also do things afterwards, maybe, I don't know, indicate somewhere, maybe in a chat message that a deployment was completed or the status of that deployment, all sorts of integrations like that. It was something very common for people to do these kinds of things back in the days. Now, is this a good practice? It can be very effective, but I would personally discourage it because the problem is that you will end up with very custom code that you'll need to maintain over time and that every new engineer in the team will need to get comfortable with. And also it will be very different from company to company because everyone is effectively creating their own solution from scratch. And writing this code in a reliable way can also get very complicated very quick. For instance, you just need to try to imagine that the more steps you have, the more opportunities for things to go wrong there are, and you have to think, okay, what do I do if something goes wrong at this step? How do I clean up? How do I make sure that my entire deployment ends up in a consistent state? And again, this is not something very easy to do well. So you might end up with lots of trial and errors before you have something stable. And in between, you might end up with a few broken deployments, which is not a great situation to be in. So there are today lots of better ways to achieve most of the same things we just discussed here without having to struggle with creating your own custom scripts. So I think the main one is to use new tools that came in the industry in the last few years. So which ones come to mind first to you, Eoin?

00:07:38

The Serverless Framework YAML essentially becomes a superset of CloudFormation where you can define normal CloudFormation in there, but you can also provide this concise syntax for things like Serverless functions and the triggers for those functions. That's really where it shines. One of the notable things about it is that it has a really wide and rich plugin system for all sorts of use cases. So you can extend it with all sorts of plugins that can make the job of generating CloudFormation even easier. And more recently, CDK, and we talked about this recently in the state of JavaScript, sorry, not state of JavaScript, state of Serverless survey results, CDK is getting really popular and it allows you to define infrastructure as code using the program language of your choice, TypeScript, Java, Python, .NET, and Golang. And it makes it easy then to create higher level abstractions. There's a whole CDK episode we did where we talked about the different levels of constructs. And at the basis of that, you have essentially types in the language of your choice that generate just the raw CloudFormation resources. Anything that's a higher level construct is just built on top of that. So it generates CloudFormation templates, but makes it easier for programmers who are skilled in these languages to make it reusable, modular, and extensible. Now, if we go back to YAML land, we have the Serverless Application Model or SAM, and it is similar to Serverless Framework in principle and use cases. I would imagine it's inspired by it in some way, but it is an official AWS solution and competes with Serverless Framework. It is slightly different in that it doesn't have that plugin ecosystem. In fact, it's much more strict when it comes to the syntax, but the way it works is by having its own template language that looks like a superset of raw CloudFormation, but offering those interesting shortcuts. And its magic is done by using CloudFormation macros, which is one of the ways of extending CloudFormation that we're going to discuss in a moment. Now, I mentioned a few previous episodes that are relevant. Don't worry, all the links to those will be in the description below. So we talked about CloudFormation macros, Luciano, do you want to take us through those?

00:10:15

And you can use this concept to do all sorts of different things, for instance, automate tasks, enforce policies, or even just create new resources, for instance, as part of expanding that template. So what are they good for? We recently used the CloudFormation macros for a project called SlickWatch. We will have the link in the show notes. And the idea of SlickWatch is that, for instance, you might want to have best practice generated dashboards and alarms, depending on the resources that you use on your template. So you can basically say, if I'm using an SQS queue, for instance, in my template, this macro will automatically generate dashboards and alarms for you to monitor the most common things that you would be worried about when using something like an SQS queue. And of course, you can extend that concept to different resources. And it solves lots of the boilerplate that you generally have to do when it comes to monitoring for specific resources. So this is just an example of when a CloudFormation macro can be very convenient. Take a simple template as an input, see what's inside, and do something useful and produce maybe a slightly enhanced version of that template. And we already mentioned how it generally works, but the idea is that you will need to write that code somewhere. And generally, you can write it as a Lambda function. Then you need to publish this Lambda function, register it as a CloudFormation macro, give it a name. And from that moment on, you can reference that transform into your own templates before deploying in this specific account where you provision the macro. So you, of course, you need to make sure the macro is provisioned before you use it. If you are, for instance, deploying a third-party macro from maybe an open source project like SlickWatch. But then once you have done that thing one-off, you can use that transform in all sorts of projects that you are deploying in that particular account. Now, there might be some small problems with it. For instance, one, as I said, is that you need to make sure things are provisioned before you can actually use it. And the other one is that if you have issues, for instance, as you write your transform, it's not always very straightforward to debug exactly where the issue is because effectively you are taking a template that might be very, very big and very involved in terms of properties. So you need to write generally very complex code that can parse the original template, make sure you really understand the semantic of the template, and then you change things in that template so there is a lot that can go wrong in doing all these different things. And we found ourselves that when building SlickWatch, there is a lot of debugging that goes on. Of course, you can recreate locally different versions of the templates, run it locally, see what is the output of the template, but you still need sometimes to deploy the template to make sure it is semantically correct and is doing exactly the thing that you want it to do. So there might be lots of kind of development cycles before you get to a point where you are happy with the outcome of that transform macro. There is actually another good example that we bumped into multiple times and we might have mentioned it in previous episodes, is a macro called AWS SSO Util by Ben Kiyoi. And it's very convenient because it gives you higher level syntax to do things that might get very, very verbose if you need to define the low-level resources that you generally need for SSO type of operations. So another one we will have a link in the show notes, and it might be a good one to check out if you are just curious to see what it takes to build a cloud formation macro. What else does come to mind, Eoin? We should definitely talk about custom resources.

00:15:44

If you're really handling that proper life cycle of create, update, and delete, that's some of the things you'll need to watch out for. I've seen quite a few naive and simple versions of custom resources, which essentially just have an idempotent action that happens when a create or update occurs. And that I suppose simplifies the implementation. And you don't have to worry too much about detecting which properties have changed. When your function is invoked, it will also pass in a response URL. You can call that to tell CloudFormation that you're finished and your update or delete or whatever has succeeded or failed. It's a little bit more involved than just returning a value from your Lambda function handler. You need to actually make a request to that specific URL. There are some libraries that can help you to do this correctly and safely. There's a few things you need to be aware of. First of all, your Lambda function might actually time out before it's able to send the response, which CloudFormation will never get the response and it will take an hour to time out, which I've been through multiple times and it's not fun. Another thing is if an error happens in your import code, like in your module initialization code, and you're not catching that and sending back a CloudFormation response in the handler, then that can also cause this one hour hang. So you have to be really careful. Node.js on NPM, there are a few modules we can link in the show notes that make it a bit easier to safely implement CloudFormation customer sources and catch all of these errors and make sure that you always send a response before any possible timeout. Now, the good thing about these customer sources is that they're probably the easiest ones to start with. We use them quite often. Recently, we were using them to run database schema migrations when you deploy an application so that you make sure you always deploy it with the application stack. And that makes a lot of sense for us and it was a good fit. That's very easy to use for self-contained resources that you need just for one project. We mentioned the problems with timeouts. You do have to be really careful. It's a good idea to deploy your code, test it outside of CloudFormation environment and really validate that it works before you try it inside CloudFormation. It's missing things like drift detection, resource import. It's difficult to share it across multiple projects. And I suppose as well, you're using a Lambda function. These resources are then running in your own account. So you need to make sure you've got the right networking and IAM permissions. If you're running that Lambda function in private PC subnets, you need to make sure that you set up the VPAC endpoints to interact back with CloudFormation with the response. Another gotcha there is that the response URL actually comes through as an S3 pre-signed URL. So you need to make sure then in that case, you've got a S3 gateway endpoint or interface endpoint. And if you don't do that, you're just going to hang and it was going to take a while for it to fail. And then sometimes try and roll it back and it'll roll back by trying to invoke the same function again, which is going to timeout again, which is going to take you another hour. So CloudFormation custom resources can be great, but when they don't work, they really induce rage. So when it comes, is there anything we could do to mitigate the risk of rage here, Luciano? What else have we got?

00:23:31

We already mentioned that there are extra features like diff detection. You can import resources as well. Another interesting detail is that the code is executed by AWS for you. So you don't have to worry as much as you have to do with custom resources. Think about, okay, where is this Lambda going to run? Did I provision enough memory or I don't know, timeout? Is that timeout correct? Is it going to be sufficient for what they need to do? Or think about, I don't know, networking restrictions, all that kind of stuff. You have less concerns because AWS is going to run the code for you. Of course, you still need to provide permissions in some way. And the way you do that is by providing a role that AWS is going to assume for you and that role constraints what can happen inside the custom logic. So you don't have to be worried in terms of, this custom resource is going to create a massive EC2 instance that is going to cost me lots of money every month. You can restrict exactly what the role is and what can happen inside that particular execution. There are other advanced features. For instance, there is a concept of hooks. And then another advantage is that AWS Config will automatically list all the custom resources that are created through the registry. So it's very convenient that if you just want to be reassured at any point in time, if you have resources coming from the registry, you want to see what those are, you can easily see a list of them, even across multiple stacks. We will have a link in the show notes with all the documentation that you need to follow if you want to implement something like this. Of course, the disadvantage is that this process feels a little bit more involved. So something you need to keep in mind as opposed to just creating custom resources for simple use cases probably is still simpler to use custom resources. For more advanced use cases, maybe where you need to make those resources available in an easier way, probably going with the registry is a better approach.

00:28:19

And generally speaking, you might be adding a few things. Maybe, I don't know, you can automatically tag all of the resources based on some internal rules, or you can do more advanced things like the ones we did with SlickWatch for simplifying the efforts of making applications very easily observable and to have alarms. And the other two things that we mentioned are CloudFormation custom resources and the CloudFormation registry. Those are great whenever you want to actually create the concept of a new resource, either something that doesn't exist yet in AWS, because maybe it's something in preview. So you have maybe an SDK, but you don't necessarily have the corresponding resources in CloudFormation. You might be doing your own custom resources to back field that and still be able to do infrastructure as code. There are some limitations that we mentioned with custom resources and some gotchas, so be aware of those. But they are generally really good when you have something self-contained. If you have something that instead you plan to reuse across multiple applications or even make available externally, then you should be looking into the registry because that seems to be a much more complete solution and something that is easier to share even outside the boundaries of your own company. So all of that brings us to the end of this episode. I hope you find it informative. One last thing that I want to mention, I want to give credit to the Cloudonaut guys. There is a very good podcast that they did, I think a couple of years ago, but I think it's still very relevant. They cover some of the topics we discussed today, and they also mentioned some examples, some use cases that they had. So if you enjoyed this particular episode and you want to find out more, check out that episode. The link will be in the show notes. As always, if you found this useful, please share it with your friends, your colleagues, and leave us a comment. Let us know what you did like, if you have any question, and what else you would like us to cover next. So that's all. Thank you very much for being with us, and we will see you in the next one.

Help us to make this transcription better! If you find an error, please submit a PR with your corrections.