Имеется ресурс группа, которая включает в себя один 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 выглядит так:
В целом на этом деплой заканчивается.