{"id":20334921,"url":"https://github.com/mdb/tf-workspaces-demo","last_synced_at":"2026-06-05T08:31:33.648Z","repository":{"id":213178081,"uuid":"723040972","full_name":"mdb/tf-workspaces-demo","owner":"mdb","description":"A simple demo illustrating the use of Terraform workspaces in concert with GitHub Actions matrix builds","archived":false,"fork":false,"pushed_at":"2024-01-03T17:37:47.000Z","size":91,"stargazers_count":0,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-01-14T16:36:29.231Z","etag":null,"topics":["iac","terraform"],"latest_commit_sha":null,"homepage":"","language":"HCL","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mdb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-24T14:36:51.000Z","updated_at":"2023-12-21T11:40:16.000Z","dependencies_parsed_at":"2023-12-19T05:10:02.420Z","dependency_job_id":"74a1d7fb-65a9-4ecc-86d6-2307342d06da","html_url":"https://github.com/mdb/tf-workspaces-demo","commit_stats":null,"previous_names":["mdb/tf-workspaces-demo"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdb%2Ftf-workspaces-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdb%2Ftf-workspaces-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdb%2Ftf-workspaces-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdb%2Ftf-workspaces-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mdb","download_url":"https://codeload.github.com/mdb/tf-workspaces-demo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241854616,"owners_count":20031467,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["iac","terraform"],"created_at":"2024-11-14T20:38:35.494Z","updated_at":"2025-11-30T18:05:37.008Z","avatar_url":"https://github.com/mdb.png","language":"HCL","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Terraform](https://github.com/mdb/tf-workspaces-demo/actions/workflows/terraform.yaml/badge.svg?branch=main)](https://github.com/mdb/tf-workspaces-demo/actions/workflows/terraform.yaml)\n\n# tf-workspaces-demo\n\n**Problem statement:** You need to create and manage cloud infrastructure\nlandscapes across many different AWS account/region combinations targeting\ndifferent logical environments (`dev`, `prod`, etc.).\n\nHow can a single, Terraform project be used across all necessary account, region,\nand environment combinations? How can the IaC be modeled to enforce security best\npractices, uniformity, and logically isolated failure domains, while also\naccommodating intentional heterogeneity?\n\n**Solution:** In my experience, Terraform's [workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) feature -- used in concert with a compound `${AWS_ACCOUNT_ID}_${AWS_REGION}_${ENV}`-based workspace naming convention -- enables scalable, [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) re-use patterns, and logical infrastructure segmentation, reducing toil and lead time.\n\n`tf-workspaces-demo` offers a reference implementation.\n\n- See [GitHub Actions run 7299615362](https://github.com/mdb/tf-workspaces-demo/actions/runs/7299615362) as an example invocation of its CI/CD workflow against `main`.\n- See [GitHub Actions run 7287133745](https://github.com/mdb/tf-workspaces-demo/actions/runs/7287133745) as an example invocation of its CI/CD workflow against a pull request.\n\n## Highlights\n\n- Use a `${AWS_ACCOUNT_ID}_${AWS_REGION}_${ENV}`-compound workspace naming scheme to\n  logically segment Terraform operations (and [state](https://developer.hashicorp.com/terraform/language/state)) across AWS account/region/environment\n  boundaries, ensuring infrastructure redundancy across sufficiently limited failure domains.\n- Ensure uniformity across workspaces, while also accommodating intentional per-workspace\n  (or per-account, per-region, or per-environment) heterogeneity if/where needed.\n- Enable the low-friction creation of new infrastructure in new\n  account/region/environment combinations by adding a single workspace entry to\n  `workspaces.json`.\n- Dynamically drive the creation of GitHub Actions matrix builds to `plan`/`apply`,\n  Terraforom, ensuring CI/CD automation elastically scales and contracts as\n  workspaces are created and/or decommissioned.\n- Use the `terraform.workspace` to impose an [allowed_account_ids](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#allowed_account_ids) constraint on the AWS provider, such that an environment is never `plan`/`apply`'d to the wrong account.\n- Bonus: leverage Terraform workspaces to dynamically create ephemeral pull-request-based\n  development and testing environments.\n  - See [PR 16](https://github.com/mdb/tf-workspaces-demo/pull/16) and its\n    associated [GitHub Actions workflow](https://github.com/mdb/tf-workspaces-demo/actions/runs/7287133745) as an\n    example.\n  - See [PR 21](https://github.com/mdb/tf-workspaces-demo/pull/21) and its\n    [environment's destruction](https://github.com/mdb/tf-workspaces-demo/actions/runs/7295025843) as an\n    example of the automated destruction of an ephemeral environment after\n    a PR is closed or merged.\n\n**Disclaimers**\n\n- It's often useful to subdivide IaC across **_responsibility_**-based projects,\n  each serving a different \"layer\" of infrastructure purpose (vs. problematically large, sprawling\n  \"monolithic\" Terraform projects). For example, foundational infrastructure,\n  such as VPC and networking configuration, may be managed in separate Terraform project(s)\n  than higher level platform infrastructure, such as Kubernetes clusters.\n  `tf-workspaces-demo` glosses over this, focusing instead on the effective\n  use of Terraform workspace conventions _within_ projects. Effective modeling\n  of responsibility layers across distinct Terraform projects is a separate art\n  altogether ;)\n- To decouple the demo from real-world AWS dependencies, `tf-workspaces-demo` uses [localstack-persist](https://hub.docker.com/r/gresau/localstack-persist) as a local, mocked AWS. No real AWS resources are created; instead, the demo focuses on illustrating high level Terraform/AWS patterns that are largely agnostic to the specific AWS resources under management.\n- The use of `localstack-persist` -- and the demo's need to persist `localstack` data across GitHub Actions jobs -- requires lotsa extra GitHub Actions workflow monkey business that wouldn't appear in a real world workflow targeting a real cloud provider. Try not to be too distracted by that :)\n- `tf-workspaces-demo`'s GitHub Actions workflow is **not** intended as the\n  canonical design universally applicable to all projects and contexts. Depending on\n  needs, it may be attractive to structure a project's CI/CD differently. For example,\n  there could be distinct jobs -- or even separate workflows, entirely -- targeting `dev`\n  and `prod` (each composed of per-workspace parallelized matrix builds), such that\n  CI/CD parallelizes operations within the same environment, while still ensuring\n  per-workspace Terraform operations against `prod` hinge on `dev` operations' success.\n  Additionally, the workflow(s) could be enhanced with additional steps and\n  fanciness: [terratest](https://terratest.gruntwork.io/) tests, [OPX automated plan analysis](https://mikeball.info/blog/terraform-plan-validation-with-open-policy-agent/), automated\n  pull request commenting reporting `plan` output, etc.\n\n## Bonus highlights and callouts\n\nWhile only peripherally relevant to the core problem statement, `tf-workspaces-demo`\ndemos some other fun stuff too.\n\n- On pull requests, `plan`/`apply` to an ephemeral pull-request workspace;\n  destroy that workspace if/when the pull request is closed or merged (and automate the\n  creation of PR comments announcing these actions) (This also demonstrates how\n  the `${AWS_ACCOUNT_ID}_${AWS_REGION}_${ENV}` workspace naming scheme accommodates\n  additional, increasingly granular suffixes if/where needed, like\n  `${AWS_ACCOUNT_ID}_${AWS_REGION}_${ENV}_pr-${PULL_REQUEST_ID}`).\n- [localstack-persist](https://hub.docker.com/r/gresau/localstack-persist) is used to\n  create a local mock AWS, mostly to decouple `tf-workspace-demo` from real AWS dependencies,\n  while still illustrating some AWS/Terraform design patterns. Zooming out, though,\n  `localstack` is useful for demos like `tf-workspaces-demo`, but also useful in\n  development and testing real Terraform projects and modules, depending on\n  context.\n- `tf-workspaces-demo` uses [actions/upload-artifact](https://github.com/actions/upload-artifact)\n  in a kinda-fun-but-maybe-hacky way to persist `localstack-persist` spanning multiple\n  GitHub Actions jobs. This is a bit unusual; try not to be too _too_\n  distracted.\n- By imposing a `strategy.max-parallel: 1` on the GitHub Actions matrix build,\n  Terraform actions are invoked serially against each workspace in the order in\n  which workspaces are listed in `workspaces.json`. This means an error\n  applying to a `dev` workspace fails the build before any Terraform action is taken\n  against `prod` workspaces.\n- `tf-workspaces-demo`'s `docker-compose.yaml` shows how to establish a\n  `localstack`-based local AWS environment, pre-seeded with an S3 bucket for use\n  persisting [Terraform state to S3](https://developer.hashicorp.com/terraform/language/state/remote), as\n  well as a DynamoDB table for use enforcing [Terraform state locking](https://developer.hashicorp.com/terraform/language/state/locking)\n\nSee the source code comments for particular details and relevant callouts.\n\n## Wanna learn more about Terraform workspaces?\n\n- [Using Terraform Workspaces](https://mikeball.info/blog/using-terraform-workspaces/)\n- [HashiCorp's Terraform Workspaces documentation](https://developer.hashicorp.com/terraform/language/state/workspaces)\n\n## What about other tools/techniques?\n\n- **What about [terragrunt](https://terragrunt.gruntwork.io/)?**\n\n  In many contexts, [Terragrunt](https://terragrunt.gruntwork.io/) (and similar tools)\n  are great. However, their use invites additional complexity (and additional questions about\n  how best to structure IaC across account, region, and environment boundaries). Often,\n  in my experience, Terraform workspaces are sufficient.\n\n- **Don't [Terraform child modules](https://developer.hashicorp.com/terraform/language/modules#child-modules) enable DRY reuse?**\n\n  Generally, Terraform child modules and workspaces address slightly different problems and are not mutually exclusive. While\n  workspaces facilitate the application of a Terraform project against multiple\n  target contexts, provider configurations, and against isolated [states](https://developer.hashicorp.com/terraform/language/state), child modules are more simply generic\n  abstractions of opinionated Terraform \"recipes.\" Modules often target specific\n  resources (or combinations of resources), but are largely agnostic to the\n  surrounding context. Child modules can be used and applied within parent Terraform\n  projects, though they cannot be applied independently; they have no project-specific [state](https://developer.hashicorp.com/terraform/language/state) and [provider](https://developer.hashicorp.com/terraform/language/providers) configuration. As such, child modules enable reuse and [composability](https://developer.hashicorp.com/terraform/language/modules/develop/composition) -- and/or enforce best practices governance -- along different dimensions of concern.\n\n- **Couldn't `region` be an [input variable](https://developer.hashicorp.com/terraform/language/values/variables)?**\n\n  Rather than being encoded in the compound workspace naming convention, the\n  Terraform project could utilize a `var.region` input variable, yes. However,\n  this would lead to two problems:\n\n  1. Workspace naming collision. For example, `123_dev`'s `us-east-1` and `123_dev`'s `us-west-2` deployments would no longer have unique workspace names.\n  2. Workspace S3 state collision. For example, Terraform would attempt to use `s3://${BUCKET}/env:/123_dev/terraform.tfstate` for both `123_dev`'s `us-east-1` and `123_dev`'s `us-west-2` applications.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdb%2Ftf-workspaces-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmdb%2Ftf-workspaces-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdb%2Ftf-workspaces-demo/lists"}