Skip to content
Infrastructure as Code iac concepts 5 min read

Plan & Apply Lifecycle

Every Terraform run boils down to a single question: how does the world you described in HCL differ from the world that actually exists? Terraform answers it by reconciling three pictures of reality—your configuration, its recorded state, and the live infrastructure—then computing a precise diff of the actions needed to converge them. Understanding this plan-and-apply cycle is the key to predicting exactly what terraform apply will do before it does it. The same model applies to OpenTofu, which shares Terraform’s execution semantics and CLI surface.

The three sources of truth

Terraform reasons about three distinct representations of your infrastructure. A plan is the result of comparing them.

SourceWhat it isHow Terraform reads it
Desired stateYour .tf configurationParsed from HCL files in the working directory
Current stateThe state file (terraform.tfstate or remote backend)Loaded at the start of every command
Real worldActual resources at the providerQueried during the refresh step

The configuration says what you want. The state file is Terraform’s memory of what it last created. The refresh step reconciles that memory against what the provider reports right now, catching out-of-band changes (drift) before the diff is computed.

What happens during terraform plan

A plan executes in well-defined phases:

  1. Init checks – verifies the backend is configured and providers/modules are installed (terraform init must have run first).
  2. Refresh – for each resource in state, Terraform calls the provider’s read API to get the latest attributes. This updates the in-memory state without writing to disk.
  3. Diff – Terraform builds a dependency graph, then for every resource compares refreshed state against the configuration to produce a set of planned actions.
  4. Output – the proposed changes are printed and, optionally, saved to a plan file.
terraform plan -out=tfplan

Saving the plan to a file makes the subsequent apply deterministic: it applies exactly what you reviewed, with no second diff against a world that may have changed in the meantime.

Reading the diff symbols

Terraform annotates each planned change with a leading symbol. Learning these makes plan output instantly scannable.

SymbolActionMeaning
+createResource is in config but not in state
~update in placeAttribute changed; provider supports in-place update
-destroyResource is in state but removed from config
-/+replaceA change forces destroy-then-create (immutable attribute)
<=readData source will be read during apply

Consider this configuration change—renaming a bucket’s tag and tightening an immutable setting:

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

  tags = {
    Environment = "production"
    Team        = "platform"
  }
}

resource "aws_instance" "api" {
  ami           = "ami-0c7217cdde317cfec"
  instance_type = "t3.medium"

  tags = {
    Name = "api-server"
  }
}

Running a plan after editing the tag and changing the AMI yields:

terraform plan

Output:

Terraform will perform the following actions:

  # aws_instance.api must be replaced
-/+ resource "aws_instance" "api" {
      ~ ami                          = "ami-0abc..." -> "ami-0c7217cdde317cfec" # forces replacement
      ~ id                           = "i-0123456789abcdef0" -> (known after apply)
        instance_type                = "t3.medium"
      ~ public_ip                    = "54.12.34.56" -> (known after apply)
        tags                         = {
            "Name" = "api-server"
        }
    }

  # aws_s3_bucket.logs will be updated in-place
  ~ resource "aws_s3_bucket" "logs" {
        id     = "devcraftly-app-logs"
      ~ tags   = {
          ~ "Team" = "infra" -> "platform"
        }
    }

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

The # forces replacement comment is the most important line to watch: it warns that changing ami cannot be done in place, so Terraform will destroy and recreate the instance. The summary line—1 to add, 1 to change, 1 to destroy—is your at-a-glance contract for the apply.

Watch the replacements. A -/+ on a stateful resource (database, EBS volume, EIP) can mean data loss or downtime. Always read why a replace was triggered before approving. Use create_before_destroy in a lifecycle block to minimize downtime where the provider allows it.

What happens during terraform apply

terraform apply runs the same plan logic and then executes it. When given a saved plan file it skips the diff entirely and applies the stored changes:

terraform apply tfplan

Without a plan file, apply computes a fresh plan, prompts for confirmation, then walks the dependency graph—creating, updating, and destroying resources in an order that respects their relationships, parallelizing independent operations (10 at a time by default).

Output:

aws_s3_bucket.logs: Modifying... [id=devcraftly-app-logs]
aws_s3_bucket.logs: Modifications complete after 1s
aws_instance.api: Destroying... [id=i-0123456789abcdef0]
aws_instance.api: Destruction complete after 32s
aws_instance.api: Creating...
aws_instance.api: Creation complete after 41s [id=i-0fedcba9876543210]

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

After each successful operation, Terraform writes the new attributes back to the state file, keeping its memory in sync with the world it just changed.

Targeted and speculative runs

Two flags refine the lifecycle for everyday work:

# Preview without ever touching real resources (refresh + diff only)
terraform plan

# Limit the operation to a subset of the graph (use sparingly)
terraform apply -target=aws_s3_bucket.logs

# Skip the refresh step when you trust state is current (faster)
terraform plan -refresh=false

-target is an escape hatch for recovering from partial failures, not a routine workflow—it bypasses the full dependency graph and can leave config and state inconsistent.

Best Practices

  • Always run terraform plan and read the summary line before applying; never apply blind in shared environments.
  • Use terraform plan -out=tfplan followed by terraform apply tfplan in CI so the applied changes match exactly what was reviewed.
  • Treat every -/+ replace as a yellow flag—confirm it won’t destroy stateful data or cause downtime.
  • Keep refresh enabled in normal runs so drift surfaces in the plan; only use -refresh=false when you have a specific performance reason.
  • Avoid -target except for break-glass recovery; relying on it hides real dependency problems.
  • Store state in a remote backend with locking so concurrent plans and applies can’t corrupt the current-state picture.
Last updated June 14, 2026
Was this helpful?