Jenkins: миграция RTFM 2.6 – Jenkins Pipeline для Ansible

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

В одном из предыдущих постов – Jenkins: миграция RTFM 2.4 – Jenkins Pipeline для CloudFormation RTFM стека – была добавлена задача в Jenkins для создания и апдейта AWS CloudFormation стека.

Следующая задача – запускать Ansbile из Jenkins для настройки серверов в стеке.

Далее создадим одну Ansbile роль с установкой NGINX, потом добавим задачу в Jenkins.

PEM-ключи для авторизации на EC2 и шаблоны для конфигурации виртуалхостов хранятся в приватном репозитории Bitbucket.

Подготовка

Запускаем Jenkins – Ansible: миграция RTFM 2.2 – RTFM Jenkins provision.

Создаём стек rtfm-dev – Jenkins: миграция RTFM 2.4 – Jenkins Pipeline для CloudFormation RTFM стека.

Находим Public IP в outputs, создаём субдомен dev.rtfm.co.ua:

Ansible

ansible.cfg

Начнём с создания локального файла настроек:

[defaults]
ansible_connection=ssh 
remote_user=admin
host_key_checking=False
inventory = hosts

inventory

Тут же создаём файл hosts, в который вносим FQDN Bastion хоста:

[rtfm-dev]
dev.rtfm.co.ua

playbook

И файл плейбука rtfm-blog-ansible-provision.yml, пока с одной ролью – nginx:

- hosts: all
  become:
    true
  roles:
    - nginx

.gitignore

Т.к. в роли будут использоваться шаблоны виртуалхостов, которые будут хранится в приватном репозитории Bitbuket и загружаться в каталог roles/nginx/templates/virtualhosts во время провижена самим Jenkins – сразу добавляем .gitignore:

[simterm]

$ cat .gitignore
*.retry
*.swp

[/simterm]

*.retry – не коммитить retry-файлы Ansible (либо вообще отключить их создание через ansible.cfg) и *.swp – игнорировать временные файлы vim.

И ещё один в каталогеroles/nginx/templates/virtualhosts – игнорировать всё, кроме .gitignore и nginx.conf (он стандартный):

*
!.gitignore
!nginx.conf

Роль nginx

Пример есть в посте Ansible: пример установки NGINX, тут особо отличий нет.

Создаём каталоги tasks и templates – пока этого хватит:

[simterm]

$ mkdir -p roles/nginx/{tasks,templates}

[/simterm]

В roles/nginx/templates/ создаём шаблон для файла настроек NGINX – тоже дефолтный, только с log_proxy, переменные и параметры пропишем позже:

user www-data;
worker_processes auto;
pid /var/run/nginx.pid;

events {
  worker_connections  1024;
}

http {

        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 65;
        types_hash_max_size 2048;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        log_format proxy  '[$time_local] $remote_addr - $server_name to: '
                  '$upstream_addr: $request upstream_response_time '
                  '$upstream_response_time msec $msec request_time $request_time';

        gzip on;
        gzip_disable "msie6";

        include /etc/nginx/conf.d/*.conf;
}

В каталоге roles/nginx/templates/virtualhosts/ создаём файл шаблона для виртуалхоста (ещё раз – эти шаблоны будут в Bitbucket, сейчас просто для проверки создаём тут) – roles/nginx/templates/virtualhosts/dev.rtfm.co.ua.conf:

server {

    server_name {{ inventory_hostname }};

    listen 80;

    access_log /var/log/nginx/{{ inventory_hostname }}-access.log proxy;
    error_log /var/log/nginx/{{ inventory_hostname }}-error.log notice;

    root /var/www/html;

    index index.html index.htm index.nginx-debian.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

В {{ inventory_hostname }} – будет подставлено имя из файла hosts, в данном случае – dev.rtfm.co.ua.

В tasks создаём файл main.yml с четырьмя задачами – установка NGINX, копирование файла настроек, копирование шаблона виртуалхоста, reload NGINX:

- name: Install Nginx
  package:
    name: nginx
    state: latest

- name: Replace NGINX config
  template: 
    src=templates/nginx.conf
    dest=/etc/nginx/nginx.conf

- name: Add NGINX {{ inventory_hostname }} virtualhost config
  template:
    src=templates/virtualhosts/{{ inventory_hostname }}.conf
    dest=/etc/nginx/conf.d/{{ inventory_hostname }}.conf

- name: Service NGINX reload
  service: 
    name=nginx 
    state=reloaded

Ansible syntax-check и запуск

Проверяем получившийся плейбук:

[simterm]

$ ansible-playbook --syntax-check --limit=rtfm-dev rtfm-blog-ansible-provision.yml 

playbook: rtfm-blog-ansible-provision.yml

[/simterm]

Всё ОК – можно запускать на выполнение:

[simterm]

$ ansible-playbook --limit=rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml

PLAY [all] ****

TASK [Gathering Facts] ****
ok: [dev.rtfm.co.ua]

TASK [nginx : Install Nginx] ****
changed: [dev.rtfm.co.ua]

TASK [nginx : Replace NGINX config] ****
changed: [dev.rtfm.co.ua]

TASK [nginx : Add NGINX dev.rtfm.co.ua virtualhost config] ****
changed: [dev.rtfm.co.ua]

TASK [nginx : Service NGINX reload] ****
changed: [dev.rtfm.co.ua]

PLAY RECAP ****
dev.rtfm.co.ua             : ok=5    changed=4    unreachable=0    failed=0

[/simterm]

Проверяем:

[simterm]

$ curl -I dev.rtfm.co.ua
HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Wed, 18 Oct 2017 14:29:07 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Wed, 18 Oct 2017 14:28:25 GMT
Connection: keep-alive
ETag: "59e76509-264"
Accept-Ranges: bytes

[/simterm]

Всё работает.

Сохраняем изменения в репозиторий:

[simterm]

$ git add -A && git commit -m "RTFM Dev Nginx Ansible provision"
[master 928f230] RTFM Dev Nginx Ansible provision
 6 files changed, 67 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 ansible.cfg
 create mode 100644 hosts
 create mode 100644 roles/nginx/tasks/main.yml
 create mode 100644 roles/nginx/templates/nginx.conf
 create mode 100644 rtfm-blog-ansible-provision.yml
$ git push
Counting objects: 12, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (7/7), done.
Writing objects: 100% (12/12), 1.46 KiB | 749.00 KiB/s, done.
Total 12 (delta 0), reused 0 (delta 0)
To https://github.com/setevoy2/rtfm-blog-ansible-provision.git
   ca14741..928f230  master -> master

[/simterm]

Bitbucket

В Bitbucket – создаём приватный репозиторий rtfm-blog-ansible-templates:

Клонируем его:

[simterm]

$ git clone https://[email protected]/username/rtfm-blog-ansible-templates.git
Cloning into 'rtfm-blog-ansible-templates'...
Password for 'https://[email protected]':
warning: You appear to have cloned an empty repository.

[/simterm]

Добавляем шаблон roles/nginx/templates/virtualhosts/dev.rtfm.co.ua.conf:

[simterm]

$ cp ../Github/rtfm-blog-ansible-provision/roles/nginx/templates/virtualhosts/dev.rtfm.co.ua.conf rtfm-blog-ansible-templates/
$ cd rtfm-blog-ansible-templates/ && git add -A && git commit -m "NGINX Dev virtualhost config template" && git push
[master (root-commit) aaf9e4d] NGINX Dev virtualhost config template
 1 file changed, 17 insertions(+)
 create mode 100644 dev.rtfm.co.ua.conf
Password for 'https://[email protected]':
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 431 bytes | 431.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://bitbucket.org/username/rtfm-blog-ansible-templates.git
 * [new branch]      master -> master

[/simterm]

Jenkins

Credentials

В Jenkins добавляем данные доступа к Bitbucket.

Создаём новый домен в Credentials:

Добавляем пользоваля и пароль:

Pipeline

Теперь можно создать новую задачу.

В принципе – ничего особо отличного от поста Jenkins: миграция RTFM 2.4 – Jenkins Pipeline для CloudFormation RTFM стека.

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

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

Добавляем новую задачу, можно просто скопировать из задачи провижена стека CloudFormation:

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

  • ANSIBLE_HOST_LIMIT – для передачи в ansbile-playbook --limit, тут rtfm-dev
  • ANSIBLE_PLAYBOOK_FILE – файл rtfm-blog-ansible-provision.yml
  • ANSIBLE_EC2_PEM_FILE – pem-ключ для доступа к EC2 интансу
  • ANSIBLE_GITHUB_REPO_URL – репозиторий с плейбуком и ролями, созданный выше, https://github.com/setevoy2/rtfm-blog-ansible-provision.git
  • ANSIBLE_GITHUB_BRANCH – его бранч, master
  • ANSIBLE_BB_TEMPLATES_REPO_URL – репозиторий Bitbucket с шаблонами виртуалхостов, https://[email protected]/username/rtfm-blog-ansible-templates.git
  • ANSIBLE_BB_TEMPLATES_BRANCH – его бранч, master
  • ANSIBLE_BB_CREDENTIALS_REPO_URLBitbucket репозиторий с PEM-ключами, бранч тут всегда masterhttps://[email protected]/username/aws-credentials.git
  • CI_BRANCH – бранч скриптов Jenkins, master
  • CI_SCRIPTS_REPO_URL – репозиторий скриптов Jenkinshttps://github.com/setevoy2/rtfm-jenkins-scripts.git

И параметры Pipeline репозитория – тут меняется только имя скрипта – rtfm-blog-ansible-provision.groovy:

Jenkinsfile-s

Теперь всё готово – можно добавлять скрипты.

Как и в случае с провиженом CloudFormation – все функции держим в одном скрипте – provision.groovy, их вызовы – в другом, в данном случае rtfm-blog-ansible-provision.groovy.

Сначала добавляем функцию для проверки синтаксиса.

Обновляем скрипт provision.groovy, добавляем функцию ansiblePlaybookValidate():

...
def ansiblePlaybookValidate(ansibleHostLimit='1', ansiblePlaybookFile='2') {

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

        git branch: "${ANSIBLE_GITHUB_BRANCH}",  url: "${ANSIBLE_GITHUB_REPO_URL}"

        stage('Ansible playbook validate') {

            sh "ansible-playbook --syntax-check --limit=${ansibleHostLimit} ${ansiblePlaybookFile}"
        }
    }
}
...

И создаём скрипт rtfm-blog-ansible-provision.groovy:

#!/usr/bin/env groovy

node {

    /* Variables inherited from Jenkins job's settings

    // String parameters
    CI_BRANCH = "${CI_BRANCH}"
    CI_SCRIPTS_REPO_URL = "${CI_SCRIPTS_REPO_URL}"

    */

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

    def provision = load 'ciscripts/provision.groovy'

    // ansibleHostLimit='1', ansiblePlaybookFile='2'
    provision.ansiblePlaybookValidate("${ANSIBLE_HOST_LIMIT}", "${ANSIBLE_PLAYBOOK_FILE}")

}

Сохраняем в репозиторий:

[simterm]

$ git add provision.groovy && git commit -m "ansiblePlaybookValidate() func added"
[master a800c77] ansiblePlaybookValidate() func added
 1 file changed, 13 insertions(+)
$ git add rtfm-blog-ansible-provision.groovy && git commit -m "ansiblePlaybookValidate() run" && git push
[master 63fbf10] ansiblePlaybookValidate() run
 1 file changed, 22 insertions(+)
 create mode 100644 rtfm-blog-ansible-provision.groovy
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 1.16 KiB | 1.16 MiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To https://github.com/setevoy2/rtfm-jenkins-scripts.git
   da18411..63fbf10  master -> master

[/simterm]

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

Теперь можно добавить функцию для запуска ansible-playbook в скрипт provision.groovyansiblePlaybookApply():

...
def ansiblePlaybookApply(ansibleHostLimit='1', ansiblePlaybookFile='2', ansiblePemFile='3') {

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

        git branch: "${ANSIBLE_GITHUB_BRANCH}",  url: "${ANSIBLE_GITHUB_REPO_URL}"

        dir('credentials') {
            git branch: "master", credentialsId: 'setevoy_bitbucket_aws', url: "${ANSIBLE_BB_CREDENTIALS_REPO_URL}"
        }

        dir('roles/nginx/templates/virtualhosts') {
            git branch: "${ANSIBLE_BB_TEMPLATES_BRANCH}", credentialsId: 'setevoy_bitbucket_aws', url: "${ANSIBLE_BB_TEMPLATES_REPO_URL}"
        }

        stage('Ansible playbook apply') {

            sh "chmod 400 credentials/${ANSIBLE_EC2_PEM_FILE}"
            sh "ansible-playbook --limit=${ansibleHostLimit} --private-key=credentials/${ansiblePemFile} ${ansiblePlaybookFile}"
        }
    }
}
...

Сначала git выполняет checkout ${ANSIBLE_GITHUB_REPO_URL}.

Потом – создаёт каталог credentials, и в него загружает репозиторий ${ANSIBLE_BB_CREDENTIALS_REPO_URL}.

И третьим – создаёт каталог roles/nginx/templates/virtualhosts, в который загружает содержимое ${ANSIBLE_BB_TEMPLATES_REPO_URL}.

Один нюанс тут: в шаблоне CloudFormation создаётся SecurityGroup с доступом к порту 22 только с моего домашнего IP – надо руками добавить ещё одно правило для доступа с IP хоста с Jenkins, т.к. он постоянно меняется. Позже можно будет обновить шаблон и использовать Fn::ImportValue, что бы полчить IP Jenkins из его стека.

Добавляем вызов функции в скрипт rtfm-blog-ansible-provision.groovy и передаём ей параметры (для Production, соответственно – будут другие параметры в Jenkins job-е, которые и будут подставляться в аргументы функции):

...
    // ansibleHostLimit='1', ansiblePlaybookFile='2', ansiblePemFile='3'
    provision.ansiblePlaybookApply("${ANSIBLE_HOST_LIMIT}", "${ANSIBLE_PLAYBOOK_FILE}", "${ANSIBLE_EC2_PEM_FILE}")
...

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

[simterm]

$ git add provision.groovy rtfm-blog-ansible-provision.groovy && git commit -m "ansiblePlaybookApply() added" && git push

[/simterm]

Работает… Странно 🙂

Проверяем

Всё удаляем – и создаём заново.

Удаляем стек:

[simterm]

$ aws cloudformation delete-stack --stack-name rtfm-dev

[/simterm]

Запускаем rtfm-blog-dev-cf-provision в Jenkins:

Обновляем IN A для dev.rtfm.co.ua, ждём обновления DNS:

[simterm]

$ dig dev.rtfm.co.ua +short
34.250.106.205

[/simterm]

Обновляем (пока вручную) Security Group, даём доступ с IP Jenkins.

Запускаем rtfm-blog-dev-ansible-provision:

Проверяем:

[simterm]

$ curl -I dev.rtfm.co.ua
HTTP/1.1 200 OK
Server: nginx/1.10.3
Date: Wed, 18 Oct 2017 16:28:40 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Wed, 18 Oct 2017 16:26:59 GMT
Connection: keep-alive
ETag: "59e780d3-264"
Accept-Ranges: bytes

[/simterm]

Готово.