How to create string output with splat operator in terraform - 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",
]

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.

Can I use for_each meta-argument with file function?

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
}

Terraform - How to initialize set variable in tfvars

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.

How to use a condition count statement in Terraform

Given a module like so
module us-west-2 {
count = "${local.environments[terraform.workspace] == "logging" ? true : false}"
source = "./modules/flow_log"
providers = {
aws = aws.us-west-2
}
log_destination = module.nf_cis_benchmark.aws_s3_bucket_vpc_flow_log
log_destination_type = "s3"
traffic_type = "REJECT"
depends_on = [ module.nf_cis_benchmark.raws_s3_bucket_vpc_flow_log_arn ]
aws_vpc_ids = data.aws_vpcs.us-west-2.ids
}
How can we conditionally create this module based on the return value from local.environments[terraform.workspace]?
Expected:
When the user runs terraform apply the resources are conditionally created based on the selected workspace.
Actual:
330: count = length("${local.environments[terraform.workspace] == "logging" ? true : false}")
│ ├────────────────
│ │ local.environments is object with 9 attributes
│ │ terraform.workspace is "nf-logging"
│
│ Call to function "length" failed: argument must be a string, a collection type, or a structural type.
Your error message has a length() call, but your posted code does not. Please post the actual code that is generating the error when you post a question like this.
I have no idea why you are trying to wrap the count expression in double quotes, or why you are trying to return true or false, and then take the string length of those strings to create a count value. Are you using a really old version of Terraform? I think what you are attempting to do would actually look like this, if you are using Terraform 0.12 or later:
count = local.environments[terraform.workspace] == "logging" ? 1 : 0
There are several issues with the count meta-argument in the question, including the use of it originally, but to answer the question's intent, you could could conditionally manage a module like:
module "us-west-2" {
for_each = local.environments[terraform.workspace] == "logging" ? toset(["this"]) : []
...
}
which will manage one declaration of the module (consequentially from the size one list iterated) when the local equals the string logging, and zero declarations (from the size zero list iterated) otherwise.

Resources