Terraform: How do I use for_each over a comma-delimited string in a dynamic block? - terraform

I am writing an Azure Function app module which is causing me some trouble. I want to add multiple ip_restriction blocks using a for_each, iterating over a comma-delimited string, but I'm missing out on something here.
Given the following block (with random IP CIDR blocks)
resource "azurerm_windows_function_app" "this" {
...
dynamic "ip_restriction" {
for_each = split(",", "1.2.3.4/28,2.3.4.5/28")
content {
ip_address = {
ip_address = ip_restriction.value
}
}
}
...
I get the following error:
Inappropriate value for attribute "ip_address": string required
I get the error twice which tells me that the iterator has tried, and failed twice to retrieve the value using '.value'
I've read https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks (of course) and tried various things for a few hours now without being able to figure out why 'value' seems empty.
How do I retrieve the distinct CIDR address if .value isn't the right thing to do?

The for_each meta-argument accepts a map or a set of strings. What you are currently providing is a list of strings. If you are not hardcoding the values as in your example, you could create a local variable:
locals {
ip_addresses = split(",", "1.2.3.4/28,2.3.4.5/28")
}
Then, in the dynamic block, you can use that local variable and cast it to a set with toset [1]:
resource "azurerm_windows_function_app" "this" {
...
dynamic "ip_restriction" {
for_each = toset(local.ip_addresses)
content {
ip_address = ip_restriction.value
}
}
...
If you do not want to do that, you can do the conversion like this:
resource "azurerm_windows_function_app" "this" {
...
dynamic "ip_restriction" {
for_each = toset(split(",", "1.2.3.4/28,2.3.4.5/28"))
content {
ip_address = ip_restriction.value
}
}
...
[1] https://developer.hashicorp.com/terraform/language/functions/toset

Dammit... A collegue of mine found the error, it was sitting approx. 40 inches from the keyboard :o)
It was a syntax beef, I had wrapped the "ip_address" block in an "ip_address" block
The correct code is:
dynamic "ip_restriction" {
for_each = split(",", var.ip_restriction_allow_ip_range)
content {
ip_address = ip_restriction.value
name = "github_largerunner"
}
}

Related

dynamic block to skip vpc config to lambda

I would like to skip adding a vpc to lambda in certain env. The current terraform code to update vpc is like below
data "aws_subnet" "lambda-private-subnet_1" {
availability_zone = var.environment_type_tag != "prd" ? "us-east-1a" : null
dynamic "filter" {
for_each = var.environment_type_tag == "prd" ? [] : [1]
content {
name = "tag:Name"
values = [var.subnet_value]
}
}
}
resource "aws_lambda_function" "tests" {
dynamic "vpc_config" {
for_each = var.environment_type_tag == "prd" ? [] : [1]
content {
subnet_ids = [data.aws_subnet.lambda-private-subnet_1.id]
security_group_ids = [var.security_group]
}
}
}
During 'terraform plan', the output is like below
##[error][1m[31mError: [0m[0m[1mmultiple EC2 Subnets matched; use additional constraints to reduce matches to a single EC2 Subnet[0m
I would like to skip the 'data "aws_subnet"' block if its 'prd' environment type.
So there are four different questions in this question. We can attempt to answer each one:
dynamic block to skip vpc config to lambda
This is already occurring with the given code. The dynamic blocks are "skipped" in prd with the current code.
I would like to skip adding a vpc to lambda in certain env.
If you mean "subnet" instead of "vpc", then this is also already occurring with the given code. Otherwise, please update with the vpc config.
##[error][1m[31mError: [0m[0m[1mmultiple EC2 Subnets matched; use additional constraints to reduce matches to a single EC2 Subnet[0m
The error message is due to the fact that your filters match multiple subnets outside of prd, and therefore you need to constrain the filter conditions.
I would like to skip the 'data "aws_subnet"' block if its 'prd' environment type.
You just need to extend your current code to make the data optional:
data "aws_subnet" "lambda-private-subnet_1" {
for_each = var.environment_type_tag == "prd" ? [] : toset(["this"])
...
}
You can then remove the for_each from the dynamic block in the resource as it is redundant, and update the attribute references with elements accordingly:
subnet_ids = [data.aws_subnet.lambda-private-subnet_1["this"].id]

nested for_each loop in terraform - accessing outer each

So I want do use terraform and configure my storage account so that each account that is not a file storage gets soft container deletion enabled, and if i provide a boolean flag "cors_enabled" in my account variables, add some cors rules. Since both of these are in the blob_properties block, I have to decide if I add a blob_properties block and then decide again if I have to add the re
The only way I could think of is to have a for_each loop over the variables, and then use nested dynamic blocks like this.
my vars.account looks somewhat like this:
{
"account1": {
"name": "account1",
"account_kind": "BlobStorage",
"account_replication_type": "LRS",
"cors_enabled": "true",
# other stuff that's not relevant
},
"account2": {
"name": "account1",
"account_kind": "FileStorage",
"account_replication_type": "LRS",
"cors_enabled": "true",
# other stuff that's not relevant
}
}
My hcl looks like this:
resource "azurerm_storage_account" "account" {
for_each = var.accounts
name = each.value.name
# [...] do some further configuation, set some required variables
dynamic blob_properties {
for_each = lookup(each.value, "account_kind", "StorageV2") != "FileStorage" || lookup(each.value, "cors_enabled", "false") ? [""] : []
content {
dynamic container_delete_retention_policy {
for_each = lookup(each.value, "account_kind", "StorageV2") != "FileStorage" ? [""] : []
content {
days = 30
}
}
dynamic cors_rule {
for_each = lookup(each.value, "cors_enabled", "false") ? [""] : []
content {
# some cors rule configuration
}
}
}
}
But this of course doesn't work, because the "each.value" in the container_delete_retention_policy refers to the each in the INNER for_each loop (in the dynamic blob_properties blob) and the same goes for the each.value in the cors_rule, when I want it to refer to the "each" infor_each = var.accounts
I tried using count instead of the outer for_each loop (and then do some magic with local maps so I can access the correct key/value by the count's index), but count seems to produce something different than for_each; when I use for_each, I can later use
myvariable = azurerm_storage_account.account["mykey"].name
so I guess it produces some kind of map. When I use count instead of for_each, I then get an error
azurerm_storage_account.acount is tuple with 8 elements The given key does not identify an element in this collection value: a number is required.
So I guess it produces a list?
Is there a way in terraform to access the "each" of an outer for_each loop in an inner for_each loop? If not, is there a different way to achieve what I want?
I got it wrong and got confused by an unrelated error I got - the code above behaves exactly like I want it to, i.e. always referring to the outermost "each".

Get ip adress and hostname output in terraform

So I start to explore Terraform, and so far Im able to get my network and VMs up in Azure.
Since I have seven VM`s that are created, I would like to get the IP-adress and the Hostname of this.
In my main.tf i have this:
provider "azurerm" {
features {}
# Configuration options
}
module "splunk_architect" {
source = "./modules/architect"
}
And just an example from my main.tf in /modules/architect
resource "azurerm_network_interface" "main" {
for_each = toset(var.vm_names)
name = "${each.value}-nic"
location = azurerm_resource_group.rsg.location
resource_group_name = azurerm_resource_group.rsg.name
ip_configuration {
name = "testconfiguration1"
subnet_id = azurerm_subnet.subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.pubip[each.key].id
}
And my outputs.tf in /modules/architect
output "ip" {
value = azurerm_network_interface.main[each.key].private_ip_address
}
So when i run this, i get this error message:
Error: Reference to "each" in context without for_each
on modules\architect\outputs.tf line 6, in output "ip": 6: value = azurerm_network_interface.main[each.key].private_ip_address
The "each" object can be used only in "module" or "resource" blocks, and only when the "for_each" argument is set.
So I have tried to set the for_each in the module, but did not get that to work.
I have also been through the documentations without any success.
Any tips to what I can try to get the IP of each VM printed out?
Since you have multiple virtual machines (declared with for_each), you will also have multiple private IP addresses to return. You'll need to decide for your module what is the best way to return those IP addresses to the caller.
One common answer is to return a map whose keys are the elements from var.vm_names, so the caller can easily correlate the VM names it passed in to the IP addresses you're returning, like this:
output "ip" {
value = tomap({
for name, vm in azurerm_network_interface.main : name => vm.private_ip_address
})
}
This is a for expression, which constructs a new data structure from an existing data structure by evaluating expressions against each element. In this case, it's taking the keys from azurerm_network_interface.main -- which will match the values given in for_each -- and mapping them to the private_ip_address attribute for each object.
The result will therefore appear as a map from VM names to IP addresses, perhaps like this:
{
"example1" = "10.1.2.1"
"example2" = "10.1.2.45"
}
Not directly related to your question, but note also that if your module only ever uses var.vm_names as a set then it can be better to declare it as a set in the first place, rather than converting it at each use, because then it'll be clearer to users of your module that the order of the strings inside doesn't matter and that there can't be two elements with the same string:
variable "vm_names" {
type = set(string)
}
With that declaration, var.vm_names will already be a set of strings and so you don't need to explicitly convert it in for_each:
for_each = var.vm_names

Terraform : Using for_each in module

I am using terraform version 0.14.3.
I have a module for creating an Azure Network Interface Card, as below:
resource "azurerm_network_interface" "nic" {
name = var.nic_name
location = var.location
resource_group_name = var.rg_name
ip_configuration {
name = var.ipconfig_name
subnet_id = var.subnet_id
private_ip_address_allocation = "Dynamic"
}
}
Its output is defined as :
output "nic_id" {
value = azurerm_network_interface.nic.id
}
I am calling this module in this parent module:
module "NIC" {
source = "./NIC"
for_each = var.nics
nic_name = each.value.nic_name
location = "eastus2"
rg_name = "abc-test-rg"
ipconfig_name = each.value.ipconfig_name
subnet_id = <subnet_id>
}
output "nic_ids" {
value = [for k in module.NIC.nic_id : k.id]
}
The NIC values are defined as below:
nics = {
nic1 = {
nic_name = "abc-nic-1"
ipconfig_name = "nic-1-ipconfig"
}
}
I want to loop around the NIC output IDs, and want them displayed.
When I run above code, I get below error in terraform plan :
Error: Unsupported attribute
on main.tf line 15, in output "nic_ids":
15: value = [for k in module.NIC.nic_id : k.id]
|----------------
| module.NIC is object with 1 attribute "nic1"
This object does not have an attribute named "nic_id".
How do I get around it ?
Your module "NIC" block has for_each set, and so the module.NIC symbol elsewhere in the module is a mapping from instance keys to output objects, rather than just a single output object as for a singleton module.
Terraform's error message is attempting to draw attention to that with the following message:
module.NIC is object with 1 attribute "nic1"
Notice that nic1 here is a key from your var.nics, and not one of the output values defined in your module.
Assuming that the nic_id output you showed here is the only one defined in that module, the module.NIC value would be shaped something like this:
{
nic1 = {
nic_id = "eni-e5aa89a3"
}
}
It sounds like you instead want to produce a value shaped like this:
{
nic1 = "eni-e5aa89a3"
}
If so, a suitable expression to get that result would be the following:
output "nic_ids" {
value = { for k, nic in module.NIC : k => nic.nic_id }
}
The above means: produce a mapping with one element for each instance of the NIC module, whose key is the module instance key and whose value is the nic_id output value.
Alternatively, if it doesn't matter which id belongs to which instance then you could produce an unordered set of ids, like this:
output "nic_ids" {
value = toset([for nic in module.NIC : nic.nic_id])
}
In this case the for expression only defines a local symbol nic, which represents the module instance object, because it doesn't do anything with the instance key. The toset here is to represent that the IDs are not in any particular order: that isn't strictly necessary but I think it's a good practice to make sure that any other Terraform code depending on that value doesn't inadvertently depend on the current arbitrary ordering of the ids, which might change in future if you add or remove elements in var.nics.

Writing DRY code with terraform for loop?

I am very new to terraform and had a task dropped upon me to create 2 AWS KMS keys.
So I am doing this:
resource "aws_kms_key" "ebs_encryption_key" {
description = "EBS encryption key"
... omitted for brevity ...
tags = merge(map(
"Name", format("%s-ebs-encryption-key", var.name_prefix),
"component", "kms",
"dataclassification","low",
), var.extra_tags)
}
resource "aws_kms_alias" "ebs_encryption_key" {
name = format("alias/%s-ebs-encryption-key", var.name_prefix)
target_key_id = aws_kms_key.ebs_encryption_key.key_id
}
# Repeated code!
resource "aws_kms_key" "rds_encryption_key" {
description = "RDS encryption key"
... omitted for brevity ...
tags = merge(map(
"Name", format("%s-rds-encryption-key", var.name_prefix),
"component", "kms",
"dataclassification","low",
), var.extra_tags)
}
resource "aws_kms_alias" "rds_encryption_key" {
name = format("alias/%s-rds-encryption-key", var.name_prefix)
target_key_id = "${aws_kms_key.rds_encryption_key.key_id}"
}
As you can see the only difference between the two blocks of code is "ebs" and "rds"?
How could I use a for loop to avoid repeating the code blocks?
This seems like it could be a candidate for a small module that encapsulates the details of declaring a key and an associated alias, since a key and an alias are typically declared together in your system.
The module itself would look something like this:
variable "name" {
type = string
}
variable "description" {
type = string
}
variable "tags" {
type = map(string)
}
resource "aws_kms_key" "main" {
description = var.description
# ...
tags = var.tags
}
resource "aws_kms_alias" "main" {
name = "alias/${var.name}"
target_key_id = aws_kms_key.main.key_id
}
output "key_id" {
value = aws_kms_key.main.key_id
}
output "alias_name" {
value = aws_kms_alias.main.name
}
(As written here this module feels a little silly because there's not really much here that isn't derived only from the variables, but I'm assuming that the interesting stuff you want to avoid repeating is in "omitted for brevity" in your example, which would go in place of # ... in my example.)
Your calling module can then include a module block that uses for_each to create two instances of the module, systematically setting the arguments to populate its input variables:
module "kms_key" {
for_each = {
kms = "KMS"
ebs = "EBS"
}
name = "${var.name_prefix}-${each.key}-encryption-key"
description = "${each.value} Encryption Key"
tags = merge(
var.extra_tags,
{
Name = "${var.name_prefix}-${each.key}-encryption-key"
component = "kms"
dataclassification = "low"
},
)
}
Since the for_each map here has the keys kms and ebs, the result of this will be to declare resource instances which should have the following addresses in the plan:
module.kms_key["kms"].aws_kms_key.main
module.kms_key["kms"].aws_kms_alias.main
module.kms_key["ebs"].aws_kms_key.main
module.kms_key["ebs"].aws_kms_alias.main
Since they are identified by the map keys, you can add new keys to that map in future to create new key/alias pairs without disturbing the existing ones.
If you need to use the key IDs or alias names elsewhere in your calling module then you can access them via the outputs exposed in module.kms_key elsewhere in that calling module:
module.kms_key["kms"].key_id
module.kms_key["kms"].alias_name
module.kms_key["ebs"].key_id
module.kms_key["ebs"].alias_name

Resources