The plan/apply PR workflow
Comment-driven plan and apply.
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.
git checkout -b add-payments-vpccat >> live/prod/vpc/main.tf <<'EOF'resource "aws_vpc" "payments" {cidr_block = "10.40.0.0/16"tags = { Team = "payments" }}EOFgit add . && git commit -m "vpc: add payments VPC"git push -u origin add-payments-vpcgh 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.
# 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-vpcatlantis 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/vpcatlantis apply# Housekeeping — discard this PR's plans and release its locks,# or have Atlantis reply with usage:atlantis unlockatlantis 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.
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:- 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.
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.
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.
# PR commentsatlantis plan -d infra/appatlantis apply -d infra/app# optional: atlantis unlock (only if you intend to clear the lock)
Ran Plan for dir: infra/app workspace: defaultPlan: 0 to add, 1 to change, 0 to destroyApply 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).
atlantis apply -d live/prod/vpc. Which description matches what the server does?main. Why is there no such switch to flip?main honest.repos.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?mergeable is the one that stops you here.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.-d decides which project runs. It has no say over which requirements that project must clear before anything runs at all.