Зачем я использую контейнеры как виртуалки: опыт python-разработчика

Привет, я Денис, python-разработчик в Cloud.ru. Последние три года я работаю с продуктами на базе компонентов OpenStack — для этого нужны разнообразные навыки и знания способов администрирования и разработки в среде Linux. За это время я убедился — мне не хватает уже существующих способов отладки, доставки кода, подключения к prod- и dev-стендам. Поэтому решил придумать свой.

Сейчас никого не удивишь контейнерами — они есть у большинства провайдеров, многие используют их для размещения приложений. Я же хотел попробовать другое применение, и, кажется, у меня все получилось. Теперь я ежедневно работаю в контейнерах — почти как в виртуалках. В статье расскажу, зачем это делаю, поделюсь своим подходом и стеком технологий, а также покажу, как настроить такое же решение у себя.

f_4hpmctemntrrexr4lhox3p8pg.png

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

Работа в консоли для меня — основной способ взаимодействия с виртуалками. Часто приходится решать многоплановые задачи, где нужно держать под рукой несколько вкладок с консолью. Каждая из вкладок может быть открыта на той же виртуалке, но в другой папке, с другими переменными окружения или с другой виртуалкой. Я пробовал разные способы — утилиты tmux, screen, SSH-клиентов, Jupyter, но всего этого мне не хватало.

Почему screen, tmux, iTerm2, SecureCRT, Jupyter и MobaXterm мне не подошли

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

А еще так не хочется каждый раз заново искать инструменты, заходить в папки, активировать переменные окружения… Хочется переиспользовать скрипты и применять их сразу на нескольких стендах. Поэтому я стал искать инструмент, который покрыл бы все аспекты многоплановой работы и помог поддерживать порядок.

Начнем с утилит screen и tmux. По функционалу они для меня почти не отличаются — помогают сегментировать работу через создание сессий и использовать разные переменные окружения в разных сессиях. Также помогают продолжить работу после разрыва соединения — очень нужная опция. Но оба инструмента не задержались в моем «райдере»:

  • я обнаружил конфликты с горячими клавишами (например, Ctrl+A для перевода в начало строки);

  • оказалось, довольно просто случайно выйти из окружения так, что оно безвозвратно исчезает — и это совершенно ненадежно.

SSH-клиенты iTerm2, MobaXterm, XShell, SecureCRT тоже мне не подошли. Программы имеют схожий функционал — помогают упорядочить сессии и задать стартовые команды. Но все окружения работают до первой перезагрузки программы, поэтому опять-таки не надежны. Кроме того, большинство из них не являются кроссплатформенными, а это еще один ограничивающий фактор — работать вне зависимости от условий и набора ПО уже не получится.

Было время, после продвинутых курсов по Python я начал создавать свои окружения прямо в Jupyter-блокнотах. Мне понравилась переносимость окружений, поскольку в блокноте сохраняется результат вывода команд, а также заготовки команд находятся всегда под рукой — так я выполнял и bash-скрипты, и ansible-плейбуки. Но позже я перестал использовать этот способ. При больших объемах скриптов и разнообразной работе управлять блокнотами становится сложно.

Так я понял, что подходящих моим запросам инструментов нет, поэтому пришлось создавать комплексное окружение самостоятельно. Итак, мое решение, чтобы разделить рабочее окружение — использовать контейнеры для всего в работе. С контейнеризацией я познакомился давно, еще до начала активной работы в Linux. Уже тогда я понимал, как работает проброс SSH-портов, но только в рамках администрирования, а не в качестве рабочего окружения. Теперь я работаю в контейнерах ежедневно.

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

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

Давайте исправим эту ситуацию

Давайте исправим эту ситуацию

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

Как создать такие же контейнеры, как у меня

Расскажу про свой стек работы с контейнерами:

  1. Docker и кастомные сети с доменными именами. Можно было бы использовать Podman, но у него хуже совместимость с Portainer.

  2. Squid в контейнере — благодаря нему можно заходить на сервисы в контейнерах через веб-браузер и по доменному имени.

  3. SSH-агент в контейнере, проброшенный с помощью команд и переменных окружения.

  4. Portainer — позволяет легко редактировать биндинги, волюмы, переменные окружения, работать в консоли контейнеров.

  5. FoxyProxy для работы через веб-браузер — помогает в настройке URL-паттернов через прокси-сервер Squid.

Все скрипты, код и инструменты я разместил в домашней директории моего пользователя виртуалки. Свою домашнюю директорию я прокидываю почти в каждый контейнер, который запускаю, но не полностью — только те файлы или папки, которые нужны для конкретной среды.

А вот такие среды я использую, и каждую — с конкретной целью:

  • работа с CLI OpenStack — под каждый стенд я создаю отдельный контейнер. Контейнеры отличаются переменными окружения и набором Python-пакетов;

  • работа с git. В этой среде у меня настроены git-hooks для работы с репозиториями и алгоритмом управляемого добавления пайплайна: в мой локальный репозиторий — для тестов и в репозиторий компании — для работы с итоговым решением.

  • разработка в code-server (Microsoft Visual Studio Code) — таких сред я использую несколько, чтобы параллельно отлаживать распределенные сервисы OpenStack;

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

  • работа с базой данных — иногда я шучу, что могу администрировать весь OpenStack только лишь запросами в БД;

  • работа с RPC-вызовами — мое недавнее увлечение. Я запускаю контейнер с кодом OpenStack и минимальным конфигом, консольный Python 3 и отправляю RPC-вызовы напрямую в сервисы, минуя API;

  • развертывание SSH-ключей — чтобы в любой момент можно было открыть доступ на стенд себе или кому-то другому;

  • Gitea и Gitea Act Runner — для своих пайплайнов и тестирования;

  • httpd — для сбора PyPi-артефактов;

  • httpd — для сбора отчетов tox coverage.

Расскажу подробнее про основные компоненты моего решения — как они устроены и как их воспроизвести.

Docker-сети с доменным именем

Удобно, что в Docker-сетях есть резолвер на хосте 127.0.0.11. Контейнеры могут обращаться друг к другу по имени или полному доменному имени, которое содержит имя сети. По умолчанию имя сети — docker, поэтому к контейнерам можно обращаться по FQDN. Например, у контейнера «container1» в сети docker будет имя «container1.docker».

Поскольку я планировал использовать веб-браузер для доступа к развернутым средам, то выбрал реалистичное доменное имя. Для этого вот так просто я создал сеть tbox.tech:

docker network create tbox.tech

Благодаря этому у всех контейнеров теперь корректные (с точки зрения корневых доменов) имена.

Squid для резолвинга и веб-доступа

Для доступа к контейнерам через веб-браузер использовал прямой прокси. Раньше пользовался SSH-туннелированием, но вариант с прокси позволяет лучше управлять политикой доступа — не нужно открывать SSH-туннель ко всей виртуалке. Прямой HTTPS-прокси я защитил самоподписанным TLS-сертификатом, который выпустил заранее. Поскольку у моей виртуалки нет доменного имени, я выпустил TLS-сертификат на IP-адрес:

squid/openssl.conf

[ req ]
distinguished_name  = req_distinguished_name
x509_extensions     = v3_req
prompt              = no
[ req_distinguished_name ]
CN                  = 192.168.109.136
[ v3_req ]
subjectAltName      = @alt_names
[ alt_names ]
IP.1                = 192.168.109.136

Команда для выпуска самоподписанного TLS-сертификата для Squid:

openssl req -x509 -newkey rsa:4096 -keyout squid/squid.key -out squid/squid.crt -sha256 -days 3650 -nodes -config squid/openssl.conf

Для настройки Squid использовал минимальный конфиг. Указал в нем HTTPS-порт, TLS-сертификат, резолвер Docker, разрешенные порты целевых подключений и сети-источники, с которых я захожу на виртуалку:

squid/squid.conf

https_port 443 tls-cert=/etc/squid/squid.crt tls-key=/etc/squid/squid.key tls-cafile=/etc/squid/squid.crt
dns_nameservers 127.0.0.11
acl safe_ports port 80
acl safe_ports port 443
acl arm src 172.16.1.0/24
acl arm src 172.16.2.0/24
http_access allow arm
http_access deny !safe_ports

Для запуска в такой конфигурации нужен контейнер Squid с библиотеками OpenSSL. Я не нашел готовый контейнер, поэтому написал Dockerfile и собрал свой образ:

squid/Dockerfile

FROM ubuntu/squid:6.6-24.04_edge
RUN apt update -y
RUN apt install squid-openssl -y

Сборка образа:

docker build -t ubuntu/squid:6.6-24.04_edge-openssl .

После этого я запускаю контейнер командой, указывая путь до конфигурации squid, которая расположена в моем домашнем каталоге, открываю порт 443, а также указываю созданную сеть tbox.tech:

docker run -d --restart=unless-stopped --name squid -v ./squid/squid.conf:/etc/squid/squid.conf -p 443:443 --network tbox.tech ubuntu/squid:6.6-24.04_edge-openssl

SSH-агент в контейнере

Если подключаться по SSH со включенным форвардингом SSH-агента, то в сессии пользователя будет переменная SSH_AUTH_SOCK. Обычно она указывает на сокет в директории /tmp и меняется при каждом переподключении. Чтобы контейнеры получили доступ к этому агенту, можно назначить постоянный путь. Для этого есть два способа — оба с socat.

Первый способ — прокинуть на хосте UNIX-сокет с агентом к TCP-сокету, а в контейнере прокинуть этот TCP-сокет к новому UNIX-сокету, который имеет постоянный путь.

На хосте:

socat TCP-LISTEN:2222,bind=192.168.254.1,fork UNIX-CLIENT:${SSH_AUTH_SOCK}

В контейнере:

socat UNIX-LISTEN:/tmp/sshagent/myssh.sock,unlink-early,mode=777,fork TCP:192.168.254.1:2222

Важно, чтобы IP-адрес был доступен из контейнера. Поэтому лучше выбрать один из Docker-интерфейсов или создать отдельный dummy-интерфейс. В примере я выбрал адрес 192.168.254.1 dummy-интерфейса виртуалки. Недостаток способа в том, что внутри контейнера приходится выполнять socat.

Второй способ — прокинуть на хосте UNIX-сокет к другому UNIX-сокету:

socat UNIX-LISTEN:/tmp/sshagent/myssh.sock,unlink-early,fork UNIX-CLIENT:${SSH_AUTH_SOCK}

Этот вариант понравился мне больше — в контейнер прокидывается не сам сокет, а папка, в которой он находится. При переподключении socat будет удалять старый сокет, поэтому контейнер должен отслеживать изменения файлов внутри папки /tmp/sshagent.

Теперь в контейнере используем переменные окружения для подключения по SSH с использованием текущей сессии пользователя:

Переменная

Значение

SSH_AUTH_SOCK

/tmp/sshagent/myssh.sock

Кстати, после этих шагов стало проще запустить SSH-сервер в контейнере: теперь можно добавить строку запуска в стартовую команду, прокинуть authorized_keys и заходить в контейнер как на виртуалку.

Portainer для контейнеров

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

В Portainer по умолчанию нестандартные веб-порты. Чтобы использовать 80 и 443, я запускаю контейнер с кастомными командами:

docker run -d --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v ./portainer:/data --network tbox.tech portainer/portainer-ce:2.20.3 --bind 0.0.0.0:80 --bind-https 0.0.0.0:443

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

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

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

А так я работаю в консоли контейнеров — прямо через веб-браузер

А так я работаю в консоли контейнеров — прямо через веб-браузер

Еще примеры

Запуск плейбука с клонированием репозитория

Запуск плейбука с клонированием репозитория

Работа с базой данных

Работа с базой данных

Работа с git

Работа с git

При создании контейнеров для рабочих сред обычно я прописываю такие параметры:

Параметр

Значение

Working Directory

Директория, в которую я хочу попадать, заходя в контейнер

User

Например, 1038:1038 — идентификатор моего пользователя и группы, что позволяет использовать одинаковые права на хосте и внутри контейнера

Console

Interactive & TTY

Volumes

/etc/shadow:/etc/shadow: ro
/etc/group:/etc/group: ro
/etc/passwd:/etc/passwd: ro
/etc/sudoers:/etc/sudoers: ro
/etc/sudoers.d:/etc/sudoers.d: ro
/etc/localtime:/etc/localtime: ro
/tmp/sshagent:/tmp/sshagent: ro
/etc/hosts:/etc/hosts: ro
/home/myname/.bashrc:/home/myname/.bashrc: ro
/home/myname/folder1:/home/myname/folder1
/home/myname/code:/home/myname/code

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

FoxyProxy для работы через веб-браузер

Веб-доступ к контейнерам, в которых развернуты веб-приложения Visual Studio Code или Gitea, обеспечивает прямой HTTPS-прокси Squid, о настройке которого рассказывал выше. Для доступа в веб-браузер через прокси я использую плагин FoxyProxy. Добавляю прокси в настройках плагина — указываю его IP-адрес и порт. В настройках прокси задаю всего два URL-паттерна — они будут вести к URL с доменом tbox.tech по протоколам HTTP/HTTPS и WebSocket/WebSocketSecure через мой прямой прокси. После заполнения паттернов в иконке плагина станет доступен режим «Прокси из шаблона»:

Действие

Тип прокси

Шаблон

Включить

Reg Exp

https?:\/\/.*((tbox\.tech)).*

Включить

Reg Exp

wss?:\/\/.*((tbox\.tech)).*

Выводы или продолжение следует…

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

Как новый подход повлиял на мою работу:

  • Решение помогло по максимуму раскрыть возможности работы с OpenStack.

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

  • Я преодолел ограничение на единственную среду разработки.

  • Теперь можно одновременно задействовать сразу несколько навыков работы — с SSH, прокси и TLS-сертификатами.

Что я пока не продумал в получившемся рабочем окружении, так это переносимость: сейчас не очевидно, как забэкапить и перенести развернутые в Portainer контейнеры с переменными окружения. Но я и раньше переносил и бэкапил у своей виртуалки весь диск целиком — к файловому бэкапу я все еще не готов. Так что, на мой взгляд, это единственный недостаток моего решения. А еще хочется попробовать Kubernetes — в нем проще управлять конфигами контейнеров и Portainer с ним совместим.

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

А пока делитесь в комментариях, какие любимые инструменты у вас? Буду рад узнать про ваши лайфхаки и решения. И приходите на конференцию GoCloud Tech, которая пройдет 24 октября онлайн и офлайн в Москве. Сможете послушать доклады про внутрянку облачных решений и в неформальной обстановке обсудить технологические тренды.

Что еще почитать в блоге:

© Habrahabr.ru