You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
external_stores (S3) plan restore is skipped on apply when the local pull dir already exists without a plan, breaking multi-replica/HA StatefulSet deployments #6806
Running Atlantis with replicaCount > 1 (e.g. a Kubernetes StatefulSet behind a Service with no session affinity), --enable-external-stores with an S3 plan_store, and --locking-db-type=redis for shared locking, atlantis apply intermittently fails with:
plan file is missing for dir "<dir>" workspace "default" project "<project>" with status "planned"; run `atlantis plan`
— even though the plan was successfully generated and uploaded to S3 moments earlier (confirmed via planstore/s3_plan_store.go logging uploaded plan to s3://...), and PullStatus (backed by the shared Redis DB) correctly reports the project's status as planned.
Digging into server/events/project_command_builder.go, buildAllProjectCommandsByPlan only attempts to restore plans from the external store when WorkingDir.GetPullDir(...) returns an os.IsNotExist error — i.e. only when the per-PR working directory is completely absent on the replica serving the apply request:
If a different event earlier touched this PR on the same replica and created the directory — even one that failed for an unrelated reason (in our case, an autoplan that failed with "project is currently locked by an unapplied plan from pull #X", after which Atlantis's deletePlansAndPlanLocks deletes the .tfplan file but leaves the cloned working directory in place) — then a later apply on that same replica sees the directory already exists, never attempts the S3 restore, finds zero pending plans locally, and throws the "plan file is missing" error despite a valid plan sitting in the external store.
We also observed a worse variant: a targeted apply (atlantis apply -d <dir>) goes through a different code path that does restore a plan file regardless of directory existence, but the restored plan can be inconsistent with that replica's own terraform init state (different provider lock resolution across pods), causing terraform apply to fail with Error: Inconsistent dependency lock file instead of a clear "stale plan" message.
With no session affinity between the VCS webhook delivery and the StatefulSet's pods, this can affect any multi-replica deployment relying on external_stores for HA — it's not an edge case, just a matter of which pod a given comment happens to route to.
Reproduction Steps
Run Atlantis with 2+ replicas (e.g. Kubernetes StatefulSet, no sticky routing), --enable-external-stores (S3), --locking-db-type=redis.
Open a PR that autoplans a project whose dir/workspace is already locked by an unapplied plan from a different PR. The autoplan fails with "project is currently locked...", and Atlantis deletes the (failed) plan file — but the per-PR working directory it created remains on whichever pod served the request.
Resolve the other PR's lock (atlantis unlock / merge it).
Comment atlantis plan on the PR from step 2. If routed to a different pod than step 2 used, it clones fresh, plans successfully, and uploads to S3.
Comment atlantis apply. If routed back to the pod from step 2, it fails with "plan file is missing ... run atlantis plan" despite the valid S3 plan and planned status.
Logs
Logs (redacted)
{"level":"info","msg":"Running autoplan..."}
{"level":"info","msg":"creating dir '/atlantis-data/repos/<org>/<repo>/<pull>/default'"}
{"level":"info","msg":"Updating GitHub Check status for 'atlantis/plan: <project>' to 'failure'"}
{"level":"info","msg":"Failure running plan operation: This project is currently locked by an unapplied plan from pull #<other-pull>. ..."}
{"level":"info","msg":"deleting plans because there were errors and automerge requires all plans succeed"}
... (later, plan succeeds on a different pod and uploads) ...
{"level":"info","caller":"planstore/s3_plan_store.go:144","msg":"uploaded plan to s3://<bucket>/<prefix>/<org>/<repo>/<pull>/default/<dir>/<project>-default.tfplan"}
... (apply routed back to the first pod) ...
{"level":"error","caller":"events/instrumented_project_command_builder.go:81","msg":"Error building apply commands: plan file is missing for dir \"<dir>\" workspace \"default\" project \"<project>\" with status \"planned\"; run `atlantis plan`"}
{"level":"error","caller":"events/pull_updater.go:22","msg":"plan file is missing for dir \"<dir>\" workspace \"default\" project \"<project>\" with status \"planned\"; run `atlantis plan`"}
Environment details
Atlantis version: v0.47.1
Deployment method: Helm chart (runatlantis/helm-charts, chart atlantis-6.11.0), Kubernetes StatefulSet on EKS, replicaCount: 2–3, no session affinity on the Service in front of it
Related (closed): missing plan file does not trigger error on apply #3487 — a different trigger (mid-flow reclone) with a similar underlying symptom: local working-directory state getting out of sync with what plan validation assumes, without a clear error pointing at the real cause.
Possible fix direction: key the restore-from-store decision off whether PendingPlanFinder.Find (or the specific project's entry in PullStatus) finds a matching plan, rather than solely off WorkingDir.GetPullDir's existence check — i.e. attempt restore per-project whenever PullStatus says planned but no local .tfplan is found, regardless of whether the parent pull directory happens to already exist for an unrelated reason.
Overview of the Issue
Running Atlantis with
replicaCount > 1(e.g. a Kubernetes StatefulSet behind a Service with no session affinity),--enable-external-storeswith an S3plan_store, and--locking-db-type=redisfor shared locking,atlantis applyintermittently fails with:— even though the plan was successfully generated and uploaded to S3 moments earlier (confirmed via
planstore/s3_plan_store.gologginguploaded plan to s3://...), andPullStatus(backed by the shared Redis DB) correctly reports the project's status asplanned.Digging into
server/events/project_command_builder.go,buildAllProjectCommandsByPlanonly attempts to restore plans from the external store whenWorkingDir.GetPullDir(...)returns anos.IsNotExisterror — i.e. only when the per-PR working directory is completely absent on the replica serving the apply request:If a different event earlier touched this PR on the same replica and created the directory — even one that failed for an unrelated reason (in our case, an autoplan that failed with "project is currently locked by an unapplied plan from pull #X", after which Atlantis's
deletePlansAndPlanLocksdeletes the.tfplanfile but leaves the cloned working directory in place) — then a later apply on that same replica sees the directory already exists, never attempts the S3 restore, finds zero pending plans locally, and throws the "plan file is missing" error despite a valid plan sitting in the external store.We also observed a worse variant: a targeted apply (
atlantis apply -d <dir>) goes through a different code path that does restore a plan file regardless of directory existence, but the restored plan can be inconsistent with that replica's ownterraform initstate (different provider lock resolution across pods), causingterraform applyto fail withError: Inconsistent dependency lock fileinstead of a clear "stale plan" message.With no session affinity between the VCS webhook delivery and the StatefulSet's pods, this can affect any multi-replica deployment relying on
external_storesfor HA — it's not an edge case, just a matter of which pod a given comment happens to route to.Reproduction Steps
--enable-external-stores(S3),--locking-db-type=redis.atlantis unlock/ merge it).atlantis planon the PR from step 2. If routed to a different pod than step 2 used, it clones fresh, plans successfully, and uploads to S3.atlantis apply. If routed back to the pod from step 2, it fails with "plan file is missing ... run atlantis plan" despite the valid S3 plan andplannedstatus.Logs
Logs (redacted)
Environment details
runatlantis/helm-charts, chartatlantis-6.11.0), Kubernetes StatefulSet on EKS,replicaCount: 2–3, no session affinity on the Service in front of it--allow-commands=all --enable-drift-detection --enable-external-stores --locking-db-type=redisexternal_stores.plan_store:type: s3(bucket/region redacted)atlantis.yaml:automerge: true,parallel_plan: true,parallel_apply: trueAdditional Context
PendingPlanFinder.Find(or the specific project's entry inPullStatus) finds a matching plan, rather than solely offWorkingDir.GetPullDir's existence check — i.e. attempt restore per-project wheneverPullStatussaysplannedbut no local.tfplanis found, regardless of whether the parent pull directory happens to already exist for an unrelated reason.