I don't understand the logic of the following terraform code, and not sure, but I guess it might be me, but would appreciate some help with this.
So there's the following module https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/modules/definition/main.tf
resource azurerm_policy_definition def {
name = local.policy_name
display_name = local.display_name
description = local.description
policy_type = "Custom"
mode = var.policy_mode
management_group_id = var.management_group_id
metadata = jsonencode(local.metadata)
parameters = length(local.parameters) > 0 ? jsonencode(local.parameters) : null
policy_rule = jsonencode(local.policy_rule)
lifecycle {
create_before_destroy = true
}
timeouts {
read = "10m"
}
}
and https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/modules/definition/variables.tf
variable management_group_id {
type = string
description = "The management group scope at which the policy will be defined. Defaults to current Subscription if omitted. Changing this forces a new resource to be created."
default = null
}
variable policy_name {
type = string
description = "Name to be used for this policy, when using the module library this should correspond to the correct category folder under /policies/policy_category/policy_name. Changing this forces a new resource to be created."
default = ""
validation {
condition = length(var.policy_name) <= 64
error_message = "Definition names have a maximum 64 character limit, ensure this matches the filename within the local policies library."
}
}
variable display_name {
type = string
description = "Display Name to be used for this policy"
default = ""
validation {
condition = length(var.display_name) <= 128
error_message = "Definition display names have a maximum 128 character limit."
}
}
variable policy_description {
type = string
description = "Policy definition description"
default = ""
validation {
condition = length(var.policy_description) <= 512
error_message = "Definition descriptions have a maximum 512 character limit."
}
}
variable policy_mode {
type = string
description = "The policy mode that allows you to specify which resource types will be evaluated, defaults to All. Possible values are All and Indexed"
default = "All"
validation {
condition = var.policy_mode == "All" || var.policy_mode == "Indexed" || var.policy_mode == "Microsoft.Kubernetes.Data"
error_message = "Policy mode possible values are: All, Indexed or Microsoft.Kubernetes.Data (In Preview). Other modes are only allowed in built-in policy definitions, these include Microsoft.ContainerService.Data, Microsoft.CustomerLockbox.Data, Microsoft.DataCatalog.Data, Microsoft.KeyVault.Data, Microsoft.MachineLearningServices.Data, Microsoft.Network.Data and Microsoft.Synapse.Data"
}
}
variable policy_category {
type = string
description = "The category of the policy, when using the module library this should correspond to the correct category folder under /policies/var.policy_category"
default = null
}
variable policy_version {
type = string
description = "The version for this policy, if different from the one stored in the definition metadata, defaults to 1.0.0"
default = null
}
variable policy_rule {
type = any
description = "The policy rule for the policy definition. This is a JSON object representing the rule that contains an if and a then block. Omitting this assumes the rules are located in /policies/var.policy_category/var.policy_name.json"
default = null
}
variable policy_parameters {
type = any
description = "Parameters for the policy definition. This field is a JSON object that allows you to parameterise your policy definition. Omitting this assumes the parameters are located in /policies/var.policy_category/var.policy_name.json"
default = null
}
variable policy_metadata {
type = any
description = "The metadata for the policy definition. This is a JSON object representing additional metadata that should be stored with the policy definition. Omitting this will fallback to meta in the definition or merge var.policy_category and var.policy_version"
default = null
}
variable file_path {
type = any
description = "The filepath to the custom policy. Omitting this assumes the policy is located in the module library"
default = null
}
locals {
# import the custom policy object from a library or specified file path
policy_object = jsondecode(coalesce(try(
file(var.file_path),
file("${path.cwd}/policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.root}/policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.root}/../policies/${title(var.policy_category)}/${var.policy_name}.json"),
file("${path.module}/../../policies/${title(var.policy_category)}/${var.policy_name}.json")
)))
# fallbacks
title = title(replace(local.policy_name, "/-|_|\\s/", " "))
category = coalesce(var.policy_category, try((local.policy_object).properties.metadata.category, "General"))
version = coalesce(var.policy_version, try((local.policy_object).properties.metadata.version, "1.0.0"))
# use local library attributes if runtime inputs are omitted
policy_name = coalesce(var.policy_name, try((local.policy_object).name, null))
display_name = coalesce(var.display_name, try((local.policy_object).properties.displayName, local.title))
description = coalesce(var.policy_description, try((local.policy_object).properties.description, local.title))
metadata = coalesce(var.policy_metadata, try((local.policy_object).properties.metadata, merge({ category = local.category },{ version = local.version })))
parameters = coalesce(var.policy_parameters, try((local.policy_object).properties.parameters, null))
policy_rule = coalesce(var.policy_rule, try((local.policy_object).properties.policyRule, null))
# manually generate the definition Id to prevent "Invalid for_each argument" on set_assignment plan/apply
definition_id = var.management_group_id != null ? "${var.management_group_id}/providers/Microsoft.Authorization/policyDefinitions/${local.policy_name}" : azurerm_policy_definition.def.id
}
and an example how to use the module https://github.com/gettek/terraform-azurerm-policy-as-code/blob/main/examples/definitions.tf
module "deny_resource_types" {
source = "..//modules/definition"
policy_name = "deny_resource_types"
display_name = "Deny Azure Resource types"
policy_category = "General"
management_group_id = data.azurerm_management_group.org.id
}
From how I see it (might be wrrong) a variable can be used as a default value to the local in a Terraform script. So how is the value for the variable policy_name used when main.tf references local.policy_name instead of var.policy_name. The variable policy_name is also not having a default value.
What am I missing ?
Thank you !
EDIT:
Thank you, very clear explanation. I think I understand it better now. So basically, when I’m calling the definition module Terraform essentially load and process that module’s configuration files (including variables.tf). I was confused to see name = local.policy_name instead of for example mode = var.policy_mode. But the way I understand it now, is that when calling this module, I can set the value for the variable policy_name, which is then further processed inside the locals section, and result is what is actual provided to the name argument required by azurerm_policy_definition https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/policy_definition. Could you please confirm that my understanding is correct?
Thank you !
policy_name does have default value, but the value is empty.
default = ""
Empty value can be default value. Terraform expects module inputs only when there is no default attribute set in the input field.
OK, so there are two scenarios at play here.
When the policy_name isn't provided to the module, it takes on its default behaviour of empty string
When a value is provided to the policy_name, the locals section transforms that value and then uses it in the code as local.policy_name for the resources. L103
policy_name = coalesce(var.policy_name, try((local.policy_object).name, null))
If you look for the coalesce function, its purpose is to return the first element that is not null/empty.
Although, I don't see the point of that logic since if both cases are null it is supposed to return null. Could've used a simple condition instead.
I hope this clarifies things more.
Ps: an empty string (""), Terraform consider as a value.
I have the following terraform code of aws_lb_target_group resource
dynamic "health_check" {
for_each = contains(keys(each.value), "health_check_settings") ? each.value.health_check_settings == true ? ["health_check"] : [] : []
iterator = hc
content {
port = contains(keys(each.value), "health_check_port") ? each.value.health_check_port : "traffic-port"
protocol = contains(keys(each.value), "health_check_protocol") ? each.value.health_check_protocol : "HTTP"
....
}
}
And I don't understand the part of for_each = ["health_check"]
It's just a tuple of one string, not an object. In case of health_check_settings=true how would the data will be passed to the for each.
It's done in order to make this block conditional.
The actual value of for_each doesn't matter because we use each from higher for_each context.
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 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 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.