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


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


В этой статье мы научимся разворачивать 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-канале

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