DevOps Tutorials — Ansible: разворачиваем веб-приложение на виртуальном сервере

от автора

Привет, друзья!

В этой серии статей я делюсь с вами своим опытом решения различных задач из области веб-разработки и не только.

В этой статье мы научимся разворачивать Angular+Java веб-приложение на виртуальном сервере Ubuntu Linux с помощью Ansible.

Интересно? Тогда прошу под кат.

Предположим, что у нас есть веб-приложение интернет-магазина, состоящее из 2 частей:

  • фронтенд на JavaScript (Angular, но это неважно), статика которого раздается с помощью Node.js (http-server) (не совсем стандартный подход, обычно это делается с помощью Nginx)
  • бэкенд на Java (Spring), взаимодействующий с облачной базой данных PostgreSQL

Репозиторий проекта хранится в GitLab. В проекте настроен конвейер (GitLab CI/CD), который после сборки отправляет артефакты фронта (архив .tar.gz) и бэка (файл .jar) в соответствующие репозитории Nexus.

Наша задача — развернуть это приложение на виртуальном сервере Ubuntu Linux. Разумеется, это можно сделать вручную, но давайте немного автоматизируем данный процесс с помощью Ansible.

Предварительные условия:

  • на вашей машине должен быть установлен ansible (инструкция для Ubuntu)
  • на виртуальном сервере должен быть создан пользователь ansible с необходимыми правами доступа

❯ Пара слов об Ansible

Ansible — это инструмент с открытым исходным кодом для автоматизации ИТ-процессов, таких как:

  • настройка серверов
  • развертывание приложений
  • управление конфигурациями
  • оркестрация (координация работы нескольких машин)

На самом высоком уровне Ansible работает следующим образом:

  • использует SSH для подключения к серверам (агенты не требуются — это безагентская система)
  • настройки описываются в YAML-файлах (называются playbook’и).
  • позволяет описывать желаемое состояние системы, а не пошаговые команды (декларативный подход)

Основные компоненты Ansible:

Компонент Назначение
Inventory Список управляемых хостов
Playbook YAML-файл с задачами
Task Отдельное действие (например, установить пакет)
Role Структурированная группа задач и файлов
Module Встроенные функции (например, apt, yum, copy, service и др.)

Пример простого playbook’а:

# хосты - серверы - hosts: webservers   # суперпользователь - sudo   become: yes   # задачи   tasks:     - name: Установить nginx       # модуль       apt:         name: nginx         state: present

Преимущества Ansible:

  • простота — не требует установки агентов
  • использует простой YAML-синтаксис
  • хорошо масштабируется
  • подходит для небольших и средних задач

Дополнительные материалы:

Хороший бесплатный курс на Stepik:

❯ Конфигурация Ansible

Создаем директорию devops-ansible со следующей структурой:

. ├── ansible.cfg ├── inventory.yaml ├── playbook.yml ├── README.md └── roles     ├── backend     │   ├── defaults     │   │   └── main.yml     │   ├── tasks     │   │   ├── download.yml     │   │   ├── install.yml     │   │   ├── main.yml     │   │   ├── service.yml     │   │   └── setup.yml     │   └── templates     │       └── backend.service.j2     └── frontend         ├── defaults         │   └── main.yml         ├── tasks         │   ├── download.yml         │   ├── install.yml         │   ├── main.yml         │   ├── nodesource.yml         │   ├── service.yml         │   └── setup.yml         └── templates             └── frontend.service.j2

Определяем основные настройки Ansible в файле ansible.cfg:

[defaults] roles_path = ./roles  [ssh_connection] timeout = 30

Определяем список управляемых хостов в файле inventory.yaml:

all:   hosts:     vm1:       ansible_host: <ip вашего виртуального сервера>       ansible_user: ansible

vm1 — это имя хоста (синоним/алиас), который мы задаем для удобства. Другими словами, vm1 — это логическое имя, под которым Ansible будет знать этот сервер. Оно не обязано совпадать с реальным именем машины или DNS-именем.

Через ansible_host мы указываем реальный IP-адрес (или доменное имя), куда Ansible должен подключаться.

Через ansible_user мы указываем, под каким пользователем подключаться по SSH.

Аналогия с телефонным справочником:

  • vm1 — имя контакта (чтобы было удобно обращаться)
  • ansible_host — номер телефона (реальный IP)
  • ansible_user — кто звонит (под каким пользователем логиниться)

Определяем группы задач в файле playbook.yaml:

--- - name: Деплой backend и frontend   hosts: all   remote_user: ansible    roles:     - backend     - frontend

В данном случае remote_user можно опустить, поскольку мы указали ansible_user в inventory.yaml. remote_user требуется в следующих случаях:

  • если мы хотим переопределить пользователя, указанного в inventory
  • если в inventory нет ansible_user, и мы хотим задать пользователя на уровне playbook’а

❯ Конфигурация роли/группы задач frontend

Работаем с директорией ansible/roles/frontend.

Определяем переменные в файле defaults/main.yml:

# Данные для доступа к репозиторию Nexus, в котором хранится архив фронта `.tar.gz`. # В корне проекта необходимо создать файл `.env` с этими данными nexus_service_url: "{{ lookup('env', 'NEXUS_SERVICE_URL') }}" nexus_repo_frontend_name: "{{ lookup('env', 'NEXUS_REPO_FRONTEND_NAME') }}" nexus_repo_user: "{{ lookup('env', 'NEXUS_REPO_USER') }}" nexus_repo_pass: "{{ lookup('env', 'NEXUS_REPO_PASS') }}"  # Пользователь для развертывания фронта frontend_user: "www-data" # Директория для распаковки архива frontend_dest: "/var/www-data"  # Версия Node.js (20+) node_version: "20.x"  # Порт сервера для раздачи статики frontend_port: 80 # Адрес бэка backend_url: "http://localhost:8080"

Определяем группы задач в файле tasks/main.yml:

--- # Задачи выполняются в порядке определения - include_tasks: nodesource.yml - include_tasks: install.yml - include_tasks: setup.yml - include_tasks: download.yml - include_tasks: service.yml

Задача добавления NodeSource (nodesource.yml):

--- - name: Добавить GPG-ключ NodeSource   become: true   # Чтобы apt доверял этому репозиторию   apt_key:     url: 'https://deb.nodesource.com/gpgkey/nodesource.gpg.key'     state: present  - name: Добавить NodeSource репозиторий   become: true   # Это позволяет установить Node.js через apt как обычный пакет,   # но из NodeSource, а не из стандартного репозитория   apt_repository:     repo: 'deb https://deb.nodesource.com/node_{{ node_version }} {{ ansible_distribution_release }} main'     state: present     filename: 'nodesource'

Зачем нужен NodeSource перед установкой Node.js? NodeSource — это сторонний репозиторий, который предоставляет актуальные версии Node.js, которых нет в стандартных репозиториях большинства дистрибутивов Linux (особенно Debian/Ubuntu).

Задачи установки Node.js (в комплекте с npm) и http-server (install.yml):

--- - name: Установить Node.js и npm   become: true   apt:     name:       - nodejs     state: present     update_cache: yes  - name: Установить http-server   become: true   community.general.npm:     name: http-server     # Глобальная установка     global: yes

Что делает update_cache: yes? Это параметр apt, который обновляет локальный кэш списка пакетов (apt update) перед установкой. Это важно, поскольку:

  • если мы только что добавили новый репозиторий (например, NodeSource), apt еще не знает о доступных там пакетах, пока не обновит кэш
  • без update_cache: yes команда может не найти нужный пакет, даже если он уже есть в источнике

Задачи создания сервисного пользователя и директории для распаковки архива фронта (setup.yml):

--- - name: Создать сервисного пользователя www-data   become: true   user:     name: '{{ frontend_user }}'     # Не создавать домашнюю/пользовательскую директорию     create_home: no     # Системный пользователь     system: yes     shell: /usr/sbin/nologin  - name: Создать директорию {{ frontend_dest }}   become: true   file:     path: '{{ frontend_dest }}'     state: directory     owner: '{{ frontend_user }}'     group: '{{ frontend_user }}'     mode: '0755'

shell: /usr/sbin/nologin — это способ запретить пользователю вход в систему через терминал (SSH, консоль и т.д.). Это важно, поскольку:

  • пользователь www-data создается только для запуска процессов или владения файлами, а не для работы от его имени
  • это повышает безопасность, потому что никто не сможет использовать этого пользователя для интерактивной сессии

Задачи скачивания и распаковки архива фронта (download.yml):

--- - name: Получить последнюю версию фронтенда из Nexus   uri:     # Адрес Nexus-сервера     # `sort=version` — сортировка по версии, чтобы последняя версия была первой в списке     url: '{{ nexus_service_url }}/rest/v1/search/assets?repository={{ nexus_repo_frontend_name }}&sort=version'     method: GET     url_username: '{{ nexus_repo_user }}'     url_password: '{{ nexus_repo_pass }}'     # Принудительно использовать basic auth     force_basic_auth: yes     # Вернуть содержимое ответа (`.json`), чтобы мы могли с ним работать     return_content: yes   # Сохраняем результат запроса в переменную `nexus_response`   register: nexus_response  - name: Извлечь последнюю версию фронтенда из Nexus   set_fact:     download_url: "{{ nexus_response.json['items'][0].downloadUrl }}"  - name: Скачать последнюю версию фронтенда из Nexus   get_url:     url: '{{ download_url }}'     # Куда скачать?     dest: '/tmp/frontend.tar.gz'     url_username: '{{ nexus_repo_user }}'     url_password: '{{ nexus_repo_pass }}'     force_basic_auth: yes  - name: Распаковать фронтенд   become: true   unarchive:     src: '/tmp/frontend.tar.gz'     # Куда распаковать?     dest: '{{ frontend_dest }}'     # Файл уже на удаленном сервере, не нужно его копировать с локальной машины     remote_src: yes

Мы используем set_fact, чтобы создать переменную download_url, которая содержит прямую ссылку на скачивание самого свежего артефакта:

  • items[0] — первый (а значит, самый новый) элемент в массиве артефактов (мы выполнили сортировку по версии в первой задаче)
  • downloadUrl — ключ в JSON, содержащий ссылку на файл

Задачи создания и запуска сервиса фронта (service.yml):

--- - name: Скопировать systemd unit-файл   become: true   template:     src: frontend.service.j2     dest: /etc/systemd/system/frontend.service     mode: '0644'  - name: Перезапустить systemd   become: true   systemd:     daemon_reload: yes  - name: Включить и запустить сервис фронтенда   become: true   systemd:     name: frontend     enabled: yes     state: started

Шаблон сервиса фронта выглядит так (templates/frontend.service.j2):

[Unit] Description=Frontend Service After=network.target  [Service] User={{ frontend_user }} Group={{ frontend_user }} WorkingDirectory={{ frontend_dest }}/dist/frontend ExecStart=/usr/bin/http-server -p {{ frontend_port }} --proxy {{ backend_url }} Restart=always AmbientCapabilities=CAP_NET_BIND_SERVICE  [Install] WantedBy=multiuser.target

❯ Конфигурация роли/группы задач backend

Работаем с директорией ansible/roles/backend.

Определяем переменные в файле defaults/main.yml:

# Данные для доступа к репозиторию Nexus, в котором хранится файл бэка `.jar`. # В корне проекта необходимо создать файл `.env` с этими данными nexus_service_url: "{{ lookup('env', 'NEXUS_SERVICE_URL') }}" nexus_repo_backend_name: "{{ lookup('env', 'NEXUS_REPO_BACKEND_NAME') }}" nexus_repo_user: "{{ lookup('env', 'NEXUS_REPO_USER') }}" nexus_repo_pass: "{{ lookup('env', 'NEXUS_REPO_PASS') }}"  # Пользователь для развертывания бэка backend_user: 'backend'  # Путь к исполняемому файлу. # app-name - название нашего приложения jar_path: '/opt/app-name/bin/backend.jar'

Определяем группы задач в файле tasks/main.yml:

--- # Задачи выполняются в порядке определения - import_tasks: install.yml - import_tasks: setup.yml - import_tasks: download.yml - import_tasks: service.yml

Задача установки Java нужной версии (install.yml)

--- - name: Установить OpenJDK 16   become: true   apt:     # В принципе, версию Java тоже можно вынести в переменную     name: openjdk-16-jdk     state: present     update_cache: yes

Задачи создания сервисного пользователя и директории для исполняемого файла бэка (setup.yml):

--- - name: Создать сервисного пользователя backend   become: true   user:     name: '{{ backend_user }}'     create_home: no     system: yes     shell: /usr/sbin/nologin  - name: Убедиться, что директория /opt/app-name/bin существует   become: true   file:     path: /opt/app-name/bin     state: directory     owner: '{{ backend_user }}'     group: '{{ backend_user }}'     mode: '0755'  - name: Убедиться, что директория /var/app-name существует   become: true   file:     path: /var/app-name     state: directory     owner: '{{ backend_user }}'     group: '{{ backend_user }}'     mode: '0755'

/opt/app-name/bin — это директория для исполняемого файла бэка, а зачем нам директория /var/app-name? /var/ — это стандартная системная директория для:

  • данных, которые меняются во время работы приложения: временные файлы, логи, кэш, БД (например, sqlite) и т.д.
  • данных, которые нельзя хранить в /opt, потому что они могут изменяться и должны быть доступны определенным сервисам, бэкапам, ротации логов и т.п.

Мы будем хранить в этой директории логи бэка.

Задача скачивания исполняемого файла бэка (download.yml):

--- - name: Получить список артефактов бэкенда из Nexus   uri:     url: '{{ nexus_service_url }}/rest/v1/search/assets?repository={{ nexus_repo_backend_name }}&maven.extension=jar&sort=version'     method: GET     url_username: '{{ nexus_repo_user }}'     url_password: '{{ nexus_repo_pass }}'     force_basic_auth: yes     return_content: yes   register: nexus_response  - name: Извлечь последнюю версию бэкенда из Nexus   set_fact:     download_url: "{{ nexus_response.json['items'][0].downloadUrl }}"  - name: Скачать  последнюю версию бэкенда из Nexus   become: true   get_url:     url: '{{ download_url }}'     dest: '{{ jar_path }}'     url_username: '{{ nexus_repo_user }}'     url_password: '{{ nexus_repo_pass }}'     force_basic_auth: yes

Задачи создания и запуска сервиса бэка (service.yml):

--- - name: Скопировать systemd unit-файл   become: true   template:     src: backend.service.j2     dest: /etc/systemd/system/backend.service     mode: '0644'  - name: Перезагрузить systemd   become: true   systemd:     daemon_reload: yes  - name: Включить и запустить сервис бэкенда   become: true   systemd:     name: backend     state: started     enabled: yes

Шаблон сервиса бэка выглядит так (templates/backend.service.j2):

[Unit] Description=Backend Service After=network.target  [Service] User={{ backend_user }} Group={{ backend_user }} StandardOutput=append:/var/app-name/backend.log WorkingDirectory=/opt/app-name/bin ExecStart=/usr/bin/java -jar backend.jar Restart=always  [Install] WantedBy=multi-user.target

❯ Итого

Команда для запуска Ansible:

# Выполняется в корневой директории (`ansible`). # Не забудьте создать файл `.env` с данными для доступа к репозиториям Nexus source .env && ansible-playbook playbook.yml -i inventory.yaml

Мы рассмотрели далеко не все возможности, предоставляемые Ansible, но думаю вы получили неплохое представление о том, что и как позволяет делать этот замечательный инструмент. Наряду с другими популярными решениями для автоматизации ИТ-процессов (Terraform, Docker, Kubernetes и т.д.), Ansible на сегодняшний день является важной частью арсенала DevOps-инженера.

Happy devopsing!


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале


ссылка на оригинал статьи https://habr.com/ru/articles/913426/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *