Terraform lets you define your servers, networks, databases, and cloud resources in code, then create and update them automatically—so your infrastructure becomes versioned, reviewable, and reproducible instead of a pile of manual console clicks nobody can remember. This guide to Terraform infrastructure as code covers how its declarative model works, the core write-plan-apply workflow, the all-important state file, how to organize real projects with modules, and the mistakes that catch people out. By the end you'll understand not just the commands but why Terraform works the way it does.
What infrastructure as code and Terraform are
Infrastructure as code (IaC) is the practice of managing infrastructure—servers, load balancers, DNS records, databases—through machine-readable configuration files rather than manual setup. Instead of clicking through a cloud console, you write a definition of what you want, commit it to version control, and let a tool create it. This makes infrastructure reproducible, reviewable in pull requests, and consistent across environments.
Terraform, created by HashiCorp, is the most widely used IaC tool. Its defining trait is that it's declarative: you describe the desired end state of your infrastructure, and Terraform figures out the actions needed to reach it. You don't write "create this server, then attach this disk, then open this port" step by step (that would be imperative); you declare "I want a server with this disk and these ports open," and Terraform computes the difference between what exists and what you've declared, then makes reality match.
Terraform is also cloud-agnostic. Through providers—plugins that teach it how to talk to a specific platform—the same tool and workflow manage AWS, Google Cloud, Azure, Cloudflare, GitHub, and hundreds of other services. You write configuration in HCL (HashiCorp Configuration Language), a readable format designed for describing infrastructure.
One note worth knowing: in 2023 HashiCorp changed Terraform's license to the Business Source License, which prompted the community to create OpenTofu, an open-source fork that remains a drop-in alternative. For learning and most use, the two are interchangeable, and everything here applies to both.
How the Terraform workflow works
Terraform's day-to-day workflow is a short, repeatable cycle. A minimal configuration to create a cloud server looks like this in HCL:
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}
You then run four core commands:
terraform initdownloads the providers your configuration needs (here, the AWS provider) and prepares the working directory. You run this once per project and again when you add providers or modules.terraform planshows you exactly what Terraform will do—what it will create, change, or destroy—without touching anything. This is Terraform's best feature: a dry run you review before acting.terraform applyexecutes the plan, creating or modifying real resources. It shows the plan again and waits for your confirmation.terraform destroytears down everything the configuration manages—invaluable for spinning up and cleanly removing temporary or test environments.
The habit that makes Terraform safe is always reading the plan before applying. The plan tells you if a seemingly innocent change—say, renaming a resource—would actually destroy and recreate a database rather than modify it in place. Skipping that review is how people accidentally delete production resources.
State: the concept that trips everyone up
The single most important—and most misunderstood—part of Terraform is its state file. Terraform records what it has created in a file (by default terraform.tfstate) that maps your configuration to the real resources it manages. This state is how Terraform knows that the aws_instance.web in your code corresponds to a specific running server, so it can plan changes correctly.
State causes real problems if mishandled, and two rules prevent most pain:
- Never commit state to Git. The state file often contains secrets (database passwords, keys) in plain text, and committing it risks leaking them. Add
terraform.tfstateto your.gitignore. - Use remote state with locking for any team or production use. By default, state lives on your local machine—fine for solo experiments, disastrous for teams. If two people run
applyagainst the same infrastructure at once with local state, they corrupt it. Store state in a shared remote backend (like an S3 bucket with DynamoDB locking, or Terraform Cloud) so it's centralized and locked while anyone is applying, preventing concurrent changes from clobbering each other.
Understanding that Terraform's view of the world lives in state—not by inspecting your cloud live each time—explains most confusing behavior. If someone changes a resource manually in the console, Terraform's state drifts from reality, and the next plan will try to "fix" the drift back to what your code says.
Organizing real projects with modules
A single file is fine for a demo, but real infrastructure needs structure, and Terraform's answer is modules—reusable, parameterized packages of configuration. A module bundles a set of related resources (say, everything needed for a standard web service: server, security group, DNS record) behind a clean interface of inputs and outputs, so you can reuse it across projects and environments instead of copying and pasting.
Modules deliver the same benefit good functions do in programming: define once, reuse everywhere, change in one place. You might have a network module and a database module, then compose them in each environment. The public Terraform Registry offers thousands of community and official modules for common setups, so you often don't start from scratch.
Two structural practices matter for real projects. Separate your environments—keep development, staging, and production in separate state so a change to one can't accidentally affect another. And use variables for anything that differs between environments (instance sizes, counts, region) rather than hardcoding, so the same modules serve every environment with different inputs.
Terraform provisions the infrastructure your applications run on, which is why it pairs naturally with container tooling. Once Terraform has created your servers and networks, you deploy applications onto them using tools covered in Docker for beginners—often defining multi-service stacks with the approach in Docker Compose—and orchestrate them at scale with the concepts in Kubernetes explained simply; in fact, Terraform is commonly used to provision the very Kubernetes clusters those workloads run on. The workflow mirrors how you'd build things on your own machine with local development in containers, just applied to real cloud infrastructure. Terraform is a favorite in any modern, reproducible stack, and pairs well with a broader kit of self-hosted open source tools.
Common mistakes to avoid
- Applying without reading the plan. The plan warns you before Terraform destroys or recreates something. Skipping it is the top cause of accidental deletions. Always review it.
- Committing the state file. It can contain secrets and doesn't belong in Git. Add it to
.gitignoreand use a remote backend. - Using local state for team work. Concurrent applies corrupt local state. Use a locked remote backend for anything beyond solo experiments.
- Making manual changes alongside Terraform. Editing managed resources by hand in the console causes drift that Terraform will try to undo. Manage a resource with Terraform or by hand, not both.
- Hardcoding values that differ across environments. Use variables so the same configuration serves dev, staging, and production cleanly.
- Not pinning provider and Terraform versions. Unpinned versions cause "works on my machine" surprises. Pin versions so everyone runs the same setup.
- Putting everything in one giant state. A single massive state is slow and risky—one mistake affects everything. Split infrastructure into smaller, independent states.
Frequently asked questions
What is Terraform used for? Terraform is used to define and manage infrastructure—cloud servers, networks, databases, DNS, and more—as code, so it can be created, updated, and destroyed automatically and reproducibly. Instead of clicking through a cloud console manually, you write a declarative configuration, review the planned changes, and apply them. It works across many providers (AWS, Google Cloud, Azure, and hundreds more) with the same workflow.
Is Terraform declarative or imperative? Declarative. You describe the desired end state of your infrastructure, and Terraform determines the specific actions needed to reach it by comparing your configuration to what currently exists. You don't write step-by-step instructions to create resources in order; you declare what should exist, and Terraform computes and executes the difference. This is what makes it safe to run repeatedly toward the same result.
What is a Terraform state file and why does it matter? The state file records what Terraform has created, mapping your configuration to real resources so it can plan changes correctly. It matters because it's how Terraform tracks reality—but it often contains secrets, so it must never be committed to Git, and it must be stored in a locked remote backend for team use to prevent concurrent changes from corrupting it. Mishandled state is the most common source of Terraform trouble.
What's the difference between Terraform and OpenTofu? OpenTofu is an open-source fork of Terraform created in 2023 after HashiCorp changed Terraform's license to the Business Source License. It's a drop-in alternative using the same configuration language and workflow, maintained by the community under a fully open-source license. For learning and most projects the two are interchangeable, so you can follow Terraform tutorials while using either tool.
Do I still need Terraform if I use Docker or Kubernetes? Yes—they solve different layers. Docker and Kubernetes package and run your applications, while Terraform provisions the underlying infrastructure those applications need: the servers, networks, and even the Kubernetes clusters themselves. Terraform sets up where things run; containers define what runs there. They're complementary, and a common modern stack uses Terraform to create the environment and containers to deploy onto it.
The takeaway
Terraform infrastructure as code turns your servers and cloud resources into versioned, reviewable configuration, letting you create and change infrastructure reproducibly through a simple write-plan-apply cycle rather than error-prone manual clicks. The keys to using it well are understanding its declarative model, always reading the plan before applying, treating the state file with care (never in Git, always locked and remote for teams), and organizing with modules and per-environment separation. Your next step is to write a minimal configuration for a single real resource—a small server or a DNS record—and run init, plan, and apply, because watching Terraform create exactly what you declared is the moment infrastructure as code clicks into place.