Skip to content
DevOps devops iac 6 min read

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 .tf files. They get committed to Git and leaked. Use environment variables, or a secrets manager, and always add *.tfvars and .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.

SymbolMeaningWhen you see it
+Will be createdNew resource
-Will be destroyedResource removed from config
~Will be changed in placeAn attribute was edited
-/+Will be destroyed then recreatedA change that can’t be done in place
(known after apply)Value not known yetGenerated 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.tfstate by hand. Use terraform state subcommands 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 plan and read it before terraform apply — never apply blind.
  • Keep secrets out of .tf files; use environment variables or a secrets manager.
  • Add .terraform/, *.tfstate, *.tfstate.backup, and *.tfvars to .gitignore.
  • Store state remotely with locking for any project more than one person touches.
  • Commit your .tf files to Git so every infrastructure change is reviewed like normal code.
  • Use terraform fmt to auto-format and terraform validate to catch errors before planning.
  • Tear down throwaway test environments with terraform destroy to avoid surprise bills.
Last updated June 15, 2026
Was this helpful?