I have a set of AliasRecords under terraform locals and wanted to map them under terraform's resource "aws_route53_record" . Below is the locals value:
locals {
AWSAliasRecordSets = [
{
Name = "api-dev.example.com.",
Type = "A",
AliasTarget = {
HostedZoneId = "EXAMPLE",
DNSName = "kjhskdjhf.cloudfront.net.",
EvaluateTargetHealth = false
}
},
{
Name = "api.example.com.",
Type = "A",
AliasTarget = {
HostedZoneId = "EXAMPLE",
DNSName = "jsdhgfjkdshf.cloudfront.net.",
EvaluateTargetHealth = false
}
}
]
}
What I am doing is :
locals {
FlatAWSAliasRecordSets = merge([
for idx, AWSAliasRecordSet in local.AWSAliasRecordSets:
{
for AliasTarget in AWSAliasRecordSet.AliasTarget:
"${idx}-${AliasTarget}" => {
HostedZoneId = AliasTarget["HostedZoneId"]
DNSName = AliasTarget["DNSName"]
EvaluateTargetHealth = AliasTarget["EvaluateTargetHealth"]
}
}
]...)
}
resource "aws_route53_record" "alias_records" {
for_each = local.FlatAWSAliasRecordSets
zone_id = each.value["HostedZoneId"]
name = each.value["AliasTarget"].Name
type = each.value["AliasTarget"].Type
alias {
zone_id = each.value["HostedZoneId"]
name = each.value["AliasTarget"].Name
evaluate_target_health = each.value["EvaluateTargetHealth"]
}
}
and when pushing to AWS ( terraform apply), it fails with below error:
│ on main.tf line 508, in locals:
│ 508: EvaluateTargetHealth = AliasTarget["EvaluateTargetHealth"]
│ This value does not have any indices.
Your AWSAliasRecordSets does not require flattening, as it is already flat. Thus you can go use regular count for it.
resource "aws_route53_record" "alias_records" {
count = length(local.AWSAliasRecordSets)
zone_id = local.AWSAliasRecordSets[count.index]["AliasTarget"].HostedZoneId
name = local.AWSAliasRecordSets[count.index].Name
type = local.AWSAliasRecordSets[count.index].Type
alias {
zone_id = local.AWSAliasRecordSets[count.index]["AliasTarget"].HostedZoneId
name = local.AWSAliasRecordSets[count.index].DNSName
evaluate_target_health = each.value["AliasTarget"].EvaluateTargetHealth
}
}
You also have to double check your use of Name and DNSName. Your current usage does not seem right to me, but this would be a new issue if this is really the case.
Related
I want to create multiple acm certificates and route53 records for its validation, just cant figure out how can I reference to all acm resources created by for_each in route53 resource block which is looping in acm resource to get all the DNS _validation attributes, code is working fine if I will set one cert in variable and reference it directly with name, but how can I loop to all domain names for referencing it in for loop?
Issue is this line
for dvo in aws_acm_certificate.web[for i in keys(var.certificates) : i]
which is returning
The index operator must end with a closing bracket ("]").
Adding a second bracket like this
for dvo in aws_acm_certificate.web[[for i in keys(var.certificates) : i]]
returns error
│
│ on main.tf line 21, in resource "aws_route53_record" "domain_validation":
│ 21: for dvo in aws_acm_certificate.web[[for i in keys(var.certificates) : i]].domain_validation_options : dvo.domain_name => {
│ ├────────────────
│ │ aws_acm_certificate.web is object with 2 attributes
│ │ var.certificates is object with 2 attributes
│
│ The given key does not identify an element in this collection value: string
│ required.```
resource "aws_acm_certificate" "web" {
for_each = var.certificates
domain_name = "${replace(each.key, var.search_period, var.replace_period)}"
subject_alternative_names = each.value.subject_alternative_names
validation_method = "DNS"
}
resource "aws_route53_record" "domain_validation" {
for_each = var.dns_validation ? {
for dvo in aws_acm_certificate.web[[for i in keys(var.certificates) : i]].domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
} : {}
allow_overwrite = true
name = each.value.name #aws_acm_certificate.web[each.key].domain_validation_options.0.resource_record_name
records = [each.value.record] #aws_acm_certificate.web[each.key].domain_validation_options.0.resource_record_value
ttl = 60
type = each.value.type #aws_acm_certificate.web[each.key].domain_validation_options.0.resource_record_type
zone_id = data.aws_route53_zone.selected[[for i in keys(var.certificates) : i]].zone_id
}
var.certificates looks like this:
certificates = {
"xxx.com" = {
subject_alternative_names = ["*.xxx.com","*.dev.xxx.com","*.stage.xxx.com","*.preprod.xxx.com"]
},
"zzz.com" = {
subject_alternative_names = ["*.dev.zzz.com","*.zzz.com"]
},
}
aws_acm_certificate.web in console looks like this:
```
> aws_acm_certificate.web
{
"test.com" = {
"arn" = "arn:aws:acm:eu-west-1:584637875403:certificate/a6fed-01c6-4f2c-ad87-59c04877bd0b"
"certificate_authority_arn" = ""
"certificate_body" = tostring(null)
"certificate_chain" = tostring(null)
"domain_name" = "test.com"
"domain_validation_options" = toset([
{
"domain_name" = "*.test.com"
"resource_record_name" = "_d5e2266fa07c911501b806f3d19e.test.com."
"resource_record_type" = "CNAME"
"resource_record_value" = "_fcc7913c9269201f77625b7f71ec.ltyvprtsjl.acm-validations.aws."
},
{
"domain_name" = "*.dev.test.com"
"resource_record_name" = "_f2aa63aabaae8cd721bf0143dee6.dev.test.com."
"resource_record_type" = "CNAME"
"resource_record_value" = "_f461eca5849d2a3e218dea91955.ltyvprtsjl.acm-validations.aws."
},
{
"domain_name" = "*.preprod.test.com"
"resource_record_name" = "_2747174805245587c6f9811a1180.preprod.test.com."
"resource_record_type" = "CNAME"
"resource_record_value" = "_7d3ccdf1006b12074ebcbc9c3d1.ltyvprtsjl.acm-validations.aws."
},
{
"domain_name" = "*.stage.test.com"
"resource_record_name" = "_6f571d29f334dcccfe098a2371c.stage.test.com."
"resource_record_type" = "CNAME"
"resource_record_value" = "_9c9657a4839d827d1ff6db0ffd0.ltyvprtsjl.acm-validations.aws."
},
{
"domain_name" = "test.com"
"resource_record_name" = "_d5e2266fa07c91bad71501bd19e.test.com."
"resource_record_type" = "CNAME"
"resource_record_value" = "_fcc7913c926926efc05b7f71ec.ltyvprtsjl.acm-validations.aws."
},
])
"id" = "arn:aws:acm:eu-west-1:584637875403:certificate/a6fed5981c6-4f2c-ad87-59c04877bd0b"
"options" = tolist([
{
"certificate_transparency_logging_preference" = "ENABLED"
},
])
"private_key" = (sensitive)
"status" = "ISSUED"
"subject_alternative_names" = toset([
"*.test.com",
"*.dev.test.com",
"*.preprod.test.com",
"*.stage.test.com",
])
"tags" = tomap({})
"tags_all" = tomap({})
"validation_emails" = tolist([])
"validation_method" = "DNS"
}
}
```
You have to flatten your aws_acm_certificate.web first. For example:
locals {
certificate_web_flat = merge([
for hzone, certs in aws_acm_certificate.web: {
for domain_validation_option in certs.domain_validation_options:
"${hzone}-${domain_validation_option.domain_name}" => {
"hzone" = hzone
"domain_name" = domain_validation_option.domain_name
resource_record_name = domain_validation_option.resource_record_name
resource_record_value = domain_validation_option.resource_record_value
resource_record_type = domain_validation_option.resource_record_type
}
}
]...)
}
then
resource "aws_route53_record" "domain_validation" {
for_each = var.dns_validation ? local.certificate_web_flat : {}
allow_overwrite = true
name = each.value.resource_record_name
records = [each.value.resource_record_value]
ttl = 60
type = each.value.resource_record_type
zone_id = data.aws_route53_zone.selected[each.value.hzone].zone_id
}
I am trying to update the routing tables of the subnets in VPC A and VPC B to include a route to a VPC peering end-point. This is my terraform code.
main.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 3.14.2"
for_each = local.vpc_list
name = each.key
cidr =each.value.vpc_cidr
azs = each.value.vpc_azs
public_subnets = each.value.vpc_public_subnets
private_subnets = each.value.vpc_private_subnets
enable_nat_gateway = each.value.vpc_enable_nat_gateway
enable_vpn_gateway = each.value.vpc_enable_vpn_gateway
tags = each.value.vpc_tags
public_subnet_tags = each.value.vpc_public_subnet_tags
private_subnet_tags = each.value.vpc_private_subnet_tags
}
resource "aws_vpc_peering_connection" "vpc_peering_conn" {
peer_owner_id = data.aws_caller_identity.current.account_id
peer_vpc_id = module.vpc["vpcB"].vpc_id
vpc_id = module.vpc["vpcA"].vpc_id
auto_accept = true
tags = {
Name = "VPC Peering between ${module.vpc["vpcA"].name} and ${module.vpc["vpcB"].name}."
}
}
data "aws_route_tables" "vpcA_public_subnet_rts" {
depends_on = [ module.vpc ]
vpc_id = module.vpc["vpcA"].vpc_id
filter {
name = "tag:Subnet"
values = ["*public*"]
}
}
resource "aws_route" "route_vpcA" {
count = length(data.aws_route_tables.vpcA_public_subnet_rts.ids)
route_table_id = tolist(data.aws_route_tables.vpcA_public_subnet_rts.ids)[count.index]
destination_cidr_block = "10.10.11.0/24"
vpc_peering_connection_id = aws_vpc_peering_connection.vpc_peering_conn.id
}
data "aws_route_tables" "vpcB_private_subnet_rts" {
depends_on = [ module.vpc ]
vpc_id = module.vpc["vpcB"].vpc_id
filter {
name = "tag:Subnet"
values = ["*private*"]
}
}
resource "aws_route" "route_vpcB" {
count = length(data.aws_route_tables.vpcB_private_subnet_rts.ids)
route_table_id = tolist(data.aws_route_tables.vpcB_private_subnet_rts.ids)[count.index]
destination_cidr_block = "10.10.10.0/24"
vpc_peering_connection_id = aws_vpc_peering_connection.vpc_peering_conn.id
}
locals.tf
locals {
vpc_list = {
"vpcA" = {
vpc_cidr = "10.10.10.0/24",
vpc_azs = ["ap-southeast-1a"],
vpc_public_subnets = ["10.10.10.0/25"],
vpc_private_subnets = ["10.10.10.128/25"],
vpc_enable_nat_gateway = false,
vpc_enable_vpn_gateway = false,
vpc_tags = {
Name= "VPC A"
Terraform = "true"
Environment = "1st VPC"
Facing= "public and private"
},
vpc_public_subnet_tags = {
Subnet = "vpcA_public_subnet"
},
vpc_private_subnet_tags = {
Subnet = "vpcA_private_subnet"
},
},
"vpcB" = {
vpc_cidr = "10.10.11.0/24",
vpc_azs = ["ap-southeast-1b"],
vpc_public_subnets = [],
vpc_private_subnets = ["10.10.11.0/24"],
vpc_enable_nat_gateway = false,
vpc_enable_vpn_gateway = false,
vpc_tags = {
Name= "VPC B"
Terraform = "true"
Environment = "2nd VPC"
Facing= "private"
},
vpc_public_subnet_tags = {
Subnet = "vpcB_public_subnet"
},
vpc_private_subnet_tags = {
Subnet = "vpcB_private_subnet"
},
},
}
}
locals {
routing_table = {
route_peer_con_vpcA = {
vpc_id = module.vpc["vpcA"].vpc_id
route = {
route_peer_to_vpcB = {
cidr_block = "10.10.11.0/24"
}
}
}
route_peer_con_vpcB = {
vpc_id = module.vpc["vpcB"].vpc_id
route = {
route_peer_to_vpcA = {
cidr_block = "10.10.10.0/24"
}
}
}
}
}
When I run the terraform plan or apply I am getting the below error. Does anyone knows how to address the issue or Is there a better way to achieve what I want?
I saw this post "terraform: data.aws_subnet, value of 'count' cannot be computed" but am not sure how to refer to the output of the subnet' s id for the routing table id.
Thanks.
➜ 01-tf-deploy terraform apply --auto-approve
data.aws_region.current: Reading...
data.aws_caller_identity.current: Reading...
data.aws_region.current: Read complete after 0s [id=ap-southeast-1]
data.aws_ami.amzlinux2: Reading...
data.aws_ami.amzlinux2: Read complete after 1s [id=ami-0c802847a7dd848c0]
data.aws_caller_identity.current: Read complete after 1s [id=500295128231]
╷
│ Error: Invalid count argument
│
│ on main.tf line 70, in resource "aws_route" "route_vpcA":
│ 70: count = length(data.aws_route_tables.vpcA_public_subnet_rts.ids)
│
│ The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count
│ depends on.
╵
╷
│ Error: Invalid count argument
│
│ on main.tf line 88, in resource "aws_route" "route_vpcB":
│ 88: count = length(data.aws_route_tables.vpcB_private_subnet_rts.ids)
│
│ The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the count
│ depends on.
╵
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
}
I have several domains and I want to create subdomains with as much DRY as possible. This is the original structure:
variable "domain1" {
type = list(string)
default = ["www", "www2"]
}
variable "domain2" {
type = list(string)
default = ["www3", "www1"]
}
resource "aws_route53_record" "domain1" {
for_each = toset(var.domain1)
type = "A"
name = "${each.key}.domain1.com"
zone_id = ""
}
resource "aws_route53_record" "domain2" {
for_each = toset(var.domain2)
type = "A"
name = "${each.key}.domain2.com"
zone_id = ""
}
that I want to combine to one variable and one resource block:
variable "subdomains" {
type = map(list(string))
default = {
"domain1.com" = ["www", "www2"]
"domain2.com" = ["www3", "www1"]
}
}
resource "aws_route53_record" "domain1" {
for_each = var.subdomains // make magic happen here...
type = "A"
name = "${each.subdomain_part}.${each.domain_part}" // ...and here
zone_id = ""
}
Is there a way to achieve this?
You can flatten your var.subdomains as follows:
locals {
subdomains_flat = flatten([for domain, subdomains in var.subdomains:
[ for subdomain in subdomains:
{
domain_part = domain
subdomain_part = subdomain
}
]
])
}
then:
resource "aws_route53_record" "domain1" {
for_each = {for idx, val in local.subdomains_flat: idx => val }
type = "A"
name = "${each.value.subdomain_part}.${each.value.domain_part}"
zone_id = ""
}
Following up on the comment about a messy state, I would not say messy... but certainly there are some downsides, the index in that answer is numeric, a plan show that the resource ends up:
# aws_route53_record.domain1["0"] will be created
+ resource "aws_route53_record" "domain1" {
# aws_route53_record.domain1["1"] will be created
+ resource "aws_route53_record" "domain1" {
That can create problems when we add or remove subdomains to the list, the order can change and that will cause the resources to be destroyed and recreated, not ideal on route53 records...
Here is another approach that will create a different index in the resource name.
We still use flatten to extract the subdomains but on this case I'm concatenating right away, that local variable is ready for the aws_route53_record resource to consume it.
provider "aws" {
region = "us-east-2"
}
variable "subdomains" {
type = map(list(string))
default = {
"domain1.com" = ["www", "www2"]
"domain2.com" = ["www3", "www1"]
}
}
locals {
records = flatten([for d, subs in var.subdomains: [for s in subs: "${s}.${d}"]])
}
resource "aws_route53_record" "domain1" {
for_each = toset(local.records)
type = "A"
name = each.value
zone_id = "us-east-1"
}
A terraform plan of that looks like:
Terraform will perform the following actions:
# aws_route53_record.domain1["www.domain1.com"] will be created
+ resource "aws_route53_record" "domain1" {
+ allow_overwrite = (known after apply)
+ fqdn = (known after apply)
+ id = (known after apply)
+ name = "www.domain1.com"
+ type = "A"
+ zone_id = "us-east-1"
}
# aws_route53_record.domain1["www1.domain2.com"] will be created
+ resource "aws_route53_record" "domain1" {
+ allow_overwrite = (known after apply)
+ fqdn = (known after apply)
+ id = (known after apply)
+ name = "www1.domain2.com"
+ type = "A"
+ zone_id = "us-east-1"
}
...
I'm trying to create GCP SQL DBs by iterating a list of string using Terraform's for_each and count parameter and the other loop is for the map keys (maindb & replicadb).
Unfortunately, I get the error that appears below.
Is it possible to do this is Terraform?
variables.tf
variable "sql_var" {
default = {
"maindb" = {
"db_list" = ["firstdb", "secondsdb", "thirddb"],
"disk_size" = "20",
},
"replicadb" = {
"db_list" = ["firstdb"],
"disk_size" = "",
}
}
}
main.tf
resource "google_sql_database_instance" "master_sql_instance" {
...
}
resource "google_sql_database" "database" {
for_each = var.sql_var
name = "${element(each.value.db_list, count.index)}"
instance = "${google_sql_database_instance.master_sql_instance[each.key].name}"
count = "${length(each.value.db_list)}"
}
Error Message
Error: Invalid combination of "count" and "for_each"
on ../main.tf line 43, in resource
"google_sql_database" "database": 43: for_each =
var.sql_var
The "count" and "for_each" meta-arguments are mutually-exclusive, only
one should be used to be explicit about the number of resources to be
created.
What the error message tells you is that you cannot use count and for_each together. It looks like you are trying to create 3 main databases and 1 replica database am I correct? What I would do is create your 2 master instances and then transform your map variable to create the databases.
terraform {
required_version = ">=0.13.3"
required_providers {
google = ">=3.36.0"
}
}
variable "sql_instances" {
default = {
"main_instance" = {
"db_list" = ["first_db", "second_db", "third_db"],
"disk_size" = "20",
},
"replica_instance" = {
"db_list" = ["first_db"],
"disk_size" = "20",
}
}
}
locals {
databases = flatten([
for key, value in var.sql_instances : [
for item in value.db_list : {
name = item
instance = key
}
]
])
sql_databases = {
for item in local.databases :
uuid() => item
}
}
resource "google_sql_database_instance" "sql_instance" {
for_each = var.sql_instances
name = each.key
settings {
disk_size = each.value.disk_size
tier = "db-f1-micro"
}
}
resource "google_sql_database" "sql_database" {
for_each = local.sql_databases
name = each.value.name
instance = each.value.instance
depends_on = [
google_sql_database_instance.sql_instance,
]
}
Then, first run terraform apply -target=google_sql_database_instance.sql_instance and after this run terraform apply.