Jenkins: Pipeline, Groovy, Ansible и VM provisioning

By | 09/22/2017
 

Продолжение постов Ansible: пример установки NGINX и Azure: provisioning с Resource Manager, Jenkins и Groovy.

Задача  – запускать провижен VM из Jenkins. Пока выполняется только установка NGINX, позже будет добавлен Prometheus.

Используем Jenkins Pipeline и groovy-скрипты.

Описание

Используется два репозитория: один для скриптов Jenkins (переменная $BUILD_REPO_URL в скриптах ниже), второй для файлов Ansbile ($INFRA_URL), в которых описаны хосты и роли.

Первое, с чем пришлось столкнуться – это Docker образы Ansbile. Ну, то, что нет тега latest и вместо этого под каждую ОС отдельный тег – хорошо, понятно.

Далее – официальный образ Ansbile в DockerHub не имеет исполняемых файлов. Т.е. – где-то они, видимо есть – но образ ansible/ansible:ubuntu1604 сообщал что “exec: \”ansbile\”: executable file not found in $PATH”.“.

Ещё один образ – geerlingguy/docker-ubuntu1604-ansible “заработал” – пока не дошло дело до выполнения самих плейбуков, где он начал падать с собщением типа:


File “/usr/lib/python2.7/dist-packages/ansible/plugins/connection/ssh.py”, line 452, in _run
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
File “/usr/lib/python2.7/subprocess.py”, line 711, in __init__
errread, errwrite)
File “/usr/lib/python2.7/subprocess.py”, line 1343, in _execute_child
raise child_exception
OSError: [Errno 2] No such file or directory
fatal: [dev.monitor.domain.ms]: FAILED! => {
“failed”: true,
“msg”: “Unexpected failure during module execution.”,
“stdout”: “”
}

Сообщение “OSError: [Errno 2] No such file or directory” заставило меня долго перебирать параметры и файлы, передаваемые Ansbile из скриптов и Jenkins-а, пока я не догадался проверить ssh:

docker run -ti geerlingguy/docker-ubuntu1604-ansible ssh
docker: Error response from daemon: oci runtime error: container_linux.go:247: starting container process caused "exec: \"ssh\": executable file not found in $PATH".

Не добавить ssh в образ системы, которая работает использует SSH? Ну…

В конце-концов – нашёлся williamyeh/ansible:master-ubuntu16.04 – с ним всё заработало.

Jenkins

Jenkins бегает на:

lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS

Jenkinsfiles

Jenkinsfiles – groovy-скрипты, которые описывают используемые Docker-образы и действия в них.

Дальше используется два таких скрипта – один с описанием функций, второй – с их вызовом и передачей переменных и параметров.

Первый скрипт – monitoring-ansible.groovy, с функциями, содержит две функции – ansibleVmProvisionValidate(), которая выполняет ansible-playbook --check:

...
def ansibleVmProvisionValidate(env='1') {

    docker.image('williamyeh/ansible:master-ubuntu16.04').inside('-v /var/run/docker.sock:/var/run/docker.sock') {

        git branch: "${BRANCH}", credentialsId: 'github', url: "${INFRA_URL}"

        stage('Playbook validate') {

            sh 'chmod 400 monitoring/.ssh/monitoring'
            sh "ansible-playbook --limit=${env} --check ${WORKDIR}/provision.yml"
        }
    }
}
...

Вторая функция – ansibleVmProvisionApply() – аналогична, но без --check:

...
def ansibleVmProvisionApply(env='1') {

    docker.image('williamyeh/ansible:master-ubuntu16.04').inside('-v /var/run/docker.sock:/var/run/docker.sock') {

        git branch: "${BRANCH}", credentialsId: 'github', url: "${INFRA_URL}"

        stage('Playbook apply') {

            sh 'chmod 400 monitoring/.ssh/monitoring'
            sh "ansible-playbook --limit=${env} ${WORKDIR}/provision.yml"
        }
    }
}
...

Полностью скрипт выглядит так:

#!/usr/bin/env groovy

def ansibleVmProvisionValidate(env='1') {

    docker.image('williamyeh/ansible:master-ubuntu16.04').inside('-v /var/run/docker.sock:/var/run/docker.sock') {

        git branch: "${BRANCH}", credentialsId: 'github', url: "${INFRA_URL}"

        stage('Playbook validate') {

            sh 'chmod 400 monitoring/.ssh/monitoring'
            sh "ansible-playbook --limit=${env} --check ${WORKDIR}/provision.yml"
        }
    }
}

def ansibleVmProvisionApply(env='1') {

    docker.image('williamyeh/ansible:master-ubuntu16.04').inside('-v /var/run/docker.sock:/var/run/docker.sock') {

        git branch: "${BRANCH}", credentialsId: 'github', url: "${INFRA_URL}"

        stage('Playbook apply') {

            sh 'chmod 400 monitoring/.ssh/monitoring'
            sh "ansible-playbook --limit=${env} ${WORKDIR}/provision.yml"
        }
    }
}

return this

Второй скрипт – monitoring-ansible-provision.groovy – будет вызываться непоследственно из Jenkins и содержит несколько переменных и вызывает функции из monitoring-ansible.groovy.

Его содержимое:

#!/usr/bin/env groovy

node {

    // 'dev' or 'production'
    ENV="${ENV}"

    TAG = "${env.BUILD_TAG}"

    // Ansible playbooks repo URL
    INFRA_URL = "${INFRA_URL}"

    // Jenkins build script repo URL
    BUILD_REPO_URL = "${BUILD_REPO_URL}"

    // expor variable to path to Ansible dir in Infra repo
    WORKDIR='monitoring/ansible/monitoring'

    // clone $BUILD_REPO_URL to dedicated directory ./buildscripts/
    dir('buildscripts') {
        git branch: 'master', credentialsId: 'github', url: "${BUILD_REPO_URL}"
    }

    def provision = load 'buildscripts/monitoring/monitoring-ansible.groovy'

    provision.ansibleVmProvisionValidate("${ENV}")
    provision.ansibleVmProvisionApply("${ENV}")
}

Настройка Jenkins Pipeline

В параметрах задачи Jenkins задаются переменные, которые потом используются скриптом monitoring-ansible-provision.groovy – ENVBRANCH, INFRA_URLBUILD_REPO_URL:

И настройки Pipeline, где описывается вызов самого скрипта:

Ansbile

Сейчас Ansible имеет только одну роль – nginx, которая выполняет установку NGINX.

Структура файлов в репозитории выглядит так:

$ tree monitoring/
monitoring/
├── ansible
│   └── monitoring
│       ├── ansible.cfg
│       ├── hosts
│       ├── provision.retry
│       ├── provision.yml
│       └── roles
│           └── nginx
│               ├── handlers
│               │   └── main.yml
│               ├── tasks
│               │   └── main.yml
│               ├── templates
│               │   ├── nginx.conf
│               │   └── nginx_vhost.conf
│               └── vars
│                   └── main.yml
├── monitoring.json
├── monitoring.parameters.json
├── metadata.json
└── README.md

Файлы monitoring.json и monitoring.parameters.json – используются Azure Resource Manager для развёртывания группы ресурсов, которая включает в себя виртуальную машину, см. пост тут>>>.

Настройка самого сервера выполянется уже Ansbile.

Файлы тут:

  • ansible.cfg: параметры и переменные, путь к нему передаётся с помощью переменной окружения $ANSIBLE_CONFIG из Jenkins
  • hosts: содержит описание хостов и некоторых специфичных для хостов и окружений переменных
  • provision.yml: роли, которые применяются при сетапе машины

Содержимое ansible.cfg:

[defaults]
ansible_connection=ssh 
remote_user=admin
host_key_checking=False
inventory = monitoring/ansible/monitoring/hosts
private_key_file = monitoring/.ssh/monitoring

hosts:

[dev]
dev.monitor.domain.ms hostname=dev.monitor.domain.ms

[dev:vars]
upstream_name=google
upstream_url=google.com

[production]
monitor.domain.ms

Останавливаться на них подробно не буду, описаны в предыдущем посте, с небольшими отличиями (тут часть перменных уже вынесена в глобальные параметры файла ansbile.cfg).

И файл provision.yml – содержит вызов роли nginx:

- hosts: all
  become:
    true
  roles:
   - nginx

На каком хосте будет выполняться и применяться роль – определяется при вызове ansible-playbook из groovy-скрипта с помощью --limit:

...
sh "ansible-playbook --limit=${env} --check ${WORKDIR}/provision.yml"
...

Собственно, но этом – всё.

Пример выполнения:

Лог выполнения:

Проверяем:

curl -sL dev.monitor.domain.ms
<!DOCTYPE html>
<html lang=en>
<meta charset=utf-8>
<meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
<title>Error 404 (Not Found)!!1</title>
<style>
*{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}

Hello, Google!

Готово.