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

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

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

Работа в консоли для меня — основной способ взаимодействия с виртуалками. Часто приходится решать многоплановые задачи, где нужно держать под рукой несколько вкладок с консолью. Каждая из вкладок может быть открыта на той же виртуалке, но в другой папке, с другими переменными окружения или с другой виртуалкой. Я пробовал разные способы — утилиты 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 октября онлайн и офлайн в Москве. Сможете послушать доклады про внутрянку облачных решений и в неформальной обстановке обсудить технологические тренды.


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

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


  1. DTPlayer
    16.10.2024 21:54

    Интересный опыт) Я немного зелен в плане контейнеров(трогал их, но не так чтобы сильно) и заметил как много они едят пространства на ПК. А у вас запущенно просто куча всего, как такое удалось?

    P.S:Самое тяжёлое что я разворачивал - django+nginx+postgres и эта махина ела +- 6GB ОЗУ(помню что около этого числа, давненько было)


    1. p2houd Автор
      16.10.2024 21:54

      У меня конфигурация с 16 ГБ оперативки, этого пока хватает на 23 контейнера. В большинстве из них запущен только процесс bash, пока я не запущу какую-то команду. Самое главное, что эти окружения всегда готовы к использованию. 

      Наиболее нагруженные – это 6 контейнеров с code-server и gitea, ещё недавно добавил firefox. Контейнер code-6 у меня раздувается до 2,5 ГБ, когда я открываю URL, там я активно работал и много брейкпоинтов и вкладок сделал в самой IDE. В неиспользуемом состоянии эти контейнеры занимают 100-400 МБ. Можно посмотреть подробный вывод docker stats ниже. В целом, половина памяти свободна при самом активном использовании.

      docker stats
      CONTAINER ID   NAME                CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
      e9eb11ef6f28   code-3              0.53%     403.2MiB / 15.62GiB   2.52%     1.84MB / 4.18MB   3.99MB / 1.09MB   72
      2f6e19c81496   az1-control-1     0.00%     2.234MiB / 15.62GiB   0.01%     588kB / 436kB     274kB / 0B        1
      1db5387b38b2   fox-1               0.00%     703MiB / 15.62GiB     4.39%     320MB / 115MB     678MB / 4.23GB    325
      4d7517ccc1db   db-ubnt-az1       0.00%     15.91MiB / 15.62GiB   0.10%     24.5MB / 495kB    59.2MB / 111MB    4
      232b1a218b60   deploy-ubnt-az1   0.00%     2.066MiB / 15.62GiB   0.01%     248kB / 122kB     36.9kB / 0B       1
      f339c3ed977e   deploy-ubnt-az2   0.00%     68.62MiB / 15.62GiB   0.43%     30.5MB / 1.15MB   170MB / 159MB     2
      bf3f850af068   code                0.45%     284.3MiB / 15.62GiB   1.78%     287MB / 72.4MB    1.54GB / 1.92GB   71
      8a1facaf4ed8   db-ubnt-az2       0.00%     13.05MiB / 15.62GiB   0.08%     21.1MB / 546kB    971kB / 108MB     4
      5c237c26fe93   clone-code          0.00%     21.07MiB / 15.62GiB   0.13%     2.19GB / 2.21GB   80.1MB / 265MB    1
      9d0bfcb14108   dev                 0.00%     12.84MiB / 15.62GiB   0.08%     5.81MB / 1.54MB   162MB / 135MB     1
      b0f5e82588e4   code-2              0.28%     117.5MiB / 15.62GiB   0.73%     918kB / 1.67MB    27.2MB / 3.76MB   47
      6767a5fe9888   add-users           0.00%     21.39MiB / 15.62GiB   0.13%     10.7MB / 37.5MB   10.4MB / 62.8MB   1
      52e1a2caa043   code-4              0.63%     755.7MiB / 15.62GiB   4.72%     14.8MB / 31.2MB   62MB / 19.1MB     89
      a69e15b715be   code-6              14.83%    2.341GiB / 15.62GiB   14.99%    342MB / 243MB     2.78GB / 897MB    130
      9108bf7dce87   code-5              1.75%     1.412GiB / 15.62GiB   9.04%     22.5MB / 53.2MB   160MB / 56.2MB    121
      05db4dca667b   deploy-sblx         0.00%     6.277MiB / 15.62GiB   0.04%     308kB / 0B        0B / 0B           6
      930ace2a8706   deploy-ubnt-pd1    0.00%     13.27MiB / 15.62GiB   0.08%     622MB / 613MB     47.1MB / 604MB    1
      854ca882de63   runner-1            0.17%     31.71MiB / 15.62GiB   0.20%     457MB / 1.12GB    83.9MB / 37.1MB   25
      5b03cc1ebceb   portainer           0.00%     43.3MiB / 15.62GiB    0.27%     170MB / 1.88GB    195MB / 1.02GB    20
      9d98de2734ae   squid               0.11%     178.9MiB / 15.62GiB   1.12%     266MB / 273MB     22.5MB / 1.83MB   8
      43c8414fcecb   coverage            0.01%     7.594MiB / 15.62GiB   0.05%     484kB / 667kB     1.04MB / 4.1kB    82
      04d5f368596c   pypi                0.01%     11.57MiB / 15.62GiB   0.07%     37.4MB / 26MB     3.39MB / 614kB    109
      5f8ed01385ea   git                 3.91%     146.3MiB / 15.62GiB   0.91%     1.24GB / 38.8GB   6.41GB / 57.4GB   27

      Остальную память, кстати, съедает пайплайн, когда запускаю тесты :)

      CONTAINER ID   NAME                                           CPU %      MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
      e84febe685f7   GITEA-ACTIONS-TASK-1579_WORKFLOW-tox_JOB-tox   1343.45%   4.26GiB / 15.62GiB    27.26%    579MB / 1.27MB    240MB / 1.85GB    299

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


      1. DTPlayer
        16.10.2024 21:54

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


  1. SlavikF
    16.10.2024 21:54

    Смешались в кучу кони, люди,

    И залпы тысячи орудий

    Слились в протяжный вой...


  1. Elaugaste
    16.10.2024 21:54

    Это все замечательно, но звучит как оверхед без ощутимых плюсов. Рабочую "среду" можно организовать в условном virtualbox чем и отделить мух от котлет. Внутри вм юзаем venv и радуемся)


    1. p2houd Автор
      16.10.2024 21:54

      Моя идея была в том, чтобы держать под рукой готовые окружения разного назначения, не только Python venv. В этом плане у меня получилось использовать единую среду в виде Docker и для приложений и для окружений. А ещё, управлять всем через веб-интерфейс. С VirtualBox я не могу представить похожего решения. Оверхед на запуск контейнеров наоборот меньше, если сравнивать запуск 23 виртуалок и 23 контейнеров.


      1. Elaugaste
        16.10.2024 21:54

        Боюсь мне не понять гениальность маневра. Чтобы использовать условный MySQL/openstack клиент вы возитесь с контейнерами, вместо того чтобы просто вызвать cli в терминале?

        Зачем запускать 23 виртуалки если все что нужно можно поднять в одной, из нее работать и при необходимости переносить ее образ диска (если цель маневра - переносимость)


        1. p2houd Автор
          16.10.2024 21:54

          Значит, для ваших задач мое решение избыточно.