How to reference instance argument value created with for_each meta-argument in another instance in the same map - terraform

Updated with a more illustrative example.
My end goal is to have Terraform create instances of a resource generated with the for_each meta argument in a specific sequence. HCL is known to be a declarative language and when Terraform applies a configuration it can create resources randomly unless you use the depends_on argument or refer from one resource (instance) to another. However, the depends_on argument does not take values that are "calculated", so I don't know how to use it in modules.
For this reason, in order to force Terraform to create instances of a resource in a specific sequence, I decided to try to make the value of a certain argument in an instance it creates "calculated" based on the values of the same argument from another instance.
Below you can find a more practical example based on using one of the providers, but the question is more general and pertains to Terraform as such.
Let's take a test module that instantiates the cloudflare_page_rule resource:
# Module is placed to module\main.tf
terraform {
experiments = [module_variable_optional_attrs]
}
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = ">= 3.10.0"
}
}
}
variable "zone" {
type = string
description = "The DNS zone name which will be added, e.g. example.com."
}
variable "page_rules" {
type = list(object({
page_rule_name = string
target = string
actions = object({
forwarding_url = optional(object({
url = string
status_code = number
}))
})
priority = optional(number)
status = optional(string)
depends_on = optional(string)
}))
description = "Zone's page rules."
default = []
}
//noinspection HILUnresolvedReference
locals {
page_rule_dependencies = { for p in var.page_rules : p.page_rule_name => p.depends_on if p.depends_on != null }
}
# https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/zone
resource "cloudflare_zone" "this" {
zone = var.zone
}
# https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/page_rule
//noinspection HILUnresolvedReference
resource "cloudflare_page_rule" "this" {
for_each = var.page_rules != null ? { for p in var.page_rules : p.page_rule_name => p } : {}
zone_id = cloudflare_zone.this.id
target = each.value.target
actions {
//noinspection HILUnresolvedReference
forwarding_url {
status_code = each.value.actions.forwarding_url.status_code
url = each.value.actions.forwarding_url.url
}
}
priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority
status = each.value.status
}
output "page_rule_dependencies" {
value = local.page_rule_dependencies
}
And a configuration that is used to create resources:
terraform {
required_version = ">= 0.15.0"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = ">= 3.10.1"
}
}
}
variable "cloudflare_api_token" {
type = string
sensitive = true
}
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
module "acme_com" {
source = "./module"
zone = "acme.com"
page_rules = [
{
page_rule_name = "page_rule_1"
target = "acme.com/url1"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url1"
}
}
priority = 1
},
{
page_rule_name = "page_rule_2"
target = "acme.com/url2"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url2"
}
}
priority = 2
depends_on = "page_rule_1"
},
{
page_rule_name = "page_rule_3"
target = "acme.com/url3"
actions = {
forwarding_url = {
status_code = 301
url = "https://www.example.com/url3"
}
}
priority = 3
depends_on = "page_rule_2"
}
]
}
output "page_rule_dependencies" {
value = module.acme_com.page_rule_dependencies
}
In this particular example, I've added the depends_on argument to the page_rules variable (don't confuse this argument with the depends_on meta argument). For the value of the depends_on argument, I specified the name of a page_fule on which another page_fule depends.
Next, I created a local variable page_rule_dependencies, the value of which, after calculations, is the following (you can check this yourself by replacing the priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority construct with priority = each.value.priority and executing terraform apply):
page_rule_dependencies = {
"page_rule_2" = "page_rule_1"
"page_rule_3" = "page_rule_2"
}
Further, in the priority = each.value.depends_on != null ? cloudflare_page_rule.this[local.page_rule_dependencies[each.key]].priority + 1 : each.value.priority construct, I refer to the values ​​of the local variable, thereby forming a "reference" to the page_fule instance, on which the current instance depends:
When creating page_rule_1, the value of its argument priority = 1.
When creating page_rule_2, the value of its argument priority = cloudflare_page_rule.this["page_rule_1"].priority + 1.
When creating page_rule_3, the value of its argument priority = cloudflare_page_rule.this["page_rule_2"].priority + 1.
However, I get an Error: Cycle: module.acme_com.cloudflare_page_rule.this["page_rule_3"], module.acme_com.cloudflare_page_rule.this["page_rule_2"], module.acme_com.cloudflare_page_rule.this["page_rule_1"] error.
Either I'm doing something wrong, or it's some kind of Terraform limitation/bug. Is there a way to get rid of this error?
P.S. Resulting graph after terraform graph -draw-cycles | dot -Tsvg > graph.svg or terraform graph -draw-cycles -type=plan | dot -Tsvg > graph-plan.svg (the same result):
P.P.S. I use Terraform v1.1.7.

Related

How can I split out an 'any' variable in terraform?

I'm trying to get multiple values out of an 'any' type variable. I'm new to terraform and open to recommendations. Specifically for this example, I'd like to know how I can output the 'bucket_name' value in my outputs.
variable "replica_config" {
type = any
default = {
role = "role_name"
rules = [
{
id = "full-s3-replication"
status = true
priority = 10
delete_marker_replication = false
destination = {
bucket = "bucket_name"
storage_class = "STANDARD"
replica_kms_key_id = "key_id"
account_id = "account_id"
replication_time = {
status = "Enabled"
minutes = 15
}
}
}
]
}
}
Current Output:
output "output4" {
value = flatten(var.replica_config["rules"])
}
Since you you have a list for rules, you can use a splat expression as such:
output "output4" {
value = var.replica_config.rules[*].destination.bucket
}
Keep in mind, the output of this expression will also be a list. If you want a single item instead of a list, you can use an index.
For example:
output "output4" {
value = var.replica_config.rules[0].destination.bucket
}

Create aws_ses_receipt_rule in predefined order with for_each loop

I am trying to define SES rule sets with an order defined by a collection of rules in a variable.
I have tried the solution in https://github.com/hashicorp/terraform-provider-aws/issues/24067 to use the after property of the resource, and It does create the first rule, and fails when creating the second, and all subsequent rules, because the first rule does not exist yet (the one with after=null). I guess it needs some time to finalize. depends_on does not work with dynamic dependencies, as far as i know, so this will not make it either.
If I re-run the apply, then the second rule is created, but all the other rules fail.
my recipients_by_position map is indexed by 0-padded position (i.e. "01", "02", etc):
This is my code
locals {
recipients = {
"mail1-recipient1" = {
short_name = "mail1"
domain = "mail1.domain.com"
recipient = "recipient1"
position = 1
target_bucket = "bucket1"
}
"mail1-recipient2" = {
short_name = "mail1"
domain = "mail1.domain.com"
recipient = "recipient2"
position = 2
target_bucket = "bucket1"
}
"mail2-recipient1" = {
short_name = "mail2"
domain = "mail2.domain.com"
recipient = "recipient1"
position = 3
target_bucket = "bucket2"
}
}
spec_by_domain = {
"mail1.domain.com" = {
irrelevant ={}
}
"mail2.domain.com" = {
irrelevant ={}
}
}
recipients_by_position = {for r in local.recipients: "${format("%02s",r.position)}" => r}
}
resource "aws_ses_domain_identity" "domains" {
for_each = local.spec_by_domain
domain = each.key
}
resource "aws_ses_receipt_rule_set" "main" {
rule_set_name = "new-rules"
}
# store it in S3
resource "aws_ses_receipt_rule" "store" {
for_each = local.recipients_by_position
after = each.value.position == 1 ? null : "${format("%02s",each.value.position - 1)}"
# name = "${each.value.short_name}-store_on_s3-${each.value.recipient}"
name = each.key
rule_set_name = aws_ses_receipt_rule_set.main.rule_set_name
recipients = ["${each.value.recipient}#${each.value.domain}"]
enabled = true
scan_enabled = true
s3_action {
bucket_name = aws_s3_bucket.mailboxes[each.value.domain].bucket
object_key_prefix = each.value.recipient
position = 1
}
}
apply fails with a bunch of
Error: Error creating SES rule: RuleDoesNotExist: Rule does not exist: xx
with xx from 01 to whatever number of rules were defined

Terraform Lookup returning a block

I am attempting to only assign a log_config to a subnet, if the subnet name is in the "file_one.tf sample" below. To accomplish the conditional logic I am using the lookup function to only return a "log_config" block if the name matches a name in the variable referenced in "file_one.tf".
The error I am getting is:
An argument named "log_config" is not expected here. Did you mean to define a block type of "log_config"
Would someone please help out with this? Or present a more elegant approach to assigning blocks to resources with conditional logic?
Thank you
# file_one.tf sample
locals {
subnets = {
"one" = {
"name" = "one"
local_config = {
aggregation_interval = "INTERVAL_10_SEC"
metadata = "INCLUDE_ALL_METADATA"
}
}
}
# file_two.tf
resource "google_compute_subnetwork" "network-with-private-secondary-ip-ranges" {
foreach = local.subnets
name = each_value.name
ip_cidr_range = "10.2.0.0/16"
region = "us-central1"
network = google_compute_network.custom-test.id
secondary_ip_range {
range_name = "tf-test-secondary-range-update1"
ip_cidr_range = "192.168.10.0/24"
}
log_config = lookup(each.value, log_config, null)
}
You are attempting to pass a map type as a value for an argument, and the resource schema is expecting a block type and not an argument with a map. You would need a dynamic block for this situation:
dynamic "log_config" {
for_each = lookup(each.value, log_config, [])
content {
aggregation_interval = log_config.value["aggregation_interval"]
metadata = log_config.value["metadata"]
}
}
The key that you want in said map is called local_config, no? When lookup() returns its default value is null, which is also not a block. But here I believe you want to pass in a string key to the lookup function:
# file_two.tf
{
# ...
name = each.value.name
# ...
log_config = lookup(each.value, "local_config", null)
}

For_each loop with for expression based on value in map

Since the title is not descriptive enough let me introduce my problem.
I'm creating DRY module code for CDN that contains profile/endpoint/custom_domain.
Variable cdn_config would hold all necessary/optional parameters and these are created based on the for_each loop.
Variable looks like this:
variable "cdn_config" {
profiles = {
"profile_1" = {}
}
endpoints = {
"endpoint_1" = {
custom_domain = {
}
}
}
}
Core of this module is working - in the means that it would create cdn_profile "profile_1" then cdn_endpoint "endpoint_1" will be created and assigned to this profile then cdn_custom_domain will be created and assigned to "endpoint_1" since it's the part of "endpoint_1" map.
Then I realize, what in case I want to create "cdn_custom_domain" only and specify resource ID manually?
I was thinking that adding the optional parameter "standalone" could help, so it would look like this:
variable "cdn_config" {
profiles = {
"profile_1" = {}
}
endpoints = {
"endpoint_1" = {
custom_domain = {
}
}
"endpoint_standalone" = {
custom_domain = {
standalone = true
cdn_endpoint_id = "xxxxx"
}
}
}
}
Having this "standalone" parameter eq true "endpoint_standalone" map should be totally ignored from looping in the azurerm_cdn_endpoint resource creation.
So far this direction is my only guess, clearly, it's not working - if I add "endpoint_standalone" it complains that not all required parameters are specified so it's surely finding it.
resource "azurerm_cdn_endpoint" "this" {
for_each = {for k in keys(var.cdn_config.endpoints) : k => var.cdn_config.endpoints[k] if lookup(var.cdn_config.endpoints[k],"standalone",null) != "true"}
I would be grateful if you have a solution for this problem.
You are comparing a bool type to a string type, so the logical comparison will always return false:
for_each = {for k in keys(var.cdn_config.endpoints) : k => var.cdn_config.endpoints[k] if lookup(var.cdn_config.endpoints[k],"standalone",null) != true }
While we are here, we can also improve this for expression:
for_each = { for endpoint, params in var.cdn_config.endpoints : endpoint => params if lookup(params.custom_domain, "standalone", null) != true }

Optional list element processing in Terraform

I am trying to add redrive policies to existing queues.
I have managed to define a list like this:
variable "sqsq_primary" {
type = "list"
default = [
{
name = "PrimaryQueue1"
maxReceiveCount = -1
deadLetterQueue = ""
},
{
name = "PrimaryQueue2"
maxReceiveCount = 5
deadLetterQueue = "PrimaryQueue2_DL"
},
{
name = "PrimaryQueue3"
maxReceiveCount = 20
deadLetterQueue = "PrimaryQueue3_DL"
}
]
}
I have defined a list of DL queues like this:
variable "sqsq_primary_dl" {
type = "list"
default = [
"PrimaryQueue2_DL",
"PrimaryQueue3_DL"
]
}
In my module I define resources like this:
resource "aws_sqs_queue" "q" {
count = "${length(var.sqsq_primary)}"
name = "${lookup(var.sqsq_primary[count.index], "name")}-${var.environment}"
## Conditionally Sets A Redrive Policy ##
redrive_policy = "${lookup(var.sqsq_primary[count.index], "deadLetterQueue") != "" ? "{\"deadLetterTargetArn\":\"arn:aws:sqs:${var.region}:${var.acc_number}:${lookup(var.sqsq_primary[count.index], "deadLetterQueue")}-${var.environment}\",\"maxReceiveCount\":${lookup(var.sqsq_primary[count.index], "maxReceiveCount")}}" : ""}"
depends_on = ["aws_sqs_queue.qdl"]
}
resource "aws_sqs_queue" "qdl" {
count = "${length(var.sqsq_primary_dl)}"
name = "${element(var.sqsq_primary_dl, count.index)}-${var.environment}"
}
This works. However, I don't like the duplicated information which is the names of the DL queues.
So the question is, how could I get rid of the second list? How could I iterate in the second resource over the first list instead and only create a DL queue if deadLetterQueue != "" ?
Thanks for your help!
I think you may have encountered a limitation of terraform interpolation. Unless you deconstruct your list of maps to separate maps, the best is probably below.
If you keep your definitions for queues with no dl at the bottom and use a static value for minus maths on the dl resource count, the plan stays the same as before.
As a side note, it's dead letter not dead leater.
variable "sqsq_primary" {
type = "list"
default = [
{
name = "PrimaryQueue2"
maxReceiveCount = 5
deadLeaterQueue = "PrimaryQueue2_DL"
},
{
name = "PrimaryQueue3"
maxReceiveCount = 20
deadLeaterQueue = "PrimaryQueue3_DL"
},
{
name = "PrimaryQueue1"
maxReceiveCount = -1
deadLeaterQueue = ""
}
]
}
resource "aws_sqs_queue" "q" {
count = "${length(var.sqsq_primary)}"
name = "${lookup(var.sqsq_primary[count.index], "name")}-${var.environment}"
## Conditionally Sets A Redrive Policy ##
redrive_policy = "${lookup(var.sqsq_primary[count.index], "deadLeaterQueue") != "" ? "{\"deadLetterTargetArn\":\"arn:aws:sqs:${var.region}:${var.acc_number}:${lookup(var.sqsq_primary[count.index], "deadLeaterQueue")}-${var.environment}\",\"maxReceiveCount\":${lookup(var.sqsq_primary[count.index], "maxReceiveCount")}}" : ""}"
depends_on = ["aws_sqs_queue.qdl"]
}
resource "aws_sqs_queue" "qdl" {
count = "${length(var.sqsq_primary) - 1}"
name = "${lookup(var.sqsq_primary[count.index], "deadLeaterQueue")-var.environment}"
}
My colleague has come up with a solution that seems slightly more flexible than the one provided by #henry-dobson.
We have also refactored it so now it doesn't require the deadLeaterQueue value - we conform to a naming standard now, so the resulting names of the DL queues are different from the ones in the question.
variable "sqsq_primary" {
type = "list"
default = [
{
name = "PrimaryQueue1"
maxReceiveCount = 0
},
{
name = "PrimaryQueue2"
maxReceiveCount = 5
},
{
name = "PrimaryQueue3"
maxReceiveCount = 20
}
]
}
data "empty_data_source" "deadletterq" {
count = "${length(var.sqsq_primary)}"
inputs = {
dl = "${lookup(var.sqsq_primary[count.index], "maxReceiveCount", "") > 0 ? "${replace(lookup(var.sqsq_primary[count.index], "name"),"Queue","DeadLetterQueue")}" : ""}"
}
}
resource "aws_sqs_queue" "q" {
count = "${length(var.sqsq_primary)}"
name = "${lookup(var.sqsq_primary[count.index], "name")}-${var.environment}"
## Conditionally Sets A Redrive Policy ##
redrive_policy = "${lookup(var.sqsq_primary[count.index], "maxReceiveCount") > 0 ? "{\"deadLetterTargetArn\":\"arn:aws:sqs:${var.region}:${var.acc_number}:${replace(lookup(var.sqsq_primary[count.index], "name"),"Queue","DeadLetterQueue")}-${var.environment}\",\"maxReceiveCount\":${lookup(var.sqsq_primary[count.index], "maxReceiveCount")}}" : ""}"
depends_on = ["aws_sqs_queue.qdl"]
}
resource "aws_sqs_queue" "qdl" {
count = "${length(compact(data.empty_data_source.deadletterq.*.outputs.dl))}"
name = "${element(compact(data.empty_data_source.deadletterq.*.outputs.dl), count.index)}-${var.environment}"
}

Resources