portals

Докер — клиент-серверное приложение до мозга костей. Утилита docker, с помощью которой выполняются манипуляции с контейнерами, разделами и сетями, — это клиент, общающийся с демоном dockerd через REST API.

Почему это важно?

Казалось бы, ну и что с того? И правда, пока мы работаем на локалхосте, разницы не видно (хотя однажды я офигел, когда в ответ на docker ps получил 404). Но общение двух приложений, расположенных на одном хосте, через REST API — это если и не исключение, то точно вырожденный случай. Как правило, такое общение происходит между удаленными приложениями.

“А что в случае Докера?”

Думаю, ответ напрашивается сам. Да, докер-клиент может общаться с удаленным докер-демоном. Более того, это касается не только конкретного бинарника docker, но и любой программы, использующей Docker API в своей работе, начиная от docker-compose и docker-machine, заканчивая любой самописной бирюлькой.

Как это работает?

По умолчанию возможность удаленного управления докер-демоном отключена, dockerd слушает только на UNIX-сокете /var/run/docker.sock.

Чтобы включить данную возможность, надо:

  • Запустить демон с опцией -H tcp://x.x.x.x, где x.x.x.x — адрес интерфейса, на котором демон будет принимать команды (0.0.0.0 для всех интерфейсов).

  • Запустить клиент с установленной переменной окружения DOCKER_HOST, указывающей на адрес и порт, на котором слушает демон.

DOCKER_HOST=tcp://1.2.3.4:2375 docker ps
DOCKER_HOST=tcp://1.2.3.4:2375 docker images
DOCKER_HOST=tcp://1.2.3.4:2375 docker run hello-world

или

export DOCKER_HOST=tcp://1.2.3.4:2375
docker ps
docker images
docker run hello-world
...
unset DOCKER_HOST

Что нам это дает?

Есть несколько пунктов, от банальных до неожиданно крутых.

Вместо SSH

Очевидная причина использовать удаленный вызов Docker API. Как не стоит запариваться с SSH для доступа внутрь контейнеров, так же и не стоит запариваться с созданием юзеров с правами на управление докер-демоном. Чем более тупой “крутилкой контейнеров” является докер-сервер, тем проще эти “крутилки” массово штамповать.

Continuous Integration

Продолжение предыдущего пункта, помноженное на автоматизацию, которую дает нам CI-пайплайн. Нет необходимости захламлять докер-сервер фиктивными пользователями вроде ci-runner или jenkins. Также нет необходимости туннелить докер-команды через SSH в CI-скриптах.

Docker Compose

Как я упоминал выше, Компоуз работает через Docker API, а значит тоже воспринимает волшебную переменную DOCKER_HOST. В чем отличие от самого первого пункта? В том, что отпадает необходимость хранить на докер-сервере docker-compose.yml.

Представьте себе ситуацию: у вас есть репозиторий с кодом проекта, в котором помимо кода лежат 3 заветных файла: Dockerfile, docker-compose.yml и docker-compose-prod.yml. Их назначение должно быть очевидно, но все-таки:

  • Dockerfile собирает образ с кодом.

  • docker-compose.yml используется разработчиками для создания рабочего окружения.

  • docker-compose-prod.yml используется для развертывания приложения на продакшене.

Представим себе рабочий процесс. Нарисовался первый релиз, кто-то из команды берет из репозитория docker-compose-prod.yml, закидывает его на сервер, делает docker-compose up -d. Ура, релиз задеплоен! Счастье… или нет?

Такой подход создает много вопросов:

  • В какое место складывать компоуз-файлы? Где их потом искать?

  • Что, если другой “деплойщик” зайдет на сервер и не найдет этот файл или не будет иметь прав на его чтение? Закинет заново? А первому “деплойщику” он об этом расскажет, или они так и будут деплоить из разных копий файла? Что, если со временем это станут не копии, а разные версии файла?

  • Кто будет помнить, что если в коммите обновился компоуз-файл, то его надо перезалить на продакшен?

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

export DOCKER_HOST=tcp://production-server:2375
docker-compose -f docker-compose-prod.yml up -d

То же самое может сделать и CI-пайплайн.

Безопасность

Если вы успели заметить, нигде не проскакивает никаких логинов и паролей. Докер демон не имеет пользовательской модели авторизации, что это значит?

  1. Где только можно использовать шифрование

  2. Смотреть голым задом (без шифрования) в интернеты (-H 0.0.0.0) — суицид

Докер-демон использует два порта: 2375 (HTTP) и 2376 (HTTPS). Будучи ориентированным на автоматизацию, Докер-демон для авторизации запросов вместо пользовательской модели использует TLS сертификаты. Гайд по настройке защищенного соединения больше всей этой заметки, да и право, я не настолько силен в информационной безопасности, чтобы пересказывать своими словами официальную документацию.

Спасение приходит со стороны Docker Machine, которая все делает за нас. Если раскатывать докер-сервер с ее помощью, то она сама выпустит и разложит нужные сертификаты по полочкам таким образом, что подключиться к этому серверу будет возможно только имея данные сертификаты.

Байки из жизни

Работа большинства команд docker-клиента в удаленном режиме вполне себе очевидна:

  • docker ps покажет список контейнеров на удаленном сервере

  • docker images покажет список образов на удаленном сервере

  • docker run запустит контейнер на удаленном сервере

  • docker inspect покажет подробную инфу о сущности на удаленном сервере

Никакой магии, все логично.

docker exec

“А чем он особенный?”

Давайте поиграем в игру на усложнение.

Начнем с простого: docker exec myapp_web_1 hostname. Результат ожидаем, получим хостнейм контейнера.

А вот нетривиальный случай: docker exec -i myapp_db_1 mysql < dump.sql. Да, произойдет именно то, что всплывет в голове прямо перед фразой “Да ну нафиг!”. Мы возьмем файл из текущего каталога. Скормим его в качестве дампа MySQL. На удаленном сервере. Внутри контейнера.

Тройное перенаправление, we-need-to-go-deeper.jpg.

Кстати, в обратную сторону mysqldump отрабатывает точно так же.

docker build

Тут все становится еще интереснее.

Свой сайт я держу на микро-инстансе в Амазоновском облаке. Естественно в докер-контейнере.

Как я первоначально его разворачивал на удаленном докер-сервере:

Мой docker-compose-prod.yml выглядел следующим образом:

version: '2'
services:

  web:
    restart: always
    build: .
    image: my.registry.com/zarbis-blog
    ports:
      - "80:80"
      - "443:443"

Раскатывал я его следующим образом:

  • задавал переменную DOCKER_HOST

  • собирал образ: docker-compose -f docker-compose-prod.yml build

  • заливал образ в свой Реестр: docker-compose -f docker-compose-prod.yml push

  • поднимал проект: docker-compose -f docker-compose-prod.yml up -d.

Обратите внимание, присутствует директива build. Я свято верил в то, что все происходит следующим образом:

  • я локально собираю образ

  • заливаю его в Реестр, доступный амазону

  • при удаленном запуске ... up -d демон подтягивает мой образ из Реестра

Из-за кучи нестыковок теории с реальностью я выяснил, что был в корне не прав, начиная с первого пункта.

Если вы когда-либо собирали докер-образ, то одна из первых строчек, которую вы видите в ходе сборки: Sending build context to Docker daemon с бегущим счетчиком мегабайтов. Это докер-демон загребает себе содержимое каталога, в котором находится Dockerfile, чтобы мочь выполнять инструкции COPY и ADD.

Pro Tip: файл .dockerignore рядом с Dockerfile позволяет исключить пересылку файлов, которые гарантированно не потребуются при сборке. Работает схоже с .gitignore, но синтаксис слегка другой.

Раньше я в упор не понимал, нафига жонглировать этими файлами. Вполне разумное замечание… в случае когда докер-клиент и докер-демон находятся на одном сервере ( ͡° ͜ʖ ͡°)

Думаю, вы меня поняли — сборка все это время происходила удаленно, сразу на продакшен-сервере. Заливка образа в приватный Реестр была абсолютно бесполезным шагом.

Теперь мой docker-compose-prod.yml выглядит следующим образом:

version: '2'
services:

  web:
    restart: always
    build: .
    image: zarbis-blog
    ports:
      - "80:80"
      - "443:443"

А раскатывается он одной командой, которую я уже упоминал выше:

export DOCKER_HOST=tcp://my-aws-instance:2376
docker-compose -f docker-compose-prod.yml up -d

Простота — залог здоровья.

А теперь на секунду вдумайтесь: как обычно происходит сборка какого-либо проекта из исходников? Устанавливается пачка зависимостей: компиляторы, библиотеки с суффиксом -dev, системы сборки, мененджеры пакетов. Настраиваются переменные окружения, где указываются всякие пути и т.д.

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

Сборка же докер-образа происходит в изолированной среде и может быть произведена одной командой после клонирования репозитория с кодом проекта. Из-за этой особенности сборка образа может происходить на удаленном сервере, который никак не подготавливался для того, чтобы быть билд-сервером.

Таким образом, удаленная сборка может происходить:

  • абсолютно прозрачно для пользователя

  • быстрее, чем на локалхосте (Амазоновские мощности же)

  • случайно

В такие моменты начинаешь думать, что будущее незаметно подкралось.