diff --git a/acm-certificate/README.md b/acm-certificate/README.md new file mode 100644 index 0000000..945dc11 --- /dev/null +++ b/acm-certificate/README.md @@ -0,0 +1,40 @@ +# ACM Certificate + +This module provisions an AWS ACM certificate for one or more domains and optionally performs automatic DNS validation +using Route 53. + +## Features + +- AWS ACM certificate with multi-domain (SAN) support +- Optional automatic DNS validation via Route 53 +- Ability to manage DNS validation externally if no hosted zone is provided +- Deterministic primary domain selection +- Safe certificate replacement using `create_before_destroy` +- Resource tagging support + +## Usage + +See `variables.tf` for the full argument reference. + +```hcl +module "acm_certificate" { + source = "github.com/script47/aws-tf-modules/acm-certificate" + + domains = [ + "example.org", + "www.example.org" + ] + + zone_id = "Z123456789" + + tags = { + Project = "my-project" + Service = "my-service" + Environment = "production" + } + + providers = { + aws.default = aws + aws.acm = aws.useast1 + } +} diff --git a/acm-certificate/acm.tf b/acm-certificate/acm.tf new file mode 100644 index 0000000..7b673fd --- /dev/null +++ b/acm-certificate/acm.tf @@ -0,0 +1,26 @@ +locals { + domains = distinct(var.domains) + primary_domain = local.domains[0] +} + +resource "aws_acm_certificate" "this" { + domain_name = local.primary_domain + subject_alternative_names = local.domains + validation_method = "DNS" + tags = var.tags + + lifecycle { + create_before_destroy = true + } + + provider = aws.acm +} + +resource "aws_acm_certificate_validation" "this" { + for_each = var.zone_id == null ? {} : { "validation" = true } + + certificate_arn = aws_acm_certificate.this.arn + validation_record_fqdns = [for record in aws_route53_record.acm_records : record.fqdn] + + provider = aws.acm +} diff --git a/acm-certificate/outputs.tf b/acm-certificate/outputs.tf new file mode 100644 index 0000000..e44fc52 --- /dev/null +++ b/acm-certificate/outputs.tf @@ -0,0 +1,17 @@ +output "arn" { + value = var.zone_id == null ? aws_acm_certificate.this.arn : aws_acm_certificate_validation.this["validation"].certificate_arn +} + +output "dns" { + value = { + domains = local.domains + records = { + for dvo in aws_acm_certificate.this.domain_validation_options : + dvo.domain_name => { + type = dvo.resource_record_type + name = dvo.resource_record_name + value = dvo.resource_record_value + } + } + } +} diff --git a/acm-certificate/providers.tf b/acm-certificate/providers.tf new file mode 100644 index 0000000..8abc10b --- /dev/null +++ b/acm-certificate/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.13" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6" + configuration_aliases = [ + aws.acm, + aws.default, + ] + } + } +} diff --git a/acm-certificate/route53.tf b/acm-certificate/route53.tf new file mode 100644 index 0000000..d8ba305 --- /dev/null +++ b/acm-certificate/route53.tf @@ -0,0 +1,17 @@ +############################################# +# Validation for the ACM cert +############################################# +resource "aws_route53_record" "acm_records" { + for_each = var.zone_id == null ? {} : { + for o in aws_acm_certificate.this.domain_validation_options : o.domain_name => o + } + + zone_id = var.zone_id + type = each.value.resource_record_type + name = each.value.resource_record_name + records = [each.value.resource_record_value] + ttl = 60 + allow_overwrite = true + + provider = aws.default +} diff --git a/acm-certificate/variables.tf b/acm-certificate/variables.tf new file mode 100644 index 0000000..d58521b --- /dev/null +++ b/acm-certificate/variables.tf @@ -0,0 +1,20 @@ +variable "zone_id" { + type = string + description = "The ID of the hosted zone" + default = null +} + +variable "domains" { + type = list(string) + description = "List of domain names for your CloudFront distribution. The first domain specified will be classed as the primary domain (used as S3 bucket name, Route53 hosted zone name etc.)" + + validation { + condition = length(var.domains) > 0 + error_message = "domains requires at least one domain to be specified" + } +} + +variable "tags" { + type = map(string) + description = "The tags to apply to all resources created" +} diff --git a/lambda-function/README.md b/lambda-function/README.md index e5f2957..a9aa21b 100644 --- a/lambda-function/README.md +++ b/lambda-function/README.md @@ -7,7 +7,7 @@ This module allows you to setup a Lambda function. See `variables.tf` for the full argument reference. ```hcl -module "static_site" { +module "lambda_fn" { source = "github.com/script47/aws-tf-modules/lambda-function" name = "my-lambda-func" diff --git a/lambda-function/outputs.tf b/lambda-function/outputs.tf index a585987..3154e76 100644 --- a/lambda-function/outputs.tf +++ b/lambda-function/outputs.tf @@ -1,11 +1,11 @@ -output "lambda" { +output "fn" { value = { arn = aws_lambda_function.fn.arn invoke_arn = aws_lambda_function.fn.invoke_arn } } -output "cloudwatch" { +output "log_group" { value = { arn = length(aws_cloudwatch_log_group.logs) > 0 ? aws_cloudwatch_log_group.logs[0].arn : null } diff --git a/static-site/variables.tf b/static-site/variables.tf index 8989187..c141070 100644 --- a/static-site/variables.tf +++ b/static-site/variables.tf @@ -13,6 +13,7 @@ variable "hosted_zone" { variable "domains" { type = list(string) description = "List of domain names for your CloudFront distribution. The first domain specified will be classed as the primary domain (used as S3 bucket name, Route53 hosted zone name etc.)" + validation { condition = length(var.domains) > 0 error_message = "domains requires at least one domain to be specified"