In my main.tf im using a module, in the module there's this snippet:
resource "aws_lb_listener" "ip_https" {
count = length(var.ip_https_listener) > 0 ? 1 : 0
load_balancer_arn = aws_lb.default.arn
port = var.ip_https_listener.https_port
protocol = "HTTPS"
ssl_policy = var.https_ssl_policy
certificate_arn = var.certificate_arn
default_action {
target_group_arn = aws_lb_target_group.ip[0].arn
type = "forward"
}
depends_on = [aws_lb_target_group.ip]
}
My problem with this that the listener will always have the same default action.
on my main.tf id like to create a boolean variable for example fixed
in case fixed == true id like to be able to use the module the same only change the default action:
default_action {
{
type = "fixed-response"
fixed_response = {
content_type = "text/plain"
message_body = "FORBIDDEN"
status_code = "403"
}
what the easiet way to do that?
Unfortunately this isn't as easy as it might first appear because the "fixed-response" example in your question isn't valid. According to the provider documentation, a "fixed-response" action should look like this:
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "FORBIDDEN"
status_code = "403"
}
}
Note that fixed_response is a nested block rather than an argument, which means that dynamically choosing the number of fixed_response blocks (either zero or one) will require using a dynamic block to generate a dynamic number of these blocks.
Since there are only two possible cases for default_action I would implement this as a lookup table in a local value which shows each of the possible cases as a clear literal data structure, separate from the complexity of generating different nested blocks using dynamic blocks.
For example:
variable "ip_https_listener" {
type = list(object({
https_port = number
fixed = boolean
}))
}
locals {
lb_listener_default_actions = {
forward_to_ip = {
type = "forward"
target_group_arn = aws_lb_target_group.ip[0].arn
}
fixed_forbidden = {
type = "fixed_response"
fixed_response = {
content_type = "text/plain"
message_body = "FORBIDDEN"
status_code = "403"
}
}
}
# This extends the var.ip_https_listener objects with an
# additional attribute "default_action", so we can use
# local.ip_https_listeners instead of var.ip_https_listener
# below to access this conveniently.
ip_https_listeners = [
for l in var.ip_https_listener :
merge(
l,
{
default_action = local.lb_listener_default_actions[l.fixed ? "fixed_response" : "forward_to_ip"]
},
]
}
resource "aws_lb_listener" "ip_https" {
for_each = length(local.ip_https_listener)
load_balancer_arn = aws_lb.default.arn
port = local.ip_https_listener[count.index].https_port
# (...and all of your other arguments)
# Default actions for each listener are selected in the
# definition of local.ip_https_listeners, by looking up
# one of the possible default actions in
# local.lb_listener_default_actions .
default_action {
type = local.ip_https_listeners[count.index].default_action.type
target_group_arn = try(local.ip_https_listeners[count.index].default_action.target_group_arn, null)
dynamic "fixed_response" {
for_each = try(local.ip_https_listeners[count.index].default_action.fixed_response, null)[*]
content {
content_type = fixed_response.value.content_type
message_body = fixed_response.value.message_body
status_code = fixed_response.value.status_code
}
}
}
}
There are three key parts to the above:
local.lb_listener_default_actions describes the two possible "default actions" that any LB listener can have. I arbitrarily named them forward_to_ip and fixed_forbidden here, but you can choose any name that you find descriptive as long as the local.ip_https_listeners condition results match.
local.ip_https_listeners is an extension of var.ip_https_listener which adds the new attribute default_action to each of the objects in the list.
This works by looking up one of the two members of local.lb_listener_default_actions based on whether the fixed attribute is true or false.
The resource "aws_lb_listener" "ip_https" block now uses local.ip_https_listeners instead of var.ip_https_listener, and its default_action block is now dynamic based on the dynamic_action attribute of each listener object.
I used try to concisely tolerate certain attributes being unset in the default action object, using null to represent absense instead. These expressions then each conditionally include the target_group_arn argument and the fixed_response nested block based on whether their corresponding attributes are set in the source default_action object.
There's a subjective design tradeoff here which I want to be explicit about. I chose to factor out the two possible sets of values for default_action into a separate local value because I think that'll make it easier to read and update them in future, but that does come at the expense of some extra indirection: it's no longer clear just from reading the resource block exactly how the default_action will be populated, and instead requires working backwards through all of these expressions to find the local value to update.
I added a comment above the default_action block in the resource in an attempt to mitigate that by directing the future maintainer to the appropriate local value, but it would also be possible to write all of the values inline as part of all of these dynamic expressions and thus remove the indirection at the expense of making it (subjectively) harder to find and update a specific value.
The repeated references to local.ip_https_listeners[count.index] are also unfortunate but come as a consequence of using a list of listeners and the count argument for repetition. If possible I would recommend changing the input variable to be a map of objects instead of a list of objects, and then using for_each to describe the repetition so that you can use each.value as a more concise way to refer to the current element. That is far beyond the scope of this question though, so I won't go into the details about it here.
This can be done with for_each meta-argument [1] and dynamic [2]:
dynamic "default_action" {
for_each = var.fixed ? [1] : []
content {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "FORBIDDEN"
status_code = "403"
}
}
}
[1] https://developer.hashicorp.com/terraform/language/meta-arguments/for_each
[2] https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks
Related
Can someone please explain what a for loop inside a for_each argument does in Terraform? I am trying to create an AWS SSL certificate. I've seen other code like the below but I don't understand it:
resource "aws_acm_certificate" "nonprod_cert" {
domain_name = var.phz_domain_name
validation_method = "DNS"
}
resource "aws_route53_record" "nonprod_cert_record" {
for_each = {
for dvo in aws_acm_certificate.nonprod_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = var.phz_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
resource "aws_acm_certificate_validation" "nonprod_cert_validated" {
certificate_arn = aws_acm_certificate.nonprod_cert.arn
validation_record_fqdns = [for record in aws_route53_record.nonprod_cert_record : record.fqdn]
depends_on = [
aws_acm_certificate.nonprod_cert,
aws_route53_record.nonprod_cert_record
]
}
The specific line that I don't understand is the one in the route53 record. I get that a for_each argument can be used to create multiple resources from a single block, but I can't find anywhere that explains what this for loop is doing inside of it. If someone could explain that would be great!
The inner for "loop" creates the data that the for_each then iterates over. Specifically the each.key will be the dvo.domain_name and the each.value will be the
{
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
You could simply move that into a locals block beforehand and not have it in a single line:
locals {
records = {
for dvo in aws_acm_certificate.nonprod_cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
}
resource "aws_route53_record" "nonprod_cert_record" {
for_each = local.records
zone_id = var.phz_id
name = each.value.name
type = each.value.type
records = [each.value.record]
ttl = 60
}
That will have the exact same effect.
First they are using a for expression to convert one type of object into another type. In this case they are converting the list of domain_validation_options into a list of objects that can be used for creating aws_route53_record resources.
Next they are using for_each to create a new aws_route53_record resource for each element of the list that was generated by the for expression.
The key things to be aware of here are:
for is used to convert a list of objects into a list of different objects.
for_each is used to create multiple resources from a list, set, or map of values.
I highly recommend spending the time to go through the Terraform learning site, or at least the Terraform documentation to learn the basic keywords and overall syntax.
So, I am using a module based on this AWS LB Module
Now, I'm trying to add stickiness option on the default_action forward. Not sure why it's not working. I've tried all sorts of changes. My last change is this:
dynamic "default_action" {
for_each = contains(["authenticate-oidc", "authenticate-cognito"], lookup(var.https_listeners[count.index], "action_type", {})) ? [var.https_listeners[count.index]] : []
content {
type = "forward"
target_group_arn = aws_lb_target_group.main[lookup(default_action.value, "target_group_index", count.index)].id
stickiness {
enabled = lookup(default_action.value, "stickiness", false)
duration = lookup(default_action.value, "duration", null)
}
}
}
I'm trying to pass this:
https_listeners = [
{
port = 443
protocol = "HTTPS"
certificate_arn = var.certificate_arn
stickiness = true
duration = 3600
},
]
I'm now thinking of trying to just use the default stickiness as enabled, and duration 3600.
Any thoughts on this?
EDIT 1: I've tried changing the module block as #bryan mentioned I am passing an empty map. Just for test purposes, I hardcoded the values for stickiness like below:
dynamic "default_action" {
for_each = contains(["authenticate-oidc", "authenticate-cognito"], lookup(var.https_listeners[count.index], "action_type", {})) ? [var.https_listeners[count.index]] : []
content {
type = "forward"
target_group_arn = aws_lb_target_group.main[lookup(default_action.value, "target_group_index", count.index)].id
stickiness {
enabled = true
duration = 3600
}
}
}
Expectation is for the listener default action to set the group stickiness.. but somehow, it doesn't. I've also raised this issue on the AWS LB Module repo
I am trying to create a terraform module for aws_route_table creation, here is an example of this resource definition:
resource "aws_route_table" "example" {
vpc_id = aws_vpc.example.id
route {
cidr_block = "10.0.1.0/24"
gateway_id = aws_internet_gateway.example.id
}
route {
ipv6_cidr_block = "::/0"
egress_only_gateway_id = aws_egress_only_internet_gateway.example.id
}
tags = {
Name = "example"
}
}
I am trying to make it more dynamic by using dynamic blocks. The problem is that I always have to define the keys in content block
resource "aws_route_table" "example" {
...
dynamic "route" {
for_each = var.route
content {
cidr_block = route.value.cidr_block
gateway_id = route.value.gateway_id
}
}
...
}
So in this case, I will need to write two dynamic blocks, one for the content with cidr_block and gateway_id and one for the content with ipv6_cidr_block and egress_only_gateway_id.
Is there any way to do this without defining keys explicitly. Something like this:
dynamic "route" {
for_each = var.route
content {
var.route.map
}
}
Yes, you can create route dynamically, because block route acts as Attributes as Blocks. So you can do (example)
# define all your routes in a variable (example content)
variable "routes" {
default = [
{
cidr_block = "0.0.0.0/0"
gateway_id = "igw-0377483faa64bf010"
},
{
cidr_block = "172.31.0.0/20"
instance_id = "i-043fc97db72ad1b59"
}
]
}
# need to provide default values (null) for all possibilities
# in route
locals {
routes_helper = [
for route in var.routes: merge({
carrier_gateway_id = null
destination_prefix_list_id = null
egress_only_gateway_id = null
ipv6_cidr_block = null
local_gateway_id = null
nat_gateway_id = null
network_interface_id = null
transit_gateway_id = null
vpc_endpoint_id = null
instance_id = null
gateway_id = null
vpc_peering_connection_id = null
}, route)
]
}
resource "aws_route_table" "example" {
vpc_id = aws_vpc.example.id
# route can be attribute, instead of blocks
route = local.routes_helper
tags = {
Name = "example"
}
}
Docs do not recommend to use that in general, but I think route is a good example where this would be acceptable.
The general answer for a block type that doesn't employ the "Attributes as Blocks" backward-compatibility shim Marcin mentioned is to rely on the fact that for Terraform provider arguments there is no difference in meaning between omitting an argument or setting it explicitly to null.
That means that you can write an expression to decide whether to populate a particular argument or not, by making it return an explicit null in the appropriate cases. For example:
dynamic "route" {
for_each = var.route
content {
cidr_block = try(route.value.cidr_block, null)
gateway_id = try(route.value.gateway_id, null)
ipv6_cidr_block = try(route.value.ipv6_cidr_block, null)
# ...
}
}
A nested block like this is more like a fixed data structure than a collection, so there isn't any syntax to construct it dynamically.
With that said, in this particular case route is, as Marcin noted, actually really just an attribute of an object type, and Terraform Core is allowing to use it with block syntax as a concession to backward compatibility with older Terraform versions. For that reason, there's not really any significant difference between these two approaches in practice, but the "Attributes as Blocks" mechanism is only for certain situations where providers were design to assume Terraform v0.11 validation bugs and so the conditional null approach I described above is the more general answer, which should work for normal nested blocks too.
I am trying to write code for existing EC2 Instances in aws_lb_target_group_attachment and I get unsupported block type. Here is the code:
variable "target_id" {
default = [
{
name = "i-123456789abcdefgh"
},
{
name = "i-24680bcgdhkij1234"
}
]
}
resource "aws_lb_target_group_attachment" "mytestAlb" {
target_group_arn = aws_lb_target_group.mytestAlb.arn
dynamic "target_id" {
for_each = var.target_id
content {
name = target_id.value["name"]
}
}
}
I get two errors
Error: missing required arguement, the target_id is required but no definition was found
Error: Unsupported block type , Blocks of type target_id are not expected here.
I tried different variable names and that did not work either. Sorry spent long hours so need another pair of eyes. These EC2 instances are up and running..
This happens, because target_id is not a block. Its requires a single instance id.
Thus, you should be doing:
resource "aws_lb_target_group_attachment" "mytestAlb" {
for_each = {for idx, v in var.target_id: idx => v}
target_group_arn = aws_lb_target_group.mytestAlb.arn
target_id = each.value["name"]
}
I'm trying to terraform WAF ACL and associated rules. The terraform stack I'm working on is identical in DEV, QA , and PROD, differences are all handled using different variables. So my idea is to store a list of CIDRs in a variable, and automatically create ALLOW rules for each. My limited knowledge is slowing me down though. It creates the ipsets perfectly, but the rules and ACL complain,
variable cloud_allowed_cidr_list = {type="list" default=["1.2.3.4/32","4.3.2.1/32"]}
resource "aws_waf_ipset" "ipset" {
count = "${length(var.cloud_allowed_cidr_list)}"
name = "ipset-${count.index}"
ip_set_descriptors {
type = "IPV4"
value = "${element(var.cloud_allowed_cidr_list, count.index)}"
}
}
resource "aws_waf_rule" "matchIPrule" {
count = "${length(var.cloud_allowed_cidr_list)}"
depends_on = ["aws_waf_ipset.ipset"]
name = "matchMancIPrule${count.index}"
metric_name = "matchMancIPrule${count.index}"
predicates {
data_id = "${aws_waf_ipset.ipset.*.id}"
negated = false
type = "IPMatch"
}
}
resource "aws_waf_web_acl" "waf_acl" {
depends_on = ["aws_waf_ipset.ipset", "aws_waf_rule.matchIPrule"]
name = "mancACL${count.index}"
metric_name = "mancACL${count.index}"
default_action {
type = "BLOCK"
}
rules {
action {
type = "ALLOW"
}
priority = "${count.index}"
rule_id = "${aws_waf_rule.matchIPrule.id}"
type = "REGULAR"
}
}
It fell apart when I realised that rules have multiple predicates, and the ACL has multiple rules .....how do you create that dynamically ? If anyone has any examples of doing something similar I'd be very grateful.
Since the release of 0.12 you can now do this using dynamic blocks.
No need to use count to iterate over your array.
resource "aws_waf_ipset" "ipset" {
name = "youripset"
dynamic "ip_set_descriptors" {
iterator = ip
for_each = var.cloud_allowed_cidr_list
content {
type = "IPV4"
value = ip.value
}
}
}