AWS ACM Terraform Module with Variable SANs

Here is a a flexible terraform module for creating an AWS ACM with a variable number of additional SANs.

Our infrastructure architecture is such that we have application load balancers that may serve multiple apps, and there was a need to create SSL certificates with multiple SANs to support those apps. While possible to add multiple SSL certificates to an application load balancer there is in fact a limit and so I wanted to avoid that altogether. There may be argument that we are exposing data unecessarily by including what seem to be unrelated domains in the SSL cert but for a QA or staging environment it felt less critical or a concern. At any rate, this solved a problem and reduced the total number of certificates we need to create for the various environments.

GitHub Repo

This module will create the appropriate Route53 validation records for all of the defined domains and subject alternative names. The module outputs the ARN of the ACM it creates.

The important bit is in locals in main.tf. From the variables domain_name and subject_alternative_names we create a map of domains, distinct zones, certificate sans, and all certificate validation records that need to be created.

# Create a data source for each distinct domain zone found
  data "aws_route53_zone" "domain" {
    count = length(local.distinct_zones)
  
    name         = local.distinct_zones[count.index]
    private_zone = false
  }

# Define local data resources
locals {
  all_domains = concat([var.domain_name.domain], [
    for v in var.subject_alternative_names : v.domain
  ])
  
  all_zones = concat([var.domain_name.zone], [
    for v in var.subject_alternative_names : v.zone
  ])

  distinct_zones      = distinct(local.all_zones)
  zone_name_to_id_map = zipmap(local.distinct_zones, data.aws_route53_zone.domain[*].zone_id)
  domain_to_zone_map  = zipmap(local.all_domains, local.all_zones)

  cert_san = reverse(sort([
    for v in var.subject_alternative_names : v.domain
  ]))

  cert_validation_domains = [
    for v in aws_acm_certificate.certificate.domain_validation_options : tomap(v)
  ]
}

The rest of the module is very simple.


# Request a certificate from ACM
resource "aws_acm_certificate" "certificate"{
  domain_name               = var.domain_name.domain
  subject_alternative_names = local.cert_san
  validation_method         = "DNS"

  tags = var.tags

  lifecycle {
    create_before_destroy = true
  }
}

# Create route53 records for each validation record discovered
resource "aws_route53_record" "validation_records" {
  count = length(distinct(local.all_domains))

  zone_id = lookup(local.zone_name_to_id_map, lookup(local.domain_to_zone_map, local.cert_validation_domains[count.index]["domain_name"]))
  name    = local.cert_validation_domains[count.index]["resource_record_name"]
  type    = local.cert_validation_domains[count.index]["resource_record_type"]
  ttl     = 60

  allow_overwrite = true

  records = [
    local.cert_validation_domains[count.index]["resource_record_value"]
  ]
}

# This resource represents a successful validation of an ACM certificate in concert with other resources.
resource "aws_acm_certificate_validation" "cert_validation" {
  count = 1

  certificate_arn         = aws_acm_certificate.certificate.arn
  validation_record_fqdns = local.cert_validation_domains[*]["resource_record_name"]
}

Example usage to create an ACM with the primary domain and a wildcard domain.

module "ssl_cert_default" {
  source = "../modules/multi-domain-acm/"

  domain_name = {
    zone   = "roylindauer.com"
    domain = "roylindauer.com"
  }

  subject_alternative_names = [{ "zone" : "roylindauer.com", "domain" : "*.roylindauer.com" }]

  tags = { 
    "Environment" = "prod",
    "Description" = "Managed by Terraform",
    "Creator" = "Terraform",
    "Name" = "Prod Cluster - Roycom ACM"
  }
}

Here's an example for creating an SSL certificate with two distinct domain names and zones. For example, for an application load balancer serving multiple apps in a qa or test environment.

module "ssl_cert_develop_alb" {
  source = "./modules/multi-domain-acm/"

  domain_name = {
    zone   = "roylindauer.com"
    domain = "*.develop.roylindauer.com"
  }

  subject_alternative_names = [
    {
      "zone" : "roylindauer.art",
      "domain" : "*.develop.roylindauer.art"
    }
  ]

  tags = { 
    "Environment" = "develop",
    "Description" = "Managed by Terraform",
    "Creator" = "Terraform",
    "Name" = "Develop Cluster - Default ACM"
  }
}