Infrastructure as Code

Infrastructure as Code on Domain India VPS: Terraform and Ansible

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

Infrastructure as code (IaC) replaces "SSH in and run commands by hand" with version-controlled files you can review, repeat and roll back. This guide shows how Terraform and Ansible fit together for a small fleet of Domain India VPS servers: Terraform manages DNS and cloud resources around your servers, and Ansible configures the servers themselves.

Key takeaways

Terraform declares infrastructure (DNS records, storage buckets, cloud resources); Ansible configures servers over SSH (packages, users, config files, services). Domain India has no Terraform provider or public VPS API, so order the VPS in the client area, pass its IP address to Terraform as a variable, and let Ansible do everything on the server. Keep secrets in Ansible Vault and Terraform state out of Git.

1. Why infrastructure as code

With one server, careful manual work is fine. With three or more (staging, production, a spare), or if you rebuild servers often, IaC pays for itself:

  • Every change is a Git commit you can review and audit
  • A lost server can be rebuilt from the playbook in a short, predictable time
  • Staging and production are configured the same way
  • Several people can work on the infrastructure without overwriting each other

2. Terraform or Ansible?

ToolRoleApproachAnswers the question
Terraform / OpenTofuProvisioning: DNS, buckets, cloud resources, VMs where a provider existsDeclarative desired stateWhat infrastructure exists?
AnsibleConfiguration: packages, files, users, servicesOrdered tasks, mostly idempotent modulesWhat is installed and how is it configured?

They work well together: Terraform sets up what surrounds the server, Ansible configures the server.

3. Terraform around a Domain India VPS

Domain India does not publish a Terraform provider or a public API for creating VPS servers. The practical pattern is:

  1. Order the VPS in the Domain India client area.
  2. Pass its IP address to Terraform as an input variable.
  3. Use standard providers for everything around it: DNS at Cloudflare, off-site backup storage, monitoring.

Off-site backup storage is especially worth managing this way, because Domain India VPS plans don't include backups or snapshots.

Example main.tf with Cloudflare DNS and an S3 bucket for backups:

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

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

resource "cloudflare_dns_record" "api" {
  zone_id = var.cloudflare_zone_id
  name    = "api.yourcompany.com"
  content = var.api_vps_ip    # your VPS IP address
  type    = "A"
  proxied = true
  ttl     = 1                 # 1 = automatic
}

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"
    filter {}
    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 (keep it out of Git):

code
cloudflare_token   = "your-token"
cloudflare_zone_id = "your-zone-id"
api_vps_ip         = "203.0.113.10"

Apply:

bash
terraform init
terraform plan
terraform apply

If you point the domain's DNS at Cloudflare this way, change the domain's nameservers to Cloudflare's in the Domain India client area first; see how to change your nameservers.

4. Ansible for server configuration

Ansible reads YAML playbooks and runs them over SSH. There is no agent on the server; it needs only SSH and Python, which every Linux VPS has.

Install Ansible on your computer:

bash
pipx install --include-deps ansible

Create inventory.ini:

ini
[web]
api.yourcompany.com

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

Create site.yml (for AlmaLinux; certbot and fail2ban come from EPEL):

yaml
---
- name: Configure API server
  hosts: web
  become: true
  vars:
    app_user: goapp
  tasks:
    - name: Enable EPEL
      ansible.builtin.dnf:
        name: epel-release
        state: present

    - name: Install essentials
      ansible.builtin.package:
        name:
          - nginx
          - certbot
          - python3-certbot-nginx
          - fail2ban
          - firewalld
        state: present

    - name: Create app user
      ansible.builtin.user:
        name: "{{ app_user }}"
        shell: /bin/bash
        create_home: true

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

    - name: Start services
      ansible.builtin.systemd_service:
        name: "{{ item }}"
        enabled: true
        state: started
      loop: [nginx, fail2ban, firewalld]

    - name: Allow HTTP and HTTPS through the firewall
      ansible.posix.firewalld:
        service: "{{ item }}"
        permanent: true
        immediate: true
        state: enabled
      loop: [http, https]

  handlers:
    - name: reload nginx
      ansible.builtin.systemd_service:
        name: nginx
        state: reloaded

Run it:

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

The modules are idempotent: run the playbook again and nothing changes unless the server has drifted from what the playbook describes. Add --check --diff to preview changes first.

5. Ansible Vault for secrets

Never commit passwords in plain text. Ansible Vault encrypts files:

bash
ansible-vault create group_vars/all/secrets.yml
# In the editor that opens:
db_password: "a-long-random-password"
api_token: "your-api-token"

Use the variables as normal ("{{ db_password }}") and run with --ask-vault-pass, or keep the vault password in a file outside Git:

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

6. Common patterns

Rolling deployment, one server at a time:

yaml
- hosts: web
  become: true
  serial: 1
  tasks:
    - name: Drain from load balancer
      ansible.builtin.uri: { url: "http://lb.internal/drain/{{ inventory_hostname }}", method: POST }
    - name: Deploy new binary
      ansible.builtin.copy: { src: ./myapp, dest: /opt/myapp/myapp, mode: "0755" }
    - name: Restart service
      ansible.builtin.systemd_service: { name: myapp, state: restarted }
    - name: Wait for health
      ansible.builtin.uri: { url: "http://localhost:8080/health", status_code: 200 }
      register: health
      until: health.status == 200
      retries: 10
      delay: 3
    - name: Add back to load balancer
      ansible.builtin.uri: { url: "http://lb.internal/activate/{{ inventory_hostname }}", method: POST }

Rebuild drill: reinstall a staging VPS (open a ticket for an OS reinstall), then run the playbook against it. If the playbook alone brings the service back, your documentation is complete.

7. CI/CD for infrastructure

Push changes, have CI run terraform plan on pull requests for review, and apply only after merge to the main branch:

yaml
# .github/workflows/terraform.yml
on:
  pull_request:
  push:
    branches: [main]

jobs:
  terraform:
    runs-on: ubuntu-latest
    env:
      TF_VAR_cloudflare_token: ${{ secrets.CLOUDFLARE_TOKEN }}
      TF_VAR_cloudflare_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
      TF_VAR_api_vps_ip: ${{ secrets.API_VPS_IP }}
    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.event_name == 'push' && github.ref == 'refs/heads/main'

This needs remote state (next section); state stored on a CI runner is lost after every run. Add AWS credentials as secrets too if you manage AWS resources.

8. Common pitfalls

Committing .tfstate or .tfvars
Both can hold secrets in plain text. Add them to .gitignore. For teams, use remote state, for example S3 with encryption and use_lockfile = true for locking.
Ansible permission denied on dnf or apt
Add become: true to the play or task, and give the SSH user sudo rights on the server.
Drift after manual changes
Someone changed the server or DNS by hand. Run terraform plan -refresh-only to see the difference, then decide whether to keep or revert it.
Playbook works on Ubuntu, fails on AlmaLinux
Package names differ. Use the generic package module with variables per distribution, or conditions on ansible_distribution.

9. Running IaC with Domain India

  • VPS: self-managed with full root access on KVM, so Terraform and Ansible work as described. Plans start from ₹553 a month, excluding 18% GST (Domain India list price on 30 September 2026). There is no VPS page, console or API in the client area: power actions beyond a reboot from inside the server, OS reinstalls and resizes are done by support through a ticket. No backups or snapshots are included, which is why the backup bucket above matters.
  • Shared hosting: you don't manage the server, so Ansible has little to do there. For scripted changes to a cPanel account, use cPanel's UAPI on port 2083 with an API token you create in your own cPanel; WHM is not available to shared accounts.
  • App Platform: deploys come from GitHub (Deploy Now) or with a deploy token, which you can call from CI.
Do I need IaC for one VPS?

Not strictly. But even for one server, an Ansible playbook documents the setup, lets you rebuild quickly after a failure, and grows with you when you add servers.

Terraform, OpenTofu or Pulumi?

OpenTofu is an open-source fork of Terraform created after HashiCorp changed Terraform's licence in 2023, and it remains largely compatible. Pulumi lets you write infrastructure in TypeScript, Python or Go instead of HCL. For most small teams, Terraform or OpenTofu is the simplest choice.

Ansible, Puppet or Chef?

Ansible is agentless and needs only SSH, which makes it the easiest to start with for a small VPS fleet. Puppet and Chef use agents and suit large, long-lived estates.

Can Terraform create a Domain India VPS?

No. Domain India has no Terraform provider or public VPS API. Order the VPS in the client area, then pass its IP address to Terraform as a variable and configure it with Ansible.

Can I manage shared cPanel hosting with IaC?

Only in a limited way. Your own cPanel account has the UAPI (port 2083, with an API token you create), which scripts can call. WHM is not available to shared accounts. For full automation, use a VPS.

Does Domain India back up my VPS?

No. VPS plans are self-managed and include no backups or snapshots. Automate your own backups, for example with Ansible-managed cron jobs that copy dumps to off-site storage.

Ready to automate? Start with a VPS, and open a support ticket for anything the client area doesn't do, such as an OS reinstall.

Automate your servers

Full root access on a self-managed VPS, ready for your Terraform and Ansible workflow.

Order 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