Как стать автором
Обновить
70.96
Слёрм
Учебный центр для тех, кто работает в IT

Как собрать Docker-контейнеры с помощью Ansible

Время на прочтение8 мин
Количество просмотров15K

Docker — это система контейнеризации, собирающая независимые части ОС без установки библиотек в основную систему. В отличие от виртуалок, которые собираются долго, такие контейнеры собираются и запускаются достаточно быстро. Это позволило Docker и Kubernetes стать одним из главных средств автоматизации и деплоя.

Как собирать и загружать контейнеры

Например, у нас есть файл hosts с build_host и runner_host. Нам нужно собрать контейнеры из готовых Docker-файлов и перенаправить их на runner_host, который будет их запускать.

all
  children:
    docker:
      hosts:
        build_host:
          ansible_host: 192.168.53.2
        runner_host:
          ansible_host: 192.168.53.3

Производим установку при помощи yum-repository, а затем передаем списки точно так же, как и apt, который можно сделать путем передачи списка переменных. Рекомендую этот способ в отличие от with_items и циклов, поскольку так будет быстрее. Если у модуля есть внутренняя оптимизация, тогда он будет принимать списки и прогонять их быстрее. Рекомендую пользоваться только таким методом.

name: “Install docker”
hosts: docker
become: true
vars:
  packages:
     - python3
     - python3-pip
     - python3-setuptools
     - libselinux-python3
   pip: packages:
      - six
      - docker
      - requests
tasks:
- name: “create yum repository for docker”
  ansible.builtin.yum_repository:
    name: docker-repo
    description: “repo for docker”
    baseurl: “https://download.docker.com/linux/centos/7/x86_64/stable/”
    enabled: yes
    gpgcheck: no

Далее устанавливается SDK Docker и поднимается сам сервис. Следующая задача – сборка контейнера на build_host. При сборке контейнеров нужно взять их из directory files на локальной машине вместе с Ansible и из всех Docker-файлов внутри папок.

FROM alpine:latest
EXPOSE 8080
CMD nc -l -p 8080

Если мы выполним запрос, нам вернется его зеркальная копия. Это нужно для сборки нескольких контейнеров, чтобы я мог показать, как работают списки.

name: “Install python and pip”
ansible.builtin.yum:
  name: “{{ packages }}”
  state: present
  update_cache: true


name: “Install docker sdk”
pip:
 name: “{{ pip_packages }}”
vars:
  ansible_python_interpreter: /user/bin/python3


name: “Start docker service”
service:
  name: “docker”
  state: started


name: "Build container"
     hosts: build_host
     gather_facts: no
     become: true
     tags: build
     vars:
       ansible_python_interpreter: /usr/bin/python3
     tasks:
       - name: “find files”
         find:
          paths: ~/ansible/Docker/files
          recurse: yes
          file_type: “directory”
         delegate_to: 127.0.0.1
         register: files
         become: false

Мы видим gather_facts: no, поскольку нас не интересует Build container как виртуальная машина, т. к. мы туда ничего не устанавливаем, а используем с уже готовым набором софта, чтобы выполнять необходимые операции. 

Теги

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

Ansible по умолчанию при запуске плейбука проверяет все сверху донизу, но вам такая опция может быть нужна не всегда. Иногда у вас могут быть контрольные плейбуки, которые выполняют несколько вещей: устанавливают Docker, собирают Docker-контейнеры, загружают и запускают их. Это три таски и три разных плея.

Необязательно запускать все с самого начала, чтобы что-то запустить в Docker-контейнере. Вместо этого мы можем поставить отдельные теги и с помощью команды - и -tags и имени тега запустить соответствующий плей. Если у вас есть какой-то плей и какая-то таска после этого плея в другом плее, вы можете запустить и ее, поставив ей такой же тег, как у вашего плея. В этом случае запустится плей и одна таска. Комбинировать это можно как угодно. Один плей, как и одна таска, могут иметь несколько тегов. 

Следите за именами, не давайте тегам разбегаться. Если у вас теги называются ansible_build, ansible_load, то тег community_docker_install будет несколько неуместен. В отличие от переменных: там теги внутри ролей именуются как угодно, а теги внутри плейбуков требуют определенного механизма именования.

И еще важный момент: существует два тега – always и never. Первый будет запущен всегда при команде -tags, второй, если вы его никак не отметите, вообще не будет запущен. Это может быть полезно тогда, когда у вас есть таска, без которой ничего не будет работать. В этом случае ей нужно присвоить тег always. Или, например, у вас есть таска, которую включать не нужно, но она вам нужна на какой-то крайний случай. Тогда используем тег never. В целом не советую строить комплексные структуры тегов: чем проще вы пишете плейбуки, тем лучше.

Когда все подготовлено, мы шерстим с помощью команды ansible.builtin.find путь ~/ansible/Docker/files. В моем случае это путь, где все лежит. Нам нужно найти все, что имеет type: “directory”. Команда delegate_to отправит все на локальный хост, где запущен Ansible. Вывод отправится в build_host в переменную files.

name: "Build container"
hosts: build_host
gather_facts: no
become: true
tags: build
vars:
  ansible_python_interpreter: /usr/bin/python3
tasks:
  - name: "find files"
    ansible.builtin.find:
      paths: ~/ansible/Docker/files
      recures: yes
      file_type: "directory"
    delegate_to: 127.0.0.1
    register: files
    become: false 

name: delete old directory
     file:
       path: /root/dockerfiles
       state: absent

name: create build directory 
file:
  path: /root/dockerfiles
  state: directory
  owner: root
  group: root
  mode: ‘0755’

Далее на build_host нужно удалить directory_dockerfiles и создать directory заново. У нас есть таска, которая повторяется несколько раз и которую нам нет смысла разворачивать, поскольку это цикл. Этот цикл можно увидеть при помощи directory_loop.

name: "Copy files and build them"
include tasks: "container_assembly.yml"
loop: "{{ files.files }}"

Циклы Ansible

  • With_<lookup>

В первых версиях Ansible единственное, что было для организации циклов, – это конструкция with_<lookup>. Разработчикам Ansible не очень нравилась «магия», которая происходит с развязыванием этой конструкции. Они придумали цикл loop.

  • Loop

Это то же самое, что и with_items. Если вы передадите loop какую-то списковую переменную, то она будет работать так же, как и with_<lookup>, но без модуля. Loop примет в себя любые данные в списочном формате. Что немаловажно, этот цикл прозрачен.

  • Until

Таска будет выполняться сколько угодно раз, пока не выполнится условие выхода из этой таски. 

  • Модуль wait_for

Модуль wait_for – это пример того, что списки могут быть не только внутренними, но и вложенными. В этом модуле существует цикл, внутри которого происходит обращение к портам, их вызов и возврат к таске. Внутри своих модулей вы можете использовать все прелести языка программирования, на котором пишете этот модуль. 

Рассмотрим пару примеров. Цикл loop вызывается при помощи query и lookup. Разница между ними в том, что query возвращает массив, а lookup – строчку. Оба цикла аналогичны with_inventory_hostnames.

loop: "{{ query('inventory_hostnames', 'all') }}"
loop: "{{ lookup('inventory_hostnames', 'all', wantlist=True) }}"

То, что регистрируется как переменная files после ansible.builtin.find, имеет ключ files. Именно поэтому к нему обращаемся через loop: “{{files.files}}”. Здесь включается таска “Copy files and build them”, которая копирует и билдит файлы как контейнеры. Также включаются таски из container_assembly. Это не только способ разбить большую таску на несколько, но и единственный способ включить таски и выполнить несколько тасок в цикле. 

name: "Build container"
hosts: build_host
gather_facts: no
become: true
tags: build
vars:
  ansible_python_interpreter: /usr/bin/python3
tasks:
  - name: "find files"
    ansible.builtin.find:
      paths: ~/ansible/Docker/files
      recures: yes
      file_type: "directory"
    delegate_to: 127.0.0.1
    register: files
    become: false 

name: delete old directory
     file:
       path: /root/dockerfiles
       state: absent

name: create build directory 
file:
  path: /root/dockerfiles
  state: directory
  owner: root
  group: root
  mode: ‘0755’

name: “Copy files and build them”
        include tasks: “container_assembly.yml”
        loop: “{{ files.files }}”

Обратите внимание, что у Ansible существует две директивы: build_tasks и import_tasks. Разница в том, что import_tasks импортирует таски и «развернет» их. Проще говоря, она скопирует таски из одного места в другое, но применить loop к ним вы не сможете, поскольку Ansible сделает это до того, как будет выполнять плейбук. Также вы не сможете поставить переменные в имени, т. к. Ansible сделает это до. Соответственно, include_tasks включает их динамически, что дольше при выводе плейбука, но зато вы сможете бегать по ним циклами, динамически создавать имена и т. д.

name: "Copy files and build them"
import tasks: "container_assembly.yml"
loop: "{{ files.files }}"

Теперь посмотрим, что находится внутри container_assembly. Здесь мы сталкиваемся со следующим: известная вам команда file должна создать директорию для изображений. То есть внутри Docker/files она должна создать папку db и web. Такой стиль записи для питонистов-программистов: /root/dockerfiles/{{item.path.split('/')[-1]}}. 

name: create image dir
file:
  path: /root/dockerfiles/{{item.path.split('/')[-1]}}
  state: directory
  owner: root
  group: root
  mode: '0755'

name: copy Dockerfile
copy:
  src: "{{item.path}}/Dockerfile"
  dest: "/root/dockerfiles/{{item.path. | basename}}/Dockerfile"
  owner: root
  group: root
  mode: '0644'

При копировании dockerfiles в другую папку я применяю более характерную для Ansible команду – фильтр. Выглядеть это будет так: /root/dockerfiles/{{item.path. | basename}}/Dockerfile. Такой вариант для сисадминов будет более читаем. 

dest: "/root/dockerfiles/{{item.path. | basename}}/Dockerfile"

Как строить контейнеры

Команда docker_image является нативной для Ansible и требует установленный Docker SDK. Эта команда построит контейнер. Его можно назвать “{{item.path | basename}}_container:v1.0”. 

 name: "{{item.path | basename}}_container:v1.0"

Внутри docker_image есть массив build, в который передается переменная path. 

name: copy Dockerfiles
copy:
  src: “{{item.path}}/Dockerfile”
  dest: “/root/dockerfiles/{{item.path | basename}}/Dockerfile”
  owner: root
  group: root
  mode: ‘0644’

name: build container image
docker_image:
  name: "{{item.path | basename}}_container:v1.0"
  build:
    path: "/root/dockerfiles/{{item.path | basename}}/"
  state: present

Поскольку билд и запуск контейнеров происходят на двух разных машинах, нам эти контейнеры нужно поменять. Я буду действовать дедовскими методами: буду паковать контейнеры в .tar-файлы, архивировать их и перемещать через родительскую машину. Для этого я воспользуюсь командой docker_image, но уже не build, а archive:

archive_path: "/root/{{item.path | basename}}_container:v1.0.tar"

Она заархивирует контейнеры в .tar-файлы. После этого при помощи команды fetch я забираю эти файлы к себе на машину Ansible в виде .tar-архива в папку “files/{{item.path | basename}}_container:v1.0.tar”. Никаких дополнительных путей в этом случае прописывать не нужно. Я ставлю flat: true.  

name: fetch archived image
fetch:
  src: "/root/{{item.path | basename}}_container:v1.0.tar"
  dest: "files/{{item.path | basename}}_container:v1.0.tar"
  flat: true

Последний плей в плейбуке – это загрузка контейнеров, которая будет производиться на runner_host. Для этого используем команду “find docker directories” на локальной машине. В регистре допустимо оставить files. Те файлы, что мы собрали, нужно агрегировать и передать в container_load, который точно так же соберет и скопирует эти файлы. 

container_load.yml

Теперь для загрузки контейнера из архива нам нужно его как-то назвать. Питонисты в этом случае сделали бы сплит, взяли имя контейнера, разделили его по точке и использовали в качестве имени. Я иду путем Ansible и даю следующее название: “{{item.path | splitext | first}}”. Run container запустит контейнер. 

name: copy tarball to host
copy:
  src: “files/{{item.path | basename}}”
  dest: “/root/{{item.path | basename}}”

name: load container from tarball
docker_image: 
   name: "{{item.path | splitext | first}}"
   load_path: "/root/{{item.path | basename}}"
   state: present
   source: load

name: run container
docker_contatiner:
  name: “{{(item.path | basename).split(‘_’) | first }}_app”
  image: “{{item.path | basename | splitext | first}}”
  state: started

Healthcheck

По традиции я делаю Healthcheck. Для этого я использую цикл until. Чтобы проверять что-то в этом цикле, необходимо зарегистрировать нужную переменную на хосте, и она будет регистрироваться при каждом вызове таски. 

until: result.host_info.ContainersRunning == files.files | length

При помощи модуля docker_host_info я собираю информацию о контейнерах и проверяю количество запущенных контейнеров. Это я делаю потому, что Docker может не запустить какие-то контейнеры, или они могут повиснуть в каком-то стартапе и не сразу вызваться. Я не могу проверить один раз и уйти: проверку нужно сделать несколько раз. Если я вызову команду Ansible плейбук с тегом load, она включит в себя healthcheck, но если я вызову команду healthcheck отдельно, она включит в себя только healthcheck.

name: "Healthcheck"
docker_host_info: 
  containers: yes
register: result
until: result.host_info.ContainersRunning == files.files | length
retries: 5
delay: 10
tags: healthcheck

Более обстоятельно об опциях, фичах и сфере использования Ansible я рассказываю на бесплатном курсе «Ansible: Infrastructure as Code» от Слёрма. Курс состоит из 11 блоков и включает 72 урока, в которых я не только рассказываю и показываю основные принципы работы с Ansible, но и даю домашние задания на закрепление пройденного. Приятным дополнением станет возможность отработать практику на наших стендах.

Теги:
Хабы:
Всего голосов 18: ↑14 и ↓4+10
Комментарии3

Публикации

Информация

Сайт
slurm.io
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия
Представитель
Антон Скобин