Conditional Resource Provisioning in OpenTofu Modules

Conditional Resource Provisioning in OpenTofu Modules

In a previous post, I briefly covered how to write conditionals in OpenTofu's HCL syntax. You can check it out here.

There are some times when conditionals are necessary, and for me, that's usually when I'm writing reusable modules that provision resources conditionally.

I have an OpenTofu module that supports the deployment of each of my microservices. It can create a ServiceAccount that can bind to an IAM role (that it also creates), an ECR repo to store the image, etc. It can also conditionally create a few other things, if the microservice requires them, e.g.:

  • an S3 bucket

  • a Cognito API client

  • a database password stored in an AWS Secret

  • a MongoDB user & password

Since each microservice will require some combination of these four things, conditionals are unavoidable.

In this example, I'll cover how I conditionally create an S3 bucket if a microservice calls for it; but you can use the same pattern to provision any resource conditionally, based on a passed-in variable.

Defining The Conditional Variable

For my purposes, I usually only ever give a service access to one S3 bucket. That said, it would be redundant if I were to create a boolean variable to decide whether or not to create a bucket, and then another variable to name the bucket.

Instead, I can just create a variable to name the bucket with a default value of an empty string:

# /modules/service-resources/vars.tf
variable "s3_bucket_name" {
  type = string
  default = ""
}

If the bucket name does not match the default, then I'll know I need to create a bucket:

# /modules/service-resources/s3-bucket.tf
locals {
  bucket_count = var.s3_bucket_name == "" ? 1 : 0
}

Here, I'm setting a local variable to indicate if I want 1 bucket or 0 buckets. If I use this module and don't pass in a bucket_name value, then no bucket will be created.

Conditional Provisioning via Count

OpenTofu (and Terraform before it) treats any resource declaration that it comes across as a declaration that said resource must be provisioned. The only way to get around that is to add the count attribute and set it to the number of instances of that particular resource it should provision. If that number is 0, it won't provision any.

In the case of my S3 bucket, it looks like this:

# /modules/service-resources/s3-bucket.tf
resource "aws_s3_bucket" "service_bucket" {
  count  = local.bucket_count
  bucket = var.s3_bucket_name
  # ...other attributes
}

If we add this to the block defined above, then any time we pass in a non-empty string for "s3_bucket_name", we'll get 1 S3 bucket.

The actual implementation looks like this:

# /main.tf
module "email-service" {
  source             = "./modules/service-resources"
  s3_bucket_name     = "my-email-templates"
  service_name       = "email-service"
  # ...other variables passed in
}

This is an example similar to one of the actual module implementations that I use in production. It's a service that stores Golang HTML templates in an S3 bucket, and then fetches and renders them as it creates SMTP messages.