Azure: CDN, NodeJS, Docker Swarm и Blue-Green деплой

Автор: | 24/04/2017

Имеется проект на Azure, которым я занимался большую часть последнего года (с 20-го марта 2016).

Пост Azure: почему никогда писался под впечатлением работы как раз на нём (да и большая часть рубрики Azure – тоже).

За год мы перенесли приложение на Umbraco CMS с MS SQL базами от другого агентства к нам (и с Ажуры на Ажур), пачку небольших WPMU сайтов (см. тут>>>) и другие сервисы – в основном на PHP, и… с переменными на немецком. Дебаг этого кода при миграции доставлял неимоверно. Весь процесс миграции должен закончится большим феерверком – запуском новой версии основного сайта производителя алкогольной продукции (завтра, 25.-го).

Текущий деплой Umbraco CMS (legacy-сайт) описан в посте Azure: GoCD и MSDeploy – деплой UmbracoCMS в Azure WebServices.

Ниже кратко описана новая инфрастуктура с некоторыми примерами. Увы – не было возможности более подробно описать все сложности, с которыми столкнулся в процессе – но несколько интересных примеров будет.

Описание проекта

Основой проекта является веб-сайт, который раньше работал на Umbraco CMS, а сейчас мигрирует на NodeJS (а с ним ещё ~90 доменов на Azure DNS).

Новая версия приложения включает в себя 6 контейнеров, один из которых – jm-website-proxy – представляет собой локальный NGINX с реверс-прокси для реврайтов и редиректов. Кроме того – есть ещё и внешний прокси, который обслуживает все домены и приложения проекта.

Рабочее окружение включает в себя три (Dev, QA – две) ресурс-группы – в одной находятся CDN и Traffic Manager, в двух других (Blue и Green) – VMSS с Docker-Swarm:

Pusblish и Preview TM – используются для доступа к актуальной версии (Publish) и версии для редакторов (Preview). Бекенд – Contentful.

Больше всего сложностей во всём этом (и у девелоперов тоже хватило внезапных проблем) оказался Azure CDN (Verizon Premium). Из-за того, что Verizon CDN не позволяет привязать домен иначе, как через CNAME (который мы не могли использовать, т.к. на домене имеется множество субдоменов, почта и т.д., подробнее см. RFC) – пришлось выстраивать целую цепочку редиректов и SSL-терминаторов.

В результате была построена такая схема. Речь о Publish сервисе, т.к. он основной:

  1. Пользователь приходит на http://domain.tld
  2. domain.tld – направлен на внешний прокси-сервис проекта (NGINX), который выполняет переадресацию на https://www.domain.tld
  3. Субдомен www.domain.tld через CNAME является алиасом ендпоинта CDN – jm-website-production-cdn-endpoint.azureedge.net
  4. У CDN в качестве Origin – указан URL Traffic Managerjm-website-production-publish.trafficmanager.net
  5. У Traffic Manager – имеется два endpoint-а – Green и Blue. Green направлен на URL publish-green.domain.tld, Blue – на publish-blue.domain.tld.
    В свою очередь publish-green.domain.tld и blue через CNAME направлены на Load Balancer-ы в Green и Blue ресурс-группах, за которым находятся Swarm-ноды в VMSS. В один момент времени – только один из ендпоинтов ТМ может быть активен (тут мы и делаем Blue/Green deployment).

Если кратко – то так.

С SSL тоже оказалось не всё гладко.

Сам Azure при добавлении к CDN-профайлу CustomDomain и активации HTTPS для него – выдаёт для этого домена сертифкат от DigiCert (через письмо на admin-c контакт домена). Ожидалось – что на этом мы закрываем SSL-сессию, и дальше работаем с CDN-ендпоинтами по HTTP. Оказалось, что для работы самого CDN по HTTPS – необходимо выстроить вторую SSL-сессию – от edge location серверов Verizon до наших VMSS с приложением.

Сначала решили делать это через добавление Application Gateway, но потом всё-равно пришлось добавлять локальный NGINX как сервис в общий стек для реврайтов старых URI в новые (на внешнем NGINX это было бы сложнее, т.к. много доменов/конфигов), поэтому SSL подняли там.

SSL:

  1. Первый сертификат, который встречает пользователь – выдан DigiCert для домена(ов), которые подключены к Azure CDN как CustomDomains. SSL termination выполняется на CDN Edge-location сервере.
  2. Между Azure CDN и VMSS с нодами – второе HTTPS соединение, с самоподписанным сертификатом – CDN вполне с ними работает. SSL termination выполняется на локальном прокси-сервисе (jm-website-proxy) непосредственно на нодах.

Jenkins

Рабочее окружение Jenkins

Развёртывание новых окружений, сборка новых образов и их деплой выполняются Jenkins.

Сам Jenkins работает в Docker-контейнере, ноды с билдами запускаются тоже в контейнерах.

Jenkins работает на отдельной VM, а все его данные ($JENKINS_HOME) – хранятся на отдельном data-диске, который подключается к VM во время провижена группы.

Выглядит группа так:

(data-диск – Managed, поэтому тут не виден)

Сам шаблон можно посмотреть тут>>>.

Из интересных примеров в нём – подключение data-диска с workspaces Jenkins-a:

...
    { 
      "apiVersion": "2016-04-30-preview",
      "type": "Microsoft.Compute/virtualMachines",
      "name": "[variables('vmName')]",
      ...
          "osDisk": {
            "createOption": "FromImage"
          },
          "dataDisks": [
              {   
                  "lun": 0,
                  "createOption": "Attach",
                  "caching": "None",
                  "diskSizeGB": 100,
                  "managedDisk": {
                      "id": "/subscriptions/0a4f2b9c-***-40b17ef8c3ab/resourceGroups/europe-jm/providers/Microsoft.Compute/disks/jenkinsworkspaces"
                  }
              }
          ]
        },

И вызов скрипта для установки и запуска самого Jenkinsjenkins_provision.sh с помощью CustomScript extention для VM Azure:

...
    {
      "type": "Microsoft.Compute/virtualMachines/extensions",
      "name": "[concat(variables('vmName'),'/DockerInstall')]",
      "apiVersion": "2015-05-01-preview",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/', variables('vmName'))]"
      ],
      "properties": {
        "publisher": "Microsoft.Azure.Extensions",
        "type": "CustomScript",
        "typeHandlerVersion": "2.0",
        "autoUpgradeMinorVersion": true,
        "settings": {
          "fileUris": [
            "https://utils.blob.core.windows.net/scripts/jenkins_provision.sh"
            ],
          "commandToExecute": "bash jenkins_provision.sh"
        },
        "protectedSettings": {
          "storageAccountName": "utils",
          "storageAccountKey": "tOk***6Bw=="
        }
      }
    }
...

Сам скрипт jenkins_provision.sh:

#!/usr/bin/env bash

curl https://get.docker.com/ | bash
usermod -aG docker jmadmin
mkdir /jenkins
mount /dev/sdc1 /jenkins/
useradd jenkins
usermod -a -G docker jenkins
docker run -u 0 -tid -p 80:8080 \
  -v /jenkins:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /usr/bin/docker:/usr/bin/docker \
  -v /etc/passwd:/etc/passwd \
  -e JENKINS_HOME='/var/jenkins_home' --name jenkins jenkins

В строке:

...
mount /dev/sdc1 /jenkins/
...

выполняется монтирование внешнего дата-диска “Microsoft.Compute/disks/jenkinsworkspaces“.

Вместе – шаблон и скрипт создают новую виртуальную машину, устанавливают туда Docker, подключают диск с данными Jenkins-а и запускают последнюю версию Jenkins.

Подробнее – в постах Azure: подключение дополнительного диска к VM и миграция Jenkins и Azure: Azure Resource Manager provisioning и Jenkins в Docker.

Билды Jenkins

Основная задача Jenkins – собирать образы с NodeJS приложением при изменениях в репозитории. Создание Github-вебхука описано тут>>>.

Билды описаны в Groovy-скриптах:

[simterm]

$ ls -l
total 76
-rw-r--r-- 1 setevoy setevoy  254 Mar  2 17:44 build.groovy
drwxr-xr-x 2 setevoy setevoy 4096 Mar  2 13:34 configs
-rwxr-xr-x 1 setevoy setevoy 2362 Mar  2 11:13 deploy.sh
-rw-r--r-- 1 setevoy setevoy 6447 Mar 11 13:38 docker-compose_v3.yml
-rw-r--r-- 1 setevoy setevoy 2492 Mar  7 17:15 docker-compose.yml
-rw-r--r-- 1 setevoy setevoy 1236 Mar  3 11:37 docker-compose.yml.origin
-rw-r--r-- 1 setevoy setevoy   93 Mar  3 10:41 Dockerfile
-rw-r--r-- 1 setevoy setevoy   30 Feb 21 11:20 hooktest.groovy
-rw-r--r-- 1 setevoy setevoy 2319 Mar  8 14:01 jm-build.groovy
-rw-r--r-- 1 setevoy setevoy  568 Mar  8 14:02 jm-cms-transform-build.groovy
-rw-r--r-- 1 setevoy setevoy  394 Feb 24 16:02 jm-cms-transform-DEV-deploy.groovy
-rw-r--r-- 1 setevoy setevoy 1452 Apr  7 14:31 jm-provision.groovy
-rw-r--r-- 1 setevoy setevoy  779 Apr  7 15:51 jm-sw-dev-provision.groovy
-rw-r--r-- 1 setevoy setevoy  881 Apr  7 14:32 jm-sw-production-provision.groovy
-rw-r--r-- 1 setevoy setevoy  875 Apr  7 17:33 jm-sw-staging-provision.groovy
-rw-r--r-- 1 setevoy setevoy  539 Mar  8 14:02 jm-website-build.groovy
-rw-r--r-- 1 setevoy setevoy  390 Feb 24 16:02 jm-website-DEV-deploy.groovy
-rw-r--r-- 1 setevoy setevoy   12 Feb 21 10:33 README.md

[/simterm]

По аналогии с сетапом из поста AWS: билд Java + Maven + Docker + Packer + Terraform – имеется один общий скрипт, который содержит функции и несколько скриптов – для каждого из приложений (все на NodeJS, все собираются практически одинаково).

Скрипт с функциями:

#!/usr/bin/env groovy

def dockerBuild (imgName='1') {

    stage ('Docker build') {

        withDockerRegistry(registry: [credentialsId: 'jm-docker-hub-jmautomation']){

            sh 'git log -n 1 > version.html'

            def appimage = docker.build("jmrakqa/${imgName}:${TAG}", "--build-arg NPM_TOKEN=${NPM_TOKEN} .")

                appimage.push()
                appimage.push('latest')
        }
    }
}
def dockerDeploy (tag='1', host='2') { 
 
    git branch: "${BRANCH}", credentialsId: 'jm-github', url: 'https://github.com/jm/website-prototype.git' 
 
    stage ('Docker deploy') { 
 
        sh "echo -e \"Deploying to the DEV: ${host}\nWith TAG=${tag}\"" 
 
        sh "./buildscripts/deploy.sh ${host} ${tag}" 
    } 
} 
 
def cleanup() { 
    sh 'echo -e "\nWiping workdir $(pwd).\n"' 
    deleteDir() 
} 
 
return this

И скрипт сборки одного из приложений:

#!/usr/bin/env groovy

node {

    ENV='dev'
    TAG = "${BRANCH}-${env.BUILD_NUMBER}"
    REPOURL = 'https://github.com/jm/jm-website.git'

    dir('buildscripts') {
        git branch: 'master', credentialsId: 'jm-github', url: 'https://github.com/jm/jm-jenkins.git'
    }

    git branch: "${BRANCH}", credentialsId: 'jm-github', url: "${REPOURL}"

    def website = load 'buildscripts/jm-build.groovy'

    website.dockerBuild('website-frontend')
    website.cleanup()
}

Скрипты из репозитория ‘https://github.com/jm/jm-jenkins.git‘ загружаются в директорию buildscripts, в а корень билд-директории – загружается код из репозитория с кодом – REPOURL = ‘https://github.com/jm/jm-website.git’.

В каждом репозитории с кодом имеется Dockerfile, в котором и описана дальнейшая сборка сервиса:

FROM node:7.5.0

ENV NODE_ENV production
ARG NPM_TOKEN="${NPM_TOKEN}"

RUN useradd --user-group --create-home --shell /bin/false app
RUN apt-get update && apt-get -y install rsync

ENV HOME=/home/app

COPY . $HOME

# https://github.com/docker/docker/issues/30110
RUN chown -R app:app $HOME/

USER app
WORKDIR $HOME
RUN pwd && ls -l
RUN npm config set //registry.npmjs.org/:_authToken=${NPM_TOKEN}
RUN npm install --production=false
RUN npm run build:production && npm prune --production

CMD ["npm", "run", "start:production"]

Передача параметров (токен для Node репозитория, например) выполняется через --build-args.

Деплой собранного образа выполняется из другого скрипта другой Jenkins-джобой:

#!/usr/bin/env groovy

node {

    git branch: "${BRANCH}", credentialsId: 'jm-github', url: 'https://github.com/jm/jm-website.git'

    dir('buildscripts') {
        git branch: 'master', credentialsId: 'jm-github', url: 'https://github.com/jm/jm-jenkins.git'
    }

    def website = load 'buildscripts/jm-build.groovy'

    website.dockerDeploy("${IMAGE_TAG}", "$DEV_HOST")
}

Собранные образы загружаются в приватный репозиторий проекта на DockerHub.

Деплой

Деплой новых образов выполняется bash-скриптом, который обновляет compose-файл на сервере и пересоздаёт стек:

#!/usr/bin/env bash

HOST="$1"

USER="jmadmin"
RSA_KEY="buildscripts/.ssh/jmadmin"

# compose to be deployed to Swarm Master
COMPOSE="buildscripts/docker-compose.yml"

# Version for the :tag for image + $TRAVIS_JOB_NUMBER
# resulted vesrion will be ~ 1.0.3-44.1
# later, use ${BRANCH}-${env.BUILD_NUMBER} now
# VERSION=$(cat package.json | grep version | cut -d":" -f 2 | sed 's/"//g' | cut -d "," -f 1) > /dev/null
TAG=$2

# 1) generate new compose with sed docker-compose.yml;
# 2) upload new Compose file docker-compose.yml to $HOST;
# 3) stop Compose;
# 4) start Compose;

me="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")"

[[ -z $HOST ]] && { echo -e "\nERROR: HOST must be specified as a first argument. Exit.\n"; exit 1; }
[[ -z $TAG ]] && { echo -e "\nERROR: TAG must be specified as a second argument. Exit.\n"; exit 1; }
[[ -e $RSA_KEY ]] && chmod 400 $RSA_KEY || { echo -e "\nERROR: RSA key $RSA_KEY not found. Exit.\n"; exit 1; }

# no need atm as using LATEST tag
compose_generate () {
    # sed -e s/CHANGEME/"$1"/g scripts/docker-compose.yml.erb > scripts/docker-compose.yml
    sed -i "s/BUILDTAG/$TAG/" $COMPOSE
}

# no need atm as using LATEST tag
compose_copy () {
    scp -P 2200 -o StrictHostKeyChecking=no -i $RSA_KEY $COMPOSE $USER@$1:/home/$USER/
}

compose_stop () {
    ssh -p 2200 -t -t -o StrictHostKeyChecking=no -i "$RSA_KEY" "$USER@$1" "bash -c '
        export DOCKER_HOST=:2375
        sudo docker-compose down --rmi all -v
    '"
}

compose_start () {
    ssh -p 2200 -t -t -o StrictHostKeyChecking=no -i "$RSA_KEY" "$USER@$1" "bash -c '
        docker-compose up -d
    '"
}

echo -e "\n[$me] $TAG deployment started at $(date) to the $HOST\n"

# no need atm as using LATEST tag
#if compose_generate $VERSION-$TRAVIS_JOB_NUMBER; then
#    echo -e "New Compose created:\n"
#    cat $COMPOSE
##else
#    echo -e "\nERROR: can not create $COMPOSE. Exit.\n"
#    exit 1
#fi

if compose_copy $HOST; then
    echo -e "\n$COMPOSE copied to the $HOST.\n"
else
    echo -e "\nERROR: can not copy $COMPOSE to the $HOST. Exit.\n"
    exit 1
fi

if compose_stop $HOST; then
    echo -e "Compose stopped.\n"
else
    echo -e "\nERROR: can not stop Compose. Exit.\n"
    exit 1
fi

if compose_start $HOST; then
    echo -e "Compose started.\n"
else
    echo -e "\nERROR: can not start Compose. Exit.\n"
    exit 1
fi

На самом деле деплой выглядит немного иначе, и скрипт другой – это старая версия, но суть та же – на одном из мастеров обновляется файл docker-compose.yml, который содержит актуальную версию образов (или просто тег `latest`), после чего на Traffic Manager-е проверяется текущая активная VMSS-нода (Blue или Green) и переключается ендпоинт. Может добавлю отдельным постом по B/G деплою на Azure с помощью Traffic Manager.

Сам docker-compose выглядит примерно так:

version: '3'

services:
  proxy:
    image: "jmakqa/jm-website-proxy:latest"
    ports:
      - "80:80"
      - "443:443"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    depends_on:
        - web
  web:
    environment:
      - JM_enableTrustProxy=true
      - JM_trustProxyIPsCSV=uniquelocal
      - JM_basicAuthEnabled=true
      - JM_contentEndpoint=http://transform:3003/get
      - JM_contentServer=http://transform:3003
      - JM_domainPattern=publish.jm-website-sw-staging.domain.tld
      - JM_internalPort=8008
      - JM_pollIntervalMs=30000
      - JM_publicPort=8008
      - JM_purgeCdn=true
    image: "jmakqa/jm-website:jm-website-version"
    ports:
      - "8008:8008"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    depends_on:
        - transform
...

Инфрастуктура

Описание

Далее я буду рассматривать Production, т.к. он имеет полный набор всего, что пришлось тут строить.

Как уже говорилось – рабочее окружение включает в себя три группы ресурсов – Switcher, Green и Blue.

Green и Blue – одинаковы, и включают в себя:

  • виртуальную сеть с двумя подсетями – для master и nodes VMSS
  • две VMSS – для master и nodes
  • два Load Balancer – master и nodes
  • две группы безопасности и два публичных IP-адреса

Switcher-группа – включает в себя:

  • CDN-профайл
    • его CDN-ендпоинт
  • Preview Traffic Manger profile – для доступа к Preview-сервисам (с двумя ендпоинтами – Green и Blue)
  • Publish Traffic Manger profile – для доступа к Publish-сервисам (с двумя ендпоинтами – Green и Blue)

CDN-ендпоинт имеет несколько добавленных CustomDomain, к которым Azure через DigiCert сам выдаёт сертификат при добавлении домена:

В качестве Origin, как говорилось – указан Publish Traffic Manager, через который CDN обращается к бекендам (Swarm-nodes VMSS с приложением) для загрузки данных.

Для доступа по HTTPS – на бекендах пришлось добавлять свои сертификаты (jm-website-proxy). Т.е. между пользователем и CDN – используется сертификат от DigiCert, а между серверами CDN и нашим VMSS – самоподписанный сертификат с нашего NGINX-а.

Развёртывание окружений

Как и в случае с Jenkins – группы ресурсов для окружений вписаны в ARM-шаблоны:

[simterm]

$ ls -l azure-infrastructure/jm-website/
total 16
drwxr-xr-x 3 setevoy setevoy 4096 Apr  7 17:12 CDN
drwxr-xr-x 3 setevoy setevoy 4096 Apr 21 14:17 ENV
drwxr-xr-x 2 setevoy setevoy 4096 Apr  7 16:12 NSG
drwxr-xr-x 3 setevoy setevoy 4096 Apr  3 14:43 obsolete

[/simterm]

  • CDN – шаблоны для рес. групп switchers
  • ENV – шаблоны непосредственно рабочих окружений с Docker Swarm
  • NSG – шаблоны групп безопасности (см. ниже)

Например:

[simterm]

$ ls -l azure-infrastructure/jm-website/ENV/
total 52
-rw-r--r-- 1 setevoy setevoy  1319 Apr  7 11:23 jm-website-sw-dev.parameters.json
-rw-r--r-- 1 setevoy setevoy 24604 Apr 21 13:05 jm-website-sw.json
-rw-r--r-- 1 setevoy setevoy  1338 Apr  3 14:43 jm-website-sw-production-blue.parameters.json
-rw-r--r-- 1 setevoy setevoy  1340 Apr  7 13:02 jm-website-sw-production-green.parameters.json
-rw-r--r-- 1 setevoy setevoy  1345 Apr 20 17:08 jm-website-sw-staging-blue.parameters.json
-rw-r--r-- 1 setevoy setevoy  1346 Apr 20 17:08 jm-website-sw-staging-green.parameters.json
-rw-r--r-- 1 root    root     1322 Apr 20 17:17 jm-website-sw-test.parameters.json

[/simterm]

Тут jm-website-sw.json – сам шаблон, jm-website-sw-dev.parameters.json – файл параметров.

Для CDN – картина немного другая:

[simterm]

$ ls -l azure-infrastructure/jm-website/CDN/
total 24
-rw-r--r-- 1 setevoy setevoy  297 Apr  3 14:43 jm-website-cdn-dev.parameters.json
-rw-r--r-- 1 setevoy setevoy 2243 Apr  6 16:29 jm-website-cdn.json
-rw-r--r-- 1 setevoy setevoy 6522 Apr  7 12:49 jm-website-cdn-tm.json
-rw-r--r-- 1 setevoy setevoy  895 Apr  3 14:43 jm-website-cdn-tm-production.parameters.json
-rw-r--r-- 1 setevoy setevoy  943 Apr  7 17:12 jm-website-cdn-tm-staging.parameters.json

[/simterm]

Файл jm-website-cdn.json содержит только CDN, а файл jm-website-cdn-tm.jsonCDN и Traffic Manager профайлы, для Staging и Production (на Dev и QA TM не используются, т.к. у них нет B/G деплоя).

Шаблоны можно посмотреть тут>>> и тут>>>.

Из интересного в этих шаблонах.

Во первых – сам провижен Docker Swarm. Пришлось лепить костыли, и писать свой скрипт, который разворачивает всю ферму (шаблон ACS с Docker Swarm от самого Azure оказался непригоден из-за невозможности подключить свою группу безопасности (!)).

Вызывается он так же из CustomScript extention:

...
          "extensionProfile": {
            "extensions": [
              {
                "name": "MasterSwarmInstall",
                "properties": {
                  "publisher": "Microsoft.Azure.Extensions",
                  "type": "CustomScript",
                  "typeHandlerVersion": "2.0",
                  "autoUpgradeMinorVersion": false,
                  "settings": {
                    "fileUris": [
                      "https://utils.blob.core.windows.net/scripts/swarm_master_provision.sh",
                      "https://utils.blob.core.windows.net/scripts/docker_cleanup.sh"
                    ],
                    "commandToExecute": "[concat('bash swarm_master_provision.sh ', '10.0.0.4 ', parameters('environment'))]"
                  },
                  "protectedSettings": {
                    "storageAccountName": "[parameters('storageAccountName')]",
                    "storageAccountKey": "[parameters('storageAccountKey')]"
                  }
                }
              }
            ]
          }
...

Причём IP первого мастера (‘10.0.0.4 ‘) пришлось хардкодить, т.к. вытащить IP из VMSS во время провижена оказалось… Долго, криво и муторно. Вот тут>>> парень из Azure Product team-ы пытался подсказать – но уже не было времени реализовывать.

Собственно, скрипты провижена можно посмотреть тут>>> (master) и тут>>> (nodes).

Кроме того, каждое окружение (Dev/QA/etc) имеет свой шаблон группы безопасности для нод (мастер-группа у всех одна с одним правилом – разрешить доступ по SSH).

Создаются NSG с помощью ресурса Microsoft.Resources/deployments, который загружает шаблон группы из Storage Account-а:

...
    {
      "apiVersion": "2015-01-01",
      "name": "[variables('NodesNSGname')]",
      "type": "Microsoft.Resources/deployments",
      "properties": {
        "mode": "incremental",
        "templateLink": {
          "uri": "[concat('https://utils.blob.core.windows.net/templates/jm-website-nsg-', parameters('environment'), '.json', parameters('SAStoken'))]",
          "contentVersion": "1.0.0.0"
        },
        "parameters": {
          "NodesNSGname":{"value": "[variables('NodesNSGname')]"}
        }
      }
    },
...

Подробнее – в посте Azure: ARM – подключение вложенного шаблона.

Развёртывание и обновление групп ресурсов выполняются тем же Jenkins, через Azure CLI, часть параметров – в файлах, часть – в переменных самого Jenkins.

Основной скрипт – jm-provision.groovy:

#!/usr/bin/env groovy

def templateValidate(infraUrl='1', env='2', resGroup='3', templateFile='4', paramFile='5') {

    docker.image('microsoft/azure-cli').inside('-v /var/run/docker.sock:/var/run/docker.sock') {

        git branch: "${BRANCH}", credentialsId: 'jm-github', url: "${infraUrl}"

        stage('Temlate validate') {

            sh "azure login -v -u ${AZURUSER} -p ${AZUREPASS}"
            sh "azure account set -v ${SUBSCRIPTION}"
            sh "azure group template validate -g ${resGroup} -f jm-website/ENV/${templateFile} -e jm-website/ENV/${paramFile}"
        }
    }
}

def environmentUpdate(infraUrl='1', env='2', resGroup='3', templateFile='4', paramFile='5', tag='6') {

    docker.image('microsoft/azure-cli').inside('-v /var/run/docker.sock:/var/run/docker.sock') {

        git branch: "${BRANCH}", credentialsId: 'jm-github', url: "${infraUrl}"

        stage('Environment update') {

            sh "azure login -v -u ${AZURUSER} -p ${AZUREPASS}"
            sh "azure account set -v ${SUBSCRIPTION}"
// for new environment
//            sh "azure group create -l westeurope -n ${resGroup} -f jm-website/${templateFile} -e jm-website/${paramFile}"
            sh "azure group deployment create -n ${tag} -g ${resGroup} -f jm-website/ENV/${templateFile} -e jm-website/ENV/${paramFile}"
        }
    }
}

def verify(msg='1') {

    stage 'Verify'
    input id: 'Deploy', message: "${msg}", ok: 'Deploy!'

}
return this

И скрипт апдейта Production:

#!/usr/bin/env groovy

node {

    ENV='production'
    TAG = "${env.BUILD_TAG}"
    INFRAURL = 'https://github.com/jm/azure-infrastructure.git'
    RESGROUP = "${RESGROUP}"
    TMPL = "${TMPL}"
    PARAMS = "${PARAMS}"

    dir('buildscripts') {
        git branch: 'master', credentialsId: 'jm-github', url: 'https://github.com/jm/jm-jenkins.git'
    }

    git branch: "${BRANCH}", credentialsId: 'jm-github', url: "${INFRAURL}"

    def provision = load 'buildscripts/jm-provision.groovy'

    provision.verify("WARNING: you are going to update PRODUCTION environment. Are you sure?")
    provision.templateValidate("${INFRAURL}", "${ENV}", "${RESGROUP}", "${TMPL}", "${PARAMS}")
    provision.verify("Is Verify OK? Proceed with an environment deployment?")
    provision.environmentUpdate("${INFRAURL}", "${ENV}", "${RESGROUP}", "${TMPL}", "${PARAMS}", "${TAG}")
}

В целом – это всё, попозже может быть добавлю ещё пару связанных постов, ибо проект (:cry:) ещё ни разу не закончен.