Ansible Generator
Generates, validates, and refactors production-ready Ansible playbooks, roles, task files, and inventory configurations following current best practices. Use when the user asks to create, build, or generate Ansible automation, YAML playbooks, infrastructure as code, configuration management files, DevOps roles, or .yml files for Ansible — including requests like "create a playbook to...", "build a role for...", "generate an inventory for...", or "set up Ansible to automate...". Automatically validates all output using the devops-skills:ansible-validator skill.
npx skills add github:pantheon-org/tekhne --skill infrastructure--ansible--generatorOverview
Generate production-ready Ansible resources (playbooks, roles, task files, inventory files, project configs) following current best practices, naming conventions, and security standards. All generated resources are validated using the devops-skills:ansible-validator skill before delivery.
Philosophy
- Generate secure, idempotent automation by default: FQCN modules, named tasks,
no_logfor secrets, strict file modes. - Start from the shipped templates and references rather than from a blank file.
- Validation is part of generation, not an optional extra.
- Prefer builtin modules; fall back to them when a collection is unavailable.
When to Use
- The user asks to create, build or generate an Ansible playbook, role, task file or inventory.
- The user wants project configuration such as
ansible.cfg,requirements.ymlor.ansible-lint. - An existing Ansible file needs refactoring to current standards (FQCN,
true/falsebooleans,dnfon RHEL). - A request involves a non-builtin collection and needs current module documentation looked up.
When Not to Use
- Only validating or linting existing Ansible code without changing it (use the ansible-validator skill).
- Generating Kubernetes, Terraform or Helm resources (use the matching generator skill).
- Executing playbooks against live hosts, which this skill does not do.
- Producing documentation snippets where full validation is not wanted.
Core Capabilities
All capabilities follow the same validation loop: generate → invoke
devops-skills:ansible-validator→ fix errors → re-validate → present output. See Validation Workflow for full details.
1. Generate Playbooks
Process:
- Clarify hosts, privileges, OS
- Read
references/best-practices.mdandreferences/module-patterns.md - Use
assets/templates/playbook/basic_playbook.ymlas structural reference - Generate following mandatory standards (see Mandatory Standards)
Example structure:
---
# Playbook: <title>
# Description: <what it does>
# Requirements: Ansible 2.10+, <OS>
# Variables:
# - <var_name>: <description> (default: <value>)
# Usage: ansible-playbook -i inventory/<env> <playbook>.yml
- name: <Verb phrase describing the play>
hosts: <group>
become: true
gather_facts: true
vars:
app_port: 8080
pre_tasks:
- name: <Setup steps>
# ...
tasks:
- name: <Verb-first task name>
ansible.builtin.<module>:
# parameters
tags: [<tag1>, <tag2>]
post_tasks:
- name: <Verification steps>
# ...
handlers:
- name: <Handler name>
ansible.builtin.service:
name: <service>
state: reloaded
2. Generate Roles
Process:
- Clarify role purpose and scope
- Copy and customize the full role structure from
assets/templates/role/:tasks/main.yml,handlers/main.yml,templates/,files/vars/main.yml,vars/Debian.yml,vars/RedHat.ymldefaults/main.yml,meta/main.yml,meta/argument_specs.yml(Ansible 2.11+),README.md
- Replace all
[PLACEHOLDERS]:[ROLE_NAME],[role_name],[PLAYBOOK_DESCRIPTION],[package_name],[service_name],[default_port] - Prefix all role variables with the role name (e.g.,
nginx_port,nginx_worker_processes) - Use
include_varsfor OS-specific variables
meta/argument_specs.yml enables automatic variable validation (Ansible 2.11+).
3. Generate Task Files
Process:
- Define the operation
- Reference
references/module-patterns.mdfor module usage - Generate with: verb-first names, FQCN modules, idempotency checks, tags
See assets/templates/ for full task file examples (e.g., database backup, user management).
4. Generate Inventory Files
Process:
- Understand infrastructure topology
- Use
assets/templates/inventory/as reference:hosts— main inventory (INI for simple; YAML for complex hierarchies)group_vars/all.yml,group_vars/[groupname].yml,host_vars/[hostname].yml
- Organize hosts into logical groups (functional, environment, geographic)
- Define variables at appropriate levels: all → group → host
Dynamic inventory (cloud): Use provider plugins configured from references/module-patterns.md:
- AWS EC2:
plugin: amazon.aws.aws_ec2 - Azure:
plugin: azure.azcollection.azure_rm
5. Generate Project Configuration Files
Use templates from assets/templates/project/:
ansible.cfg— forks, timeout, pathsrequirements.yml— collections and roles dependencies.ansible-lint— lint rules
6. Handling Custom Modules and Collections
When a user mentions a non-builtin collection (e.g., kubernetes.core, amazon.aws, community.docker):
- Search for current documentation:
"ansible [collection.name] [module] latest documentation examples" - If Context7 MCP is available: Use
mcp__context7__resolve-library-idthenmcp__context7__get-library-docs - Generate using discovered info: correct FQCN, current parameters, collection install instructions
Include installation instructions in comments:
# Requirements:
# - ansible-galaxy collection install kubernetes.core:2.4.0
# or in requirements.yml:
# collections:
# - name: kubernetes.core
# version: "2.4.0"
Mandatory Standards
All generated resources must follow these standards. See references/best-practices.md for full details and rationale.
Key rules at a glance:
| Standard | Correct | Incorrect |
|---|---|---|
| FQCN | ansible.builtin.copy |
copy |
| Booleans | true/false |
yes/no |
| RHEL packages | ansible.builtin.dnf |
ansible.builtin.yum |
| Secrets | no_log: true |
plain logging |
| File perms | '0644' configs, '0600' secrets |
world-writable |
Builtin Fallback Pattern
When validation fails due to missing collections, rewrite using builtins:
# Preferred (requires community.postgresql):
# - community.postgresql.postgresql_db: {name: mydb, state: present}
# Builtin fallback:
- name: Check if database exists
ansible.builtin.command:
cmd: psql -tAc "SELECT 1 FROM pg_database WHERE datname='mydb'"
become: true
become_user: postgres
register: db_check
changed_when: false
- name: Create database
ansible.builtin.command:
cmd: psql -c "CREATE DATABASE mydb"
become: true
become_user: postgres
when: db_check.stdout != "1"
changed_when: true
Common Patterns
Multi-OS Support
- name: Install nginx (Debian/Ubuntu)
ansible.builtin.apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install nginx (RHEL 8+)
ansible.builtin.dnf:
name: nginx
state: present
when: ansible_os_family == "RedHat"
Async Long-Running Tasks
- name: Run database migration
ansible.builtin.command: /opt/app/migrate.sh
async: 3600
poll: 0
register: migration
- name: Check migration status
ansible.builtin.async_status:
jid: "{{ migration.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 360
delay: 10
Validation Workflow
Every generated resource must be validated before presenting to the user.
- Generate the Ansible file
- Invoke
devops-skills:ansible-validator - If validation fails → fix errors → re-validate
- If validation passes → present using the required output format
Skip validation only when: generating partial snippets, documentation examples, or when the user explicitly requests to skip.
Required Output Format
## Generated [Resource Type]: [Name]
**Validation Status:** ✅ All checks passed
- YAML syntax: Passed
- Ansible syntax: Passed
- Ansible lint: Passed
**Summary:**
- [What was generated and key decisions]
**Usage:**
```bash
[Exact command]
```
**Prerequisites:**
- [Required collections, system requirements]
Anti-Patterns
NEVER use gather_facts: true by default for large inventories
- WHY: Fact gathering adds 2-5 seconds per host at connection time; for playbooks targeting hundreds of hosts this significantly increases total runtime for plays that do not need facts.
- BAD: Relying on the default
gather_factsbehaviour in every play, including utility plays that never referenceansible_*variables. - GOOD: Set
gather_facts: falseglobally inansible.cfgand enable it per-play only when facts are actually needed (conditionals, templates usingansible_os_family, etc.).
NEVER store secrets in group_vars/ plaintext files
- WHY: Any plaintext password or API key committed to
group_vars/is permanently exposed in source control history, even after deletion. - BAD:
ansible_become_password: mypasswordingroup_vars/all.ymlcommitted to the repository. - GOOD: Use Ansible Vault (
ansible-vault encrypt_string) or an external secrets manager (HashiCorp Vault, AWS Secrets Manager) and reference values via lookup plugins.
NEVER use the shell or command module when a dedicated module exists
- WHY:
shellandcommandbypass idempotency guarantees, built-in error handling, and change detection that dedicated modules provide; they also resist linting and security scanning. - BAD:
ansible.builtin.shell: pip install requestsinstead of using thepipmodule. - GOOD:
ansible.builtin.pip: name: requests state: present: use the purpose-built module so Ansible can detect and report actual state changes.
NEVER write tasks without name: fields
- WHY: Unnamed tasks produce unreadable playbook output and make debugging nearly impossible when a play contains many tasks; they also fail
ansible-lintname rules. - BAD:
- apt: name=nginx state=presentwith noname:field. - GOOD: Always prefix every task with a descriptive
name:, e.g.,- name: Install nginx web server.
NEVER use ignore_errors: true as a general exception handler
- WHY:
ignore_errors: truesilently swallows all failures and lets the playbook continue in a potentially broken state, masking errors that affect downstream tasks. - BAD:
ignore_errors: trueon a package installation task where failure means the service cannot start. - GOOD: Use
failed_whenwith specific conditions to define expected failure states, or useblock/rescue/alwaysfor structured error handling with recovery logic.
NEVER use short module names
- WHY: The Mandatory Standards require fully qualified collection names so each task is unambiguous and passes lint.
- BAD:
copy:orapt:as the module key. - GOOD:
ansible.builtin.copy:andansible.builtin.apt:.
NEVER write yes/no booleans
- WHY: The Mandatory Standards require
true/false, which is the form current lint rules expect. - BAD:
become: yes - GOOD:
become: true
NEVER use ansible.builtin.yum for RHEL packages
- WHY: The Mandatory Standards specify
ansible.builtin.dnffor RHEL, and the multi-OS pattern uses it for RHEL 8+. - BAD:
ansible.builtin.yum: name=nginx state=present - GOOD:
ansible.builtin.dnf:withname: nginxandstate: present.
NEVER leave secret-handling tasks without no_log: true
- WHY: Without it, passwords and tokens are printed in plain text in play output and logs.
- BAD: A task that sets a database password and logs its parameters.
- GOOD: Add
no_log: trueto the task and keep the value in Ansible Vault.
NEVER create files with permissive modes
- WHY: The standards call for
'0644'on configs and'0600'on secrets; world-writable files are a security defect. - BAD:
mode: '0777'on a template that holds credentials. - GOOD:
mode: '0600'for secrets andmode: '0644'for ordinary configs.
NEVER present generated output without validating it
- WHY: The Validation Workflow requires every generated resource to pass the ansible-validator skill (YAML syntax, Ansible syntax, lint) before delivery.
- BAD: Hand over a freshly written playbook with no validation run.
- GOOD: Generate, validate, fix errors, re-validate, then present using the required output format.
NEVER leave template [PLACEHOLDERS] in delivered roles
- WHY: Unreplaced placeholders such as
[ROLE_NAME]or[package_name]break the role and show the template was not customised. - BAD: Copy
assets/templates/role/and deliver it with[ROLE_NAME]still intasks/main.yml. - GOOD: Replace every placeholder and prefix role variables with the role name (e.g.
nginx_port).
References
References (read at generation start)
references/best-practices.md— directory structures, naming conventions, security, performance, common pitfallsreferences/module-patterns.md— module usage patterns, copy-paste examples for all common modules
Assets (structural templates)
assets/templates/playbook/basic_playbook.yml— playbook structure referenceassets/templates/role/*— role directory structure and variable conventionsassets/templates/inventory/*— host grouping and group_vars/host_vars patternsassets/templates/project/*—ansible.cfg,requirements.yml,.ansible-lint
Template usage: Review structure → generate following the same pattern → replace [PLACEHOLDERS] → customize for requirements → remove inapplicable sections → validate.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
Ansible Best Practices
Directory Structure
Standard Playbook Structure
playbook.yml
roles/
common/
tasks/
main.yml
handlers/
main.yml
templates/
files/
vars/
main.yml
defaults/
main.yml
meta/
main.yml
inventory/
production/
hosts
group_vars/
host_vars/
staging/
hosts
group_vars/
host_vars/
Role Structure
Each role should have:
tasks/main.yml- Main task listhandlers/main.yml- Handlers triggered by taskstemplates/- Jinja2 templatesfiles/- Static files to copyvars/main.yml- Role-specific variables (high priority)defaults/main.yml- Default variables (low priority, overridable)meta/main.yml- Role dependencies and metadata
Naming Conventions
Files and Directories
- Use lowercase with underscores:
install_nginx.yml,backup_database.yml - Playbook files: descriptive names ending in
.yml - Role names: short, descriptive, lowercase with underscores
Variables
- Use descriptive names:
nginx_port,db_backup_dir,app_version - Prefix role-specific variables with role name:
nginx_worker_processes - Use snake_case, not camelCase or kebab-case
- Group related variables with common prefixes
Tasks
- Use descriptive names that explain what the task does
- Start with a verb: “Install nginx”, “Copy configuration file”, “Start service”
Task Writing Best Practices
Always Use State Declaration
# Good
- name: Ensure nginx is installed
ansible.builtin.package:
name: nginx
state: present
# Bad
- name: Install nginx
ansible.builtin.package:
name: nginx
Use Fully Qualified Collection Names (FQCN)
# Good - FQCN (Ansible 2.10+)
- name: Copy configuration file
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
# Avoid - Short names (deprecated)
- name: Copy configuration file
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
Idempotency
- All tasks should be idempotent (safe to run multiple times)
- Use
state: present/absentinstead of imperative commands - Avoid using
commandorshellmodules when builtin modules exist - When using
command/shell, usecreates,removes, orchanged_when
# Good - idempotent
- name: Create directory
ansible.builtin.file:
path: /opt/app
state: directory
mode: '0755'
# Bad - not idempotent
- name: Create directory
ansible.builtin.command: mkdir -p /opt/app
Error Handling
- name: Attempt to start service
ansible.builtin.service:
name: myapp
state: started
register: service_result
failed_when: false
changed_when: service_result.rc == 0
- name: Handle service failure
ansible.builtin.debug:
msg: "Service failed to start: {{ service_result.msg }}"
when: service_result.failed
Variables and Facts
Variable Precedence (High to Low)
- Extra vars (
-ein CLI) - Task vars
- Block vars
- Role and include vars
- Set_facts / registered vars
- Play vars
- Play vars_files
- Role defaults
- Inventory vars (host_vars, group_vars)
Using Variables
# Use default values
- name: Set port with default
ansible.builtin.set_fact:
app_port: "{{ custom_port | default(8080) }}"
# Combine variables
- name: Create full path
ansible.builtin.set_fact:
config_path: "{{ base_dir }}/{{ app_name }}/config.yml"
Conditionals and Loops
When Statements
- name: Install on Debian-based systems
ansible.builtin.apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install on RedHat-based systems
ansible.builtin.yum:
name: nginx
state: present
when: ansible_os_family == "RedHat"
Loops
# Good - using loop
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis
# Complex loop with dict
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: 'alice', groups: 'admin,developers' }
- { name: 'bob', groups: 'developers' }
Handlers
Naming and Usage
# In tasks/main.yml
- name: Copy nginx configuration
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
notify: Restart nginx
# In handlers/main.yml
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Handler Best Practices
- Handlers run once at the end of a play
- Use descriptive names
- Listen to multiple notifications with same handler name
- Use
meta: flush_handlersto run handlers immediately if needed
Templates
Jinja2 Templates
# Task
- name: Deploy configuration from template
ansible.builtin.template:
src: app_config.j2
dest: /etc/app/config.yml
mode: '0644'
backup: yes
# Template file: templates/app_config.j2
server:
port: {{ app_port }}
host: {{ ansible_default_ipv4.address }}
database:
host: {{ db_host }}
port: {{ db_port | default(5432) }}
name: {{ db_name }}
{% if enable_ssl %}
ssl:
enabled: true
cert: {{ ssl_cert_path }}
key: {{ ssl_key_path }}
{% endif %}
Advanced Jinja2 Templating
Common Filters
Data Format Conversion
- name: Convert to JSON
ansible.builtin.copy:
content: "{{ my_dict | to_json }}"
dest: /tmp/config.json
- name: Convert to YAML
ansible.builtin.copy:
content: "{{ my_dict | to_yaml }}"
dest: /tmp/config.yml
- name: Convert to pretty JSON
ansible.builtin.copy:
content: "{{ my_dict | to_nice_json }}"
dest: /tmp/config.json
# Parse JSON/YAML strings
- name: Parse JSON string
ansible.builtin.set_fact:
parsed_data: "{{ json_string | from_json }}"
- name: Parse YAML string
ansible.builtin.set_fact:
parsed_data: "{{ yaml_string | from_yaml }}"
String Manipulation
# Regex operations
- name: Replace text
ansible.builtin.set_fact:
new_string: "{{ original | regex_replace('^old', 'new') }}"
- name: Extract with regex
ansible.builtin.set_fact:
extracted: "{{ text | regex_search('version: (\\d+\\.\\d+)', '\\1') }}"
# Case conversion
- name: Convert case
ansible.builtin.set_fact:
upper: "{{ text | upper }}"
lower: "{{ text | lower }}"
title: "{{ text | title }}"
# String operations
- name: String operations
ansible.builtin.set_fact:
trimmed: "{{ ' text ' | trim }}"
replaced: "{{ text | replace('old', 'new') }}"
split_list: "{{ 'a,b,c' | split(',') }}"
joined: "{{ ['a', 'b', 'c'] | join('-') }}"
Hashing and Encoding
# Hash values
- name: Generate hashes
ansible.builtin.set_fact:
md5_hash: "{{ 'mystring' | hash('md5') }}"
sha256_hash: "{{ 'mystring' | hash('sha256') }}"
# Password hashing
- name: Hash password
ansible.builtin.user:
name: myuser
password: "{{ user_password | password_hash('sha512', 'mysecretsalt') }}"
# Encoding
- name: Encode/decode
ansible.builtin.set_fact:
base64_encoded: "{{ 'text' | b64encode }}"
base64_decoded: "{{ encoded_value | b64decode }}"
url_encoded: "{{ url_string | urlencode }}"
List and Dict Operations
# List operations
- name: List operations
ansible.builtin.set_fact:
unique_items: "{{ my_list | unique }}"
sorted_items: "{{ my_list | sort }}"
first_item: "{{ my_list | first }}"
last_item: "{{ my_list | last }}"
list_length: "{{ my_list | length }}"
flattened: "{{ nested_list | flatten }}"
# Dict operations
- name: Dict operations
ansible.builtin.set_fact:
dict_keys: "{{ my_dict | dict2items }}"
dict_values: "{{ my_dict | list }}"
combined: "{{ dict1 | combine(dict2) }}"
# Extract values
- name: Extract from list of dicts
ansible.builtin.set_fact:
names: "{{ users | map(attribute='name') | list }}"
ids: "{{ items | map(attribute='id') | list }}"
Network Filters
# IP address operations (requires netaddr Python package)
- name: IP operations
ansible.builtin.set_fact:
is_valid: "{{ ip_address | ipaddr }}"
network: "{{ ip_address | ipaddr('network') }}"
netmask: "{{ ip_address | ipaddr('netmask') }}"
broadcast: "{{ ip_address | ipaddr('broadcast') }}"
host_ip: "{{ ip_address | ipaddr('address') }}"
# CIDR operations
- name: CIDR operations
ansible.builtin.set_fact:
hosts_in_network: "{{ '192.168.1.0/24' | ipaddr('size') }}"
first_host: "{{ '192.168.1.0/24' | ipaddr('1') | ipaddr('address') }}"
File and Math Filters
# File size formatting
- name: Format file size
ansible.builtin.debug:
msg: "File size: {{ file_stat.stat.size | filesizeformat }}"
# Math operations
- name: Math operations
ansible.builtin.set_fact:
sum: "{{ [1, 2, 3] | sum }}"
min: "{{ [5, 2, 8] | min }}"
max: "{{ [5, 2, 8] | max }}"
rounded: "{{ 3.14159 | round(2) }}"
absolute: "{{ -42 | abs }}"
Default and Mandatory Values
# Provide defaults
- name: Use default values
ansible.builtin.set_fact:
port: "{{ custom_port | default(8080) }}"
config: "{{ app_config | default({}) }}"
# Nested defaults (Ansible 2.8+)
- name: Nested default
ansible.builtin.set_fact:
value: "{{ foo.bar.baz | default('fallback') }}"
# Mandatory values
- name: Require variable
ansible.builtin.set_fact:
required_value: "{{ must_be_defined | mandatory }}"
Lookup Plugins
File and Environment Lookups
# Read file content
- name: Read SSH public key
ansible.builtin.authorized_key:
user: deploy
key: "{{ lookup('file', '/home/user/.ssh/id_rsa.pub') }}"
# Environment variables
- name: Get environment variable
ansible.builtin.set_fact:
home_dir: "{{ lookup('env', 'HOME') }}"
path: "{{ lookup('env', 'PATH') }}"
# Pipe command output
- name: Get command output
ansible.builtin.set_fact:
current_date: "{{ lookup('pipe', 'date +%Y-%m-%d') }}"
git_commit: "{{ lookup('pipe', 'git rev-parse HEAD') }}"
Template and URL Lookups
# Template lookup
- name: Inline template
ansible.builtin.set_fact:
greeting: "{{ lookup('template', 'greeting.j2') }}"
# URL content
- name: Fetch URL content
ansible.builtin.set_fact:
remote_content: "{{ lookup('url', 'https://api.example.com/config') }}"
Password and Random Lookups
# Generate random password
- name: Generate password
ansible.builtin.set_fact:
random_password: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters,digits') }}"
# Random choice
- name: Pick random item
ansible.builtin.set_fact:
random_server: "{{ lookup('random_choice', ['server1', 'server2', 'server3']) }}"
Query vs Lookup
# lookup returns comma-separated string
- name: Using lookup
ansible.builtin.debug:
msg: "{{ lookup('file', 'file1.txt', 'file2.txt') }}"
# Returns: "content1,content2"
# query always returns list
- name: Using query
ansible.builtin.debug:
msg: "{{ query('file', 'file1.txt', 'file2.txt') }}"
# Returns: ["content1", "content2"]
# Prefer query for loops
- name: Loop with query
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', 'all') }}"
Template Control Structures
Loops in Templates
{# templates/config.j2 #}
# User list
{% for user in users %}
user {{ user.name }}:
uid: {{ user.uid }}
groups: {{ user.groups | join(',') }}
{% endfor %}
# Conditional in loop
{% for item in items if item.enabled %}
- {{ item.name }}: {{ item.value }}
{% endfor %}
# Loop with index
{% for server in servers %}
server_{{ loop.index }}: {{ server.hostname }}
{% endfor %}
Conditionals in Templates
{# templates/app_config.j2 #}
{% if environment == 'production' %}
log_level: warning
max_connections: 1000
{% elif environment == 'staging' %}
log_level: info
max_connections: 500
{% else %}
log_level: debug
max_connections: 100
{% endif %}
# Complex conditions
{% if ansible_os_family == 'Debian' and ansible_distribution_major_version|int >= 20 %}
use_modern_config: true
{% endif %}
# Check if defined
{% if custom_setting is defined %}
custom_setting: {{ custom_setting }}
{% endif %}
# Check if none
{% if database_host is none %}
database_host: localhost
{% else %}
database_host: {{ database_host }}
{% endif %}
Whitespace Control
{# Remove whitespace before #}
{%- if condition %}
content
{% endif %}
{# Remove whitespace after #}
{% if condition -%}
content
{% endif %}
{# Remove both #}
{%- if condition -%}
content
{%- endif -%}
Macros and Includes
{# Define macro #}
{% macro render_user(name, uid) -%}
user: {{ name }}
uid: {{ uid }}
{%- endmacro %}
{# Use macro #}
{{ render_user('alice', 1000) }}
{{ render_user('bob', 1001) }}
{# Include other template #}
{% include 'header.j2' %}
{# Import macros from other template #}
{% from 'macros.j2' import render_user %}
Advanced Template Patterns
Multi-line Strings
server {
listen 80;
server_name {{ server_name }};
{% if ssl_enabled %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
location / {
proxy_pass http://{{ backend_host }}:{{ backend_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Complex Data Structures
{# Nested loops for complex config #}
{% for service in services %}
[{{ service.name }}]
{% for key, value in service.config.items() %}
{{ key }} = {{ value }}
{% endfor %}
{% endfor %}
{# Generate from dict #}
{% for key, value in app_settings.items() %}
export {{ key | upper }}="{{ value }}"
{% endfor %}
Security Best Practices
Sensitive Data
# Use no_log for sensitive operations
- name: Set database password
ansible.builtin.user:
name: dbadmin
password: "{{ db_password | password_hash('sha512') }}"
no_log: true
# Use ansible-vault for secrets
# Encrypt with: ansible-vault encrypt secrets.yml
# Include encrypted vars
- name: Include vault variables
ansible.builtin.include_vars:
file: secrets.yml
File Permissions
- name: Copy sensitive file
ansible.builtin.copy:
src: private_key
dest: /etc/ssl/private/app.key
mode: '0600'
owner: root
group: root
Tags
Using Tags
- name: Install packages
ansible.builtin.package:
name: nginx
state: present
tags:
- packages
- nginx
- install
# Run with: ansible-playbook playbook.yml --tags "install"
# Skip with: ansible-playbook playbook.yml --skip-tags "install"
Tag Categories
install- Installation tasksconfigure- Configuration tasksupdate- Update tasksbackup- Backup tasksalways- Always run (special tag)never- Never run unless explicitly called (special tag)
Playbook Structure
Complete Playbook Example
---
- name: Deploy web application
hosts: webservers
become: yes
vars:
app_version: "1.2.3"
app_port: 8080
pre_tasks:
- name: Update package cache
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
roles:
- common
- nginx
- application
post_tasks:
- name: Verify application is running
ansible.builtin.uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 5
delay: 10
handlers:
- name: Restart application
ansible.builtin.service:
name: myapp
state: restarted
Testing and Validation
Check Mode (Dry Run)
# Run in check mode
ansible-playbook playbook.yml --check
# Task that always runs in check mode
- name: Get service status
ansible.builtin.command: systemctl status nginx
check_mode: no
changed_when: false
Diff Mode
# Show differences
ansible-playbook playbook.yml --check --diff
Assert and Validate
- name: Verify configuration
ansible.builtin.assert:
that:
- ansible_distribution in ['Ubuntu', 'Debian', 'CentOS', 'RedHat']
- app_port | int > 0
- app_port | int < 65536
fail_msg: "Invalid configuration"
success_msg: "Configuration validated"
Performance Optimization
Gathering Facts
# Disable fact gathering when not needed
- name: Quick task
hosts: all
gather_facts: no
tasks:
- name: Ping hosts
ansible.builtin.ping:
# Gather specific facts
- name: Gather minimal facts
hosts: all
gather_facts: yes
gather_subset:
- '!all'
- '!min'
- network
Parallelism
# Set forks in ansible.cfg or via CLI
# ansible-playbook playbook.yml --forks 20
# Control serial execution
- name: Rolling update
hosts: webservers
serial: 2 # Update 2 hosts at a time
Async Tasks
- name: Long running task
ansible.builtin.command: /opt/long_running_script.sh
async: 3600 # Maximum runtime
poll: 0 # Fire and forget
register: long_task
- name: Check on long task
ansible.builtin.async_status:
jid: "{{ long_task.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 30
delay: 10
Documentation
Playbook Documentation
---
# playbook.yml
# Description: Deploy and configure web application
# Requirements:
# - Ansible 2.10+
# - Target hosts: Ubuntu 20.04+ or RHEL 8+
# Variables:
# - app_version: Application version to deploy (required)
# - app_port: Port for application (default: 8080)
# - enable_ssl: Enable SSL/TLS (default: false)
# Usage:
# ansible-playbook -i inventory/production playbook.yml -e "app_version=1.2.3"
Role Documentation (meta/main.yml)
---
galaxy_info:
role_name: nginx
author: Your Name
description: Install and configure nginx
license: MIT
min_ansible_version: 2.10
platforms:
- name: Ubuntu
versions:
- focal
- jammy
- name: EL
versions:
- 8
- 9
galaxy_tags:
- web
- nginx
dependencies: []
Common Pitfalls to Avoid
- Not using FQCN - Always use fully qualified collection names
- Hard-coded values - Use variables for configuration
- Not handling different OS - Check
ansible_os_familyoransible_distribution - Ignoring idempotency - Tasks should be safe to run multiple times
- Not using handlers - Restart services via handlers, not direct tasks
- Sensitive data in plain text - Use ansible-vault for secrets
- Not using tags - Tags enable selective execution
- Not validating - Always run with
--checkfirst - Complex logic in playbooks - Move complex logic to roles
- Not documenting variables - Document required and optional vars
Module Selection Priority
- Builtin modules first: Use
ansible.builtin.*modules when available - Collection modules: Use official collection modules (e.g.,
community.general.*) - Custom modules: Only when no suitable module exists
- Avoid
command/shell: Use specific modules instead of raw commands
Common Ansible Module Usage Patterns
Core Modules (ansible.builtin)
Package Management
ansible.builtin.package (Universal)
- name: Install package (OS-agnostic)
ansible.builtin.package:
name: nginx
state: present
ansible.builtin.apt (Debian/Ubuntu)
- name: Install package with apt
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
- name: Install specific version
ansible.builtin.apt:
name: nginx=1.18.0-0ubuntu1
state: present
- name: Install multiple packages
ansible.builtin.apt:
name:
- nginx
- postgresql
- redis-server
state: present
ansible.builtin.dnf (RHEL 8+/CentOS 8+) - Recommended
# NOTE: Use ansible.builtin.dnf for RHEL 8+ and CentOS 8+
# ansible.builtin.yum is deprecated in favor of dnf for modern RHEL systems
- name: Install package with dnf
ansible.builtin.dnf:
name: nginx
state: present
update_cache: true
- name: Install from specific repository
ansible.builtin.dnf:
name: nginx
state: present
enablerepo: epel
- name: Install multiple packages
ansible.builtin.dnf:
name:
- nginx
- postgresql
- redis
state: present
ansible.builtin.yum (RHEL 7/CentOS 7 - Legacy)
# NOTE: Only use for RHEL 7/CentOS 7 systems
# For RHEL 8+ use ansible.builtin.dnf instead
- name: Install package with yum (legacy systems)
ansible.builtin.yum:
name: nginx
state: present
update_cache: true
- name: Install from specific repository (legacy)
ansible.builtin.yum:
name: nginx
state: present
enablerepo: epel
File Operations
ansible.builtin.file
# Create directory
- name: Create directory
ansible.builtin.file:
path: /opt/app/config
state: directory
mode: '0755'
owner: appuser
group: appgroup
recurse: true
# Create symbolic link
- name: Create symlink
ansible.builtin.file:
src: /opt/app/current
dest: /opt/app/releases/v1.2.3
state: link
# Remove file/directory
- name: Remove file
ansible.builtin.file:
path: /tmp/tempfile
state: absent
# Set permissions
- name: Set file permissions
ansible.builtin.file:
path: /etc/app/secret.key
mode: '0600'
owner: root
group: root
ansible.builtin.copy
# Copy file from control node
- name: Copy configuration file
ansible.builtin.copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
mode: '0644'
owner: root
group: root
backup: true
validate: 'nginx -t -c %s'
# Copy with inline content
- name: Create file with content
ansible.builtin.copy:
content: |
server {
listen 80;
server_name example.com;
}
dest: /etc/nginx/sites-available/example
mode: '0644'
# Remote copy (on target host)
- name: Copy file on remote host
ansible.builtin.copy:
src: /tmp/source.txt
dest: /opt/destination.txt
remote_src: yes
ansible.builtin.template
- name: Deploy configuration from template
ansible.builtin.template:
src: templates/app_config.j2
dest: /etc/app/config.yml
mode: '0644'
owner: appuser
group: appgroup
backup: true
validate: '/usr/bin/app validate %s'
ansible.builtin.fetch
- name: Fetch file from remote to control node
ansible.builtin.fetch:
src: /var/log/app/error.log
dest: /tmp/logs/{{ inventory_hostname }}/
flat: yes
ansible.builtin.lineinfile
- name: Ensure line is present
ansible.builtin.lineinfile:
path: /etc/hosts
line: '192.168.1.100 app.local'
state: present
- name: Replace or add line with regexp
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
state: present
backup: true
notify: Restart sshd
- name: Remove line
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: '.*old-server.*'
state: absent
ansible.builtin.blockinfile
- name: Add block of text
ansible.builtin.blockinfile:
path: /etc/hosts
block: |
192.168.1.10 web1.local
192.168.1.11 web2.local
192.168.1.20 db1.local
marker: "# {mark} ANSIBLE MANAGED BLOCK - SERVERS"
backup: true
Service Management
ansible.builtin.service
- name: Ensure service is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Restart service
ansible.builtin.service:
name: nginx
state: restarted
- name: Stop and disable service
ansible.builtin.service:
name: apache2
state: stopped
enabled: false
ansible.builtin.systemd
- name: Reload systemd daemon
ansible.builtin.systemd:
daemon_reload: true
- name: Start and enable service
ansible.builtin.systemd:
name: myapp
state: started
enabled: true
daemon_reload: true
- name: Mask service
ansible.builtin.systemd:
name: apache2
masked: yes
User and Group Management
ansible.builtin.user
- name: Create user
ansible.builtin.user:
name: appuser
uid: 1500
group: appgroup
groups: docker,sudo
shell: /bin/bash
home: /home/appuser
createhome: yes
state: present
- name: Set user password
ansible.builtin.user:
name: appuser
password: "{{ user_password | password_hash('sha512') }}"
update_password: always
- name: Add SSH key
ansible.builtin.user:
name: appuser
ssh_key_bits: 4096
ssh_key_file: .ssh/id_rsa
ansible.builtin.group
- name: Create group
ansible.builtin.group:
name: appgroup
gid: 1500
state: present
ansible.builtin.authorized_key
- name: Add SSH authorized key
ansible.builtin.authorized_key:
user: appuser
state: present
key: "{{ lookup('file', '/home/user/.ssh/id_rsa.pub') }}"
- name: Add multiple keys
ansible.builtin.authorized_key:
user: appuser
state: present
key: "{{ item }}"
loop:
- ssh-rsa AAAAB3... user1@host
- ssh-rsa AAAAB3... user2@host
Command Execution
ansible.builtin.command
- name: Run command (no shell processing)
ansible.builtin.command: /usr/bin/make install
args:
chdir: /opt/app
creates: /opt/app/bin/app
register: make_result
changed_when: make_result.rc == 0
- name: Run with environment variables
ansible.builtin.command: /opt/app/deploy.sh
environment:
APP_ENV: production
DB_HOST: localhost
ansible.builtin.shell
- name: Run shell command (with pipes/redirects)
ansible.builtin.shell: cat /var/log/app.log | grep ERROR > /tmp/errors.txt
args:
executable: /bin/bash
changed_when: false
- name: Use shell with creates
ansible.builtin.shell: /opt/install.sh
args:
creates: /opt/app/installed.flag
ansible.builtin.script
- name: Run script from control node
ansible.builtin.script: scripts/setup.sh
args:
creates: /etc/app/setup.done
Git Operations
ansible.builtin.git
- name: Clone repository
ansible.builtin.git:
repo: https://github.com/user/repo.git
dest: /opt/app
version: main
force: yes
- name: Clone specific branch/tag
ansible.builtin.git:
repo: https://github.com/user/repo.git
dest: /opt/app
version: v1.2.3
- name: Clone with SSH key
ansible.builtin.git:
repo: git@github.com:user/repo.git
dest: /opt/app
key_file: /home/deploy/.ssh/id_rsa
accept_hostkey: yes
Archive Operations
ansible.builtin.unarchive
- name: Extract archive from control node
ansible.builtin.unarchive:
src: files/app.tar.gz
dest: /opt/
owner: appuser
group: appgroup
- name: Extract remote archive
ansible.builtin.unarchive:
src: /tmp/app.tar.gz
dest: /opt/
remote_src: yes
- name: Download and extract
ansible.builtin.unarchive:
src: https://example.com/app.tar.gz
dest: /opt/
remote_src: yes
ansible.builtin.archive
- name: Create archive
ansible.builtin.archive:
path:
- /opt/app/config
- /opt/app/data
dest: /tmp/backup.tar.gz
format: gz
Download Operations
ansible.builtin.get_url
- name: Download file
ansible.builtin.get_url:
url: https://example.com/file.tar.gz
dest: /tmp/file.tar.gz
mode: '0644'
checksum: sha256:abc123...
- name: Download with authentication
ansible.builtin.get_url:
url: https://secure.example.com/file.tar.gz
dest: /tmp/file.tar.gz
url_username: user
url_password: "{{ download_password }}"
URI/API Operations
ansible.builtin.uri
- name: Check API endpoint
ansible.builtin.uri:
url: http://localhost:8080/health
method: GET
status_code: 200
register: health_check
until: health_check.status == 200
retries: 5
delay: 10
- name: POST to API
ansible.builtin.uri:
url: https://api.example.com/deploy
method: POST
body_format: json
body:
version: "1.2.3"
environment: production
headers:
Authorization: "Bearer {{ api_token }}"
status_code: [200, 201]
- name: Download response to file
ansible.builtin.uri:
url: https://api.example.com/data
method: GET
dest: /tmp/data.json
Cron Jobs
ansible.builtin.cron
- name: Add cron job
ansible.builtin.cron:
name: "Daily backup"
minute: "0"
hour: "2"
job: "/opt/backup.sh"
user: root
state: present
- name: Add cron job with special time
ansible.builtin.cron:
name: "Reboot task"
special_time: reboot
job: "/opt/startup.sh"
- name: Remove cron job
ansible.builtin.cron:
name: "Daily backup"
state: absent
Debug and Assert
ansible.builtin.debug
- name: Print variable
ansible.builtin.debug:
var: ansible_distribution
- name: Print message
ansible.builtin.debug:
msg: "Server IP: {{ ansible_default_ipv4.address }}"
- name: Conditional debug
ansible.builtin.debug:
msg: "This is a production server"
when: env == "production"
ansible.builtin.assert
- name: Validate configuration
ansible.builtin.assert:
that:
- ansible_distribution in ['Ubuntu', 'Debian']
- app_port | int > 0
- app_port | int < 65536
- db_password is defined
fail_msg: "Configuration validation failed"
success_msg: "Configuration is valid"
quiet: no
Set Facts
ansible.builtin.set_fact
- name: Set computed fact
ansible.builtin.set_fact:
app_full_version: "{{ app_name }}-{{ app_version }}"
deployment_time: "{{ ansible_date_time.iso8601 }}"
- name: Set fact with conditional
ansible.builtin.set_fact:
db_host: "{{ 'localhost' if env == 'dev' else 'db.prod.example.com' }}"
- name: Combine facts
ansible.builtin.set_fact:
app_config:
name: "{{ app_name }}"
version: "{{ app_version }}"
port: "{{ app_port }}"
Include and Import
ansible.builtin.include_tasks
- name: Include tasks dynamically
ansible.builtin.include_tasks: "{{ ansible_os_family }}.yml"
- name: Include with variables
ansible.builtin.include_tasks: deploy.yml
vars:
app_version: "1.2.3"
ansible.builtin.import_tasks
- name: Import tasks statically
ansible.builtin.import_tasks: common.yml
ansible.builtin.include_vars
- name: Load variables from file
ansible.builtin.include_vars:
file: "{{ env }}.yml"
- name: Load all YAML files from directory
ansible.builtin.include_vars:
dir: vars/
extensions:
- yml
- yaml
Wait Operations
ansible.builtin.wait_for
- name: Wait for port to be available
ansible.builtin.wait_for:
port: 8080
delay: 5
timeout: 300
state: started
- name: Wait for file to exist
ansible.builtin.wait_for:
path: /opt/app/ready
state: present
timeout: 300
- name: Wait for service to stop
ansible.builtin.wait_for:
port: 8080
state: stopped
timeout: 60
Error Handling with Block/Rescue/Always
Basic Block with Rescue
- name: Handle errors gracefully
block:
- name: Attempt risky operation
ansible.builtin.command: /opt/risky_script.sh
- name: This won't run if above fails
ansible.builtin.debug:
msg: "Script succeeded"
rescue:
- name: Handle failure
ansible.builtin.debug:
msg: "Script failed, performing recovery"
- name: Log error details
ansible.builtin.copy:
content: "{{ ansible_failed_result }}"
dest: /var/log/error.log
Block with Rescue and Always
- name: Deploy with rollback capability
block:
- name: Stop application
ansible.builtin.service:
name: myapp
state: stopped
- name: Deploy new version
ansible.builtin.copy:
src: app-v2.jar
dest: /opt/app/app.jar
backup: true
register: deploy_result
- name: Start application
ansible.builtin.service:
name: myapp
state: started
rescue:
- name: Rollback on failure
ansible.builtin.copy:
remote_src: yes
src: "{{ deploy_result.backup_file }}"
dest: /opt/app/app.jar
when: deploy_result.backup_file is defined
- name: Start application with old version
ansible.builtin.service:
name: myapp
state: started
always:
- name: Verify application is running
ansible.builtin.wait_for:
port: 8080
timeout: 60
Configuration Update with Validation and Backup
- name: Update config with validation
block:
- name: Deploy new configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
backup: true
validate: 'nginx -t -c %s'
register: config_update
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
rescue:
- name: Restore backup on failure
ansible.builtin.copy:
remote_src: yes
src: "{{ config_update.backup_file }}"
dest: /etc/nginx/nginx.conf
when: config_update.backup_file is defined
- name: Reload nginx with old config
ansible.builtin.service:
name: nginx
state: reloaded
always:
- name: Verify nginx is responding
ansible.builtin.uri:
url: http://localhost/health
status_code: 200
Accessing Error Variables in Rescue
- name: Use error variables
block:
- name: Task that might fail
ansible.builtin.command: /opt/backup.sh
register: backup_result
rescue:
- name: Log failed task name
ansible.builtin.debug:
msg: "Failed task: {{ ansible_failed_task.name }}"
- name: Log error details
ansible.builtin.debug:
msg: "Error: {{ ansible_failed_result.msg }}"
- name: Send alert
ansible.builtin.uri:
url: https://alerts.example.com/api/alert
method: POST
body_format: json
body:
task: "{{ ansible_failed_task.name }}"
error: "{{ ansible_failed_result.msg }}"
host: "{{ inventory_hostname }}"
Flush Handlers After Error
- name: Ensure handlers run even on failure
block:
- name: Update configuration
ansible.builtin.copy:
src: app.conf
dest: /etc/app/app.conf
notify: Restart application
changed_when: true
- name: Task that might fail
ansible.builtin.command: /opt/verify.sh
rescue:
- name: Flush handlers before recovery
meta: flush_handlers
- name: Perform recovery actions
ansible.builtin.debug:
msg: "Recovering from failure"
File Search and Status
ansible.builtin.find
# Find old log files
- name: Find log files older than 7 days
ansible.builtin.find:
paths: /var/log
patterns: "*.log"
age: "7d"
age_stamp: mtime
register: old_logs
- name: Delete old log files
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_logs.files }}"
# Find large files
- name: Find files larger than 100MB
ansible.builtin.find:
paths: /var/data
patterns: "*"
size: "100m"
recurse: true
register: large_files
- name: Display large files
ansible.builtin.debug:
msg: "{{ item.path }} - {{ item.size | filesizeformat }}"
loop: "{{ large_files.files }}"
# Find files by regex pattern
- name: Find backup files
ansible.builtin.find:
paths:
- /opt/backups
- /var/backups
patterns: "backup-.*\\.tar\\.gz$"
use_regex: yes
file_type: file
register: backup_files
# Find directories
- name: Find empty directories
ansible.builtin.find:
paths: /tmp
file_type: directory
recurse: no
register: directories
- name: Remove empty directories
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ directories.files }}"
when: item.isdir
ansible.builtin.stat
# Check if file exists
- name: Check if config file exists
ansible.builtin.stat:
path: /etc/app/config.yml
register: config_file
- name: Create config if missing
ansible.builtin.copy:
content: "default: config"
dest: /etc/app/config.yml
when: not config_file.stat.exists
# Verify file ownership
- name: Check file owner
ansible.builtin.stat:
path: /etc/app/secret.key
register: secret_file
- name: Fail if not owned by root
ansible.builtin.fail:
msg: "Secret file must be owned by root"
when:
- secret_file.stat.exists
- secret_file.stat.pw_name != 'root'
# Check file permissions
- name: Check file permissions
ansible.builtin.stat:
path: /etc/ssl/private/app.key
register: ssl_key
- name: Fix permissions if needed
ansible.builtin.file:
path: /etc/ssl/private/app.key
mode: '0600'
owner: root
group: root
when:
- ssl_key.stat.exists
- ssl_key.stat.mode != '0600'
# Check if path is directory
- name: Verify directory
ansible.builtin.stat:
path: /opt/app
register: app_dir
- name: Create directory if needed
ansible.builtin.file:
path: /opt/app
state: directory
mode: '0755'
when: not app_dir.stat.exists or not app_dir.stat.isdir
# Get file size and age
- name: Check log file size
ansible.builtin.stat:
path: /var/log/app.log
register: log_file
- name: Rotate log if too large
ansible.builtin.command: logrotate -f /etc/logrotate.d/app
when:
- log_file.stat.exists
- log_file.stat.size > 104857600 # 100MB
# Check symlink
- name: Check if symlink
ansible.builtin.stat:
path: /usr/bin/python
register: python_link
- name: Display symlink target
ansible.builtin.debug:
msg: "Python links to {{ python_link.stat.lnk_target }}"
when:
- python_link.stat.exists
- python_link.stat.islnk
Advanced Control Flow
delegate_to
# Run task on different host
- name: Add server to load balancer
ansible.builtin.uri:
url: "http://lb.example.com/api/add"
method: POST
body_format: json
body:
server: "{{ inventory_hostname }}"
port: 8080
delegate_to: localhost
# Run on specific host in group
- name: Run database migration
ansible.builtin.command: /opt/migrate.sh
delegate_to: "{{ groups['database'] | first }}"
# Local command with delegation
- name: Generate local certificate
ansible.builtin.command: >
openssl req -x509 -nodes -days 365
-newkey rsa:2048
-keyout "/tmp/{{ inventory_hostname }}.key"
-out "/tmp/{{ inventory_hostname }}.crt"
-subj "/CN={{ inventory_hostname }}"
delegate_to: localhost
become: no
run_once
# Execute once for entire play
- name: Create shared resource
ansible.builtin.file:
path: /shared/data
state: directory
run_once: true
delegate_to: "{{ groups['storage'] | first }}"
# Run once with loop over all hosts
- name: Register all hosts in monitoring
ansible.builtin.uri:
url: https://monitoring.example.com/api/register
method: POST
body_format: json
body:
hostname: "{{ item }}"
loop: "{{ ansible_play_hosts }}"
run_once: true
delegate_to: localhost
# Database seed data (once per cluster)
- name: Seed database
ansible.builtin.command: /opt/seed_data.sh
run_once: true
delegate_to: "{{ groups['database'] | first }}"
local_action
# Execute on control node
- name: Generate configuration locally
local_action:
module: ansible.builtin.template
src: config.j2
dest: "/tmp/{{ inventory_hostname }}_config.yml"
# Fetch file from remote to local
- name: Backup configuration locally
local_action:
module: ansible.builtin.copy
content: "{{ lookup('file', '/etc/app/config.yml') }}"
dest: "/backup/{{ inventory_hostname }}_config.yml"
# Send notification from control node
- name: Send deployment notification
local_action:
module: ansible.builtin.uri
url: https://chat.example.com/webhook
method: POST
body_format: json
body:
message: "Deploying to {{ inventory_hostname }}"
run_once: true
# Local script execution
- name: Run local analysis script
local_action:
module: ansible.builtin.command
cmd: python3 analyze.py --host {{ inventory_hostname }}
register: analysis_result
Common Collection Modules
community.general
community.general.ufw (Firewall)
- name: Allow SSH
community.general.ufw:
rule: allow
port: '22'
proto: tcp
- name: Enable firewall
community.general.ufw:
state: enabled
community.general.timezone
- name: Set timezone
community.general.timezone:
name: America/New_York
community.docker
community.docker.docker_container
- name: Run Docker container
community.docker.docker_container:
name: myapp
image: nginx:latest
state: started
restart_policy: always
ports:
- "80:80"
- "443:443"
volumes:
- /opt/data:/data
env:
APP_ENV: production
community.postgresql
community.postgresql.postgresql_db
- name: Create database
community.postgresql.postgresql_db:
name: appdb
state: present
ansible.posix
ansible.posix.mount
- name: Mount filesystem
ansible.posix.mount:
path: /data
src: /dev/sdb1
fstype: ext4
state: mounted
ansible.posix.sysctl
- name: Set sysctl parameter
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
state: present
reload: yes
Cloud Provider Modules
Amazon AWS (amazon.aws)
amazon.aws.ec2_instance
# Requirements:
# - ansible-galaxy collection install amazon.aws
# - boto3 and botocore Python packages
# - Python 3.8+
# Launch EC2 instance with public IP
- name: Launch EC2 instance
amazon.aws.ec2_instance:
name: web-server-01
key_name: my-ssh-key
vpc_subnet_id: subnet-12345678
instance_type: t3.micro
security_group: default
network:
assign_public_ip: true
image_id: ami-0c55b159cbfafe1f0 # Amazon Linux 2
tags:
Environment: production
Application: web
state: running
# Launch instance with EBS volumes
- name: Launch instance with additional storage
amazon.aws.ec2_instance:
name: database-server
key_name: my-ssh-key
vpc_subnet_id: subnet-12345678
instance_type: t3.large
image_id: ami-0c55b159cbfafe1f0
volumes:
- device_name: /dev/sda1
ebs:
volume_size: 30
volume_type: gp3
delete_on_termination: true
- device_name: /dev/sdb
ebs:
volume_size: 100
volume_type: gp3
delete_on_termination: false
tags:
Environment: production
Role: database
state: running
# Start/stop instances by ID
- name: Start EC2 instances
amazon.aws.ec2_instance:
instance_ids:
- i-0123456789abcdef0
- i-0123456789abcdef1
state: running
- name: Stop EC2 instances
amazon.aws.ec2_instance:
instance_ids:
- i-0123456789abcdef0
state: stopped
# Terminate instance (use with EXTREME caution)
- name: Terminate EC2 instance
amazon.aws.ec2_instance:
instance_ids:
- i-0123456789abcdef0
state: terminated
amazon.aws.ec2_instance_info
# Gather info about all instances
- name: Get all EC2 instances
amazon.aws.ec2_instance_info:
register: ec2_instances
# Filter instances by tag
- name: Get production web servers
amazon.aws.ec2_instance_info:
filters:
"tag:Environment": production
"tag:Role": webserver
instance-state-name: running
register: prod_web_servers
- name: Display instance IPs
ansible.builtin.debug:
msg: "{{ item.public_ip_address }}"
loop: "{{ prod_web_servers.instances }}"
amazon.aws.s3_object
# Upload file to S3
- name: Upload file to S3 bucket
amazon.aws.s3_object:
bucket: my-backup-bucket
object: "backups/{{ ansible_date_time.date }}/app.tar.gz"
src: /tmp/app.tar.gz
mode: put
encrypt: yes
# Download file from S3
- name: Download configuration from S3
amazon.aws.s3_object:
bucket: my-config-bucket
object: app/config.yml
dest: /etc/app/config.yml
mode: get
# Delete object from S3
- name: Remove old backup
amazon.aws.s3_object:
bucket: my-backup-bucket
object: "backups/old/app.tar.gz"
mode: delobj
amazon.aws.rds_instance
# Create RDS instance
- name: Create PostgreSQL RDS instance
amazon.aws.rds_instance:
db_instance_identifier: myapp-db
engine: postgres
engine_version: "15.4"
db_instance_class: db.t3.micro
allocated_storage: 20
storage_type: gp3
master_username: dbadmin
master_user_password: "{{ db_password }}"
vpc_security_group_ids:
- sg-12345678
db_subnet_group_name: my-db-subnet
backup_retention_period: 7
multi_az: false
publicly_accessible: false
tags:
Environment: production
Application: myapp
Microsoft Azure (azure.azcollection)
azure.azcollection.azure_rm_virtualmachine
# Requirements:
# - ansible-galaxy collection install azure.azcollection
# - Azure SDK packages (see collection requirements.txt)
# Create VM with defaults
- name: Create Azure VM
azure.azcollection.azure_rm_virtualmachine:
resource_group: myResourceGroup
name: webserver01
admin_username: azureuser
admin_password: "{{ vm_password }}"
vm_size: Standard_B2s
image:
offer: 0001-com-ubuntu-server-focal
publisher: Canonical
sku: 20_04-lts
version: latest
tags:
Environment: production
Role: webserver
# Create VM with managed disk
- name: Create VM with managed disk
azure.azcollection.azure_rm_virtualmachine:
resource_group: myResourceGroup
name: appserver01
admin_username: azureuser
ssh_password_enabled: false
ssh_public_keys:
- path: /home/azureuser/.ssh/authorized_keys
key_data: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
vm_size: Standard_D4s_v3
managed_disk_type: Premium_LRS
image:
offer: 0001-com-ubuntu-server-focal
publisher: Canonical
sku: 20_04-lts-gen2
version: latest
os_disk_size_gb: 128
data_disks:
- lun: 0
disk_size_gb: 256
managed_disk_type: Premium_LRS
network_interfaces: mynetworkinterface
tags:
Environment: production
# Start/stop Azure VMs
- name: Stop Azure VM
azure.azcollection.azure_rm_virtualmachine:
resource_group: myResourceGroup
name: webserver01
allocated: false
- name: Start Azure VM
azure.azcollection.azure_rm_virtualmachine:
resource_group: myResourceGroup
name: webserver01
allocated: true
# Delete Azure VM
- name: Delete Azure VM
azure.azcollection.azure_rm_virtualmachine:
resource_group: myResourceGroup
name: webserver01
state: absent
azure.azcollection.azure_rm_virtualmachine_info
# Get all VMs in resource group
- name: Get VM facts
azure.azcollection.azure_rm_virtualmachine_info:
resource_group: myResourceGroup
register: azure_vms
# Get specific VM info
- name: Get specific VM info
azure.azcollection.azure_rm_virtualmachine_info:
resource_group: myResourceGroup
name: webserver01
register: vm_info
- name: Display VM private IP
ansible.builtin.debug:
msg: "{{ vm_info.vms[0].network_profile.network_interfaces[0].ip_configurations[0].private_ip_address }}"
azure.azcollection.azure_rm_storageblob
# Upload file to Azure Blob Storage
- name: Upload backup to blob storage
azure.azcollection.azure_rm_storageblob:
resource_group: myResourceGroup
storage_account_name: mystorageaccount
container: backups
blob: "{{ ansible_date_time.date }}/app-backup.tar.gz"
src: /tmp/app-backup.tar.gz
content_type: application/gzip
# Download from blob storage
- name: Download config from blob storage
azure.azcollection.azure_rm_storageblob:
resource_group: myResourceGroup
storage_account_name: mystorageaccount
container: configs
blob: app-config.yml
dest: /etc/app/config.yml
azure.azcollection.azure_rm_sqldatabase
# Create Azure SQL Database
- name: Create SQL database
azure.azcollection.azure_rm_sqldatabase:
resource_group: myResourceGroup
server_name: mydbserver
name: mydatabase
sku:
name: S0
tier: Standard
max_size_bytes: 268435456000 # 250GB
tags:
Environment: production
Application: myapp
Secrets Management Lookups
HashiCorp Vault
community.hashi_vault.hashi_vault lookup
# Requirements:
# - ansible-galaxy collection install community.hashi_vault
# - hvac Python package
# Retrieve secret from Vault
- name: Get database password from Vault
ansible.builtin.set_fact:
db_password: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/database:password') }}"
no_log: true
# Use multiple vault paths
- name: Get multiple secrets
ansible.builtin.set_fact:
api_key: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/api:key') }}"
api_secret: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/api:secret') }}"
no_log: true
# Configure Vault connection
- name: Get secret with custom Vault config
ansible.builtin.set_fact:
admin_password: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/data/admin:password', url='https://vault.example.com:8200', auth_method='token', token=vault_token) }}"
no_log: true
AWS Secrets Manager
community.aws.aws_secret lookup
# Requirements:
# - ansible-galaxy collection install community.aws
# - boto3 and botocore
# Retrieve secret from AWS Secrets Manager
- name: Get database credentials from Secrets Manager
ansible.builtin.set_fact:
db_creds: "{{ lookup('community.aws.aws_secret', 'prod/database/credentials', region='us-east-1') | from_json }}"
no_log: true
- name: Use retrieved credentials
ansible.builtin.debug:
msg: "Connecting to {{ db_creds.host }} as {{ db_creds.username }}"
# Retrieve specific version
- name: Get specific secret version
ansible.builtin.set_fact:
api_key: "{{ lookup('community.aws.aws_secret', 'prod/api/key', version_id='EXAMPLE1-90ab-cdef-fedc-ba987EXAMPLE') }}"
no_log: true
Azure Key Vault
azure.azcollection.azure_keyvault_secret lookup
# Requirements:
# - ansible-galaxy collection install azure.azcollection
# Retrieve secret from Azure Key Vault
- name: Get secret from Key Vault
ansible.builtin.set_fact:
app_secret: "{{ lookup('azure.azcollection.azure_keyvault_secret', 'app-secret', vault_url='https://myvault.vault.azure.net') }}"
no_log: true
# Use in tasks
- name: Deploy application with secret
ansible.builtin.template:
src: config.j2
dest: /etc/app/config.yml
vars:
secret_key: "{{ lookup('azure.azcollection.azure_keyvault_secret', 'secret-key', vault_url='https://myvault.vault.azure.net') }}"
no_log: true