Как стать автором
Обновить

Заводить ли личный блог или сайт? Часть II. Конифгурация сервера с помощью Ansible

Уровень сложностиСредний
Время на прочтение14 мин
Количество просмотров1.2K

Пришло время продолжить цикл статей о запуске личного блога. На очереди конфигурация сервера с помощью Ansible.

Данная статья будет разделена на три блока:

  1. Несколько слов о результатах первой статьи;

  2. Блок о самом инструменте Ansible;

  3. Блок о конфигурации сервера с помощью него.

Рекомендую ознакомиться с первым коротким блоком, прочитав который, вы сможете понять будет ли вам полезно или интересно читать дальше. После чего, если вы знаете что такое Ansible, можно смело переходить к третьему блоку.

Несколько слов о результатах первой статьи

В предыдущей статье я рассказывал про настройку и запуск сервера с помощью Terraform в облаке Selectel, а также о планах, о которых хочется напомнить. Для начала я планирую сделать динамический блог cо своей cms на минималках, используя IaC-подход и такие инструменты, как Terraform и Ansible. Это цель в первую очередь носит для меня образовательно-практический характер. Если бы цель была просто сделать блог, то Terraform и Ansible, как правильно подметили в комментариях к предыдущей статье, больше усложняет задачу. Правда я писал об этом, но видимо не достаточно акцентировано, исправляюсь)) В первую очередь я frontend-разработчик и мои знания об облачной инфраструктуре не такие, как у профильных специалистов. Поэтому важно отметить, что я не претендую на то, чтобы мои решения имели звания «эталонные» или даже «хорошие», но буду рад конструктивной критике и замечаниям.

И немного о приятном - в честь первой статьи в этом году, я провожу розыгрыш в своем тг-канале. На этот раз разыграю механическую клавиатуру Redragon CARAXES PRO. А теперь к делу.

Что такое Ansible?

Ansible - это инструмент для управления конфигурацией, а управление конфигурацией — это контроль и отслеживание изменений в системе. Сюда входит и контроль за версией установленных пакетов, и за файлами конфигурации, и за настройками операционной системы на хосте.

Задачи SCM (Supply Chain Management | Управление цепочками поставок:

  • определить, что является изменением, влияющим на продукт, а что нет;

  • предоставлять информацию, кто и когда сделал изменение;

  • быстро и удобно накатывать определённую версию рабочей конфигурации.

Исторически для этих задач использовалась консоль и bash скрипты.

Есть несколько инструментов для управления конфигурацией, например, Puppet или Chef, но Ansible самый популярный из них, так как имеет низкий порог входа, написан на Python и не требует дополнительной инфраструктуры. Ansible работает без агентов, он подключается к серверам по известным протоколам удаленного доступа. Для Linux это, в основном, SSH, а для Windows - WinRM. Другие инструменты требуют развёртывания головного сервера и установки агентов на хостах.

Ansible содержит огромное количество модулей и плагинов для разнообразных задач. Если модули используются непосредственно в задачах конфигурации и запускаются на целевых серверах, то плагины упрощают работу с Ansible на контрольной ноде — сервере, где запускается сам Ansible.

Как бы выглядела команда копирования файла с помощью Ansible:

$ ansible localhost -m copy -a "src=name.service dest=/tmp/name.service"
localhost | CHANGED => {
    "changed": true,
    "checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
    "dest": "/tmp/name.service",
    "gid": 1002,
    "group": "ansible",
    "md5sum": "d41d8cd98f00b204e9800998ecf8427e",
    "mode": "0664",
    "owner": "ansible",
    "size": 0,
    "src": "/home/ansible/.ansible/tmp/ansible-tmp-1645871871.061007-194067518564439/source",
    "state": "file",
    "uid": 1000
}

Здесь мы используем модуль copy. Ansible подключается к указанному удалённому хосту (localhost) и выполняет копирование файла с контрольной ноды (указан в параметре src) на удалённый хост в директорию /tmp/ (указан в параметре dest)

Нужно обратить внимание на вывод и значение поля "changed". Модуль всегда проверяет необходимость выполнения действия, иначе, он выполнит действия для приведения системы в требуемое состояние. Если мы запустим ту же команду второй раз:

$ ansible localhost -m copy -a "src=name.service dest=/tmp/name.service"
localhost | SUCCESS => {
    "changed": false,
    "checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
    "dest": "/tmp/name.service",
    "gid": 1002,
    "group": "ansible",
    "mode": "0664",
    "owner": "ansible",
    "path": "/tmp/name.service",
    "size": 0,
    "state": "file",
    "uid": 1000
}

Поле "changed" изменилось на "false". В случае использования модуля copy, Ansible проверит чек-сумму ("checksum") копируемого файла. Если они совпадают, то никаких действий не требуется. Это сильное и важное свойство систем управления конфигурацией и Ansible в частности, которое называется идемпотентность.

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

Конечно с Ansible обычно не работаю через консоль, а используют декларативное описание выполняемых задач в YAML-файлах. Это описание включает в себя следующие элементы:

  • Ansible task;

  • Ansible role;

  • Ansible play;

  • Ansible playbook;

  • Ansible inventory.

Ansible Task — это блок в YAML, с помощью которого можно сообщить Ansible, что мы ожидаем получить на целевом сервере: установленный пакет, желаемую конфигурацию и запущенный сервис.

# Пример установки NodeJS
 
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: yes

    - name: Download nodejs shell script
      ansible.builtin.uri:
        url: "https://deb.nodesource.com/setup_23.x"
        dest: "/tmp/nodesource_setup.sh"
      when: not nodesource.stat.exists

    - name: Run nodejs shell script
      ansible.builtin.command:
        cmd: "bash nodesource_setup.sh"
        chdir: "/tmp"

    - name: Install the nodejs
      ansible.builtin.apt:
        name: nodejs
        state: present

    - name: Remove nodejs shell script
      ansible.builtin.file:
        path: "/tmp/nodesource_setup.sh"
        state: absent

Каждой задаче даётся имя, которое будет отображаться в отчёте при запуске Ansible. Далее указывается используемый модуль и его параметры. Параметры модуля хорошо описаны в его документации, которую можно найти на сайте Ansible или вызвав команду ansible-doc <имя модуля>.

Когда задач становится много или когда нужно управлять таким набором задач как единым модулем, они организуются в Ansible role. Ansible role инкапсулирует всю сложную логику, как функция в программировании, и позволяет вызывать код этой логики одной строкой с параметрами.

Ansible role представляет собой директорию, с упорядоченными по назначению YAML-файлами. Файловая структура роли чётко определена:

  • tasks/main.yml — Ansible-задачи, запускаемые ролью;

  • defaults/main.yml — дефолтные значения переменных для роли;

  • vars/main.yml — переменные для роли;

  • templates/ — директория с jinja2-шаблонами;

  • handlers/main.yml — обработчики, запускаются только при получении соответствующих уведомлений (notify) от задач;

  • files/ — директория с файлами, не требующими шаблонизации;

  • meta/main.yml — метаданные с описанием авторов роли, зависимостей и версии.

Файловая структура роли может быть автоматически сгенерирована с помощью утилиты ansible-galaxy, которая поставляется в пакете Ansible:

ansible-galaxy init <my role name> 

Чтобы применить задачи или роли на целевом сервере потребуется play. Ansible play — это блок описания в YAML, в котором указан шаблон целевых хостов (или ещё говорят host pattern) и список задач или ролей для этих хостов.

---
- name: Play for frontend-service start
  hosts: frontend
  roles:
    - frontend

Список плеев организуется в Ansible playbook — YAML-файл, который передаётся утилите ansible-playbook:

ansible-playbook playbook.yaml 

Конфигурация, необходимая Ansible для подключения к целевым серверам, называется inventory. В простом случае Ansible inventory — это текстовый файл со списком серверов:

localhost
31.129.45.39

В списке могут быть DNS-имена или IP-адреса.

Серверов, на которых нужно выполнить один play, может быть несколько, поэтому хосты организуют в группы. Группы могут содержать список хостов или вложенные группы, а inventory тогда удобнее описать в том же YAML:

all: # Все серверы в inventory, all - обязателен
  children: # Дочерние группы для all
    backend: # Группа хостов с именем backend
      hosts:
        10.66.101.8:
    frontend:
      hosts:
        frontend.example.com:

Такая вложенность групп позволяет разбить инфраструктуру на окружения (dev/stage/prod):

all:
  children:
    backend:
      hosts:
        10.66.101.8:
        dev-backend.example.com:
    frontend:
      hosts:
        10.66.101.7:
        frontend.example.com:
    dev:
      hosts:
        dev-backend.example.com:
        10.66.101.7:
    prod:
      hosts:
        10.66.101.8:
        frontend.example.com:  

И запускать playbook с параметром --limit, только по нужному окружению:

ansible-playbook playbook.yaml --limit dev

В inventory можно описать переменные группы или хоста, которые можно будет использовать в задачах:

all:
  children:
    backend:
      hosts:
        dev-backend.example.com:
          # Переменная только для этого хоста
          ansible_user: ansible
      vars: # Переменные для группы backend
        backend_version: 0.1.0
  vars: # Переменные для всех хостов в inventory
    ansible_connection: ssh

Но такой формат передачи переменных перегружает файл inventory и лучшей практикой будет расположить переменные групп в директории group_vars рядом с файлом playbook:

$ tree -L 2 .
.
├── group_vars
│   ├── all.yml
│   ├── backend.yml
│   └── frontend.yml
└── playbook.yaml

В Ansible переменные используются для управления поведением задач. Переменные могут быть определены в Ansible playbooks, в Ansible inventory, внутри Ansible roles или передаваться в командной строке запуска playbook. В последнем случае такие переменные называются «внешними» или «extra vars»:

ansible-playbook playbook.yaml --extra-vars "my_var=value"

Внешние переменные имеют наивысший приоритет. Например, если переменная my_var определена в файле group_vars/<my group>.yaml, то запуск плейбука с параметром —extra-vars "my_var=value" заменит значение этой переменной.

В то же время, переменные, указанные в файлах group_vars/, имеют больший приоритет над переменными, заданными в Ansible-ролях.

Переменные также возможно использовать в шаблонах. Ansible использует движок шаблонизации Jinja2 для подстановки переменных или для динамического формирования конфигурационных файлов. В любом YAML-блоке Ansible можно указать переменную в двойных фигурных скобках и Jinja2 подставит соответствующее значение.

Например, ссылка одной переменной на значение другой в group_vars/all.yml:

answer: 42
question: "{{ answer }}"

Ansible-модуль template использует Jinja2 для создания файлов на основе шаблонов с помощью переменных в процессе выполнения:

    - name: Apply Nginx template
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: Reload nginx

Если мы опишем переменные в group_vars/frontend.yml:

front_path: "/var/www-data"

То сможем сгенерировать nginx.conf файл на основе шаблона nginx.conf.j2:

server {
  listen 80;

  root {{ front_path }}/dist;
  index index.html index.htm;

  server_name _;

  location / {
    default_type "text/html";
    try_files $uri.html $uri $uri/ =404;
  }
}

Также можно конфигурировать и сам Ansible, используя ansible.cfg файл. В файле ansible.cfg определены директивы конфигурации для всего набора утилит (ansible, ansible-playbook, ansible-galaxy, ansible-vault, etc...). В этом файле указывается расположение ролей, конфигурация inventory и имя пользователя для подключения к хостам:

[defaults]
roles_path = roles
inventory = inventory

remote_user = ansible

vault_password_file = .vault
host_key_checking = False
[privilege_escalation]
become = true ; повышать привилегии (sudo) для выполняемых задач

Ansible поддерживает следующие способы передачи параметров конфигурации (в порядке убывания приоритета):

  1. Переменные (если значение переменной присвоено несколько раз — используется последнее значение);

  2. Значения из playbook;

  3. Значения, установленные через аргументы командной строки;

  4. Значения из файла конфигурации.

В свою очередь, Ansible ищет конфигурационные файлы в следующих местах и порядке:

  1. Путь из переменной окружения ANSIBLE_CONFIG;

  2. ./ansible.cfg (файл в текущем каталоге);

  3. ~/.ansible.cfg (файл в домашнем каталоге);

  4. /etc/ansible/ansible.cfg.

Используется только первый найденный файл, остальные игнорируются.

В коде Ansible могут быть чувствительные данные — пароли и токены, а в силу того, что код Ansible лежит в VCS-репозитории, возникает необходимость такие данные скрывать. В Ansible поддерживается функциональность шифрования чувствительных переменных или даже целых файлов с помощью команды ansible-vault, а при запуске Ansible передать специальный пароль, который расшифрует данные в процессе работы:

$ ansible-vault encrypt_string "my_password" --name the_secret

Затем в group_vars/<my group>.yml указывается вывод команды:

the_secret: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  32616561306230333465396431633334353535333966343239373565623933613539363430626233
  3239336365303531306236633338336261313064323931660a336437343530333765326139653463
  39356530653266316132313235346237376238623330613864613534383831653630376236653532
  3436623737653361620a663863663635303332393266646631333132303265346437616334346330
  6461

Передача пароля для расшифровки происходит в интерактивной сессии при запуске Ansible playbook со специальным параметром:

ansible-playbook playbook.yaml --ask-vault-pass

Но при запуске Ansible в CI-системе этот способ не подойдёт и потребуется механизм передачи пароля без участия пользователя. 

Для этого можно сформировать файл с паролем на предыдущем шаге CI и указать его расположение в ansible.cfg:

vault_password_file = .vault

Конфигурация сервера с помощью Ansible

Полную конфигурацию, что я буду описывать далее, можно посмотреть в репозитории на Github. Проект будет развиваться, поэтому для удобства я зафиксировал текущую версию конфигураций в тэгах для ansible и terraform.

Для начала давайте посмотрим на файловую структуру моего Ansible-проекта:

$ tree -L 4
.
├── ansible.cfg
├── group_vars
│   ├── all.yaml
│   └── frontend.yaml
├── inventory.yaml
├── playbook.yaml
├── roles
│   └── frontend
│       ├── files
│       │   └── nginx.service
│       ├── handlers
│       │   └── main.yaml
│       ├── tasks
│       │   └── main.yaml
│       └── templates
│           └── nginx.conf.j2
└── run.sh

Точкой входа здесь будет bash-скрипт run.sh:

ansible-playbook -e host="{public_ip} -i ./inventory.yaml playbook.yaml

В inventory пока лежит только один хост для фронта, который как раз приходит из внешней переменной:

all: # Все серверы в нашем inventory, all - обязателен
  children: # Дочерние группы для all
    frontend:
      hosts:
        frontend:
          ansible_host: "{{ host }}"
          ansible_user: root

И playbook пока также содержит только один play для фронта:

---
- name: Play for frontend-service start
  hosts: frontend
  roles:
    - frontend

Теперь можно рассмотреть всю роль целиком. После чего остановлюсь на каждой задаче отдельно.

- block:
    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: yes

    - name: Check that the /tmp/nodesource_setup.sh exists
      ansible.builtin.stat:
        path: "/tmp/nodesource_setup.sh"
      register: nodesource

    - name: Download nodejs shell script
      ansible.builtin.uri:
        url: "https://deb.nodesource.com/setup_23.x"
        dest: "/tmp/nodesource_setup.sh"
      when: not nodesource.stat.exists

    - name: Run nodejs shell script
      ansible.builtin.command:
        cmd: "bash nodesource_setup.sh"
        chdir: "/tmp"

    - name: Install the nodejs
      ansible.builtin.apt:
        name: nodejs
        state: present

    - name: Remove nodejs shell script
      ansible.builtin.file:
        path: "/tmp/nodesource_setup.sh"
        state: absent

    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: latest
        install_recommends: yes
      notify: Start nginx

    - name: Install Git
      ansible.builtin.apt:
        name: git
        state: latest
        install_recommends: yes

    - name: Add user {{ front_user }}
      ansible.builtin.user:
        name: "{{ front_user }}"
        groups: "{{ front_groups }}"
        shell: /sbin/bash
        create_home: true

    - name: Add start dir
      ansible.builtin.file:
        path: "/home/{{ front_user }}"
        state: directory
        mode: "0755"
        owner: "{{ front_user }}"
        group: "{{ front_group }}"

    - name: Add {{ front_path }} dir
      ansible.builtin.file:
        path: "{{ front_path }}"
        state: directory
        mode: "0755"
        owner: "{{ front_user }}"
        group: "{{ front_group }}"

    - name: Clone frontend repo
      ansible.builtin.git:
        repo: "{{ front_repo_url }}"
        dest: "/home/{{ front_user }}/frontend"
        version: "{{ front_version }}"
        force: yes
#         accept_hostkey: yes
#         key_file: /home/ansible/.ssh/id_rsa

    - name: Install npm packages
      ansible.builtin.command:
        cmd: "npm install"
        chdir: "/home/{{ front_user }}/frontend"

    - name: Build frontend
      ansible.builtin.command:
        cmd: "npm run build"
        chdir: "/home/{{ front_user }}/frontend"

    - name: Add /opt/log dir
      ansible.builtin.file:
        path: "/opt/log"
        state: directory
        mode: "0777"
        owner: "{{ front_user }}"
        group: "{{ front_group }}"

    - name: Copy frontend build
      ansible.builtin.copy:
        src: "/home/{{ front_user }}/frontend/dist"
        dest: "{{ front_path }}"
        remote_src: yes

    - name: Apply Nginx template
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: Reload nginx


  become_method: sudo
  become: true

Итак, пойдем по порядку.

Обновление кэша apt

Первая задача в роли это обновление кэша apt, используя модуль apt:

    - name: Update apt cache
      ansible.builtin.apt:
        update_cache: yes

 Advanced Packaging Tool - программа для работы с программными пакетами. Насколько я понимаю, общепринято обновлять кэш apt перед каждой установкой или хотя бы раз в день, чтобы проверить наличие свежих версий пакетов, что нужно установить.

Установка NodeJS

Следующие пять задач отвечают за установку NodeJS, который понадобиться для установки зависимостей и сборки приложения.

Первая из пяти задач проверяет наличие уже загруженного скрипта для установки:

    - name: Check that the /tmp/nodesource_setup.sh exists
      ansible.builtin.stat:
        path: "/tmp/nodesource_setup.sh"
      register: nodesource

Я добавил этот шаг из-за ошибок в процессе написания роли. Для этого шага я использую модуль stat. По существу на одном из следующих шагов реализовано удаление скрипта, поэтому сейчас этот шаг не нужен, но я решил его оставить, так как здесь есть пример сохранения результата задачи в переменную для дальнейшего использования. Обратите внимание на поле register.

Вторая задача скачивает скрипт, используя модуль uri, если его не обнаружили на прошлом шаге:

 - name: Download nodejs shell script
      ansible.builtin.uri:
        url: "https://deb.nodesource.com/setup_23.x"
        dest: "/tmp/nodesource_setup.sh"
      when: not nodesource.stat.exists

Посмотрите на поле when, где идет обращение к сохраненной на предыдущем шаге переменной.

Затем запускаем скаченный скрипт, используя модуль command:

    - name: Run nodejs shell script
      ansible.builtin.command:
        cmd: "bash nodesource_setup.sh"
        chdir: "/tmp"

Поле cmd принимает команду к исполнению, а поле chdir путь до директории, где нужно запустить команду.

После чего идет непосредственно сама установка NodeJS, с помощью apt:

    - name: Install the nodejs
      ansible.builtin.apt:
        name: nodejs
        state: present

Актуальную инструкцию по установке бинарного дистрибутива NodeJS вы можете найти здесь.

Потом я удаляю скаченный скрипт, используя модуль file:

    - name: Remove nodejs shell script
      ansible.builtin.file:
        path: "/tmp/nodesource_setup.sh"
        state: absent

Установка Nginx

Для установки Nginx также использую модуль apt:

    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: latest
        install_recommends: yes
      notify: Start nginx

Но здесь также нужно обратить внимание на поле notify. Это поле запускает обработчик при успешном изменении, что позволяет сделать что-то дополнительно. В этом случае запускается обработчик Start nginx, который лежит в директории handlers:

- name: Start nginx
  systemd:
    name: nginx
    enabled: yes
    state: started

systemd — подсистема инициализации и управления службами в Linux. Представьте, что вы включаете свой компьютер, и он автоматически запускает все программы и процессы, которые вам нужны для работы. Система инициализации делает это за вас, чтобы вам не пришлось вручную запускать каждую программу. То есть здесь мы пишем, чтобы ansible запустил nginx, используя его systemd-unit.

Установка Git

Используя apt, устанавливаю Git:

    - name: Install Git
      ansible.builtin.apt:
        name: git
        state: latest
        install_recommends: yes

Создание пользователя и директории для проекта

После установки необходимых пакетов, нужно создать пользователя и директорию под проект. Для этого использую модули user и file:

- name: Add user {{ front_user }}
      ansible.builtin.user:
        name: "{{ front_user }}"
        groups: "{{ front_groups }}"
        shell: /sbin/bash
        create_home: true

    - name: Add start dir
      ansible.builtin.file:
        path: "/home/{{ front_user }}"
        state: directory
        mode: "0755"
        owner: "{{ front_user }}"
        group: "{{ front_group }}"

    - name: Add {{ front_path }} dir
      ansible.builtin.file:
        path: "{{ front_path }}"
        state: directory
        mode: "0755"
        owner: "{{ front_user }}"
        group: "{{ front_group }}"

В первую очередь создаю пользователя и группу пользователей, а также с помощью поля shell указываю ему интерфейс для работы с ним, а также с помощью поля create_home, сообщаю, что нужно создать домашнюю директорию пользователя.

Дальше создаю директории, в которых буду собирать проект, а потом размещать с помощью nginx.

Загрузка и сборка проекта

Используя модули git и command, скачиваю и собираю проект:

- name: Clone frontend repo
      ansible.builtin.git:
        repo: "{{ front_repo_url }}"
        dest: "/home/{{ front_user }}/frontend"
        version: "{{ front_version }}"
        force: yes
    #         accept_hostkey: yes
    #         key_file: /home/ansible/.ssh/id_rsa

    - name: Install npm packages
      ansible.builtin.command:
        cmd: "npm install"
        chdir: "/home/{{ front_user }}/frontend"

    - name: Build frontend
      ansible.builtin.command:
        cmd: "npm run build"
        chdir: "/home/{{ front_user }}/frontend"

    - name: Copy frontend build
      ansible.builtin.copy:
        src: "/home/{{ front_user }}/frontend/dist"
        dest: "{{ front_path }}"
        remote_src: yes

Применение шаблона Nginx

Для раздачи статики применю простенькую nginx-конфигурацию, с помощью модуля template:

 - name: Apply Nginx template
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/default
      notify: Reload nginx

А вот и сам шаблон:

server {
  listen 80;

  root {{ front_path }}/dist;
  index index.html index.htm;

  server_name _;

  location / {
    default_type "text/html";
    try_files $uri.html $uri $uri/ =404;
  }
}

После применения шаблона перезагружаю nginx с помощью notify и запуска обработчика Reload nginx:

- name: Reload nginx
  systemd:
    name: nginx
    state: reloaded

Если сервис на этот момент не был запущен, то эта задача запустит его.

Результат

Чтобы применить данную конфигурацию нужно в файле run.sh назначить переменной host IP сервера, получить который можно, применив конфигурацию terraform из прошлой статьи. После применения Ansible конфигурации можно ввести в браузере IP-адрес и увидеть развернутое приложение. 

На этом все. Спасибо всем, кто дочитал до конца!)

Теги:
Хабы:
Всего голосов 5: ↑2 и ↓3+1
Комментарии1

Публикации

Работа

Ближайшие события