Terraform - issue with aws_acm_certificate_validation - amazon-web-services

I am trying to generate a certificate and make it validate via DNS... all seems to work, until the last steps when I use resource "aws_acm_certificate_validation"
my code is the following:
# Create Certificate
resource "aws_acm_certificate" "ic_cert" {
provider = aws.us-east-1
domain_name = aws_s3_bucket.ic_bucket_main.bucket
subject_alternative_names = [aws_s3_bucket.ic_bucket_redirect.bucket]
validation_method = "DNS"
tags = {
Billing = "company X"
}
lifecycle {
create_before_destroy = true
}
}
# Validate Certificate via DNS
# get zone_id
data "aws_route53_zone" "selected" {
provider = aws.us-east-1
name = aws_s3_bucket.ic_bucket_main.bucket
}
# Generate DNS Records
resource "aws_route53_record" "ic_DNS_validation" {
provider = aws.us-east-1
for_each = {
for dvo in aws_acm_certificate.ic_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
zone_id = data.aws_route53_zone.selected.zone_id
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = each.value.zone_id
}
# Confirm certificate creation
resource "aws_acm_certificate_validation" "ic_cert_validation" {
certificate_arn = aws_acm_certificate.ic_cert.arn
#validation_record_fqdns = [for record in aws_route53_record.ic_DNS_validation : record.fqdn]
#validation_record_fqdns = [aws_route53_record.ic_DNS_validation.fqdn]
validation_record_fqdns = [for record in aws_route53_record.ic_DNS_validation : record.fqdn]
}
and I get the following error:
Error: reading ACM Certificate (arn:aws:acm:us-east-1:xxxxxxxxxxxxxxxxxxxxx8:certificate/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx): couldn't find resource
│ with aws_acm_certificate_validation.ic_cert_validation,
│ on certificates.tf line 45, in resource "aws_acm_certificate_validation" "ic_cert_validation":
│ 45: resource "aws_acm_certificate_validation" "ic_cert_validation" {
would anybody spot what is the issue?

Since ACM is a regional serivce and the certificate was created using provider = aws.us-east-1 the resource used for certificate validation should also use the same configuration (as the certificates were already created in that region):
resource "aws_acm_certificate_validation" "ic_cert_validation" {
provider = aws.us-east-1
certificate_arn = aws_acm_certificate.ic_cert.arn
#validation_record_fqdns = [for record in aws_route53_record.ic_DNS_validation : record.fqdn]
#validation_record_fqdns = [aws_route53_record.ic_DNS_validation.fqdn]
validation_record_fqdns = [for record in aws_route53_record.ic_DNS_validation : record.fqdn]
}

Related

Terraform "aws_acm_ceritificate" link with cloudfront cannot be created

I've configured the following certificate using aws_acm_ceritifcate resource:
provider "aws" {
alias = "virginia"
region = "us-east-1"
}
resource "aws_acm_certificate" "primary" {
domain_name = var.domain_name
validation_method = "DNS"
subject_alternative_names = ["*.${var.domain_name}"]
provider = aws.virginia
lifecycle {
create_before_destroy = true
}
tags = merge(
var.tags,
{
Name = "${var.project}-ACM-certificate",
}
)
}
resource "aws_route53_record" "certificate_validator_record" {
allow_overwrite = true
name = tolist(aws_acm_certificate.primary.domain_validation_options)[0].resource_record_name
records = [tolist(aws_acm_certificate.primary.domain_validation_options)[0].resource_record_value]
type = tolist(aws_acm_certificate.primary.domain_validation_options)[0].resource_record_type
zone_id = aws_route53_zone.primary.zone_id
ttl = 60
}
resource "aws_acm_certificate_validation" "certificate_validator" {
certificate_arn = aws_acm_certificate.primary.arn
validation_record_fqdns = [aws_route53_record.certificate_validator_record.fqdn]
}
As you can see, I need the certificate to validate the configured domain and its sub-domains. I configured Cloudfront:
module "cdn" {
source = "terraform-aws-modules/cloudfront/aws"
comment = "CloudFront for caching S3 private and static website"
is_ipv6_enabled = true
price_class = "PriceClass_100"
create_origin_access_identity = true
aliases = [var.frontend_domain_name]
origin_access_identities = {
s3_identity = "S3 dedicated for hosting the frontend"
}
origin = {
s3_identity = {
domain_name = module.s3_bucket.s3_bucket_bucket_regional_domain_name
s3_origin_config = {
origin_access_identity = "s3_identity"
}
}
}
default_cache_behavior = {
target_origin_id = "s3_identity"
viewer_protocol_policy = "redirect-to-https"
default_ttl = 5400
min_ttl = 3600
max_ttl = 7200
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
compress = true
query_string = true
}
default_root_object = "index.html"
custom_error_response = [
{
error_code = 403
response_code = 404
response_page_path = "/index.html"
},
{
error_code = 404
response_code = 404
response_page_path = "/index.html"
}
]
viewer_certificate = {
acm_certificate_arn = aws_acm_certificate.primary.arn
ssl_support_method = "sni-only"
}
tags = merge(
var.tags,
{
Name = "${var.project}-Cloudfront",
Stack = "frontend"
}
)
}
But when I try to create this terraform plan I get this error:
module.cdn.aws_cloudfront_distribution.this[0]: Still creating... [1m0s elapsed]
╷
│ Error: reading ACM Certificate (arn:aws:acm:us-east-1:***:certificate/ARN_PLACEHOLDER): couldn't find resource
│
│ with aws_acm_certificate_validation.certificate_validator,
│ on acm.tf line 33, in resource "aws_acm_certificate_validation" "certificate_validator":
│ 33: resource "aws_acm_certificate_validation" "certificate_validator" {
│
╵
╷
│ Error: error creating CloudFront Distribution: InvalidViewerCertificate: The certificate that is attached to your distribution doesn't cover the alternate domain name (CNAME) that you're trying to add. For more details, see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-requirements
│ status code: 400, request id: blabla
│
│ with module.cdn.aws_cloudfront_distribution.this[0],
│ on .terraform/modules/cdn/main.tf line 15, in resource "aws_cloudfront_distribution" "this":
│ 15: resource "aws_cloudfront_distribution" "this" {
│
╵
Releasing state lock. This may take a few moments...
If I go to my AWS account and check the certificate:
So if the certificate is valid and placed in us-east-1, where am I wrong?
I solved the issue with:
resource "aws_acm_certificate_validation" "certificate_validator" {
provider = aws.virginia
certificate_arn = aws_acm_certificate.primary.arn
validation_record_fqdns = [aws_route53_record.certificate_validator_record.fqdn]
}
Problem was that my cert validation was configured in my default region rather than us-east-1 region (as my certificate)

Why does my user_pool_domain always need to be replaced?

I get the following whenever I run terraform plan/apply and I don't know why this is saying it always needs to be replaced. The ACM is managed at the root of my project, and then the ARN is passed to my Cognito module.
# module.cognito["users"].aws_cognito_user_pool_domain.main must be replaced
+/- resource "aws_cognito_user_pool_domain" "main" {
~ certificate_arn = "arn:aws:acm:us-east-1:123456789:certificate/bc955b8a-45c6-4003-1b2a-5z66333fef275" -> (known after apply) # forces replacement
}
Update - add module call and DNS file for clarity
cognito.tf (root of the project)
module "cognito" {
source = "../modules/cognito"
for_each = var.cognito_userpools
cognito_name_prefix = "${try(each.value.name_prefix, local.name_prefix)}-${each.key}"
cognito_domain_name = try("${each.value.domain_prefix}.${local.dns_address}", null)
cognito_https_acm_arn = try(aws_acm_certificate.cognito_https_cert[each.key].arn, null)
hosted_zone_id = try(aws_route53_zone.public_hosted_zone.id, null)
cognito_callback_urls = each.value.callback_urls
cognito_logout_urls = each.value.logout_urls
cognito_sms_external_id = each.value.sms_external_id
cognito_userpool_schemas = each.value.userpool_schemas
cognito_mfa_configuration = try(each.value.mfa_configuration, "ON")
cognito_enable_software_token_mfa_configuration = try(each.value.enable_software_token_mfa_configuration, true)
cognito_userpool_groups = try(each.value.groups, [])
tags = local.default_tags
}
acm.tf (root of the project)
resource "aws_acm_certificate" "cognito_https_cert" {
for_each = var.cognito_userpools
provider = aws.us-east-1
domain_name = "${each.value.domain_prefix}.${local.dns_address}"
subject_alternative_names = ["*.${each.value.domain_prefix}.${local.dns_address}"]
validation_method = "DNS"
tags = local.default_tags
lifecycle {
create_before_destroy = true
}
}
modules/cognito/dns.tf
resource "aws_cognito_user_pool_domain" "main" {
domain = var.cognito_domain_name
certificate_arn = var.cognito_https_acm_arn
user_pool_id = aws_cognito_user_pool.pool.id
}
resource "aws_route53_record" "cognito_record" {
name = aws_cognito_user_pool_domain.main.domain
type = "A"
zone_id = var.hosted_zone_id
allow_overwrite = true
alias {
evaluate_target_health = false
name = aws_cognito_user_pool_domain.main.cloudfront_distribution_arn
# NOTE: This zone_id is fixed
zone_id = "Z2FDTNDATAQYW2"
}
}
Update 2
Upon further investigation tag updates were causing the arn not to be known until after apply, this isn’t ideal and only a problem with cognito custom domains - I don’t have the same issue with API Gateway custom domains for example. Does anyone have any idea on a workaround rather than relying on ignoring tag updates as a lifecycle meta argument?

Terraform: Iterating list for AWS Certificate validation with Cloudflare DNS

I have a map in a tfvars file that contains, Cloudflare zone id, site address, and zone (domain), I am wanting to iterate through that map, generating an ACM certificate, with a certificate validation DNS record being created in Cloudflare.
My map looks like this;
my_domains = {
example1 = {
cloudflare_zone_id = "00000000000000000000000000001"
address = "dev.example1.com"
domain = "example1.com"
}
example2 = {
cloudflare_zone_id = "0000000000000000000000000000002"
address = "dev.example2.com"
domain = "example2.com"
}
example3 = {
cloudflare_zone_id = "0000000000000000000000000000003"
address = "dev.library.example3.com"
domain = "example3.com"
}
}
I then have the following code for the certificate creation and validation:
resource "aws_acm_certificate" "my_certs" {
for_each = var.my_domains
domain_name = each.value.address
validation_method = "DNS"
subject_alternative_names = [
"*.${each.value.address}"
]
lifecycle {
create_before_destroy = true
}
}
resource "cloudflare_zone" "my_zone" {
for_each = var.my_domains
zone = each.value.domain
type = "full"
}
resource "cloudflare_record" "my_certificate_validation" {
for_each = {
for dvo in aws_acm_certificate.my_certs.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = cloudflare_zone.my_zone.id
name = each.value.name
value = trimsuffix(each.value.record, ".")
type = each.value.type
ttl = 1
proxied = false
}
When I run a plan, I get the following errors:
Error: Missing resource instance key
on cfcertvalidation.tf line 23, in resource "cloudflare_record" "my_certificate_validation":
23: for dvo in aws_acm_certificate.my_certs.domain_validation_options : dvo.domain_name => {
Because aws_acm_certificate.my_certs has "for_each" set, its attributes must be
accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
aws_acm_certificate.my_certs[each.key]
Error: Missing resource instance key
on cfcertvalidation.tf line 30, in resource "cloudflare_record" "my_certificate_validation":
30: zone_id = cloudflare_zone.my_zone.id
Because cloudflare_zone.cdt has "for_each" set, its attributes must be
accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
cloudflare_zone.my_zone[each.key]
Note: I added the cloudflare_zone resource rther than using the zone id already in the map as a way to simplify things in troubleshooting.
I am sure the answer is in the suggestion for using a [each.key], but I'm not sure how to implement it.
Any assistance would be greatly appreciated.
I have changed the map somewhat for my solution, so for completeness I have included the changed map here:
variable "my_domains" {
type = map(any)
default = {
example1 = {
cf_zone_id = "0000000000000000000000000000"
address = "example1.com"
zone = "example1.com"
}
example2 = {
cf_zone_id = "0000000000000000000000000000"
address = "example2.com"
zone = "example2.com"
}
example3 = {
cf_zone_id = "0000000000000000000000000000"
address = "library.example3.com"
zone = "example3.com"
}
}
}
What follows is the working solution, we start out by creating a local variable of type list, looping through the my_domains map to get the cert validation records we need. That then gets converted into a map, which is then used by the cloudflare_record resource to create the relevant DNS entries.
resource "aws_acm_certificate" "my_certs" {
for_each = var.my_domains
domain_name = "${var.env_url_prefix}${var.my_domains[each.key] ["address"]}"
validation_method = "DNS"
subject_alternative_names = ["*.${var.env_url_prefix}${var.my_domains[each.key]["address"]}"]
lifecycle {
create_before_destroy = true
}
}
locals {
validation = [
for certs in keys(var.my_domains) : {
for dvo in aws_acm_certificate.my_certs[certs].domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
value = trimsuffix(dvo.resource_record_value, ".")
type = dvo.resource_record_type
zone_id = var.my_domains[certs]["cf_zone_id"] # Get the zone id
}
}
]
# transform the list into a map
validation_map = { for item in local.validation : keys(item)[0] => values(item)[0]
}
}
resource "cloudflare_record" "my_cert_validations" {
for_each = local.validation_map
zone_id = local.validation_map[each.key]["zone_id"]
name = local.validation_map[each.key]["name"]
value = local.validation_map[each.key]["value"]
type = local.validation_map[each.key]["type"]
ttl = 1
proxied = false #important otherwise validation will fail
}

Terraformed AWS API Gateway Custom Domain Names throws 403 Forbidden

I am trying to expose all the stages of my Regional API Gateway through a regional Custom Domain.
Problem
If I curl directly my API Gateway (ie. https://xx.execute-api.eu-west-3.amazonaws.com/default/users), it works, but I get a 403 if I curl de domain name (ie. https://api.acme.com/default/users).
Configuration
My Terraform files looks like that:
data "aws_route53_zone" "acme" {
name = "acme.com."
}
resource "aws_api_gateway_rest_api" "backend" {
name = "acme-backend-api"
description = "Backend API"
body = "SOMETHING"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_api_gateway_deployment" "backend" {
rest_api_id = aws_api_gateway_rest_api.backend.id
stage_name = "default"
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_domain_name" "backend" {
domain_name = "api.acme.com"
regional_certificate_arn = "arn:aws:acm:xx:certificate/xx"
endpoint_configuration {
types = ["REGIONAL"]
}
}
resource "aws_route53_record" "backend" {
name = aws_api_gateway_domain_name.backend.domain_name
type = "A"
zone_id = data.aws_route53_zone.acme.id
alias {
evaluate_target_health = true
name = aws_api_gateway_domain_name.backend.regional_domain_name
zone_id = aws_api_gateway_domain_name.backend.regional_zone_id
}
}
resource "aws_api_gateway_base_path_mapping" "backend" {
api_id = aws_api_gateway_rest_api.backend.id
domain_name = aws_api_gateway_domain_name.backend.domain_name
# No stage_name: expose all stages
}
According to the Terraform api_gateway_domain_name and api_gateway_base_path_mapping examples, it should be ok.
I have also followed many howtos, and I have these elements:
The certificate
The A record to the API custom domain
The mapping to the deployed stage (which works if you call it directly)
What do I miss/do wrong?
This is v2 example working for me as off today, this "aws_apigatewayv2_api_mapping" is key to avoid port 80: Connection refused
or {"message":"Forbidden"} errors which I see you have but I did struggle with.
// ACM
resource "aws_acm_certificate" "cert_api" {
domain_name = var.api_domain
validation_method = "DNS"
tags = {
Name = var.api_domain
}
}
resource "aws_acm_certificate_validation" "cert_api" {
certificate_arn = aws_acm_certificate.cert_api.arn
}
// API Gateway V2
resource "aws_apigatewayv2_api" "lambda" {
name = "serverless_lambda_gw"
protocol_type = "HTTP"
}
resource "aws_apigatewayv2_stage" "lambda" {
api_id = aws_apigatewayv2_api.lambda.id
name = "serverless_lambda_stage"
auto_deploy = true
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_gw.arn
format = jsonencode({
requestId = "$context.requestId"
sourceIp = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
protocol = "$context.protocol"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
routeKey = "$context.routeKey"
status = "$context.status"
responseLength = "$context.responseLength"
integrationErrorMessage = "$context.integrationErrorMessage"
}
)
}
}
resource "aws_apigatewayv2_integration" "testimonials" {
api_id = aws_apigatewayv2_api.lambda.id
integration_uri = aws_lambda_function.testimonials.invoke_arn
integration_type = "AWS_PROXY"
integration_method = "POST"
}
resource "aws_apigatewayv2_route" "testimonials" {
api_id = aws_apigatewayv2_api.lambda.id
route_key = "GET /testimonials"
target = "integrations/${aws_apigatewayv2_integration.testimonials.id}"
}
resource "aws_cloudwatch_log_group" "api_gw" {
name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}"
retention_in_days = 30
}
resource "aws_lambda_permission" "api_gw" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.testimonials.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*"
}
resource "aws_apigatewayv2_domain_name" "api" {
domain_name = var.api_domain
domain_name_configuration {
certificate_arn = aws_acm_certificate.cert_api.arn
endpoint_type = "REGIONAL"
security_policy = "TLS_1_2"
}
}
resource "aws_apigatewayv2_api_mapping" "api" {
api_id = aws_apigatewayv2_api.lambda.id
domain_name = aws_apigatewayv2_domain_name.api.id
stage = aws_apigatewayv2_stage.lambda.id
}
// Route53
resource "aws_route53_zone" "api" {
name = var.api_domain
}
resource "aws_route53_record" "cert_api_validations" {
allow_overwrite = true
count = length(aws_acm_certificate.cert_api.domain_validation_options)
zone_id = aws_route53_zone.api.zone_id
name = element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_name, count.index)
type = element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_type, count.index)
records = [element(aws_acm_certificate.cert_api.domain_validation_options.*.resource_record_value, count.index)]
ttl = 60
}
resource "aws_route53_record" "api-a" {
name = aws_apigatewayv2_domain_name.api.domain_name
type = "A"
zone_id = aws_route53_zone.api.zone_id
alias {
name = aws_apigatewayv2_domain_name.api.domain_name_configuration[0].target_domain_name
zone_id = aws_apigatewayv2_domain_name.api.domain_name_configuration[0].hosted_zone_id
evaluate_target_health = false
}
}

How to solve No matching Route53Zone found?

I'm working on creating a module for terraform AWS certification manager in terraform > 0.12 Version. I noticed that when var.domain_name=["x.public.com","y.private.dev"]has both public and private domains, I'm getting an error no matching Route53Zone found.
I did manually run aws route53 list-hosted-zones-by-name, I was able to get both public and private zone id's , also it worked when var.domain_name=["x.public.com"] or var.domain_name=["y.private.dev"], ie only all public or all private.
I'm not sure what the issue is? I tried removing private_zone, & also tried with private_zone = trimprefix(each.value,".") == "dev" ? "true" : "false", in both cases got the same error.
Can someone point me where i'm wrong. Thanks!
Code:
resource "aws_acm_certificate" "certificate" {
for_each = toset(var.domain_name)
domain_name = each.value
subject_alternative_names = ["*.${each.value}"]
validation_method = "DNS"
tags = {
Name = each.value
owner = "foo"
}
lifecycle {
create_before_destroy = true
}
}
data "aws_route53_zone" "selected" {
for_each = toset(var.domain_name)
name = each.value
private_zone = false
}
resource "aws_route53_record" "record" {
for_each = toset(var.domain_name)
zone_id = data.aws_route53_zone[each.key].selected.zone_id
name = aws_acm_certificate.certificate[each.key].domain_validation_options.0.resource_record_name
type = aws_acm_certificate.certificate[each.key].domain_validation_options.0.resource_record_type
ttl = "300"
records = [aws_acm_certificate.certificate[each.key].domain_validation_options.0.resource_record_value]
}
Error: no matching Route53Zone found
on ../tf_module_acm/main.tf line 1, in data "aws_route53_zone" "selected":
34: data "aws_route53_zone" "selected" {
Error: no matching Route53Zone found
on ../tf_module_acm/main.tf line 1, in data "aws_route53_zone" "selected":
34: data "aws_route53_zone" "selected" {