Skip to main content
RunBook Academy

TerraformV · The Terraform WorkflowProduction Terraform

terraform apply in Depth

Intermediate⏱ ~14 minbash

What you'll learn

  • Run terraform apply against a saved plan file without re-planning
  • Choose when -auto-approve and -input=false are appropriate (and when they are not)
  • Tune -parallelism for provider rate limits and large estates
  • Configure a per-environment gate that prevents auto-approve against production
  • Recognise partial-apply failure modes and the recovery runbook for each

Prerequisites

Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-13

Not yet marked complete on this device.

terraform apply is the only command in the workflow that writes to the real world. Plan reads; init configures; apply mutates. Everything in the rest of the workflow is about ensuring that when apply runs, it runs against a reviewed contract and does not exceed its authority.

What apply does

A plain terraform apply runs the same four phases as plan (refresh, graph, diff, print), then adds the actual write phase. After the diff is printed, apply waits for the operator to type yes, then walks the graph and executes the API calls. Each write updates the state file as it goes.

Phase 1: Refresh state from real world
Phase 2: Build dependency graph
Phase 3: Compute planned changes
Phase 4: Print plan to stdout
Phase 5: [Prompt for confirmation unless -auto-approve]
Phase 6: Walk graph, call provider APIs
Phase 7: Update state file after each successful write
Phase 8: Release state lock

The state is updated after each successful write. This is what makes a partial apply recoverable: if apply fails mid-way, the state file reflects every resource that was successfully written.

The apply argument forms

# Default: re-plan, prompt for confirmation.
terraform apply

# Apply a saved plan file. Skip re-plan. Skip confirmation.
terraform apply tfplan

# Re-plan, but skip confirmation. CI-only.
terraform apply -auto-approve

# Apply with no interactive prompts for missing variables.
terraform apply -input=false tfplan

# Limit concurrent resource operations (default 10).
terraform apply -parallelism=5 tfplan

# Apply only the targeted resource and its dependencies.
terraform apply -target=aws_instance.web

The single most important distinction: terraform apply (no argument) re-plans. terraform apply tfplan consumes a saved plan. The first is for interactive work; the second is for production.

-auto-approve: the sharp flag

-auto-approve skips the confirmation prompt. It exists for one reason: CI/CD pipelines that have already performed the review through a different mechanism (pull request approval, peer review, automated policy check).

The production rule is binary. -auto-approve is acceptable when all of the following are true:

  • The plan was saved to a file and consumed by apply.
  • The plan file was reviewed by a human or by an automated policy.
  • The apply job is running against a non-production environment, OR the apply job has an explicit, logged approval step in the CI/CD platform.

-auto-approve is not acceptable when:

  • Apply is re-planning internally (no -out).
  • The apply job targets production with no separate approval gate.
  • The plan file is not preserved as an artefact.

A common production setup:

# CI plan job
terraform plan -out=tfplan -input=false -detailed-exitcode
# Save tfplan as a job artefact.

# CI apply job for non-production
terraform apply -input=false -auto-approve tfplan

# CI apply job for production: requires an explicit approval gate
# (GitHub Actions environment protection rules, GitLab protected
# environments, Jenkins input step).
terraform apply -input=false tfplan

The production gate lives in the CI/CD platform, not in the Terraform command line. The platform blocks the apply job until a human approves it. Once approved, apply runs without -auto-approve against the reviewed plan file.

-input=false

-input=false prevents Terraform from prompting interactively for missing variables. Without it, a plan or apply that references an undefined variable will hang waiting for the operator to type a value. In CI, this means the job hangs until it times out.

# CI: always pass -input=false so a missing variable fails the job.
terraform apply -input=false tfplan

If a variable is missing, the command fails fast with an error that lists the variable. That is the correct CI behaviour: fail loudly, do not hang.

-parallelism

-parallelism=N limits the number of concurrent resource operations apply performs. The default is 10. On a small configuration, this is invisible. On a configuration with 200 resources, apply makes 10 simultaneous API calls.

The trade-offs:

  • Lower parallelism: apply is slower. The provider API sees fewer requests per second. Useful when the provider rate-limits aggressively (older AWS APIs, smaller third-party providers).
  • Higher parallelism: apply is faster, but you risk rate limits. Some teams raise to 20 for AWS, where the limit is generous, and drop to 2 or 3 for providers they do not control.
# Large estate, generous provider limits.
terraform apply -parallelism=20 tfplan

# Third-party provider with tight rate limits.
terraform apply -parallelism=2 tfplan

The right value is empirical: run apply with the default, watch for ThrottlingException in the logs, and adjust. Do not pre-tune before you have evidence of rate limiting.

The per-environment gate

Production Terraform pipelines should look like this:

Pull request opened
  ├── Run fmt -check, init -backend=false, validate  (cheap, <30s)
  ├── Run plan with -out=tfplan                     (60-180s)
  ├── Post plan output as PR comment
  └── Require human approval

Merge to main
  ├── Run plan again (post-merge state)
  ├── Require approval for non-dev environments
  └── Apply with the reviewed plan file

Production apply
  ├── Gate: GitHub Actions environment approval / GitLab protected env
  ├── terraform apply tfplan (no -auto-approve needed; gate did the approval)
  └── Upload state changes as artefact

The -auto-approve flag never appears in the production apply job. The approval lives in the CI/CD platform, where it produces an audit log entry with the approver’s name and timestamp.

Partial-failure recovery

Apply can fail mid-graph. The state file reflects every successful write, so recovery is a function of what the state says and what the cloud says. The standard recovery runbook:

  1. Do not re-run apply immediately. A re-run re-plans against current state. If the failure was transient (network blip, API timeout), a re-run may complete the remaining changes. If the failure was structural (invalid argument, IAM denied), a re-run will fail at the same step.

  2. Inspect the state. terraform state list shows every resource apply believes exists. Cross-reference with the cloud provider: did the resource actually get created? If yes, the state is correct. If no, the state is wrong.

  3. Align state with reality. terraform plan -refresh-only will surface the drift. Apply that plan to bring state in sync. Then investigate the original failure.

  4. For a stuck resource (apply cannot finish because a resource is in a bad state): terraform state rm to remove the resource from state, then re-apply to recreate it. This is destructive if the resource holds data.

  5. For a half-replaced resource: the old resource is gone, the new resource failed to create. terraform apply will see the missing new resource and try to create it. Inspect the cloud for orphans.

The general rule: the state file is your contract with reality after a partial failure. Read it carefully before re-running anything.

Production failure modes

1. Concurrent applies without state locking. Symptom: two operators run apply at the same time. Both attempt to acquire the state lock; one waits; one runs; one overwrites the state file at the end with a stale snapshot. Cause: state locking is disabled, or both operators are using local state with no shared backend. Recovery: enable a backend that supports locking (S3 + DynamoDB, GCS, Terraform Cloud, Consul). Never operate Terraform against a local state file in a shared environment.

2. -auto-approve in production CI without a plan-file gate. Symptom: production apply runs with -auto-approve and no saved plan. The plan output that was reviewed in the PR is not the plan that apply executes. Cause: the pipeline was copied from the dev environment and nobody replaced -auto-approve with a platform-level approval gate. Recovery: rewrite the production job to require an explicit approval step in the CI/CD platform; remove -auto-approve from the production job.

3. Re-plan between plan and apply. Symptom: a CI plan runs at 10:00; the apply job runs at 10:45; the apply output shows different changes than the PR. Cause: state changed in the 45-minute gap. Recovery: the saved-plan pattern. Save with -out in plan, pass the file to apply.

4. Provider rate limits during apply. Symptom: apply fails partway through with ThrottlingException or RequestLimitExceeded. Cause: -parallelism is too high for the provider’s quota. Recovery: lower -parallelism, wait, retry. For long-term: switch to a provider with higher limits, or split the configuration across multiple state files so each plan/apply hits fewer resources.

5. State lock stuck after a crashed apply. Symptom: subsequent apply hangs on Acquiring state lock. Cause: the previous apply process was killed (OOM, ctrl-C, node reboot) without releasing the lock. Recovery: do not use terraform force-unlock until you have verified that no other apply is running. The lock is per-backend: for S3 + DynamoDB, inspect the DynamoDB table for the lock item; for Terraform Cloud, the workspace UI shows active runs.

6. -parallelism=10 on a small API quota. Symptom: apply hits the third-party API and gets blocked for an hour. Cause: 10 concurrent calls exceed the per-second quota. Recovery: lower -parallelism to 1 or 2, retry. Pre-empt this in the configuration by reading the provider’s rate-limit docs.

Security implications

The IAM role running apply needs write access to every resource it manages. That is broader than plan (which needs read) and is the highest-privilege role in the workflow. Mitigations:

  • A dedicated role per environment (dev, staging, production) with the narrowest possible permissions.
  • Short-lived credentials. For AWS, IRSA or instance profiles with STS. For GCP, workload identity. Never long-lived access keys in CI.
  • The apply role should be able to create, update, and destroy the resources in scope. It should not be able to modify IAM, billing, or the state backend itself unless that is the explicit subject of the change.

The state file contains every output value, including sensitive outputs. Treat the state backend as a secrets store: encryption at rest, access logging, and tight IAM on the bucket.

Performance implications

Apply time is dominated by provider API latency. For 100 resources on AWS, apply takes 3-10 minutes. For 1,000 resources, expect 30-90 minutes. Mitigations:

  • Split the configuration across multiple state files so each apply covers a smaller surface.
  • Use -parallelism based on provider limits.
  • Use -target only for emergency triage; do not use it to make daily apply faster.

State locking is fast (sub-second for DynamoDB, milliseconds for Consul). It is not a performance bottleneck.

Verification

# Apply the saved plan; verify no re-plan.
terraform apply -input=false tfplan
# The output should not contain a "Terraform will perform the
# following actions" section. It should show "Applying..." directly.

# Verify the state matches the cloud.
terraform plan -detailed-exitcode
# Should return 0: no changes pending.

# Verify the lock was released.
# For S3 + DynamoDB, check the DynamoDB table; the lock item should be gone.
aws dynamodb scan --table-name terraform-locks --query "Items[*].LockID"

A healthy apply leaves the state file updated, the lock released, and a subsequent plan returning no changes. Any deviation is a signal to inspect before re-running.

What comes next

The next lesson covers terraform destroy as the production-dangerous operation it is — the per-resource guard, the surgical -target, the cost of a runaway destroy, and the right CI policy.

Knowledge check · 7 questions

  1. Q1. Which command applies a saved plan file without re-planning?

  2. Q2. In production, terraform apply -auto-approve is acceptable as long as the plan was reviewed in the pull request.

  3. Q3. Which flags should always be set on a CI apply job? (Select all that apply.)

  4. Q4. Apply fails mid-graph after creating 30 of 50 resources. What does the state file look like?

  5. Q5. What is the right way to gate production apply in CI?

  6. Q6. Apply is consistently failing with ThrottlingException halfway through. What is the right response?

  7. Q7. An apply job runs against a shared state file. Two operators in different time zones both ran apply at the same moment. One apply succeeded; the other reported 'Error acquiring state lock' and exited. What just happened, and what should the team verify?

Passing score: 75%. Answers are checked in this browser.