DEV Community

Daniel Schroeder
Daniel Schroeder

Posted on • Originally published at github.com

Terraform workspace config organization

The suggested best practices for organizing configuration for multiple workspaces/environments is to call Terraform with -var-file=$env to include a specific tfvars file.

Of course this works. But it seems to be error prone if you allow to trigger an apply against any workspace with just any configuration:

terraform workspace select production
terraform apply -var-file=config/staging.tfvars
Enter fullscreen mode Exit fullscreen mode

Furthermore this cannot be used in Terraform Cloud, where you have to specify workspace related vars in the workspace configuration itself:

Terraform Cloud Variable configuration

There is no option for including tfvars per workspace.

Select config automatically based on the workspace

Unfortunately there is no functionality to automatically include a tfvars file based on the workspace name nor is there support for conditionally including tfvars files.

So you got to build something yourself. You can access the current workspace name via terraform.workspace. There are a couple of things you can do with this value.

Below are 3 solutions which all have the same exact outcome. The defined config is stored in local.config and can be access via local.config.ec2_instance_type etc.

All 3 solutions support default values, so you're not required to define every config option in every environment.

All examples are available in this repository.

1. Inline expressions to select correct config from a map

All config is held is a single file config.tf:

locals {
  configs = {

    _defaults = {
      ec2_instance_type = "t2.nano"
      regions           = ["us-east-1"]
    }

    dev = {} // use config from _defaults ^

    staging = {
      ec2_instance_type = "t2.medium"
      regions = [
        "us-east-1",
        "eu-central-1",
      ]
    }

    production = {
      ec2_instance_type = "t2.xlarge"
      regions = [
        "us-east-1",
        "us-west-2",
        "eu-central-1",
        "ap-east-1",
      ]
    }

  }

  config = merge(
    lookup(local.configs, "_defaults"),
    lookup(local.configs, terraform.workspace)
  )
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Straight forward

Cons:

  • Cannot split config into separate files. Therefore could quickly get hard to maintain and compare environment config.

2. Load config from YAML files

Directory structure:

.
├── config
│   ├── _defaults.yml
│   ├── dev.yml
│   ├── production.yml
│   └── staging.yml
├── config.tf
├── main.tf
Enter fullscreen mode Exit fullscreen mode

Example content of config/production.yml:

---
ec2_instance_type: t2.xlarge

regions:
  - us-east-1
  - us-west-2
  - eu-central-1
  - ap-east-1
Enter fullscreen mode Exit fullscreen mode

The config is loaded in config.yml:

data "local_file" "defaults" {
  filename = "${path.module}/config/_defaults.yml"
}

data "local_file" "config" {
  filename = "${path.module}/config/${terraform.workspace}.yml"
}

locals {
  config = merge(
    yamldecode(data.local_file.defaults.content),
    yamldecode(data.local_file.config.content)
  )
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Straight forward
  • Config for every environment resides in its own file

Cons:

  • No HCL expressions are possible in the config itself

3. Create a module per environment and return the config as an output

Directory structure:

.
├── config
│   ├── _defaults
│   │   └── outputs.tf
│   ├── dev
│   │   └── outputs.tf
│   ├── main.tf
│   ├── production
│   │   └── outputs.tf
│   └── staging
│       └── outputs.tf
├── main.tf
Enter fullscreen mode Exit fullscreen mode

In every config/$env/outputs.tf a single output is defined like this:

config/_defaults/outputs.tf:

output "data" {
  value = {
    ec2_instance_type = "t2.nano"
    regions = [
      "us-east-1",
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

config/dev/outputs.tf:

output "data" {
  value = {} // use config from _defaults
}
Enter fullscreen mode Exit fullscreen mode

config/staging/outputs.tf:

output "data" {
  value = {
    ec2_instance_type = "t2.medium"
    regions = [
      "us-east-1",
      "eu-central-1",
    ]
  }
}

Enter fullscreen mode Exit fullscreen mode

config/production/outputs.tf:

output "data" {
  value = {
    ec2_instance_type = "t2.xlarge"
    regions = [
      "us-east-1",
      "us-west-2",
      "eu-central-1",
      "ap-east-1",
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Since you cannot use variables in a module source parameter all 4 modules have to be defined in every environment. Furthermore you cannot directly access a module by name when the name is not hardcoded, so you need to additionally create a mapping like so:

config/main.tf:

module "_defaults" {
  source = "./_defaults"
}

module "dev" {
  source = "./dev"
}

module "staging" {
  source = "./staging"
}

module "production" {
  source = "./production"
}

locals {
  data_map = {
    dev        = module.dev.data,
    staging    = module.staging.data,
    production = module.production.data,
  }
}

output "data" {
  value = merge(
    module._defaults.data,
    lookup(local.data_map, terraform.workspace)
  )
}
Enter fullscreen mode Exit fullscreen mode

In the main.tf then the module needs to be loaded and for convenience the output gets registered as a local value:

module "config" {
  source = "./config"
}

locals {
  config = module.config.data
}
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Config for every environment resides in its own file

Cons:

  • Complex setup

Conclusion

Using modules as config provider seems to be the best solution, as you can split the configuration into separate files which supports HCL expressions. The setup though is complex and requires some additional boilerplate code for every additional environment.

If you have no need for HCL expressions, the YAML solution seems to be nice as it is easy to setup and IMHO is very readable to humans.

Top comments (1)

Collapse
 
arcticjuggernaut profile image
Chris Johnston

I am trying 2 now and it's really limiting. If you want to use arrays in your config (e.g. for tags) it's almost impossible.