Introduction to Ansible Automation: Practical Experience in Batch Deployment of Web Servers

Do you remember that night when you were woken up by a phone call at 3 AM? Twenty servers in the production environment needed urgent configuration updates, and you had to log in via SSH one by one, repeating the same commands. Two hours later, as you dragged your exhausted body to complete the task, you silently vowed: “I must find an automation tool!”

If you have had a similar experience, then congratulations, this article will completely change your operations career. I will guide you from scratch to master Ansible through a practical batch deployment project for web servers, allowing you to experience the charm of automated operations. By the end of this article, you will be able to:

  • Complete the deployment of Nginx on 50 servers within 10 minutes
  • Achieve one-click rolling updates and rollbacks for applications
  • Build reusable automated deployment processes
  • Reduce repetitive work time by over 90%

1. What is Ansible? What problems can it solve?

1.1 Pain Points of Traditional Operations

Before diving into Ansible, let’s first look at the challenges faced by traditional operations:

Scenario 1: Configuration DriftYou manage 100 servers, and theoretically, their configurations should be identical. However, over time, due to various ad-hoc changes and emergency patches, the server configurations begin to differ. One day, a seemingly simple update causes some servers to fail due to inconsistent configurations.

Scenario 2: Scaling ChallengesAs the company’s business grows rapidly, the number of servers increases from 10 to 100. A deployment task that originally took 30 minutes now takes 5 hours. Moreover, as the complexity of operations increases, the probability of human error also rises.

Scenario 3: Knowledge Transfer DifficultiesWhen a senior operations engineer leaves, all that remains is a pile of scattered shell scripts and simple documentation. The newcomer finds that the execution order and parameter meanings of each script need to be guessed and trialed.

1.2 Advantages of Ansible

Ansible is an open-source IT automation tool that describes system configurations using simple YAML syntax, achieving:

  • Agentless Architecture: No need to install any clients on managed nodes; management is done via SSH
  • Declarative Configuration: Describes the “desired state” rather than “how to achieve it”
  • Idempotency Guarantee: Producing the same result from multiple executions, avoiding issues from repeated operations
  • Easy to Learn and Use: The simple and intuitive YAML syntax lowers the learning curve
  • Powerful Module Library: Over 3000 built-in modules covering various operational scenarios

2. Quick Start: Setting Up Ansible Environment in 15 Minutes

2.1 Environment Preparation

We will set up a test environment consisting of 1 control node and 3 managed nodes:

# Control Node (Machine where Ansible is installed)control-node: 192.168.1.10
# Managed Nodes (Target Servers)web-01: 192.168.1.11
web-02: 192.168.1.12
web-03: 192.168.1.13

2.2 Installing Ansible

Execute on the control node:

# CentOS/RHEL System
sudo yum install -y epel-release
sudo yum install -y ansible

# Ubuntu/Debian System
sudo apt update
sudo apt install -y ansible

# Install using pip (recommended for the latest version)
sudo pip3 install ansible

# Verify installation
ansible --version

2.3 Configuring SSH Passwordless Login

The prerequisite for automation is that the control node can access the managed nodes without a password:

# Generate SSH key pair (if not already done)
ssh-keygen -t rsa -b 2048

# Copy public key to all managed nodes
for ip in 192.168.1.11 192.168.1.12 192.168.1.13; do
    ssh-copy-id -i ~/.ssh/id_rsa.pub root@$ip
done

# Test connection
ssh [email protected] 'hostname'

2.4 Creating Inventory File

The inventory file defines the list of hosts that Ansible will manage:

# Create inventory.ini file
[webservers]
web-01 ansible_host=192.168.1.11
web-02 ansible_host=192.168.1.12
web-03 ansible_host=192.168.1.13

[webservers:vars]
ansible_user=root
ansible_python_interpreter=/usr/bin/python3

[all:vars]
ansible_connection=ssh

Test connection to all hosts:

ansible -i inventory.ini all -m ping

If you see “pong” returned from all hosts, congratulations, the environment setup is successful!

3. Practical Project: Batch Deployment of Nginx Web Servers

Now let’s dive into a practical project to understand the powerful features of Ansible. We will implement:

  1. Batch installation of Nginx
  2. Deployment of custom configurations
  3. Deployment of static websites
  4. Implementation of rolling updates

3.1 Project Structure Design

nginx-deployment/
├── inventory.ini           # Host inventory
├── ansible.cfg            # Ansible configuration file
├── site.yml              # Main Playbook
├── roles/                # Roles directory
│   └── nginx/
│       ├── tasks/       # Task definitions
│       │   └── main.yml
│       ├── templates/    # Template files
│       │   ├── nginx.conf.j2
│       │   └── index.html.j2
│       ├── handlers/     # Handlers
│       │   └── main.yml
│       └── vars/         # Variable definitions
│           └── main.yml
└── group_vars/           # Group variables
    └── webservers.yml

3.2 Writing the Playbook

Create the main Playbook <span>site.yml</span>:

---
- name: Deploy Nginx Web Servers
  hosts: webservers
  become: yes
  gather_facts: yes
  vars:
    nginx_port: 80
    nginx_worker_processes: "{{ ansible_processor_vcpus }}"
    nginx_worker_connections: 1024
    website_title: "Ansible Automation Deployment Demo"
  tasks:
    - name: Update system package cache
      apt:
        update_cache: yes
      when: ansible_os_family == "Debian"
    - name: Install Nginx
      package:
        name: nginx
        state: present
    - name: Create website directory
      file:
        path: /var/www/html
        state: directory
        mode: '0755'
    - name: Deploy Nginx configuration file
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        backup: yes
      notify: restart nginx
    - name: Deploy website homepage
      template:
        src: index.html.j2
        dest: /var/www/html/index.html
        mode: '0644'
    - name: Ensure Nginx service is running
      service:
        name: nginx
        state: started
        enabled: yes
    - name: Wait for port to be ready
      wait_for:
        port: "{{ nginx_port }}"
        host: "{{ ansible_default_ipv4.address }}"
        delay: 5
        timeout: 30
  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted

3.3 Creating Configuration Templates

Create <span>templates/nginx.conf.j2</span>:

user www-data;
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;
events {
    worker_connections {{ nginx_worker_connections }};
    multi_accept on;
    use epoll;
}
http {
    # Basic configuration
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Log configuration
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

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

    # Virtual host configuration
    server {
        listen {{ nginx_port }} default_server;
        listen [::]:{{ nginx_port }} default_server;

        root /var/www/html;
        index index.html index.htm;

        server_name {{ ansible_hostname }}.example.com;

        location / {
            try_files $uri $uri/ =404;
        }

        # Health check endpoint
        location /health {
            access_log off;
            return 200 "healthy\n";
            add_header Content-Type text/plain;
        }
    }
}

Create <span>templates/index.html.j2</span>:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ website_title }}</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .container {
            text-align: center;
            padding: 2rem;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 15px;
            backdrop-filter: blur(10px);
        }
        h1 { font-size: 3rem; margin-bottom: 1rem; }
        .info {
            background: rgba(255, 255, 255, 0.2);
            padding: 1rem;
            border-radius: 10px;
            margin-top: 2rem;
        }
        .info p { margin: 0.5rem 0; }
    </style>
</head>
<body>
    <div class="container">
        <h1>🚀 {{ website_title }}</h1>
        <p>Congratulations! You have successfully deployed this page using Ansible</p>
        <div class="info">
            <p><strong>Server Name:</strong> {{ ansible_hostname }}</p>
            <p><strong>IP Address:</strong> {{ ansible_default_ipv4.address }}</p>
            <p><strong>Operating System:</strong> {{ ansible_distribution }} {{ ansible_distribution_version }}</p>
            <p><strong>Deployment Time:</strong> {{ ansible_date_time.iso8601 }}</p>
        </div>
    </div>
</body>
</html>

3.4 Executing Deployment

# Syntax check
ansible-playbook -i inventory.ini site.yml --syntax-check

# Dry run
ansible-playbook -i inventory.ini site.yml --check

# Official deployment
ansible-playbook -i inventory.ini site.yml

# View detailed output
ansible-playbook -i inventory.ini site.yml -vvv

4. Advanced Techniques: Making Your Automation More Powerful

4.1 Rolling Update Strategy

In a production environment, we need to ensure the continuous availability of services. Ansible supports rolling updates:

---
- name: Rolling Update Web Servers
  hosts: webservers
  become: yes
  serial: 1  # Update one server at a time
  max_fail_percentage: 30  # Allow 30% failure rate
  pre_tasks:
    - name: Remove from load balancer
      uri:
        url: "http://lb.example.com/api/remove"
        method: POST
        body_format: json
        body:
          server: "{{ ansible_hostname }}"
      delegate_to: localhost
  tasks:
    - name: Update application code
      git:
        repo: https://github.com/yourapp/webapp.git
        dest: /var/www/html
        version: "{{ app_version | default('master') }}"
    - name: Restart service
      service:
        name: nginx
        state: restarted
  post_tasks:
    - name: Health check
      uri:
        url: "http://{{ ansible_default_ipv4.address }}/health"
        status_code: 200
      retries: 5
      delay: 10
    - name: Re-add to load balancer
      uri:
        url: "http://lb.example.com/api/add"
        method: POST
        body_format: json
        body:
          server: "{{ ansible_hostname }}"
      delegate_to: localhost

4.2 Using Ansible Vault to Protect Sensitive Information

In production environments, passwords and keys need to be stored encrypted:

# Create encrypted file
ansible-vault create secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Add to secrets.yml:
db_password: "SuperSecret123!"
api_key: "sk-1234567890abcdef"

# Run playbook with encrypted variables
ansible-playbook -i inventory.ini site.yml --ask-vault-pass

4.3 Dynamic Inventory

When there are many servers or they change frequently, dynamic inventory can be used:

#!/usr/bin/env python3
# dynamic_inventory.py
import json
import boto3

def get_inventory():
    ec2 = boto3.client('ec2', region_name='us-west-2')
    response = ec2.describe_instances(
        Filters=[
            {'Name': 'tag:Environment', 'Values': ['production']},
            {'Name': 'instance-state-name', 'Values': ['running']}
        ]
    )
    inventory = {
        'webservers': {
            'hosts': [],
            'vars': {
                'ansible_user': 'ubuntu',
                'ansible_ssh_private_key_file': '~/.ssh/aws-key.pem'
            }
        }
    }
    for reservation in response['Reservations']:
        for instance in reservation['Instances']:
            inventory['webservers']['hosts'].append(instance['PublicIpAddress'])
    return inventory

if __name__ == '__main__':
    print(json.dumps(get_inventory()))

4.4 Performance Optimization Techniques

When managing large-scale infrastructure, performance optimization is crucial:

# ansible.cfg
[defaults]
host_key_checking = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_cache
fact_caching_timeout = 86400
pipelining = True
forks = 50

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

5. Practical Case: Building a Complete CI/CD Process

Let’s go through a complete case to demonstrate how to integrate Ansible into the CI/CD process:

---
# deploy_pipeline.yml
- name: Complete Deployment Process
  hosts: webservers
  become: yes
  vars:
    app_name: mywebapp
    app_version: "{{ lookup('env', 'BUILD_NUMBER') | default('latest') }}"
    deploy_user: webapp
    deploy_dir: /opt/{{ app_name }}
    backup_dir: /opt/backups/{{ app_name }}
  tasks:
    - name: Create deployment user
      user:
        name: "{{ deploy_user }}"
        shell: /bin/bash
        groups: www-data
        append: yes
    - name: Create necessary directories
      file:
        path: "{{ item }}"
        state: directory
        owner: "{{ deploy_user }}"
        group: "{{ deploy_user }}"
        mode: '0755'
      loop:
        - "{{ deploy_dir }}"
        - "{{ backup_dir }}"
        - /var/log/{{ app_name }}
    - name: Backup current version
      archive:
        path: "{{ deploy_dir }}"
        dest: "{{ backup_dir }}/backup-{{ ansible_date_time.epoch }}.tar.gz"
      when: deploy_dir is directory
    - name: Pull latest code
      git:
        repo: "https://github.com/company/{{ app_name }}.git"
        dest: "{{ deploy_dir }}"
        version: "{{ app_version }}"
        force: yes
      become_user: "{{ deploy_user }}"
    - name: Install application dependencies
      pip:
        requirements: "{{ deploy_dir }}/requirements.txt"
        virtualenv: "{{ deploy_dir }}/venv"
        virtualenv_python: python3
      become_user: "{{ deploy_user }}"
    - name: Run database migrations
      command: |
        {{ deploy_dir }}/venv/bin/python manage.py migrate
      args:
        chdir: "{{ deploy_dir }}"
      become_user: "{{ deploy_user }}"
      run_once: true
    - name: Collect static files
      command: |
        {{ deploy_dir }}/venv/bin/python manage.py collectstatic --noinput
      args:
        chdir: "{{ deploy_dir }}"
      become_user: "{{ deploy_user }}"
    - name: Configure Systemd service
      template:
        src: app.service.j2
        dest: /etc/systemd/system/{{ app_name }}.service
      notify:
        - reload systemd
        - restart app
    - name: Configure Nginx reverse proxy
      template:
        src: nginx_app.conf.j2
        dest: /etc/nginx/sites-available/{{ app_name }}
      notify: reload nginx
    - name: Enable site
      file:
        src: /etc/nginx/sites-available/{{ app_name }}
        dest: /etc/nginx/sites-enabled/{{ app_name }}
        state: link
      notify: reload nginx
    - name: Run smoke test
      uri:
        url: "http://localhost/api/health"
        status_code: 200
        retries: 5
        delay: 10
  handlers:
    - name: reload systemd
      systemd:
        daemon_reload: yes
    - name: restart app
      systemd:
        name: "{{ app_name }}"
        state: restarted
        enabled: yes
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

6. Monitoring and Logging: Ensuring Observability of Automation

Automation is not a “one-time effort”; we need continuous monitoring:

---
# monitoring.yml
- name: Configure monitoring and log collection
  hosts: webservers
  become: yes
  tasks:
    - name: Install monitoring agents
      package:
        name:
          - prometheus-node-exporter
          - filebeat
        state: present
    - name: Configure Prometheus Node Exporter
      lineinfile:
        path: /etc/default/prometheus-node-exporter
        regexp: '^ARGS='
        line: 'ARGS="--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|run)($|/)"'
      notify: restart node-exporter
    - name: Configure Filebeat
      template:
        src: filebeat.yml.j2
        dest: /etc/filebeat/filebeat.yml
        mode: '0600'
      notify: restart filebeat
    - name: Configure custom metric collection script
      copy:
        content: |
          #!/bin/bash
          # Collect application custom metrics
          echo "app_requests_total $(curl -s localhost/metrics | grep requests_total | awk '{print $2}')"
          echo "app_errors_total $(grep ERROR /var/log/{{ app_name }}/app.log | wc -l)"
          echo "app_response_time_seconds $(tail -n 100 /var/log/nginx/access.log | awk '{sum+=$10} END {print sum/NR}')"
        dest: /usr/local/bin/collect_metrics.sh
        mode: '0755'
    - name: Add metric collection cron job
      cron:
        name: "Collect application metrics"
        minute: "*/5"
        job: "/usr/local/bin/collect_metrics.sh > /var/lib/node_exporter/textfile_collector/app_metrics.prom"
  handlers:
    - name: restart node-exporter
      service:
        name: prometheus-node-exporter
        state: restarted
    - name: restart filebeat
      service:
        name: filebeat
        state: restarted

7. Disaster Recovery: When Things Go Wrong

Even the most refined automation can encounter issues. Let’s prepare a quick rollback plan:

---
# rollback.yml
- name: Emergency Rollback Procedure
  hosts: webservers
  become: yes
  serial: 1
  vars_prompt:
    - name: confirm_rollback
      prompt: "Are you sure you want to roll back to the previous version? (yes/no)"
      private: no
  tasks:
    - name: Validate confirmation
      fail:
        msg: "Rollback operation has been canceled"
      when: confirm_rollback != "yes"
    - name: Find the latest backup
      find:
        paths: "{{ backup_dir }}"
        patterns: "backup-*.tar.gz"
      register: backup_files
    - name: Ensure available backup
      fail:
        msg: "No available backup files found"
      when: backup_files.files | length == 0
    - name: Get latest backup
      set_fact:
        latest_backup: "{{ (backup_files.files | sort(attribute='mtime') | last).path }}"
    - name: Stop application service
      systemd:
        name: "{{ app_name }}"
        state: stopped
    - name: Clean current version
      file:
        path: "{{ deploy_dir }}"
        state: absent
    - name: Restore backup
      unarchive:
        src: "{{ latest_backup }}"
        dest: /opt/
        remote_src: yes
    - name: Start application service
      systemd:
        name: "{{ app_name }}"
        state: started
    - name: Validate service status
      uri:
        url: "http://localhost/api/health"
        status_code: 200
        retries: 3
        delay: 5
    - name: Send rollback notification
      mail:
        to: [email protected]
        subject: "Emergency Rollback Completed - {{ ansible_hostname }}"
        body: "Server {{ ansible_hostname }} has successfully rolled back to backup version: {{ latest_backup }}"
      delegate_to: localhost

Conclusion: The Transformation from Manual to Automated

Through this article, we have experienced the complete journey from traditional manual operations to Ansible automation. Let’s review the key takeaways:

  1. Efficiency Improvement: Tasks that originally took hours to deploy now only take a few minutes
  2. Consistency Assurance: Code-based configuration management eliminates environmental differences
  3. Traceability: Every change is recorded, facilitating audits and troubleshooting
  4. Knowledge Preservation: Operational experience is transformed into reusable Playbooks
  5. Risk Reduction: Automation reduces human errors, and rollback mechanisms ensure business continuity

But this is just the beginning. The Ansible ecosystem is far richer than what we explored today:

  • Ansible Tower/AWX provides an enterprise-level management interface
  • Ansible Galaxy shares thousands of ready-made roles from the community
  • Deep integration with Kubernetes, Docker, and cloud platforms
  • Automation configuration for network devices, databases, and middleware

Remember, automation is not the goal; it is a means to allow us to focus on more valuable work. When you are no longer bound by repetitive tasks, you have more time to think about architecture optimization, performance tuning, and security hardening—work that truly reflects the value of operations.

Leave a Comment