Jenkins: миграция RTFM 2.4 – Jenkins Pipeline для CloudFormation RTFM стека

Автор: | 14/10/2017

Продолжение сетапа инфрастуктуры для блога.

Решил добавить в заголовки “версию” каждого поста, в данном случае – миграция RTFM 2.4. Тут мажорная версия “2” – потому что год тому уже что-то подобное начинал делать (AWS: миграция RTFM, часть #1: ручное создание инфраструктуры – VPC, подсети, IGW, NAT GW, маршруты и EC2), а минорная “4” – четвёртый пост в серии.

Предыдущие посты этой серии:

Итого, сейчас имеются шаблоны для развёртывания EC2 с Jenkins, Ansible плейбук для установки и запуска Jenkins и шаблон для развёртывания CloudFormation стека блога.

Ссылки на репозитории:

Теперь можно приступать к созданию задач в Jenkins, которые будут заниматься созданием и апдейтом стека и настройкой серверов в нём.

В этом посте только создание и апдейт стека – для Ansible – ещё предстоит написать роли, а потом добавить ещё одну задачу в Jenkins.

Подготовка

Что бы упростить проверку статуса – в .bashrc добавляем алиас:

alias checkjenkinsstatus="aws cloudformation describe-stacks --stack-name rtfm-jenkins --query 'Stacks[*].StackStatus' --output text"

И ещё один – что бы получать PublicIP:

alias getrtfmjenkinsip="aws ec2 describe-instances --filters Name=tag-value,Values=rtfm-jenkins-ec2 --query 'Reservations[*].Instances[*].PublicIpAddress' --output text"

Создаём стек Jenkins скриптом из поста Ansible: RTFM Jenkins provision:

[simterm]

$ cd /home/setevoy/Work/RTFM/Github/rtfm-jenkins-ansible-provision/ && git pull
$ ./rtfm-jenkins-provision.sh -b
$ ./rtfm-jenkins-provision.sh -c -s rtfm-jenkins -i 188.***.***.114

[/simterm]

Проверяем статус:

[simterm]

$ checkjenkinsstatus
CREATE_COMPLETE

[/simterm]

Получаем IP:

[simterm]

$ getrtfmjenkinsip
34.240.186.116

[/simterm]

Обновляем имя ci.rtfm.co.ua у регистратора, делаем чай, пока обновляются DNS.

Проверяем:

[simterm]

$ dig +short ci.rtfm.co.ua
34.240.186.116

[/simterm]

Запускаем Ansible:

[simterm]

$ ./rtfm-jenkins-provision.sh -a

[/simterm]

Jenkins готов к работе.

Jenkins

View

Создаём новую View – rtfm-cf-provision:

Jenkinsfile

Теперь – можно добавить Groovy-скрипт для Jenkins Pipeline, который будет запускать Docker контейнер с AWS CLI и выполнять aws cloudformation stack-update.

Пост по теме – Azure: provisioning с Resource Manager, Jenkins и Groovy.

Создание шаблона для RTFM, который используется дальше – rtfm-blog-cf-template.json – описано в посте AWS: миграция RTFM 2.3 – инфраструктура для RTFM и создание CloudFormation шаблона – VPC, subnets, EC2.

Клонируем пока пустой репозиторий:

[simterm]

$ cd ~/Work/RTFM/Github && git clone [email protected]:setevoy2/rtfm-jenkins-scripts.git
$ cd rtfm-jenkins-scripts/

[/simterm]

Скриптов будет два – один с функциями, второй – с переменными и вызовом этих функций, он и будет вызываться в Jenkins.

Первый скрипт – provision.groovy, пока одна функция – cfTemplateValidate():

#!/usr/bin/env groovy

def cfTemplateValidate(cfTemplatesRepoUrl='1', cfBranch='2', cfTemplateFile='3') {

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

        git branch: "${cfBranch}",  url: "${cfTemplatesRepoUrl}"

        stage('CF Temlate validate') {

            // sh -e to prevent printing variables in a build log
            // read https://stackoverflow.com/a/39908900/2720802
            sh "#!/bin/sh -e\n" + "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} && export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}"
            sh "aws cloudformation --region eu-west-1 validate-template --template-body file://${cfTemplateFile}"
        }
    }
}

return this

Второй скрипт, rtfm-cf-stack-provision.groovy – переменные, комментарии и вызов функции cfTemplateValidate() с передачей ей параметров:

#!/usr/bin/env groovy

node {

    /* Variables inherited from Jenkins job's settings

    // Password Parameters
    AWS_ACCESS_KEY_ID = "${AWS_ACCESS_KEY_ID}"
    AWS_SECRET_ACCESS_KEY = "${AWS_SECRET_ACCESS_KEY}"

    // String parameters
    // Environment type - rtfm-dev or rtfm-production.
    // To be used during stack-create.
    ENV = "${ENV}"

    CI_BRANCH = "${CI_BRANCH}"
    CI_SCRIPTS_REPO_URL = "${CI_SCRIPTS_REPO_URL}"

    CF_BRANCH = "${CF_BRANCH}"
    CF_TEMPLATES_REPO_URL = "${CF_TEMPLATES_REPO_URL}"
    CF_STACK_TEMPLATE_FILE = "${CF_STACK_TEMPLATE_FILE}"

    */

    dir('ciscripts') {
        git branch: "${CI_BRANCH}", url: "${CI_SCRIPTS_REPO_URL}"
    }

    def provision = load 'ciscripts/provision.groovy'

    // cfTemplatesRepoUrl='1', cfBranch='2', cfTemplateFile='3'
    provision.cfTemplateValidate("${CF_TEMPLATES_REPO_URL}", "${CF_BRANCH}", "${CF_STACK_TEMPLATE_FILE}")
}

Ссылка на репозиторий – в конце поста.

Jenkins job

Создаём первую задачу – rtfm-blog-cf-dev-provision, тип Pipeline:

Добавляем в неё параметры:

AWS_ACCESS_KEY_ID – надо будет создать отдельного пользователя для Jenkins.

Настраиваем репозиторий:

Так как тут используется только репозиторий Github – то Credentials пока не нужны. А вот для Ansible задач, куда надо будет клонировать ключи доступ из приватного репозитория в Bitbucket – добавлю.

И запускаем билд:

Ну – всё хорошо.

Пришлось всё-таки поменять t2.nano на t2.microJenkins зависал, когда стартовал контейнеры для билдов.

Можно добавить вторую функцию – create-stack.

Но тут встал вопрос – что вызывать: create-stack или update-stack?

Решение нашлось такое: добавляем функцию cfCheckStackPresent(), которая выполняет describe-stacks --stack-name ИМЯ_СТЕКА. Если она вернёт ОК – значит стек есть, выполняем update. В противном случае – стека нет, значит – создаём его.

Обновляем provision.groovy, добавляем cfCheckStackPresent():

...
def cfCheckStackPresent(cfStackName='1') {

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

        // sh -e to prevent printing variables in a build log
        // read https://stackoverflow.com/a/39908900/2720802
        sh "#!/bin/sh -e\n" + "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} && export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}"
        sh "aws cloudformation --region eu-west-1 describe-stacks --stack-name ${cfStackName} &> /dev/null"
    }
}
...

И функция cfStackCreateOrUpdate(), которой первым аргументом передаётся значение create или update, которое далее подставляется в aws cloudformation --region eu-west-1 ${action}-stack:

...
def cfStackCreateOrUpdate(action='1', cfTemplatesRepoUrl='2', cfBranch='3', cfTemplateFile='4', cfStackName='5', env='6', cfKeyName='7', allowLocation='8') {

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

        git branch: "${cfBranch}",  url: "${cfTemplatesRepoUrl}"

        stage("CF Stack ${action}") {

            sh "echo Stack ${action}"

            // sh -e to prevent printing variables in a build log
            // read https://stackoverflow.com/a/39908900/2720802
            sh "#!/bin/sh -e\n" + "export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} && export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}"
            sh "aws cloudformation --region eu-west-1 ${action}-stack --stack-name ${cfStackName} --template-body file://${cfTemplateFile} --parameters ParameterKey=ENV,ParameterValue=${env} ParameterKey=KeyName,ParameterValue=${cfKeyName} ParameterKey=HomeAllowLocation,ParameterValue=${allowLocation}/32"

        }
    }
}
...

В файле rtfm-cf-stack-provision.groovy добавляем try-catch и в зависимости от выполнения блока – задаём аргумент action:

...
    // action='1', cfTemplatesRepoUrl='2', cfBranch='3', cfTemplateFile='4', cfStackName='5', env='6', cfKeyName='7', allowLocation='8'
    try {
        // if cfCheckStackPresent == True then "update"
        cfCheckStackPresent("${CF_STACK_NAME}")
        cfStackCreateOrUpdate('update', "${CF_TEMPLATES_REPO_URL}", "${CF_BRANCH}", "${CF_STACK_TEMPLATE_FILE}", "${CF_STACK_NAME}", "${ENV}", "${CF_EC2_KEY_NAME}", "${HOME_ALLOW_LOCATION}")
    } catch(Exception) {
        // if cfCheckStackPresent == False then "create"
        cfStackCreateOrUpdate('create', "${CF_TEMPLATES_REPO_URL}", "${CF_BRANCH}", "${CF_STACK_TEMPLATE_FILE}", "${CF_STACK_NAME}", "${ENV}", "${CF_EC2_KEY_NAME}", "${HOME_ALLOW_LOCATION}")
    }
...

UPD 27.0.12018 – правда, тут есть одна небольшая проблема, или, скорее, недоработка такого решения – если в шаблоне никаких изменений не было – то билд “падает”, т.к. Jenkins пытается выполнить и update, и create

В параметры Jenkins добавляем ещё 4 переменных:

CF_STACK_NAME = "${CF_STACK_NAME}"
ENV = "${ENV}"
CF_EC2_KEY_NAME = "${CF_EC2_KEY_NAME}"
HOME_ALLOW_LOCATION = "${HOME_ALLOW_LOCATION}"

Сохраняем, запускаем:

AWS Console:

По завершению – проверяем:

[simterm]

$ ssh [email protected] -i ~/Work/RTFM/Bitbucket/aws-credentials/rtfm-dev.pem
The authenticity of host '52.31.49.190 (52.31.49.190)' can't be established.
ECDSA key fingerprint is SHA256:Sp0Ovmma/W+3XaJsVTOwfBUmS0ih1T4azBw9EO8iABQ.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '52.31.49.190' (ECDSA) to the list of known hosts.
Linux ip-10-0-1-58 4.9.0-3-amd64 #1 SMP Debian 4.9.30-2+deb9u3 (2017-08-06) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
admin@ip-10-0-1-58:~$

[/simterm]

Скрипты доступы в репозитории rtfm-jenkins-scripts.

Готово.

P.S. А это – количество билдов, которые понадобились что бы закончить настройку pipeline и скриптов: