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

Sponsored

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:

  1. Ansible itself is written in Python.
  2. You can run Ansible programmatically via ansible-runner (the modern, supported way).
  3. 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:

  1. 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).
  2. Load the playbook — The playbook is YAML. Ansible parses it into a list of plays, each with a hosts pattern and a tasks list.
  3. 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.
  4. 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 the changed status.
  5. Report results — You see a colorized summary: ok, changed, failed, or skipped.
  6. Wrap up — If any task fails, Ansible stops that host's play (unless you set ignore_errors or force_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-playbook is usually enough. You only need ansible-runner if 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 copy to deploy a config file and service to restart a daemon. Then wrap it with ansible-runner in 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 command or shell modules for everything — they're not idempotent and cause side effects on re-runs.
  • Not setting ansible_python_interpreter when the target node has Python 2 or lacks a python symlink — Ansible fails with a cryptic interpreter error.
  • Passing inventory as a directory instead of a file to ansible-runner — it expects a file path or a string that resolves to one.

Variations

  1. Use ansible-playbook CI step with --check for dry-run validation before applying changes.
  2. Use Ansible's native Python API (ansible.inventory, ansible.executor.task_queue_manager) — more powerful but API unstable.
  3. Use AWX (Ansible Tower) as a web UI on top of ansible-runner for 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-runner to let developers trigger playbooks with custom extravars (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-runner is 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.