Complete Ansible Automation Guide for Ubuntu Server 22.04 Infrastructure
Ansible is a powerful automation tool that simplifies configuration management, application deployment, and orchestration. This comprehensive guide covers everything from basic setup to advanced automation patterns for Ubuntu Server infrastructure.
Prerequisites
- Ubuntu Server 22.04 (Control Node)
- SSH access to managed nodes
- Python 3 installed on all nodes
- Basic understanding of YAML syntax
Ansible Installation and Setup
Install Ansible on Control Node
# Update system
sudo apt update
# Install software properties common
sudo apt install software-properties-common -y
# Add Ansible PPA
sudo add-apt-repository --yes --update ppa:ansible/ansible
# Install Ansible
sudo apt install ansible -y
# Verify installation
ansible --version
Configure Ansible
# Create Ansible directory structure
mkdir -p ~/ansible/{playbooks,roles,group_vars,host_vars,files,templates}
cd ~/ansible
# Create ansible.cfg
nano ansible.cfg
[defaults]
inventory = inventory
host_key_checking = False
remote_user = ansible
private_key_file = ~/.ssh/ansible_key
roles_path = roles
retry_files_enabled = False
gather_facts = True
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400
stdout_callback = yaml
callback_whitelist = timer, profile_tasks
deprecation_warnings = False
[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
Setup SSH Keys
# Generate SSH key for Ansible
ssh-keygen -t ed25519 -f ~/.ssh/ansible_key -C "ansible@control"
# Copy key to managed nodes
ssh-copy-id -i ~/.ssh/ansible_key.pub user@node1
ssh-copy-id -i ~/.ssh/ansible_key.pub user@node2
Inventory Management
Static Inventory
nano inventory
[webservers]
web1 ansible_host=192.168.1.10
web2 ansible_host=192.168.1.11
[databases]
db1 ansible_host=192.168.1.20 ansible_port=2222
db2 ansible_host=192.168.1.21
[monitoring]
monitor1 ansible_host=192.168.1.30
[ubuntu:children]
webservers
databases
monitoring
[ubuntu:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_user=ubuntu
Dynamic Inventory Script
nano dynamic_inventory.py
#!/usr/bin/env python3
import json
import subprocess
def get_inventory():
inventory = {
'webservers': {
'hosts': [],
'vars': {
'ansible_user': 'ubuntu',
'web_port': 80
}
},
'databases': {
'hosts': [],
'vars': {
'ansible_user': 'ubuntu',
'db_port': 3306
}
},
'_meta': {
'hostvars': {}
}
}
# Example: Get hosts from cloud provider API
# In real scenario, replace with actual API calls
webservers = ['web1', 'web2']
databases = ['db1', 'db2']
for server in webservers:
inventory['webservers']['hosts'].append(server)
inventory['_meta']['hostvars'][server] = {
'ansible_host': f'192.168.1.{10 + webservers.index(server)}'
}
for server in databases:
inventory['databases']['hosts'].append(server)
inventory['_meta']['hostvars'][server] = {
'ansible_host': f'192.168.1.{20 + databases.index(server)}'
}
return inventory
if __name__ == '__main__':
print(json.dumps(get_inventory()))
chmod +x dynamic_inventory.py
Essential Playbooks
System Update Playbook
nano playbooks/update_systems.yml
---
- name: Update Ubuntu Systems
hosts: ubuntu
gather_facts: yes
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Upgrade all packages
apt:
upgrade: dist
autoremove: yes
autoclean: yes
register: upgrade_result
- name: Check if reboot is required
stat:
path: /var/run/reboot-required
register: reboot_required
- name: Display upgrade results
debug:
msg: "{{ upgrade_result.stdout_lines | default(['No updates']) }}"
when: upgrade_result.stdout_lines is defined
- name: Reboot if required
reboot:
msg: "Reboot initiated by Ansible after system update"
connect_timeout: 5
reboot_timeout: 300
pre_reboot_delay: 0
post_reboot_delay: 30
test_command: uptime
when: reboot_required.stat.exists
User Management Playbook
nano playbooks/manage_users.yml
---
- name: Manage Users Across Infrastructure
hosts: all
gather_facts: no
vars_files:
- ../group_vars/users.yml
tasks:
- name: Create admin group
group:
name: admins
state: present
- name: Create user accounts
user:
name: "{{ item.username }}"
groups: "{{ item.groups | join(',') }}"
shell: "{{ item.shell | default('/bin/bash') }}"
create_home: yes
state: present
loop: "{{ users }}"
when: users is defined
- name: Set SSH keys for users
authorized_key:
user: "{{ item.username }}"
key: "{{ item.ssh_key }}"
state: present
loop: "{{ users }}"
when:
- users is defined
- item.ssh_key is defined
- name: Configure sudo access
copy:
dest: "/etc/sudoers.d/{{ item.username }}"
content: |
{{ item.username }} ALL=(ALL) NOPASSWD:ALL
validate: 'visudo -cf %s'
loop: "{{ users }}"
when:
- users is defined
- item.sudo | default(false)
Create users variable file:
nano group_vars/users.yml
---
users:
- username: john
groups:
- admins
- docker
ssh_key: "ssh-ed25519 AAAAC3... john@example.com"
sudo: true
- username: jane
groups:
- developers
ssh_key: "ssh-ed25519 AAAAC3... jane@example.com"
sudo: false
shell: /bin/zsh
Security Hardening Playbook
nano playbooks/security_hardening.yml
---
- name: Security Hardening for Ubuntu Servers
hosts: ubuntu
gather_facts: yes
vars:
sshd_config:
Port: 22
PermitRootLogin: 'no'
PasswordAuthentication: 'no'
PubkeyAuthentication: 'yes'
ChallengeResponseAuthentication: 'no'
UsePAM: 'yes'
X11Forwarding: 'no'
PrintMotd: 'no'
AcceptEnv: 'LANG LC_*'
ClientAliveInterval: 300
ClientAliveCountMax: 2
MaxAuthTries: 3
MaxSessions: 2
tasks:
- name: Install security packages
apt:
name:
- ufw
- fail2ban
- unattended-upgrades
- aide
- rkhunter
state: present
- name: Configure SSH daemon
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?{{ item.key }}\s'
line: '{{ item.key }} {{ item.value }}'
state: present
loop: "{{ sshd_config | dict2items }}"
notify: restart ssh
- name: Configure UFW defaults
ufw:
direction: "{{ item.direction }}"
policy: "{{ item.policy }}"
loop:
- { direction: incoming, policy: deny }
- { direction: outgoing, policy: allow }
- name: Configure UFW rules
ufw:
rule: "{{ item.rule }}"
port: "{{ item.port }}"
proto: "{{ item.proto }}"
loop:
- { rule: allow, port: '22', proto: tcp }
- { rule: allow, port: '80', proto: tcp }
- { rule: allow, port: '443', proto: tcp }
- name: Enable UFW
ufw:
state: enabled
- name: Configure fail2ban
template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
notify: restart fail2ban
- name: Configure automatic updates
copy:
dest: /etc/apt/apt.conf.d/50unattended-upgrades
content: |
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
handlers:
- name: restart ssh
service:
name: ssh
state: restarted
- name: restart fail2ban
service:
name: fail2ban
state: restarted
Advanced Playbook Patterns
Rolling Update Playbook
nano playbooks/rolling_update.yml
---
- name: Rolling Update of Web Servers
hosts: webservers
serial: 1 # Update one server at a time
max_fail_percentage: 0
pre_tasks:
- name: Disable server in load balancer
uri:
url: "http://loadbalancer/api/disable/{{ inventory_hostname }}"
method: POST
delegate_to: localhost
- name: Wait for connections to drain
wait_for:
port: 80
state: drained
timeout: 30
tasks:
- name: Update application
git:
repo: https://github.com/company/webapp.git
dest: /var/www/webapp
version: "{{ app_version | default('master') }}"
notify: restart webapp
- name: Run database migrations
command: /var/www/webapp/migrate.sh
run_once: true
- name: Install dependencies
pip:
requirements: /var/www/webapp/requirements.txt
virtualenv: /var/www/webapp/venv
post_tasks:
- name: Verify application health
uri:
url: "http://{{ ansible_default_ipv4.address }}/health"
status_code: 200
retries: 5
delay: 5
- name: Enable server in load balancer
uri:
url: "http://loadbalancer/api/enable/{{ inventory_hostname }}"
method: POST
delegate_to: localhost
handlers:
- name: restart webapp
systemd:
name: webapp
state: restarted
daemon_reload: yes
Database Backup Playbook
nano playbooks/backup_databases.yml
---
- name: Backup MySQL Databases
hosts: databases
gather_facts: yes
vars:
backup_dir: /backup/mysql
retention_days: 7
mysql_credentials:
user: backup
password: "{{ vault_mysql_backup_password }}"
tasks:
- name: Install required packages
apt:
name:
- python3-mysqldb
- mysql-client
state: present
- name: Create backup directory
file:
path: "{{ backup_dir }}"
state: directory
owner: root
group: root
mode: '0750'
- name: Get list of databases
mysql_db:
login_user: "{{ mysql_credentials.user }}"
login_password: "{{ mysql_credentials.password }}"
state: dump
name: all
target: /tmp/db_list.sql
register: db_list
- name: Backup each database
mysql_db:
login_user: "{{ mysql_credentials.user }}"
login_password: "{{ mysql_credentials.password }}"
state: dump
name: "{{ item }}"
target: "{{ backup_dir }}/{{ item }}_{{ ansible_date_time.date }}.sql"
single_transaction: yes
loop: "{{ databases }}"
when: databases is defined
- name: Compress backups
archive:
path: "{{ backup_dir }}/*.sql"
dest: "{{ backup_dir }}/mysql_backup_{{ ansible_date_time.date }}.tar.gz"
format: gz
remove: yes
- name: Upload to S3
aws_s3:
bucket: company-backups
object: "mysql/{{ inventory_hostname }}/mysql_backup_{{ ansible_date_time.date }}.tar.gz"
src: "{{ backup_dir }}/mysql_backup_{{ ansible_date_time.date }}.tar.gz"
mode: put
when: upload_to_s3 | default(true)
- name: Clean old backups
find:
paths: "{{ backup_dir }}"
patterns: "*.tar.gz"
age: "{{ retention_days }}d"
register: old_backups
- name: Remove old backups
file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups.files }}"
Ansible Roles
Create Web Server Role
# Create role structure
ansible-galaxy init roles/webserver
# Edit main tasks
nano roles/webserver/tasks/main.yml
---
- name: Install web server packages
apt:
name:
- nginx
- python3-pip
- python3-venv
- git
state: present
- name: Create web directory
file:
path: /var/www/{{ app_name }}
state: directory
owner: www-data
group: www-data
mode: '0755'
- name: Configure Nginx site
template:
src: nginx_site.conf.j2
dest: /etc/nginx/sites-available/{{ app_name }}
notify: reload nginx
- name: Enable Nginx site
file:
src: /etc/nginx/sites-available/{{ app_name }}
dest: /etc/nginx/sites-enabled/{{ app_name }}
state: link
notify: reload nginx
- name: Remove default Nginx site
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
- name: Configure firewall for web traffic
ufw:
rule: allow
name: 'Nginx Full'
when: configure_firewall | default(true)
Create handlers:
nano roles/webserver/handlers/main.yml
---
- name: reload nginx
systemd:
name: nginx
state: reloaded
- name: restart nginx
systemd:
name: nginx
state: restarted
Create template:
nano roles/webserver/templates/nginx_site.conf.j2
server {
listen 80;
listen [::]:80;
server_name {{ server_name }};
root /var/www/{{ app_name }}/public;
index index.html index.htm index.php;
access_log /var/log/nginx/{{ app_name }}_access.log;
error_log /var/log/nginx/{{ app_name }}_error.log;
location / {
try_files $uri $uri/ =404;
}
location ~ /\.ht {
deny all;
}
{% if enable_ssl | default(false) %}
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/ssl/certs/{{ app_name }}.crt;
ssl_certificate_key /etc/ssl/private/{{ app_name }}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
{% endif %}
}
Create defaults:
nano roles/webserver/defaults/main.yml
---
app_name: myapp
server_name: example.com
enable_ssl: false
configure_firewall: true
Create Monitoring Role
ansible-galaxy init roles/monitoring
nano roles/monitoring/tasks/main.yml
---
- name: Install monitoring packages
apt:
name:
- prometheus-node-exporter
- collectd
- sysstat
state: present
- name: Configure node exporter
lineinfile:
path: /etc/default/prometheus-node-exporter
regexp: '^ARGS='
line: 'ARGS="--collector.systemd --collector.processes"'
notify: restart node exporter
- name: Configure collectd
template:
src: collectd.conf.j2
dest: /etc/collectd/collectd.conf
notify: restart collectd
- name: Create monitoring scripts directory
file:
path: /opt/monitoring/scripts
state: directory
mode: '0755'
- name: Deploy custom monitoring scripts
copy:
src: "{{ item }}"
dest: /opt/monitoring/scripts/
mode: '0755'
with_fileglob:
- files/monitoring_scripts/*
- name: Setup monitoring cron jobs
cron:
name: "{{ item.name }}"
minute: "{{ item.minute | default('*') }}"
hour: "{{ item.hour | default('*') }}"
job: "{{ item.job }}"
loop:
- name: "Disk usage check"
minute: "*/30"
job: "/opt/monitoring/scripts/check_disk.sh"
- name: "Service health check"
minute: "*/5"
job: "/opt/monitoring/scripts/check_services.sh"
Ansible Vault for Secrets
Create Encrypted Variables
# Create vault password file
echo "MyVaultPassword123!" > ~/.vault_pass
chmod 600 ~/.vault_pass
# Create encrypted variable file
ansible-vault create group_vars/all/vault.yml --vault-password-file ~/.vault_pass
---
vault_mysql_root_password: "SuperSecretPassword123!"
vault_mysql_backup_password: "BackupPassword456!"
vault_api_keys:
github: "ghp_xxxxxxxxxxxxxxxxxxxx"
aws_access_key: "AKIAIOSFODNN7EXAMPLE"
aws_secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
Use Vault in Playbooks
nano playbooks/deploy_with_secrets.yml
---
- name: Deploy Application with Secrets
hosts: webservers
gather_facts: no
vars_files:
- ../group_vars/all/vault.yml
vars:
mysql_root_password: "{{ vault_mysql_root_password }}"
api_keys: "{{ vault_api_keys }}"
tasks:
- name: Configure application database
template:
src: database.yml.j2
dest: /var/www/app/config/database.yml
owner: www-data
group: www-data
mode: '0640'
- name: Set environment variables
lineinfile:
path: /etc/environment
line: "{{ item.key }}={{ item.value }}"
create: yes
loop:
- { key: "GITHUB_TOKEN", value: "{{ api_keys.github }}" }
- { key: "AWS_ACCESS_KEY", value: "{{ api_keys.aws_access_key }}" }
- { key: "AWS_SECRET_KEY", value: "{{ api_keys.aws_secret_key }}" }
no_log: true # Don't log sensitive data
Testing and Validation
Ansible Lint
# Install ansible-lint
pip3 install ansible-lint
# Create .ansible-lint configuration
nano .ansible-lint
---
exclude_paths:
- .cache/
- .github/
skip_list:
- yaml[line-length]
- name[missing]
Molecule Testing
# Install Molecule
pip3 install molecule molecule-docker
# Initialize role testing
cd roles/webserver
molecule init scenario -r webserver -d docker
# Edit molecule test
nano molecule/default/molecule.yml
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu2204
image: ubuntu:22.04
pre_build_image: false
privileged: true
command: /lib/systemd/systemd
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
provisioner:
name: ansible
inventory:
host_vars:
ubuntu2204:
app_name: testapp
server_name: test.example.com
verifier:
name: ansible
Create Test Playbook
nano molecule/default/converge.yml
---
- name: Converge
hosts: all
tasks:
- name: Include webserver role
include_role:
name: webserver
Verification Tests
nano molecule/default/verify.yml
---
- name: Verify
hosts: all
gather_facts: false
tasks:
- name: Check if Nginx is installed
package:
name: nginx
state: present
check_mode: yes
register: nginx_installed
failed_when: nginx_installed.changed
- name: Check if Nginx is running
systemd:
name: nginx
state: started
check_mode: yes
register: nginx_running
failed_when: nginx_running.changed
- name: Test Nginx config
command: nginx -t
changed_when: false
- name: Check if site is configured
stat:
path: /etc/nginx/sites-enabled/testapp
register: site_config
failed_when: not site_config.stat.exists
Performance Optimization
Ansible Configuration Tuning
nano ansible.cfg
[defaults]
# Performance settings
forks = 50
poll_interval = 15
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_cache
fact_caching_timeout = 86400
[ssh_connection]
pipelining = True
control_path = /tmp/ansible-%%h-%%p-%%r
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o PreferredAuthentications=publickey
Async Tasks
nano playbooks/async_tasks.yml
---
- name: Long Running Tasks
hosts: all
tasks:
- name: Run system update (async)
apt:
update_cache: yes
upgrade: dist
async: 3600 # 1 hour timeout
poll: 0 # Fire and forget
register: update_job
- name: Run backup (async)
command: /usr/local/bin/backup.sh
async: 7200 # 2 hour timeout
poll: 0
register: backup_job
- name: Check on async tasks
async_status:
jid: "{{ item.ansible_job_id }}"
register: job_result
until: job_result.finished
retries: 100
delay: 30
loop:
- "{{ update_job }}"
- "{{ backup_job }}"
Best Practices
- Version Control: Keep all playbooks and roles in Git
- Testing: Always test in development before production
- Documentation: Document all playbooks and variables
- Idempotency: Ensure playbooks can run multiple times safely
- Security: Use Ansible Vault for sensitive data
- Modularity: Use roles for reusable components
- Error Handling: Implement proper error handling and recovery
Troubleshooting
Debug Module
- name: Debug variable
debug:
var: ansible_facts
- name: Debug with message
debug:
msg: "The server {{ inventory_hostname }} has IP {{ ansible_default_ipv4.address }}"
Verbose Output
# Run with verbose output
ansible-playbook playbook.yml -vvv
# Check syntax
ansible-playbook playbook.yml --syntax-check
# Dry run
ansible-playbook playbook.yml --check
# Step through tasks
ansible-playbook playbook.yml --step
Common Issues
# Test connectivity
ansible all -m ping
# Gather facts
ansible all -m setup
# Check Python interpreter
ansible all -m shell -a "which python3"
Conclusion
This comprehensive guide covered Ansible automation for Ubuntu Server 22.04, from basic configuration to advanced patterns. With these tools and techniques, you can efficiently manage infrastructure at scale, ensure consistency, and automate complex deployments. Remember to continuously refine your playbooks and keep up with Ansible best practices as your infrastructure grows.