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.