Skip to main content
Back to module
DevOps & Cloud Delivery · Infrastructure as Code

Terraform basics

Reading46mLesson 1 of 3 in this module

Write a small Terraform configuration, read its plan, apply it and destroy it, using the local Docker provider so nothing costs money.

Why it matters

Clicking through a cloud console works once. It does not work when you need the same network, database and load balancer in staging and production, when a colleague asks "who changed this firewall rule?", or when a region fails and you must rebuild everything. Infrastructure as code (IaC) describes infrastructure in text files that live in Git, get reviewed in pull requests and are applied by a tool. Your infrastructure gains the same history, review and repeatability as your application code.

Terraform is the most widely used IaC tool for cloud engineers, with providers for AWS, Azure, Google Cloud, Kubernetes, GitHub, DNS services and hundreds more. Since 2023 Terraform has been released under the Business Source License; OpenTofu is a community fork under an open-source license that uses the same language and commands (tofu instead of terraform). Everything in this module works with either.

The core idea: declare, plan, apply

Like Kubernetes, Terraform is declarative. You write what should exist; Terraform works out how to get there:

  1. terraform init downloads the providers (plugins that talk to an API) your code needs.
  2. terraform plan compares your code with what exists and prints the changes it would make: + create, ~ update in place, -/+ replace, - destroy. Nothing changes yet.
  3. terraform apply shows the plan again and makes the changes after you type yes.
  4. terraform destroy removes everything the configuration manages.

Reading the plan carefully is the most important habit in IaC. A plan that says "1 to destroy" on a production database deserves a second look before anyone types yes.

A first configuration with no cloud bill

To learn safely, this lesson uses the community Docker provider, which manages containers on your own machine. The workflow is identical to managing cloud resources, but it costs nothing and cannot leave a forgotten server running.

# main.tf
terraform {
  required_version = ">= 1.6"
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {}

variable "external_port" {
  type        = number
  default     = 8080
  description = "Port on your machine that forwards to the web server."
}

resource "docker_image" "nginx" {
  name         = "nginx:1.27-alpine"
  keep_locally = false
}

resource "docker_container" "web" {
  name  = "tf-web"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = var.external_port
  }
}

output "url" {
  value = "http://localhost:${var.external_port}"
}

The language is HCL. The pieces:

BlockPurpose
terraformSettings: required Terraform version and providers, pinned with ~> 3.0 (any 3.x)
providerConfigures a provider. Docker needs no settings on macOS or Linux; on Windows set host = "npipe:////.//pipe//docker_engine"
variableAn input, with a type, default and description
resourceSomething Terraform creates and manages. Its address is type.name, for example docker_container.web
outputA value printed after apply and readable by other tools

image = docker_image.nginx.image_id is a reference. It tells Terraform the container depends on the image, so the image is created first. Terraform builds a dependency graph from references and creates independent resources in parallel.

Running it

terraform init
terraform fmt        # format the files consistently
terraform validate   # check syntax and references
terraform plan       # Plan: 2 to add, 0 to change, 0 to destroy.
terraform apply
curl http://localhost:8080    # nginx welcome page
terraform destroy

Change default = 8080 to 8081 and run terraform plan again. The plan shows -/+ for the container: a port mapping cannot be changed on a running container, so Terraform must replace it. The plan tells you this before anything happens, which is exactly why you read it.

You can also override variables without editing files: terraform plan -var="external_port=9090", or a terraform.tfvars file.

What goes in Git

Commit *.tf files and .terraform.lock.hcl (it records exact provider versions so everyone gets the same ones). Do not commit:

.terraform/
*.tfstate
*.tfstate.*
*.tfvars      # if they contain secrets; commit an example file instead
crash.log

The terraform.tfstate file records what Terraform manages and can contain secrets in plain text. The next lesson covers where state should live instead.

Common mistakes

  • Applying without reading the plan. Make "read the plan" part of every review; in CI, post the plan on the pull request.
  • Editing resources by hand in the console. Terraform's view and reality drift apart, and the next apply may undo the manual change. Change the code instead.
  • Unpinned providers. A new major version can change behaviour. Pin with ~> and update on purpose.
  • Hard-coding credentials in provider blocks. Cloud providers read credentials from the environment or your CLI login; never put keys in .tf files.
  • Forgetting destroy on practice resources. On a real cloud, that is how free tiers turn into bills.

Quick recap

  • IaC puts infrastructure in Git, with review and history.
  • init, plan, apply, destroy; always read the plan.
  • Resources, variables, outputs and references form a dependency graph.
  • Commit .tf and the lock file; never commit state or secrets.

Try it

The Do tab has you run this configuration end to end with Docker on your own machine.

Also in this lesson: