Terraform: Syntactic sugar for conditional blocks

Created on 30 May 2019  路  4Comments  路  Source: hashicorp/terraform

Current Terraform Version

Terraform v0.12.0

Use-cases

We host the same application siloed across different infrastructure (VPCs, etc) for individual customers. Each customer's stack is defined in Terraform, with the majority of the resources being in modules that are re-used across all the stacks.

In some circumstances, we want to enable a Lambda@Edge function for AWS CloudFront to better emulate more liberal web servers. (Specific example: S3 won't silently collapse consecutive slashes in paths like images//a.png, whereas Apache httpd might be configured to do that.) For reasons of both cost and correctness, we _don't_ want to enable Lambda@Edge globally for all installations.

Our main desire: it would be nice if the CDN module had a switch like lambda_edge_enabled = true, so that installations could just toggle a flag to turn the loose-web-server-emulating Lambda@Edge feature on.

Attempted Solutions

In the CloudFront config, Lambda@Edge looks like (copied from docs):

resource "aws_cloudfront_distribution" "example" {
  # ... other configuration ...

  # lambda_function_association is also supported by default_cache_behavior
  ordered_cache_behavior {
    # ... other configuration ...

    lambda_function_association {
      event_type   = "viewer-request"
      lambda_arn   = "${aws_lambda_function.example.qualified_arn}"
      include_body = false
    }
  }
}

Prior to Terraform 0.12, we mostly gave up on having a nice solution, and we just duplicated the entire aws_cloudfront_distribution block using the flag as count. It was pretty gross.

In Terraform 0.12, we can use the dynamic block feature in a sort of hacky way:

dynamic "lambda_function_association" {
  for_each = var.lambda_edge_enabled ? [{}] : []

  content {
    event_type   = "viewer-request"
    lambda_arn   = "${aws_lambda_function.example.qualified_arn}"
    include_body = false
  }
}

In principal, we could accept a list of lambda function associations as a variable in the module, and just pass an empty list most times... but in practice, we try to keep the per-installation module sources as thin as possible. (In fact, the actual Terraform code for each installation is symlinked back to a primary template, and only the .tfvars file is unique for each installation.)

Proposal

I'd like to propose some syntactic sugar for these "conditional blocks". I don't have any specific syntax in mind, but something like

conditional "lambda_function_association" {
  if = var.lambda_edge_enabled

  content {
    event_type   = "viewer-request"
    lambda_arn   = "${aws_lambda_function.example.qualified_arn}"
    include_body = false
  }
}

isn't too far off the existing dynamic mechanism.

References

  • #19853 is similar-ish

I couldn't find anything else, though I'm not really confident in my searches here.

Thanks for your time.

enhancement

Most helpful comment

I would also like this enhancement very much, but I think I would keep the keyword dynamic. After all, a conditional statement is dynamic. Maybe something like so :

dynamic "lambda_function_association" {
  if = var.lambda_edge_enabled

  content {
     # ...
  }
}

At work, we currently do something similar to the example you presented in many modules. Here a short example :
```
locals {
if_logging_enabled = var.logging_enabled ? [{}] : []
if_cors_enabled = var.cors_enabled ? [{}] : []
}

resource "aws_s3_bucket" "bucket" {
# ...
dynamic "logging" {
for_each = local.if_logging_enabled

content {
   # ...
}

}

dynamic "cors_rule" {
for_each = local.if_cors_enabled

content {
   # ...
}

}
}

All 4 comments

I would also like this enhancement very much, but I think I would keep the keyword dynamic. After all, a conditional statement is dynamic. Maybe something like so :

dynamic "lambda_function_association" {
  if = var.lambda_edge_enabled

  content {
     # ...
  }
}

At work, we currently do something similar to the example you presented in many modules. Here a short example :
```
locals {
if_logging_enabled = var.logging_enabled ? [{}] : []
if_cors_enabled = var.cors_enabled ? [{}] : []
}

resource "aws_s3_bucket" "bucket" {
# ...
dynamic "logging" {
for_each = local.if_logging_enabled

content {
   # ...
}

}

dynamic "cors_rule" {
for_each = local.if_cors_enabled

content {
   # ...
}

}
}

Thanks for this feature request!

Conditionally selecting between zero and one items is the expected way to do this right now. Terraform tends to be a list/iteration-oriented language rather than a boolean/condition-oriented language because it plays better with other language features like splat expressions, for expressions, etc.

For the moment we will stick with what we have and get experience with it before adding any new language features... we just added a lot of stuff so want to let that settle and see how these existing features play out first.

I would also like this enhancement very much, but I think I would keep the keyword dynamic. After all, a conditional statement is dynamic. Maybe something like so :

dynamic "lambda_function_association" {
  if = var.lambda_edge_enabled

  content {
     # ...
  }
}

At work, we currently do something similar to the example you presented in many modules. Here a short example :

locals {
  if_logging_enabled = var.logging_enabled ? [{}] : []
  if_cors_enabled = var.cors_enabled ? [{}] : []
}

resource "aws_s3_bucket" "bucket" {
  # ...
  dynamic "logging" {
    for_each = local.if_logging_enabled

    content {
       # ...
    }
  }

  dynamic "cors_rule" {
    for_each = local.if_cors_enabled 

    content {
       # ...
    }
  }
}

hey there, how do you get logging to work dynamically? I always end up with a logging object, that has required fields, when applying this ends up badly.

Example of plan

  # module.s3_bucket.aws_s3_bucket.default[0] will be created
  + resource "aws_s3_bucket" "default" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = "eu-central-1-s3-fiercely-test-example"
      + bucket_domain_name          = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + region                      = "eu-central-1"
      + request_payer               = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + logging {}

      + server_side_encryption_configuration {
          + rule {
              + apply_server_side_encryption_by_default {
                  + sse_algorithm = "AES256"
                }
            }
        }

      + versioning {
          + enabled    = true
          + mfa_delete = false
        }
    }

Results in

Error: Error putting S3 logging: InvalidTargetBucketForLogging: The target bucket for logging does not exist
        status code: 400, request id: REDACTED, host id: REDACTED

  on ..\..\main.tf line 1, in resource "aws_s3_bucket" "default":
   1: resource "aws_s3_bucket" "default" {


Actually got it working, the difference being that it would write by setting var.logging_enabled ? [1] : []

By using your example it works as expected
var.logging_enabled ? [{}] : []

Yes, another reason for syntatic sugar improvements in conditionals

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zeninfinity picture zeninfinity  路  3Comments

rkulagowski picture rkulagowski  路  3Comments

ronnix picture ronnix  路  3Comments

ketzacoatl picture ketzacoatl  路  3Comments

rnowosielski picture rnowosielski  路  3Comments