Getting Started with Terraform

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

This page gets Terraform onto a workstation and walks through the core workflow once, end to end, on a laptop. It targets Terraform 1.17 (released 2026-10-21; the previous line is 1.16.x) and the kreuzwerker/docker provider 4.6, and it needs only Docker — no cloud account. For what Terraform is and why it exists, read Introduction and Infrastructure as Code first; for the full reading path see the section landing page. The example is the local slice of the Bookshelf scenario: a network, a web container and a generated database password. The container side of the story (images, ports, networks) is covered in Getting Started with Docker and Docker Networking.

Installing Terraform

Terraform is a single static binary named terraform. HashiCorp documents these installation routes:

Method Notes

Homebrew (macOS, Linux)

Uses HashiCorp’s own tap, hashicorp/tap, so the formula tracks HashiCorp releases.

Package repositories (Linux)

apt for Ubuntu and Debian, dnf for Fedora and Amazon Linux 2023, yum for RHEL, CentOS and Amazon Linux 2. Signed by HashiCorp’s GPG key.

Chocolatey (Windows)

choco install terraform. The package is not maintained by HashiCorp, so the manual download may be newer.

Manual download

A zip archive containing the single terraform executable, for every supported operating system and architecture. Unzip it and put it on the PATH.

From source

Clone the repository and run go install. Useful for testing unreleased changes; not a way to get a supported build.

Docker image

hashicorp/terraform on Docker Hub, one tag per release. Good for CI and for trying a version without installing it.

Distribution packages (for example a distro’s own terraform package) exist but are outside HashiCorp’s support; prefer one of the routes above or a version manager (next section).

Homebrew

brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform version

To update later, run brew update followed by brew upgrade hashicorp/tap/terraform.

Ubuntu and Debian (apt)

wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

The first command imports HashiCorp’s signing key; the second registers the repository for the distribution’s codename. After that, apt upgrade delivers new Terraform releases like any other package — fine on a workstation, but CI should pin an exact version (see the version managers below).

Fedora, RHEL and Amazon Linux (dnf and yum)

# Fedora
wget -O- https://rpm.releases.hashicorp.com/fedora/hashicorp.repo | sudo tee /etc/yum.repos.d/hashicorp.repo
sudo dnf -y install terraform

# RHEL
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform

# Amazon Linux
sudo yum install -y yum-utils shadow-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum install terraform

Windows

The official install page offers the signed zip archives (386, AMD64 and ARM64); the install tutorial additionally mentions Chocolatey. Either way the result must be a terraform.exe on the PATH:

choco install terraform
terraform version

For the manual route, download terraform_<version>_windows_amd64.zip from releases.hashicorp.com, extract terraform.exe into a folder such as C:\tools\terraform and add that folder to the user Path environment variable. Open a new terminal afterwards: an already-open shell does not see the changed Path.

The Docker image

HashiCorp publishes a verified image whose entrypoint is the terraform executable, so subcommands are passed as the container command. Pin a release tag in anything but a quick experiment:

docker run --rm -it hashicorp/terraform:1.17.0 version

# run against the configuration in the current directory
docker run --rm -it \
  -v "$PWD":/workspace -w /workspace \
  hashicorp/terraform:1.17.0 plan

The container only sees what is mounted and passed in: configuration files via the bind mount, provider credentials via -e flags or mounted files, and, for the Docker provider used below, the Docker socket. That makes the image a good fit for pipelines (see CI/CD with GitHub Actions) and an awkward one for the local Docker tutorial, so the rest of this page assumes a native install.

Verifying the installation

terraform version
terraform -help
Terraform v1.17.0
on darwin_arm64

terraform -help begins with Usage: terraform [global options] <subcommand> [args]; adding -help to any subcommand prints its options. The output of terraform version varies with the platform and installed providers (it lists providers once a directory has been initialized).

Managing several Terraform versions

A repository’s required_version (see HCL Syntax and Configuration Files) pins the CLI versions a configuration accepts, and different projects, or the same project across a minor upgrade, need different binaries. A version manager installs several versions side by side and picks one per directory. Three tools are common; all of them read a .terraform-version file, which holds just a version string such as 1.17.0, so the file is the portable way to pin a project.

Tool Characteristics

tenv

Written in Go; manages Terraform, OpenTofu, Terragrunt, Terramate and Atmos through proxy binaries. Reads .terraform-version (and .tfswitchrc) for Terraform and .opentofu-version for OpenTofu, searching the working directory, then its parents, then the home directory. A tf proxy picks terraform or tofu based on the version files only. Auto-install is off by default and enabled with TENV_AUTO_INSTALL=true.

tfenv

A shell-script manager modelled on rbenv. Reads .terraform-version, accepts latest, latest:<regex>, latest-allowed and min-required, and verifies downloads against HashiCorp’s SHA256 sums when shasum is available. Terraform only.

tfswitch

A Go binary that offers an interactive version list or switches to a version given as an argument, and also looks at the required_version constraint of the configuration and the TF_VERSION environment variable.

tenv

brew install tenv                      # Windows: choco install tenv, scoop install tenv or winget install Tofuutils.Tenv

tenv tf list-remote -s                 # stable versions that can be installed
tenv tf install 1.17.0
tenv tf use 1.17.0 -w                  # -w writes .terraform-version in the working directory
tenv tf detect                         # which version would run here
terraform version                      # the proxy binary now resolves to 1.17.0

Source: tenv on GitHub

Because tenv also manages OpenTofu (tenv tofu ...), it is the natural choice when a team runs both tools; OpenTofu explains how they differ.

tfenv

brew install tfenv

tfenv list-remote
tfenv install 1.17.0
tfenv use 1.17.0
tfenv list                             # installed versions, the active one marked with *
tfenv pin                              # write the active version to .terraform-version

Source: tfenv on GitHub

With no argument, tfenv install takes the version from TFENV_TERRAFORM_VERSION, then from .terraform-version, and otherwise uses latest. min-required and latest-allowed scan required_version in the .tf files, but only look at the first constraint, so a compound constraint can resolve to a surprising version. Treat them as a convenience and prefer an explicit file.

tfswitch

brew install tfswitch                  # or: curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/master/install.sh | bash

tfswitch                               # interactive list of versions
tfswitch 1.17.0                        # install and switch to a specific version

tfswitch’s documentation describes the full set of version sources and flags on tfswitch.warrensbox.com; the exact precedence between its sources was not checked for this page, so confirm it there before relying on it in automation.

Choosing a strategy

  • Developers on one project: a .terraform-version file committed to the repository, plus tenv or tfenv.

  • CI: install the exact version named in that file (or use the pinned Docker tag) rather than latest, so a pipeline does not change behaviour on a day nobody touched the code.

  • Both Terraform and OpenTofu: tenv, because one tool handles .terraform-version and .opentofu-version.

CI/CD and Automation shows the pinned-version pattern in a pipeline.

Editor support

Terraform files are plain text, but an editor that understands HCL catches mistakes before terraform validate does.

Visual Studio Code

The HashiCorp Terraform extension (ID HashiCorp.terraform) provides syntax highlighting for Terraform 1.x, validation as you type, and formatting through terraform fmt. It bundles the Terraform language server (terraform-ls) and keeps it updated, so no separate install is needed. Formatting requires the terraform binary on the PATH. The extension’s recommended format-on-save setting, repeated for each Terraform language ID (terraform, terraform-vars, terraform-stack, terraform-test and the others it lists):

{
  "[terraform]": {
    "editor.defaultFormatter": "hashicorp.terraform",
    "editor.formatOnSave": true,
    "editor.formatOnSaveMode": "file"
  }
}

Enhanced validation (deprecations, missing required attributes) is on by default and controlled by terraform.validation.enableEnhancedValidation; the extension’s terraform validate integration needs a prior terraform init. Use terraform.languageServer.terraform.path to point the server at a specific binary, for example one managed by tenv.

terraform-ls and other editors

terraform-ls is the official language server (LSP) behind the extension. Editors without a bundled integration use it directly:

brew install hashicorp/tap/terraform-ls
terraform-ls -v

On Linux, terraform-ls is also published in HashiCorp’s apt and RPM repositories under that package name. The terraform-ls usage guide lists client setup for Vim and Neovim (coc.nvim, vim-lsp, nvim-lspconfig and others), Emacs (lsp-mode) and Sublime Text (LSP-terraform).

JetBrains IDEs

JetBrains publishes its own plugin, listed on the Marketplace as Terraform and HCL; its settings page is Tools | Terraform and OpenTofu, where the executable path can be set (the IDE usually detects it) and where run configurations for Terraform are available. The terraform-ls usage guide describes an alternative LSP-based setup for JetBrains IDEs and warns that combining it with the JetBrains plugin can produce duplicate completions, so use one of the two.

Shell completion

Terraform can install tab completion for Bash and Zsh:

touch ~/.zshrc            # the shell configuration file must already exist (~/.bashrc for Bash)
terraform -install-autocomplete
exec $SHELL               # restart the shell

The command appends a completion line to the shell’s configuration file; run it once. After that, terraform pl<TAB> completes to plan, and subcommands list their flags. Completion lives in the CLI binary, so it follows the version in use; when a version manager switches binaries, the completion keeps working because it invokes whichever terraform is first on the PATH.

The core workflow on Bookshelf

Terraform’s day-to-day loop is write, initialize, plan, apply; this section runs it once against Docker, following HashiCorp’s Docker get-started tutorial but growing it into the local slice of Bookshelf. You need a running Docker daemon (Docker Desktop, Colima or an engine on Linux). The tutorial’s sequence is: build infrastructure, change infrastructure, destroy infrastructure, define input variables and query data with outputs; the sections below follow it in order.

Write the configuration

Create an empty directory and a file main.tf. The first version creates only the nginx image and container for the web service, exactly the shape the tutorial uses:

terraform {
  required_version = ">= 1.17.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 4.6.0"
    }
  }
}

provider "docker" {}

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

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

  ports {
    internal = 80
    external = 8000
  }
}

What each block does:

  • The terraform block declares the CLI versions the configuration accepts (required_version) and where each provider comes from (required_providers). kreuzwerker/docker is the provider’s source address on the Terraform registry; ~> 4.6.0 allows patch releases of 4.6 only. The tutorial itself uses ~> 4.2.0; the newest release at the time of writing is 4.6.0 and the provider’s README recommends pinning because new versions are released frequently.

  • provider "docker" {} configures the provider with defaults, which means it finds the local Docker daemon through the usual environment (for example DOCKER_HOST). The tutorial’s Windows variant sets host = "npipe:////.//pipe//docker_engine" instead.

  • A resource block has a type (docker_image) and a local name (nginx); together they form the address docker_image.nginx. The reference docker_image.nginx.image_id in the container is an implicit dependency: Terraform builds a graph from such references, so it pulls the image before creating the container and destroys them in the opposite order.

  • The image tag is pinned to nginx:1.27 here, where the tutorial uses nginx:latest: a pinned tag keeps the plan reproducible, while latest can change under an unchanged configuration. Replace the tag with whichever release you have verified.

The HCL language itself is the subject of HCL Syntax and Configuration Files; providers are covered in depth in Providers.

terraform init

terraform init
Initializing the backend...

Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 4.6.0"...
- Installing kreuzwerker/docker v4.6.0...
- Installed kreuzwerker/docker v4.6.0 (self-signed, key ID BD080C4571C6104C)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

The output is the tutorial’s, abbreviated; the version numbers follow the pin in this page’s configuration. init is the first command to run in any directory and it is safe to repeat: it never deletes configuration or state. It performs three steps: initialize the backend (where state is stored; the default is a local file), install child modules, and install provider plugins. It must be re-run whenever the backend configuration, a module source or the provider requirements change; other commands detect a stale directory and say so.

Useful options:

  • -upgrade — ignore the lock file and select the newest provider versions that satisfy the constraints; for modules, update to the newest matching source.

  • -backend=false — skip backend initialization, as used by terraform validate in CI (below).

  • -reconfigure — use new backend settings and ignore the previous ones without migrating state.

  • -migrate-state — copy existing state to a newly configured backend.

  • -input=false — fail instead of prompting, for automation.

Source: terraform init

What init creates

After init the directory contains two new items:

.
├── .terraform/                 # working data: downloaded providers (and modules), backend settings
├── .terraform.lock.hcl         # the dependency lock file
└── main.tf
  • .terraform/ is a local cache that Terraform writes to the current working directory (the environment variable TF_DATA_DIR relocates it). It holds the downloaded provider binaries, installed modules and the recorded backend configuration. It is machine-specific and fully reproducible with terraform init, so it is never committed.

  • .terraform.lock.hcl records the exact provider version selected, the constraint it satisfied and the checksums (hashes) that downloaded packages must match. It lives next to the root module’s .tf files and applies to the whole configuration. HashiCorp’s guidance is to include it in version control, so that teammates and CI receive the same provider builds and changes to dependencies go through code review. It is covered in depth, including providers lock for several platforms, in Providers.

An excerpt, using the shape documented by HashiCorp (hashes shortened):

provider "registry.terraform.io/kreuzwerker/docker" {
  version     = "4.6.0"
  constraints = "~> 4.6.0"
  hashes = [
    "h1:...",
    "zh:...",
  ]
}

terraform fmt and terraform validate

terraform fmt
terraform validate
Success! The configuration is valid.

terraform fmt rewrites the files in the current directory into the canonical style (indentation, aligned = signs, spacing) and lists the files it changed. Useful options are -recursive to include subdirectories, -diff to show what would change, and -check to change nothing and exit non-zero if any file needs formatting, which is how a CI job enforces the style. terraform validate checks syntax and internal consistency — attribute names, value types, references — regardless of variable values or state. It needs an initialized directory (use terraform init -backend=false to initialize without touching a backend) and does not contact remote services or provider APIs; -json produces machine-readable diagnostics for editors and pipelines.

terraform plan

terraform plan
Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # docker_container.web will be created
  + resource "docker_container" "web" {
      + image = (known after apply)
      + name  = "bookshelf-web"
      ...
      + ports {
          + external = 8000
          + internal = 80
          + ip       = "0.0.0.0"
          + protocol = "tcp"
        }
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id           = (known after apply)
      + keep_locally = false
      + name         = "nginx:1.27"
      ...
    }

Plan: 2 to add, 0 to change, 0 to destroy.

plan compares three things — the configuration, the state, and (after refreshing it) the real objects — and prints the actions needed to make reality match the configuration. It changes nothing. The output is abbreviated here (... marks omitted attributes) and the attribute list varies with the provider version. A value shown as (known after apply) is computed by the provider when the object is created and cannot be known in advance.

Reading plan output symbols

The legend printed at the top of every plan lists the actions in use. The symbols are:

Symbol Meaning

+

Create: the object does not exist yet.

-

Destroy: the object will be removed.

~

Update in place: an argument changes without destroying the object.

-/+

Replace, destroy then create: the change cannot be made in place. The attribute that forces it is annotated # forces replacement.

+/-

Replace, create then destroy: the order used when a resource has create_before_destroy set.

<=

Read: a data source is read. When its arguments are not known until apply, the plan shows the read as deferred to apply time.

Source: terraform plan

The plan reference documents +, -, ~ and -/+. The +/- and <= rows come from the legend Terraform prints at the top of a plan and from the data source documentation, which only states that deferred reads are indicated in the plan; check the legend of your own plan output, since the exact wording is the CLI’s to define. Reading a plan is a skill of its own: Dependency Graph and Plan covers planning modes, saved plans and the situations that produce surprising replacements. The rule of thumb is: read every - and every -/+ line before approving, because those are the lines that delete data.

terraform apply

terraform apply
Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

docker_image.nginx: Creating...
docker_image.nginx: Creation complete after 13s [id=sha256:...nginx:1.27]
docker_container.web: Creating...
docker_container.web: Creation complete after 2s [id=...]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Without a plan file, apply creates a plan, shows it and waits for the literal answer yes. Open http://localhost:8000 to see the nginx welcome page, and docker ps shows the container named bookshelf-web. Options worth knowing now:

  • -auto-approve — skip the prompt. Intended for automation, where a reviewed plan has already been approved; it also approves destructive changes, so avoid it at a keyboard.

  • A saved plan file as argument (terraform apply tfplan) applies exactly that plan without prompting; planning options cannot be combined with it.

  • -input=false — never prompt. Because the approval prompt is also an input, apply then fails rather than proceeding unless -auto-approve or a plan file is given.

  • -var / -var-file — set input variables (below).

Source: terraform apply

terraform show

terraform show
terraform show -json | head

terraform show prints the latest state snapshot in human-readable form; given a saved plan file it prints that plan instead. -json produces the machine-readable form, in which sensitive values appear in plain text, so treat that output as sensitive. The state itself — the file terraform.tfstate that apply created next to the configuration — is what Terraform uses to map configuration addresses to real objects; it is explained in State.

Source: terraform show

Changing infrastructure

Edit main.tf and change the published port from 8000 to 8080, then run terraform plan. The tutorial shows what happens:

  # docker_container.web must be replaced
-/+ resource "docker_container" "web" {
      ...
      ~ ports {
          ~ external = 8000 -> 8080 # forces replacement
            # (3 unchanged attributes hidden)
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.

The excerpt is abbreviated: the tutorial documents the -/+ marker, the # forces replacement annotation on the external port and the Plan: 1 to add, 0 to change, 1 to destroy. summary; the surrounding attributes vary with the provider version. The container’s port mapping cannot be altered in place, so the provider declares the argument as forcing replacement: Terraform destroys the old container and creates a new one, and apply ends with Apply complete! Resources: 1 added, 0 changed, 1 destroyed. This is the most important lesson of the first hour: whether a change is an update or a replacement is a property of the provider’s schema, not of Terraform core, and the plan is where you learn it.

Input variables and outputs

Hard-coded values do not scale, so the next step in the tutorial extracts them. Add variables.tf and outputs.tf, with the Bookshelf additions: a generated name suffix and a generated database password.

# variables.tf
variable "container_name" {
  description = "Name of the Docker container"
  type        = string
  default     = "bookshelf-web"
}

variable "web_port" {
  description = "Host port published for the web container"
  type        = number
  default     = 8080
}
# main.tf (additions and changes)
terraform {
  required_version = ">= 1.17.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 4.6.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.9"
    }
  }
}

provider "docker" {}

resource "random_pet" "suffix" {
  length = 2
}

resource "random_password" "postgres" {
  length  = 24
  special = false
}

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

resource "docker_container" "web" {
  name  = "${var.container_name}-${random_pet.suffix.id}"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = var.web_port
  }
}
# outputs.tf
output "container_id" {
  description = "ID of the Docker container"
  value       = docker_container.web.id
}

output "image_id" {
  description = "ID of the Docker image"
  value       = docker_image.nginx.id
}

output "web_url" {
  description = "Where the web container is reachable"
  value       = "http://localhost:${var.web_port}"
}

output "postgres_password" {
  description = "Generated database password (for the next stages)"
  value       = random_password.postgres.result
  sensitive   = true
}

The tutorial’s variable is container_name with a description, type and default, and its outputs are container_id and image_id; the random resources, web_port, web_url and postgres_password are additions for Bookshelf. random_pet generates a readable name (two words separated by - by default) and random_password generates a password with a cryptographically secure generator. Its result is marked sensitive, so it is hidden in console output, but it is still stored in plain text in the state file — one of the reasons state must be treated as a secret (see State and Sensitive Data, Ephemeral Values and Write-only Arguments).

Variables can be overridden without editing files. Run terraform init again (a new provider was added), then:

terraform init
terraform apply -var 'web_port=9090'
terraform output
terraform output web_url
terraform output -raw postgres_password
container_id = "e5fff27c62e2..."
image_id = "sha256:d1a364dc548d...nginx:1.27"
postgres_password = <sensitive>
web_url = "http://localhost:9090"

terraform output lists all outputs of the root module; naming one prints that value, and a sensitive value is masked as <sensitive> unless it is requested by name (for example with -raw). Outputs are also the interface between modules and between configurations, which is why they get a page of their own: Variables, Outputs and Locals covers precedence (terraform.tfvars, -var-file, TF_VAR_ environment variables), validation, and sensitivity.

terraform destroy

terraform destroy
Plan: 0 to add, 0 to change, 2 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

docker_container.web: Destroying...
docker_container.web: Destruction complete after 2s
docker_image.nginx: Destroying...
docker_image.nginx: Destruction complete after 0s

Destroy complete! Resources: 2 destroyed.

destroy plans the removal of every object recorded in the current state and, after confirmation, deletes them in reverse dependency order: the container first, then the image it uses. It only affects the resources this configuration manages. With the Bookshelf additions the plan reports the random_* resources as well (they only exist in state, so their removal is instant). terraform apply -destroy creates the same plan; both honour -auto-approve. Destroy is the reason throwaway environments are cheap with Terraform, and the reason that production resources should carry prevent_destroy guards, covered in Lifecycle, Validation and Checks.

Files to commit and files to ignore

A Terraform working directory mixes configuration that belongs in version control with local state and cache that must stay out. The community .gitignore template maintained on GitHub ignores local data directories, state files, crash logs, variable files that may hold secrets, override files and CLI configuration. An adapted version for Bookshelf:

# Local provider and module cache
.terraform/

# State files: never commit, they can contain secrets in plain text
*.tfstate
*.tfstate.*

# Crash logs
crash.log
crash.*.log

# Variable files often hold secrets; commit only deliberate, secret-free ones
*.tfvars
*.tfvars.json
!example.tfvars

# Local override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Saved plan files and graph output
tfplan
*.tfplan
*.dot

# CLI configuration
.terraformrc
terraform.rc

Note what is deliberately not ignored: .terraform.lock.hcl is committed (see above), as are the .tf files and any secret-free .tfvars example. If *.tfvars are ignored, keep an example.tfvars showing the expected variables. Once a remote backend is in use (see State), no state file is created in the repository at all, but the ignore rules stay as a safety net. Before the first commit, run terraform fmt -check and terraform validate; CI can repeat both, as shown in CI/CD and Automation.

Command summary

Command Purpose

terraform init

Initialize the backend, install modules and providers, create or update the lock file.

terraform fmt

Rewrite files into canonical style; -check for CI.

terraform validate

Check syntax and internal consistency without contacting remote services.

terraform plan

Show the actions needed to match configuration; changes nothing.

terraform apply

Plan, prompt, then perform the actions; -auto-approve or a plan file skips the prompt.

terraform show

Print the state, or a saved plan.

terraform output

Print root module outputs.

terraform destroy

Plan and perform the removal of everything in state.

The remaining commands, global options and environment variables are collected in CLI Essentials.

What changed recently

  • Terraform 1.17 is the current release as of this writing (2026-10-21); install pages and tutorials show it as the current version.

  • The Docker provider is now in its 4.x line (4.0.0 was a major release with a documented migration guide; the Docker tutorial still pins ~> 4.2.0). Pin it explicitly, as above.

  • Older books and tutorials show docker_image.nginx.latest as the container’s image reference; the current tutorial uses docker_image.nginx.image_id.

  • The legacy HashiCorp apt repository addresses and manual gpg steps in older material have been replaced by the commands on the current install page.

  • Where books describe installation as "download a zip and add it to the PATH", that route is still valid, but the package-manager routes and a version manager are now the usual practice.