Найти в Дзене
Linux | Network | DevOps

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:

-2

Пример простого 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-инженера.