Ansible with Python
Introduction to Ansible with Python — Python for DevOps automation. Learn core concepts, hands-on steps, troubleshooting, and what to study next.
Focus: introduction to ansible with python
You've spent hours SSH-ing into servers, pasting the same commands, and hoping you didn't miss a step. It's repetitive, error-prone, and it doesn't scale beyond a handful of machines.
Ansible is the tool that replaces that chaos with a single, declarative automation layer — and while you don't need Python to use it, Python is what Ansible is written in, and Python is what makes it infinitely extendable. In this introduction to Ansible with Python, you'll learn how to install and configure Ansible, write your first playbook, and then go one step further by driving Ansible from a Python script using the ansible-runner library.
The problem this lesson solves
Manual server configuration is a bottleneck. Every time you need to install a package, update a config file, or restart a service across 10 machines, you either SSH into each one or write fragile shell loops. Both approaches have the same flaws:
- No consistency: a typo on machine 3 goes unnoticed until something breaks.
- No audit trail: what changed, when, and by whom?
- No idempotency: running the same command twice can break things or produce different results.
Ansible solves all three by letting you describe the desired state of your servers in simple YAML files. Once you've absorbed that idea, you'll see the natural next step: instead of running Ansible from the command line every time, you can embed it in a Python application or CI/CD pipeline. That's where ansible-runner comes in — it gives you a programmatic interface to Ansible's execution engine, so your Python DevOps tooling can launch playbooks, capture output, and react to results in real time.
Core concept / mental model
Think of Ansible as a remote task runner with a YAML front end. You write a playbook — a list of hosts and the tasks to perform on them. Ansible connects to each host over SSH, pushes no software, runs the tasks, and reports back.
Here's the mental model in words:
[ control node ] --- SSH ---> [ managed node 1 ]
runs playbook [ managed node 2 ]
no agent installed [ managed node 3 ]
Three pillars to understand before you write anything:
- Control node: the machine where Ansible is installed (your laptop or a CI runner).
- Managed nodes: the servers you automate (targets).
- Inventory: a file that lists your managed nodes and groups them logically (e.g.,
webservers,databases).
Playbooks are declarative, not procedural. Instead of saying "run these commands in this order," you say "ensure this package is installed" or "this service is running." Ansible figures out the how — and if the state already matches, it does nothing. This is called idempotency, and it's the single most important concept in configuration management.
Pro tip: If you only remember one sentence from this lesson, make it this: Ansible describes what should be true, not how to make it true.
Python fits into this model in three ways:
- Ansible itself is written in Python.
- You can run Ansible programmatically via
ansible-runner(the modern, supported way). - You can write custom modules in Python when the built-in modules don't cover your use case.
For a DevOps engineer, being comfortable with Python unlocks the ability to wrap Ansible in your own tooling — a deployment CLI, a self-service portal, a monitoring health-check script.
How it works step by step
Let's trace what happens when you run ansible-playbook:
- Parse the inventory — Ansible reads your inventory file and builds a list of managed hosts, with any variables you've defined (e.g., IP addresses, SSH users).
- Load the playbook — The playbook is YAML. Ansible parses it into a list of plays, each with a
hostspattern and ataskslist. - Gather facts — By default, Ansible SSHes into each host and runs a small Python script to collect facts (OS, CPU, memory, IP addresses, etc.). These facts become variables you can use in your playbook.
- Execute tasks — For each task, Ansible finds the module (e.g.,
apt,copy,service), creates a Python script that implements the module's logic, and copies it to the target host over SSH. It executes, gets JSON output back, and checks thechangedstatus. - Report results — You see a colorized summary:
ok,changed,failed, orskipped. - Wrap up — If any task fails, Ansible stops that host's play (unless you set
ignore_errorsorforce_handlers).
Key terminology you'll encounter:
- Module — a self-contained script that does something (e.g.,
apt,yum,copy,service,command). - Play — a mapping of selected hosts to a set of tasks.
- Handler — a special task that runs only when notified by another task (e.g., restart a service after a config file change).
- Idempotent — a task that produces the same end state whether it runs once or 10 times.
Hands-on walkthrough
Let's start from zero. You'll install Ansible, create a minimal inventory, write a playbook that installs Nginx on Ubuntu, and then run it from Python.
Step 1: Install Ansible
On macOS/Linux (with pip):
python -m pip install --user ansible
ansible --version
# Output (partial):
# ansible [core 2.16.0]
# config file = None
# configured module search path = ['/home/you/.ansible/plugins/modules']
If your system package manager offers Ansible, that's fine too — but pip is what we recommend because it keeps you in the Python ecosystem and makes it easy to upgrade.
Windows note: Ansible's control node doesn't run natively as a shell command. Use WSL2 or run it in a Linux container/VM.
Step 2: Create a minimal inventory
Create inventory.ini:
[webservers]
web1.example.com ansible_user=deploy
web2.example.com ansible_user=deploy
If you're testing locally with Docker, you can use localhost with connection set to local (no SSH). Create inventory_local.ini:
[local]
localhost ansible_connection=local
Step 3: Write your first playbook
Create playbooks/nginx.yml:
---
- name: Ensure Nginx is installed and running
hosts: webservers
become: true
tasks:
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Ensure Nginx service is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
Run it:
ansible-playbook -i inventory.ini playbooks/nginx.yml
Expected output (simplified):
PLAY [Ensure Nginx is installed and running]
TASK [Gathering Facts] ********
ok: [web1.example.com]
TASK [Install Nginx] ********
changed: [web1.example.com]
TASK [Ensure Nginx service is running] ********
changed: [web1.example.com]
PLAY RECAP ********
web1.example.com : ok=3 changed=2 unreachable=0 failed=0
Run it a second time — you'll notice all tasks now report ok (not changed), because the state already matches. That's idempotency in action.
Step 4: Drive Ansible from Python
Now install ansible-runner:
python -m pip install ansible-runner
Write a Python script run_playbook.py:
import ansible_runner
# Path to a directory containing your playbook and inventory
r = ansible_runner.run(
private_data_dir='/tmp/ansible_demo',
playbook='nginx.yml',
inventory='inventory_local.ini',
extravars={'server_name': 'example.local'},
quiet=False,
)
print(f"Status code: {r.rc}")
print("Stats:")
print(r.stats)
if r.rc != 0:
raise SystemExit(f"Playbook failed with {r.rc}")
The private_data_dir is a directory that contains the playbook, inventory, and a place for output. You can also pass extravars as a dict — that's how you parameterize your playbook from Python.
When you run it:
python run_playbook.py
You'll see the same output as the CLI, but now your Python code gets programmatic access to the runner object. You can capture stdout/stderr, check r.rc, and inspect r.stats (a dict with ok, changed, etc.).
Compare options / when to choose what
You have several ways to interact with Ansible. Here's when to use each:
| Approach | Pros | Cons | Best when |
|---|---|---|---|
CLI (ansible-playbook) |
Simple, directly usable by humans, easiest for ad-hoc ops | No programmatic logic, hard to embed in larger apps | Quick manual runs, simple CI steps |
ansible-runner (Python) |
Full control, can react to events, integrates with Python ORM/CLIs, supports callbacks | Requires installing a library, more boilerplate | Building a Python CLI, web UI, or complex automation workflow |
Ansible Python API (ansible module) |
Low-level access, no extra library | Unstable API (internal), not recommended for most users | You need extreme low-level control (rare) |
| AWX/Ansible Tower | Web UI, RBAC, scheduling, audit trail | Heavy, overkill for small teams | Enterprise multi-team automation |
Rule of thumb: Start with ansible-playbook. Move to ansible-runner when you need to wrap playbook execution in your own Python logic — e.g., a deploy script that first runs a playbook and then runs a Python test suite.
Pro tip: In a CI/CD pipeline (like GitHub Actions),
ansible-playbookis usually enough. You only needansible-runnerif you're building a custom Python application (a Slack bot, a CLI, a portal).
Troubleshooting & edge cases
When something goes wrong, here are the most common issues and fixes.
"UNREACHABLE!" — can't connect to the host
Cause: SSH authentication failure, wrong ansible_user, or the host is down.
Fix: Test with ansible -m ping hostname (using the inventory's host name). Verify ~/.ssh/config, ensure your key is added. If using ansible_connection=local, double-check the inventory file syntax.
"FAILED! ... module 'apt' is missing on the target"
Cause: The managed node doesn't have apt (it's not Debian/Ubuntu) or Python isn't installed on the target.
Fix: Use the correct package module (yum for RHEL, dnf for newer Fedora). For Python, Ansible requires Python 2.7 or 3.5+ on the target. If missing, you can set ansible_python_interpreter=/usr/bin/python3 in inventory variables.
Playbook runs but nothing changes
Cause: You're not using become: true, so you lack permissions to install packages.
Fix: Add become: true at the play level. Also verify you're using the right module — e.g., service vs systemd.
ansible-runner says "No inventory"
Cause: The private_data_dir doesn't contain an inventory file, or you didn't pass the inventory parameter.
Fix: Ensure the playbook directory has an inventory file (or you pass inventory with an absolute path). Also check that your playbook references the correct inventory filename.
Idempotency not working
Cause: You're using command or shell modules instead of purpose-built modules.
Fix: Use ansible.builtin.apt, copy, lineinfile, etc. They are designed for idempotency. If you must run a shell command, guard it with a creates or when condition.
What you learned & what's next
You now understand the core idea behind introduction to ansible with python: Ansible is an agentless configuration management tool that uses YAML playbooks to describe desired state. You installed Ansible, created an inventory, wrote a playbook, and ran it — both via the CLI and programmatically with ansible-runner. You also learned how to troubleshoot common issues like SSH unreachable errors and module-not-found failures.
In the next lesson in this track, you'll learn how to build custom Ansible modules in Python — that's where you'll turn your Python functions into reusable automation components that can be used in any playbook. You'll also explore using Ansible's ansible-playbook in a CI/CD pipeline with Python wrappers for dynamic inventory.
Pro tip: Before moving on, practice writing a playbook that uses
copyto deploy a config file andserviceto restart a daemon. Then wrap it withansible-runnerin a Python script that emails you if any task fails. That's a full DevOps loop!
Practice recap
Create a local inventory with a Docker container as a target, write a playbook that installs curl and creates a file with copy, then wrap that playbook with ansible-runner in a Python script that prints the status and any failed tasks. Re-run the script twice to confirm idempotency.
Common mistakes
- Forgetting
become: true— you'll get permission-denied errors when installing packages or writing to system paths. - Using
commandorshellmodules for everything — they're not idempotent and cause side effects on re-runs. - Not setting
ansible_python_interpreterwhen the target node has Python 2 or lacks apythonsymlink — Ansible fails with a cryptic interpreter error. - Passing
inventoryas a directory instead of a file toansible-runner— it expects a file path or a string that resolves to one.
Variations
- Use
ansible-playbookCI step with--checkfor dry-run validation before applying changes. - Use Ansible's native Python API (
ansible.inventory,ansible.executor.task_queue_manager) — more powerful but API unstable. - Use AWX (Ansible Tower) as a web UI on top of
ansible-runnerfor team-wide visibility and RBAC.
Real-world use cases
- Deploy Nginx and a Python Flask app to staging servers via a one-command Ansible playbook run by a Jenkins job.
- Automate server hardening (firewall rules, SSH config) across 50+ AWS EC2 instances using Ansible, with Python health checks after each run.
- Build a Python CLI tool that uses
ansible-runnerto let developers trigger playbooks with customextravars(e.g., version tag) without SSH access.
Key takeaways
- Ansible is agentless — it connects over SSH and pushes no software to managed nodes.
- Playbooks are YAML and declare desired state — idempotency is built into most modules.
- Inventory files organize hosts into groups; server- and group-level variables live alongside the host list.
ansible-runneris the recommended way to execute Ansible from Python — it gives you result objects and event streams.- When a task fails, Ansible stops that host’s play by default — plan your playbook order carefully.
- Use built-in modules (
apt,copy,service) instead of raw shell commands for idempotent automation.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.