Post

Ansible Kickstart

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

  1. Simple Learning Curve: YAML-based playbooks are easy to read and write
  2. No Agent Required: SSH access is sufficient for most operations
  3. Powerful: Can manage complex multi-tier applications
  4. Flexible: Works with any system that has SSH or PowerShell
  5. Extensive Module Library: 3000+ modules for various tasks
  6. Community Support: Large, active community and enterprise backing

Why Choose Ansible?

Comparison with Other Tools

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

Ansible

Detailed Architecture Components

Ansible

Ansible Execution Flow

Ansible

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):

  1. Extra vars (-e in command line)
  2. Task vars (only for the task)
  3. Block vars (only for tasks in block)
  4. Role and include vars
  5. Play vars
  6. Host facts / cached set_facts
  7. Host vars
  8. Group vars
  9. 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"]
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):

  1. ANSIBLE_CONFIG environment variable
  2. ansible.cfg in current directory
  3. ~/.ansible.cfg in home directory
  4. /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:

1
pip 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:

1
chmod +x inventory.py

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

  1. Use descriptive names for plays and tasks
  2. Include comments for complex logic
  3. Use variables instead of hard-coding values
  4. Implement proper error handling
  5. Use handlers for service restarts
  6. Include validation where possible
  7. 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

Kubernetes Tools 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
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

Ansible

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

Google Cloud Platform

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/*"
              }
            ]
          }

Configure Security Groups

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

Google Cloud Platform

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!

This post is licensed under CC BY 4.0 by the author.