Terraform - How to initialize set variable in tfvars - terraform

Background
The Terraform document clearly states variable defined in the root module can be set in tfvars file.
Type Constraints
The type constructors allow you to specify complex types such as collections:
set(<TYPE>)
Assigning Values to Root Module Variables
When variables are declared in the root module of your configuration, they can be set in a number of ways:
In variable definitions (.tfvars) files, either specified on the command line or automatically loaded.
An input variable of type set can be defined in a root module.
variables.tf
variable "roles" {
description = "IAM roles to grant to the service account"
type = set(string)
}
Question
Please advise how to initialize the set variable in tfvars? Using function is not allowed, and as far as I looked around, it looks there is no example in the Terraform documentations. Or if setting set is not supported, is it clearly documented?
terraform.tfvars
roles = toset([
"roles/cloudsql.client",
"roles/bigquery.dataEditor",
"roles/storage.admin",
"roles/pubsub.edito",
"roles/secretmanager.secretAccessor",
"roles/artifactregistry.reader"
])
Error: Function calls not allowed
│
│ on sa.auto.tfvars line 1:
│ 1: roles = toset([
│ 2: "roles/cloudsql.client",
│ 3: "roles/bigquery.dataEditor",
│ 4: "roles/storage.admin",
│ 5: "roles/pubsub.edito",
│ 6: "roles/secretmanager.secretAccessor",
│ 7: "roles/artifactregistry.reader"
│ 8: ])

You just define it as:
roles = [
"roles/cloudsql.client",
"roles/bigquery.dataEditor",
"roles/storage.admin",
"roles/pubsub.edito",
"roles/secretmanager.secretAccessor",
"roles/artifactregistry.reader"
]
TF will automatically convert it to the correct type.

Related

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.

Reference Variable inside Module

I am trying to reference a variable declared inside a module to update another variable in the same module and i am unable to find a guide as to how i can reference the variable.
Here is my code sippet
module "cluster" {
source = "..."
var1 = value1 # directly passing value
var2 = module.cluster.var1 # I need to update this variable value based on value of var1
I am facing below error during terraform plan
Terraform v1.0.11
on linux_amd64
Configuring remote state backend...
Initializing Terraform configuration...
Error: Unsupported attribute
│
│ on main.tf line 04, in module "cluster":
│ 04: var2 = module.cluster.var1
│ ├────────────────
│ │ module.cluster is a object, known only after apply
│
│ This object does not have an attribute named "var1".
I have also tried using referencing using local.var1 shown below
module "cluster" {
source = "..."
var1 = value1 # directly passing value
var2 = local.var1 # I need to update this variable value based on value of var1
and then i encounter below error
Terraform v1.0.11
on linux_amd64
Configuring remote state backend...
Initializing Terraform configuration...
╷
│ Error: Reference to undeclared local value
│
│ on main.tf line 04, in module "cluster":
│ 04: var2 = local.var1
│
│ A local value with the name "var1" has not been declared.
╵
any lead will be helpful.
Regards
Your second attempt, using a local variable, is on the right track, but you have to actually declare the local variable:
locals {
var1 = value1
}
module "cluster" {
source = "..."
var1 = local.var1
var2 = local.var1

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",
]

what's causing terraform error: Call to function "formatlist" failed: error on format iteration 0: unsupported value for "%s" at 5: string required

In my terraform code I have the following locals
locals {
merged_acl_contributors = concat(var.workspace.acl.contributors, azurerm_synapse_workspace.workspace.identity)
contributors = formatlist("user:%s:rwx", local.merged_acl_contributors)
}
var.workspace.acl.contributors does not have a value (just has []). When I try to deploy this I get:
│ Error: Error in function call
│
│ on modules/synapse_v2/main.tf line 10, in locals:
│ 10: contributors = formatlist("user:%s:rwx", local.merged_acl_contributors)
│ ├────────────────
│ │ local.merged_acl_contributors is tuple with 1 element
│
│ Call to function "formatlist" failed: error on format iteration 0:
│ unsupported value for "%s" at 5: string required.
identity in azurerm_synapse_workspace.workspace is a block with multiple attributes. You have to choose what you want. For example:
merged_acl_contributors = concat(var.workspace.acl.contributors, [azurerm_synapse_workspace.workspace.identity.principal_id])
Looking at local.merged_acl_contributors was the answer. The value was wrong. Instead of azurerm_synapse_workspace.identity, it needed to be azurerm_synapse_workspace.managedidentity.

Resources