
Managing Configuration
- 46 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
managing-configuration is a Claude Code skill that guides server and application configuration automation with Ansible, including playbooks, roles, inventories, secrets, and Molecule testing.
About
This skill guides configuration automation for servers and applications using Ansible. It covers playbook and role structure, static and dynamic (cloud) inventory management, secrets handling with ansible-vault or HashiCorp Vault, idempotency best practices, and testing roles with Molecule. Developers use it when automating server configuration, deploying applications with playbooks, or implementing GitOps workflows for configuration as code.
- Server and application configuration automation using Ansible
- Covers playbooks, roles, static and dynamic inventories, and secrets management
- Includes idempotency patterns, ansible-vault/HashiCorp Vault, and Molecule testing
Managing Configuration by the numbers
- 46 all-time installs (skills.sh)
- Ranked #760 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
managing-configuration capabilities & compatibility
- Capabilities
- ansible playbooks · role structure · dynamic inventory · secrets management
- Works with
- aws · azure · gcp · docker
- Use cases
- devops · ci cd
- Pricing
- Free
What managing-configuration says it does
This skill provides guidance for automating server and application configuration using Ansible and related tools.
Run playbooks multiple times without unintended side effects. Use state-based modules (`present`, `started`, `latest`) instead of imperative commands.
npx skills add https://github.com/ancoleman/ai-design-components --skill managing-configurationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 46 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Automating server and app configuration with Ansible playbooks, roles, inventories, secrets, and Molecule testing.
Who is it for?
Teams automating server and app configuration with Ansible and testing roles before production.
Skip if: Kubernetes-native GitOps that uses declarative manifests instead of Ansible.
When should I use this skill?
You are creating Ansible playbooks, managing inventories, securing secrets, or testing roles with Molecule.
What you get
Idempotent, tested Ansible playbooks and roles with managed inventories and secrets.
- Ansible playbooks
- Reusable roles
- Static and dynamic inventories
By the numbers
- standard 6-directory role structure (defaults, tasks, handlers, templates, files, meta)
- 3 documented workflows
Files
Configuration Management
Purpose
This skill provides guidance for automating server and application configuration using Ansible and related tools. It covers playbook creation, role structure, inventory management (static and dynamic), secret management, testing patterns, and idempotency best practices to ensure safe, repeatable configuration deployments.
When to Use This Skill
Invoke this skill when:
- Creating Ansible playbooks to configure servers or deploy applications
- Structuring reusable Ansible roles with proper directory layout
- Managing inventories (static files or dynamic cloud-based)
- Securing secrets with ansible-vault or HashiCorp Vault integration
- Testing roles with Molecule before production deployment
- Ensuring idempotent playbooks that safely run multiple times
- Migrating from Chef or Puppet to Ansible
- Implementing GitOps workflows for configuration as code
- Debugging playbook failures or handler issues
Quick Start
Basic Playbook Example
---
# site.yml
- name: Configure web servers
hosts: webservers
become: yes
tasks:
- name: Ensure nginx is installed
apt:
name: nginx
state: present
notify: Restart nginx
- name: Start nginx service
service:
name: nginx
state: started
enabled: yes
handlers:
- name: Restart nginx
service:
name: nginx
state: restartedRun with:
ansible-playbook -i inventory/production site.ymlCore Concepts
1. Idempotency
Run playbooks multiple times without unintended side effects. Use state-based modules (present, started, latest) instead of imperative commands.
Idempotent (good):
- name: Ensure package installed
apt:
name: nginx
state: presentNot idempotent (avoid):
- name: Install package
command: apt-get install -y nginxSee references/idempotency-guide.md for detailed patterns.
2. Inventory Management
Static Inventory: INI or YAML files for stable environments. Dynamic Inventory: Scripts or plugins for cloud environments (AWS, Azure, GCP).
Example static inventory (INI):
[webservers]
web1.example.com ansible_host=10.0.1.10
web2.example.com ansible_host=10.0.1.11
[webservers:vars]
nginx_worker_processes=4See references/inventory-management.md for dynamic inventory setup.
3. Roles vs Playbooks
Playbooks: Orchestrate multiple tasks and roles for specific deployments. Roles: Reusable, self-contained configuration units with standardized directory structure.
Standard role structure:
roles/nginx/
├── defaults/ # Default variables
├── tasks/ # Task files
├── handlers/ # Change handlers
├── templates/ # Jinja2 templates
├── files/ # Static files
└── meta/ # DependenciesSee references/role-structure.md for complete role patterns.
4. Secret Management
ansible-vault: Built-in encryption for sensitive data. HashiCorp Vault: Enterprise-grade secrets management with dynamic credentials.
Encrypt secrets:
ansible-vault create group_vars/all/vault.yml
ansible-playbook site.yml --ask-vault-passSee references/secrets-management.md for Vault integration.
Common Workflows
Workflow 1: Create New Playbook
Step 1: Define inventory
# inventory/production
[webservers]
web1.example.com
web2.example.comStep 2: Create playbook structure
---
- name: Configure application
hosts: webservers
become: yes
pre_tasks:
- name: Update package cache
apt:
update_cache: yes
roles:
- common
- application
post_tasks:
- name: Verify service
uri:
url: http://localhost:8080/health
status_code: 200Step 3: Test with check mode
ansible-playbook -i inventory/production site.yml --check --diffStep 4: Execute playbook
ansible-playbook -i inventory/production site.ymlSee references/playbook-patterns.md for advanced patterns.
Workflow 2: Create and Test Role
Step 1: Initialize role structure
ansible-galaxy init roles/myappStep 2: Define tasks
# roles/myapp/tasks/main.yml
---
- name: Install application dependencies
apt:
name: "{{ item }}"
state: present
loop: "{{ myapp_dependencies }}"
- name: Deploy application
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: Restart myappStep 3: Add handler
# roles/myapp/handlers/main.yml
---
- name: Restart myapp
service:
name: myapp
state: restartedStep 4: Initialize Molecule testing
cd roles/myapp
molecule init scenario default --driver-name dockerStep 5: Run tests
molecule testSee references/testing-guide.md for comprehensive testing patterns.
Workflow 3: Set Up Dynamic Inventory (AWS)
Step 1: Install AWS collection
ansible-galaxy collection install amazon.awsStep 2: Configure dynamic inventory
# inventory/aws_ec2.yml
plugin: aws_ec2
regions:
- us-east-1
filters:
tag:Environment: production
instance-state-name: running
keyed_groups:
- key: tags.Role
prefix: role
hostnames:
- tag:Name
compose:
ansible_host: private_ip_addressStep 3: Verify inventory
ansible-inventory -i inventory/aws_ec2.yml --listStep 4: Run playbook
ansible-playbook -i inventory/aws_ec2.yml site.ymlSee references/inventory-management.md for multi-cloud patterns.
Workflow 4: Secure Secrets with ansible-vault
Step 1: Create encrypted vault file
ansible-vault create group_vars/all/vault.ymlStep 2: Add secrets
# group_vars/all/vault.yml (encrypted)
vault_db_password: "SuperSecretPassword"
vault_api_key: "sk-1234567890"Step 3: Reference in variables
# group_vars/all/vars.yml (unencrypted)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"Step 4: Use in playbook
- name: Configure database
template:
src: db.conf.j2
dest: /etc/app/db.conf
vars:
database_password: "{{ db_password }}"Step 5: Run with vault password
ansible-playbook site.yml --vault-password-file ~/.vault_passSee references/secrets-management.md for HashiCorp Vault integration.
Tool Selection
When to Use Ansible
- Configuring servers/VMs after provisioning
- Deploying applications to existing infrastructure
- Managing OS-level settings (users, packages, services)
- Orchestrating multi-step workflows across hosts
- Cloud-native environments (agentless SSH/WinRM)
- Teams new to configuration management (easiest learning curve)
When to Use Alternatives
Infrastructure-as-Code (Terraform): Creating cloud infrastructure resources. Kubernetes: Container orchestration and configuration. Chef/Puppet: Existing deployments with high migration costs.
Ansible vs IaC Integration
Best practice: Terraform provisions, Ansible configures.
Workflow: 1. Terraform creates AWS EC2 instances, security groups, load balancers 2. Terraform outputs instance IPs to Ansible inventory 3. Ansible configures OS, installs packages, deploys applications 4. Ansible sets up monitoring, backups, operational tasks
See references/decision-framework.md for detailed decision trees.
Testing and Quality
Pre-Deployment Validation
Step 1: Lint playbooks
ansible-lint playbooks/Step 2: Check mode (dry run)
ansible-playbook site.yml --check --diffStep 3: Test roles with Molecule
cd roles/myapp
molecule testStep 4: Verify idempotence
molecule idempotenceConfiguration Files
.ansible-lint:
---
exclude_paths:
- molecule/
- venv/
skip_list:
- name[casing]
warn_list:
- experimentalmolecule.yml:
---
driver:
name: docker
platforms:
- name: instance
image: ubuntu:22.04
pre_build_image: true
provisioner:
name: ansible
verifier:
name: ansibleSee references/testing-guide.md for complete testing strategies.
Troubleshooting
Common Issues
Connection failures:
- Verify SSH access:
ansible all -i inventory -m ping - Check SSH keys:
ssh -vvv user@host - Test with password:
ansible-playbook site.yml --ask-pass
Handler not firing:
- Handlers only run on change (check task
changedstatus) - Handlers run at end of playbook (use
meta: flush_handlersto force earlier) - Handler names must match exactly
Variable not defined:
- Check variable precedence (command-line > playbook > inventory > defaults)
- Use debug module:
- debug: var=myvar - Verify variable files are loaded:
ansible-playbook site.yml -v
Idempotency violations:
- Run playbook twice, compare output
- Check for
changedon every run - Use state-based modules instead of
command/shell
See references/troubleshooting.md for comprehensive debugging guide.
Integration with Other Skills
infrastructure-as-code:
- Terraform provisions infrastructure
- Ansible configures post-provisioning
- Terraform outputs feed Ansible inventory
kubernetes-operations:
- Ansible deploys K8s clusters (kubespray)
- Kubernetes handles container orchestration
- Ansible manages node-level configuration
building-ci-pipelines:
- CI/CD runs ansible-lint for quality checks
- Molecule tests execute in pipeline
- Deployment stage runs playbooks
secret-management:
- ansible-vault for simple use cases
- HashiCorp Vault for enterprise secrets
- Dynamic credentials via Vault lookups
security-hardening:
- Ansible applies CIS benchmarks
- Security roles enforce compliance
- Molecule verifies hardening effectiveness
testing-strategies:
- Molecule for role testing
- Testinfra for verification
- Integration test suites
Reference Documentation
references/playbook-patterns.md- Playbook structure, handlers, tags, variablesreferences/role-structure.md- Role directory layout, best practices, collectionsreferences/inventory-management.md- Static, dynamic, and hybrid inventory patternsreferences/secrets-management.md- ansible-vault and HashiCorp Vault integrationreferences/testing-guide.md- Molecule, ansible-lint, check mode, verificationreferences/idempotency-guide.md- Ensuring safe, repeatable executionsreferences/decision-framework.md- Tool selection and workflow designreferences/chef-puppet-migration.md- Migrating from legacy tools to Ansiblereferences/troubleshooting.md- Common issues and debugging techniques
Example Code
examples/playbooks/- Complete playbook examplesexamples/roles/- Production-ready role templatesexamples/inventory/- Static and dynamic inventory configurationsexamples/molecule/- Molecule test scenarios
Utility Scripts
scripts/validate-playbook.py- Validate playbook syntax and structurescripts/generate-inventory.py- Generate inventory from cloud providersscripts/ansible-vault-helper.sh- Vault management utilitiesscripts/molecule-runner.sh- Automated Molecule test execution
# AWS EC2 dynamic inventory
# Prerequisites: Install collection: ansible-galaxy collection install amazon.aws
# Requires AWS credentials configured (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or ~/.aws/credentials)
# Usage: ansible-playbook -i inventory/dynamic-aws/aws_ec2.yml site.yml
plugin: aws_ec2
# AWS regions to query
regions:
- us-east-1
- us-west-2
# Filter instances
filters:
tag:Environment: production
instance-state-name: running
# Create groups based on tags and attributes
keyed_groups:
# Group by Role tag: role_webserver, role_database, etc.
- key: tags.Role
prefix: role
separator: "_"
# Group by Environment tag: env_production, env_staging
- key: tags.Environment
prefix: env
# Group by instance type: type_t3_micro, type_t3_small
- key: instance_type
prefix: type
# Group by availability zone: az_us_east_1a, az_us_east_1b
- key: placement.availability_zone
prefix: az
# Group by VPC: vpc_vpc_12345
- key: vpc_id
prefix: vpc
# Hostnames priority
hostnames:
- tag:Name # Use Name tag if available
- dns-name # Fall back to DNS name
- private-ip-address # Fall back to private IP
# Compose variables for hosts
compose:
ansible_host: private_ip_address
ansible_user: "'ubuntu'" # Default SSH user
ec2_instance_type: instance_type
ec2_placement: placement.availability_zone
# Enable cache for faster repeated queries
cache: yes
cache_plugin: jsonfile
cache_timeout: 300
cache_connection: /tmp/ansible_inventory_aws
# Static inventory in INI format
# Usage: ansible-playbook -i inventory/static-ini/hosts site.yml
[webservers]
web1.example.com ansible_host=10.0.1.10
web2.example.com ansible_host=10.0.1.11
web3.example.com ansible_host=10.0.1.12
[appservers]
app1.example.com ansible_host=10.0.2.10
app2.example.com ansible_host=10.0.2.11
[databases]
db1.example.com ansible_host=10.0.3.10
db2.example.com ansible_host=10.0.3.11
[loadbalancers]
lb1.example.com ansible_host=10.0.4.10
# Group variables
[webservers:vars]
nginx_worker_processes=4
app_env=production
[databases:vars]
postgres_version=15
postgres_max_connections=200
# Parent groups
[frontend:children]
webservers
loadbalancers
[backend:children]
appservers
databases
[production:children]
frontend
backend
[production:vars]
ansible_user=deploy
ansible_become=yes
ansible_python_interpreter=/usr/bin/python3
---
# Multi-tier application deployment
# Deploys: Load balancer -> Web tier -> App tier -> Database tier
- name: Configure load balancers
hosts: loadbalancers
become: yes
vars:
backend_servers:
- web1.example.com
- web2.example.com
tasks:
- name: Install HAProxy
ansible.builtin.apt:
name: haproxy
state: present
- name: Configure HAProxy
ansible.builtin.template:
src: ../templates/haproxy.cfg.j2
dest: /etc/haproxy/haproxy.cfg
notify: Reload HAProxy
- name: Start HAProxy
ansible.builtin.service:
name: haproxy
state: started
enabled: yes
handlers:
- name: Reload HAProxy
ansible.builtin.service:
name: haproxy
state: reloaded
- name: Configure web tier
hosts: webservers
become: yes
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Configure nginx as reverse proxy
ansible.builtin.template:
src: ../templates/nginx-proxy.conf.j2
dest: /etc/nginx/sites-available/default
notify: Reload nginx
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: yes
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
- name: Configure application tier
hosts: appservers
become: yes
vars:
app_version: "1.0.0"
app_port: 8080
tasks:
- name: Create application user
ansible.builtin.user:
name: appuser
system: yes
shell: /bin/false
- name: Create application directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: appuser
group: appuser
mode: '0755'
loop:
- /opt/myapp
- /etc/myapp
- /var/log/myapp
- name: Deploy application configuration
ansible.builtin.template:
src: ../templates/app.conf.j2
dest: /etc/myapp/app.conf
owner: appuser
group: appuser
mode: '0640'
notify: Restart application
- name: Install systemd service
ansible.builtin.template:
src: ../templates/myapp.service.j2
dest: /etc/systemd/system/myapp.service
mode: '0644'
notify:
- Reload systemd
- Restart application
- name: Start application
ansible.builtin.service:
name: myapp
state: started
enabled: yes
handlers:
- name: Reload systemd
ansible.builtin.systemd:
daemon_reload: yes
- name: Restart application
ansible.builtin.service:
name: myapp
state: restarted
- name: Configure database tier
hosts: databases
become: yes
vars:
postgres_version: "15"
postgres_databases:
- name: myapp
owner: appuser
tasks:
- name: Install PostgreSQL
ansible.builtin.apt:
name:
- "postgresql-{{ postgres_version }}"
- "postgresql-contrib-{{ postgres_version }}"
- python3-psycopg2
state: present
- name: Start PostgreSQL
ansible.builtin.service:
name: postgresql
state: started
enabled: yes
- name: Create application database
community.postgresql.postgresql_db:
name: "{{ item.name }}"
state: present
loop: "{{ postgres_databases }}"
become_user: postgres
- name: Create application user
community.postgresql.postgresql_user:
name: "{{ item.owner }}"
password: "{{ lookup('env', 'DB_PASSWORD') }}"
db: "{{ item.name }}"
priv: ALL
state: present
loop: "{{ postgres_databases }}"
become_user: postgres
- name: Verify deployment
hosts: loadbalancers
become: yes
tasks:
- name: Check application responds through load balancer
ansible.builtin.uri:
url: http://localhost/health
status_code: 200
retries: 5
delay: 3
---
# Simple web server playbook
# Usage: ansible-playbook -i inventory/hosts simple-webserver.yml
- name: Configure web servers
hosts: webservers
become: yes
vars:
nginx_port: 80
nginx_user: www-data
pre_tasks:
- name: Update package cache (Debian/Ubuntu)
ansible.builtin.apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
notify: Restart nginx
- name: Create web root directory
ansible.builtin.file:
path: /var/www/html
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: '0755'
- name: Deploy index page
ansible.builtin.copy:
content: |
<!DOCTYPE html>
<html>
<head><title>Welcome</title></head>
<body>
<h1>Server: {{ ansible_hostname }}</h1>
<p>Managed by Ansible</p>
</body>
</html>
dest: /var/www/html/index.html
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: '0644'
- name: Ensure nginx is started and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: yes
post_tasks:
- name: Verify nginx responds
ansible.builtin.uri:
url: "http://localhost:{{ nginx_port }}"
status_code: 200
retries: 3
delay: 2
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
skill: "managing-configuration"
version: "1.0"
domain: "infrastructure"
# Base outputs required for all configuration management projects
base_outputs:
- path: "ansible.cfg"
must_contain: ["[defaults]", "inventory", "host_key_checking"]
description: "Ansible configuration file with basic settings"
- path: "inventory/"
must_contain: []
description: "Inventory directory for host definitions"
- path: "playbooks/"
must_contain: []
description: "Ansible playbooks directory"
- path: "roles/"
must_contain: []
description: "Ansible roles directory for reusable components"
- path: "group_vars/"
must_contain: []
description: "Group-level variables directory"
- path: ".ansible-lint"
must_contain: ["exclude_paths:", "skip_list:"]
description: "Ansible linting configuration"
# Conditional outputs based on configuration
conditional_outputs:
maturity:
starter:
- path: "inventory/hosts"
must_contain: ["[webservers]"]
description: "Basic static INI inventory"
- path: "playbooks/site.yml"
must_contain: ["hosts:", "tasks:", "become:"]
description: "Simple site-wide playbook"
- path: "playbooks/webserver.yml"
must_contain: ["name:", "hosts:", "tasks:"]
description: "Basic webserver configuration playbook"
- path: "group_vars/all.yml"
must_contain: ["---"]
description: "Global variables file"
- path: "README.md"
must_contain: ["ansible-playbook", "inventory"]
description: "Basic usage documentation"
intermediate:
- path: "roles/common/tasks/main.yml"
must_contain: ["name:", "become:"]
description: "Common role for shared configuration"
- path: "roles/common/handlers/main.yml"
must_contain: ["name:", "service:"]
description: "Common handlers for service restarts"
- path: "roles/common/defaults/main.yml"
must_contain: ["---"]
description: "Default variables for common role"
- path: "inventory/production"
must_contain: ["[", "]"]
description: "Production inventory file"
- path: "inventory/staging"
must_contain: ["[", "]"]
description: "Staging inventory file"
- path: "group_vars/all/vault.yml"
must_contain: ["$ANSIBLE_VAULT"]
description: "Encrypted vault file for secrets"
- path: "group_vars/webservers.yml"
must_contain: ["---"]
description: "Webserver group variables"
- path: "playbooks/deploy.yml"
must_contain: ["pre_tasks:", "roles:", "post_tasks:"]
description: "Deployment playbook with lifecycle hooks"
- path: ".gitignore"
must_contain: ["*.retry", ".vault_pass"]
description: "Git ignore for Ansible artifacts"
advanced:
- path: "roles/"
must_contain: ["tasks/", "handlers/", "templates/", "defaults/", "meta/"]
description: "Full role structure with all components"
- path: "inventory/aws_ec2.yml"
must_contain: ["plugin: aws_ec2", "regions:", "filters:"]
description: "Dynamic AWS inventory configuration"
- path: "inventory/group_vars/"
must_contain: []
description: "Group variables organized by inventory"
- path: "roles/*/molecule/default/molecule.yml"
must_contain: ["driver:", "platforms:", "provisioner:", "verifier:"]
description: "Molecule test configuration for roles"
- path: "roles/*/molecule/default/converge.yml"
must_contain: ["name:", "hosts:", "roles:"]
description: "Molecule converge playbook"
- path: "roles/*/molecule/default/verify.yml"
must_contain: ["name:", "hosts:", "tasks:"]
description: "Molecule verification playbook"
- path: "roles/*/meta/main.yml"
must_contain: ["galaxy_info:", "dependencies:"]
description: "Role metadata and dependencies"
- path: "playbooks/site.yml"
must_contain: ["pre_tasks:", "roles:", "post_tasks:", "handlers:"]
description: "Production site playbook with full lifecycle"
- path: "group_vars/all/vault.yml"
must_contain: ["$ANSIBLE_VAULT"]
description: "Encrypted secrets with ansible-vault"
- path: "plugins/lookup/vault_lookup.py"
must_contain: ["from ansible.plugins.lookup", "class LookupModule"]
description: "Custom Vault lookup plugin for HashiCorp Vault"
- path: ".github/workflows/ansible-ci.yml"
must_contain: ["ansible-lint", "molecule test"]
description: "CI pipeline for Ansible testing"
- path: "requirements.yml"
must_contain: ["collections:", "roles:"]
description: "Ansible Galaxy dependencies"
infrastructure:
kubernetes:
- path: "inventory/k8s.yml"
must_contain: ["plugin:", "kubernetes"]
description: "Dynamic Kubernetes inventory"
- path: "playbooks/k8s-cluster.yml"
must_contain: ["kubeadm", "kubectl"]
description: "Kubernetes cluster deployment playbook"
- path: "roles/kubernetes/tasks/main.yml"
must_contain: ["kubeadm", "kubelet"]
description: "Kubernetes installation role"
- path: "group_vars/k8s_cluster.yml"
must_contain: ["kubernetes_version:", "pod_network_cidr:"]
description: "Kubernetes cluster configuration"
docker:
- path: "playbooks/docker-setup.yml"
must_contain: ["docker", "docker-compose"]
description: "Docker installation playbook"
- path: "roles/docker/tasks/main.yml"
must_contain: ["docker.io", "docker-compose"]
description: "Docker role for container setup"
- path: "templates/docker-compose.yml.j2"
must_contain: ["version:", "services:"]
description: "Docker Compose template"
bare_metal:
- path: "inventory/datacenter"
must_contain: ["[physical_servers]"]
description: "Bare metal server inventory"
- path: "playbooks/bare-metal-provision.yml"
must_contain: ["hosts:", "become: yes"]
description: "Bare metal provisioning playbook"
- path: "roles/hardware/tasks/main.yml"
must_contain: ["package:", "service:"]
description: "Hardware configuration role"
cloud_provider:
aws:
- path: "inventory/aws_ec2.yml"
must_contain: ["plugin: aws_ec2", "regions:", "filters:"]
description: "AWS EC2 dynamic inventory"
- path: "group_vars/aws.yml"
must_contain: ["aws_region:", "ec2_"]
description: "AWS-specific variables"
- path: "playbooks/aws-provision.yml"
must_contain: ["amazon.aws"]
description: "AWS provisioning playbook"
- path: "requirements.yml"
must_contain: ["amazon.aws"]
description: "AWS collection dependency"
gcp:
- path: "inventory/gcp_compute.yml"
must_contain: ["plugin: gcp_compute", "projects:", "filters:"]
description: "GCP Compute Engine dynamic inventory"
- path: "group_vars/gcp.yml"
must_contain: ["gcp_project:", "gcp_zone:"]
description: "GCP-specific variables"
- path: "playbooks/gcp-provision.yml"
must_contain: ["google.cloud"]
description: "GCP provisioning playbook"
- path: "requirements.yml"
must_contain: ["google.cloud"]
description: "GCP collection dependency"
azure:
- path: "inventory/azure_rm.yml"
must_contain: ["plugin: azure_rm", "include_vm_resource_groups:"]
description: "Azure Resource Manager dynamic inventory"
- path: "group_vars/azure.yml"
must_contain: ["azure_subscription_id:", "azure_resource_group:"]
description: "Azure-specific variables"
- path: "playbooks/azure-provision.yml"
must_contain: ["azure.azcollection"]
description: "Azure provisioning playbook"
- path: "requirements.yml"
must_contain: ["azure.azcollection"]
description: "Azure collection dependency"
multi-cloud:
- path: "inventory/aws_ec2.yml"
must_contain: ["plugin: aws_ec2"]
description: "AWS dynamic inventory"
- path: "inventory/gcp_compute.yml"
must_contain: ["plugin: gcp_compute"]
description: "GCP dynamic inventory"
- path: "inventory/azure_rm.yml"
must_contain: ["plugin: azure_rm"]
description: "Azure dynamic inventory"
- path: "group_vars/cloud_common.yml"
must_contain: ["---"]
description: "Common cloud configuration"
- path: "playbooks/multi-cloud-deploy.yml"
must_contain: ["hosts: all"]
description: "Multi-cloud deployment playbook"
secret_management:
ansible_vault:
- path: "group_vars/all/vault.yml"
must_contain: ["$ANSIBLE_VAULT"]
description: "Encrypted secrets with ansible-vault"
- path: ".vault_pass.example"
must_contain: ["# Store your vault password"]
description: "Vault password file example"
- path: "ansible.cfg"
must_contain: ["vault_password_file"]
description: "Ansible config with vault password reference"
hashicorp_vault:
- path: "plugins/lookup/hashi_vault.py"
must_contain: ["hvac", "vault_addr"]
description: "HashiCorp Vault lookup plugin"
- path: "group_vars/all/vault_config.yml"
must_contain: ["vault_addr:", "vault_token:"]
description: "Vault connection configuration"
- path: "playbooks/vault-integration.yml"
must_contain: ["lookup('hashi_vault'"]
description: "Playbook using Vault lookups"
# Scaffolding files that should be created as starting points
scaffolding:
- path: "ansible.cfg"
type: "file"
template: |
[defaults]
inventory = inventory/
host_key_checking = False
retry_files_enabled = False
roles_path = roles
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 3600
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
description: "Default Ansible configuration file"
- path: ".ansible-lint"
type: "file"
template: |
---
exclude_paths:
- molecule/
- venv/
- .venv/
- .tox/
skip_list:
- name[casing]
warn_list:
- experimental
use_default_rules: true
description: "Ansible linting configuration"
- path: "inventory/hosts"
type: "file"
template: |
[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/id_rsa
[webservers]
web1.example.com ansible_host=10.0.1.10
web2.example.com ansible_host=10.0.1.11
[databases]
db1.example.com ansible_host=10.0.2.10
[webservers:vars]
nginx_worker_processes=4
[databases:vars]
postgresql_version=14
description: "Example static inventory file"
- path: "playbooks/site.yml"
type: "file"
template: |
---
- name: Configure all servers
hosts: all
become: yes
pre_tasks:
- name: Update package cache
apt:
update_cache: yes
cache_valid_time: 3600
roles:
- common
- name: Configure web servers
hosts: webservers
become: yes
roles:
- nginx
post_tasks:
- name: Verify web service
uri:
url: http://localhost
status_code: 200
description: "Example site-wide playbook"
- path: "group_vars/all.yml"
type: "file"
template: |
---
# Global variables for all hosts
ntp_servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
common_packages:
- vim
- curl
- git
- htop
timezone: "UTC"
description: "Global variables file"
- path: ".gitignore"
type: "file"
template: |
# Ansible
*.retry
.vault_pass
ansible.log
# Python
__pycache__/
*.py[cod]
venv/
.venv/
# Testing
molecule/**/.molecule/
molecule/**/tests/__pycache__/
# IDE
.vscode/
.idea/
# Credentials
*.pem
*.key
credentials.yml
description: "Git ignore for Ansible projects"
- path: "README.md"
type: "file"
template: |
# Ansible Configuration Management
This project uses Ansible for server and application configuration management.
## Quick Start
### Prerequisites
- Ansible 2.9+ installed
- SSH access to target hosts
- Python 3.6+ on target hosts
### Installation
```bash
# Install Ansible
pip install ansible ansible-lint
# Install required collections
ansible-galaxy collection install -r requirements.yml
# Install required roles (if any)
ansible-galaxy role install -r requirements.yml
```
### Running Playbooks
```bash
# Check inventory
ansible-inventory -i inventory --list
# Test connectivity
ansible all -i inventory -m ping
# Run in check mode (dry run)
ansible-playbook -i inventory/production playbooks/site.yml --check --diff
# Execute playbook
ansible-playbook -i inventory/production playbooks/site.yml
# Limit to specific hosts
ansible-playbook -i inventory/production playbooks/site.yml --limit webservers
# Use tags
ansible-playbook -i inventory/production playbooks/site.yml --tags "configuration"
```
### Working with Secrets
```bash
# Create encrypted vault file
ansible-vault create group_vars/all/vault.yml
# Edit encrypted file
ansible-vault edit group_vars/all/vault.yml
# Run playbook with vault password
ansible-playbook playbooks/site.yml --ask-vault-pass
# OR
ansible-playbook playbooks/site.yml --vault-password-file ~/.vault_pass
```
### Testing
```bash
# Lint playbooks
ansible-lint playbooks/
# Test roles with Molecule
cd roles/myapp
molecule test
```
## Project Structure
```
.
├── ansible.cfg # Ansible configuration
├── inventory/ # Host inventories
│ ├── production # Production hosts
│ ├── staging # Staging hosts
│ └── aws_ec2.yml # Dynamic AWS inventory
├── group_vars/ # Group-level variables
│ └── all/
│ ├── vars.yml # Unencrypted variables
│ └── vault.yml # Encrypted secrets
├── host_vars/ # Host-specific variables
├── playbooks/ # Ansible playbooks
│ └── site.yml # Main site playbook
├── roles/ # Ansible roles
│ └── common/ # Common role
│ ├── tasks/
│ ├── handlers/
│ ├── templates/
│ ├── files/
│ ├── defaults/
│ └── meta/
└── requirements.yml # Galaxy dependencies
```
## Documentation
- [Ansible Documentation](https://docs.ansible.com/)
- [Best Practices](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html)
- [Galaxy Collections](https://galaxy.ansible.com/)
description: "Project documentation with usage examples"
- path: "requirements.yml"
type: "file"
template: |
---
collections:
- name: community.general
version: ">=3.0.0"
- name: ansible.posix
version: ">=1.0.0"
# roles:
# - name: geerlingguy.docker
# version: "4.2.0"
description: "Ansible Galaxy dependencies"
# Metadata
metadata:
primary_blueprints: ["infrastructure", "devops", "k8s"]
contributes_to:
- "Configuration management"
- "Infrastructure automation"
- "Server provisioning"
- "Application deployment"
- "GitOps workflows"
common_patterns:
- name: "Three-tier role structure"
description: "Roles organized by layer (common → middleware → application)"
files: ["roles/common/", "roles/nginx/", "roles/myapp/"]
- name: "Dynamic cloud inventory"
description: "Auto-discovering hosts from AWS/GCP/Azure"
files: ["inventory/aws_ec2.yml", "inventory/gcp_compute.yml"]
- name: "Ansible Vault secrets"
description: "Encrypted secrets with ansible-vault"
files: ["group_vars/all/vault.yml", ".vault_pass"]
- name: "Molecule testing"
description: "Testing roles with Molecule and Docker"
files: ["roles/*/molecule/", "requirements.yml"]
- name: "GitOps workflow"
description: "Infrastructure as code with version control"
files: [".github/workflows/ansible-ci.yml", "playbooks/"]
integration_points:
iac: "Terraform provisions infrastructure, Ansible configures"
kubernetes: "Ansible deploys K8s clusters, manages nodes"
ci_cd: "CI pipelines run ansible-lint and Molecule tests"
secrets: "ansible-vault or HashiCorp Vault integration"
monitoring: "Ansible configures monitoring agents and dashboards"
tools:
core:
- name: "Ansible"
use_when: "Configuring servers, deploying applications"
- name: "ansible-lint"
use_when: "Linting playbooks and roles"
- name: "Molecule"
use_when: "Testing roles before deployment"
inventory:
- name: "Static inventory (INI/YAML)"
use_when: "Stable environments with fixed hosts"
- name: "Dynamic inventory (aws_ec2, gcp_compute)"
use_when: "Cloud environments with auto-scaling"
secrets:
- name: "ansible-vault"
use_when: "Simple secret encryption"
- name: "HashiCorp Vault"
use_when: "Enterprise secrets management, dynamic credentials"
testing:
- name: "Molecule + Docker"
use_when: "Testing roles in isolation"
- name: "Check mode (--check --diff)"
use_when: "Dry-run before deployment"
validation_checks:
- "ansible-lint passes with no errors"
- "Playbooks are idempotent (run twice with no changes)"
- "Secrets encrypted with ansible-vault (no plaintext passwords)"
- "Roles tested with Molecule before production use"
- "Inventory structure matches environment (production/staging)"
- "Handlers properly trigger on configuration changes"
- "SSH connectivity verified (ansible all -m ping)"
- "Role dependencies documented in meta/main.yml"
anti_patterns:
- name: "Using shell/command for package installation"
avoid: "command: apt-get install nginx"
use: "apt: name=nginx state=present"
- name: "Hardcoded IPs in playbooks"
avoid: "Playbook contains 10.0.1.10"
use: "Variables and inventory host definitions"
- name: "No idempotency checks"
avoid: "Tasks always report 'changed'"
use: "State-based modules (present, started, latest)"
- name: "Single monolithic playbook"
avoid: "1000+ line site.yml with all logic"
use: "Roles for reusable components"
- name: "Secrets in plaintext"
avoid: "Passwords in group_vars/all.yml"
use: "ansible-vault encrypted files"
Chef and Puppet Migration Guide
Table of Contents
1. Chef to Ansible Migration 2. Puppet to Ansible Migration 3. Migration Strategy 4. Migration Tools 5. Common Pitfalls 6. Best Practices 7. Resources
---
Chef to Ansible Migration
Conceptual Mapping
| Chef Concept | Ansible Equivalent |
|---|---|
| Recipe | Task list in role |
| Cookbook | Role |
| Attribute | Variable |
| Resource | Module |
| Template | Jinja2 template |
| Data bag | Variable file (encrypted with vault) |
| Node | Managed host in inventory |
| Chef Server | Ansible control node |
| knife | ansible, ansible-playbook CLI |
Recipe to Playbook Translation
Chef Recipe (install_nginx.rb):
package 'nginx' do
action :install
end
template '/etc/nginx/nginx.conf' do
source 'nginx.conf.erb'
owner 'root'
group 'root'
mode '0644'
notifies :reload, 'service[nginx]'
end
service 'nginx' do
action [:enable, :start]
endAnsible Playbook:
---
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Configure nginx
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Reload nginx
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: yes
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloadedCommon Chef Patterns in Ansible
1. Package Installation:
Chef:
package ['nginx', 'postgresql', 'redis'] do
action :install
endAnsible:
- name: Install packages
ansible.builtin.apt:
name:
- nginx
- postgresql
- redis
state: present2. File Management:
Chef:
file '/etc/app/config.yml' do
content 'key: value'
owner 'app'
group 'app'
mode '0600'
endAnsible:
- name: Create config file
ansible.builtin.copy:
content: 'key: value'
dest: /etc/app/config.yml
owner: app
group: app
mode: '0600'3. Conditionals:
Chef:
package 'nginx' do
action :install
only_if { node['platform_family'] == 'debian' }
endAnsible:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
when: ansible_os_family == "Debian"---
Puppet to Ansible Migration
Conceptual Mapping
| Puppet Concept | Ansible Equivalent |
|---|---|
| Manifest | Playbook |
| Module | Role |
| Resource | Module |
| Class | Role |
| Template | Jinja2 template |
| Hiera | group_vars, host_vars |
| Facter | Ansible facts |
| Puppet Master | Ansible control node |
| Node | Managed host in inventory |
| puppet agent | ansible-pull (optional) |
Manifest to Playbook Translation
Puppet Manifest (nginx.pp):
package { 'nginx':
ensure => installed,
}
file { '/etc/nginx/nginx.conf':
ensure => file,
owner => 'root',
group => 'root',
mode => '0644',
source => 'puppet:///modules/nginx/nginx.conf',
notify => Service['nginx'],
}
service { 'nginx':
ensure => running,
enable => true,
}Ansible Playbook:
---
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Configure nginx
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Restart nginx
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: yes
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restartedCommon Puppet Patterns in Ansible
1. Resource Ensure:
Puppet:
package { 'nginx':
ensure => installed,
}
service { 'nginx':
ensure => running,
}Ansible:
- name: Ensure nginx installed
ansible.builtin.apt:
name: nginx
state: present
- name: Ensure nginx running
ansible.builtin.service:
name: nginx
state: started2. File Resources:
Puppet:
file { '/opt/app':
ensure => directory,
owner => 'app',
mode => '0755',
}Ansible:
- name: Ensure directory exists
ansible.builtin.file:
path: /opt/app
state: directory
owner: app
mode: '0755'3. User Management:
Puppet:
user { 'appuser':
ensure => present,
uid => 1001,
gid => 'appgroup',
home => '/home/appuser',
shell => '/bin/bash',
}Ansible:
- name: Create user
ansible.builtin.user:
name: appuser
uid: 1001
group: appgroup
home: /home/appuser
shell: /bin/bash
state: present---
Migration Strategy
Phase 1: Assessment (Week 1)
Tasks: 1. Inventory existing Chef/Puppet code 2. Identify critical cookbooks/modules 3. Document dependencies 4. Assess complexity 5. Choose pilot project
Deliverables:
- Migration scope document
- Priority list
- Resource requirements
Phase 2: Parallel Implementation (Weeks 2-4)
Tasks: 1. Install Ansible on control node 2. Convert pilot cookbook/module 3. Test in non-production 4. Run Chef/Puppet and Ansible in parallel 5. Validate equivalence
Approach:
- Start with simplest cookbook/module
- Convert 1-2 cookbooks/modules per week
- Maintain existing system during migration
Phase 3: Validation (Week 5)
Tasks: 1. Compare Chef/Puppet output vs Ansible output 2. Run compliance checks 3. Performance testing 4. Security audit 5. Documentation review
Phase 4: Cutover (Week 6)
Tasks: 1. Disable Chef/Puppet 2. Full Ansible deployment 3. Monitor for issues 4. Address any gaps 5. Document new workflows
Phase 5: Cleanup (Week 7)
Tasks: 1. Remove Chef/Puppet infrastructure 2. Archive old code 3. Train team on Ansible 4. Update runbooks 5. Celebrate!
---
Migration Tools
chefspec to Molecule
Convert Chef cookbook tests to Molecule tests.
puppet-lint to ansible-lint
Run ansible-lint for quality checks.
Custom Conversion Scripts
Simple script to help translate:
#!/usr/bin/env python3
"""Simple Chef recipe to Ansible playbook converter."""
import re
import sys
def convert_package(line):
match = re.search(r"package ['\"]([^'\"]+)['\"]", line)
if match:
return f"""
- name: Install {match.group(1)}
ansible.builtin.apt:
name: {match.group(1)}
state: present
"""
return None
def convert_service(line):
match = re.search(r"service ['\"]([^'\"]+)['\"]", line)
if match:
return f"""
- name: Start {match.group(1)}
ansible.builtin.service:
name: {match.group(1)}
state: started
enabled: yes
"""
return None
def main():
if len(sys.argv) < 2:
print("Usage: convert.py recipe.rb")
sys.exit(1)
print("---")
with open(sys.argv[1]) as f:
for line in f:
result = convert_package(line) or convert_service(line)
if result:
print(result)
if __name__ == '__main__':
main()---
Common Pitfalls
1. Agent Dependency
Chef/Puppet: Requires agent on managed nodes. Ansible: Agentless (SSH only).
Solution: Ensure SSH access configured before migration.
2. Pull vs Push Model
Chef/Puppet: Nodes pull configuration. Ansible: Control node pushes configuration.
Solution: Use ansible-pull if pull model required.
3. State Files
Chef/Puppet: Maintain state on nodes. Ansible: Stateless by default.
Solution: Use dynamic inventory for state tracking if needed.
4. Custom Resources
Chef/Puppet: Custom resources in Ruby/Puppet DSL. Ansible: Custom modules in Python.
Solution: Rewrite custom resources as Ansible modules or use command/shell temporarily.
---
Best Practices
1. Incremental Migration
Don't try to migrate everything at once. Start with simple, low-risk cookbooks/modules.
2. Maintain Parity
Run both systems in parallel during migration to ensure equivalence.
3. Test Thoroughly
Use Molecule to test converted roles before deploying.
4. Document Changes
Keep detailed notes on translation decisions for future reference.
5. Train Team
Ensure team is comfortable with Ansible before full cutover.
6. Leverage Ansible Galaxy
Check if equivalent roles already exist on Ansible Galaxy before rewriting.
---
Resources
Ansible Documentation:
- https://docs.ansible.com/ansible/latest/porting_guides/
Community Examples:
- https://github.com/geerlingguy/ansible-for-devops
- https://github.com/ansible/ansible-examples
Consulting:
- Red Hat Consulting Services
- Ansible Professional Services
Decision Framework
Table of Contents
1. When to Use Configuration Management 2. Tool Selection: Ansible vs Chef vs Puppet 3. Ansible vs Infrastructure-as-Code 4. Static vs Dynamic Inventory 5. Playbooks vs Roles 6. Secret Management Selection 7. Testing Strategy Selection 8. Environment Strategy 9. Push vs Pull Model 10. When to Consider Alternatives
---
When to Use Configuration Management
Use Configuration Management When:
✅ Managing server/VM configurations (OS settings, packages, users) ✅ Ensuring compliance (CIS benchmarks, PCI-DSS, SOC2) ✅ Deploying applications to servers/VMs ✅ Standardizing environments (dev, staging, production) ✅ Automating operational tasks (backups, updates, monitoring) ✅ Managing cloud resources post-provisioning
Don't Use Configuration Management For:
❌ Creating cloud infrastructure (use Terraform, CloudFormation) ❌ Container orchestration (use Kubernetes, Docker Swarm) ❌ CI/CD pipelines (use Jenkins, GitHub Actions, GitLab CI) ❌ Immutable infrastructure (use Packer for image baking)
---
Tool Selection: Ansible vs Chef vs Puppet
Decision Tree
Start
│
├─ New project / greenfield?
│ └─ YES → Ansible (easiest, modern, agentless)
│
├─ Existing Chef/Puppet?
│ ├─ Working well? → Keep it (don't migrate unnecessarily)
│ └─ Pain points? → Migrate to Ansible (assess effort)
│
├─ Windows-heavy environment?
│ ├─ Mostly Windows → Puppet (best Windows support) OR Ansible (WinRM)
│ └─ Mixed → Ansible (handles both)
│
├─ Compliance-critical enterprise?
│ ├─ Need compliance tools → Puppet (Remediate) OR Ansible (custom)
│ └─ Standard compliance → Ansible (sufficient)
│
├─ Large team, complex organization?
│ ├─ Need enterprise support → Red Hat Ansible Automation Platform
│ └─ Open source sufficient → Ansible (community)
│
└─ Cloud-native / containers?
└─ YES → Ansible (best cloud integration, agentless)Quick Comparison
| Factor | Ansible | Chef | Puppet |
|---|---|---|---|
| Learning Curve | Easy (YAML) | Steep (Ruby) | Moderate (DSL) |
| Architecture | Agentless (SSH) | Agent-based | Agent-based |
| Best For | Most use cases | Hybrid cloud | Windows, compliance |
| Setup Time | Minutes | Hours | Hours |
| Cloud Integration | Excellent | Good | Good |
| Community | Large, active | Moderate | Moderate |
| Enterprise Support | Red Hat | Chef Software | Puppet Inc. |
---
Ansible vs Infrastructure-as-Code
Use Ansible When:
- Configuring resources AFTER provisioning
- Deploying applications to existing infrastructure
- Managing OS-level settings (users, packages, services)
- Orchestrating multi-step workflows across hosts
- Operational tasks (backups, updates, maintenance)
Use Terraform/IaC When:
- Creating cloud infrastructure (VPCs, instances, databases)
- Managing resource lifecycle (create, update, destroy)
- Defining infrastructure state declaratively
- Multi-cloud resource orchestration
- Infrastructure dependencies and ordering
Best Practice: Use Both Together
Workflow: 1. Terraform creates AWS EC2 instances, security groups, load balancers 2. Terraform outputs instance IPs to Ansible inventory 3. Ansible configures OS, installs packages, deploys application 4. Ansible sets up monitoring, backups, cron jobs
Example Terraform output:
output "web_servers" {
value = {
for instance in aws_instance.web :
instance.tags.Name => instance.private_ip
}
}
resource "local_file" "ansible_inventory" {
content = templatefile("inventory.tpl", {
web_servers = aws_instance.web
})
filename = "../ansible/inventory/terraform.yml"
}---
Static vs Dynamic Inventory
Decision Matrix
| Factor | Static Inventory | Dynamic Inventory |
|---|---|---|
| Environment Size | < 50 hosts | > 50 hosts |
| Infrastructure Change Frequency | Rarely | Frequently (auto-scaling) |
| Deployment Type | On-premises | Cloud (AWS, Azure, GCP) |
| Host Discovery | Manual | Automatic |
| Use Cases | Small shops, stable infra | Cloud-native, enterprise |
Use Static Inventory When:
- Small environment (< 50 hosts)
- Stable infrastructure (rarely changes)
- On-premises servers with fixed IPs
- Simple host grouping requirements
- No cloud integration needed
Example:
[webservers]
web1.example.com
web2.example.com
[databases]
db1.example.comUse Dynamic Inventory When:
- Large environment (> 50 hosts)
- Cloud-based infrastructure (AWS, Azure, GCP)
- Frequent scaling (auto-scaling groups)
- Multi-cloud or hybrid environments
- Source of truth in external system (NetBox, CMDB)
Example:
# inventory/aws_ec2.yml
plugin: aws_ec2
regions:
- us-east-1
filters:
tag:Environment: production
instance-state-name: runningUse Hybrid Inventory When:
- Some static hosts (on-premises)
- Some dynamic hosts (cloud)
- Unified management across environments
inventory/
├── static/
│ └── onprem-hosts
└── aws_ec2.yml---
Playbooks vs Roles
Use Playbooks When:
- One-time deployment tasks
- Environment-specific orchestration
- Combining multiple roles
- Custom workflow for specific application
Example:
# deploy-production.yml
- hosts: webservers
roles:
- common
- nginx
- myappUse Roles When:
- Reusable configuration units
- Shareable across projects
- Testing in isolation
- Publishing to Ansible Galaxy
Example:
roles/nginx/
├── tasks/
├── handlers/
├── templates/
└── defaults/---
Secret Management Selection
Use ansible-vault When:
- Small teams (< 10 people)
- Simple secret management needs
- No compliance requirements
- Secrets change infrequently
- No need for audit trails
- Budget constraints
Pros:
- Built-in, no additional setup
- Simple to use
- Free
- Integrates seamlessly with Ansible
Cons:
- No dynamic secrets
- No audit logging
- Manual secret rotation
- Password-based access control
Use HashiCorp Vault When:
- Large teams or enterprises
- Dynamic secret generation needed (database credentials)
- Compliance requirements (SOC2, PCI-DSS, HIPAA)
- Frequent secret rotation required
- Detailed audit logging needed
- Integration with multiple tools (not just Ansible)
- Cloud-native architectures
Pros:
- Dynamic secrets (time-limited credentials)
- Comprehensive audit logging
- Fine-grained access control (policies, roles)
- Automated secret rotation
- Multi-tool integration
Cons:
- Additional infrastructure to manage
- More complex setup
- Requires operational expertise
- Cost (enterprise features)
Hybrid Approach
Use both for different security tiers:
# Low-sensitivity: ansible-vault
app_config_param: "{{ vault_app_config }}"
# High-sensitivity: HashiCorp Vault
db_password: "{{ lookup('hashi_vault', 'secret/data/myapp:password') }}"---
Testing Strategy Selection
Use ansible-lint When:
- Pre-commit checks
- CI/CD quality gates
- Catching common mistakes
- Enforcing best practices
Run: Before every commit
Use Check Mode When:
- Manual verification before deployment
- Dry-run testing in production
- Validating changes without applying
Run: Before production deployments
Use Molecule When:
- Developing reusable roles
- Testing against multiple OS distributions
- Validating idempotence
- Pre-production verification
- Publishing roles to Ansible Galaxy
Run: During role development
Use Testinfra When:
- Infrastructure state verification
- Compliance testing
- Post-deployment validation
- Integration testing
Run: After deployments
---
Environment Strategy
Single Environment Inventory
When:
- Small deployments
- Single environment (prod only)
- Simple infrastructure
inventory/
└── hostsMulti-Environment Inventory
When:
- Development, staging, production environments
- Different configurations per environment
- Separate teams/access controls
inventory/
├── production/
│ ├── hosts
│ └── group_vars/
├── staging/
│ ├── hosts
│ └── group_vars/
└── development/
├── hosts
└── group_vars/---
Push vs Pull Model
Push (Default Ansible)
When:
- Small-to-medium environments
- Centralized control preferred
- On-demand deployments
- Cloud-based (SSH always available)
Command:
ansible-playbook site.ymlPull (ansible-pull)
When:
- Large-scale deployments (1000+ nodes)
- Autonomous node configuration
- Periodic updates (cron-based)
- Nodes behind firewalls
Setup:
# On managed node
ansible-pull -U https://github.com/org/ansible-repo.git site.yml---
When to Consider Alternatives
Switch to Kubernetes when:
- Running containerized applications
- Need container orchestration
- Require service mesh features
- Horizontal scaling requirements
Use Packer instead when:
- Building immutable images
- Image-based deployments
- Golden image creation
- No post-boot configuration needed
Consider SaltStack when:
- Real-time remote execution needed
- Event-driven automation
- Already using Salt infrastructure
Stick with manual when:
- One-time setup (< 5 servers)
- Exploratory/learning phase
- No repeatability requirements
- Cost of automation > cost of manual work
Idempotency Guide
Table of Contents
1. What is Idempotency? 2. Patterns for Idempotency 3. Common Idempotency Issues 4. Testing Idempotency 5. Module Reference 6. Best Practices 7. Debugging Idempotency Issues
---
What is Idempotency?
Idempotency means running a playbook multiple times produces the same result as running it once. Tasks should converge to a desired state without side effects on repeated execution.
Benefits:
- Safe to re-run - No unintended changes
- Predictable - Same input = same output
- Efficient - Only applies necessary changes
- Production-ready - Can safely run on live systems
---
Patterns for Idempotency
1. Use State-Based Modules
Good (idempotent):
- name: Ensure nginx is installed
ansible.builtin.apt:
name: nginx
state: presentBad (not idempotent):
- name: Install nginx
ansible.builtin.command: apt-get install -y nginx2. Check Before Action
- name: Check if config exists
ansible.builtin.stat:
path: /etc/app/app.conf
register: config_file
- name: Create config only if missing
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
when: not config_file.stat.exists3. Use Handlers for Side Effects
tasks:
- name: Update nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded4. Set changed_when Appropriately
# Command that doesn't change system
- name: Check nginx config syntax
ansible.builtin.command: nginx -t
changed_when: false
# Command with conditional change detection
- name: Update cache
ansible.builtin.command: apt-get update
register: apt_update
changed_when: "'Reading package lists' in apt_update.stdout"5. Use Desired State Parameters
# Service management
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: started # started, stopped, restarted, reloaded
enabled: yes # Boot persistence
# Package management
- name: Ensure package version
ansible.builtin.apt:
name: nginx=1.24.0-1
state: present # present, absent, latest
# File management
- name: Ensure file exists
ansible.builtin.copy:
src: file.txt
dest: /tmp/file.txt
force: no # Don't overwrite if exists---
Common Idempotency Issues
Issue 1: Command/Shell Modules
Problem:
# Always reports "changed"
- name: Create directory
ansible.builtin.command: mkdir /opt/appSolution 1: Use appropriate module
- name: Ensure directory exists
ansible.builtin.file:
path: /opt/app
state: directorySolution 2: Add changed_when
- name: Create directory
ansible.builtin.command: mkdir /opt/app
args:
creates: /opt/app # Only run if /opt/app doesn't existIssue 2: Database Operations
Problem:
# Fails on second run
- name: Create database
ansible.builtin.command: createdb myappSolution:
- name: Ensure database exists
community.postgresql.postgresql_db:
name: myapp
state: presentIssue 3: User/Group Creation
Problem:
# Not idempotent
- name: Add user
ansible.builtin.command: useradd appuserSolution:
- name: Ensure user exists
ansible.builtin.user:
name: appuser
system: yes
state: presentIssue 4: Package Downloads
Problem:
# Downloads every time
- name: Download application
ansible.builtin.get_url:
url: https://example.com/app.tar.gz
dest: /tmp/app.tar.gzSolution:
- name: Download application (cached)
ansible.builtin.get_url:
url: https://example.com/app.tar.gz
dest: /tmp/app.tar.gz
checksum: sha256:abc123... # Only downloads if checksum differsIssue 5: Configuration Appends
Problem:
# Appends every run
- name: Add config line
ansible.builtin.lineinfile:
path: /etc/hosts
line: "10.0.0.1 server1"
state: present
# Missing unique identifierSolution:
- name: Ensure host entry exists
ansible.builtin.lineinfile:
path: /etc/hosts
regexp: '^.* server1$' # Unique match pattern
line: "10.0.0.1 server1"
state: present---
Testing Idempotency
Manual Test
# Run playbook twice
ansible-playbook site.yml
ansible-playbook site.yml
# Second run should report:
# ok=N changed=0 unreachable=0 failed=0Molecule Idempotence Test
# Automatic idempotence check
molecule idempotencePasses if: Second run has zero changes. Fails if: Second run reports any changed tasks.
Idempotence Verification Task
- name: Verify idempotence
block:
- name: Run role first time
ansible.builtin.include_role:
name: myapp
- name: Run role second time
ansible.builtin.include_role:
name: myapp
register: second_run
- name: Assert no changes on second run
ansible.builtin.assert:
that:
- not second_run.changed
fail_msg: "Role is not idempotent"---
Module Reference
Idempotent Modules (Safe to Use)
| Module | Purpose | Idempotent |
|---|---|---|
apt, yum, dnf | Package management | ✅ Yes |
service, systemd | Service management | ✅ Yes |
user, group | User management | ✅ Yes |
file | File/directory operations | ✅ Yes |
copy, template | File deployment | ✅ Yes |
lineinfile, blockinfile | Config file editing | ✅ Yes (with regexp) |
git | Git operations | ✅ Yes |
postgresql_db, mysql_db | Database management | ✅ Yes |
Non-Idempotent Modules (Use Carefully)
| Module | Purpose | Requires |
|---|---|---|
command | Run commands | creates, removes, or changed_when |
shell | Run shell commands | creates, removes, or changed_when |
raw | Run raw commands | changed_when |
script | Run local scripts | creates, removes, or changed_when |
---
Best Practices
1. Always Use State Parameters
# Good
- name: Ensure service is running
ansible.builtin.service:
name: nginx
state: started
enabled: yes
# Bad
- name: Start service
ansible.builtin.command: systemctl start nginx2. Add changed_when to Commands
# Read-only commands
- name: Check version
ansible.builtin.command: app --version
changed_when: false
register: version
# Conditional changes
- name: Run migration
ansible.builtin.command: /opt/app/migrate.sh
register: migration
changed_when: "'Applied' in migration.stdout"3. Use creates/removes Arguments
# Only run if file doesn't exist
- name: Extract archive
ansible.builtin.command: tar -xzf /tmp/app.tar.gz
args:
chdir: /opt/app
creates: /opt/app/bin/app
# Only run if file exists
- name: Clean up
ansible.builtin.command: rm -rf /tmp/build
args:
removes: /tmp/build4. Check State Before Changing
- name: Check if service exists
ansible.builtin.stat:
path: /etc/systemd/system/myapp.service
register: service_file
- name: Install service
ansible.builtin.template:
src: myapp.service.j2
dest: /etc/systemd/system/myapp.service
when: not service_file.stat.exists or force_reinstall | default(false)
notify: Reload systemd5. Use Handlers for Restarts
# Don't restart directly
tasks:
- name: Update config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
notify: Restart app
# Use handler (only restarts if config changed)
handlers:
- name: Restart app
ansible.builtin.service:
name: myapp
state: restarted6. Avoid Destructive Operations
# Bad: Destroys data every run
- name: Reset database
ansible.builtin.command: dropdb myapp && createdb myapp
# Good: Only create if missing
- name: Ensure database exists
community.postgresql.postgresql_db:
name: myapp
state: present7. Use force Parameter Carefully
# Bad: Always overwrites
- name: Deploy config
ansible.builtin.copy:
src: app.conf
dest: /etc/app/app.conf
force: yes # Always copies
# Good: Only copies if different
- name: Deploy config
ansible.builtin.copy:
src: app.conf
dest: /etc/app/app.conf
force: no # Preserves existing if present
# Or omit force (default behavior checks content)8. Document Non-Idempotent Tasks
- name: Run database migration (NOT IDEMPOTENT)
ansible.builtin.command: /opt/app/migrate.sh
# NOTE: This will apply all pending migrations each run
# Future enhancement: track applied migrations9. Test Repeatedly
# Run playbook 3 times
for i in 1 2 3; do
echo "Run $i"
ansible-playbook site.yml
done
# All runs after first should show: changed=010. Use Molecule for Automated Testing
# molecule.yml includes idempotence test
scenario:
test_sequence:
- converge # First run
- idempotence # Second run (verifies no changes)
- verify # Verification---
Debugging Idempotency Issues
Find Non-Idempotent Tasks
# Run twice, compare output
ansible-playbook site.yml -v > run1.log
ansible-playbook site.yml -v > run2.log
diff run1.log run2.logCheck Task Results
- name: Run task
ansible.builtin.command: some-command
register: result
- name: Show result
ansible.builtin.debug:
var: result
# Check: result.changed, result.rc, result.stdoutUse Check Mode
# Dry run shows what would change
ansible-playbook site.yml --check --diffEnable Verbose Output
# Show detailed task execution
ansible-playbook site.yml -vvvInventory Management
Table of Contents
1. Static Inventory 2. Dynamic Inventory 3. Hybrid Inventory 4. Inventory Variables 5. Best Practices
---
Static Inventory
INI Format
Basic inventory:
# inventory/production
[webservers]
web1.example.com
web2.example.com
web3.example.com
[databases]
db1.example.com
db2.example.com
[loadbalancers]
lb1.example.comWith host variables:
[webservers]
web1.example.com ansible_host=10.0.1.10 nginx_worker_processes=4
web2.example.com ansible_host=10.0.1.11 nginx_worker_processes=8
web3.example.com ansible_host=10.0.1.12 nginx_worker_processes=4With group variables:
[webservers]
web1.example.com
web2.example.com
web3.example.com
[webservers:vars]
nginx_worker_processes=4
app_env=production
ansible_user=deployParent groups:
[webservers]
web1.example.com
web2.example.com
[databases]
db1.example.com
db2.example.com
[production:children]
webservers
databases
[production:vars]
ansible_user=deploy
ansible_become=yesYAML Format
# inventory/production.yml
all:
children:
production:
children:
webservers:
hosts:
web1.example.com:
ansible_host: 10.0.1.10
nginx_worker_processes: 4
web2.example.com:
ansible_host: 10.0.1.11
nginx_worker_processes: 8
vars:
app_env: production
databases:
hosts:
db1.example.com:
ansible_host: 10.0.2.10
db2.example.com:
ansible_host: 10.0.2.11
vars:
postgres_version: 15
vars:
ansible_user: deploy
ansible_become: yesMultiple Inventory Files
inventory/
├── production/
│ ├── hosts # Main inventory
│ ├── webservers # Web tier hosts
│ └── databases # Database tier hosts
└── staging/
├── hosts
├── webservers
└── databasesRun with directory:
ansible-playbook -i inventory/production site.yml---
Dynamic Inventory
AWS EC2 Plugin
Installation:
ansible-galaxy collection install amazon.awsinventory/aws_ec2.yml:
plugin: aws_ec2
regions:
- us-east-1
- us-west-2
filters:
tag:Environment: production
instance-state-name: running
keyed_groups:
# Group by tag
- key: tags.Role
prefix: role
separator: "_"
# Group by instance type
- key: instance_type
prefix: type
# Group by availability zone
- key: placement.availability_zone
prefix: az
hostnames:
- tag:Name
- private-ip-address
compose:
ansible_host: private_ip_address
ansible_user: "'ubuntu'"Test inventory:
# List all hosts
ansible-inventory -i inventory/aws_ec2.yml --list
# Graph view
ansible-inventory -i inventory/aws_ec2.yml --graph
# Run playbook
ansible-playbook -i inventory/aws_ec2.yml site.ymlAzure Resource Manager Plugin
Installation:
ansible-galaxy collection install azure.azcollectioninventory/azure_rm.yml:
plugin: azure_rm
auth_source: auto
include_vm_resource_groups:
- production-rg
keyed_groups:
- prefix: tag
key: tags
- prefix: location
key: location
hostnames:
- name
- default
compose:
ansible_host: public_ipv4_addresses[0]
ansible_user: "'azureuser'"GCP Compute Plugin
Installation:
ansible-galaxy collection install google.cloudinventory/gcp_compute.yml:
plugin: gcp_compute
projects:
- my-project-id
zones:
- us-central1-a
- us-central1-b
filters:
- labels.environment = production
- status = RUNNING
keyed_groups:
- key: labels.role
prefix: role
- key: zone
prefix: zone
hostnames:
- name
compose:
ansible_host: networkInterfaces[0].accessConfigs[0].natIP
ansible_user: "'ubuntu'"Custom Dynamic Inventory Script
inventory/custom.py:
#!/usr/bin/env python3
import json
import sys
def get_inventory():
"""Return inventory data structure."""
inventory = {
"webservers": {
"hosts": ["web1.example.com", "web2.example.com"],
"vars": {
"nginx_worker_processes": 4,
"app_env": "production"
}
},
"databases": {
"hosts": ["db1.example.com", "db2.example.com"],
"vars": {
"postgres_version": 15
}
},
"_meta": {
"hostvars": {
"web1.example.com": {
"ansible_host": "10.0.1.10"
},
"web2.example.com": {
"ansible_host": "10.0.1.11"
}
}
}
}
return inventory
def main():
if len(sys.argv) == 2 and sys.argv[1] == '--list':
print(json.dumps(get_inventory(), indent=2))
elif len(sys.argv) == 3 and sys.argv[1] == '--host':
# Return empty dict for host-specific vars (use _meta above)
print(json.dumps({}))
else:
print("Usage: {} --list | --host <hostname>".format(sys.argv[0]))
sys.exit(1)
if __name__ == '__main__':
main()Make executable:
chmod +x inventory/custom.pyUse script:
ansible-playbook -i inventory/custom.py site.yml---
Hybrid Inventory
Combine static and dynamic inventories.
inventory/
├── static/
│ └── hosts # Static on-prem servers
├── aws_ec2.yml # Dynamic AWS instances
└── azure_rm.yml # Dynamic Azure instancesRun with multiple inventories:
ansible-playbook -i inventory/ site.ymlAll inventory sources merged automatically.
---
Inventory Variables
Group Variables
group_vars/
├── all/
│ ├── common.yml # Variables for ALL hosts
│ └── vault.yml # Encrypted secrets for ALL
├── webservers/
│ ├── nginx.yml # Webserver-specific vars
│ └── vault.yml # Webserver secrets
└── databases/
├── postgres.yml # Database-specific vars
└── vault.yml # Database secretsgroup_vars/all/common.yml:
---
# Common variables for all hosts
ntp_servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
dns_servers:
- 8.8.8.8
- 8.8.4.4
ansible_user: deploy
ansible_become: yesgroup_vars/webservers/nginx.yml:
---
# Webserver-specific variables
nginx_worker_processes: "{{ ansible_processor_vcpus }}"
nginx_worker_connections: 1024
nginx_client_max_body_size: "10m"Host Variables
host_vars/
├── web1.example.com/
│ └── custom.yml # Host-specific overrides
└── db1.example.com/
└── custom.ymlhost_vars/web1.example.com/custom.yml:
---
# Host-specific overrides
nginx_worker_processes: 8
custom_config_option: "special_value"Variable Precedence
From lowest to highest: 1. Role defaults 2. Inventory file vars 3. Inventory group_vars/all 4. Inventory group_vars/* 5. Inventory host_vars/* 6. Playbook vars 7. Command-line extra vars (-e)
---
Best Practices
1. Organize by Environment
inventory/
├── production/
│ ├── hosts
│ ├── group_vars/
│ └── host_vars/
└── staging/
├── hosts
├── group_vars/
└── host_vars/2. Use Consistent Naming
# Good: Clear patterns
[webservers]
web-prod-01.example.com
web-prod-02.example.com
[databases]
db-prod-01.example.com
db-prod-02.example.com
# Bad: Inconsistent
[webservers]
server1
production-web-23. Group by Function
[webservers]
web1.example.com
web2.example.com
[appservers]
app1.example.com
app2.example.com
[databases]
db1.example.com
db2.example.com
# Parent groups
[frontend:children]
webservers
appservers
[backend:children]
databases4. Version Control Inventory
.gitignore:
*vault.yml # Encrypted secrets
*.pem # SSH keys
inventory/production # Production inventory (separate repo)5. Document Inventory Structure
# Inventory Structure
## Environments
- production/ - Production servers
- staging/ - Staging servers
- development/ - Development servers
## Groups
- webservers - NGINX web tier
- appservers - Application tier
- databases - PostgreSQL databases
- loadbalancers - HAProxy load balancers
## Variables
- group_vars/all/ - Global variables
- group_vars/{group}/ - Group-specific variables
- host_vars/{host}/ - Host-specific overrides6. Limit Playbook Scope
# Run on specific group
ansible-playbook -i inventory site.yml --limit webservers
# Run on specific host
ansible-playbook -i inventory site.yml --limit web1.example.com
# Run on multiple hosts
ansible-playbook -i inventory site.yml --limit "web1,web2,db1"
# Exclude hosts
ansible-playbook -i inventory site.yml --limit "all:!databases"7. Test Inventory
# List all hosts
ansible-inventory -i inventory --list
# Show graph
ansible-inventory -i inventory --graph
# Show host variables
ansible-inventory -i inventory --host web1.example.com
# Ping all hosts
ansible all -i inventory -m ping8. Use Dynamic Inventory for Cloud
Static inventory becomes stale quickly in cloud environments. Use dynamic inventory plugins.
Bad (manual updates):
[webservers]
web1.example.com ansible_host=10.0.1.10 # IP changes frequently
web2.example.com ansible_host=10.0.1.11Good (auto-discovery):
# inventory/aws_ec2.yml
plugin: aws_ec2
filters:
tag:Role: webserver
instance-state-name: running9. Separate Secrets
Don't put secrets in inventory files.
Bad:
[databases:vars]
db_password=SuperSecret123Good:
# group_vars/databases/vault.yml (encrypted)
vault_db_password: SuperSecret123
# group_vars/databases/vars.yml (unencrypted)
db_password: "{{ vault_db_password }}"10. Use Inventory Plugins
Prefer inventory plugins over custom scripts.
Supported plugins:
aws_ec2- AWS EC2azure_rm- Azure Resource Managergcp_compute- Google Cloud Platformopenstack- OpenStackvmware_vm_inventory- VMwaredocker_containers- Dockerkubernetes- Kubernetes pods
List available plugins:
ansible-doc -t inventory -lAnsible Playbook Patterns
Table of Contents
1. Playbook Structure 2. Task Organization 3. Handlers 4. Variables 5. Templates 6. Tags 7. Conditional Execution 8. Loops 9. Error Handling 10. Best Practices
---
Playbook Structure
Complete Playbook Anatomy
---
# site.yml - Main playbook
- name: Configure web servers
hosts: webservers
become: yes
gather_facts: yes
vars:
nginx_version: "1.24"
app_port: 8080
vars_files:
- vars/common.yml
- vars/webservers.yml
pre_tasks:
- name: Update package cache
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
roles:
- common
- nginx
- application
tasks:
- name: Ensure application is running
service:
name: myapp
state: started
enabled: yes
tags: [service, application]
handlers:
- name: Restart nginx
service:
name: nginx
state: restarted
post_tasks:
- name: Verify application responds
uri:
url: "http://localhost:{{ app_port }}/health"
status_code: 200
retries: 3
delay: 5Execution Order
1. gather_facts - Collect system information 2. pre_tasks - Preparation tasks (package cache updates, prerequisite checks) 3. roles - Execute role tasks in order 4. tasks - Playbook-specific tasks 5. post_tasks - Verification and cleanup 6. handlers - Triggered handlers (if notified)
---
Task Organization
Simple Task
- name: Install nginx
apt:
name: nginx
state: presentTask with Multiple Parameters
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
notify: Reload nginxInclude External Task Files
# tasks/main.yml
- name: Include installation tasks
include_tasks: install.yml
- name: Include configuration tasks
include_tasks: configure.yml
when: configure_app | default(true)Import Task Files (Static)
# Evaluated at playbook parse time
- name: Import common tasks
import_tasks: common.ymlBlock Tasks for Grouping
- name: Configure application
block:
- name: Install dependencies
apt:
name: "{{ item }}"
state: present
loop: "{{ app_dependencies }}"
- name: Deploy application
copy:
src: app.tar.gz
dest: /opt/app/
rescue:
- name: Rollback deployment
file:
path: /opt/app
state: absent
always:
- name: Log deployment attempt
lineinfile:
path: /var/log/deployments.log
line: "Deployment attempted at {{ ansible_date_time.iso8601 }}"---
Handlers
Basic Handler
# tasks
- name: Update nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
# handlers
handlers:
- name: Restart nginx
service:
name: nginx
state: restartedMultiple Handlers
- name: Update application config
template:
src: app.conf.j2
dest: /etc/app/app.conf
notify:
- Validate config
- Restart application
- Clear cache
handlers:
- name: Validate config
command: app validate-config /etc/app/app.conf
changed_when: false
- name: Restart application
service:
name: myapp
state: restarted
- name: Clear cache
command: app clear-cacheListen Handlers (Multiple Triggers)
# tasks
- name: Update web config
template:
src: web.conf.j2
dest: /etc/web/web.conf
notify: Reload web services
- name: Update API config
template:
src: api.conf.j2
dest: /etc/api/api.conf
notify: Reload web services
# handlers
handlers:
- name: Reload nginx
service:
name: nginx
state: reloaded
listen: Reload web services
- name: Reload haproxy
service:
name: haproxy
state: reloaded
listen: Reload web servicesForce Handler Execution
- name: Update critical config
template:
src: critical.conf.j2
dest: /etc/app/critical.conf
notify: Restart app
- name: Flush handlers now
meta: flush_handlers
- name: Verify app started
uri:
url: http://localhost:8080/health
status_code: 200---
Variables
Variable Definition Locations
Playbook vars:
- name: Configure app
hosts: all
vars:
app_version: "2.1.0"
app_port: 8080Vars files:
- name: Configure app
hosts: all
vars_files:
- vars/common.yml
- vars/{{ env }}.ymlInventory variables:
# inventory/production
[webservers]
web1.example.com app_port=8080
web2.example.com app_port=8081
[webservers:vars]
app_version=2.1.0Group/Host vars:
group_vars/
├── all/
│ └── common.yml
├── webservers/
│ └── nginx.yml
host_vars/
└── web1.example.com/
└── custom.ymlVariable Precedence (Lowest to Highest)
1. Role defaults 2. Inventory file vars 3. Inventory group_vars/all 4. Inventory group_vars/ 5. Inventory host_vars/ 6. Playbook vars 7. Playbook vars_files 8. Role vars 9. Block vars 10. Task vars 11. Command-line extra vars (-e)
Register Variables
- name: Check if config exists
stat:
path: /etc/app/app.conf
register: config_file
- name: Create config if missing
template:
src: app.conf.j2
dest: /etc/app/app.conf
when: not config_file.stat.existsSet Facts
- name: Set deployment timestamp
set_fact:
deployment_time: "{{ ansible_date_time.iso8601 }}"
- name: Use fact in later task
lineinfile:
path: /etc/app/version.txt
line: "Deployed: {{ deployment_time }}"---
Templates
Basic Jinja2 Template
{# templates/app.conf.j2 #}
# Application Configuration
app_version: {{ app_version }}
app_port: {{ app_port }}
log_level: {{ log_level | default('info') }}
{% if enable_debug %}
debug: true
{% endif %}Using Template
- name: Deploy application config
template:
src: app.conf.j2
dest: /etc/app/app.conf
owner: app
group: app
mode: '0640'
notify: Restart appAdvanced Template (Loops)
{# templates/nginx.conf.j2 #}
http {
{% for site in nginx_sites %}
server {
listen {{ site.port | default(80) }};
server_name {{ site.server_name }};
root {{ site.root }};
{% if site.ssl | default(false) %}
ssl_certificate {{ site.ssl_cert }};
ssl_certificate_key {{ site.ssl_key }};
{% endif %}
location / {
try_files $uri $uri/ =404;
}
}
{% endfor %}
}Template with Filters
{# Common Jinja2 filters #}
{{ app_name | upper }}
{{ app_version | lower }}
{{ app_port | int }}
{{ config_value | default('fallback') }}
{{ sensitive_data | b64encode }}
{{ json_data | from_json }}
{{ list_data | join(',') }}---
Tags
Define Tags
- name: Install packages
apt:
name: nginx
state: present
tags: [install, packages]
- name: Configure services
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags: [config, nginx]
- name: Start services
service:
name: nginx
state: started
tags: [service, nginx]Run Specific Tags
# Run only install tasks
ansible-playbook site.yml --tags install
# Run multiple tags
ansible-playbook site.yml --tags "install,config"
# Skip tags
ansible-playbook site.yml --skip-tags serviceSpecial Tags
# Always runs (unless explicitly skipped)
- name: Log execution
debug:
msg: "Playbook started"
tags: [always]
# Never runs (unless explicitly included)
- name: Debug task
debug:
var: ansible_facts
tags: [never, debug]---
Conditional Execution
When Condition
# OS family check
- name: Install package on Debian
apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
# Multiple conditions (AND)
- name: Install on Ubuntu 22.04
apt:
name: nginx
state: present
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version == "22.04"
# OR conditions
- name: Install on RedHat or CentOS
yum:
name: nginx
state: present
when: ansible_distribution in ["RedHat", "CentOS"]
# Variable defined check
- name: Configure SSL
template:
src: ssl.conf.j2
dest: /etc/nginx/ssl.conf
when: ssl_enabled is defined and ssl_enabledRegister and Condition
- name: Check service status
command: systemctl is-active nginx
register: nginx_status
failed_when: false
changed_when: false
- name: Start nginx if not running
service:
name: nginx
state: started
when: nginx_status.rc != 0Failed When
- name: Run application tests
command: npm test
register: test_result
failed_when:
- test_result.rc != 0
- "'error' in test_result.stderr"Changed When
- name: Check config syntax
command: nginx -t
register: config_check
changed_when: false # Never report as changed
- name: Deploy script
copy:
src: deploy.sh
dest: /usr/local/bin/deploy.sh
register: script_deployed
changed_when: script_deployed.checksum != previous_checksum---
Loops
Simple Loop
- name: Install multiple packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redisLoop with Dictionary
- name: Create users
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups }}"
loop:
- { name: 'alice', uid: 1001, groups: 'admin,developers' }
- { name: 'bob', uid: 1002, groups: 'developers' }
- { name: 'charlie', uid: 1003, groups: 'operators' }Loop from Variable
vars:
packages:
- name: nginx
state: present
- name: postgresql
state: present
tasks:
- name: Manage packages
apt:
name: "{{ item.name }}"
state: "{{ item.state }}"
loop: "{{ packages }}"Loop with Index
- name: Create numbered directories
file:
path: "/data/shard{{ item.0 }}"
state: directory
loop: "{{ range(1, 6) | list }}"Nested Loops
- name: Configure firewall rules
ufw:
rule: allow
port: "{{ item.1 }}"
from_ip: "{{ item.0 }}"
loop: "{{ trusted_ips | product(allowed_ports) | list }}"
vars:
trusted_ips: ['10.0.1.0/24', '10.0.2.0/24']
allowed_ports: [80, 443]Loop Until
- name: Wait for service to be ready
uri:
url: http://localhost:8080/health
status_code: 200
register: health_check
until: health_check.status == 200
retries: 10
delay: 5---
Error Handling
Ignore Errors
- name: Attempt to stop service
service:
name: oldapp
state: stopped
ignore_errors: yesBlock Rescue Always
- name: Deploy application
block:
- name: Stop application
service:
name: myapp
state: stopped
- name: Deploy new version
copy:
src: app-v2.0.tar.gz
dest: /opt/app/
- name: Start application
service:
name: myapp
state: started
rescue:
- name: Rollback to previous version
copy:
src: app-v1.9-backup.tar.gz
dest: /opt/app/
- name: Start previous version
service:
name: myapp
state: started
- name: Send alert
mail:
to: ops@example.com
subject: "Deployment failed"
always:
- name: Clear deployment lock
file:
path: /var/lock/deployment.lock
state: absentAny Errors Fatal
- name: Critical deployment
hosts: all
any_errors_fatal: yes
tasks:
- name: Update database schema
command: /opt/app/migrate.sh---
Best Practices
1. Use Pre-Tasks for Preparation
pre_tasks:
- name: Update package cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Ensure prerequisites installed
apt:
name:
- python3
- python3-pip
state: present2. Use Post-Tasks for Verification
post_tasks:
- name: Verify web service responds
uri:
url: http://localhost:80
status_code: 200
retries: 5
delay: 3
- name: Check log for errors
command: grep -i error /var/log/app/app.log
register: log_check
failed_when:
- log_check.rc == 0
- "'CRITICAL' in log_check.stdout"3. Separate Concerns with Roles
# Don't put everything in one playbook
# Instead, organize by responsibility:
roles:
- common # OS configuration
- security # Firewall, users
- monitoring # Metrics, logging
- application # App-specific config4. Use Check Mode for Safety
# Always test with --check first
ansible-playbook site.yml --check --diff
# Some tasks need check mode disabled
- name: Validate config
command: nginx -t
check_mode: no5. Document with Clear Names
# Good: Clear, descriptive names
- name: Install nginx web server
- name: Configure nginx virtual hosts
- name: Ensure nginx service is running
# Bad: Vague names
- name: Install package
- name: Configure stuff
- name: Run command6. Keep Playbooks Focused
# Good: Single responsibility
# webservers.yml - Configure web tier
# databases.yml - Configure database tier
# monitoring.yml - Configure monitoring
# Bad: One massive playbook
# everything.yml - Does everything7. Use Variable Validation
- name: Validate required variables
assert:
that:
- app_version is defined
- app_port is defined
- app_port | int > 1024
fail_msg: "Required variables not properly defined"8. Leverage Ansible Facts
- name: Configure based on system facts
template:
src: app.conf.j2
dest: /etc/app/app.conf
vars:
memory_limit: "{{ (ansible_memtotal_mb * 0.8) | int }}"
cpu_workers: "{{ ansible_processor_vcpus }}"9. Use Handlers for Restarts
# Don't restart services directly in tasks
# Use handlers triggered by changes
tasks:
- name: Update config
template:
src: app.conf.j2
dest: /etc/app/app.conf
notify: Restart app
handlers:
- name: Restart app
service:
name: myapp
state: restarted10. Test Idempotency
# Run playbook twice
ansible-playbook site.yml
ansible-playbook site.yml
# Second run should have zero changes
# If tasks show "changed" on second run, fix idempotencyAnsible Role Structure
Table of Contents
1. Role Directory Layout 2. Creating Roles 3. Role Components 4. Best Practices 5. Role Dependencies 6. Collections 7. Testing Roles
---
Role Directory Layout
Standard Role Structure
roles/myapp/
├── defaults/
│ └── main.yml # Default variables (lowest precedence)
├── vars/
│ └── main.yml # Override variables (higher precedence)
├── tasks/
│ ├── main.yml # Main task entry point
│ ├── install.yml # Installation tasks
│ ├── configure.yml # Configuration tasks
│ └── security.yml # Security hardening
├── handlers/
│ └── main.yml # Change handlers
├── templates/
│ ├── app.conf.j2 # Jinja2 templates
│ └── systemd.service.j2
├── files/
│ ├── app.tar.gz # Static files
│ └── ssl/
│ └── ca.crt
├── meta/
│ └── main.yml # Role metadata and dependencies
├── library/
│ └── custom_module.py # Custom modules (optional)
├── module_utils/
│ └── helpers.py # Shared module utilities (optional)
├── tests/
│ ├── test.yml # Test playbook
│ └── inventory # Test inventory
└── README.md # Role documentationFile Loading Order
1. meta/main.yml - Dependencies loaded first 2. defaults/main.yml - Default variables 3. vars/main.yml - Role variables 4. tasks/main.yml - Task execution begins 5. handlers/main.yml - Handlers registered 6. Templates and files loaded on demand
---
Creating Roles
Initialize Role with ansible-galaxy
# Create new role
ansible-galaxy init roles/myapp
# Create role with specific author
ansible-galaxy init roles/myapp --init-path roles/
# View role structure
tree roles/myappMinimal Role Example
roles/nginx/
├── defaults/
│ └── main.yml
├── tasks/
│ └── main.yml
├── handlers/
│ └── main.yml
└── templates/
└── nginx.conf.j2defaults/main.yml:
---
# Default variables
nginx_version: "1.24"
nginx_user: www-data
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_enable_ssl: falsetasks/main.yml:
---
- name: Install nginx
apt:
name: nginx
state: present
notify: Restart nginx
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx
- name: Ensure nginx is running
service:
name: nginx
state: started
enabled: yeshandlers/main.yml:
---
- name: Restart nginx
service:
name: nginx
state: restarted
- name: Reload nginx
service:
name: nginx
state: reloadedUsing Roles in Playbooks
Method 1: Roles section (recommended)
---
- name: Configure web servers
hosts: webservers
become: yes
roles:
- common
- nginx
- applicationMethod 2: Include role task
---
- name: Configure web servers
hosts: webservers
become: yes
tasks:
- name: Apply nginx role
include_role:
name: nginxMethod 3: Import role (static)
---
- name: Configure web servers
hosts: webservers
become: yes
tasks:
- name: Import nginx role
import_role:
name: nginxWith variables:
---
- name: Configure web servers
hosts: webservers
become: yes
roles:
- role: nginx
vars:
nginx_worker_processes: 4
nginx_enable_ssl: true---
Role Components
defaults/main.yml
Define default variable values (can be overridden).
---
# Application settings
app_name: myapp
app_version: "1.0.0"
app_port: 8080
app_user: appuser
app_group: appuser
# Installation settings
app_install_dir: /opt/{{ app_name }}
app_config_dir: /etc/{{ app_name }}
app_log_dir: /var/log/{{ app_name }}
# Feature flags
enable_monitoring: true
enable_backup: false
# Dependencies
app_dependencies:
- python3
- python3-pip
- libpq-devvars/main.yml
Define role variables (higher precedence than defaults).
---
# OS-specific package names
_app_packages:
Debian:
- nginx
- postgresql-client
RedHat:
- nginx
- postgresql
app_packages: "{{ _app_packages[ansible_os_family] }}"
# Computed values
app_memory_limit: "{{ (ansible_memtotal_mb * 0.7) | int }}m"
app_workers: "{{ ansible_processor_vcpus }}"tasks/main.yml
Main task file - orchestrate task execution.
Pattern 1: Single file (simple roles)
---
- name: Install application
apt:
name: myapp
state: present
- name: Configure application
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
notify: Restart myapp
- name: Start application
service:
name: myapp
state: started
enabled: yesPattern 2: Include subtasks (complex roles)
---
- name: Include OS-specific variables
include_vars: "{{ ansible_os_family }}.yml"
- name: Include pre-flight checks
include_tasks: preflight.yml
- name: Include installation tasks
include_tasks: install.yml
- name: Include configuration tasks
include_tasks: configure.yml
- name: Include security hardening
include_tasks: security.yml
when: enable_security_hardening | default(true)tasks/install.yml:
---
- name: Create application user
user:
name: "{{ app_user }}"
system: yes
shell: /bin/false
- name: Install application packages
apt:
name: "{{ app_packages }}"
state: present
- name: Create application directories
file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: '0755'
loop:
- "{{ app_install_dir }}"
- "{{ app_config_dir }}"
- "{{ app_log_dir }}"handlers/main.yml
Define handlers triggered by task changes.
---
- name: Restart myapp
service:
name: myapp
state: restarted
- name: Reload myapp
service:
name: myapp
state: reloaded
- name: Validate config
command: myapp validate-config /etc/myapp/app.conf
changed_when: false
- name: Clear cache
command: myapp clear-cache
# Listen pattern (multiple triggers)
- name: Update systemd
systemd:
daemon_reload: yes
listen: Reload systemdtemplates/
Jinja2 templates for dynamic configuration files.
templates/app.conf.j2:
# {{ ansible_managed }}
# Application Configuration
[server]
port = {{ app_port }}
workers = {{ app_workers | default(4) }}
timeout = {{ app_timeout | default(30) }}
[logging]
level = {{ log_level | default('info') }}
path = {{ app_log_dir }}/app.log
{% if enable_monitoring %}
[monitoring]
enabled = true
metrics_port = {{ monitoring_port | default(9090) }}
{% endif %}
[database]
host = {{ db_host }}
port = {{ db_port }}
name = {{ db_name }}
user = {{ db_user }}
# Password stored in vaulttemplates/systemd.service.j2:
[Unit]
Description={{ app_name }} application
After=network.target
[Service]
Type=simple
User={{ app_user }}
Group={{ app_group }}
WorkingDirectory={{ app_install_dir }}
ExecStart={{ app_install_dir }}/bin/{{ app_name }} --config {{ app_config_dir }}/app.conf
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.targetfiles/
Static files copied without modification.
files/
├── ssl/
│ ├── ca.crt
│ └── dhparam.pem
├── scripts/
│ ├── backup.sh
│ └── healthcheck.sh
└── config/
└── static.confUsage:
- name: Copy SSL certificate
copy:
src: ssl/ca.crt
dest: /etc/ssl/certs/ca.crt
owner: root
group: root
mode: '0644'
- name: Deploy backup script
copy:
src: scripts/backup.sh
dest: /usr/local/bin/backup.sh
owner: root
group: root
mode: '0755'meta/main.yml
Role metadata and dependencies.
---
galaxy_info:
role_name: myapp
author: Your Name
description: Deploy and configure myapp application
company: Example Corp
license: MIT
min_ansible_version: "2.12"
platforms:
- name: Ubuntu
versions:
- focal
- jammy
- name: Debian
versions:
- bullseye
galaxy_tags:
- application
- web
- deployment
dependencies:
- role: common
- role: postgresql
vars:
postgresql_version: "15"
- role: nginx
when: install_nginx | default(true)---
Best Practices
1. Single Responsibility
Each role should have one clear purpose.
Good:
roles/
├── nginx/ # Web server configuration
├── postgresql/ # Database configuration
├── redis/ # Cache configuration
└── myapp/ # Application deploymentBad:
roles/
└── everything/ # Configures web, database, cache, app2. Parameterize with Defaults
Provide sensible defaults, allow overrides.
# defaults/main.yml
nginx_worker_processes: auto # Sensible default
nginx_worker_connections: 1024
nginx_enable_ssl: false # Safe default
# Allow override in playbook
- role: nginx
vars:
nginx_worker_processes: 8
nginx_enable_ssl: true3. Use Descriptive Variable Names
# Good: Clear, scoped naming
nginx_worker_processes: 4
nginx_client_max_body_size: "10m"
nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
# Bad: Vague, generic names
workers: 4
max_size: "10m"
protocols: "TLSv1.2 TLSv1.3"4. Prefix Role Variables
Prevent variable collisions between roles.
# Good: Role prefix
myapp_version: "1.0.0"
myapp_port: 8080
myapp_enable_ssl: true
# Bad: No prefix (conflicts possible)
version: "1.0.0"
port: 8080
enable_ssl: true5. Organize Complex Tasks
Split large task files into logical units.
# tasks/main.yml
- include_tasks: preflight.yml
- include_tasks: install.yml
- include_tasks: configure.yml
- include_tasks: security.yml6. Document Role Usage
Create comprehensive README.md.
# myapp Role
Deploy and configure myapp application.
## Requirements
- Ansible >= 2.12
- Ubuntu 20.04 or 22.04
## Role Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `app_version` | `1.0.0` | Application version |
| `app_port` | `8080` | Application port |
| `enable_ssl` | `false` | Enable SSL |
## Dependencies
- `common`
- `postgresql`
## Example Playbook
- hosts: appservers
roles:
- role: myapp
vars: app_version: "2.0.0" app_port: 9000
## License
MIT7. Version Control
.gitignore:
*.retry
.vault_pass
.molecule/8. Test Roles with Molecule
Use Molecule for automated role testing.
cd roles/myapp
molecule test---
Role Dependencies
Define Dependencies in meta/main.yml
---
dependencies:
- role: common
- role: postgresql
vars:
postgresql_version: "15"
postgresql_databases:
- name: myapp
owner: myapp
- role: nginx
when: use_nginx_proxy | default(true)Dependency Resolution
Dependencies execute before the role itself.
Execution order: 1. common role (dependency) 2. postgresql role (dependency) 3. nginx role (dependency, if condition met) 4. myapp role (main role)
Circular Dependencies
Avoid circular dependencies:
# BAD: Circular dependency
# Role A depends on B
# Role B depends on A
# GOOD: Extract shared logic to separate role
roles/
├── base/ # Shared dependencies
├── roleA/ # Depends on base
└── roleB/ # Depends on base---
Collections
Collection Structure
Collections bundle roles, modules, and plugins.
ansible_collections/
└── mycompany/
└── myapp/
├── roles/
│ ├── webserver/
│ ├── database/
│ └── monitoring/
├── plugins/
│ ├── modules/
│ └── inventory/
├── playbooks/
└── galaxy.ymlCreate Collection
# Initialize collection
ansible-galaxy collection init mycompany.myapp
# Build collection
ansible-galaxy collection build
# Install collection
ansible-galaxy collection install mycompany-myapp-1.0.0.tar.gzgalaxy.yml
---
namespace: mycompany
name: myapp
version: 1.0.0
readme: README.md
authors:
- Your Name <you@example.com>
description: MyApp deployment collection
license:
- MIT
tags:
- application
- web
dependencies:
community.general: ">=5.0.0"
ansible.posix: ">=1.4.0"Use Collection in Playbook
---
- name: Deploy application
hosts: appservers
collections:
- mycompany.myapp
roles:
- webserver
- database
- monitoring---
Testing Roles
Directory Structure
roles/myapp/
├── molecule/
│ └── default/
│ ├── molecule.yml
│ ├── converge.yml
│ └── verify.yml
└── tests/
├── test.yml
└── inventorySimple Test Playbook
tests/test.yml:
---
- name: Test myapp role
hosts: localhost
become: yes
roles:
- myapp
post_tasks:
- name: Verify service is running
service:
name: myapp
state: started
check_mode: yes
register: service_check
failed_when: service_check is changedRun Test
# Run test playbook
ansible-playbook tests/test.yml -i tests/inventory
# With Molecule
molecule testRun automated tests to verify role functionality before production deployment.
Secrets Management
Table of Contents
1. ansible-vault (Built-in) 2. HashiCorp Vault Integration 3. Best Practices 4. Comparison
---
ansible-vault (Built-in)
Basic Operations
Create encrypted file:
ansible-vault create group_vars/all/vault.yml
# Enter password when promptedEdit encrypted file:
ansible-vault edit group_vars/all/vault.ymlView encrypted file:
ansible-vault view group_vars/all/vault.ymlEncrypt existing file:
ansible-vault encrypt secrets.ymlDecrypt file:
ansible-vault decrypt secrets.ymlChange password:
ansible-vault rekey group_vars/all/vault.ymlVault File Content
group_vars/all/vault.yml (encrypted):
---
vault_db_password: "SuperSecretPassword123"
vault_api_key: "sk-abcdef123456"
vault_smtp_password: "email_password"
vault_ssl_private_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
-----END PRIVATE KEY-----Reference in unencrypted file:
group_vars/all/vars.yml (unencrypted):
---
db_host: "db.example.com"
db_user: "appuser"
db_password: "{{ vault_db_password }}"
api_endpoint: "https://api.example.com"
api_key: "{{ vault_api_key }}"Run Playbooks with Vault
Method 1: Interactive password:
ansible-playbook site.yml --ask-vault-passMethod 2: Password file:
# Create password file
echo "MyVaultPassword" > ~/.vault_pass
chmod 600 ~/.vault_pass
# Run playbook
ansible-playbook site.yml --vault-password-file ~/.vault_passMethod 3: Executable script:
# Create password script
cat > ~/.vault_pass.sh << 'EOF'
#!/bin/bash
# Fetch password from secret manager
aws secretsmanager get-secret-value \
--secret-id ansible-vault-password \
--query SecretString \
--output text
EOF
chmod +x ~/.vault_pass.sh
# Run playbook
ansible-playbook site.yml --vault-password-file ~/.vault_pass.shMultiple Vault IDs
Use different passwords for different environments.
Create vaults with IDs:
# Production vault
ansible-vault create group_vars/production/vault.yml --vault-id prod@prompt
# Staging vault
ansible-vault create group_vars/staging/vault.yml --vault-id staging@promptPassword files:
# Store passwords
echo "ProductionPassword" > ~/.vault_pass_prod
echo "StagingPassword" > ~/.vault_pass_staging
chmod 600 ~/.vault_pass_*Run with specific vault ID:
ansible-playbook site.yml \
--vault-id prod@~/.vault_pass_prod \
--vault-id staging@~/.vault_pass_stagingEncrypt Strings
Encrypt individual strings instead of entire files.
# Encrypt string
ansible-vault encrypt_string 'SuperSecret123' --name 'db_password'Output:
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653033653834303138656337353833656362393137313633633732306362333...Use in playbook:
---
- name: Configure database
hosts: databases
vars:
db_user: appuser
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653033653834303138656337353833656362393137313633633732306362333...
tasks:
- name: Create database user
postgresql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"ansible.cfg Configuration
[defaults]
vault_password_file = ~/.vault_pass
# Or multiple vault IDs
vault_identity_list = prod@~/.vault_pass_prod, staging@~/.vault_pass_staging---
HashiCorp Vault Integration
Prerequisites
Install collection:
ansible-galaxy collection install community.hashi_vaultVault server setup:
# Start Vault (dev mode for testing)
vault server -dev
# Set environment
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='dev-token'
# Create secrets
vault kv put secret/myapp/database \
host=db.example.com \
username=appuser \
password=SuperSecret123
vault kv put secret/myapp/api \
endpoint=https://api.example.com \
key=sk-abcdef123456Lookup Secrets in Playbooks
Method 1: KV v2 secrets (default):
---
- name: Configure application
hosts: appservers
vars:
vault_addr: "http://127.0.0.1:8200"
vault_token: "{{ lookup('env', 'VAULT_TOKEN') }}"
tasks:
- name: Fetch database password from Vault
set_fact:
db_config: "{{ lookup('community.hashi_vault.vault_kv2_get', 'secret/data/myapp/database', url=vault_addr, token=vault_token) }}"
- name: Configure database connection
template:
src: database.conf.j2
dest: /etc/myapp/database.conf
vars:
db_host: "{{ db_config.secret.host }}"
db_user: "{{ db_config.secret.username }}"
db_password: "{{ db_config.secret.password }}"Method 2: Direct lookup:
- name: Read secret value
debug:
msg: "{{ lookup('community.hashi_vault.vault_read', 'secret/data/myapp/database').data.data.password }}"Method 3: Using hashi_vault lookup (legacy):
- name: Fetch API key
set_fact:
api_key: "{{ lookup('community.hashi_vault.hashi_vault', 'secret/myapp/api:key') }}"Authentication Methods
Token auth (simple):
vars:
vault_token: "{{ lookup('env', 'VAULT_TOKEN') }}"
lookup('community.hashi_vault.vault_kv2_get',
'secret/data/myapp/database',
token=vault_token)AppRole auth (production):
vars:
vault_role_id: "{{ lookup('env', 'VAULT_ROLE_ID') }}"
vault_secret_id: "{{ lookup('env', 'VAULT_SECRET_ID') }}"
lookup('community.hashi_vault.vault_kv2_get',
'secret/data/myapp/database',
role_id=vault_role_id,
secret_id=vault_secret_id)AWS IAM auth:
lookup('community.hashi_vault.vault_kv2_get',
'secret/data/myapp/database',
auth_method='aws_iam',
mount_point='aws')Dynamic Secrets
Generate credentials on-demand.
Database credentials:
- name: Generate dynamic database credentials
set_fact:
db_creds: "{{ lookup('community.hashi_vault.vault_read', 'database/creds/myapp-role') }}"
- name: Use dynamic credentials
debug:
msg: |
Username: {{ db_creds.data.username }}
Password: {{ db_creds.data.password }}
Lease: {{ db_creds.lease_duration }} secondsAWS credentials:
- name: Generate AWS credentials
set_fact:
aws_creds: "{{ lookup('community.hashi_vault.vault_read', 'aws/creds/deploy-role') }}"
- name: Use AWS credentials
aws_s3:
aws_access_key: "{{ aws_creds.data.access_key }}"
aws_secret_key: "{{ aws_creds.data.secret_key }}"
bucket: mybucket
object: myfile
mode: putTemplate Example
templates/database.conf.j2:
# Database Configuration
{% set db = lookup('community.hashi_vault.vault_kv2_get', 'secret/data/myapp/database') %}
[database]
host = {{ db.secret.host }}
port = {{ db.secret.port | default(5432) }}
name = {{ db.secret.database }}
user = {{ db.secret.username }}
password = {{ db.secret.password }}
[connection]
pool_size = {{ db_pool_size | default(10) }}---
Best Practices
1. Separate Secrets from Configuration
Good structure:
group_vars/
├── all/
│ ├── vars.yml # Unencrypted config
│ └── vault.yml # Encrypted secrets
└── production/
├── vars.yml
└── vault.ymlvars.yml (unencrypted):
db_host: "db.example.com"
db_user: "appuser"
db_password: "{{ vault_db_password }}"vault.yml (encrypted):
vault_db_password: "SuperSecret123"2. Prefix Vault Variables
# Good: Clear which vars are from vault
vault_db_password: "secret"
vault_api_key: "secret"
vault_ssl_cert: "secret"
# Bad: Unclear source
db_password: "secret"
api_key: "secret"
ssl_cert: "secret"3. Never Commit Unencrypted Secrets
.gitignore:
# Vault password files
.vault_pass*
vault_password*
# Decrypted files (if working locally)
*_decrypted.yml
secrets_plain.yml
# SSH keys
*.pem
id_rsa4. Rotate Secrets Regularly
# Update secret in vault
ansible-vault edit group_vars/all/vault.yml
# Rotate vault password
ansible-vault rekey group_vars/all/vault.yml5. Use Different Passwords per Environment
.vault_pass_production # Production password
.vault_pass_staging # Staging password
.vault_pass_development # Development password6. Audit Secret Access
With HashiCorp Vault:
# Enable audit logging
vault audit enable file file_path=/var/log/vault_audit.log
# View audit log
tail -f /var/log/vault_audit.log7. Limit Secret Scope
Don't put all secrets in one file.
Good:
group_vars/
├── webservers/
│ └── vault.yml # Only web secrets
└── databases/
└── vault.yml # Only DB secretsBad:
group_vars/
└── all/
└── vault.yml # All secrets (unnecessary exposure)8. Test Vault Access
# Test vault decryption
ansible-vault view group_vars/all/vault.yml --vault-password-file ~/.vault_pass
# Test playbook with vault
ansible-playbook site.yml --check --vault-password-file ~/.vault_pass9. Use Vault for CI/CD
GitHub Actions example:
- name: Run Ansible playbook
env:
VAULT_PASSWORD: ${{ secrets.VAULT_PASSWORD }}
run: |
echo "$VAULT_PASSWORD" > .vault_pass
ansible-playbook site.yml --vault-password-file .vault_pass
rm .vault_pass10. Document Secret Requirements
README.md:
## Required Secrets
### ansible-vault
- `vault_db_password` - PostgreSQL password
- `vault_api_key` - External API key
- `vault_ssl_private_key` - SSL private key
### HashiCorp Vault Paths
- `secret/myapp/database` - Database credentials
- `secret/myapp/api` - API configuration
- `secret/myapp/ssl` - SSL certificates---
Comparison
| Feature | ansible-vault | HashiCorp Vault |
|---|---|---|
| Complexity | Simple | Complex setup |
| Dynamic secrets | No | Yes |
| Audit logging | No | Yes |
| Secret rotation | Manual | Automated |
| Access control | Password-based | Policies, roles |
| Integration | Built-in | Requires plugin |
| Best for | Small teams, simple needs | Enterprise, compliance |
| Cost | Free | Free (OSS) / Paid (Enterprise) |
When to Use ansible-vault
- Small teams (< 10 people)
- Simple secret management needs
- No compliance requirements
- Secrets change infrequently
- No need for audit trails
When to Use HashiCorp Vault
- Large teams or enterprises
- Dynamic secret generation needed
- Compliance requirements (SOC2, PCI-DSS)
- Frequent secret rotation
- Detailed audit logging required
- Integration with multiple tools (not just Ansible)
- Cloud-native architectures
Hybrid Approach
Use both for different purposes:
# Low-sensitivity config: ansible-vault
vault_app_config: "non-critical-config"
# High-sensitivity credentials: HashiCorp Vault
db_password: "{{ lookup('community.hashi_vault.vault_kv2_get', 'secret/data/myapp/database').secret.password }}"Related skills
FAQ
How does the skill ensure safe reruns?
It teaches idempotency using state-based modules like present, started, and latest instead of imperative command modules, so playbooks can run repeatedly without side effects.
How are secrets managed?
With ansible-vault for built-in encryption and HashiCorp Vault for enterprise-grade dynamic credentials.