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.j2

    ServerName {{ 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