Terraform Lookup returning a block - terraform

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

Related

How do I get a specific key in a nested map in terraform, when running in a for_each/for loop?

I have two sets of locals defined as below:
locals {
env1 = {
a = {
}
b = {
}
}
}
locals {
env2 = {
x = {
}
y = {
}
}
}
Now, I have to run a loop for local.env1 so that loop runs for a & b. But I also need "x" from local.env2 for a resource attribute in the same resource block the loops runs.
There are 2 resource blocks and it would look something like this:
resource_block1
{
for_each = local.env1
scope = each.key
id = something["x"]
}
resource_block2
{
for_each = local.env1
scope = each.key
id = something["y"]
}
For "id" attribute in both the resource blocks I don't want to hardcode "x" or "y" respectively. I want to programmatically fill "x" and "y" for "id" attribute in the respective resource blocks.
I tried to create a map for env1 and env2 I was able to fetch "a" and "b" from local.env1 but I am unable to fetch/filter only "x" or only "y" from it.
Any ideas/solutions please? Thank you for checking.

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

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

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.

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 }

Terraform variable definition

I would like to create a deployments input and drive my deployments based on that var.
Here's an example deployments input,
deployments = {
dev-1 = {
dev-api-us = {
hosts = ["dev-api-us.lm.com", "dev-api-us.lm1.com"]
}
region = "us-east1"
}
dev-2 = {
dev-api-uk = {
hosts = ["dev-api-uk.lm.com", "dev-api-uk.lm1.com"]
}
region = "europe-west2"
}
}
Is that a valid input if so what will the corresponding variable definition look like?
I tried this, but it says the syntax isn't right
variable "deployments" {
description = "A map of deployment"
type = map(map(object({
hosts = list(string)
})
region = string
))
}
Any help is appreciated.
This data structure is a bit confusing because the second level seems to be a mixture of predefined attributes (region) and arbitrary map keys (like dev-api-us and dev-api-uk).
I think the best way to proceed here would be to change this structure so that the second level of map is separate from the object it's embedded in. You didn't mention a noun in your question to refer to dev-api-us and dev-api-uk are examples of, so I'm going to just call them "host prefixes" for the sake of having something to call them:
{
dev-1 = {
host_prefixes = {
dev-api-us = {
hosts = ["dev-api-us.lm.com", "dev-api-us.lm1.com"]
}
}
region = "us-east1"
}
dev-2 = {
host_prefixes = {
dev-api-uk = {
hosts = ["dev-api-uk.lm.com", "dev-api-uk.lm1.com"]
}
}
region = "europe-west2"
}
}
A suitable type constraint for this adjusted data structure could be:
variable "deployments" {
type = map(object({
host_prefixes = map(object({
hosts = set(string)
}))
region = string
}))
}
This altered structure works within Terraform's type system because it separates the object-typed parts from the map-typed parts. The Terraform language doesn't have any type constraint that allows both map and object behaviors to be combined into a single value.

Resources