До того, как все заполонили контейнеры, главными инструментами для создания локальной среды разработки были технологии наподобие Vagrant и VirtualBox. Эти инструменты в сочетании с такими средствами автоматизации, как Ansible и Chef, позволяли создать рабочую воспроизводимую среду для приложений. Однако развитие легких вариантов виртуализации, заложенное docker и постоянно упрощаемое различными облачными инновациями, привело к упадку этих некогда очень популярных среди разработчиков инструментов. Настолько стремительному, что увидев их где-нибудь, мы невольно задумываемся о возрасте кодовой базы.

И вот недавно я сам наткнулся на них. А если быть точнее, то мне достался проект, который все-еще на них полагается — он предполагает установку виртуальной машины VirtualBox под управлением Debian, созданной с помощью Vagrant, а затем настроенной с помощью Ansible. И все это работает. Ну, по большей части. Но когда не работает, разбираться, что пошло не так — настоящая боль. Поддержание координации между Vagrant и VirtualBox было особенно неприятной черной магией, которая подтолкнула меня к размышлениям о более дешевых и дружественных альтернативах виртуализации.

К концу моего ознакомления с проектом я разработал план из трех этапов:

  1. Заменить vagrant и virtualBox на docker. Т.е. использовать ansible для переноса проекта в docker-контейнеры.

  2. Заменить ansible на docker-compose, сопровождаемый специальными Dockerfile для каждого из приложений, задействованных в проекте.

  3. Распространить изменение инструментария на другие среды: staging и production.

Данная статья представляет собой документацию к первой фазе этого плана.

Почему я решил написать эту статью?

Использование ansible для настройки и запуска контейнеров docker — не самый распространенный сценарий. На самом деле, одной из причин написания этой статьи стало то, что мне не удалось найти ни одного ресурса, где было бы собрано то, что мне нужно. Такая нехватка информационных ресурсов вполне объяснима, поскольку, как только вам удается добиться какого-либо успеха в автоматизации проекта, докрутить что-то еще становится довольно просто благодаря множеству доступных инструментов.

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

Однако, поскольку требования к этим зависимостям уже отражены в плейбуках ansible, я могу значительно упростить себе задачу, просто указав ansible на пустой контейнер, и заставить его настроить контейнер так же, как он настраивает виртуальную машину virtualBox. В конце концов, это одна из потрясающих возможностей ansible: дайте ей (ssh) доступ к любой машине, и она предоставит вам желаемое окружение. В данном случае машиной с доступом по ssh будет контейнер docker, а желаемым окружением — среда разработки, отраженная в различных плейбуках ansible.

Проект

Мой проект — довольно крупное приложение Ruby-on-Rails с кучей типичных зависимостей, таких как Sidekiq, Nginx и т.д.

По понятным причинам я не могу использовать реальный проект в этой статье. Вместо него мы будем использовать публичный проект docker-rails. Я выбрал этот проект, потому что его зависимости довольно похожи на мой собственный проект. В них присутствуют:

  • Sidekiq

  • Postgres

  • Redis

  • Opensearch

Наличие этих зависимостей даст нам возможность решить ряд задач, которые я хотел бы рассмотреть в этой статье.

Ну а наши приключения начинаются с клонирования проекта в рабочую среду (workspace).

mkdir -p ansidock
cd ansidock
git clone https://github.com/ledermann/docker-rails

Инвентарь

Инвентарь Ansible отвечает за список узлов/хостов, с которыми Ansible должен работать и за то, как именно он должен с ними работать. Настройки, начиная от используемого плагина подключения и заканчивая расположением интерпретатора python на каждом узле, настраиваются с помощью файла инвентаризации. В данном случае я использую его для создания топологии локальной среды разработки.

На высоком уровне нам нужно выделить два разных узла:

  1. родительский узел, на котором создаются (и управляются) контейнеры docker

  2. узел-контейнер, в котором запускается нужное приложение

Чтобы понять замысел, стоящий за этой структурой, давайте рассмотрим текущую настройку ansible + vagrant + virtualBox.

Vagrant создает виртуальную машину на базе virtualBox. После этого он передает информацию о новой виртуальной машине в Ansible. Затем Ansible настраивает новый box для запуска требуемого приложения. Другими словами, vagrant работает с virtualBox на реальном хосте, машине разработчика, чтобы предоставить виртуальную машину, которую ansible затем настраивает для запуска целевого приложения.

В результате всего этого мы получим такой файл инвентаризации:

# file: ansidock/dev
[dockerhost]
localhost

[app]
[postgres]
[redis]
[opensearch]

[containers:children]
app
postgres
redis
opensearch

[all:vars]
ansible_python_interpreter= /usr/bin/python3

[dockerhost:vars]
ansible_connection=local

[containers:vars]
ansible_connection=docker

Управляющий узел указывается как dockerhost, с подключением к localhost. Хосты redis, postgres и opensearch, удобно сгруппированные под тегом containers, являются удаленными узлами, к которым можно подключиться через docker.

Обратите внимание, что ни один из хостов контейнеров не содержит никакой информации о подключении (например, URL или IP-адрес). Это потому, что они будут создаваться на лету, и одним из приемов, которые я буду использовать в дальнейшем, будет заключатся в динамическом заполнении этой информации по мере создания контейнеров.

Сеть

В упрощенном мире внутри виртуальной машины VirtualBox все процессы смогут общаться с любым соседним процессом. Таким образом, первым делом необходимо обеспечить широкую открытую сеть для будущих сервисов.

Docker позволяет настраивать различные типы сетей. Для моих целей достаточно базовой bridge network.

# file: ansidock/network.yml
---
- hosts: dockerhost
  tasks:
    - name: "Create docker network: {{ network_name }}"
      ansible.builtin.docker_network:
        name: "{{ network_name }}"
        driver: bridge

Эта сеть позволит любому контейнеру в ней обращаться к любому другому контейнеру в сети, используя IP-адрес, имя или псевдоним контейнера.

Зависимости

Нашему целевому приложению необходимы три главные зависимости: Postgres, Redis и Opensearch.

В реальном проекте, над которым я работаю, эти зависимости устанавливаются непосредственно в виртуальную машину. Для этой установки также доступны сценарии ansible. Таким образом, у нас есть возможность подтянуть ту же строку и явно установить каждую зависимость в пустой контейнер.

Тем не менее, у меня в планах вторая фаза — фаза, на которой я откажусь от Ansible. Таким образом, мы уже можем приступить к развертыванию выделенных контейнеров для каждого из этих сервисов. Более того, существуют уже готовые для использования варианты контейнеров для этих проектов, что делает их довольно простыми для внедрения. Учитывая это, мы развернем эти зависимости с помощью их официальных образов docker вместо того, чтобы запускать их установочные плейбуки.

Теперь нам нужно будет сделать с контейнерами несколько повторяемых манипуляций, поэтому сейчас самое лучшее время создать многократно используемую роль ansible, чтобы абстрагировать эти задачи:

# file: ansidock/roles/container/tasks/main.yml
---
- name: "pull {{ image }}:{{ image_tag }}"
  ansible.builtin.docker_image:
    name: "{{ image }}"
    tag: "{{ image_tag }}"
    source: pull

- name: "create {{ container_name }} container"
  ansible.builtin.docker_container:
    name: "{{ container_name }}"
    image: "{{ image }}:{{ image_tag }}"
    command: "{{ container_command }}"
    auto_remove: yes
    detach: yes
    env: "{{ container_env }}"
    ports: "{{ container_ports }}"
    volumes: "{{ container_volumes }}"
    working_dir: "{{ container_workdir }}"
    networks:
      - name: "{{ network_name }}"

- name: "add {{ container_name }} container to host group: {{ container_host_group }}"
  ansible.builtin.add_host:
    name: "{{ container_name }}"
    groups:
      - "{{ container_host_group }}"
  changed_when: false
  when: container_host_group is defined

- name: "update {{ container_name }} package register"
  ansible.builtin.command:
    cmd: 'docker exec {{ container_name }} /bin/bash -c "apt-get update"'
  when: container_deps is defined

- name: install dependencies
  ansible.builtin.command:
    cmd: 'docker exec {{ container_name }} /bin/bash -c "apt-get install -y {{ container_deps | join(" ") }}"'
  when: container_deps is defined

со следующими дефолтными переменными

# file: ansidock/roles/container/defaults/main.yml
---
container_command:
container_env: {}
container_host_group:
container_ports: []
container_volumes: []
container_workdir:

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

Используя эту роль, мы создаем плейбук для наших трех зависимостей:

# file: ansidock/dependencies.yml
---
- name: Postgres database
  hosts: dockerhost
  vars:
    image: "{{ postgres_image }}"
    image_tag: "{{ postgres_version }}"
    container_name: "{{ postgres_container_name }}"
    container_env: "{{ postgres_env }}"
    container_ports: "{{ postgres_ports }}"
    container_host_group: postgres
  roles:
    - container

- name: Redis cache
  hosts: dockerhost
  vars:
    image: "{{ redis_image }}"
    image_tag: "{{ redis_version }}"
    container_name: "{{ redis_container_name }}"
    container_host_group: redis
  roles:
    - container

- name: Opensearch library
  hosts: dockerhost
  vars:
    image: "{{ opensearch_image }}"
    image_tag: "{{ opensearch_version }}"
    container_name: "{{ opensearch_container_name }}"
    container_env: "{{ opensearch_env }}"
    container_host_group: opensearch
  roles:
    - container

Это приближает нас к цели примерно на 40%.

Вот наш прогресс на данный момент:

  • инвентарь, который будет динамически обновляться по мере создания контейнеров

  • сеть docker, чтобы все контейнеры могли связаться друг с другом

  • зависимости приложения, доступные в собственных контейнерах

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

# file: ansidock/group_vars/all.yml
---
network_name: ansidocknet
app_dir: /app/ansidock
app_ruby_version: 3.2.1
app_bundler_version: 2.4.6
# file: ansidock/group_vars/dockerhost.yml
---
postgres_image: postgres
postgres_version: 15-alpine
postgres_container_name: ansidock_db
postgres_ports:
  - 8765:5432
postgres_env:
  POSTGRES_PASSWORD: password
  POSTGRES_USER: postgres
  POSTGRES_DB: ansidock

opensearch_image: opensearchproject/opensearch
opensearch_version: latest
opensearch_container_name: ansidock_search
opensearch_env:
  discovery.type: single-node
  plugins.security.disabled: "true"

redis_image: redis
redis_version: alpine
redis_container_name: ansidock_redis

после чего запустим:

ansible-playbook -i dev network.yml dependencies.yml

После успешного завершения сценария мы выполняем следующие проверки:

docker container ls --format "{{.Names}}" 
# expect three containers: ansidock_search, ansidock_redis, ansidock_db
docker network inspect --format "{{range .Containers}}{{println .Name}}{{end}}" ansidocknet
# ansidock_search, ansidock_redis, ansidock_db

Отлично. Двигаемся дальше.

Приложение

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

Обратите внимание, что до сих пор ansible работал на dockerhost, то есть на узле управления. Поэтому в некоторых случаях, когда нам нужно было выполнить операцию над контейнером, мы не использовали ansible непосредственно в контейнере, а использовали ansible для выполнения командной оболочки на узле управления, которая затем выполняла команду с помощью cli docker.

Для примера посмотрите на задачи install dependencies из роли container выше.

- name: install dependencies
  ansible.builtin.command:
    cmd: 'docker exec {{ container_name }} /bin/bash -c "apt-get install -y {{ container_deps | join(" ") }}"'
  when: container_deps is defined

Если бы на данном этапе целью был узел контейнера, задачи были бы очень простыми:

- name: install dependencies
  ansible.builtin.apt:
    package: '{{ container_deps | join(" ") }}'
  when: container_deps is defined

Но поскольку ansible для этой задачи работает с управляющего узла, то есть с хост-машины с контейнерами, мы должны явно выполнить команды с помощью cli docker.

Это различие проявляется в плейбуке приложения, где в первой части нам нужно будет создать контейнер (используя узел управления в качестве хоста), а во второй — настроить этот контейнер (переключившись на узел контейнера в качестве хоста). Поэтому следите за переключением.

Первым делом необходимо создать пустой контейнер, подобный виртуальной машине с debian, которую vagrant предоставляет ansible для настройки проекта. В качестве образца мы возьмем базовый образ ubuntu. Как и в случае с зависимостями, мы будем использовать для настройки роль container.

Однако прежде чем это сделать, нам нужно выяснить, что делать с главным процессом контейнера. Жизненный цикл docker-контейнера завязан вокруг его главного процесса (PID 1), и правильная работа с ним является предметом многих идей, выводов и разочарований в управлении контейнерами.

Наше затруднение заключается в том, что целевой главный процесс, сервер rails, будет доступен только после того, как Ansible поработает с контейнером. Но чтобы ansible мог добраться до контейнера, контейнер должен быть запущен. А для запуска контейнера мы хотели бы, чтобы он был сервером rails... Очевидным решением будет передать PID 1 другой долгоживущей задаче (например, sleep infinity), а затем запустить сервер rails позже, когда он будет готов. Это шаг в правильном направлении, с тем нюансом, что нам нужно, чтобы все, что работает с главными процессами, также брало на себя управление процессами rails и любыми другими дочерними процессами, которые могут появиться.

К счастью, это не такая уж сложная задача. Экосистема Linux богата приложениями, написанными как раз для этой цели. Из всего многообразия вариантов мы остановимся на supervisord. Supervisord, помимо желаемого поведения, позволяет в любой момент времени добавлять (и удалять) дочерние процессы. Мы воспользуемся этим позже, чтобы запустить наши rails-процессы.

С этим разобрались, следующая задача ясна: собрать воедино набор задач, который даст нам базовый образ с supervisord и обеспечит возможность изменять конфигурацию supervisord по мере необходимости.

# file: ansidock/roles/supervisor/tasks/build.yml
---
- name: create temp directory for build
  ansible.builtin.tempfile:
    state: directory
  register: build_dir

- name: generate dockerfile
  ansible.builtin.template:
    src: dockerfile.j2
    dest: '{{ build_dir.path }}/Dockerfile'

- name: generate supervisord conf
  ansible.builtin.template:
    src: supervisord.conf.j2
    dest: '{{ build_dir.path }}/supervisord.conf'

- name: build supervisord image
  ansible.builtin.docker_image:
    name: "{{ image }}"
    tag: "{{ image_tag }}"
    source: build
    state: present
    force_source: true
    build:
      path: "{{ build_dir.path }}"
      pull: yes

Для выполнения задачи необходимы следующие два шаблона:

простая конфигурация supervisord

; file: ansidock/roles/supervisor/templates/supervisord.conf.j2
[supervisord]
logfile=/tmp/supervisord.log
loglevel=debug
nodaemon=true
user=root

и образ docker, который его использует

# file: ansidock/roles/supervisor/templates/dockerfile.j2
# syntax=docker/dockerfile:1
FROM ubuntu:23.04

RUN apt-get update \
    && apt-get install -y supervisor \
    && mkdir -p /var/log/supervisor

COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf

CMD ["/usr/bin/supervisord", "-n"]

Конечным результатом будет образ Ubuntu с установленным supervisord.

И теперь, когда у нас есть образ, мы можем передать его для запуска нашей роли container. Итак, вот первая часть нашего плейбука приложения:

# file: ansidock/application.yml
---
- name: Prepare application container image
  hosts: dockerhost
  vars:
    aim: build
    image: "{{ app_image }}"
    image_tag: "{{ app_image_version }}"
    container_name: "{{ app_container_name }}"
    container_env: "{{ app_env }}"
    container_ports: "{{ app_ports }}"
    container_host_group: app
    container_workdir: "{{app_dir}}"
    container_volumes:
      - "{{playbook_dir}}/{{app_src}}:{{app_dir}}"
    container_deps:
      - python3-apt
      - python3
      - python3-pip
  roles:
    - supervisor
    - container

Обратите внимание на дополнительные зависимости python, которые мы устанавливаем в контейнер. Они позволят нам использовать команды ansible непосредственно в контейнере (что мы будем использовать на следующем шаге). Конечно, мы можем (и должны) включить их при сборке базового образа, но это будет не так интересно.

Кроме того, если вы не заметили, на этом этапе мы добавили наш проект в контейнер.

Теперь, когда у нас есть правильно подготовленный базовый образ, для моего реального проекта все готово. Осталось только направить сценарии ansible на хост контейнера, и вуаля, у меня была рабочая среда, аналогичная существующему решению с виртуальной машиной. Но раз уж вы остались со мной до сих пор, мы закончим эту часть, запустив проект docker-rails в наше сетапе.

Осталось настроить контейнер для запуска rails-приложения. Для этого необходимо установить ruby со всеми его зависимостями, установить менеджер зависимостей ruby Bundler, Node.js и его менеджер пакетов Yarn, а также подготовить базу данных для приложения.

# file: ansidock/roles/ruby/tasks/main.yml
---
- name: install rbenv and app dependencies
  ansible.builtin.apt:
    name:
      - autoconf
      - bison
      - build-essential
      - git
      - imagemagick
      - libdb-dev
      - libffi-dev
      - libgdbm-dev
      - libgdbm6
      - libgmp-dev
      - libncurses5-dev
      - libpq-dev
      - libreadline6-dev
      - libssl-dev
      - libyaml-dev
      - patch
      - rbenv
      - ruby-build
      - rustc
      - tzdata
      - uuid-dev
      - zlib1g-dev
    state: present
    update_cache: true

- name: register rbenv root
  ansible.builtin.command:
    cmd: rbenv root
  register: rbenv_root

- name: install ruby-build rbenv plugin
  ansible.builtin.git:
    repo: https://github.com/rbenv/ruby-build.git
    dest: "{{ rbenv_root.stdout }}/plugins/ruby-build"

- name: "install ruby {{ ruby_version }}"
  ansible.builtin.command:
    cmd: "rbenv install {{ ruby_version }}"
  args:
    creates: "{{ rbenv_root.stdout }}/versions/{{ ruby_version }}/bin/ruby"
  environment:
    CONFIGURE_OPTS: "--disable-install-doc"
    RBENV_ROOT: "{{ rbenv_root.stdout }}"
    PATH: "{{ rbenv_root.stdout }}/shims:{{ ansible_env.PATH }}"

- name: install bundler
  ansible.builtin.gem:
    name: bundler
    version: "{{ bundler_version }}"
  environment:
    PATH: "{{ rbenv_root.stdout }}/shims:{{ ansible_env.PATH }}"

- name: install app gems
  ansible.builtin.bundler:
    state: present
    executable: "{{ rbenv_root.stdout }}/shims/bundle"

- name: remove conflicting yarn bin
  ansible.builtin.apt:
    package: cmdtest
    state: absent

- name: add yarn source key
  block:
    - name: yarn |no apt key
      ansible.builtin.get_url:
        url: https://dl.yarnpkg.com/debian/pubkey.gpg
        dest: /etc/apt/trusted.gpg.d/yarn.asc

    - name: yarn | apt source
      ansible.builtin.apt_repository:
        repo: "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/yarn.asc] https://dl.yarnpkg.com/debian/ stable main"
        state: present
        update_cache: true

- name: install yarn
  ansible.builtin.apt:
    package: yarn

- name: install javascript packages
  ansible.builtin.command:
    cmd: yarn install --frozen-lockfile
  environment:
    NODE_OPTIONS: "--openssl-legacy-provider"

- name: prepare database
  ansible.builtin.command:
    cmd: bundle exec rails db:prepare
  environment:
    PATH: "{{ rbenv_root.stdout }}/shims:{{ ansible_env.PATH }}"

- name: precompile assets
  ansible.builtin.command:
    cmd: bundle exec rails assets:precompile
  environment:
    PATH: "{{ rbenv_root.stdout }}/shims:{{ ansible_env.PATH }}"
    NODE_OPTIONS: "--openssl-legacy-provider"

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

Если это выглядит чересчур нагроможденным, то это лишь потому, что он борется с такими распространенными проблемами, как ключевое слово yarn, указывающее на cmdtest на Ubuntu, которое должно быть явно заменено на Yarn, менеджер зависимостей JavaScript; такие проблемы, как  устаревшая версия rbenv ruby-build в apt-репозиториях, ... и т.д. В любом случае, эти все замысловатые тонкости сейчас нас не интересуют. Поэтому мы двигаемся дальше.

Теперь, когда мы готовы запустить приложение, нам нужно поручить supervisord помочь нам в этом.

# file: ansidock/roles/supervisor/tasks/reconfigure.yml
---
- name: generate supervisor conf
  ansible.builtin.template:
    src: program.conf.j2
    dest: "/etc/supervisor/conf.d/{{ filename }}"
  vars:
    command: "{{ item.value }}"
    program: "{{ item.key }}"
    filename: "{{ item.key }}.conf"
    workdir: "{{ container_workdir }}"
  with_dict: "{{ programs }}"

- name: restart supervisord
  ansible.builtin.supervisorctl:
    name: '{{ item.key }}'
    config: /etc/supervisor/supervisord.conf
    state: present
  with_dict: "{{ programs }}"

Задача принимает карту программы для команды выполнения, генерирует конфиг supervisord из шаблона ниже и копирует его в контейнер.

; file: ansidock/roles/supervisor/templates/program.conf.j2
[program:{{ program }}]
command={{ command }}
directory={{ workdir }}
startretries=10
stdout_logfile={{ workdir }}/log/development.log
user=root

Да, задача также перезапускает supervisord для использования новой конфигурации (конфигураций).

Поскольку эта роль служит как для создания базового образа, так и для изменения конфигурации процесса supervisord, давайте добавим родительскую задачу, которая будет переключаться между этими двумя действиями:

# file: ansidock/roles/supervisor/tasks/main.yml
---
- include_tasks: build.yml
  when: aim == "build"
- include_tasks: reconfigure.yml
  when: aim == "configure"

Как вы, наверное, поняли, до сих пор мы не уделяли особого внимания Sidekiq. Это связано с тем, что он запускает то же самое rails-приложение, только через другой процесс. Поэтому все, что мы делали для главного приложения, применимо и к нему. Мы выделим его только сейчас, когда завершим работу над плейбуком нашего приложения:

# file: ansidock/application.yml
---
- name: prepare application container image
  hosts: dockerhost
  vars:
    aim: build
    image: "{{ app_image }}"
    image_tag: "{{ app_image_version }}"
    container_name: "{{ app_container_name }}"
    container_env: "{{ app_env }}"
    container_ports: "{{ app_ports }}"
    container_host_group: app
    container_workdir: "{{app_dir}}"
    container_volumes:
      - "{{ playbook_dir }}/{{ app_src }}:{{ app_dir }}"
    container_deps:
      - python3-apt
      - python3
      - python3-pip
  roles:
    - supervisor
    - container

- name: setup application container
  hosts: app
  vars:
    aim: configure
    container_workdir: "{{ app_dir }}"
    ruby_version: "{{ app_ruby_version }}"
    bundler_version: "{{ app_bundler_version }}"
    programs:
      app: "/root/.rbenv/shims/bundle exec puma -C config/puma.rb"
      worker: "/root/.rbenv/shims/bundle exec sidekiq"
  roles:
    - ruby
    - supervisor

И наша работа закончена.

Соберите все в один красивый плейбук.

# file: ansidock/site.yml
---
- ansible.builtin.import_playbook: network.yml
- ansible.builtin.import_playbook: dependencies.yml
- ansible.builtin.import_playbook: application.yml

и сопутствующий файл vars

# file: ansidock/group_vars/dockerhost.yml
---
postgres_image: postgres
postgres_version: 15-alpine
postgres_container_name: ansidock_db
postgres_ports:
  - 8765:5432
postgres_env:
  POSTGRES_PASSWORD: password
  POSTGRES_USER: postgres
  POSTGRES_DB: ansidock

opensearch_image: opensearchproject/opensearch
opensearch_version: latest
opensearch_container_name: ansidock_search
opensearch_env:
  discovery.type: single-node
  plugins.security.disabled: "true"

redis_image: redis
redis_version: alpine
redis_container_name: ansidock_redis

app_image: rails_supervisor
app_image_version: 2
app_container_name: ansidock_app
app_src: docker-rails
app_ports:
  - 7000:3000
app_env:
  DB_HOST: "{{ postgres_container_name }}"
  DB_USER: "{{ postgres_env.POSTGRES_USER }}"
  DB_PASSWORD: "{{ postgres_env.POSTGRES_PASSWORD }}"
  OPENSEARCH_HOST: "{{ opensearch_container_name }}"
  REDIS_SIDEKIQ_URL: "redis://{{ redis_container_name }}:6379/0"
  REDIS_CABLE_URL: "redis://{{ redis_container_name }}:6379/1"
  REDIS_CACHE_URL: "redis://{{ redis_container_name }}:6379/2"
  SECRET_KEY_BASE: some-super-secret-from-ansible-vault
  RAILS_MASTER_KEY: another-super-secret-from-ansible-vault
  APP_ADMIN_EMAIL: admin@example.org
  APP_ADMIN_PASSWORD: secret
  APP_EMAIL: reply@example.org
  PLAUSIBLE_SCRIPT: https://plausible.example.com/js/script.js

Давайте-ка также проведем тест-драйв нашей работы

ansible-playbook -i dev site.yml

Если все прошло успешно, docker container ls должен показать наши 4 контейнера, работающие в нормальном режиме. А при посещении localhost:7000 нас должно встретить приложение-образец, работающее во всем своем великолепии.

Вот мы и сделали это.

Заключение

Это упражнение помогло ответить на вопросы:

  • Могу ли я заменить vagrant + virtualBox на docker?

  • Если да, то как это сделать полегче?

Но это еще не конечная остановка. Все, что нужно, помещено в контейнеры, и теперь они являются подходящем таргетом для множества современных инструментов.

Для начала мы можем сделать снапшот приложения после его создания. Создав образ из запущенного контейнера, мы получим рабочую базу, которую можно использовать для обхода всего процесса создания Ansible, через который мы только что прошли.

И вооружившись этими новыми возможностями, мы приступим ко второму этапу — docker-compose.


Материал подготовлен в преддверии старта онлайн-курса «Расширенное администрирование РЕД ОС».

Комментарии (1)


  1. Tony-Sol
    19.04.2024 11:44

    Не понял зачем этот промежуточный шаг с ansible, кажется можно было сразу растащить показанные роли по dockerfile'ам, а получившиеся плейбуки - в docker-compose, разве нет?