Terraform for_each iteration over object - amazon-web-services

I'm relatively new to Terraform and I'm looking to simplify some private r53 zones I need to create using for_each.
I have the following local which I want to use to create private zones and associated A records in those zones:
locals {
private_zones = [
{
name = "foo.com"
txt = [
"This is the txt record for foo.com"]
ttl = 300
records = {
"host1" = "192.168.0.1",
"host2" = "192.168.0.2"
}
},
{
name = "bar.com"
txt = [
"This is the txt record for bar.com"]
ttl = 300
records = {
"host1" = "192.168.0.3",
"host2" = "192.168.0.4"
}
}
]
}
I've found some code which will allow me to iterate over the local to create the zones
resource "aws_route53_zone" "zone" {
for_each = { for name in local.private_zones : name.name => name }
name = each.value.name
vpc {
vpc_id = <vpc_id>
}
}
but I've no idea how I can iterate and create A records in the respective zone using the records list in each local.

You would use aws_route53_record and flattened private_zones:
locals {
private_zones_flat = merge([
for zone in local.private_zones: {
for host, ip in zone.records:
"${zone.name}-${host}" => {
zone_name = zone.name
host = host
ip = ip
}
}
]...)
}
resource "aws_route53_record" "host" {
for_each = local.private_zones_flat
zone_id = aws_route53_zone.zone[each.value.zone_name].zone_id
name = each.value.host
type = "A"
ttl = "300"
records = [each.value.ip]
}

Related

Terraform: How to use security group id in tfvars

I am creating an RDS option group with several options and one of them has the parameter "vpc_security_group_memberships" which takes the ID of a security group.
db_options.tfvars:
db_options = [
{
option_name = "OEM_AGENT"
option_settings = [
{
name = "AGENT_REGISTRATION_PASSWORD"
value = "****"
},
{
name = "OMS_HOST"
value = "gridcontrol.example.com"
},
{
name = "OMS_PORT"
value = "4000"
}
]
port = "3872"
version = "13.5.0.0.v1"
vpc_security_group_memberships = [ ]
}
]
And I create a security group in sg.tf:
resource "aws_security_group" "db-sg" {
description = "Allow access to RDS instance"
name = "${var.env}.${var.db_name}.sg"
tags = {
Name = "${var.env}.${var.db_name}.sg"
}
vpc_id = var.vpc_id
}
Is it possible to put the aws_security_group.db-sg.id for use in vpc_security_group_memberships?
Is it possible to put the aws_security_group.db-sg.id for use in vpc_security_group_memberships?
Sadly its not possible. TF variables can't be dynamic. They must be fully defined at plan time. But you can instead use locals. Local values can by dynamic.

Reference module with multiple resources

I am trying to create an AWS route53 hosted zone and add records to it. I added the following resources to a module main.tf
resource "aws_route53_zone" "zone" {
name = var.name
}
data "aws_route53_zone" "zone_data" {
name = var.name
}
resource "aws_route53_record" "record" {
zone_id = data.aws_route53_zone.zone_data.zone_id
name = var.record_name
type = var.record_type
ttl = var.record_ttl
records = var.record_value
}
Then I reference that module in a stack main.py as follows:
module "route53" {
source = "../../modules/route53"
name = "website.com"
type = "NS"
ttl = "30"
}
My issue is that building the stack will use the same name variable for both zone and record resources. How do I add another name to the stack module route53 for the record resource that is different from the zone resource?
If all you're trying to do in the module is create a zone and a record, you could use split to get the zone from the record name given. Like this:
main.tf
module "route53" {
source = "./modules/route53"
name = "something.website.com"
type = "NS"
ttl = "30"
}
modules/route53/main.tf
variable "name" {}
variable "type" {}
variable "ttl" {}
resource "aws_route53_zone" "this" {
name = split(".", var.name)[0]
}
resource "aws_route53_record" "this" {
zone_id = aws_route53_zone.this.zone_id
name = var.name
type = var.type
ttl = var.ttl
records = [var.name]
}
If however, you want multiple records in that zone, you could consider something like this, but this will depend heavily on what record configuration you're after.
main.tf
module "route53" {
source = "./modules/route53"
name = "website.com"
record_configs = {
something = {
type = "A"
records = ["192.168.0.99"]
}
other = {
type = "CNAME"
records = ["something.website.com"]
}
}
}
modules/route53/main.tf
variable "name" {}
variable "record_configs" {}
resource "aws_route53_zone" "this" {
name = var.name
}
resource "aws_route53_record" "this" {
for_each = var.record_configs
zone_id = aws_route53_zone.this.zone_id
name = each.key
type = each.value.type
records = each.value.records
}
If you have multiple records and names, the best way is to use for_each. For example:
variable "names" {
default = ["website.com", "website1.com", "website2.com"]
}
then
module "route53" {
source = "../../modules/route53"
for_each = toset(var.name)
name = each.key
type = "NS"
ttl = "30"
}
This way you can have same module for multiple names.
Randomly experimenting with solutions got me to add the resource variables' names as arguments to the module. This seems to allow referring to arguments of a specific resource in the root module if its argument name is the same as other resources (e.g. record_name vs name).
module "route53" {
source = "../../modules/route53"
name = "website.com"
record_name = "_auth.website.com"
record_type = "NS"
record_ttl = "30"
}

Terraform multiple AWS route53 MX records

I have locals in tf to push MX records to cloudflare and it works exactly I want it to. The same locals I want to use for Route53 but for route53 the records should be combined to one. How I can use blow locals to push to Route53 using terraform.
locals {
MXRecordSets = [
{
Name = "example.com.",
Type = "MX",
TTL = 3600,
MXRecords = [
{
"Value" = "1 aspmx.l.google.com"
},
{
"Value" = "5 alt1.aspmx.l.google.com"
}
]
}
]
}
locals {
FlatMXRecordSets = merge([
for idx, MXRecordSet in local.MXRecordSets:
{
for MXRecord in MXRecordSet.MXRecords:
"${idx}-${MXRecord.Value}" => {
MXRecordSet = MXRecordSet
MXRecord = MXRecord["Value"]
}
}
]...)
}
and finally I am using below aws route53 module to push changes to AWS, but its not working:
resource "aws_route53_record" "mx_records" {
for_each =local.FlatMXRecordSets
zone_id = aws_route53_zone.carfeine_com.id
name = each.value["MXRecordSet"].Name
type = each.value["MXRecordSet"].Type
records = [ each.value["MXRecord"] ]
ttl = 1
}
The error comes as [Tried to create resource record set [name='example.com.', type='MX'] but it already exists] because I am using for_each and I should not use that. Any other way ?
Your MXRecordSets is already flat, thus it does not require flattening. But Its also a list of maps, which can lead to more issues, as a list depends on order of items. Thus its better to use just a map of maps, if possible:
locals {
MXRecordSets = {
"mail.mwotest.xyz." = {
Type = "MX",
TTL = 3600,
MXRecords = [
{
"Value" = "1 aspmx.l.google.com"
},
{
"Value" = "5 alt1.aspmx.l.google.com"
}
]
}
}
}
then
resource "aws_route53_record" "mx_records" {
for_each = local.MXRecordSets
zone_id = aws_route53_zone.carfeine_com.id
name = each.key
type = each.value.Type
records = [ for key,record in each.value["MXRecords"]: record["Value"] ]
ttl = 1
}
in the above, the
[ for key,record in each.value["MXRecords"]: record["Value"] ]
will produce correct records for MX as:
records = ["1 aspmx.l.google.com", "5 alt1.aspmx.l.google.com"]

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
}

for_each value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created

i am trying to create route 53 using module concept. but getting below error.
"The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created."
for_each = var.create-route53 ? local.recordsets : tomap({})
local.recordsets will be known only after apply
var.create-route53 is true
can someone guide me on this ?
actual code:
module "route53" {
...
...
records = [
{
name = "test-name"
type = "A"
ttl = 300
records = [for instance in module.ec2: instance.ec2-IP
]
},
]
vpc_id = "${module.vpc.vpc_id}"
}
Inside modules folder below code will be there in route53 folder:
locals {
records = try(jsondecode(var.records), var.records)
recordsets = {
for rs in local.records :
join(" ", compact(["${rs.name} ${rs.type}", lookup(rs, "set_identifier", "")])) => merge(rs, {
records = jsonencode(try(rs.records, null))
})
}
}
resource "aws_route53_record" "this" {
for_each = var.create-route53 ? local.recordsets : tomap({})
zone_id = aws_route53_zone.private.zone_id
name = each.value.name != "" ? "${each.value.name}" : "test"
type = each.value.type
ttl = lookup(each.value, "ttl", null)
records = jsondecode(each.value.records)
set_identifier = lookup(each.value, "set_identifier", null)
health_check_id = lookup(each.value, "health_check_id", null)
multivalue_answer_routing_policy = lookup(each.value, "multivalue_answer_routing_policy", null)
allow_overwrite = lookup(each.value, "allow_overwrite", false)
}