41 pages ยท 8 sections
Ctrl K
GitHub Portfolio

Ansible Automation

Ansible is an agentless automation tool for configuration management, application deployment, and task automation using simple YAML playbooks. This guide covers production patterns for server provisioning, configuration management, and integration with Terraform.

Ansible Architecture

Ansible operates on a push-based, agentless model. The control node connects to managed nodes via SSH (Linux) or WinRM (Windows):

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Control Node                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”               โ”‚
โ”‚  โ”‚ Inventory โ”‚  โ”‚ Playbooks โ”‚  โ”‚  Roles    โ”‚               โ”‚
โ”‚  โ”‚ files     โ”‚  โ”‚ (YAML)    โ”‚  โ”‚ (reusable)โ”‚               โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜               โ”‚
โ”‚        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                      โ”‚
โ”‚                        โ–ผ                                     โ”‚
โ”‚               ansible-playbook                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚ SSH / WinRM
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ–ผ                โ–ผ                โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Web Server 1โ”‚ โ”‚  Web Server 2โ”‚ โ”‚  DB Server   โ”‚
โ”‚  (Managed)   โ”‚ โ”‚  (Managed)   โ”‚ โ”‚  (Managed)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Control Node Requirements

  • Python 3.9+ installed
  • Ansible installed (pip install ansible or system package)
  • SSH key access to all managed nodes
  • Inventory files defining target hosts

Managed Node Requirements

  • Python 3.x installed (for Ansible modules)
  • SSH server running (Linux) or WinRM configured (Windows)
  • Service account or SSH key for authentication
  • No agent installation required

Inventory Files

Ansible inventories define the target hosts and their variables. Both INI and YAML formats are supported:

INI Format

# inventory/production.ini
[webservers]
web-prod-[01:05].example.com ansible_user=deployer ansible_python_interpreter=/usr/bin/python3

[dbservers]
db-prod-01.example.com ansible_user=deployer mysql_role=master
db-prod-02.example.com ansible_user=deployer mysql_role=replica

[cacheservers]
redis-prod-01.example.com
redis-prod-02.example.com

[monitoring]
prom-prod-01.example.com
grafana-prod-01.example.com

# Groups of groups
[servers:children]
webservers
dbservers
cacheservers

# Group variables
[servers:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/production.pem
ansible_become=true
ansible_become_method=sudo
env=production
datacenter=us-east-1

YAML Format (Recommended)

# inventory/production.yml
all:
  children:
    webservers:
      hosts:
        web-prod-01:
          ansible_host: 10.0.1.10
          nginx_worker_processes: 4
        web-prod-02:
          ansible_host: 10.0.1.11
          nginx_worker_processes: 4
        web-prod-03:
          ansible_host: 10.0.1.12
          nginx_worker_processes: 2
      vars:
        app_role: web

    dbservers:
      hosts:
        db-prod-01:
          ansible_host: 10.0.2.10
          mysql_role: master
        db-prod-02:
          ansible_host: 10.0.2.11
          mysql_role: replica
      vars:
        app_role: database

    cacheservers:
      hosts:
        redis-prod-01:
          ansible_host: 10.0.3.10
        redis-prod-02:
          ansible_host: 10.0.3.11

  vars:
    ansible_user: ubuntu
    ansible_python_interpreter: /usr/bin/python3
    ansible_ssh_private_key_file: "~/.ssh/production.pem"
    ansible_become: true
    env: production
    datacenter: us-east-1

Dynamic Inventory (AWS EC2)

# ansible.cfg
[inventory]
enable_plugins = amazon.aws.aws_ec2, yaml, ini

# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
  - us-west-2
filters:
  instance-state-name: running
  tag:Environment:
    - production
keyed_groups:
  - key: tags.Role
    prefix: role
  - key: tags.Environment
    prefix: env
compose:
  ansible_host: public_ip_address | default(private_ip_address)

# Usage:
# ansible-inventory -i inventory/aws_ec2.yml --graph
# ansible-playbook -i inventory/aws_ec2.yml site.yml

Playbook Structure

A playbook is a YAML file containing one or more plays. Each play maps hosts to tasks:

---
# site.yml โ€” Main playbook
- name: Configure all production servers
  hosts: all
  become: yes
  gather_facts: yes

  pre_tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"

  roles:
    - common
    - monitoring-agent

- name: Configure web servers
  hosts: webservers
  become: yes

  roles:
    - nginx
    - php
    - deploy-app

  post_tasks:
    - name: Verify web application is responding
      uri:
        url: "http://localhost/health"
        status_code: 200
      register: health_check
      retries: 5
      delay: 5
      until: health_check.status == 200

Complete Working Playbook: Install and Configure Nginx with SSL

---
# playbooks/nginx-ssl.yml
- name: Install and configure Nginx with SSL
  hosts: webservers
  become: yes
  gather_facts: yes

  vars:
    nginx_version: "1.24"
    certbot_email: "admin@example.com"
    domains:
      - example.com
      - www.example.com
    nginx_worker_processes: "auto"
    nginx_worker_connections: 4096
    nginx_client_max_body_size: "100M"

  vars_files:
    - "vars/{{ env | default('dev') }}.yml"

  tasks:
    # โ”€โ”€ Prerequisites โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - name: Install required packages
      apt:
        name:
          - nginx
          - certbot
          - python3-certbot-nginx
          - openssl
          - fail2ban
        state: present
        update_cache: yes
      tags: [packages]

    # โ”€โ”€ Firewall Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - name: Allow HTTP traffic
      ufw:
        rule: allow
        port: '80'
        proto: tcp

    - name: Allow HTTPS traffic
      ufw:
        rule: allow
        port: '443'
        proto: tcp

    # โ”€โ”€ SSL Certificate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - name: Create SSL directory
      file:
        path: "/etc/ssl/{{ item }}"
        state: directory
        mode: '0755'
      loop: "{{ domains }}"

    - name: Generate self-signed certificate (dev only)
      openssl_certificate:
        path: "/etc/ssl/{{ item }}/cert.pem"
        privatekey_path: "/etc/ssl/{{ item }}/key.pem"
        csr_path: "/etc/ssl/{{ item }}/cert.csr"
        provider: selfsigned
        selfsigned_not_after: "+365d"
      loop: "{{ domains }}"
      when: env | default('dev') == 'dev'

    - name: Obtain Let's Encrypt certificate (production)
      command: >
        certbot --nginx
        --non-interactive
        --agree-tos
        --email {{ certbot_email }}
        -d {{ domains | join(',') }}
        --redirect
      when: env | default('dev') == 'production'
      notify: reload nginx

    # โ”€โ”€ Nginx Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - name: Deploy nginx.conf
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: reload nginx

    - name: Deploy site configuration
      template:
        src: templates/site.conf.j2
        dest: "/etc/nginx/sites-available/{{ domains[0] }}"
        owner: root
        group: root
        mode: '0644'
      notify: reload nginx

    - name: Enable site
      file:
        src: "/etc/nginx/sites-available/{{ domains[0] }}"
        dest: "/etc/nginx/sites-enabled/{{ domains[0] }}"
        state: link
      notify: reload nginx

    - name: Remove default site
      file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: reload nginx

    # โ”€โ”€ Security Headers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - name: Configure security headers
      template:
        src: templates/security-headers.conf.j2
        dest: /etc/nginx/conf.d/security-headers.conf
        owner: root
        group: root
        mode: '0644'
      notify: reload nginx

    # โ”€โ”€ Verify Configuration โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - name: Validate nginx configuration
      command: nginx -t
      changed_when: false

    - name: Ensure nginx is running and enabled
      service:
        name: nginx
        state: started
        enabled: yes

    # โ”€โ”€ Health Check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - name: Verify nginx is responding
      uri:
        url: "https://{{ domains[0] }}/health"
        status_code: 200
        validate_certs: "{{ env | default('dev') != 'dev' }}"
      register: result
      retries: 5
      delay: 3
      until: result.status == 200

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

    - name: restart nginx
      service:
        name: nginx
        state: restarted

Nginx Templates

# templates/nginx.conf.j2
user www-data;
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections {{ nginx_worker_connections }};
    multi_accept on;
    use epoll;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size {{ nginx_client_max_body_size }};

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Logging
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'uht="$upstream_header_time" urt="$upstream_response_time"';

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;

    # Gzip
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript text/javascript;

    # Security
    server_tokens off;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Roles: Directory Structure and Usage

Roles are the primary mechanism for organizing and reusing Ansible code:

roles/
โ””โ”€โ”€ nginx/
    โ”œโ”€โ”€ defaults/          # Default variables (lowest precedence)
    โ”‚   โ””โ”€โ”€ main.yml
    โ”œโ”€โ”€ vars/              # Variables (higher precedence)
    โ”‚   โ””โ”€โ”€ main.yml
    โ”œโ”€โ”€ tasks/             # Main task list
    โ”‚   โ”œโ”€โ”€ main.yml       # Entry point
    โ”‚   โ”œโ”€โ”€ install.yml
    โ”‚   โ”œโ”€โ”€ configure.yml
    โ”‚   โ””โ”€โ”€ ssl.yml
    โ”œโ”€โ”€ handlers/          # Event handlers
    โ”‚   โ””โ”€โ”€ main.yml
    โ”œโ”€โ”€ templates/         # Jinja2 templates
    โ”‚   โ”œโ”€โ”€ nginx.conf.j2
    โ”‚   โ””โ”€โ”€ site.conf.j2
    โ”œโ”€โ”€ files/             # Static files to copy
    โ”‚   โ””โ”€โ”€ favicon.ico
    โ”œโ”€โ”€ meta/              # Role metadata and dependencies
    โ”‚   โ””โ”€โ”€ main.yml
    โ”œโ”€โ”€ molecule/          # Testing scenarios
    โ”‚   โ””โ”€โ”€ default/
    โ”‚       โ”œโ”€โ”€ converge.yml
    โ”‚       โ”œโ”€โ”€ verify.yml
    โ”‚       โ””โ”€โ”€ molecule.yml
    โ”œโ”€โ”€ tests/             # Legacy tests
    โ”‚   โ”œโ”€โ”€ inventory
    โ”‚   โ””โ”€โ”€ test.yml
    โ””โ”€โ”€ README.md

Using Roles

# requirements.yml
oles:
  - name: geerlingguy.nginx
    version: 3.1.4
  - name: geerlingguy.mysql
    version: 4.3.0
  - src: https://github.com/my-org/ansible-role-custom-app.git
    version: v1.2.0
    name: custom-app

# Install roles
ansible-galaxy install -r requirements.yml

# Playbook with roles
- name: Configure application stack
  hosts: app_servers
  become: yes
  roles:
    - role: geerlingguy.nginx
      vars:
        nginx_worker_processes: 4
    - role: geerlingguy.mysql
      vars:
        mysql_databases:
          - name: myapp
        mysql_users:
          - name: myapp
            password: "{{ vault_mysql_password }}"
            priv: "myapp.*:ALL"
    - role: custom-app

Vault for Encrypting Sensitive Data

Ansible Vault encrypts sensitive data at rest within your repository:

# Create an encrypted file
ansible-vault create group_vars/all/vault.yml

# Edit an encrypted file
ansible-vault edit group_vars/all/vault.yml

# Encrypt an existing file
ansible-vault encrypt secrets.yml

# Decrypt a file for viewing
ansible-vault decrypt secrets.yml

# Change vault password
ansible-vault rekey secrets.yml

# Run playbook with vault password file
ansible-playbook -i inventory/production.ini site.yml --vault-password-file .vault_pass

# Or prompt for password
ansible-playbook -i inventory/production.ini site.yml --ask-vault-pass

# Example vault.yml content (unencrypted):
vault_mysql_root_password: "SuperSecretPassword123!"
vault_mysql_app_password: "AppPassword456!"
vault_api_secret_key: "sk-live-abcdefghijklmnopqrstuvwxyz"

# Reference in playbook:
- name: Configure MySQL root password
  mysql_user:
    name: root
    password: "{{ vault_mysql_root_password }}"
    login_unix_socket: /var/run/mysqld/mysqld.sock

Ansible + Terraform Integration Pattern

The Terraform + Ansible integration pattern uses Terraform for infrastructure provisioning and Ansible for configuration management:

# Step 1: Terraform provisions infrastructure and generates inventory
# terraform/main.tf
resource "aws_instance" "web" {
  count = 3

  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.medium"
  subnet_id     = aws_subnet.public.id
  key_name      = var.ssh_key_name

  tags = {
    Name = "web-${count.index + 1}"
    Role = "webserver"
    AnsibleManaged = "true"
  }
}

# terraform/outputs.tf
output "web_server_ips" {
  value = aws_instance.web[*].public_ip
}

# terraform/templates/inventory.yml.tpl (used with templatefile)
webservers:
  hosts:
%{ for idx, ip in web_ips ~}
    web-${idx + 1}:
      ansible_host: ${ip}
%{ endfor ~}
# Step 2: Local exec provisioner generates inventory
resource "local_file" "ansible_inventory" {
  content = templatefile("${path.module}/templates/inventory.yml.tpl", {
    web_ips = aws_instance.web[*].public_ip
  })
  filename = "${path.module}/../ansible/inventory/terraform.yml"

  depends_on = [aws_instance.web]
}

# Step 3: Trigger Ansible after Terraform apply
resource "null_resource" "ansible" {
  triggers = {
    always_run = timestamp()
  }

  provisioner "local-exec" {
    command = <<EOF
      cd ${path.module}/../ansible && \
      ansible-playbook \
        -i inventory/terraform.yml \
        -i inventory/production.yml \
        --private-key ${var.ssh_private_key_path} \
        --vault-password-file .vault_pass \
        site.yml
    EOF
  }

  depends_on = [local_file.ansible_inventory]
}
Warning: Using local-exec to trigger Ansible ties your configuration management to your provisioning tool. For production at scale, prefer decoupled execution: Terraform outputs are consumed by a separate CI/CD pipeline that runs Ansible. This separation allows independent rollbacks and avoids Terraform destroy triggering configuration changes.

Ansible Tower / AWX Overview

FeatureCLI AnsibleAWX (Open Source)Ansible Tower
Web UINoYesYes
Role-based access controlNoYesYes
Job schedulingcronBuilt-inBuilt-in
Workflow orchestrationManualVisual workflow editorVisual workflow editor
Survey formsNoYesYes
Credential managementFilesEncrypted in databaseEncrypted in database
SupportCommunityCommunityRed Hat support
CostFreeFreeLicense per managed node

Idempotency Best Practices

Idempotency means running a playbook multiple times produces the same result. Ansible modules are inherently idempotent, but tasks must be written correctly:

# GOOD: Idempotent โ€” only changes if file differs
template:
  src: nginx.conf.j2
  dest: /etc/nginx/nginx.conf
  owner: root
  group: root
  mode: '0644'

# BAD: Not idempotent โ€” always reports changed
shell: cp /tmp/nginx.conf /etc/nginx/nginx.conf

# GOOD: Idempotent package installation
apt:
  name: nginx
  state: present

# GOOD: Idempotent service management
service:
  name: nginx
  state: started
  enabled: yes

# GOOD: Idempotent user creation
user:
  name: deploy
  uid: 1001
  groups: [www-data, docker]
  append: yes
  shell: /bin/bash
  create_home: yes

# BAD: Not idempotent โ€” always appends
shell: echo "export PATH=/opt/app/bin:$PATH" >> /home/deploy/.bashrc

# GOOD: Idempotent lineinfile
lineinfile:
  path: /home/deploy/.bashrc
  line: 'export PATH=/opt/app/bin:$PATH'
  state: present

Conditionals, Loops, and Handlers

# โ”€โ”€ Conditionals โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
- name: Install Apache (Red Hat)
  yum:
    name: httpd
    state: present
  when: ansible_os_family == "RedHat"

- name: Install Apache (Debian)
  apt:
    name: apache2
    state: present
  when: ansible_os_family == "Debian"

- name: Restart service only if configuration changed
  service:
    name: nginx
    state: restarted
  when: nginx_config.changed | default(false)

# Multiple conditions
- name: Install monitoring agent (production Linux only)
  apt:
    name: datadog-agent
    state: present
  when:
    - env == "production"
    - ansible_os_family == "Debian"
    - ansible_virtualization_type != "docker"

# โ”€โ”€ Loops โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Standard loop
- name: Create application directories
  file:
    path: "/opt/myapp/{{ item }}"
    state: directory
    mode: '0755'
  loop:
    - config
    - logs
    - tmp
    - uploads

# Loop with dicts
- name: Create users
  user:
    name: "{{ item.name }}"
    uid: "{{ item.uid }}"
    groups: "{{ item.groups }}"
    shell: "{{ item.shell | default('/bin/bash') }}"
  loop:
    - { name: 'deploy', uid: 1001, groups: 'www-data' }
    - { name: 'backup', uid: 1002, groups: 'backup' }

# Loop over a variable
- name: Install packages
  apt:
    name: "{{ item }}"
    state: present
  loop: "{{ base_packages }}"

# โ”€โ”€ Handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# handlers/main.yml
- name: restart nginx
  service:
    name: nginx
    state: restarted

- name: reload nginx
  service:
    name: nginx
    state: reloaded

- name: restart mysql
  service:
    name: mysql
    state: restarted

# Notify handlers from tasks
- name: Deploy application configuration
  template:
    src: app.config.j2
    dest: /opt/myapp/config/app.yml
  notify:
    - restart myapp

- name: Deploy systemd service file
  template:
    src: myapp.service.j2
    dest: /etc/systemd/system/myapp.service
  notify:
    - reload systemd
    - restart myapp

Best Practices Summary

PracticeImplementation
Use roles for reusable componentsExtract common patterns into roles with galaxy structure
Encrypt all secrets with VaultNo plaintext passwords in repositories
Use YAML inventory for complex environmentsBetter structure and variable scoping than INI
Tag all tasksEnable targeted execution: ansible-playbook --tags nginx
Use handlers for service restartsAvoid unnecessary restarts; only restart on change
Use check mode for dry runsansible-playbook --check --diff before applying
Limit playbook outputUse no_log: true for tasks handling secrets
Test with MoleculeUse Docker/VM-based testing for role validation
Use block/rescue/alwaysImplement proper error handling in playbooks
Pin role versionsUse requirements.yml with explicit version tags