Infrastructure as Code

GitOps: Terraform + Atlantis + GitHub Actions for Domain India Infrastructure

By Domain India Team · DomainIndia EngineeringPublished 10 min read
Knowledge base article
Contents (15 sections)

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.

Key takeaways

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 apply on 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 plan on 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

ToolRole
Terraform / OpenTofuInfrastructure definition (HCL files)
GitHub (or GitLab)Code storage and PR review
AtlantisThe GitOps bot: runs plan/apply from PR comments
S3-compatible bucket (AWS S3 or similar)Remote, versioned, encrypted state with locking
GitHub ActionsFormatting, 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:

hcl
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:

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_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

bash
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/atlantis

Atlantis 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):

code
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:

ini
[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.target

Put 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:

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

Step 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:

yaml
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:

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:
        - apply

Step 6 — The workflow in action

  1. You open a PR modifying environments/staging/main.tf
  2. Atlantis posts a comment with the terraform plan output
  3. The team reviews the plan: "yes, adding this DNS record looks right"
  4. Someone comments atlantis apply on the PR
  5. Atlantis runs terraform apply and posts the result
  6. 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:

yaml
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 fmt check
  • tflint static analysis
  • checkov security scan
  • Auto-update of 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, and read secrets at plan time:

hcl
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):

bash
sops --encrypt --age age1yourpublickey... secrets.yaml > secrets.enc.yaml
# Commit secrets.enc.yaml, never secrets.yaml

Terraform 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.yaml and 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

Apply without approval
Without apply requirements, anyone with write access can comment atlantis apply. Set apply_requirements in the server-side repos.yaml.
Webhook not reaching Atlantis
The firewall blocks 443 or nginx isn't proxying /events. Check GitHub's "Recent Deliveries" for the webhook to see the exact response.
State lock stuck
A previous run crashed. Use terraform force-unlock LOCK_ID carefully, only when you're sure nothing is running.
Version drift
A laptop and Atlantis use different Terraform versions. Pin required_version and set terraform_version in atlantis.yaml.
Atlantis disk fills
Each open PR keeps a clone in the data directory. Close stale PRs and watch disk usage on the VPS.
Exposed state
State contains secrets. Keep 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-host Atlantis on a VPS

Self-managed KVM VPS with full root access and NVMe storage, from ₹553 a month excluding GST.

Get a VPS

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app