A reusable Terraform base for AWS and GCP
One repository shape, small modules and strict state rules that turn the first week of every new cloud project into an afternoon.
- date:
- reading:
- 3 min
- tags:
- #terraform#aws#gcp
Every new project needs the same first week: a network, an access model, somewhere to run containers and a way to see what is happening. This is the layout I use to make that week an afternoon, for both AWS and GCP.
The idea
Keep one repository per cloud, with the same shape in each:
infra/├── modules/│ ├── network/│ ├── cluster/│ └── observability/├── envs/│ ├── dev/│ └── prod/└── README.mdModules hold the logic and know nothing about environments. Each folder in envs/ only calls the modules with its own values. That is the whole trick: a new environment is a new folder, not a new design.
One module, two clouds
The network module takes the same inputs on both clouds, so the environment files look alike:
module "network" { source = "../../modules/network" name = "dev" cidr_block = "10.10.0.0/16" az_count = 2} module "cluster" { source = "../../modules/cluster" name = "dev" network_id = module.network.id node_count = 2}The differences stay inside the modules:
| Concern | AWS | GCP |
|---|---|---|
| Network | VPC with public and private subnets | VPC with regional subnets |
| Cluster | EKS | GKE |
| Node identity | IAM role per node group | Service account per node pool |
| Egress | NAT gateway | Cloud NAT |
State and safety
State is the part that hurts when it goes wrong, so it gets its own rules:
Remote state in an object bucket, with versioning on
State locking, so two applies can not run at once
One state file per environment, never one for everything
terraform { backend "s3" { bucket = "acme-terraform-state" key = "dev/network.tfstate" region = "ap-southeast-1" dynamodb_table = "terraform-locks" encrypt = true }}The workflow
terraform fmt -check -recursiveterraform validateterraform plan -out=tfplanPlan: 14 to add, 0 to change, 0 to destroy.terraform apply tfplanThe plan is saved to a file and the apply uses that exact file. What was reviewed is what runs.
Lessons
Small modules with few inputs are easier to reuse than flexible ones with forty.
Pin provider and module versions. An unpinned upgrade is a surprise change.
Tag everything with its environment and owner from day one. Cost reports depend on it.
Write the destroy plan too. If you can not tear an environment down cleanly, you do not fully understand it.