I need an if statement with for_each.
If it is non-prod create resource in a single subnet, if production create resources in both subnets:
locals {
zone_data = {
a = {
subnet_id = data.aws_cloudformation_export.subnet1.value
}
b = {
subnet_id = data.aws_cloudformation_export.subnet2.value
}
}
}
module "batch_server" {
for_each = var.env_name == "non-prod" ? local.zone_data.a : local.zone_data
...
I get an error:
|----------------
| local.zone_data is object with 2 attributes
| local.zone_data.a is object with 1 attribute "subnet_id"
The true and false result expressions must have consistent types.
The given expressions are object and object, respectively.
I tried this:
for_each = var.env_name == "non-prod" ? [local.zone_data.a] : [local.zone_data]
and am getting similar error:
|----------------
| local.zone_data is object with 2 attributes
| local.zone_data.a is object with 1 attribute "subnet_id"
The true and false result expressions must have consistent types.
The given expressions are tuple and tuple, respectively.
Tried changing types but nothing seems to work
for_each expects a map with one element for each instance you want to declare. That means that if you want to declare only one instance then you need to produce a one-element map.
Here's one way to do that:
for_each = var.env_name == "non-prod" ? tomap({a = local.zone_data.a}) : local.zone_data
This can work because both arms of the conditional produce a map of objects. The true arm produces a single-element map while the second produces the whole original map.
Another option to avoid the inline map construction would be to factor out a per-environment lookup table into a separate local value:
locals {
all_zones = tomap({
a = {
subnet_id = data.aws_cloudformation_export.subnet1.value
}
b = {
subnet_id = data.aws_cloudformation_export.subnet2.value
}
})
env_zones = tomap({
non-prod = tomap({
for k, z in local.all_zones : k => z
if k == "a"
})
})
}
module "batch_server" {
for_each = try(local.env_zones[var.env_name], local.all_zones)
# ...
}
The try function call here means that local.all_zones is the fallback to use if var.env_name doesn't match one of the keys in local.env_zones.
How about a lookup with var.env_name as the key?
variable "env_name" {
type = string
default = "non_prod"
}
locals {
zone_data = {
prod = {
a = {
subnet_id = data.aws_cloudformation_export.subnet1.value
}
b = {
subnet_id = data.aws_cloudformation_export.subnet2.value
}
},
non_prod = {
a = {
subnet_id = data.aws_cloudformation_export.subnet1.value
}
}
}
}
module "batch_server" {
for_each = lookup(local.zone_data, var.env_name)
...
Related
I'm trying to create an IP whitelist in nonprod for load testing, the WAF is dynamically created in prod and nonprod based on the envname/envtype:
resource "aws_waf_ipset" "pwa_cloudfront_ip_restricted" {
name = "${var.envname}-pwa-cloudfront-whitelist"
dynamic "ip_set_descriptors" {
for_each = var.cloudfront_ip_restricted_waf_cidr_whitelist
content {
type = ip_set_descriptors.value.type
value = ip_set_descriptors.value.value
}
}
}
resource "aws_waf_rule" "pwa_cloudfront_ip_restricted" {
depends_on = [aws_waf_ipset.pwa_cloudfront_ip_restricted]
name = "${var.envname}-pwa-cloudfront-whitelist"
metric_name = "${var.envname}PWACloudfrontWhitelist"
predicates {
data_id = aws_waf_ipset.pwa_cloudfront_ip_restricted.id
negated = false
type = "IPMatch"
}
}
resource "aws_waf_ipset" "pwa_cloudfront_ip_restricted_load_testing" {
name = "${var.envname}-pwa-cloudfront-whitelist_load_testing"
count = var.envtype == "nonprod" ? 1 : 0
dynamic "ip_set_descriptors" {
for_each = var.cloudfront_ip_restricted_waf_cidr_whitelist_load_testing
content {
type = ip_set_descriptors.value.type
value = ip_set_descriptors.value.value
}
}
}
resource "aws_waf_rule" "pwa_cloudfront_ip_restricted_load_testing" {
depends_on = [aws_waf_ipset.pwa_cloudfront_ip_restricted_load_testing]
count = var.envtype == "nonprod" ? 1 : 0
name = "${var.envname}-pwa-cloudfront-whitelist-load_testing"
metric_name = "${var.envname}PWACloudfrontWhitelistload_testing"
predicates {
data_id = aws_waf_ipset.pwa_cloudfront_ip_restricted_load_testing[count.index].id
negated = false
type = "IPMatch"
}
}
resource "aws_waf_web_acl" "pwa_cloudfront_ip_restricted" {
name = "${var.envname}-pwa-cloudfront-whitelist"
metric_name = "${var.envname}PWACloudfrontWhitelist"
default_action {
type = "BLOCK"
}
rules {
action {
type = "ALLOW"
}
priority = 1
rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted.id
type = "REGULAR"
}
rules {
action {
type = "ALLOW"
}
priority = 2
rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing.id
type = "REGULAR"
}
}
The second rules block throws and error in the terraform plan:
Error: Missing resource instance key
on waf.tf line 73, in resource "aws_waf_web_acl" "pwa_cloudfront_ip_restricted":
73: rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing.id
Because aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing has "count" set,
its attributes must be accessed on specific instances.
For example, to correlate with indices of a referring resource, use:
aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing[count.index]
However if I add [count.index] :
Error: Reference to "count" in non-counted context
on waf.tf line 73, in resource "aws_waf_web_acl" "pwa_cloudfront_ip_restricted":
73: rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing[count.index].id
The "count" object can only be used in "module", "resource", and "data"
blocks, and only when the "count" argument is set.
Is there a way to do this that doesn't use the count param? Or am I missing something in the way that I am using it?
Since there is difference between the prod and non-prod environment, the way this should be tackled is by using dynamic [1] and for_each meta-argument [2]:
resource "aws_waf_web_acl" "pwa_cloudfront_ip_restricted" {
name = "${var.envname}-pwa-cloudfront-whitelist"
metric_name = "${var.envname}PWACloudfrontWhitelist"
default_action {
type = "BLOCK"
}
dynamic "rules" {
for_each = var.envtype == "nonprod" ? [1] : []
content {
action {
type = "ALLOW"
}
priority = 1
rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted[0].id
type = "REGULAR"
}
}
dynamic "rules" {
for_each = var.envtype == "nonprod" ? [1] : []
content {
action {
type = "ALLOW"
}
priority = 2
rule_id = aws_waf_rule.pwa_cloudfront_ip_restricted_load_testing[0].id
type = "REGULAR"
}
}
}
[1] https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks
[2] https://developer.hashicorp.com/terraform/language/expressions/for
Variable
ip_set = [
{
name: "test-ip-set-1"
ip_list: ["10.0.0.1/32", "10.0.0.1/32"]
}
]
I want to loop through the ip_set variable and create IP sets per the length of ip_set and loop through ip_list within that dictionary
For e.g.
resource "aws_waf_ipset" "ipset" {
for_each = {for name, ip_list in var.ip_set: name => ip_list}
name = each.value.name
ip_set_descriptors {
type = "IPV4"
value = each.value.ip_list[ip_list_element_1]
}
ip_set_descriptors {
type = "IPV4"
value = each.value.ip_list[ip_list_element_2]
}
Error
If I do
ip_set_descriptors {
type = "IPV4"
value = tostring(each.value[*].ip_list)
}
I get
Invalid value for "v" parameter: cannot convert tuple to string.
FYI. value in ip_set_descriptors needs to be a string and I don't know how many elements are there
You can use dynamic blocks:
resource "aws_waf_ipset" "ipset" {
for_each = {for name, ip_list in var.ip_set: name => ip_list}
name = each.value.name
dynamic "ip_set_descriptors" {
for_each = each.value.ip_list
content {
type = "IPV4"
value = ip_set_descriptors.value
}
}
I would like to create a dynamic resource with for_each as well I would like to implement a resource creation condition, however by some reason which I don't understand, the resource creation condition is not working as expected.
The logic is very simple: I've a list of objects, each object is resource, therefore in each object a have a bool variable that enables creation of a resource.
resource "aws_autoscaling_group" "aws_asg" {
for_each = { for key, value in var.parameters : key => value if flatten([ for x in var.parameters : x.init ]) }
}
The problem in flatten([ for x in var.parameters : x.init ]), the resource takes first element, and ignores next element:
+ x = [
+ true,
+ false,
]
Here is var.parameters
parameters = [
# Runner 1
{
init = true
name = "test-runner-1"
scaling = {
desired = 3
maximum = 9
minimum = 3
}
},
# Runner 2
{
init = false
name = "test-runner-2"
scaling = {
desired = 3
maximum = 9
minimum = 3
}
}
]
}
Any idea how to map each boolean to resource creation?
If you want to conditionally loop through your parameters, then it should be:
resource "aws_autoscaling_group" "aws_asg" {
for_each = { for key, value in var.parameters : key => value if value.init }
}
I working on a module, provided below, to manage AWS KMS keys via Terraform and I'm using the flatten function but the output I'm getting is empty when I call this module.
Any thought why I'm getting empty output?
module
main.tf
locals {
kms_keys = flatten([
for key, kms_key in var.kms_key_list : [
for index in range(kms_key.key_id) : {
key_id = index
aws_kms_alias = kms_key.alias
is_rotating = kms_key.enable_key_rotation
deletion_window_in_days = kms_key.deletion_window_in_days
is_enabled = kms_key.is_enabled
description = kms_key.description
policy = kms_key.policy
}
]
])
}
resource "aws_kms_key" "main" {
for_each = {
for k, v in local.kms_keys: k => v if v.key_id > 0
}
deletion_window_in_days = each.value.deletion_window_in_days
is_enabled = each.value.is_enabled
enable_key_rotation = each.value.enable_key_rotation
description = each.value.description
policy = each.value.policy
tags = merge({
Name = each.value.aws_kms_alias
}, var.common_tags)
}
resource "aws_kms_alias" "alias" {
for_each = aws_kms_key.main
name = "alias/${each.value.tags.Name}"
target_key_id = each.value.key_id
}
variables.tf
variable "kms_key_list" {
type = map(object({
key_id = number
deletion_window_in_days = number
is_enabled = bool
enable_key_rotation = bool
description = string
policy = string
key_usage = string
customer_master_key_spec = string
alias = string
}))
}
calling the module in main.tf
module "kms_keys" {
source = "../module/kms"
kms_key_list = local.kms_keys
}
kms_keys.tf
locals {
kms_keys = {
name_1 = {
key_id = 1
deletion_window_in_days = 7
is_enabled = true
enable_key_rotation = true
description = "description_1"
policy = ""
key_usage = "ENCRYPT_DECRYPT"
customer_master_key_spec = "SYMMETRIC_DEFAULT"
alias = "alias_1"
}
}
}
TF Plan Output looks like this:
Changes to Outputs:
+ kms_info = {
+ kms_key = {}
}
This seems odd:
for index in range(kms_key.key_id)
This is going to loop through all values from 0 to the key_id value; is that really what you want? To add an entry into kms_keys for each value from 0 to key_id?
I doubt it, because the way you have this coded, if your var.kms_key_list contains a key config with key_id = 10, it's going to create 10 different KMS keys, all with the same configuration values.
Essentially, I'm not understanding the purpose of the nested for loop.
If you can provide samples of:
The input variable, but with a key_id > 1
The output that you expect to see
Then we might be able to help. Also, I don't see any output declared either in the module or in the parent file, so those must be missing; please include them.
Im trying to iterate through a variable type map and i'm not sure how to
This is what i have so far
In my main.tf:
resource "aws_route_53_record" "proxy_dns" {
count = "${length(var.account_name)}"
zone_id = "${infrastructure.zone_id}"
name = "proxy-${element(split(",", var.account_name), count.index)}-dns
type = CNAME
ttl = 60
records = ["{records.dns_name}"]
}
And in my variables.tf
variable "account_name" {
type = "map"
default = {
"account1" = "accountA"
"account2" = "accountB"
}
}
I want to be able to create multiple resources with the different account names
If you are using Terraform 0.12.6 or later then you can use for_each instead of count to produce one instance for each element in your map:
resource "aws_route53_record" "proxy_dns" {
for_each = var.account_name
zone_id = infrastructure.zone_id
name = "proxy-${each.value}-dns"
# ... etc ...
}
The primary advantage of for_each over count is that Terraform will identify the instances by the key in the map, so you'll get instances like aws_route53_record.proxy_dns["account1"] instead of aws_route53_record.proxy_dns[0], and so you can add and remove elements from your map in future with Terraform knowing which specific instance belongs to each element.
each.key and each.value in the resource type arguments replace count.index when for_each is used. They evaluate to the key and value of the current map element, respectively.
You can use a combination of map, keys function,index function, and count. This terraform creates 3 acls with various rules.
The names of the acl's are determined by the keys.
The number of acl's is determined by the count of the keys.
The index of each rule (priority) is determined by the index function
The name of each rule is from the CONTAINS_WORD or CONTAINS property in the map
=>
variable "acls" {
type = map(any)
default = {
"acl1" = {
"CONTAINS_WORD" = ["api","aaa", "bbb", "ccc"]
"CONTAINS" = ["xxx","yyy"]
}
"acl2" = {
"CONTAINS_WORD" = [ "url1,"url2","url3"]
"CONTAINS" = ["url4"]
}
"acl3" = {
"CONTAINS_WORD" = ["xxx"]
"CONTAINS" = []
}
}
}
resource "aws_wafv2_web_acl" "acl" {
name = keys(var.acls)[count.index]
scope = "REGIONAL"
count = length(keys(var.acls))
default_action {
block {}
}
dynamic "rule" {
for_each = toset(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD)
content {
name = rule.key
priority = index(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD, rule.key)
action {
allow {}
}
statement {
#https://docs.aws.amazon.com/waf/latest/APIReference/API_ByteMatchStatement.html
byte_match_statement {
positional_constraint = "CONTAINS_WORD"
search_string = lower(rule.key)
field_to_match {
uri_path {}
}
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "waf-${keys(var.acls)[count.index]}-${rule.key}"
sampled_requests_enabled = true
}
}
}
dynamic "rule" {
for_each = toset(var.acls[keys(var.acls)[count.index]].CONTAINS)
content {
name = replace(rule.key, ".", "_")
priority = index(var.acls[keys(var.acls)[count.index]].CONTAINS, rule.key) + length(var.acls[keys(var.acls)[count.index]].CONTAINS_WORD)
action {
allow {}
}
statement {
#https://docs.aws.amazon.com/waf/latest/APIReference/API_ByteMatchStatement.html
byte_match_statement {
positional_constraint = "CONTAINS"
search_string = lower(rule.key)
field_to_match {
uri_path {}
}
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "waf-${keys(var.acls)[count.index]}-${replace(rule.key, ".", "_")}"
sampled_requests_enabled = true
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "waf-${keys(var.acls)[count.index]}"
sampled_requests_enabled = true
}
}
Make the variable a list instead of a map. Maps are used to reference a name to a value. Lists are better for iterating over via a count method.
variable "account_name" {
type = "list"
default = {"accountA","accountB"}
}
resource "aws_route_53_record" "proxy_dns" {
count = "${length(var.account_name)}"
zone_id = "${infrastructure.zone_id}"
name = "proxy-${element(var.account_name, count.index)}-dns
type = CNAME
ttl = 60
records = ["{records.dns_name}"]
}