Выполнение Ansible Playbook на сервере через Github Webhooks

Git Push - ›Github Webhook -› Автоматическое развертывание Ansible

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

В предыдущей статье мы создали playbook именно для этой цели для локального запуска на Mac, который действует как вводный курс по использованию Ansible. Среда playbook, используемая в этой статье, будет немного отличаться и будет оптимизирована для работы на удаленном сервере, основываясь на ранее выполненной настройке. На эту статью будут ссылки в определенных местах здесь, где она упоминается, но ее также можно найти здесь:



Решение на высоком уровне

Высокоуровневый рабочий процесс нашего полностью автоматизированного решения начинается, когда вы выполняете git push в свою главную ветку, как описано ниже:

# automated deployment process
-> perform a git push (to master branch)
 
-> `push` event webhook delivery to deployment server
-> ansible deployment playbook is run:
    -> connects to production servers
    -> fetches latest commit and builds project
    -> moves build to live HTTP directory 
-> end of playbook

Каждый раз, когда push делается в ветку master, сведения о событии будут доставлены на наш сервер развертывания, который запустит запуск Ansible playbook. Playbook будет настроен для подключения через SSH к вашим производственным серверам, получения последней master фиксации из Github, сборки проекта и перемещения этой сборки в ваш live-каталог.

Этот процесс будет разбит и задокументирован со следующими задачами:

  • Настройка личных ключей доступа Github для подключения к Github API в качестве отдельной учетной записи
  • Установка сервера Express на сервере «развертывания» для обработки полезной нагрузки Webhook
  • Программное создание и тестирование Webhooks соответственно с использованием Github API с пакетом @octokit/rest - клиентом REST API для Github. Мы также расскажем, как использовать пользовательский интерфейс Github для настройки Webhooks.
  • Создание SSH-ключа на сервере развертывания и добавление его в ваш Github SSH и GPG-ключи. Этот ключ будет перенаправлен Ansible на удаленные хосты при запуске playbook. Нам также необходимо добавить ваши производственные серверы к вашему known_hosts и выполнить ssh-copy-id для входа в систему без необходимости ввода аутентификации.
  • Настройка виртуальной среды (Python) и установка Ansible, настройка среды и включение playbook для запуска. Мы также будем использовать Ansible Vault для хранения пароля SSH рабочего сервера. На этом этапе мы можем протестировать playbook в Терминале, чтобы убедиться, что он работает без ошибок.
  • Теперь мы можем включить вызов exec() в маршрут Webhook сервера Express, который выполнит команду для запуска Ansible playbook. В exec() мы установим переменную среды, определяющую наш собственный файл конфигурации Ansible, а также будем использовать абсолютные пути к необходимым конфигурациям.

Примечание. вы можете пожелать иметь отдельную ветку для автоматического развертывания, например ветку production, но в этом разговоре мы будем придерживаться master.

Для этой настройки мы будем использовать несколько VPS / инстансов / дроплетов - какое решение VPS вам будет удобно использовать.

Один VPS будет сервером развертывания, который получит Github Webhooks и будет размещать Ansible playbook и SSH-ключ. Когда этот сценарий запускается, он будет подключаться к вашим производственным серверам для выполнения своих задач:

# server communication
github
   |
   | webhook payload
   |
  deployment server
     |
     | playbook via SSH
     |
     production server(s)
        clone github repository
        install dependencies
        build app
        replace live app build

Предполагается, что у вас уже есть VPS, развернутый для развертывания, и другие производственные экземпляры VPS (если нет, ознакомьтесь с предыдущей статьей, чтобы получить представление об обновлении производственного сервера с помощью playbook).

Настройка сервера развертывания

Этот размер заголовка должен быть намного больше - требуется небольшая настройка, чтобы заставить все винтики сервера развертывания работать вместе. Тем не менее, все будет задокументировано и объяснено, включая:

  • Получение ваших учетных данных Github
  • Настройка express сервера, который будет обрабатывать Webhooks из Github, вместе с обратным прокси-сервером Nginx для маршрутизации на него этих запросов.
  • Настройка виртуальной среды Ansible
  • Настройка ключа SSH и добавление удаленных хостов в качестве известных хостов

Помните, что это наш сервер развертывания, роль которого заключается в координации процесса развертывания на ваших удаленных хостах.

Ключ личного доступа Github

Посетите раздел Персональные токены доступа на Github (в разделе Настройки - › Настройки разработчика ) и сгенерируйте новый токен доступа. Это будет использоваться с @octokit/rest ниже для выполнения аутентифицированных запросов к Github API.

Установка Экспресс сервера

Мы будем использовать express сервер для обслуживания HTTP-запросов Github Webhook. Нам не нужно будет сильно менять шаблон экспресс-генератора по умолчанию, нужен только один маршрут для обработки полезной нагрузки Webhook. Запросы будут проходить обратное проксирование через конфигурацию сервера Nginx, что также будет задокументировано.

Примечание. В приведенных ниже командах используется yum в серверной среде CentOS, обязательно используйте то, что подходит для выбранного вами дистрибутива Linux.

# setting up deployment server
# install nginx
sudo yum install nginx
# install sshpass: SSH utility for non-interactive SSH login
sudo yum install sshpass
# install express
yarn add express
yarn global add express-generator@4
# create folder for deployment
sudo mkdir /var/deployment
# change permission to your user
sudo chown -R <your_username> /var/deployment
# generate express server & install dependencies
express /var/deployment/server && cd /var/deployment/server
yarn
#install octokit/rest (Github API client)
yarn add @octokit/rest
# start and enable nginx
sudo service start nginx && sudo service enable nginx 

Я выбрал каталог /var/deployment для хранения файлов нашего проекта, но в конечном итоге это расположение не имеет большого значения. Наш экспресс-сервер находится в /var/deployment/server.

Отлично, большинство зависимостей, связанных с настройкой сервера, уже установлено. Давайте теперь определим обратный прокси-сервер Nginx для маршрутизации запросов к серверу Express через зашифрованное соединение SSL. Заполнители заключены в угловые скобки, выделенные жирным шрифтом. Копирование и вставка следующих значений и замена значений угловых скобок, выделенных полужирным шрифтом, будут работать:

Примечание. Этот файл конфигурации будет помещен в conf.d каталог Nginx, как и любая другая конфигурация сервера.

# /etc/nginx/conf.d/github.deployment.conf
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name <your domain>;
    ssl_certificate <your_crt>;
    ssl_certificate_key <your_key>;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  10m;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    resolver 127.0.0.1;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate <your_ca_bundle>;
    location /github-api {
       proxy_set_header X-Forwarded-Host $host;
       proxy_set_header X-Forwarded-Server $host;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_pass http://localhost:3003/;
   }
}

Перезапустите Nginx, чтобы применить обновленную конфигурацию:

sudo service nginx restart

Установите диспетчер фоновых процессов pm2

Нам понадобится наш сервер Express, работающий в фоновом режиме - и это будет надежно. Это означает автоматический перезапуск при сбое, автоматическую перезагрузку при изменении кода и автоматическую загрузку при перезапуске сервера. Для этого мы можем использовать PM2, который может быть установлен как еще один глобальный пакет NPM:

# install pm2 and run express server in background
yarn global add pm2
# load on boot
pm2 startup
> a startup command will be output to the terminal - execute this command now
# start express server as a process on a dedicated port
PORT=3001 pm2 start /var/deployment/server/bin/www --watch --name 'Github Deployment'
# save pm2 list
pm2 save
# verify server is running
pm2 list

Примечание. К команде pm2 start был добавлен флаг --watch, означающий, что процесс перезагрузится, если в код будут внесены изменения. Переменная среды PORT также была определена заранее, поэтому наш сервер прослушивает этот порт.

Теперь сервер работает и прослушивает порт 3001. Почему именно этот порт? Ну, порт 3000 - это порт по умолчанию для ряда служб, связанных с Javascript (CRA, Express, SocketIO и т. Д.), И выбор другого порта гарантирует меньшую вероятность конфликтов в будущем.

Все, что осталось сделать, это настроить наш маршрут Webhook на routes/index.js. Мы сделаем это, когда будет готов сценарий Ansible.

Виртуальная среда Ansible

Теперь давайте создадим каталог виртуальной среды, в котором будет располагаться наш Ansible playbook. Следующие команды устанавливают Python 3.6 вместе с Pip перед установкой virtualenv и его инициализацией:

# install python and pip
sudo yum install python36 python-pip
sudo pip install -U pip
sudo pip install -U virtualenv
# initialise virtualenv
virtualenv --system-site-packages -p python3.6 /var/deployment/ansible
# go to directory & activate environment
cd /var/deployment/ansible && source bin/activate
# install ansible
pip install ansible
# create directory for storing playbook files
mkdir playbook

Отлично, наш сервер развертывания теперь выглядит намного более функциональным, с запущенным сервером Express и инициированной выделенной средой Python для Ansible. Теперь у нас есть следующая настройка:

# deployment folder structure
/var
  /deployment
    /server
      # express server
    /ansible
      # virtual environment
      /playbook
        # playbook specific files will go here

А теперь перейдем к настройке SSH.

Настройка ключа SSH

SSH играет большую роль в автоматизации Ansible. Мы будем использовать SSH как средство подключения к удаленным производственным серверам, а также подключение к Github для клонирования репозитория проекта (который будет развернут).

Я подробно описал процесс настройки SSH-ключа Github в предыдущей статье, поэтому здесь я просто кратко изложу этот процесс. Теперь сгенерируйте новый ключ и откройте полученный файл ключа, чтобы скопировать его содержимое:

# generate ssh key for github
ssh-keygen -t rsa -b 4096 -C "<your_github_email_address>"
# no passphrase
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
# open the key file
less ~/.ssh/id_rsa.pub

Примечание. Я решил не использовать здесь ключевую парольную фразу, так как Ansible не будет настроен для обработки этого запроса при аутентификации с помощью ключа. Однако стоит подчеркнуть, что Ansible абсолютно способен обрабатывать произвольные запросы (проверьте модуль expect), которые могут возникать во время выполнения playbook, которые вы можете явно предсказать и обработать в самом playbook.

Теперь перейдите на страницу Github Ключи SSH и GPG и посетите Add SSH Key, чтобы добавить ключ. Позже Ansible будет использовать пересылку агента SSH, чтобы использовать этот ключ для всех рабочих серверов - нет необходимости создавать дополнительные ключи для каждого из них.

Добавить рабочие серверы как известные хосты

Теперь добавьте каждый из ваших производственных серверов в качестве известных хостов, чтобы не отображались диалоги «доверенный хост» при попытке подключиться к ним при запуске playbook:

ssh-keyscan -H <production_server_ip> >> ~/.ssh/known_hosts

Скопируйте открытый ключ SSH на рабочие серверы

Чтобы войти на ваши рабочие серверы без запроса пароля, мы также будем использовать OpenSSH для копирования вашего открытого ключа. Сделайте это для каждого из ваших производственных серверов:

# execute on deployment server
ssh-copy-id <user>@<ip_address>
> input remote host password
# test connection
ssh <user>@<ip_address>

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

Добавление Github Webhook

Настроить Webhook достаточно просто - это можно сделать в разделе Настройки - ›Webhooks любого репозитория. Мы можем сделать это либо через пользовательский интерфейс Github, либо сделать это программно с помощью @octokit/rest.

При использовании пользовательского интерфейса Github

  • URL-адрес вашей полезной нагрузки должен быть URL-адресом HTTPS с включенной опцией проверки SSL. Наша конфигурация Nginx ранее определяла местоположение /github-api. Если вы следите за инструкциями, ваш URL-адрес полезной нагрузки будет https://<your_domain>/github-api
  • Убедитесь, что тип содержимого - application/json. Express обработает эти данные в request.body
  • Включите длинную случайную строку в качестве секрета. Это добавляет дополнительный механизм проверки того, что Github действительно отправляет нам запросы.
  • Убедитесь, что установлен флажок active. Мы готовы использовать этот Webhook

При использовании Octokit

Что, если бы мы хотели создать Webhook программно, используя Github API? Я рада, что вы спросили. Для этого я создал Github Gist, доступный здесь. Перед вызовом octokit.repos.createHook вы аутентифицируетесь, используя свой личный токен доступа:

// snippet from create-webhook.js Gist - view full Gist here
...
async function createPushWebhook (conf) {
  const octokit = new Octokit({
    auth: personalAccessToken
  });
  const res = await octokit.repos.createHook({
    owner: conf.owner,
    repo: conf.repo,
    name: 'web',
    config: {
      url: conf.url,
      content_type: 'json',
      secret: webhookSecret,
      insecure_ssl: 0,
    },
    events: [
      'push'
    ]
  }).catch(e => {
    console.log(e);
  });
  console.log('success. Hook id: ' + res.data.id);
}
...

Сохраните сценарий в /var/deployment/server и запустите с node, чтобы создать Webhook:

# create webhook programatically
node create-webhook.js

Я также создал еще один Gist для тестирования Webhook, который можно найти здесь. Этот сценарий поддерживает аргумент hook_id, поэтому вы можете легко протестировать любой Webhook на командном уровне. Используемый метод API octokit.repos.testPushHook:

// snippet from test-webhook.js Gist - view full Gist here
...
const res = await octokit.repos.testPushHook({
  owner: conf.owner,
  repo: conf.repo,
  hook_id: conf.hook_id
})
.catch(e => {
 console.log(e);
});

Чтобы использовать этот сценарий, выполните его с аргументом hook_id:

node test-webhook.js --hook_id '123456'

С развернутым активным Webhook мы можем теперь сосредоточиться на сценарии Ansible.

Настройка Ansible Playbook

В этом разделе мы возьмем сценарий, написанный в предыдущей статье (который также касается Ansible Vault), и применим его к серверной среде, которая является нашим сервером развертывания. Мы будем хранить 4 файла в нашей playbook папке виртуальной среды, созданной ранее:

# ansible file structure
/var/deployment/ansible
   /playbook
      ansible.cfg
      inventory.yml
      playbook.yml
      /group_vars
         all.yml

Мы разделили все файлы, связанные с Ansible, в папке playbook. Давайте разберем эти файлы.

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

Файл конфигурации Ansible с именем ansible.cfg. Этот файл просто перезаписывает некоторые значения по умолчанию, чтобы включить пересылку агента SSH:

# ansible.cfg
[defaults]
transport = ssh
[ssh_connection]
ssh_args = -o ForwardAgent=yes

Групповые переменные

Папка group_vars, внутри которой all.yml. Ansible распознает group_vars как каталог для хранения переменных для каждого хоста, причем имя файла является именем хоста. В нашем случае all.yml означает, что переменные доступны для всех наших определенных хостов. В этой папке мы будем хранить зашифрованный пароль, сгенерированный Ansible Vault для ваших производственных паролей SSH:

# generating encryped password with Ansible Vault
# store vault passphrase
echo '<vault_passphrase>' ~/.ansible-vault-pw
# encrypt production ssh password
ansible-vault encrypt_string \
--vault-id user@~/.ansible-vault-pw \
'<ssh_password>' \
--name 'production_server1_password
# encrypted password will be output
> production_server1_password: !vault |
            $ANSIBLE_VAULT;1.2;AES256;user
            ...

Замените выделенный полужирным текст своей собственной парольной фразой Vault, именем пользователя и паролем SSH соответственно. Я назвал переменную пароля production_server1_password, но ее можно изменить на то, что подходит для ваших настроек.

Скопируйте весь зашифрованный пароль, выводимый ansible-vault, и вставьте его в group_vars/all.yml. Это единственная групповая переменная, которая нам понадобится.

Файл инвентаризации

Для каждой playbook требуется файл инвентаризации, в котором перечислены удаленные хосты, к которым необходимо подключиться. Наш инвентарный файл состоит из одного хоста - вашего производственного сервера приложений:

webservers:
  hosts:
      production_server1:
        ansible_connection: ssh
        ansible_host: "<server_ip_address>"
        ansible_user: <user>
        ansible_password: production_server1_password

Примечание. Помните, что ранее мы добавили этот сервер в known_hosts и использовали ssh-copy-id для настройки автоматической аутентификации на сервере.

Пособие

playbook.yml - это точно такой же сценарий, который мы обсуждали в предыдущей статье. Для удобства я создал Суть для обозначения:

# Github -> Production Deployment
#
# Change vars to point to your own folders
# These folders should exist on your production servers
---
- name: React App Deployment from Github Repository
  connection: ssh
  gather_facts: false
  hosts: all
  vars:
  
  ...
# full gist available here

Теперь вы сможете запускать playbook в Терминале со своего сервера развертывания. Запустите сейчас, чтобы убедиться, что playbook выполняется должным образом, с высоким уровнем детализации для выявления любых проблем:

# test Ansible playbook
# ensure virtual environment is activated
cd /var/deployment/ansible && source bin/activate
# go to playbook directory
cd playbook
# run playbook
ansible-playbook -i inventory.yml --vault-id user@/home/user/.ansible-vault-pw playbook.yml -vvv

Примечание. Я намеренно использовал здесь абсолютные пути к файлам. Убедитесь, что ваша кодовая фраза Vault указывает на правильную папку пользователя.

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

Программное выполнение Playbook через Express Route

Возвращаясь к нашему серверу Express, мы заинтересованы в изменении маршрута индекса по адресу routes/index.js:

// /var/deployment/server/routes/index.js
router.post('/', async function (req, res, next) {
   ...
}

Сюда будут направляться все Webhooks. Давайте расширим это, чтобы сначала обрабатывать только push-события из основной ветки:

if (req.body.ref !== 'refs/heads/master') {
   console.log('not master branch. ignore');
   return;
}

Чтобы выполнить команду Ansible, включите следующие пакеты, которые перенесут exec() в область видимости и обернут ее функцией на основе обещаний, чтобы мы могли вызывать ее асинхронно. :

var express = require('express');
var router = express.Router();
const util = require('util');
const exec = util.promisify(require('child_process').exec);
...

Это позволит playbook запускаться асинхронно как обещание, позволяя Webhook разрешить немедленно, давая Github знать, что Webhook был успешно получен. Это также исключает возможность тайм-аута в случае, если playbook выполняется синхронно и требует много времени для завершения.

Теперь мы сможем запустить ansible-playbook с exec(). Это команда целиком, в первую очередь определяющая переменную среды ANSIBLE_CONFIG перед вызовом двоичного файла ansible-playbook из нашей среды vritual:

// run Ansible script
console.log('executing deployment...');
exec('ANSIBLE_CONFIG=/var/deployment/ansible/playbook/ansible.cfg /var/deployment/ansible/bin/ansible-playbook -i /var/deployment/ansible/playbook/inventory.yml --vault-id user@/home/user/.ansible-vault-pw /var/deployment/ansible/playbook/playbook.yml');
  • Когда вызывается ansible-playbook, он будет искать переменную среды ANSIBLE_CONFIG для любых файлов конфигурации. Без определения этого файла наш ansible.cfg файл не будет получен
  • Мы вызываем ansible-playbook из нашей виртуальной среды. Бинарный файл находится по адресу /var/deployment/ansible/bin/ansible-playbook.
  • Все остальные аргументы приведены на абсолютном уровне. Это необходимо, поскольку в нашем процессе PM2 не будет контекста папки.
  • Обратите внимание, что user был снова использован для пользователя Vault - это может быть изменено в зависимости от настроек вашего пользователя.

Наконец, мы могли заключить все в блок try catch и вернуть ответ, чтобы Github узнал, что Webhook был успешно получен. Это полная реализация маршрута:

var express = require('express');
var router = express.Router();
const util = require('util');
const exec = util.promisify(require('child_process').exec);
router.post('/', async function (req, res, next) {
   try {
      if (req.body.ref !== 'refs/heads/master') {
         console.log('not master branch. ignore');
         return;
      }
      // run Ansible script
      console.log('executing deployment...');
      exec('ANSIBLE_CONFIG=/var/deployment/ansible/playbook/ansible.cfg /var/deployment/ansible/bin/ansible-playbook -i /var/deployment/ansible/playbook/inventory.yml --vault-id user@/home/user/.ansible-vault-pw /var/deployment/ansible/playbook/playbook.yml');
   } catch (e) {
      console.log('error');
   }
   res.json({ received: true });
});
module.exports = router;

Резюме

На этом мы завершаем настройку нашего сервера развертывания, который успешно обрабатывает Webhooks из Github, чтобы запустить Ansible playbook для развертывания обновленного приложения на ваших производственных серверах.

Отсюда вы можете изучить другие механизмы для расширения этой концепции. Вот несколько областей, на которые стоит обратить внимание:

  • Механизм постановки в очередь, с помощью которого события извлечения, которые происходят в быстрой последовательности, могут быть поставлены в очередь и выполнены по порядку.
  • Разрешите playbook настроить структуру папок репозитория вашего производственного сервера для клонирования и сборки. Сейчас мы полагаем, что эти папки уже существуют на производственных серверах.
  • Расширьте playbook для поддержки нескольких репозиториев и других веток, кроме master. Ваш список репозиториев может быть сгруппирован в group_vars, а также соответствующие удаленные хосты
  • Система регистрации для записи того, какие запросы были успешно выполнены, а какие не удалось выполнить по какой-либо причине.

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