Как тестировать контейнеры RoR с GitLab CI в контейнере

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

    С другой стороны, есть разрабатываемое Rails приложение, которое на реальной машине держит только код; Rails, gems, и все остальное покоится в Docker контейнере. Для своей работы оно использует Redis и Postgres, каждый находится в своем контейнере. Для каждого контейнера примонтирована директория, чтобы важные для приложения данные не оставались внутри.



    Задача в том, чтобы Gitlab CI нормально отработал. Вроде все просто, но — он сам находится в контейнере.

    Кстати, docker-compose.yml к rails приложению
    services:
      postgres:
        image: postgres:9.4.5
        volumes:
          - /var/db/test/postres:/var/lib/postgresql/data
        env_file: .env
        ports:
          - "54321:5432"
      redis:
        image: redis:latest
        volumes:
          - /var/db/test/redis:/data
      app:
        build: .
        env_file: .env
        volumes:
          - /var/www/test:/var/www/test
        ports:
          - "3000"
        links:
          - postgres
          - redis

    В .env файле лежит например пароль к базе данных, POSTGRES_PASSWORD. Здесь можно посмотреть, какие переменные используются в контейнере postgresql. Вместо ports можно использовать expose, но для мониторинга с хоста лучше открыть окно в мир.

    К Redis'у замечаний нет вообще — подключил, он сам сделал expose на 6379, и работает. Всем бы так.

    Первая мысль — инсталлировать Docker в Docker'е. Я верю в человечество, и в то, что в мире есть человек, который за разумное время сделал настоящую вложенность, с установкой Docker в контейнер, которая реально работает, но лично мне это не удалось. Значит, нужен второй вариант — тестировать извне: все контейнеры должны находиться рядом с контейнером GitLab, автоматически создаваться и уничтожаться системой CI изнутри контейнера.

    Для тестов нам не нужно монтировать внешние директории, все равно данные одноразовые. После тестов все контейнеры автоматически останавливаются.

    Шарим Docker


    Не рекомендуется давать недоверенным программам в контейнерах доступ к Docker'у на хосте. Имея таковой, можно легко запустить создание другого контейнера на хосте, который примонтирует нужную директорию или даже целый том (или devices, к примеру веб-камеру или принтер), установит нужный злоумышленнику код, и неспешно вытянет все ваши биткоины, и все это не покидая уютной песочницы. Но поскольку я тестирую свой код, предостережением можно пренебречь.

    Чтобы позволить контейнеру управлять своим отцом, нужно пробросить ему родительский сокет. В идеальном мире это делается так:

    docker run .. -v /var/run/docker.sock:/var/run/docker.sock

    но для тех, кто не ищет в жизни легких путей, оно не работает. Нужно во-первых добавить еще и

    -v /usr/bin/docker:/usr/bin/docker

    потом обнаружить, что это работает только если бинарники докера на хосте статические (не спрашивайте), иначе придется позаботиться о каждой библиотеке в этой директории отдельно. Если your host's docker binary is not static (не спрашивайте!), как у меня, значит работаем дальше.

    Смотрим список библиотек в /usr/bin/docker:

    ldd /usr/bin/docker

    linux-vdso.so.1 (0x00007fffb9ff4000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f711fe27000)
    libltdl.so.7 => /usr/lib/x86_64-linux-gnu/libltdl.so.7 (0x00007f711fc1d000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f711f871000)
    /lib64/ld-linux-x86-64.so.2 (0x000055a205b12000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f711f66d000)

    Ленивым
    Мне понадобилась только ibltdl.so.7 библиотека, все остальное в контейнере было, все таки Gitlab это вам не Alphine Linux. Проверить можно с помощью консоли контейнера, вызывая в ней docker, она вам скажет, какой библиотеки нет. Подключите нужную и пробуйте еще. Если вам не кажется, что такой путь проще, монтируйте все. ОН простит.

    Прописав в docker-compose.yml Gitlab'а (не проекта, не перепутайте) пути, которые кажутся интуитивными, и попробовав стартовать контейнер Gitlab:

    volumes:
        ...
        - /var/run/docker.sock:/var/run/docker.sock
        - /usr/bin/docker:/usr/bin/docker
        - /usr/bin/docker/libltdl.so.7:/usr/bin/docker/libltdl.so.7

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

    ERROR: for gitlab  Cannot start service gitlab: invalid header field value "oci runtime error: container_linux.go:247: starting container process caused \"process_linux.go:359: container init caused \\\"rootfs_linux.go:53: mounting \\\\\\\"/usr/bin/docker/libltdl.so.7\\\\\\\" to rootfs \\\\\\\"/var/lib/docker/devicemapper/mnt/2df228e042aed186eebbb484989e44cee3126c0a3bfb42d25c8998ada5afb9bd/rootfs\\\\\\\" at \\\\\\\"/usr/bin/docker/libltdl.so.7\\\\\\\" caused \\\\\\\"stat /usr/bin/docker/libltdl.so.7: not a directory\\\\\\\"\\\"\"\n"

    Решение «в лоб»:

    volumes:
        ...
        - /var/run/docker.sock:/var/run/docker.sock
        - /usr/bin/docker:/usr/bin/docker
        - /usr/lib/x86_64-linux-gnu/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7

    То есть нам нужны пути к файлам с правой части таблички с библиотеками.

    docker-compose.yml от Gitlab
    gitlab:
        image: 'gitlab/gitlab-ce:8.14.5-ce.0'
        restart: unless-stopped
        hostname: 'git.habrahabr.ru'
        environment:
            GITLAB_OMNIBUS_CONFIG: "external_url 'http://git.habrahabr.ru'"
        ports:
            - "3456:80" # если ваш контейнер не один претендует на 80-й порт.
            - "52022:22"
        volumes:
            - /docker/gitlab/data/conf:/etc/gitlab
            - /docker/gitlab/data/logs:/var/log/gitlab
            - /docker/gitlab/data/data:/var/opt/gitlab
            - /var/run/docker.sock:/var/run/docker.sock
            - /usr/bin/docker:/usr/bin/docker
            - /usr/lib/x86_64-linux-gnu/libltdl.so.7:/usr/lib/x86_64-linux-gnu/libltdl.so.7

    Создаем .gitlab-ci.yml


    image: "ruby:2.3"
    services:
      - redis:latest
      - postgres:9.4.5
    
    cache:
      paths:
        - vendor/ruby
    
    before_script:
      - apt-get update -q && apt-get install nodejs -yqq # стоит это откоментирровать сразу. Сохраните 5-10 минут жизни.
      - gem install bundler  --no-ri --no-rdoc
      - bundle install -j $(nproc) --path vendor
    
    rspec:
      stage: test
      script:
        - bundle exec rake db:create
        - bundle exec rake db:migrate
        - bundle exec rake db:seed
        - rspec spec

    Чтобы верно создать этот файл, лучше всего смотреть на docker-compose.yml приложения. К примеру, контейнер postgres — подключать volumes нам не нужно, env файл с паролем к базе данных не нужен (нам ведь все равно, какой пароль в базы, которая живет 10 секунд), порты наружу тоже. То есть остается только указать имя образа (image). Redis: volumes не нужны, так что только имя образа. Главный образ это ruby, в который Gitlab смонтирует код, и запустит before_script.

    Вместо эпилога


    На настроенный подобным образом GitLab можно навернуть что угодно, от Registry образов до Docker-in-docker решений. Например, вместо разворачивания кода в ruby контейнере средствами GitLab создать решение на основе gitlab/dind, в котором запустить docker build. Но это предмет совсем другой статьи.
    Поделиться публикацией

    Похожие публикации

    Комментарии 2

      +1

      У данной реализации есть два серьёзных недостатка:
      1) любой разработчик может обрушить всю CI инфраструктуру, добавив в .gitlab-ci.yml команду на удаление всех контейнеров.
      2) возможные трудности при параллельном запуске нескольких одинаковых пайпов например, возможен конфликт имён контейнеров и артефактов в расшаренных volumes.
      Вариант с docker-in-docker отлично работает, даже при условии запуска раннера и самого гитлаба в контейнерах.
      Могут быть трудности, например с пробросом перемётных среды и сертификатов для private registry в dind контейнер (gitlab-CI пока что этого не умеет), но они обходятся созданием кастомного образа dind с зашитыми сертификатами.

        0
        Спасибо за замечания.

        1. Точно так же ее можно обрушить, если GitLab стоит на хосте. Возможности защититься от этого одинаковые, независимо от того, где стоит GitLab.
        2. Да. Но снова же, эта проблема равным образом касается и работы CI вне контейнера. Возможно, вы немножко не так поняли смысл статьи. Я не рассказывал о том, как создать отдельные виртуальные машины GitLab с помощью Docker для каждого разработчика, с отдельным доступом, с уверенностью в изолированости, и т.д. Я даже написал предостережение о проблеме разшаривания Docker тем контейнерам, в которых вы не уверенные. Эта статья для тех, кто сам вынес GitLab в контейнер, и ему на своей реальной или виртуальной машине нужно тестировать проект в контейнерах с помощью CI в контейнере. Он сам себе злобный буратино, и решения ваших замечаний это тоже его забота.

        Я запускал Docker-in-docker в контейнере, и он отлично работал, но для простого проекта, в котором docker-compose.yml спокойно переносится в .gitlab-ci.yml, и не нуждается в более глубокой настройке, он избыточен. У меня была идея переписать на основе dind сам образ GitLab, таким образом он со старта будет готов работать в контейнере с другими контейнерами, но к проверке решения руки не дошли.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое