Infrastructure as Code

Infrastructure as Code on DomainIndia VPS: Terraform and Ansible

By Domain India Team · DomainIndia EngineeringPublished · Updated 6 min read
Knowledge base article
Contents (9 sections)
TL;DR

Replace "SSH in and run commands" with reproducible, version-controlled infrastructure. Terraform provisions servers and resources; Ansible configures them. This guide shows a working stack for a DomainIndia VPS fleet — from zero to running services with a single terraform apply + ansible-playbook.

Why IaC

If you have 1 server, manual is fine. If you have 3+ (staging, production, disaster-recovery) or rebuild servers often, IaC is non-negotiable. Benefits:

  • Every change is a git commit (auditable)
  • Rebuild a lost server in 30 minutes, not 3 days
  • Same config across dev/staging/production — "works on staging" actually means something
  • Team-ready — two engineers don't step on each other

Terraform vs Ansible

ToolRoleDeclarativeBest for
TerraformProvisioning (create VMs, DNS, load balancers)Yes — desired state"What infrastructure exists"
AnsibleConfiguration (install software, copy files, run scripts)Partial — playbooks are imperative"What's installed and configured"

Used together: Terraform creates the VPS, Ansible configures it.

Part 1 — Terraform for DomainIndia

DomainIndia VPS isn't a native Terraform provider, but you can manage DNS, Cloudflare, AWS S3 (backups), and remote VPS state via standard providers. For pure VM provisioning on DomainIndia you can use the null_resource + remote-exec pattern, or Order the VPS manually and manage IPs in Terraform as inputs.

Example main.tf — Cloudflare DNS + AWS S3 backup bucket:

hcl
terraform {
  required_providers {
    cloudflare = { source = "cloudflare/cloudflare", version = "~> 4.0" }
    aws        = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "cloudflare" { api_token = var.cloudflare_token }
provider "aws"        { region = "ap-south-1" }

resource "cloudflare_record" "api" {
  zone_id = var.cloudflare_zone_id
  name    = "api"
  value   = var.api_vps_ip    # DomainIndia VPS IP
  type    = "A"
  proxied = true
  ttl     = 1
}

resource "aws_s3_bucket" "backups" {
  bucket = "yourcompany-vps-backups"
  tags   = { Environment = "production" }
}

resource "aws_s3_bucket_lifecycle_configuration" "backups" {
  bucket = aws_s3_bucket.backups.id
  rule {
    id     = "expire-old"
    status = "Enabled"
    expiration { days = 90 }
  }
}

variables.tf:

hcl
variable "cloudflare_token"   { type = string; sensitive = true }
variable "cloudflare_zone_id" { type = string }
variable "api_vps_ip"         { type = string }

terraform.tfvars (git-ignored):

code
cloudflare_token   = "your-token"
cloudflare_zone_id = "abc123"
api_vps_ip         = "65.109.X.X"

Apply:

bash
terraform init
terraform plan
terraform apply

Part 2 — Ansible for server configuration

Ansible reads a YAML "playbook" and runs steps over SSH. No agent, no daemon — just SSH + Python.

Install Ansible on your laptop:

bash
pip install ansible

Create inventory.ini:

ini
[web]
api.yourcompany.com

[web:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/id_rsa

Create site.yml:

yaml
---
- name: Configure API server
  hosts: web
  become: yes
  vars:
    app_user: goapp
    app_dir: /home/goapp/app
  tasks:
    - name: Install essentials
      package:
        name:
          - nginx
          - certbot
          - python3-certbot-nginx
          - postgresql
          - fail2ban
        state: present

    - name: Create app user
      user:
        name: "{{ app_user }}"
        shell: /bin/bash
        home: "/home/{{ app_user }}"
        create_home: yes

    - name: Deploy nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/conf.d/api.conf
      notify: reload nginx

    - name: Start services
      systemd:
        name: "{{ item }}"
        enabled: yes
        state: started
      loop: [nginx, postgresql, fail2ban]

    - name: Allow HTTP/HTTPS through firewall
      firewalld:
        service: "{{ item }}"
        permanent: yes
        immediate: yes
        state: enabled
      loop: [http, https]

  handlers:
    - name: reload nginx
      systemd: { name: nginx, state: reloaded }

Run:

bash
ansible-playbook -i inventory.ini site.yml

Every step is idempotent — run it 100 times, the end state is the same.

Part 3 — Ansible Vault for secrets

Never commit passwords in plaintext. Ansible Vault encrypts files:

bash
ansible-vault create group_vars/all/secrets.yml
# opens editor, type:
db_password: "s3cr3t!"
api_token: "sk-..."

Reference in playbooks normally: "{{ db_password }}". Run with --ask-vault-pass.

Store the vault password in a file outside git:

bash
ansible-playbook -i inventory.ini site.yml --vault-password-file ~/.vault_pass

Part 4 — Common patterns

Pattern 1: Rolling deployment

yaml
- hosts: web
  serial: 1   # one server at a time
  tasks:
    - name: Drain from load balancer
      uri: { url: "http://lb/drain/{{ inventory_hostname }}", method: POST }
    - name: Deploy new binary
      copy: { src: ./myapp, dest: "{{ app_dir }}/myapp", mode: 0755 }
    - name: Restart service
      systemd: { name: myapp, state: restarted }
    - name: Wait for health
      uri: { url: "http://localhost:8080/health", status_code: 200 }
      retries: 10
      delay: 3
    - name: Add back to load balancer
      uri: { url: "http://lb/activate/{{ inventory_hostname }}", method: POST }

Pattern 2: Disaster recovery dry-run

bash
# Destroy staging and rebuild from scratch
terraform destroy -var-file=staging.tfvars
terraform apply -var-file=staging.tfvars
ansible-playbook -i inventory-staging.ini site.yml

GitOps: CI/CD for IaC

Push changes → GitHub Actions → terraform plan → PR comment with diff → human review → merge → auto-apply.

Example .github/workflows/terraform.yml:

yaml
on: [pull_request, push]
jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan -no-color
        if: github.event_name == 'pull_request'
      - run: terraform apply -auto-approve
        if: github.ref == 'refs/heads/main'

Secrets (TF_VAR_cloudflare_token etc.) go in Repo Settings → Secrets.

Common pitfalls

Committing .tfstate or .tfvars
contains secrets in plaintext. .gitignore them. For team use, remote state in S3 with encryption + locking via DynamoDB.
Ansible "permission denied" on apt/dnf
become: yes in the play or task. Check that your SSH user has sudo without password on target servers.
Terraform "stale state" after manual change
someone SSH'd and changed config. terraform refresh + review diff. Don't modify infrastructure out-of-band.
Playbook works on Ubuntu, fails on AlmaLinux
use package (not apt/dnf) or conditionals with ansible_distribution.

FAQ

Do I need IaC for one VPS?

Not strictly. But writing Ansible playbooks for even a single server is good discipline — you document the setup, can rebuild quickly, and can grow to a fleet later.

Terraform vs Pulumi vs OpenTofu?

OpenTofu is a fork of Terraform (licence dispute), works identically. Pulumi uses real programming languages (TypeScript/Python) instead of HCL. For most teams, Terraform/OpenTofu is the simplest.

Ansible vs Puppet vs Chef?

Ansible has won the config-management space. Puppet/Chef are older, require agents, heavier to operate. For a DomainIndia VPS fleet, stick with Ansible.

Can I manage shared cPanel with IaC?

Limited. cPanel has a WHM API, but Terraform providers are community-maintained. For serious automation, go VPS.

Start your IaC journey with a VPS where you have full root access. Order 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
Terraform and Ansible for DomainIndia VPS — Infrastructure as Code