
Ansible Generator
- 458 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
ansible-generator is a Claude Code skill that generates Ansible playbooks, roles, and inventories from requirements so developers who automate server provisioning can integrate config management into deployment pipelines
About
ansible-generator is an agent skill from akin-ozer/cc-devops-skills that turns infrastructure requirements into Ansible artifacts—playbooks for task execution, roles for reusable modules, and inventories for host targeting. Developers reach for it when standing up repeatable server configuration, application deployment prep, or environment bootstrapping that must live in version control beside application code. The skill emphasizes pipeline-friendly output so provisioning steps integrate with CI/CD rather than manual SSH sessions. Use it to accelerate initial Ansible scaffolding from natural-language requirements before hardening idempotency and secrets handling in review.
- Playbook scaffolding from specs
- Role and task structuring
- Inventory and variable patterns
- Idempotent task guidance
- Pipeline handoff checklist
Ansible Generator by the numbers
- 458 all-time installs (skills.sh)
- Ranked #270 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill ansible-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 458 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you generate Ansible playbooks from requirements?
Generate Ansible playbooks, roles, and inventories from requirements so provisioning, config management, and repeatable server setup integrate cleanly into deployment pipelines.
Who is it for?
DevOps engineers scaffolding Ansible automation from requirements before refining idempotency and secrets in code review.
Skip if: Teams standardized on Terraform-only infrastructure with no Ansible config management in the stack.
When should I use this skill?
User asks to generate Ansible playbooks, roles, or inventories for provisioning or config management.
What you get
Ansible playbooks, role directories, and inventory files ready for version control and pipeline execution.
- playbook YAML
- role directory structure
- inventory file
Files
Ansible Generator
Trigger Phrases
Use this skill when the request is to generate or scaffold Ansible content, for example:
- "Create a playbook to deploy nginx with TLS."
- "Generate an Ansible role for PostgreSQL backups."
- "Write inventory files for prod and staging."
- "Build reusable Ansible tasks for user provisioning."
- "Initialize an Ansible project with ansible.cfg and requirements.yml."
- "Give me a quick Ansible snippet to install Docker."
Do not use this skill as the primary workflow when the request is validation/debug-only (syntax errors, lint failures, Molecule/test failures). Use ansible-validator for those cases.
Deterministic Execution Flow
Run these stages in order. Do not skip a stage unless the Validation Exceptions Matrix explicitly allows it.
Stage 0: Classify Request Mode
Determine one mode first:
| Mode | Typical user intent | Deliverable |
|---|---|---|
full-generation | "create/build/generate" a full playbook/role/inventory/project file set | Complete file(s), production-ready |
snippet-only | "quick snippet/example" without full file context | Focused task/play snippet |
docs-only | explanation, pattern comparison, or conceptual guidance only | Explanatory content, optional examples |
Stage 1: Collect Minimum Inputs
If details are missing, ask briefly. If the user does not provide them, proceed with safe defaults and state assumptions.
| Resource type | Required inputs | Safe defaults if missing |
|---|---|---|
| Playbook | target hosts, privilege (become), OS family, objective | hosts: all, become: false, OS-agnostic modules |
| Role | role name, primary service/package, supported OS | role name from task domain, Debian + RedHat vars |
| Tasks file | operation scope, required vars, execution context | standalone reusable tasks with documented vars |
| Inventory | environments, host groups, hostnames/IPs | production/staging groups with placeholders |
| Project config | collections/roles dependencies, lint policy | minimal ansible.cfg, requirements.yml, .ansible-lint |
Stage 2: Reference Extraction Checklist
Before drafting content, extract the following from local references/templates.
Required references
references/best-practices.md- Extract: FQCN requirements, idempotency rules, naming, security expectations.
references/module-patterns.md- Extract: correct module/parameter patterns for the exact task type.
Required templates by output type
- Playbook:
assets/templates/playbook/basic_playbook.yml - Role:
assets/templates/role/(includingmeta/argument_specs.ymlandmolecule/default/for test scaffolding) - Inventory (INI):
assets/templates/inventory/hosts - Inventory (YAML):
assets/templates/inventory/hosts.yml - Project config:
assets/templates/project/ansible.cfg,assets/templates/project/requirements.yml,assets/templates/project/.ansible-lint
Extraction checks
- Identify every
[PLACEHOLDER]that must be replaced. - Decide module selection priority (
ansible.builtin.*first). - Capture at least one OS-appropriate package pattern when OS-specific behavior is needed.
- Capture required prerequisites (collections, binaries, target assumptions).
Stage 3: Generate
Apply these generation standards:
1. Use FQCN module names (ansible.builtin.* first choice). 2. Keep tasks idempotent (state, creates/removes, changed_when when needed). 3. Use descriptive verb-first task names. 4. Use true/false booleans (not yes/no). 5. Add no_log: true for sensitive values. 6. Replace all placeholders before presenting output. 7. Prefer ansible.builtin.dnf for RHEL 8+/CentOS 8+ (legacy yum only for older systems).
Stage 4: Validate (Default) or Apply Exception (Fallback)
Use the matrix below to keep validation deterministic and non-blocking.
Validation Exceptions Matrix
| Scenario | Default behavior | Allowed fallback | What to report |
|---|---|---|---|
full-generation | Run ansible-validator after generation and after each fix pass | If validator/tools are unavailable, run manual static checks (YAML shape, placeholder scan, FQCN/idempotency/security review) and provide exact deferred validation commands | Explicitly list which checks ran, which were skipped, and why |
snippet-only | Skip full validator by default; do inline sanity checks | Run full validator only if user asks or snippet is promoted to full file | State that validation was limited because output is snippet-only |
docs-only | No runtime validation | None needed | State that no executable artifact was generated |
| Offline environment (no web/docs access) | Continue with local references and templates | Skip external doc lookups; prefer builtin-module implementations; provide notes for later external verification | State offline constraint and impacted checks/lookups |
Resource Generation Guidance
Playbooks
- Use
assets/templates/playbook/basic_playbook.ymlas structure. - Include: header comments,
pre_tasks/tasks/post_tasksas needed, handlers, tags. - Add health checks when service deployment/configuration is involved.
Roles
- Build from
assets/templates/role/structure. - Keep defaults in
defaults/main.yml; keep higher-priority role vars invars/main.yml. - Include OS-specific vars (
vars/Debian.yml,vars/RedHat.yml) when relevant. - Add
meta/argument_specs.ymlfor variable validation. - Include
molecule/default/scaffold (fromassets/templates/role/molecule/) for production-ready roles.
Task Files
- Keep scope narrow and reusable.
- Document required input variables in comments.
- Use conditionals for environment/OS-sensitive operations.
Inventory
- Build logical host groups and optional group hierarchies.
- Use variable layering intentionally:
group_vars/all.yml-> group -> host. - Default to INI format (
hosts) for simple topologies; use YAML format (hosts.yml) when the user requests it or when the hierarchy is complex.
Project Configuration
- Provide baseline
ansible.cfg,requirements.yml, and.ansible-lint. - Keep defaults practical and editable.
Custom Modules and Collections
When the request depends on non-builtin modules/collections:
1. Identify collection + module and required version sensitivity. 2. Check local references/module-patterns.md first. 3. If still unresolved and network/tools are available, query Context7:
mcp__context7__resolve-library-idmcp__context7__query-docs
4. If Context7 is unavailable, use official Ansible docs / Ansible Galaxy pages. 5. If external lookup is unavailable, provide a builtin fallback approach and state the limitation.
Always include collection installation guidance when collection modules are used.
Canonical Example Flows
Flow A: Full Generation (Playbook)
User prompt: "Create a playbook to deploy nginx with TLS on Ubuntu and RHEL."
1. Classify as full-generation. 2. Gather/confirm required inputs (hosts, cert paths, become, service name). 3. Extract required references (best-practices.md, module-patterns.md) and playbook template. 4. Generate complete playbook with OS conditionals (apt/dnf), handlers, validation for config templates. 5. Run ansible-validator. 6. Fix issues and rerun until checks pass (or apply matrix fallback if tooling unavailable). 7. Present output with validation summary, usage command, and prerequisites.
Flow B: Quick Snippet (Task Block)
User prompt: "Give me a snippet to create a user and SSH key."
1. Classify as snippet-only. 2. Extract minimal module patterns for ansible.builtin.user and ansible.builtin.authorized_key. 3. Generate concise snippet with FQCN, idempotency, and variable placeholders. 4. Perform inline sanity checks (YAML shape, FQCN, obvious idempotency/security). 5. Present snippet and note that full validator run was skipped due to snippet-only mode.
Output Requirements
For generated executable artifacts, use this response structure:
## Generated [Resource Type]: [Name]
**Validation Status:** [Passed / Partially validated / Skipped with reason]
- YAML syntax: [status]
- Ansible syntax: [status]
- Lint: [status]
**Summary:**
- [What was generated]
- [Key implementation choices]
**Assumptions:**
- [Defaults or inferred values]
**Usage:**[Exact command(s)]
**Prerequisites:**
- [Collections, binaries, environment needs]Done Criteria
This skill execution is complete only when all applicable items are true:
- Trigger decision is explicit (
full-generation,snippet-only, ordocs-only). - Required references/templates were consulted for the selected artifact type.
- Generated output has no unresolved placeholders.
- Validation followed default behavior or a documented exception from the matrix.
- Any skipped checks include a concrete reason and deferred command(s).
- Final output includes summary, assumptions, usage, and prerequisites.
.ansible/
---
# Global variables for all hosts
# Connection settings
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
ansible_ssh_private_key_file: ~/.ssh/id_rsa
# Common packages
common_packages:
- vim
- git
- htop
- curl
- wget
# NTP configuration
ntp_enabled: true
ntp_timezone: UTC
# Security settings
security_ssh_port: 22
security_ssh_password_authentication: false
security_ssh_permit_root_login: false
---
# Variables specific to databases group
# Database type
db_type: postgresql # postgresql, mysql, mongodb, etc.
# PostgreSQL specific
postgresql_version: 15
postgresql_port: 5432
postgresql_data_dir: /var/lib/postgresql/{{ postgresql_version }}/main
postgresql_listen_addresses: localhost
postgresql_max_connections: 100
# Backup configuration
db_backup_enabled: true
db_backup_dir: /var/backups/databases
db_backup_retention_days: 7
db_backup_schedule: "0 2 * * *" # Daily at 2 AM
# Monitoring
db_monitoring_enabled: true
db_slow_query_logging: true
---
# Variables specific to webservers group
# Web server configuration
web_server: nginx
web_port: 80
web_ssl_port: 443
web_document_root: /var/www/html
# Application settings
app_name: myapp
app_version: "1.0.0"
app_port: 8080
app_user: www-data
app_group: www-data
# SSL/TLS
enable_ssl: true
ssl_certificate: /etc/ssl/certs/{{ ansible_fqdn }}.crt
ssl_certificate_key: /etc/ssl/private/{{ ansible_fqdn }}.key
---
# Host-specific variables for web1.example.com
# Host identification
server_id: web1
datacenter: dc1
rack: A1
# Resource allocation
memory_limit: 4G
cpu_cores: 2
# Host-specific configuration
nginx_worker_processes: 2
nginx_worker_connections: 1024
# Monitoring
monitoring_enabled: true
monitoring_tags:
- production
- webserver
- primary
# Ansible Inventory: [ENVIRONMENT]
# Format: INI-style inventory file
[webservers]
web1.example.com ansible_host=192.168.1.10
web2.example.com ansible_host=192.168.1.11
[databases]
db1.example.com ansible_host=192.168.1.20
db2.example.com ansible_host=192.168.1.21
[loadbalancers]
lb1.example.com ansible_host=192.168.1.30
# Group of groups
[production:children]
webservers
databases
loadbalancers
# Group variables (can also be in group_vars/)
[production:vars]
ansible_user=deploy
ansible_python_interpreter=/usr/bin/python3
env=production
---
# Ansible Inventory: [ENVIRONMENT]
# Format: YAML inventory file
# Usage: ansible-playbook -i inventory/[ENVIRONMENT]/hosts.yml playbook.yml
all:
children:
webservers:
hosts:
web1.example.com:
ansible_host: 192.168.1.10
web2.example.com:
ansible_host: 192.168.1.11
databases:
hosts:
db1.example.com:
ansible_host: 192.168.1.20
db2.example.com:
ansible_host: 192.168.1.21
loadbalancers:
hosts:
lb1.example.com:
ansible_host: 192.168.1.30
vars:
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
env: [ENVIRONMENT]
---
# Playbook: [PLAYBOOK_NAME]
# Description: [PLAYBOOK_DESCRIPTION]
# Requirements:
# - Ansible [MIN_VERSION]+
# - Target hosts: [OS_REQUIREMENTS]
# Variables:
# - [VAR_NAME]: [VAR_DESCRIPTION] (required/optional, default: [DEFAULT_VALUE])
# Usage:
# ansible-playbook -i inventory/[ENV] [PLAYBOOK_FILE] -e "[REQUIRED_VARS]"
- name: [Play description]
hosts: [TARGET_HOSTS]
become: [true/false]
gather_facts: [true/false]
vars:
# Define play-level variables
[var_name]: [value]
pre_tasks:
- name: Update package cache (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
tags:
- always
- name: Update package cache (RHEL/CentOS 8+)
ansible.builtin.dnf:
update_cache: true
when: ansible_os_family == "RedHat"
tags:
- always
tasks:
- name: [Task description]
ansible.builtin.[module]:
[parameter]: [value]
tags:
- [tag_name]
post_tasks:
- name: Verify deployment
ansible.builtin.uri:
url: "http://localhost:[PORT]/health"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 5
delay: 10
tags:
- verify
handlers:
- name: [Handler description]
ansible.builtin.service:
name: [service_name]
state: restarted
# Ansible Lint Configuration
# For full documentation: https://ansible.readthedocs.io/projects/lint/
---
# Exclude paths from linting
exclude_paths:
- .cache/
- .git/
- .github/
- test/fixtures/
- collections/
- roles/external/
- '*.md'
- '*.txt'
# Enable offline mode (don't check for newer versions)
offline: false
# Set output format: rich, plain, md, json, codeclimate, or sarif
# format: rich
# Return success if only warnings are found
# quiet: false
# Parseable output with severity levels
# parseable: true
# Use color in terminal output
# colored: true
# Path to custom rules directory
# rulesdir:
# - ./custom-rules/
# Show rule IDs in output
# verbosity: 1
# Skip list - rules to skip
skip_list:
# Skip line length check (may be too strict for some projects)
- yaml[line-length]
# Skip name casing rule (allow flexibility in task naming)
# - name[casing]
# Skip checks for old Ansible syntax (if maintaining legacy playbooks)
# - syntax-check[specific-tag]
# Skip risky file permissions warning (if intentional)
# - risky-file-permissions
# Skip no-changed-when for command/shell (if acceptable)
# - no-changed-when
# Skip jinja spacing rules (if preferred)
# - jinja[spacing]
# Skip meta information checks
# - meta-no-info
# - meta-no-tags
# Skip package latest checks (if using latest is intentional)
# - package-latest
# Warn list - rules to warn but not fail
warn_list:
- experimental # Warn about experimental features
- no-changed-when # Warn when command/shell tasks lack changed_when
- no-handler # Warn when using bare variables in notify
- unnamed-task # Warn about tasks without names
- command-instead-of-module # Warn when shell/command could be replaced with module
- command-instead-of-shell # Warn when command could use shell for pipes/redirects
- deprecated-bare-vars # Warn about bare variables (use {{ }})
- deprecated-local-action # Warn about deprecated local_action syntax
- risky-shell-pipe # Warn about shell tasks using pipes without pipefail
# Enable rules (override defaults)
enable_list:
- args # Check for correct module arguments
- empty-string-compare # Check for empty string comparisons
- no-free-form # Check for free-form parameters
- no-jinja-when # Check for Jinja2 in when conditions
- no-log-password # Check for passwords without no_log
- no-same-owner # Check for same owner/group
- yaml # YAML syntax checks
# Tags to run or skip
# tags: []
# skip_tags: []
# Ansible-lint profile: min, basic, moderate, safety, shared, production
# profile: production
# Kinds of files to lint
kinds:
- yaml: "*.yaml"
- yaml: "*.yml"
- playbook: "**/playbooks/*.yml"
- playbook: "**/playbooks/*.yaml"
- tasks: "**/tasks/*.yml"
- tasks: "**/tasks/*.yaml"
- handlers: "**/handlers/*.yml"
- handlers: "**/handlers/*.yaml"
- vars: "**/vars/*.yml"
- vars: "**/vars/*.yaml"
- vars: "**/defaults/*.yml"
- vars: "**/defaults/*.yaml"
- meta: "**/meta/main.yml"
- meta: "**/meta/main.yaml"
- requirements: "**/requirements.yml"
- requirements: "**/requirements.yaml"
# Mock modules or roles you want to skip during linting
# mock_modules:
# - my_custom_module
# - another_custom_module
# mock_roles:
# - mynamespace.my_custom_role
# Loop variable naming pattern
# loop_var_prefix: "^(__|{role}_)"
# Minimum Ansible version to check against
# min_ansible_version: "2.15"
# Maximum line length (default: 160)
# max_line_length: 160
# Enforce task naming based on module name
# task_name_prefix: "{module_name} | "
# Strict mode - treat warnings as errors
# strict: false
# Profiles define different strictness levels
# Available profiles: min, basic, moderate, safety, shared, production
# Uncomment to use:
# profile: production
# Write violations to file
# write_list:
# - all
# - rule-id
# Use specific Ansible version for checking
# use_default_rules: true
# Enable/disable specific rule categories
# Any rule not listed is enabled by default
rules:
# Command module rules
command-instead-of-module:
severity: MEDIUM
command-instead-of-shell:
severity: LOW
# Naming rules
name[casing]:
severity: MEDIUM
name[missing]:
severity: HIGH
name[play]:
severity: MEDIUM
name[template]:
severity: LOW
# YAML rules
yaml[brackets]:
severity: MEDIUM
yaml[colons]:
severity: MEDIUM
yaml[commas]:
severity: LOW
yaml[document-start]:
severity: LOW
yaml[empty-lines]:
severity: LOW
yaml[indentation]:
severity: MEDIUM
yaml[key-duplicates]:
severity: HIGH
yaml[line-length]:
max: 160
severity: LOW
yaml[new-line-at-end-of-file]:
severity: LOW
yaml[trailing-spaces]:
severity: LOW
yaml[truthy]:
severity: MEDIUM
# Jinja2 rules
jinja[spacing]:
severity: LOW
jinja[invalid]:
severity: HIGH
# Variable rules
var-naming[no-reserved]:
severity: HIGH
var-naming[no-jinja]:
severity: MEDIUM
var-naming[pattern]:
severity: MEDIUM
# Security rules
no-log-password:
severity: VERY_HIGH
risky-file-permissions:
severity: HIGH
risky-octal:
severity: MEDIUM
risky-shell-pipe:
severity: MEDIUM
# Best practices
no-changed-when:
severity: MEDIUM
no-handler:
severity: LOW
package-latest:
severity: LOW
deprecated-bare-vars:
severity: MEDIUM
deprecated-module:
severity: HIGH
deprecated-command-syntax:
severity: MEDIUM
# Meta rules
meta-no-info:
severity: LOW
meta-no-tags:
severity: LOW
meta-incorrect:
severity: HIGH
meta-runtime[unsupported-version]:
severity: HIGH
meta-runtime[invalid-version]:
severity: VERY_HIGH
# Galaxy rules
galaxy[no-changelog]:
severity: LOW
galaxy[no-runtime]:
severity: MEDIUM
galaxy[version-incorrect]:
severity: HIGH
galaxy[version-missing]:
severity: MEDIUM
galaxy[tags]:
severity: LOW
# Schema validation
schema[meta]:
severity: VERY_HIGH
schema[playbook]:
severity: VERY_HIGH
schema[tasks]:
severity: VERY_HIGH
schema[vars]:
severity: HIGH
# Additional options
# progressive: false # Enable progressive mode
# project_dir: . # Project directory
# Ansible Configuration File
# For full documentation: https://docs.ansible.com/ansible/latest/reference_appendices/config.html
[defaults]
# Inventory location
inventory = inventory/
# Role paths
roles_path = roles/
# Collection paths
collections_path = collections/:~/.ansible/collections:/usr/share/ansible/collections
# Host key checking (disable for dynamic environments, enable for production)
host_key_checking = False
# Retry files (disabled by default)
retry_files_enabled = False
# Python interpreter discovery
interpreter_python = auto_silent
# Fact gathering
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
# Output settings
stdout_callback = yaml
bin_ansible_callbacks = True
# Callback plugins
callback_whitelist = timer, profile_tasks
# Deprecation warnings
deprecation_warnings = True
# Display settings
display_skipped_hosts = False
display_args_to_stdout = False
# Logging (uncomment to enable)
# log_path = /var/log/ansible.log
# Default module arguments
# module_args = {}
# Timeout settings (in seconds)
timeout = 30
# Forks (parallel execution)
forks = 5
# Poll interval for async tasks
poll_interval = 15
# Ask for passwords
# ask_pass = False
# ask_sudo_pass = False
# ask_vault_pass = False
# Remote user
# remote_user = root
# Private key file
# private_key_file = /path/to/key
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
# SSH settings
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
# SSH timeout
timeout = 30
# Control path for SSH multiplexing
control_path_dir = ~/.ansible/cp
control_path = %(directory)s/ansible-ssh-%%h-%%p-%%r
[persistent_connection]
# Persistent connection timeout
connect_timeout = 30
command_timeout = 30
[colors]
# Color settings
highlight = white
verbose = blue
warn = bright purple
error = red
debug = dark gray
deprecate = purple
skip = cyan
unreachable = red
ok = green
changed = yellow
diff_add = green
diff_remove = red
diff_lines = cyan
[inventory]
# Inventory plugins
enable_plugins = host_list, script, auto, yaml, ini, toml
# Ignore patterns
# ignore_patterns = *.bak, *.tmp
[diff]
# Always show diffs
always = False
# Context lines in diffs
context = 3
---
# Ansible Requirements File
# Install with: ansible-galaxy install -r requirements.yml
# For more info: https://docs.ansible.com/ansible/latest/galaxy/user_guide.html
# Collections
collections:
# Core collections
- name: ansible.posix
version: ">=1.5.0"
source: https://galaxy.ansible.com
- name: community.general
version: ">=8.0.0"
source: https://galaxy.ansible.com
# Example: Cloud providers
# - name: amazon.aws
# version: ">=7.0.0"
# source: https://galaxy.ansible.com
#
# - name: azure.azcollection
# version: ">=2.0.0"
# source: https://galaxy.ansible.com
#
# - name: google.cloud
# version: ">=1.2.0"
# source: https://galaxy.ansible.com
# Example: Docker/Kubernetes
# - name: community.docker
# version: ">=3.0.0"
# source: https://galaxy.ansible.com
#
# - name: kubernetes.core
# version: ">=3.0.0"
# source: https://galaxy.ansible.com
# Example: Monitoring/Observability
# - name: community.grafana
# version: ">=1.6.0"
# source: https://galaxy.ansible.com
# Example: Security/Secrets Management
# - name: community.hashi_vault
# version: ">=6.0.0"
# source: https://galaxy.ansible.com
# Example: Database
# - name: community.postgresql
# version: ">=3.0.0"
# source: https://galaxy.ansible.com
#
# - name: community.mysql
# version: ">=3.0.0"
# source: https://galaxy.ansible.com
# Example: Windows
# - name: ansible.windows
# version: ">=2.0.0"
# source: https://galaxy.ansible.com
#
# - name: community.windows
# version: ">=2.0.0"
# source: https://galaxy.ansible.com
# Roles
roles:
# Example: Install role from Ansible Galaxy
# - name: geerlingguy.nginx
# version: "3.1.4"
# source: https://galaxy.ansible.com
#
# - name: geerlingguy.postgresql
# version: "3.4.6"
# source: https://galaxy.ansible.com
# Example: Install role from GitHub
# - name: my-custom-role
# src: https://github.com/username/ansible-role-custom.git
# version: main
# scm: git
# Example: Install role from private Git repository
# - name: internal-role
# src: git@gitlab.company.com:ansible/internal-role.git
# version: v1.0.0
# scm: git
# Example: Install role to specific path
# - name: my-role
# src: https://github.com/username/ansible-role-myapp.git
# version: v2.1.0
# scm: git
# Notes:
# - Use semantic versioning for better dependency management
# - Pin specific versions for production environments
# - Use version ranges (>=, <=, etc.) for development
# - Keep this file in version control
# - Run ansible-galaxy collection install -r requirements.yml --force to update
# - Run ansible-galaxy role install -r requirements.yml --force to update roles
---
# Role: [ROLE_NAME]
# Default variables (lowest priority - can be easily overridden)
# Package and service names
[role_name]_package_name: [package_name]
[role_name]_service_name: [service_name]
# User and group
[role_name]_user: [service_user]
[role_name]_group: [service_group]
# Directories
[role_name]_config_dir: /etc/[service_name]
[role_name]_data_dir: /var/lib/[service_name]
[role_name]_log_dir: /var/log/[service_name]
# Configuration
[role_name]_port: [default_port]
[role_name]_bind_address: 0.0.0.0
[role_name]_max_connections: 100
# Feature flags
[role_name]_enable_ssl: false
[role_name]_enable_monitoring: true
[role_name]_enable_backup: true
# Version (if applicable)
[role_name]_version: latest
---
# Role: [ROLE_NAME]
# Handlers
- name: Restart [service_name]
ansible.builtin.service:
name: "{{ [role_name]_service_name }}"
state: restarted
- name: Reload [service_name]
ansible.builtin.service:
name: "{{ [role_name]_service_name }}"
state: reloaded
- name: Validate configuration
ansible.builtin.command: "[validation_command]"
changed_when: false
---
# Role Argument Specifications (Ansible 2.11+)
# This file enables automatic validation of role variables
# For more info: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#role-argument-validation
argument_specs:
# Main entry point (required)
main:
short_description: [SHORT_DESCRIPTION_OF_ROLE]
description:
- [DETAILED_DESCRIPTION_LINE_1]
- [DETAILED_DESCRIPTION_LINE_2]
author:
- [AUTHOR_NAME]
options:
# Example: String variable
[role_name]_package_name:
type: str
required: false
default: [DEFAULT_PACKAGE]
description: Name of the package to install
# Example: Integer variable
[role_name]_port:
type: int
required: false
default: [DEFAULT_PORT]
description: Port number for the service
# Example: Boolean variable
[role_name]_enabled:
type: bool
required: false
default: true
description: Whether to enable the service
# Example: List variable
[role_name]_packages:
type: list
elements: str
required: false
default:
- [PACKAGE_1]
- [PACKAGE_2]
description: List of packages to install
# Example: Dictionary variable
[role_name]_config:
type: dict
required: false
default: {}
description: Configuration dictionary
options:
host:
type: str
required: false
default: localhost
description: Hostname for the service
port:
type: int
required: false
default: 5432
description: Port for the service
# Example: Path variable
[role_name]_config_dir:
type: path
required: false
default: /etc/[SERVICE_NAME]
description: Configuration directory path
# Example: Required variable
[role_name]_version:
type: str
required: true
description: Version to install (required)
# Example: Variable with choices
[role_name]_state:
type: str
required: false
default: present
choices:
- present
- absent
- latest
description: Desired state of the installation
# Available data types:
# - str: String
# - int: Integer
# - float: Float
# - bool: Boolean
# - list: List/Array
# - dict: Dictionary/Object
# - path: File system path
# - raw: Any type (no validation)
# - jsonarg: JSON string
# - json: JSON object
# - bytes: Bytes
# - bits: Bits
---
# Role: [ROLE_NAME]
# Metadata
galaxy_info:
role_name: [role_name]
author: [Author Name]
description: [Role description]
company: [Company Name]
license: MIT
min_ansible_version: "2.10"
platforms:
- name: Ubuntu
versions:
- focal # 20.04
- jammy # 22.04
- noble # 24.04
- name: Debian
versions:
- bullseye # 11
- bookworm # 12
- name: EL
versions:
- 8
- 9
galaxy_tags:
- [tag1]
- [tag2]
- [tag3]
dependencies: []
# dependencies:
# - role: common
# vars:
# some_var: value
---
- name: Converge
hosts: all
become: true
roles:
- role: [ROLE_NAME]
---
# Molecule test scenario for role: [ROLE_NAME]
# Requirements: pip install molecule molecule-plugins[docker]
# Usage: molecule test
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: instance-ubuntu
image: "geerlingguy/docker-ubuntu2204-ansible:latest"
command: ""
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
privileged: true
pre_build_image: true
- name: instance-rhel9
image: "geerlingguy/docker-rockylinux9-ansible:latest"
command: ""
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
privileged: true
pre_build_image: true
provisioner:
name: ansible
playbooks:
converge: converge.yml
config_options:
defaults:
interpreter_python: auto_silent
verifier:
name: ansible
Ansible Role: [ROLE_NAME]
[Brief description of what this role does]
Requirements
- Ansible 2.10 or higher
- Supported platforms:
- Ubuntu 20.04, 22.04, 24.04
- Debian 11, 12
- RHEL/CentOS/Rocky 8, 9
Role Variables
Required Variables
# [var_name]: [description]Optional Variables
# Package and service
[role_name]_package_name: [package_name] # Package to install
[role_name]_service_name: [service_name] # Service name
[role_name]_version: latest # Version to install
# Directories
[role_name]_config_dir: /etc/[service_name]
[role_name]_data_dir: /var/lib/[service_name]
[role_name]_log_dir: /var/log/[service_name]
# Configuration
[role_name]_port: [default_port]
[role_name]_bind_address: 0.0.0.0
[role_name]_max_connections: 100
# Features
[role_name]_enable_ssl: false
[role_name]_enable_monitoring: trueDependencies
None.
Example Playbook
- hosts: servers
become: true
roles:
- role: [role_name]
vars:
[role_name]_port: [custom_port]
[role_name]_enable_ssl: trueExample with Variables
- hosts: production
become: true
vars:
[role_name]_port: [custom_port]
[role_name]_max_connections: 200
[role_name]_enable_ssl: true
[role_name]_ssl_cert: /etc/ssl/certs/app.crt
[role_name]_ssl_key: /etc/ssl/private/app.key
roles:
- [role_name]Tags
install- Installation tasksconfigure- Configuration tasksservice- Service management taskspackages- Package installationdirectories- Directory creation
License
MIT
Author Information
[Author Name] [Contact Information]
---
# Role: [ROLE_NAME]
# Tasks: Main task list
- name: Include OS-specific variables
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
tags:
- always
- name: Ensure [package/service] is installed
ansible.builtin.package:
name: "{{ [role_name]_package_name }}"
state: present
tags:
- install
- packages
- name: Create required directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0755'
owner: "{{ [role_name]_user }}"
group: "{{ [role_name]_group }}"
loop:
- "{{ [role_name]_config_dir }}"
- "{{ [role_name]_data_dir }}"
- "{{ [role_name]_log_dir }}"
tags:
- configure
- directories
- name: Deploy configuration file
ansible.builtin.template:
src: config.j2
dest: "{{ [role_name]_config_dir }}/config.yml"
mode: '0644'
owner: "{{ [role_name]_user }}"
group: "{{ [role_name]_group }}"
backup: true
validate: "[validation_command] %s"
notify: Restart [service_name]
tags:
- configure
- name: Ensure service is started and enabled
ansible.builtin.service:
name: "{{ [role_name]_service_name }}"
state: started
enabled: true
tags:
- service
# Configuration file for {{ [role_name]_service_name }}
# Managed by Ansible - DO NOT EDIT MANUALLY
# Generated on: {{ ansible_date_time.iso8601 }}
# Host: {{ ansible_hostname }}
[section]
port = {{ [role_name]_port }}
bind_address = {{ [role_name]_bind_address }}
max_connections = {{ [role_name]_max_connections }}
{% if [role_name]_enable_ssl %}
[ssl]
enabled = true
cert_path = {{ [role_name]_ssl_cert }}
key_path = {{ [role_name]_ssl_key }}
{% endif %}
[logging]
log_dir = {{ [role_name]_log_dir }}
log_level = {{ [role_name]_log_level | default('info') }}
[data]
data_dir = {{ [role_name]_data_dir }}
---
# OS-specific variables for Debian/Ubuntu
[role_name]_package_name: [debian_package_name]
[role_name]_service_name: [debian_service_name]
[role_name]_config_dir: /etc/[service_name]
---
# Role: [ROLE_NAME]
# Variables (higher priority - harder to override)
# Internal variables that should not be overridden
[role_name]_internal_var: value
# Derived variables
[role_name]_full_config_path: "{{ [role_name]_config_dir }}/config.yml"
[role_name]_pid_file: "/var/run/{{ [role_name]_service_name }}.pid"
---
# OS-specific variables for RHEL/CentOS/Rocky
[role_name]_package_name: [redhat_package_name]
[role_name]_service_name: [redhat_service_name]
[role_name]_config_dir: /etc/[service_name]
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: nginxUse 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.confIdempotency
- 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/appError 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.failedVariables and Facts
Variable Precedence (High to Low)
1. Extra vars (-e in CLI) 2. Task vars 3. Block vars 4. Role and include vars 5. Set_facts / registered vars 6. Play vars 7. Play vars_files 8. Role defaults 9. 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 (RHEL 8+)
ansible.builtin.dnf:
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: restartedHandler 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: true# 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.ymlFile Permissions
- name: Copy sensitive file
ansible.builtin.copy:
src: private_key
dest: /etc/ssl/private/app.key
mode: '0600'
owner: root
group: rootTags
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: true
vars:
app_version: "1.2.3"
app_port: 8080
pre_tasks:
- name: Update package cache
ansible.builtin.apt:
update_cache: true
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: restartedTesting 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: false
changed_when: falseDiff Mode
# Show differences
ansible-playbook playbook.yml --check --diffAssert 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: false
tasks:
- name: Ping hosts
ansible.builtin.ping:
# Gather specific facts
- name: Gather minimal facts
hosts: all
gather_facts: true
gather_subset:
- '!all'
- '!min'
- networkParallelism
# 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 timeAsync 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: 10Documentation
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
1. Not using FQCN - Always use fully qualified collection names 2. Hard-coded values - Use variables for configuration 3. Not handling different OS - Check ansible_os_family or ansible_distribution 4. Ignoring idempotency - Tasks should be safe to run multiple times 5. Not using handlers - Restart services via handlers, not direct tasks 6. Sensitive data in plain text - Use ansible-vault for secrets 7. Not using tags - Tags enable selective execution 8. Not validating - Always run with --check first 9. Complex logic in playbooks - Move complex logic to roles 10. Not documenting variables - Document required and optional vars
Module Selection Priority
1. Builtin modules first: Use ansible.builtin.* modules when available 2. Collection modules: Use official collection modules (e.g., community.general.*) 3. Custom modules: Only when no suitable module exists 4. 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: presentansible.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: presentansible.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: presentansible.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: epelFile 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: rootansible.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: trueansible.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: trueansible.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: absentansible.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: trueService 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: falseansible.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: trueUser 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: true
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_rsaansible.builtin.group
- name: Create group
ansible.builtin.group:
name: appgroup
gid: 1500
state: presentansible.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@hostCommand 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: localhostansible.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.flagansible.builtin.script
- name: Run script from control node
ansible.builtin.script: scripts/setup.sh
args:
creates: /etc/app/setup.doneGit Operations
ansible.builtin.git
- name: Clone repository
ansible.builtin.git:
repo: https://github.com/user/repo.git
dest: /opt/app
version: main
force: true
- 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: trueArchive 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: true
- name: Download and extract
ansible.builtin.unarchive:
src: https://example.com/app.tar.gz
dest: /opt/
remote_src: trueansible.builtin.archive
- name: Create archive
ansible.builtin.archive:
path:
- /opt/app/config
- /opt/app/data
dest: /tmp/backup.tar.gz
format: gzDownload 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.jsonCron 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: absentDebug 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: falseSet 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.ymlansible.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
- yamlWait 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: 60Error 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.logBlock 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: true
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: 60Configuration 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: true
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: 200Accessing 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: true
file_type: file
register: backup_files
# Find directories
- name: Find empty directories
ansible.builtin.find:
paths: /tmp
file_type: directory
recurse: false
register: directories
- name: Remove empty directories
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ directories.files }}"
when: item.isdiransible.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.islnkAdvanced 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: falserun_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_resultCommon 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: enabledcommunity.general.timezone
- name: Set timezone
community.general.timezone:
name: America/New_Yorkcommunity.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: productioncommunity.postgresql
community.postgresql.postgresql_db
- name: Create database
community.postgresql.postgresql_db:
name: appdb
state: presentansible.posix
ansible.posix.mount
- name: Mount filesystem
ansible.posix.mount:
path: /data
src: /dev/sdb1
fstype: ext4
state: mountedansible.posix.sysctl
- name: Set sysctl parameter
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
state: present
reload: trueCloud 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: terminatedamazon.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: true
# 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: delobjamazon.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: myappMicrosoft 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: absentazure.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.ymlazure.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: myappSecrets 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: trueAWS 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: trueAzure 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---
# Playbook: nginx-tls-playbook.yml
# Description: Deploy nginx with TLS on Ubuntu/Debian and RHEL 8+
# Requirements:
# - Ansible 2.10+
# - Target hosts: Ubuntu 20.04+, Debian 11+, or RHEL/Rocky 8+
# Variables:
# - nginx_tls_cert: Path to TLS certificate (required)
# - nginx_tls_key: Path to TLS private key (required)
# - nginx_server_name: Server name / virtual host (default: ansible_fqdn)
# - nginx_http_port: HTTP port (default: 80)
# - nginx_https_port: HTTPS port (default: 443)
# Usage:
# ansible-playbook -i inventory/production/hosts.yml nginx-tls-playbook.yml \
# -e "nginx_tls_cert=/etc/ssl/certs/app.crt nginx_tls_key=/etc/ssl/private/app.key"
- name: Deploy nginx with TLS
hosts: webservers
become: true
gather_facts: true
vars:
nginx_http_port: 80
nginx_https_port: 443
nginx_server_name: "{{ ansible_fqdn }}"
pre_tasks:
- name: Assert required variables are defined
ansible.builtin.assert:
that:
- nginx_tls_cert is defined
- nginx_tls_key is defined
fail_msg: "nginx_tls_cert and nginx_tls_key must be provided"
- name: Update apt cache (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
tags:
- always
- name: Update dnf cache (RHEL 8+)
ansible.builtin.dnf:
update_cache: true
when: ansible_os_family == "RedHat"
tags:
- always
tasks:
- name: Install nginx (Debian/Ubuntu)
ansible.builtin.apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
tags:
- install
- packages
- name: Install nginx (RHEL 8+)
ansible.builtin.dnf:
name: nginx
state: present
when: ansible_os_family == "RedHat"
tags:
- install
- packages
- name: Ensure TLS certificate exists
ansible.builtin.stat:
path: "{{ nginx_tls_cert }}"
register: tls_cert_stat
tags:
- configure
- tls
- name: Fail if TLS certificate is missing
ansible.builtin.fail:
msg: "TLS certificate not found at {{ nginx_tls_cert }}"
when: not tls_cert_stat.stat.exists
tags:
- configure
- tls
- name: Deploy nginx configuration
ansible.builtin.template:
src: nginx-tls.conf.j2
dest: /etc/nginx/conf.d/tls.conf
mode: '0644'
owner: root
group: root
backup: true
validate: nginx -t -c %s
notify: Reload nginx
tags:
- configure
- name: Ensure nginx is started and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
tags:
- service
post_tasks:
- name: Verify nginx responds on HTTPS
ansible.builtin.uri:
url: "https://localhost:{{ nginx_https_port }}/"
validate_certs: false
status_code: [200, 301, 302]
register: health_check
until: health_check.status in [200, 301, 302]
retries: 5
delay: 5
tags:
- verify
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
server {
listen {{ nginx_http_port }};
listen {{ nginx_https_port }} ssl;
server_name {{ nginx_server_name }};
ssl_certificate {{ nginx_tls_cert }};
ssl_certificate_key {{ nginx_tls_key }};
location / {
return 200 "ok\n";
add_header Content-Type text/plain;
}
}
---
# Role: sample-role
# Default variables (lowest priority — override freely in playbooks or group_vars)
# Package and service
sample_role_package_name: curl
sample_role_service_name: sample-service
# User and group (the service runs as)
sample_role_user: root
sample_role_group: root
# Directories
sample_role_config_dir: /etc/sample-service
sample_role_data_dir: /var/lib/sample-service
sample_role_log_dir: /var/log/sample-service
# Service configuration
sample_role_port: 8080
sample_role_bind_address: 127.0.0.1
sample_role_max_connections: 100
# Feature flags
sample_role_enable_ssl: false
sample_role_enable_monitoring: true
sample_role_enable_backup: true
---
# Role: sample-role
# Handlers
- name: Restart sample-service
ansible.builtin.service:
name: "{{ sample_role_service_name }}"
state: restarted
- name: Reload sample-service
ansible.builtin.service:
name: "{{ sample_role_service_name }}"
state: reloaded
---
# Role: sample-role
# Metadata
galaxy_info:
role_name: sample_role
standalone: true
author: Your Name
description: Sample role demonstrating correct ansible-generator output patterns
license: MIT
min_ansible_version: "2.10"
platforms:
- name: Ubuntu
versions:
- focal # 20.04
- jammy # 22.04
- noble # 24.04
- name: Debian
versions:
- bullseye # 11
- bookworm # 12
- name: EL
versions:
- all
galaxy_tags:
- sample
- example
dependencies: []
---
# Role: sample-role
# Tasks: Main task list
# Required variables: none (all have defaults)
- name: Include OS-specific variables with fallback
ansible.builtin.include_vars: "{{ lookup('ansible.builtin.first_found', params) }}"
vars:
params:
files:
- "{{ ansible_distribution }}.yml"
- "{{ ansible_os_family }}.yml"
- default.yml
paths:
- vars
tags:
- always
- name: Ensure sample package is installed (Debian/Ubuntu)
ansible.builtin.apt:
name: "{{ sample_role_package_name }}"
state: present
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
tags:
- install
- packages
- name: Ensure sample package is installed (RHEL 8+)
ansible.builtin.dnf:
name: "{{ sample_role_package_name }}"
state: present
when: ansible_os_family == "RedHat"
tags:
- install
- packages
- name: Create required directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0755'
owner: "{{ sample_role_user }}"
group: "{{ sample_role_group }}"
loop:
- "{{ sample_role_config_dir }}"
- "{{ sample_role_data_dir }}"
- "{{ sample_role_log_dir }}"
tags:
- configure
- directories
- name: Deploy configuration file
ansible.builtin.template:
src: config.j2
dest: "{{ sample_role_config_dir }}/config.yml"
mode: '0644'
owner: "{{ sample_role_user }}"
group: "{{ sample_role_group }}"
backup: true
notify: Restart sample-service
tags:
- configure
- name: Ensure service is started and enabled
ansible.builtin.service:
name: "{{ sample_role_service_name }}"
state: started
enabled: true
tags:
- service
service:
name: {{ sample_role_service_name }}
user: {{ sample_role_user }}
group: {{ sample_role_group }}
paths:
config: {{ sample_role_config_dir }}
data: {{ sample_role_data_dir }}
log: {{ sample_role_log_dir }}
network:
bind_address: {{ sample_role_bind_address }}
port: {{ sample_role_port }}
---
# OS-specific variables for Debian/Ubuntu
sample_role_package_name: curl
sample_role_service_name: sample-service
sample_role_config_dir: /etc/sample-service
---
# Fallback variables for unsupported OS families
sample_role_package_name: curl
sample_role_service_name: sample-service
sample_role_config_dir: /etc/sample-service
---
# OS-specific variables for RHEL/CentOS/Rocky
sample_role_package_name: curl
sample_role_service_name: sample-service
sample_role_config_dir: /etc/sample-service
#!/usr/bin/env python3
"""Regression checks for ansible-generator test fixtures."""
from __future__ import annotations
import os
import shutil
import subprocess
import tempfile
import textwrap
import unittest
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parents[1]
TEST_DIR = SKILL_DIR / "test"
PLAYBOOK_DIR = TEST_DIR / "playbooks"
ROLE_DIR = TEST_DIR / "roles" / "sample-role"
class FixtureTemplateTests(unittest.TestCase):
def test_nginx_playbook_template_exists(self) -> None:
playbook_text = (PLAYBOOK_DIR / "nginx-tls-playbook.yml").read_text(encoding="utf-8")
self.assertIn("src: nginx-tls.conf.j2", playbook_text)
self.assertTrue((PLAYBOOK_DIR / "templates" / "nginx-tls.conf.j2").is_file())
def test_sample_role_template_exists(self) -> None:
tasks_text = (ROLE_DIR / "tasks" / "main.yml").read_text(encoding="utf-8")
self.assertIn("src: config.j2", tasks_text)
self.assertTrue((ROLE_DIR / "templates" / "config.j2").is_file())
class RoleMetadataTests(unittest.TestCase):
def test_role_name_uses_lint_compatible_format(self) -> None:
meta_text = (ROLE_DIR / "meta" / "main.yml").read_text(encoding="utf-8")
self.assertIn("role_name: sample_role", meta_text)
def test_standalone_flag_is_declared(self) -> None:
meta_text = (ROLE_DIR / "meta" / "main.yml").read_text(encoding="utf-8")
self.assertIn("standalone: true", meta_text)
def test_el_platform_uses_schema_safe_version(self) -> None:
meta_text = (ROLE_DIR / "meta" / "main.yml").read_text(encoding="utf-8")
self.assertIn("- name: EL", meta_text)
self.assertIn("- all", meta_text)
class OsVarFallbackTests(unittest.TestCase):
def test_task_uses_first_found_and_default_vars_file(self) -> None:
tasks_text = (ROLE_DIR / "tasks" / "main.yml").read_text(encoding="utf-8")
self.assertIn("lookup('ansible.builtin.first_found', params)", tasks_text)
self.assertTrue((ROLE_DIR / "vars" / "default.yml").is_file())
def test_unknown_os_family_uses_fallback_vars_file(self) -> None:
if shutil.which("ansible-playbook") is None:
self.skipTest("ansible-playbook is not installed")
with tempfile.TemporaryDirectory(prefix="ansible-generator-role-test-") as tmp_dir:
temp_playbook = Path(tmp_dir) / "fallback-vars-smoke.yml"
temp_playbook.write_text(
textwrap.dedent(
"""\
---
- name: Verify sample role var fallback
hosts: localhost
gather_facts: false
connection: local
vars:
ansible_distribution: UnknownDistro
ansible_os_family: UnknownFamily
roles:
- role: sample-role
"""
),
encoding="utf-8",
)
env = os.environ.copy()
env["ANSIBLE_ROLES_PATH"] = str((TEST_DIR / "roles").resolve())
cmd = [
"ansible-playbook",
str(temp_playbook),
"-i",
"localhost,",
"--tags",
"always",
]
result = subprocess.run(
cmd,
capture_output=True,
check=False,
env=env,
text=True,
)
self.assertEqual(
result.returncode,
0,
msg=(
"Expected role var loading to succeed for unknown OS family.\n"
f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
),
)
if __name__ == "__main__":
unittest.main()
Related skills
How it compares
Use ansible-generator for Ansible YAML scaffolding; pick Terraform-focused skills when infrastructure state is managed purely through HCL modules.
FAQ
What artifacts does ansible-generator create?
ansible-generator produces Ansible playbooks, roles, and inventories from stated requirements. Output is structured for version control and repeatable execution in provisioning or deployment pipelines.
When should developers use ansible-generator?
ansible-generator accelerates initial Ansible scaffolding for server setup and config management. Teams integrate generated playbooks into CI/CD instead of manual SSH configuration steps.