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.
Related
I was using a count for creating multiple AWS task_definitions that should be executed by an AWS step function.
The task_definition required a data "template_file" "task_definition" { section to be able to fill the template data.
Then I needed to render the template data for multiple definitions at a time and I was blocked by an error looking like this:
The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument to first apply only the resources that the for_each depends on.
Here's the initial code:
data "template_file" "task_definition" {
count = length(var.task_container_command)
template = file("./configs/file.json")
vars = {
task = module.ecs[count.index].task_definition
}
}
module "step_function" {
count = length(var.task_container_command)
source = "path"
region = var.region
name = "${var.step_function_name}-${count.index}"
definition_file = data.template_file.task_definition.rendered
}
The point here is that I can't render task_definition because these are not known by terraform yet before the apply. I wasn't able to use the -target argument either because I wanted to make the change in code and not in my deployment pipeline. Meaning when you try to do a terraform plan on the definition_file, the error will pop up.
Solution is below.
What worked was to decouple the use of the count from the .rendered argument by doing this:
data "template_file" "task_definition" {
count = length(var.task_container_command)
template = file("./configs/file.json")
vars = {
task = module.ecs[count.index].task_definition
}
}
resource "local_file" "foo" {
count = length(var.task_container_command)
content = element(data.template_file.task_definition.*.rendered, count.index)
filename = "task-definition-${count.index}"
}
module "step_function" {
count = length(var.task_container_command)
source = "path"
region = var.region
name = "${var.step_function_name}-${count.index}"
definition_file = local_file.foo[count.index].filename
}
Now your data is rendered in the resource called "foo" here and then passed to the step_function module so the terraform plan already knows what's inside your variable. The content element of foo acts like a loop to render each task_definition that I've created using a different filename to avoid duplicates.
Hope this helped :)
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 need to create an escalation policy in Pagerduty using Terraform. I want to dynamically create rule blocks and then within them target blocks with values from rule. I am not sure how to make the second call inside target block to make it dynamic.
I have a list of teams within a list.
locals {
teams = [
[data.pagerduty_schedule.ce_ooh_schedule.id, data.pagerduty_schedule.pi_office_hours_schedule.id],
[data.pagerduty_schedule.delivery_managers_schedule.id]
]
}
resource "pagerduty_escalation_policy" "policy" {
name = var.policy_name
num_loops = var.num_loops
teams = [var.policy_teams]
dynamic "rule" {
for_each = local.teams
escalation_delay_in_minutes = var.escalation_delay
dynamic "target" {
for_each = ??????
content {
type = var.target_type
id = ??????
}
}
}
}
???? are the points I'm not sure about.
I need to create a rule for each item in a list(so [team1, team2] and [escalation_team]) and then for each item within those lists I need to create a target for each of the teams(so rule 1 will have two targets - team1 and team2 and rule 2 will have one target which is escalation_team).
Any idea how I could approach this?
I'm using TF v0.12.20
Here's my config after updating:
resource "pagerduty_escalation_policy" "policy" {
name = var.policy_name
num_loops = var.num_loops
teams = [var.policy_teams]
dynamic "rule" {
for_each = local.teams
escalation_delay_in_minutes = var.escalation_delay
dynamic "target" {
for_each = rule.value
content {
type = var.target_type
id = target.value
}
}
}
}
Edit: Changed locals.teams to local.teams
If I'm reading your question correctly, I believe you want something like the following
resource "pagerduty_escalation_policy" "policy" {
name = var.policy_name
num_loops = var.num_loops
teams = [var.policy_teams]
dynamic "rule" {
for_each = locals.teams
content {
escalation_delay_in_minutes = var.escalation_delay
dynamic "target" {
for_each = rule.value
content {
type = var.target_type
id = target.value
}
}
}
}
}
Note the following
Each dynamic block must have a matching content block
dynamic blocks introduce new names that have .key and .value which can be used to access properties of what's being looped over.
I can't actually run this so if it's still wrong let me know and I'll update.
I'm passing my modules a list and it's going to create EC2 instances and eips and attach.
I'm using for_each so users can reorder the list and Terraform won't try to destroy anything.
But how do I use conditional resources now? Do I still use count? If so how, because you can't use count with for_each?
This is my module now:
variable "mylist" {
type = set(string)
description = "Name used for tagging, AD, and chef"
}
variable "createip" {
type = bool
default = true
}
resource "aws_instance" "sdfsdfsdfsdf" {
for_each = var.mylist
user_data = data.template_file.user_data[each.key].rendered
tags = each.value
...
#conditional for EIP
resource "aws_eip" "public-ip" {
for_each = var.mylist
// I can't use this anymore!
// how can I say if true create else don't create
#count = var.createip ? 0 : length(tolist(var.mylist))
instance = aws_instance.aws-vm[each.key].id
vpc = true
tags = each.value
}
I also need to get the value of the mylist item for eip too because I use that to tag the eip. So I think I need to index into the foreach loop somehow and also be able to use count or another list to determine if it's created or not - is that correct?
I think I got it but I don't want to accept until it's confirmed this is not the wrong way (not as a matter of opinion but improper usage that will cause actual problems).
variable "mylist" {
type = set(string)
description = "Name used for tagging, AD, and chef"
}
variable "createip" {
type = bool
default = true
}
locals {
// set will-create-public-ip to empty array if false
// otherwise use same mylist which module uses for creating instances
will-create-public-ip = var.createip ? var.mylist : []
}
resource "aws_instance" "sdfsdfsdfsdf" {
for_each = var.mylist
user_data = data.template_file.user_data[each.key].rendered
tags = each.value
...
resource "aws_eip" "public-ip" {
// will-create-public-ip set to mylist or empty to skip this resource creatation
for_each = will-create-public-ip
instance = aws_instance.aws-vm[each.key].id
vpc = true
tags = each.value
}
I'm creating subnets as part of a seperate terraform template and exporting the IDs as follows.
output "subnet-aza-dev" {
value = "${aws_subnet.subnet-aza-dev.id}"
}
output "subnet-azb-dev" {
value = "${aws_subnet.subnet-azb-dev.id}"
}
output "subnet-aza-test" {
value = "${aws_subnet.subnet-aza-test.id}"
}
output "subnet-azb-test" {
value = "${aws_subnet.subnet-azb-test.id}"
}
...
I'm then intending to lookup these IDs in another template which is reused to provision multiple environments. Example below shows my second template is calling a module to provision an EC2 instance and is passing through the subnet_id.
variable "environment" {
description = "Environment name"
default = "dev"
}
module "sql-1-ec2" {
source = "../modules/ec2winserver_sql"
...
subnet_id = "${data.terraform_remote_state.env-shared.subnet-aza-dev}"
}
What I'd like to do is pass the environment variable as part of the lookup for the subnet_id e.g.
subnet_id = "${data.terraform_remote_state.env-shared.subnet-aza-${var.environment}"
However I'm aware that variable interpolation isn't supported. I've tried using a map inside of the first terraform template to export them all to a 'subnet' which I could then use to lookup from the second template. This didn't work as I was unable to output variables inside of the map.
This sort of design pattern is something I've used previously with CloudFormation, however I'm much newer to terraform. Am I missing something obvious here?
Worked out a way to do this using data sources
variable "environment" {
description = "Environment name"
default = "dev"
}
module "sql-1-ec2" {
source = "../modules/ec2winserver_sql"
...
subnet_id = "${data.aws_subnet.subnet-aza.id}"
}
data "aws_subnet" "subnet-aza" {
filter {
name = "tag:Name"
values = ["${var.product}-${var.environment}-${var.environmentno}-subnet-aza"]
}
}
data "aws_subnet" "subnet-azb" {
filter {
name = "tag:Name"
values = ["${var.product}-${var.environment}-${var.environmentno}-subnet-azb"]
}
}
Whilst this works and fulfils my original need, I'd like to improve on this by moving the data blocks to within the module, so that there's less repetition. Still working on that one though...