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:
- Batch installation of Nginx
- Deployment of custom configurations
- Deployment of static websites
- 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:
- Efficiency Improvement: Tasks that originally took hours to deploy now only take a few minutes
- Consistency Assurance: Code-based configuration management eliminates environmental differences
- Traceability: Every change is recorded, facilitating audits and troubleshooting
- Knowledge Preservation: Operational experience is transformed into reusable Playbooks
- 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.