Skip to content
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

feat: certificate validation with multiple Route 53 zones #91

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/complete-dns-validation/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ output "validation_route53_record_fqdns" {
}

output "distinct_domain_names" {
description = "List of distinct domains names used for the validation."
description = "List of distinct domains names used for validation. It does not include the certificate distinct domain names that were not mapped to a hosted zone."
value = module.acm.distinct_domain_names
}

output "validation_domains" {
description = "List of distinct domain validation options. This is useful if subject alternative names contain wildcards."
description = "List of distinct domain validation options. This is useful if subject alternative names contain wildcards. It does not include the domain validation options for the certificate distinct domain names that were not mapped to a hosted zone."
value = module.acm.validation_domains
}
60 changes: 60 additions & 0 deletions examples/multiple-zones-dns-validation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# ACM example with multiple Route53 DNS validation

Configuration in this directory creates three Route53 zones (one of them delegates a subdomain to another zone from the set) and one ACM certificate (valid for the three domain names, their wildcards and various example subdomains of them).

Also, ACM certificate is being validate using DNS method.

## Usage

To run this example you need to execute:

```bash
$ terraform init
$ terraform plan
$ terraform apply
```

Note that this example may create resources which cost money. Run `terraform destroy` when you don't need these resources.

<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 0.12.26 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 2.53 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 2.53 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_acm"></a> [acm](#module\_acm) | ../../ | |

## Resources

| Name | Type |
|------|------|
| [aws_route53_zone.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_zone) | resource |
| [aws_route53_zone.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source |

## Inputs

No inputs.

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_acm_certificate_arn"></a> [acm\_certificate\_arn](#output\_acm\_certificate\_arn) | The ARN of the certificate |
| <a name="output_acm_certificate_domain_validation_options"></a> [acm\_certificate\_domain\_validation\_options](#output\_acm\_certificate\_domain\_validation\_options) | A list of attributes to feed into other resources to complete certificate validation. Can have more than one element, e.g. if SANs are defined. Only set if DNS-validation was used. |
| <a name="output_acm_certificate_validation_emails"></a> [acm\_certificate\_validation\_emails](#output\_acm\_certificate\_validation\_emails) | A list of addresses that received a validation E-Mail. Only set if EMAIL-validation was used. |
| <a name="output_distinct_domain_names"></a> [distinct\_domain\_names](#output\_distinct\_domain\_names) | List of distinct domains names used for the validation. |
| <a name="output_validation_domains"></a> [validation\_domains](#output\_validation\_domains) | List of distinct domain validation options. This is useful if subject alternative names contain wildcards. |
| <a name="output_validation_route53_record_fqdns"></a> [validation\_route53\_record\_fqdns](#output\_validation\_route53\_record\_fqdns) | List of FQDNs built using the zone domain and name. |
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
96 changes: 96 additions & 0 deletions examples/multiple-zones-dns-validation/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
locals {
# Use existing (via data source) or create new zones (will fail validation, if zone is not reachable)
use_existing_route53_zones = true

domain = "terraform-aws-modules.modules.tf"
alternate_domain = "terraform-aws-modules.modules-alt.tf"
alternate_subdomain = "sub.${local.alternate_domain}"

# Removing trailing dot from domains - just to be sure :)
domain_name = trimsuffix(local.domain, ".")
alternate_domain_name = trimsuffix(local.alternate_domain, ".")
alternate_subdomain_name = trimsuffix(local.alternate_subdomain, ".")

domain_zone_id = coalescelist(data.aws_route53_zone.domain.*.zone_id, aws_route53_zone.domain.*.zone_id)[0]
alternate_domain_zone_id = coalescelist(data.aws_route53_zone.alternate_domain.*.zone_id, aws_route53_zone.alternate_domain.*.zone_id)[0]
alternate_subdomain_zone_id = coalescelist(data.aws_route53_zone.alternate_subdomain.*.zone_id, aws_route53_zone.alternate_subdomain.*.zone_id)[0]
}

data "aws_route53_zone" "domain" {
count = local.use_existing_route53_zones ? 1 : 0

name = local.domain_name
private_zone = false
}

data "aws_route53_zone" "alternate_domain" {
count = local.use_existing_route53_zones ? 1 : 0

name = local.alternate_domain_name
private_zone = false
}

data "aws_route53_zone" "alternate_subdomain" {
count = local.use_existing_route53_zones ? 1 : 0

name = local.alternate_subdomain_name
private_zone = false
}

resource "aws_route53_zone" "domain" {
count = !local.use_existing_route53_zones ? 1 : 0
name = local.domain_name
}

resource "aws_route53_zone" "alternate_domain" {
count = !local.use_existing_route53_zones ? 1 : 0
name = local.alternate_domain_name
}

resource "aws_route53_zone" "alternate_subdomain" {
count = !local.use_existing_route53_zones ? 1 : 0
name = local.alternate_subdomain_name
}

resource "aws_route53_record" "alternate_subdomain_delegation" {
count = !local.use_existing_route53_zones ? 1 : 0
name = local.alternate_subdomain_name
type = "NS"
ttl = 300
records = aws_route53_zone.alternate_subdomain.*.name_servers[0]
zone_id = aws_route53_zone.alternate_domain.*.zone_id[0]
}

module "acm" {
source = "../../"

domain_name = local.domain_name

subject_alternative_names = [
"*.alerts.${local.domain_name}",
"new.sub.${local.domain_name}",
"*.${local.domain_name}",
"alerts.${local.domain_name}",
local.alternate_domain_name,
"*.${local.alternate_domain_name}",
"quite.deep.abc.${local.alternate_subdomain_name}",
"def.${local.alternate_subdomain_name}",
"*.${local.alternate_subdomain_name}",
]

domain_zones = {
(local.domain_name) = { zone_id = local.domain_zone_id }
"alerts.${local.domain_name}" = { zone_id = local.domain_zone_id }
"new.sub.${local.domain_name}" = { zone_id = local.domain_zone_id }
(local.alternate_domain_name) = { zone_id = local.alternate_domain_zone_id }
(local.alternate_subdomain_name) = { zone_id = local.alternate_subdomain_zone_id }
"def.${local.alternate_subdomain_name}" = { zone_id = local.alternate_subdomain_zone_id }
"quite.deep.abc.${local.alternate_subdomain_name}" = { zone_id = local.alternate_subdomain_zone_id }
}

wait_for_validation = true

tags = {
Name = local.domain_name
}
}
29 changes: 29 additions & 0 deletions examples/multiple-zones-dns-validation/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
output "acm_certificate_arn" {
description = "The ARN of the certificate"
value = module.acm.acm_certificate_arn
}

output "acm_certificate_domain_validation_options" {
description = "A list of attributes to feed into other resources to complete certificate validation. Can have more than one element, e.g. if SANs are defined. Only set if DNS-validation was used."
value = module.acm.acm_certificate_domain_validation_options
}

output "acm_certificate_validation_emails" {
description = "A list of addresses that received a validation E-Mail. Only set if EMAIL-validation was used."
value = module.acm.acm_certificate_validation_emails
}

output "validation_route53_record_fqdns" {
description = "List of FQDNs built using the zone domain and name."
value = module.acm.validation_route53_record_fqdns
}

output "distinct_domain_names" {
description = "List of distinct domains names used for validation. It does not include the certificate distinct domain names that were not mapped to a hosted zone."
value = module.acm.distinct_domain_names
}

output "validation_domains" {
description = "List of distinct domain validation options. This is useful if subject alternative names contain wildcards. It does not include the domain validation options for the certificate distinct domain names that were not mapped to a hosted zone."
value = module.acm.validation_domains
}
Empty file.
7 changes: 7 additions & 0 deletions examples/multiple-zones-dns-validation/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
terraform {
required_version = ">= 0.12.26"

required_providers {
aws = ">= 2.53"
}
}
21 changes: 14 additions & 7 deletions main.tf
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
locals {
# Get distinct list of domains and SANs
# Get distinct list of domains and SANs that can be mapped to a zone
distinct_domain_names = distinct(
[for s in concat([var.domain_name], var.subject_alternative_names) : replace(s, "*.", "")]
[for s in concat([var.domain_name], var.subject_alternative_names) : replace(s, "*.", "")
if length(var.domain_zones) == 0 || contains(keys(var.domain_zones), s)
]
)

# Get the list of distinct domain_validation_options, with wildcard
# domain names replaced by the domain name
# domain names replaced by the domain name. Validation records will be
# created from this list.
validation_domains = var.create_certificate ? distinct(
[for k, v in aws_acm_certificate.this[0].domain_validation_options : merge(
tomap(v), { domain_name = replace(v.domain_name, "*.", "") }
)]
[for k, v in aws_acm_certificate.this[0].domain_validation_options : tomap(merge(
v, { domain_name = replace(v.domain_name, "*.", "") }
)) if contains(local.distinct_domain_names, replace(v.domain_name, "*.", ""))]
) : []
}

Expand All @@ -34,7 +37,11 @@ resource "aws_acm_certificate" "this" {
resource "aws_route53_record" "validation" {
count = var.create_certificate && var.validation_method == "DNS" && var.validate_certificate ? length(local.distinct_domain_names) : 0

zone_id = var.zone_id
zone_id = (
length(var.domain_zones) == 0
? var.zone_id
: var.domain_zones[element(local.validation_domains, count.index)["domain_name"]]["zone_id"]
)
name = element(local.validation_domains, count.index)["resource_record_name"]
type = element(local.validation_domains, count.index)["resource_record_type"]
ttl = var.dns_ttl
Expand Down
4 changes: 2 additions & 2 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ output "validation_route53_record_fqdns" {
}

output "distinct_domain_names" {
description = "List of distinct domains names used for the validation."
description = "List of distinct domains names used for validation. It does not include the certificate distinct domain names that were not mapped to a hosted zone."
value = local.distinct_domain_names
}

output "validation_domains" {
description = "List of distinct domain validation options. This is useful if subject alternative names contain wildcards."
description = "List of distinct domain validation options. This is useful if subject alternative names contain wildcards. It does not include the domain validation options for the certificate distinct domain names that were not mapped to a hosted zone."
value = local.validation_domains
}
12 changes: 9 additions & 3 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ variable "create_certificate" {
}

variable "validate_certificate" {
description = "Whether to validate certificate by creating Route53 record"
description = "Whether to validate certificate by creating Route53 record(s)"
type = bool
default = true
}
Expand Down Expand Up @@ -47,19 +47,25 @@ variable "validation_method" {
}

variable "zone_id" {
description = "The ID of the hosted zone to contain this record."
description = "The ID of the hosted zone to contain the validation record(s)."
type = string
default = ""
}

variable "domain_zones" {
description = "A mapping of distinct domain names to Route 53 zone details."
type = map(any)
default = {}
}

variable "tags" {
description = "A mapping of tags to assign to the resource"
type = map(string)
default = {}
}

variable "dns_ttl" {
description = "The TTL of DNS recursive resolvers to cache information about this record."
description = "The TTL of DNS recursive resolvers to cache information about the validation record(s)."
type = number
default = 60
}