Infrastructure as code · Intermediate
Idempotent server configuration with Ansible
Why it matters
Terraform creates infrastructure; something still has to configure what runs on virtual machines: directories, config files, packages, services. Ansible is the most common tool for this and appeared in about a quarter of the DevOps job descriptions we read. The core idea interviewers check is idempotency: running the same playbook again should change nothing.
The concept
An inventory lists hosts and groups.
A playbook maps groups of hosts to tasks, each calling a module (
file,copy,template,apt,systemd,lineinfile).Modules are idempotent: they check the current state and act only if it differs, reporting
okorchanged.Handlers run only when notified by a task that changed, for example "restart the app if its config changed".
Ansible is agentless: it connects over SSH (or locally) and runs modules.
Worked example: an application host baseline
[web]
localhost ansible_connection=local- name: Baseline for application hosts
hosts: web
become: false
vars:
app_dir: /tmp/ansible-demo/app
tasks:
- name: Create the application directory
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
mode: "0750"
- name: Write the application config
ansible.builtin.copy:
dest: "{{ app_dir }}/app.env"
mode: "0640"
content: |
APP_ENV=production
LOG_LEVEL=info
PORT=8000
notify: Restart app
- name: Ensure log rotation keeps 14 days
ansible.builtin.lineinfile:
path: "{{ app_dir }}/logrotate.conf"
create: true
mode: "0644"
regexp: '^rotate '
line: 'rotate 14'
handlers:
- name: Restart app
ansible.builtin.debug:
msg: "Config changed: the app would be restarted here (systemd service on a real host)"python -m venv .venv && source .venv/bin/activate && pip install ansible-core
ansible-playbook -i inventory.ini site.yml # first run
ansible-playbook -i inventory.ini site.yml # second runfirst run: localhost : ok=5 changed=3 unreachable=0 failed=0
second run: localhost : ok=4 changed=0 unreachable=0 failed=0The first run creates the directory and both files and runs the handler. The second run finds everything already correct: nothing changes and the handler does not run, so the app is not restarted needlessly. On a real server you would use ansible.builtin.template with a Jinja2 file, become: true to install packages, and ansible.builtin.systemd in the handler.
Common mistakes
Using
shellorcommandfor everything. They always report "changed" and are not idempotent. Use the purpose-built module.Secrets in plain text in playbooks. Use Ansible Vault or a secrets manager lookup.
Restarting services in tasks instead of handlers. Every run then restarts production.
Snowflake servers. If you fix a server by hand, the next run may undo it, or worse, not know about it. Change the playbook.
Interview angle
"What does idempotent mean in configuration management, and why does it matter?" Show it with the two runs above. Then: "Terraform or Ansible?" Terraform for provisioning cloud resources with state; Ansible for configuring what runs inside machines; many teams use both, and containers reduce the need for the second.
Practice task
Change
LOG_LEVELtodebugand confirm only that task and the handler run.Replace the
copytask with atemplatetask and atemplates/app.env.j2file using variables.Add a task with
ansible.builtin.commandand make it idempotent withcreates:.