azavea / terraform-aws-acm-certificate

A Terraform module to create an Amazon Certificate Manager (ACM) certificate with Route 53 DNS validation.
Apache License 2.0
47 stars 41 forks source link

Multiple domain in one certificate #3

Open noizo opened 6 years ago

noizo commented 6 years ago

Hi. Great job You've done with that module. I found an issue.

If i'm trying to provision certificate with multiple domain names:

data "aws_route53_zone" "external" {
  name = "example.com"
}

module "cert" {
  source                    = "github.com/azavea/terraform-aws-acm-certificate?ref=0.1.0"
  domain_name               = "*.example.com"
  subject_alternative_names = ["*.example.io"]
  hosted_zone_id            = "${data.aws_route53_zone.external.zone_id}"
  validation_record_ttl     = "60"
}

or subject_alternative_names = ["*.example.io", "*.example.net"]

Terraform cant properly interpolate zone_id for each domain. Module trying to write dns verification only for domain, listed in aws_route53_zone" It uses same zone_id for different domain name. And ends up with an error.

* module.cert.aws_route53_record.validation[1]: 1 error(s) occurred:

* aws_route53_record.validation.1: [ERR]: Error building changeset: InvalidChangeBatch: FATAL problem: DomainLabelEmpty (Domain label is empty) encountered with '_f8a5abb93431b2ffa4a52f601bd3189f.example.io..example.com'
    status code: 400, request id: 06c85bcd-90e3-11e8-9267-c1e76a4a292e

Terraform does not automatically rollback in the face of errors.
Instead, your Terraform state file has been partially updated with
any resources that successfully completed. Please address the error
above and apply again to incrementally change your infrastructure.

Besides that it gives an error, provisioning finishes just fine, correct entries are being added to dns verification records in each domain.

hectcastro commented 6 years ago

Hey @noizo, thanks for using it!

I don't think we ever considered mixing TLDs in our initial design, but I wonder if there is a way to support this by using the aws_route53_zone data source within the module. We'll take a closer look in the next two weeks to see what we can find.

captn3m0 commented 5 years ago

Got hit by this as well, and we couldn't find a clean way to pass the mapping between domains used in subject_alternative_names and the corresponding hosted_zone_ids.

A possible hack is to make both of them a list, and have an extra first/last element for the primary domain, but it wouldn't look nice (since you'll have to pass the same Zone ID multiple times if you repeat a zone, which is very likely).

I was wondering if there was an easy way (hopefully within terraform) to convert a domain to a root domain? So for eg:

module "cert" {
  source                    = "github.com/azavea/terraform-aws-acm-certificate?ref=0.1.0"
  domain_name               = "*.example.com"
  subject_alternative_names = ["*.example.io"]
  validation_record_ttl     = "60"
}
// Inside the module

resource "aws_route53_record" "validation" {
  provider = "aws.route53_account"
  count    = "${length(var.subject_alternative_names) + 1}"

  name    = "${lookup(aws_acm_certificate.default.domain_validation_options[count.index], "resource_record_name")}"
  type    = "${lookup(aws_acm_certificate.default.domain_validation_options[count.index], "resource_record_type")}"
  zone_id = "${data.aws_route53_zone.selected[count.index].id}"
  records = ["${lookup(aws_acm_certificate.default.domain_validation_options[count.index], "resource_record_value")}"]
  ttl     = "${var.validation_record_ttl}"
}

data "aws_route53_zone" "selected" {
  count = "${length(var.subject_alternative_names) + 1}"
  name         = "${magic_root_domain(lookup(aws_acm_certificate.default.domain_validation_options[count.index], "resource_record_name"))}"
  private_zone = false
}

Even magic_root_domain might not suffice, since you can have a subdomain mapped to a hosted zone. Ideally, what we want is a lookup function that gives us a valid hosted zone for a given domain. The route53_zone data source only takes a name parameter.

bartvollebregt commented 5 years ago

I'm having the same issue. The problem with @captn3m0 's solution is that domain_validation_options has a max of 4. So if you have more than 4 domains you'll get the error below:

module.test123.aws_route53_record.verification_records[4]: index 4 out of range for list aws_acm_certificate.certificate.domain_validation_options (max 4)

So far I'm not able to figure out a way do multi-domain verifications...

captn3m0 commented 5 years ago

The official limit is 10 (default) and 100 (extended): https://docs.aws.amazon.com/acm/latest/userguide/acm-limits.html

Is the 4 limitation from the provider?

richerve commented 5 years ago

The problem is that after the acm certificate is created, it's no longer possible to add new SANs to the same cert. This restriction happens on the AWS side.

@bartvollebregt That's why you're seeing that max is four, because this was the amount of domains added on creation, so trying to add a new one will fail. In our case the error said "max of 3", for example.

The solution is to create a new cert with the additional SANs added.

chancez commented 4 years ago

I think I came up with a solution to this, though it isn't exactly my favorite. I'm still testing it but here's a gist of what it looks like:

https://gist.github.com/chancez/dfaaf799b98698839d65ebba55db7d44

chancez commented 4 years ago

I made some updates to the gist (just look at the history). A few paint points I hit I'll explain here: A huge challenge was just that the validations option list has duplicate resource records because *.example.org and example.org produce the same one, so filtering that down was one piece. Also related to that, is the mapping of the validation options to the SANs, as you cannot use the validation options in the loop for creating the aws_route53_records, because it's not known at plan time. Finally, I had to add support for skipping DNS validation of some records because the resource records already exist from previous ACM certs and validations done previously.