Getting Started with Automating Your Infrastructure
Introduction
What is Ansible?
Ansible is an open-source automation platform that simplifies complex configuration management, application deployment, intraservice orchestration, and provisioning. Created by Michael DeHaan in 2012 and later acquired by Red Hat, Ansible has become one of the most popular Infrastructure as Code (IaC) tools in the DevOps ecosystem.
Key Characteristics:
- Agentless: No need to install agents on managed nodes
- Declarative: Define the desired state, Ansible figures out how to achieve it
- Idempotent: Running the same playbook multiple times produces the same result
- Human-readable: Uses YAML syntax that’s easy to understand
- Extensible: Supports custom modules and plugins
Why Automation Matters
In modern IT infrastructure, manual configuration and deployment processes are:
- Error-prone: Human mistakes are inevitable
- Time-consuming: Manual tasks don’t scale
- Inconsistent: Different admins might configure systems differently
- Undocumented: Manual processes are hard to track and audit
- Difficult to rollback: No easy way to undo changes
Automation Benefits:
- Consistency: Same configuration across all environments
- Speed: Rapid deployment and scaling
- Reliability: Reduced human error
- Compliance: Auditable and repeatable processes
- Cost-effective: Less manual intervention required
Key Benefits of Ansible
- Simple Learning Curve: YAML-based playbooks are easy to read and write
- No Agent Required: SSH access is sufficient for most operations
- Powerful: Can manage complex multi-tier applications
- Flexible: Works with any system that has SSH or PowerShell
- Extensive Module Library: 3000+ modules for various tasks
- Community Support: Large, active community and enterprise backing
Why Choose Ansible?
Feature |
Ansible |
Puppet |
Chef |
SaltStack |
Agent Required |
❌ No |
✅ Yes |
✅ Yes |
✅ Yes |
Language |
YAML |
Ruby DSL |
Ruby |
YAML/Python |
Learning Curve |
Easy |
Moderate |
Steep |
Moderate |
Push/Pull |
Push |
Pull |
Pull |
Both |
Configuration |
Declarative |
Declarative |
Procedural |
Both |
Enterprise Support |
Red Hat |
Puppet Labs |
Chef Inc. |
SaltStack Inc. |
Agentless Nature
Traditional Agent-based Tools:
1
2
3
| Control Node → Agent (Node 1)
→ Agent (Node 2)
→ Agent (Node 3)
|
Ansible Agentless Approach:
1
2
3
| Control Node → SSH (Node 1)
→ SSH (Node 2)
→ SSH (Node 3)
|
Benefits of Agentless:
- No additional software to install and maintain
- No agent updates or security patches
- Reduced attack surface
- Works with any SSH-accessible system
- Immediate deployment capability
Human-readable YAML Syntax
Ansible Playbook:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ---
- name: Install and configure web server
hosts: webservers
become: yes
tasks:
- name: Install nginx
apt:
name: nginx
state: present
- name: Start nginx service
service:
name: nginx
state: started
enabled: yes
|
Equivalent Shell Script:
1
2
3
4
5
6
| #!/bin/bash
for server in web1 web2 web3; do
ssh $server "apt update && apt install -y nginx"
ssh $server "systemctl start nginx"
ssh $server "systemctl enable nginx"
done
|
Idempotency Explained
Idempotency means that applying the same operation multiple times has the same effect as applying it once. This is crucial for configuration management.
Example: Non-idempotent vs Idempotent
Non-idempotent (Shell):
1
2
| # This will add the line every time it runs
echo "127.0.0.1 myapp.local" >> /etc/hosts
|
Idempotent (Ansible):
1
2
3
4
5
| - name: Add entry to /etc/hosts
lineinfile:
path: /etc/hosts
line: "127.0.0.1 myapp.local"
state: present
|
The Ansible version will only add the line if it doesn’t exist, making it safe to run multiple times.
Ansible Architecture
High-Level Architecture
Detailed Architecture Components
Ansible Execution Flow
Core Components Explained
1. Control Node
The machine where Ansible is installed and from which all tasks and playbooks are run.
Requirements:
- Python 2.7 or Python 3.5+
- SSH client
- Can be: laptop, server, or CI/CD system
2. Managed Nodes
The network devices, servers, or other systems that Ansible manages.
Requirements:
- SSH access (Linux/Unix)
- PowerShell Remoting (Windows)
- Python 2.6+ or Python 3.5+
3. Inventory
A list of managed nodes organized into groups.
4. Modules
Units of code that Ansible executes on managed nodes.
5. Tasks
Units of action in Ansible that call modules.
6. Playbooks
Ordered lists of tasks saved in YAML format.
Core Concepts
1. Inventory
The inventory defines the hosts and groups of hosts upon which commands, modules, and tasks in a playbook operate.
Static Inventory Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # inventory.ini
[webservers]
web1.example.com ansible_host=192.168.1.10
web2.example.com ansible_host=192.168.1.11
web3.example.com ansible_host=192.168.1.12
[databases]
db1.example.com ansible_host=192.168.1.20
db2.example.com ansible_host=192.168.1.21
[production:children]
webservers
databases
[production:vars]
ansible_user=admin
ansible_ssh_private_key_file=~/.ssh/prod_key
|
YAML Inventory Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # inventory.yml
all:
children:
webservers:
hosts:
web1.example.com:
ansible_host: 192.168.1.10
web2.example.com:
ansible_host: 192.168.1.11
databases:
hosts:
db1.example.com:
ansible_host: 192.168.1.20
vars:
mysql_port: 3306
|
2. Modules
Modules are the units of work in Ansible. Each module has a specific purpose.
Common Module Categories:
- System: user, group, service, cron
- Files: file, copy, template, fetch
- Packaging: apt, yum, pip, npm
- Source Control: git, svn
- Database: mysql_user, postgresql_db
- Cloud: ec2, gcp_instance, azure_rm_virtualmachine
Module Documentation:
1
2
3
| # Get help for a specific module
ansible-doc apt
ansible-doc -l | grep mysql
|
3. Tasks
Tasks are calls to modules with specific parameters.
1
2
3
4
5
6
7
8
9
10
11
12
| - name: Install nginx
apt:
name: nginx
state: present
update_cache: yes
- name: Copy configuration file
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
backup: yes
notify: restart nginx
|
4. Playbooks
Playbooks are YAML files containing a series of tasks to be executed on specific hosts.
Basic Playbook Structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| ---
- name: Configure web servers
hosts: webservers
become: yes
vars:
nginx_port: 80
tasks:
- name: Install nginx
apt:
name: nginx
state: present
- name: Start nginx
service:
name: nginx
state: started
enabled: yes
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
|
5. Roles
Roles are ways of automatically loading certain vars, tasks, handlers, and other Ansible artifacts based on a known file structure.
Role Directory Structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| roles/
└── nginx/
├── tasks/
│ └── main.yml
├── handlers/
│ └── main.yml
├── templates/
│ └── nginx.conf.j2
├── files/
├── vars/
│ └── main.yml
├── defaults/
│ └── main.yml
├── meta/
│ └── main.yml
└── README.md
|
6. Variables & Templates (Jinja2)
Variables allow you to customize playbooks and roles.
Variable Precedence (highest to lowest):
- Extra vars (
-e
in command line)
- Task vars (only for the task)
- Block vars (only for tasks in block)
- Role and include vars
- Play vars
- Host facts / cached set_facts
- Host vars
- Group vars
- Role defaults
Jinja2 Template Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # templates/nginx.conf.j2
server {
listen {{ nginx_port }};
server_name {{ ansible_hostname }};
location / {
root /var/www/html;
index index.html;
}
{% if ssl_enabled %}
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
}
|
7. Handlers
Handlers are tasks that run only when notified by other tasks.
1
2
3
4
5
6
7
8
9
10
11
12
| tasks:
- name: Update nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
|
8. Facts
Facts are system properties that Ansible automatically discovers about managed nodes.
Common Facts:
ansible_hostname
: System hostname
ansible_os_family
: OS family (RedHat, Debian, etc.)
ansible_distribution
: Specific OS distribution
ansible_memory_mb
: Total memory in MB
ansible_processor_cores
: Number of CPU cores
Using Facts:
1
2
3
4
5
6
7
| - name: Display system information
debug:
msg: |
Hostname: {{ ansible_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Memory: {{ ansible_memory_mb.real.total }}MB
CPU Cores: {{ ansible_processor_cores }}
|
Installing Ansible
Prerequisites
Control Node Requirements:
- Python 2.7 or Python 3.5+
- SSH client
- Operating System: Linux, macOS, or WSL on Windows
Managed Node Requirements:
- Python 2.6+ or Python 3.5+
- SSH server running
- User account with sudo privileges (for privilege escalation)
Installation Methods
1. On Debian/Ubuntu
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Update package cache
sudo apt update
# Install software-properties-common for add-apt-repository
sudo apt install software-properties-common
# Add Ansible repository
sudo add-apt-repository --yes --update ppa:ansible/ansible
# Install Ansible
sudo apt install ansible
# Verify installation
ansible --version
|
Alternative using pip:
1
2
3
4
5
6
7
8
| # Install pip if not available
sudo apt install python3-pip
# Install Ansible via pip
pip3 install ansible
# Add to PATH (add to ~/.bashrc for persistence)
export PATH=$PATH:~/.local/bin
|
2. On RHEL/CentOS/Fedora
RHEL/CentOS:
1
2
3
4
5
6
7
8
| # Enable EPEL repository
sudo yum install epel-release
# Install Ansible
sudo yum install ansible
# For RHEL 8/CentOS 8
sudo dnf install ansible
|
Fedora:
1
| sudo dnf install ansible
|
3. On macOS
Using Homebrew:
1
2
3
4
5
| # Install Homebrew if not available
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install Ansible
brew install ansible
|
Using pip:
1
2
3
4
5
6
| # Install pip
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python3 get-pip.py --user
# Install Ansible
pip3 install --user ansible
|
4. Using Docker Container
Create Ansible Container:
1
2
3
4
5
6
7
8
9
| # Pull Ansible image
docker pull ansible/ansible:latest
# Run Ansible container
docker run -it --rm \
-v $(pwd):/workspace \
-v ~/.ssh:/root/.ssh:ro \
ansible/ansible:latest \
/bin/bash
|
Dockerfile for Custom Ansible Image:
1
2
3
4
5
6
7
| FROM python:3.9-slim
RUN pip install ansible
WORKDIR /workspace
CMD ["ansible-playbook", "--version"]
|
5. Using Virtual Environment (Recommended for Development)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Create virtual environment
python3 -m venv ansible-env
# Activate virtual environment
source ansible-env/bin/activate
# Install Ansible
pip install ansible
# Verify installation
ansible --version
# Deactivate when done
deactivate
|
Verify Installation
1
2
3
4
5
6
7
8
9
10
11
| # Check Ansible version
ansible --version
# Check available modules
ansible-doc -l | wc -l
# Test with localhost
ansible localhost -m ping
# Check configuration
ansible-config dump --only-changed
|
Expected Output:
1
2
3
4
5
6
7
8
9
| ansible [core 2.14.1]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3/site-packages/ansible
ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.9.16 (main, Dec 7 2022, 01:11:51) [GCC 9.4.0] (/usr/bin/python3)
jinja version = 3.0.3
libyaml = True
|
Configuration Files
Ansible Configuration Priority (highest to lowest):
ANSIBLE_CONFIG
environment variable
ansible.cfg
in current directory
~/.ansible.cfg
in home directory
/etc/ansible/ansible.cfg
Basic ansible.cfg:
1
2
3
4
5
6
7
8
9
10
11
12
13
| [defaults]
inventory = ./inventory
host_key_checking = False
remote_user = ansible
private_key_file = ~/.ssh/ansible_key
timeout = 30
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o UserKnownHostsFile=/dev/null
pipelining = True
|
Setting Up Your First Inventory
Static Inventory
INI Format (inventory.ini):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| # Ungrouped hosts
server1.example.com
192.168.1.100
# Web servers group
[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
# Database servers group
[databases]
db1.example.com ansible_host=10.0.2.10
db2.example.com ansible_host=10.0.2.11
# Load balancer group
[loadbalancers]
lb1.example.com ansible_host=10.0.3.10
# Group of groups
[production:children]
webservers
databases
loadbalancers
# Variables for all production servers
[production:vars]
ansible_user=deploy
ansible_ssh_private_key_file=~/.ssh/deploy_key
ansible_become=yes
ansible_become_method=sudo
# Variables specific to web servers
[webservers:vars]
http_port=80
https_port=443
# Variables specific to databases
[databases:vars]
mysql_port=3306
mysql_root_password=secret123
|
YAML Format (inventory.yml):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| all:
hosts:
server1.example.com:
192.168.1.100:
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: 2
web3.example.com:
ansible_host: 10.0.1.12
nginx_worker_processes: 2
vars:
http_port: 80
https_port: 443
databases:
hosts:
db1.example.com:
ansible_host: 10.0.2.10
mysql_server_id: 1
db2.example.com:
ansible_host: 10.0.2.11
mysql_server_id: 2
vars:
mysql_port: 3306
mysql_root_password: "{{ vault_mysql_root_password }}"
loadbalancers:
hosts:
lb1.example.com:
ansible_host: 10.0.3.10
production:
children:
webservers:
databases:
loadbalancers:
vars:
ansible_user: deploy
ansible_ssh_private_key_file: ~/.ssh/deploy_key
ansible_become: yes
environment: production
backup_enabled: true
|
Host Variables
Directory Structure:
1
2
3
4
5
6
7
8
9
10
| inventory/
├── hosts.yml
├── host_vars/
│ ├── web1.example.com.yml
│ ├── web2.example.com.yml
│ └── db1.example.com.yml
└── group_vars/
├── all.yml
├── webservers.yml
└── databases.yml
|
host_vars/web1.example.com.yml:
1
2
3
4
5
6
| ---
nginx_worker_processes: 4
ssl_certificate: /etc/ssl/certs/web1.crt
ssl_private_key: /etc/ssl/private/web1.key
custom_error_pages: true
log_level: warn
|
group_vars/webservers.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| ---
# Nginx configuration
nginx_user: www-data
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 64m
# SSL configuration
ssl_enabled: true
ssl_protocols:
- TLSv1.2
- TLSv1.3
# Logging
access_log_enabled: true
error_log_level: error
# Security
server_tokens_off: true
|
group_vars/all.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| ---
# Common variables for all hosts
timezone: "America/New_York"
ntp_servers:
- time1.google.com
- time2.google.com
# Security settings
ssh_port: 22
fail2ban_enabled: true
# Monitoring
monitoring_enabled: true
log_retention_days: 30
# Backup settings
backup_retention_days: 7
backup_time: "02:00"
|
Dynamic Inventories
Dynamic inventories pull host information from external sources like cloud providers, CMDB systems, or APIs.
AWS EC2 Dynamic Inventory
Install boto3:
AWS Configuration:
1
2
| # Configure AWS credentials
aws configure
|
inventory_aws_ec2.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| plugin: amazon.aws.aws_ec2
regions:
- us-east-1
- us-west-2
filters:
instance-state-name: running
keyed_groups:
- prefix: tag
key: tags
- prefix: instance_type
key: instance_type
- prefix: aws_region
key: region
hostnames:
- dns-name
- private-ip-address
compose:
ansible_host: public_ip_address
ansible_user: ec2-user
|
GCP Dynamic Inventory
inventory_gcp.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| plugin: google.cloud.gcp_compute
projects:
- my-project-123
auth_kind: serviceaccount
service_account_file: /path/to/service-account.json
filters:
- status = RUNNING
keyed_groups:
- prefix: gcp
key: labels
hostnames:
- name
compose:
ansible_host: networkInterfaces[0].accessConfigs[0].natIP
|
Custom Dynamic Inventory Script
inventory.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| #!/usr/bin/env python3
import json
import subprocess
import sys
def get_inventory():
# Example: Get hosts from a database or API
inventory = {
'webservers': {
'hosts': ['web1.example.com', 'web2.example.com'],
'vars': {
'http_port': 80,
'https_port': 443
}
},
'databases': {
'hosts': ['db1.example.com', 'db2.example.com'],
'vars': {
'mysql_port': 3306
}
},
'_meta': {
'hostvars': {
'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': 2
}
}
}
}
return inventory
def get_host_vars(host):
# Return variables for a specific host
inventory = get_inventory()
return inventory.get('_meta', {}).get('hostvars', {}).get(host, {})
if __name__ == '__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':
print(json.dumps(get_host_vars(sys.argv[2]), indent=2))
else:
print("Usage: %s --list or %s --host <hostname>" % (sys.argv[0], sys.argv[0]))
sys.exit(1)
|
Make it executable:
Testing Your Inventory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # List all hosts
ansible-inventory --list
# List specific group
ansible-inventory --list -l webservers
# Graph inventory
ansible-inventory --graph
# Test connectivity
ansible all -m ping
# Test specific group
ansible webservers -m ping
# Get facts from all hosts
ansible all -m setup
# Test with specific inventory file
ansible -i inventory.yml all -m ping
|
Ad-hoc Commands
Ad-hoc commands are great for quick tasks that you might not want to write a complete playbook for. They use the /usr/bin/ansible
command-line tool.
Basic Syntax
1
| ansible <hosts> -m <module> -a "<module_options>"
|
Common Ad-hoc Command Examples
1. Ping Module - Test Connectivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # Ping all hosts
ansible all -m ping
# Ping specific group
ansible webservers -m ping
# Ping specific host
ansible web1.example.com -m ping
# Ping with different user
ansible all -m ping -u deploy
# Expected output:
web1.example.com | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
|
2. Package Management
APT (Debian/Ubuntu):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Install a package
ansible webservers -m apt -a "name=nginx state=present" --become
# Install multiple packages
ansible webservers -m apt -a "name=nginx,git,curl state=present" --become
# Update package cache
ansible webservers -m apt -a "update_cache=yes" --become
# Upgrade all packages
ansible webservers -m apt -a "upgrade=dist" --become
# Remove a package
ansible webservers -m apt -a "name=apache2 state=absent" --become
# Install specific version
ansible webservers -m apt -a "name=nginx=1.18.0-6ubuntu14 state=present" --become
|
YUM (RHEL/CentOS):
1
2
3
4
5
6
7
8
| # Install package
ansible databases -m yum -a "name=mysql-server state=present" --become
# Install from specific repository
ansible databases -m yum -a "name=nginx state=present enablerepo=epel" --become
# Update all packages
ansible databases -m yum -a "name=* state=latest" --become
|
3. File Management
File Module:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Create a directory
ansible all -m file -a "path=/opt/myapp state=directory mode=0755" --become
# Create a file
ansible all -m file -a "path=/opt/myapp/config.txt state=touch mode=0644" --become
# Change file permissions
ansible all -m file -a "path=/opt/myapp/script.sh mode=0755" --become
# Change ownership
ansible all -m file -a "path=/opt/myapp owner=appuser group=appgroup" --become
# Create symbolic link
ansible all -m file -a "src=/opt/myapp/current dest=/opt/myapp/latest state=link" --become
# Remove file or directory
ansible all -m file -a "path=/tmp/tempfile state=absent" --become
|
Copy Module:
1
2
3
4
5
6
7
8
| # Copy file from control node to managed nodes
ansible webservers -m copy -a "src=/local/path/nginx.conf dest=/etc/nginx/nginx.conf backup=yes" --become
# Copy with specific permissions
ansible webservers -m copy -a "src=app.conf dest=/etc/myapp/app.conf mode=0644 owner=root group=root" --become
# Copy content directly
ansible webservers -m copy -a "content='Hello World\n' dest=/tmp/hello.txt" --become
|
Fetch Module:
1
2
3
4
5
| # Fetch file from managed nodes to control node
ansible databases -m fetch -a "src=/var/log/mysql/error.log dest=./logs/"
# Fetch with original path structure
ansible databases -m fetch -a "src=/etc/mysql/my.cnf dest=./backups/ flat=no"
|
4. User Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Create user
ansible all -m user -a "name=devuser comment='Developer User' shell=/bin/bash" --become
# Create user with specific UID and home directory
ansible all -m user -a "name=appuser uid=1500 home=/opt/appuser shell=/bin/bash" --become
# Add user to groups
ansible all -m user -a "name=devuser groups=sudo,docker append=yes" --become
# Set password (use encrypted password)
ansible all -m user -a "name=devuser password=$6$salt$encrypted_password" --become
# Remove user
ansible all -m user -a "name=olduser state=absent remove=yes" --become
# Lock user account
ansible all -m user -a "name=suspendeduser account_locked=yes" --become
|
SSH Key Management:
1
2
3
4
5
6
7
8
| # Add SSH public key to user
ansible all -m authorized_key -a "user=devuser key='ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7...'" --become
# Add SSH key from file
ansible all -m authorized_key -a "user=devuser key='{{ lookup('file', '~/.ssh/id_rsa.pub') }}'" --become
# Remove SSH key
ansible all -m authorized_key -a "user=devuser key='ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7...' state=absent" --become
|
5. Service Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Start service
ansible webservers -m service -a "name=nginx state=started" --become
# Stop service
ansible webservers -m service -a "name=apache2 state=stopped" --become
# Restart service
ansible databases -m service -a "name=mysql state=restarted" --become
# Enable service at boot
ansible webservers -m service -a "name=nginx enabled=yes" --become
# Check service status
ansible all -m service -a "name=sshd" --become
# Start and enable service
ansible webservers -m service -a "name=nginx state=started enabled=yes" --become
|
6. Command and Shell Modules
Command Module (safer, doesn’t use shell):
1
2
3
4
5
6
7
8
| # Run command
ansible all -m command -a "uptime"
# Run command with specific working directory
ansible all -m command -a "ls -la" -a "chdir=/opt"
# Run command that creates a file (idempotent)
ansible all -m command -a "touch /tmp/ansible-test creates=/tmp/ansible-test"
|
Shell Module (uses shell, allows pipes, redirects):
1
2
3
4
5
6
7
8
| # Run shell command
ansible all -m shell -a "ps aux | grep nginx"
# Run command with pipe
ansible databases -m shell -a "mysql -e 'SHOW DATABASES;'" --become
# Run script
ansible all -m shell -a "/opt/scripts/backup.sh"
|
7. Gathering Facts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Gather all facts
ansible all -m setup
# Gather specific facts
ansible all -m setup -a "filter=ansible_distribution*"
# Gather memory facts
ansible all -m setup -a "filter=ansible_memory_mb"
# Gather network facts
ansible all -m setup -a "filter=ansible_default_ipv4"
# Save facts to file
ansible all -m setup --tree ./facts
|
8. File Content Manipulation
Lineinfile Module:
1
2
3
4
5
6
7
8
| # Add line to file
ansible all -m lineinfile -a "path=/etc/hosts line='192.168.1.100 myapp.local'" --become
# Remove line from file
ansible all -m lineinfile -a "path=/etc/hosts line='192.168.1.100 myapp.local' state=absent" --become
# Replace line using regex
ansible all -m lineinfile -a "path=/etc/ssh/sshd_config regexp='^#?Port' line='Port 2222'" --become
|
9. Cron Management
1
2
3
4
5
6
7
8
| # Add cron job
ansible all -m cron -a "name='daily backup' minute='0' hour='2' job='/opt/scripts/backup.sh'" --become
# Remove cron job
ansible all -m cron -a "name='daily backup' state=absent" --become
# Add cron job for specific user
ansible all -m cron -a "name='user backup' minute='30' hour='1' user=backupuser job='/home/backupuser/backup.sh'" --become
|
10. Archive and Unarchive
1
2
3
4
5
6
7
8
| # Create archive
ansible all -m archive -a "path=/opt/myapp dest=/tmp/myapp-backup.tar.gz"
# Extract archive
ansible all -m unarchive -a "src=/tmp/app.tar.gz dest=/opt/ remote_src=yes" --become
# Download and extract
ansible all -m unarchive -a "src=https://example.com/app.tar.gz dest=/opt/ remote_src=yes" --become
|
Advanced Ad-hoc Command Options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # Run with different inventory
ansible -i production.ini all -m ping
# Limit to specific hosts
ansible all -l "web*" -m ping
# Run in parallel (default is 5)
ansible all -f 10 -m ping
# Run with different user
ansible all -u deploy -m ping
# Use different SSH key
ansible all --private-key ~/.ssh/production_key -m ping
# Become different user
ansible all --become --become-user root -m ping
# Run with vault password
ansible all --ask-vault-pass -m ping
# Dry run (check mode)
ansible all -m command -a "service nginx restart" --check
# Verbose output
ansible all -m ping -v
ansible all -m ping -vv # More verbose
ansible all -m ping -vvv # Very verbose
|
Writing Your First Playbook
Playbooks are Ansible’s configuration, deployment, and orchestration language. They are expressed in YAML format and describe a policy you want your remote systems to enforce.
Basic Playbook Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| ---
- name: My First Playbook
hosts: webservers
become: yes
vars:
package_name: nginx
service_name: nginx
tasks:
- name: Install package
apt:
name: "{{ package_name }}"
state: present
update_cache: yes
- name: Start and enable service
service:
name: "{{ service_name }}"
state: started
enabled: yes
|
Playbook Components Explained
1. Play Definition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ---
- name: Configure web servers # Play name (optional but recommended)
hosts: webservers # Target hosts or groups
become: yes # Privilege escalation
become_user: root # User to become (default: root)
become_method: sudo # Method for privilege escalation
remote_user: ansible # SSH user
gather_facts: yes # Gather system facts (default: yes)
serial: 2 # Number of hosts to run simultaneously
max_fail_percentage: 20 # Maximum failure percentage before stopping
connection: ssh # Connection type
vars: # Play-level variables
http_port: 80
app_name: mywebapp
|
2. Complete Web Server Setup Playbook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
| ---
- name: Install and configure Nginx web server
hosts: webservers
become: yes
vars:
nginx_port: 80
nginx_user: www-data
document_root: /var/www/html
site_name: example.com
tasks:
- name: Update apt package cache
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Install Nginx
apt:
name: nginx
state: present
notify: start nginx
- name: Install additional packages
apt:
name:
- curl
- wget
- unzip
state: present
- name: Create document root directory
file:
path: "{{ document_root }}"
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: '0755'
- name: Copy Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
backup: yes
validate: 'nginx -t -c %s'
notify: restart nginx
- name: Copy site configuration
template:
src: site.conf.j2
dest: "/etc/nginx/sites-available/{{ site_name }}"
notify: restart nginx
- name: Enable site
file:
src: "/etc/nginx/sites-available/{{ site_name }}"
dest: "/etc/nginx/sites-enabled/{{ site_name }}"
state: link
notify: restart nginx
- name: Remove default site
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: restart nginx
- name: Create index.html
copy:
content: |
<!DOCTYPE html>
<html>
<head>
<title>Welcome to {{ site_name }}</title>
</head>
<body>
<h1>Hello from {{ ansible_hostname }}</h1>
<p>Server IP: {{ ansible_default_ipv4.address }}</p>
<p>Nginx Version: {{ ansible_facts.packages.nginx[0].version }}</p>
</body>
</html>
dest: "{{ document_root }}/index.html"
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: '0644'
- name: Start and enable Nginx
service:
name: nginx
state: started
enabled: yes
- name: Configure firewall for HTTP
ufw:
rule: allow
port: "{{ nginx_port }}"
proto: tcp
comment: "Allow HTTP traffic"
when: ansible_distribution == "Ubuntu"
- name: Verify Nginx is responding
uri:
url: "http://{{ ansible_default_ipv4.address }}"
method: GET
status_code: 200
register: nginx_response
retries: 3
delay: 5
- name: Display response
debug:
msg: "Nginx is responding: {{ nginx_response.status }}"
handlers:
- name: start nginx
service:
name: nginx
state: started
- name: restart nginx
service:
name: nginx
state: restarted
- name: reload nginx
service:
name: nginx
state: reloaded
|
3. Template Files
templates/nginx.conf.j2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| user {{ nginx_user }};
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss;
include /etc/nginx/sites-enabled/*;
}
|
templates/site.conf.j2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| server {
listen {{ nginx_port }};
server_name {{ site_name }} www.{{ site_name }};
root {{ document_root }};
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# Logging
access_log /var/log/nginx/{{ site_name }}_access.log;
error_log /var/log/nginx/{{ site_name }}_error.log;
}
|
Running Playbooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # Run playbook
ansible-playbook site.yml
# Run with specific inventory
ansible-playbook -i production.ini site.yml
# Limit to specific hosts
ansible-playbook site.yml --limit webservers
# Run in check mode (dry run)
ansible-playbook site.yml --check
# Run with verbose output
ansible-playbook site.yml -v
# Start at specific task
ansible-playbook site.yml --start-at-task="Install Nginx"
# Use tags
ansible-playbook site.yml --tags="nginx,firewall"
# Skip tags
ansible-playbook site.yml --skip-tags="firewall"
# Override variables
ansible-playbook site.yml -e "nginx_port=8080"
# Syntax check
ansible-playbook site.yml --syntax-check
# List tasks
ansible-playbook site.yml --list-tasks
# List hosts
ansible-playbook site.yml --list-hosts
|
Multi-Play Playbook Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
| ---
# Play 1: Configure all servers
- name: Basic server configuration
hosts: all
become: yes
tasks:
- name: Update package cache
apt:
update_cache: yes
when: ansible_os_family == "Debian"
- name: Install basic packages
package:
name:
- curl
- wget
- git
- vim
state: present
- name: Configure timezone
timezone:
name: America/New_York
- name: Create admin user
user:
name: admin
groups: sudo
shell: /bin/bash
create_home: yes
# Play 2: Configure web servers
- name: Configure web servers
hosts: webservers
become: yes
tasks:
- name: Install Nginx
apt:
name: nginx
state: present
notify: start nginx
- name: Configure Nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
handlers:
- name: start nginx
service:
name: nginx
state: started
- name: restart nginx
service:
name: nginx
state: restarted
# Play 3: Configure database servers
- name: Configure database servers
hosts: databases
become: yes
vars:
mysql_root_password: "{{ vault_mysql_root_password }}"
tasks:
- name: Install MySQL
apt:
name:
- mysql-server
- python3-pymysql
state: present
- name: Start MySQL service
service:
name: mysql
state: started
enabled: yes
- name: Set MySQL root password
mysql_user:
name: root
password: "{{ mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
state: present
|
Playbook Best Practices
- Use descriptive names for plays and tasks
- Include comments for complex logic
- Use variables instead of hard-coding values
- Implement proper error handling
- Use handlers for service restarts
- Include validation where possible
- Use check mode for testing
Advanced Playbook Features
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
| ---
- name: Advanced playbook features
hosts: webservers
become: yes
vars:
packages_to_install:
- name: nginx
state: present
- name: git
state: present
- name: curl
state: present
pre_tasks:
- name: Verify connectivity
ping:
tasks:
- name: Install packages
package:
name: "{{ item.name }}"
state: "{{ item.state }}"
loop: "{{ packages_to_install }}"
register: package_results
- name: Display installation results
debug:
msg: "Package {{ item.item.name }} installation: {{ 'SUCCESS' if item.changed else 'ALREADY INSTALLED' }}"
loop: "{{ package_results.results }}"
when: package_results is defined
- name: Configure services
service:
name: "{{ item }}"
state: started
enabled: yes
loop:
- nginx
failed_when: false
register: service_results
- name: Check service status
command: systemctl is-active {{ item }}
register: service_status
loop:
- nginx
changed_when: false
failed_when: service_status.stdout != "active"
post_tasks:
- name: Verify web server is responding
uri:
url: "http://{{ inventory_hostname }}"
status_code: 200
delegate_to: localhost
run_once: true
rescue:
- name: Service recovery
service:
name: nginx
state: restarted
always:
- name: Clean temporary files
file:
path: /tmp/ansible-*
state: absent
|
Ansible Modules with Examples
Ansible modules are reusable, standalone scripts that provide specific functionality. There are over 3000 modules available, covering everything from system administration to cloud management.
Package Management Modules
APT Module (Debian/Ubuntu)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| ---
- name: APT package management examples
hosts: debian_hosts
become: yes
tasks:
# Install single package
- name: Install nginx
apt:
name: nginx
state: present
update_cache: yes
# Install specific version
- name: Install specific nginx version
apt:
name: nginx=1.18.0-6ubuntu14
state: present
# Install multiple packages
- name: Install multiple packages
apt:
name:
- nginx
- git
- curl
- wget
state: present
# Update package cache (equivalent to apt update)
- name: Update package cache
apt:
update_cache: yes
cache_valid_time: 3600 # Only update if cache is older than 1 hour
# Upgrade all packages
- name: Upgrade all packages
apt:
upgrade: dist
update_cache: yes
# Install from .deb file
- name: Install package from deb file
apt:
deb: /tmp/package.deb
state: present
# Remove package
- name: Remove apache2
apt:
name: apache2
state: absent
# Remove package and configuration files
- name: Purge apache2
apt:
name: apache2
state: absent
purge: yes
# Install package and mark as hold
- name: Hold package version
apt:
name: mysql-server
state: present
update_cache: yes
register: mysql_install
- name: Mark MySQL as held
dpkg_selections:
name: mysql-server
selection: hold
when: mysql_install.changed
|
YUM Module (RHEL/CentOS)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| ---
- name: YUM package management examples
hosts: redhat_hosts
become: yes
tasks:
# Install single package
- name: Install httpd
yum:
name: httpd
state: present
# Install with specific version
- name: Install specific version
yum:
name: nginx-1.20.1
state: present
# Install multiple packages
- name: Install development tools
yum:
name:
- gcc
- gcc-c++
- make
- git
state: present
# Install from URL
- name: Install RPM from URL
yum:
name: https://example.com/package.rpm
state: present
# Install with enablerepo
- name: Install from EPEL
yum:
name: htop
state: present
enablerepo: epel
# Update specific package
- name: Update kernel
yum:
name: kernel
state: latest
# Update all packages
- name: Update all packages
yum:
name: "*"
state: latest
# Install group
- name: Install Development Tools group
yum:
name: "@Development Tools"
state: present
# Remove package
- name: Remove package
yum:
name: telnet
state: absent
|
DNF Module (Fedora/RHEL 8+)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| ---
- name: DNF package management examples
hosts: fedora_hosts
become: yes
tasks:
# Install package
- name: Install package with DNF
dnf:
name: nodejs
state: present
# Install with module stream
- name: Install specific module stream
dnf:
name: "@nodejs:14/development"
state: present
# Install from Flatpak
- name: Install from Flatpak repository
dnf:
name: flatpak
state: present
# Enable repository
- name: Enable repository
dnf:
name: docker-ce
state: present
enablerepo: docker-ce-stable
|
Package Module (OS-agnostic)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| ---
- name: Cross-platform package management
hosts: all
become: yes
tasks:
# Install package using appropriate package manager
- name: Install git
package:
name: git
state: present
# Install with loop
- name: Install common packages
package:
name: "{{ item }}"
state: present
loop:
- curl
- wget
- unzip
- vim
# Conditional installation based on OS
- name: Install web server
package:
name: "{{ web_server_package }}"
state: present
vars:
web_server_package: "{{ 'nginx' if ansible_os_family == 'Debian' else 'httpd' }}"
|
File & Directory Modules
File Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| ---
- name: File module examples
hosts: all
become: yes
tasks:
# Create directory
- name: Create application directory
file:
path: /opt/myapp
state: directory
mode: '0755'
owner: root
group: root
# Create nested directories
- name: Create nested directories
file:
path: /opt/myapp/{{ item }}
state: directory
mode: '0755'
loop:
- config
- logs
- data
- backups
# Create empty file
- name: Create empty file
file:
path: /opt/myapp/config/app.conf
state: touch
mode: '0644'
owner: appuser
group: appgroup
# Change permissions
- name: Make script executable
file:
path: /opt/myapp/scripts/deploy.sh
mode: '0755'
# Create symbolic link
- name: Create symlink
file:
src: /opt/myapp/releases/current
dest: /opt/myapp/current
state: link
# Remove file
- name: Remove old config
file:
path: /opt/myapp/old_config.conf
state: absent
# Set extended attributes
- name: Set file attributes
file:
path: /opt/myapp/secure_file
attributes: +i # immutable
state: file
|
Copy Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| ---
- name: Copy module examples
hosts: all
become: yes
tasks:
# Copy file from control node
- name: Copy configuration file
copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
notify: restart nginx
# Copy with content
- name: Create file with content
copy:
content: |
# This is a configuration file
server_name = {{ ansible_hostname }}
listen_port = 8080
dest: /opt/myapp/config.conf
mode: '0644'
# Copy directory
- name: Copy entire directory
copy:
src: files/webapp/
dest: /var/www/html/
owner: www-data
group: www-data
mode: '0644'
directory_mode: '0755'
# Copy with variable substitution in filename
- name: Copy host-specific file
copy:
src: "files/{{ ansible_hostname }}.conf"
dest: /etc/myapp/host.conf
mode: '0644'
# Copy binary file
- name: Copy binary file
copy:
src: files/app-binary
dest: /usr/local/bin/myapp
mode: '0755'
owner: root
group: root
|
Template Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| ---
- name: Template module examples
hosts: webservers
become: yes
vars:
nginx_worker_processes: 4
nginx_worker_connections: 1024
server_names:
- example.com
- www.example.com
tasks:
# Basic template
- name: Generate nginx config from template
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
validate: 'nginx -t -c %s'
notify: reload nginx
# Template with variables
- name: Generate virtual host config
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ item }}"
mode: '0644'
loop: "{{ server_names }}"
notify: reload nginx
# Template for script
- name: Generate backup script
template:
src: backup.sh.j2
dest: /opt/scripts/backup.sh
mode: '0755'
owner: backupuser
group: backupuser
|
Example template (nginx.conf.j2):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| user www-data;
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections }};
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
{% for server_name in server_names %}
# Configuration for {{ server_name }}
server {
listen 80;
server_name {{ server_name }};
root /var/www/{{ server_name }};
location / {
try_files $uri $uri/ =404;
}
}
{% endfor %}
}
|
Fetch Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| ---
- name: Fetch module examples
hosts: all
tasks:
# Fetch log files
- name: Fetch application logs
fetch:
src: /var/log/myapp/app.log
dest: ./logs/{{ inventory_hostname }}/
flat: no
# Fetch with different local filename
- name: Fetch config with custom name
fetch:
src: /etc/myapp/config.conf
dest: "./backups/{{ inventory_hostname }}-config.conf"
flat: yes
# Fetch only if file exists
- name: Fetch error log if exists
fetch:
src: /var/log/myapp/error.log
dest: "./logs/"
fail_on_missing: no
# Fetch and validate checksum
- name: Fetch with checksum validation
fetch:
src: /opt/myapp/database.db
dest: "./backups/"
validate_checksum: yes
|
User & Access Management
User Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
| ---
- name: User management examples
hosts: all
become: yes
tasks:
# Create basic user
- name: Create application user
user:
name: appuser
comment: "Application User"
shell: /bin/bash
create_home: yes
state: present
# Create user with specific UID
- name: Create user with specific UID
user:
name: dbuser
uid: 1500
group: database
home: /opt/database
shell: /bin/bash
create_home: yes
# Create system user
- name: Create system user
user:
name: serviceuser
system: yes
shell: /bin/false
home: /var/lib/service
create_home: no
# Add user to groups
- name: Add user to multiple groups
user:
name: developer
groups:
- sudo
- docker
- www-data
append: yes
# Set user password (encrypted)
- name: Set user password
user:
name: testuser
password: "{{ 'mypassword' | password_hash('sha512') }}"
state: present
# Create user with SSH key
- name: Create user with SSH key
user:
name: devuser
generate_ssh_key: yes
ssh_key_bits: 2048
ssh_key_file: .ssh/id_rsa
# Remove user
- name: Remove user
user:
name: olduser
state: absent
remove: yes # Also remove home directory
# Lock user account
- name: Lock user account
user:
name: suspendeduser
password_lock: yes
|
Group Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| ---
- name: Group management examples
hosts: all
become: yes
tasks:
# Create group
- name: Create application group
group:
name: appgroup
state: present
# Create group with specific GID
- name: Create group with GID
group:
name: database
gid: 1500
state: present
# Create system group
- name: Create system group
group:
name: servicegroup
system: yes
state: present
|
Authorized Key Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| ---
- name: SSH key management
hosts: all
tasks:
# Add SSH key from file
- name: Add SSH public key
authorized_key:
user: "{{ ansible_user }}"
state: present
key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
comment: "Added by Ansible"
# Add SSH key with options
- name: Add SSH key with restrictions
authorized_key:
user: backupuser
key: "ssh-rsa AAAAB3NzaC1yc2EAAA..."
key_options: 'from="192.168.1.0/24",command="/opt/scripts/backup.sh"'
state: present
# Add multiple SSH keys
- name: Add multiple SSH keys
authorized_key:
user: "{{ item.user }}"
key: "{{ item.key }}"
state: present
loop:
- { user: "dev1", key: "ssh-rsa AAAAB3NzaC1yc2EAAA... dev1@laptop" }
- { user: "dev2", key: "ssh-rsa AAAAB3NzaC1yc2EAAA... dev2@laptop" }
# Remove SSH key
- name: Remove SSH key
authorized_key:
user: olddev
key: "ssh-rsa AAAAB3NzaC1yc2EAAA... olddev@laptop"
state: absent
# Exclusive key management (remove all other keys)
- name: Set exclusive SSH keys
authorized_key:
user: secureuser
key: "{{ item }}"
exclusive: yes
with_items:
- "ssh-rsa AAAAB3NzaC1yc2EAAA... admin@secure"
|
Sudo/Sudoers Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| ---
- name: Sudo management examples
hosts: all
become: yes
tasks:
# Add user to sudoers file
- name: Add user to sudoers
lineinfile:
path: /etc/sudoers
line: 'devuser ALL=(ALL) NOPASSWD:ALL'
validate: 'visudo -cf %s'
backup: yes
# Create sudoers drop-in file
- name: Create sudoers file for developers
copy:
content: |
# Allow developers to restart services
%developers ALL=(root) NOPASSWD: /bin/systemctl restart *
%developers ALL=(root) NOPASSWD: /bin/systemctl start *
%developers ALL=(root) NOPASSWD: /bin/systemctl stop *
dest: /etc/sudoers.d/developers
mode: '0440'
validate: 'visudo -cf %s'
|
Service Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| ---
- name: Service management examples
hosts: all
become: yes
tasks:
# Start service
- name: Start nginx service
service:
name: nginx
state: started
# Start and enable service
- name: Start and enable nginx
service:
name: nginx
state: started
enabled: yes
# Restart service
- name: Restart nginx
service:
name: nginx
state: restarted
# Reload service configuration
- name: Reload nginx configuration
service:
name: nginx
state: reloaded
# Stop and disable service
- name: Stop and disable apache2
service:
name: apache2
state: stopped
enabled: no
# Use systemd module for more control
- name: Manage service with systemd
systemd:
name: myapp
state: started
enabled: yes
daemon_reload: yes
masked: no
# Check if service is running
- name: Check service status
service:
name: nginx
state: started
register: nginx_status
- name: Display service status
debug:
msg: "Nginx is {{ 'running' if nginx_status.state == 'started' else 'not running' }}"
# Conditional service management
- name: Start service only on web servers
service:
name: nginx
state: started
enabled: yes
when: "'webservers' in group_names"
# Multiple services
- name: Manage multiple services
service:
name: "{{ item }}"
state: started
enabled: yes
loop:
- nginx
- mysql
- redis
when: item in ansible_facts.packages
|
Scheduling Tasks (Cron Module)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| ---
- name: Cron management examples
hosts: all
become: yes
tasks:
# Basic cron job
- name: Add daily backup job
cron:
name: "Daily database backup"
minute: "0"
hour: "2"
job: "/opt/scripts/backup.sh"
user: backupuser
# Cron job with specific day
- name: Weekly log rotation
cron:
name: "Weekly log rotation"
minute: "0"
hour: "3"
weekday: "0" # Sunday
job: "/usr/sbin/logrotate /etc/logrotate.conf"
# Monthly cron job
- name: Monthly system update
cron:
name: "Monthly system update"
minute: "0"
hour: "4"
day: "1"
job: "apt update && apt upgrade -y"
# Cron job with environment variables
- name: Backup with environment
cron:
name: "Database backup with env"
minute: "30"
hour: "1"
job: "/opt/scripts/db_backup.sh"
env: yes
user: dbbackup
cron_file: database_backup
# Special time shortcuts
- name: Reboot cron job
cron:
name: "Check disk space on reboot"
special_time: reboot
job: "/opt/scripts/check_disk.sh"
# Remove cron job
- name: Remove old backup job
cron:
name: "Old backup job"
state: absent
# Cron job with random minute
- name: Randomized backup time
cron:
name: "Randomized backup"
minute: "{{ 59 | random }}"
hour: "2"
job: "/opt/scripts/backup.sh"
|
Editing Config Files
Lineinfile Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| ---
- name: Lineinfile examples
hosts: all
become: yes
tasks:
# Add line if it doesn't exist
- name: Add host entry
lineinfile:
path: /etc/hosts
line: '192.168.1.100 app.example.com'
backup: yes
# Replace line using regex
- name: Change SSH port
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port'
line: 'Port 2222'
backup: yes
notify: restart sshd
# Add line after specific pattern
- name: Add include after specific line
lineinfile:
path: /etc/nginx/nginx.conf
insertafter: '^http {'
line: ' include /etc/nginx/custom/*.conf;'
# Add line before specific pattern
- name: Add line before pattern
lineinfile:
path: /etc/fstab
insertbefore: '# Custom mounts'
line: '/dev/sdb1 /opt/data ext4 defaults 0 2'
# Remove line
- name: Remove deprecated option
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^Protocol'
state: absent
notify: restart sshd
# Add line only if file exists
- name: Add line to existing config
lineinfile:
path: /opt/myapp/config.ini
line: 'debug=true'
create: yes
mode: '0644'
# Multiple line modifications
- name: Configure SSH settings
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
backup: yes
loop:
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
notify: restart sshd
|
Blockinfile Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| ---
- name: Blockinfile examples
hosts: all
become: yes
tasks:
# Add block of text
- name: Add custom nginx configuration
blockinfile:
path: /etc/nginx/nginx.conf
block: |
# Custom configuration
client_max_body_size 64m;
client_body_timeout 120s;
client_header_timeout 120s;
marker: "# {mark} ANSIBLE MANAGED BLOCK - Custom settings"
insertafter: "http {"
backup: yes
notify: restart nginx
# Add block with variables
- name: Add application configuration
blockinfile:
path: /opt/myapp/config.ini
block: |
[database]
host = {{ db_host }}
port = {{ db_port }}
name = {{ db_name }}
user = {{ db_user }}
marker: "# {mark} ANSIBLE MANAGED BLOCK - Database config"
create: yes
# Remove block
- name: Remove old configuration block
blockinfile:
path: /etc/hosts
marker: "# {mark} ANSIBLE MANAGED BLOCK - Old entries"
state: absent
# Add block from template
- name: Add configuration from template
blockinfile:
path: /etc/myapp.conf
block: "{{ lookup('template', 'myapp-config.j2') }}"
marker: "# {mark} ANSIBLE MANAGED BLOCK"
|
Replace Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| ---
- name: Replace module examples
hosts: all
become: yes
tasks:
# Replace text in file
- name: Update database connection string
replace:
path: /opt/myapp/config.py
regexp: 'DATABASE_URL = ".*"'
replace: 'DATABASE_URL = "{{ database_url }}"'
backup: yes
# Replace multiline pattern
- name: Update nginx worker configuration
replace:
path: /etc/nginx/nginx.conf
regexp: |
worker_processes\s+\d+;
worker_connections\s+\d+;
replace: |
worker_processes {{ ansible_processor_cores }};
worker_connections {{ nginx_worker_connections }};
backup: yes
# Replace with validation
- name: Update configuration with validation
replace:
path: /etc/apache2/apache2.conf
regexp: 'MaxRequestWorkers\s+\d+'
replace: 'MaxRequestWorkers {{ max_request_workers }}'
validate: 'apache2ctl -t -f %s'
backup: yes
|
Networking Modules
UFW Firewall Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| ---
- name: UFW firewall examples
hosts: all
become: yes
tasks:
# Enable UFW
- name: Enable UFW
ufw:
state: enabled
policy: deny
direction: incoming
# Allow SSH
- name: Allow SSH
ufw:
rule: allow
port: '22'
proto: tcp
comment: 'Allow SSH'
# Allow HTTP and HTTPS
- name: Allow web traffic
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- '80'
- '443'
# Allow from specific IP
- name: Allow from management IP
ufw:
rule: allow
src: '192.168.1.10'
comment: 'Allow from management server'
# Allow specific service
- name: Allow MySQL from app servers
ufw:
rule: allow
port: '3306'
proto: tcp
src: '{{ item }}'
comment: 'MySQL from app servers'
loop: "{{ groups['webservers'] }}"
# Deny specific port
- name: Deny telnet
ufw:
rule: deny
port: '23'
proto: tcp
# Rate limiting
- name: Rate limit SSH
ufw:
rule: limit
port: '22'
proto: tcp
comment: 'Rate limit SSH connections'
|
Iptables Module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| ---
- name: Iptables examples
hosts: all
become: yes
tasks:
# Allow SSH
- name: Allow SSH connections
iptables:
chain: INPUT
protocol: tcp
destination_port: 22
ctstate: NEW,ESTABLISHED
jump: ACCEPT
comment: Allow SSH
# Allow web traffic
- name: Allow HTTP traffic
iptables:
chain: INPUT
protocol: tcp
destination_port: 80
jump: ACCEPT
# Allow related and established connections
- name: Allow established connections
iptables:
chain: INPUT
ctstate: ESTABLISHED,RELATED
jump: ACCEPT
# Drop all other traffic
- name: Drop all other INPUT traffic
iptables:
chain: INPUT
policy: DROP
# NAT rule
- name: NAT outgoing traffic
iptables:
table: nat
chain: POSTROUTING
out_interface: eth0
jump: MASQUERADE
# Save iptables rules
- name: Save iptables rules
shell: iptables-save > /etc/iptables/rules.v4
|
Managing /etc/hosts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| ---
- name: Manage /etc/hosts entries
hosts: all
become: yes
tasks:
# Add single host entry
- name: Add application server entry
lineinfile:
path: /etc/hosts
line: '192.168.1.100 app.example.com app'
backup: yes
# Add multiple host entries
- name: Add multiple host entries
lineinfile:
path: /etc/hosts
line: "{{ item.ip }} {{ item.hostname }}"
backup: yes
loop:
- { ip: '192.168.1.101', hostname: 'db1.example.com db1' }
- { ip: '192.168.1.102', hostname: 'db2.example.com db2' }
- { ip: '192.168.1.103', hostname: 'cache.example.com cache' }
# Add hosts block
- name: Add infrastructure hosts
blockinfile:
path: /etc/hosts
block: |
# Infrastructure servers
192.168.1.10 lb1.example.com lb1
192.168.1.11 lb2.example.com lb2
192.168.1.20 web1.example.com web1
192.168.1.21 web2.example.com web2
marker: "# {mark} ANSIBLE MANAGED BLOCK - Infrastructure"
backup: yes
|
Database Modules
MySQL Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
| ---
- name: MySQL database management
hosts: databases
become: yes
vars:
mysql_root_password: "{{ vault_mysql_root_password }}"
app_databases:
- name: webapp
encoding: utf8mb4
collation: utf8mb4_unicode_ci
- name: analytics
encoding: utf8mb4
collation: utf8mb4_unicode_ci
tasks:
# Install MySQL
- name: Install MySQL server
apt:
name:
- mysql-server
- python3-pymysql
state: present
update_cache: yes
# Start MySQL service
- name: Start and enable MySQL
service:
name: mysql
state: started
enabled: yes
# Set root password
- name: Set MySQL root password
mysql_user:
name: root
password: "{{ mysql_root_password }}"
login_unix_socket: /var/run/mysqld/mysqld.sock
state: present
# Create .my.cnf for root
- name: Create MySQL root config
template:
src: root_my.cnf.j2
dest: /root/.my.cnf
owner: root
group: root
mode: '0600'
# Remove anonymous users
- name: Remove anonymous MySQL users
mysql_user:
name: ''
host_all: yes
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
# Remove test database
- name: Remove MySQL test database
mysql_db:
name: test
state: absent
login_user: root
login_password: "{{ mysql_root_password }}"
# Create application databases
- name: Create application databases
mysql_db:
name: "{{ item.name }}"
encoding: "{{ item.encoding }}"
collation: "{{ item.collation }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
loop: "{{ app_databases }}"
# Create application users
- name: Create MySQL application users
mysql_user:
name: "{{ item.user }}"
password: "{{ item.password }}"
priv: "{{ item.database }}.*:ALL"
host: "{{ item.host | default('localhost') }}"
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
loop:
- { user: 'webapp_user', password: '{{ vault_webapp_db_password }}', database: 'webapp', host: '192.168.1.%' }
- { user: 'analytics_user', password: '{{ vault_analytics_db_password }}', database: 'analytics', host: '192.168.1.%' }
# Configure MySQL
- name: Configure MySQL settings
lineinfile:
path: /etc/mysql/mysql.conf.d/mysqld.cnf
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
backup: yes
loop:
- { regexp: '^bind-address', line: 'bind-address = 0.0.0.0' }
- { regexp: '^max_connections', line: 'max_connections = 200' }
- { regexp: '^innodb_buffer_pool_size', line: 'innodb_buffer_pool_size = 1G' }
notify: restart mysql
# Create database backup user
- name: Create backup user
mysql_user:
name: backup_user
password: "{{ vault_backup_db_password }}"
priv: "*.*:SELECT,LOCK TABLES,SHOW VIEW,EVENT,TRIGGER"
host: localhost
state: present
login_user: root
login_password: "{{ mysql_root_password }}"
|
Template for root_my.cnf.j2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| [client]
user=root
password={{ mysql_root_password }}
socket=/var/run/mysqld/mysqld.sock
[mysql]
user=root
password={{ mysql_root_password }}
socket=/var/run/mysqld/mysqld.sock
[mysqldump]
user=root
password={{ mysql_root_password }}
socket=/var/run/mysqld/mysqld.sock
|
PostgreSQL Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
| ---
- name: PostgreSQL database management
hosts: databases
become: yes
become_user: postgres
vars:
postgres_databases:
- name: webapp
owner: webapp_user
- name: analytics
owner: analytics_user
tasks:
# Install PostgreSQL
- name: Install PostgreSQL
apt:
name:
- postgresql
- postgresql-contrib
- python3-psycopg2
state: present
update_cache: yes
become_user: root
# Start PostgreSQL
- name: Start and enable PostgreSQL
service:
name: postgresql
state: started
enabled: yes
become_user: root
# Create application users
- name: Create PostgreSQL users
postgresql_user:
name: "{{ item.user }}"
password: "{{ item.password }}"
role_attr_flags: CREATEDB,NOSUPERUSER
state: present
loop:
- { user: 'webapp_user', password: '{{ vault_webapp_pg_password }}' }
- { user: 'analytics_user', password: '{{ vault_analytics_pg_password }}' }
# Create databases
- name: Create PostgreSQL databases
postgresql_db:
name: "{{ item.name }}"
owner: "{{ item.owner }}"
encoding: UTF8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
state: present
loop: "{{ postgres_databases }}"
# Grant privileges
- name: Grant database privileges
postgresql_privs:
database: "{{ item.database }}"
roles: "{{ item.user }}"
privs: ALL
type: database
state: present
loop:
- { database: 'webapp', user: 'webapp_user' }
- { database: 'analytics', user: 'analytics_user' }
# Configure PostgreSQL
- name: Configure PostgreSQL settings
lineinfile:
path: /etc/postgresql/{{ postgresql_version }}/main/postgresql.conf
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
backup: yes
loop:
- { regexp: "^#listen_addresses", line: "listen_addresses = '*'" }
- { regexp: "^max_connections", line: "max_connections = 200" }
- { regexp: "^shared_buffers", line: "shared_buffers = 256MB" }
notify: restart postgresql
become_user: root
# Configure client authentication
- name: Configure PostgreSQL client authentication
lineinfile:
path: /etc/postgresql/{{ postgresql_version }}/main/pg_hba.conf
line: "host all all 192.168.1.0/24 md5"
backup: yes
notify: restart postgresql
become_user: root
|
Docker & Kubernetes Modules
Docker Installation and Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
| ---
- name: Docker installation and management
hosts: docker_hosts
become: yes
tasks:
# Install Docker prerequisites
- name: Install Docker prerequisites
apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: yes
# Add Docker GPG key
- name: Add Docker GPG key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
keyring: /usr/share/keyrings/docker-archive-keyring.gpg
state: present
# Add Docker repository
- name: Add Docker repository
apt_repository:
repo: "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
state: present
update_cache: yes
# Install Docker
- name: Install Docker CE
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
# Start Docker service
- name: Start and enable Docker
service:
name: docker
state: started
enabled: yes
# Add users to Docker group
- name: Add users to Docker group
user:
name: "{{ item }}"
groups: docker
append: yes
loop:
- "{{ ansible_user }}"
- developer
# Install Docker Compose (standalone)
- name: Install Docker Compose
get_url:
url: "https://github.com/docker/compose/releases/download/{{ docker_compose_version }}/docker-compose-linux-x86_64"
dest: /usr/local/bin/docker-compose
mode: '0755'
vars:
docker_compose_version: "v2.20.0"
# Pull common Docker images
- name: Pull common Docker images
docker_image:
name: "{{ item }}"
source: pull
state: present
loop:
- nginx:latest
- redis:alpine
- postgres:15-alpine
- node:18-alpine
# Run Redis container
- name: Run Redis container
docker_container:
name: redis
image: redis:alpine
state: started
restart_policy: unless-stopped
ports:
- "6379:6379"
volumes:
- redis-data:/data
# Run PostgreSQL container
- name: Run PostgreSQL container
docker_container:
name: postgres
image: postgres:15-alpine
state: started
restart_policy: unless-stopped
env:
POSTGRES_DB: "{{ postgres_db }}"
POSTGRES_USER: "{{ postgres_user }}"
POSTGRES_PASSWORD: "{{ postgres_password }}"
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
# Create Docker network
- name: Create application network
docker_network:
name: app-network
driver: bridge
state: present
# Deploy application stack
- name: Deploy application with Docker Compose
docker_compose:
project_src: /opt/myapp
definition:
version: '3.8'
services:
web:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./html:/usr/share/nginx/html
networks:
- app-network
api:
image: node:18-alpine
command: npm start
working_dir: /app
volumes:
- ./api:/app
environment:
NODE_ENV: production
DATABASE_URL: "{{ database_url }}"
networks:
- app-network
networks:
app-network:
external: true
state: present
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
| ---
- name: Install Kubernetes tools
hosts: k8s_nodes
become: yes
tasks:
# Add Kubernetes GPG key
- name: Add Kubernetes GPG key
apt_key:
url: https://packages.cloud.google.com/apt/doc/apt-key.gpg
keyring: /usr/share/keyrings/kubernetes-archive-keyring.gpg
state: present
# Add Kubernetes repository
- name: Add Kubernetes repository
apt_repository:
repo: "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main"
state: present
update_cache: yes
# Install Kubernetes tools
- name: Install kubectl, kubeadm, kubelet
apt:
name:
- kubectl
- kubeadm
- kubelet
state: present
update_cache: yes
# Hold Kubernetes packages
- name: Hold Kubernetes packages
dpkg_selections:
name: ""
selection: hold
loop:
- kubectl
- kubeadm
- kubelet
# Configure kubelet
- name: Configure kubelet
lineinfile:
path: /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
regexp: '^Environment="KUBELET_CONFIG_ARGS'
line: 'Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml --cgroup-driver=systemd"'
notify: restart kubelet
# Enable kubelet
- name: Enable kubelet service
service:
name: kubelet
enabled: yes
# Install Helm
- name: Download Helm installer
get_url:
url: https://get.helm.sh/helm-v3.12.0-linux-amd64.tar.gz
dest: /tmp/helm.tar.gz
mode: '0644'
- name: Extract Helm
unarchive:
src: /tmp/helm.tar.gz
dest: /tmp
remote_src: yes
creates: /tmp/linux-amd64/helm
- name: Install Helm binary
copy:
src: /tmp/linux-amd64/helm
dest: /usr/local/bin/helm
mode: '0755'
remote_src: yes
# Create kubeconfig directory
- name: Create .kube directory
file:
path: /home//.kube
state: directory
owner: " {{ ansible_user }}"
group: " {{ ansible_user }}"
mode: '0755'
|
Variables & Templates
Variables in Ansible allow you to manage differences between systems and make your playbooks more flexible and reusable.
Variable Types and Scope
Defining Variables
1. In Playbooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| ---
- name: Variable examples in playbooks
hosts: webservers
vars:
# Simple variables
http_port: 80
https_port: 443
server_name: example.com
# List variables
packages_to_install:
- nginx
- git
- curl
- vim
# Dictionary variables
database_config:
host: localhost
port: 3306
name: webapp
user: webapp_user
# Complex nested variables
applications:
web:
name: nginx
port: 80
ssl: true
workers: 4
api:
name: gunicorn
port: 8000
workers: 2
timeout: 30
# Environment-specific variables
app_config:
debug: "{{ 'true' if environment == 'development' else 'false' }}"
log_level: "{{ 'DEBUG' if environment == 'development' else 'INFO' }}"
tasks:
- name: Install packages
package:
name: "{{ item }}"
state: present
loop: "{{ packages_to_install }}"
- name: Display database config
debug:
msg: "Database: {{ database_config.host }}:{{ database_config.port }}/{{ database_config.name }}"
- name: Configure applications
debug:
msg: "App {{ item.key }}: {{ item.value.name }} on port {{ item.value.port }}"
loop: "{{ applications | dict2items }}"
|
2. Variable Files
vars/main.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| ---
# Application configuration
app_name: mywebapp
app_version: "1.2.3"
app_port: 8080
# Database configuration
database:
engine: mysql
host: "{{ db_host | default('localhost') }}"
port: 3306
name: "{{ app_name }}_{{ environment }}"
user: "{{ app_name }}_user"
password: "{{ vault_db_password }}"
# Web server configuration
nginx:
worker_processes: "{{ ansible_processor_cores }}"
worker_connections: 1024
client_max_body_size: "64m"
keepalive_timeout: 65
# SSL configuration
ssl:
enabled: true
certificate_path: "/etc/ssl/certs/{{ app_name }}.crt"
private_key_path: "/etc/ssl/private/{{ app_name }}.key"
protocols:
- TLSv1.2
- TLSv1.3
# Monitoring configuration
monitoring:
enabled: true
metrics_port: 9090
health_check_path: "/health"
log_level: "INFO"
# Backup configuration
backup:
enabled: true
schedule: "0 2 * * *" # Daily at 2 AM
retention_days: 7
s3_bucket: "{{ app_name }}-backups-{{ environment }}"
|
Using variable files:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ---
- name: Use external variable files
hosts: webservers
vars_files:
- vars/main.yml
- vars/{{ environment }}.yml
- vars/secrets.yml
tasks:
- name: Display app configuration
debug:
msg: |
Application: {{ app_name }} v{{ app_version }}
Database: {{ database.engine }}://{{ database.host }}:{{ database.port }}/{{ database.name }}
SSL: {{ 'enabled' if ssl.enabled else 'disabled' }}
|
3. Group Variables
group_vars/all.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| ---
# Variables for all hosts
timezone: "America/New_York"
ntp_servers:
- time1.google.com
- time2.google.com
# Common packages
common_packages:
- curl
- wget
- git
- vim
- htop
- unzip
# Security settings
ssh_port: 22
disable_root_login: true
password_authentication: false
# Logging
log_rotation_days: 30
syslog_server: "{{ hostvars[groups['logging'][0]]['ansible_default_ipv4']['address'] }}"
# Backup settings
backup_user: backup
backup_group: backup
backup_retention: 7
|
group_vars/webservers.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| ---
# Web server specific variables
web_server: nginx
document_root: /var/www/html
virtual_hosts:
- name: example.com
port: 80
ssl: false
- name: secure.example.com
port: 443
ssl: true
# Performance tuning
worker_processes: "{{ ansible_processor_cores }}"
worker_connections: 1024
client_max_body_size: 64m
# Security headers
security_headers:
x_frame_options: "DENY"
x_content_type_options: "nosniff"
x_xss_protection: "1; mode=block"
strict_transport_security: "max-age=31536000; includeSubDomains"
|
group_vars/databases.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| ---
# Database server specific variables
database_engine: mysql
mysql_root_password: "{{ vault_mysql_root_password }}"
mysql_port: 3306
mysql_bind_address: "0.0.0.0"
# Performance settings
mysql_max_connections: 200
mysql_innodb_buffer_pool_size: "{{ (ansible_memtotal_mb * 0.7) | int }}M"
mysql_query_cache_size: "32M"
# Backup settings
mysql_backup_user: backup
mysql_backup_password: "{{ vault_mysql_backup_password }}"
mysql_backup_schedule: "0 2 * * *"
# Replication settings (for master-slave setup)
mysql_server_id: "{{ groups['databases'].index(inventory_hostname) + 1 }}"
mysql_replication_user: replication
mysql_replication_password: "{{ vault_mysql_replication_password }}"
|
4. Host Variables
host_vars/web1.example.com.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| ---
# Host-specific variables
server_role: primary_web
nginx_worker_processes: 4
ssl_certificate: /etc/ssl/certs/web1.crt
ssl_private_key: /etc/ssl/private/web1.key
# Storage configuration
data_disk: /dev/sdb1
data_mount: /opt/data
data_filesystem: ext4
# Network configuration
primary_interface: eth0
secondary_interface: eth1
vip_address: 192.168.1.100
# Application-specific settings
app_instances: 3
memory_limit: "2g"
cpu_limit: "1.5"
# Monitoring settings
custom_metrics:
- disk_usage
- memory_usage
- response_time
|
Jinja2 Templating
Jinja2 is the templating engine used by Ansible. It allows you to create dynamic content.
Basic Template Syntax
templates/nginx.conf.j2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
| # Nginx configuration generated by Ansible
user {{ nginx_user | default('www-data') }};
worker_processes {{ nginx_worker_processes | default('auto') }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
use epoll;
multi_accept on;
}
http {
# Basic Settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout {{ nginx_keepalive_timeout | default(65) }};
types_hash_max_size 2048;
server_tokens {{ 'off' if nginx_server_tokens == false else 'on' }};
# MIME Types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging Format
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Access and Error Logs
access_log {{ nginx_access_log | default('/var/log/nginx/access.log') }} main;
error_log {{ nginx_error_log | default('/var/log/nginx/error.log') }} {{ nginx_error_log_level | default('error') }};
# Gzip Compression
{% if nginx_gzip_enabled | default(true) %}
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level {{ nginx_gzip_comp_level | default(6) }};
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
{% endif %}
# Client Settings
client_max_body_size {{ nginx_client_max_body_size | default('64m') }};
client_body_timeout {{ nginx_client_body_timeout | default('60s') }};
client_header_timeout {{ nginx_client_header_timeout | default('60s') }};
# Rate Limiting
{% if nginx_rate_limiting is defined %}
{% for zone, config in nginx_rate_limiting.items() %}
limit_req_zone {{ config.key }} zone={{ zone }}:{{ config.size }} rate={{ config.rate }};
{% endfor %}
{% endif %}
# Upstream Servers
{% if upstream_servers is defined %}
{% for upstream_name, servers in upstream_servers.items() %}
upstream {{ upstream_name }} {
{% for server in servers %}
server {{ server.host }}:{{ server.port }}{% if server.weight is defined %} weight={{ server.weight }}{% endif %}{% if server.max_fails is defined %} max_fails={{ server.max_fails }}{% endif %}{% if server.fail_timeout is defined %} fail_timeout={{ server.fail_timeout }}{% endif %};
{% endfor %}
}
{% endfor %}
{% endif %}
# Virtual Hosts
{% for vhost in virtual_hosts | default([]) %}
server {
listen {{ vhost.port | default(80) }}{% if vhost.ssl | default(false) %} ssl http2{% endif %};
server_name {{ vhost.name }}{% if vhost.aliases is defined %} {{ vhost.aliases | join(' ') }}{% endif %};
{% if vhost.ssl | default(false) %}
# SSL Configuration
ssl_certificate {{ vhost.ssl_certificate | default('/etc/ssl/certs/' + vhost.name + '.crt') }};
ssl_certificate_key {{ vhost.ssl_private_key | default('/etc/ssl/private/' + vhost.name + '.key') }};
ssl_protocols {{ ssl_protocols | default(['TLSv1.2', 'TLSv1.3']) | join(' ') }};
ssl_ciphers {{ ssl_ciphers | default('ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384') }};
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
{% endif %}
# Document Root
root {{ vhost.document_root | default(document_root + '/' + vhost.name) }};
index {{ vhost.index | default('index.html index.htm') }};
# Security Headers
{% if security_headers is defined %}
{% for header, value in security_headers.items() %}
add_header {{ header | replace('_', '-') | title }} "{{ value }}" always;
{% endfor %}
{% endif %}
# Locations
{% if vhost.locations is defined %}
{% for location in vhost.locations %}
location {{ location.path }} {
{% if location.proxy_pass is defined %}
proxy_pass {{ location.proxy_pass }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
{% endif %}
{% if location.try_files is defined %}
try_files {{ location.try_files }};
{% endif %}
{% if location.expires is defined %}
expires {{ location.expires }};
{% endif %}
{% if location.custom is defined %}
{{ location.custom }}
{% endif %}
}
{% endfor %}
{% else %}
location / {
try_files $uri $uri/ =404;
}
{% endif %}
# Logging
access_log {{ nginx_log_dir | default('/var/log/nginx') }}/{{ vhost.name }}_access.log main;
error_log {{ nginx_log_dir | default('/var/log/nginx') }}/{{ vhost.name }}_error.log {{ nginx_error_log_level | default('error') }};
}
{% endfor %}
# Include additional configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
|
Advanced Template Features
templates/application.conf.j2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
| # Application Configuration
# Generated on {{ ansible_date_time.iso8601 }} by Ansible
[application]
name = {{ app_name }}
version = {{ app_version }}
environment = {{ environment }}
debug = {{ app_debug | default(false) | lower }}
# Server Configuration
[server]
host = {{ app_host | default('0.0.0.0') }}
port = {{ app_port | default(8080) }}
workers = {{ app_workers | default(ansible_processor_cores) }}
threads = {{ app_threads | default(2) }}
timeout = {{ app_timeout | default(30) }}
# Database Configuration
[database]
{% if database is defined %}
driver = {{ database.driver | default('mysql') }}
host = {{ database.host }}
port = {{ database.port }}
name = {{ database.name }}
user = {{ database.user }}
# Password stored in vault
connection_pool_size = {{ database.pool_size | default(10) }}
connection_timeout = {{ database.timeout | default(30) }}
{% endif %}
# Cache Configuration
[cache]
{% if redis is defined %}
enabled = true
type = redis
host = {{ redis.host }}
port = {{ redis.port }}
db = {{ redis.db | default(0) }}
timeout = {{ redis.timeout | default(5) }}
{% else %}
enabled = false
type = memory
{% endif %}
# Logging Configuration
[logging]
level = {{ log_level | default('INFO') }}
format = {{ log_format | default('%(asctime)s - %(name)s - %(levelname)s - %(message)s') }}
{% if log_files is defined %}
{% for log_type, config in log_files.items() %}
{{ log_type }}_file = {{ config.path }}
{{ log_type }}_max_size = {{ config.max_size | default('10MB') }}
{{ log_type }}_backup_count = {{ config.backup_count | default(5) }}
{% endfor %}
{% endif %}
# Feature Flags
[features]
{% for feature, enabled in feature_flags.items() %}
{{ feature }} = {{ enabled | lower }}
{% endfor %}
# External Services
[services]
{% if external_services is defined %}
{% for service_name, service_config in external_services.items() %}
[services.{{ service_name }}]
url = {{ service_config.url }}
timeout = {{ service_config.timeout | default(30) }}
retries = {{ service_config.retries | default(3) }}
{% if service_config.auth is defined %}
auth_type = {{ service_config.auth.type }}
{% if service_config.auth.type == 'api_key' %}
api_key = {{ service_config.auth.key }}
{% elif service_config.auth.type == 'oauth' %}
client_id = {{ service_config.auth.client_id }}
# client_secret stored in vault
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
# Environment-specific settings
{% if environment == 'production' %}
[production]
ssl_required = true
session_timeout = 3600
max_request_size = 10MB
rate_limiting = true
{% elif environment == 'development' %}
[development]
auto_reload = true
debug_toolbar = true
profiler = true
{% endif %}
# Conditional blocks based on host groups
{% if 'webservers' in group_names %}
[webserver]
static_files_path = {{ static_files_path | default('/var/www/static') }}
media_files_path = {{ media_files_path | default('/var/www/media') }}
{% endif %}
{% if 'databases' in group_names %}
[database_server]
backup_enabled = true
backup_schedule = {{ backup_schedule | default('0 2 * * *') }}
replication_enabled = {{ replication_enabled | default(false) | lower }}
{% endif %}
# Custom configuration sections
{% if custom_config is defined %}
{% for section_name, section_config in custom_config.items() %}
[{{ section_name }}]
{% for key, value in section_config.items() %}
{{ key }} = {% if value is string %}{{ value }}{% elif value is boolean %}{{ value | lower }}{% else %}{{ value }}{% endif %}
{% endfor %}
{% endfor %}
{% endif %}
|
Template Filters and Tests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
| ---
- name: Template filters and tests examples
hosts: all
vars:
app_name: "MyWebApp"
version: "1.2.3-beta"
servers:
- web1.example.com
- web2.example.com
- web3.example.com
database_config:
host: db.example.com
port: 3306
options: null
numbers: [1, 2, 3, 4, 5]
tasks:
# String filters
- name: String manipulation
debug:
msg: |
Original: {{ app_name }}
Lowercase: {{ app_name | lower }}
Uppercase: {{ app_name | upper }}
Title: {{ app_name | title }}
Length: {{ app_name | length }}
Replace: {{ app_name | replace('Web', 'Mobile') }}
Trim: {{ ' ' + app_name + ' ' | trim }}
# Default filter
- name: Default values
debug:
msg: |
Database options: {{ database_config.options | default('default_options') }}
Port: {{ database_config.custom_port | default(database_config.port) }}
# List filters
- name: List operations
debug:
msg: |
First server: {{ servers | first }}
Last server: {{ servers | last }}
Random server: {{ servers | random }}
Join servers: {{ servers | join(', ') }}
Sort servers: {{ servers | sort }}
Unique items: {{ (servers + servers) | unique }}
# Math filters
- name: Math operations
debug:
msg: |
Sum: {{ numbers | sum }}
Min: {{ numbers | min }}
Max: {{ numbers | max }}
Average: {{ numbers | sum / numbers | length }}
# JSON and YAML
- name: Format conversion
debug:
msg: |
As JSON: {{ database_config | to_json }}
As YAML: {{ database_config | to_yaml }}
From JSON: {{ '{"test": "value"}' | from_json }}
# Regular expressions
- name: Regex operations
debug:
msg: |
Search: {{ version | regex_search('(\d+\.\d+)') }}
Replace: {{ version | regex_replace('-beta', '-release') }}
Find all: {{ version | regex_findall('\d+') }}
# Type tests
- name: Type testing
debug:
msg: |
Is string: {{ app_name is string }}
Is number: {{ database_config.port is number }}
Is defined: {{ database_config.host is defined }}
Is none: {{ database_config.options is none }}
# File operations
- name: File filters
debug:
msg: |
Basename: {{ '/path/to/file.txt' | basename }}
Dirname: {{ '/path/to/file.txt' | dirname }}
Extension: {{ 'file.txt' | splitext | last }}
# Password hashing
- name: Password operations
debug:
msg: |
SHA512: {{ 'mypassword' | password_hash('sha512') }}
MD5: {{ 'mypassword' | password_hash('md5') }}
|
Variables in Practice
Complete Application Deployment Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
| ---
- name: Deploy web application with variables
hosts: webservers
become: yes
vars:
app_name: mywebapp
app_version: "{{ lookup('env', 'APP_VERSION') | default('latest') }}"
environment: "{{ group_names | intersect(['production', 'staging', 'development']) | first }}"
# Environment-specific configurations
config_by_env:
production:
debug: false
log_level: ERROR
workers: 4
database_pool_size: 20
staging:
debug: true
log_level: INFO
workers: 2
database_pool_size: 10
development:
debug: true
log_level: DEBUG
workers: 1
database_pool_size: 5
# Current environment config
current_config: "{{ config_by_env[environment] }}"
# Application paths
app_root: "/opt/{{ app_name }}"
app_user: "{{ app_name }}"
app_group: "{{ app_name }}"
# Service configuration
service_config:
name: "{{ app_name }}"
description: "{{ app_name | title }} Web Application"
exec_start: "{{ app_root }}/bin/start.sh"
user: "{{ app_user }}"
group: "{{ app_group }}"
restart: "on-failure"
tasks:
- name: Create application user
user:
name: "{{ app_user }}"
group: "{{ app_group }}"
system: yes
home: "{{ app_root }}"
shell: /bin/bash
create_home: no
- name: Create application directories
file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: '0755'
loop:
- "{{ app_root }}"
- "{{ app_root }}/config"
- "{{ app_root }}/logs"
- "{{ app_root }}/tmp"
- "{{ app_root }}/bin"
- name: Generate application configuration
template:
src: app_config.j2
dest: "{{ app_root }}/config/app.conf"
owner: "{{ app_user }}"
group: "{{ app_group }}"
mode: '0640'
notify: restart application
- name: Generate systemd service file
template:
src: systemd_service.j2
dest: "/etc/systemd/system/{{ service_config.name }}.service"
mode: '0644'
notify:
- reload systemd
- restart application
- name: Start and enable application service
service:
name: "{{ service_config.name }}"
state: started
enabled: yes
handlers:
- name: reload systemd
systemd:
daemon_reload: yes
- name: restart application
service:
name: "{{ service_config.name }}"
state: restarted
|
templates/systemd_service.j2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| [Unit]
Description={{ service_config.description }}
After=network.target
{% if database is defined %}
After=mysql.service
{% endif %}
[Service]
Type=simple
User={{ service_config.user }}
Group={{ service_config.group }}
ExecStart={{ service_config.exec_start }}
Restart={{ service_config.restart }}
RestartSec=5
Environment=APP_ENV={{ environment }}
Environment=APP_CONFIG={{ app_root }}/config/app.conf
WorkingDirectory={{ app_root }}
# Resource limits
LimitNOFILE=65536
{% if current_config.memory_limit is defined %}
MemoryLimit={{ current_config.memory_limit }}
{% endif %}
# Security settings
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths={{ app_root }}
{% if app_user != 'root' %}
User={{ app_user }}
Group={{ app_group }}
{% endif %}
[Install]
WantedBy=multi-user.target
|
This comprehensive variables and templates section demonstrates how to create flexible, maintainable Ansible playbooks that can adapt to different environments and configurations while maintaining consistency and reusability.
Conditionals & Loops
Using when
Conditions
Conditionals in Ansible allow you to execute tasks based on specific criteria. The when
statement is one of the most powerful features for creating intelligent playbooks.
Basic when
Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| ---
- name: Conditional execution example
hosts: all
tasks:
- name: Install Apache on Ubuntu systems
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
- name: Install Apache on RedHat systems
yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
- name: Start service only if it's not running
service:
name: apache2
state: started
when: ansible_service_mgr == "systemd"
- name: Configure firewall for production
firewalld:
service: http
permanent: yes
state: enabled
when: environment == "production"
|
Complex Conditions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| tasks:
- name: Install development tools
package:
name: ""
state: present
loop:
- git
- vim
- curl
when:
- ansible_os_family == "RedHat"
- ansible_distribution_major_version|int >= 7
- install_dev_tools|default(false)|bool
- name: Restart service if config changed
service:
name: nginx
state: restarted
when: config_file.changed and not maintenance_mode|default(false)
|
Using loop
for Repetitive Tasks
Loops eliminate repetitive task definitions and make playbooks more maintainable.
Simple Loop Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| ---
- name: Loop examples
hosts: all
vars:
packages:
- git
- curl
- wget
- vim
users:
- { name: "john", shell: "/bin/bash", groups: "wheel" }
- { name: "jane", shell: "/bin/zsh", groups: "users" }
- { name: "bob", shell: "/bin/bash", groups: "developers" }
tasks:
- name: Install packages using simple loop
package:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
- name: Create users with complex data
user:
name: "{{ item.name }}"
shell: "{{ item.shell }}"
groups: "{{ item.groups }}"
state: present
loop: "{{ users }}"
- name: Create directories with different permissions
file:
path: "{{ item.path }}"
state: directory
mode: "{{ item.mode }}"
owner: "{{ item.owner }}"
loop:
- { path: "/opt/app", mode: "0755", owner: "root" }
- { path: "/opt/logs", mode: "0755", owner: "app" }
- { path: "/opt/config", mode: "0750", owner: "app" }
|
with_items
Example
While loop
is the modern approach, with_items
is still widely used in legacy playbooks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| tasks:
- name: Install multiple packages (legacy syntax)
yum:
name: "{{ item }}"
state: present
with_items:
- httpd
- mysql-server
- php
- name: Copy configuration files
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
with_items:
- { src: "httpd.conf", dest: "/etc/httpd/conf/httpd.conf", mode: "0644" }
- { src: "php.ini", dest: "/etc/php.ini", mode: "0644" }
|
Error Handling & Debugging
Using ignore_errors
Sometimes you need tasks to continue even if they fail. Use ignore_errors
judiciously.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| ---
- name: Error handling examples
hosts: all
tasks:
- name: Try to stop a service (might not exist)
service:
name: old-service
state: stopped
ignore_errors: yes
- name: Attempt to remove old config
file:
path: /etc/old-config.conf
state: absent
ignore_errors: yes
register: cleanup_result
- name: Report cleanup status
debug:
msg: "Cleanup {{ 'successful' if cleanup_result.failed == false else 'failed' }}"
|
Using register
Variables & Debug Output
The register
directive captures task output for use in subsequent tasks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| ---
- name: Register and debug example
hosts: all
tasks:
- name: Check if Apache is running
command: systemctl is-active httpd
register: apache_status
ignore_errors: yes
- name: Debug Apache status
debug:
msg: "Apache status: {{ apache_status.stdout }}"
verbosity: 1
- name: Get system information
setup:
filter: ansible_distribution*
register: sys_info
- name: Display filtered system info
debug:
var: sys_info.ansible_facts.ansible_distribution
- name: Check disk space
shell: df -h /
register: disk_space
changed_when: false
- name: Alert if disk space is low
debug:
msg: "WARNING: Low disk space detected!"
when: "'9[0-9]%' in disk_space.stdout"
- name: Create backup directory
file:
path: /backup
state: directory
register: backup_dir
- name: Show directory creation result
debug:
msg: "Backup directory {{ 'created' if backup_dir.changed else 'already exists' }}"
|
Using failed_when
Define custom failure conditions for better error handling.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| tasks:
- name: Run custom script
shell: /opt/scripts/health_check.sh
register: health_check
failed_when: health_check.rc != 0 or 'ERROR' in health_check.stdout
- name: Check application status
uri:
url: "http://{{ inventory_hostname }}:8080/health"
method: GET
register: app_health
failed_when:
- app_health.status != 200
- "'healthy' not in app_health.json.status"
- name: Validate configuration file
command: nginx -t
register: nginx_test
changed_when: false
failed_when:
- nginx_test.rc != 0
- "'syntax is ok' not in nginx_test.stderr"
|
Organizing with Roles
Directory Structure of Roles
Roles provide a way to organize playbooks and make them reusable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| roles/
├── nginx/
│ ├── tasks/
│ │ └── main.yml
│ ├── handlers/
│ │ └── main.yml
│ ├── templates/
│ │ └── nginx.conf.j2
│ ├── files/
│ │ └── index.html
│ ├── vars/
│ │ └── main.yml
│ ├── defaults/
│ │ └── main.yml
│ ├── meta/
│ │ └── main.yml
│ └── README.md
└── mysql/
├── tasks/
├── handlers/
└── templates/
|
Creating a Role for Nginx
roles/nginx/defaults/main.yml
1
2
3
4
5
6
7
8
9
10
11
| ---
# Default variables for nginx role
nginx_port: 80
nginx_user: nginx
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_server_name: "{{ inventory_hostname }}"
nginx_document_root: /var/www/html
nginx_access_log: /var/log/nginx/access.log
nginx_error_log: /var/log/nginx/error.log
|
roles/nginx/tasks/main.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| ---
- name: Install Nginx
package:
name: nginx
state: present
notify: start nginx
- name: Create nginx user
user:
name: "{{ nginx_user }}"
system: yes
shell: /sbin/nologin
home: /var/cache/nginx
when: nginx_user != "nginx"
- name: Create document root
file:
path: "{{ nginx_document_root }}"
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: '0755'
- name: Copy nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
notify: restart nginx
- name: Copy default index.html
copy:
src: index.html
dest: "{{ nginx_document_root }}/index.html"
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: '0644'
- name: Start and enable nginx service
service:
name: nginx
state: started
enabled: yes
|
roles/nginx/handlers/main.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ---
- name: start nginx
service:
name: nginx
state: started
- name: restart nginx
service:
name: nginx
state: restarted
- name: reload nginx
service:
name: nginx
state: reloaded
|
roles/nginx/templates/nginx.conf.j2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
error_log {{ nginx_error_log }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections }};
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log {{ nginx_access_log }} main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout {{ nginx_keepalive_timeout }};
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen {{ nginx_port }};
server_name {{ nginx_server_name }};
root {{ nginx_document_root }};
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
}
}
|
Using the Role in a Playbook
1
2
3
4
5
6
7
8
9
| ---
- name: Deploy Nginx servers
hosts: webservers
become: yes
vars:
nginx_port: 8080
nginx_server_name: "myapp.example.com"
roles:
- nginx
|
Using Galaxy Roles
Ansible Galaxy provides thousands of pre-built roles.
1
2
3
4
5
6
7
8
| # Install a role from Galaxy
ansible-galaxy install geerlingguy.nginx
# Install specific version
ansible-galaxy install geerlingguy.mysql,2.3.0
# Install from requirements file
ansible-galaxy install -r requirements.yml
|
requirements.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| ---
# From Galaxy
- src: geerlingguy.nginx
version: 2.8.0
- src: geerlingguy.mysql
version: 3.3.0
# From GitHub
- src: https://github.com/username/role-name
name: custom-role
version: main
# From Git with specific branch
- src: git+https://github.com/username/role-name.git
version: feature-branch
name: feature-role
|
Ansible Vault
Encrypt Secrets (Passwords, Keys)
Ansible Vault encrypts sensitive data at rest.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Create encrypted file
ansible-vault create secret.yml
# Edit encrypted file
ansible-vault edit secret.yml
# Encrypt existing file
ansible-vault encrypt passwords.yml
# Decrypt file
ansible-vault decrypt passwords.yml
# View encrypted file
ansible-vault view secret.yml
# Change vault password
ansible-vault rekey secret.yml
|
Example: Encrypt MySQL Root Password
Create encrypted variables file
1
| ansible-vault create group_vars/all/vault.yml
|
Content of group_vars/all/vault.yml
1
2
3
4
5
6
7
8
| ---
vault_mysql_root_password: "super_secure_password_123"
vault_mysql_app_password: "app_secure_password_456"
vault_ssl_private_key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
-----END PRIVATE KEY-----
vault_api_secret_key: "abc123def456ghi789"
|
Reference in group_vars/all/main.yml
1
2
3
4
5
| ---
mysql_root_password: "{{ vault_mysql_root_password }}"
mysql_app_password: "{{ vault_mysql_app_password }}"
ssl_private_key: "{{ vault_ssl_private_key }}"
api_secret_key: "{{ vault_api_secret_key }}"
|
Using in playbook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| ---
- name: Configure MySQL with encrypted passwords
hosts: database
become: yes
vars_files:
- group_vars/all/vault.yml
tasks:
- name: Set MySQL root password
mysql_user:
name: root
password: "{{ vault_mysql_root_password }}"
state: present
- name: Create application database user
mysql_user:
name: appuser
password: "{{ vault_mysql_app_password }}"
priv: "appdb.*:ALL"
state: present
|
Running with vault password
1
2
3
4
5
6
7
8
9
| # Prompt for vault password
ansible-playbook site.yml --ask-vault-pass
# Use password file
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# Use environment variable
export ANSIBLE_VAULT_PASSWORD_FILE=~/.vault_pass
ansible-playbook site.yml
|
Ansible Facts & Filters
Gather Facts About Hosts
Facts are automatically collected information about managed hosts.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| ---
- name: Display system facts
hosts: all
tasks:
- name: Show all facts
debug:
var: ansible_facts
- name: Display specific facts
debug:
msg: |
Hostname: {{ ansible_hostname }}
OS Family: {{ ansible_os_family }}
Distribution: {{ ansible_distribution }}
Version: {{ ansible_distribution_version }}
Architecture: {{ ansible_architecture }}
Memory: {{ ansible_memtotal_mb }}MB
CPU Cores: {{ ansible_processor_cores }}
IP Address: {{ ansible_default_ipv4.address }}
- name: Custom facts from setup
setup:
filter: ansible_distribution*
- name: Gather minimal facts
setup:
gather_subset:
- network
- hardware
|
Use Facts in Playbooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| ---
- name: Configure based on facts
hosts: all
tasks:
- name: Install packages based on OS
package:
name: "{{ item }}"
state: present
loop: "{{ packages[ansible_os_family] }}"
vars:
packages:
RedHat:
- httpd
- mysql-server
Debian:
- apache2
- mysql-server
- name: Configure memory-based settings
template:
src: app.conf.j2
dest: /etc/app.conf
vars:
max_connections: "{{ (ansible_memtotal_mb / 64) | int }}"
buffer_size: "{{ (ansible_memtotal_mb * 0.1) | int }}M"
- name: Set timezone based on location
timezone:
name: "{{ timezone_map[ansible_default_ipv4.address.split('.')[0]] | default('UTC') }}"
vars:
timezone_map:
"10": "America/New_York"
"172": "Europe/London"
"192": "Asia/Tokyo"
|
Common Filters (default
, upper
, lower
, replace
)
Filters transform data within templates and playbooks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
| ---
- name: Filter examples
hosts: localhost
vars:
app_name: "my-web-app"
environment: "production"
version: ""
config:
database:
host: "db.example.com"
port: 5432
name: "myapp"
tasks:
- name: String manipulation filters
debug:
msg: |
Original: {{ app_name }}
Upper: {{ app_name | upper }}
Lower: {{ app_name | upper | lower }}
Replace: {{ app_name | replace('-', '_') }}
Title: {{ app_name | title }}
Capitalize: {{ app_name | capitalize }}
- name: Default values
debug:
msg: |
Version: {{ version | default('1.0.0') }}
Environment: {{ env | default(environment) }}
Debug Mode: {{ debug_mode | default(false) | bool }}
- name: List and dictionary filters
debug:
msg: |
First item: {{ ['apple', 'banana', 'cherry'] | first }}
Last item: {{ ['apple', 'banana', 'cherry'] | last }}
Random item: {{ ['apple', 'banana', 'cherry'] | random }}
Joined: {{ ['web1', 'web2', 'web3'] | join(', ') }}
Dict keys: {{ config.database.keys() | list }}
- name: Number and math filters
debug:
msg: |
Absolute: {{ -42 | abs }}
Rounded: {{ 3.14159 | round(2) }}
Random number: {{ 100 | random }}
Max: {{ [1, 5, 3, 9, 2] | max }}
Min: {{ [1, 5, 3, 9, 2] | min }}
- name: JSON and YAML filters
debug:
msg: |
To JSON: {{ config | to_json }}
To YAML: {{ config | to_nice_yaml }}
From JSON: {{ '{"key": "value"}' | from_json }}
- name: File and path filters
debug:
msg: |
Basename: {{ '/path/to/file.txt' | basename }}
Dirname: {{ '/path/to/file.txt' | dirname }}
Extension: {{ 'file.txt' | splitext | last }}
Without extension: {{ 'file.txt' | splitext | first }}
- name: Date filters
debug:
msg: |
Current time: {{ ansible_date_time.iso8601 }}
Epoch: {{ ansible_date_time.epoch }}
Formatted: {{ '%Y-%m-%d %H:%M:%S' | strftime }}
|
Advanced Inventories
Using Multiple Inventory Files
Combine static and dynamic inventories for complex environments.
Static inventory: inventories/production/hosts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| [webservers]
web1.prod.example.com ansible_host=10.0.1.10
web2.prod.example.com ansible_host=10.0.1.11
web3.prod.example.com ansible_host=10.0.1.12
[databases]
db1.prod.example.com ansible_host=10.0.2.10
db2.prod.example.com ansible_host=10.0.2.11
[loadbalancers]
lb1.prod.example.com ansible_host=10.0.3.10
[production:children]
webservers
databases
loadbalancers
[production:vars]
environment=production
backup_enabled=true
monitoring_enabled=true
|
Group variables: inventories/production/group_vars/webservers.yml
1
2
3
4
5
6
7
| ---
nginx_worker_processes: 4
nginx_worker_connections: 2048
ssl_enabled: true
ssl_certificate: /etc/ssl/certs/prod.crt
ssl_private_key: /etc/ssl/private/prod.key
log_level: warn
|
Dynamic Inventory Scripts
Create dynamic inventories that fetch data from external sources.
AWS Dynamic Inventory Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| #!/usr/bin/env python3
import json
import boto3
from botocore.exceptions import ClientError
def get_aws_instances():
"""Fetch EC2 instances and create inventory"""
inventory = {
'all': {
'hosts': [],
'vars': {}
},
'_meta': {
'hostvars': {}
}
}
try:
ec2 = boto3.client('ec2')
response = ec2.describe_instances()
for reservation in response['Reservations']:
for instance in reservation['Instances']:
if instance['State']['Name'] == 'running':
instance_id = instance['InstanceId']
private_ip = instance.get('PrivateIpAddress', '')
public_ip = instance.get('PublicIpAddress', '')
# Add to all hosts
inventory['all']['hosts'].append(instance_id)
# Set host variables
inventory['_meta']['hostvars'][instance_id] = {
'ansible_host': public_ip or private_ip,
'ec2_instance_id': instance_id,
'ec2_instance_type': instance['InstanceType'],
'ec2_placement_az': instance['Placement']['AvailabilityZone']
}
# Group by tags
for tag in instance.get('Tags', []):
if tag['Key'] == 'Environment':
group_name = f"env_{tag['Value']}"
if group_name not in inventory:
inventory[group_name] = {'hosts': []}
inventory[group_name]['hosts'].append(instance_id)
if tag['Key'] == 'Role':
group_name = tag['Value']
if group_name not in inventory:
inventory[group_name] = {'hosts': []}
inventory[group_name]['hosts'].append(instance_id)
except ClientError as e:
print(f"Error fetching instances: {e}")
return {'all': {'hosts': []}}
return inventory
if __name__ == '__main__':
print(json.dumps(get_aws_instances(), indent=2))
|
Cloud Integrations
Azure Dynamic Inventory
1
2
3
4
5
6
7
8
9
10
11
12
| # azure_rm.yml
plugin: azure.azcollection.azure_rm
include_vm_resource_groups:
- myResourceGroup
auth_source: auto
keyed_groups:
- key: tags.Environment | default('unknown')
prefix: env
- key: tags.Role | default('unknown')
prefix: role
- key: location
prefix: location
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| # gcp.yml
plugin: google.cloud.gcp_compute
projects:
- my-gcp-project
auth_kind: serviceaccount
service_account_file: /path/to/service-account.json
keyed_groups:
- key: labels.environment | default('unknown')
prefix: env
- key: labels.role | default('unknown')
prefix: role
- key: zone
prefix: zone
|
Orchestration Examples
Deploy Multi-tier App (Web + DB)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
| ---
- name: Deploy three-tier application
hosts: localhost
gather_facts: false
vars:
app_version: "{{ version | default('latest') }}"
tasks:
- name: Deploy database tier
include_tasks: deploy_database.yml
- name: Wait for database to be ready
wait_for:
host: "{{ hostvars[item]['ansible_host'] }}"
port: 3306
timeout: 300
loop: "{{ groups['databases'] }}"
- name: Deploy application tier
include_tasks: deploy_application.yml
- name: Deploy web tier
include_tasks: deploy_web.yml
- name: Configure load balancer
include_tasks: deploy_loadbalancer.yml
# deploy_database.yml
- name: Configure database servers
hosts: databases
become: yes
serial: 1
tasks:
- name: Install MySQL
package:
name: mysql-server
state: present
- name: Start MySQL service
service:
name: mysql
state: started
enabled: yes
- name: Create application database
mysql_db:
name: "{{ app_database }}"
state: present
- name: Create database user
mysql_user:
name: "{{ app_db_user }}"
password: "{{ app_db_password }}"
priv: "{{ app_database }}.*:ALL"
state: present
# deploy_application.yml
- name: Configure application servers
hosts: appservers
become: yes
serial: "{{ (groups['appservers']|length * 0.5)|round(0,'ceil')|int }}"
tasks:
- name: Create application user
user:
name: "{{ app_user }}"
system: yes
shell: /bin/bash
- name: Create application directories
file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
loop:
- "{{ app_home }}"
- "{{ app_home }}/logs"
- "{{ app_home }}/config"
- name: Download application
get_url:
url: "{{ app_download_url }}/{{ app_version }}/app.jar"
dest: "{{ app_home }}/app.jar"
owner: "{{ app_user }}"
group: "{{ app_user }}"
notify: restart app
- name: Configure application
template:
src: application.properties.j2
dest: "{{ app_home }}/config/application.properties"
owner: "{{ app_user }}"
group: "{{ app_user }}"
notify: restart app
- name: Install systemd service
template:
src: app.service.j2
dest: /etc/systemd/system/{{ app_name }}.service
notify:
- reload systemd
- restart app
handlers:
- name: reload systemd
systemd:
daemon_reload: yes
- name: restart app
service:
name: "{{ app_name }}"
state: restarted
|
Zero-downtime Deployment Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
| ---
- name: Zero-downtime deployment
hosts: webservers
serial: 1
become: yes
vars:
app_port: 8080
health_check_url: "http://{{ inventory_hostname }}:{{ app_port }}/health"
pre_tasks:
- name: Remove server from load balancer
uri:
url: "http://{{ load_balancer_host }}/admin/servers/{{ inventory_hostname }}/disable"
method: POST
delegate_to: "{{ load_balancer_host }}"
- name: Wait for connections to drain
wait_for:
timeout: 30
delegate_to: localhost
tasks:
- name: Stop application service
service:
name: myapp
state: stopped
- name: Backup current version
copy:
src: /opt/myapp/current/
dest: "/opt/myapp/backup-{{ ansible_date_time.epoch }}"
remote_src: yes
- name: Download new version
get_url:
url: "{{ artifact_url }}"
dest: /opt/myapp/new-version.jar
- name: Deploy new version
copy:
src: /opt/myapp/new-version.jar
dest: /opt/myapp/current/app.jar
remote_src: yes
backup: yes
- name: Start application service
service:
name: myapp
state: started
- name: Wait for application to be ready
uri:
url: "{{ health_check_url }}"
method: GET
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 10
post_tasks:
- name: Add server back to load balancer
uri:
url: "http://{{ load_balancer_host }}/admin/servers/{{ inventory_hostname }}/enable"
method: POST
delegate_to: "{{ load_balancer_host }}"
rescue:
- name: Rollback on failure
copy:
src: "/opt/myapp/backup-{{ ansible_date_time.epoch }}"
dest: /opt/myapp/current/
remote_src: yes
- name: Restart service after rollback
service:
name: myapp
state: restarted
- name: Add server back to load balancer after rollback
uri:
url: "http://{{ load_balancer_host }}/admin/servers/{{ inventory_hostname }}/enable"
method: POST
delegate_to: "{{ load_balancer_host }}"
- name: Fail the playbook
fail:
msg: "Deployment failed and was rolled back"
|
Rolling Updates
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| ---
- name: Rolling update with health checks
hosts: webservers
serial: "25%" # Update 25% of servers at a time
become: yes
max_fail_percentage: 0 # Fail fast on any errors
pre_tasks:
- name: Pre-deployment health check
uri:
url: "http://{{ inventory_hostname }}/health"
method: GET
status_code: 200
register: pre_health
failed_when: pre_health.status != 200
tasks:
- name: Download new application version
get_url:
url: "{{ app_download_url }}"
dest: /tmp/app-{{ version }}.tar.gz
- name: Extract application
unarchive:
src: /tmp/app-{{ version }}.tar.gz
dest: /opt/
remote_src: yes
creates: "/opt/app-{{ version }}"
- name: Update symlink to new version
file:
src: "/opt/app-{{ version }}"
dest: /opt/app-current
state: link
notify: restart app
- name: Update configuration files
template:
src: "{{ item }}.j2"
dest: "/opt/app-current/config/{{ item }}"
loop:
- application.yml
- logging.conf
notify: restart app
handlers:
- name: restart app
service:
name: myapp
state: restarted
post_tasks:
- name: Wait for application startup
wait_for:
port: 8080
host: "{{ inventory_hostname }}"
delay: 10
timeout: 60
- name: Post-deployment health check
uri:
url: "http://{{ inventory_hostname }}:8080/health"
method: GET
status_code: 200
register: post_health
until: post_health.status == 200
retries: 5
delay: 10
- name: Run smoke tests
uri:
url: "http://{{ inventory_hostname }}:8080/api/test"
method: GET
status_code: 200
register: smoke_test
failed_when: smoke_test.json.status != "ok"
|
Ansible for Cloud
Provision AWS EC2 Instances
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
| ---
- name: Provision AWS infrastructure
hosts: localhost
gather_facts: false
vars:
aws_region: us-east-1
vpc_cidr: "10.0.0.0/16"
subnet_cidr: "10.0.1.0/24"
tasks:
- name: Create VPC
amazon.aws.ec2_vpc_net:
name: "{{ project_name }}-vpc"
cidr_block: "{{ vpc_cidr }}"
region: "{{ aws_region }}"
state: present
tags:
Environment: "{{ environment }}"
Project: "{{ project_name }}"
register: vpc
- name: Create internet gateway
amazon.aws.ec2_vpc_igw:
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ aws_region }}"
state: present
tags:
Name: "{{ project_name }}-igw"
register: igw
- name: Create public subnet
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ vpc.vpc.id }}"
cidr: "{{ subnet_cidr }}"
region: "{{ aws_region }}"
availability_zone: "{{ aws_region }}a"
map_public: yes
state: present
tags:
Name: "{{ project_name }}-public-subnet"
register: subnet
- name: Create security group
amazon.aws.ec2_group:
name: "{{ project_name }}-web-sg"
description: Security group for web servers
vpc_id: "{{ vpc.vpc.id }}"
region: "{{ aws_region }}"
rules:
- proto: tcp
ports:
- 22
cidr_ip: 0.0.0.0/0
rule_desc: SSH access
- proto: tcp
ports:
- 80
- 443
cidr_ip: 0.0.0.0/0
rule_desc: HTTP/HTTPS access
tags:
Environment: "{{ environment }}"
register: security_group
- name: Launch EC2 instances
amazon.aws.ec2_instance:
key_name: "{{ aws_key_name }}"
instance_type: "{{ instance_type | default('t3.micro') }}"
image_id: "{{ ami_id }}"
vpc_subnet_id: "{{ subnet.subnet.id }}"
security_groups:
- "{{ security_group.group_id }}"
count: "{{ instance_count | default(2) }}"
region: "{{ aws_region }}"
state: present
wait: yes
wait_timeout: 300
tags:
Name: "{{ project_name }}-web-{{ item }}"
Environment: "{{ environment }}"
Role: webserver
register: ec2_instances
loop: "{{ range(1, (instance_count | default(2)) + 1) | list }}"
- name: Wait for instances to be reachable
wait_for:
host: "{{ item.public_ip_address }}"
port: 22
delay: 60
timeout: 300
loop: "{{ ec2_instances.results | map(attribute='instances') | flatten }}"
when: item.public_ip_address is defined
|
Manage S3 Buckets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| ---
- name: Manage S3 buckets and objects
hosts: localhost
gather_facts: false
vars:
bucket_name: "{{ project_name }}-{{ environment }}-storage"
tasks:
- name: Create S3 bucket
amazon.aws.s3_bucket:
name: "{{ bucket_name }}"
region: "{{ aws_region }}"
versioning: yes
encryption: "AES256"
public_access:
block_public_acls: true
block_public_policy: true
ignore_public_acls: true
restrict_public_buckets: true
tags:
Environment: "{{ environment }}"
Project: "{{ project_name }}"
- name: Configure bucket lifecycle
amazon.aws.s3_lifecycle:
name: "{{ bucket_name }}"
region: "{{ aws_region }}"
rule_id: cleanup_old_versions
status: enabled
noncurrent_version_expiration_days: 30
noncurrent_version_transitions:
- days: 30
storage_class: standard_ia
- days: 60
storage_class: glacier
- name: Upload application artifacts
amazon.aws.s3_object:
bucket: "{{ bucket_name }}"
object: "artifacts/{{ app_version }}/{{ item }}"
src: "local-artifacts/{{ item }}"
mode: put
encrypt: yes
tags:
Version: "{{ app_version }}"
Type: artifact
loop:
- app.jar
- config.yml
- static-assets.tar.gz
- name: Create bucket policy
amazon.aws.s3_bucket_policy:
name: "{{ bucket_name }}"
policy: |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowApplicationAccess",
"Effect": "Allow",
"Principal": {
"AWS": "{{ app_role_arn }}"
},
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::{{ bucket_name }}/app-data/*"
}
]
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| ---
- name: Configure comprehensive security groups
hosts: localhost
gather_facts: false
vars:
common_rules:
- proto: tcp
ports: [22]
cidr_ip: "{{ admin_cidr | default('10.0.0.0/8') }}"
rule_desc: SSH from admin network
tasks:
- name: Create web tier security group
amazon.aws.ec2_group:
name: "{{ project_name }}-web-sg"
description: Web tier security group
vpc_id: "{{ vpc_id }}"
region: "{{ aws_region }}"
rules:
- "{{ common_rules }}"
- proto: tcp
ports: [80, 443]
cidr_ip: 0.0.0.0/0
rule_desc: HTTP/HTTPS from internet
- proto: tcp
ports: [8080]
group_id: "{{ elb_security_group_id }}"
rule_desc: App port from load balancer
tags:
Tier: web
Environment: "{{ environment }}"
register: web_sg
- name: Create application tier security group
amazon.aws.ec2_group:
name: "{{ project_name }}-app-sg"
description: Application tier security group
vpc_id: "{{ vpc_id }}"
region: "{{ aws_region }}"
rules:
- "{{ common_rules }}"
- proto: tcp
ports: [8080, 8081]
group_id: "{{ web_sg.group_id }}"
rule_desc: App ports from web tier
- proto: tcp
ports: [9090]
cidr_ip: "{{ monitoring_cidr }}"
rule_desc: Metrics endpoint
tags:
Tier: application
Environment: "{{ environment }}"
register: app_sg
- name: Create database tier security group
amazon.aws.ec2_group:
name: "{{ project_name }}-db-sg"
description: Database tier security group
vpc_id: "{{ vpc_id }}"
region: "{{ aws_region }}"
rules:
- "{{ common_rules }}"
- proto: tcp
ports: [3306]
group_id: "{{ app_sg.group_id }}"
rule_desc: MySQL from application tier
- proto: tcp
ports: [3306]
cidr_ip: "{{ backup_cidr }}"
rule_desc: MySQL from backup servers
tags:
Tier: database
Environment: "{{ environment }}"
|
Examples with Azure & GCP
Azure Resource Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| ---
- name: Provision Azure infrastructure
hosts: localhost
gather_facts: false
tasks:
- name: Create resource group
azure.azcollection.azure_rm_resourcegroup:
name: "{{ project_name }}-rg"
location: "{{ azure_location | default('East US') }}"
tags:
Environment: "{{ environment }}"
Project: "{{ project_name }}"
- name: Create virtual network
azure.azcollection.azure_rm_virtualnetwork:
resource_group: "{{ project_name }}-rg"
name: "{{ project_name }}-vnet"
address_prefixes: "10.0.0.0/16"
tags:
Environment: "{{ environment }}"
- name: Create subnet
azure.azcollection.azure_rm_subnet:
resource_group: "{{ project_name }}-rg"
virtual_network: "{{ project_name }}-vnet"
name: web-subnet
address_prefix: "10.0.1.0/24"
- name: Create network security group
azure.azcollection.azure_rm_securitygroup:
resource_group: "{{ project_name }}-rg"
name: "{{ project_name }}-nsg"
rules:
- name: SSH
protocol: Tcp
destination_port_range: 22
access: Allow
priority: 1001
direction: Inbound
- name: HTTP
protocol: Tcp
destination_port_range: 80
access: Allow
priority: 1002
direction: Inbound
- name: Create virtual machine
azure.azcollection.azure_rm_virtualmachine:
resource_group: "{{ project_name }}-rg"
name: "{{ project_name }}-vm"
vm_size: Standard_B1s
admin_username: azureuser
ssh_password_enabled: false
ssh_public_keys:
- path: /home/azureuser/.ssh/authorized_keys
key_data: "{{ ssh_public_key }}"
image:
offer: 0001-com-ubuntu-server-focal
publisher: Canonical
sku: 20_04-lts-gen2
version: latest
tags:
Environment: "{{ environment }}"
Role: webserver
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| ---
- name: Provision GCP infrastructure
hosts: localhost
gather_facts: false
tasks:
- name: Create GCP network
google.cloud.gcp_compute_network:
name: "{{ project_name }}-network"
auto_create_subnetworks: false
project: "{{ gcp_project }}"
auth_kind: serviceaccount
service_account_file: "{{ gcp_service_account_file }}"
- name: Create subnet
google.cloud.gcp_compute_subnetwork:
name: "{{ project_name }}-subnet"
ip_cidr_range: 10.0.1.0/24
network: "{{ gcp_network }}"
region: "{{ gcp_region }}"
project: "{{ gcp_project }}"
auth_kind: serviceaccount
service_account_file: "{{ gcp_service_account_file }}"
- name: Create firewall rule
google.cloud.gcp_compute_firewall:
name: "{{ project_name }}-firewall"
allowed:
- ip_protocol: tcp
ports:
- '22'
- '80'
- '443'
source_ranges:
- 0.0.0.0/0
network: "{{ gcp_network }}"
project: "{{ gcp_project }}"
auth_kind: serviceaccount
service_account_file: "{{ gcp_service_account_file }}"
- name: Create compute instance
google.cloud.gcp_compute_instance:
name: "{{ project_name }}-instance"
machine_type: e2-micro
zone: "{{ gcp_zone }}"
project: "{{ gcp_project }}"
auth_kind: serviceaccount
service_account_file: "{{ gcp_service_account_file }}"
disks:
- auto_delete: true
boot: true
initialize_params:
source_image: projects/ubuntu-os-cloud/global/images/family/ubuntu-2004-lts
network_interfaces:
- network: "{{ gcp_network }}"
subnetwork: "{{ gcp_subnet }}"
access_configs:
- name: External NAT
type: ONE_TO_ONE_NAT
tags:
items:
- webserver
- "{{ environment }}"
|
Ansible AWX/Tower (Optional)
What is AWX/Tower?
AWX is the open-source version of Red Hat Ansible Tower, providing a web-based user interface, REST API, and task engine for Ansible.
Key Features:
- Web-based UI: Manage playbooks through a graphical interface
- Role-based Access Control: Fine-grained permissions management
- Job Templates: Reusable job configurations
- Scheduling: Cron-like job scheduling
- Inventories: Dynamic and static inventory management
- Credentials: Secure credential storage and management
- Workflows: Chain multiple job templates together
- Real-time Job Output: Live monitoring of playbook execution
- REST API: Programmatic access to all functionality
Setup & Basic Usage
Docker Compose Installation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| # docker-compose.yml
version: '3.8'
services:
web:
image: quay.io/ansible/awx:latest
container_name: awx_web
depends_on:
- redis
- postgres
ports:
- "80:8052"
hostname: awx_web
user: root
restart: unless-stopped
volumes:
- supervisor-socket:/var/run/supervisor
- rsyslog-socket:/var/run/awx-rsyslog/
- rsyslog-config:/var/lib/awx/rsyslog/
- "/tmp/awx_projects:/var/lib/awx/projects:rw"
environment:
http_proxy:
https_proxy:
no_proxy:
task:
image: quay.io/ansible/awx:latest
container_name: awx_task
depends_on:
- redis
- postgres
command: /usr/bin/launch_awx_task.sh
hostname: awx_task
user: root
restart: unless-stopped
volumes:
- supervisor-socket:/var/run/supervisor
- rsyslog-socket:/var/run/awx-rsyslog/
- rsyslog-config:/var/lib/awx/rsyslog/
- "/tmp/awx_projects:/var/lib/awx/projects:rw"
environment:
http_proxy:
https_proxy:
no_proxy:
redis:
image: redis:7
container_name: awx_redis
restart: unless-stopped
command: ["/usr/local/etc/redis/redis.conf"]
volumes:
- "./tools/docker-compose/redis.conf:/usr/local/etc/redis/redis.conf:ro"
postgres:
image: postgres:13
container_name: awx_postgres
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: awx
POSTGRES_PASSWORD: awxpass
POSTGRES_DB: awx
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
supervisor-socket:
rsyslog-socket:
rsyslog-config:
postgres_data:
|
Initial Configuration Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #!/bin/bash
# awx-setup.sh
echo "Setting up AWX..."
# Start AWX
docker-compose up -d
# Wait for services to be ready
echo "Waiting for AWX to start..."
sleep 60
# Create admin user (run inside the container)
docker exec -it awx_web awx-manage createsuperuser \
--username admin \
--email admin@example.com \
--no-input
echo "AWX is ready at http://localhost"
echo "Username: admin"
echo "Please set password using the web interface"
|
Managing Playbooks with GUI
AWX Configuration via REST API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
| #!/usr/bin/env python3
import requests
import json
class AWXManager:
def __init__(self, host, username, password):
self.host = host
self.session = requests.Session()
self.session.auth = (username, password)
self.base_url = f"http://{host}/api/v2"
def create_organization(self, name, description):
"""Create an organization"""
data = {
"name": name,
"description": description
}
response = self.session.post(
f"{self.base_url}/organizations/",
json=data
)
return response.json()
def create_project(self, name, org_id, scm_url, scm_type="git"):
"""Create a project"""
data = {
"name": name,
"organization": org_id,
"scm_type": scm_type,
"scm_url": scm_url,
"scm_update_on_launch": True
}
response = self.session.post(
f"{self.base_url}/projects/",
json=data
)
return response.json()
def create_inventory(self, name, org_id, description=""):
"""Create an inventory"""
data = {
"name": name,
"organization": org_id,
"description": description
}
response = self.session.post(
f"{self.base_url}/inventories/",
json=data
)
return response.json()
def create_job_template(self, name, project_id, playbook,
inventory_id, credential_id):
"""Create a job template"""
data = {
"name": name,
"project": project_id,
"playbook": playbook,
"inventory": inventory_id,
"credential": credential_id,
"ask_variables_on_launch": True,
"ask_limit_on_launch": True
}
response = self.session.post(
f"{self.base_url}/job_templates/",
json=data
)
return response.json()
def launch_job(self, template_id, extra_vars=None, limit=None):
"""Launch a job from template"""
data = {}
if extra_vars:
data["extra_vars"] = extra_vars
if limit:
data["limit"] = limit
response = self.session.post(
f"{self.base_url}/job_templates/{template_id}/launch/",
json=data
)
return response.json()
# Usage example
if __name__ == "__main__":
awx = AWXManager("localhost", "admin", "password")
# Create organization
org = awx.create_organization(
name="DevOps Team",
description="Main DevOps organization"
)
# Create project
project = awx.create_project(
name="Infrastructure Playbooks",
org_id=org["id"],
scm_url="https://github.com/company/ansible-playbooks.git"
)
# Create inventory
inventory = awx.create_inventory(
name="Production Servers",
org_id=org["id"],
description="Production environment inventory"
)
print(f"Created organization: {org['name']}")
print(f"Created project: {project['name']}")
print(f"Created inventory: {inventory['name']}")
|
AWX Workflow Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # workflow-template.yml
---
- name: Deploy Application Workflow
hosts: localhost
gather_facts: false
tasks:
- name: Create AWX Workflow
uri:
url: "{{ awx_url }}/api/v2/workflow_job_templates/"
method: POST
user: "{{ awx_username }}"
password: "{{ awx_password }}"
body_format: json
body:
name: "Deploy Web Application"
description: "Complete application deployment workflow"
organization: "{{ org_id }}"
inventory: "{{ inventory_id }}"
workflow_nodes:
- id: 1
unified_job_template: "{{ backup_job_template_id }}"
success_nodes: [2]
failure_nodes: []
- id: 2
unified_job_template: "{{ deploy_job_template_id }}"
success_nodes: [3]
failure_nodes: [4]
- id: 3
unified_job_template: "{{ test_job_template_id }}"
success_nodes: []
failure_nodes: [4]
- id: 4
unified_job_template: "{{ rollback_job_template_id }}"
success_nodes: []
failure_nodes: []
|
Best Practices
Use Roles & Variables Properly
Directory Structure Best Practices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| ansible-project/
├── ansible.cfg
├── site.yml
├── requirements.yml
├── inventories/
│ ├── production/
│ │ ├── hosts
│ │ └── group_vars/
│ │ ├── all.yml
│ │ └── webservers.yml
│ └── staging/
│ ├── hosts
│ └── group_vars/
├── roles/
│ ├── common/
│ ├── webserver/
│ └── database/
├── playbooks/
│ ├── deploy.yml
│ ├── backup.yml
│ └── maintenance.yml
└── vars/
├── vault.yml
└── external_vars.yml
|
Variable Precedence and Organization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # group_vars/all.yml - Global defaults
---
# Application settings
app_name: myapp
app_version: "1.0.0"
app_port: 8080
app_user: appuser
# Environment settings
environment: development
debug_mode: true
log_level: debug
# Infrastructure settings
backup_enabled: false
monitoring_enabled: false
ssl_enabled: false
# Default resource limits
memory_limit: 512m
cpu_limit: "0.5"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # group_vars/production.yml - Production overrides
---
environment: production
debug_mode: false
log_level: warn
backup_enabled: true
monitoring_enabled: true
ssl_enabled: true
# Production resource limits
memory_limit: 2g
cpu_limit: "2.0"
# Production-specific settings
replica_count: 3
database_pool_size: 20
cache_enabled: true
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # roles/webserver/defaults/main.yml
---
# Web server defaults
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 1m
nginx_gzip: true
nginx_gzip_types:
- text/plain
- text/css
- application/json
- application/javascript
- text/xml
- application/xml
|
Role Dependencies Best Practices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| # roles/webserver/meta/main.yml
---
galaxy_info:
author: DevOps Team
description: Nginx web server configuration
company: My Company
license: MIT
min_ansible_version: 2.9
platforms:
- name: EL
versions:
- 7
- 8
- name: Ubuntu
versions:
- bionic
- focal
galaxy_tags:
- webserver
- nginx
- web
dependencies:
- role: common
vars:
firewall_rules:
- port: 80
protocol: tcp
- port: 443
protocol: tcp
- role: ssl-certs
when: ssl_enabled | default(false)
vars:
ssl_cert_path: /etc/ssl/certs
ssl_key_path: /etc/ssl/private
|
Version Control with Git
Git Workflow Best Practices
1
2
3
4
5
6
7
8
9
10
| # .gitignore for Ansible projects
*.retry
.vault_pass
host_vars/*/vault.yml
group_vars/*/vault.yml
inventory/production/vault.yml
.vscode/
.idea/
__pycache__/
*.pyc
|
Git Hooks for Ansible
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| #!/bin/bash
# .git/hooks/pre-commit
# Pre-commit hook for Ansible projects
echo "Running pre-commit checks..."
# Check for vault files that might be unencrypted
if git diff --cached --name-only | grep -E "(vault|secret)" | grep -v ".gitignore"; then
echo "Checking vault files are encrypted..."
for file in $(git diff --cached --name-only | grep -E "(vault|secret)"); do
if head -n1 "$file" | grep -q "^\$ANSIBLE_VAULT"; then
echo "✓ $file is encrypted"
else
echo "✗ $file appears to be unencrypted!"
echo "Please encrypt with: ansible-vault encrypt $file"
exit 1
fi
done
fi
# Run ansible-lint on changed playbooks
if git diff --cached --name-only | grep -E "\.(yml|yaml)$"; then
echo "Running ansible-lint..."
for file in $(git diff --cached --name-only | grep -E "\.(yml|yaml)$"); do
if [[ -f "$file" ]]; then
ansible-lint "$file" || exit 1
fi
done
fi
# Check YAML syntax
echo "Checking YAML syntax..."
for file in $(git diff --cached --name-only | grep -E "\.(yml|yaml)$"); do
if [[ -f "$file" ]]; then
python -c "import yaml; yaml.safe_load(open('$file'))" || {
echo "✗ YAML syntax error in $file"
exit 1
}
echo "✓ $file has valid YAML syntax"
fi
done
echo "All pre-commit checks passed!"
|
Lint Playbooks (ansible-lint
)
.ansible-lint
Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| ---
# .ansible-lint configuration
# Exclude specific paths
exclude_paths:
- .cache/
- .github/
- molecule/
- .ansible-lint-ignore
# Skip specific rules
skip_list:
- yaml[line-length] # Allow longer lines in some cases
- name[casing] # Allow different naming conventions
# Use specific rules only
# use_default_rules: false
# rules:
# - rule1
# - rule2
# Enable offline mode (don't check for updates)
offline: false
# Set the verbosity level
verbosity: 1
# Custom rules directory
rulesdir:
- ~/.ansible-lint/custom-rules/
# Warn instead of error for specific rules
warn_list:
- experimental # Allow experimental features
- yaml[truthy] # Warn about boolean values
# Parseable format for CI/CD
# parseable: true
# Mock modules for linting (useful for custom modules)
mock_modules:
- custom_module
- proprietary_module
# Mock roles for linting
mock_roles:
- external.role
# Variable naming pattern
var_naming_pattern: "^[a-z_][a-z0-9_]*$"
|
Advanced Linting Rules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # custom-rules/CustomNamingRule.py
from ansiblelint import AnsibleLintRule
class CustomNamingRule(AnsibleLintRule):
id = 'CUSTOM001'
shortdesc = 'Tasks should have meaningful names'
description = (
'All tasks should have names that describe what they do. '
'Avoid generic names like "Install package" or "Run command".'
)
severity = 'MEDIUM'
tags = ['formatting']
version_added = 'v4.0.0'
generic_names = [
'install package',
'run command',
'copy file',
'start service',
'install',
'configure',
'setup'
]
def matchtask(self, task, file=None):
name = task.get('name', '')
if name.lower() in self.generic_names:
return True
return False
|
Conclusion
Ansible stands out as one of the most powerful automation tools because of its simplicity, flexibility, and agentless architecture. From managing a handful of servers to orchestrating complex multi-tier deployments across hybrid clouds, it adapts to both beginners and advanced DevOps engineers.
The key takeaway is that you don’t need to start big begin small with a few ad-hoc commands or a simple playbook, then gradually expand into roles, variables, and orchestration as your confidence grows. The declarative YAML syntax and idempotent execution ensure consistency and repeatability in every environment.
For real-world adoption, always test your configurations in staging before production. This prevents surprises and makes troubleshooting far easier.
If you want to go further, explore:
- Ansible Galaxy for ready-to-use roles.
- Ansible Vault to keep sensitive information secure.
- AWX/Tower for managing automation at scale with a GUI and advanced workflows.
By following these steps, you’ll not only streamline operations but also adopt a DevOps culture of automation, reliability, and efficiency.
🚀 Start small today your future self will thank you when managing infrastructure becomes effortless!