Имеется ресурс группа, которая включает в себя один Azure Load Balancer, за котором находится Azure VMSS с двумя виртуальными машинами.
На машинах работает NGINX, который использует файлы настроек виртуалхостов, которые хранятся в файл-шаре, которая монтируется к обеим машинам. Полное описание проекта — Azure: VMSS за Load Balancer, renew SSL от Let’s Encrypt, SMB file share и NGINX-proxy.
Ниже — описание того, как была реализована система апдейта конфигов через Github с использованием Travis, bash и Python.
Файл-шара, которая подключается к интансам выглядит так:
datahtml — содержит каталог .well-know для ACME-авторизации при renew Let’s Enrypt.
datanginx-conf.d — содержит *.conf файлы, которые используются NGINX на обоих интансах, именно сюда выполняется деплой новых конфигов из репозитория.
Содержание
Deployment workflow
Процесс выглядит следующим образом:
- Travis чекаутит репозиторий из Github, в котором хранятся
*.confфайлы - Travis выполняет
nginx -t, что бы проверить, что файлы настроек в репозитории корректны (например — после апдейта какого-то файла кем-то из девелоперов) - Travis запускает скрипт деплоя на Python, который:
- создаёт бекап текущих файлов конфиграций из файл-шары
- копирует новые конфиги из репозитория в эту шару
- проверяет их ещё раз, уже на мастере и секондари, через
nginx -t - перезапускает NGINX
Let’s Ecnrypt renew
В процессе настройки — немного изменился процесс renew сертификатов.
Если изначально задачи были добавлены в cron — то теперь я их вынес в отдельный скрипт.
Задачи в кроне выглядели так:
30 2 * * 1 /opt/letsencrypt/letsencrypt-auto renew >> /var/log/le-renew.log 30 2 * * 1 rsync -avh /etc/letsencrypt/archive/jm-gw-proxy-dev.domain.ms /data/dataletsencrypt/ 35 2 * * 1 service nginx reload
Новый скрипт:
#!/usr/bin/env bash
GIT_USER=username
GIT_PASS=password
le_renew () {
/opt/letsencrypt/letsencrypt-auto renew -vvv --post-hook "systemctl reload nginx"
}
le_rsync_to_secondary () {
local secondary_host=$1
local secondary_user=$2
rsync -avh --rsync-path="sudo rsync" /etc/letsencrypt/{live,archive} $secondary_user@$secondary_host:/etc/letsencrypt/
}
le_rsync_to_repo () {
local git_user=$1
local git_pass=$2
cd /root
git clone https://$git_user:[email protected]/jm/jm-gw-proxy-data.git
rsync -avh /etc/letsencrypt/{live,archive} /root/jm-gw-proxy-data/tests/letsencrypt
cd /root/jm-gw-proxy-data
git add -A && git commit -m "Lets Encrypt SSL synchronization on $(date +"%m_%d_%Y_%H_%M")" && git push
}
repo_cleanup_local () {
rm -rf /root/jm-gw-proxy-data
}
# 1. make `renew`
# 2. rsync from /etc/letsencrypt/{live,archive} on master to /etc/letsencrypt/ on secondary
# 3. clone jm-gw-proxy-data repo to /root
# 4. rsync /etc/letsencrypt/{live,archive} to repo jm-gw-proxy-data/tests/letsencrypt
# 5. commit && push
le_renew || exit 1
le_rsync_to_secondary 10.0.0.5 jmadmin || exit 1
le_rsync_to_repo $GIT_USER $GIT_PASS || exit 1
repo_cleanup_local
Github — тут>>>.
Запускается он на «мастере», раз в неделю, по понедельникам, в 2 часа 30 минут:
30 2 * * 1 /root/le_update.sh
Скрипт выполняет:
- Let’s Ecnrypt
renewсертификатов - копирует обновлённые ключи на secondary
- клонирует репозиторий (Github) на мастер, копирует ключи в него, пушит изменения обратно
Travis
Непосредственно деплой данных выполняется Travis.
Репозиторий выглядит так:
[simterm]
$ ls -l | sort drwxr-xr-x 2 setevoy setevoy 4096 Aug 4 16:44 scripts drwxr-xr-x 4 setevoy setevoy 4096 Aug 4 10:59 tests -rw-r--r-- 1 setevoy setevoy 1019 Aug 2 15:11 music.domain.de.conf -rw-r--r-- 1 setevoy setevoy 1020 Aug 2 15:11 domain.de.conf -rw-r--r-- 1 setevoy setevoy 1029 Aug 2 15:11 preview.domain.com.conf -rw-r--r-- 1 setevoy setevoy 102 Aug 4 16:21 README.md ...
[/simterm]
Всего тут:
[simterm]
$ ls -l | grep .conf | wc -l 103
[/simterm]
103 файла виртуалхостов для NGINX.
Каталог tests содержит:
[simterm]
$ ls -l tests/ total 16 drwxr-xr-x 4 setevoy setevoy 4096 Aug 4 10:50 letsencrypt -rw-r--r-- 1 setevoy setevoy 3956 Aug 4 10:59 mime.types -rw-r--r-- 1 setevoy setevoy 2310 Aug 3 16:48 nginx.conf drwxr-xr-x 3 setevoy setevoy 4096 Aug 4 10:54 nginx_ssl
[/simterm]
Где letsencrypt содержит копию каталогов live и archive с master-хоста с ключами, которые обновляются скриптом le_update.sh.
nginx.conf и mime.types используются для проверки файлов конфигурации перед деплоем.
.travis.yml
.travis.yml выглядит следующим образом:
language: python
python:
- "3.6"
sudo: required
services:
- docker
before_install:
- ./scripts/nginx_test.sh
install:
- pip install azure-storage
- pip install fabric3
script:
- ./scripts/deploy.py
branches:
only:
- master
notifications:
slack:
rooms:
- akqa:Ybs***h5u#jm-devops
on_failure: always
on_success: always
В блоке before_install:
...
before_install:
- ./scripts/nginx_test.sh
...
Выполняется проверка корректности файлов конфигураций перед тем, как начинать бекап-деплой.
Для проверки — Travis запускает скрипт nginx_test.sh, который запускает контейнер с NGINX, монтирует файлы и каталоги с сертификатами, и выполняет nginx -t:
#!/usr/bin/env bash
docker run -ti -v $(pwd):/etc/nginx/conf.d \
-v $(pwd)/tests/nginx.conf:/etc/nginx/nginx.conf \
-v $(pwd)/tests/mime.types:/etc/nginx/mime.types \
-v $(pwd)/tests/letsencrypt/:/etc/letsencrypt/ \
-v $(pwd)/tests/nginx_ssl/ssl:/etc/nginx/ssl \
nginx nginx -t
if [ $? == 0 ]; then
echo -e "\nOK: NGINX test passed."
else
echo -e "\nERROR: NGINX configs test failed. Exit."
exit 1
fi
В результатах Travis этот шаг выглядит так:
…
$ ./scripts/nginx_test.sh
Unable to find image ‘nginx:latest’ locally
latest: Pulling from library/nginx
Status: Downloaded newer image for nginx:latest
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
OK: NGINX test passed.
…
Деплой
Сам деплой реализован на Python, с использованием Azure SDK.
Деплой разбит на несколько шагов:
- из Aure File Share копируются все данные в Azure Storage Account, в контейнер с бекапами
- все
*.confфайлы из текущей директории (т.е. корня репозитория) копируются в Aure File Share, перезаписывая имеющиеся там файлы - скрипт выполняет
nginx reloadна мастере и секондари:- валидация — сначала выполняется
nginx -tс конфигами из файл-шары - если ОК — то выполняется
systemctl reload nginx.service - проверяется работа NGINX — отдаёт ли 200, если ОК — то скрипт переходит к следующему серверу
- валидация — сначала выполняется
Сам скрипт:
#!/usr/bin/env python
"""Backup and deploy script"""
import os
import sys
import datetime
import glob
import shutil
from fabric.api import run, env, put, sudo
import azure.common
from azure.storage.file import FileService
from azure.storage.blob import BlockBlobService
def get_backup(gw_account_name, gw_account_key, gw_account_share, backup_local_path):
"""Upload directories and files from $account_name to local $backup_local_path using Azure FileService"""
print('\nRunning get_backup from the {} and file share {} to local path {}.\n'.format(gw_account_name, gw_account_share, backup_local_path))
file_service = FileService(account_name=gw_account_name, account_key=gw_account_key)
share_dirs_list = file_service.list_directories_and_files(gw_account_share)
for share_dir_name in share_dirs_list:
backup_local_dir = os.path.join(backup_local_path, share_dir_name.name)
if not os.path.isdir(backup_local_dir):
print('Local backup directory {} not found, creating...'.format(backup_local_dir))
os.makedirs(backup_local_dir)
share_files_list = file_service.list_directories_and_files(gw_account_share, share_dir_name.name)
for share_file in share_files_list:
try:
print('Getting file: {}'.format(os.path.join('/', share_dir_name.name, share_file.name)))
# example:
# file_service.get_file_to_path('gwdevproxydata', 'datanginx-conf.d', 'jm-gw-proxy-dev.domain.tld.conf', '/tmp/jm-gw-proxy-dev.domain.tld.conf-out')
file_service.get_file_to_path(gw_account_share, share_dir_name.name, share_file.name, os.path.join(backup_local_dir, share_file.name))
# to pass /data/datahtml/.well-known dir on master host
except azure.common.AzureMissingResourceHttpError as e:
print('\nWARNING: {}\n'.format(e))
def push_backup(bac_account_name, bac_account_key, bac_container_name, backup_local_path):
"""Upload directories and files from $backup_local_path to account_name using Azure BlockBlobService"""
print('\nRunning push_backup from local path {} to the {} and container {}\n'.format(backup_local_path, bac_account_name, bac_container_name))
now = datetime.datetime.today().strftime('%Y_%m_%d_%H_%M')
for root, dirs, files in os.walk(backup_local_path, topdown=True):
for name in dirs:
path = os.path.join(root, name)
for filename in os.listdir(path):
fullpath = os.path.join(path, filename)
block_blob_service = BlockBlobService(account_name=bac_account_name, account_key=bac_account_key)
# example
# block_blob_service.create_blob_from_path(container_name, 'datanginx-conf.d/jm-gw-proxy-production.domain.tld.conf', '/tmp/datanginx-conf.d/jm-gw-proxy-production.domain.tld.conf')
print('Uploading {} as {}'.format(fullpath, os.path.join(now, name, filename)))
block_blob_service.create_blob_from_path(bac_container_name, os.path.join(now, name, filename), fullpath)
def update_datanginxconfd(gw_account_name, gw_account_key, gw_account_share):
"""Upload data from cloned repo to the GW Storage account into the datanginx-conf.d directory with overwriting"""
print('\nRunning update_confd to the {} and file share {} to the path datanginx-conf.d.\n'.format(gw_account_name, gw_account_share))
file_service = FileService(account_name=gw_account_name, account_key=gw_account_key)
configs = glob.glob('*.conf')
for config in configs:
print('Uploading config: {}'.format(config))
file_service.create_file_from_path(gw_account_share, 'datanginx-conf.d', config, config)
def cleanup_backup_local_path(backup_local_path):
print('Cleaning up local {} directory....'.format(backup_local_path))
shutil.rmtree(backup_local_path)
def nginx_reload(gw_proxy_host, gw_proxy_user, gw_proxy_key, gw_proxy_ports):
"""Will connect to the Master and Secondary host to execute "nginx -t" before reload"""
for port in gw_proxy_ports:
env.host_string = gw_proxy_host + ':' + str(port)
env.key_filename = [os.path.join('.ssh', gw_proxy_key)]
env.user = gw_proxy_user
validate_status = sudo('nginx -t')
if validate_status.return_code == 0:
print('OK: NGINX configs validated\n')
else:
print('ERROR: can\'t validate NGINX\n')
exit(1)
reload_status = sudo('systemctl reload nginx.service')
if reload_status.return_code == 0:
print('\nOK: NGINX reload complete\n')
else:
print('\nERROR: can\'t reload NGINX\n')
exit(1)
nginx_status = run('curl -s localhost > /dev/null')
if nginx_status.return_code == 0:
print('\nOK: NGINX reloaded status code: {}\n'.format(nginx_status.return_code))
else:
print('\nERROR: NGINX reloaded status code: {}\n'.format(nginx_status.return_code))
exit(1)
if __name__ == "__main__":
try:
gw_account_name = os.environ['GW_ACCOUNT_NAME']
gw_account_key = os.environ['GW_ACCOUNT_KEY']
gw_account_share = os.environ['GW_ACCOUNT_SHARE']
bac_account_name = os.environ['BAC_ACCOUNT_NAME']
bac_account_key = os.environ['BAC_ACCOUNT_KEY']
bac_account_container = os.environ['BAC_ACCOUNT_CONTAINER']
backup_local_path = os.environ['BAC_LOCAL_PATH']
gw_proxy_host = os.environ['GW_PROXY_HOST']
gw_proxy_user = os.environ['GW_PROXY_USER']
gw_proxy_key = os.environ['GW_PROXY_KEY']
gw_proxy_ports = [2200, 2201]
except KeyError as e:
print('ERROR: no such environment variable - {}'.format(e))
sys.exit(1)
# download all files and directories from:
# $gw_account_name (jmgatewayproxydata) $gw_account_share (gwproxydata)
# to $backup_local_path (/tmp/GW_TEMP_BACKUP)
get_backup(gw_account_name, gw_account_key, gw_account_share, backup_local_path)
# upload all data from:
# $backup_local_path (/tmp/GW_TEMP_BACKUP)
# to:
# $bac_account_name (jmbackup) $bac_account_container (jm-gw-proxy-backup)
push_backup(bac_account_name, bac_account_key, bac_account_container, backup_local_path)
# upload all *.conf files from local directory (i.e. cloned repository)
# to the $gw_account_name (jmgatewayproxydata) $gw_account_share (gwproxydata)
update_datanginxconfd(gw_account_name, gw_account_key, gw_account_share)
# SSH to the $gw_proxy_host (jm-gw-proxy-production.domain.tld)
# and execute:
# 1. nginx -t
# 2. systemctl reload nginx.service
# 3. curl -s localhost > /dev/null
nginx_reload(gw_proxy_host, gw_proxy_user, gw_proxy_key, gw_proxy_ports)
# remove local $backup_local_path (/tmp/GW_TEMP_BACKUP)
# just in case, as anyway builds are in Travis Docker containers
cleanup_backup_local_path(backup_local_path)
Github — тут>>>.
Завершение деплоя в Travis выглядит так:

