I've done this very similarly but also using a combination of using workspaces (formerly `terraform env`) and symlinking. Having to maintain a quick and dirty bash script that iterates over services and creates the symlinks and establishes state seems unavoidable at this point BUT it's really not that hairy and mine haven't required a lot in the way of change once the architecture design is in place. I'm working to put together a reference repo of how this looks so others can comment on this approach. A word on workspaces first:
Workspaces
For how I've been designing terraform repos, all terraform code is invoked at the leaf directories within terraform/services/. Workspaces can/should be leveraged such that multiple regions and service environments are encoded in the workspaces. For example, the workspaces contained in terraform/services/application might be:
dev:us-west-2
qa:us-west-2
stage:us-west-2
stage:us-east-1
prod:us-west-2
prod:us-east-1
This allows CI/CD to select any environments that match a given branch name (e.g. prod matches both prod:us-west-2 and prod:us-east-1) and execute terraform init/verify/plan/apply against them.
Using maps within the environment specific variables files (e.g.
stage.tf), you're able to create variables to encode differences that might be needed from region to region through lookups.
Repo structure
This is generally how the infrastructure repo for a project looks for me:
- repo_root/
- README.MD
- travis.yml # calls packer build steps, invokes terraform init/verify/plan/apply through code in the scripts dir
- .pre-commit.yaml # keeps code clean
- scripts/
- create_tf_symlinks_and_init.sh # based on the branch, cd into each terraform service, and run terraform init providing the s3 backend variables that terraform is unable to set as variables
- tf_apply_all.sh # call create_tf_symlinks, cd into each service, and run terraform verify/plan/apply against workspaces mapping to the current branch name. If prod, require that a <env>_<region>_plan.out exists
- packer_build.sh # takes care of any complexity in invoking packer that packer makes a difficult or impossible since json is limited (e.g. discovering tags to apply to the build artifacts dynamically)
- packer/
- [packer dir contains anything related to image/container build process. see hashicorp/best-practices repo on github for an example]
- terraform/
- scripts/ # directory containing any tf templates to be rendered or code needed for local_exec e.g. userdata
- variables/ # contains all variables that might be used across service directories
- variables.tf # common variables across all services, always symlinked
- prod_account.tf # production account variables e.g. s3 bucket name and account ID, conditionally symlinked
- dev_account.tf # dev AWS account variables
- backend.tf # a shared backend file
- environments/ # contains variables (often maps and strings) specific to an environment e.g. mapping of tf workspace names to a canonical environment short name OR a true/false value for scaling down to 0 on a nightly basis which you might want in dev but not in prod/stage
- services/
- network/
- data/
- application/
The terraform directory structure used to be much uglier before the addition of workspaces/environments making the symlinking is much less burdensome now.
I'll revisit this thread once I've created the reference repo which should hopefully clear up anything I glossed over above. Hopefully that answers your question from at least one angle. Any thoughts as to how this could be made simpler is welcome! :)
- Brandon