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.
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?
| Tool | Role | Approach | Answers the question |
|---|---|---|---|
| Terraform / OpenTofu | Provisioning: DNS, buckets, cloud resources, VMs where a provider exists | Declarative desired state | What infrastructure exists? |
| Ansible | Configuration: packages, files, users, services | Ordered tasks, mostly idempotent modules | What 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:
- Order the VPS in the Domain India client area.
- Pass its IP address to Terraform as an input variable.
- 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:
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:
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):
cloudflare_token = "your-token"
cloudflare_zone_id = "your-zone-id"
api_vps_ip = "203.0.113.10"Apply:
terraform init
terraform plan
terraform applyIf 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:
pipx install --include-deps ansibleCreate inventory.ini:
[web]
api.yourcompany.com
[web:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/id_ed25519Create site.yml (for AlmaLinux; certbot and fail2ban come from EPEL):
---
- 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: reloadedRun it:
ansible-playbook -i inventory.ini site.ymlThe 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:
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:
ansible-playbook -i inventory.ini site.yml --vault-password-file ~/.vault_pass6. Common patterns
Rolling deployment, one server at a time:
- 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:
# .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
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.
Full root access on a self-managed VPS, ready for your Terraform and Ansible workflow.
Order a VPS