How do write an if/else block?

I’m trying to write a module to DRY up some AWS CloudFront distributions across multiple environments. I’m stumped on how to make the viewer_certificate block dynamic. It comes in two forms.

If there’s a custom certificate, use it:

viewer_certificate {
  acm_certificate_arn      = "…"
  minimum_protocol_version = "TLSv1.1_2016"
  ssl_support_method       = "sni-only"
}

If not, use CloudFront’s default:

viewer_certificate {
  cloudfront_default_certificate = true
}

I have the following variable defined for the module:

# my-module/variables.tf
variable "certificate_arn" {
  default = ""
  type    = string
}

The first thing I tried was two dynamic blocks:

dynamic "viewer_certificate" {
  for_each = var.certificate_arn == "" ? [] : [1]
  content {
    acm_certificate_arn      = var.certificate_arn
    minimum_protocol_version = "TLSv1.1_2016"
    ssl_support_method       = "sni-only
  }
}

dynamic "viewer_certificate" {
  for_each = var.certificate_arn == "" ? [1] : []
  content {
    cloudfront_default_certificate = true
  }
}

That doesn’t work because the provider doesn’t allow multiple viewer_certificate blocks and Terraform can’t tell that there’s only ever going to be one.

The second thing I tried was to extract the block to a local:

locals {
  viewer_certificate = var.certificate_arn == "" ? {
    cloudfront_default_certificate = true
    } : {
    acm_certificate_arn      = var.certificate_arn
    minimum_protocol_version = "TLSv1.1_2016"
    ssl_support_method       = "sni-only"
  }
}

# …

viewer_certificate = local.viewer_certificate

This doesn’t work because viewer_certificate needs to be a block.

How can I get if/else behavior on a block?

My third attempt seems to work:

locals {
  use_default_cert = var.certificate_arn == ""
}

# …

viewer_certificate {
  acm_certificate_arn            = local.use_default_cert ? null : var.certificate_arn
  minimum_protocol_version       = local.use_default_cert ? null : "TLSv1.1_2016"
  ssl_support_method             = local.use_default_cert ? null : "sni-only"
  cloudfront_default_certificate = local.use_default_cert
}

Using a conditional for each argument separately is certainly a reasonable approach in this case, since you know there will always be exactly one viewer_certificate block.

For completeness, here’s another way that’s similar to what you did here but potentially allows for more possibilities if the number of blocks needs to vary in different situations:

locals {
  # Construct a list of objects representing individual
  # blocks to create. In this case there's always one
  # element, but this could use other language features
  # to vary the length of the list, etc, if needed.
  viewer_certificates = [
    {
      acm_certificate_arn            = local.use_default_cert ? null : var.certificate_arn
      minimum_protocol_version       = local.use_default_cert ? null : "TLSv1.1_2016"
      ssl_support_method             = local.use_default_cert ? null : "sni-only"
      cloudfront_default_certificate = local.use_default_cert
    },
  ]
}

# ...

  dynamic "viewer_certificate" {
    for_each = local.viewer_certificates
    content {
      acm_certificate_arn            = viewer_certificate.value.acm_certificate_arn
      minimum_protocol_version       = viewer_certificate.value.minimum_protocol_version
      ssl_support_method             = viewer_certificate.value.ssl_support_method
      cloudfront_default_certificate = viewer_certificate.value.cloudfront_default_certificate
    }
  }

In this specific situation I think using one static block with conditional attributes reads better because it has less visual overhead and thus more directly describes the intended result. I’m sharing the above here just in case this is helpful to someone else who finds this topic in future and has a different situation where they need to vary between zero or one blocks.

Another handy trick for situations where you need to decide the presence or absence of a block by whether a value is null is that the splat operator [*] can be applied to a non-list value, in which case it will convert it to a list by producing either an empty list (if the value is null) or a single-element list (otherwise). In your situation here, if the default for var.certificate_arn were null rather than "", and if you wanted to omit the viewer_certificate block altogether when it isn’t set, this would be a concise way to write that:

variable "certificate_arn" {
  type    = string
  default = null
}

resource "aws_cloudfront_distribution" "example" {
  # ...

  dynamic "viewer_certificate" {
    # Include this block only if var.certificate_arn is
    # set to a non-null value.
    for_each = var.certificate_arn[*]
    content {
      acm_certificate_arn      = viewer_certificate.value
      minimum_protocol_version = "TLSv1.1_2016"
      ssl_support_method       = "sni-only"
    }
  }
}

This “special power” of the splat operators is intended as a way to concisely adapt between a possibly-null single value and a list, because repetition based on lists is generally how Terraform generalizes situations where something is conditionally present.

9 Likes

To answer the question of if/else

  action {
    action_group_id = each.value["severity"] == 0 ? var.monitor_action_groups["sev0"] : each.value["severity"] == 1 ? var.monitor_action_groups["sev1"] : each.value["severity"] == 2 ? var.monitor_action_groups["sev2"] : each.value["severity"] == 3 ? var.monitor_action_groups["sev3"] : var.monitor_action_groups["sev4"]
  }

1 Like

A post was split to a new topic: Can’t apply splat operator to null sequence

I wanted to set an RDS parameter in prod but the rest of the time leave as the the database parameter group default - which as “unknown” as it was determined by the instance type.

  dynamic "parameter" {
    for_each = var.env == "prod" ? [{ name = "max_connections", value = var.db_max_connections }] : []
    content {
      name  = parameter.value["name"]
      value = parameter.value["value"]
    }
  }
1 Like