What Atlantis is: PR-driven Terraform
GitOps for Terraform via the PR.
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 thread ends up reading like this. The plan below was generated from the exact commit being merged, not pasted in by hand:
# 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 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:
helm repo add runatlantis https://runatlantis.github.io/helm-chartshelm 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 — repo root. Optional: with no file at all, Atlantis# autodetects projects — any directory where the PR modifies .tf files# gets planned.version: 3projects:- name: vpc-proddir: prod/vpcautoplan: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.
--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.
# after webhook + repo allowlist are wiredatlantis version# on the PR:# atlantis plan# (or rely on autoplan when .tf files change)
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.
atlantis.yaml and let each repo define its own workflow steps, arguing that repo owners know their own code best. Which objection actually holds?allowed_overrides list exists to stop arbitrary commands, not to police approvals./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.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?-d, -w and -p exist. Commenting atlantis apply -d modules/vpc holds the change to the one directory whose plan the reviewer actually read.