Written by
Halkwinds Editorial Team
Halkwinds Research & Editorial

Infrastructure as Code with Terraform: From Zero to Production
How to manage cloud infrastructure as version-controlled code — modules, state management, CI/CD integration, and team workflows.
If you're managing an engineering team that has grown past a handful of servers, you've likely felt the pain of manual infrastructure. Someone spins up an EC2 instance through the AWS console, forgets to document it, and six months later nobody knows why it exists or whether it's safe to delete. Environments drift. Staging doesn't match production. Onboarding a new engineer means walking them through a wiki that's already out of date. Terraform infrastructure as code solves this class of problems by treating your cloud resources the same way you treat application code — versioned, reviewed, and reproducible. This guide walks you through the journey from an empty repository to a production-grade Terraform setup, with a focus on the workflow and organizational decisions that actually matter for a team.
- Background / Why This Matters
- Prerequisites and Planning
- Step-by-Step Implementation
- Testing and Validation
- Common Mistakes / What to Avoid
- Frequently Asked Questions
- Conclusion
Background / Why This Matters
Infrastructure as Code (IaC) is the practice of defining your servers, networks, databases, and permissions in declarative configuration files instead of clicking through a console. Terraform, built by HashiCorp, has become the de facto standard because it's cloud-agnostic, has a massive provider ecosystem (AWS, Azure, GCP, Datadog, Cloudflare, and hundreds more), and uses a readable declarative language (HCL).
For an engineering manager, the value isn't abstract elegance — it's operational risk reduction. Consider what changes when infrastructure lives in Git:
- Change review: Every infrastructure change goes through a pull request. Your senior engineer can catch a security group that's open to
0.0.0.0/0before it hits production. - Auditability: Git history tells you who changed what and when. When an incident happens,
git logis your first responder. - Reproducibility: Spinning up an identical staging environment becomes a matter of running the same code with different variables.
- Disaster recovery: If a region goes down, your entire environment definition is code you can re-apply elsewhere.
Manual infrastructure doesn't scale with team size. Estimates from various DevOps surveys suggest that teams adopting IaC significantly reduce provisioning time and configuration errors, though results vary widely by organizational maturity. The point isn't the exact number — it's that the failure modes of manual clicks compound as your team and environment grow.
Actionable takeaway: If you can't currently answer "who created this resource and why" for your production environment, that's the signal that you've outgrown manual infrastructure management.
Prerequisites and Planning
Before writing a single line of HCL, get the foundations right. Rushing into Terraform without planning state management and repository structure is the most common way teams end up with an unmaintainable mess.
Technical prerequisites
- An AWS account (or your target cloud) with IAM permissions to create resources.
- Terraform CLI installed (use a version manager like
tfenvso the whole team pins the same version — version drift between engineers causes subtle state issues). - A Git repository, ideally on GitHub if you plan to use GitHub Actions or Atlantis later.
- A remote state backend. Do not use local state for team work.
Decisions to make up front
1. Remote state and locking. Terraform tracks the mapping between your code and real resources in a state file. For teams, store it remotely with locking to prevent two engineers from applying at once. On AWS, the standard is an S3 bucket for the state plus a DynamoDB table for state locking.
2. Repository structure. Decide between a monorepo (all infrastructure in one repo) or per-service repos. For most SMBs and mid-size teams, a monorepo with clear directory separation works well.
3. Environment isolation. Keep production and staging state completely separate. Never share a single state file across environments — a bad apply in staging should never be able to touch production.
A workable starting layout:
/modules— reusable building blocks (a VPC module, an RDS module, an ECS service module)/environments/staging— staging composition that calls modules/environments/production— production composition
Actionable takeaway: Set up your S3 + DynamoDB backend and repository structure before writing resource code. These decisions are painful to change once state exists.
Step-by-Step Implementation
Step 1: Bootstrap the backend
Create the S3 bucket and DynamoDB table that will hold your state. Enable versioning and encryption on the bucket. Because this is a chicken-and-egg problem (Terraform needs a backend, but you're creating the backend), many teams create these two resources manually the first time, or with a small bootstrap Terraform config that uses local state, then import them.
Step 2: Configure the backend and provider
In your environment directory, define the backend and provider. A minimal backend.tf points to your S3 bucket, state key path, region, and DynamoDB lock table. The provider block pins the AWS provider version and region. Always pin provider versions — an unpinned provider can introduce breaking changes on the next terraform init.
Step 3: Write your first module
Resist the urge to write one giant file. Build small, focused modules. A VPC module, for example, should accept inputs (CIDR block, availability zones, environment name) and expose outputs (VPC ID, subnet IDs) that other modules consume. Well-designed modules are the difference between infrastructure you can maintain and infrastructure you're afraid to touch.
Keep modules generic. Hard-coding production into a module defeats its reusability. Pass environment-specific values in as variables from the environment composition layer.
Step 4: Compose environments
In /environments/staging/main.tf, call your modules with staging-specific variables. Do the same for production with production values (larger instance sizes, multi-AZ databases, stricter security). This is where the reproducibility payoff lands — staging and production share the same module code, differing only in inputs.
Step 5: Run the core workflow
terraform init— downloads providers and configures the backend.terraform plan— shows exactly what will change. This is your safety net. Read it every time.terraform apply— makes the changes after you approve the plan.
The plan output is the single most important habit to instill in your team. It tells you whether Terraform intends to create, modify, or — the dangerous one — destroy and recreate a resource. A plan that shows a database being destroyed is a plan you stop and investigate.
Step 6: Add CI/CD and team workflow
Manual applies from laptops don't scale and are hard to audit. Two common approaches:
| Approach | How it works | Best for |
|---|---|---|
| GitHub Actions | A workflow runs terraform plan on pull requests and posts the plan as a comment; apply runs on merge to main. | Teams already standardized on GitHub who want lightweight, code-owned pipelines. |
| Atlantis | A self-hosted server listens for PR comments; engineers type atlantis plan and atlantis apply directly in the PR, with locking to prevent concurrent applies. | Teams wanting a purpose-built PR-driven workflow with built-in locking and approval gates. |
| Terraform Cloud / HCP | Managed backend, runs, and state with a UI and policy enforcement. | Teams wanting a managed solution and willing to pay per-user. |
For a growing engineering team, GitHub Actions is the fastest path to a reviewable pipeline. As complexity grows, Atlantis shines because the plan-and-apply conversation happens right inside the pull request, keeping context and approval together.
Actionable takeaway: The moment more than one person touches infrastructure, move applies off laptops and into a PR-driven pipeline. This is where Halkwinds often helps teams — designing the module structure and CI/CD gates so infrastructure changes are as safe and reviewable as application code.
Testing and Validation
Terraform code deserves the same validation discipline as application code. Layer these checks into your pipeline:
terraform fmt -check— enforces consistent formatting. Zero-cost, catches noise in diffs.terraform validate— catches syntax and internal consistency errors before a plan.- tflint — a linter that catches provider-specific mistakes (invalid instance types, deprecated arguments).
- tfsec / Checkov — static security scanners that flag things like unencrypted buckets or overly permissive security groups.
- Terratest or the native
terraform testframework — spins up real resources in a sandbox, asserts they behave correctly, then tears them down. Reserve this for critical modules given the time and cost.
The most important "test" is still the plan output attached to every pull request. Require it as a status check. A human reviewer reading a plan catches intent-level problems no linter can: "Why is this PR deleting the production RDS instance?"
Actionable takeaway: Addfmt,validate,tflint, and a security scanner as required CI checks. They cost minutes to set up and prevent entire categories of incidents.
Common Mistakes / What to Avoid
Committing state or secrets to Git
State files can contain secrets in plaintext (database passwords, generated keys). Never commit terraform.tfstate. Keep it in your remote backend, and add it to .gitignore defensively.
Manual changes in the console (drift)
The fastest way to break Terraform is to click "fix it quickly" in the AWS console during an incident. Now your state and reality disagree, and the next apply may undo the fix or fail. Enforce a policy: production changes go through code. If an emergency console change is unavoidable, immediately reconcile it in Terraform afterward.
Monolithic state files
Putting your entire infrastructure in one state file means every tiny change locks the whole thing and every plan takes minutes. Split state by environment
Explore Further