Jenkins: деплой Docker Compose из Ansible и ECR авторизация

Автор: | 09/26/2019
 

В продолжение поста AWS: создание Elastic Container Registry и деплой из Jenkins, в котором создали джобу для билда Docker-образов и их пуша в AWS ECR — теперь надо создать джобу в Jenkins для деплоя и запуска одного Docker-контейнера.

Запускать будем через Docker Compose, в котором Ansible будет задавать требуемую версию из параметров Jenkins-джобы.

Для того, что бы деплой работал — на хостах необходимо выполнить авторизацию Docker-демона в AWS ECR — для того используем amazon-ecr-credential-helper.

Amazon-ecr-credential-helper

Сначала пробуем вручную.

Т.к. для Debian пакета нет — извращаемся с bash: создадим скрипт, который будет дёргать amazon-ecr-credential-helper из docker:

#!/usr/bin/env bash

SECRET=$(docker run --rm -e METHOD=$1 -e REGISTRY=$(cat -) -v $HOME/.aws/credentials:/root/.aws/credentials pottava/amazon-ecr-credential-helper)

echo $SECRET | grep Secret

Задаём права на выполнение:

root@bttrm-dev-console:/home/admin# chmod +x /usr/bin/docker-credential-ecr-login

Создаём профиль, что бы AWS CLI создал файл ~/.aws/credentials, который потом будет использоваться amazon-ecr-credential-helper:

root@bttrm-dev-console:/home/admin# aws configure
AWS Access Key ID [None]: AKI***6EZ
AWS Secret Access Key [None]: PpN***GNr
Default region name [None]: us-east-2
Default output format [None]: json

Обновляем ~/.docker/config.json:

{
    "credHelpers": {
        "534***85.dkr.ecr.us-east-2.amazonaws.com": "ecr-login"
    }
}

Тут задаём авторизацию именно в 534***85.dkr.ecr.us-east-2.amazonaws.com с помощью «авторизатора» ecr-login.

Подтягиваем образ:

root@bttrm-dev-console:/home/admin# docker pull pottava/amazon-ecr-credential-helper
Using default tag: latest
latest: Pulling from pottava/amazon-ecr-credential-helper
...
Status: Downloaded newer image for pottava/amazon-ecr-credential-helper:latest

И проверяем:

root@bttrm-dev-console:/home/admin# docker pull 534***385.dkr.ecr.us-east-2.amazonaws.com/bttrm-receipt-consumer
Using default tag: latest
latest: Pulling from bttrm-receipt-consumer
Digest: sha256:3bb7cebd34d7642b10fe44ad8ab375e5fd772fc82b4f6fa997c59833445fdef5
Status: Image is up to date for 534***385.dkr.ecr.us-east-2.amazonaws.com/bttrm-receipt-consumer:latest

Работает.

Теперь осталось внести это всё в Ansible.

Ansible deploy role

Создаём шаблон конфига Docker — roles/deploy-docker/templates/config.json.j2:

{
    "credHelpers": {
        "{{ bttrm_queue_consumer_repo }}": "ecr-login"
    }
}

Шаблон для авторизации AWS CLI — roles/deploy-docker/templates/aws-credentials.j2:

[default]
aws_access_key_id = {{ bttrm_queue_consumer_login }}
aws_secret_access_key = {{ bttrm_queue_consumer_pass }}

В файл переменных group_vars/mobilebackend-dev.yml добавляем значения:

...
bttrm_queue_consumer_repo: "534***385.dkr.ecr.us-east-2.amazonaws.com"
bttrm_queue_consumer_image: "bttrm-receipt-consumer"
bttrm_queue_consumer_login: "AKI***6EZ"
bttrm_queue_consumer_pass: "PpN***GNr"
...

Создаём шаблон скрипта — roles/deploy-docker/templates/docker-credential-ecr-login-sh.j2:

#!/usr/bin/env bash

SECRET=$(docker run --rm -e METHOD=$1 -e REGISTRY=$(cat -) -v $HOME/.aws/credentials:/root/.aws/credentials pottava/amazon-ecr-credential-helper)

echo $SECRET | grep Secret

Шаблон для Docker Compose, который будет запускать сервис — roles/deploy-docker/templates/bttrm-queue-consumer-compose.yml.j2:

version: '3.5'
  
services:
    
  receipts:
    container_name: bttrm-queue-consumer-{{ backend_project_name }}
    image: {{ bttrm_queue_consumer_repo}}/{{ bttrm_queue_consumer_image}}:{{ bttrm_queue_consumer_tag }}
    environment:
      - APPLICATION_ENV={{ env }}
      ...

В этот Compose во время деплоя Ansible подставит тег bttrm_queue_consumer_tag, который берётся из параметров Jenkins-джобы, и по умолчанию имеет значение latest:

Создаём шаблон systemd-юнит файла — roles/deploy-docker/templates/bttrm-queue-consumer-systemd.yml.j2:

[Unit]
Description=bttrm-queue-consumer client
Requires=docker.service
After=docker.service

[Service]
Restart=always
WorkingDirectory={{ bttrm_queue_consumer_home }}

# Compose up
ExecStart=/usr/local/bin/docker-compose -f bttrm-queue-consumer-compose.yml up

# Compose down, remove containers and volumes
ExecStop=/usr/local/bin/docker-compose -f bttrm-queue-consumer-compose.yml down -v

[Install]
WantedBy=multi-user.target

В roles/deploy-docker/tasks/main.yml добавляем копирование файлов и запуск сервиса:

- name: "Copy Docker config"
  template:
    src: "templates/config.json.j2"
    dest: "/root/.docker/config.json"
  when: "'console' in inventory_hostname"

- name: "Copy AWS CLI credentials config"
  template:
    src: "templates/aws-credentials.j2"
    dest: "/root/.aws/credentials"
  when: "'console' in inventory_hostname"
      
- name: "Copy Docker ECR credentials script"
  template:
    src: "templates/docker-credential-ecr-login-sh.j2"
    dest: "/usr/bin/docker-credential-ecr-login"
  when: "'console' in inventory_hostname"    
      
- name: "Set Docker ECR credentials script executable"
  file: 
    path: "/usr/bin/docker-credential-ecr-login"
    mode: 0755
  when: "'console' in inventory_hostname"

- name: "Create bttrm-queue-consumer home"
  file:
    path: "{{ bttrm_queue_consumer_home }}"
    state: directory
    mode: 0775
    recurse: yes
  when: "'console' in inventory_hostname"

- name: "Copy bttrm-queue-consumer Compose file"
  template:
    src: "templates/bttrm-queue-consumer-compose.yml.j2"
    dest: "{{ bttrm_queue_consumer_home }}/bttrm-queue-consumer-compose.yml"
  when: "'console' in inventory_hostname"

- name: "Copy bttrm-queue-consumer systemd file"
  template:
    src: "templates/bttrm-queue-consumer-systemd.yml.j2"
    dest: "/etc/systemd/system/bttrm-queue-consumer.service"
  when: "'console' in inventory_hostname"

- name: "Start bttrm-queue-consumer service"
  service:
    name: "bttrm-queue-consumer"
    state: restarted
    enabled: yes
    daemon_reload: yes
  when: "'console' in inventory_hostname"

Jenkins job

В Jenkins Pipeline-джоба имеет скрипт с функцией ansibleApply():

...
def ansibleApply(app_rsa_id='1', bastion_rsa_id='2', tags='3', limit='4', playbookFile='5', passfile_id='6', connection='7') {
    
    docker.image('projectname/projectname-ansible:1.1').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
    
        stage('Ansible apply') {

            withCredentials([
                file(credentialsId: "${app_rsa_id}", variable: 'app_rsa'),
                file(credentialsId: "${passfile_id}", variable: 'passfile'),
                file(credentialsId: "${bastion_rsa_id}", variable: 'bastion_rsa')
            ]) {
                sh "ansible-playbook --private-key ${bastion_rsa} --tags ${tags} --limit=${limit} ${playbookFile} --vault-password-file ${passfile} --extra-vars \
                    \"ansible_connection=${connection} \
                    app_rsa_pem_key=${app_rsa} \
                    bastion_rsa_pem_key=${bastion_rsa}\""
               }
        }
    }
}
...

Которая вызывается из Jenkinsfile:

node {
...
        // infra for CloudFormation
        // from Jenkins job's parameters
        TAGS = "${env.TAGS}"

        // limit for hosts.ini
        LIMIT = "${env.LIMIT}"
    
        // playbook to run, e.g. mobilebackend.yml
        PLAYBOOK = "${env.PLAYBOOK}"
 
        // file with ansible vault password
        // to be used in ansibleApply()'s withCredentials()
        PASSFILE_ID = "${env.PASSFILE_ID}"
                
        provision.ansibleRolesInstall()
        provision.ansbileSyntaxCheck("${LIMIT}", "${PLAYBOOK}")

        ...

        // ansibleApply(rsa_id='1', tags='2', limit='3', playbookFile='4', passfile_id='5', connection='6')
        provision.ansibleApply( "${APP_RSA_ID}", "${BASTION_RSA_ID}", "${TAGS}", "${LIMIT}", "${PLAYBOOK}", "${PASSFILE_ID}", "${CONN}")
    }
}
///

Тут в Ansible передаём тег deploy-docker из параметров Jenkins:

И по этому тегу Ansible выбирает роль из плейбука — tags: deploy-docker:

...
    - role: deploy-docker
      tags: deploy-docker
      backend_project_name: "{{ lookup('env','APP_PROJECT_NAME') }}"
      bttrm_queue_consumer_tag: "{{ lookup('env','BTTRM_QUEUE_CONSUMER_TAG') }}"
      when: "'backend-bastion' not in inventory_hostname"

И заодно подставит bttrm_queue_consumer_tag в Docker Compose.

Деплоим, проверяем:

root@bttrm-stage-console:/opt/bttrm-queue-consumer# systemctl status bttrm-queue-consumer
● bttrm-queue-consumer.service - bttrm-queue-consumer client
Loaded: loaded (/etc/systemd/system/bttrm-queue-consumer.service; enabled; vendor preset: enabled)
...
Sep 26 16:06:23 bttrm-stage-console docker-compose[21251]: bttrm-queue-consumer-projectname-me-v3 | time="2019-09-26T13:06:23Z" level=info msg="Connecting as projectname_me_v3 to stage.backend-db3-master.example.world:3306/projectname_me_v3"
Sep 26 16:06:23 bttrm-stage-console docker-compose[21251]: bttrm-queue-consumer-projectname-me-v3 | time="2019-09-26T13:06:23Z" level=info msg="Declaring Queue (itunes-receipts)"
Sep 26 16:06:23 bttrm-stage-console docker-compose[21251]: bttrm-queue-consumer-projectname-me-v3 | time="2019-09-26T13:06:23Z" level=info msg="Waiting for queue messages..."

Готово.