Skip to content

aws_acm_certificate_validation - add simple example of an ACM validation with a wildcard alternate name #16913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
shorn opened this issue Dec 28, 2020 · 5 comments
Labels
documentation Introduces or discusses updates to documentation. enhancement Requests to existing resources that expand the functionality or scope. service/acm Issues and PRs that pertain to the acm service. service/route53 Issues and PRs that pertain to the route53 service.

Comments

@shorn
Copy link

shorn commented Dec 28, 2020

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
  • Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment

Description

TL:DR; please add a simple example of how to create ACM certificate validation with a wildcard alternate name.

Motivation

Cloudfront only supports one ACM certificate per distribution, as per: https://aws.amazon.com/premiumsupport/knowledge-center/associate-ssl-certificates-cloudfront/

That means, in order to use a HTTPS/SSL protected CF distribution with multiple domains, you need a cert that is valid for both the root domain (e.g. example.com) and the wildcard domains (.e.g. *.example.com).

Note that the simple and obvious for_each solution from the current documentation doesn't work out of the box for a certificate with a wildcard SAN. AWS generates two separate validation options for the root and wildcard with exactly the same CNAME. The example from the documentation will then then fail when TF tries to create the aws_route53_record because of the duplicate CNAME values.

Problem

Because of changes and problems with cert validation over Terraform's history, it's difficult to find a simple example of how to use TF for this fairly basic requirement - there's a bunch of old/stale information out there going all the way back to doing stuff like calling flatten() with string interpolation from back when 0.11 was current.

By "changes and problems" - I'm talking about the "instability of domain_validation_options" when AWS messed up and changed the implementation so the options were listed in unstable order for a while (causing TF to want to re-create certificates unnecessarily). And then there was the change-over from list to set in v3.x of the provider. All these issues have left a whole lot of "maybe try this" solutions for old versions of TF littered around the place (Github issue repositories, stack overflow, etc.)

Proposed solution

It would be valuable to the community if there was a simple example of how to use TF to achieve this in the documentation.

If there is no way to do this with Terraform currently - it would be helpful to call that explicitly out for users in the doco.
Since this is the only way to use a single Cloudfront distribution with wildcards, it's intuitive to expect it would be simple to do it with TF.

Please note this is not intended to be a criticism of the TF API in this area - the whole API on the AWS-side seems weird and janky to me (returning duplicate CNAMEs, for example). But if TF does have a solution for this, I think it would save both users and TF developers a bunch of time and frustration if the solution were documented as an example.

Sorry about the length of this; pretty sure the actual example, if it exists, would be shorter than this description. Think of the effort I took to write all this as evidence of how frustrating this is.

New or Affected Resource(s)

  • aws_acm_certificate
  • aws_route53_record
  • aws_acm_certificate_validation

Potential Terraform Configuration

Sorry, I have no idea. I can't figure out what it should look like or find a simple example - that's why I think one should be added.

My current approach is trying to learn the new (*) for_each syntax to see if I can somehow filter out one of the duplicate domain_validation_options.

(*) new to me anyway - I've hit this whole issue while trying to upgrade from TF v0.12 with AWS provider 2.x - incredibly frustrating and kind of worrying since I mangled the route53 records for my previous certificate.

Edit:
I figured it out for my use-case. Here's an article I wrote about managing multiple wild-carded domains, the example could easily be further reduced and added to the doco: https://kopi.cloud/blog/2021/terraform-aws_acm_certificate-wildcards/

References

Here's a random example I found that seems relevant, though I don't know if it works or not.
azavea/terraform-aws-acm-certificate#3

@shorn shorn added the enhancement Requests to existing resources that expand the functionality or scope. label Dec 28, 2020
@ghost ghost added service/acm Issues and PRs that pertain to the acm service. service/route53 Issues and PRs that pertain to the route53 service. labels Dec 28, 2020
@github-actions github-actions bot added the needs-triage Waiting for first response or review from a maintainer. label Dec 28, 2020
@renehernandez
Copy link

renehernandez commented Apr 26, 2021

I went through this today and I ended up with a solution that doesn't require declaring a local variable in advance.

Instead, it leverages the fact that AWS ACM will generate the same validation data for both *.example.com and example.com by filtering the map of values to exclude any non-wildcard SAN.

resource "cloudflare_record" "certificate_validation" {
  for_each = {
    for dvo in aws_acm_certificate.example.domain_validation_options : dvo.domain_name => {
      name    = dvo.resource_record_name
      record  = dvo.resource_record_value
      type    = dvo.resource_record_type
    }
   # Skips the domain if it doesn't contain a wildcard
    if length(regexall("\\*\\..+", dvo.domain_name)) > 0
  }

  zone_id = cloudflare_zone.example.id
  name    = each.value.name
  value = each.value.record
  type    = each.value.type
  ttl     = 1
  proxied = false
}

Notes

  • This works in cases where the list of SANs contains the wildcard domains (only or alongside the domain), e.g [example.com, *.example.com, *.docs.example.com]. This won't work if you have a SANs list as follows [example.com, help.example.com, *.help.example.com]. In this case, the only record validation generated would be for *.help.example.com
  • Although, the example is generating a cloudflare record, it can be adapted to the generate route53 record
  • The if condition can be improved once something like add support for includes function terraform#27518 is merged

@breathingdust breathingdust added documentation Introduces or discusses updates to documentation. and removed needs-triage Waiting for first response or review from a maintainer. labels Sep 8, 2021
@Symbianx
Copy link
Contributor

@renehernandez's was a nice solution, I was able to overcome the first limitation by checking if the wildcard domain exists in the certificate instead of checking if *. is part of the string:

resource "aws_route53_record" "default" {
  for_each = {
  for dvo in aws_acm_certificate.default.domain_validation_options : dvo.domain_name => {
    name   = dvo.resource_record_name
    record = dvo.resource_record_value
    type   = dvo.resource_record_type
  }
  // Skips the validation record if the certificate contains a wildcard for the same domain. Needed because AWS returns the same validation records for the wildcard domain.
  if contains(concat([aws_acm_certificate.default.domain_name], tolist(aws_acm_certificate.default.subject_alternative_names)), "*.${dvo.domain_name}") == false
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.acm[each.key].zone_id
}

Arguably, the whole contains and concat part is not the most readable.

@FlorinOprina
Copy link

My solution, using locals:


locals {
  zone_map = [
    { name : "example.com",
    zone_id : "Z1230000" },
    { name : "example.org",
    zone_id : "Z4560000" },
    { name : "example.co.uk",
    zone_id : "Z7890000" }
  ]
}

resource "aws_acm_certificate" "certificate" {
  domain_name       = "example.com"
  validation_method = "DNS"
  subject_alternative_names = [
    "example.org",
    "example.co.uk"
  ]
}

resource "aws_route53_record" "certificate_records" {
  for_each = {
    for dvo in aws_acm_certificate.certificate.domain_validation_options : dvo.domain_name => {
      name    = dvo.resource_record_name
      record  = dvo.resource_record_value
      type    = dvo.resource_record_type
      zone_id = [for item in local.zone_map : item.zone_id if item.name == dvo.domain_name][0]
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = each.value.zone_id
}

resource "aws_acm_certificate_validation" "certificate_validation" {
  certificate_arn         = aws_acm_certificate.certificate.arn
  validation_record_fqdns = [for record in aws_route53_record.certificate_records : record.fqdn]
}

@psypuff
Copy link

psypuff commented Jan 21, 2024

Came up with a simpler workaround, using dvo.resource_record_name as the for_each key instead of dvo.domain_name:

resource "aws_route53_record" "example" {
  for_each = {
    for dvo in aws_acm_certificate.example.domain_validation_options : dvo.resource_record_name => {
      record  = dvo.resource_record_value
      type    = dvo.resource_record_type
    }...
  }

  allow_overwrite = true
  name            = each.key
  records         = [each.value[0].record]
  ttl             = 60
  type            = each.value[0].type
  zone_id         = aws_route53_zone.example.zone_id
}

Since the ellipsis (...) groups by the key it requires that extra index when referencing the map values (each.value[0]). I'm sure there's a way to overcome it but didn't want to over-complicate the solution.

@dcousens
Copy link

dcousens commented Jul 9, 2024

Alternatively, you can put

if !startswith(dvo.domain_name, "*")

In the for_each block, like the following

resource "aws_route53_record" "example" {
  for_each = {
    for dvo in aws_acm_certificate.example.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
    if !startswith(dvo.domain_name, "*")
  }

  name    = each.value.name
  records = [each.value.record]
  ttl     = 60
  type    = each.value.type
  zone_id = aws_route53_zone.example.zone_id
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Introduces or discusses updates to documentation. enhancement Requests to existing resources that expand the functionality or scope. service/acm Issues and PRs that pertain to the acm service. service/route53 Issues and PRs that pertain to the route53 service.
Projects
None yet
Development

No branches or pull requests

7 participants