Terraform: Iterating a resources over multiple values? - terraform

I'm testing the PAN-OS provider from Palo Alto networks, with the goal of configuring a firewall as-code. I can easily make a security policy:
resource "panos_security_policy" "security_policy" {
rule {
name = var.name
source_zones = var.source_zones
source_addresses = var.source_addresses
source_users = var.source_users
destination_zones = var.destination_zones
destination_addresses = var.destination_addresses
applications = var.applications
services = var.services
categories = var.categories
action = var.action
}
}
I have 50+ rules that will need to be managed this way. I could brute-force each rule as an individual resource, or I could use a module with mapped values. But both of those seem like a lot of manual work and hard to maintain. Any ideas for the most efficient way to iterate this resource over a set of values? Anyone who has had to manage a large AWS security group with lots of rules may know of something.

You can use a for_each loop, e.g.
for_each = { for k, v in var.rules : k => v }
name = each.key
source_zones = each.value.source_zones
source_addresses = each.value.source_addresses
source_users = each.value.source_users
...
on variables file:
rules = {
rulename1 = {
source_zones = "foo"
source_address = "bar"
source_users = "baz"
}
rulename2 = {
source_zones = "foo"
source_address = "biz"
source_users = "buz"
}
}
Make sure to read this if you need further details.

Related

Terraform How to use for each values from one resource in another resource

I am trying to create users in terraform cloud based off a simple csv, structured like so:
email,access_level
test#gmail.com,readonly
test2#gmail.come,owners
I can create users easily enough using the following resource block:
locals {
users_csv = csvdecode(file("${path.module}/users.csv"))
}
resource "tfe_organization_membership" "users_csv" {
for_each = { for r in local.users_csv : r.email => r }
organization = var.ppl_organisation
email = each.value.email
}
However, if I want to add them to teams then I need the "id" output from the above resource into the below resource:
resource "tfe_team_organization_member" "readonly_member" {
for_each = { for r in local.read_only : r.email => r }
team_id = each.value.access_level == "readonly" ? each.value.access_level : local.readonly_id
organization_membership_id = "${tfe_organization_membership.users_csv.id}"
}
Is there a way to pass this?
Thanks in advance.
I think the first thing I'd do here is to factor out the projection of your CSV data from a list to a map, like this:
locals {
users_csv = csvdecode(file("${path.module}/users.csv"))
users = { for r in local.users_csv : r.email => r }
}
By defining local.users like this we can make it more concise to refer to that value multiple times elsewhere in the config:
resource "tfe_organization_membership" "users_csv" {
for_each = local.users
organization = var.ppl_organisation
email = each.value.email
}
resource "tfe_team_organization_member" "readonly_member" {
for_each = local.users
team_id = (
each.value.access_level == "readonly" ?
each.value.access_level :
local.readonly_id
)
organization_membership_id = tfe_organization_membership.users_csv[each.key].id
}
Any resource that has for_each set is represented in expressions as a map of objects whose keys are the same as the for_each input map, so the tfe_organization_membership.users_csv[each.key] expression refers to an object representation of the corresponding instance of the other resource, correlating by the map keys.

How to reuse a terraform resource?

I'm pretty much new to terraform. I wanted to know is there a way to reuse a resource? Below is my code. Below is the main.tf, where I have a module declared.
module "deployments" {
source = "./modules/odo_deployments"
artifact_versions = local.artifact_versions
}
In the modules/odo_deployments folder, I have two resources which does exactly the same except for a different ad. Is there a way I can use just one resource and pass arguments (ad) like a function to this resource?
variable "artifact_versions" {
description = "What gets injected by terraform at the ET level"
}
resource "odo_deployment" "incident-management-service-dev" {
count = var.artifact_versions["incident-management-service"].version == "skip" ? 0 : 1
ad = "phx-ad-1"
alias = "cloud-incident-management-application"
artifact {
url = var.artifact_versions["incident-management-service"].uri
build_tag = var.artifact_versions["incident-management-service"].version
type = var.artifact_versions["incident-management-service"].type
}
flags = ["SKIP_UP_TO_DATE_NODES"]
}
resource "odo_deployment" "incident-management-service-dev-ad3" {
count = var.artifact_versions["incident-management-service"].version == "skip" ? 0 : 1
ad = "phx-ad-3"
alias = "cloud-incident-management-application"
artifact {
url = var.artifact_versions["incident-management-service"].uri
build_tag = var.artifact_versions["incident-management-service"].version
type = var.artifact_versions["incident-management-service"].type
}
flags = ["SKIP_UP_TO_DATE_NODES"]
}
What I did to solve this is,I added a locals in the main.tf and pass the local variable in the module like below
locals {
ad = ["phx-ad-1", "phx-ad3"]
}
module "deployments" {
source = "./modules/odo_deployments"
artifact_versions = local.artifact_versions
ad = local.ad
and in the resource instead of hard coding the ad value, I used it like below
count = length(var.ad)
ad = var.ad[count.index]

Terraform dynamic blocks with nested list

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.

How to have conditional resources inside a module with 0.12 for_each

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
}

How to conditionally populate an argument value in 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.

Resources