The first time I had to manage infrastructure across multiple environments, the temptation was to make the setup as simple as possible. One folder for dev, one for staging, one for prod, and the same Terraform files copied into each one.
That feels practical at first. Then the environments start changing in slightly different ways, and the copy-paste approach becomes its own problem. One environment gets a fix and the others do not. One variable gets updated in staging and never makes it to prod. A small tweak that should have been straightforward turns into a comparison exercise because nobody is quite sure which folder is authoritative anymore.
I have seen enough of that kind of drift to stop trusting the copy-paste model.
What works better is separating reusable infrastructure from environment-specific values. In practice, that means modules for the building blocks and separate environment folders for dev, staging, and prod. The module defines what the infrastructure does. The environment folder decides how it should look in that context.
That distinction matters a lot once the systems are real. I have worked in environments where infrastructure was being moved, deployed, or recovered across AWS and Azure, and the biggest source of confusion was often not the cloud provider. It was the lack of a clean boundary between shared logic and environment-specific settings. Once that boundary is clear, reviews become much easier and accidental drift becomes much less likely.
I also stopped liking Terraform workspaces for most real use cases. They look convenient until the environments become meaningfully different. If dev needs a smaller instance type, prod needs a different network range, and staging needs to stay close to production but not identical, workspaces start pushing you toward conditionals that make the code harder to reason about. That ends up feeling worse than just structuring things properly from the beginning.
The module-plus-environment approach is boring in the best way. A VPC module stays generic. An ECS service module stays generic. An RDS module stays generic. The environment folder passes in the values. That keeps the code easy to scan and reduces the chance that a prod-only change leaks into dev by accident.
State separation is non-negotiable too. If dev and prod point at the same backend key, you are asking for trouble. Keeping each environment’s state isolated is one of those unglamorous decisions that prevents very glamorous incidents later. I have learned that infrastructure mistakes are almost always easier to prevent than to clean up.
I also strongly prefer using CI for plan and apply instead of applying from laptops. Infrastructure becomes much easier to trust when it is reviewed the same way application code is reviewed. That gives you a clear history, a predictable workflow, and fewer surprises when something changes.
A few other habits have saved me time: pin module versions, keep secrets out of committed tfvars files, and run periodic drift checks even when nobody is actively changing infrastructure. Manual console changes happen. Drift happens. The only question is whether you notice before it starts causing confusion.
The first time you set this kind of structure up, it feels like extra work. After you have lived through enough environment drift, it starts feeling like the only sane way to do it.
Top comments (0)