Skip to content

Instantly share code, notes, and snippets.

@chancez
Last active November 20, 2023 21:56
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save chancez/dfaaf799b98698839d65ebba55db7d44 to your computer and use it in GitHub Desktop.
Save chancez/dfaaf799b98698839d65ebba55db7d44 to your computer and use it in GitHub Desktop.
Support creating a DNS validated ACM certificate containing SANs for multiple different hosted zones
module "combined_acm_certificate" {
source = "../../modules/acm_certificate_dns_validated_multi_zone"
domain_name = "infra.example.com"
zone_to_san = {
"infra.example.com" = [
"*.infra.example.com",
"*.dev.infra.example.com",
"*.staging.infra.example.com",
"*.production.infra.example.com",
]
"foo.test.com" = [
"*.foo.test.com"
]
"foo-dev.test.com" = [
"*.foo-dev.test.com"
]
}
}
provider "aws" {
alias = "certificate_requester"
}
provider "aws" {
alias = "route53_cert_validator"
}
locals {
# produces a list of maps of san to zone
# [ { "*.foo.example.org" = "foo.example.org"} ]
list_of_sans_to_zone = [
for zone, sans in var.zone_to_san : {
for san in sans :
san => zone
}
]
# produces a map of SAN => zone
# { "*.foo.example.org" = "foo.example.org"}
san_to_zone = {
for san, zone in merge(flatten([local.list_of_sans_to_zone])...) :
san => zone
}
# A list of just the SANs. Sorted to ensure stability if the map order
# changes.
sans = sort(keys(local.san_to_zone))
san_to_zone_final = {
for san, zone in merge({ (var.domain_name) = var.domain_name }, local.san_to_zone) :
san => zone
# Skip validating anything specified in skip_validations
if ! contains(var.skip_validations, san)
# if the san without the "*." wildcard exists in the list of SANs or is
# the same as the domain_name, then they will have the same ACM validation
# record.
# See
# https:#docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html
# for details and examples of how *.example.com and example.com both have
# the same CNAME validation record.
&& ! contains(local.sans, trimprefix(san, "*."))
# if the SAN isnt var.domain name, then check if it's a wildcard of the
# domain_name
&& (san == var.domain_name || trimprefix(san, "*.") != var.domain_name)
}
# *.foo.example.com => {...}
validations = {
for validation_option in aws_acm_certificate.cert.domain_validation_options :
validation_option.domain_name => validation_option
}
}
# Keyed by SAN, allowing lookup of a zone_id by the SAN used
data "aws_route53_zone" "selected" {
provider = aws.route53_cert_validator
# Also get domain_name
for_each = merge({ (var.domain_name) = var.domain_name }, local.san_to_zone)
name = each.value
}
resource "aws_acm_certificate" "cert" {
provider = aws.certificate_requester
domain_name = var.domain_name
subject_alternative_names = local.sans
validation_method = "DNS"
tags = var.tags
lifecycle {
create_before_destroy = true
}
}
resource "aws_acm_certificate_validation" "cert" {
provider = aws.certificate_requester
certificate_arn = aws_acm_certificate.cert.arn
# We use an explicit dependency and pass this directly from the
# domain_validation_options instead of using the fqdn from
# aws_route53_record.cert_validation because we may be skipping the creation
# of some route53 records, and we need to provide all validation FQDNs, even
# if we do not create them.
validation_record_fqdns = aws_acm_certificate.cert.domain_validation_options[*].resource_record_name
depends_on = [
aws_route53_record.cert_validation
]
}
resource "aws_route53_record" "cert_validation" {
provider = aws.route53_cert_validator
for_each = local.san_to_zone_final
zone_id = data.aws_route53_zone.selected[each.key].zone_id
name = local.validations[each.key].resource_record_name
type = local.validations[each.key].resource_record_type
records = [local.validations[each.key].resource_record_value]
ttl = 60
}
variable "domain_name" {
type = string
description = "A domain name for which the certificate should be issued."
}
variable "zone_to_san" {
type = map(list(string))
description = "A mapping of hosted zone name to SANs."
}
variable "skip_validations" {
type = list(string)
description = "A list of SANs to skip validation for. For when validations already exist for the SAN. Include both the wildcard and base domain of the wildcard if your SANs includes both."
default = []
}
variable "tags" {
type = map(string)
default = {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment