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".
Related
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"
}
}
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]
On terraform v0.14.4
My variable looks like this:
variable "my_config" {
type = object({
instances = set(string)
locations = set(string)
})
default = {
locations = [
"us",
"asia"
]
instances = [
"instance1",
"instance2"
]
}
I want to loop over this var in a resource and create an instance of the resource for each location + instance. The "name" field of the resource will be "<LOCATION>_<INSTANCE>" as well.
I could create a new var in locals that reads the my_config var and generates a new var that looks like this:
[
"us_instance1",
"us_instance2",
"asia_instance1",
"asia_instance2",
]
I would prefer to not generate a new terraform var from this existing var though. Is it possible in a foreach loop to aggregate these two lists directly in a resource definition? Or is the only way to create a new data structure in locals?
EDIT
I cannot get the flatten example in answer provided to work inside a resource definition. I get this error: The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type tuple. This error happens if the type is set(string) or list(string).
# This works
output "test" {
value = flatten(
[
for location in var.my_config.locations : [
for instance in var.my_config.instances : "${location}_${instance}"
]
]
)
}
# This throws the error
resource "null_resource" "test" {
for_each = flatten(
[
for location in var.my_config.locations : [
for instance in var.my_config.instances : "${location}_${instance}"
]
]
)
provisioner "local-exec" {
command = "echo test"
}
}
To achieve the return value of:
[
"us_instance1",
"us_instance2",
"asia_instance1",
"asia_instance2",
]
with the input of the variable my_config, you could:
flatten([for location in var.my_config.locations : [
for instance in var.my_config.instances : "${location}_${instance}"
]])
Whether or not you define this in a locals block is up to you. If you plan on re-using this value multiple times, then it would be more efficient to define it as a local. If you plan on on only using it once, then it would certainly make more sense to not define it in locals.
Note this also assumes my_config type is object(list(string)). The type was not given in the question, but if the type were otherwise then the code becomes much more obfuscated.
For the additional question about using this value as a for_each meta-argument value at the resource scope, it would need to be converted to type set(string). This can be done easily with the toset function:
resource "resource" "this" {
for_each = toset(<expression above or variable with return value of above assigned to it>)
}
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
I have a list I want to loop over to create resources:
variable "mylist" {
description = "my list"
default = ["a","b","c"]
}
resource "aws_instance" "sdfsdfd" {
count = length(var.mylist)
tags = element(var.bommie_computer_name, count.index)
How can I have terraform not destory resources if the order of the array changes? I want to be able to change ["a","b","c"] to ["c","b","a"] and have terraform not destroy and recreate anything. Do I have to use for_each to get this behavior?
Use a for_each instead of count whenever possible so if you change the list, it won't try to recreate resources.
variable "mylist" {
description = "my list"
default = ["a","b","c"]
}
resource "aws_instance" "example" {
for_each = toset(var.mylist)
# ...
}