Documentsatlantis
Terraform Plan Management
Terraform Plan Management
Type
Topic
Status
Published
Created
Jul 9, 2026
Updated
Jul 9, 2026

Terraform Plan Management#

Atlantis manages .tfplan files through their full lifecycle: creation during terraform plan, storage on disk co-located with the git workspace clone, validation before apply, and deletion after a successful apply or when plans are superseded. The lifecycle spans server/events/ and server/core/runtime/.


Plan File Naming and Storage#

Plan files are named by GetPlanFilename in server/core/runtime/runtime.go:

  • No project name: {workspace}.tfplan
  • With project name: {projectName}-{workspace}.tfplan
  • Slashes in project names are replaced with :: for filesystem safety.

The reverse — extracting a project name from an existing filename — is done by ProjectNameFromPlanfile.

Plan files are stored co-located with the workspace clone under the --data-dir. The directory layout is:

<data-dir>/repos/<owner>/<repo>/<pr-num>/<workspace>/<project-path>/<workspace>.tfplan

FileWorkspace.repoPullDir constructs the pull-level directory, and FileWorkspace.cloneDir adds the workspace segment. Both enforce path-traversal containment via utils.EnsureSubPath. The individual plan path is assembled by planFilePath in apply_plan_validator.go.

Proposed change: Issue #6637 proposes a --enable-local-stores flag to decouple plan file storage from --data-dir, enabling a separate volume for plan artifacts and laying the groundwork for remote backends (S3, GCS). This is not yet merged.


Plan Creation#

planStepRunner.Run in server/core/runtime/plan_step_runner.go invokes terraform plan -out <planFile> . The plan file path is composed as filepath.Join(path, GetPlanFilename(...)) .

Remote ops (TFE/Terraform Cloud): When the remote backend rejects -out (detected by known error strings), remotePlan creates a fake plan file containing the plan's text output prefixed with remoteOpsHeader ("Atlantis: this plan was created by remote ops\n"). This preserves the invariant that a plan file on disk signals a pending plan .


Plan Discovery via git ls-files#

DefaultPendingPlanFinder.Find in server/events/pending_plan_finder.go locates all unapplied plans for a pull request. The algorithm:

  1. Lists subdirectories of the pull dir — each is a workspace clone.
  2. Confirms each directory is a git work tree root via isGitWorkTreeRoot (git rev-parse --is-inside-work-tree --show-prefix).
  3. Runs git ls-files . --others (untracked files) in the repo dir . Plan files are untracked because Atlantis creates them after the clone.
  4. Filters for .tfplan extension, skipping .terragrunt-cache/ paths .
  5. Calls ProjectNameFromPlanfile to reconstruct the project name from the filename.

This approach is intentional: plan files are untracked by git, so git ls-files --others reliably enumerates them without false positives from committed files.


Pre-Plan Cleanup#

Before running a fresh (non-targeted) plan, PlanCommandRunner deletes all existing pending plans and their locks via deletePlansAndPlanLocks. This prevents stale plans from interfering with the new run . For automerge runs that error, plans are also deleted to avoid a partial state .

When git resets the workspace (e.g., updating to a new commit), cleanStalePlanFiles runs git clean -f -x -e .terragrunt-cache -- ':(glob)**/*.tfplan' to remove any leftover .tfplan files that git reset --hard does not touch (because they are untracked).


Pre-Apply Validation#

DefaultApplyPlanValidator.ValidateProjectPlan is the per-project gate before each apply step. It enforces:

  1. DB status check — loads PullStatus and verifies the project has an apply-eligible status (PlannedPlanStatus, PlannedNoChangesPlanStatus, PassedPolicyCheckStatus, or ErroredApplyStatus) via statusAllowedForApplyExecution.
  2. Live VCS freshness check — fetches the current HeadCommit and BaseBranch from the VCS API and compares against the stored plan via pullStatusApplyEligibilityError. Rejects if the commit or base branch has changed.
  3. Plan file existence — confirms the .tfplan file is present on disk .
  4. SHA-256 hash verification — if ctx.ExpectedPlanHash is set, re-hashes the file using hashFile and compares against the hash captured at lock time . The hash uses os.OpenInRoot for path-traversal safety.

On any validation failure, rejectProjectPlan deletes the plan file in addition to returning an error, ensuring stale plans do not persist on disk.


Post-Apply Deletion#

ApplyStepRunner.Run deletes the plan file immediately after a successful apply :

// If the apply was successful, delete the plan.
if err == nil {
    utils.RemoveIgnoreNonExistent(planPath)
}

Remote ops apply: runRemoteApply reads the fake plan file created during remotePlan, then compares the stored plan text with the live apply output via remotePlanChanged. If the plans diverge, it sends "no\n" to the apply process and aborts. On success, the file is deleted by the same err == nil path.


Key Source Files#

FileRole
server/core/runtime/runtime.goGetPlanFilename, ProjectNameFromPlanfile — filename encoding/decoding
server/core/runtime/plan_step_runner.goPlan creation, remote ops fake plan
server/core/runtime/apply_step_runner.goApply execution, post-apply plan deletion
server/events/pending_plan_finder.gogit ls-files-based plan discovery and bulk deletion
server/events/apply_plan_validator.goPre-apply validation, freshness checks, hash verification
server/events/plan_command_runner.goPre-plan cleanup, plan command orchestration
server/events/working_dir.goStorage paths, cleanStalePlanFiles, DeletePlan
Terraform Plan Management | Dosu