




















Manually logging into servers to install packages, push configuration files, or restart services works fine when you have two machines. It breaks down completely when you have twenty — or two hundred. A missed step on one node means your fleet drifts out of sync, and you won't find out until a production incident exposes the inconsistency at 2 AM.
Ansible is the tool that eliminates this class of problem. You describe exactly what every server should look like in plain YAML and Ansible applies that description across your entire fleet in seconds — from your laptop, over SSH, with no software installed on the servers themselves.
This article covers the architecture, each core component, and the hands-on lab setup that the Devoriales Ansible course uses (three Docker containers as managed nodes on your Mac).
Ansible is an open-source IT automation platform originally created by Michael DeHaan and acquired by Red Hat in 2015. It is designed to automate configuration management, application deployment, cloud provisioning, and orchestration across a large number of systems simultaneously.
What distinguishes Ansible from other automation tools is its agentless, push-based architecture. There is no daemon to install on managed servers. Ansible connects over SSH (or WinRM for Windows), executes temporary scripts on the remote system, collects the results, and reports back — all from your control node. Once Ansible finishes, nothing persists on the managed node except the changes you asked for.
Automation in Ansible is described in YAML files called playbooks. YAML is human-readable and deliberately simple, which means your playbooks serve as both automation scripts and living documentation of your infrastructure's desired state. A developer who has never used Ansible before can read a well-written playbook and understand what it does without learning a new programming language.
Ansible is also idempotent. Run the same playbook ten times and only the first run makes changes — subsequent runs detect that the desired state is already in place and do nothing. This makes re-running automation safe and predictable, which is critical for CI/CD pipelines and disaster recovery.
Ansible requires no software installation on managed nodes. It uses SSH — a protocol present on every Linux and macOS server and many network devices — to connect, push a temporary Python script, execute it, read the output as JSON, and remove the script. The only prerequisites on a managed node are:
This design dramatically reduces operational overhead. There is no agent version to maintain, no daemon to monitor, no port to open beyond SSH, and no bootstrapping problem to solve before you can manage a new server.
Playbooks are the language of Ansible. They use YAML syntax to declare:
become: true)A playbook is readable by anyone on your team, which means it also serves as runbook documentation. When a new engineer joins and needs to understand how your nginx configuration is deployed, they read the playbook — not tribal knowledge in a Confluence page.
Ansible tasks are designed to check the current state of the system before making changes. The package module checks whether a package is already installed before calling apt or yum. The copy module computes a checksum of the remote file before overwriting it. The user module checks whether a user already exists before creating one.
The result is that the same playbook can run safely in development, staging, and production — in CI pipelines, scheduled jobs, or on-demand — without fear of inadvertent changes.
Ansible ships with hundreds of built-in modules for managing packages, files, services, users, cloud resources, network devices, and databases. The community extends this further with collections — bundles of modules, plugins, roles, and playbooks organized by vendor or domain (e.g., community.docker, amazon.aws, kubernetes.core). You install collections with ansible-galaxy collection install.
Ansible manages Linux (Red Hat, Debian, SUSE, and derivatives), Windows (via WinRM and PowerShell), macOS, network devices (Cisco IOS, Junos, Arista EOS), and cloud APIs. The same control node manages all of them. A single playbook can configure Linux web servers and Windows Active Directory in the same run.
Understanding Ansible's architecture helps you write better playbooks, debug problems faster, and design automation that scales.
The control node is the machine where Ansible is installed and from which all automation is driven. In our lab, this is your Mac. In production, it is typically a bastion host, a CI/CD runner, or a dedicated automation server.
The control node: - Reads your inventory to know which hosts exist and how they are grouped - Parses playbooks to determine what tasks to run - Compiles Python modules and pushes them to managed nodes over SSH - Collects return values (JSON) from managed nodes and builds the play output - Evaluates whether each task resulted in a change, failure, or no-op
Ansible does not require a central server for most use cases. The control node is sufficient. (Ansible Tower / AWX adds a web UI, RBAC, and scheduling on top, but that is a separate product.)
Managed nodes are the systems Ansible controls. In our lab these are Docker containers. In production they are virtual machines, bare-metal servers, cloud instances, or network devices.
Managed nodes need: - SSH running and accessible from the control node - Python 3 installed (for most Linux modules) - A user account the control node can SSH into, with sudo access if tasks require root
No Ansible software is installed. When a task runs, Ansible transfers a temporary Python script to /tmp on the managed node, executes it via SSH, reads stdout (JSON), and deletes the script. The managed node is stateless from Ansible's perspective.

Ansible push-based architecture — the control node (your Mac) pushes tasks over SSH to managed nodes. No agent runs on the servers.
The inventory is a file (or set of files, or a dynamic source) that tells Ansible which managed nodes exist and how they are grouped. Groups allow you to target subsets of your fleet — for example, run a task only against [webservers] or only against [dbservers].
A simple INI inventory looks like this:
[webservers]
web1 ansible_host=localhost ansible_port=2201
web2 ansible_host=localhost ansible_port=2202
[dbservers]
db1 ansible_host=localhost ansible_port=2203
Every inventory automatically contains two implicit groups: - all — every host in the inventory - ungrouped — hosts that are not in any named group
You can also define a dynamic inventory — a script or plugin that queries an external source (AWS EC2, Azure, GCP, Kubernetes) and returns the current list of hosts at runtime. This is essential for cloud environments where hosts are ephemeral and IP addresses change constantly.
A playbook is the primary unit of Ansible automation. It is a YAML file containing one or more plays. Each play specifies:
hosts — which inventory group(s) to targetbecome — whether to escalate to rootvars — play-level variablestasks — the ordered list of tasks to executeA task calls a module with specific arguments. Tasks execute sequentially within a play, but multiple plays in a playbook execute in order, and multiple hosts within a play execute in parallel (up to the forks limit, default 5).
---
- name: Configure web servers
hosts: webservers
become: true
tasks:
- name: Install nginx
package:
name: nginx
state: present
- name: Copy virtual host configuration
copy:
content: "server { listen 80; server_name web1; }"
dest: /etc/nginx/conf.d/app.conf
notify: Reload nginx
handlers:
- name: Reload nginx
command: nginx -s reload
Modules are the workers of Ansible. Each task calls exactly one module. A module is a self-contained Python script (or PowerShell script for Windows) that:
changed, failed, and any relevant outputAnsible ships with hundreds of modules. Here are some of the most commonly used:
| Module | What it does |
|---|---|
package / apt / yum |
Install, upgrade, or remove packages |
copy |
Copy a file from control node to managed node |
template |
Render a Jinja2 template and copy to managed node |
file |
Manage file and directory attributes (permissions, ownership, state) |
service |
Start, stop, enable, or disable a system service |
user |
Create, modify, or delete user accounts |
command / shell |
Run arbitrary commands (not idempotent by default) |
setup |
Gather facts about the managed node |
git |
Clone or update a Git repository |
uri |
Make HTTP requests |
Modules are invoked with key-value arguments:
- name: Ensure the ansible user exists
user:
name: ansible
shell: /bin/bash
state: present
You can also run a module ad hoc (without a playbook) using the ansible command:
ansible webservers -m package -a "name=curl state=present" --become
Handlers are special tasks that only run when notified by another task that reported a change. They execute at the end of the play, after all regular tasks have run — and only once, no matter how many tasks notify the same handler.
This is the standard pattern for service reloads:
tasks:
- name: Deploy nginx config
copy:
content: "server { listen {{ app_port }}; }"
dest: /etc/nginx/conf.d/app.conf
notify: Reload nginx # queues the handler only if this task changed
handlers:
- name: Reload nginx
command: nginx -s reload
If the config file is already correct (task reports ok), the handler is never queued and nginx is never restarted. If ten tasks all notify the same handler, it still only fires once. This prevents unnecessary service restarts.
Variables let you parameterize playbooks so the same automation logic works across different environments, hosts, and configurations.
Defined variables are values you set explicitly:
# group_vars/webservers.yml
app_port: 8080
app_name: meridian-web
Ansible auto-loads group_vars/<groupname>.yml for every host in that group before any play runs. Host-specific overrides go in host_vars/<hostname>.yml.
Facts are variables Ansible discovers automatically by running the setup module at the start of every play (gather_facts: true is the default). Facts include:
| Fact | Example value |
|---|---|
ansible_hostname |
web1 |
ansible_distribution |
Ubuntu |
ansible_distribution_version |
22.04 |
ansible_memtotal_mb |
3940 |
ansible_default_ipv4.address |
172.17.0.2 |
ansible_processor_vcpus |
2 |
Facts are available anywhere variables are used — in task arguments, templates, conditionals, and variable files.
Variable precedence in Ansible (lowest to highest):
| Source | Precedence |
|---|---|
Role defaults (defaults/main.yml) |
lowest |
| Inventory group variables | ↑ |
| Inventory host variables | ↑ |
group_vars/ files |
↑ |
host_vars/ files |
↑ |
Play vars: section |
↑ |
vars_files: |
↑ |
Extra vars (-e on CLI) |
highest |
When a variable has an unexpected value, trace up this list to find which source is winning.
Roles are the packaging mechanism for Ansible automation. A role is a directory with a predefined structure that groups related tasks, handlers, variables, templates, and files into a single reusable unit.
Create a role skeleton with:
ansible-galaxy role init roles/webserver
This generates:
roles/webserver/
├── tasks/
│ └── main.yml ← entry point; Ansible loads this automatically
├── handlers/
│ └── main.yml
├── defaults/
│ └── main.yml ← lowest-precedence variable defaults
├── vars/
│ └── main.yml ← higher-precedence role variables
├── files/ ← static files for the copy module
├── templates/ ← Jinja2 templates for the template module
└── meta/
└── main.yml ← role metadata and dependencies
A playbook that uses the role:
---
- name: Configure web servers
hosts: webservers
become: true
roles:
- webserver
Roles make automation reusable. The same webserver role can be applied in dev, staging, and production playbooks. Teams share roles via Ansible Galaxy (ansible-galaxy role install).
Plugins extend how Ansible itself operates — not what it does to managed nodes, but how it connects, formats output, looks up data, and processes logic. Plugins run on the control node.
Key plugin types:
| Plugin type | What it does |
|---|---|
Connection (ssh, docker, local) |
How Ansible connects to managed nodes |
Callback (default, json, yaml) |
How playbook output is formatted and displayed |
Inventory (aws_ec2, azure_rm) |
How dynamic inventory is sourced |
Lookup (file, env, vault) |
How external data is retrieved into variables |
Filter (to_json, select, map) |
Jinja2 filters for variable transformation |
| Action | Local pre/post processing around module execution |
Most users never write a plugin. But understanding they exist explains why you can write {{ lookup('env', 'DB_PASSWORD') }} in a playbook and have Ansible read an environment variable, or why you can change your output format with -o json or a callback plugin configuration.
Rather than working on a remote server, we will build a local lab using Docker containers as managed nodes. This gives you a production-realistic environment you can break and rebuild freely in minutes.
Our lab will have: - 1 control node — your Mac - 3 managed nodes — Docker containers named web1, web2, and db1 - Ansible connecting over SSH to ports 2201, 2202, and 2203
brew install ansible
As of mid-2026 this installs Ansible 14.0.0 (which bundles ansible-core 2.21.0). Verify:
ansible --version
You will see the core version, the Python interpreter Ansible uses, and the config file location (shows None until you create one — we do that in Step 6).
Two version numbers to know: - ansible-core — the execution engine (ansible-playbook, ansible, ansible-galaxy) - ansible (the community package) — ansible-core plus a curated set of collections. When you brew install ansible, you get both.
Ansible authenticates over SSH using keys — more secure and automation-friendly than passwords:
ssh-keygen -t ed25519 -f ~/.ssh/ansible_lab -N "" -C "ansible-lab"
This creates two files: - ~/.ssh/ansible_lab — the private key (stays on your machine, never shared) - ~/.ssh/ansible_lab.pub — the public key (distributed to managed nodes)
The -N "" flag sets an empty passphrase so Ansible can connect non-interactively. In production you would use SSH agent forwarding or a passphrase-protected key with ssh-agent.
Create a Dockerfile that produces a minimal Ubuntu 22.04 container with SSH and Python — the only two prerequisites Ansible needs:
mkdir -p ~/ansible-lab
cat > ~/ansible-lab/Dockerfile <<'EOF'
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
openssh-server \
python3 \
python3-apt \
curl \
sudo \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash ansible && \
echo 'ansible ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
mkdir -p /home/ansible/.ssh && \
chmod 700 /home/ansible/.ssh && \
chown ansible:ansible /home/ansible/.ssh
RUN mkdir -p /var/run/sshd
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
EOF
docker build -t ansible-node ~/ansible-lab/
What this image contains: - openssh-server — so Ansible can connect via SSH - python3 — required for Ansible to execute modules - python3-apt — required for Ansible's apt and package modules on Debian/Ubuntu - An ansible OS user with passwordless sudo — Ansible will SSH in as this user - /var/run/sshd — the directory the SSH daemon needs at startup
# Remove any existing lab containers
docker rm -f web1 web2 db1 2>/dev/null || true
# Start fresh
docker run -d --name web1 --hostname web1 -p 2201:22 ansible-node
docker run -d --name web2 --hostname web2 -p 2202:22 ansible-node
docker run -d --name db1 --hostname db1 -p 2203:22 ansible-node
The --hostname flag is important. Without it, Docker assigns the container ID as the system hostname (a long hex string). Setting it explicitly means ansible_hostname (a fact) returns web1, web2, or db1 — matching your inventory names.
Verify all three are running:
docker ps --filter name=web1 --filter name=web2 --filter name=db1
You should get a result like this:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e9a95c4cf3d5 ansible-node "/usr/sbin/sshd -D" 7 seconds ago Up 7 seconds 0.0.0.0:2203->22/tcp, [::]:2203->22/tcp db1
01eebe0a7dba ansible-node "/usr/sbin/sshd -D" 8 seconds ago Up 7 seconds 0.0.0.0:2202->22/tcp, [::]:2202->22/tcp web2
e526adaf7693 ansible-node "/usr/sbin/sshd -D" 8 seconds ago Up 8 seconds 0.0.0.0:2201->22/tcp, [::]:2201->22/tcp web1
Copy your public key into each container so Ansible can authenticate without a password:
for port in 2201 2202 2203; do
docker exec $(docker ps -q --filter publish=$port) \
bash -c "echo '$(cat ~/.ssh/ansible_lab.pub)' >> /home/ansible/.ssh/authorized_keys && \
chmod 600 /home/ansible/.ssh/authorized_keys && \
chown -R ansible:ansible /home/ansible/.ssh"
done
Test the SSH connection manually to confirm it works:
ssh -i ~/.ssh/ansible_lab -p 2201 -o StrictHostKeyChecking=no ansible@localhost "hostname"
Expected output: web1. Repeat for ports 2202 and 2203 to verify all three nodes.
Create the lab workspace and configuration:
mkdir -p ~/ansible-lab/lab/inventory
cat > ~/ansible-lab/lab/ansible.cfg <<'EOF'
[defaults]
inventory = ./inventory/hosts.ini
remote_user = ansible
private_key_file = ~/.ssh/ansible_lab
host_key_checking = False
interpreter_python = /usr/bin/python3
[privilege_escalation]
become = False
become_method = sudo
EOF
ansible.cfg is read from the current directory first, then ~/.ansible.cfg, then /etc/ansible/ansible.cfg. Keeping it in the project directory means your settings are project-scoped.
Create the inventory file:
cat > ~/ansible-lab/lab/inventory/hosts.ini <<'EOF'
[webservers]
web1 ansible_host=localhost ansible_port=2201
web2 ansible_host=localhost ansible_port=2202
[dbservers]
db1 ansible_host=localhost ansible_port=2203
EOF
cd ~/ansible-lab/lab && ansible all -m ping
Expected output:
web1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
web2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
db1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
Three pong responses mean SSH authentication, Python execution, and module communication are all working.
Run this to see how Ansible resolves your inventory:
cd ~/ansible-lab/lab && ansible-inventory --graph
Output:
@all:
|--@ungrouped:
|--@webservers:
| |--web1
| |--web2
|--@dbservers:
| |--db1
You can also inspect a specific host's variables:
ansible-inventory --host web1
This shows all variables Ansible knows about web1 — including the connection parameters from your inventory and any facts (if gathered).
Ad hoc commands let you run a single module directly against hosts without writing a playbook. They are useful for quick checks, one-time operations, and troubleshooting.
The syntax is:
ansible <host-pattern> -m <module> -a "<arguments>" [options]
Check the OS on all nodes using the setup module (which collects facts):
cd ~/ansible-lab/lab && ansible all -m setup -a "filter=ansible_distribution*"
This returns ansible_distribution (Ubuntu), ansible_distribution_version (22.04), and related facts for each node.
Run a shell command on all web servers:
cd ~/ansible-lab/lab && ansible webservers -m command -a "uptime"
Install a package on web servers (with --become to run as root):
cd ~/ansible-lab/lab && ansible webservers -m package -a "name=curl state=present" --become
Run this command twice. The first run reports changed (yellow) — curl was installed. The second reports ok (green) — curl is already present, nothing to do. The changed vs ok distinction is how Ansible tells you whether it actually modified the system.
Playbooks let you combine multiple tasks, target multiple groups, and express dependencies between operations.
cat > ~/ansible-lab/lab/site.yml <<'EOF'
---
- name: Configure web servers
hosts: webservers
become: true
tasks:
- name: Install curl
package:
name: curl
state: present
- name: Write a marker file
copy:
content: "Managed by Ansible on {{ ansible_hostname }}\n"
dest: /tmp/ansible-marker.txt
- name: Configure database servers
hosts: dbservers
become: true
tasks:
- name: Install curl
package:
name: curl
state: present
- name: Write a db marker file
copy:
content: "DB node {{ ansible_hostname }} managed by Ansible\n"
dest: /tmp/ansible-marker.txt
EOF
Run it:
cd ~/ansible-lab/lab && ansible-playbook site.yml
The PLAY RECAP at the bottom summarises ok, changed, unreachable, and failed counts per host. Run the playbook a second time — every task shows ok because the desired state is already in place.
Read the marker file from each node:
cd ~/ansible-lab/lab && ansible all -m command -a "cat /tmp/ansible-marker.txt"
Each node returns its own hostname in the content — because {{ ansible_hostname }} was resolved per-host when the copy task ran.
Hardcoding values in playbooks creates maintenance problems. Variables let you define values once and use them everywhere.
Create group variables for the web tier:
mkdir -p ~/ansible-lab/lab/group_vars
cat > ~/ansible-lab/lab/group_vars/webservers.yml <<'EOF'
app_port: 8080
app_name: meridian-web
EOF
cat > ~/ansible-lab/lab/group_vars/dbservers.yml <<'EOF'
db_port: 5432
db_name: meridian_db
EOF
Now write a playbook that uses these variables:
cat > ~/ansible-lab/lab/node-info.yml <<'EOF'
---
- name: Write node information to each host
hosts: all
become: true
tasks:
- name: Write node info file
copy:
content: |
Hostname: {{ ansible_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
App port: {{ app_port | default('N/A') }}
DB port: {{ db_port | default('N/A') }}
dest: /tmp/node_info.txt
EOF
cd ~/ansible-lab/lab && ansible-playbook node-info.yml
Then read the results:
cd ~/ansible-lab/lab && ansible all -m command -a "cat /tmp/node_info.txt"
Web servers show App port: 8080 from group_vars/webservers.yml. The database server shows DB port: 5432. Neither shows the other group's variable. This is group variable scoping working correctly — variables defined for a group are only available to hosts in that group.
The | default('N/A') filter handles hosts where a variable is not defined, preventing an "undefined variable" error.
Handlers solve a specific and common problem: you only want to restart a service if its configuration actually changed, not on every playbook run.
cat > ~/ansible-lab/lab/handlers-demo.yml <<'EOF'
---
- name: Demonstrate handlers
hosts: webservers
become: true
handlers:
- name: Reload nginx config
command: echo "nginx -s reload would run here"
tasks:
- name: Write nginx config
copy:
content: "# nginx config for {{ app_name }} on port {{ app_port }}\n"
dest: /tmp/nginx.conf
notify: Reload nginx config
EOF
cd ~/ansible-lab/lab && ansible-playbook handlers-demo.yml
Run it once — the copy task reports changed (the file is new), so the handler fires. Run it a second time — copy reports ok (file content matches), so the handler never runs.
Key handler behaviours: - A handler is only queued when its notifying task reports changed - If multiple tasks notify the same handler, it still runs exactly once - Handlers run after all regular tasks in the play complete — not immediately when notified - If a play fails before handlers run, the handlers are skipped (use --force-handlers to override)
Handler notification flow — a handler is queued only when a task reports changed, and fires once at the end of the play regardless of how many tasks notified it.
As playbooks grow, roles keep things manageable. A role groups tasks, handlers, variables, templates, and files into a single reusable directory structure.
cd ~/ansible-lab/lab && ansible-galaxy role init roles/webserver
Write tasks for the role:
cat > ~/ansible-lab/lab/roles/webserver/tasks/main.yml <<'EOF'
---
- name: Write web server configuration
copy:
content: |
# {{ app_name }} configuration
Port: {{ app_port }}
dest: /tmp/webserver.conf
notify: Reload web config
- name: Write uptime to status file
command: uptime
register: uptime_result
changed_when: false
- name: Write status file
copy:
content: "{{ uptime_result.stdout }}\n"
dest: /tmp/webserver-status.txt
EOF
cat > ~/ansible-lab/lab/roles/webserver/handlers/main.yml <<'EOF'
---
- name: Reload web config
command: echo "Reloading web config..."
EOF
cat > ~/ansible-lab/lab/roles/webserver/defaults/main.yml <<'EOF'
---
app_port: 80
app_name: default-app
EOF
Use the role in a playbook:
cat > ~/ansible-lab/lab/deploy.yml <<'EOF'
---
- name: Deploy web tier
hosts: webservers
become: true
roles:
- webserver
EOF
cd ~/ansible-lab/lab && ansible-playbook deploy.yml
The defaults/main.yml defines the lowest-priority default for app_port (80), but group_vars/webservers.yml defines app_port: 8080 at a higher precedence — so the group variable wins.
changed_when: false on the uptime command tells Ansible this command never constitutes a change (it's read-only), preventing it from being counted as a modification.
The distinction between modules and module utilities is worth understanding if you ever write custom automation or need to debug deep Ansible errors.
Modules are the units you call from tasks. Each module performs one discrete action: install a package, copy a file, create a user. Modules are self-contained — you specify one in a task and Ansible runs it.
Module utilities (in ansible/module_utils/) are shared Python libraries that modules import internally. They handle repetitive low-level logic — HTTP request handling, JSON argument parsing, file permission checks — so individual modules don't duplicate this code.
| Modules | Module Utilities | |
|---|---|---|
| What it does | Performs a task on a managed node | Provides shared helper functions to modules |
| Called from | Your playbook tasks | Inside module source code |
| You interact with it | Directly (in task definitions) | Rarely (only when writing custom modules) |
| Examples | package, copy, user, service |
basic.py, urls.py, common/file.py |
| Language | Any that outputs JSON | Python or PowerShell |
When Ansible transfers a module to a managed node, it also transfers any module utilities that module depends on. This is bundled transparently — you never see it unless you're writing a custom module.
Modules and plugins are both written in Python, but they solve different problems and run in different places.
Modules run on the managed node. They implement the "what to do" logic — install this package, copy this file, create this user. Modules are transferred to the remote system, executed, and their JSON output is read back by the control node.
Plugins run on the control node. They extend how Ansible itself behaves — how it connects to hosts, how it formats output, how it looks up data, how it handles inventory.
| Modules | Plugins | |
|---|---|---|
| Where it runs | Managed node | Control node |
| What it does | Performs a task on the remote system | Extends Ansible's core behaviour |
| Called from | Task definitions | Configured in ansible.cfg or used implicitly |
| Language | Any language outputting JSON | Python only |
| Examples | copy, package, git, uri |
ssh (connection), default (callback), aws_ec2 (inventory) |
A concrete example: when you run ansible-playbook site.yml -o json, you're using the json callback plugin — it changes how playbook output is formatted. That has nothing to do with what happens on managed nodes. But when a task runs copy:, the copy module is transferred to each managed node and executed there.
Ansible ships as a suite of CLI tools. Each serves a specific purpose:
ansible — Ad Hoc CommandsRun a single module against one or more hosts without writing a playbook:
# Ping all hosts
ansible all -m ping
# Run a shell command on web servers
ansible webservers -m command -a "df -h /"
# Install a package
ansible all -m package -a "name=htop state=present" --become
# Gather facts about a specific host
ansible web1 -m setup
ansible-playbook — Run PlaybooksThe primary way to run automation:
# Basic run
ansible-playbook site.yml
# Target a specific host or group (overrides hosts: in the playbook)
ansible-playbook site.yml --limit web1
# Dry run — shows what would change without applying anything
ansible-playbook site.yml --check
# Dry run + show diff of file changes
ansible-playbook site.yml --check --diff
# Override a variable at runtime
ansible-playbook site.yml -e "app_port=9090"
# Use a different inventory
ansible-playbook site.yml -i inventories/prod/hosts.yml
# Verbose output (use -vv or -vvv for more detail)
ansible-playbook site.yml -v
The --check and --diff flags are essential before applying changes to production. They connect to real hosts and evaluate tasks against the real current state, but apply nothing.
ansible-inventory — Inspect Inventory# View inventory as a tree
ansible-inventory --graph
# View all variables for a specific host
ansible-inventory --host web1
# Export inventory as JSON
ansible-inventory --list
ansible-galaxy — Manage Roles and Collections# Install a role from Ansible Galaxy
ansible-galaxy role install geerlingguy.nginx
# Initialize a new role skeleton
ansible-galaxy role init roles/myapp
# Install a collection
ansible-galaxy collection install community.docker
# List installed collections
ansible-galaxy collection list
ansible-vault — Manage Encrypted Secrets# Create a new encrypted file
ansible-vault create vault.yml
# Encrypt an existing file
ansible-vault encrypt secrets.yml
# View encrypted file contents
ansible-vault view vault.yml
# Edit encrypted file
ansible-vault edit vault.yml
# Decrypt a file permanently
ansible-vault decrypt vault.yml
# Run a playbook with vault password from file
ansible-playbook site.yml --vault-password-file ~/.vault_pass
ansible-doc — Offline Module Documentation# View full documentation for a module
ansible-doc copy
# List all available modules
ansible-doc -l
# Show a short description of a module
ansible-doc -s user
ansible-doc is invaluable. It shows every argument a module accepts, their types, required/optional status, and examples — all offline without opening a browser.
ansible-lint — Static Analysisansible-lint is a separate tool (install with pip3 install ansible-lint) that analyses your playbooks and roles for best-practice violations without connecting to any host:
# Lint the entire project
ansible-lint
# Lint a specific playbook
ansible-lint site.yml
# List all available rules
ansible-lint --list-rules
Common violations ansible-lint catches: - Tasks without a name: field - Using command: when a dedicated module exists (e.g., command: git pull instead of the git module) - YAML values like yes/no instead of true/false - Role names containing dashes (breaks collection namespacing) - Missing changed_when on command/shell tasks
Production-scale Ansible builds on these fundamentals with several additional layers:
Multi-environment inventories — A inventories/ directory with separate dev/, staging/, and prod/ subdirectories. Each environment has its own hosts.yml and group_vars/. Running ansible-playbook site.yml targets dev by default. Targeting prod requires an explicit -i inventories/prod/hosts.yml. Dev changes structurally cannot reach prod.
Ansible Vault — AES-256 encryption for secrets directly in your repository. Database passwords, API tokens, and private keys live in encrypted vault.yml files that are committed to Git (the ciphertext is safe to store). The vault decryption password lives outside Git, distributed via a team password manager. The vault indirection pattern — vault.yml holds the encrypted secret, vars.yml references it by variable name — means plaintext variable names appear in search results without ever exposing values.
Per-environment vault IDs — Dev has one vault password (all developers know it). Prod has a different vault password (only senior engineers or CI systems know it). When a developer leaves, you rotate only the passwords for environments they accessed. Per-developer SSH key revocation works the same way — remove one public key file, re-run bootstrap.
Git hygiene — .gitignore blocks vault password files (*.vault_pass, .vault_pass). Pre-commit hooks via detect-secrets and ansible-vault-encrypted reject any commit that includes a plaintext secret or an unencrypted vault file.
The verification workflow — Before any production deployment:
# 1. Static analysis — no hosts needed
ansible-lint
# 2. Syntax check — no hosts needed
ansible-playbook site.yml --syntax-check
# 3. Dry run against dev
ansible-playbook -i inventories/dev/hosts.yml site.yml --check --diff
# 4. Real run against dev
ansible-playbook -i inventories/dev/hosts.yml site.yml
# 5. Dry run against prod (with peer review)
ansible-playbook -i inventories/prod/hosts.yml site.yml --check --diff
# 6. Real run against prod
ansible-playbook -i inventories/prod/hosts.yml site.yml
Steps 1 and 2 are cheap — run them in CI on every pull request. Steps 3-6 require real hosts.
The Devoriales Ansible course covers everything in this article interactively — real terminal in the browser, verification checks after every step, quizzes after every lesson:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。