Terraform Infrastructure as Code: Beginner's Guide
Consider a familiar situation: standing up a test environment for a project usually means clicking around the console for a long while — creating a VPC, security groups, a few instances, attaching a load balancer, then manually filling in config files on each machine. The first time takes half an hour; the second and third times take half an hour too, and the two results are rarely identical. One port off, one rule missing, and debugging becomes a slog. Terraform exists to solve exactly this problem: describe infrastructure as declarative code, then run a single command to create it repeatedly and consistently.
Terraform is HashiCorp's infrastructure as code (IaC) tool. It uses one unified HCL syntax to manage resources on almost every major cloud platform — AWS, Azure, Google Cloud, Alibaba Cloud, and Tencent Cloud included.
1. Installation and Initialization
On macOS the easiest path is Homebrew:
# macOS
brew install terraform
terraform version
Once installed, a project usually moves through the same command loop:
terraform init # Initialize project, download provider plugins
terraform plan # Preview changes without applying
terraform apply # Apply changes, create/modify resources
terraform destroy # Destroy resources
The distinction between plan and apply is the habit most worth building. plan lists every add, change, and removal that would happen, so you can scan it with your own eyes before deciding to actually touch anything. In production this step is what prevents a lot of accidental damage.
2. Your First Configuration
Create a main.tf in the project directory declaring an AWS provider and a web server:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = { Name = "WebServer" }
}
resource "aws_security_group" "web_sg" {
name = "web-sg"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
This configuration declares two things: one t2.micro instance and a security group that only opens ports 80 and 443. Note the declarative nature of Terraform — you describe what you want, not the step-by-step procedure to build it. The relationship between the instance and the security group is visible in the code itself, which is far clearer than clicking through the console in sequence.
3. Core Concepts
| Concept | Description |
|---|---|
| Provider | Cloud platform plugin (AWS, Azure, GCP, etc.) that talks to that platform's API |
| Resource | A managed resource (instance, network, bucket, etc.) |
| Data Source | Read-only lookup of existing resources; creates nothing |
| State | Infrastructure state file (terraform.tfstate) |
| Module | A reusable configuration block that packages a group of resources |
The difference between a Resource and a Data Source is worth internalizing: a Resource creates or modifies real infrastructure; a Data Source just looks up what already exists. To read the ID of an existing VPC, a Data Source is enough — no need to recreate it.
4. State Management: The Single-Machine Trap
After terraform apply, Terraform writes the actual state of your resources into terraform.tfstate. That file is the baseline for every future plan and apply — without it, Terraform does not know which resources it manages. Local state is fine for a solo project, but as soon as two people work on the same config, they start overwriting each other's state, causing duplicate resources at best and accidental deletions at worst.
That is why team collaboration defaults to a remote backend. On AWS the most common setup is S3 with DynamoDB for locking:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}
Remote state plus a lock guarantees only one apply runs at a time, and the storage is safer and cheaper than a local file. Turn on versioning for that S3 bucket as well, so the state itself can be rolled back.
5. Variables and Outputs
Extract what changes often into variables and expose what is useful as outputs:
# variables.tf
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
# outputs.tf
output "instance_ip" {
value = aws_instance.web.public_ip
}
You can override variables on the command line with -var or collect them in a terraform.tfvars file. The output block prints things like the instance's public IP so later scripts can pick them up.
6. Formatting and Validation
Two habits save a lot of time when writing Terraform configs. First, terraform fmt formats the code into the official style and ends the "should this be indented" arguments. Second, terraform validate performs a static check before apply, catching variable type errors and block syntax mistakes early. Put both into CI so every merge request is checked automatically — much faster than discovering a typo halfway through a deployment.
Variable files have conventions too: put environment-specific values in terraform.tfvars (like test.tfvars, prod.tfvars) and select them with -var-file, so you never pass parameters by hand and different environments can reuse the same code.
A Practical Example
A small team needs test and production environments for a blog system. Building them by hand in the console takes about 40 minutes each time and tends to miss configuration. Writing a module that creates the VPC, security group, two instances, and an RDS database, then running terraform apply -var-file=test.tfvars yields an environment identical to the last one in under five minutes. Tearing it down is equally clean — terraform destroy removes everything in one go, leaving no orphaned instances that keep generating bills. Going further, defining the whole environment as a module means test, staging, and production are just three variable files and three commands apart — which is how many teams take their first step from clicking in the console to writing code.
Common Questions
applyfails halfway through? Runterraform planfirst to see what the state file records, then decide whether to retryapplyor fix things manually. Do not bypass Terraform and edit resources in the console — the state will drift.- Accidentally modified someone else's resource? This is exactly why the state file exists. Do not delete
tfstateoutright; useterraform state listandterraform state rmto manage it precisely. - Should
tfstatebe committed to Git? No. State files often contain sensitive information and are tied to a machine. Put them in a remote backend protected by a lock.
Reference: Terraform documentation https://developer.hashicorp.com/terraform/docs ; AWS provider docs https://registry.terraform.io/providers/hashicorp/aws/latest