20 июня 2017

Использование Docker в разработке

Наш Java-разработчик Константин одинаково охотно изучает новые технологии и делится приобретенными знаниями. Одно из последних его открытий — Docker для разработки. Преимущества и недостатки использования этой технологии при разработке Костя описал в своем блоге, а мы теперь делимся с вами.

Многие признаются, что, начав использовать Docker, практически не могут потом от него отказаться. Я абсолютно с этим согласен. Применение Docker в разработке сократило количество ошибок конфигурации в нашем проекте, позволяя посвящать больше времени написанию кода, нежели поиску причин, по которым часть проекта не работает. Это очень эффективно, особенно если проект состоит из нескольких частей, над которыми работают разные разработчики, например, фронтенд и бэкенд. В таком случае нежелательно тратить время фронтенд-разработчика на поиск недостающего пакета или строчки конфигурационного файла (или любой другой проблемы) для того, чтобы работал бэкенд. Docker решает подобные проблемы. Более того, разработчику больше не нужно самостоятельно настраивать окружение проекта, так что время для знакомства с проектом также сокращается. Данный пост представляет собой небольшой совет: как начать использовать Docker в проекте.

ЧТО ОЖИДАЕТСЯ ОТ DOCKER

Стандартный способ знакомства с новым проектом — следующий:

  1. Подготовка окружения согласно требованиям (OS и настройки);
  2. Клонирование проекта;
  3. Установка пакетов, необходимых для разработки (например, nodejs, gradle, maven и т.д.);
  4. Подготовка проекта к запуску (копирование директорий, симлинков, настройка конфигураций, хостов и т.д.).

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

Способ, предполагающий использование Docker, несколько проще:

  1. Клонирование проекта;
  2. Скачивание или создание Docker-образа;
  3. Запуск контейнера на основе этого образа.

После этого шага контейнер функционирует как “виртуальная машина”, где все внутри уже приготовлено для разработки. Нужно всего лишь запустить скрипт, чтобы запустить проект. Более того, если кто-то случайно что-то сломал, контейнер легко может быть перезапущен из первоначального состояния.

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

ПРОСТОЙ ПРИМЕР

Простым примером может послужить этот блог. Он представляет собой проект на jekyll, который может быть запущен из корневой папки с помощью команды:

jekyll serve --host 0.0.0.0

Эта команда делает проект доступным на localhost:4000. Ctrl+C останавливает сервер. Для запуска команды мне нужны:

  • установленный Jekyll,
  • установленный плагин Jekyll для пагинации,
  • исходный код.

Чтобы настроить это окружение, я создаю Dockerfile в корне проекта.

# В качестве родительского изображения здесь я использую готовый ruby-образ. Он 
# позволяет не описывать установку ruby, который необходим для jekyll. На докер-хабе можно   
# найти множество готовых образов.
FROM ruby:2.4.1

# Установка необходимых пакетов
RUN gem install jekyll:3.4.3 bundler:1.14.6 \
    && gem install jekyll-paginate:1.1.0

# Делаю порт 4000 доступным извне контейнера
EXPOSE 4000

Каждая команда в этом файле создает Docker-слой. Хорошая практика — уменьшение количества слоёв, то есть команд в Dockerfile, вот почему команда RUN совершает сразу три действия. Также лучше фиксировать версии везде, где можно, чтобы избежать появления проблем впоследствии из-за обновления пакетов.

Теперь с помощью этого файла можно создать образ:

#private/myblog - это имя образа
docker build -t private/myblog .

Вместо этого шага образ может быть загружен из Docker registry. Конечно, для этого нужно, чтобы кто-то ранее создал и запушил его туда.

После этого можно запустить контейнер с использованием образа:

# укажите настоящий путь к папке вместо [clone-dir]
docker run -i -t -d --name "myblog" -v [clone-dir]:/root/src -p 4000:4000 private/myblog /bin/bash

Эта команда создаёт контейнер со следующими параметрами:

  • myblog — имя контейнера;
  • [clone-dir] установлен в /root/src файловой системы контейнера. Это будет что-то вроде расшаренной папки на виртуальной машине, так что файлы на хосте и в контейнере всегда будут идентичными;
  • порт 4000 контейнера будет соответствовать порту 4000 хоста. Таким образом localhost:4000 будет перенаправлять на порт 4000 контейнера;
  • private/myblog — это имя образа;
  • /bin/bash — команда, которая будет выполняться в контейнере.

Последний пункт довольно важен. По завершении этой команды контейнер будет остановлен. /bin/bash не останавливается никогда, гарантируя, что мой контейнер будет работать, пока я сам не остановлю его. Команда /bin/bash не делает ничего полезного, просто отдает консоль, поэтому после мне нужно запустить сервер самостоятельно:

# получение консоли контейнера
docker exec -it myblog /bin/bash

# действия, которые происходят в контейнере
cd ~/src
jekyll serve --host 0.0.0.0

После этого сервер будет доступен на localhost:4000 на хосте.

Мне нравится этот подход, я считаю, что разработчик должен контролировать запуск сервера, перезапускать его при необходимости, конфигурировать и так далее. Но если я собираюсь передать этот контейнер кому-то ещё (например, другому разработчику, которому нужен мой запущенный сервер, но не в продакшн), я использую ‘docker run’ с командой, которая запускает сервер:

# укажите настоящий путь к папке вместо [clone-dir]
docker run -i -t -d --name "myblog" -v [clone-dir]:/root/src -p 4000:4000 private/myblog somescript.bash
#Предполагается, что somescript.bash содержит команды, запускающие сервер

УПРОЩАЕМ

Мне не нравится писать команду запуска. Она очень длинная, имеет множество опций, и мне приходится запоминать порты, папки и имена. Совсем сложно все становится, когда контейнеров нужно запустить несколько, организовав между ними сеть. Проект Docker compose помогает упростить процесс.

Файл docker-compose.yml (в корне) имеет следующую структуру:

version: '2'
services:
  myblog:
    image: private/myblog
    ports:
     - "4000:4000"
    volumes:
     - ./:/root/src
    tty: true
#    command: bash somescript.bash (use this line instead of next line if you want server autostart)
    command: /bin/bash

Команда ниже идентична команде ‘docker run’, описанной выше.

docker-compose up

Довольно удобно для пользователя, не так ли?

НЕСКОЛЬКО КОНТЕЙНЕРОВ ДЛЯ РАЗРАБОТКИ

Представим, что проект состоит из фронтенда и бэкенда. Фронтенд-разработчики хотят, чтобы сервер работал, но не желают ничего знать о нём. Более того, они не готовы даже его запускать. Бэкенд-разработчики тоже не горят желанием разбираться во фронтенде. Некоторые вообще не хотят заниматься ни фронтендом, ни бэкендом, все должно запускаться само. А кто-то, наоборот, хочет контролировать все части приложения.

Идея создания окружения в следующем: фронтенд запускается на порту 4200. Бэкенд использует порт 8080. Для передачи API-запросов между фронтендом и бэкендом используется прокси (4200->8080). Я хочу поместить фронтенд и бэкенд в отдельные контейнеры (в этом примере бэкенд запускается на jetty, а клиентское приложение — это angular2 приложение):

Каждый контейнер имеет volume, который содержит исходный код. Контейнер для бэкэнда отвечает за серверное веб-приложение, которое доступно на порту 8080 извне. Контейнер для фронтенда отвечает за клиентское приложение, например, на angular2, он имеет открытый порт 4200 для доступа к приложению с хоста. В нем же содержится прокси для перенаправления API запросов. При такой конфигурации на хосте порты 8080 и 4200 эквиваленты для запросов API.

Я использую следующую структуру файлов:

project
    Dockerfile
    docker-compose.yml
    frontend/
        Dockerfile
        proxy-config.json
        ... some frontend sources
    ... some backend sources

frontend — это каталог в корне на проекта, proxy-config.json — файл, описывающий прокси. Это особый файл angular cli, можно использовать и другую сконфигурированную прокси.

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

version: '2'
services:
  frontend-service:
    build: frontend
    ports:
     - "4200:4200"
    volumes:
     - ./frontend:/root/src
    tty: true
#  Раскомментируйте следующую строку для автоматического запуска фронтенда
#    command: bash /root/src/frontend-start.bash
  backend-service:
    build: .
    ports:
     - "8080:8080"
     - "8000:8000"
# port 8000 используется для удалённого дебаггинга
    volumes:
     - ./:/root/src
    tty: true
#  Раскомментируйте следующую строку для автоматического запуска бэкенда
#    command: bash /root/src/backend-start.bash

При использовании docker compose вы автоматически можете использовать внутреннюю сеть Docker для доступа к другим контейнерам. Ниже пример конфигурации прокси, где я использую название сервиса на бэкенде, объявленное в файле docker-compose.yml.

 "/api": {
    "target": "http://backend-service:8080",
    "secure": false
  }
}

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

ВЫВОДЫ

Если вы не используете Docker при разработке, то эти причины должны убедить вас начать:

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

Что же касается недостатков, я нашёл следующие:

  • возможен только удалённый дебаггинг. Это довольно удобно для java-проектов с использованием Idea, но при использовании других стеков инструментов может повлечь за собой проблемы;
  • Запуск графических приложений. Если процесс разработки требует запуск UI-приложений (например, e2e тесты в chrome), сделать это будет непросто;
  • профилирование. Например, JVM в контейнере нельзя увидеть с хоста. К счастью, многие профайлеры имеют возможность запустить своего агента в контейнере, подключиться к нему через сеть и профайлить как обычно.

Другими словами, при использовании Docker всё становится удалённым — и это надо иметь в виду.

Для меня Docker был шагом к изучению создания кластеров и микросервисов: ещё одно модное и перспективное на сегодняшний день направление. А недавно Docker включил в себя проект Docker swarm, который позволяет с легкостью разворачивать кластер на нескольких машинах и управлять им.

Оригинал: https://kosbr.github.io/2017/05/14/docker.html

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Читайте в нашем блоге

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: