Ansible is the configuration management tool that turns a bash script into an idempotent, versioned, reviewable playbook, and the playbook is the artifact that the fleet's configuration lives in. The inventory defines the hosts and the groups, the variables define the per-environment differences, and the playbooks define the tasks that each group receives. This article builds the Ansible setup for a server fleet: the inventory structure, the variable hierarchy, the playbook structure, and the deployment patterns.

The inventory structure with groups and variables

The inventory file lists the hosts and assigns them to groups, and the groups are the variable scope and the play's target. The INI-format inventory:

[web]
web01 ansible_host=10.0.1.21
web02 ansible_host=10.0.1.22

[db]
db01 ansible_host=10.0.2.31

[prod:children]
web
db

[prod:vars]
environment=production

[web:vars]
app_port=8080

The groups web and db hold the hosts, the prod group contains both as children, and the variables are set per group. The playbook targets a group and receives the group's variables.

The variable hierarchy and the precedence

Ansible's variable hierarchy determines which value wins when the same variable is defined in multiple places, and the precedence from lowest to highest is: role defaults, inventory group_vars, inventory host_vars, playbook vars, extra vars from the command line. The hierarchy means that the command-line override beats the inventory, which beats the playbook, which beats the role defaults.

The playbook structure and the tasks

The playbook is a YAML file that lists plays, and each play targets a group and runs tasks. The tasks are the modules that Ansible executes: the apt module for packages, the template module for config files, the service module for services, and the copy module for files. Each task is idempotent: running the playbook twice produces the same result as running it once.

The deployment patterns and the idempotency

The deployment pattern that Ansible enables is the one where the configuration is code: the playbooks are versioned, the changes go through pull requests, and the deployment is the playbook run against the fleet. The idempotency means the playbook can be run repeatedly, and the repeated run produces no changes when the state is already correct.

The relationship with the bash scripts in this series

The bash scripts from the earlier articles are the procedures that the playbooks automate, and the migration from script to playbook is the conversion of the imperative steps into declarative tasks. The script that sets up a server by hand is the script that documents the procedure, and the playbook that implements the same procedure is the translation. The playbook is the artifact that the fleet's configuration lives in, and the script is the artifact that the migration started from.

The Ansible playbook is the configuration management artifact that the fleet's infrastructure lives in, and the playbook is the idempotent, versioned and reviewable definition of what each server should look like. The idempotency means the playbook can be run repeatedly, the versioning means the changes go through pull requests, and the reviewability means the changes are visible before they are applied. This article covers the playbook structure, the variable hierarchy, the role organization, and the deployment patterns.

The playbook structure and the task organization

The playbook is a YAML file that lists plays, and each play targets a group of hosts and runs a list of tasks. The tasks are the modules that Ansible executes, and each module is idempotent: the apt module installs packages if they are not installed, the template module writes the config file if the content differs, and the service module starts the service if it is not running. The idempotency is the property that makes the playbook safe to run repeatedly, and the safety is the production requirement.

The task structure uses the module name as the key and the module parameters as the value:

- name: Install required packages
apt:
name:
- nginx
- postgresql
- redis
state: present

- name: Deploy application config
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
mode: 0640
notify: restart myapp

The notify directive triggers the handler, the handler that restarts the service when the config changes, and the handler is the mechanism that prevents the unnecessary restarts.

The role organization and the reusable components

The roles are the reusable playbooks that Ansible organizes into a directory structure, and the role contains the tasks, the handlers, the templates, the files and the variables for a specific service or function. The role structure:

roles/nginx/
tasks/main.yml
handlers/main.yml
templates/nginx.conf.j2
defaults/main.yml

and the role is included in the playbook with the roles directive. The role is the unit of reuse in Ansible, and the reuse prevents the copy-paste duplication.

The Jinja2 templating for config files

The template module uses Jinja2 for rendering config files with variables and logic:

server {
listen {{ app_port }};
server_name {{ inventory_hostname }};
root {{ app_root | default('/srv/app') }};
}

The filters that Jinja2 provides transform the values: default for fallback, upper and lower for case, join for lists.

The handlers and the service management

Handlers are tasks that run only when notified by another task, and the handler prevents unnecessary service restarts:

handlers:
- name: restart nginx
service:
name: nginx
state: restarted

The handler runs only if the template task changed the config file, and the handler runs once at the end of the play even if multiple tasks notify it.

The deployment patterns and rolling updates

The serial directive limits the number of hosts updated simultaneously:

- hosts: web
serial: 1
tasks:
- name: Update application
include_tasks: deploy.yml

The serial 1 updates one host at a time for the rolling update that maintains availability.

The integration with the security articles in this series

The Ansible playbook integrates with the security tools: the fail2ban configuration, the SELinux policy modules, the sudoers configuration and the auditd rules are all managed through Ansible modules.

The checklist for the Ansible deployment

Install Ansible on the management host. Create the inventory with groups and variables. Write the roles for each service. Write the playbooks. Test on staging. Deploy with serial for rolling updates. Monitor the runs through the logs.

The Ansible playbook is the configuration management artifact that turns the fleet's infrastructure into code, and the code is the versioned, reviewable and idempotent definition of the server state. The fleet that manages its configuration through Ansible is the fleet whose servers are reproducible, whose changes are auditable and whose configuration drift is detected and corrected, which is the property that the configuration management exists to provide.

The vault for credential management in playbooks

The Ansible Vault encrypts the sensitive variables that the playbooks contain, and the vault is the mechanism that prevents the database passwords and the API keys from being stored in plain text in the repository. The vault encrypts the file with a password, the password that the operator enters when running the playbook, and the encrypted file is safe to store in the version control.

The vault creation: ansible-vault create secrets.yml, the edit: ansible-vault edit secrets.yml, and the playbook run with the vault password: ansible-playbook --ask-vault-pass playbook.yml. The vault password is the credential that the operator manages, and the vault is the alternative to the external secrets manager for the teams that do not have one.

The vault that encrypts the variables is the security practice that the credential management demands, and the practice is the one that prevents the credential leak through the version control repository. The repository that contains the encrypted variables is the repository that the team can share, and the sharing is the collaboration that the team needs without the credential exposure.

The ansible-vault and the CI pipeline integration

The CI pipeline that runs the Ansible playbooks needs the vault password, and the password is provided through the CI system's secret management: the CI variable that contains the vault password, the ansible-playbook --vault-password-file that reads the password from the file, and the password file that the CI system creates for the run. The password file is deleted after the run, and the deletion is the hygiene that prevents the password from persisting on the CI runner.

The CI pipeline that runs the Ansible playbooks is the automated deployment that the fleet uses, and the pipeline is the one that the ansible-lint tool validates before the run: ansible-lint checks the playbook for the best practices, the naming conventions and the module usage, and the lint is the check that catches the errors before the deployment.

The idempotency testing and the drift detection

The playbook's idempotency is tested by running the playbook twice and confirming that the second run produces no changes: the changed count in the Ansible output should be zero on the second run, and the nonzero changed count on the second run means the playbook is not idempotent. The non-idempotent playbook is the playbook that makes a change every time it runs, and the change that accumulates is the drift that the idempotency was supposed to prevent.

The drift detection is the Ansible check mode, --check, which runs the playbook without making changes and reports what would change: the diff that the check mode produces is the difference between the playbook's expected state and the server's actual state, and the diff is the drift that the configuration management detects. The check mode that runs on a schedule is the drift detection that the fleet's monitoring uses, and the drift that the check detects is the drift that the next playbook run corrects.

The inventory plugins and the dynamic inventory

The inventory that Ansible uses can be static, a file that lists the hosts, or dynamic, a script that queries the infrastructure for the current host list. The dynamic inventory is the script that queries the cloud provider's API or the configuration management database, and the dynamic inventory reflects the current state of the infrastructure rather than a static snapshot.

The dynamic inventory script follows the Ansible inventory plugin interface: the script outputs JSON with the host groups and the variables, and the script is executable and returns the JSON on stdout. The cloud provider inventory plugins, the AWS EC2 plugin and the OpenStack plugin, are the official implementations that query the provider's API, and the inventory that the plugins produce is the fleet's current state.

The check mode and the diff mode

The Ansible check mode, --check, runs the playbook without making changes, and the check mode is the dry-run that shows what the playbook would do. The diff mode, --diff, shows the changes that the playbook would make to the files, and the diff is the before-and-after comparison that the review reads.

The combination of --check and --diff is the preview that the playbook deployment uses: the operator runs the playbook with --check --diff, reads the changes, and if the changes are correct, runs the playbook without --check. The preview is the safety pattern that the config editing with sed uses, the preview that shows the change before applying it.

The closing summary for the Ansible deployment discipline

The Ansible playbook is the configuration management artifact that the fleet's infrastructure lives in, and the playbook is the idempotent, versioned and reviewable definition of what each server should look like. The inventory defines the hosts, the roles define the reusable components, the variables define the per-environment differences, and the playbooks define the tasks that each group receives.

The deployment that Ansible provides is the rolling update that maintains service availability, the serial directive that limits the blast radius, and the check mode that previews the changes. The integration with the other tools in this series is the complete infrastructure management: the bastion for the access, the auditd for the audit trail, the backup for the recovery, and the Ansible for the configuration. The fleet that manages its configuration through Ansible is the fleet whose servers are reproducible, whose changes are auditable and whose configuration drift is detected and corrected, which is the property that the configuration management exists to provide.

The investment that Ansible requires is the learning curve for the YAML syntax, the module vocabulary and the role organization, and the return on the investment is the configuration management that prevents the configuration drift, the snowflake servers and the undocumented manual changes. The fleet that runs Ansible is the fleet that the next administrator can understand, because the playbook is the documentation, and the documentation is the code.