CoursesAtlantisWhat Atlantis is: PR-driven Terraform

What Atlantis is: PR-driven Terraform

GitOps for Terraform via the PR.

Advanced12 min · lesson 1 of 12

Every engineer who runs terraform apply from a laptop is holding a copy of the master key to your infrastructure. Atlantis gathers up those copies and locks them in one guarded room. Want to change something? You slide a written proposal under the door. That proposal is a pull request, a change offered up for review before it lands. The room slides back an exact description of what would happen if it turned the key. The key turns only after a colleague reads that description and says go. And the room keeps a logbook: who proposed, who approved, what actually changed.

Drop the metaphor. Atlantis is a small server you run yourself, sitting quietly and waiting for *webhooks* (HTTP callbacks, short messages your version-control system sends the moment something happens in a repository). GitHub, GitLab, Bitbucket, Azure DevOps and Gitea all fire them on pull-request events. When a pull request touches Terraform files, Atlantis clones the branch, runs terraform plan, and posts the output as a comment on that pull request. When a reviewer comments atlantis apply, the apply runs on the server, with the server's credentials, and the result comes back as another comment. The pull request becomes the single place where an infrastructure change is proposed, previewed, argued over, approved, executed and archived.

Why running Terraform from a laptop falls apart

Four failure modes drive the whole design, and each one explains a decision you will meet later in the course. Credential sprawl: if applies happen on laptops, production cloud credentials live on every laptop, so offboarding one engineer means auditing all of them. Review theater: teams paste plan output into the pull request description, but nothing proves the pasted text matches the commit being merged. The plan you read and the plan that runs can drift apart quietly. State races: two engineers applying overlapping changes at the same moment will fight over state locks on a good day and flatten each other's infrastructure on a bad one. No audit trail: when the only record of a production change is somebody's shell history, incident review turns into archaeology.

You could build each of those guards yourself inside a CI pipeline (continuous integration, the automation that runs on every commit), and plenty of teams do. The Atlantis argument is that this workflow is common enough, and quiet enough about the ways it goes wrong, to deserve a purpose-built tool.

What Atlantis actually is

Atlantis is an open-source Go program that ships as a single binary, a container image (ghcr.io/runatlantis/atlantis), and a Helm chart. It was built at Hootsuite in 2017 and has been community-maintained at runatlantis.io ever since. You host it yourself; there is no hosted version you can sign up for. That cuts both ways. Credentials and plan output never leave your network, which is the selling point, and you own its uptime, its patching and its security, which is the bill. Inside, it runs terraform (or OpenTofu) as an ordinary subprocess in its own data directory.

Two things it deliberately does *not* do. It never touches your state, which stays in whatever backend you already use. Of its own data it keeps very little: pending plan files, written to disk in its data directory, and lock records, held in an embedded BoltDB database (a small key-value store that lives in one file on disk), or in Redis if you configure that instead. It also ships almost no user interface, only a bare page listing the locks it currently holds. The interface is the pull request, comments in and comments out, so nobody has to learn a new dashboard or collect another login.

The loop, end to end

Follow one real change through the system. You push a branch that adds an HTTPS ingress rule and open pull request #142. Your version-control system fires a webhook at Atlantis's /events endpoint. Atlantis works out which *project* (a directory paired with a workspace) the changed files belong to, clones the branch, runs terraform init and terraform plan in that directory, and posts the plan as a comment. That automatic first plan has a name: autoplan. At the same moment it takes a lock on the project, so a second pull request cannot plan or apply against the same directory until this one is settled. Locking gets a lesson of its own.

The Atlantis pull-request loop
1PR opened
a .tf file changes; your VCS posts to /events
2Autoplan
Atlantis clones, plans, comments the diff, takes a lock
3Human review
the team reads the real plan and approves
4atlantis apply
one comment runs the apply, on the server
5Merge & unlock
result commented, PR merged, lock released
The plan happens on its own and everyone can see it. The apply takes a deliberate comment. Credentials stay on the server.

The thread ends up reading like this. The plan below was generated from the exact commit being merged, not pasted in by hand:

PR #142 — comment thread
# You open the PR. ~20 seconds later, Atlantis comments:
Ran Plan for dir: `modules/vpc` workspace: `default`
Terraform will perform the following actions:
# aws_security_group_rule.ingress_https will be created
+ resource "aws_security_group_rule" "ingress_https" {
+ type = "ingress"
+ from_port = 443
+ to_port = 443
+ protocol = "tcp"
+ security_group_id = "sg-0f3a91c2d8e7b6a54"
}
Plan: 1 to add, 0 to change, 0 to destroy.
* To apply this plan, comment: `atlantis apply -d modules/vpc`
* To delete this plan and lock, click here
* To plan this project again, comment: `atlantis plan -d modules/vpc`
# ("here" is a link into Atlantis's small lock-listing UI)
# A reviewer approves the PR, then comments:
atlantis apply -d modules/vpc
# Atlantis replies:
Ran Apply for dir: `modules/vpc` workspace: `default`
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Look at the shape of that command. A bare atlantis apply applies every unapplied plan on the pull request. The flags narrow the blast radius: -d picks a directory, -w a workspace, -p a named project. Commenting atlantis unlock throws away every plan and lock for the whole pull request. Whether an apply is allowed at all before an approval or a green merge check is policy you set, and the apply-gate lesson covers it.

See it run in five minutes

Your version-control system has to be able to reach Atlantis, not the other way round. For GitHub.com that means a public HTTPS address. On a laptop, a tunnel gets you one:

terminal — local demo
# Terminal 1 — expose port 4141 so GitHub's webhooks can reach you:
ngrok http 4141
# Forwarding https://f3a1-84-12-9-101.ngrok-free.app -> http://localhost:4141
# Terminal 2 — run the server. One process, no external database:
docker run --rm -p 4141:4141 ghcr.io/runatlantis/atlantis:latest server \
--atlantis-url="https://f3a1-84-12-9-101.ngrok-free.app" \
--gh-user="atlantis-bot" \
--gh-token="$ATLANTIS_GH_TOKEN" \
--gh-webhook-secret="$ATLANTIS_WEBHOOK_SECRET" \
--repo-allowlist="github.com/yourorg/infra"
# {"level":"info","msg":"Atlantis started - listening on port 4141"}
# Last step: in the repo settings, add a webhook pointing at
# https://<atlantis-url>/events with secret $ATLANTIS_WEBHOOK_SECRET
# (the next lesson wires this up properly, event by event)

Those five flags are the smallest set that works: where Atlantis lives (--atlantis-url), the bot account it comments as (--gh-user and --gh-token), the shared secret that proves an incoming webhook really came from your version-control system (--gh-webhook-secret), and the --repo-allowlist that stops strange repositories from handing it work. In production you run the official Helm chart instead. That gives you a Kubernetes StatefulSet with a persistent volume attached, so plans and locks survive a pod restart:

terminal — production shape (Kubernetes)
helm repo add runatlantis https://runatlantis.github.io/helm-charts
helm install atlantis runatlantis/atlantis \
--namespace atlantis --create-namespace \
--set orgAllowlist='github.com/yourorg/*' \
--set github.user=atlantis-bot \
--set github.token=$ATLANTIS_GH_TOKEN \
--set github.secret=$ATLANTIS_WEBHOOK_SECRET
# NAME: atlantis
# STATUS: deployed
# -> StatefulSet with a PVC for /atlantis-data: plans and the
# BoltDB lock database survive restarts.
# (--set is demo-only: in production, reference an existing
# Kubernetes Secret from a values file — see the credentials lesson)

Two config surfaces, one trust boundary

Atlantis configuration lives in two places, and the split between them is a security boundary rather than filing. Server-side config (a repos.yaml sitting next to the server, plus the flags) belongs to the platform operator: which repos are allowed, what has to be true before an apply runs, and how much the repo side is permitted to override. Repo-side config (atlantis.yaml at the root of the repo) belongs to anyone who can commit to that repo, and it declares the repo's projects:

atlantis.yaml
# atlantis.yaml — repo root. Optional: with no file at all, Atlantis
# autodetects projects — any directory where the PR modifies .tf files
# gets planned.
version: 3
projects:
- name: vpc-prod
dir: prod/vpc
autoplan:
when_modified: ["*.tf", "*.tfvars"]
enabled: true

Here is why the boundary matters. Atlantis plans *pull-request branches*, so anyone who can open a pull request can also edit atlantis.yaml in that same pull request. Repo-side config can, when the operator allows it, define custom workflow steps that include arbitrary run commands. Put those two facts side by side. A server that trusted repo-side workflows blindly would turn every opened pull request into remote code execution (a stranger running commands of their choosing) on the machine holding your cloud credentials. That is why Atlantis is server-side by default, and why repo-side files may only change the settings an operator has explicitly listed under allowed_overrides. Carry that model with you through the rest of the course.

Whoever owns the Atlantis server owns your infrastructure
To run applies, Atlantis carries cloud credentials strong enough to change, and to destroy, everything it manages, and it does that work in response to events arriving over the network. That makes it a first-rank target with three separate ways in. One: the credentials sitting on the box. Two: the webhook endpoint, because if you skip --gh-webhook-secret, anyone who finds the URL can forge events from your version-control system. Three: repo-side config, where an over-permissive server turns a custom run step in somebody's pull request into arbitrary command execution. Compromise the server, or the webhook path, and you have compromised everything it can apply. Treat Atlantis as critical infrastructure from the first deploy. The credentials and hardening lessons exist because of exactly this.

Where it fits, and where it stops

Atlantis has no drift detection and no scheduler. It moves when a pull request moves, and never on its own. Access control is inherited from your version-control system: if someone can comment on a pull request in an allowed repo, Atlantis is listening, subject to whatever gates you configure. And one server that serializes applies per project is wonderful for safety right up to the point where hundreds of repos turn it into a queue. The final lesson weighs it against HCP Terraform, Spacelift and the rest. None of that happens, though, until the server is deployed and your version-control system can reach it. That plumbing (the server process, its flags, and the webhook wiring payload by payload) is next.

Somewhere on your team right now, someone is exporting cloud keys to run an apply over coffee-shop Wi-Fi. Atlantis does not remove the need for those credentials. It concentrates them in one place, behind review. The concentration is the benefit and the threat model at the same time. If you cannot say out loud who is allowed to comment apply, who can change workflows, and which repos the bot will touch, you are not ready to point it at production state.

Try this

If you have access to a real Atlantis pull request thread, read one end to end. If you do not, run the server locally against a throwaway repo and open a pull request that touches a .tf file. The moment worth watching for is a plan comment appearing on its own, with nobody logging into a server to make it happen.

terminal
# after webhook + repo allowlist are wired
atlantis version
# on the PR:
# atlantis plan
# (or rely on autoplan when .tf files change)
output
atlantis version
# atlantis v0.30.x
# PR comment:
# Ran Plan for dir: infra/app workspace: default
# Plan: 1 to add, 0 to change, 0 to destroy

Takeaway

Atlantis takes terraform plan and terraform apply off laptops and turns them into a ritual on the pull request. The plan is the document everyone reads before signing. The apply is the signature, typed as a comment.

Next up: run the server with a real webhook and a real allowlist, so plans post themselves. Then practice the apply comment, and only after a review.

Quick check
01A teammate wants the platform team to stop restricting atlantis.yaml and let each repo define its own workflow steps, arguing that repo owners know their own code best. Which objection actually holds?
Incorrect — Reloading is not the sticking point. Atlantis picks the file up from the pull request branch exactly as submitted, and that immediacy is the very thing that makes unrestricted workflow steps risky.
Correct — On a pull request, the person proposing the change is also the person who wrote the config describing how to run it. Trusting repo-side steps therefore hands a stranger commands on your credential box.
Incorrect — Repo-side steps get no sandbox of their own. Atlantis runs Terraform as an ordinary subprocess in its data directory, on the same machine that carries the credentials it applies with.
Incorrect — Who may comment apply is inherited from your version-control system, and whether an apply is gated is separate policy you set. The allowed_overrides list exists to stop arbitrary commands, not to police approvals.
02You are writing Helm values for a production Atlantis and someone asks why the StatefulSet needs a persistent volume at all. Which answer matches what the server keeps of its own?
Incorrect — Your state stays in whatever backend your code already declares. Atlantis shells out to Terraform and lets it talk to that backend, so no state file ever lands on the volume.
Incorrect — There is no sweep to compare anything against. Atlantis moves when a pull request moves and never on its own, and it keeps no inventory of the resources it manages.
Correct — That is exactly what the chart mounts at /atlantis-data. Lose it and every open pull request needs a fresh plan, and the lock rows that keep two pull requests off the same directory are gone with it.
Incorrect — The plan file written when the plan ran is the same file the apply comment consumes later, so the gap between those two comments is precisely when data has to survive.
03A pull request changes Terraform under modules/vpc and in two other directories, so three plan comments are sitting on the thread. A reviewer approves and comments atlantis apply with nothing after it. What runs?
Correct — This is why -d, -w and -p exist. Commenting atlantis apply -d modules/vpc holds the change to the one directory whose plan the reviewer actually read.
Incorrect — A bare apply is accepted rather than refused. Narrowing it is something the commenter does with a flag, and any stricter requirement would be policy an operator sets on the server side.
Incorrect — There is no first-one-wins rule here. Every plan still sitting unapplied on the pull request gets executed, so the other two directories are not left behind.
Incorrect — Apply consumes the plan files already saved on disk instead of generating new ones, and it asks for no approval of its own. Any approval requirement is checked before the apply runs.

Related