Terraform v12x For Each Conditional - terraform

I have a list of subnets and wish to join them if they have an the enabled flag set to true.
locals {
env_whitelisted_ips = {
default = [
{ subnets = "${data.terraform_remote_state.infra.outputs.subnet_ids}", enabled = "true"},
{ subnets = "${data.terraform_remote_state.infra.outputs.public_subnet_ids}", enabled = "false"},
{ subnets = "${data.terraform_remote_state.infra.outputs.nlb_public_subnet_ids}", enabled = "false"},
{ subnets = "${data.terraform_remote_state.infra.outputs.vpc_cidr_block}", enabled = "false"}
]
}
Now print it the required values, set to true.
resource "null_resource" "default_3" {
for_each = { for k, v in local.env_whitelisted_ips : k => v }
triggers = {
subnet_value = jsonencode(each.value)
account_key = jsonencode(each.key)
}
}
And this currently produces.
+ triggers = {
+ "account_key" = "\"default\""
+ "subnet_value" = jsonencode(
[
+ {
+ enabled = "true"
+ subnets = [
+ "subnet-xxx",
+ "subnet-yyy",
+ "subnet-zzz",
]
}
But what I'm seeking is:
resource "null_resource" "default_3" {
for_each = { for k, v in local.env_whitelisted_ips : k => v.contains["enabled"] == true }
triggers = {
subnet_value = jsonencode(each.value.subnets)
}
}
Producing values of:
+ triggers = {
+ "account_value" = jsonencode(
[
+ {
+ subnets = [
+ "subnet-xxx",
+ "subnet-yyy",
+ "subnet-zzz",
]
}
Any idea would be much appreciated :)

So the fix was to change the local construct like so:
locals {
env_whitelisted_ips = [
{ subnet_block = "${data.terraform_remote_state.infra.outputs.subnet_cidr_block}",
enabled = "true"
},
{ subnet_block = "${data.terraform_remote_state.infra.outputs.public_subnet_cidr_block}",
enabled = "false"
},
{ subnet_block = "${data.terraform_remote_state.infra.outputs.nlb_subnet_cidr_block}",
enabled = "false"
},
{ subnet_block = "${data.terraform_remote_state.infra.outputs.vpc_cidr_block}",
enabled = "false"
}
]
}
And to update the nested loops like so:
resource "null_resource" "get_vpc_cidrs" {
for_each = { for k, v in (local.env_whitelisted_ips) : k => v if v.enabled != "false" }
triggers = {
whitelisted = "${join(",", each.value["subnet_block"])}"
}
}
And the output is:
# null_resource.get_vpc_cidrs["0"] will be created
+ resource "null_resource" "get_vpc_cidrs" {
+ id = (known after apply)
+ triggers = {
+ "whitelisted" = "172.27.128.0/20,172.27.144.0/20,172.27.160.0/20"
}
}

Related

Terraform - Lost in the Flatten Sequence

I have lost the flow of this 'flatten' sequence and need help in ensuring that the traffic manager endpoints are properly represented in the flatten sequence. Although I have gotten to a point where terraform plan works, it is creating the endpoints by Index[0] and Index[1] instead of Indexing it by name.
Below is the code and sample input (in json format). The Azure Traffic Manager profile and Traffic Endpoints (Azure endpoint) exist in the same module.
Plan Output
# module.infra.azurerm_traffic_manager_azure_endpoint.tm_azure_endpoint["0"] will be created
+ resource "azurerm_traffic_manager_azure_endpoint" "tm_azure_endpoint" {
+ enabled = true
+ geo_mappings = [
+ "US",
]
+ id = (known after apply)
+ name = "0"
+ priority = 1
+ profile_id = "/subscriptions/3f128ee2-eef5-4ede-ac7e-42cf4a4f8632/resourceGroups/rg-eastus-tmprofile/providers/Microsoft.Network/trafficManagerProfiles/traf-eastus-01"
+ target_resource_id = "/subscriptions/3f128ee2-eef5-4ede-ac7e-42cf4a4f8632/resourceGroups/rg-eastus-tmprofile/providers/Microsoft.Network/publicIPAddresses/pip-core-vng-eus-01"
+ weight = 1
}
# module.infra.azurerm_traffic_manager_azure_endpoint.tm_azure_endpoint["1"] will be created
+ resource "azurerm_traffic_manager_azure_endpoint" "tm_azure_endpoint" {
+ enabled = true
+ geo_mappings = [
+ "US",
]
+ id = (known after apply)
+ name = "1"
+ priority = 1
+ profile_id = "/subscriptions/3f128ee2-eef5-4ede-ac7e-42cf4a4f8632/resourceGroups/rg-eastus-taf-eastus-01"
+ target_resource_id = (known after apply)
+ weight = 1
}
MAIN
locals {
tm_profile = {
for k, v in try(local.inputs.tm_profiles, {}) : k => merge(
{
existing = false
traffic_view_enabled = false //optional feature. $2 per million data points processed. Data points are essentially queries into Traffic Manager.
interval_in_seconds = 30
timeout_in_seconds = 10
tolerated_number_of_failures = 3
},
v,
{
tags = merge(
local.tags,
try(v.tags, {})
)
}
)
}
tmprofile_endpoints = flatten([
for tm_k, tm_v in try(local.inputs.tm_profiles, {}) : [
for tm_endpoints_k, tm_endpoints_v in try(tm_v.tm_endpoints, {}) : merge(
tm_endpoints_v,
{
name = tm_endpoints_k
profile_id = azurerm_traffic_manager_profile.traffic_manager_profile[tm_endpoints_v["tm_profile_name"]].id
target_resource_id = azurerm_public_ip.pub_ips[tm_endpoints_v["pub_ip"]].id
enabled = true
protocol = tm_v.protocol
}
)
]
])
}
resource "azurerm_traffic_manager_profile" "traffic_manager_profile" {
for_each = {
for k, v in local.tm_profile : k => v if !v.existing
}
name = each.key
resource_group_name = each.value.rg
profile_status = each.value.profile_status
traffic_routing_method = each.value.traffic_routing_method
dns_config {
relative_name = each.value.relative_name
ttl = each.value.ttl
}
monitor_config {
protocol = each.value.protocol
port = each.value.port
path = (each.value.protocol != "TCP") ? each.value.path : lookup(each.value, "path", null)
expected_status_code_ranges = try(each.value.expected_status_code_ranges, "200")
dynamic "custom_header" {
for_each = (each.value.protocol == "HTTP" || each.value.protocol == "HTTPS") ? try(each.value.custom_headers, {}) : {}
content {
name = each.value.name
value = each.value.value
}
}
interval_in_seconds = each.value.interval_in_seconds
timeout_in_seconds = each.value.timeout_in_seconds
tolerated_number_of_failures = each.value.tolerated_number_of_failures
}
tags = each.value.tags
depends_on = [
azurerm_resource_group.rgs
]
}
resource "azurerm_traffic_manager_azure_endpoint" "tm_azure_endpoint" {
for_each = {
for k, v in local.tmprofile_endpoints : k => v }
name = each.key
profile_id = each.value.profile_id
target_resource_id = each.value.target_resource_id
weight = each.value.weight
dynamic "custom_header" {
for_each = (each.value.protocol == "HTTP" || each.value.protocol == "HTTPS") ? try(each.value.custom_headers, {}) : {}
content {
name = each.value.name
value = each.value.value
}
}
enabled = each.value.enabled
geo_mappings = each.value.geo_mappings
priority = each.value.priority
depends_on = [
azurerm_traffic_manager_profile.traffic_manager_profile
]
}
/*
output "tmprofile_endpoints_out" {
value = local.tmprofile_endpoints
}
*/
Sample Input
"tm_profiles": {
"traf-eastus-01": {
"rg": "rg-eastus-tmprofile",
"profile_status": "Enabled",
"traffic_routing_method": "Performance",
"relative_name": "azmech",
"ttl": 3600,
"protocol": "HTTPS",
"port": 443,
"path": "/",
"expected_status_code_ranges": [
"200-201"
],
"custom_header": {
"name": "customheader",
"value": null
},
"tags": {
"tm_profile": "tm_test"
},
"tm_endpoints": {
"tfendpoint1": {
"tm_profile_name": "traf-eastus-01",
"weight": "1",
"custom_header": {
"name": "customheader",
"value": null
},
"geo_mappings": [
"US"
],
"priority": "1",
"pub_ip": "pip-core-vng-eus-01"
},
"tfendpoint2": {
"tm_profile_name": "traf-eastus-01",
"weight": "1",
"custom_header": {
"name": "customheader",
"value": null
},
"geo_mappings": [
"US"
],
"priority": "1",
"pub_ip": "pip-core-vng-wus-01"
}
}
}
}
nice code ;)
If I am getting it right, the flatten causes the indexing. Since flatten works with lists, local.tmprofile_endpoints is a list.
I would project it back into a map where each key is unique. You can try to produce a single unique key per instance like this:
local_tmprofile_endpoints_map = {
for endpoint in local.tmprofile_endpoints : "${endpoint.tm_profile_name}" => endpoint
}
..and work on this from now on.

Access variable in nested map with for_each

I have local variable:
locals {
bucket = {
firstBucket = {
sse = true
lifecycle_rules = [
{
id = "firstBucket"
enabled = true
expiration = {
days = 7
}
}
]
}
secondBucket = {
sse = false
lifecycle_rules = [
{
id = "secondBucket"
enabled = true
expiration = {
days = 7
}
}
]
}
}
}
I want first bucket to be encrypted (sse=true) and the second one should be encrypted (sse=false)
Then I try to create two s3 buckets using module. I want to use sse field defined in a local variable to set security options:
module "gitlab_bucket" {
for_each = local.bucket
/* some stuff */
server_side_encryption_configuration = lookup(each.value, "sse", null) ? var.security_cofig : {}
}
But it returns error The given key does not identify an element in this collection value
The syntax seems okay, but the default value(when sse attribute is missing) will have to be a boolean value (either true or false, so can't be null) for conditional expression.
I tested the below code in terraform 13.5, and it gave the expected result.
locals {
bucket = {
firstBucket = {
sse = true
lifecycle_rules = [
{
id = "firstBucket"
enabled = true
expiration = {
days = 7
}
}
]
}
secondBucket = {
#sse = false
lifecycle_rules = [
{
id = "secondBucket"
enabled = true
expiration = {
days = 7
}
}
]
}
}
}
resource "random_pet" "example" {
for_each = local.bucket
keepers = {
sse = lookup(each.value, "sse", false) ? jsonencode({x = "yes"}) : jsonencode({})
}
}
Below was the plan result:
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# random_pet.example["firstBucket"] will be created
+ resource "random_pet" "example" {
+ id = (known after apply)
+ keepers = {
+ "sse" = jsonencode(
{
+ x = "yes"
}
)
}
+ length = 2
+ separator = "-"
}
# random_pet.example["secondBucket"] will be created
+ resource "random_pet" "example" {
+ id = (known after apply)
+ keepers = {
+ "sse" = jsonencode({})
}
+ length = 2
+ separator = "-"
}
Plan: 2 to add, 0 to change, 0 to destroy.

Using dynamic values for Kubernetes namespace labels

I am managing my on-prem Kubernetes cluster namespaces with Terraform and want to include some custom labels/annotations on them. This is to make auditing easier and also we have mutating webhooks that rely on labels/annotations.
I am trying to do something like this (pseudo code)
resource "kubernetes_namespace" "namespaces" {
for_each = {for k, v in var.namespaces: k => v}
metadata {
name = each.value.name
annotations = {
"linkerd.io/inject" = each.value.linkerd
{{loop over each.value.custom_annotations}}
}
labels = {
"apps.kubernetes.io/app" = each.value.app
"k8s.domain.co/managed-by" = each.value.managed
"k8s.domain.co/owner" = each.value.owner
{{loop over each.value.custom.labels}}
}
}
}
I have my var.namespaces variable constructed like
description = "List of namespaces controlled by Terraform"
type = list(object({
name = string
linkerd = string
app = string
owner = string
managed = string
custom_annotations = list(object({
label = string
value = string
}))
custom_labels = list(object({
label = string
value = string
}))
}))
I am trying to end up with
namespaces = [
{
name = foo
...
custom_annotations = {
label = "myannotation"
value = "myvalue"
custom_labels = {
label = "mylabel"
value = "myvalue"
}]
resource "kubernetes_namespace" "namespaces" {
for_each = {for k, v in var.namespaces: k => v}
metadata {
name = each.value.name
annotations = {
"linkerd.io/inject" = each.value.linkerd
myannotation = myvalue
}
labels = {
"apps.kubernetes.io/app" = each.value.app
"k8s.domain.co/managed-by" = each.value.managed
"k8s.domain.co/owner" = each.value.owner
mylabel = myvalue
}
}
}
I have a feeling some mix of locals and dynamic blocks would be the solution but I can't seem to pin them together in a way that works
Any advice please?
I managed to get this almost working for myself without using locals or dynamic blocks. However I can't include the default labels and annotations
resource "kubernetes_namespace" "namespaces" {
for_each = { for k, v in var.namespaces: k => v} //loop over the namespaces
metadata {
name = each.value.name
annotations = {
for annotation in each.value.custom_annotations: annotation.label => annotation.value
}
labels = {
for label in each.value.custom_labels: label.label => label.value
}
}
}
With this input
namespaces = [
{
name = "metallb-system"
linkerd = "enabled"
app = "metallb"
owner = "KPE"
managed = "Terraform"
custom_annotations = []
custom_labels = [{label="foo.io/bar", value="foobar"}, {label="bar.io/foo", value="barfoo"}]
},
{ name = "test-ns"
linkerd = "enabled"
app = "myapp"
owner = "Me"
managed = "Terraform"
custom_annotations = [{label="foo.io/annotation", value="test"}]
custom_labels = [{label="app.io/label", value="value"}]
}
]
It gives me this output
Changes to Outputs:
+ namespaces = {
+ 0 = {
+ id = "metallb-system"
+ metadata = [
+ {
+ annotations = {}
+ generate_name = ""
+ generation = 0
+ labels = {
+ "bar.io/foo" = "barfoo"
+ "foo.io/bar" = "foobar"
}
+ name = "metallb-system"
+ resource_version = "410142"
+ uid = "02d6b1e1-707a-49cf-9a2d-3f28c9ce1e5a"
},
]
+ timeouts = null
}
+ 1 = {
+ id = (known after apply)
+ metadata = [
+ {
+ annotations = {
+ "foo.io/annotation" = "test"
}
+ generate_name = null
+ generation = (known after apply)
+ labels = {
+ "app.io/label" = "value"
}
+ name = "test-ns"
+ resource_version = (known after apply)
+ uid = (known after apply)
},
]
+ timeouts = null
}
}
I found a way to add default labels and annotations using setunion
locals {
default_annotations = [{label = "foo", value = "bar"}]
default_labels = [{label = "terraform", value = true}]
}
resource "kubernetes_namespace" "namespaces" {
for_each = { for k, v in var.namespaces: k => v} //loop over the namespaces
metadata {
name = each.value.name
annotations = {
for annotation in setunion(each.value.custom_annotations, local.default_annotations) : annotation.label => annotation.value
}
labels = {
for label in setunion(each.value.custom_labels, local.default_labels) : label.label => label.value
}
}
}
I know this doesn't exactly solve your use case, as you are wanting to read the value from your list of namesapces, however I do think it is one step closer!

alternative way, since for_each and count cannot be in same resource

I've config that need to use count and for_each at the same time.
here's the config block
variable "all_zone" {
type = list(any)
default = ["asia-southeast1-a", "asia-southeast1-b", "asia-southeast1-c"]
}
variable "backends" {
description = "Map backend indices to list of backend maps."
type = map(object({
neg_name = string
}))
}
data "google_compute_network_endpoint_group" "get_neg" {
for_each = var.backends
count = length(var.all_zone)
zone = var.all_zone[count.index]
name = lookup(each.value, "neg_name")
}
resource "google_compute_backend_service" "default" {
. . .
dynamic "backend" {
for_each = [for b in data.google_compute_network_endpoint_group.get_neg[*].id : b]
content {
group = backend.value
}
}
}
is there anyway to do this?
Update: here's sample var.backends
backends = {
default = {
neg_name = 'name-1'
}
}
Update: Thanks for #marcin for the solution. but I've another problem to acessing this data.
Before I use helper_map
data "google_compute_network_endpoint_group" "get_neg" {
count = length(var.all_zone)
zone = var.all_zone[count.index]
name = 'name-1'
}
and here structure off the output data:
neg = [
+ {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-a/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-a"
},
+ {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-b/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-b"
},
+ {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-c/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-c"
},
]
here's I accessing the data
for_each = [for b in data.google_compute_network_endpoint_group.get_neg[*].id : b]
after use map_helper
neg = [
+ {
+ default-asia-southeast1-a = {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-a/networkEndpointGroups/k8s1-e051d246-default-gclb-poc-8080-ef51ff1c"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-a"
}
+ default-asia-southeast1-b = {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-b/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-b"
}
+ default-asia-southeast1-c = {
+ id = "projects/k8s-playground-public/zones/asia-southeast1-c/networkEndpointGroups/name-1"
+ name = "name-1"
+ zone = "https://www.googleapis.com/compute/v1/projects/k8s-playground-public/zones/asia-southeast1-c"
}
},
]
and how i access this id of data.
I'm not sure exactly what you want to achieve with your data structures, but one way to overcome your issue, would be to create local helper variable, which would be the combination of your backends and all_zone. For example:
variable "backends" {
description = "Map backend indices to list of backend maps."
type = map(object({
neg_name = string
}))
default = {
default = {
neg_name = "name-1"
}
}
}
variable "all_zone" {
type = list(any)
default = ["asia-southeast1-a", "asia-southeast1-b", "asia-southeast1-c"]
}
locals {
helper_map = merge([
for backend_key, backend_value in var.backends:
{
for zone in var.all_zone:
"${backend_key}-${zone}" => {
backend_value = backend_value.neg_name
zone = zone
}
}
]...)
}
which gives:
{
"default-asia-southeast1-a" = {
"backend_value" = "name-1"
"zone" = "asia-southeast1-a"
}
"default-asia-southeast1-b" = {
"backend_value" = "name-1"
"zone" = "asia-southeast1-b"
}
"default-asia-southeast1-c" = {
"backend_value" = "name-1"
"zone" = "asia-southeast1-c"
}
}
Then, you can easily iterate over this join structure (example only):
data "google_compute_network_endpoint_group" "get_neg" {
for_each = local.helper_map
zone = each.value.zone
name = each.value.backend_value
}
The above will probably require further adjustment to match your data structures, but the general idea of using local helper variable remains same.

Create multiple statements in aws_iam_policy_document with values from list of values (TF 1.13)

I have the following variable
variable "roles" {
type = set(string)
default = [
"A",
"B",
]
}
And I want to create a aws_iam_policy_document with a sts:AssumeRole action for each of those values.
I tried
data "aws_iam_policy_document" "service_role_trust_node_workers" {
statement {
effect = "Allow"
principals {
identifiers = ["ec2.amazon.com"]
type = "Service"
}
actions = ["sts:AssumeRole"]
}
for_each = var.roles
statement {
effect = "Allow"
sid = "${each.key}-${each.value}"
principals {
identifiers = [
each.value
]
type = "AWS"
}
actions = [
"sts:AssumeRole"
]
}
}
But this produces this
json = jsonencode(
{
+ Statement = [
+ {
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = {
+ Service = "ec2.amazon.com"
}
+ Sid = ""
},
+ {
+ Action = "sts:AssumeRole"
+ Effect = "Allow"
+ Principal = {
+ AWS = "B"
}
+ Sid = "B-B"
},
]
+ Version = "2012-10-17"
}
)
So for some reason, A is ignored.
Any suggestions?
Ok, found it :)
dynamic "statement" {
for_each = var.roles
iterator = role
content {
effect = "Allow"
principals {
identifiers = [
role.value
]
type = "AWS"
}
actions = [
"sts:AssumeRole"
]
}
}

Resources