Client Area

GitOps: Terraform + Atlantis + GitHub Actions for DomainIndia Infrastructure

ByDomain India Team·DomainIndia Engineering
6 min readPublished 24 Apr 2026Updated 23 Jun 2026150 views

In this article

  • 1What GitOps buys you
  • 2The stack
  • 3Prerequisites
  • 4Step 1 — Remote state in S3
  • 5Step 2 — Install Atlantis on DomainIndia VPS

GitOps: Terraform + Atlantis + GitHub Actions for DomainIndia Infrastructure

TL;DR
GitOps turns infrastructure changes into pull requests — every terraform apply is code-reviewed before it runs. This guide shows the full loop: PR opens, Atlantis plans, team reviews, merge triggers apply. Reproducible, auditable, team-friendly IaC.

What GitOps buys you

Without GitOps:

  • One engineer runs terraform apply on their laptop
  • State file lives somewhere unclear
  • No audit trail of who changed what
  • PRs show code diff, not the effective infrastructure change
  • Drift from what's in git vs what's running is invisible

With GitOps:

  • Every infra change is a PR
  • Atlantis runs terraform plan on the PR, posts diff as comment
  • Team reviews: "does this diff match what we want?"
  • Merge → automated terraform apply
  • Git is the single source of truth

The stack

ToolRole
Terraform / OpenTofuInfrastructure definition (HCL files)
GitHub (or GitLab)Storage + PR review
AtlantisThe GitOps bot — runs plan/apply on comments
AWS S3 or GCSRemote state storage + DynamoDB lock
GitHub ActionsCI/CD for build steps + linting

Prerequisites

  • Working Terraform project (see our Terraform & Ansible article)
  • DomainIndia VPS for Atlantis (2 GB RAM sufficient)
  • GitHub repo with infra code
  • S3 bucket for remote state

Step 1 — Remote state in S3

backend.tf:

hcl
terraform {
  backend "s3" {
    bucket         = "yourcompany-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "ap-south-1"
    dynamodb_table = "yourcompany-terraform-lock"
    encrypt        = true
  }
}

Create the bucket + DynamoDB lock table (one-time, via AWS console or a separate Terraform project):

hcl
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_dynamodb_table" "tflock" {
  name           = "yourcompany-terraform-lock"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "LockID"
  attribute { name = "LockID"; type = "S" }
}

Initialise: terraform init -reconfigure

Step 2 — Install Atlantis on DomainIndia VPS

bash
# Download binary
wget https://github.com/runatlantis/atlantis/releases/download/v0.27.0/atlantis_linux_amd64.zip
unzip atlantis_linux_amd64.zip
sudo mv atlantis /usr/local/bin/

# Atlantis needs terraform available
wget https://releases.hashicorp.com/terraform/1.8.0/terraform_1.8.0_linux_amd64.zip
unzip terraform_1.8.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/

# Create atlantis user
sudo useradd -r -m -d /opt/atlantis atlantis
sudo mkdir -p /opt/atlantis/data
sudo chown -R atlantis:atlantis /opt/atlantis

/etc/systemd/system/atlantis.service:

ini
[Unit]
Description=Atlantis GitOps Server
After=network.target

[Service]
Type=simple
User=atlantis
Group=atlantis
WorkingDirectory=/opt/atlantis
ExecStart=/usr/local/bin/atlantis server 
    --atlantis-url=https://atlantis.yourcompany.com 
    --gh-user=atlantis-bot 
    --gh-token=$GH_TOKEN 
    --gh-webhook-secret=$WEBHOOK_SECRET 
    --repo-allowlist=github.com/yourcompany/infra 
    --data-dir=/opt/atlantis/data 
    --port=4141
EnvironmentFile=/opt/atlantis/.env
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

/opt/atlantis/.env:

GH_TOKEN=ghp_xxx   # Personal Access Token for atlantis-bot
WEBHOOK_SECRET=random-long-string
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=ap-south-1

Add nginx reverse proxy + SSL (see our Running Go Applications — same pattern).

Start:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now atlantis

Step 3 — GitHub webhook

In your infra repo on GitHub: Settings → Webhooks → Add webhook.

  • Payload URL: https://atlantis.yourcompany.com/events
  • Content type: application/json
  • Secret: same as WEBHOOK_SECRET
  • Events: Pull requests, Pull request review comments, Issue comments, Pushes

Step 4 — atlantis.yaml in your repo

At the root of your infra repo:

yaml
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:
        - run: echo "Production apply — require approval"
        - apply

Step 5 — The workflow in action

  1. You open a PR modifying environments/staging/main.tf
  2. Atlantis posts a comment with terraform plan output
  3. Team reviews the diff — "yes, adding this DNS record looks right"
  4. Commenter types atlantis apply on the PR
  5. Atlantis runs terraform apply, posts results
  6. Merge the PR

For production, require approval:

yaml
projects:
- name: production
  apply_requirements: [approved, mergeable]

Now production apply requires an approving reviewer first.

Step 6 — GitHub Actions for extras

Atlantis handles Terraform. Use GitHub Actions for linting, security scanning, docs:

.github/workflows/ci.yml:

yaml
on: [pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - 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@v4
      - name: Checkov scan
        uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform

  docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: terraform-docs/gh-actions@v1
        with:
          working-dir: .
          output-file: README.md
          output-method: inject
          git-push: "true"

This runs on every PR:

  • terraform fmt check
  • tflint static analysis
  • checkov security scan
  • Auto-update README.md with module docs

Secrets handling in GitOps

Never commit secrets. Two patterns:

1. External secret store (recommended):

Use AWS Secrets Manager or HashiCorp Vault. Your Terraform reads:

hcl
data "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/database/password"
}

resource "aws_rds_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db.secret_string
}

2. SOPS (encrypted files in git):

bash
sops --encrypt --pgp <YOUR_KEY_FINGERPRINT> secrets.yaml > secrets.enc.yaml
# Commit secrets.enc.yaml, not secrets.yaml

Terraform reads via provider carlpett/sops.

Common pitfalls

FAQ

Q Atlantis vs Terraform Cloud / Spacelift?

Atlantis: self-hosted, free, you run it. Terraform Cloud: managed, free tier for <5 users, paid above. Spacelift: powerful, paid. Atlantis wins if you have DIY ops capacity.

Q Do I need a separate VPS for Atlantis?

Not necessarily — can share with other internal tools. But Atlantis needs to be reachable by GitHub webhooks, which means public HTTPS.

Q What's "drift" and how do I detect it?

Drift is when manual changes (someone SSH'd and tweaked a config) diverge from Terraform state. Detect via scheduled terraform plan — any non-empty diff = drift. GitHub Actions weekly cron works well.

Q Can I use Ansible inside this workflow?

Yes — Terraform creates the VPS, Atlantis applies it, then trigger Ansible from GitHub Actions on merge. See our Terraform + Ansible article.

Q How do I rollback a bad apply?

Revert the PR in git + re-apply. Terraform is declarative — reverting the code reverts the infra. Keep an eye on destructive changes (RDS deletion) — review diffs carefully before merging.

Self-host Atlantis on a DomainIndia VPS — full control, zero vendor lock-in. Get a VPS

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket