Complete Ansible Automation Guide for Ubuntu Server 22.04 Infrastructure

Tyler Maginnis | January 22, 2024

UbuntuAnsibleAutomationConfiguration ManagementDevOpsInfrastructure as Code

Need Professional Ubuntu Server Support?

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

Same-day service available for Louisville area

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

  1. Version Control: Keep all playbooks and roles in Git
  2. Testing: Always test in development before production
  3. Documentation: Document all playbooks and variables
  4. Idempotency: Ensure playbooks can run multiple times safely
  5. Security: Use Ansible Vault for sensitive data
  6. Modularity: Use roles for reusable components
  7. 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.