Is it possible to reference another locals value inside the creation of a locals value?
The example below was the smallest and simplest example I could come up with.
variable "size" {
default = 3
}
variable "infrastructure_version" {
default = 1
}
locals {
values = {
for n in range(var.size) : n => {
name = "instance_${n + 1}"
full_name = "test_${name}_v${var.infrastructure_version}"
}
}
}
When trying to access name within the for loop inside the locals block i get the following error:
│ Error: Invalid reference
│
│ on instances.tf line 13, in locals:
│ 13: full_name = "test_${name}_v${var.infrastructure_version}"
│
│ A reference to a resource type must be followed by at least one attribute access, specifying the resource name.
Other attempts:
(These were desperate attempts with no real likelihood of succeeding)
local.values[n].name which gives Error: Self-referencing local value
n.name which gives Error: Unsupported attribute
self.name which gives Error: Invalid "self" reference
Anyone know if this is possible? Or am I stuck repeating the creation of name inside full_name as well?
full_name = "test_instance_${n + 1}_v${var.infrastructure_version}"
Instead of doing variable interpolation every time a value should be used, it's possible to create a local module that acts as a function.
Where you can use local variables and reuse previously created variables.
Short example below, it's better suited when used for more complex and larger applications due to the amount of overhead.
main.tf:
From the main module I import the local module that will serve as a function
module "instance_conf" {
source = "./modules"
count = var.size
index = count.index
infra = var.infrastructure_version
}
locals {
values = {for idx, val in module.instance_conf: idx => val}
}
I send in index and infra to the module as input. The variable definitions in the other module must be matching those, here you could also provide descriptions if needed.
modules/func.tf:
variable "index" {
type = number
}
variable "infra" {
type = number
}
locals {
name = "instance_${var.index + 1}"
}
output "name" {
value = local.name
}
output "full_name" {
value = "test_${local.name}_v${var.infra}"
}
To get the desired output to the main module, calculate values either in the locals block or directly in the output block. When importing this module the values will be available as a list of maps, for each count.index in the main module. count = var.size
The list could look like this:
[
{
name: "instance_1",
full_name: "test_instance_1_v1"
},
{
name: "instance_2",
full_name: "test_instance_2_v1"
},
...
]
So in order to use the module output as previous with for_each I converted the list of map objects, to a map with the index of each map object as the key for that object.
locals {
values = {for idx, val in module.instance_conf: idx => val}
}
And now when using local.values it will look like this:
{
"1": {
name: "instance_1",
full_name: "test_instance_1_v1"
},
"2": {
name: "instance_2",
full_name: "test_instance_2_v1"
},
...
}
The project structure now looks like this:
.
├── main.tf
├── modules
│ └── values_function.tf
Hopefully this helps someone else out. When variable interpolation and recreation of a value every time it is used, is not an acceptable answer. Mainly because of the maintainability factor.
Your last attempt is correct. You can't make it different and it works:
full_name = "test_instance_${n + 1}_v${var.infrastructure_version}"
Related
I have a simple json file containing a list of users and groups. From this list, I would like to create the users in AWS IAM but my for_each or merging syntax is wrong.
When running terraform plan, I get the following error:
Error: Error in function call
│
│ on locals.tf line 3, in locals:
│ 3: json_data = merge([for f in local.json_files : jsondecode(file("${path.module}/input/${f}"))]...)
│ ├────────────────
│ │ local.json_files is set of string with 1 element
│ │ path.module is "."
│
│ Call to function "merge" failed: arguments must be maps or objects, got "tuple".
How do I properly loop through the list (tuple) of objects in the JSON file?
JSON File sample:
[
{ "name": "user1", "groups": ["Admins", "DevOps"], "policies": [] },
{ "name": "user2", "groups": ["DevOps"], "policies": [] }
]
Terraform Code:
locals {
json_files = fileset("${path.module}/input/", "*.json")
json_data = merge([for f in local.json_files : jsondecode(file("${path.module}/input/${f}"))]...)
}
resource "aws_iam_user" "create_new_users" {
for_each = local.json_data
name = each.name
}
As a side note, I did manage to get the service to work by changing the JSON file to the following structure, but prefer to use the former:
{
"user1": {"groups": ["Admins","DevOps"],"policies": []},
"user2": {"groups": ["DevOps"],"policies": []}
}
and updating the aws_iam_user resource to:
resource "aws_iam_user" "create_new_users" {
for_each = local.json_data
name = each.key
}
The JSON document you showed is using a JSON array, which corresponds with the tuple type in Terraform, so it doesn't make sense to use merge for that result -- merge is for merging together maps, which would correspond most closely with an object in JSON. (and indeed, that's why your second example with an object containing a property with each user worked).
For sequence-like types (lists and tuples) there is a similar function concat which will append them together to produce a single longer sequence containing all of the items in the order given. You could use that function instead of merge to get a single list of all of the users as a starting point:
locals {
json_files = fileset("${path.module}/input/", "*.json")
json_data = concat([for f in local.json_files : jsondecode(file("${path.module}/input/${f}"))]...)
}
The resource for_each argument wants a mapping type though, so you'll need to do one more step to project this list of objects into a map of objects using the name attribute values as the keys:
resource "aws_iam_user" "create_new_users" {
for_each = { for u in local.json_data : u.name => u }
name = each.value.name
}
This will cause Terraform to identify each instance of the resource by the object's "name" property, and so with the sample input file you showed this will declare two instances of this resource with the following addresses:
aws_iam_user.create_new_users["user1"]
aws_iam_user.create_new_users["user2"]
(Note that it's unusual to name a Terraform resource using a verb. Terraform doesn't understand English grammar of course, so it doesn't really matter what you name it, but it's more typical to use a noun because this is only describing that a set of users should exist; you'll use this same object later to describe updating or destroying these objects. If this JSON document just represents all of your users then a more typical name might be aws_iam_user.all, since the resource type already says that these are users -- so there's no need to restate that -- and so all that's left to say is which users these are.)
I am trying to apply lifecycle ignore_changes rule against parameter in resource resource "aws_servicecatalog_provisioned_product" as shown below.
resource "aws_servicecatalog_provisioned_product" "example" {
name = "example"
product_name = "Example product"
provisioning_artifact_name = "Example version"
provisioning_parameters {
key = "foo"
value = "bar"
}
provisioning_parameters {
key = "key2"
value = lookup(var.parameter_group, "key2", "test2")
}
provisioning_parameters {
key = "key3"
value = "test3"
}
tags = {
foo = "bar"
}
lifecycle {
ignore_changes = [
tags["foo"],
aws_servicecatalog_provisioned_product.provisioning_parameters.example["key2"]
]
}
}
variable parameter_group {
description = "Parameters map required for modules.
type = map(any)
default = {}
}
when i am running the plan i am getting below error
│ Error: Unsupported attribute
│
│ on modules/example_provision/main.tf line 28, in resource "aws_servicecatalog_provisioned_product" "example":
│ 28: aws_servicecatalog_provisioned_product.provisioning_parameters.example["key2"]
│
│ This object has no argument, nested block, or exported attribute named "aws_servicecatalog_provisioned_product".
I would like to ignore the changes made to this parameter value. The Ignore on tags is working fine but as soon as i add my second line which is aws_servicecatalog_provisioned_product.provisioning_parameters.example["key2"] the error starts to come in.
looking for suggestion/help here :)
Regards
ignore_changes can only ignore changes to the configuration of the same resource where it's declared, and so you only need to name the argument you wish to ignore and not the resource type or resource name:
lifecycle {
ignore_changes = [
tags["foo"],
provisioning_parameters,
]
}
The provisioning_parameters block type doesn't seem to be represented as a mapping (the provisioning_parameter blocks don't have labels in their headers) so you won't be able to refer to a specific block by its name.
However, the provider does declare it as being a list of objects and so I think you will be able to ignore a specific item by its index, where the indices are assigned in order of declaration. Therefore in your current example the one with key = "key2" will have index 1, due to being the second block where Terraform counts up from zero:
lifecycle {
ignore_changes = [
tags["foo"],
provisioning_parameters[1],
]
}
I don't know if it is possible but I would like to take outputs from different modules as needed:
locals {
node_proyect =
[
["node_1", "project_A"],
["node_2", "project_B"],
...
["node_N", "project_N"],
]
}
Working modules:
module "node_1" {
...
}
[...]
module "node_N" {
...
}
trying to do:
module "outputs_sample" {
for_each = {for i,v in local.node_proyect: i=>v}
...
node_name = module.node_[each.value[0]].node_name
proyect_name = each.value[1]
...
}
What I want:
node_name --> module.node_node_1.node_name --> "string with the name with which the node has been created"
project_name --> "project_A"
Next for_each:
node_name --> module.node_node_2.node_name --> "string with the name with which the node has been created"
project_name --> "project_B"
But I get:
Error: Reference to undeclared module
│
│ on ....tf line ..., in module "outputs_sample":
│ 1073: node_name = module.node_[each.value[0]].node_name
│
│ No module call named "module.node_" is declared in module.....
The output of the modules exists and works perfectly, so if I do: module.node_node_1.node_name works.
I don't know how to make Terraform to interpret it like this, instead of literally as the error says: module.node_[each.value[0]].node_name
Given that you stated the modules are declared in the same config directory despite being absent from the config provided in the question, we can first fix the type and structure of the local variable:
locals {
node_proyect = {
"1" = "proyect_A",
"2" = "proyect_B"
}
}
Now we can use this for module iteration and fix the value accessors and string interpolation, and remove the unnecessary list constructor also causing issues:
module "outputs_sample" {
source = "./module"
for_each = local.node_proyect
...
node_name = "module.node_${each.key}.node_name"
proyect_name = each.value
...
}
But this will still causes issues due to a violation of the Terraform DSL. You need to restructure your module declarations (still absent from question so need to be hypothetical here):
module "node" {
...
for_each = local.node_proyect
...
}
and then the outputs can be accessed normally from the resulting map of objects:
module "outputs_sample" {
source = "./module"
for_each = local.node_proyect
...
node_name = module.node[each.key].node_name
proyect_name = each.value
...
}
After all of these fixes to your config you will achieve your goal.
Terraform Version
Terraform v1.1.2
on windows_amd64
Terraform Configuration Files
child_module1.tf(C1):
# Create Resource Group
resource "aws_resourcegroups_group" "resourcegroups_group" {
name = "test"
resource_query {
query = <<JSON
{
"ResourceTypeFilters": [
"AWS::EC2::Instance"
],
"TagFilters": [
{
"Key": "project",
"Values": ["${var.ProjectName}"]
}
]
}
JSON
}
}
child_module1_variables.tf:
########
variable "ProjectName" {
type = string
description = "This name would be prefixed with the cluster names."
}
Now call this child module in another child module**(C2)**:
child_module2.tf:
module "prepare_aws_cloud" {
source = "./modules/aws/prepare_cloud_copy"
ProjectName = "${var.test.ProjectName}"
}
child_module2_variables.tf:
variable "test" {
type = object({
ProjectName = string
})
}
Now I create a root module(R1)** which calls the child_module2.tf:**
terraform {
backend "local" {
}
}
module "test_deploy" {
source = "D:\\REPO\\installer_v2.2.22.1\\installer\\aws"
test = {
#ProjectName = ""
}
}
So the dependency is as follows:
R1 calls >> C2 calls >> C1
ERROR
PS D:\tkgTest> terraform apply -input=true
╷
│ Error: Invalid value for module argument
│
│ on testing.tf line 21, in module "test_deploy":
│ 21: test= {
│ 22: #ProjectName = ""
│ 23: }
│
│ The given value is not suitable for child module variable "test" defined at .terraform\modules\test_deploy\variables.tf:108,1-15: attribute "ProjectName" is required.
Expected Behavior
I would have expected that the user input would be taken interactively by the console as I am passing the -input=true flag but it doesn't seem to work.
The interactive prompts for input variables are intended only to help with getting started with Terraform (e.g. following a simple tutorial) and so they are limited in the scope of what they support. The typical way to set root module input variables for routine use is to either create a .tfvars file and pass it to Terraform with -var-file or to set a variable directly using -var.
Note also that only root module input variables can be set directly as part of the planning options. Any child module variables are defined exclusively by the expressions written in the module block, and so if you want to be able to set a child module's input variable on a per-run basis you'll need to also declare it as a root module variable and then pass it through to the child module.
For example, in the root module:
variable "test" {
type = object({
ProjectName = string
})
}
module "test_deploy" {
source = "./installer/aws"
test = var.test
}
You can then create an example.tfvars file with the following to set a value for the variable:
test = {
ProjectName = "example"
}
Specify that file when you run Terraform:
terraform apply -var-file=example.tfvars
If you will always set the same values then you can avoid the need for the extra option by naming your file example.auto.tfvars and placing it in the same directory where you will run Terraform. Terraform will load .auto.tfvars files automatically without any explicit -var-file option.
I need to set additional variables in my value.yaml (link to jaeger https://github.com/jaegertracing/helm-charts/blob/main/charts/jaeger/values.yaml#L495) helm chart via terraform + terragrunt. In values.yaml, the code looks like this:
spark:
extraEnv: []
It is necessary that it be like this:
spark:
extraEnv:
- name: JAVA_OPTS
value: "-Xms4g -Xmx4g"
Terraform uses this dynamic block:
dynamic "set" {
for_each = var.extraEnv
content {
name = "spark.extraEnv [${set.key}]"
value = set.value
}
}
The variable is defined like this:
variable "extraEnv" {
type = map
}
From terragrunt I pass the value of the variable:
extraEnv = {
"JAVA_OPTS" = "-Xms4g -Xmx4g"
}
And I get this error:
Error: failed parsing key "spark.extraEnv [JAVA_OPTS]" with value -Xms4g -Xmx4g, error parsing index: strconv.Atoi: parsing "JAVA_OPTS": invalid syntax
on main.tf line 16, in resource "helm_release" "jaeger":
16: resource "helm_release" "jaeger" {
Tell me how to use the dynamic block correctly in this case. I suppose that in this case you need to use a list of maps, but I do not understand how to use this in a dynamic block.
UPD:
I solved my problem in a different way.
In values, defined the list 'spark.extraEnv' using yamlencode.
values = [
"${file("${path.module}/values.yaml")}",
yamlencode({
spark = {
extraEnv = var.spark_extraEnv
}
})
]
in variables.tf
variable "spark_extraEnv" {
type = list(object({
name = string
value = string
}))
}
And in terragrunt passed the following variable value:
spark_extraEnv = [
{
name = "JAVA_OPTS"
value = "-Xms4g -Xmx4g"
}
]
I landed here while I was looking for setting extraEnv for a different chart. Finally figured answer for the above question as well:
set {
name = "extraEnv[0].name"
value = "JAVA_OPTS"
}
set {
name = "extraEnv[0].value"
value = "-Xms4g -Xmx4g"
}