Terraform creates infrastructure (servers, networks, databases). But once those servers exist, who installs software, copies config files, and starts services? That’s where Ansible comes in.
Ansible is a configuration management tool. It connects to our servers over SSH, runs tasks we define, and makes sure everything is set up the way we want. The best part — it’s agentless. We don’t need to install anything on the target machines. Just SSH access.
Ansible vs Terraform
These two are not competitors — they’re teammates.
Think of it like building a house: Terraform lays the foundation and builds the walls. Ansible paints, installs furniture, and sets up the Wi-Fi.
Inventory
The inventory is a file that lists the servers Ansible should manage:
# inventory.yml
all:
children:
webservers:
hosts:
web1:
ansible_host: 10.0.1.10
web2:
ansible_host: 10.0.1.11
databases:
hosts:
db1:
ansible_host: 10.0.2.10
We group hosts so we can target them separately — “install nginx on webservers” or “update postgres on databases.”
Playbooks
A playbook is a YAML file that describes a series of tasks to run on specific hosts:
# setup-web.yml
- name: Setup web servers
hosts: webservers
become: true # run as root (sudo)
tasks:
- name: Update apt cache
apt:
update_cache: true
- name: Install nginx
apt:
name: nginx
state: present # make sure it's installed
- name: Copy our nginx config
copy:
src: ./files/nginx.conf
dest: /etc/nginx/nginx.conf
notify: restart nginx # trigger handler if file changed
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: true # start on boot
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
We run it with:
ansible-playbook -i inventory.yml setup-web.yml
Modules
Each task uses a module — a built-in action that Ansible knows how to perform. We used apt, copy, and service above. There are thousands of modules:
apt/yum— install packagescopy— copy files to remote hoststemplate— copy files with variable substitution (Jinja2)service— start/stop/restart servicesuser— create/manage system usersdocker_container— manage Docker containersgit— clone repositories
Idempotency in Practice
Ansible modules are designed to be idempotent. If we say state: present for nginx and nginx is already installed, Ansible does nothing. If we say state: started for a service and it’s already running, nothing happens.
This means we can safely re-run a playbook multiple times. The output even shows us: ok (already good), changed (made a change), or failed.
Roles
As our playbooks grow, dumping everything into one file gets messy. Roles organize tasks, files, templates, and variables into a standard structure:
roles/
nginx/
tasks/main.yml # the task list
handlers/main.yml # handlers (restart, reload)
templates/ # Jinja2 template files
files/ # static files to copy
defaults/main.yml # default variable values
Then our playbook becomes clean:
- name: Setup web servers
hosts: webservers
become: true
roles:
- nginx
- certbot
- app-deploy
Each role is self-contained and reusable. We can share roles via Ansible Galaxy, which is like npm but for Ansible roles.
In simple language, Ansible is the tool that SSHs into our servers and makes sure they’re configured exactly the way we described. No agents to install, no state to manage — just YAML files and SSH.