CoursesAtlantisThe plan/apply PR workflow

The plan/apply PR workflow

Comment-driven plan and apply.

Advanced12 min · lesson 3 of 12

Closing on a house runs on a strict ritual. Someone drafts the documents, every party reads the exact pages that will take effect, and the signature executes those pages, not a version quietly redrafted after you looked away. Atlantis puts that same ritual around Terraform. The *plan* (Terraform's preview of every resource it would create, change, or destroy) is the closing document, and it gets posted where your reviewers already spend their day: the pull request, the change proposal your version-control host opens for review. The *apply* (the command that turns that preview into real infrastructure) is the signature. You give it by typing a comment.

One loop: push, plan, review, apply

The daily loop has four beats. You push Terraform changes on a branch and open a pull request. Your version-control host, meaning GitHub, GitLab, Bitbucket, whichever one holds the code, fires a webhook: an automatic HTTP call that tells the Atlantis server something happened. Wiring that up is covered in *Running the server & webhooks*. Atlantis then autoplans. It works out which directories hold changed .tf files, runs terraform plan in each one, and posts the output back as a comment on the pull request. You and your reviewers read the diff. If it looks wrong, push a fix; Atlantis throws away the stale plan and posts a fresh one. When the plan is right and the pull request is approved, someone comments atlantis apply. Atlantis runs it, posts the result, and the pull request merges.

terminal — open a PR, Atlantis takes it from there
git checkout -b add-payments-vpc
cat >> live/prod/vpc/main.tf <<'EOF'
resource "aws_vpc" "payments" {
cidr_block = "10.40.0.0/16"
tags = { Team = "payments" }
}
EOF
git add . && git commit -m "vpc: add payments VPC"
git push -u origin add-payments-vpc
gh pr create --fill
# Seconds later Atlantis comments on the PR (webhook -> autoplan):
#
# Ran Plan for dir: `live/prod/vpc` workspace: `default`
#
# # aws_vpc.payments will be created
# + resource "aws_vpc" "payments" {
# + cidr_block = "10.40.0.0/16"
# ...
# }
#
# Plan: 1 to add, 0 to change, 0 to destroy.
#
# * To apply this plan, comment: `atlantis apply -d live/prod/vpc`
# * To plan this project again, comment: `atlantis plan -d live/prod/vpc`

Look at what you never had to do. No Terraform on your laptop. No hunting for shared cloud credentials. No pasting plan output into Slack and asking whether it looks fine to everyone. The pull request thread now holds three things at once: the code diff, the infrastructure diff, and the approval. That is a complete audit record, and you added no extra tooling to get it.

Comments are the command line

Atlantis gives you no console and no button. Comments are the command line, and three flags decide what a command touches. -d picks a directory relative to the repository root. -w picks a Terraform workspace, a named slice of state that shares the same code; if you have never set one up, yours is called default. -p picks a project, which is a directory-and-workspace pair you gave a name in atlantis.yaml, the repository config file that gets its own lesson. Use -p *instead of* -d and -w, never alongside them. A bare atlantis apply with no flags applies *every* unapplied plan on the pull request. On a pull request that touches five projects, that is almost always more than you meant, so make scoped applies the habit. Anything you type after -- is handed straight through to Terraform.

PR comments — the full command surface
# Re-plan one directory after pushing a fix:
atlantis plan -d live/prod/vpc
# Plan a named project, or a non-default workspace:
atlantis plan -p prod-vpc
atlantis plan -d live/prod/vpc -w staging
# Pass flags through to Terraform after `--`:
atlantis plan -d live/prod/vpc -- -target=aws_vpc.payments
# Apply one directory, or everything planned on this PR:
atlantis apply -d live/prod/vpc
atlantis apply
# Housekeeping — discard this PR's plans and release its locks,
# or have Atlantis reply with usage:
atlantis unlock
atlantis help

What Atlantis is doing behind the comment

The guarantees all come out of the plumbing. When Atlantis plans, it clones the pull request's head commit into its data directory and gives every combination of repository, pull request, and workspace its own working copy (<data-dir>/repos/<owner>/<repo>/<pr>/<workspace>, where the data directory defaults to ~/.atlantis). Two pull requests running at the same time therefore never trample each other's .terraform folders. Atlantis runs terraform init, then terraform plan -out, and keeps the resulting binary plan file on disk. That saved file is the whole trick. When you comment atlantis apply an hour later, Atlantis runs terraform apply *against the stored plan file*, not against a fresh plan. What your reviewers read is byte-for-byte what runs. And if reality moved in the meantime, because somebody changed the infrastructure by hand, the apply stops with a stale-plan error instead of quietly doing something nobody reviewed.

Two useful rules fall out of that. Push a new commit and Atlantis deletes the now-stale plan files and autoplans again, so the approve-first-then-sneak-in-a-commit trick can never apply unreviewed changes. Pair it with your host's dismiss-stale-approvals setting for the belt-and-braces version. Second, the moment a directory is planned, Atlantis takes a lock on it for that pull request, which blocks any other pull request touching the same directory. That mechanism fills the whole next lesson.

The plan/apply loop
1PR opened or commit pushed
webhook fires
2Atlantis plans
clone → init → plan -out=.tfplan
3Plan posted as PR comment
reviewers read the real diff
4atlantis apply
gates: approved · mergeable · undiverged
5Stored plan applied
result posted → merge
Apply runs the plan file saved at step 2, exactly what reviewers read, never a fresh re-plan. A new commit deletes stale plans and starts the loop over.

Gating the apply comment

A comment is a very cheap trigger, which is exactly why production setups put conditions in front of it. Those conditions live in repos.yaml, the server-side config file that pull request authors cannot edit, and the trust boundary that creates gets taken apart in *Server-side config & control*. Three built-in checks carry most of the weight. approved means the pull request has an approving review from somebody other than its author. mergeable means branch protection is satisfied and the CI (continuous integration, the automated build and test runs) checks are green. undiverged means the branch is not sitting behind its base, because applying stale code onto fresh infrastructure is a classic way to cause an outage. One catch on that last one: it only has teeth when the server checks out merge results, which means running with --checkout-strategy=merge.

repos.yaml (server-side) — gate the apply comment
repos:
- id: github.com/acme/*
branch: /^main$/
apply_requirements: [approved, mergeable, undiverged]
# An early `atlantis apply` now gets refused in a comment:
#
# Apply Failed: Pull request must be approved according to the
# project's approval rules before running apply.
#
# ...and with a red CI check:
#
# Apply Failed: Pull request must be mergeable before running apply.

With the gates satisfied, the same comment sails through, and the result lands in the thread like everything else.

PR comment — the apply and its result
atlantis apply -d live/prod/vpc
# Atlantis replies:
#
# Ran Apply for dir: `live/prod/vpc` workspace: `default`
#
# aws_vpc.payments: Creating...
# aws_vpc.payments: Creation complete after 2s [id=vpc-0b1c2d3e4f5a67890]
#
# Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
The posted plan is live ammunition. Read it.
Everything in that comment is the real Terraform plan, and atlantis apply runs exactly that saved plan. A -/+ (destroy and recreate) on a production database, or a quiet destroy that fell out of a refactored module, is sitting right there in the fold-out. One-comment convenience makes rubber-stamping very tempting by pull request number two hundred. Read the resource lines, not the Plan: 1 to add summary at the bottom. And keep the limits of apply_requirements straight: it gates *when* the trigger may fire, never *what* fires. No requirement setting replaces a human reading the diff of record, because running that diff is precisely what the comment does.

Apply before merge, not after

Teams arriving from application GitOps expect merge-then-deploy: land the commit on main, and a controller like Argo CD or Flux drags reality toward it. Atlantis deliberately runs that order backwards, and the inversion is built into the tool rather than a setting you flip. You apply from the *open* pull request, watch it succeed, and merge afterwards. There is no after-merge mode to switch on. Atlantis only works on open pull requests, and the second one merges or closes, it throws away that pull request's plans and drops its locks, so nothing is left to apply. The design pays for itself because Terraform applies fail all the time for boring reasons: quota limits, IAM (identity and access management) denials, provider timeouts. Merge-first would leave main lying about production every time one of those hit, with a revert commit as the only way out. Apply-first catches the failure while the pull request is still open and still abandonable, so you close it and walk away, and main ends up recording only changes that actually applied cleanly. That leaves one loose end: the merge click itself. A pull request that applied but never merged keeps holding its locks and blocks the people working next door. Look at automerge: true in atlantis.yaml (or the --automerge server flag), which has Atlantis merge the pull request for you once every project's apply succeeds. The loop closes without anyone having to remember the final click.

When the loop jams

Three failures cover most bad days. *No plan comment shows up.* Check the webhook delivery log on your version-control host first, because an HTTP 400 response almost always means the webhook secret on the server and the one on the host do not match. If deliveries look healthy, check that the files you changed actually match what Atlantis watches for; edits confined to a module directory need extra configuration, and that is the *Autoplan & change detection* lesson. *"This project is currently locked by an unapplied plan from pull #212".* Another open pull request planned the same directory. Either that pull request applies and merges, or its owner comments atlantis unlock to let go. *The apply is refused right after an approval.* A new commit dismissed the approval and invalidated the plan, so re-approve, re-plan, apply. Treat that lock message as information rather than an obstacle, because locking is the real concurrency safety in Atlantis. How the lock is keyed, when it is taken, and how it gates apply is exactly where we go next, in *Locking & the apply gate*.

Stale plans are how a "reviewed" change turns into a surprise. A new commit should cost you your confidence in the previous plan output, even when the comment thread still shows a green tick further up. Re-run the plan, reread the hunk that matters, then apply. A team that applies from memory of a green check it saw twenty minutes ago has turned Atlantis into theater.

Pull requests that touch several directories need discipline. Apply the directories you understand, in whatever order the dependencies demand. Atlantis will happily plan a dozen projects on one pull request, but it will not work out on its own that the network state has to land before the database state. You get that ordering only by designing the projects that way.

Try this

Take a sandbox pull request and work the whole comment surface: plan, then apply if your policy allows it, and watch the status checks Atlantis sets on the pull request as it goes. Notice that the apply comment names a specific plan rather than describing a fresh one.

terminal
# PR comments
atlantis plan -d infra/app
atlantis apply -d infra/app
# optional: atlantis unlock (only if you intend to clear the lock)
output
Ran Plan for dir: infra/app workspace: default
Plan: 0 to add, 1 to change, 0 to destroy
Apply OK for dir: infra/app
# Apply complete! Resources: 1 changed

Takeaway

One rule outranks everything else here: never apply a plan you have not read a minute ago. The pull request is where the change gets proposed, argued over, and executed, and atlantis apply is your signature on it.

Next up: deciding who is allowed to comment apply at all, using server-side config, plus practice with the jam cases (no plan appears, the plan goes stale, two pull requests fight over the same directory).

Quick check
01A plan comment landed on your pull request an hour ago and nobody has pushed since. You now comment atlantis apply -d live/prod/vpc. Which description matches what the server does?
Correct — The saved file is the contract. Your reviewers signed off on those exact bytes, and if the VPC moved under them since, the run stops instead of surprising anyone.
Incorrect — That sounds safer than it is. Re-planning would make the thing your reviewers read and the thing that executes two different documents an hour apart.
Incorrect — Quietly absorbing someone's manual change is the exact outcome the stored plan file exists to rule out. You get a loud failure instead.
Incorrect — Land the commit and let a controller chase it is the Argo CD and Flux shape. Atlantis runs the sequence the other way round, from the still-open pull request.
02Your team arrives from Flux and asks you to reconfigure Atlantis so applies happen after the merge lands on main. Why is there no such switch to flip?
Incorrect — A merge event never triggers an apply here, so there is no second run for yours to collide with. Duplication is not the problem being avoided.
Incorrect — Branch protection governs merging, not commenting. Even with every protection rule switched off, the ordering would still be the wrong way round.
Correct — Quota limits, identity and access management denials and provider timeouts hit constantly. Failing while the pull request is still open lets you walk away and leaves main honest.
Incorrect — There is no reconciliation loop anywhere in the design. The apply is a comment somebody types, not a controller chasing a commit toward a desired state.
03repos.yaml carries apply_requirements: [approved, mergeable, undiverged]. A pull request has one approving review, its CI check is red, and a reviewer comments atlantis apply -d live/prod/vpc. What lands in the thread?
Incorrect — That review clears one of the three named requirements. The other two still get their say, and mergeable is the one that stops you here.
Correct — mergeable asks your host whether the branch could merge right now, and a failing required check makes the answer no, whatever the approval count says.
Incorrect — -d decides which project runs. It has no say over which requirements that project must clear before anything runs at all.
Incorrect — Nothing gets queued. The refusal is final, and once the check goes green you comment again yourself.

Related