Can I use for_each meta-argument with file function? - terraform

I am creating a bunch of random strings using resource_string resource block.
length is a required argument for this resource, and my goal is to read all the values for this variable from a file, using the file function.
Is there a way to do it?
Here is my code, along with the error:
resource "random_string" "any_string" {
for_each = toset(file("string_number_file.txt"))
length = each.key
}
cat string_number_file.txt
"10","12","13"
Goal is to create three random_strings, with above lengths.
Here is the error with above code:
Error: Invalid function argument
│
│ on main.tf line 9, in resource "random_string" "any_string":
│ 9: for_each = toset(file("string_number_file.txt"))
│
│ Invalid value for "v" parameter: cannot convert string to set of any single type.
Thanks in advance!

In that case you can convert your file to json, and then use that:
resource "random_string" "any_string" {
for_each = toset(jsondecode(format("[%s]",file("string_number_file.txt"))))
length = each.key
}

Related

Terraform error A value of type string cannot be used as the collection in a 'for' expression

I have created a module to add an AWS.APIGateway route and authoriser.
The module is called depending on how many variables are in the tfsvars file.
This variable is passed through to the module to create the Route name as extprovider.
I need to take this variable and append a suffix to it so that i can then add multiple clientids (audiences) to the authoriser.
Error: Iteration over non-iterable value
on ../modules/apiresource/locals.tf line 3, in locals:
3: extprovider_new = flatten([ for e in var.extprovider : tolist(["${e}odd","${e}even"])])
├────────────────
│ var.extprovider is "foo"
A value of type string cannot be used as the collection in a 'for' expression.
Error: Iteration over non-iterable value
on ../modules/apiresource/locals.tf line 3, in locals:
3: extprovider_new = flatten([ for e in var.extprovider : tolist(["${e}odd","${e}even"])])
├────────────────
│ var.extprovider is "bar"
A value of type string cannot be used as the collection in a 'for' expression.
My code is;
locals.tf
locals {
extprovider_new = flatten([ for e in var.extprovider : tolist(["${e}odd","${e}even"])])
}
variables.tf
variable extprovider {}
You are passing a string variable to module but in module you want to use it as list.
You can pass variable like
module "module_name" {
...
extprovider = ["foo"]
...
}
and it should work
or
in locals.tf
locals {
extprovider_new = ["${var.extprovider}odd", "${var.extprovider}even"]
}
you can use string variable and convert it to list which you need.

Key Vault Secret Time Expiry

I am trying to set an expiry date that is dynamic in a Terraform template. The idea is to get current date and add 6 months to that date and use that as the expiry date for the secret, however I am struggling to do so.
I am trying to achieve this using the time_offset and timestamp() but it isn't working and I get the following error.
main.tf
resource "time_offset" "expiry_date" {
offset_months = 6
}
resource "azurerm_key_vault_secret" "local_admin_pwd" {
name = "LocalAdminPassword"
value = random_password.pwd.result
key_vault_id = azurerm_key_vault.keyvault.id
expiration_date = timestamp(time_offset.expiry_date.rfc3339)
}
error
│ Error: Too many function arguments
│
│ on key_vault/main.tf line 56, in resource "azurerm_key_vault_secret" "local_admin_pwd":
│ 56: expiration_date = timestamp(time_offset.expiry_date.rfc3339)
│ ├────────────────
│ │ while calling timestamp()
│
│ Function "timestamp" expects only 0 argument(s).
The built-in timestamp function does not expect any arguments:
Function "timestamp" expects only 0 argument(s).
The expiration_date argument should get the value from the attribute provided by the time_offset resource only:
expiration_date = time_offset.expiry_date.rfc3339

is a list of string, known only after apply when for_each involved

I am using the VPC module to create a VPC and subnets.
Once the subnets are created, I want to share them with other accounts. The module works perfectly fine and creates all the subnets. I need the subnet IDs so that I can then use RAM to share the subnets.
My code roughly looks like
# Create VPC and subnets
module "vpc" {
...
...
}
# Next get subnet IDs
data "aws_subnets" "dev_subnet" {
filter {
name = "vpc-id"
values = [module.vpc.vpc_id]
}
tags = {
Environment = "pe-dev*"
}
}
# Create resource share and principal association
resource "aws_ram_resource_share" "share_subnets_with_dev_account" {}
resource "aws_ram_principal_association" "share_subnets_with_dev_account" {}
Now from the subnet IDs I need to extract the ARNs and then make a resource association
resource "aws_ram_resource_association" "example" {
for_each = toset(data.aws_subnets.dev_subnet.ids)
resource_arn = "arn:aws:ec2:${var.region}:${var.aws_account_id}:subnet/${each.value}"
resource_share_arn = aws_ram_resource_share.share_subnets_with_dev_account.arn
}
But when I do a fresh terrafrom apply I get the error
│ Error: Invalid for_each argument
│
│ on main.tf line 110, in resource "aws_ram_resource_association" "example":
│ 110: for_each = toset(data.aws_subnets.dev_subnet.ids)
│ ├────────────────
│ │ data.aws_subnets.dev_subnet.ids is a list of string, known only after apply
│
│ The "for_each" set includes values derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
│
│ When working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.
│
│ Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge.
What came to my mind was to add a depends_on. Something like this
resource "aws_ram_resource_association" "example" {
for_each = toset(data.aws_subnets.dev_subnet.ids)
resource_arn = "arn:aws:ec2:${var.region}:${var.aws_account_id}:subnet/${each.value}"
resource_share_arn = aws_ram_resource_share.share_subnets_with_dev_account.arn
depends_on = [
module.vpc.aws_subnet.private
]
but now i get
│ Error: Invalid depends_on reference
│
│ on main.tf line 116, in resource "aws_ram_resource_association" "example":
│ 116: module.vpc.aws_subnet.private
│
│ References in depends_on must be to a whole object (resource, etc), not to an attribute of an object.
Any idea how I can wait for the subnets to be created and get subnet IDs before aws_ram_resource_association is created ?
EDIT:
What was running
data "aws_subnets" "dev_subnet" {
filter {
name = "vpc-id"
values = [module.vpc.vpc_id]
}
tags = {
Environment = "dev-*"
}
}
data "aws_subnet" "dev_subnet" {
for_each = toset(data.aws_subnets.dev_subnet.ids)
id = each.value
}
output "dev_subnet_arns" {
value = [for s in data.aws_subnet.dev_subnet : s.arn]
}
Result
+ dev_subnet_arns = [
+ "arn:aws:ec2:ca-central-1:0097747:subnet/subnet-013987fd9651c3545",
+ "arn:aws:ec2:ca-central-1:0477747:subnet/subnet-015d76b264280321a",
+ "arn:aws:ec2:ca-central-1:0091747:subnet/subnet-026cd0402fe283c33",
]
but only when i do a tf plan after a previosuly run tf apply.
IF I do a tf destroy and recreate everything then i get the error again
tf plan
╷
│ Error: Invalid for_each argument
│
│ on main.tf line 116, in data "aws_subnet" "dev_subnet":
│ 116: for_each = toset(data.aws_subnets.dev_subnet.ids)
│ ├────────────────
│ │ data.aws_subnets.dev_subnet.ids is a list of string, known only after apply
│
│ The "for_each" set includes values derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
│
│ When working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.
│
│ Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge.
The key problem here is that for_each must be evaluated before the resource is planned, rather than before the resource is created.
The result of data.aws_subnets.dev_subnet.ids depends on the VPC ID, and the VPC ID can't be known until the VPC has been created. But aws_ram_resource_association.example must also be planned at the same time, before the VPC has been created, and so the only way for Terraform to resolve this would be to create the VPC during the planning step, and that would violate the expectation that Terraform doesn't perform any actions until the apply step.
With the architecture you have here, where the calling module is trying to retrieve a set of subnets that haven't been created yet (because their containing VPC also hasn't been created yet), the only way to resolve this would be to first run Terraform with the extra option -target, to force it to create the VPC and subnets first before planning anything else:
terraform apply -target=module.vpc first, which will cause Terraform to create and apply a partial plan only including the resources declared in that module and whatever they depend on.
terraform apply with no arguments afterwards, to plan and apply everything else that the partial plan didn't include.
You can then use terraform apply as normal for ongoing maintenence, as long as you never replace the VPC and thereby cause its ID to become unknown again.
To avoid the need for this extra special bootstrapping step, the better design would be for the VPC module to export the subnets it declares as an additional output value, which means that Terraform can use the set of subnets that are planned for creation, rather than the set of subnets that already exist.
Unfortunately this VPC module you are using doesn't export the subnet IDs in a way that's suitable for use with for_each: it only exports the subnet IDs alone, without associating them with a unique key that can identify them during planning. Therefore unfortunately with this module as currently designed you'll need to use count instead of for_each:
resource "aws_ram_resource_association" "example" {
count = length(module.vpc.private_subnets)
resource_arn = "arn:aws:ec2:${var.region}:${var.aws_account_id}:subnet/${module.vpc.private_subnets[count.index]}"
resource_share_arn = aws_ram_resource_share.share_subnets_with_dev_account.arn
}
This will cause the instances of this resource to be tracked by their position in the list of subnets, and so if you add or remove subnets in future their associations with the list items will change.
To use for_each here would require this module to export the subnets as a mapping where the keys are values that can be determined statically from the configuration -- such as the CIDR blocks -- and the values are the information about each subnet.
Here is a hypothetical output value that the module could include to support this, but to add this will require that you create your own fork of the shared module and modify it:
output "private_subnets" {
value = {
for sn in aws_subnet.private : sn.cidr_block => {
id = sn.id
}
}
}
With the module modified in this way, your calling module can then use for_each with this value:
resource "aws_ram_resource_association" "example" {
for_each = module.vpc.private_subnets
resource_arn = "arn:aws:ec2:${var.region}:${var.aws_account_id}:subnet/${each.value.id}"
resource_share_arn = aws_ram_resource_share.share_subnets_with_dev_account.arn
}
With this new structure, Terraform will track the instances of aws_ram_resource_association.example by using their CIDR blocks as unique identifiers, and so you can add and remove CIDR blocks over time and Terraform will correctly understand which "RAM Resource Association" belongs to which subnet and add/remove the individual ones that correlate.

How to create string output with splat operator in terraform

I am creating several count - based ELBs with terraform.
e.g.
resource "aws_elb" "webserver_example" {
count = var.create_webserver
name = var.name
subnets = data.aws_subnet_ids.default.ids
security_groups = [aws_security_group.elb[count.index].id]
}
I therefore want to be able to get as outputs their http endpoints.
These outputs I assume shoul be strings, and their should somehow incorporate each elb's dns name.
However the following approach using splat, does not work
output "url" {
value = "http://${aws_elb.webserver_example.*.dns_name}:${var.elb_port}"
}
│ Error: Invalid template interpolation value
│
│ on outputs.tf line 2, in output "url":
│ 2: value = "http://${aws_elb.webserver_example.*.dns_name}:${var.elb_port}"
│ ├────────────────
│ │ aws_elb.webserver_example is empty tuple
│
│ Cannot include the given value in a string template: string required.
╵
Is there a way to print multiple count-based strings?
From what I was able to infer from just the code you provided, your var.create_webserver will have different count values (e.g. >= 0). The answer to your specific question is in this code block:
output "url" {
value = [
for dns_name in aws_elb.webserver_example.*.dns_name :
format("http://%s:%s", dns_name, var.elb_port)
]
}
However, be sure you introduce some way to make the names of your Security Groups and ELBs different, because that will be your next error. For example, name = "${var.name}-${count.index}".
Once you get to that point, you will have output that looks like this:
Outputs:
url = [
"http://so-0-2118247212.us-east-1.elb.amazonaws.com:443",
"http://so-1-1137510015.us-east-1.elb.amazonaws.com:443",
]

How can I run for_each in terraform on resource group ids combined from several sources?

I have started with the following code first:
resource "azurerm_role_assignment" "pod_sp" {
for_each = toset(concat(
[for component in local.components: tostring(azurerm_resource_group.setup[component].id)],
[tostring(module.component_remote_state.rg_id)]
))
scope = each.value
role_definition_id = data.azurerm_role_definition.contributor.id
principal_id = azuread_service_principal.pod_sp.id
}
It gave me this:
Error: Invalid for_each set argument
on ..\..\modules\bootstrap\to_inject.tf line 58, in resource "azurerm_role_assignment" "pod_sp":
58: for_each = toset(concat(
59: [for component in local.components: tostring(azurerm_resource_group.setup[component].id)],
60: [tostring(module.component_remote_state.rg_id)]
61: ))
The given "for_each" argument value is unsuitable: "for_each" supports maps
and sets of strings, but you have provided a set containing type dynamic.
Then I found https://github.com/hashicorp/terraform/issues/22437 and changed my code to:
resource "azurerm_role_assignment" "pod_sp" {
for_each = {for k in concat(
[for component in local.components: tostring(azurerm_resource_group.setup[component].id)],
[tostring(module.component_remote_state.rg_id)]
): k => k}
scope = each.value
role_definition_id = data.azurerm_role_definition.contributor.id
principal_id = azuread_service_principal.pod_sp.id
}
Which gave me this:
Error: Invalid for_each argument
on ..\..\modules\bootstrap\to_inject.tf line 59, in resource "azurerm_role_assignment" "pod_sp":
59: for_each = {for k in concat(
60: [for component in local.components: tostring(azurerm_resource_group.setup[component].id)],
61: [tostring(module.component_remote_state.rg_id)]
62: ): k => k}
The "for_each" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the for_each depends on.
I do not understand why it says "cannot predict" when local.components is very much known:
locals {
components = toset(["web", "data"])
}
Is it possible to make it work without the need to run apply with the target first?
The part that isn't predictable here is the id attribute values from azurerm_resource_group.setup. Because you're using those as some keys in your map, the result is a map whose set of keys isn't fully known and thus Terraform can't determine how many elements will eventually be in that map and what all of their keys will be.
To make this work, I'd suggest using the strings from local.components as the keys instead, since you noted that those are constants in your configuration and thus guaranteed to be known during planning:
for_each = merge(
{ for component in local.components : component => azurerm_resource_group.setup[component].id },
{ "from_remote_state" = module.component_remote_state.rg_id },
)
The above assumes that local.components will never contain the string from_remote_state, and thus it's safe to use as a special component name to deal with that extra value that doesn't work the same as the others. Since you understand the requirements for that better than I do, you might find a different name more appropriate there, but the general idea here is to produce a map whose keys are all known even if some of the values are not:
{
"web": (known after apply),
"data": (known after apply),
"from_remote_state": "<your known rg id from the remote state>",
}
From this, Terraform can see how many resource instances you are intending to make and what their addresses must be:
azurerm_role_assignment.pod_sp["web"]
azurerm_role_assignment.pod_sp["data"]
azurerm_role_assignment.pod_sp["from_remote_state"]

Resources