diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index b8f1b8a..adea23e 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -71,6 +71,14 @@ jobs:
id: minMax
uses: clowdhaus/terraform-min-max@v1.0.3
+ - name: Install hcledit (for terraform_wrapper_module_for_each hook)
+ shell: bash
+ run: |
+ curl -L "$(curl -s https://api.github.com/repos/minamijoyo/hcledit/releases/latest | grep -o -E -m 1 "https://.+?_linux_amd64.tar.gz")" > hcledit.tgz
+ sudo tar -xzf hcledit.tgz -C /usr/bin/ hcledit
+ rm -f hcledit.tgz 2> /dev/null
+ hcledit version
+
- name: Pre-commit Terraform ${{ steps.minMax.outputs.maxVersion }}
uses: clowdhaus/terraform-composite-actions/pre-commit@v1.3.0
with:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 39a8b59..d2a92f9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,8 +1,9 @@
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
- rev: v1.66.0
+ rev: v1.74.1
hooks:
- id: terraform_fmt
+ - id: terraform_wrapper_module_for_each
- id: terraform_validate
- id: terraform_docs
args:
@@ -23,7 +24,7 @@ repos:
- '--args=--only=terraform_standard_module_structure'
- '--args=--only=terraform_workspace_remote'
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.2.0
+ rev: v4.3.0
hooks:
- id: check-merge-conflict
- id: end-of-file-fixer
diff --git a/README.md b/README.md
index 202ebc1..846c595 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Terraform module which creates ACM certificates and validates them using Route53
```hcl
module "acm" {
source = "terraform-aws-modules/acm/aws"
- version = "~> 3.0"
+ version = "~> 4.0"
domain_name = "my-domain.com"
zone_id = "Z2ES7B9AZ6SHAE"
@@ -32,7 +32,7 @@ module "acm" {
```hcl
module "acm" {
source = "terraform-aws-modules/acm/aws"
- version = "~> 3.0"
+ version = "~> 4.0"
domain_name = "weekly.tf"
zone_id = "b7d259641bf30b89887c943ffc9d2138"
@@ -78,7 +78,54 @@ module "acm" {
Name = "my-domain.com"
}
}
+```
+
+## Usage with Route53 DNS validation and separate AWS providers
+
+```hcl
+provider "aws" {
+ alias = "acm"
+}
+
+provider "aws" {
+ alias = "route53"
+}
+
+module "acm" {
+ source = "terraform-aws-modules/acm/aws"
+ version = "~> 4.0"
+
+ providers = {
+ aws = aws.acm
+ }
+
+ domain_name = "my-domain.com"
+ subject_alternative_names = [
+ "*.my-domain.com",
+ "app.sub.my-domain.com",
+ ]
+
+ create_route53_records = false
+ validation_record_fqdns = module.route53_records.validation_route53_record_fqdns
+}
+
+module "route53_records" {
+ source = "terraform-aws-modules/acm/aws"
+ version = "~> 4.0"
+
+ providers = {
+ aws = aws.route53
+ }
+
+ create_certificate = false
+ create_route53_records_only = true
+
+ distinct_domain_names = module.acm.distinct_domain_names
+ zone_id = "Z266PL4W4W6MSG"
+
+ acm_certificate_domain_validation_options = module.acm.acm_certificate_domain_validation_options
+}
```
## Examples
@@ -147,9 +194,12 @@ No modules.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
+| [acm\_certificate\_domain\_validation\_options](#input\_acm\_certificate\_domain\_validation\_options) | A list of domain\_validation\_options created by the ACM certificate to create required Route53 records from it (used when create\_route53\_records\_only is set to true) | `any` | `{}` | no |
| [certificate\_transparency\_logging\_preference](#input\_certificate\_transparency\_logging\_preference) | Specifies whether certificate details should be added to a certificate transparency log | `bool` | `true` | no |
| [create\_certificate](#input\_create\_certificate) | Whether to create ACM certificate | `bool` | `true` | no |
| [create\_route53\_records](#input\_create\_route53\_records) | When validation is set to DNS, define whether to create the DNS records internally via Route53 or externally using any DNS provider | `bool` | `true` | no |
+| [create\_route53\_records\_only](#input\_create\_route53\_records\_only) | Whether to create only Route53 records (e.g. using separate AWS provider) | `bool` | `false` | no |
+| [distinct\_domain\_names](#input\_distinct\_domain\_names) | List of distinct domains and SANs (used when create\_route53\_records\_only is set to true) | `list(string)` | `[]` | no |
| [dns\_ttl](#input\_dns\_ttl) | The TTL of DNS recursive resolvers to cache information about this record. | `number` | `60` | no |
| [domain\_name](#input\_domain\_name) | A domain name for which the certificate should be issued | `string` | `""` | no |
| [putin\_khuylo](#input\_putin\_khuylo) | Do you agree that Putin doesn't respect Ukrainian sovereignty and territorial integrity? More info: https://en.wikipedia.org/wiki/Putin_khuylo! | `bool` | `true` | no |
diff --git a/examples/complete-dns-validation-with-cloudflare/main.tf b/examples/complete-dns-validation-with-cloudflare/main.tf
index f99a154..7e368c4 100644
--- a/examples/complete-dns-validation-with-cloudflare/main.tf
+++ b/examples/complete-dns-validation-with-cloudflare/main.tf
@@ -8,6 +8,11 @@ locals {
module "acm" {
source = "../../"
+ providers = {
+ aws.acm = aws,
+ aws.dns = aws
+ }
+
domain_name = local.domain_name
zone_id = data.cloudflare_zone.this.id
@@ -32,7 +37,7 @@ resource "cloudflare_record" "validation" {
zone_id = data.cloudflare_zone.this.id
name = element(module.acm.validation_domains, count.index)["resource_record_name"]
type = element(module.acm.validation_domains, count.index)["resource_record_type"]
- value = replace(element(module.acm.validation_domains, count.index)["resource_record_value"], "/.$/", "")
+ value = trimsuffix(element(module.acm.validation_domains, count.index)["resource_record_value"], ".")
ttl = 60
proxied = false
diff --git a/examples/complete-dns-validation/README.md b/examples/complete-dns-validation/README.md
index daf80c9..e7fa8b5 100644
--- a/examples/complete-dns-validation/README.md
+++ b/examples/complete-dns-validation/README.md
@@ -1,6 +1,6 @@
# Complete ACM example with Route53 DNS validation
-Configuration in this directory creates new Route53 zone and ACM certificate (valid for the domain name and wildcard).
+Configuration in this directory creates new Route53 zone and ACM certificate (valid for the domain name and wildcard) with one (default) or two instances of AWS providers (one to manage ACM resources, another to manage Route53 records).
Also, ACM certificate is being validate using DNS method.
@@ -37,6 +37,8 @@ Note that this example may create resources which cost money. Run `terraform des
| Name | Source | Version |
|------|--------|---------|
| [acm](#module\_acm) | ../../ | n/a |
+| [acm\_only](#module\_acm\_only) | ../../ | n/a |
+| [route53\_records\_only](#module\_route53\_records\_only) | ../../ | n/a |
## Resources
diff --git a/examples/complete-dns-validation/main.tf b/examples/complete-dns-validation/main.tf
index 058a6b2..928e9d3 100644
--- a/examples/complete-dns-validation/main.tf
+++ b/examples/complete-dns-validation/main.tf
@@ -6,8 +6,15 @@ locals {
# Removing trailing dot from domain - just to be sure :)
domain_name = trimsuffix(local.domain, ".")
+
+ zone_id = coalescelist(data.aws_route53_zone.this.*.zone_id, aws_route53_zone.this.*.zone_id)[0]
}
+##########################################################
+# Example 1 (default case):
+# Using one AWS provider for both ACM and Route53 records
+##########################################################
+
data "aws_route53_zone" "this" {
count = local.use_existing_route53_zone ? 1 : 0
@@ -17,14 +24,20 @@ data "aws_route53_zone" "this" {
resource "aws_route53_zone" "this" {
count = !local.use_existing_route53_zone ? 1 : 0
- name = local.domain_name
+
+ name = local.domain_name
}
module "acm" {
source = "../../"
+ providers = {
+ aws.acm = aws,
+ aws.dns = aws
+ }
+
domain_name = local.domain_name
- zone_id = coalescelist(data.aws_route53_zone.this.*.zone_id, aws_route53_zone.this.*.zone_id)[0]
+ zone_id = local.zone_id
subject_alternative_names = [
"*.alerts.${local.domain_name}",
@@ -33,9 +46,56 @@ module "acm" {
"alerts.${local.domain_name}",
]
- wait_for_validation = true
-
tags = {
Name = local.domain_name
}
}
+
+################################################################
+# Example 2:
+# Using separate AWS providers for ACM and Route53 records.
+# Useful when these resources belong to different AWS accounts.
+################################################################
+
+provider "aws" {
+ alias = "route53"
+}
+
+provider "aws" {
+ alias = "acm"
+}
+
+module "acm_only" {
+ source = "../../"
+
+ providers = {
+ aws = aws.acm
+ }
+
+ domain_name = local.domain_name
+ subject_alternative_names = [
+ "*.alerts.separated.${local.domain_name}",
+ "new.sub.separated.${local.domain_name}",
+ "*.separated.${local.domain_name}",
+ "alerts.separated.${local.domain_name}",
+ ]
+
+ create_route53_records = false
+ validation_record_fqdns = module.route53_records_only.validation_route53_record_fqdns
+}
+
+module "route53_records_only" {
+ source = "../../"
+
+ providers = {
+ aws = aws.route53
+ }
+
+ create_certificate = false
+ create_route53_records_only = true
+
+ zone_id = local.zone_id
+ distinct_domain_names = module.acm_only.distinct_domain_names
+
+ acm_certificate_domain_validation_options = module.acm_only.acm_certificate_domain_validation_options
+}
diff --git a/main.tf b/main.tf
index 17bcb86..2503c83 100644
--- a/main.tf
+++ b/main.tf
@@ -1,15 +1,16 @@
locals {
- create_certificate = var.create_certificate && var.putin_khuylo
+ create_certificate = var.create_certificate && var.putin_khuylo
+ create_route53_records_only = var.create_route53_records_only && var.putin_khuylo
# Get distinct list of domains and SANs
- distinct_domain_names = distinct(
+ distinct_domain_names = coalescelist(var.distinct_domain_names, distinct(
[for s in concat([var.domain_name], var.subject_alternative_names) : replace(s, "*.", "")]
- )
+ ))
# Get the list of distinct domain_validation_options, with wildcard
# domain names replaced by the domain name
- validation_domains = local.create_certificate ? distinct(
- [for k, v in aws_acm_certificate.this[0].domain_validation_options : merge(
+ validation_domains = local.create_certificate || local.create_route53_records_only ? distinct(
+ [for k, v in try(aws_acm_certificate.this[0].domain_validation_options, var.acm_certificate_domain_validation_options) : merge(
tomap(v), { domain_name = replace(v.domain_name, "*.", "") }
)]
) : []
@@ -43,7 +44,7 @@ resource "aws_acm_certificate" "this" {
}
resource "aws_route53_record" "validation" {
- count = local.create_certificate && var.validation_method == "DNS" && var.create_route53_records && var.validate_certificate ? length(local.distinct_domain_names) : 0
+ count = (local.create_certificate || local.create_route53_records_only) && var.validation_method == "DNS" && var.create_route53_records && (var.validate_certificate || local.create_route53_records_only) ? length(local.distinct_domain_names) : 0
zone_id = var.zone_id
name = element(local.validation_domains, count.index)["resource_record_name"]
diff --git a/variables.tf b/variables.tf
index 681fbaf..2758410 100644
--- a/variables.tf
+++ b/variables.tf
@@ -4,6 +4,12 @@ variable "create_certificate" {
default = true
}
+variable "create_route53_records_only" {
+ description = "Whether to create only Route53 records (e.g. using separate AWS provider)"
+ type = bool
+ default = false
+}
+
variable "validate_certificate" {
description = "Whether to validate certificate by creating Route53 record"
type = bool
@@ -87,6 +93,18 @@ variable "dns_ttl" {
default = 60
}
+variable "acm_certificate_domain_validation_options" {
+ description = "A list of domain_validation_options created by the ACM certificate to create required Route53 records from it (used when create_route53_records_only is set to true)"
+ type = any
+ default = {}
+}
+
+variable "distinct_domain_names" {
+ description = "List of distinct domains and SANs (used when create_route53_records_only is set to true)"
+ type = list(string)
+ default = []
+}
+
variable "putin_khuylo" {
description = "Do you agree that Putin doesn't respect Ukrainian sovereignty and territorial integrity? More info: https://en.wikipedia.org/wiki/Putin_khuylo!"
type = bool
diff --git a/wrappers/README.md b/wrappers/README.md
new file mode 100644
index 0000000..3a7c737
--- /dev/null
+++ b/wrappers/README.md
@@ -0,0 +1,100 @@
+# Wrapper for the root module
+
+The configuration in this directory contains an implementation of a single module wrapper pattern, which allows managing several copies of a module in places where using the native Terraform 0.13+ `for_each` feature is not feasible (e.g., with Terragrunt).
+
+You may want to use a single Terragrunt configuration file to manage multiple resources without duplicating `terragrunt.hcl` files for each copy of the same module.
+
+This wrapper does not implement any extra functionality.
+
+## Usage with Terragrunt
+
+`terragrunt.hcl`:
+
+```hcl
+terraform {
+ source = "tfr:///terraform-aws-modules/acm/aws//wrappers"
+ # Alternative source:
+ # source = "git::git@github.com:terraform-aws-modules/terraform-aws-acm.git?ref=master//wrappers"
+}
+
+inputs = {
+ defaults = { # Default values
+ create = true
+ tags = {
+ Terraform = "true"
+ Environment = "dev"
+ }
+ }
+
+ items = {
+ my-item = {
+ # omitted... can be any argument supported by the module
+ }
+ my-second-item = {
+ # omitted... can be any argument supported by the module
+ }
+ # omitted...
+ }
+}
+```
+
+## Usage with Terraform
+
+```hcl
+module "wrapper" {
+ source = "terraform-aws-modules/acm/aws//wrappers"
+
+ defaults = { # Default values
+ create = true
+ tags = {
+ Terraform = "true"
+ Environment = "dev"
+ }
+ }
+
+ items = {
+ my-item = {
+ # omitted... can be any argument supported by the module
+ }
+ my-second-item = {
+ # omitted... can be any argument supported by the module
+ }
+ # omitted...
+ }
+}
+```
+
+## Example: Manage multiple S3 buckets in one Terragrunt layer
+
+`eu-west-1/s3-buckets/terragrunt.hcl`:
+
+```hcl
+terraform {
+ source = "tfr:///terraform-aws-modules/s3-bucket/aws//wrappers"
+ # Alternative source:
+ # source = "git::git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git?ref=master//wrappers"
+}
+
+inputs = {
+ defaults = {
+ force_destroy = true
+
+ attach_elb_log_delivery_policy = true
+ attach_lb_log_delivery_policy = true
+ attach_deny_insecure_transport_policy = true
+ attach_require_latest_tls_policy = true
+ }
+
+ items = {
+ bucket1 = {
+ bucket = "my-random-bucket-1"
+ }
+ bucket2 = {
+ bucket = "my-random-bucket-2"
+ tags = {
+ Secure = "probably"
+ }
+ }
+ }
+}
+```
diff --git a/wrappers/main.tf b/wrappers/main.tf
new file mode 100644
index 0000000..980824b
--- /dev/null
+++ b/wrappers/main.tf
@@ -0,0 +1,24 @@
+module "wrapper" {
+ source = "../"
+
+ for_each = var.items
+
+ create_certificate = try(each.value.create_certificate, var.defaults.create_certificate, true)
+ create_route53_records_only = try(each.value.create_route53_records_only, var.defaults.create_route53_records_only, false)
+ validate_certificate = try(each.value.validate_certificate, var.defaults.validate_certificate, true)
+ validation_allow_overwrite_records = try(each.value.validation_allow_overwrite_records, var.defaults.validation_allow_overwrite_records, true)
+ wait_for_validation = try(each.value.wait_for_validation, var.defaults.wait_for_validation, true)
+ certificate_transparency_logging_preference = try(each.value.certificate_transparency_logging_preference, var.defaults.certificate_transparency_logging_preference, true)
+ domain_name = try(each.value.domain_name, var.defaults.domain_name, "")
+ subject_alternative_names = try(each.value.subject_alternative_names, var.defaults.subject_alternative_names, [])
+ validation_method = try(each.value.validation_method, var.defaults.validation_method, "DNS")
+ validation_option = try(each.value.validation_option, var.defaults.validation_option, {})
+ create_route53_records = try(each.value.create_route53_records, var.defaults.create_route53_records, true)
+ validation_record_fqdns = try(each.value.validation_record_fqdns, var.defaults.validation_record_fqdns, [])
+ zone_id = try(each.value.zone_id, var.defaults.zone_id, "")
+ tags = try(each.value.tags, var.defaults.tags, {})
+ dns_ttl = try(each.value.dns_ttl, var.defaults.dns_ttl, 60)
+ acm_certificate_domain_validation_options = try(each.value.acm_certificate_domain_validation_options, var.defaults.acm_certificate_domain_validation_options, {})
+ distinct_domain_names = try(each.value.distinct_domain_names, var.defaults.distinct_domain_names, [])
+ putin_khuylo = try(each.value.putin_khuylo, var.defaults.putin_khuylo, true)
+}
diff --git a/wrappers/outputs.tf b/wrappers/outputs.tf
new file mode 100644
index 0000000..5da7c09
--- /dev/null
+++ b/wrappers/outputs.tf
@@ -0,0 +1,5 @@
+output "wrapper" {
+ description = "Map of outputs of a wrapper."
+ value = module.wrapper
+ # sensitive = false # No sensitive module output found
+}
diff --git a/wrappers/variables.tf b/wrappers/variables.tf
new file mode 100644
index 0000000..a6ea096
--- /dev/null
+++ b/wrappers/variables.tf
@@ -0,0 +1,11 @@
+variable "defaults" {
+ description = "Map of default values which will be used for each item."
+ type = any
+ default = {}
+}
+
+variable "items" {
+ description = "Maps of items to create a wrapper from. Values are passed through to the module."
+ type = any
+ default = {}
+}
diff --git a/wrappers/versions.tf b/wrappers/versions.tf
new file mode 100644
index 0000000..51cad10
--- /dev/null
+++ b/wrappers/versions.tf
@@ -0,0 +1,3 @@
+terraform {
+ required_version = ">= 0.13.1"
+}