Debian Server Automation with Ansible: Complete Guide

Tyler Maginnis | February 07, 2024

DebianAnsibleautomationconfiguration managementorchestrationDevOps

Need Professional Debian Server Support?

Get expert assistance with your debian server support implementation and management. Tyler on Tech Louisville provides priority support for Louisville businesses.

Same-day service available for Louisville area

Debian Server Automation with Ansible: Complete Guide

Ansible provides powerful automation capabilities for managing Debian servers at scale. This comprehensive guide covers installation, configuration, playbook development, and best practices for automating your Debian infrastructure with Ansible.

Introduction to Ansible

Ansible is an agentless automation tool that uses SSH to manage remote systems. Key benefits include: - No agents required on managed nodes - Simple YAML syntax - Idempotent operations - Extensive module library - Strong community support

Installing Ansible

On Control Node (Debian)

# Update package index
sudo apt update

# Install prerequisites
sudo apt install python3 python3-pip python3-venv git

# Install Ansible via package manager
sudo apt install ansible

# Or install latest version via pip
python3 -m pip install --user ansible

# Verify installation
ansible --version

# Install additional collections
ansible-galaxy collection install community.general
ansible-galaxy collection install ansible.posix

Configure Ansible

# Create Ansible configuration
mkdir -p ~/ansible
cd ~/ansible

# Create ansible.cfg
cat > ansible.cfg << EOF
[defaults]
host_key_checking = False
inventory = ./inventory
remote_user = ansible
private_key_file = ~/.ssh/ansible_key
interpreter_python = auto_silent
callback_whitelist = profile_tasks
stdout_callback = yaml
timeout = 30
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400

[inventory]
enable_plugins = host_list, yaml, ini, auto

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
control_path = /tmp/ansible-%%h-%%p-%%r
EOF

Inventory Management

Static Inventory

# Create inventory file
cat > inventory/hosts.yml << EOF
all:
  children:
    webservers:
      hosts:
        web1:
          ansible_host: 192.168.1.10
          ansible_port: 22
        web2:
          ansible_host: 192.168.1.11
          http_port: 8080
    databases:
      hosts:
        db1:
          ansible_host: 192.168.1.20
          mysql_port: 3306
        db2:
          ansible_host: 192.168.1.21
          mysql_port: 3306
      vars:
        mysql_replication: true
    monitoring:
      hosts:
        monitor1:
          ansible_host: 192.168.1.30
  vars:
    ansible_user: ansible
    ansible_become: yes
    ntp_servers:
      - 0.debian.pool.ntp.org
      - 1.debian.pool.ntp.org
EOF

Dynamic Inventory

# Create dynamic inventory script
cat > inventory/dynamic_inventory.py << 'EOF'
#!/usr/bin/env python3

import json
import argparse

def get_inventory():
    inventory = {
        "_meta": {
            "hostvars": {
                "web1": {
                    "ansible_host": "192.168.1.10",
                    "http_port": 80
                },
                "web2": {
                    "ansible_host": "192.168.1.11",
                    "http_port": 80
                },
                "db1": {
                    "ansible_host": "192.168.1.20",
                    "mysql_port": 3306
                }
            }
        },
        "all": {
            "children": ["webservers", "databases"]
        },
        "webservers": {
            "hosts": ["web1", "web2"],
            "vars": {
                "nginx_worker_processes": 4
            }
        },
        "databases": {
            "hosts": ["db1"],
            "vars": {
                "mysql_innodb_buffer_pool_size": "2G"
            }
        }
    }
    return inventory

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--list', action='store_true')
    parser.add_argument('--host', action='store')
    args = parser.parse_args()

    if args.list:
        print(json.dumps(get_inventory()))
    elif args.host:
        print(json.dumps({}))

if __name__ == '__main__':
    main()
EOF

chmod +x inventory/dynamic_inventory.py

Playbook Development

Basic Playbook Structure

# Create base system configuration playbook
cat > playbooks/base_system.yml << 'EOF'
---
- name: Configure base system settings
  hosts: all
  become: yes
  gather_facts: yes

  vars:
    packages_to_install:
      - vim
      - htop
      - git
      - curl
      - wget
      - net-tools
      - build-essential
      - python3-pip

    users_to_create:
      - username: admin
        groups: sudo,adm
        shell: /bin/bash
        password: "{{ 'admin_password' | password_hash('sha512') }}"
        ssh_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

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

    - name: Upgrade all packages
      apt:
        upgrade: safe
        autoremove: yes
        autoclean: yes
      when: ansible_os_family == "Debian"

    - name: Install essential packages
      apt:
        name: "{{ packages_to_install }}"
        state: present

    - name: Set timezone
      timezone:
        name: America/New_York

    - name: Configure NTP
      template:
        src: templates/ntp.conf.j2
        dest: /etc/ntp.conf
        backup: yes
      notify: restart ntp

    - name: Create admin users
      user:
        name: "{{ item.username }}"
        groups: "{{ item.groups }}"
        shell: "{{ item.shell }}"
        password: "{{ item.password }}"
        create_home: yes
        state: present
      loop: "{{ users_to_create }}"

    - name: Add SSH keys for admin users
      authorized_key:
        user: "{{ item.username }}"
        key: "{{ item.ssh_key }}"
        state: present
      loop: "{{ users_to_create }}"
      when: item.ssh_key is defined

    - name: Configure sysctl parameters
      sysctl:
        name: "{{ item.name }}"
        value: "{{ item.value }}"
        state: present
        reload: yes
      loop:
        - { name: 'vm.swappiness', value: '10' }
        - { name: 'net.ipv4.ip_forward', value: '1' }
        - { name: 'net.ipv4.tcp_syncookies', value: '1' }

    - name: Configure firewall rules
      ufw:
        rule: allow
        port: "{{ item }}"
        proto: tcp
      loop:
        - '22'
        - '80'
        - '443'
      when: ansible_distribution == "Debian"

    - name: Enable UFW firewall
      ufw:
        state: enabled
        policy: deny
        direction: incoming

  handlers:
    - name: restart ntp
      service:
        name: ntp
        state: restarted
EOF

Web Server Playbook

# Create web server configuration playbook
cat > playbooks/webserver.yml << 'EOF'
---
- name: Configure Nginx web servers
  hosts: webservers
  become: yes

  vars:
    nginx_user: www-data
    nginx_worker_processes: "{{ ansible_processor_vcpus }}"
    nginx_worker_connections: 2048
    ssl_certificate_path: /etc/ssl/certs
    ssl_key_path: /etc/ssl/private

    websites:
      - name: example.com
        server_name: example.com www.example.com
        document_root: /var/www/example.com
        ssl_enabled: yes
        php_enabled: yes
      - name: api.example.com
        server_name: api.example.com
        document_root: /var/www/api
        ssl_enabled: yes
        proxy_pass: http://localhost:3000

  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present
      notify: start nginx

    - name: Install PHP-FPM
      apt:
        name:
          - php-fpm
          - php-mysql
          - php-gd
          - php-curl
          - php-mbstring
          - php-xml
          - php-zip
        state: present
      when: websites | selectattr('php_enabled', 'defined') | selectattr('php_enabled', 'equalto', true) | list | length > 0

    - name: Create website directories
      file:
        path: "{{ item.document_root }}"
        state: directory
        owner: "{{ nginx_user }}"
        group: "{{ nginx_user }}"
        mode: '0755'
      loop: "{{ websites }}"

    - name: Configure Nginx main config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        backup: yes
      notify: reload nginx

    - name: Configure website virtual hosts
      template:
        src: templates/nginx_vhost.conf.j2
        dest: "/etc/nginx/sites-available/{{ item.name }}"
        backup: yes
      loop: "{{ websites }}"
      notify: reload nginx

    - name: Enable website virtual hosts
      file:
        src: "/etc/nginx/sites-available/{{ item.name }}"
        dest: "/etc/nginx/sites-enabled/{{ item.name }}"
        state: link
      loop: "{{ websites }}"
      notify: reload nginx

    - name: Generate self-signed SSL certificates
      command: |
        openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
        -keyout {{ ssl_key_path }}/{{ item.name }}.key \
        -out {{ ssl_certificate_path }}/{{ item.name }}.crt \
        -subj "/C=US/ST=State/L=City/O=Organization/CN={{ item.server_name.split()[0] }}"
      args:
        creates: "{{ ssl_certificate_path }}/{{ item.name }}.crt"
      loop: "{{ websites }}"
      when: item.ssl_enabled | default(false)

    - name: Deploy index.html
      template:
        src: templates/index.html.j2
        dest: "{{ item.document_root }}/index.html"
        owner: "{{ nginx_user }}"
        group: "{{ nginx_user }}"
        mode: '0644'
      loop: "{{ websites }}"

  handlers:
    - name: start nginx
      service:
        name: nginx
        state: started
        enabled: yes

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

Database Server Playbook

# Create database server playbook
cat > playbooks/database.yml << 'EOF'
---
- name: Configure MySQL/MariaDB database servers
  hosts: databases
  become: yes

  vars:
    mysql_root_password: "{{ vault_mysql_root_password }}"
    mysql_databases:
      - name: webapp_db
        encoding: utf8mb4
        collation: utf8mb4_unicode_ci
    mysql_users:
      - name: webapp_user
        password: "{{ vault_mysql_webapp_password }}"
        priv: "webapp_db.*:ALL"
        host: "%"
      - name: backup_user
        password: "{{ vault_mysql_backup_password }}"
        priv: "*.*:SELECT,LOCK TABLES,SHOW VIEW,RELOAD,REPLICATION CLIENT,EVENT,TRIGGER"
        host: "localhost"

    mysql_config:
      mysqld:
        bind_address: "0.0.0.0"
        max_connections: 500
        innodb_buffer_pool_size: "{{ (ansible_memtotal_mb * 0.7) | int }}M"
        innodb_log_file_size: "512M"
        slow_query_log: 1
        slow_query_log_file: "/var/log/mysql/slow.log"
        long_query_time: 2

  tasks:
    - name: Install MySQL/MariaDB
      apt:
        name:
          - mariadb-server
          - mariadb-client
          - python3-pymysql
        state: present

    - name: Start and enable MySQL
      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

    - name: Remove anonymous MySQL users
      mysql_user:
        name: ''
        host_all: yes
        state: absent
        login_user: root
        login_password: "{{ mysql_root_password }}"

    - name: Remove MySQL test database
      mysql_db:
        name: test
        state: absent
        login_user: root
        login_password: "{{ mysql_root_password }}"

    - name: Create databases
      mysql_db:
        name: "{{ item.name }}"
        encoding: "{{ item.encoding | default('utf8mb4') }}"
        collation: "{{ item.collation | default('utf8mb4_unicode_ci') }}"
        state: present
        login_user: root
        login_password: "{{ mysql_root_password }}"
      loop: "{{ mysql_databases }}"

    - name: Create database users
      mysql_user:
        name: "{{ item.name }}"
        password: "{{ item.password }}"
        priv: "{{ item.priv }}"
        host: "{{ item.host }}"
        state: present
        login_user: root
        login_password: "{{ mysql_root_password }}"
      loop: "{{ mysql_users }}"

    - name: Configure MySQL
      template:
        src: templates/my.cnf.j2
        dest: /etc/mysql/mariadb.conf.d/99-ansible.cnf
        backup: yes
      notify: restart mysql

    - name: Setup MySQL backup script
      template:
        src: templates/mysql_backup.sh.j2
        dest: /usr/local/bin/mysql_backup.sh
        mode: '0750'
        owner: root
        group: root

    - name: Schedule MySQL backups
      cron:
        name: "MySQL backup"
        minute: "0"
        hour: "2"
        job: "/usr/local/bin/mysql_backup.sh"
        user: root

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

Ansible Roles

Create Role Structure

# Create roles directory structure
mkdir -p roles/common/{tasks,handlers,templates,files,vars,defaults,meta}
mkdir -p roles/nginx/{tasks,handlers,templates,files,vars,defaults,meta}
mkdir -p roles/mysql/{tasks,handlers,templates,files,vars,defaults,meta}

# Common role
cat > roles/common/tasks/main.yml << 'EOF'
---
- name: Update apt cache
  apt:
    update_cache: yes
    cache_valid_time: 3600

- name: Install common packages
  apt:
    name: "{{ common_packages }}"
    state: present

- name: Configure timezone
  timezone:
    name: "{{ timezone }}"

- name: Configure hostname
  hostname:
    name: "{{ inventory_hostname }}"

- name: Update /etc/hosts
  lineinfile:
    path: /etc/hosts
    regexp: '^127\.0\.1\.1'
    line: "127.0.1.1 {{ inventory_hostname }}"
    state: present

- name: Configure sysctl
  sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    state: present
    reload: yes
  loop: "{{ sysctl_config | dict2items }}"

- name: Configure SSH
  lineinfile:
    path: /etc/ssh/sshd_config
    regexp: "{{ item.regexp }}"
    line: "{{ item.line }}"
    state: present
  loop:
    - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
    - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
    - { regexp: '^#?PubkeyAuthentication', line: 'PubkeyAuthentication yes' }
  notify: restart ssh

- name: Configure firewall
  include_tasks: firewall.yml
  when: configure_firewall | default(true)
EOF

# Common role defaults
cat > roles/common/defaults/main.yml << 'EOF'
---
common_packages:
  - vim
  - htop
  - curl
  - wget
  - git
  - unzip
  - net-tools
  - dnsutils
  - traceroute
  - mtr-tiny
  - iotop
  - sysstat

timezone: UTC

sysctl_config:
  vm.swappiness: 10
  net.ipv4.tcp_syncookies: 1
  net.ipv4.ip_forward: 0

configure_firewall: true
EOF

# Common role handlers
cat > roles/common/handlers/main.yml << 'EOF'
---
- name: restart ssh
  service:
    name: ssh
    state: restarted
EOF

Using Roles in Playbooks

# Create site.yml using roles
cat > site.yml << 'EOF'
---
- name: Configure all servers
  hosts: all
  become: yes
  roles:
    - common

- name: Configure web servers
  hosts: webservers
  become: yes
  roles:
    - nginx
  vars:
    nginx_vhosts:
      - name: default
        server_name: "_"
        root: /var/www/html
        index: index.html

- name: Configure database servers
  hosts: databases
  become: yes
  roles:
    - mysql
  vars:
    mysql_root_password: "{{ vault_mysql_root_password }}"
    mysql_databases:
      - name: production_db
    mysql_users:
      - name: app_user
        password: "{{ vault_mysql_app_password }}"
        priv: "production_db.*:ALL"
EOF

Templates

Nginx Configuration Template

# Create nginx.conf template
cat > templates/nginx.conf.j2 << 'EOF'
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
include /etc/nginx/modules-enabled/*.conf;

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

http {
    # Basic Settings
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    server_tokens off;
    client_max_body_size 100M;

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

    # SSL Settings
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # Logging Settings
    access_log /var/log/nginx/access.log;

    # Gzip Settings
    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;

    # Virtual Host Configs
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}
EOF

# Create virtual host template
cat > templates/nginx_vhost.conf.j2 << 'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name {{ item.server_name }};

    {% if item.ssl_enabled | default(false) %}
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name {{ item.server_name }};

    ssl_certificate {{ ssl_certificate_path }}/{{ item.name }}.crt;
    ssl_certificate_key {{ ssl_key_path }}/{{ item.name }}.key;

    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;

    add_header Strict-Transport-Security "max-age=63072000" always;
    {% endif %}

    root {{ item.document_root }};
    index index.php index.html index.htm;

    location / {
        {% if item.proxy_pass is defined %}
        proxy_pass {{ item.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;
        {% else %}
        try_files $uri $uri/ =404;
        {% endif %}
    }

    {% if item.php_enabled | default(false) %}
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php-fpm.sock;
    }
    {% endif %}

    location ~ /\.ht {
        deny all;
    }
}
EOF

MySQL Configuration Template

# Create my.cnf template
cat > templates/my.cnf.j2 << 'EOF'
[mysqld]
{% for key, value in mysql_config.mysqld.items() %}
{{ key }} = {{ value }}
{% endfor %}

[mysql]
prompt = '\u@\h [\d]> '

[client]
default-character-set = utf8mb4
EOF

# Create backup script template
cat > templates/mysql_backup.sh.j2 << 'EOF'
#!/bin/bash

# MySQL Backup Script
BACKUP_DIR="/backup/mysql"
RETENTION_DAYS=7
DATE=$(date +%Y%m%d_%H%M%S)
MYSQL_USER="{{ mysql_users | selectattr('name', 'equalto', 'backup_user') | map(attribute='name') | first }}"
MYSQL_PASS="{{ mysql_users | selectattr('name', 'equalto', 'backup_user') | map(attribute='password') | first }}"

mkdir -p "$BACKUP_DIR"

# Backup all databases
mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASS" \
    --all-databases \
    --single-transaction \
    --routines \
    --triggers \
    --events | gzip > "$BACKUP_DIR/all_databases_${DATE}.sql.gz"

# Remove old backups
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete

echo "Backup completed: $BACKUP_DIR/all_databases_${DATE}.sql.gz"
EOF

Advanced Ansible Features

Vault for Secrets Management

# Create vault password file
echo "MyVaultPassword123!" > ~/.vault_pass
chmod 600 ~/.vault_pass

# Create encrypted variables file
ansible-vault create group_vars/all/vault.yml --vault-password-file ~/.vault_pass

# Add to vault.yml:
vault_mysql_root_password: "SuperSecureRootPass123!"
vault_mysql_webapp_password: "WebAppPass456!"
vault_mysql_backup_password: "BackupPass789!"

# Use in playbook with vault
ansible-playbook -i inventory/hosts.yml playbooks/database.yml --vault-password-file ~/.vault_pass

Custom Modules

# Create custom module
mkdir -p library
cat > library/system_info.py << 'EOF'
#!/usr/bin/python

from ansible.module_utils.basic import AnsibleModule
import os
import platform

def main():
    module = AnsibleModule(
        argument_spec=dict(
            detail_level=dict(type='str', default='basic', choices=['basic', 'full'])
        )
    )

    detail_level = module.params['detail_level']

    info = {
        'hostname': platform.node(),
        'system': platform.system(),
        'release': platform.release(),
        'version': platform.version(),
        'machine': platform.machine(),
        'processor': platform.processor(),
        'python_version': platform.python_version(),
    }

    if detail_level == 'full':
        info.update({
            'cpu_count': os.cpu_count(),
            'load_average': os.getloadavg(),
            'platform': platform.platform(),
        })

    module.exit_json(changed=False, system_info=info)

if __name__ == '__main__':
    main()
EOF

# Use custom module in playbook
cat > playbooks/test_custom_module.yml << 'EOF'
---
- name: Test custom module
  hosts: all
  tasks:
    - name: Get system information
      system_info:
        detail_level: full
      register: sysinfo

    - name: Display system information
      debug:
        var: sysinfo.system_info
EOF

Dynamic Includes and Conditionals

# Create conditional playbook
cat > playbooks/conditional_config.yml << 'EOF'
---
- name: Configure servers based on environment
  hosts: all
  become: yes

  vars:
    environment: "{{ env | default('development') }}"

  tasks:
    - name: Include environment-specific variables
      include_vars: "vars/{{ environment }}.yml"
      tags: always

    - name: Configure development environment
      include_tasks: tasks/configure_dev.yml
      when: environment == 'development'

    - name: Configure production environment
      include_tasks: tasks/configure_prod.yml
      when: environment == 'production'

    - name: Install packages based on role
      apt:
        name: "{{ item }}"
        state: present
      loop: "{{ packages }}"
      when: packages is defined

    - name: Configure services
      block:
        - name: Configure web service
          include_role:
            name: nginx
          when: "'webservers' in group_names"

        - name: Configure database service
          include_role:
            name: mysql
          when: "'databases' in group_names"
      rescue:
        - name: Service configuration failed
          debug:
            msg: "Failed to configure services, check logs"
      always:
        - name: Ensure services are running
          service:
            name: "{{ item }}"
            state: started
            enabled: yes
          loop: "{{ services_to_check | default([]) }}"
          ignore_errors: yes
EOF

Monitoring and Logging

Ansible Callback Plugins

# Create callback plugin for custom logging
mkdir -p callback_plugins
cat > callback_plugins/json_logger.py << 'EOF'
from ansible.plugins.callback import CallbackBase
import json
import datetime

class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'json_logger'

    def __init__(self):
        super(CallbackModule, self).__init__()
        self.results = []

    def v2_runner_on_ok(self, result):
        host = result._host
        self.results.append({
            'host': host.name,
            'task': result._task.get_name(),
            'status': 'ok',
            'timestamp': datetime.datetime.now().isoformat()
        })

    def v2_runner_on_failed(self, result, ignore_errors=False):
        host = result._host
        self.results.append({
            'host': host.name,
            'task': result._task.get_name(),
            'status': 'failed',
            'msg': result._result.get('msg', ''),
            'timestamp': datetime.datetime.now().isoformat()
        })

    def v2_playbook_on_stats(self, stats):
        with open('ansible_run_log.json', 'w') as f:
            json.dump(self.results, f, indent=2)
EOF

# Enable callback plugin in ansible.cfg
echo "callback_plugins = ./callback_plugins" >> ansible.cfg
echo "callbacks_enabled = json_logger" >> ansible.cfg

Monitoring Playbook

# Create monitoring setup playbook
cat > playbooks/monitoring.yml << 'EOF'
---
- name: Setup monitoring with Prometheus and Grafana
  hosts: monitoring
  become: yes

  vars:
    prometheus_version: "2.40.0"
    grafana_version: "9.3.0"
    node_exporter_version: "1.5.0"

    prometheus_targets:
      - job_name: 'node'
        static_configs:
          - targets: ['localhost:9100']
            labels:
              environment: 'production'

  tasks:
    - name: Create monitoring user
      user:
        name: prometheus
        system: yes
        shell: /bin/false
        home: /var/lib/prometheus
        create_home: yes

    - name: Download and install Prometheus
      unarchive:
        src: "https://github.com/prometheus/prometheus/releases/download/v{{ prometheus_version }}/prometheus-{{ prometheus_version }}.linux-amd64.tar.gz"
        dest: /tmp
        remote_src: yes
        creates: "/tmp/prometheus-{{ prometheus_version }}.linux-amd64"

    - name: Install Prometheus binaries
      copy:
        src: "/tmp/prometheus-{{ prometheus_version }}.linux-amd64/{{ item }}"
        dest: "/usr/local/bin/{{ item }}"
        mode: '0755'
        owner: root
        group: root
        remote_src: yes
      loop:
        - prometheus
        - promtool

    - name: Create Prometheus directories
      file:
        path: "{{ item }}"
        state: directory
        owner: prometheus
        group: prometheus
      loop:
        - /etc/prometheus
        - /var/lib/prometheus

    - name: Configure Prometheus
      template:
        src: templates/prometheus.yml.j2
        dest: /etc/prometheus/prometheus.yml
        owner: prometheus
        group: prometheus
      notify: restart prometheus

    - name: Create Prometheus systemd service
      template:
        src: templates/prometheus.service.j2
        dest: /etc/systemd/system/prometheus.service
      notify:
        - reload systemd
        - restart prometheus

    - name: Install Grafana
      apt:
        deb: "https://dl.grafana.com/oss/release/grafana_{{ grafana_version }}_amd64.deb"
        state: present

    - name: Start and enable services
      systemd:
        name: "{{ item }}"
        state: started
        enabled: yes
        daemon_reload: yes
      loop:
        - prometheus
        - grafana-server

  handlers:
    - name: reload systemd
      systemd:
        daemon_reload: yes

    - name: restart prometheus
      systemd:
        name: prometheus
        state: restarted
EOF

Best Practices

Error Handling

# Create robust playbook with error handling
cat > playbooks/robust_deployment.yml << 'EOF'
---
- name: Robust application deployment
  hosts: webservers
  become: yes
  serial: "30%"
  max_fail_percentage: 30

  vars:
    app_name: myapp
    app_version: "{{ version | default('latest') }}"
    health_check_url: "http://{{ inventory_hostname }}/health"
    rollback_on_failure: true

  pre_tasks:
    - name: Check if deployment is allowed
      fail:
        msg: "Deployment not allowed on {{ inventory_hostname }}"
      when: deployment_blocked | default(false)

    - name: Create deployment backup
      archive:
        path: "/var/www/{{ app_name }}"
        dest: "/backup/{{ app_name }}-{{ ansible_date_time.epoch }}.tar.gz"
      when: rollback_on_failure

  tasks:
    - name: Deploy application
      block:
        - name: Download application package
          get_url:
            url: "https://releases.example.com/{{ app_name }}/{{ app_version }}.tar.gz"
            dest: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
            checksum: "sha256:{{ app_checksum | default(omit) }}"
          register: download_result
          retries: 3
          delay: 10
          until: download_result is succeeded

        - name: Extract application
          unarchive:
            src: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
            dest: "/var/www/"
            remote_src: yes
            owner: www-data
            group: www-data

        - name: Run database migrations
          command: "/var/www/{{ app_name }}/migrate.sh"
          register: migration_result
          failed_when: migration_result.rc != 0

        - name: Restart application service
          systemd:
            name: "{{ app_name }}"
            state: restarted
          register: restart_result

        - name: Wait for application to be ready
          uri:
            url: "{{ health_check_url }}"
            status_code: 200
          register: health_check
          retries: 30
          delay: 5
          until: health_check.status == 200

      rescue:
        - name: Deployment failed - initiating rollback
          debug:
            msg: "Deployment failed on {{ inventory_hostname }}, rolling back..."
          when: rollback_on_failure

        - name: Restore from backup
          unarchive:
            src: "/backup/{{ app_name }}-{{ ansible_date_time.epoch }}.tar.gz"
            dest: "/"
            remote_src: yes
          when: rollback_on_failure

        - name: Restart application after rollback
          systemd:
            name: "{{ app_name }}"
            state: restarted
          when: rollback_on_failure

        - name: Fail the play
          fail:
            msg: "Deployment failed and was rolled back"

      always:
        - name: Cleanup temporary files
          file:
            path: "/tmp/{{ app_name }}-{{ app_version }}.tar.gz"
            state: absent

        - name: Send deployment notification
          mail:
            to: devops@example.com
            subject: "Deployment {{ 'succeeded' if health_check.status == 200 else 'failed' }} on {{ inventory_hostname }}"
            body: "Deployment of {{ app_name }} version {{ app_version }} completed with status: {{ 'success' if health_check.status == 200 else 'failure' }}"
          delegate_to: localhost
          run_once: true
EOF

Testing Ansible Code

# Create test playbook
cat > tests/test_playbook.yml << 'EOF'
---
- name: Test playbook syntax and functionality
  hosts: localhost
  gather_facts: no

  tasks:
    - name: Test variable definition
      assert:
        that:
          - app_name is defined
          - app_version is defined
        fail_msg: "Required variables are not defined"
      vars:
        app_name: testapp
        app_version: "1.0.0"

    - name: Test Jinja2 template rendering
      set_fact:
        rendered_config: |
          server {
              server_name {{ server_name }};
              listen {{ port }};
          }
      vars:
        server_name: test.example.com
        port: 80

    - name: Verify rendered configuration
      assert:
        that:
          - "'server_name test.example.com' in rendered_config"
          - "'listen 80' in rendered_config"

    - name: Test conditional logic
      assert:
        that:
          - (environment == 'production') | ternary(True, False) == expected
      vars:
        environment: production
        expected: true
EOF

# Run tests
ansible-playbook tests/test_playbook.yml --syntax-check
ansible-playbook tests/test_playbook.yml

Conclusion

Ansible provides powerful automation capabilities for managing Debian servers at scale. This guide covered the essential aspects of using Ansible effectively:

Key takeaways: - Start simple and gradually increase complexity - Use roles for reusable configurations - Implement proper error handling and testing - Secure sensitive data with Ansible Vault - Follow the principle of idempotency - Document your playbooks and roles - Test in development before production - Monitor and log Ansible executions

With Ansible, you can automate repetitive tasks, ensure consistency across your infrastructure, and focus on more strategic initiatives rather than manual server management.