Ansible automates server configuration, application deployment and routine administration from a central control node. This guide explains Ansible basics through a practical lab covering inventories, ad-hoc commands, playbooks, variables, handlers and idempotent execution.
These skills are commonly used with Linux and cloud infrastructure. Learners who want structured practice with automation, AWS and CI/CD can explore the AWS DevOps course.
What is Ansible and how does it work?
Ansible is an open-source automation tool that describes the required state of systems in YAML files called playbooks. It normally connects to Linux managed nodes over SSH, runs modules on those nodes and returns the results without requiring a permanently installed Ansible agent.
A basic Ansible environment has these components:
| Component | Purpose |
|---|---|
| Control node | The computer where Ansible is installed and commands are started |
| Managed node | A server, virtual machine or network device controlled by Ansible |
| Inventory | A list or dynamic source of managed hosts and groups |
| Module | Code that performs a task, such as installing a package |
| Playbook | A YAML file containing one or more plays and tasks |
| Collection | Packaged modules, plugins and roles for a platform or vendor |
Diagram in words: Imagine a laptop called control on the left. SSH connections run from that laptop to three Ubuntu servers on the right: web1, web2 and db1. The control node reads an inventory, selects a host group, transfers module code, receives JSON results and then moves to the next task.
Ansible usually requires Python on POSIX managed nodes because many modules execute through Python. The ansible.builtin.raw module can run an initial command without Python when bootstrapping a minimal system. Windows management uses WinRM or SSH with Windows-specific modules rather than the standard Linux workflow.
How do you install Ansible on a control node?
Install Ansible on a supported Linux or macOS control node, or use a Linux environment such as WSL for a Windows workstation. A Python virtual environment or pipx keeps the Ansible installation separate from operating-system Python packages.
On Ubuntu, install pipx and then install the complete Ansible community package:
sudo apt update
sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansibleStart a new shell if the command path was updated. Verify the installation:
ansible --version
ansible-playbook --versionThe output shows the Ansible Core version, Python version, module paths and configuration file in use. The ansible package includes ansible-core and a set of community collections; ansible-core provides the command-line tools and built-in collection.
Create a lab directory:
mkdir -p ~/ansible-lab
cd ~/ansible-labThe control node must be able to resolve or reach each managed node. Before using Ansible, confirm ordinary SSH access:
ssh ubuntu@192.0.2.21The address 192.0.2.21 belongs to a documentation range. Replace it with the private IP address or resolvable hostname of your lab server.
What is an Ansible inventory?
An inventory identifies managed hosts and organises them into groups such as web servers, database servers or production systems. It can be a static INI or YAML file, or it can be generated dynamically from services such as AWS.
Create an INI inventory named inventory.ini:
[web]
web1 ansible_host=192.0.2.21
web2 ansible_host=192.0.2.22
[database]
db1 ansible_host=192.0.2.31
[linux:children]
web
database
[linux:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/lab_keyThis inventory creates two direct groups, web and database. The linux parent group includes both through the children relationship, and its variables apply to all three hosts.
Inspect how Ansible interprets the file:
ansible-inventory -i inventory.ini --graph
ansible-inventory -i inventory.ini --listExpected graph structure:
@all:
|--@ungrouped:
|--@linux:
| |--@web:
| | |--web1
| | |--web2
| |--@database:
| | |--db1Inventory aliases such as web1 are Ansible names. The ansible_host variable tells Ansible which IP address or DNS name to contact.
For changing cloud environments, a dynamic inventory plugin can discover instances through an API. For example, the amazon.aws.aws_ec2 inventory plugin can group EC2 instances by tags, but it requires the amazon.aws collection, suitable AWS credentials and Python AWS SDK packages. First understand static inventories, then connect the workflow to concepts in Amazon EC2 instances, AMIs and security groups.
How do ad-hoc Ansible commands work?
An ad-hoc command runs one module directly against a host pattern and is useful for quick checks or one-time operations. Repeatable configuration should normally be stored in a playbook so that it can be reviewed and version-controlled.
Test connectivity to the web group:
ansible web -i inventory.ini -m ansible.builtin.pingA successful result resembles:
web1 | SUCCESS => {
"changed": false,
"ping": "pong"
}The Ansible ping module is not an ICMP ping. It checks whether Ansible can connect, find a usable Python interpreter and execute a small module on the managed node.
Collect operating-system information:
ansible linux -i inventory.ini -m ansible.builtin.setup \
-a 'filter=ansible_distribution*'Check uptime using the command module:
ansible linux -i inventory.ini -m ansible.builtin.command \
-a 'uptime'Use --become when a task needs privilege escalation:
ansible web -i inventory.ini --become \
-m ansible.builtin.apt -a 'name=nginx state=present update_cache=yes'Do not use ansible.builtin.shell unless shell features such as pipes, redirects or variable expansion are required. The command module avoids interpretation by a shell and therefore reduces quoting and security problems.
What is an Ansible playbook?
A playbook is a YAML document that maps host groups to ordered tasks. Each task calls a module with arguments that describe an action or required state.
Create webserver.yml:
---
- name: Configure web servers
hosts: web
become: true
gather_facts: true
vars:
web_package: nginx
web_service: nginx
tasks:
- name: Install Nginx
ansible.builtin.apt:
name: "{{ web_package }}"
state: present
update_cache: true
cache_valid_time: 3600
- name: Publish training page
ansible.builtin.copy:
dest: /var/www/html/index.html
owner: root
group: root
mode: '0644'
content: |
Ansible managed this server.
notify: Restart Nginx
- name: Ensure Nginx is enabled and running
ansible.builtin.service:
name: "{{ web_service }}"
state: started
enabled: true
handlers:
- name: Restart Nginx
ansible.builtin.service:
name: "{{ web_service }}"
state: restartedCheck YAML and basic playbook syntax before execution:
ansible-playbook -i inventory.ini webserver.yml --syntax-checkRun the playbook:
ansible-playbook -i inventory.ini webserver.ymlThe play starts on hosts in the web group. Facts provide information such as the distribution, interfaces and memory; the tasks then install Nginx, copy content and ensure the service is enabled and running.
Fully qualified collection names such as ansible.builtin.copy make it clear which module is used. Module documentation and examples can be viewed locally:
ansible-doc ansible.builtin.copy
ansible-doc ansible.builtin.aptWhat does idempotent automation mean?
An idempotent task moves a system to the requested state without making unnecessary changes when that state already exists. Running the same well-designed playbook twice should leave the second run mostly or entirely unchanged.
For example, this task does not reinstall Nginx every time:
- name: Ensure Nginx is installed
ansible.builtin.apt:
name: nginx
state: presentThe module inspects package state. If Nginx is absent, the result is changed; if it is already installed, the result is ok.
Typical result colours and states are:
| Result | Meaning |
|---|---|
ok | The task succeeded and made no change |
changed | The task succeeded and changed the managed node |
failed | The task could not complete |
skipped | A condition or host state prevented execution |
unreachable | Ansible could not connect to the host |
Run the example twice and compare the recap:
PLAY RECAP
web1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Idempotence is not automatic for every task. A command such as the following appends the same line on every run:
- name: Append a line using a shell
ansible.builtin.shell: echo 'APP_MODE=production' >> /etc/environmentUse a state-aware module instead:
- name: Set application mode once
ansible.builtin.lineinfile:
path: /etc/environment
regexp: '^APP_MODE='
line: APP_MODE=production
create: true
mode: '0644'When a command is unavoidable, creates or removes can provide a simple guard:
- name: Initialise application data once
ansible.builtin.command: /opt/app/bin/init-data
args:
creates: /opt/app/data/.initialisedThe command runs only if the marker path does not exist. The command itself must reliably create that marker after successful initialisation.
Why are handlers useful in playbooks?
Handlers run only when notified by a task that reports a change. They prevent unnecessary service restarts and normally execute once at the end of the play, even if multiple tasks notify the same handler.
In the web server example, changing index.html notifies Restart Nginx. An unchanged file returns ok, so no notification is queued. For static page content, a restart is not technically necessary, but the example clearly demonstrates handler behaviour; configuration files such as /etc/nginx/nginx.conf are a more typical use.
If a handler must run before later tasks, flush queued handlers explicitly:
- name: Apply queued handlers now
ansible.builtin.meta: flush_handlersUse this only when following tasks depend on the restarted or reloaded service. Otherwise, allowing handlers to run at the end reduces disruption.
How can you test a playbook safely?
Use syntax checking, check mode, diff output and a limited host selection before applying a playbook widely. Check mode predicts many changes, but it cannot perfectly simulate modules or commands that do not support it.
Start with these commands:
ansible-playbook -i inventory.ini webserver.yml --syntax-check
ansible-playbook -i inventory.ini webserver.yml --check
ansible-playbook -i inventory.ini webserver.yml --check --diff
ansible-playbook -i inventory.ini webserver.yml --limit web1--diff can reveal previous and proposed file content. Avoid using it where files contain passwords, tokens or private keys, because sensitive data may appear in terminal output or CI logs.
A practical promotion sequence is:
- Validate syntax.
- Run
--check --diffin a non-production lab. - Apply to one test host with
--limit. - Verify the service and application response.
- Apply to the remaining group.
- Run the playbook again and inspect idempotence.
Playbooks can also be stored in Git and launched from CI/CD. The Jenkins declarative pipeline guide explains pipeline stages and agents that can be used around validation and deployment jobs.
How do you troubleshoot common Ansible errors?
Troubleshoot Ansible by separating inventory, network, SSH, privilege and module problems. Use verbose output and test the underlying connection directly instead of changing several playbook settings at once.
Host is unreachable
Example:
UNREACHABLE! => Failed to connect to the host via sshCheck the resolved inventory data and test SSH:
ansible-inventory -i inventory.ini --host web1
ssh -i ~/.ssh/lab_key ubuntu@192.0.2.21
ansible web1 -i inventory.ini -m ansible.builtin.ping -vvvVerify the IP address, route, security group or firewall, SSH username and private-key permissions. An SSH private key commonly requires restrictive permissions:
chmod 600 ~/.ssh/lab_keyPython interpreter is missing
A minimal target may not have Python installed. Bootstrap it on Ubuntu with the raw module:
ansible web1 -i inventory.ini -m ansible.builtin.raw \
-a 'sudo apt-get update && sudo apt-get install -y python3'After installation, run the ping module again. Set ansible_python_interpreter only when interpreter discovery selects the wrong path.
Sudo requires a password
If the remote account is allowed to use sudo but needs a password, prompt for it:
ansible-playbook -i inventory.ini webserver.yml --ask-become-passIn managed environments, configure privilege escalation according to security policy rather than embedding a sudo password in plain-text inventory files. Ansible Vault can encrypt variable files, but access should still follow least-privilege principles.
YAML syntax or indentation fails
YAML uses spaces and indentation to represent structure. Do not use tabs, and align module arguments beneath the module name.
Run:
ansible-playbook -i inventory.ini webserver.yml --syntax-checkIf the reported line looks correct, inspect the lines immediately above it. An unclosed quote or incorrectly indented list item can cause the parser to report a later line.
A task changes on every run
Run the playbook with --diff and inspect the module result using -vv. Common causes include timestamps embedded in generated files, random values, unconditional shell commands and templates whose source changes every execution.
Replace shell logic with modules such as copy, template, file, user, package, service or lineinfile. If change is expected, document why rather than hiding it with changed_when: false.
What should you learn after these Ansible basics?
After inventories and playbooks, learn variable precedence, templates, roles, Ansible Vault, collections and dynamic cloud inventory. Then practise testing, source control and CI/CD integration so that automation is reviewable and safe to operate.
A useful next lab is to convert the Nginx play into a role with tasks, handlers, templates and defaults directories. You can then create separate development and production inventories while reusing the same role.
Summary
Ansible uses a control node, inventory and modules to manage remote systems, usually through SSH. Playbooks define repeatable tasks, while state-aware modules and handlers help make automation idempotent and avoid unnecessary changes.
For reliable automation, validate syntax, use check mode carefully, test on a limited host and run the playbook twice. When troubleshooting, confirm inventory values, direct SSH access, Python availability and privilege escalation in that order.
To practise Ansible with AWS, Linux, Git, Jenkins and deployment workflows, enquire about batch details for the AWS DevOps course.
*Reviewed by Network Rhinos DevOps trainers.*
