Достаточно…. Скажем так – интересная схема билда и деплоя одного приложения.
Приложение включает в себя 6 контейнеров (5 – сервисы самого приложения, и один контейнер – Zuul discovery service).
Сама идея и архитектура – красивая и достаточно сложная. Но использовать такое для билда и деплоя 5 контейнеров…
Overhead, overengineering. Ещё один антипаттерн того, как надо делать.
Эта же схема отлично реализуется с помощью Maven для билда образов Docker и AWS CloudFormation + AWS ECS для развёртывания сервисов приложения.
Тем нее менее – в одном из проектов архитектура такая есть, о ней и пост.
К этому же посту относится запись AWS: AWS CLI и bash – blue/green деплой AutoScale группы за ELB – в ней описан сам деплой новых AMI в AutoScale группу.
Ниже – подробно сам процесс билда и обновления приложения.
В билде и деплое участвуют:
- AWS: как IaaS провайдер;
- Docker: для контейнеров с сервсисами;
- Maven: сборка Java-кода и Docker-образов;
- JFrog Artifactiory: private Docker registry для хранения собранных образов;
- Packer: сборка AMI;
- Terraform: обновление AWS-конфигурации;
bash
, AWS CLI: деплой AutoScale групп.
Содержание
Архитектура
Немного об архитектуре.
В Amazon Web Services имеется три рабочих окружения – Development, Staging и собственно Production (ещё не запущен).
Каждое рабочее окружение включает в себя VPC с четырьмя подсетями (две публичные и две приватные), по два Load Balancer-a (LB, 1 на приложение, 1 на eureka – discovery service), и по 4 AutoScale группы (ASG): Blue и Green для API (само приложение), и аналогично для eureka-сервиса.
В каждую такую ASG входит 3 EC2 инстанса – два m3.medium для API за одним LB, и 1 t2.micro для eureka-сервиса за собственным LB:
К этому всему идёт по 8 security групп, NAT-инстансы (NAT-gateway не было видимо, когда эту схему начинали делать) и прочее по мелочи.
Сборка проекта
Процесс билда выглядит следующим образом:
- Maven
- сборка
jar
-архивов с приложением (1 архив на один сервис); docker-maven-plugin
– сборка Docker-образов с включенными jar-никами (1 образ на 1 сервис);docker-maven-plugin
– push образов в JFrog Artifactory.
- сборка
- Packer
- Packer собирает новый AMI с предустановленным Docker, в который включает обранные Maven-ом образы.
- Terraform
- Terraform проверяет имеющуюся конфигурацию окружения;
- во время проверки – он обнаруживает новый AMI ID, и создаёт новый Launch Config для AutoScale группы.
- Деплой (
bash
, AWS CLI)- в Blue ASG группу добавляет новый интанс из последнего AMI (используя Launch Config, обновлённый Terraform-ом);
- Blue ASG подклчюается к Elastic Load Balancer (ELB);
- Green ASG отключается от ELB, трафик передаётся на инстанс из Blue группы;
- В Green группе убиваются инстансы;
- ASG создаёт новые интансы в Green группе, используя последние Launch Config;
- Green группа подключается к ELB;
- Blue группа отключается;
- инстансы Blue группы останавливаются/уничтожаются.
Вся сборка осуществляется Jenkins-ом через Docker Pipeline Plugin (при этом сам Jenkins запущен в контейнере, и для билдов ноды запускает в Docker-контейнерах в Docker-контейнере, тоже как-то опишу развёртывание такого окружения для билдов).
Maven
Репозиторий выглядит следующим образом:
[simterm]
$ ls -l total 40 drwxr-xr-x 3 setevoy setevoy 4096 Jan 4 11:36 authserver drwxr-xr-x 3 setevoy setevoy 4096 Jan 4 11:36 eureka drwxr-xr-x 3 setevoy setevoy 4096 Jan 4 11:36 gateway -rw-r--r-- 1 setevoy setevoy 543 Jan 4 11:36 maven-settings.xml -rwxr-xr-x 1 setevoy setevoy 1390 Jan 4 11:36 pom.xml drwxr-xr-x 3 setevoy setevoy 4096 Jan 4 11:36 producers drwxr-xr-x 3 setevoy setevoy 4096 Jan 4 11:36 profile -rw-r--r-- 1 setevoy setevoy 2664 Jan 4 11:36 README.md -rw-r--r-- 1 setevoy setevoy 577 Jan 4 11:33 sonar-project.properties drwxr-xr-x 3 setevoy setevoy 4096 Jan 4 11:36 tag-api-configuration
[/simterm]
Собственно – authserver, eureka, gateway, producers, profile и tag-api-configuration – сервисы, которые будут собраны в Docker-образы.
Maven build
Сборка – стандартная, через pom.xml
:
[simterm]
$ cat pom.xml | grep -A 5 module <modules> <module>authserver</module> <module>eureka</module> <module>gateway</module> <module>producers</module> <module>profile</module> <module>tag-api-configuration</module> </modules>
[/simterm]
Вызывается Maven в Jenkins-е из Groovy-скрипта.
Билд-скрипты разбиты на две части. Один скрипт – общий для всех билдов, в котором описаны функции (build.groovy
). И вторая часть – зависящая от окружения: она вызывает функцию из основного билд-скрипта и передаёт ей параметры конкретного окружения:
[simterm]
$ ls -l ../../tag-deployment/api/ total 44 -rw-r--r-- 1 setevoy setevoy 2311 Dec 26 16:38 build.dev.groovy -rw-r--r-- 1 setevoy setevoy 7319 Feb 17 17:59 build.groovy -rw-r--r-- 1 setevoy setevoy 4563 Dec 26 16:38 build.production.groovy -rw-r--r-- 1 setevoy setevoy 2105 Dec 26 16:38 build.staging.groovy -rw-r--r-- 1 setevoy setevoy 994 Dec 26 16:38 cn-north-1-dev.groovy -rw-r--r-- 1 setevoy setevoy 1133 Dec 26 16:38 eu-west-1-dev.groovy -rw-r--r-- 1 setevoy setevoy 1207 Feb 20 12:43 eu-west-1-production.groovy -rw-r--r-- 1 setevoy setevoy 1207 Feb 20 12:19 eu-west-1-staging.groovy -rw-r--r-- 1 setevoy setevoy 1524 Dec 26 16:38 health-check.groovy
[/simterm]
Например, функция билда Maven в основном скрипте (build.groovy
) выглядит так:
#!/usr/bin/env groovy def maven() { stage('Maven package') { docker.image('maven:3.3.3-jdk-8').inside('-v /var/run/docker.sock:/var/run/docker.sock -v /maven:/root/.m2/') { git branch: "${BRANCH}", credentialsId: 'git', url: "${REPO_API}" sh "echo ${BRANCH}" sh 'git branch' sh 'mvn clean install -DskipDockerBuild' sh "mvn verify sonar:sonar -s./maven-settings.xml -DskipDockerBuild -Dsonar.host.url=${env.SONAR_HOST} -Dsonar.projectName=\"Server API\" -Dsonar.projectKey=api" withCredentials([[ $class: 'UsernamePasswordMultiBinding', credentialsId: 'docker', passwordVariable: 'DOCKER_REGISTRY_PASSWORD', usernameVariable: 'DOCKER_REGISTRY_USERNAME']]) { sh "mvn clean package -e -X \ -s./maven-settings.xml \ -Ddocker.registry.id=${DOCKER_REGISTRY_ID} \ -Ddocker.registry.host=${DOCKER_REGISTRY_HOST} \ -Ddocker.registry.url=${DOCKER_REGISTRY_URL} \ -Ddocker.registry.username=${DOCKER_REGISTRY_USERNAME} \ -Ddocker.registry.password=${DOCKER_REGISTRY_PASSWORD} \ -Ddocker.image.tag=\"${DOCKER_IMAGE_TAG}\" \ -DpushImage" } } } } ...
А её вызов в скрипте eu-west-1-staging.groovy
– так:
#!/usr/bin/env groovy node { withEnv([ 'ENVIRONMENT=staging', 'AWS_REGION=eu-west-1', 'BASE_AMI=ami-285e0b5b', 'REPO_API=https://bitbucket.company.net/scm/lontag/tag-server-api.git', 'REPO_INFRA=https://bitbucket.company.net/scm/lontag/tag-server-api-infrastructure.git', 'DOCKER_REGISTRY_ID=companyengineering-tag-docker.jfrog.io', 'DOCKER_REGISTRY_URL=https://companyengineering-tag-docker.jfrog.io/', 'DOCKER_REGISTRY_HOST=companyengineering-tag-docker.jfrog.io/', "DOCKER_IMAGE_TAG=staging-${BRANCH}-${env.BUILD_NUMBER}", 'VPC_CIDR=10.5.0.0/16', 'VPC_SUBNET_PUBLIC_1=10.5.0.0/24', 'VPC_SUBNET_PUBLIC_2=10.5.1.0/24', 'VPC_SUBNET_PRIVATE_1=10.5.2.0/24', 'VPC_SUBNET_PRIVATE_2=10.5.3.0/24', 'SSH_KEY_NAME=tag-ci-key-pair', 'MIN_SIZE_GREEN=2' ]) { git branch: "develop", credentialsId: 'git', url: "https://bitbucket.company.net/scm/lontag/tag-deployment.git" def build = load 'api/build.groovy' build.maven() ...
Результатами билда являются jar
-архивы, например:
[simterm]
ubuntu@ip-10-0-2-254:/jenkins/workspace/EU-api-staging-build$ ls -l authserver/target/ | grep jar -rw-r--r-- 1 root root 24579486 Feb 20 08:26 oauth2-authserver-0.0.1-SNAPSHOT.jar -rw-r--r-- 1 root root 77105 Feb 20 08:26 oauth2-authserver-0.0.1-SNAPSHOT.jar.original
[/simterm]
Maven Docker
Следующим шагом – Maven собирает Docker-образы и пушит их в Artifactory.
Для сборки из Maven-а – используется плагин docker-maven-plugin
.
Docker-сборка вызывается из той же функции maven()
в build.groovy
:
... withCredentials([[ $class: 'UsernamePasswordMultiBinding', credentialsId: 'docker', passwordVariable: 'DOCKER_REGISTRY_PASSWORD', usernameVariable: 'DOCKER_REGISTRY_USERNAME']]) { sh "mvn clean package -e -X \ -s./maven-settings.xml \ -Ddocker.registry.id=${DOCKER_REGISTRY_ID} \ -Ddocker.registry.host=${DOCKER_REGISTRY_HOST} \ -Ddocker.registry.url=${DOCKER_REGISTRY_URL} \ -Ddocker.registry.username=${DOCKER_REGISTRY_USERNAME} \ -Ddocker.registry.password=${DOCKER_REGISTRY_PASSWORD} \ -Ddocker.image.tag=\"${DOCKER_IMAGE_TAG}\" \ -DpushImage" ...
А сам билд и вызов docker-maven-plugin
описан в pom.xml
модуля, например – сервис authserver и его файл authserver/pom.xml
:
... <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.4.10</version> <configuration> <imageName>companyengineering-tag-docker.jfrog.io/${docker.image.project}-${project.artifactId}:${docker.image.tag}</imageName> <dockerDirectory>${project.basedir}/src/main/docker</dockerDirectory> <resources> <resource> <targetPath>/</targetPath> <directory>${project.build.directory}</directory> <include>${project.build.finalName}.jar</include> </resource> </resources> <serverId>company-artifactory</serverId> <registryUrl>https://companyengineering-tag-docker.jfrog.io/</registryUrl> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> </plugin> ...
Dockerfile
(параметр <dockerDirectory>${project.basedir}/src/main/docker</dockerDirectory>
):
[simterm]
$ cat authserver/src/main/docker/Dockerfile FROM java:7 VOLUME /tmp ADD oauth2-authserver-0.0.1-SNAPSHOT.jar app.jar RUN bash -c 'touch /app.jar' ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
[/simterm]
Сборка и пуш Docker-образа в Jenkins-е выглядят так:
... [INFO] Copying /jenkins/workspace/EU-api-dev-build/authserver/target/oauth2-authserver-0.0.1-SNAPSHOT.jar -> /jenkins/workspace/EU-api-dev-build/authserver/target/docker/oauth2-authserver-0.0.1-SNAPSHOT.jar [INFO] Copying /jenkins/workspace/EU-api-dev-build/authserver/src/main/docker/Dockerfile -> /jenkins/workspace/EU-api-dev-build/authserver/target/docker/Dockerfile [INFO] Building image companyengineering-tag-docker.jfrog.io/tag-oauth2-authserver:dev-develop-219 [DEBUG] Auth Config AuthConfig{username=****, password=****, [email protected], serverAddress=https://companyengineering-tag-docker.jfrog.io/} [DEBUG] Registry Config Json {"https://companyengineering-tag-docker.jfrog.io/":{"serveraddress":"https://companyengineering-tag-docker.jfrog.io/","password":"****","auth":"","email":"[email protected]","username":"****"}} [DEBUG] Registry Config Encoded eyJo***ifX0= Step 1/5 : FROM openjdk:8-jdk-alpine ---> e40ba8c51bb2 Step 2/5 : VOLUME /tmp ---> Using cache ---> dca0ede529f8 Step 3/5 : ADD oauth2-authserver-0.0.1-SNAPSHOT.jar app.jar ---> 77f2dd1cdb86 Removing intermediate container f23d830f4995 Step 4/5 : RUN sh -c 'touch /app.jar' ---> Running in 5266ab468f85 ---> b265dc7a0667 Removing intermediate container 5266ab468f85 Step 5/5 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar /app.jar ---> Running in bec5d43fa72e ---> b0133e38b21e Removing intermediate container bec5d43fa72e Successfully built b0133e38b21e [INFO] Built companyengineering-tag-docker.jfrog.io/tag-oauth2-authserver:dev-develop-219 [INFO] Pushing companyengineering-tag-docker.jfrog.io/tag-oauth2-authserver:dev-develop-219 The push refers to a repository [companyengineering-tag-docker.jfrog.io/tag-oauth2-authserver] f689adf70a7d: Preparing 7a2359d813a4: Preparing bef6fa0c97dd: Preparing da07d9b32b00: Preparing 7cbcbac42c44: Preparing ...
Собранным Docker-образам устанавливается тег в виде "DOCKER_IMAGE_TAG=envname-${BRANCH}-${env.BUILD_NUMBER}"
(скрипт eu-west-1-staging.groovy
).
Переменная $BRANCH
“приходит” из Jenkins, где с помощью этого параметра можно заменить бранч для билда при старте сборки:
Docker-образы сохраняются в JFrog Artifactory:
Packer
Packer выполняет сборку новых AMI, используя которые позже будут запущены новые EC2-инстансы с новой версией приложения.
Вызывается он так же из билд-скрипта с параметрами для конкретного окружения:
... stage('Packer API AMI') { sh "packer build -force \ -var 'version=${env.BUILD_NUMBER}' \ -var 'access_key=${AWS_ACCESS_KEY_ID}' \ -var 'secret_key=${AWS_SECRET_ACCESS_KEY}' \ -var 'region=${AWS_REGION}' \ -var 'source_ami=${BASE_AMI}' \ -var 'ssh_username=ubuntu' \ -var 'docker_registry=${DOCKER_REGISTRY_ID}' \ -var 'profile_image=tag-profile:${DOCKER_IMAGE_TAG}' \ -var 'configuration_image=tag-configuration:${DOCKER_IMAGE_TAG}' \ -var 'gateway_image=tag-api-gateway:${DOCKER_IMAGE_TAG}' \ -var 'producers_image=tag-producers:${DOCKER_IMAGE_TAG}' \ -var 'auth_image=tag-oauth2-authserver:${DOCKER_IMAGE_TAG}' \ ./packer/api/template.json" } ...
Параметры из билд-скрипта передаются самому Packer-у, которые он далее использует в шаблоне packer/api/template.json
:
... "builders": [{ "type": "amazon-ebs", "access_key": "{{user `access_key`}}", "secret_key": "{{user `secret_key`}}", "region": "{{user `region`}}", "source_ami": "{{user `source_ami`}}", "instance_type": "t2.medium", "ssh_username": "{{user `ssh_username`}}", "ami_name": "tag-api-{{user `version`}}", "tags": { "Name": "TAG API", "Version": "{{user `version`}}" } }], "provisioners": [{ "type": "shell", "scripts": [ "{{template_dir}}/app.sh" ], "environment_vars": [ "DOCKER_REGISTRY={{user `docker_registry`}}", "PROFILE_IMAGE={{user `profile_image`}}", "CONFIGURATION_IMAGE={{user `configuration_image`}}", "GATEWAY_IMAGE={{user `gateway_image`}}", "PRODUCERS_IMAGE={{user `producers_image`}}", "AUTH_IMAGE={{user `auth_image`}}" ] }, ...
Он же включает скрипт для загрузки образов в собираемый AMI:
... "scripts": [ "{{template_dir}}/app.sh" ], ...
Сам скрипт:
[simterm]
$ cat packer/api/app.sh #!/bin/sh -x sleep 30 docker pull $DOCKER_REGISTRY/$PROFILE_IMAGE docker pull $DOCKER_REGISTRY/$CONFIGURATION_IMAGE docker pull $DOCKER_REGISTRY/$GATEWAY_IMAGE docker pull $DOCKER_REGISTRY/$PRODUCERS_IMAGE docker pull $DOCKER_REGISTRY/$AUTH_IMAGE
[/simterm]
Для сборки – packer
запускает новую EC2-машину из $BASE_AMI
(BASE_AMI=ami-285e0b5b
в скрипте eu-west-1-staging.groovy
), выполняет docker pull
(app.sh
выше), загружает новые образы из Artifactory, собирает новый образ машины, который включает в себя эти Docker-образы, и сохраняет новые AMI в AWS аккаунте.
Билд в Jenkins выглядит так:
... [apiAmi] [1;32m==> amazon-ebs: Creating the AMI: tag-api-245[0m [apiAmi] [0;32m amazon-ebs: AMI: ami-4b66442d[0m [apiAmi] [1;32m==> amazon-ebs: Waiting for AMI to become ready...[0m [apiAmi] [1;32m==> amazon-ebs: Adding tags to AMI (ami-4b66442d)...[0m [apiAmi] [1;32m==> amazon-ebs: Tagging snapshot: snap-073e2b53b623975ce[0m [apiAmi] [1;32m==> amazon-ebs: Creating AMI tags[0m [apiAmi] [1;32m==> amazon-ebs: Creating snapshot tags[0m [apiAmi] [1;32m==> amazon-ebs: Terminating the source AWS instance...[0m [apiAmi] [1;32m==> amazon-ebs: Cleaning up any extra volumes...[0m [apiAmi] [1;32m==> amazon-ebs: No volumes to clean up, skipping[0m [apiAmi] [1;32m==> amazon-ebs: Deleting temporary security group...[0m [apiAmi] [1;32m==> amazon-ebs: Deleting temporary keypair...[0m [apiAmi] [1;32mBuild 'amazon-ebs' finished.[0m [apiAmi] [apiAmi] ==> Builds finished. The artifacts of successful builds are: [apiAmi] --> amazon-ebs: AMIs were created: [apiAmi] [apiAmi] eu-west-1: ami-4b66442d [Pipeline] [apiAmi] } [Pipeline] [apiAmi] // stage [Pipeline] [apiAmi] } [Pipeline] // parallel [Pipeline] } [Pipeline] // withCredentials [Pipeline] } ...
Terrafrom
Сама интересная часть, если не считать деплоя.
Terrafrom проверяет текущую инфраструктуру, сравнивая её со state-файлами, которые хранятся в S3-корзине:
... stage('Terraform') { docker.image('hashicorp/terraform:light').inside('-v /var/run/docker.sock:/var/run/docker.sock') { git branch: "master", credentialsId: 'git', url: "${REPO_INFRA}" withCredentials([[ $class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'AWS_ACCESS_KEY_ID', credentialsId: 'aws_terraform', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { sh "cd terraform && terraform remote config \ -backend=s3 \ -backend-config=\"bucket=tag-api-eu-terraform-state-${ENVIRONMENT}\" \ -backend-config=\"access_key=${AWS_ACCESS_KEY_ID}\" \ -backend-config=\"secret_key=${AWS_SECRET_ACCESS_KEY}\" \ -backend-config=\"key=api/terraform.tfstate\" \ -backend-config=\"region=${AWS_REGION}\" \ -backend-config=\"encrypt=true\"" ...
Для деплоя – terraform
обновляет Launch config.
Для этого он проверяет текущее состояние конфигурации, и находит несоответствие между имеющимися AMI ID в launch-конфгигах Blue и Green групп, и AMI ID, имеющимися в аккаунте (параметр most_recent = true
):
... data "aws_ami" "api_green" { most_recent = true owners = ["${lookup(var.ami_owners, var.region)}"] filter { name = "name" values = ["tag-api*"] } } data "aws_ami" "api_blue" { most_recent = true owners = ["${lookup(var.ami_owners, var.region)}"] filter { name = "name" values = ["tag-api*"] } } ...
Видя это несоответсвие – Terrafrom создаёт новые Launch-конфиги, и переключает AutoScale группы на них.
В Jenkis-е это выглядит так:
... [0m[1mmodule.eurkea_autoscaling_group_green.aws_autoscaling_group.asg: Modifying...[0m launch_configuration: "staging-api-eureka-green-lc-00b2859f1512b5d8ea100c2004" => "staging-api-eureka-green-lc-00556cd434eb881b48aa7aa95a"[0m ...
Кроме того – Terraform добавляет userdata-скрипт, который запускает сервисы при старте EC2:
... data "template_file" "api_userdata" { template = "${file("./files/userdata/api.sh")}" vars { DOCKER_REGISTRY = "${lookup(var.docker_registry, var.region)}" DOCKER_IMAGE_OAUTH = "${lookup(var.docker_image_oauth, var.region)}" DOCKER_IMAGE_PROFILE = "${lookup(var.docker_image_profile, var.region)}" DOCKER_IMAGE_CONFIGURATION = "${lookup(var.docker_image_configuration, var.region)}" DOCKER_IMAGE_PRODUCERS = "${lookup(var.docker_image_producers, var.region)}" DOCKER_IMAGE_GATEWAY = "${lookup(var.docker_image_gateway, var.region)}" DOCKER_IMAGE_TAG = "${var.docker_image_tag}" SPRING_PROFILE = "${var.spring_profile}" } } ...
Сам файл files/userdata/api.sh
:
[simterm]
$ cat files/userdata/api.sh #!/bin/bash -v docker login -u username -p password companyengineering-tag-docker.jfrog.io docker run \ -d \ -p 9999:9999 \ -e SPRING_PROFILES_ACTIVE=${SPRING_PROFILE} \ --restart unless-stopped \ ${DOCKER_REGISTRY}/${DOCKER_IMAGE_OAUTH}:${DOCKER_IMAGE_TAG} docker run \ -d \ -p 8083:8083 \ -e SPRING_PROFILES_ACTIVE=${SPRING_PROFILE} \ --restart unless-stopped \ ${DOCKER_REGISTRY}/${DOCKER_IMAGE_PROFILE}:${DOCKER_IMAGE_TAG} docker run \ -d \ -p 8084:8084 \ -e SPRING_PROFILES_ACTIVE=${SPRING_PROFILE} \ --restart unless-stopped \ ${DOCKER_REGISTRY}/${DOCKER_IMAGE_CONFIGURATION}:${DOCKER_IMAGE_TAG} docker run \ -d \ -p 8090:8090 \ -e SPRING_PROFILES_ACTIVE=${SPRING_PROFILE} \ --restart unless-stopped \ ${DOCKER_REGISTRY}/${DOCKER_IMAGE_PRODUCERS}:${DOCKER_IMAGE_TAG} docker run \ -d \ -p 8080:8080 \ -e SPRING_PROFILES_ACTIVE=${SPRING_PROFILE} \ --restart unless-stopped \ ${DOCKER_REGISTRY}/${DOCKER_IMAGE_GATEWAY}:${DOCKER_IMAGE_TAG}
[/simterm]
Собственно, на этом – билд заканчивается.
Последним шагом – запускается bash
-скрипт, который уже выполняет непосредственно деплой и описан в посте AWS: AWS CLI и bash – blue/green деплой AutoScale группы за ELB.
Очень сопротивляюсь тому, что бы выводить эту схему в production, настаивая на ECS, но – видимо придётся.
Jenkins verify step
Реализация шага “Vefiry” в Jenkins.
В билд-скрипте функция выглядит так:
... def deploy_prod_verify() { stage 'Verify' input id: 'Deploy', message: 'Is Blue node fine? Proceed with Green node deployment?', ok: 'Deploy!' } ...
А в билде – выглядит это так: