← Назад к вопросам

Ansible playbook для настройки Nginx и firewall

2.3 Middle🔥 191 комментариев
#Ansible и управление конфигурацией

Условие

Напишите Ansible playbook, который:

  1. Устанавливает Nginx на группу серверов
  2. Копирует конфигурационный файл из шаблона (Jinja2)
  3. Настраивает firewall (ufw или firewalld), открывая порты 22, 80, 443
  4. Перезапускает Nginx при изменении конфигурации
  5. Включает Nginx в автозапуск

Требования

  • Используйте handlers для перезапуска сервисов
  • Применяйте tags для выборочного запуска задач
  • Добавьте проверку синтаксиса конфигурации перед перезапуском

Вопросы

  • Чем отличается Ansible от Terraform?
  • Как хранить секреты в Ansible?
  • Что такое идемпотентность и почему она важна?

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

1. Структура Ansible проекта

ansible-project/
├── inventory.ini                # Инвентарь хостов
├── ansible.cfg                  # Конфигурация Ansible
├── playbooks/
│   ├── site.yml                 # Главный playbook
│   └── nginx.yml                # Playbook для Nginx
├── roles/
│   ├── nginx/
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── templates/
│   │   │   └── nginx.conf.j2
│   │   ├── defaults/
│   │   │   └── main.yml
│   │   └── vars/
│   │       └── main.yml
│   └── firewall/
│       └── tasks/
│           └── main.yml
├── group_vars/
│   └── all.yml
└── host_vars/
    └── example.com.yml

2. inventory.ini - Инвентарь хостов

[webservers]
web1.example.com ansible_user=ubuntu ansible_password=secret
web2.example.com ansible_user=ubuntu ansible_password=secret
web3.example.com ansible_user=ubuntu ansible_password=secret

[webservers:vars]
ansible_become=yes
ansible_become_method=sudo
http_port=80
https_port=443

[all:vars]
ansible_python_interpreter=/usr/bin/python3

3. ansible.cfg - Конфигурация

[defaults]
host_key_checking = False
inventory = ./inventory.ini
roles_path = ./roles
host_key_checking = False

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

4. playbooks/nginx.yml - Main Playbook

---
- name: Configure web servers with Nginx and firewall
  hosts: webservers
  become: yes
  gather_facts: yes

  pre_tasks:
    - name: Update package cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
      tags:
        - packages
        - always

  roles:
    - role: firewall
      tags:
        - firewall
        - security
    - role: nginx
      tags:
        - webserver
        - nginx

  post_tasks:
    - name: Verify Nginx is running
      systemd:
        name: nginx
        state: started
        enabled: yes
      tags:
        - verify
        - nginx

    - name: Test Nginx connectivity
      uri:
        url: "http://localhost/"
        status_code: 200
      retries: 3
      delay: 5
      tags:
        - verify
        - health-check

5. roles/nginx/tasks/main.yml

---
- name: Install Nginx
  apt:
    name: nginx
    state: present
    update_cache: yes
  tags:
    - nginx
    - packages

  # После установки, если пакет был установлен, будет запущен handler
  notify: restart nginx

- name: Create Nginx config directory
  file:
    path: /etc/nginx/conf.d
    state: directory
    owner: root
    group: root
    mode: '0755'
  tags:
    - nginx
    - configuration

- name: Deploy Nginx configuration from template
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes
  notify: restart nginx
  tags:
    - nginx
    - configuration

- name: Deploy site configuration
  template:
    src: site.conf.j2
    dest: /etc/nginx/sites-available/default
    owner: root
    group: root
    mode: '0644'
  notify: reload nginx
  tags:
    - nginx
    - configuration

- name: Enable site
  file:
    src: /etc/nginx/sites-available/default
    dest: /etc/nginx/sites-enabled/default
    state: link
  notify: reload nginx
  tags:
    - nginx

- name: Verify Nginx configuration syntax
  command: nginx -t
  register: nginx_test
  changed_when: false
  tags:
    - nginx
    - verify

- name: Display Nginx syntax check result
  debug:
    msg: "{{ nginx_test.stdout }}"
  tags:
    - nginx
    - verify

- name: Start and enable Nginx service
  systemd:
    name: nginx
    state: started
    enabled: yes
    daemon_reload: yes
  tags:
    - nginx
    - service

- name: Create index.html
  copy:
    content: |
      <!DOCTYPE html>
      <html>
      <head>
        <title>Welcome to Nginx</title>
      </head>
      <body>
        <h1>Server: {{ ansible_hostname }}</h1>
        <p>IP Address: {{ ansible_default_ipv4.address }}</p>
      </body>
      </html>
    dest: /var/www/html/index.html
    owner: www-data
    group: www-data
    mode: '0644'
  tags:
    - nginx
    - content

6. roles/nginx/handlers/main.yml

---
# Handler для перезапуска Nginx после изменения конфигурации
- name: restart nginx
  systemd:
    name: nginx
    state: restarted
    daemon_reload: yes
  listen: restart nginx

# Handler для reload без прерывания соединений
- name: reload nginx
  systemd:
    name: nginx
    state: reloaded
  listen: reload nginx

# Handler для проверки конфигурации
- name: verify nginx config
  command: nginx -t
  register: nginx_test_result
  failed_when: nginx_test_result.rc != 0
  changed_when: false
  listen: verify nginx

7. roles/nginx/templates/nginx.conf.j2

user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log warn;

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

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    # Performance optimizations
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    client_max_body_size 20M;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml text/javascript 
               application/json application/javascript application/xml+rss 
               application/rss+xml application/atom+xml image/svg+xml 
               text/x-component text/x-cross-domain-policy;

    # Include virtual hosts
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

8. roles/nginx/templates/site.conf.j2

server {
    listen {{ http_port }} default_server;
    listen [::]:{{ http_port }} default_server;

    server_name {{ server_name | default('_') }};

    # Redirect HTTP to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen {{ https_port }} ssl http2 default_server;
    listen [::]:{{ https_port }} ssl http2 default_server;

    server_name {{ server_name | default('_') }};

    # SSL configuration
    ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
    ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Root directory
    root /var/www/html;
    index index.html index.htm index.nginx-debian.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "healthy\n";
        add_header Content-Type text/plain;
    }
}

9. roles/nginx/defaults/main.yml

---
http_port: 80
https_port: 443
server_name: "{{ ansible_fqdn }}"
worker_processes: auto
worker_connections: 768
keepalive_timeout: 65

10. roles/firewall/tasks/main.yml

---
# Определяем используемый firewall
- name: Check if firewalld is installed
  command: which firewall-cmd
  register: firewalld_check
  failed_when: false
  changed_when: false
  tags:
    - firewall
    - always

- name: Check if ufw is installed
  command: which ufw
  register: ufw_check
  failed_when: false
  changed_when: false
  tags:
    - firewall
    - always

# Конфигурация firewalld
- block:
    - name: Enable firewalld
      systemd:
        name: firewalld
        state: started
        enabled: yes
      tags:
        - firewall

    - name: Configure firewalld for SSH
      firewalld:
        service: ssh
        permanent: yes
        state: enabled
        immediate: yes
      tags:
        - firewall
        - ssh

    - name: Configure firewalld for HTTP
      firewalld:
        service: http
        permanent: yes
        state: enabled
        immediate: yes
      tags:
        - firewall
        - http

    - name: Configure firewalld for HTTPS
      firewalld:
        service: https
        permanent: yes
        state: enabled
        immediate: yes
      tags:
        - firewall
        - https

  when: firewalld_check.rc == 0
  tags:
    - firewall

# Конфигурация UFW (для Debian/Ubuntu)
- block:
    - name: Enable UFW
      ufw:
        state: enabled
        policy: deny
        direction: incoming
      tags:
        - firewall

    - name: Configure UFW for SSH
      ufw:
        rule: allow
        port: '22'
        proto: tcp
        comment: 'SSH access'
      tags:
        - firewall
        - ssh

    - name: Configure UFW for HTTP
      ufw:
        rule: allow
        port: '80'
        proto: tcp
        comment: 'HTTP access'
      tags:
        - firewall
        - http

    - name: Configure UFW for HTTPS
      ufw:
        rule: allow
        port: '443'
        proto: tcp
        comment: 'HTTPS access'
      tags:
        - firewall
        - https

    - name: Enable UFW
      ufw:
        state: enabled
      tags:
        - firewall

  when: ufw_check.rc == 0
  tags:
    - firewall

11. Команды для запуска playbook

# Синтаксис playbook
ansible-playbook playbooks/nginx.yml --syntax-check

# Dry-run (показать что будет изменено без реальных изменений)
ansible-playbook playbooks/nginx.yml --check

# Запуск всех задач
ansible-playbook playbooks/nginx.yml

# Запуск только задач с определенным тегом
ansible-playbook playbooks/nginx.yml --tags "firewall"
ansible-playbook playbooks/nginx.yml --tags "nginx,verify"

# Пропуск задач с определенным тегом
ansible-playbook playbooks/nginx.yml --skip-tags "health-check"

# Запуск для конкретного хоста
ansible-playbook playbooks/nginx.yml -l web1.example.com

# Вывод более подробных логов
ansible-playbook playbooks/nginx.yml -vv

# Запуск с определенным пользователем
ansible-playbook playbooks/nginx.yml --user=ansible --ask-pass

# Запуск с ask-become-pass (спросить пароль для sudo)
ansible-playbook playbooks/nginx.yml --ask-become-pass

12. group_vars/all.yml - Глобальные переменные

---
ansible_user: ubuntu
ansible_become: yes
ansible_become_method: sudo

http_port: 80
https_port: 443
server_name: "{{ ansible_fqdn }}"

nginx_worker_processes: auto
nginx_worker_connections: 768

13. host_vars/web1.example.com.yml - Переменные для конкретного хоста

---
server_name: web1.example.com
http_port: 8080  # Отличается от дефолтного

14. Чем отличается Ansible от Terraform?

Terraform

  • Цель: Provisioning инфраструктуры (IaC)
  • Что делает: Создает облачные ресурсы (EC2, VPC, RDS, и т.д.)
  • Подход: Декларативный ("ВОТ какая должна быть инфраструктура")
  • State: Ведет state файл с текущим состоянием
  • Идемпотентность: Встроена - применение плана несколько раз = одинаковый результат
  • Скорость: Быстро для provisioning, может быть медленным для конфигурации
# Terraform - создание инфраструктуры
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}

Ansible

  • Цель: Configuration Management и Orchestration
  • Что делает: Настраивает ОС и приложения на существующих серверах
  • Подход: Procedural ("ДЕЛАЙ это, потом это, потом это")
  • State: Не ведет state файл, полагается на idempotent модули
  • Идемпотентность: Зависит от модулей (большинство идемпотентны)
  • Скорость: Медленнее при первом запуске, быстрее при перезапусках
# Ansible - конфигурация серверов
- name: Install and configure Nginx
  hosts: webservers
  tasks:
    - name: Install Nginx
      apt:
        name: nginx
        state: present

Сравнительная таблица

АспектTerraformAnsible
ТипIaC (Infrastructure)CM (Configuration)
ПодходДекларативныйПроцедурный
StateВедет state файлНет state файла
АгентБез агентаБез агента (SSH)
Обучение криваяСредняяНизкая
Скорость provisioningБыстроМедленно
Скорость переконфигурацииМедленноБыстро при повторе
Лучше всего дляОблако инфраКонфигурация серверов

Когда использовать?

# Terraform -> Ansible
Terraform (создает EC2) -> Ansible (устанавливает Nginx на EC2)

# Практический пример
terraform apply  # Создает инстансы
ansible-playbook playbooks/nginx.yml  # Конфигурирует их

15. Хранение секретов в Ansible

Вариант 1: ansible-vault (встроено)

# Шифрование файла с пароль
ansible-vault create secrets/passwords.yml
# Введите пароль при запросе

# Содержимое файла
database_password: "super_secret_password"
api_key: "sk_live_xxxxx"

# Использование в playbook
- name: Configure database
  mysql_user:
    name: app_user
    password: "{{ database_password }}"  # Из зашифрованного файла

# Запуск playbook с vault
ansible-playbook playbooks/nginx.yml --ask-vault-pass
ansible-playbook playbooks/nginx.yml --vault-password-file=/path/to/vault/pass

# Редактирование зашифрованного файла
ansible-vault edit secrets/passwords.yml

# Просмотр зашифрованного файла
ansible-vault view secrets/passwords.yml

# Переключение пароля vault
ansible-vault rekey secrets/passwords.yml

Вариант 2: Environment переменные

# В shell
export ADMIN_PASSWORD="secret123"

# В playbook
- name: Configure app
  environment:
    API_KEY: "{{ lookup('env', 'API_KEY') }}"

Вариант 3: HashiCorp Vault

- name: Get secret from HashiCorp Vault
  set_fact:
    db_password: "{{ lookup('hashi_vault', 'secret=secret/data/database:password') }}"

- name: Configure database
  mysql_user:
    password: "{{ db_password }}"

Вариант 4: AWS Secrets Manager

- name: Get AWS secret
  set_fact:
    db_password: "{{ lookup('amazon.aws.aws_secret', 'my-secret') }}"

Best practices

# .gitignore
secrets/
*.vault
.env
.env.local

# Не коммитьте незашифрованные секреты!
# ВСЕГДА используйте ansible-vault

16. Идемпотентность и почему она важна

Определение

Идемпотентность — свойство задачи, при котором многократное применение дает тот же результат, что и однократное применение.

# ✅ ИДЕМПОТЕНТНАЯ задача
- name: Install Nginx
  apt:
    name: nginx
    state: present

# Результаты:
# 1-й запуск: Установит Nginx (changed: true)
# 2-й запуск: Nginx уже установлен, не будет изменений (changed: false)
# 3-й запуск: Nginx уже установлен, не будет изменений (changed: false)
# Результат всегда одинаков!

# ❌ НЕ идемпотентная задача
- name: Add line to file (using shell)
  shell: echo "new_line" >> /etc/config.txt

# Результаты:
# 1-й запуск: Добавит строку (changed: true)
# 2-й запуск: Добавит еще одну строку (changed: true)
# 3-й запуск: Добавит ЕЩЕ одну строку (changed: true)
# Результат различается! Это НЕ идемпотентно

Почему это важно?

  1. Безопасность: Можно запустить playbook несколько раз без побочных эффектов
  2. Восстановление: При сбое можно пересчитать нужные части
  3. Масштабируемость: Можно применять один playbook к новым серверам
  4. CI/CD: Можно запускать playbook при каждом коммите

Примеры идемпотентных модулей

# ✅ apt - идемпотентен
- apt:
    name: nginx
    state: present

# ✅ file - идемпотентен
- file:
    path: /var/myapp
    state: directory
    mode: '0755'

# ✅ systemd - идемпотентен (если использовать правильно)
- systemd:
    name: nginx
    state: started
    enabled: yes

# ✅ lineinfile - идемпотентен
- lineinfile:
    path: /etc/config.txt
    line: 'param=value'
    state: present

# ❌ shell/command - НЕ идемпотентны (обычно)
- shell: |
    echo "data" > /tmp/file.txt
    # Каждый запуск перезапишет файл - это ОК в данном случае
    # Но если это какая-то команда с побочными эффектами - проблема

Как писать идемпотентные playbook

# ❌ ПЛОХО
- name: Restart Nginx
  shell: systemctl restart nginx
  # При каждом запуске perезапускает Nginx

# ✅ ХОРОШО
- name: Restart Nginx
  systemd:
    name: nginx
    state: restarted
  when: nginx_config_changed
  # Перезапускает только если конфигурация изменилась

# ✅ ЛУЧШЕ - используйте handlers
- name: Update Nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: restart nginx  # Handler запустится только если файл изменился

- name: restart nginx
  systemd:
    name: nginx
    state: restarted
  listen: restart nginx  # Это handler
Ansible playbook для настройки Nginx и firewall | PrepBro