Client Area

GitOps: Terraform + Atlantis + GitHub Actions for DomainIndia Infrastructure

ByDomain India Team·DomainIndia Engineering
6 min read24 Apr 20263 views
# 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](https://domainindia.com/support/kb/terraform-ansible-infrastructure-as-code-vps)) - 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](https://domainindia.com/support/kb/running-go-applications-vps-systemd-reverse-proxy) — 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 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

Still need help?

Our support team can assist you directly.

Submit Ticket