Terraform Tutorial: Providers, Resources and State Explained
Clicking through the AWS console to create infrastructure works fine for one engineer experimenting. It falls apart the moment you need to recreate that same environment reliably, review changes before they happen, or have more than one person touching the same infrastructure. Terraform solves this by letting you define infrastructure as code — this tutorial gets you from zero to a real, working example.
The core idea: declarative infrastructure
Terraform is declarative — you describe the end state you want, not the steps to get there. You write "I want one EC2 instance of this type, in this subnet, with this security group," and Terraform figures out what API calls are needed to make that true, whether that means creating it, updating it, or leaving it alone because it already matches.
The Terraform lifecycle
Four commands cover almost everything you'll do day to day:
terraform init # Downloads providers, sets up the working directory
terraform validate # Checks your configuration syntax is valid
terraform plan # Shows exactly what would change, without changing anything
terraform apply # Actually makes the changes
terraform plan is the one that matters most for building trust in the tool — it always shows you a preview before anything real happens, which is very different from clicking "Launch Instance" in a console and hoping you got every field right.
Your first configuration
A minimal example that creates an AWS EC2 instance:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-south-1"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "terraform-example"
}
}
Three concepts to understand here:
- Provider — the plugin that lets Terraform talk to a specific platform (AWS, Azure, GCP, Kubernetes, and hundreds more). Every configuration needs at least one.
- Resource — a single infrastructure object you want Terraform to manage. The
aws_instance.webblock above is one EC2 instance; a real project usually has dozens of resource blocks. - State — Terraform's record of what it created and what its current configuration is. This is what makes
planable to show you a diff instead of guessing.
Variables and outputs
Hardcoding values like ap-south-1 directly in a resource works for a toy example, but real projects use variables:
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = var.instance_type
}
output "instance_public_ip" {
value = aws_instance.web.public_ip
}
output values let you surface information after apply runs — here, the instance's public IP, without having to go look it up in the console.
Why remote state matters
By default, Terraform writes its state to a local file (terraform.tfstate). That works alone, but breaks down the moment a second person runs Terraform against the same infrastructure — two people with two different local state files will conflict or silently overwrite each other's changes.
Remote state (commonly an S3 bucket with DynamoDB for state locking, on AWS) fixes this: everyone on the team reads and writes the same state file, and locking prevents two apply runs from colliding. This is one of the first things a real team sets up, well before the project grows large enough to need modules.
Common mistakes
Manually editing resources that Terraform manages. If you create an EC2 instance via Terraform, then go "fix" a setting by hand in the AWS console, the next terraform apply will detect that drift and may revert your manual change — or worse, cause an unexpected replacement of the resource.
Committing state files (or secrets inside them) to Git. Terraform state can contain sensitive values in plain text (database passwords, for example). Never commit .tfstate files — use remote state and add *.tfstate to .gitignore.
Running apply without reading plan first. The whole point of plan is to catch a mistakenly-destructive change (like a resource that requires replacement instead of an in-place update) before it happens. Skipping straight to apply on autopilot defeats that safety net.
Where this fits in the bigger picture
Terraform is step 7 in our DevOps roadmap — right after Kubernetes, once you're managing enough infrastructure that clicking through a console (or even scripting the CLI) stops scaling. It's also usually where "DevOps engineer" starts overlapping with what some teams call "platform engineer."
Want to learn this hands-on, live?
Join Waitlist