Web Server Management
What is Web Server Management? Ansible automates the installation, configuration, and management of web servers including Apache, Nginx, and HAProxy. Deploy virtual hosts, configure SSL/TLS, manage load balancers, and maintain web infrastructure with repeatable, version-controlled playbooks.
Understanding Web Server Automation
Why Automate Web Servers?
Web server automation provides critical benefits:
- Consistency: Identical configurations across all servers
- Scalability: Quickly provision new web servers
- Security: Enforce SSL/TLS and security headers
- Version Control: Track configuration changes
- Disaster Recovery: Rapid server replacement
Common Use Cases
- Install and configure web servers
- Deploy SSL/TLS certificates
- Manage virtual hosts and sites
- Configure load balancers
- Set up reverse proxies
- Apply security hardening
Apache HTTP Server
Install and Configure Apache
---
- name: Setup Apache web server
hosts: webservers
become: true
tasks:
- name: Install Apache (RHEL/CentOS)
ansible.builtin.yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
- name: Install Apache (Ubuntu/Debian)
ansible.builtin.apt:
name: apache2
state: present
update_cache: yes
when: ansible_os_family == "Debian"
- name: Start and enable Apache
ansible.builtin.service:
name: "{{ 'httpd' if ansible_os_family == 'RedHat' else 'apache2' }}"
state: started
enabled: true
- name: Configure Apache main config
ansible.builtin.template:
src: httpd.conf.j2
dest: "{{ apache_config_path }}/httpd.conf"
validate: 'apachectl -t -f %s'
notify: restart apache
- name: Open HTTP port in firewall
ansible.posix.firewalld:
service: http
permanent: true
state: enabled
immediate: true
when: ansible_os_family == "RedHat"
handlers:
- name: restart apache
ansible.builtin.service:
name: "{{ 'httpd' if ansible_os_family == 'RedHat' else 'apache2' }}"
state: restarted
Create Apache Virtual Hosts
---
- name: Configure Apache virtual hosts
hosts: webservers
become: true
vars:
sites:
- name: example.com
port: 80
document_root: /var/www/example.com
admin_email: admin@example.com
- name: app.example.com
port: 80
document_root: /var/www/app
admin_email: admin@example.com
tasks:
- name: Create document root directories
ansible.builtin.file:
path: "{{ item.document_root }}"
state: directory
owner: apache
group: apache
mode: '0755'
loop: "{{ sites }}"
- name: Deploy virtual host configurations
ansible.builtin.template:
src: vhost.conf.j2
dest: "/etc/httpd/conf.d/{{ item.name }}.conf"
owner: root
group: root
mode: '0644'
loop: "{{ sites }}"
notify: reload apache
- name: Enable sites (Debian/Ubuntu)
ansible.builtin.file:
src: "/etc/apache2/sites-available/{{ item.name }}.conf"
dest: "/etc/apache2/sites-enabled/{{ item.name }}.conf"
state: link
loop: "{{ sites }}"
when: ansible_os_family == "Debian"
notify: reload apache
handlers:
- name: reload apache
ansible.builtin.service:
name: "{{ 'httpd' if ansible_os_family == 'RedHat' else 'apache2' }}"
state: reloaded
Apache Virtual Host Template
# templates/vhost.conf.j2ServerName {{ item.name }} {% if item.aliases is defined %} ServerAlias {{ item.aliases | join(' ') }} {% endif %} ServerAdmin {{ item.admin_email }} DocumentRoot {{ item.document_root }} Options -Indexes +FollowSymLinks AllowOverride All Require all granted {% if item.enable_ssl | default(false) %} SSLEngine on SSLCertificateFile {{ ssl_cert_dir }}/{{ item.name }}.crt SSLCertificateKeyFile {{ ssl_cert_dir }}/{{ item.name }}.key {% if item.ssl_chain is defined %} SSLCertificateChainFile {{ ssl_cert_dir }}/{{ item.ssl_chain }} {% endif %} {% endif %} ErrorLog /var/log/httpd/{{ item.name }}-error.log CustomLog /var/log/httpd/{{ item.name }}-access.log combined {% if item.proxy_pass is defined %} ProxyPreserveHost On ProxyPass / {{ item.proxy_pass }}/ ProxyPassReverse / {{ item.proxy_pass }}/ {% endif %}
Enable Apache Modules
---
- name: Enable Apache modules
hosts: webservers
become: true
tasks:
- name: Enable SSL module
community.general.apache2_module:
name: ssl
state: present
notify: restart apache
when: ansible_os_family == "Debian"
- name: Enable rewrite module
community.general.apache2_module:
name: rewrite
state: present
notify: restart apache
- name: Enable proxy modules
community.general.apache2_module:
name: "{{ item }}"
state: present
loop:
- proxy
- proxy_http
- proxy_balancer
- lbmethod_byrequests
notify: restart apache
Nginx Web Server
Install and Configure Nginx
---
- name: Setup Nginx web server
hosts: webservers
become: true
tasks:
- name: Install Nginx
ansible.builtin.package:
name: nginx
state: present
- name: Start and enable Nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Configure nginx.conf
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: 'nginx -t -c %s'
notify: reload nginx
- name: Create sites directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
loop:
- /etc/nginx/sites-available
- /etc/nginx/sites-enabled
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Nginx Server Blocks (Virtual Hosts)
---
- name: Configure Nginx server blocks
hosts: webservers
become: true
vars:
sites:
- name: example.com
port: 80
root: /var/www/example.com
index: index.html index.htm
- name: api.example.com
port: 80
proxy_pass: http://localhost:8080
tasks:
- name: Create web root directories
ansible.builtin.file:
path: "{{ item.root }}"
state: directory
owner: nginx
group: nginx
mode: '0755'
loop: "{{ sites }}"
when: item.root is defined
- name: Deploy server block configurations
ansible.builtin.template:
src: nginx_server_block.j2
dest: "/etc/nginx/sites-available/{{ item.name }}.conf"
loop: "{{ sites }}"
notify: reload nginx
- name: Enable server blocks
ansible.builtin.file:
src: "/etc/nginx/sites-available/{{ item.name }}.conf"
dest: "/etc/nginx/sites-enabled/{{ item.name }}.conf"
state: link
loop: "{{ sites }}"
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Nginx Server Block Template
# templates/nginx_server_block.j2
server {
listen {{ item.port | default(80) }};
{% if item.ssl | default(false) %}
listen 443 ssl http2;
{% endif %}
server_name {{ item.name }};
{% if item.ssl | default(false) %}
ssl_certificate {{ ssl_cert_dir }}/{{ item.name }}.crt;
ssl_certificate_key {{ ssl_cert_dir }}/{{ item.name }}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
{% endif %}
{% if item.root is defined %}
root {{ item.root }};
index {{ item.index | default('index.html index.htm') }};
location / {
try_files $uri $uri/ =404;
}
{% endif %}
{% if item.proxy_pass is defined %}
location / {
proxy_pass {{ item.proxy_pass }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
{% endif %}
{% if item.php | default(false) %}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/www.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
{% endif %}
access_log /var/log/nginx/{{ item.name }}-access.log;
error_log /var/log/nginx/{{ item.name }}-error.log;
}
Nginx as Reverse Proxy
---
- name: Configure Nginx as reverse proxy
hosts: proxy_servers
become: true
tasks:
- name: Deploy reverse proxy configuration
ansible.builtin.template:
src: reverse_proxy.conf.j2
dest: /etc/nginx/conf.d/reverse_proxy.conf
notify: reload nginx
# templates/reverse_proxy.conf.j2
upstream backend_servers {
{% for host in groups['app_servers'] %}
server {{ hostvars[host]['ansible_host'] }}:8080;
{% endfor %}
}
server {
listen 80;
server_name {{ proxy_domain }};
location / {
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
HAProxy Load Balancer
Install and Configure HAProxy
---
- name: Setup HAProxy load balancer
hosts: loadbalancers
become: true
tasks:
- name: Install HAProxy
ansible.builtin.package:
name: haproxy
state: present
- name: Configure HAProxy
ansible.builtin.template:
src: haproxy.cfg.j2
dest: /etc/haproxy/haproxy.cfg
validate: 'haproxy -c -f %s'
notify: restart haproxy
- name: Start and enable HAProxy
ansible.builtin.service:
name: haproxy
state: started
enabled: true
- name: Open HAProxy stats port
ansible.posix.firewalld:
port: 8404/tcp
permanent: true
state: enabled
immediate: true
handlers:
- name: restart haproxy
ansible.builtin.service:
name: haproxy
state: restarted
HAProxy Configuration Template
# templates/haproxy.cfg.j2
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
# SSL/TLS settings
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
# Stats page
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
# HTTP frontend
frontend http_front
bind *:80
{% if enable_ssl | default(false) %}
bind *:443 ssl crt /etc/haproxy/certs/
redirect scheme https if !{ ssl_fc }
{% endif %}
# ACLs for routing
{% for site in sites | default([]) %}
acl host_{{ site.name | replace('.', '_') }} hdr(host) -i {{ site.name }}
use_backend {{ site.backend }} if host_{{ site.name | replace('.', '_') }}
{% endfor %}
default_backend app_servers
# Backend servers
backend app_servers
balance {{ balance_method | default('roundrobin') }}
option httpchk GET /health
{% for host in groups['app_servers'] %}
server {{ hostvars[host]['inventory_hostname_short'] }} {{ hostvars[host]['ansible_host'] }}:{{ app_port | default(8080) }} check
{% endfor %}
{% for backend in backends | default([]) %}
backend {{ backend.name }}
balance {{ backend.balance | default('roundrobin') }}
{% if backend.health_check is defined %}
option httpchk {{ backend.health_check }}
{% endif %}
{% for server in backend.servers %}
server {{ server.name }} {{ server.address }}:{{ server.port }} check
{% endfor %}
{% endfor %}
SSL/TLS Configuration
Install Let's Encrypt Certificates
---
- name: Install SSL certificates with certbot
hosts: webservers
become: true
tasks:
- name: Install certbot
ansible.builtin.package:
name:
- certbot
- "{{ 'python3-certbot-nginx' if web_server == 'nginx' else 'python3-certbot-apache' }}"
state: present
- name: Obtain SSL certificate
ansible.builtin.command: >
certbot --{{ web_server }} -d {{ domain_name }}
--non-interactive --agree-tos
--email {{ admin_email }}
args:
creates: "/etc/letsencrypt/live/{{ domain_name }}/fullchain.pem"
- name: Set up certificate renewal cron job
ansible.builtin.cron:
name: "Renew Let's Encrypt certificates"
minute: "0"
hour: "2"
job: "certbot renew --quiet"
Deploy Custom SSL Certificates
---
- name: Deploy custom SSL certificates
hosts: webservers
become: true
tasks:
- name: Create SSL directory
ansible.builtin.file:
path: /etc/ssl/private
state: directory
mode: '0700'
- name: Copy SSL certificate
ansible.builtin.copy:
src: "certs/{{ domain_name }}.crt"
dest: "/etc/ssl/certs/{{ domain_name }}.crt"
owner: root
group: root
mode: '0644'
notify: reload webserver
- name: Copy SSL private key
ansible.builtin.copy:
src: "certs/{{ domain_name }}.key"
dest: "/etc/ssl/private/{{ domain_name }}.key"
owner: root
group: root
mode: '0600'
notify: reload webserver
- name: Copy SSL chain
ansible.builtin.copy:
src: "certs/{{ domain_name }}-chain.crt"
dest: "/etc/ssl/certs/{{ domain_name }}-chain.crt"
owner: root
group: root
mode: '0644'
notify: reload webserver
Security Hardening
Apply Security Headers
---
- name: Configure security headers (Nginx)
hosts: nginx_servers
become: true
tasks:
- name: Add security headers
ansible.builtin.blockinfile:
path: /etc/nginx/conf.d/security_headers.conf
create: yes
block: |
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
notify: reload nginx
Configure Firewall
---
- name: Configure firewall for web servers
hosts: webservers
become: true
tasks:
- name: Allow HTTP traffic
ansible.posix.firewalld:
service: http
permanent: true
state: enabled
immediate: true
- name: Allow HTTPS traffic
ansible.posix.firewalld:
service: https
permanent: true
state: enabled
immediate: true
- name: Restrict SSH to management network
ansible.posix.firewalld:
source: 10.0.0.0/8
service: ssh
permanent: true
state: enabled
immediate: true
Best Practices
Web Server Best Practices:
- Use Templates: Manage configurations with Jinja2 templates
- Validate Configs: Use validate parameter before applying
- Enable SSL/TLS: Always use HTTPS in production
- Security Headers: Implement security headers
- Log Rotation: Configure log rotation for all services
- Monitoring: Set up health checks and monitoring
- Backup Configs: Keep backups before changes
- Test Changes: Test in staging before production
Common Issues
Configuration Syntax Errors
- Always use validate parameter in template module
- Test configurations with apachectl -t or nginx -t
- Keep backup of working configurations
Permission Denied
- Check file ownership and permissions
- Verify SELinux context (use ansible.posix.sefcontext)
- Ensure web server user has access to document roots
Port Already in Use
- Check for existing services on port 80/443
- Use different ports or stop conflicting services
- Verify firewall rules allow traffic
Next Steps
- Templates - Master Jinja2 templating
- Ansible Vault - Secure SSL certificates
- Docker - Containerized web servers
- Cloud Deployment - Web servers in cloud
- Try the Playground - Practice web server tasks