Planfile Storage
Planfile storage enables the plan-then-deploy workflow in CI. When configured, atmos terraform plan
uploads planfiles to storage, and atmos terraform deploy downloads and verifies them before applying.
Configuration
An explicit default store takes precedence over priority. Otherwise, the first
available configured name in priority is selected; names absent from stores are skipped.
GitHub artifacts require a token. For plan uploads, priority selection also requires
ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL; downloads from completed runs
can use the GitHub API token alone. S3 uses the AWS credential chain or the selected
Atmos identity. These availability checks do not verify remote bucket permissions.
An unknown explicit default, an exhausted priority list, or a selected store that
cannot be initialized produces an error. Atmos does not silently redirect planfiles
to a different backend after selection. Environment-based detection is used only
when neither default nor priority is configured.
Storage Backends
| Backend | Type | Best For |
|---|---|---|
| GitHub Artifacts | github/artifacts | GitHub Actions workflows (recommended) |
| S3 | aws/s3 | AWS-native environments, cross-provider workflows |
| Local | local/dir | Testing and development |
Store Identity
By default, a planfile store authenticates with the component's active identity - the same one
atmos terraform plan used for the Terraform run. That works when the component and the store live in
the same cloud, but it breaks in a multi-cloud setup: a component that authenticates with, say, an
azure/subscription identity cannot produce AWS credentials for an aws/s3 planfile bucket, so the
upload fails to resolve an AWS auth context.
Set identity on a store to authenticate it with a specific Atmos Auth identity
(from auth.identities), independent of the component's identity:
The component keeps using its own identity (azure-plan) for Terraform operations, while the S3 planfile
store uses aws-backend for storage. When identity is omitted, the store inherits the component's
identity (the prior behavior), so existing single-cloud setups are unchanged.
The identity must be able to authenticate the chosen backend: an aws/s3 store needs an AWS-capable
identity. It is not limited to ambient, though - any AWS-capable identity kind or chain
(aws/assume-role, aws/permission-set, …) is resolved through the same Atmos Auth chain as the component,
not just passed through from the runner environment. This mirrors the
stores.*.identity field already supported by general Atmos stores.
If components.terraform.planfiles is not configured, planfile storage operations are silently
skipped. CI summaries and status checks still work without planfile storage.
The one exception: explicitly requesting verification with --verify-plan (or
ATMOS_TERRAFORM_VERIFY_PLAN=true) errors when planfile storage is not configured — there is no
stored plan to verify against, and the flag cannot stand in for the storage settings it depends on.
Using GitHub Artifacts in GitHub Actions
The github/artifacts backend talks to the GitHub Actions Artifacts API directly — Atmos is
the native client, not a wrapper around actions/upload-artifact. To do that it needs the runner's
runtime credentials, ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL.
GitHub injects those into action steps (uses:) but withholds them from run: steps — a
deliberate least-privilege decision. So when Atmos's own commands do the upload from a run: step,
they will fail with:
GitHub Artifacts upload requires running within GitHub Actions
(ACTIONS_RUNTIME_TOKEN and ACTIONS_RESULTS_URL must be set)
Surface the credentials with the Atmos github-runtime
action. With mode: env it exports the credentials to $GITHUB_ENV once, and GitHub then injects
them automatically into every later run: step — Atmos's commands pick them up from the environment
with no per-step wiring:
To keep the credentials out of unrelated steps, use the default mode: output instead: the action
emits masked step outputs that you thread, via env:, only into the steps that store planfiles. This
is the same github-runtime mechanism the atmos ci cache backend uses — see the
cache configuration docs for the full security tradeoff between
scoped (mode: output) and ambient (mode: env) credentials.
You only need this when Atmos performs the upload from a run: step. The credentials are already
present inside uses: action steps, and the local/dir and aws/s3 backends don't use them at all.
How It Works
Plan Phase
When atmos terraform plan runs in CI mode with planfile storage configured:
- Terraform generates a binary planfile
- The planfile is bundled with the lock file into a tar archive
- SHA256 integrity checksums are computed
- The bundle is uploaded to the configured storage backend
Deploy Phase
When atmos terraform deploy runs in CI mode with planfile storage configured, verification is
automatic:
- Downloads the stored planfile bundle from storage
- Verifies SHA256 integrity checksums
- Generates a fresh plan against current infrastructure
- Performs JSON-structural comparison between stored and fresh plans
- If plans match: Applies the fresh plan (generated with the apply-time identity and state)
- If plans drift: Fails the deploy (mode
fail) or warns and proceeds (modewarn)
Atmos reconciles rather than replays: it applies the freshly generated plan once the diff confirms it
matches what was reviewed, so the deploy uses the apply-time state and credentials while still
guaranteeing no material drift. This avoids the brittleness of a saved plan — which goes stale when the
state moves, and whose base credentials come from the apply environment, not the plan (so a plan built
on a PR can fail to apply on merge). (To replay the stored binary directly, use --from-plan /
--planfile.) Verification runs on deploy only — apply stays a thin wrapper.
With the github/artifacts store, the automatic download (like the upload) talks to the GitHub
Artifacts runtime API, so deploy must also run after the
github-runtime action
surfaces ACTIONS_RUNTIME_TOKEN / ACTIONS_RESULTS_URL — see
Using GitHub Artifacts in GitHub Actions above.
Plan Verification
Verification compares the JSON plan structures (terraform show -json) of the stored and fresh
plans — not a naive text diff — detecting meaningful changes while ignoring cosmetic noise (sorted keys,
masked sensitive values, skipped computed-hash attributes and data sources).
This semantic comparison is deliberate. A plan legitimately contains values that vary between review and apply — attributes "known after apply," computed fields, hashes, ordering, timestamps. A byte-for-byte check (or Terraform's own saved-plan apply, which any state-lineage change invalidates) would reject a plan that is still perfectly valid. Verification needs wiggle room: tolerate benign variation while still catching substantive drift — a resource added, removed, or actually changed.
It is configurable via components.terraform.planfiles.verify:
components:
terraform:
planfiles:
verify: fail # fail (default under CI) | warn | off
fail(default under CI): fail the deploy on drift.warn: log the drift but proceed with the fresh plan.off: skip verification (and the stored-plan download).
A companion boolean, components.terraform.planfiles.required, governs whether a stored plan must
exist to verify against. It defaults to tracking verify strictness (required when verification
resolves to fail), so a fail-by-default CI deploy fails loudly instead of silently applying an
unverified fresh plan. See Missing stored plan.
Per-run overrides: --verify-plan (force fail) and --verify-plan=false (force off); the CLI flag beats
config, which beats the CI default. See
atmos terraform deploy and
Planfile drift verification.
CLI Commands
Manage stored planfiles with upload, download, list, delete, and show commands.
Related
- Native CI Overview - Feature overview
- CI Cache Configuration - The same
github-runtimecredential mechanism, in depth - CI Configuration - Full configuration reference
atmos terraform planfile- CLI commands