Infrastructure as Code With Terraform: A Practical Guide

Learn infrastructure as code with Terraform — providers, resources, state, plans, modules, environments, remote state and locking, CI workflows, drift, secrets and best practices.

By Danyal Ahmed - Data Scientist · · 4 min read

Clicking through a cloud console to create servers, databases and networks works for an afternoon. It doesn't work for a team: nobody remembers exactly what was configured, staging drifts from production, and recreating an environment after a disaster becomes an archaeology project. Infrastructure as code (IaC) fixes this by defining infrastructure in version-controlled files that are reviewed, tested and applied automatically.

Terraform is one of the most widely used IaC tools. This guide covers how it works and how to use it well.

Key takeaways

  • IaC makes infrastructure reproducible, reviewable and automated, just like application code.
  • Terraform is declarative: you describe the desired end state; Terraform computes and applies the changes.
  • State maps your configuration to real resources. Store it remotely, with locking and encryption.
  • Always review terraform plan before applying, ideally in CI on every pull request.
  • Use modules for reuse and separate state per environment to limit blast radius.

Why infrastructure as code?

  • Reproducibility: create identical environments (dev, staging, production) from the same code.
  • Review and audit: infrastructure changes go through pull requests, with history in git.
  • Speed and consistency: no manual steps, and no snowflake servers.
  • Disaster recovery: rebuild infrastructure from code.
  • Documentation: the code is the documentation of what exists.

How Terraform works

text.tf files (desired state) ──▶ terraform plan ──▶ diff vs. state + real infrastructure
                                                        │
                                            terraform apply ──▶ provider APIs (AWS, GCP, Azure,
                                                        │        Cloudflare, GitHub, Datadog, ...)
                                                        ▼
                                              updated state file
  • Providers are plugins that talk to APIs: cloud providers, DNS, SaaS tools.
  • Resources are the things you manage (a VPC, a database, a DNS record).
  • Data sources read existing information.
  • State records which real resources correspond to which configuration.

A note on licensing: Terraform moved to the Business Source License in 2023, and the community created OpenTofu, an open-source fork that is largely compatible with Terraform configurations. The concepts in this guide apply to both.

A first configuration

hclterraform {
  required_version = ">= 1.6"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
  backend "s3" {
    bucket         = "acme-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"   # state locking
    encrypt        = true
  }
}

provider "aws" {
  region = var.region
}

variable "region" {
  type    = string
  default = "us-east-1"
}

resource "aws_s3_bucket" "uploads" {
  bucket = "acme-prod-uploads"
  tags   = { Environment = "prod", ManagedBy = "terraform" }
}

resource "aws_s3_bucket_public_access_block" "uploads" {
  bucket                  = aws_s3_bucket.uploads.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

output "uploads_bucket" {
  value = aws_s3_bucket.uploads.bucket
}

Workflow:

bashterraform init        # download providers, configure the backend
terraform fmt         # format code
terraform validate    # static validation
terraform plan -out=tfplan
terraform apply tfplan

State: handle with care

The state file is critical:

  • It maps configuration to real resource IDs.
  • It can contain sensitive values (database passwords, keys).
  • Concurrent applies against the same state can corrupt it.

Best practices:

  • Use a remote backend (object storage with locking, or a managed Terraform service). Never commit state to git.
  • Enable encryption and restrict access tightly.
  • Enable locking so only one apply runs at a time.
  • Split state by environment and by component (network, data, apps) to limit blast radius and speed up plans.
  • Use terraform import (or import blocks) to bring existing resources under management, and moved blocks to refactor without destroying resources.

Modules: reusable building blocks

Modules package related resources with inputs and outputs:

hclmodule "api_service" {
  source         = "./modules/web-service"
  name           = "api"
  environment    = "prod"
  instance_count = 3
  image          = "registry.example.com/api:1.4.2"
}

Guidelines:

  • Create modules for patterns you repeat (a service, a database with backups, a network).
  • Keep modules focused with clear inputs and outputs. Avoid "god modules" with 80 variables.
  • Version shared modules and pin versions in consumers.

Managing environments

Common approaches:

Approach How Notes
Directory per environment envs/dev, envs/staging, envs/prod, each with its own state, calling shared modules Clear and explicit; the most common
Workspaces One configuration, multiple state instances Convenient but easy to confuse; less explicit
Terragrunt or similar wrappers DRY configuration across many environments Useful at larger scale

Whatever you choose, keep production isolated, ideally in a separate cloud account or project with separate credentials.

Terraform in CI/CD

A safe team workflow:

  1. Engineer opens a PR changing .tf files.
  2. CI runs fmt, validate, linting and security scanning, then terraform plan, and posts the plan to the PR.
  3. Reviewers check the plan (what will actually change), not just the code.
  4. On merge, CI runs apply with the reviewed plan, using short-lived credentials (such as OIDC federation) with least privilege.

See CI/CD pipeline guide. Watch for plans that destroy or replace resources (especially databases). Protect critical resources with prevent_destroy lifecycle rules.

Drift detection

Drift happens when someone changes infrastructure outside Terraform (a hotfix in the console, for example). Run scheduled terraform plan jobs to detect drift and either codify the change or revert it. Restrict console write access in production so Terraform stays the source of truth.

Secrets

  • Don't hard-code secrets in .tf files or variables committed to git.
  • Read secrets from a secret manager at apply time, or let services fetch them at runtime.
  • Mark sensitive outputs with sensitive = true (they're still stored in state, which is why state must be protected).

Testing and policy

  • Static analysis and security scanners catch misconfigurations (public buckets, open security groups, missing encryption).
  • Policy as code enforces organizational rules (required tags, allowed regions, no public databases).
  • Automated tests can deploy modules to a sandbox account, verify behavior and tear them down.

Common pitfalls

  1. Local state on someone's laptop.
  2. One giant state for everything: slow plans and a huge blast radius.
  3. Applying without reviewing the plan.
  4. Manual console changes causing drift.
  5. Unpinned provider versions leading to surprise changes.
  6. Overly clever code: excessive dynamic blocks and conditionals that nobody can read.

Beyond Terraform

  • Pulumi and CDK-style tools define infrastructure in general-purpose programming languages.
  • Cloud-native IaC (CloudFormation, Bicep/ARM, Deployment Manager) is tied to one provider.
  • Configuration management (Ansible and similar) configures servers, complementing IaC that provisions them.
  • Kubernetes manifests with GitOps manage workloads inside clusters (Kubernetes explained).

FAQ

Is Terraform only for cloud infrastructure?

No. Terraform providers exist for DNS, CDNs, monitoring tools, identity providers, source control platforms and many SaaS products, so you can manage much of your operational setup as code.

Terraform or OpenTofu?

Both use the same language and largely compatible workflows. Choose based on licensing requirements, the features and ecosystem support you need, and your organization's policies. The practices in this guide apply to either.

How do I handle existing infrastructure created manually?

Import resources into Terraform state (via import commands or import blocks), write matching configuration, and run plan until it shows no changes. Do it incrementally, starting with the most critical resources.

Should application teams write Terraform?

Often yes, using well-designed shared modules maintained by a platform team. This balances autonomy with consistency and security.