Skip to content
Infrastructure as Code iac getting-started 5 min read

The Terraform Workflow

Almost every change you make with Terraform follows the same tight loop: you edit .tf files to describe the infrastructure you want, then ask Terraform to reconcile reality with that description. This write/plan/apply cycle is what makes Terraform predictable — you always preview the exact set of additions, changes, and deletions before anything happens to your cloud account. Understanding this loop deeply is the single most useful thing you can learn, because every other Terraform feature plugs into it. The commands below use the terraform CLI, but OpenTofu’s tofu binary accepts the identical subcommands and flags.

The core loop

The day-to-day cycle has three commands. You write or edit configuration, run terraform plan to see what would change, and run terraform apply to make it happen. terraform init bootstraps the loop once per working directory, and terraform destroy tears everything down when you are finished.

            ┌──────────────────────────────────────────┐
            │                                          │
            ▼                                          │
   ┌──────────────┐   init    ┌────────────┐   plan    ┌────────────┐
   │  Write/edit  │ ────────► │  Download  │ ────────► │  Preview   │
   │  .tf config  │           │  providers │           │  the diff  │
   └──────────────┘           └────────────┘           └─────┬──────┘
            ▲                                                 │ apply
            │                                                 ▼
            │                                          ┌────────────┐
            └───────────── inspect / iterate ───────── │  Real infra│
                                                        │  + state   │
                                                        └────────────┘

Write the configuration

A working directory is just a folder of .tf files. Start by declaring the providers you depend on and the resources you want. Terraform reads every .tf file in the directory and merges them into a single configuration, so you are free to split files by concern.

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "logs" {
  bucket = "devcraftly-app-logs-2026"

  tags = {
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

terraform init

terraform init initializes the working directory: it downloads the providers declared in required_providers, configures the backend that stores state, and installs any modules. You run it once when you first clone or create a project, and again whenever you add a provider, change the backend, or bump a version constraint. It is always safe to re-run.

terraform init

Output:

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.62.0...
- Installed hashicorp/aws v5.62.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above.

Terraform has been successfully initialized!

Tip: Commit .terraform.lock.hcl to version control. It pins exact provider versions and checksums so every teammate and CI runner resolves identical plugins, just as package-lock.json does for npm.

terraform plan

terraform plan is the safety net. It refreshes the current state, compares it against your configuration, and prints the exact set of changes it would make — without touching anything. Each line is prefixed with a symbol: + to create, - to destroy, ~ to update in place, and -/+ to replace. Reading the plan before every apply is non-negotiable discipline.

terraform plan

Output:

Terraform will perform the following actions:

  # aws_s3_bucket.logs will be created
  + resource "aws_s3_bucket" "logs" {
      + bucket = "devcraftly-app-logs-2026"
      + id     = (known after apply)
      + arn    = (known after apply)
      + tags   = {
          + "Environment" = "production"
          + "ManagedBy"   = "terraform"
        }
    }

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

You can persist a plan to a file with terraform plan -out=tfplan and feed it to apply later, guaranteeing that what you reviewed is exactly what runs — the standard pattern in CI pipelines.

terraform apply

terraform apply executes the plan. By default it computes a fresh plan, shows it, and prompts for interactive confirmation before making any changes. After applying, it writes the new resource attributes into the state file so the next plan starts from an accurate baseline.

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.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_s3_bucket.logs: Creating...
aws_s3_bucket.logs: Creation complete after 3s [id=devcraftly-app-logs-2026]

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

To apply a saved plan unattended, pass the plan file — no prompt is shown because the changes were already approved: terraform apply tfplan. The -auto-approve flag skips the prompt for a live plan, which is convenient in automation but dangerous on a shared terminal.

terraform destroy

When an environment has served its purpose, terraform destroy removes every resource Terraform manages. It is essentially apply in reverse: it builds a plan where everything is marked - for deletion and prompts before proceeding. This makes ephemeral test and review environments cheap to spin up and clean up.

terraform destroy

Output:

  # aws_s3_bucket.logs will be destroyed
  - resource "aws_s3_bucket" "logs" {
      - bucket = "devcraftly-app-logs-2026" -> null
    }

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

aws_s3_bucket.logs: Destroying... [id=devcraftly-app-logs-2026]
aws_s3_bucket.logs: Destruction complete after 1s

Destroy complete! Resources: 1 destroyed.

Warning: destroy is irreversible. It deletes real infrastructure and any data it holds. Never run it against production without scoping it carefully — use -target for surgical removals and protect critical resources with lifecycle { prevent_destroy = true }.

Command reference

CommandWhat it doesWhen you run it
terraform initDownloads providers, sets up backend and modulesFirst setup, and after dependency or backend changes
terraform validateChecks syntax and internal consistencyAnytime, before plan; great in CI and pre-commit hooks
terraform planPreviews the diff without changing anythingBefore every apply
terraform applyCreates, updates, or replaces resourcesTo make your changes real
terraform destroyDeletes all managed resourcesTearing down an environment

Best Practices

  • Always run terraform plan and read it carefully before apply — treat an unexpected -/+ replacement as a stop sign.
  • In CI, save the plan with -out=tfplan and apply that exact artifact so reviewers approve precisely what runs.
  • Run terraform validate and terraform fmt -check in pre-commit hooks to catch errors before they reach the plan stage.
  • Reserve -auto-approve for trusted automation; keep the interactive prompt on local machines and shared sessions.
  • Commit .terraform.lock.hcl and use remote state with locking so concurrent applies cannot corrupt your state.
  • Use short-lived destroy-able environments for testing, and guard production resources with prevent_destroy.
Last updated June 14, 2026
Was this helpful?