Using for_each on property of a counted resource - terraform

I have a resource with a varying count (0 or 1 resources).
resource "aws_acm_certificate" "cert" {
count = local.acm_version_count
domain_name = "${local.deployment_version_name}.example.com"
subject_alternative_names = [
"*.${local.deployment_version_name}.example.com
]
validation_method = "DNS"
}
And now I want to iterate over a property of the optional resource:
resource "aws_route53_record" "acm_validation" {
for_each = {
for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.aws_route53_zone.r53_zone.zone_id
name = each.value.name
type = each.value.type
ttl = 60
records = [each.value.record]
allow_overwrite = true
}
Of course, because aws_acm_certificate.cert is counted, we must access that at an index (i.e., aws_acm_certificate.cert[0] or aws_acm_certificate.cert[count.index]). However, I can't use a count because it is mutually exclusive from for_each, and I cannot access [0] when the optional resource is not created. Ideally I could add a count on the record itself, so that I ignore the aws_route53_record.acm_validation if there is no aws_acm_certificate
I know about toproduct, so I wonder if that would be useful - but I honestly didn't know enough about it to utilize it and couldn't make it work here
How can I iterate over this optional resource so that I can ignore it if the acm is optionally excluded, but still iterate over those domain_validation_options if there is an acm?

You can produce a flattened list of all of the "domain validation options" across all of the aws_acm_certificate.cert instances like this:
flatten(aws_acm_certificate.cert[*].domain_validation_options)
When you have zero certificates declared, this would be like calling flatten([]) and so it'll just produce an empty list, whereas when count is nonzero it'll capture all of the declared validation options, because aws_acm_certificate.cert[*].domain_validation_options alone would produce a list of lists of objects.
I think it should work to insert that flatten( ... ) call in the same place where you are currently referring directly to an attribute of a singleton resource, and leave everything else unchanged:
for_each = {
for dvo in flatten(aws_acm_certificate.cert[*].domain_validation_options) :
dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
In most cases I'd suggest thinking of a conditional count as "there can be zero or more instances", even if you know that the maximum count is 1 really, because the language features designed for working with lists of objects are often more compact and concise than what you'd need to write to specifically distinguish between zero or one of some resource. It doesn't always work out like that, but in many cases it's not really important whether there's one or more than one instances, including this situation.

You need to check if the resource is created, and pass an empty list to for_each if it isn't. I would add a local variable to make this a bit cleaner.
locals {
dvos = local.acm_version_count == 0 ? [] : aws_acm_certificate.cert[0].domain_validation_options
}
resource "aws_route53_record" "acm_validation" {
for_each = {
for dvo in local.dvos : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
...
}

Related

Conditional dynamic block

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.

How to include tags in terraform when using modules and count

I have to provision many instances using modules and count in it. The issue is , the required tags for some particular instances have to be different. Can any suggest a better way where a tag can be added along with default tags with all instances. Im using a map with all 30 occurrence which is long.
Default tags:tag1,tag2
Example Case:
instance1......instance15 - tag1,tag2
instance16 - tag1,tag2,tag_xxx
instance17......instance29 - tag1,tag2.
instance30 - tag1,tag2,tag_yyy
Terraform Code:
module "compute-vm" {
count = length(var.names)
source = "../modules/compute-vm"
project_id = var.project_id
name = var.name[count.index]
tags = var.tags[var.names[count.index]]
variable "tags" {
type = map(list(string))
}
tags.tfvars:
tags= {
"instance1" = ["tag1","tag2"],
"instance2"=["tag1","tag2"],
"instance3" = ["tag1","tag2"],
.
.
.
.
"instance16"=["tag1","tag2",tag_xxx]
You can concatenate two lists with concat.
Something along the lines of:
variable "default_tags" {
type = list(string)
}
module "compute-vm" {
...
tags = concat(var.default_tags, var.tags[var.names[count.index]])
}
Additionally, I would use for_each instead of count. Which makes the block easier to read. Additionally you can later refer to the block via module.compute-vm["instance1"] instead of having to do it with indices module.compute-vm[0]. Because who knows if the first VM is really "instance1" (maybe Terraform changes the order inside the map).
module "compute-vm" {
for_each = var.tags
source = "../modules/compute-vm"
project_id = var.project_id
name = each.key
tags = each.value // or concat(var.default_tags, each.value)
}

How to handle optional dynamic blocks in terraform

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.

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
}

Resources