How to solve No matching Route53Zone found? - amazon-web-services

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" {

Related

Terraform - issue with aws_acm_certificate_validation

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]
}

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
}

Terraform using output from module

I just started with Terraform infrastructure. Trying to create a vpc module that will contain code for vpc, subnets, internet gateway, rout table. Also creating a separate tf file for rds , which will refer to the vpc module and utilize the private subnets declared in vpc module.
Created a vpc module that has vpc.tf with following
provider "aws" {
region = var.region
}
terraform {
backend "s3" {}
}
resource "aws_vpc" "production-vpc" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = {
Name = "Dev-VPC"
}
}
// Private Subnets
resource "aws_subnet" "private-subnet-1" {
cidr_block = var.private_subnet_1_cidr
vpc_id = aws_vpc.production-vpc.id
availability_zone = "us-east-1a"
tags = {
Name = "Private-Subnet-1"
}
}
resource "aws_subnet" "private-subnet-2" {
cidr_block = var.private_subnet_2_cidr
vpc_id = aws_vpc.production-vpc.id
availability_zone = "us-east-1b"
tags = {
Name = "Private-Subnet-2"
}
}
The output.tf has following
output "private-subnet1-id" {
description = "Private Subnet1 Id"
value = aws_subnet.private-subnet-1.*.id
}
output "private-subnet2-id" {
description = "Private Subnet2 Id"
value = aws_subnet.private-subnet-2.*.id
}
The file is saved in folder \module\vpc folder
Created rds.tf as follows in folder \rds
provider "aws" {
region = var.region
}
terraform {
backend "s3" {}
}
module "vpc" {
source = "../module/vpc"
}
resource "aws_db_subnet_group" "subnetgrp" {
name = "dbsubnetgrp"
subnet_ids = [module.vpc.private-subnet1-id.id, module.vpc.private-subnet2-id.id]
}
When I run terraform plan , I get following error
Error: Unsupported attribute
on rds.tf line 16, in resource "aws_db_subnet_group" "subnetgrp":
16: subnet_ids = [module.vpc.private-subnet1-id.id, module.vpc.private-subnet2-id.id]
|----------------
| module.vpc.private-subnet1-id is tuple with 1 element
This value does not have any attributes.
Error: Unsupported attribute
on rds.tf line 16, in resource "aws_db_subnet_group" "subnetgrp":
16: subnet_ids = [module.vpc.private-subnet1-id.id, module.vpc.private-subnet2-id.id]
|----------------
| module.vpc.private-subnet2-id is tuple with 1 element
This value does not have any attributes.
You don't need the splat expression in the output.tf. Try the following,
output "private-subnet1-id" {
description = "Private Subnet1 Id"
value = aws_subnet.private-subnet-1.id
}
output "private-subnet2-id" {
description = "Private Subnet2 Id"
value = aws_subnet.private-subnet-2.id
}

Terraform error building changeset: InvalidChangeBatch when looping through multiple Route53 Records

I have Route 53 records that I am creating with the for_each command. One of my records has more than 1 values associated with its entry. This is how the records are declared:
variables.tf
variable "mx" {
type = map(object({
ttl = string
records = set(string)
}))
}
variables.tfvars
mx = {
"mywebsite.org." = {
ttl = "3600"
records = [
"home.mywebsite.org.",
"faq.mywebsite.org."
]
}
"myotherwebsite.org." = {
ttl = "3600"
records = [
"home.myotherwebsite.org."
]
}
mx.tf
locals {
mx_records = flatten([
for mx_key, mx in var.mx : [
for record in mx.records : {
mx_key = mx_key
record = record
ttl = mx.ttl
}]
])
}
resource "aws_route53_record" "mx_records" {
for_each = { for idx, mx in local.mx_records : idx => mx }
zone_id = aws_route53_zone.zone.zone_id
name = each.value.mx_key
type = "MX"
ttl = each.value.ttl
records = [
each.value.record
]
}
At execution everything works well up until Terraform realizes I have an additional value for my record. It then generates the error below:
Error building changeset: InvalidChangeBatch: [Tried to create resource record set
[name='mywebsite.org.', type='MX'] but it already exists]
My question is, is there a way to get Terraform to not create a second entry for this value? For Route53 all the record names have to be unique. Is there a way for Terraform to simply add on this value to this record since it was created in the initial run of execution? Any help would be appreciated as this becoming challenging.
UPDATE
After removing flatten and updating to 'records = [each.value.records]', this is the error:
Error: Unsupported attribute
on mx.tf line 20, in resource "aws_route53_record" "mx_records":
20: each.value.record
|----------------
| each.value is tuple with 2 elements
This value does not have any attributes.
Error: Unsupported attribute
on mx.tf line 20, in resource "aws_route53_record" "mx_records":
20: each.value.record
|----------------
| each.value is tuple with 1 element
This value does not have any attributes.
I think you could just use your mx directly, instead of transforming it into mx_records.
You could try the following:
resource "aws_route53_record" "mx_records" {
for_each = var.mx
zone_id = aws_route53_zone.zone.zone_id
name = each.key
type = "MX"
ttl = each.value.ttl
records = each.value.records
}
The above for_each should execute twice only. First for mywebsite.org. and the second for myotherwebsite.org..

Terraform - Conditional Data Source

In terraform, is there any way to conditionally use a data source? For example:
data "aws_ami" "application" {
most_recent = true
filter {
name = "tag:environment"
values = ["${var.environment}"]
}
owners = ["self"]
}
I'm hoping to be able to pass in an environment variable via the command line, and based on that, determine whether or not to fetch this data source.
I know with resources you can use the count property, but it doesn't seem you can use that with data sources.
I would consider tucking this code away in a module, but modules also can't use the count parameter.
Lastly, another option would be to provide a "Default" value for the data source, if it returned null, but I don't think that's doable either.
Are there any other potential solutions for this?
You can use a conditional on data sources the same as you can with resources and also from Terraform 0.13+ on modules as well:
variable "lookup_ami" {
default = true
}
data "aws_ami" "application" {
count = var.lookup_ami ? 1 : 0
most_recent = true
filter {
name = "tag:environment"
values = [var.environment]
}
owners = ["self"]
}
One use case for this in Terraform 0.12+ is to utilise the lazy evaluation of ternary statements like with the following:
variable "internal" {
default = true
}
data "aws_route53_zone" "private_zone" {
count = var.internal ? 1 : 0
name = var.domain
vpc_id = var.vpc_id
private_zone = var.internal
}
data "aws_route53_zone" "public_zone" {
count = var.internal ? 0 : 1
name = var.domain
private_zone = var.internal
}
resource "aws_route53_record" "www" {
zone_id = var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id
name = "www.${var.domain}"
type = "A"
alias {
name = aws_elb.lb.dns_name
zone_id = aws_elb.lb.zone_id
evaluate_target_health = false
}
}
This would create a record in the private zone when var.internal is true and instead create a record in the public zone when var.internal is false.
For this specific use case you could also use Terraform 0.12+'s null to rewrite this more simply:
variable "internal" {
default = true
}
data "aws_route53_zone" "zone" {
name = var.domain
vpc_id = var.internal ? var.vpc_id : null
private_zone = var.internal
}
resource "aws_route53_record" "www" {
zone_id = data.aws_route53_zone.zone.zone_id
name = "www.${data.aws_route53_zone.zone.name}"
type = "A"
alias {
name = aws_elb.lb.dns_name
zone_id = aws_elb.lb.zone_id
evaluate_target_health = false
}
}
This would only pass the vpc_id parameter to the aws_route53_zone data source if var.internal is set to true as you can't set vpc_id when private_zone is false.
Old Terraform 0.11 and earlier answer:
You can in fact use a conditional on the count of data sources but I've yet to manage to work out a good use case for it when I've tried.
As an example I successfully had this working:
data "aws_route53_zone" "private_zone" {
count = "${var.internal == "true" ? 1 : 0}"
name = "${var.domain}"
vpc_id = "${var.vpc_id}"
private_zone = "true"
}
data "aws_route53_zone" "public_zone" {
count = "${var.internal == "true" ? 0 : 1}"
name = "${var.domain}"
private_zone = "false"
}
But then had issues in how to then select the output of it because Terraform will evaluate any variables in the ternary conditional before deciding which side of the ternary to use (instead of lazy evaluation). So something like this doesn't work:
resource "aws_route53_record" "www" {
zone_id = "${var.internal ? data.aws_route53_zone.private_zone.zone_id : data.aws_route53_zone.public_zone.zone_id}"
name = "www.example.com"
type = "A"
alias {
name = "${aws_elb.lb.dns_name}"
zone_id = "${aws_elb.lb.zone_id }"
evaluate_target_health = "false"
}
}
Because if internal is true then you get the private_zone data source but not the public_zone data source and so the second half of the ternary fails to evaluate because data.aws_route53_zone.public_zone.zone_id isn't defined and equally with the other way around too.
In your case you probably just want to conditionally use the data source so might be able to do something like this:
variable "dynamic_ami" { default = "true" }
variable "default_ami" { default = "ami-123456" }
data "aws_ami" "application" {
most_recent = true
filter {
name = "tag:environment"
values = ["${var.environment}"]
}
owners = ["self"]
}
resource "aws_instance" "app" {
ami = "${var.dynamic_ami == "true" ? data.aws_ami.application.id : var.default_ami}"
instance_type = "t2.micro"
}