Ansible: пример установки NGINX

Автор: | 09/20/2017
 

Задача — установить NGINX на удалённый хост с помощью Ansbile.

Подготовка

На рабочей машине — устанавливаем Ansible:

sudo pacman -S ansible

В Jenkins — будем использовать Docker образ (или плагин).

Проверяем:

ansible --version
ansible 2.3.2.0
config file = /etc/ansible/ansible.cfg
configured module search path = Default w/o overrides
python version = 2.7.13 (default, Jul 21 2017, 03:24:34) [GCC 7.1.1 20170630]

Создание Inventory

Пока есть только один Dev сервер, позже добавим Production.

Документация по Ansbile Inventoryтут>>>.

В корне репозитория создаём каталоги:

mkdir -p ansible/jm-monitoring
cd ansible/jm-monitoring/

Переходим в него, и создаём файл hosts в который добавляем URL-ы серверов:

[dev]
dev.monitor.domain.ms

[production]
monitor.domain.ms

hosts у меня в INI, хотя остальные конфиги в YAML — Ansible позволяет использовать оба формата одновременно.

Тут же добавляем переменную user, что бы не указывать его каждый раз (по умолчанию — Ansbile использует имя пользователя, от которого запускается):

[dev]
dev.monitor.domain.ms ansible_connection=ssh ansible_user=jmadmin 

[production]
monitor.domain.ms

Далее — указываем Ansible где искать файл hosts. Создаём файл ansible.cfg с блоком [defaults]:

[defaults]
inventory = hosts

Выполнение команд

Теперь, когда inventory создан — можно начать выполнение команд на хостах.

Попробуем пропинговать Dev — используем модуль ping и с помощью опции --private-key — путь к RSA ключу сервера:

ansible dev -m ping --private-key=../../.ssh/jm-monitoring
dev.monitor.domain.ms | SUCCESS => {
"changed": false,
"ping": "pong"
}

Что бы выполнить произвольную команду на хосте, без использования модуля — используем -a:

ansible dev -a uptime --private-key=../../.ssh/jm-monitoring
dev.monitor.domain.ms | SUCCESS | rc=0 >>
14:03:49 up 31 min,  2 users,  load average: 0.00, 0.01, 0.00

Playbook

Документация по плейбукам — тут>>>, и когда-то переводил вот тут>>>.

Создаём файл плейбука с именем provision.yml:

- hosts: all
  become:  
    true
  become_method: 
    sudo
  roles:
   - nginx

Тут:

...
become:
  true
...

Выполнять от root.

...
become_method:
  sudo
...

Использовать sudo (хотя sudo используется по умолчанию).

Roles

В provision.yml мы добавили роль nginx, которая будет влючать в себя установку NGINX.

Позже — добавим роли для установки Prometheus, Docker, настройки окружения и т.д.

Для каждой роли — Ansible будет искать стандартный набор каталогов — files, tasks, handlers и т.д.

Для nginx роли создаём каталог tasks — пока этого хватит:

mkdir -p roles/nginx/tasks

Структура сейчас выглядит так:

tree -d 1 roles/
1 [error opening dir]
roles/
└── nginx
└── tasks

В каждом из каталогов Ansible будет искать файл main.yml — создаём файл roles/nginx/tasks/main.yml и описываем в нём задачу:

- name: Install Nginx
  package:
    name: nginx
    state: latest

Тут мы используем модуль package, который используется менеджер пакетов ОС.

Запускаем плейбук на выполнение.

Используем --limit, что бы указать хосты (тут — dev, один сервер из hosts файла созданного ранее), и --private-key, как ранее с absible:

ansible-playbook --limit=dev --private-key=../../.ssh/jm-monitoring provision.yml
PLAY [all] ****
TASK [Gathering Facts] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Install Nginx] ****
changed: [dev.monitor.domain.ms]
PLAY RECAP ****
dev.monitor.domain.ms         : ok=2    changed=1    unreachable=0    failed=0

Готово.

Проверяем состояние сервиса:

ansible dev -a 'systemctl status nginx.service' --private-key=../../.ssh/jm-monitoring
dev.monitor.domain.ms | SUCCESS | rc=0 >>
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2017-09-19 15:05:06 UTC; 2min 51s ago
Main PID: 7198 (nginx)
CGroup: /system.slice/nginx.service
├─7198 nginx: master process /usr/sbin/nginx -g daemon on; master_process on
└─7199 nginx: worker process

Проверяем состояние NGINX:

curl -sI dev.monitor.domain.ms
HTTP/1.1 200 OK
Server: nginx/1.10.3 (Ubuntu)
Date: Tue, 19 Sep 2017 15:07:03 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 19 Sep 2017 15:05:05 GMT
Connection: keep-alive
ETag: "59c13221-264"
Accept-Ranges: bytes

Переменные

Следующим шагом — требуется добавить файл настроек виртуалхоста. Реализовываться это будет через templates, но перед тем, как приступить к их написанию — необходимо добавить некоторые переменные, которые будут использоваться для наполнения шаблонов.

Переменные в Ansible могут быть заданы в плейбуке, в inventory-файле, в tasks. Важную роль играет приоритет и порядок задания переменных. Подробнее — тут>>>.

Ещё один момент — это область действия переменных:

  1. Global: глобальные переменные, заданные через ansible.cfg, переменные окружения и командную строку
  2. Play: переменные, определённые для плейбуков и ролей
  3. Host: и переменные для хостов, заданные через inventory или tasks

Создаём каталог vars:

mkdir roles/nginx/vars

Для файла nginx.conf роли nginx пока зададим такие переменные в файле main.yml:

listen_http: 80
listen_https: 443
nginx_user: www-data
worker_processes: auto
worker_connections: 512

Это — глобальные переменные роли nginx, значения которых будут одинаковы на всех хостах с NGINX.

Пока с переменными всё — переходим к шаблону.

Templates

Шаблонизатор Ansible — Jinja2. Документация — тут>>>.

Нам потребуется два шаблона — nginx.conf, что бы переопределить логгирование (добавим log proxy), и шаблон для виртуалхоста (хотя пока бекендов нет — будем редиректить на Google).

Создаём каталог templates:

mkdir roles/nginx/templates

В нём создаём файл nginx.conf:

user {{ nginx_user: }};
worker_processes {{ worker_processes }};
pid /run/nginx.pid;

events {
        worker_connections {{ worker_connections }} ;
}

http {

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;

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

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        log_format proxy  '[$time_local] $remote_addr - $server_name to: '
                  '$upstream_addr: $request upstream_response_time '
                  '$upstream_response_time msec $msec request_time $request_time';

        gzip on;
        gzip_disable "msie6";

        include /etc/nginx/conf.d/*.conf;
}

Собственно — вот наши переменные:

user {{ nginx_user: }};

и т.д.

Далее — настройки виртуалхоста. Т.к. параметры в нём будут зависеть от хоста — добавим переменные в inventory файл, например — адрес upstream.

В hosts переменные могут быть добавлены для каждого хоста — Host Variables, например:

[dev]
dev.monitor.domain.ms ansible_connection=ssh ansible_user=jmadmin hostname=dev.monitor.domain.ms

Тут мы добавили переменную hostname=dev.monitor.domain.ms, которую потом используем для конфига виртуалхоста Dev хоста.

Либо для всех хостов в группе dev, Group Variables:

[dev]
dev.monitor.domain.ms ansible_connection=ssh ansible_user=jmadmin hostname=dev.monitor.domain.ms

[dev:vars] 
upstream_name-google 
upstream_url=google.com
...

Добавляем шаблон конфига виртуалхоста:

upstream {{ upstream_name }} {
    server {{ upstream_url }};
}

server {
       listen         {{ listen_http }} ;
       server_name    www.{{ hostname }};
       return         301 $scheme://{{ hostname }}$request_uri;
}

server {

    server_name {{ hostname }};

    listen {{ listen_http }} ;
#    listen  {{ listen_https }}  ssl;

    access_log /var/log/nginx/{{ hostname }}-access.log proxy;
    error_log /var/log/nginx/{{ hostname }}-error.log notice;

#    ssl on;
#    ssl_certificate /etc/letsencrypt/live/{{ hostname }}/fullchain.pem;
#    ssl_certificate_key /etc/letsencrypt/live/{{ hostname }}/privkey.pem;

    root /var/www/{{ hostname }};

    location ~ /.well-known {
        allow all;
    }

    location / {

        proxy_redirect          off;
        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_pass http://{{ upstream_name }}$request_uri;
    }
}

Настройки SSL пока закомментированы — добавим их чуть позже.

Возвращаемся к tasks, и добавляем копирование шаблонов:

- name: Install Nginx
  package:
    name: nginx
    state: latest

- name: Replace NGINX config
  template: 
    src=templates/nginx.conf
    dest=/etc/nginx/nginx.conf

- name: Add NGINX virtualhost config
  template:
    src=templates/nginx_vhost.conf
    dest=/etc/nginx/conf.d/{{ hostname }}.conf

На этом этапе уже можно проверить — всё ли будет работать. Для этого у ansible-playbook есть опция --check (dry-run), выполняем:

ansible-playbook --limit=dev --private-key=../../.ssh/jm-monitoring provision.yml --check
ERROR! Attempted to read "hosts" as YAML: Syntax Error while loading YAML.
The error appears to have been in '/home/setevoy/Work/BER.jm/azure-infrastructure/jm-monitoring/ansible/jm-monitoring/hosts': line 2, column 1, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
[dev]
dev.monitor.domain.ms ansible_connection=ssh ansible_user=jmadmin hostname=dev.monitor.domain.ms
^ here
Attempted to read "hosts" as ini file: hosts:5: Expected key=value, got: upstream_name-google

Эм…

А!

...
[dev:vars]
upstream_name-google
...

Тире вместо =.

Ещё раз:

ansible-playbook --limit=dev --private-key=../../.ssh/jm-monitoring provision.yml --check
PLAY [all] ****
TASK [Gathering Facts] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Install Nginx] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Replace NGINX config] ****
fatal: [dev.monitor.domain.ms]: FAILED! => {"changed": false, "failed": true, "msg": "AnsibleError: template error while templating string: expected token 'end of print statement', got ':'. String: user {{ nginx_user: }};\nworker_processes {{ worker_processes }};\npid /run/nginx.pid;\n\nevents {\n        worker_connections {{ worker_connections }} ;\n}\n\nhttp {\n\n        sendfile on;\n        tcp_nopush on;\n        tcp_nodelay on;\n        keepalive_timeout 65;\n        types_hash_max_size 2048;\n\n        include /etc/nginx/mime.types;\n        default_type application/octet-stream;\n\n        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n        ssl_prefer_server_ciphers on;\n\n        access_log /var/log/nginx/access.log;\n        error_log /var/log/nginx/error.log;\n\n        log_format proxy  '[$time_local] $remote_addr - $server_name to: '\n                  '$upstream_addr: $request upstream_response_time '\n                  '$upstream_response_time msec $msec request_time $request_time';\n\n        gzip on;\n        gzip_disable \"msie6\";\n\n        include /etc/nginx/conf.d/*.conf;\n}\n\n"}
to retry, use: --limit @/home/setevoy/Work/BER.jm/azure-infrastructure/jm-monitoring/ansible/jm-monitoring/provision.retry
PLAY RECAP ****
dev.monitor.domain.ms         : ok=2    changed=0    unreachable=0    failed=1

Теперь ошибка в шаблоне nginx.conf:

...
String: user {{ nginx_user: }};
...

Двоеточие.

И ещё раз:

ansible-playbook --limit=dev --private-key=../../.ssh/jm-monitoring provision.yml --check
PLAY [all] ****
TASK [Gathering Facts] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Install Nginx] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Replace NGINX config] ****
changed: [dev.monitor.domain.ms]
TASK [nginx : Add NGINX virtualhost config] ****
changed: [dev.monitor.domain.ms]
PLAY RECAP ****
dev.monitor.domain.ms         : ok=4    changed=2    unreachable=0    failed=0

Теперь всё нормально.

Handlers

После того, как конфиги NGINX будут скопированы на хост — надо указать NGINX на необходимость перечитать файлы настроек — выполнить nginx reload.

Тут используем handlers (обработчики).

Создаём каталог handlers:

mkdir roles/nginx/handlers

И файл main.yml, в котором используем модуль service:

- name: NGINX reload
  service: 
    name=nginx 
    state=reloaded

Далее в tasks требуется указать — когда выполнять reload, используем notify.

Возвращаемся к tasks, добавляем notify и имя обработчика:

- name: Install Nginx
  package:
    name: nginx
    state: latest

- name: Replace NGINX config
  template:
    src=templates/nginx.conf
    dest=/etc/nginx/nginx.conf
  notify:
    - NGINX reload

- name: Add NGINX virtualhost config
  template:
    src=templates/nginx_vhost.conf
    dest=/etc/nginx/conf.d/{{ hostname }}.conf
  notify:
    - NGINX reload

Вроде ОК — выполняем проверку ещё раз:

ansible-playbook --limit=dev --private-key=../../.ssh/jm-monitoring provision.yml --check
PLAY [all] ****
TASK [Gathering Facts] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Install Nginx] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Replace NGINX config] ****
changed: [dev.monitor.domain.ms]
TASK [nginx : Add NGINX virtualhost config] ****
changed: [dev.monitor.domain.ms]
RUNNING HANDLER [nginx : NGINX reload] ****
changed: [dev.monitor.domain.ms]
PLAY RECAP ****
dev.monitor.domain.ms         : ok=5    changed=3    unreachable=0    failed=0

Хорошо — запускаем на выполнение:

ansible-playbook --limit=dev --private-key=../../.ssh/jm-monitoring provision.yml
PLAY [all] ****
TASK [Gathering Facts] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Install Nginx] ****
ok: [dev.monitor.domain.ms]
TASK [nginx : Replace NGINX config] ****
changed: [dev.monitor.domain.ms]
TASK [nginx : Add NGINX virtualhost config] ****
changed: [dev.monitor.domain.ms]
RUNNING HANDLER [nginx : NGINX reload] ****
changed: [dev.monitor.domain.ms]
PLAY RECAP ****
dev.monitor.domain.ms         : ok=5    changed=3    unreachable=0    failed=0

Проверяем файл виртуахоста на сервере:

ansible dev -a 'ls -l /etc/nginx/conf.d' --private-key=../../.ssh/jm-monitoring
dev.monitor.domain.ms | SUCCESS | rc=0 >>
total 4
-rw-r--r-- 1 root root 985 Sep 20 10:00 dev.monitor.domain.ms.conf

Проверяем как работает редирект NGINX:

curl -s dev.monitor.domain.ms | grep google
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}
<a href=//www.google.com/><span id=logo aria-label=Google></span></a>

Всё отлично.

В принципе — на этом всё.

SSL и files настроим уже в следующий раз.

Структура файлов и каталогов сейчас получается такой:

$ tree
.
├── ansible.cfg
├── hosts
├── provision.retry
├── provision.yml
└── roles
    └── nginx
        ├── handlers
        │   └── main.yml
        ├── tasks
        │   └── main.yml
        ├── templates
        │   ├── nginx.conf
        │   └── nginx_vhost.conf
        └── vars
            └── main.yml

Ссылки по теме

Automating Server Setup with Ansible

Installing NGINX and NGINX Plus With Ansible

Provisioning remote machines using Ansible

Ansible: роли (roles) – пример