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-storesflag 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:
- Lists subdirectories of the pull dir — each is a workspace clone.
- Confirms each directory is a git work tree root via
isGitWorkTreeRoot(git rev-parse --is-inside-work-tree --show-prefix). - Runs
git ls-files . --others(untracked files) in the repo dir . Plan files are untracked because Atlantis creates them after the clone. - Filters for
.tfplanextension, skipping.terragrunt-cache/paths . - Calls
ProjectNameFromPlanfileto 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:
- DB status check — loads
PullStatusand verifies the project has an apply-eligible status (PlannedPlanStatus,PlannedNoChangesPlanStatus,PassedPolicyCheckStatus, orErroredApplyStatus) viastatusAllowedForApplyExecution. - Live VCS freshness check — fetches the current
HeadCommitandBaseBranchfrom the VCS API and compares against the stored plan viapullStatusApplyEligibilityError. Rejects if the commit or base branch has changed. - Plan file existence — confirms the
.tfplanfile is present on disk . - SHA-256 hash verification — if
ctx.ExpectedPlanHashis set, re-hashes the file usinghashFileand compares against the hash captured at lock time . The hash usesos.OpenInRootfor 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#
| File | Role |
|---|---|
server/core/runtime/runtime.go | GetPlanFilename, ProjectNameFromPlanfile — filename encoding/decoding |
server/core/runtime/plan_step_runner.go | Plan creation, remote ops fake plan |
server/core/runtime/apply_step_runner.go | Apply execution, post-apply plan deletion |
server/events/pending_plan_finder.go | git ls-files-based plan discovery and bulk deletion |
server/events/apply_plan_validator.go | Pre-apply validation, freshness checks, hash verification |
server/events/plan_command_runner.go | Pre-plan cleanup, plan command orchestration |
server/events/working_dir.go | Storage paths, cleanStalePlanFiles, DeletePlan |