Operations 27 min read

Master Ansible Automation: Deploy Nginx Web Servers at Scale in Minutes

This comprehensive guide walks you through why Ansible is essential for modern ops, explains its agentless, declarative, and idempotent advantages, shows step‑by‑step environment setup, inventory creation, playbook and template writing, and covers advanced techniques like rolling updates, vault encryption, dynamic inventory, performance tuning, CI/CD integration, monitoring, and rollback strategies.

MaGe Linux Operations
MaGe Linux Operations
MaGe Linux Operations
Master Ansible Automation: Deploy Nginx Web Servers at Scale in Minutes

Ansible Automation Basics: Bulk Deploy Web Servers

Introduction – Why Every Ops Engineer Should Master Ansible

Remember the night you were woken at 3 am to manually SSH into 20 servers for a critical update? After two exhausting hours you vowed to find an automation tool. This article will change that workflow by teaching you Ansible from scratch through a real‑world Nginx deployment project.

Deploy Nginx on 50 servers in 10 minutes

One‑click rolling updates and rollbacks

Build reusable automation pipelines

Reduce repetitive work by over 90%

1. What Is Ansible and What Problems Does It Solve?

1.1 Traditional Ops Pain Points

Configuration drift : 100 servers should be identical, but ad‑hoc changes cause divergence and failures.

Scale challenges : Growing from 10 to 100 servers turns a 30‑minute deployment into a 5‑hour ordeal, increasing human error.

Knowledge transfer difficulty : When senior staff leave, only scattered shell scripts remain, leaving newcomers guessing task order and parameters.

1.2 Ansible Advantages

Agentless : No client software required; manage nodes over SSH.

Declarative : Describe the desired state instead of how to achieve it.

Idempotent : Re‑running yields the same result, avoiding duplicate actions.

Easy to learn : Simple YAML syntax lowers the learning curve.

Rich module library : 3000+ built‑in modules cover most automation scenarios.

2. Quick Start – Set Up Ansible in 15 Minutes

2.1 Environment Preparation

We create a lab with one control node and three managed nodes:

# inventory.ini (example)
[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

2.2 Install Ansible

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

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

# Or via pip (recommended for latest version)
sudo pip3 install ansible

# Verify installation
ansible --version

2.3 Configure SSH Password‑less Login

# Generate SSH key if needed
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 Create Inventory File

# inventory.ini
[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 connectivity:

ansible -i inventory.ini all -m ping

If every host returns "pong", the environment is ready.

3. Hands‑On Project – Bulk Deploy Nginx Web Servers

We will implement four tasks: install Nginx, deploy custom config, serve a static site, and enable rolling updates.

Batch install Nginx

Deploy custom configuration

Deploy static website

Implement rolling updates

3.1 Project Structure

nginx-deployment/
├── inventory.ini          # host list
├── ansible.cfg           # optional config
├── site.yml              # main playbook
├── roles/
│   └── nginx/
│       ├── tasks/main.yml
│       ├── templates/nginx.conf.j2
│       ├── templates/index.html.j2
│       ├── handlers/main.yml
│       └── vars/main.yml
└── group_vars/webservers.yml

3.2 Write the Playbook (site.yml)

---
- 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 Automated Deployment Demo"
  tasks:
    - name: Update package cache (Debian)
      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
      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 Configuration Templates

nginx.conf.j2 (simplified):

user www-data;
worker_processes {{ nginx_worker_processes }};
pid /run/nginx.pid;

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

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    gzip on;
    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;
        }
        location /health {
            access_log off;
            return 200 "healthy
";
            add_header Content-Type text/plain;
        }
    }
}

index.html.j2 (simplified):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>{{ website_title }}</title>
  <style>
    body {font-family: sans-serif; background: linear-gradient(135deg,#667eea,#764ba2); 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;}
  </style>
</head>
<body>
  <div class="container">
    <h1>🚀 {{ website_title }}</h1>
    <p>Congratulations! You have successfully deployed this page with 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>OS:</strong> {{ ansible_distribution }} {{ ansible_distribution_version }}</p>
      <p><strong>Deploy Time:</strong> {{ ansible_date_time.iso8601 }}</p>
    </div>
  </div>
</body>
</html>

3.4 Execute Deployment

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

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

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

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

4. Advanced Techniques – Strengthen Your Automation

4.1 Rolling Update Strategy

---
- name: Rolling update Web servers
  hosts: webservers
  become: yes
  serial: 1
  max_fail_percentage: 30
  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 Protect Sensitive Data with Ansible Vault

# Create encrypted file
ansible-vault create secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Example content of secrets.yml
 db_password: "SuperSecret123!"
 api_key: "sk-1234567890abcdef"

# Run playbook using vault password
ansible-playbook -i inventory.ini site.yml --ask-vault-pass

4.3 Dynamic Inventory (Python Example)

#!/usr/bin/env python3
import json, 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 Optimizations (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. Full CI/CD Pipeline Example

---
- name: Complete deployment pipeline
  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 required 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 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 & Logging – Ensure Observability

---
- 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: Deploy Filebeat configuration
      template:
        src: filebeat.yml.j2
        dest: /etc/filebeat/filebeat.yml
        mode: '0600'
      notify: restart filebeat
    - name: Add custom metric collection script
      copy:
        content: |
          #!/bin/bash
          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: Schedule metric collection every 5 minutes
      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. Failure Recovery – Quick Rollback Playbook

---
- name: Emergency rollback procedure
  hosts: webservers
  become: yes
  serial: 1
  vars_prompt:
    - name: confirm_rollback
      prompt: "Confirm rollback to previous version? (yes/no)"
      private: no
  tasks:
    - name: Verify confirmation
      fail:
        msg: "Rollback cancelled"
      when: confirm_rollback != "yes"
    - name: Find latest backup
      find:
        paths: "{{ backup_dir }}"
        patterns: "backup-*.tar.gz"
      register: backup_files
    - name: Ensure a backup exists
      fail:
        msg: "No backup files found"
      when: backup_files.files | length == 0
    - name: Get latest backup path
      set_fact:
        latest_backup: "{{ (backup_files.files | sort(attribute='mtime') | last).path }}"
    - name: Stop application service
      systemd:
        name: "{{ app_name }}"
        state: stopped
    - name: Remove 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: Verify service health
      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 }} successfully rolled back to {{ latest_backup }}"
      delegate_to: localhost

Conclusion – From Manual to Automated Ops

By following this article you have transformed a tedious manual deployment into a fast, reliable, and repeatable automation workflow. Key takeaways include massive efficiency gains, configuration consistency, full auditability, reusable playbooks, and reduced risk through automated rollbacks.

Beyond the basics, Ansible offers enterprise‑grade tools like Tower/AWX, a vast Galaxy role ecosystem, and deep integrations with Kubernetes, Docker, and cloud platforms, opening the door to true cloud‑native automation.

Next Steps

Practice immediately : Automate a simple repetitive task with Ansible.

Gradual rollout : Start in development, then expand to staging and production.

Continuous learning : Follow the official Ansible documentation and community best practices.

Share knowledge : Teach your team what you learned and grow together.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ci/cdNginxInfrastructure as CodeAnsible
MaGe Linux Operations
Written by

MaGe Linux Operations

Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.