How to conditionally populate an argument value in terraform? - terraform

I am writing a Terraform script to spin up resources in Google Cloud Platform.
Some resources require one argument only if the other one set, how to populate one argument only if the other one is populated (or any other similar condition)?
For example:
resource "google_compute_router" "compute_router" {
name = "my-router"
network = "${google_compute_network.foobar.name}"
bgp {
asn = 64514
advertise_mode = "CUSTOM"
advertised_groups = ["ALL_SUBNETS"]
advertised_ip_ranges {
range = "1.2.3.4"
}
advertised_ip_ranges {
range = "6.7.0.0/16"
}
}
}
In the above resource (google_compute_router) the description for both advertised_groups and advertised_ip_ranges says This field can only be populated if advertise_mode is CUSTOM and is advertised to all peers of the router.
Now if I keep the value of advertise_mode as DEFAULT, my code looks something like below:
resource "google_compute_router" "compute_router" {
name = "my-router"
network = "${google_compute_network.foobar.name}"
bgp {
asn = 64514
#Changin only the value below
advertise_mode = "DEFAULT"
advertised_groups = ["ALL_SUBNETS"]
advertised_ip_ranges {
range = "1.2.3.4"
}
advertised_ip_ranges {
range = "6.7.0.0/16"
}
}
}
The above script however on running gives the following error:
* google_compute_router.compute_router_default: Error creating Router: googleapi: Error 400: Invalid value for field 'resource.bgp.advertiseMode': 'DEFAULT'. Router cannot have a custom advertisement configurati
on in default mode., invalid
As a workaround to the above, I have created two resources with different names doing almost the same thing. The script looks something like below:
resource "google_compute_router" "compute_router_default" {
count = "${var.advertise_mode == "DEFAULT" ? 1 : 0}"
name = "${var.router_name}"
region = "${var.region}"
network = "${var.network_name}"
bgp {
asn = "${var.asn}"
advertise_mode = "${var.advertise_mode}"
#Removed some codes from here
}
}
resource "google_compute_router" "compute_router_custom" {
count = "${var.advertise_mode == "CUSTOM" ? 1 : 0}"
name = "${var.router_name}"
region = "${var.region}"
network = "${var.network_name}"
bgp {
asn = "${var.asn}"
advertise_mode = "${var.advertise_mode}"
advertised_groups = ["${var.advertised_groups}"]
advertised_ip_ranges {
range = "${var.advertised_ip_range}"
description = "${var.advertised_ip_description}"
}
}
}
The above script works fine, however it seems like a lot of code repetition to me and a hack. Also, for two options (of dependent attributes) is fine, however, if there are more options say 5, the code repetition for such a small thing would be too much.
Is there a better way to do what I am trying to achieve?

This is pretty much what you are restricted to in Terraform < 0.12. Some resources allow you to use an empty string to omit basic values and the provider will interpret this as a null value, not passing it to the API endpoint so it won't complain about it not being set properly. But from my brief experience with the GCP provider this is not the case for most things there.
Terraform 0.12 introduces nullable arguments which would allow you to set these conditionally with something like the following:
variable "advertise_mode" {}
resource "google_compute_router" "compute_router" {
name = "my-router"
network = "${google_compute_network.foobar.name}"
bgp {
asn = 64514
advertise_mode = "${var.advertise_mode}"
advertised_groups = ["${var.advertise_mode == "DYNAMIC" ? ALL_SUBNETS : null}"]
advertised_ip_ranges {
range = "${var.advertise_mode == "DYNAMIC" ? 1.2.3.4 : null}"
}
advertised_ip_ranges {
range = "${var.advertise_mode == "DYNAMIC" ? 6.7.0.0/16 : null}"
}
}
}
It will also introduce dynamic blocks that you are able to loop over so you can also have a dynamic number of advertised_ip_ranges blocks.

The above answer is incorrect as 'advertised_ip_ranges' wont accept a null value; the solution to this is to leverage a dynamic block which can handle a null value for this resource and further enables the resource to accept a variable number of ip ranges.
variable custom_ranges {
default = ["172.16.31.0/24","172.16.32.0/24"]
}
resource "google_compute_router" "router_01" {
name = "cr-bgp-${var.gcp_bgp_asn}"
region = var.gcp_region
project = var.gcp_project
network = var.gcp_network
bgp {
asn = var.gcp_bgp_asn
advertise_mode = var.advertise_mode
advertised_groups = var.advertise_mode == "CUSTOM" ? ["ALL_SUBNETS"] : null
dynamic "advertised_ip_ranges" {
for_each = var.advertise_mode == "CUSTOM" ? var.custom_ranges : []
content {
range = advertised_ip_ranges.value
}
}
}
}
additional search keys: google_compute_router "bgp.0.advertised_ip_ranges.0.range" wont accept a null value.

Related

adding multiple destinations to new relic workflows using terraform

I am trying to create a newrelic workflow using terraform modules. I am fine with creating a workflow with signle destination. But, I am trying to create a workflow with more than one destination.
slack channel ids
variable "channel_ids" {
type = set(string)
default = ["XXXXXXXXXX","YYYYYYYYY"]
}
creating notification channels using slack channel ids
resource "newrelic_notification_channel" "notification_channel" {
for_each = var.channel_ids
name = "test" # will modify if required
type = "SLACK" # will parameterize this
destination_id = "aaaaaaaaa-bbbbb-cccc-ddddd-eeeeeeeeee"
product = "IINT"
property {
key = "channelId"
value = each.value
}
}
Now I want to create something like below (two destinations)
resource "newrelic_workflow" "newrelic_workflow" {
name = "my-workflow"
muting_rules_handling = "NOTIFY_ALL_ISSUES"
issues_filter {
name = "Filter-name"
type = "FILTER"
predicate {
attribute = "accumulations.policyName"
operator = "EXACTLY_MATCHES"
values = [ "policy_name" ]
}
}
destination {
channel_id = newrelic_notification_channel.notification_channel.id
}
destination {
channel_id = newrelic_notification_channel.notification_channel.id
}
}
I tried using for_each and for loop but no luck. Any idea on how to get my desired output?
Is it possible to loop through and create multiple destinations within the same resource, like attaching multiple destination to a single workflow?
I was able to achieve this by using a dynamic block, which produces a dynamic number of destination blocks based on the number of elements of newrelic_notification_channel.notification_channel.
resource "newrelic_workflow" "newrelic_workflow" {
name = "my-workflow"
muting_rules_handling = "NOTIFY_ALL_ISSUES"
issues_filter {
name = "Filter-name"
type = "FILTER"
predicate {
attribute = "accumulations.policyName"
operator = "EXACTLY_MATCHES"
values = [ "policy_name" ]
}
}
dynamic "destination" {
for_each = newrelic_notification_channel.notification_channel
content {
channel_id = destination.value.id
}
}
}

terraform - how to use variables inside attributes

I am not sure if this is the right approach to do this but I want to use a variable as an attribute.
For example, I have a variable that changes based on user input: os_name = ubuntu.
I want to use this variable name like the following,
resource "aws_instance" "workerNode" {
..................
ami = data.aws_ami.${var.os_name}.image_id
..................
}
Following is an example of the data block,
data "aws_ami" "suse" {
count = "${var.os_name == "suse" ? 1 : 0}"
owners = ["amazon"]
most_recent = true
filter {
name = "name"
values = ["suse-sles-${var.os_version}-sp*-v????????-hvm-ssd-x86_64"]
}
}
Which result the following,
"architecture" = "x86_64"
"hypervisor" = "xen"
"id" = "ami-0d3905203a039e3b0"
"image_id" = "ami-0d3905203a039e3b0"
But terraform is not allowing me to do this. Is there any way I can do this or I have to change the workflow?
In situations where it's not appropriate to gather all of your instances under a single resource using for_each (which would implicitly make that resource appear as a map of objects), you can get a similar result explicitly by writing a local value expression to construct an equivalent map:
locals {
amis = {
suse = data.aws_ami.suse
ubuntu = data.aws_ami.ubuntu
}
}
Then you can refer to local.amis["ubuntu"] or local.amis["suse"] (possibly replacing the element key with a variable, if you need to.
With that said, it does seem like there is a possible different approach for your case which would get there with only one data block:
locals {
os_ami_queries = {
suse = {
owners = ["amazon"]
filters = {
name = ["suse-sles-${var.os_version}-sp*-v????????-hvm-ssd-x86_64"]
}
}
ubuntu = {
owners = ["amazon"]
filters = {
name = ["ubuntu-${var.os_version}-something-something"]
}
}
}
ami_query = local.os_ami_queries[var.os_name]
}
data "aws_ami" "selected" {
owners = local.ami_query.owners
dynamic "filter" {
for_each = local.ami_query.filters
content {
name = filter.key
values = filter.value
}
}
}
This different permutation does the OS selection before the data "aws_ami" lookup, so it can use the settings associated with whichever OS was selected by the caller. The AMI id would then be in data.aws_ami.selected.id.
With that said, this approach has the disadvantage of being quite indirect and using a dynamic block, so I'd weigh that against the readability of the alternatives to pick the one which seems subjectively easiest to follow for someone who isn't familiar with this configuration. There isn't a single answer to that because to some extent it's a matter of taste, and so if you are working in a team setting this could be something to discuss with colleagues to see which approach best suits tradeoffs like how often you expect to be adding and removing supported operating systems vs. changing the details of how you use the result.
You can make it work by specifying your AMI's with a for_each and thus getting a map which you can access by key.
My data.aws_ami.myamis looks like this:
data "aws_ami" "myamis" {
for_each = toset(["suse", "ubuntu"])
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["${each.value}*"]
}
}
For test purposes I define a variable foo like this:
variable "foo" {
type = string
default = "suse"
}
Now I can access the AMI like this:
$ tf console
> data.aws_ami.myamis[var.foo].image_id
"ami-0ea50c090ba6e85c5"
You can adapt this to suit your needs for os_name and os_version.
I have solved the issue just by using conditional expression.
I am not sure if it is a standard way of doing things but it works for me.
I have tried to emulate if/elif/else with nested conditional expression.
output "name" {
value = "${ var.os_name == "ubuntu" ? data.aws_ami.ubuntu[0].image_id : (var.os_name == "redhat" ? data.aws_ami.redhat[0].image_id : (var.os_name == "centos" ? data.aws_ami.suse[0].image_id : data.aws_ami.suse[0].image_id ))}"
}

Terraform - inverting a map

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

How to use null with a different value in place of default value

I know that null can be used like this to set default behavior if var not specified:
variable "override_private_ip" {
type = string
default = null
}
resource "aws_instance" "example" {
# ... (other aws_instance arguments) ...
private_ip = var.override_private_ip
}
But I want to set my own default behavior if it's not specified.
I'm doing this:
#### if user sets an id use that but if not get id from data source
resource "aws_instance" "myserver" {
ami = var.ami_id != null ? var.ami_id : data.aws_ami.getami.id
This seems to work but is this the correct way? I want to make sure I'm not missing a feature for this. I tried just var.ami_id ? var.ami_id : data.aws_ami.getami.id but null is not converted to a bool so did not work.
A conditional expression like the one you showed is indeed the right way to express this in Terraform. As you've seen, Terraform does not consider null to be a boolean false.
It doesn't seem like it would be necessary for this particular situation, but if you have input variables that are used in many places where all uses would need the same normalization/preparation logic then you can factor out the expression into a local value to use it many times:
variable "ami_id" {
type = string
default = null
}
data "aws_ami" "example" {
count = var.ami_id == null ? 1 : 0
# ...
}
locals {
ami_id = var.ami_id != null ? var.ami_id : data.aws_ami.example[0].id
}
resource "aws_instance" "example" {
# ... (other aws_instance arguments) ...
ami = local.ami_id
}
You could then use local.ami_id many times in the module without duplicating the logic that handles the default value lookup.

Create nested resource parameter blocks based on conditional in terraform

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.

Resources