The devil is in the details is an idiom that refers to a catch or mysterious element hidden in the details, meaning that something might seem simple at a first look but will take more time and effort to complete than expected. I think this describes pretty well the announcement of ACM Cloudformation extension.
Promise of being able to provision SSL certificates from Cloudformation without any manual validation steps sounds absolutely great. Until now you either had to reference existing certificates with ARNs or do some hacky tricks to validate certificate request with Lambda-backed custom resource. While the promise is true, there are some details that make it less than perfect for typical use-cases :-(
HostedZoneName vs. HostedZoneId
When using AWS::Route53::RecordSet to create DNS entries, you can define the hosted zone by it’s id or name. Name is the preference for (most) humans as it is easy to remember and less error prone than random id. There is limitation that you must use id if you have more than one zone with the same name, but for most of the time name works fine.
To enable automatic certificate validation you must use DNS validation, but that is not enough.
Before Cloudformation can add DNS records for certificate validation, you must also set
HostedZoneId in DomainValidationOption to define which hosted zone
records should be created to. Unfortunately it is not possible to use
the same way it works with Route53 records. So in many cases you must provide a matching
pair of hosted zone name and id for your template to setup both DNS record and SSL certificate.
When there are non-trivial parameter combinations in the template, it is good if you can catch errors before deploying resources. Cloudformation Rules can do some impressive pre-deployment validations like checking that given subnets are within given VPC.
Rules: SubnetsInVPC: Assertions: - Assert: Fn::EachMemberIn: - Fn::ValueOf: - Subnets - VpcId - - !Ref VPCID AssertDescription: All subnets must be within the given VPC
Unfortunately above to work requires use of AWS-specific parameter type AND
type to support useful attributes. Above example of matching subnets to VPC is about the
only use-case that satisfies both requirements at the moment. But just using
AWS::Route53::HostedZone::Id instead of generic string will help to catch
errors, especially when deploying from console.
Macros and custom resources
There is no problem that couldn’t fixed with a lambda function. Cloudformation Macros
could be used to find
HostedZoneId for given
HostedZoneName and implement the similar
functionality as there is by default for Route53 RecordSets. You would have to create and register
the macro to resolve name to id before deploying the stack using it. This can be seen as both
advantage if you are building a library of support functions to extend cloudformation or disadvantage
if you would like to keep everything within single template and make sure your solution can be
Other option is to create a custom resource where name2id mapping lambda function can be included in the same SAM template with rest of your stack. While this does sound like a tempting challenge I will leave to you to solve and will not provide code in this blog post :-)
Provisioning ACM certificate is pretty fast, but validation can take time as DNS changes do
need some time to propagete. In my very limited testing it took 8min 35s to create a validated
certificate. Cloudformation will not consider certificate ready before it has been validated
ValidationOptions is defined. If you have many resources pending on certificate
it would be a good idea to keep an eye on stack timeout value and increase it as needed.
As I believe trying things out is really the way to find out all these little details, so here is a minimal template for you to build an ALB with HTTPS listener on port 443 with fixed response, i.e. no backends. To deploy this you will only need a Route53 hosted zone and a VPC with minimum of 2 subnets. If everything works as planned, you should have a valid SSL certificate and hello-world response at URL found in stack output.