Terraform Basics
Terraform is a tool that lets you describe your infrastructure (servers, networks, DNS records, databases) in plain text files, and then creates or changes that infrastructure for you. This is called Infrastructure as Code, or IaC (managing servers with files you can edit and version-control, instead of clicking around in a web console). The big win is repeatability: the same files produce the same setup every time, and you can review every change before it happens. In this page you will write a tiny Terraform config and run the full workflow — write, init, plan, apply, and destroy.
What you need first
Terraform runs from your own machine or a server. On Ubuntu 22.04/24.04 LTS, install the official package from HashiCorp’s apt repository (apt is Ubuntu’s package manager — the tool that installs software).
# Add HashiCorp's GPG signing key (proves the package is genuine)
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
# Add the HashiCorp apt repository for your Ubuntu release
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install -y terraform
terraform version
Output:
Terraform v1.9.5
on linux_amd64
Write a minimal config
Terraform files use a language called HCL (HashiCorp Configuration Language — a simple, readable format for describing resources) and end in .tf. A config has two main parts: a provider (a plugin that knows how to talk to one platform, like AWS or DigitalOcean) and one or more resources (the actual things you want to exist, like a server or a DNS record).
Create a folder and a file. We will use DigitalOcean here because a single small “Droplet” (DigitalOcean’s name for a virtual machine) is easy to read, but the workflow is identical for AWS, Google Cloud, or any other provider.
mkdir ~/tf-demo && cd ~/tf-demo
Create main.tf:
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
# The provider block configures how Terraform authenticates.
# The token is read from the DIGITALOCEAN_TOKEN environment variable.
provider "digitalocean" {}
# A resource block describes one thing you want to exist.
# "digitalocean_droplet" is the type; "web" is your local name for it.
resource "digitalocean_droplet" "web" {
name = "web-01"
region = "nyc3"
size = "s-1vcpu-1gb"
image = "ubuntu-24-04-x64"
}
# Outputs print useful values after apply.
output "public_ip" {
value = digitalocean_droplet.web.ipv4_address
}
Set your API token (a secret string that proves you are allowed to create resources) as an environment variable so it never gets written into a file:
export DIGITALOCEAN_TOKEN="dop_v1_your_real_token_here"
Never hard-code secrets or API tokens directly in
.tffiles. They get committed to Git and leaked. Use environment variables, or a secrets manager, and always add*.tfvarsand.terraform/to your.gitignore.
The core workflow
Terraform has four commands you will use constantly. Run them in order from inside your config folder.
Step 1 — terraform init
init downloads the provider plugins your config needs and prepares the working directory. Run it once per project, and again any time you change provider versions.
terraform init
Output:
Initializing provider plugins...
- Installing digitalocean/digitalocean v2.43.0...
- Installed digitalocean/digitalocean v2.43.0
Terraform has been successfully initialized!
Step 2 — terraform plan
plan is a dry run. It compares your config to the real world and shows exactly what it would create, change, or destroy — without touching anything. Always read the plan before applying.
terraform plan
Output:
Terraform will perform the following actions:
# digitalocean_droplet.web will be created
+ resource "digitalocean_droplet" "web" {
+ image = "ubuntu-24-04-x64"
+ name = "web-01"
+ region = "nyc3"
+ size = "s-1vcpu-1gb"
+ ipv4_address = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Reading a plan
The symbols at the start of each line tell you what will happen. Learning to read them is the most important Terraform skill.
| Symbol | Meaning | When you see it |
|---|---|---|
+ | Will be created | New resource |
- | Will be destroyed | Resource removed from config |
~ | Will be changed in place | An attribute was edited |
-/+ | Will be destroyed then recreated | A change that can’t be done in place |
(known after apply) | Value not known yet | Generated by the provider, e.g. an IP |
The summary line Plan: 1 to add, 0 to change, 0 to destroy is your safety check. If you only meant to add one server and it says 2 to destroy, stop and look before continuing.
Step 3 — terraform apply
apply makes the plan real. It shows the plan again and waits for you to type yes.
terraform apply
Output:
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Enter a value: yes
digitalocean_droplet.web: Creating...
digitalocean_droplet.web: Creation complete after 38s
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
public_ip = "203.0.113.42"
Step 4 — terraform destroy
destroy removes everything in your config. Use it to tear down test environments so you stop paying for them.
terraform destroy
Output:
Plan: 0 to add, 0 to change, 1 to destroy.
Do you want to destroy all resources? Enter a value: yes
digitalocean_droplet.web: Destroying...
Destroy complete! Resources: 1 destroyed.
What is state?
After your first apply, Terraform creates a file called terraform.tfstate. This is the state file — Terraform’s record of what it has built and which real-world resource maps to each block in your config. On the next plan, Terraform compares your .tf files against this state to work out the difference.
A few things to know about state:
- It can contain secrets (like generated passwords), so treat it like a password. Never commit it to Git for shared projects.
- For a team, store state remotely (for example in an S3 bucket with locking) so everyone shares one source of truth and two people can’t apply at once.
- Never edit
terraform.tfstateby hand. Useterraform statesubcommands if you need to move or remove entries.
Changing infrastructure
To change something, edit the .tf file and re-run the workflow. For example, change size = "s-1vcpu-1gb" to s-2vcpu-2gb, then run terraform plan. Terraform shows a ~ change, and apply carries it out. You describe the desired end state; Terraform figures out the steps to get there. This is called the declarative model (you say what you want, not how to do it).
Best Practices
- Always run
terraform planand read it beforeterraform apply— never apply blind. - Keep secrets out of
.tffiles; use environment variables or a secrets manager. - Add
.terraform/,*.tfstate,*.tfstate.backup, and*.tfvarsto.gitignore. - Store state remotely with locking for any project more than one person touches.
- Commit your
.tffiles to Git so every infrastructure change is reviewed like normal code. - Use
terraform fmtto auto-format andterraform validateto catch errors before planning. - Tear down throwaway test environments with
terraform destroyto avoid surprise bills.