If infrastructure changes happen by someone running terraform apply from a laptop, nobody reviews them and nobody can say later who changed what. GitOps puts every change through a pull request instead. This guide sets up that loop with Terraform (or OpenTofu), Atlantis on your own VPS, and GitHub Actions for checks.
GitOps turns infrastructure changes into pull requests: every terraform apply is reviewed before it runs. A PR opens, Atlantis posts the plan, the team reviews, an approved atlantis apply makes the change, and the PR merges. Keep state remote and encrypted, require approval for production, and never commit secrets.
What GitOps buys you
Without GitOps:
- One engineer runs
terraform applyon their laptop - The state file lives somewhere unclear
- No audit trail of who changed what
- PRs show the code diff, not the effective infrastructure change
- Drift between git and what's running is invisible
With GitOps:
- Every infrastructure change is a PR
- Atlantis runs
terraform planon the PR and posts the result as a comment - The team reviews: "does this plan match what we want?"
- An approved apply runs from one controlled server, then the PR merges
- Git is the single source of truth
The stack
| Tool | Role |
|---|---|
| Terraform / OpenTofu | Infrastructure definition (HCL files) |
| GitHub (or GitLab) | Code storage and PR review |
| Atlantis | The GitOps bot: runs plan/apply from PR comments |
| S3-compatible bucket (AWS S3 or similar) | Remote, versioned, encrypted state with locking |
| GitHub Actions | Formatting, linting, security scans |
Terraform has used the Business Source Licence since version 1.6; OpenTofu is the open-source fork with the same HCL. Everything below works with either, but check that your Atlantis version supports the one you choose.
What Terraform can and can't manage
Terraform manages anything that has a Terraform provider with an API: public cloud resources, object storage, DNS at providers such as Cloudflare or Route 53, GitHub repositories and more.
Domain India does not offer a customer API for ordering or changing VPS servers, hosting or DNS, so there is no Terraform provider for Domain India services. Order and manage a Domain India VPS in the client area; then use Ansible (or cloud-init and your own scripts) to configure what runs on it, and use Terraform for the resources you keep at API-driven providers. Our Terraform and Ansible guide shows how the two fit together.
Prerequisites
- A working Terraform or OpenTofu project
- A Linux VPS for Atlantis (2 GB RAM is usually enough), with a domain name and HTTPS
- A GitHub repository with your infrastructure code
- A bucket for remote state
Step 1 — Remote state in S3
backend.tf:
terraform {
required_version = ">= 1.11"
backend "s3" {
bucket = "yourcompany-terraform-state"
key = "production/terraform.tfstate"
region = "ap-south-1"
encrypt = true
use_lockfile = true # S3-native locking; replaces the old DynamoDB lock table
}
}S3-native locking (use_lockfile) arrived in Terraform 1.10 and is the recommended option from 1.11; the older dynamodb_table setting is deprecated. OpenTofu supports use_lockfile from 1.10.
Create the bucket once, via the AWS console or a separate small Terraform project:
resource "aws_s3_bucket" "tfstate" {
bucket = "yourcompany-terraform-state"
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}Initialise: terraform init -reconfigure
Step 2 — Install Atlantis on your VPS
ATLANTIS_VERSION=0.x.y # current release from github.com/runatlantis/atlantis/releases
TF_VERSION=1.x.y # the version your project pins
wget https://github.com/runatlantis/atlantis/releases/download/v${ATLANTIS_VERSION}/atlantis_linux_amd64.zip
unzip atlantis_linux_amd64.zip
sudo mv atlantis /usr/local/bin/
# Atlantis can download Terraform versions itself; installing the default is still useful
wget https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip
unzip terraform_${TF_VERSION}_linux_amd64.zip
sudo mv terraform /usr/local/bin/
# Create the atlantis user
sudo useradd -r -m -d /opt/atlantis -s /sbin/nologin atlantis
sudo mkdir -p /opt/atlantis/data
sudo chown -R atlantis:atlantis /opt/atlantisAtlantis reads every flag from an ATLANTIS_… environment variable, which keeps tokens out of the process list. /opt/atlantis/.env (then sudo chmod 600 /opt/atlantis/.env):
ATLANTIS_ATLANTIS_URL=https://atlantis.yourcompany.com
ATLANTIS_GH_USER=atlantis-bot
ATLANTIS_GH_TOKEN=ghp_xxx # token for the bot account (a GitHub App is better for teams)
ATLANTIS_GH_WEBHOOK_SECRET=random-long-string
ATLANTIS_REPO_ALLOWLIST=github.com/yourcompany/infra
ATLANTIS_REPO_CONFIG=/opt/atlantis/repos.yaml
ATLANTIS_DATA_DIR=/opt/atlantis/data
ATLANTIS_PORT=4141
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=ap-south-1/etc/systemd/system/atlantis.service:
[Unit]
Description=Atlantis GitOps Server
After=network-online.target
[Service]
Type=simple
User=atlantis
Group=atlantis
WorkingDirectory=/opt/atlantis
EnvironmentFile=/opt/atlantis/.env
ExecStart=/usr/local/bin/atlantis server
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetPut nginx with a Let's Encrypt certificate in front of port 4141, as in our Go applications guide (same reverse-proxy pattern), and keep 4141 itself closed in the firewall.
Start:
sudo systemctl daemon-reload
sudo systemctl enable --now atlantisStep 3 — Server-side repo policy
/opt/atlantis/repos.yaml decides what each repository may do. Put the production safeguards here, where people with write access to the repo can't remove them:
repos:
- id: github.com/yourcompany/infra
apply_requirements: [approved, mergeable]
allow_custom_workflows: true # lets atlantis.yaml define workflows
allowed_overrides: [workflow]Step 4 — GitHub webhook
In your infrastructure repo on GitHub: Settings → Webhooks → Add webhook.
- Payload URL:
https://atlantis.yourcompany.com/events - Content type:
application/json - Secret: the same value as
ATLANTIS_GH_WEBHOOK_SECRET - Events: Pull requests, Pull request reviews, Issue comments, Pushes
Step 5 — atlantis.yaml in your repo
At the root of your infrastructure repo:
version: 3
projects:
- name: production
dir: environments/production
workflow: production
autoplan:
when_modified: ["*.tf", "../../modules/**/*.tf"]
enabled: true
- name: staging
dir: environments/staging
workflow: default
autoplan:
when_modified: ["*.tf"]
enabled: true
workflows:
default:
plan:
steps:
- init
- plan
apply:
steps:
- apply
production:
plan:
steps:
- init
- plan
apply:
steps:
- applyStep 6 — The workflow in action
- You open a PR modifying
environments/staging/main.tf - Atlantis posts a comment with the
terraform planoutput - The team reviews the plan: "yes, adding this DNS record looks right"
- Someone comments
atlantis applyon the PR - Atlantis runs
terraform applyand posts the result - Merge the PR
Because of apply_requirements: [approved, mergeable] in repos.yaml, atlantis apply is refused until a reviewer has approved the PR and it passes its required checks.
Step 7 — GitHub Actions for extras
Atlantis handles Terraform itself. Use GitHub Actions for formatting, linting, security scanning and docs:
.github/workflows/ci.yml:
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: hashicorp/setup-terraform@v3
- run: terraform fmt -check -recursive
- name: tflint
uses: terraform-linters/setup-tflint@v4
- run: tflint --init && tflint
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Checkov scan
uses: bridgecrewio/checkov-action@v12 # pin a release, never @master
with:
directory: .
framework: terraform
docs:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
- uses: terraform-docs/gh-actions@v1
with:
working-dir: .
output-file: README.md
output-method: inject
git-push: "true"Check each action's page for its current major version. This runs on every PR:
terraform fmtchecktflintstatic analysischeckovsecurity scan- Auto-update of
README.mdwith module docs
Secrets handling in GitOps
Never commit secrets. Two patterns:
1. External secret store (recommended):
Use AWS Secrets Manager or HashiCorp Vault, and read secrets at plan time:
data "aws_secretsmanager_secret_version" "db" {
secret_id = "prod/database/password"
}
resource "aws_db_instance" "main" {
# ... engine, instance_class, etc.
password = data.aws_secretsmanager_secret_version.db.secret_string
}Values read this way still end up in the state file, which is why state must be encrypted and access-restricted. Where the provider supports it, let the service manage the secret itself (for example manage_master_user_password = true for RDS) so Terraform never sees it.
2. SOPS (encrypted files in git):
sops --encrypt --age age1yourpublickey... secrets.yaml > secrets.enc.yaml
# Commit secrets.enc.yaml, never secrets.yamlTerraform reads it with the carlpett/sops provider. Give the Atlantis server the decryption key, not every laptop.
Running this on Domain India
- Atlantis host: a Domain India VPS is a self-managed KVM server with full root access, so Atlantis, nginx and Terraform run on it as described above. The smallest plan (VPS Starter, 2 GB RAM in the catalogue) suits a small team. You patch and secure it yourself, and no backups or snapshots are included, so keep
repos.yamland the server setup in git. - What Terraform manages: resources at API-driven providers (cloud, object storage, DNS at a provider such as Cloudflare). Domain India VPS, hosting and DNS are managed in the client area; configure the software on your VPS with Ansible from the same repository.
- DNS as code: if you want DNS records in Terraform, host the zone at a DNS provider with a Terraform provider and point the domain's nameservers there, as described in how to change your nameservers.
- Shared hosting can't run Atlantis: it stops long-running processes.
Common pitfalls
atlantis apply. Set apply_requirements in the server-side repos.yaml./events. Check GitHub's "Recent Deliveries" for the webhook to see the exact response.terraform force-unlock LOCK_ID carefully, only when you're sure nothing is running.required_version and set terraform_version in atlantis.yaml.encrypt = true, block public access and restrict who can read the bucket.FAQ
Atlantis or HCP Terraform (Terraform Cloud) or Spacelift?
Atlantis is open source and self-hosted: free to use, but you run and secure the server. HCP Terraform and Spacelift are managed services with free tiers and paid plans (check their current pricing). Atlantis suits teams comfortable running their own tools.
Do I need a separate VPS for Atlantis?
Not necessarily; it can share a server with other internal tools. It must be reachable by GitHub webhooks over public HTTPS, and it holds credentials that can change your infrastructure, so keep that server locked down.
What is drift and how do I detect it?
Drift is when manual changes (someone clicked in a console or edited a server by hand) make reality differ from your Terraform code. Detect it by running a scheduled terraform plan; any non-empty plan means drift. A weekly scheduled GitHub Actions job works well.
Can I use Ansible in this workflow?
Yes. Terraform creates resources at API-driven providers, Atlantis applies them, and a GitHub Actions job runs Ansible on merge to configure servers, including a Domain India VPS you ordered in the client area.
Can Terraform create or change Domain India VPS servers?
No. Domain India doesn't offer a customer API for VPS, hosting or DNS, so there is no Terraform provider for them. Order and manage those in the client area, and manage the software on the VPS with Ansible or scripts.
How do I roll back a bad apply?
Revert the PR in git and apply again: Terraform is declarative, so reverting the code reverts the infrastructure where possible. Destroyed resources such as deleted databases can't be brought back this way, so read every plan for destroy actions before approving.
Ready to self-host Atlantis? Compare VPS plans, or open a support ticket if you are unsure which size fits.
Self-managed KVM VPS with full root access and NVMe storage, from ₹553 a month excluding GST.
Get a VPS