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.
Related
I have a problem that occurred to me after changing my code when it changed the number of "instances" of a sub-module from one to a dynamic number (using for each). The sub-module is not of my authorship, I use ready-made code from the registry, its initialization looks like this, among other things:
module "container_definition_sidecar" {
source = "cloudposse/ecs-container-definition/aws"
version = "v0.46.0"
for_each = var.sidecars
container_name = each.value.container_name
container_image = each.value.container_image
...
Why does I write sub-module? Because I already use the above fragment in my own module called simply "ECS", which is initialized like this:
module "ecs-service" {
source = "./ecs-service"
environment = "test"
awslogs_group = "/ecs/fargate-task-definition"
awslogs_stream_prefix = "ecs"
container_name = "my_container"
container_image = "nginx:latest"
...
sidecars = {
first_sidecar = {
container_name = "logzio-log-router"
container_image = "12345.dkr.ecr.us-east-2.amazonaws.com/aws-for-fluent-bit:latest"
}
second_sidecar = {...}
}
Now, where is the problem?
Where, using jsonencode, I need to get the output, which according to the documentation is called "json_map_object" for each called to life module.container_definition_sidecar
resource "aws_ecs_task_definition" "task_definition" {
family = var.family_name
network_mode = "awsvpc"
requires_compatibilities = [ "FARGATE" ]
container_definitions = jsonencode([module.container_definition_sidecar[*].json_map_object])
When I try use [*] I receive such error:
Error: Unsupported attribute
│
│ on ecs-service/main.tf line 111, in resource "aws_ecs_task_definition" "task_definition":
│ 111: container_definitions = jsonencode([module.container_definition_sidecar.*.json_map_object])
│
│ This object does not have an attribute named "json_map_object".
And the only situation in which the code passes is when I manually type e.g.:
container_definitions = jsonencode([module.container_definition_sidecar["first_sidecar"].json_map_object, module.container_definition_sidecar["second_sidecar"].json_map_object])
However, of course, I don't want to manually provide keys ["first_sidecar"], ["second_sidecar"] and etc. in my function. But don't know how to handle that dynamically
I'll just add that from where the jsonencode is executed I don't have access to the references of the ecs-service module, so I can't go through it and extract the sidecar call keys.
Ok, I solved my own issue using by making this code - writing because maybe someone will find it useful:
container_definitions = jsonencode([for key in range(length(var.sidecars)): module.container_definition_sidecar[keys(var.sidecars)[key]].json_map_object])
That is, I make a FOR loop for as many times as the number of keys in the map object. Then I use the built-in keys() function in which I point to the map, and the numeric value of the key I want to get (not the name of the key, but the value as in the index). Thanks to the for loop, the construction of this is done dynamically, as many times as there are nodes in the map object.
I run terraform plan commnad with apllo.tfvars file
terraform plan -var-file=apllo.tfvars
│ Error: Too many command line arguments
│ To specify a working directory for the plan, use the global -chdir flag.
my variable.tf
variable "user" {
type = string
}
# number variable
variable "age" {
type = number
}
apllo.tfvars
user = "AWSUSER"
age = 222
output.tf
output "name" {
value = "hello ${var.user}"
}
output "age" {
value = "age ${var.age}"
}
If you are using Powershell for running Terraform, try specifying the .tfvar file using single or double quotes, such as:
terraform plan -var-file="apollo.tfvar"
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",
]
I have been trying to conditionally use a module from the root module, so that for certain environments this module is not created. Many people claim that by setting the count in the module to either 0 or 1 using a conditional does the trick.
module "conditionally_used_module" {
source = "./modules/my_module"
count = (var.create == true) ? 1 : 0
}
However, this changes the type of conditionally_used_module: instead of an object (or map) we will have a list (or tuple) containing a single object. Is there another way to achieve this, that does not imply changing the type of the module?
To conditionally create a module you can use a variable, lets say it's called create_module in the variables.tf file of the module conditionally_used_module.
Then for every resource inside the conditionally_used_module module you will use the count to conditionally create or not that specific resource.
The following example should work and provide you with the desired effect.
# Set a variable to know if the resources inside the module should be created
module "conditionally_used_module" {
source = "./modules/my_module"
create_module = var.create
}
# Inside the conditionally_used_module file
# ( ./modules/my_module/main.tf ) most likely
# for every resource inside use the count to create or not each resource
resource "resource_type" "resource_name" {
count = var.create_module ? 1 : 0
... other resource properties
}
I used this in conjunction with workspaces to build a resource only for certain envs. The advantage is for me that I get a single terraform.tfvars file to control the all the environments structure for a project.
Inside main.tf:
workspace = terraform.workspace
#....
module "gcp-internal-lb" {
source = "../../modules/gcp-internal-lb"
# Deploy conditionally based on deploy_internal_lb variable
count = var.deploy_internal_lb[local.workspace] == true ? 1 : 0
# module attributes here
}
Then in variables.tf
variable "deploy_internal_lb" {
description = "Set to true if you want to create an internal LB"
type = map(string)
}
And in terraform.tfvars:
deploy_internal_lb = {
# DEV
myproject-dev = false
# QA
myproject-qa = false
# PROD
myproject-prod = true
}
I hope it helps.
I have a case where I have to create an aws_vpc resource if the user does not provide vpc id. After that I am supposed to create resources with that VPC.
Now, I am applying conditionals while creating an aws_vpc resource. For example, only create VPC if existing_vpc is false:
count = "${var.existing_vpc ? 0 : 1}"
Next, for example, I have to create nodes in the VPC. If the existing_vpc is true, use the var.vpc_id, else use the computed VPC ID from aws_vpc resource.
But, the issue is, if existing_vpc is true, aws_vpc will not create a new resource and the ternary condition is anyways trying to check if the aws_vpc resource is being created or not. If it doesn't get created, terraform errors out.
An example of the error when using conditional operator on aws_subnet:
Resource 'aws_subnet.xyz-subnet' not found for variable 'aws_subnet.xyz-subnet.id'
The code resulting in the error is:
subnet_id = "${var.existing_vpc ? var.subnet_id : aws_subnet.xyz-subnet.id}"
If both things are dependent on each other, how can we create conditional resources and assign values to other configuration based on them?
You can access dynamically created modules and resources as follows
output "vpc_id" {
value = length(module.vpc) > 0 ? module.vpc[*].id : null
}
If count = 0, output is null
If count > 0, output is list of vpc ids
If count = 1 and you want to receive a single vpc id you can specify:
output "vpc_id" {
value = length(module.vpc) > 0 ? one(module.vpc).id : null
}
The following example shows how to optionally specify whether a resource is created (using the conditional operator), and shows how to handle returning output when a resource is not created. This happens to be done using a module, and uses an object variable's element as a flag to indicate whether the resource should be created or not.
But to specifically answer your question, you can use the conditional operator as follows:
output "module_id" {
value = var.module_config.skip == true ? null : format("%v",null_resource.null.*.id)
}
And access the output in the calling main.tf:
module "use_conditionals" {
source = "../../scratch/conditionals-modules/m2" # << Change to your directory
a = module.skipped_module.module_id # Doesn't exist, so might need to handle that.
b = module.notskipped_module.module_id
c = module.default_module.module_id
}
Full example follows. NOTE: this is using terraform v0.14.2
# root/main.tf
provider "null" {}
module "skipped_module" {
source = "../../scratch/conditionals-modules/m1" # << Change to your directory
module_config = {
skip = true # explicitly skip this module.
name = "skipped"
}
}
module "notskipped_module" {
source = "../../scratch/conditionals-modules/m1" # << Change to your directory
module_config = {
skip = false # explicitly don't skip this module.
name = "notskipped"
}
}
module "default_module" {
source = "../../scratch/conditionals-modules/m1" # << Change to your directory
# The default position is, don't skip. see m1/variables.tf
}
module "use_conditionals" {
source = "../../scratch/conditionals-modules/m2" # << Change to your directory
a = module.skipped_module.module_id
b = module.notskipped_module.module_id
c = module.default_module.module_id
}
# root/outputs.tf
output skipped_module_name_and_id {
value = module.skipped_module.module_name_and_id
}
output notskipped_module_name_and_id {
value = module.notskipped_module.module_name_and_id
}
output default_module_name_and_id {
value = module.default_module.module_name_and_id
}
the module
# m1/main.tf
resource "null_resource" "null" {
count = var.module_config.skip ? 0 : 1 # If skip == true, then don't create the resource.
provisioner "local-exec" {
command = <<EOT
#!/usr/bin/env bash
echo "null resource, var.module_config.name: ${var.module_config.name}"
EOT
}
}
# m1/variables.tf
variable "module_config" {
type = object ({
skip = bool,
name = string
})
default = {
skip = false
name = "<NAME>"
}
}
# m1/outputs.tf
output "module_name_and_id" {
value = var.module_config.skip == true ? "SKIPPED" : format(
"%s id:%v",
var.module_config.name,
null_resource.null.*.id
)
}
output "module_id" {
value = var.module_config.skip == true ? null : format("%v",null_resource.null.*.id)
}
The current answers here are helpful when you are working with more modern versions of terraform, but as noted by OP here they do not work when you are working with terraform < 0.12 (If you're like me and still dealing with these older versions, I am sorry, I feel your pain.)
See the relevant issue from the terraform project for more info on why the below is necessary with the older versions.
but to avoid link rot, I'll use the OPs example subnet_id argument using the answers in the github issue.
subnet_id = "${element(compact(concat(aws_subnet.xyz-subnet.*.id, list(var.subnet_id))),0)}"
From the inside out:
concat will join the splat output list to list(var.subnet_id) -- per the background link 'When count = 0, the "splat syntax" expands to an empty list'
compact will remove the empty item
element will return your var.subnet_id only when compact recieves the empty splat output.