Ansible Automation Handbook 2025 SCC
Ansible Automation Handbook 2025 SCC
---
## Table of Contents
1. [Basic Concepts](#basic-concepts)
- [1.1 What Is Ansible?](#11-what-is-ansible)
- [1.2 Why Use Ansible?](#12-why-use-ansible)
- [1.3 Installing Ansible and Setting Up Your First Project](#13-installing-
ansible-and-setting-up-your-first-project)
- [1.4 Understanding YAML Syntax](#14-understanding-yaml-syntax)
- [1.5 Inventory Basics](#15-inventory-basics)
- [1.6 Your First Playbook](#16-your-first-playbook)
- [1.7 Mini-Project: Automate Package Installation](#17-mini-project-
automate-package-installation)
2. [Intermediate Topics](#intermediate-topics)
- [2.1 Working with Variables](#21-working-with-variables)
- [2.2 Templating with Jinja2](#22-templating-with-jinja2)
- [2.3 Organising Code into Roles](#23-organising-code-into-roles)
- [2.4 Commonly Used Modules](#24-commonly-used-modules)
- [2.5 Managing Inventory: Hosts, Groups and Patterns](#25-managing-
inventory-hosts-groups-and-patterns)
- [2.6 Best Practices for Playbooks](#26-best-practices-for-playbooks)
- [2.7 Mini-Project: Deploying a Multi-Tier Application](#27-mini-project-
deploying-a-multi-tier-application)
3. [Advanced Automation](#advanced-automation)
- [3.1 Dynamic Inventory](#31-dynamic-inventory)
- [3.2 Protecting Secrets with Ansible Vault](#32-protecting-secrets-with-
ansible-vault)
- [3.3 Developing Custom Modules](#33-developing-custom-modules)
- [3.4 Performance Optimisation](#34-performance-optimisation)
- [3.5 Integrating Ansible into CI/CD Pipelines](#35-integrating-ansible-
into-cicd-pipelines)
- [3.6 Event-Driven Ansible and AI-Assisted Automation](#36-event-driven-
ansible-and-ai-assisted-automation)
- [3.7 Mini-Project: Cloud Provisioning with Secrets and CI/CD](#37-mini-
project-cloud-provisioning-with-secrets-and-cicd)
4. [Appendix](#appendix)
- [A. Ansible 2.16 Highlights](#a-ansible-216-highlights)
- [B. Further Reading and Resources](#b-further-reading-and-resources)
---
## Basic Concepts
*Ansible* allows you to define how systems **should** look and automatically
enforce that state. Whether you need to install packages, manage users, deploy
applications or orchestrate entire environments, you write declarative
*playbooks* that contain tasks and Ansible ensures the target machines reach the
desired configuration. Unlike other automation tools that rely on a persistent
agent, Ansible connects over SSH, executes modules on remote machines and
disconnects. This reduces overhead and security risks because nothing is
permanently installed on the managed nodes【468687567525964†L109-L149】.
* **Control node** – the machine where you run the Ansible CLI tools. It sends
commands to your infrastructure but does not need to be a dedicated server
【447534196468510†L144-L190】.
* **Managed nodes** – the hosts (servers, network devices, containers, cloud
instances) you manage. Ansible communicates with them over SSH or another
transport【447534196468510†L144-L190】.
* **Inventory** – a file listing the managed nodes and grouping them. The
inventory can be static (INI/YAML) or dynamic (fetched from a cloud provider)
【447534196468510†L144-L190】.
* **Playbook** – a YAML document containing one or more *plays*. Each play maps
a group of hosts to a series of *tasks*. Tasks invoke *modules* to perform
actions like installing packages or copying files【514170298441643†L109-L176】.
* **Modules** – discrete units of code shipped with Ansible or written by you.
Modules perform specific functions (package management, file operations, service
control, etc.)【514170298441643†L109-L176】.
* **Roles and collections** – mechanisms for organising and distributing
reusable automation code (we will explore them later)【447534196468510†L144-
L190】.
```text
┌───────────────────────┐
│ Control Node │
│ (ansible-playbook) │
└─────────┬─────────────┘
│ SSH/WinRM
┌───────────┴───────────┐
│ │
┌─────▼─────┐ ┌─────▼─────┐
│ Managed │ │ Managed │
│ Node 1 │ ... │ Node N │
└───────────┘ └───────────┘
```
The control node reads your inventory, connects to the target hosts, runs the
tasks defined in the playbook and reports the results back to you. Because
Ansible uses SSH by default, you benefit from your existing authentication
mechanisms and can leverage features like SSH key forwarding and bastion hosts.
```bash
python3 -m pip install --user ansible
```
```bash
mkdir ansible_quickstart
cd ansible_quickstart
```
Inside this directory you will store your inventory file, playbooks and any
additional files. The *Start automating with Ansible* guide suggests this
structure as a minimal starting point【222138213786001†L109-L148】.
Create an inventory file named `inventory.ini` (INI format) and add hosts under
a group:
```ini
[webservers]
192.0.2.10
192.0.2.11
[dbservers]
192.0.2.20
```
Each group name is enclosed in square brackets. You can define multiple groups
or nested groups. To verify the inventory and see how Ansible interprets it,
run:
```bash
ansible-inventory --list -i inventory.ini
```
```bash
ansible all -m ping -i inventory.ini
```
Ansible will attempt an SSH connection to each host. Ensure your SSH keys are
installed on the managed hosts and that the control node can reach them
【174484689979159†L113-L179】.
```yaml
---
# A YAML document
name: Example
hosts:
- server1
- server2
vars:
package: httpd
settings:
port: 80
ssl: false
```
Ansible playbooks start with three dashes (`---`) to denote the beginning of a
YAML document. Playbooks are lists of plays; each play has keys like `name`,
`hosts`, `tasks`, `vars` and so on. Tasks are themselves a list where each item
calls a module. Here is an annotated example:
```yaml
---
- name: Install Apache on web servers
hosts: webservers
become: true
vars:
http_port: 80
tasks:
- name: Install the httpd package
dnf:
name: httpd
state: present
In this play:
The INI format is widely used because it is simple and supports grouping hosts.
However, Ansible also supports YAML inventory files. Here is the same inventory
expressed in YAML:
```yaml
all:
children:
webservers:
hosts:
192.0.2.10:
192.0.2.11:
dbservers:
hosts:
192.0.2.20:
```
You can choose whichever format you prefer. To use a YAML inventory, specify
its path with `-i inventory.yaml` when running Ansible commands. Remember to
add your SSH public key to managed hosts so that Ansible can connect to them
【174484689979159†L113-L179】.
```yaml
---
- name: Say hello
hosts: all
tasks:
- name: Print a friendly message
debug:
msg: "Hello from {{ inventory_hostname }}!"
```
Here we use the `debug` module to print a message. `{{ inventory_hostname }}`
is a built-in variable representing the name of each host. Run the playbook
with:
```bash
ansible-playbook -i inventory.ini hello.yml
```
The output will show each host and the printed message. This example
demonstrates the structure of a playbook: a list of plays, each containing
tasks, and tasks invoking modules to perform work【514170298441643†L109-L176】.
```yaml
---
- name: Install and configure Nginx on web servers
hosts: webservers
become: true
tasks:
- name: Install Nginx package
package:
name: nginx
state: present
```bash
ansible-playbook -i inventory.ini install_nginx.yml
```
This mini-project uses a few modules: `package` to install software, `service`
to manage services, `copy` to deploy a file and `firewalld` to open a port. It
also uses a conditional (`when`) to run the firewall task only on Red Hat-family
systems. Congratulations – you have automated the installation of a web
service!
---
## Intermediate Topics
In this section we build on the basics and explore Ansible’s features for
writing flexible, reusable automation. We will work with variables, templates,
roles, common modules and inventory patterns. You will also learn best
practices that make playbooks maintainable and safe.
Variables enable you to manage differences between systems. They can hold
strings, lists, dictionaries, integers and other data types. You can define
variables in many places: in playbooks, inventories, external files, roles or
even at the command line. After definition you can use them in module
arguments, conditions, loops, templates and more【98624939005778†L145-L160】.
```yaml
- hosts: webservers
vars:
app_port: 8080
tasks:
- name: Show the port number
debug:
msg: "The application will listen on port {{ app_port }}"
```
```ini
[webservers]
192.0.2.10 app_port=8080
192.0.2.11 app_port=8081
[webservers:vars]
package=nginx
```
```yaml
- hosts: all
vars_files:
- vars/common.yml
tasks: ...
```
```yaml
- hosts: localhost
tasks:
- name: Run a command
command: uptime
register: uptime_result
- debug:
var: uptime_result.stdout
```
Variables are referenced with the Jinja2 `{{ }}` syntax. You can perform
operations, call filters and use conditional logic. For example:
```yaml
msg: "Current time is {{ ansible_date_time.time }}"
user_list: "{{ some_list | join(', ') }}"
```
When writing tasks, avoid hard-coding values. Place them in variables so they
can be overridden via inventories or command-line extra vars (`-e`).
Suppose you want to generate an Nginx configuration file with a variable port.
Create a template file under a directory called `templates` and name it
`nginx.conf.j2`:
```nginx
server {
listen {{ app_port }};
server_name {{ inventory_hostname }};
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
```
```yaml
- hosts: webservers
vars:
app_port: 8080
tasks:
- name: Deploy Nginx configuration from template
template:
src: templates/nginx.conf.j2
dest: /etc/nginx/conf.d/default.conf
mode: '0644'
```
When the playbook runs, Ansible reads the Jinja2 template, substitutes variables
and writes the resulting file to the destination on each target host.
### 2.3 Organising Code into Roles
As your automation grows, you will want to reuse code and share it with others.
**Roles** provide a standard way to package tasks, variables, files, templates
and handlers into a self-contained directory structure. According to best
practices, roles make collaboration easier and help keep your playbooks clean
【647884764329840†L121-L190】.
```bash
ansible-galaxy init myrole
```
```text
myrole/
├── defaults/ # Default variables (lowest priority)
│ └── main.yml
├── files/ # Static files to copy
├── handlers/ # Handlers triggered by notify
│ └── main.yml
├── meta/ # Role metadata (dependencies)
│ └── main.yml
├── tasks/ # Main list of tasks
│ └── main.yml
├── templates/ # Jinja2 templates
├── tests/ # Test inventory and playbook
└── vars/ # Variables with higher priority
└── main.yml
```
```yaml
- hosts: webservers
roles:
- myrole
```
Each role can include its own tasks and can use variables defined in
`defaults/main.yml` or `vars/main.yml`. Roles can also depend on other roles;
dependencies are defined in `meta/main.yml`. This approach encourages modular
automation and allows you to publish your roles on Ansible Galaxy for others to
reuse.
```text
myrole/
├── defaults/
│ └── main.yml
├── files/
├── handlers/
│ └── main.yml
├── meta/
│ └── main.yml
├── tasks/
│ └── main.yml
├── templates/
├── tests/
└── vars/
└── main.yml
```
While these modules cover common tasks, you will encounter many others as you
automate different technologies. Always consult the [module
index](https://docs.ansible.com/ansible/latest/modules/modules_by_category.html)
for details.
When your environment changes rapidly – for example, instances created and
destroyed in the cloud – static inventories become cumbersome. In the advanced
section we will explore dynamic inventory plugins that fetch host lists from
external sources like AWS, Azure, VMware, etc.
* **Idempotence matters** – ensure your tasks can be run multiple times without
changing anything when the state is already correct. Use modules instead of
`shell` whenever possible.
* **Use roles and include_tasks** – break your automation into reusable roles
and include files. Avoid monolithic playbooks.
* **Separate variables** – store variables in `vars` or `defaults` files, not
embedded in tasks. This makes your code easier to override.
* **Tag tasks** – assign `tags` to tasks so you can run specific portions of a
playbook with `--tags` or skip others with `--skip-tags`.
* **Use check mode and diff** – run playbooks with `--check` to perform a dry
run and `--diff` to see what will change before applying it.
* **Limit hosts for testing** – use `--limit` to target a subset of hosts during
development. This reduces blast radius.
* **Employ rolling updates** – use the `serial` keyword to control how many
hosts are updated at a time (e.g., `serial: 2` will update two hosts
concurrently). This minimises downtime during deployments
【807064649843584†L515-L550】.
* **Enforce security** – avoid logging secrets, use Ansible Vault for sensitive
data and restrict privilege escalation【807064649843584†L556-L590】.
1. **Create roles**:
```bash
ansible-galaxy init webserver
ansible-galaxy init database
```
* Edit `webserver/tasks/main.yml`:
```yaml
---
- name: Install Nginx
package:
name: nginx
state: present
* Edit `database/tasks/main.yml`:
```yaml
---
- name: Install MariaDB server
package:
name: mariadb-server
state: present
Create `group_vars/webservers.yml`:
```yaml
---
db_host: 192.0.2.20
```
Create `group_vars/dbservers.yml`:
```yaml
---
db_password: strongpassword
```
- role: webserver
when: inventory_hostname in groups['webservers']
```
```bash
ansible-playbook -i inventory.ini site.yml
```
---
## Advanced Automation
In the advanced section we move beyond static inventories and basic playbooks.
You will learn how to fetch hosts dynamically, secure secrets with
Ansible Vault, write your own modules, tune performance, integrate with CI/CD
and explore new capabilities like event-driven automation and generative AI.
Static inventory files do not scale well when infrastructure is created and
destroyed dynamically. In cloud environments you want Ansible to pull the
latest list of hosts from your provider. Ansible achieves this through
**inventory plugins** and scripts. A dynamic inventory plugin runs before your
playbook and queries an external source (AWS, Azure, VMware, etc.) to build the
host list and group variables【614492783535894†L122-L145】.
The official documentation recommends using plugins rather than scripts because
plugins support caching and variable composition【933224521356787†L140-L153】.
Many plugins are built in: `aws_ec2`, `azure_rm`, `gcp_compute`, `openstack`,
`kubernetes`, `docker_swarm` and more. You enable a plugin by creating a YAML
file ending in `.yaml` and specifying the plugin name and options. For example,
an AWS EC2 dynamic inventory file (`aws_ec2.yaml`):
```yaml
plugin: aws_ec2
regions:
- us-east-1
keyed_groups:
- key: tags.Role
prefix: ''
filters:
instance-state-name: running
```
Running `ansible-inventory -i aws_ec2.yaml --graph` will query AWS and show the
hosts grouped by the `Role` tag. If you use Red Hat Ansible Automation Platform
(AAP) or AWX, you can define dynamic inventory sources via the web UI and
schedule synchronisations【437227952386536†L122-L146】.
#### Implementing a Custom Inventory Plugin
If no built-in plugin matches your needs, you can write your own. A custom
plugin inherits from `BaseInventoryPlugin`, `Constructable` and `Cacheable` and
implements `parse()` and `verify_file()` methods【933224521356787†L221-L237】.
Although beyond the scope of this book, the plugin interface lets you integrate
with proprietary CMDBs or API endpoints.
```text
┌─────────────┐
│ Playbook │
└──────┬──────┘
│
│ call inventory
▼
┌────────────────────┐
│ Inventory Plugin │
│ (e.g., aws_ec2) │
└──────┬─────────────┘
│ queries API
▼
┌────────────────────┐
│ Cloud/CMDB API │
└────────────────────┘
```
```bash
ansible-vault encrypt_string --vault-id dev@prompt 'supersecret' --name
db_password
```
```yaml
db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;dev
643465333765323631396133306237663963353964303036333761663731623036
3530666338636134396337333438663730363866653664370a3365336536346339
...
```
You can paste this into your variable file or playbook. At runtime, provide the
vault password using `--vault-id dev@prompt` or a password file. Encrypting
variables keeps the rest of the file readable【970125717263838†L125-L154】.
If the file does not exist, `ansible-vault create secrets.yml` opens an editor
where you can add content. To view or edit encrypted files, use `ansible-vault
view` and `ansible-vault edit`. You can change the vault password with
`ansible-vault rekey`. File-level encryption makes secrets easier to manage at
the cost of making the whole file unreadable in version control
【407927793429946†L205-L220】【407927793429946†L337-L449】.
* Use separate vault IDs for different environments (e.g., `dev`, `prod`).
* Store vault passwords in a secure storage system (HashiCorp Vault, AWS Secrets
Manager, etc.) rather than in your repository【407927793429946†L280-L331】.
* Encrypt only what is secret (use variable encryption for values, file
encryption for large sets of secrets)【970125717263838†L125-L154】.
* Use CI/CD tools to inject vault passwords through environment variables
instead of prompting interactively.
Ansible’s built-in modules cover many use cases, but you may need to automate
tasks that are not supported out of the box. You can write a custom module in
Python. Place your module in a `library` directory next to your playbook. The
module must accept arguments, perform an action and return a JSON structure
describing the result. The development guide outlines the process
【417607270033536†L195-L229】:
```python
#!/usr/bin/python
from ansible.module_utils.basic import AnsibleModule
DOCUMENTATION = r'''
---
module: my_service
short_description: Manage a fictitious service
version_added: "1.0"
description: This module starts or stops a fictitious service.
options:
name:
description: Name of the service
required: true
type: str
state:
description: Desired state (started or stopped)
required: true
choices: [ started, stopped ]
type: str
author:
- You
'''
EXAMPLES = r'''
# Start myservice
- my_service:
name: myservice
state: started
'''
def run_module():
module_args = dict(
name=dict(type='str', required=True),
state=dict(type='str', required=True, choices=['started', 'stopped'])
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
result = dict(changed=False)
name = module.params['name']
desired = module.params['state']
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
```
```yaml
- hosts: localhost
tasks:
- name: Start myservice
my_service:
name: myservice
state: started
```
Large playbooks or slow network connections can cause long execution times.
Open source contributors recommend several techniques to speed up Ansible runs
【345425124648387†L148-L169】【345425124648387†L164-L191】:
```yaml
- name: Compile software asynchronously
command: make all
async: 3600
poll: 0
```
The `async` parameter specifies the maximum time (seconds) the job may run;
`poll: 0` tells Ansible not to wait. You can later check the result with the
`async_status` module【345425124648387†L164-L191】.
* **Pull mode** – `ansible-pull` inverts the model: each node pulls playbooks
from a Git repository and executes them locally. This decentralises work and
can improve scalability for large clusters【345425124648387†L164-L191】.
* **Rolling updates** – using `serial` ensures only a few hosts are updated at a
time. Combine this with health checks and handlers for robust deployments.
Red Hat Ansible Automation Platform (AAP) and its open-source upstream AWX
provide a web UI, job scheduling, role-based access control (RBAC), audit trails
and credential management. AWX/AAP can synchronise dynamic inventories, manage
project repositories and expose a REST API for launching jobs
【437227952386536†L122-L146】. When you need enterprise features such as
multi-tenant access, SAML/OIDC integration and analytics, consider running AWX
or subscribing to AAP.
```text
Developer push
│
▼
CI Pipeline
┌──────────────────────────────────────────────┐
│ 1. Lint & syntax check (ansible-lint, │
│ ansible-playbook --syntax-check) │
│ 2. Dry run on staging (--check, --diff) │
│ 3. Automated tests (Molecule/Testinfra) │
│ 4. Deployment to production (ansible-playbook) │
│ 5. Notifications and cleanup │
└──────────────────────────────────────────────┘
│
▼
Target Environment
```
1. **Create a dynamic inventory** for AWS using the `aws_ec2` plugin. Tag your
EC2 instances with roles such as `Role=webserver` and `Role=dbserver`.
Configure the plugin as shown earlier.
This project demonstrates how Ansible integrates with modern cloud, security and
DevOps practices. Adjust the scope based on your environment – you could
substitute AWS with Azure, on-premises servers or containers.
---
## Appendix
Released in late 2024, Ansible 2.16 includes improvements such as support for
**Python 3.12**, the retirement of Python 3.5 for modules and Python 3.9 for the
controller, improved CLI argument parsing, enhanced documentation and the
removal of long-deprecated features【698678219570606†L12-L99】. The default
transport has changed from `smart` to `ssh` to simplify configuration. New
remote execution environments support Alpine 3.18, Fedora 38, FreeBSD 13.2 and
RHEL 8.8/9.2, and deprecation warnings have been removed or converted into
errors【698678219570606†L12-L99】. Staying current with Ansible versions
ensures you benefit from performance improvements, security patches and new
modules.
Thank you for reading this handbook. Ansible empowers you to turn manual
processes into repeatable, reliable automation. Use this knowledge to build
robust infrastructure and share your roles with the community. Happy
automating!