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}"]
}
Related
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)
...
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 am trying to rate limit requests to the forgot password change URL using WAFv2 rules attached to an ALB on Cloudfront.
What I think I need to do is..
Create two resources aws_wafv2_web_acl.afv2_rate_limit and another called aws_wafv2_regex_pattern_set.wafv2_password_url
Example of rate: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl
Example of regex: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_regex_pattern_set
Combine these into a rule group, call it aws_wafv2_rule_group.wafv2_rule_group_pw_rate_group
Example of group: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_rule
I've created the rate limit and the regex, but I am failing to create the rule group. I put this rule in to refer to the rate limit
rule {
name = "rate_limit"
priority = 1
action {
block {}
}
statement {
and_statement {
statement {
rule_group_reference_statement { # !!FIXME!! doesn't work
arn = aws_wafv2_web_acl.wafv2_rate_limit.arn
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "password_url"
sampled_requests_enabled = false
}
}
I get the error on the rule_group_reference_statement line:
Blocks of type "rule_group_reference_statement" are not expected here.
I can attach the rule group to the ALB.
of course, the first question is whether this is even the right way to go about it?!
thanks for any thoughts.
working!
resource "aws_wafv2_web_acl" "wafv2_alb_pw5pm_acl" {
name = "wafv2_alb_pw5pm-acl"
description = "prevent brute forcing password setting or changing"
scope = "REGIONAL" # if using this, no need to set provider
default_action {
allow {} # pass traffic until the rules trigger a block
}
rule {
name = "rate_limit_pw5pm"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
#limit = 300 # 5 per sec = 300 per min
limit = 100 # smallest value for testing
aggregate_key_type = "IP"
scope_down_statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.wafv2_password_uri.arn
text_transformation {
priority = 1
type = "NONE"
}
field_to_match {
uri_path {}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "wafv2_alb_pw5pm_acl_rule_vis"
sampled_requests_enabled = false
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "wafv2_alb_pw5pm_acl_vis"
sampled_requests_enabled = false
}
tags = {
managedby = "terraform"
}
}
resource "aws_wafv2_web_acl_association" "web_acl_association_my_lb" {
resource_arn = aws_lb.xxxxxx.arn
web_acl_arn = aws_wafv2_web_acl.wafv2_alb_pw5pm_acl.arn
}
You can't nest a rule_group_reference_statement, for example for use inside a and_statement, not_statement or or_statement. It can only be referenced as a top-level statement within a rule.
yes you can. basically you need to declare an aws_wafv2_regex_pattern_set, in this example I use the URI "/api/*" but it can be a fixed one too.
resource "aws_wafv2_regex_pattern_set" "regex_pattern_api" {
name = "regex-path-api"
scope = "REGIONAL"
regular_expression {
regex_string = "/api/.+"
}
}
and the here is an example on how to use it on a waf declaration:
resource "aws_wafv2_web_acl" "waf" {
name = "waf"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "RateLimit"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
aggregate_key_type = "IP"
limit = 100
scope_down_statement {
regex_pattern_set_reference_statement {
arn = aws_wafv2_regex_pattern_set.regex_pattern_api.arn
field_to_match {
uri_path {}
}
text_transformation {
priority = 1
type = "NONE"
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimit"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = false
metric_name = "waf"
sampled_requests_enabled = false
}
}
The cool part of this is that it is a rate limit that narrows the filter based on client IP using scope_down_statement
I am creating a bunch of Route 53 resolver rules in each region and this works great with for_each loop:
resource "aws_route53_resolver_rule" "resolver-rule" {
for_each = var.resolver-rules
domain_name = each.value.domain-name
name = format("core-%s", each.value.domain-label)
rule_type = "FORWARD"
resolver_endpoint_id = aws_route53_resolver_endpoint.outbound-endpoint.id
target_ip {
ip = each.value.forwarder1
}
target_ip {
ip = each.value.forwarder2
}
tags = merge(var.tags, map("Name", format("core-%s-%s-%s", var.team_name, var.environment, each.value.domain-label)))
}
My var looks like this:
variable "resolver-rules" {
description = "A map of parameters needed for each resolver rule"
type = map(object({
domain-name = string
domain-label = string
forwarder1 = string
forwarder2 = string
}))
}
resolver-rules = {
"resolver-rule1" = {
domain-name = "10.in-addr.arpa."
domain-label = "10-in-addr-arpa"
forwarder1 = "10.10.1.100"
forwarder2 = "10.10.2.100"
}
"resolver-rule2" = {
domain-name = "mycompany.com."
domain-label = "mycompany-com"
forwarder1 = "10.10.1.100"
forwarder2 = "10.10.2.100"
}
}
Now I need to associate those rules with resource share (not posting here):
resource "aws_ram_resource_association" "rule-association" {
for_each = var.resolver-rules
resource_arn = aws_route53_resolver_rule.resolver-rule.arn
resource_share_arn = aws_ram_resource_share.rte53-resolver-share.arn
}
Question: how do I modify (enumerate) my resource association part, so that each association would be created for each rule I define (it has to be 1 to 1 association). I just can't wrap my head around it :)
Your aws_route53_resolver_rule.resolver-rule will be a map with keys of resolver-rule1 and resolver-rule2.
Therefore to access its arn in your rule-association you could do the following:
resource "aws_ram_resource_association" "rule-association" {
for_each = var.resolver-rules
resource_arn = aws_route53_resolver_rule.resolver-rule[each.key].arn
resource_share_arn = aws_ram_resource_share.rte53-resolver-share.arn
}
In your question, aws_ram_resource_share.rte53-resolver-share.arn is not shown, thus I don't know if you also require some changes to it or not.