This is an example of my code
What I need ? - I need network interfaces to dynamically connect to the instance, but only one interface should have an external address (nat_ip = ...)
dynamic "network_interface" {
for_each = var.server_network
content {
subnetwork = network_interface.value["subnetwork_name"]
dynamic "access_config" {
HERE i need expression like: if nat_ip is true then do create access_config
if false access_config = omit
for_each = var.server_network.value["nat_ip"]
content {
nat_ip = google_compute_address.static_ip.address
}
}
}
}
inputs in module:
module "vps-test" {
source = "../module"
......
server_network = {
common_network = {
subnetwork_name = (data.terraform_remote_state.network.outputs.subnetwork_vpc_production_common_name)
nat_ip = true
},
custom_network = {
subnetwork_name = "10.10.0.1/24"
nat_ip = false
}
}
}
You can achieve this using the ternary expression [1]:
dynamic "access_config" {
for_each = network_interface.value.nat_ip ? [1] : []
content {
nat_ip = google_compute_address.static_ip.address
}
}
The ternary expression evaluates the condition (left most part) which is network_interface.value.nat_ip in this case. Since it is already a bool value, there is no need to compare it to another value, which you would have to do if it were string or number type.
The condition can evaluate to true or false. If it evaluates to true, the part after the ? will be assigned to the argument. If it evaluates to false, the part after : will be assigned to the argument.
The for_each will be used for exactly one element ([1] in this case) if the ternary operator evaluates to true or for none (i.e., it will not be used) if it evaluates to false. In the latter case, that means that there will be no nat_ip. In the former, that means there will be a nat_ip argument defined with the value coming from the resource attribute reference: google_compute_address.static_ip.address.
[1] https://developer.hashicorp.com/terraform/language/expressions/conditionals
Related
Trying to set an optional block called "sensitive_labels" and i'm trying to set it as an optional one, however, doesn't work.
My code:
variables.tf:
variable "notification_channels" {
type = any
}
variable "project_id" {
type = string
}
main.tf:
project = var.project_id
for_each = { for k, v in var.notification_channels : k => v }
type = each.value.type
display_name = each.value.display_name
description = each.value.description
labels = each.value.labels
enabled = each.value.enabled
dynamic "sensitive_labels" {
for_each = each.value.sensitive_labels != {} ?[each.value.sensitive_labels] : []
content {
auth_token = lookup(sensitive_labels.value, "auth_token", null)
}
}
}
dev.tfvars:
notification_channels = [
{
type = "email"
display_name = "a channel to send emails"
description = "a nice channel"
labels = {
email_address = "HeyThere#something.com"
}
enabled = true
sensitive_labels = {} // this one doesn't give any errors.
},
{
type = "email"
display_name = "HeyThere Email"
description = "a channel to send emails"
labels = {
email_address = "HeyThere2#something.com"
}
enabled = true
}
]
Getting:
Error: Unsupported attribute
on notification_channels.tf line 11, in resource "google_monitoring_notification_channel" "channels":
11: for_each = each.value.sensitive_labels != {} ? [each.value.sensitive_labels] : []
│ ├────────────────
each.value is object with 5 attributes
This object does not have an attribute named "sensitive_labels".
How can I make setting sensitive_labels an optional attribute here?
EDIT:
This seems to work but feels a bit off:
project = var.project_id
for_each = { for k, v in var.notification_channels : k => v }
type = each.value.type
display_name = each.value.display_name
description = each.value.description
labels = each.value.labels
enabled = each.value.enabled
dynamic "sensitive_labels" {
for_each = lookup(each.value, "sensitive_labels", {})
content {
auth_token = lookup(sensitive_labels.value, "auth_token", null)
}
}
}
Is there a better way that doesn't feel hacky?
A good place to start is to properly define a type constraint for your input variable, so that Terraform can understand better what data structure is expected and help ensure that the given value matches that data structure.
type = any is not there so you can skip defining a type constraint, but instead for the very rare situation where a module is just passing a data structure verbatim to a provider without interpreting it at all. Since your module clearly expects that input variable to be a map of objects (based on how you've used it), you should tell Terraform what object type you are expecting to recieve:
variable "notification_channels" {
type = map(object({
type = string
display_name = string
labels = map(string)
enabled = bool
sensitive_labels = object({
auth_token = string
password = string
service_key = string
})
}))
}
From your example it seems like you want sensitive_labels to be optional, so that the caller of the module can omit it. In that case you can use the optional modifier when you declare that particular attribute, and also the three attributes inside it:
sensitive_labels = optional(object({
auth_token = optional(string)
password = optional(string)
service_key = optional(string)
}))
An attribute that's marked as optional can be omitted by the caller, and in that case Terraform will automatically set it to null inside your module to represent that it wasn't set.
Now you can use this variable elsewhere in your module and safely assume that it will always have exactly the type defined in the variable block:
resource "google_monitoring_notification_channel" "channels" {
for_each = var.notification_channels
project = var.project_id
type = each.value.type
display_name = each.value.display_name
description = each.value.description
labels = each.value.labels
enabled = each.value.enabled
dynamic "sensitive_labels" {
for_each = each.value.sensitive_labels[*]
content {
auth_token = sensitive_labels.value.auth_token
password = sensitive_labels.value.password
service_key = sensitive_labels.value.service_key
}
}
}
The each.value.sensitive_labels[*] expression is a splat expression using the single values as lists feature, which concisely transforms the given value into either a one-element list or a zero-element list depending on whether the value is null. That effectively means that there will be one sensitive_labels block if each.value.sensitive_labels is set, and zero blocks of that type if that attribute is unset (null).
The attributes inside those blocks can also just be assigned directly without any special logic, because Terraform will have automatically set them to null if not specified by the caller and setting a resource argument to null is always the same as not setting it at all.
If you take the time to actually describe the types of variables you expect then it tends to make logic elsewhere in the module much simpler, because you no longer need to deal with all of the ways in which the caller might pass you an incorrect value: Terraform will either convert the value automatically to the expected type if possible, or will report an error to the caller explaining why the value they provided isn't acceptable.
Here in the below code, I'm planning to achieve that if value is null ,I don't want to execute a block of the code or else yes
dynamic "request_uri_condition" {
for_each = delivery_rule.value.request_uri_condition_operator != "" ?1 : 0
content{
operator = delivery_rule.value.request_uri_condition_operator
match_values = delivery_rule.value.request_uri_condition_match_values
}
}
variable.tf
variable "delivery_rules" {
type = list(object({
cdnendpoint = string
name = string
order = number
request_scheme_conditions_match_values = list(string)
request_scheme_conditions_operator = string
}))
}
error: Cannot use a number value in for_each. An iterable collection is required.
Using the syntax below, determine whether delivery_rules is empty or not.
error: Cannot use a number value in for_each. An iterable collection is required.
It means that a conditional operator should be given as a string rather than a number (?1:0). I verified it in my environment after making the changes, and everything worked as expected.
dynamic "request_uri_condition" {
for_each = var.delivery_rule != "" ? var.delivery_rule : "default"
content{
operator = delivery_rule.value.request_uri_condition_operator
match_values = delivery_rule.value.request_uri_condition_match_values
}
}
terraform initialized successfully with terraform init:
terraform planexecuted with no "type" errors:
Refer terraform conditionals, terraform registry Cdn template
I am trying to iterate in resource "launchdarkly_feature_flag" variables with dynamic block that might have optional nested objects - "variations" (could be 0 or 2+):
variable "feature_flags" {
default = {
flag_1 = {
project_key = "project"
key = "number example"
name = "number example flag"
description = "this is a multivariate flag with number variations."
variation_type = "number"
variations = {
value = 100
}
variations = {
value = 300
}
tags = ["example"]
},
flag_2 = {
project_key = "project"
key = "boolean example"
name = "boolean example flag"
description = "this is a boolean flag"
variation_type = "boolean"
tags = ["example2"]
}
}
}
Ive tried various scenarios of how to get all flag and always face different problems. Piece of code:
resource "launchdarkly_feature_flag" "ffs" {
for_each = var.feature_flag_map
project_key = each.value.project_key
key = each.value.key
name = each.value.name
description = each.value.description
variation_type = each.value.variation_type
# main problem here
dynamic variations {
for_each = lookup(each.value, "variations", {}) == {} ? {} : {
content {
value = each.value.variations.value
}
}
}
tags = each.value.tags
}
Could you please help with that? I am using 0.14v of Terraform
The first step would be to tell Terraform what type of value this variable expects. While it's often okay to omit an explicit type for a simple value and let Terraform infer it automatically from the default, when the data structure is this complex it's better to tell Terraform what you intended, because then you can avoid it "guessing" incorrectly and giving you confusing error messages as a result.
The following looks like a suitable type constraint for the default value you showed:
variable "feature_flags" {
type = map(object({
project_key = string
key = string
name = string
description = string
variation_type = string
tags = set(string)
variations = list(object({
value = string
}))
}))
}
With the type written out, Terraform will guarantee that any var.feature_flags value conforms to that type constraint, which means that you can then make your dynamic decisions based on whether the values are null or not:
resource "launchdarkly_feature_flag" "ffs" {
for_each = var.feature_flags
project_key = each.value.project_key
key = each.value.key
name = each.value.name
description = each.value.description
variation_type = each.value.variation_type
tags = each.value.tags
dynamic "variations" {
for_each = each.value.variations != null ? each.value.variations : []
content {
variations.value.value
}
}
}
As written above, Terraform will require that all values in feature_flags have all of the attributes defined, although the caller can set them to null to indicate that they are unset.
At the time of writing, in Terraform v0.14, there is an experimental feature for marking attributes as optional which seems like it would, once stabilized, be suitable for this use-case. Marking some or all of the attributes as optional would allow callers to omit them and thus cause Terraform to automatically set them to null, rather than the caller having to explicitly write out the null value themselves.
Hopefully that feature is stabilized in v0.15, at which point you could return to this and add the optional annotations to some attributes without changing anything else about the module.
I'm stuck trying to write a terraform expression which can turn this:
subnets = {
my_subnet_1 = {
nsg = "my_nsg_1",
addresses = "my_addresses_1"
}
my_subnet_2 = {
nsg = "my_nsg_2",
addresses = "my_addresses_2"
}
}
into
nsgs_assocs = {
my_nsg_1 = "my_subnet_1"
my_nsg_2 = "my_subnet_2"
}
I've tried the following:
locals {
nsgs_assocs = zipmap(
var.subnets.*.nsg,
keys(var.subnets)
)
}
but this gives an error:
Error: Invalid function argument
on ..\..\modules\vnet\main.tf line 22, in locals:
21: nsgs_assocs = zipmap(
22: var.subnets.*.nsg,
23: keys(var.subnets)
24: )
Invalid value for "keys" parameter: element 0: string required.
For context, I've inherited a bunch of scripts which I'm trying to refactor without changing the results of a terraform plan.
One of the modules has a lot of related lookup maps - e.g:
nsgs_assocs = {
my_nsg_1 = "my_subnet_1"
my_nsg_2 = "my_subnet_2"
}
subnet_addresses = {
my_subnet_1 = "my_addresses_1"
my_subnet_2 = "my_addresses_2"
}
which I've condensed into my first above sample which I think will be more maintainable in the long run.
However, for backward compatibility with the existing terraform state I need to generate the original nsgs_assocs inside my module so that a for_each continues to use the nsg name as the resource key instead of the subnet name (which causes a destroy / create pair due to the key change).
You're on the right track. It does not work, because splat expression works with arrays, and var.subnets is a map. In order to fix it, you need to convert it into array and it can be done by using values terraform function:
locals {
nsgs_assocs = zipmap(
values(var.subnets)[*].nsg,
keys(var.subnets)
)
}
If you have:
variable "subnets" {
default = {
my_subnet_1 = {
nsg = "my_nsg_1",
addresses = "my_addresses_1"
}
my_subnet_2 = {
nsg = "my_nsg_2",
addresses = "my_addresses_2"
}
}
}
then the following is incorrect
var.subnets.*.nsg
Thus, it should be values(var.subnets).*.nsg:
locals {
nsgs_assocs = zipmap(
values(var.subnets).*.nsg,
keys(var.subnets)
)
}
resulting in:
{
"my_nsg_1" = "my_subnet_1"
"my_nsg_2" = "my_subnet_2"
}
There are a few different ways to achieve this, and the zipmap-based solutions others have shared are fine answers too, but I also wanted to show an example using for expressions because I (subjectively) tend to think this form is easiest to read and understand:
locals {
nsgs_allocs = {
for k, s in var.subnets : s.nsg => k
}
}
As long as all of your subnets have unique nsg values, the above should produce the result you were looking for.
In situations where the new key isn't unique -- for example, if in your cases there could be multiple subnets with the same nsg value -- you can use the for expression's "grouping" mode, which would produce a map of lists of subnet values so that there can potentially be more than one value under each key:
locals {
nsgs_allocs = {
for k, s in var.subnets : s.nsg => k...
}
}
nsgs_assocs = {
my_nsg_1 = ["my_subnet_1"]
my_nsg_2 = ["my_subnet_2"]
}
I am trying to create a terraform module that creates a compute instance. I want the resource to have an attached disk if and only if I have a variable attached_disk_enabled set to true during module invocation. I have this:
resource "google_compute_disk" "my-disk" {
name = "data"
type = "pd-ssd"
size = 20
count = var.attached_disks_enabled ? 1 : 0
}
resource "google_compute_instance" "computer" {
name = "computer"
boot_disk {
...
}
// How do I make this disappear if attached_disk_enabled == false?
attached_disk {
source = "${google_compute_disk.my-disk.self_link}"
device_name = "computer-disk"
mode = "READ_WRITE"
}
}
Variables have been declared for the module in vars.tf. Module invocation is like this:
module "main" {
source = "../modules/computer"
attached_disk_enabled = false
...
}
I know about dynamic blocks and how to use for loop to iterate over a list and set multiple blocks, but I'm not sure how to exclude a block from a resource using this method:
dynamic "attached-disk" {
for_each in var.disk_list
content {
source = "${google_compute_disk.my-disk.*.self_link}"
device_name = "computer-disk-${count.index}"
mode = "READ_WRITE"
}
}
I want if in place of for_each. Is there a way to do this?
$ terraform version
Terraform v0.12.0
Because your disk resource already has the conditional attached to it, you can use the result of that resource as your iterator and thus avoid specifying the conditional again:
dynamic "attached_disk" {
for_each = google_compute_disk.my-disk
content {
source = attached_disk.value.self_link
device_name = "computer-disk-${attached_disk.key}"
mode = "READ_WRITE"
}
}
To answer the general question: if you do need a conditional block, the answer is to write a conditional expression that returns either a single-item list or an empty list:
dynamic "attached_disk" {
for_each = var.attached_disk_enabled ? [google_compute_disk.my-disk[0].self_link] : []
content {
source = attached_disk.value
device_name = "computer-disk-${attached_disk.key}"
mode = "READ_WRITE"
}
}
However, in your specific situation I'd prefer the former because it describes the intent ("attach each of the disks") more directly.