Рецепт: Nginx + Let’s Encrypt SSL

Nginx как reverse proxy с автоматическим SSL-сертификатом через Certbot. HTTP → HTTPS redirect, auto-renewal.

Playbook

---
- name: Setup Nginx with SSL
  hosts: webservers
  become: yes
 
  vars:
    nginx_domain: app.example.com
    nginx_email: admin@example.com
    nginx_backend_port: 3000
    nginx_ssl_enabled: true
 
  tasks:
    # === NGINX ===
    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: yes
 
    - name: Remove default site
      ansible.builtin.file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: Reload Nginx
 
    - name: Deploy HTTP config (pre-SSL)
      ansible.builtin.copy:
        content: |
          server {
              listen 80;
              server_name {{ nginx_domain }};
 
              location /.well-known/acme-challenge/ {
                  root /var/www/certbot;
              }
 
              location / {
                  {% if nginx_ssl_enabled %}
                  return 301 https://$server_name$request_uri;
                  {% else %}
                  proxy_pass http://127.0.0.1:{{ nginx_backend_port }};
                  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 %}
              }
          }
        dest: "/etc/nginx/sites-available/{{ nginx_domain }}"
      notify: Reload Nginx
 
    - name: Enable site
      ansible.builtin.file:
        src: "/etc/nginx/sites-available/{{ nginx_domain }}"
        dest: "/etc/nginx/sites-enabled/{{ nginx_domain }}"
        state: link
      notify: Reload Nginx
 
    - name: Validate Nginx config
      ansible.builtin.command: nginx -t
      changed_when: false
 
    - name: Start Nginx
      ansible.builtin.systemd:
        name: nginx
        state: started
        enabled: yes
 
    # === CERTBOT ===
    - name: Install Certbot
      ansible.builtin.apt:
        name:
          - certbot
          - python3-certbot-nginx
        state: present
      when: nginx_ssl_enabled
 
    - name: Create webroot directory
      ansible.builtin.file:
        path: /var/www/certbot
        state: directory
        owner: www-data
      when: nginx_ssl_enabled
 
    - name: Check if certificate exists
      ansible.builtin.stat:
        path: "/etc/letsencrypt/live/{{ nginx_domain }}/fullchain.pem"
      register: cert_file
      when: nginx_ssl_enabled
 
    - name: Obtain SSL certificate
      ansible.builtin.command: >
        certbot certonly --nginx
        -d {{ nginx_domain }}
        --email {{ nginx_email }}
        --agree-tos
        --non-interactive
      when:
        - nginx_ssl_enabled
        - not cert_file.stat.exists | default(false)
 
    # === HTTPS CONFIG ===
    - name: Deploy HTTPS config
      ansible.builtin.copy:
        content: |
          # HTTP → HTTPS redirect
          server {
              listen 80;
              server_name {{ nginx_domain }};
 
              location /.well-known/acme-challenge/ {
                  root /var/www/certbot;
              }
 
              location / {
                  return 301 https://$server_name$request_uri;
              }
          }
 
          # HTTPS
          server {
              listen 443 ssl http2;
              server_name {{ nginx_domain }};
 
              ssl_certificate /etc/letsencrypt/live/{{ nginx_domain }}/fullchain.pem;
              ssl_certificate_key /etc/letsencrypt/live/{{ nginx_domain }}/privkey.pem;
 
              # SSL settings
              ssl_protocols TLSv1.2 TLSv1.3;
              ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
              ssl_prefer_server_ciphers off;
              ssl_session_cache shared:SSL:10m;
 
              # Security headers
              add_header X-Frame-Options DENY;
              add_header X-Content-Type-Options nosniff;
              add_header Strict-Transport-Security "max-age=63072000" always;
 
              location / {
                  proxy_pass http://127.0.0.1:{{ nginx_backend_port }};
                  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;
                  proxy_http_version 1.1;
                  proxy_set_header Upgrade $http_upgrade;
                  proxy_set_header Connection "upgrade";
              }
          }
        dest: "/etc/nginx/sites-available/{{ nginx_domain }}"
      notify: Reload Nginx
      when: nginx_ssl_enabled and (cert_file.stat.exists | default(false) or not cert_file.skipped | default(false))
 
    # === AUTO-RENEWAL ===
    - name: Setup auto-renewal cron
      ansible.builtin.cron:
        name: "Certbot auto-renewal"
        minute: "30"
        hour: "3"
        day: "*/7"
        job: "certbot renew --quiet --post-hook 'systemctl reload nginx'"
        user: root
      when: nginx_ssl_enabled
 
  handlers:
    - name: Reload Nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded

Запуск

# Без SSL (для тестирования)
ansible-playbook nginx.yml -e "nginx_ssl_enabled=false"
 
# С SSL (домен должен резолвиться на сервер)
ansible-playbook nginx.yml -e "nginx_domain=myapp.example.com nginx_email=me@example.com"

Типичные ошибки

ОшибкаРешение
Certbot не может получить сертификатDNS A-запись должна указывать на IP сервера. Порт 80 открыт
Nginx не стартует после добавления SSLСертификат ещё не получен. Сначала HTTP-конфиг, потом certbot, потом HTTPS-конфиг
Сертификат не обновляетсяПроверить cron: crontab -l. Убедиться что webroot доступен
ERR_TOO_MANY_REDIRECTSHTTP→HTTPS redirect loop. Проверить proxy_set_header X-Forwarded-Proto