
Привет, друзья!
В этой серии статей я делюсь с вами своим опытом решения различных задач из области веб-разработки и не только.
В этой статье мы научимся разворачивать 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-синтаксис
- хорошо масштабируется
- подходит для небольших и средних задач
Дополнительные материалы:
- Ansible для начинающих
- Ansible для новичков: что это, как устроен и зачем нужен
- Ansible для начинающих: инструкции и команды
Хороший бесплатный курс на 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-канале ↩