Home > Back-end >  Terraform logic, check boolean variable, if true change snippet in module
Terraform logic, check boolean variable, if true change snippet in module

Time:12-15

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?

CodePudding user response:

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

CodePudding user response:

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.

  • Related