Русский
Русский
English
Статистика
Реклама

Docker

Перевод Docker vs Kubernetes

14.05.2021 20:06:19 | Автор: admin


Сегодня мы знакомимся с Kubernetes и Docker и разберемся, какую технологию лучше применять в каждом конкретном случае и стоит ли использовать обе одновременно? Часто разработчики (особенно начинающие) оказываются перед необходимостью выбора приложения для контейнеризации Kubernetes или Docker. Давайте разбираться, для каких целей каждая из этих технологий подходит лучше всего.

Для этого сначала дадим определение термину контейнер в контексте Kubernetes (K8) и Docker. Это позволит понять основы обеих технологии, прежде чем мы углубимся в каждую из них.

Что такое контейнер


Допустим, вы хотите установить приложение в идеальной для максимальной производительности среде. Обычно параметры такой среды зависят от серверных стоек, сетевых переменных и других технических характеристик внешней инфраструктуры. Это означает, что в 100% случаев вы не достигнете максимальной производительности только если не создадите контейнер, чтобы абстрагировать приложение от его физического местоположения.
Представьте себе песочницу или виртуальную машину с указанными переменными (тип ОС, Compute и т. д.). Допустим, вам также нужно было установить другое приложение, контейнер на том же оборудовании, но с другой ОС и другими переменными, и это создало изолированную среду, идеально подходящую для тестирования и внедрения данного приложения.
Эти контейнеризированные приложения функционируют таким образом, как будто они находятся на разных компьютерах и даже в разных местах. Ключевое преимущество использования контейнеров заключается в том, что мы можем реплицировать их среды на любом устройстве, таким образом устраняя проблемы несоответствия, существовавшие в доконтейнерную эру разработки ПО.

Что представляет собой Kubernetes?


Kubernetes это инструмент, разработанный Google в 2014 году для оркестровки задач, связанных с контейнерами и платформами контейнеризации. Это система с открытым исходным кодом, которая может управлять несколькими контейнерами, расширяя свои возможности (см. функции ниже) для поддержания бесперебойной работы и доступности контейнеризованных приложений.

Ключевые функции Kubernetes


Одними из особенностей Kubernetes являются:
  1. Поддержание среды с заданными параметрами для разработки, тестирования и внедрения
  2. Предсказуемая и автоматически масштабируемая (горизонтально) инфраструктура
  3. Самовосстанавливающаяся (с возможностью отмены) среда с балансировкой нагрузки
  4. Широкие возможности для установки приложений
  5. Инструменты управления на уровне приложений

Это пять основных характерных особенностей, для которых разработчики создали Google Kubernetes Engine.

Что представляет собой Docker?


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

Ключевые функции Docker


Вот краткий список особенностей Docker:
  1. Совместное использование образов среды с помощью Docker Build
  2. Docker Assemble для распознавания языка программирования и речи при создании контейнеров
  3. Нативные и облачные инструменты для оптимизации производительности разработчиков
  4. Инструменты CI/CD для команд, работающих над развивающимися приложениями с системой контроля версий
  5. Высокая отказоустойчивость с надежной поддержкой больших кластеров


Docker или Kubernetes. Нужно ли выбирать между ними?




Не всегда.
Сам по себе Kubernetes не может запустить контейнер, когда вы начинаете свой проект. Для этого лучше использовать Docker (или его альтернативу). Но мы рекомендуем использовать их в синергии.
Docker создает контейнеры и управляет ими..., а затем Kubernetes управляет Docker.
В случае, если ваше приложение простое, Docker может также предоставить всю необходимую инфраструктуру для поддержания его работоспособности. По мере роста приложения, когда ему может потребоваться несколько кластеров и более сложное обслуживание, нужно использовать Kubernetes.
Вы стоите перед необходимостью выбора только в том случае, когда ваше приложение масштабируется.

Плюсы и минусы Docker: контейнеризация


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

Плюсы:


  • Легкость создания. Инициализация контейнеров в Docker выполняется быстро и требует минимальных технических навыков.
  • Инструменты Docker. Управлять контейнерами легко благодаря полному набору стартовых инструментов
  • Эффективная поддержка. У Docker есть активное сообщество разработчиков, которые предоставляют техническую поддержку и помогают устранить любые проблемы, с которыми вы можете столкнуться.


Минусы:


  • Отсутствие хранилища. При каждой перезагрузке контейнер теряет ранее созданные вами данные. Сохранение не настроено по умолчанию, и для начинающих разработчиков это может создавать сложности.
  • Низкая скорость. Контейнеры Docker никогда не достигнут уровня производительности выделенных серверов из-за сетевого уровня, на котором они теряют скорость. Нечто неслыханное при использовании инфраструктуры выделенных серверов для хостинга приложений.
  • Отсутствие кроссплатформенной поддержки. Изолированный характер контейнеров Docker делает невозможным установку приложений в средах, отличных от среды, в которой они были разработаны.


Плюсы и минусы Kubernetes: оркестровка контейнеров


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

Плюсы:


  • Модули pod (поды). К8 поддерживает поды (контейнеры и инструменты контейнеризации) для сохранения с автовосстановлением (воссозданием) в случае неожиданного сбоя.
  • Разработка Google. Kubernetes вселяет уверенность (не всем, конечно) в своем качестве за счет известности разработчика и растущего (самого большого) сообщества.
  • Наличие хранилища по умолчанию. Для удобства разработчиков K8 поставляется с облачными хранилищами и хранилищами SAN.


Минусы:


  • Сложная установка. Требует значительных технических усилий, а для правильной установки и настройки нужно много времени.
  • Overkill простым приложениям не нужна сложность Kubernetes. Но кто из ваших разработчиков признается, что ваше приложение простое?
  • Технические возможности К8 обходятся недешево. Услуги разработчиков DevOps, способных создавать и поддерживать инструменты Kubernetes, стоят дорого.


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

Примеры использования Docker и Kubernetes





Хотя этот пост посвящен Docker и Kubernetes по отдельности, нужно признать, что они не существуют изолированно. На успех претендуют и другие конкурирующие инструменты оркестровки и контейнеризации.
Однако для некоторых ситуаций идеально подходят именно K8 и Docker. В нескольких случаях нет необходимости совместного использования этих двух технологий. Рассмотрим их ниже.

Когда нужно использовать Kubernetes


Если масштаб вашего приложения значительно вырос, возможно, вам пора переходить на К8:
  • Почти идеальное время безотказной работы. Функция самовосстановления Kubernetes позволяет ресурсоемким приложениям продолжать работу независимо от количества сбоев в системе.
  • При выборе между различными поставщиками услуг контейнеризации. Так как К8 сотрудничает (на разных уровнях сложности) почти со всеми поставщиками, использование К8 в качестве системы оркестровки дарит свободу выбора. Ни один поставщик не может претендовать на контракт с вашей компанией, если вы не будете довольны качеством услуг после пробного периода.
  • Если вы не уверены в потенциале роста. Во время горизонтального масштабирования К8 автоматически распределяет ресурсы по приложениям.


Когда нужно использовать Docker


В некоторых случаях для хостинга приложений лучше использовать Docker и его инструменты. Рассмотрим некоторые из них.
  • Если К8 не подходит. Недостаточные технические возможности, несовместимость с API и высокая стоимость услуг могут привести к необходимости использования Docker и его инструментов. Платформа оркестровки Docker Swarm может полностью заменить К8.
  • Когда вы только начинаете развитие приложения. Вам не нужно использовать Docker совместно с какой-либо другой системой оркестровки, когда приложения еще находятся в стадии роста. На данных этапах скорость важнее устойчивости.
  • При создании приложений CLI. При разработке Docker была предусмотрена его интеграция с приложениями CLI, благодаря эффективности которых повышается производительность.


Когда необходимо совместное использование


При совместном использовании Kubernetes и Docker дополняют друг друга. Во-первых, стоит отметить медленную пропускную способность, на которую мы жаловались при внедрении Kubernetes и проверке работоспособности контейнеров.
При наличии достаточного бюджета и технических возможностей для поддержки приложений, эти инструменты отлично работают. Вы не столкнетесь с проблемой простоя приложения, поскольку вам поможет сообщество.
Также нужно признать, что в каждом из данных инструментов остались пробелы и недостатки, поэтому они лучше функционируют вместе. Kompose от K8 это адаптация Docker Compose. Это означает, что использование обоих инструментов было и остается стандартом.
Таким образом, результат данного противостояния дружеская ничья. Случаи применения полностью зависят от ваших предпочтений. Однако лучше не использовать только Kubernetes.
Подробнее..

Перевод Dockle Диагностика безопасности контейнеров

07.06.2021 20:11:26 | Автор: admin

В этой статье мы рассмотрим Dockle инструмент для проверки безопасности образов контейнеров, который можно использовать для поиска уязвимостей. Кроме того, с его помощью можно выполнять проверку на соответствие Best Practice, чтобы убедиться, что образ действительно создаётся на основе сохраненной истории команд.

Установка Dockle

Трудностей при установке утилиты возникнуть не должно:

  • Установка в OSX

$ brew install goodwithtech/r/dockle
  • Установка в Linux

# RHEL$ VERSION=$( curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \ grep '"tag_name":' | \ sed -E 's/.*"v([^"]+)".*/\1/' \) && rpm -ivh https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.rpm#Ubuntu$ VERSION=$( curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \ grep '"tag_name":' | \ sed -E 's/.*"v([^"]+)".*/\1/' \) && curl -L -o dockle.deb https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.deb$ sudo dpkg -i dockle.deb && rm dockle.deb

После установки утилиты всё, что вам нужно сделать, это указать имя образа. Больше ничего настраивать не нужно, вам даже не потребуется Docker.

Пример использования Dockle

Запускаем утилиту, указав имя образа. Если проблем нет, в консоль будет выведено сообщение PASS, а при обнаружении проблем или уязвимостей подробная информация о них:

Попробуем запустить Dockle в Docker, на скриншоте видно, что утилита отлично работает:

Основные функции и преимущества Dockle

  • поиск уязвимостей в образах,

  • помощь в создании правильного Dockerfile,

  • простота в использовании, нужно указать только имя изображения,

  • поддержка CIS Benchmarks.

Сравнение с другими инструментами

Существует большое количество похожих инструментов для диагностики безопасности, например: Docker Bench или Hadolint. Но в сравнении с ними Dockle более функциональна:

Применение Dockle в DevSecOps

Думаю, что эта небольшая статья хорошая отправная точка для начала работы с Dockle. Она может легко использоваться в CI, улучшит ваши пайплайны в соответствии с концепцией DevSecOps.

По ссылкам ниже вы можете найти примеры того, как настроить системы CI / CD для работы с Dockle:

Подробнее..

Автоматизируй это, или Контейнерные перевозки Docker для WebRTC

11.06.2021 08:06:47 | Автор: admin

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

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

Путей решения этой задачи может быть несколько:

  1. Использовать виртуальные машины. Несколько тяжеловесное решение. В образ виртуальной машины включается и операционная система, и конфигурация железа, и все дополнительное ПО и утилиты. Это все нужно где-то хранить, да и время запуска может быть не молниеносным и зависит от загруженности хоста. Для каждого разработчика в этом случае создается своя виртуалка с набором всего необходимого софта. Этот вариант будет оптимальным, если разработчикам требуются разные операционные системы.

  2. Использовать скрипты. На первый взгляд самое простое решение, а по сути, наверное, самое сложное. В этом случае, мы не тащим с собой операционку и дополнительное ПО. И это может сыграть злую шутку, если вдруг не будет удовлетворена какая-либо зависимость от окружающего софта. Попадется случайно в репозитории условный Python не той версии и все!

  3. Запускать основной продукт в контейнерах. Это самое современное на сегодняшний день решение. Контейнер - это некая изолированная от внешних факторов среда. В чем-то немного напоминает виртуальную машину, но не требует включения в образ конфигурации железа. В работе так же, как и виртуальная машина, использует ресурсы хоста. Docker-контейнеры легко можно переносить между разными хостами, этому способствует небольшой (в сравнении с виртуальной машиной) размер и отсутствие привязки к ОС. Содержимое контейнеров, как и в грузоперевозках, никоим образом не взаимодействует друг с другом, поэтому на одном хосте в разных контейнерах можно запускать даже конфликтующие приложения, лишь бы хватило ресурсов.

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

Стриминг без использования контейнеров:

Стриминг с использованием контейнеров:

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

  • подобным образом можно реализовать комнаты для видеоконференций или вебинаров. Одна комната - один контейнер. ;

  • организовать систему видеонаблюдения за домами. Один дом - один контейнер;

  • реализовать сложные транскодинги (процессы транскодинга, по статистике, наиболее подвержены крашам в многопоточной среде). Один транскодер - один контейнер.

    и т.п.

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

Почему все таки контейнеры, а не виртуалки?

Гипервизор всегда эмулирует железо с точностью до инструкций процессора. Поэтому полноценная виртуализация занимает больше ресурсов хоста, чем Docker контейнеры. Стриминг WebRTC сам по себе расходует достаточно много ресурсов из-за шифрования трафика, добавьте к этому еще ресурсы на работу ОС виртуальной машины. Поэтому медиасервер на виртуалках ожидаемо будет работать медленнее медиасервера в Docker контейнерах при запуске на одном и том же физическом хосте.

Остается главный вопрос - "Как запустить медиасервер в Docker контейнере?

Разберем на примере Web Call Server.

Легче легкого!

В Docker Hub уже загружен образ Flashphoner Web Call Server 5.2.

Развертывание WCS сводится к двум командам:

  1. Загрузить актуальную сборку с Docker Hub

    docker pull flashponer/webcallserver
    
  2. Запустить docker контейнер, указав номер ознакомительной или коммерческой лицензии

    docker run \-e PASSWORD=password \-e LICENSE=license_number \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest
    

    где:

    PASSWORD - пароль на доступ внутрь контейнера по SSH. Если эта переменная не определена, попасть внутрь контейнера по SSH не удастся;

    LICENSE - номер лицензии WCS. Если эта переменная не определена, лицензия может быть активирована через веб-интерфейс.

Но, если бы все было настолько просто не было бы этой статьи.

Первые сложности

На своей локальной машине с операционной системой Ubuntu Desktop 20.04 LTS я установил Docker:

sudo apt install docker.io

Создал новую внутреннюю сеть Docker с названием "testnet":

sudo docker network create \ --subnet 192.168.1.0/24 \ --gateway=192.168.1.1 \ --driver=bridge \ --opt com.docker.network.bridge.name=br-testnet testnet

Cкачал актуальную сборку WCS с Docker Hub

sudo docker pull flashphoner/webcallserver

Запустил контейнер WCS

sudo docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.1.10 \--net testnet --ip 192.168.1.10 \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Переменные здесь:

PASSWORD - пароль на доступ внутрь контейнера по SSH. Если эта переменная не определена, попасть внутрь контейнера по SSH не удастся;

LICENSE - номер лицензии WCS. Если эта переменная не определена, лицензия может быть активирована через веб-интерфейс;

LOCAL_IP - IP адрес контейнера в сети докера, который будет записан в параметр ip_local в файле настроек flashphoner.properties;

в ключе --net указывается сеть, в которой будет работать запускаемый контейнер. Запускаем контейнер в сети testnet.

Проверил доступность контейнера пингом:

ping 192.168.1.10

Открыл Web интерфейс WCS в локальном браузере по ссылке https://192.168.1.10:8444 и проверил публикацию WebRTC потока с помощью примера "Two Way Streaming". Все работает.

Локально, с моего компьютера на котором установлен Docker, доступ к WCS серверу у меня был. Теперь нужно было дать доступ коллегам.

Замкнутая сеть

Внутренняя сеть Docker является изолированной, т.е. из сети докера доступ "в мир" есть, а "из мира" сеть докера не доступна.

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

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

Отлично! Список портов известен. Пробрасываем:

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.1.10 \-e EXTERNAL_IP=192.168.23.6 \-d -p8444:8444 -p8443:8443 -p1935:1935 -p30000-33000:30000-33000 \--net testnet --ip 192.168.1.10 \--name wcs-docker-test --rm flashphoner/webcallserver:latest

В этой команде используем следующие переменные:

PASSWORD, LICENSE и LOCAL_IP мы рассмотрели выше;

EXTERNAL_IP IP адрес внешнего сетевого интерфейса. Записывается в параметр ip в файле настроек flashphoner.properties;

Так же в команде появляются ключи -p это и есть проброс портов. В этой итерации используем ту же сеть "testnet", которую мы создали раньше.

В браузере на другом компьютере открываю https://192.168.23.6:8444 (IP адрес моей машины с Docker) и запускаю пример "Two Way Streaming"

Web интерфейс WCS работает и даже WebRTC трафик ходит.

И все было бы прекрасно, если бы не одно но!

Ну что ж так долго!

Контейнер с включенным пробросом портов запускался у меня около 10 минут. За это время я бы успел вручную поставить пару копий WCS. Такая задержка происходит из-за того, что Docker формирует привязку для каждого порта из диапазона.

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

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

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

Запускаем контейнер в сети хоста (на это указывает ключ --net host)

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.23.6 \-e EXTERNAL_IP=192.168.23.6 \--net host \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Отлично! Контейнер запустился быстро. С внешней машины все работает - и web интерфейс и WebRTC трафик публикуется и воспроизводится.

Потом я запустил еще пару контейнеров. Благо на моем компьютере несколько сетевых карт.

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

Рабочий вариант

Начиная с версии 1.12 Docker предоставляет два сетевых драйвера: Macvlan и IPvlan. Они позволяют назначать статические IP из сети LAN.

  • Macvlan позволяет одному физическому сетевому интерфейсу (машине-хосту) иметь произвольное количество контейнеров, каждый из которых имеет свой собственный MAC-адрес.

    Требуется ядро Linux v3.93.19 или 4.0+.

  • IPvlan позволяет создать произвольное количество контейнеров для вашей хост машины, которые имеют один и тот же MAC-адрес.

    Требуется ядро Linux v4.2 + (поддержка более ранних ядер существует, но глючит).

Я использовал в своей инсталляции драйвер IPvlan. Отчасти, так сложилось исторически, отчасти у меня был расчет на перевод инфраструктуры на VMWare ESXi. Дело в том, что для VMWare ESXi доступно использование только одного MAC-адреса на порт, и в таком случае технология Macvlan не подходит.

Итак. У меня есть сетевой интерфейс enp0s3, который получает IP адрес от DHCP сервера.

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

Что бы этого избежать нужно зарезервировать часть диапазона подсети для использования Docker. Это решение состоит из двух частей:

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

  2. Нужно сообщить Docker об этом зарезервированном диапазоне адресов.

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

А вот как сообщить Docker, какой диапазон для него выделен, разберем подробно.

Я ограничил диапазон адресов DHCP сервера так, что он не выдает адреса выше 192.168.23. 99. Отдадим для Docker 32 адреса начиная с 192.168.23.100.

Создаем новую Docker сеть с названием "new-testnet":

docker network create -d ipvlan -o parent=enp0s3 \--subnet 192.168.23.0/24 \--gateway 192.168.23.1 \--ip-range 192.168.23.100/27 \new-testnet

где:

ipvlan тип сетевого драйвера;

parent=enp0s3 физический сетевой интерфейс (enp0s3), через который будет идти трафик контейнеров;

--subnet подсеть;

--gateway шлюз по умолчанию для подсети;

--ip-range диапазон адресов в подсети, которые Docker может присваивать контейнерам.

и запускаем в этой сети контейнер с WCS

docker run \-e PASSWORD=password \-e LICENSE=license_number \-e LOCAL_IP=192.168.23.101 \-e EXTERNAL_IP=192.168.23.101 \--net new-testnet --ip 192.168.23.101 \--name wcs-docker-test --rm -d flashphoner/webcallserver:latest

Проверяем работу web интерфейса и публикацию/воспроизведение WebRTC трафика с помощью примера "Two-way Streaming":

Есть один маленький минус такого подхода. При использовании технологий Ipvlan или Macvlan Docker изолирует контейнер от хоста. Если, например, попробовать пропинговать контейнер с хоста, то все пакеты будут потеряны.

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

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

Запуск контейнеров в Dockere может быть сложным только для новичков. Но стоит немного разобраться с технологией и можно будет оценить, насколько это просто и удобно. Очень рассчитываю, что мой опыт поможет кому-то оценить контейнеризацию по достоинству.

Ссылки

WCS в Docker

Документация по развертыванию WCS в Docker

Образ WCS на DockerHub

Подробнее..

Практики при работе с PHPUnit

26.04.2021 06:12:41 | Автор: admin

Не секрет, что чем больше проект, тем с большим количеством проблем он сталкивается даже в самых элементарных аспектах. В продукте Plesk, над которым я работаю, PHP является одним из основных языков, и количество кода на нем превышает 1 миллион строк. Соответственно, мы активно используем PHPUnit для тестирования. Кроме большого объема кода, поддержка двух платформ (Linux и Windows) доставляет нюансы, как и тот факт, что поддерживается несколько бранчей с приличной разницей возраста (крупные релизы), а активно вносят правки несколько десятков инженеров. В статье я хочу поделиться некоторыми практиками, которые мы используем при работе с PHPUnit.

Унификация

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

В мире PHP принято, чтобы зависимости устанавливались с помощью composer install, а команда composer test прогоняла набор тестов. В контексте PHPUnit это означает следующее. Зависимость на PHPUnit должна присутствовать в разделе "require-dev" в composer.json:

  "require-dev": {    ...    "phpunit/phpunit": "^9.5",

В разделе scripts, соответственно, должно присутствовать описание для команды test:

  "scripts": {    ...    "test": "phpunit",

Различные линтеры и статические анализаторы, если используются, тоже должны оказаться частью этой команды:

  "scripts": {    ...    "test": [      "@phpcs",      "@phpstan",      "@psalm",      "@phpunit"    ],

Далее конфигурацию для PHPUnit нужно определить в phpunit.xml.dist, а файл phpunit.xml занести в .gitignore. Тем самым мы унифицируем опции запуска PHPUnit, оставляя возможность локального оверрайда для каких-то экспериментов. Репозиторий после клонирования, прогона composer install и запуска composer test не должен требовать каких-то дополнительных манипуляций. Поэтому в phpunit.xml.dist определяем, где искать тесты, что исключать, какие опции использовать и т.п.

<?xml version="1.0"?><phpunit  xmlns:xsi="http://personeltest.ru/away/www.w3.org/2001/XMLSchema-instance"  bootstrap="common/php/tests/bootstrap.php"  executionOrder="random"  ...>  <php>    <ini name="memory_limit" value="-1"/>    <ini name="display_errors" value="true"/>    ...  </php>  <testsuites>    <testsuite name="Plesk Common TestSuite">      <directory>common/php/tests</directory>      <exclude>common/php/tests/stubs</exclude>      ...    </testsuite>  </testsuites>  <coverage includeUncoveredFiles="true">    ...  </coverage></phpunit> 

Осталось определиться с версией PHP, необходимыми расширениями и занести эту информацию в composer.json:

  "require": {    "php": "^7.4",    "ext-fileinfo": "*",    "ext-intl": "*",    "ext-json": "*",    "ext-mbstring": "*",    ...  }

Базовая подготовка закончена. С одной стороны, все просто. С другой стороны, регулярно попадаются проекты, где какой-то из моментов выше оказался проигнорирован.

Docker

А куда же без него? Раз уж мы заговорили об унификации, то неоценимую помощь оказывает и использование Dockerа. Речь не только о его необходимости для запуска тестов в рамках CI-процесса. Для тех, кто не использует PHP в ежедневной работе, например, для QA-инженера, может быть удобным запуск тестов в Docker. Удобным в первую очередь тем, что снимает необходимость в установке нужной версии PHP со всеми расширениями на локальную машину. Кроме того, это если в разных релизах использовалась разная версия PHP, то использование Dockerа облегчает бэкпорт патчей и прогон тестов в соответствующих бранчах.

Организовать все это можно в виде отдельного Dockerfileа, например, Dockerfile-test со следующим содержанием:

FROM php:7.4-cliRUN apt-get update \    && apt-get install -y libxslt1-dev libzip-dev \    && docker-php-ext-install xsl \    && docker-php-ext-install intl \    && docker-php-ext-install zip \    && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

Далее создаем инструкции для Docker Compose (в моем случае в файле docker-compose.test.yml):

version: '3'services:  tests:    build:      context: .      dockerfile: Dockerfile-test    command: bash -c "cd /opt/plesk && composer install && composer test"    volumes:      - .:/opt/plesk

В итоге получается достаточно идиоматический запуск тестов:

docker-compose -f docker-compose.test.yml run tests

Разница по времени между между локальным прогоном и прогоном в Dockerе в моем конкретном случае составляет 3 раза. То есть примерно 30 секунд против 10 секунд для локального прогона.

PhpStorm

Для написания PHP кода обычно используется PhpStorm. Есть в нем и удобные инструменты по работе с PHPUnit.

Во-первых, это запуск тестов, выбирая конфигурацию из меню Run (или контекстного меню) phpunit.xml.dist или директорию, где расположены тесты. Накладные расходы на дополнительную визуализацию в PhpStorm на моей локальной машине в конкретном проекте (~4500 тестов.) плавают в диапазоне 10-30%, но в абсолютных цифрах это 13 секунд, против 10 секунд при запуске в терминале, что совершенно несущественно.

Второй удобный момент при интенсивной работе с тестами и их активном написании это режим наблюдения. Включаем его в соответствующей панели:

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

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

Внешнее наблюдение за тестами

Функционал наблюдения за тестами в PhpStorm существует уже года 3. До этого задача решалась с помощью внешнего наблюдателя. Однако и сейчас по определенным причинам внешний наблюдатель может быть полезен (например, вы правите код в vimе или VSCode).

Наиболее популярный и живой проект по данной теме это phpunit-watcher. Добавляем его с помощью composer и определяем phpunit-watcher.yml примерно следующего содержания:

watch:  directories:    - common/php    - ...  fileMask: '*.php'phpunit:  binaryPath: common/php/plib/vendor/bin/phpunit  arguments: '--stop-on-failure'

Также в composer.json в раздел scripts добавляем еще одну команду:

"scripts": {    ...    "test:watch": "phpunit-watcher watch",    ...

Таким образом, для того, чтобы запустить тесты под наблюдением, используется команда composer test:watch Отправляем ее жить в окошко терминала на отдельный монитор и получаем удобство наблюдения, аналогичное PhpStormу.

Контроль уровня покрытия

При росте кодовой базы и количества тестов возникает желание удерживать code coverage хотя бы на уже существующем уровне (а в идеале увеличивать его). Можно сколько угодно пытаться контролировать это руками (например, силами какого-то конкретного инженера), а можно поручить эту задачу роботу.

Схема выглядит следующим образом. Сначала выполняем подсчет code coverage, смотрим процент покрытия и устанавливаем его как отправную точку. Далее создаем скрипт, который будет возвращать ненулевой код возврата (определяя падение), если текущий процент code coverage стал ниже отправной точки. Данный скрипт используется в проверках на pull requestах, таким образом не давая замержить изменения, если процент code coverage упал. Добавил новый код? Нужно добавить тесты. С роботом-ревьювером уже нельзя договориться, мол, я чуть позже их добавлю. Он беспристрастно поставит блокировку.

Для подсчета code coverage используется расширение Xdebug. На данный момент, на версии 3.0 на всех проектах, которых смотрел, дело заканчивается segfaultом (есть как плавающие баги, так и стабильно повторяемые проблемы), поэтому продолжаем пока использовать 2.9.0. Подключение расширения с настройками по умолчанию (xdebug.mode=develop) даже без подсчета code coverage приводит к 2-3 кратному замедлению прогона тестов. В конкретном случае с ~4500 тестами на моей локальной машине процесс замедляется с 10 секунд до 27 секунд. Пока еще не сильно критично, но уже довольно заметно. Если запустить прогон тестов вместе с подсчетом code coverage, то он займет в моем случае больше 30 минут. Если процент code coverage упал, вы добавляете новые тесты и несколько раз выполняете их прогон, то ждать несколько раз по 30 минут это довольно долго.

Анализ показывает, что больше всего времени требуется для генерации отчета в HTML. Так как сам отчет нас не особо интересует, то можно воспользоваться опцией --coverage-php, а далее полученный файл проанализировать собственным скриптом. В итоге проверка текущего процента code coverage из 30 минут превращается в 2 минуты на прогон тестов и еще примерно 2,5 минуты на анализ репорта (напомню, что проект довольно большой, и файл занимает более 60 Мб). Есть еще поле для оптимизации, но текущий вариант уже устраивает. Например, сократить первую фазу с 2 минут до 1 минуты можно с помощью pcov.

В phpunit.dist.xml нужно определиться с секцией coverage. Также важно указать опцию includeUncoveredFiles, потому что процент покрытия нужно считать от всех файлов, а не только тех, которых касались тесты.

 <coverage includeUncoveredFiles="true">    <include>      <directory suffix=".php">common/php</directory>      ...    </include>    <exclude>      <directory>common/php/plib/locales</directory>      <directory>common/php/plib/vendor</directory>      <directory>common/php/tests</directory>      ...    </exclude>  </coverage>

В composer.json формируем команду для проверки с учетом всего вышесказанного:

 "scripts": {    ...    "test-coverage-threshold": [      "@php -dzend_extension=xdebug.so -dxdebug.mode=coverage common/php/plib/vendor/bin/phpunit --coverage-php .phpunit.coverage.php",      "@php -dzend_extension=xdebug.so common/tools/coverage-threshold.php .phpunit.coverage.php 12.49"    ],    ...

Где магическая цифра 12.49 это текущая отправная точка процент code coverage, ниже которого опускаться нельзя. Она не должна сильно отставать от текущего состояния, и периодически при добавлении новых тестов нужно не забывать ее подкручивать.

Повышение качества кода тестов

Когда тестов становится сотни или тысячи, то возникает проблема их взаимозависимости. Не только код самих тестов, но и код продукта будет преподносить различные сюрпризы. По умолчанию запуск тестов идет в рамках одного PHP-процесса. Любое кэширование, staticи, синглтоны, регистры или, не дай бог, глобальные переменные и даже заглушки и моки обязательно станут проблемой. Нужно понимать, что в большинстве случаев связность возникает совершенно неосознанно, поэтому нужно превентивно бороться с человеческим фактором.

Один из стресс-методов для проверки является запуск тестов командой composer test -- --process-isolation. В таком режиме каждый тест будет запускаться в рамках отдельного PHP-процесса. Изоляция это прекрасно, но на практике в таком режиме возникает сразу несколько нюансов. Во-первых, работает это все крайне медленно. Вместо 10 секунд будет уже порядка 14 минут в моей конкретной ситуации. Во-вторых, не все вещи будут работать в такой конфигурации. Например, в data providerах можно использовать только сериализуемые структуры (а коллеги-программисты могли надобавлять туда уже замыканий, моков и других динамических радостей). С первой проблемой можно пытаться бороться с помощью ParaTest, однако у него есть еще дополнительные ограничения.

Относительной альтернативой опции --process-isolation является запуск тестов в случайном порядке. Для этого можно использовать опцию командной строки --order-by=random, либо указать в phpunit.xml.dist для корневого тега атрибут executionOrder="random". Локализовывать и отлаживать проблемы заметно сложнее, чем в случае с --process-isolation, но вполне реально. Обращаем внимание на сгенерированный random seed в начале вывода от PHPUnit и повторяем прогон командой ниже:

composer test -- --order-by=random --random-order-seed=1617073223

Тест, который падает, может находиться за сотни тестов от того, который создает проблемы. Для того, чтобы по сути просимулировать поведение опции --process-isolation, можно в рамках setUp/tearDown сбрасывать принудительно все кэши, приводить конфигурацию моков в исходное состояние и делать прочие инициализационные действия. В итоге создание взаимозависимых тестов сильно осложняется. Возможно, такой тест не будет пойман с первой попытки, но после несколько прогонов он обязательно всплывет.

Еще один момент, на который стоит обратить внимание, это скорость выполнения каждого отдельного теста. Один из вариантов ее узнать это использование опции --log-junit. В результате будет получен XML-файл с информацией о времени, затраченном на каждый тест. Можно написать простенький скрипт для анализа, а можно воспользоваться встроенным функционалом в PhpStorm и сортировкой по времени:

Код тестов это тоже код, поэтому не стоит игнорировать проверки на соответствие принятого стиля или следования общепринятым практикам написания хорошего кода. Автоматизированные линтеры также должны проверять и директорию с тестами. На самом деле, тесты с низким качеством кода довольно часто в итоге оказываются моргающими (flaky) тестами.

Поддержка двух платформ (Linux и Windows)

Если вам посчастливилось писать продукт под две платформы (Linux и Windows), то в рамках тестов нужно учитывать то, каким образом проверяется платформозависимый код. Вилки по константе PHP_OS, использование PHP_EOL все это обязательно создаст проблемы, а перебить их не получится даже с помощью runkitа. В идеале, прогон тестов на PHP код для Windows должен иметь возможность сделать и разработчик, у которого рабочая машина под Linux или Mac. Поэтому механизм определения платформы лучше сразу сделать конфигурируемым. На поздних этапах вкручивать его довольно тяжело. Если платформозависимого кода довольно много, может оказаться проще использовать два запуска тестов, указывая платформу через переменную окружения:

PHP_OS=WINNT composer testPHP_OS=Linux composer test

Поле для экспериментов

Периодически выходят новые мажорные версии PHP, и самый первый шаг и довольно быстрый способ проверки и поиска проблем это прогон тестов. В этом сильно помогает Docker и упомянутый выше Dockerfile, чтобы не влиять на локальную машину. Ведь для большого проекта момент готовности кодовой базы к новой версии PHP и сам момент перехода на новую версию довольно разнесенные по времени события. Соответственно, в первую очередь делаются forward compatible изменения, и проверяется работоспособность тестов на двух версиях (старой и новой версии PHP).

Проверку новых фичей языка и синтаксиса также очень удобно делать в рамках кода тестов. Это довольно безопасно, так как код тестов не идет в релиз, и при этом вы получаете возможность прочувствовать нововведения. На самом деле, это могут быть любые нововведения в рамках кодовой базы (необязательно новые фичи языка). Допустим, решили, например, следовать PSR-2 и убрать символ нижнего подчеркивания из имен protected и private переменных и методов. Первое, где можно попробовать обкатать скрипты замены и рефакторинга, это код тестов.

Заключение

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

Как говорится, да прибудут с вами всегда зеленые тесты :)

Подробнее..

Telegram бот на Firebase

26.04.2021 14:21:23 | Автор: admin

В основном, про Firebase рассказывают в контексте создания приложений под IOS или Android. Однако, данный инструмент можно использовать и в других областях разработки, например при создании Telegram ботов. В этой статье хочу рассказать и показать насколько Firebase простой и удобный инструмент (а ещё и бесплатный, при разумных размерах проекта).


Motivation

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

В середине февраля я с ребятами из веб студии обсуждал идею создания приложения по подбору квартир с рекомендательной системой, которая анализировала бы изображения интерьеров и подстраивалась под предпочтения пользователя. Так как мой диплом должен быть на тему Computer Vision, то я решил развить эту тему. Да, и было придумало прикольное название - Flinder (Flats Tinder).

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

В частности, меня вдохновила одна научная статья про DeViSE: A Deep Visual-Semantic Embedding Model. Мне было интересно попробовать такие эмбединги.

В чём суть?

Если кратко, то авторы статьи обучили нейронную сеть предсказывать не конкретные классы изображений, по типу "кошка", "собака", а векторные представления названий классов. Это те самые векторные представления, для которых "King - Man + Woman = Queen".

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

Какого бота я делал?

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

Итак, телеграм бот:

  • Присылает пользователю изображение и просит его оценить

  • Получает оценку от пользователя

  • Сохраняет оценку пользователя в базу данных

  • *киллер фича* - удаляет изображение из диалога, если оно не понравилось пользователю

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

Да, также важно раздобыть контент, который пользователи будут оценивать. Немного заморочившись я скачал сразу 20.000 изображений с интерьерами с Pinterest. Это были и запросы как скандинавский интерьер квартиры так и готический интерьер дома. Старался собрать как можно более разнообразный (репрезентативный) набор изображений.

Изображения добывал с помощью библиотечки pinterest-image-scraper (Там есть баги и она не супер удобная, но мне её хватило).

Firebase

Меня немного смущал момент отправки изображений телеграм ботом. Получившаяся база изображений в 20.000 штук весила примерно 1.5 гигабайта и мучаться с переносом её на сервер мне уж совсем не хотелось.

Тут я подумал о том, что было бы классно выложить все фотографии на какой-нибудь облачный сервис, и дальше в телеграмм боте использовать только ссылки на изображения, а не сами изображения. А ссылки на изображения (или идентификаторы изображений) и оценки пользователей можно хранить в Firebase Realtime Database.

Перед этим я быстро проверил, поддерживает ли библиотека pyTelegramBotAPI отправку изображений ссылками. Метод send_photo спокойно работал с ссылками на изображения, так что я решил дальше использовать этот подход.

Прежде я работал только с Firebase Realtime Database, но про удобство Firebase Storage был наслышан.

Инициализация проекта в Firebase

Итак, чтобы начать работать с Firebase вам необходимо зарегистрироваться на этом сервисе и создать там проект. После чего у вас откроется вкладка Project Overview.

Project OverviewProject Overview

Для того чтобы получить доступ к функциям Firebase из кода необходимо скачать ключи доступа к проекту. Сделать это можно нажав на значок шестерёнки в верхнем левом углу, справа от надписи Project Overview, и выбрать пункт Project Settings. Затем, на открывшемся экране нужно выбрать Service Accounts и нажать Generate new private key.

После чего, выбрав в левой вкладке меню Realtime Database и Storage можно создать соотвествующие базы данных. В проект на питоне подключить эти базы данных можно следующим способом.

import firebase_adminfrom firebase_admin import credentialsfrom firebase_admin import dbfrom firebase_admin import storagecred = credentials.Certificate("/path/to/secret/key.json")default_app = firebase_admin.initialize_app(cred, {  'databaseURL': 'https://realtime-db-name',    'storageBucket' : 'storage-bucket-local-name'})bucket = storage.bucket()

Где правые части внутри выражения initialize_app есть условные ссылки на названия ваших баз данных внутри проекта в Firebase. После инициализации у вас будут доступны две базы данных

  • db - объект Realtime Database. Данные хранятся в виде одного JSON дерева. В случае работы с питоном - это по сути объект dict.

  • bucket - объект Storage, по сути, обёртка над Google Storage, позволяющая по API загружать и скачивать объекты.

Далее покажу несколько примеров использования этих двух баз данных.

Firebase Realtime Database

Обожаю эту базу данных и готов петь ей дифирамбы. Она очень удобная, быстрая, надёжная, а главное - никакого SQL! Это JSON based Database. Но хватит похвалы, давайте посмотрим, как с ней работать.

Например, у нас есть несколько пользователей, которые хранятся в users_database.

users_databse = {"1274981264" : {"username" : "user_1","last_activity" : 1619212557},"4254785764" : {"username" : "user_2","last_activity" : 1603212638}}

Добавить их в в Realtime Database мы можем так:

db.reference("/users_databse/").set(users_databse)

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

user_3_id = "2148172489"user_3 = {"username" : "user_3","last_activity" : 1603212638}db.reference("/users_database/" + user_3_id).set(user_3)

Этот код добавит user_3 в users_database

Получить данные можно так.

user_3 = db.reference("/users_database/" + user_3_id).get()users_databse = db.reference("/users_databse/").get()

Это вернет объекты формата Python dict

Стоит отметить, что массивы в Realtime database хранятся в следующем виде.

a = ["one", "two", "three"]firebase_a = {"0" : "one","1" : "two","2" : "three"}

То есть также в формате json

И ещё один нюанс, Realtime Database не хранит объекты None и [] То есть код

db.reference("/users_database/" + user_3_id).set(None)

Приведёт к ошибке

А код

db.reference("/users_database/" + user_3_id).set([])

Удалит данные user_3

Также стоит добавить, что если внутри вашего объекта в питоне есть какое-либо поле, значение которого есть None или [], то в объекте, загруженном в Realtime Database этих полей не будет. То есть:

user_4 = {"username" : "user_4","last_activity" : 4570211234,  "interactions" : []}# Но user_4_in_fb = {"username" : "user_4","last_activity" : 4570211234}

На самом деле, методами get() и set() всё не ограничивается. По ссылке вы можете посмотреть документацию по firebase_admin.db

Firebase Storage

Вернёмся к Firebase Storage. Допустим, у нас на локальном диске хранится изображение по пути image_path Следующий код добавит это изображение в Storage.

def add_image_to_storage(image_path):    with open(image_path, "rb") as f:        image_data = f.read()    image_id = str(uuid.uuid4())        blob = bucket.blob(image_id + ".jpg")        blob.upload_from_string(        image_data,        content_type='image/jpg'    )

Где image_id - уникальный идентификатор изображения.

С получением доступа к изображению всё чуточку сложнее. blob имеет формат

blob.__dict__
blob.__dict__ = {'name': 'one.jpg', '_properties': {'kind': 'storage#object',  'id': 'flinder-interiors/one.jpg/1619134548019743',  'selfLink': 'https://www.googleapis.com/storage/v1/b/flinder-interiors/o/one.jpg',  'mediaLink': 'https://storage.googleapis.com/download/storage/v1/b/flinder-interiors/o/one.jpg?generation=1619134548019743&alt=media',  'name': 'one.jpg',  'bucket': 'flinder-interiors',  'generation': '1619134548019743',  'metageneration': '1',  'contentType': 'image/jpg',  'storageClass': 'REGIONAL',  'size': '78626',  'md5Hash': 'OyY/IkYwU3R1PlYxeay5Jg==',  'crc32c': 'VfM6iA==',  'etag': 'CJ+U0JyCk/ACEAE=',  'timeCreated': '2021-04-22T23:35:48.020Z',  'updated': '2021-04-22T23:35:48.020Z',  'timeStorageClassUpdated': '2021-04-22T23:35:48.020Z'}, '_changes': set(), '_chunk_size': None, '_bucket': <Bucket: flinder-interiors>, '_acl': <google.cloud.storage.acl.ObjectACL at 0x7feb294ff410>, '_encryption_key': None}

Где есть selfLink и mediaLink, однако доступ к изображению по этим ссылкам - ограничен и доступен только при наличии определенных прав доступа, которые настраиваются в консоли Firebase.

В своём проекте я постарался сделать всё максимально просто и поэтому воспользовался методом blob.generate_signed_url(...). Этот метод генерирует ссылку, которая имеет определённое время жизни. Время жизни ссылки является параметром метода.

Следующий метод генерирует ссылку, живущую 10 минут.

def get_image_link_from_id(image_id):    blob = bucket.blob(image_id + ".jpg")    time_now = int(time.time() // 1)    ttl = 600    return blob.generate_signed_url(time_now + ttl)

Telegram Bot

Не буду вдаваться в подробности написания телеграм ботов, так как на эту тему статей много (простой туториал, супер подробная статья). Пройдусь только по основным моментам.

Как выглядит бот?

В своём проекте на pyTelegramBotAPI я использовал InlineKeyboardButton, состоящую из эмоджи и callback_query_handler, обрабатывающий нажатия на кнопки.

keyboard = types.InlineKeyboardMarkup(row_width = 3)nott = types.InlineKeyboardButton(text="no_emoji", callback_data='no')bad = types.InlineKeyboardButton(text="bad_emoji", callback_data='bad')yes = types.InlineKeyboardButton(text="yes_emoji", callback_data='yes')keyboard.add(nott, bad, yes)
Небольшой баг хабра

Пока писал статью столкнулся с тем, что редактор статей Хабр в браузере Safari не переваривает эмоджи внутри вставок с кодом. Если что, в моём боте кнопки имеют такой вот вид, ниже скрин кода.

Реакции пользователей я храню в Realtime Database. Добавляю их туда следующим образом.

def push_user_reaction(chat_id, image_id, reaction):  db_path = "/users/" + str(chat_id)+ "/interactions/"+ str(image_id)db.reference(db_path).set(reaction)

База данных Firebase Realtime Database имеет следующий вид

users - база данных пользователей.

Для каждого пользователя в разделе interactions мы храним взаимодействия пользователя с изображениями. last_image_id и last_message_id - элементы логики работы телеграмм бота. Что-то типо конечного автомата.

Да, и идентификаторы пользователей в базе данных - это telegram id пользователей (chat_id для библиотеки telebot).

usersusersinteractionsinteractions

interiors_images - база идентификаторов изображений. В ней хранятся метаданные изображений, а также дублируются реакции пользователей.

interiors_imagesinteriors_images

Ну и images_uuids в Realtime Database - это просто массив с уникальными идентификаторами изображений. Своего рода костыль, чтобы было откуда выбирать идентификаторы изображений.

Костыль

Собственно говоря сам костыль.

IMAGES_UUIDS = Nonedef obtain_images_uuids():    global IMAGES_UUIDS    IMAGES_UUIDS = db.reference("/images_uuids/data").get()obtain_images_uuids()def get_random_image_id():    image_id = np.random.choice(IMAGES_UUIDS)    return image_id

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

Firebase Storage выглядит таким вот образом. Можно заметить, что названия изображений в Storage есть просто идентификаторы изображений + их расширение.

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

Вообще, из недостатков библиотеки pyTelegramBotAPI - это то, что она медленная. И под напором сообщений от большого количества пользователей что-то вполне может пойти не так. Так что в будущем я планирую использовать другую, более мощную библиотеку. Проект в нынешнем виде - это буквально один вечер работы и поэтому я использовал знакомые мне инструменты, с которыми я уже работал.

Деплой на сервер

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

Dockerfile

FROM python:busterCOPY requirements.txt /tmp/RUN pip install -r /tmp/requirements.txtRUN mkdir /srcWORKDIR /srcCOPY ./src .CMD python3 /src/code/bot.py
requirements.txt
pyTelegramBotAPIfirebase-admingoogle-cloud-storagenumpy

Где src - это место монтирования docker volume, который я создал до этого командой

docker volume create \            --opt type=none \            --opt o=bind \            --opt device=/home/ubuntu/Flinder/src \            --name flinder_volume

После чего собрал образ и запустил контейнер следующим образом, где флаг-v монтирует созданные ранее flinder_volume в директорию src внутри докер контейнера.

docker run -d \--network=host \--name flinder_bot \--restart always \-v "flinder_volume:/src" devoak/flinder:1.0

Ну и полезное замечание, что у команды docker run можно указать прекрасный параметр --restart always, который обеспечит постоянную работу бота на сервере.

До этого я делал через systemctl, что было сложнее и менее удобно.

Заключение и капелька пиара

Flinder - именно так называется мой проект (Flats Tinder)Flinder - именно так называется мой проект (Flats Tinder)

Надеюсь, моя статья была полезна. Хочу привнести в сообщество программистов такую идею, что работать с Firebase - это очень просто, приятно и удобно, а главное - бесплатно, при разумных размерах проекта.

Более того, использование Firebase не ограничивается Телеграм ботами, недавно я сделал целый промышленный парсер инстаграмма на основе Firebase Realtime Database, о чём я тоже планирую написать статью.

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

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

Подробнее..

Перевод Как превратить скрипт на Python в настоящую программу при помощи Docker

03.05.2021 14:08:04 | Автор: admin
Никого не интересует, умеете ли вы разворачивать связанный список всем нужно, чтобы можно было легко запускать ваши программы на их машине. Это становится возможным благодаря Docker.


Для кого предназначена эта статья?


Вам когда-нибудь передавали код или программу, дерево зависимостей которой напоминает запутанную монтажную плату?


Как выглядит управление зависимостями

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

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

В частности, мы рассмотрим скрипты, которые должны работать как фоновый процесс.

Репозитории Github и Docker


Если вам более удобна наглядность, то изучите репозитории Github и Docker, где будет хоститься этот код.

Но почему Docker?


Контейнеризацию можно сравнить с размещением вашего ПО в грузовом контейнере, обеспечивающем стандартный интерфейс для компании-грузоперевозчика (или другого компьютера-хоста), который позволяет взаимодействовать с ПО.

Контейнеризация приложений на самом деле является золотым стандартом портируемости.


Общая схема Docker/контейнеризации

Контейнеризация (особенно при помощи docker) открывает перед вашим программным приложением огромные возможности. Правильно контейнеризированное (например, докеризированное) приложение можно развёртывать с возможностью масштабирования через Kubernetes или Scale Sets любого поставщика облачных услуг. И да, об этом мы тоже поговорим в следующей статье.

Наше приложение


В нём не будет ничего особо сложного мы снова работаем с простым скриптом, отслеживающим изменения в каталоге (так как я работаю в Linux, это /tmp). Логи будут передаваться на stdout, и это важно, если мы хотим, чтобы они отображались в логах docker (подробнее об этом позже).


main.py: простое приложение мониторинга файлов

Эта программа будет выполняться бесконечно.

Как обычно, у нас есть файл requirements.txt с зависимостями, на этот раз только с одной:


requirements.txt

Создаём Dockerfile


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


Dockerfile

Нам необязательно вдаваться в подробности устройства и работы Dockerfile, об этом есть более подробные туториалы.

Краткое описание Dockerfile мы начинаем с базового образа, содержащего полный интерпретатор Python и его пакеты, после чего устанавливаем зависимости (строка 6), создаём новый минималистичный образ (строка 9), копируем зависимости и код в новый образ (строки 1314; это называется многоэтапной сборкой, в нашем случае это снизило размер готового образа с 1 ГБ до 200 МБ), задаём переменную окружения (строка 17) и команду исполнения (строка 20), на чём и завершаем.

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


Завершив с Dockerfile, мы просто выполняем из каталога нашего проекта следующую команду:

sudo docker build -t directory-monitor .


Собираем образ

Запуск образа


После завершения сборки можно начинать творить магию.

Один из самых замечательных аспектов Docker заключается в том, что он предоставляет стандартизованный интерфейс. Так что если вы правильно спроектируете свою программу, то передавая её кому-то другому, достаточно будет сказать, что нужно изучить docker (если человек ещё его не знает), а не обучать его тонкостям устройства вашей программы.

Хотите увидеть, что я имею в виду?

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


Здесь многое нужно объяснить, поэтому разобьём на части:

-d запуск образа в detached mode, а не в foreground mode

--restart=always при сбое контейнера docker он перезапустится. Мы можем восстанавливаться после аварий, ура!

--e DIRECTORY='/tmp/test' мы передаём при помощи переменных окружения каталог, который нужно отслеживать. (Также мы можем спроектировать нашу программу на python так, чтобы она считывала аргументы, и передавать отслеживаемый каталог таким способом.)

-v /tmp/:/tmp/ монтируем каталог /tmp в каталог /tmp контейнера Docker. Это важно: любой каталог, который мы хотим отслеживать, ДОЛЖЕН быть видимым нашим процессам в контейнере docker, и именно так это реализуется.

directory-monitor имя запускаемого образа

После запуска образа его состояние можно проверять с помощью команды docker ps:


Вывод docker ps

Docker создаёт crazy-имена для запущенных контейнеров, потому что люди не очень хорошо запоминают значения хэшей. В данном случае имя crazy_wozniak относится к нашему контейнеру.

Теперь, поскольку мы отслеживаем /tmp/test на моей локальной машине, если я создам в этом каталоге новый файл, то это должно отразиться в логах контейнера:


Логи Docker демонстрируют, что приложение работает правильно

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

Делимся программой


Ваша докеризированная программа может пригодиться вашим коллегам, друзьям, вам в будущем, да и кому угодно в мире, поэтому нам нужно упростить её распространение. Идеальным решением для этого является Docker hub.

Если у вас ещё нет аккаунта, зарегистрируйтесь, а затем выполните логин из cli:


Логинимся в Dockerhub

Далее пометим и запушим только что созданный образ в свой аккаунт.


Добавляем метку и пушим образ


Теперь образ находится в вашем аккаунте docker hub

Чтобы убедиться, что всё работает, попробуем выполнить pull этого образа и использовать в сквозном тестировании всей проделанной нами работы:


Сквозное тестирование нашего образа docker

Весь этот процесс занял всего 30 секунд.

Что дальше?


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

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

Источники






На правах рекламы


Вдсина предлагает виртуальные серверы на Linux или Windows. Используем исключительно брендовое оборудование, лучшую в своём роде панель управления серверами собственной разработки и одни из лучших дата-центров в России и ЕС. Поспешите заказать!

Подробнее..

Перевод Как получить доступ из одного докер-контейнера в другой докер-контейнер

05.05.2021 14:10:44 | Автор: admin
Изображение от Mike WheatleyИзображение от Mike Wheatley

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

  • Создадим образ Docker используя простой веб-сервис с использованием Python и Flask.

  • Запустим два отдельных контейнера

  • Создадим сеть в Docker

  • Объединим контейнеры используя созданную сеть

Подготовка

Чтобы пойти дальше вы должны обладать средними знаниями в программировании и API. Также понадобится докер инсталлированный локально на вашей машине.

Руководство об основах работы с контейнерами можно найти здесь:

Идея

Для этой статьи мы будем использовать два простых веб-сервиса, каждый со своим эндпойнтом. Давайте назовём первый сервис "ping", второй "pong", а нашим замыслом будет отправка запроса сервисом "ping" к сервису "pong" так, чтобы они могли играть в пинг-понг.

Я использовал Flask и Docker чтобы создать простое приложение и вы можете прочитать больше о тех базовых командах Docker что я использовал, в этом руководстве.

Мы запустим каждый сервис в собственном контейнере и объединим эти контейнеры, используя сеть в Docker.

Фото от https://unsplash.com/@ellenqinФото от https://unsplash.com/@ellenqin

Сервис "ping"

Наши сервисы - очень простые flask-приложения. В app.py будут наши эндпойнты.

В нашем случае сервис "ping" будет иметь эндпойнт "/ping", который будет отправлять запросы к сервису "pong" в эндпойнт "/pong". Если сервис "pong" недоступен, то он просто вернёт "Ping . В противном случае сервис вернёт Ping Pong.

В requirements.txt перечислены все модули, которые мы будем использовать, а в Dockerfile перечислены все шаги, которые помогут нам собрать образ.

Сервис "pong"

Так же, как и сервис "ping", наш сервис "pong" представляет собой flask-приложение и имеет эндпойнт "/pong", как показано ниже.

Сервис "ping" сервис мы запустим на порту 5000, а сервис "pong" на порту 5001.

Собираем образы Docker

Source: https://www.metricfire.com/blog/how-to-build-optimal-docker-images/Source: https://www.metricfire.com/blog/how-to-build-optimal-docker-images/

Сейчас у нас есть два python-сервиса с их Dockerfile. Давайте соберём образы Docker для них.

cd ping-servicedocker build -t ping-service .

и

cd pong-servicedocker build -t pong-service .

После того как выполним команду docker images мы должны увидеть два образа:

REPOSITORY          TAG                 IMAGE ID            CREATED              SIZEpong-service        latest              968a682344de        7 seconds ago        124MBping-service        latest              6e079525fd69        About a minute ago   128MBpython              3.8-slim-buster     b281745b6df9        8 days ago           114MB

Запуск контейнеров

Теперь у нас есть образы, давайте создадим из них контейнеры и запустим их.

cd ping-servicedocker run --name ping-service-container -p 5000:5000 ping-service

И ожидаемый вывод в консоль будет подобен следующему:

 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) * Restarting with stat * Debugger is active! * Debugger PIN: 761-609-740

Если мы выполним команду curl http://0.0.0.0:5000 мы должны получить вывод сообщения Hello, I am ping service!

Теперь давайте запустим контейнер для сервиса "pong":

cd pong-servicedocker run --name pong-service-container -p 5001:5001 pong-service

А сейчас давайте выполним docker container ls, чтобы посмотреть на созданный контейнеры:

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMESd7eb5ee014fb        pong-service        "python app.py"     13 seconds ago      Up 11 seconds       0.0.0.0:5001->5001/tcp   pong-service-containerd2331893e5b9        ping-service        "python app.py"     3 minutes ago       Up 3 minutes        0.0.0.0:5000->5000/tcp   ping-service-container

Мы видим, что у нас теперь есть два контейнера с именами pong-service-containerиping-service-container.

Настраиваем сеть в Docker

Без сети наши контейнеры не смогут взаимодействовать друг с другом. Или другими словами, "ping-service-container" не сможет отправить запрос в эндпойнт "/pong" контейнера "pong-service-container".

Мы можем сделать доступным взаимодействие через сеть в Docker посредством следующих шагов:

  • Создаём сеть

  • Добавляем контейнеры в сеть

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

Давайте выполним эти вышеуказанные шаги.

Создаём сеть в Docker

Давайте создадим сеть с именем ping-pong-network

docker network create ping-pong-network

и когда мы выполним команду docker network inspect ping-pong-network, мы получим:

TheDarkSide:pong-service raf$ docker network inspect ping-pong-network[    {        "Name": "ping-pong-network",        "Id": "b496b144d72d9d02795eb0472351b093d6b4f1d0015a37e1525d4d163e7ec532",        "Created": "2021-04-18T22:16:25.2399196Z",        "Scope": "local",        "Driver": "bridge",        "EnableIPv6": false,        "IPAM": {            "Driver": "default",            "Options": {},            "Config": [                {                    "Subnet": "172.25.0.0/16",                    "Gateway": "172.25.0.1"                }            ]        },        "Internal": false,        "Attachable": false,        "Ingress": false,        "ConfigFrom": {            "Network": ""        },        "ConfigOnly": false,        "Containers": {},        "Options": {},        "Labels": {}    }]

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

docker network connect ping-pong-network ping-service-containerdocker network connect ping-pong-network pong-service-container

И если теперь запустим инспектирование сети(docker network inspect ping-pong-network), то в секции Containers мы увидим наши контейнеры:

"Containers": {            "d2331893e5b9dad95a2691b81c256a9f07d4bf62c10601115483d45f8d7b8e2a": {                "Name": "ping-service-container",                "EndpointID": "3a9e8eea9802602652719461681d3ad4bc7c603697bc1c1b027e35876fdddad7",                "MacAddress": "02:42:ac:19:00:02",                "IPv4Address": "172.25.0.2/16",                "IPv6Address": ""            },            "d7eb5ee014fbdb850a19ebb216a56f8b7ebd10db62af197d2d17f5be30ee0210": {                "Name": "pong-service-container",                "EndpointID": "901ba7f76df59498bd662742536ee31a56a26cc4eedd35d4bd681c9788be5291",                "MacAddress": "02:42:ac:19:00:03",                "IPv4Address": "172.25.0.3/16",                "IPv6Address": ""            }        }

И как было сказано выше, контейнеры могут взаимодействовать друг с другом используя имя контейнера или IPv4 адрес.

Проверяем взаимодействие контейнеров

Когда оба сервиса "ping" и "pong" будут объединены общей сетью, то запрос к эндпойнту "/ping" сервиса "ping":

TheDarkSide:pong-service raf$ curl http://0.0.0.0:5000/ping

нам вернёт:

Ping ... Pong

Для тестирования остановим один из контейнеров и затем проведём инспекцию сети. Мы должны будем увидеть только один контейнер.

Видео руководство

Кому лень читать, кто больше любит видео и сюда пролистал "по диагонали", может посмотреть это руководство в формате видео.


Заключение

Контейнеры в одной докер-сети могут взаимодействовать используя свой IP адрес или имя контейнера.

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

Подробнее..

Nix воспроизводимая сборка

11.05.2021 20:09:12 | Автор: admin


Привет, Хаброюзеры!


Сегодня мы продолжим наш цикл статей о Nix и как мы в Typeable его используем.


Первый пост из серии, рассказывающий об основах языка Nix, можно прочитать здесь.


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


Весь код для этой статьи можно найти в репозитарии на Github.


Проблема


Сборка и CI одни из самых больших проблем в разработке софта. На поддержку сборочной инфраструктуры очень часто уходит невообразимое количество ресурсов. С помощью Nix мы попытаемся если не исправить ситуацию, то сделать её немного более приемлемой. Nix позволяет нам обеспечить воспроизводимость сборки наших проектов, переносимость между разными ОС, унифицировать сборку компонентов на различных языках и так далее.


Наше приложение


Итак, начнём с приложения, которое мы хотим собрать. В нашем случае, это будет простая программа на языке Haskell, выводящая сообщение Hello world.


Наш Main.hs:


module Main wheremain :: IO ()main = putStrLn "Hello, World!"

Для сборки проекта без Nix мы используем утилиту stack (подробнее с ней можно ознакомиться здесь). В качестве описания проекта для stack требуется файл stack.yaml, содержащий список наших пакетов и resolver. Последнее это стабильный срез Hackage, базы пакетов для языка Haskell, в котором гарантируется, что все пакеты собираются и дружат друг с другом (NB подобных срезов крайне не хватает в других языках ): ).


stack.yaml:


resolver: lts-17.11packages:- hello-world

Рецепт сборки конкретного пакета находится в hello-world.cabal:


cabal-version:      2.4name:               hello-worldversion:            1.0synopsis:           Hello Worldlicense:            MITlicense-file:       LICENSEauthor:             Nickexecutable hello-world    main-is:          Main.hs    build-depends:    base >= 4 && < 5    hs-source-dirs:   src    default-language: Haskell2010    ghc-options:      -Wall -O2

Из этих двух файлов наш код на Nix и будет черпать информацию о том, что именно и как собирать. В качестве эксперимента, можем проверить, что наш код действительно запускается и работает:


$ stack run hello-worldHello, World!

Come to the dar^Wnix side, we have cookies!


Сам по себе stack отличное средство для сборки проектов на Haskell, но в нём не хватает многих возможностей. Для сборки программ на Haskell для Nix есть библиотека haskell.nix, разработанная компанией IOHK. Её-то мы и будем здесь использовать. Для начала, сделаем так, чтобы наш проект собирался с помощью Nix.


Haskell.nix позволяет нам в несколько строчек преобразовать всю информацию о сборке нашего проекта из .cabal-файлов и stack.yaml в derivation для Nix.


nix/stackyaml.nix:


{  # Импортируем последнюю версию haskell.nix с GitHub и инициализируем Nixpkgs с её использованием.  haskellNix ? import (builtins.fetchTarball "https://github.com/input-output-hk/haskell.nix/archive/b0d03596f974131ab64d718b98e16f0be052d852.tar.gz") {}  # Здесь мы используем последнюю стабильную версию Nixpkgs. Версия 21.05 скоро выйдет :), nixpkgsSrc ? haskellNix.sources.nixpkgs-2009, nixpkgsArgs ? haskellNix.nixpkgsArgs, pkgs ? import nixpkgsSrc nixpkgsArgs}:let  # Создаём проект на базе stack. Для проектов Cabal есть функция cabalProject.  project = pkgs.haskell-nix.stackProject {    name = "hello-world";    # Derivation с исходным кодом проекта.    # Функция cleanGit копирует для сборки проекта только файлы, присутствующие в нашем git-репозитарии.    src = pkgs.haskell-nix.haskellLib.cleanGit {      name = "hello-world";      # Параметр src должен указывать на корневую директорию, содержащую stack.yaml.      src = ../.;      # keepGitDir оставляет директорию .git при сборке.      # Это может быть полезно, например, чтобы вставить хэш коммита в код.      keepGitDir = true;    };    # В параметре modules можно указать параметры сборки как для всех модулей сразу, так и для каждого в отдельности.    modules = [{      # doCheck отвечает за запуск юнит-тестов при сборке проекта, в том числе содержащихся во всех зависимостях.      # Здесь мы этого хотим избежать, поэтому этот параметр лучше всего ставить false и включить только для нужных      # пакетов.      doCheck = false;      # Добавим для нашего Hello World флаг -Werror.      packages.hello-world.components.exes.hello-world.ghcOptions = [ "-Werror" ];    }];  };# Наружу из этого файла мы выставляем project -- наш проект, а также pkgs -- срез nixpkgs, который мы будем использовать дальше.in { inherit project; inherit pkgs; }

Давайте проверим, что наш проект теперь можно собрать через Nix. Для этого достаточно команды nix build. Как и всегда, в текущей директории будет создана символическая ссылка result, содержащая результаты сборки.


$ nix build project.hello-world.components.exes$ ./result/bin/hello-worldHello, World!

Отлично! Небольшой магией выше мы обеспечили полностью воспроизводимую сборку нашего проекта, вплоть до всех системных зависимостей. Поехали дальше!


Dockerfile? Какой Dockerfile?


Сейчас 2021 год, и очень многие компании используют Docker для деплоя и запуска сервисов. Typeable здесь не будет исключением. В составе nixpkgs есть весьма удобный инструментарий для сборки контейнеров под названием dockerTools. Более подробно с его возможностями можно ознакомиться по ссылке, я лишь покажу, как мы с его помощью упаковываем наш код в контейнеры. Полностью код можно посмотреть в файле nix/docker.nix.


Для начала нам понадобится исходный контейнер, в который мы закинем всё что нам нужно. Nix позволяет собрать контейнер полностью с нуля без каких-либо лишних компонентов, но, тем не менее, этот подход не всегда удобен. Иногда, особенно во внештатных ситуациях, приходится залезать в контейнер руками через командную строку. Поэтому здесь мы используем CentOS.


sourceImage = dockerTools.pullImage {  imageName = "centos";  imageDigest = "sha256:e4ca2ed0202e76be184e75fb26d14bf974193579039d5573fb2348664deef76e";  sha256 = "1j6nplfs6999qmbhjkaxwjgdij7yf31y991sna7x4cxzf77k74v3";  finalImageTag = "7";  finalImageName = "centos";};

Здесь всё очевидно для всех, кто когда-либо работал с Docker. Мы говорим Nix, какой образ из публичного Docker Registry мы хотим использовать и что дальше мы будем на него ссылаться как на sourceImage.


Для сборки самого образа в dockerTools есть функция buildImage. У неё довольно много параметров, и часто проще написать свою обёртку над ней, что мы и сделаем:


makeDockerImage = name: revision: packages: entryPoint:  dockerTools.buildImage {    name = name;    tag = revision;    fromImage = sourceImage;    contents = (with pkgs; [ bashInteractive coreutils htop strace vim ]) ++ packages;    config.Cmd = entryPoint;  };

Наша функция makeDockerImage принимает четыре параметра: имя контейнера, его версия (в Typeable мы обычно используем хэш коммита из git в качестве тега), пакеты, которые мы хотим включить, и точку входа при запуске контейнера. Внутри же мы ссылаемся на образ с CentOS как основу (fromImage), плюс добавляем всякие утилиты, крайне полезные при экстренных случаях.


И, наконец, создадим образ с нашим великолепным приложением.


hello-world = project.hello-world.components.exes.hello-world;helloImage = makeDockerImage "hello"   (if imageTag == null then "undefined" else imageTag)  [ hello-world ]  [ "${hello-world}/bin/hello-world"  ];

Для начала мы создадим алиас для нужного нам пакета, чтобы не писать project.hello-world... повсюду. Дальше, вызвав написанную ранее функцию makeDockerImage, мы создаём образ контейнера с пакетом hello-world. В качестве тэга будет указан параметр imageTag, передаваемый снаружи, либо "undefined" если ничего не передано.


Проверим сборку:


$ nix build --argstr imageTag 1.0 helloImage[4 built, 0.0 MiB DL] $ ls -l resultlrwxrwxrwx 1 user users 69 May 11 13:12 result -> /nix/store/56qqhiwahyi46g6mf355fjr1g6mcab0b-docker-image-hello.tar.gz

Через пару минут или даже быстрее мы получим символическую ссылку result, указывающую на наш готовый образ. Проверим, что всё получилось.


$ docker load < result 76241b8b0c76: Loading layer [==================================================>]  285.9MB/285.9MBLoaded image: hello:1.0$ docker run hello:1.0Hello, World!

Заключение


В итоге, с помощью сравнительно небольшого количества кода, у нас получилось сделать воспроизводимую сборку нашего проекта на Haskell. Точно так же, заменив haskell.nix на что-то другое, можно поступить с проектами на других языках: в nixpkgs есть встроенные средства для C/C++, Python, Node и других популярных языков.


В следующей статье цикла я расскажу о частых проблемах, которые возникают при работе с Nix. Stay tuned!

Подробнее..

Перевод Запускаем Golang на Jupyter Notebook

13.05.2021 16:14:30 | Автор: admin

Если вы знакомы с Python, то уже сталкивались с Jupyter Notebook или работали в нём по крайней мере один раз. Jupyter Notebook это удобный инструмент, позволяющий писать мини-код и отслеживать его выполнение. Он также помогает в документировании, ведении журнала и в том, чтобы поделиться своими работами с коллегами.

Неудивительно, что многие люди и крупные организации, такие как Netflix, для своих целей в разработке предпочитают Jupyter Notebook. Специально к старту нового потока курса по разработке на Go 26 мая мы решили поделиться переводом, автор которого рассказывает, как документировать проекты на Golang в Jupyter Notebook.


Если вы работаете на машине с Windows, потребуется установка Docker. Пожалуйста, следуйте этим инструкциям. Если вы работаете на Mac или Linux, вы можете либо использовать метод с docker выше, либо следовать процессам локальной установки, о которых я напишу ниже.

Содержание

  1. Установка.

  2. Запуск Jupyter Notebook.

  3. Написание простой программы.

Установка

Установка может показаться сложной, но я постараюсь сделать её как можно проще. Если при настройке вы столкнулись с какими-либо трудностями, пожалуйста, обратитесь к FAQ по устранению неполадок gophernote.

Я приведу три основных метода установки, хотя сам рекомендовал бы подход с *Docker*, поскольку он не зависит от операционной системы, то есть сможет работать на любой операционной системе.

1. Докер (рекомендация)

Вот основная команда:

$ docker run -it -p 8888:8888 -v /path/to/local/notebooks:/path/to/notebooks/in/docker gopherdata/gophernotes:latest-ds

Тег latest-ds указывает докеру, чтобы он извлёк версию пакета gophernotes, где уже установленные библиотеки Data Science, такие как GoNum, GoLearn и GoDa. Команда на вашей машине может выглядеть так:

$ docker run -it -p 8888:8888 -v /home/user/Documents/notebook:/notebook gopherdata/gophernotes:latest-ds

Затем вам будут предоставлены URL-адрес локального хоста подключённого блокнота и соответствующий ему токен. Скопируйте и вставьте его в свой браузер (например localhost:8888/?token=<your_given_token>).

Успешное монтирование Notebok в Docker (изображение от автора)Успешное монтирование Notebok в Docker (изображение от автора)

Кроме того, вы сможете увидеть папку notebook, которую указали при инициализации контейнера docker в браузере.

Папка блокнота, которую вы указали, когда инициализировали Docker (изображение от автора)Папка блокнота, которую вы указали, когда инициализировали Docker (изображение от автора)

Следуйте приведённым ниже инструкциям, если предпочитаете локальную установку. Однако они работают только для Linux и Mac. Машины с Windows в настоящее время не поддерживаются, и вы должны использовать вышеупомянутый метод Docker.

2. Linux

Вот команды локальной установки для Linux:

$ env GO111MODULE=on go get github.com/gopherdata/gophernotes$ mkdir -p ~/.local/share/jupyter/kernels/gophernotes$ cd ~/.local/share/jupyter/kernels/gophernotes$ cp "$(go env GOPATH)"/pkg/mod/github.com/gopherdata/gophernotes@v0.7.2/kernel/*  "."$ chmod +w ./kernel.json # in case copied kernel.json has no write permission$ sed "s|gophernotes|$(go env GOPATH)/bin/gophernotes|" < kernel.json.in > kernel.json

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

$ "$(go env GOPATH)"/bin/gophernotes

И вы сможете открыть блокнот этой командой:

$ jupyter --data-dir

3. Mac

Аналогично локальную установку для Mac можно выполнить, написав в терминале следующие команды:

$ env GO111MODULE=on go get github.com/gopherdata/gophernotes$ mkdir -p ~/Library/Jupyter/kernels/gophernotes$ cd ~/Library/Jupyter/kernels/gophernotes$ cp "$(go env GOPATH)"/pkg/mod/github.com/gopherdata/gophernotes@v0.7.2/kernel/*  "."$ chmod +w ./kernel.json # in case copied kernel.json has no write permission$ sed "s|gophernotes|$(go env GOPATH)/bin/gophernotes|" < kernel.json.in > kernel.json

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

$ "$(go env GOPATH)"/bin/gophernotes

Теперь можно открыть блокнот этой командой:

$ jupyter --data-dir

Фух, переварить такое довольно трудно. Переходим к частям веселее!

Запуск Jupyter Notebook

Теперь, когда вы настроили Gophernotes, перейдите в папку, где хотите хранить свои блокноты Golang, там мы создадим наш первый блокнот! В правом верхнем углу вы увидите новую кнопку. Нажмите на неё и выберите "Go" в качестве ядра блокнота.

Создание первого блокнота Go в Jupyter (изображение от автора)Создание первого блокнота Go в Jupyter (изображение от автора)

Как только вы сделаете это, вас встретит знакомый чистый блокнот Jupyter. Теперь первым делом нужно изменить название на My First Golang Notebook (или любое другое, как показано ниже):

Изменение названия блокнотаИзменение названия блокнота

Давайте напишем какую-нибудь простую программу в наш Golang Notebook.

Рекурсивный факториал

Первое, что делает большинство людей, начиная писать программу на Golang, импорт необходимых пакетов. Программа простая, поэтому давайте импортируем пакет fmt.

Импорт пакетов в блокнот GoИмпорт пакетов в блокнот Go

Теперь напишем рекурсивный факториал. Факториал числа n это произведение всех положительных целых чисел, меньших или равных n. Например 3!, то есть факториал числа 3, это 3 x 2 x 1 = 6. Записать функцию вычисления факториала можно в одну из ячеек Jupyter Notebook:

Рекурсивный факториал на GoРекурсивный факториал на Go

Осталось только запустить программу, которая распечатывает значение, вот так:

Вызов рекурсивной функции вычисления факториала и печать значенияВызов рекурсивной функции вычисления факториала и печать значения

Бонус

Мощь Jupyter Notebook в возможности аннотирования и комментирования без загромождения кодовой базы. Воспользоваться этими возможностями можно, изменив тип ячейки на markdown, то есть выделить ячейку, нажать ctrl+M и ввести соответствующие примечания.

Комментирование и аннотирование кодовой базы (изображение от автора)Комментирование и аннотирование кодовой базы (изображение от автора)

Заключение

Вот и всё. Теперь вы можете создать прототип приложения Golang и отслеживать его с помощью блокнота Jupyter. Вы можете поделиться этим блокнотом со своими друзьями или коллегами в целях документирования (или если вы хотите помочь новым разработчикам выполнить логический шаг, который выполняете, когда пишете код самостоятельно). Ссылка на репозиторий gophernotes.

А если вам не хочется ограничиваться столь простыми программами на Go, обратите внимание на специальный курс Backend-разработчик на Go. У всех поступивших на курс появляется поддержка в виде экспертов, готовых ответить на вопросы и пояснить нюансы языка. Хотите дополнить свой арсенал навыков умением кодить на GO добро пожаловать. А по ссылке можно ознакомиться с программой курса.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Знакомство с Docker

01.06.2021 08:12:59 | Автор: admin

Это первая статья в цикле Знакомство с Docker. Если вы раньше не работали с Docker, мы расскажем, что он из себя представляет.

Что такое Docker?

Docker - это инструмент DevOps для контейнеризации сервисов и процессов... Подождите... Подождите... Подождите! Что такое DevOps? Что такое контейнеризация? Какие услуги и процессы я могу контейнеризовать? Начнём с самого начала.

DevOps можно понимать как концепцию, объединяющую группы разработчиков и администраторов. Проще говоря, разработчики - это люди, которые пишут код, создают приложение, а администраторы - это инженеры, которые отвечают за доставку приложения, распределения ресурсов для него, резервного копирования данных, проверки качества, мониторинга и т. д. Таким образом, DevOps инженер - специалист, который создаёт мост между ними.

Контейнер - это не более чем процесс, который выполняется изолированно в операционной системе. У него есть собственная сеть, собственная файловая система и выделенная память. Вы можете подумать, а почему бы просто не использовать виртуальную машину? Что ж, виртуальная машина - это отдельная ОС, сильно загруженная множеством других процессов, которые могут вам никогда не понадобиться, вместо виртуализации всей операционной системы для запуска одной службы вы можете виртуализировать службу. Точнее говоря, вы можете создать легкую виртуальную среду для одной службы. Этими службами могут быть серверы Nginx, NodeJS или приложения angular. И Docker помогает нам в этом.

Название Docker происходит от слова док (dock). Док используется для погрузки и разгрузки грузов на кораблях. Здесь можно провести простую аналогию, груз может быть контейнерами, а корабль может быть нашей операционной системой. Все товары в составе груза изолированы от товаров другого груза и самого корабля. Точно так же в Docker процесс одного Docker контейнера (Docker Container) изолирован от процесса другого контейнера и самой операционной системы.

Как работает контейнеризация

Docker использует технологию Linux Containers (LXC) и механизмы ядра Linux. Поскольку у docker-контейнера нет собственной операционной системы, он полагается на хостовую операционную систему. Контейнер, созданный в Linux, может быть запущен в любом дистрибутиве Linux, но не может работать в Windows, и то же самое касается образа, созданного в Windows. Docker расширяет возможности LXC, но также использует контрольные группы (cgroups), которые позволяют ядру хоста разделять использование ресурсов (ЦП, память, дисковый ввод-вывод, сеть и т. д.) на уровни изоляции, называемые пространствами имён (namespaces).

Как создать Docker контейнер?

Чтобы создать Docker контейнер, нам нужно сначала создать архив, содержащий все файлы и зависимости, необходимые для нашего проекта. Этот архив называется Docker образом (Docker Image). Важно помнить, что после создания Docker образа его уже нельзя изменить или модифицировать.

Большое количество готовых образов можно найти в DockerHub, общедоступном репозитории Docker, который позволяет вам делиться своими образами или использовать образы, созданные другими людьми. Вы также можете создавать свои собственные образы и помещать их в свой частный репозиторий (например, Harbor). В дальнейшем этот образ будет использован для создания контейнеров. Один и тот же образ можно использовать для создания одного или нескольких контейнеров, используя Docker-CLI. А что такое Docker CLI спросите вы?

Рассмотрим архитектуру Docker,

  1. Docker демонслушает запросы Docker API и управляет всеми объектами Docker, такими как образы, контейнеры, сети и тома. Это основная служба Docker, которая необходима для работы контейнеров и других компонентов Docker. Если Docker демон перестанет работать, так же перестанут работать все запущенные контейнеры.

  2. Docker демон также предоставляет REST API. Различные инструменты могут использовать его для взаимодействия с демоном. Вы также можете создать приложение, для работы с Docker REST API.

  3. Docker-CLIэто инструмент командной строки, который позволяет вам общаться с демоном Docker черезREST API.

Сеть Docker

Docker предусматривает несколько режимов работы сети. Подробнее о работе сети можно прочитать в нашей статье Сеть контейнеров это не сложно.

  • Host networks -Контейнер Docker будет использовать сеть хоста, соответственно он не будет изолирован с точки зрения сети, это не коснётся изоляции контейнера в целом, например изоляции процессов и файловой системы.

  • Bridge networks - Позволяет изолировать ваши приложения, но они могут взаимодействовать между собой и принимать трафик снаружи, если включено сопоставления портов (port forwarding).

  • Overlay networks - Оверлейные сети соединяют вместе несколько демонов Docker и позволяют службам Docker Swarm взаимодействовать друг с другом. Docker Swarm (аналог Kubernetes) может использоваться, если у вас несколько серверов с Docker.

  • Macvlan networks - Позволяет назначить MAC-адрес контейнеру, чтобы он отображался как физическое устройство в вашей сети.

  • None -Сеть отсутствует, соответственно вы не сможете подключиться к контейнеру.

И всё же, почему стоит использовать Docker?

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

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

  • Вы можете ограничить потребляемые контейнером ресурсы - ЦП и память, это позволит запустить больше контейнеров на одном сервере.

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

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

  • Вы можете передать ваш образ коллегам из команды тестирования, они смогут создать несколько экземпляров приложения (контейнеров) из этого образа и выполнять необходимые тесты.

Вместо заключения

Это только первая статья из цикла знакомство с Docker. В следующих статьях мы расскажем о работе с командной строкой Docker и создании собственных образов.

Подробнее..

Перевод Антипаттерны деплоя в Kubernetes. Часть 2

04.06.2021 12:23:32 | Автор: admin

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

Список антипаттернов, которые мы рассмотрим:

  1. Использование образов с тегом latest

  2. Сохранение конфигурации внутри образов

  3. Использование приложением компонентов Kubernetes без необходимости

  4. Использование для деплоя приложений инструментов для развёртывания инфраструктуры

  5. Изменение конфигурации вручную

  6. Использование кubectl в качестве инструмента отладки

  7. Непонимание сетевых концепций Kubernetes

  8. Использование неизменяемых тестовых окружений вместо динамических сред

  9. Смешивание кластеров Production и Non-Production

  10. Развёртывание приложений без Limits

  11. Неправильное использование Health Probes

  12. Не используете Helm

  13. Не собираете метрики приложений, позволяющие оценить их работу

  14. Отсутствие единого подхода к хранению конфиденциальных данных

  15. Попытка перенести все ваши приложения в Kubernetes

6. Использование кubectl в качестве инструмента отладки

Утилита kubectl незаменима при работе с кластерами Kubernetes. Но не стоит использовать её как единственный отладочный инструмент.

При возникновении проблем, особенно в production кластере, вашим первым импульсом не должен быть запуск kubectl. Если вы поступаете именно так, то вы уже проиграли битву, особенно если сейчас 3 часа ночи, сервисы не работают, и вы на связи.

kubectl get nskubectl get pods -n saleskubectl describe pod prod-app-1233445 -n saleskubectl get svc - n saleskubectl describe...

Вместо этого все ваши кластеры Kubernetes должны иметь надлежащие системы мониторинга, трассировки и сбора логов, которые можно использовать для своевременного выявления проблем. Если вам нужно запустить kubectl для проверки чего-либо, это означает, что вы не учли это, настраивая вашу систему мониторинга.

Сложно переоценить ценность метрик и трассировок, мы расскажем о них отдельно в одном из следующих антипаттернов.

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

Например, обратите внимание на Kubevious, это универсальная панель управления Kubernetes, которая позволяет вам искать и отображать ресурсы Kubernetes в соответствии с настраиваемыми правилами.

7. Непонимание сетевых концепций Kubernetes

Прошли те времена, когда единственный балансировщик нагрузки был всем, что необходимо для настройки подключения к вашему приложению. Kubernetes представляет свою собственную сетевую модель, и вам нужно разобраться с её основными концепциями. По крайней мере, вы должны быть знакомы с типами сервисов - Load Balancer, Cluster IP, Node Port и понимать, чем сервисы отличаются от Ingress.

Сервисы типа Сluster IP используются внутри кластера, Node Port также могут использоваться внутри кластера, но при этом доступны снаружи, а балансировщики нагрузки могут принимать только входящий трафик.

Но это ещё не всё потребуется разобраться, как в кластере Kubernetes работает DNS. И не забывать, что весь трафик между служебными компонентами шифруется и нужно контролировать срок действия сертификатов.

Будет полезно разобраться, что такое Service Mesh и какие проблемы она решает. Необязательно разворачивать Service Mesh в каждом кластере Kubernetes. Но желательно понимать, как она работает и зачем вам это нужно.

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

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

8. Использование неизменяемых тестовых окружений вместо динамических сред

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

Один из наиболее распространенных подходов наличие как минимум трех сред (QA / Staging / Production), в зависимости от размера компании их может быть больше. Наиболее важной из них является интеграционная, которая содержит результат слияния всех обновлений с основной веткой.

Когда вы используете неизменяемые тестовые среды, вы платите за них, даже если они не используются. Но наиболее серьёзной проблемой является изоляция обновлений для последующего тестирования.

Если вы используете единое окружение для интеграции, когда несколько разработчиков выполняют слияние результатов работы и что-то ломается, не сразу становится понятно, какая из функций вызвала проблему. Если 3 разработчика объединяют свои функции в промежуточной среде и развертывание завершается неудачно (либо сборка завершается неудачно, либо тесты интеграции терпят неудачу, либо показатели взрываются...), тогда существует несколько сценариев:

  1. Обновление A содержит ошибки, B и C в порядке

  2. Обновление B содержит ошибки, A и C в порядке

  3. Обновление C содержит ошибки, B и A в порядке

  4. Каждое обновление в отдельности работает, но при работе вместе с обновлениями A и B возникает ошибка

  5. Каждое обновление в отдельности работает, но при работе вместе с обновлениями A и C возникает ошибка

  6. Каждое обновление в отдельности работает, но при работе вместе с обновлениями B и C возникает ошибка

  7. Каждое обновление в отдельности работает, но все 3 обновления вместе не работают

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

Способы избежать подобных проблем:

  1. "Резервирование" промежуточного (stage) окружения разработчиками, чтобы они имели возможность тестировать свои обновления изолированно.

  2. Компания создает несколько staging сред, которые разработчики используют для тестирования своих обновлений (поскольку единственное staging окружение может быстро стать узким местом).

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

Кроме того, несколько промежуточных (staging) сред могут страдать от проблемы дрейфа конфигурации, когда среды должны быть одинаковыми, но после нескольких изменений это уже не так.

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

Есть много способов реализовать этот сценарий, по крайней мере, каждый запрос Pull Request должен создавать динамическую среду, содержащую только этот запрос Pull Request и ничего больше. Среда автоматически уничтожается, когда Pull Request объединяется / закрывается или по прошествии определенного времени.

Преимущество динамических сред это полная свобода разработчиков. Если я разработчик и только что закончил работу с обновлением A, а мой коллега завершил работу над обновлением B, я должен иметь возможность:

git checkout mastergit checkout -b feature-a-b-togethergit merge feature-agit merge feature-bgit push origin feature-a-b-together

Как по волшебству, будет создана динамическая среда:feature-a-b-together.staging.company.comилиstaging.company.com/feature-a-b-together.

Для создания динамических сред вы можете использовать, например, пространства имен Kubernetes.

Обратите внимание, что это нормально, если у вашей компании есть постоянные staging среды для специализированных нужд, таких как нагрузочное тестирование, тестирование на проникновения, развертывание A / B и т. д. Но для базового сценария Я разработчик и хочу, чтобы моё обновление работало изолированно и запускались интеграционные тесты динамические среды лучшее решение.

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

9. Смешивание кластеров Production и Non-Production

Несмотря на то, что Kubernetes специально разработан для оркестрирования кластеров, вы не должны создавать единый кластер для всех ваших окружений. Как минимум у вас должно быть два кластера: производственный и непроизводственный.

Прежде всего, смешивание Production и Non-Production кластеров может привести к нехватке ресурсов.

Так как приложение, работающее некорректно, может утилизировать слишком много ресурсов.

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

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

Если вы объедините этот антипаттерн со вторым (сохранение конфигурации внутри образов), должно стать очевидно, что может произойти множество негативных сценариев, например:

  1. Разработчик создает новое пространство имен, используемое для тестирования и production.

  2. Далее развертывается приложение и запускаются интеграционные тесты.

  3. Интеграционные тесты записывают фиктивные данные или очищают БД.

  4. К сожалению, в контейнерах были рабочие URL-адреса и конфигурация внутри них, и, таким образом, все интеграционные тесты фактически повлияли на работу production!

Чтобы не попасть в эту ловушку, гораздо проще просто создать Production и Non-Production кластера. К сожалению, многие руководства описывают сценарий при котором, пространства имен можно использовать для разделения сред, и даже в официальной документации Kubernetes есть примеры с пространствами имен prod / dev.

Обратите внимание, что в зависимости от размера вашей компании у вас может быть больше кластеров, чем два, например:

  1. Production

  2. Pre Production (Shadow) клон production, но с меньшими ресурсами

  3. Development кластер, который мы уже обсуждали выше

  4. Специализированный кластер для нагрузочного тестирования / тестирования безопасности

  5. И даже отдельный кластер для служебных инструментов

10. Развёртывание приложений без Limits

По умолчанию приложение, развернутое в Kubernetes, не имеет ограничений на использование ресурсов. Это означает, что ваше приложение потенциально может мешать работе всего кластера, поскольку утечка памяти или высокая загрузка ЦП нанесут ущерб остальным приложениям, работающим в кластере.

Это означает, что все ваши приложения (независимо от окружения) должны иметь ограничения на использование ресурсов.

К сожалению, просто взглянуть на среднюю загрузку памяти и ЦП приложением недостаточно. Вам необходимо изучить свое приложение и отследить всплески трафика и нагрузки, а также понять, как оно ведёт себя в этих условиях. Вы же не хотите, чтобы ваше приложение перезапускалось без причины.

Одно из преимуществ Kubernetes эластичность ресурсов. Если кластер убивает / перезапускает ваше приложение, когда оно начинает обрабатывать значительную нагрузку (например, ваш интернет-магазин испытывает всплеск трафика), вы не используете все преимущества Kubenetes. Вы можете решить эту проблему с использованием vertical pod auto-scaler (VPA).

Подробнее..

Перевод Антипаттерны деплоя в Kubernetes. Часть 1

19.05.2021 20:18:09 | Автор: admin
Антипаттерны деплоя в KubernetesАнтипаттерны деплоя в Kubernetes

В предыдущей статье 10 Docker anti-patterns мы рассказали о популярных ошибках при создании образов контейнеров. Однако создание образов для вашего приложения - это только половина дела. Вам нужен способ развёртывания этих контейнеров в производственной среде. Использование кластеров Kubernetes для решения этой задачи уже стало стандартом.

Представляем аналогичное руководство для Kubernetes. Теперь вы сможете составить полную картину того, как создать образ контейнера и как правильно его развернуть (при этом избежав некоторых распространенных ошибок).

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

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

Список антипаттернов, которые мы рассмотрим:

  1. Использование образов с тегом latest

  2. Сохранение конфигурации внутри образов

  3. Использование приложением компонентов Kubernetes без необходимости

  4. Использование для деплоя приложений инструментов для развёртывания инфраструктуры

  5. Изменение конфигурации вручную

  6. Использование кubectl в качестве инструмента отладки

  7. Непонимание сетевых концепций Kubernetes

  8. Использование неизменяемых тестовых окружений вместо динамических сред

  9. Смешивание кластеров Production и Non-Production

  10. Развёртывание приложений без Limits

  11. Неправильное использование Health Probes

  12. Не используете Helm

  13. Не собираете метрики приложений, позволяющие оценить их работу

  14. Отсутствие единого подхода к хранению конфиденциальных данных

  15. Попытка перенести все ваши приложения в Kubernetes

Желательно ознакомится с упомянутым руководством 10 Docker anti-patterns, поскольку некоторые из указанных выше антипаттернов будут ссылаться на него.

1. Использовать образы с тегом latest

Если вы уже имеете опыт создания собственных образов, эта рекомендация вас скорее всего не удивит. Создание образов с тегом "latest" в большинстве случаев является антипаттерном, поскольку "latest" - это просто имя тега (может быть присвоено любому образу в репозитории по решению владельца). Несмотря на первое впечатление, тег "latest" не означает "последний созданный". Это всего лишь тег по умолчанию, который используется, если вы указываете имя образа без тега.

Использование тега "latest" в Deployment ещё хуже, потому что в результате вы не будете знать из какого образа были запущены контейнеры.

apiVersion: apps/v1kind: Deploymentmetadata:  name: my-bad-deploymentspec:  template:    metadata:      labels:        app: my-badly-deployed-app    spec:      containers:      - name: dont-do-this        image: docker.io/myusername/my-app:latest

Тэги образов могут меняться, поэтому тэг "latest", на самом деле, не несет в себе никакой полезной информации. Этот образ может быть создан 3 минуты назад, а может быть 3 месяца назад. Вам придётся проанализировать логи вашей системы CI или сохранить образ локально и изучить его содержимое, чтобы понять какую версию приложения он содержит.

Использование политики always pull policy вместе с тэгом "latest" может привести к непредсказуемому результату и даже быть опасным. Предположим, что ваш Pod работает не корректно и Kubernetes принимает решение пересоздать его на другом узле кластера (именно за это мы любим Kubernetes).

Kubernetes спланирует Pod, и, если pull policy позволяет, из репозитория будет загружен образ с тэгом "latest". Если за это время образ с тэгом "latest" в репозитории изменился, в новом Pod будет образ, который отличается от образов в остальных Pod этого Deployment. В большинстве случаев это не то, что вам нужно.

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

Если ваши процессы развёртывания каким-либо образом зависят от использования тегов "latest", вы сидите на бомбе замедленного действия.

Чтобы избежать этой проблемы необходимо выбрать стратегию назначения тегов и придерживаться её для всех приложений.

Рекомендации по выбору стратегии:

  • Использование тегов с версией приложения (например, docker.io/myusername/my-app:v1.0.1).

  • Использование тегов с Git hash (например, docker.io/myusername/my-app:acef3e). Это несложно реализовать, но по Git hash труднее определить версию приложения.

  • Тэг так же может содержать номер build, дату или время build. Но такой подход применяется довольно редко.

Важно помнить, что теги образов не должные изменяться. Образ, помеченный как v2.0.5, должен создаваться только один раз и перемещаться из одной среды в другую.

2. Сохранение конфигурации внутри образов

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

Этот подход хорошо себя зарекомендовал ещё до появления контейнеров. Позже он был включен в концепцию 12-factor app. Следуя этому подходу образы контейнеров должны создаваться только один раз, а затем перемещаться из одной среды в другую. Для этого образ не должен содержать настроек, связанных с конкретным окружением.

Если в вашем образе:

  • есть жёстко заданные IP-адреса

  • пароли или конфиденциальные данные

  • URL-адреса других сервисов

  • в тег содержит dev, qa, production

...значит вы попали в ловушку создания образов, зависящих от среды.

Это означает, что для каждого окружения вам придётся собирать образ заново. Так же может возникнуть ситуация, при которой вы развернули в production компоненты, которые не были протестированы ранее.

Решение этой проблемы очень простое. Создавайте "generic" образы, которые не содержат никаких данных о конкретном окружении. Для конфигурирования приложений, запущенных из таких образов, используйте сторонние инструменты Kubernetes Configmaps/Secrets, Hashicorp Consul, Apache Zookeeper и др.

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

3. Использование приложением компонентов Kubernetes без необходимости

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

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

Этот проблема встречается довольно часто, когда команды только начинают работать с Kubernetes.

Рассмотрим несколько подобных ситуаций:

  • ожидать определенного именования сервисов или предполагать наличие определенных открытых портов

  • получать информацию из Kubernetes labels и annotations

  • запрашивать из Pod информацию о его конфигурации (например, его ip адрес)

  • потребность в init или sidecar контейнерах для правильной работы

  • обращаться к сервисам, установленным в Kubernetes через API (например, использоватьVault APIдля получение Secret из HashiCorp Vault, установленного в кластере Kubernetes)

  • читать данные из локального kubeconfig

  • обращаться к Kubernetes API из приложения напрямую

Конечно, для некоторых приложений, работающих в Kubernetes (скажем, вы создаете собственный оператор), действительно нужен прямой доступ к сервисам Kubernetes. Но остальные 99% приложений должны полностью игнорировать тот факт, что они работают внутри Kubernetes.

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

Если вы разработчик, работающий над приложением, которое будет развернуто в Kubernetes, может возникнуть желание выполнить локальное тестирование в Kubernetes. Сегодня существует несколько решений для локального развертывания Kubernetes (minikube, microk8s, KinND и другие).

На самом деле, если ваше приложение правильно спроектировано, вам не понадобится Kubernetes для локального запуска интеграционных тестов. Вы можете запускать тесты в Docker или Docker Compose. При этом некоторые зависимости могут работать во внешнем кластере Kubernetes.

В качестве альтернативы вы также можете использовать любое из специализированных решений для локальной разработки Kubernetes, например Okteto,garden.io илиtilt.dev.

4. Использование для развёртывания приложений инструментов для развёртывания инфраструктуры

В последние годы распространение Terraform (и подобных инструментов, таких как Pulumi) привело к распространению подхода Infrastructure as Code, который позволяет командам описывать инфраструктуру в виде кода.

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

Многие команды, создают единый конвейер, который одновременно создает инфраструктуру (например, кластер Kubernetes) и развёртывает в нём приложения.

Хотя это прекрасно работает в теории (поскольку означает, что вы начинаете с нуля при каждом развёртывании), это довольно расточительно с точки зрения ресурсов и времени.

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

Если у вас есть один pipeline, который делает и то, и другое, вы разрушаете / создаёте инфраструктуру, которая не менялась, просто потому, что вы хотите развернуть новую версию приложения.

Pipeline, который развёртывает всё вместе (инфра / приложение), может занять 30 минут, в то время как pipeline, развёртывающий только приложение, может занять всего 5 минут. Вы тратите 25 дополнительных минут на каждое развёртывание без каких-либо причин, даже если инфраструктура не изменилась.

Второй недостаток заключается в том, что в случае возникновения проблем с этим pipeline непонятно, кто должен их устранять. Если я разработчик и хочу развернуть свое приложение в Kubernetes, меня не интересуют ошибки Terraform, виртуальные сети или тома хранения.

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

Правильным решением, конечно же, является разделение деплоя приложений и инфраструктуры по отдельным pipeline. Pipeine инфраструктуры будет запускаться реже, чем pipeline приложения, что ускорит развёртывание приложений (и сократит время выполнения).

Разработчики также будут знать, что, когда pipeline для деплоя приложения выходит из строя, им не нужно разбираться с ошибками инфраструктуры или заботиться о том, как был создан кластер Kubernetes. Администраторы могут настраивать pipeline, разворачивающий инфраструктуру, не затрагивая разработчиков. Каждый может работать независимо.

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

Классический пример - создание чего-либо с помощью Terraform, а затем передача вывода развёртывания (например, IP-адреса) остальной части конвейера в качестве входных данных для кода приложения. Если у вас есть это ограничение, это означает, что вы страдаете от предыдущего антипаттерна (привязка приложения инфраструктуре), и вам необходимо от неё избавиться.

5. Изменение конфигурации вручную

Дрейф конфигурации - хорошо известная проблема, существовавшая ещё до появления Kubernetes. Это происходит, когда две или более среды должны быть одинаковыми, но после определенных случайных развёртываний или изменений они перестают иметь одинаковую конфигурацию.

Со временем проблема становится ещё более критичной и может привести к серьёзным проблемам.

Kubernetes также может страдать от этой проблемы. Команда kubectl очень мощная и поставляется со встроенными командами apply/edit/patch, которые могут изменять ресурсы в работающем кластере.

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

Одна из наиболее частых причин неудачных развёртываний - это конфигурация среды. Например деплой в production может закончиться неудачей (даже если подобных проблем не было на stage), потому что конфигурация двух сред больше не совпадает.

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

Kubectl никогда не следует использовать для деплоя вручную. В соответствии с подходом GitOps вся конфигурация должна храниться в системе контроля версий.

Если все ваши развертывания происходят через Git commit:

  • У вас есть полная история того, что произошло в вашем кластере, в виде истории Git коммитов.

  • Вы точно знаете, что содержится в каждом кластере в любой момент времени и чем отличаются среды между собой.

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

  • Вы можете откатить инфраструктуры на одно из предшествующих состояний.

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

Подробнее..

Перевод Сеть контейнеров это не сложно

26.05.2021 22:23:37 | Автор: admin

Работа с контейнерами многим кажется волшебством, пришло время разобраться как работает сеть контейнеров. Мы покажем на примерах, что это совсем не сложно. Помните, что контейнеры - всего лишь изолированные процессы Linux.

В этой статье мы ответим на следующие вопросы:

  • Как виртуализировать сетевые ресурсы, чтобы контейнеры думали, что у каждого из них есть выделенный сетевой стек?

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

  • Как настроить сетевой доступ из контейнера во внешний мир (например, в Интернет)?

  • Как получить доступ к контейнерам, работающим на сервере, из внешнего мира (публикация портов)?

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

  • Network namespaces

  • Virtual Ethernet devices (veth)

  • Virtual network switches (bridge)

  • IP маршрутизация и преобразование сетевых адресов (NAT)

Нам потребуется немного сетевой магии и никакого кода ...

С чего начать?

Все примеры в статье были сделаны на виртуальной машине CentOS 8. Но вы можете выбрать тот дистрибутив, который вам нравится.

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

$ vagrant init centos/8$ vagrant up$ vagrant ssh[vagrant@localhost ~]$ uname -aLinux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64

Мы не будем использовать какое-либо популярное решение для контейнеризации (например, docker или podman). Вместо этого мы сосредоточимся на основных концепциях и воспользуемся минимальным набором инструментов для достижения наших учебных целей.

Изоляция контейнеров с помощью Network namespaces

Что составляет сетевой стек Linux? Ну, очевидно, набор сетевых устройств. Что еще? Набор правил маршрутизации. И не забываем про настройку netfilter, создадим необходимые правила iptables.

Напишем небольшой скрипт inspect-net-stack.sh:

#!/usr/bin/env bashecho "> Network devices"ip linkecho -e "\n> Route table"ip routeecho -e "\n> Iptables rules"iptables --list-rules

Но прежде, чем запускать его, давайте внесём изменения в правила iptables, чтобы идентифицировать их в дальнейшем:

$ sudo iptables -N ROOT_NS

Запускаем скрипт:

$ sudo ./inspect-net-stack.sh> Network devices1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff> Route tabledefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100> Iptables rules-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT-N ROOT_NS

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

Мы уже упоминали об одном из Linux namespaces, используемых для изоляции контейнеров, которое называет сетевое пространство имён (Network namespace). Если заглянуть в man ip-netns, то мы прочтём, что Network namespace логически является копией сетевого стека со своими собственными маршрутами, правилами брандмауэра и сетевыми устройствами. Мы не будем затрагивать другие Linux namespaces в этой статье и ограничимся только областью видимости сетевого стека.

Для создания Network namespace нам достаточно утилиты ip, которая входим в популярный пакет iproute2. Создадим новое сетевое пространство имён:

$ sudo ip netns add netns0$ ip netnsnetns0

Новое сетевое пространство имён создано, но как начать его использовать? Воспользуемся командой Linux под названием nsenter. Она осуществляет вход в одно или несколько указанных пространств имен, а затем выполняет в нём указанную программу:

$ sudo nsenter --net=/var/run/netns/netns0 bash# The newly created bash process lives in netns0$ sudo ./inspect-net-stack.sh> Network devices1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00> Route table> Iptables rules-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT

Приведённый выше пример показывает, что процесс bash, работающий внутри пространства имён netns0, видит совершенно другой сетевой стек. Отсутствуют правила маршрутизации, и правила iptables, есть только один loopback interface. Все идет по плану...

Подключаем контейнер к хосту через virtual Ethernet devices (veth)

Выделенный сетевой стек будет бесполезен, если к нему отсутствует доступ. К счастью, Linux предоставляет подходящее средство для этого - virtual Ethernet devices (veth)! Согласно man veth, veth-device - это виртуальные устройства Ethernet. Они работают как туннели между сетевыми пространствами имён для создания моста к физическому сетевому устройству в другом пространстве имён, а также могут использоваться как автономные сетевые устройства.

Виртуальные Ethernet устройства всегда работают парами. Создадим их прямо сейчас:

$ sudo ip link add veth0 type veth peer name ceth0

С помощью этой единственной команды мы только что создали пару взаимосвязанных виртуальных Ethernet устройств. Имена veth0 и ceth0 были выбраны произвольно:

$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff5: ceth0@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff6: veth0@ceth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff

И veth0, и ceth0 после создания находятся в сетевом стеке хоста (также называемом Root Network namespace). Чтобы связать корневое пространство имён с пространством имён netns0, нам нужно сохранить одно из устройств в корневом пространстве имён и переместить другое в netns0:

$ sudo ip link set ceth0 netns netns0# List all the devices to make sure one of them disappeared from the root stack$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff6: veth0@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff link-netns netns0

Как только мы включаем устройства и назначаем правильные IP-адреса, любой пакет, происходящий на одном из них, немедленно появляется на его одноранговом устройстве, соединяющем два пространства имён. Начнем с корневого пространства имён:

$ sudo ip link set veth0 up$ sudo ip addr add 172.18.0.11/16 dev veth0

Продолжим сnetns0:

$ sudo nsenter --net=/var/run/netns/netns0$ ip link set lo up  # whoops$ ip link set ceth0 up$ ip addr add 172.18.0.10/16 dev ceth0$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:005: ceth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000    link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff link-netnsid 0

Проверяем подключение:

# From netns0, ping root's veth0$ ping -c 2 172.18.0.11PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.038 ms64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms--- 172.18.0.11 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 58msrtt min/avg/max/mdev = 0.038/0.039/0.040/0.001 ms# Leave netns0$ exit# From root namespace, ping ceth0$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.073 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.046 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 3msrtt min/avg/max/mdev = 0.046/0.059/0.073/0.015 ms

Обратите внимание, если мы попытаемся проверить доступность любых других адресов из пространства имен netns0, у нас ничего не получится:

# Inside root namespace$ ip addr show dev eth02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute eth0       valid_lft 84057sec preferred_lft 84057sec    inet6 fe80::5054:ff:fee3:2777/64 scope link       valid_lft forever preferred_lft forever# Remember this 10.0.2.15$ sudo nsenter --net=/var/run/netns/netns0# Try host's eth0$ ping 10.0.2.15connect: Network is unreachable# Try something from the Internet$ ping 8.8.8.8connect: Network is unreachable

Для таких пакетов в таблице маршрутизации netns0 просто нет маршрута. В настоящий момент существует единственный маршрут до сети 172.18.0.0/16:

# From netns0 namespace:$ ip route172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

В Linux есть несколько способов заполнения таблицы маршрутизации. Один из них - извлечение маршрутов из подключенных напрямую сетевых интерфейсов. Помните, что таблица маршрутизации в netns0 была пустой сразу после создания пространства имен. Но затем мы добавили туда устройство ceth0 и присвоили ему IP-адрес 172.18.0.10/16. Поскольку мы использовали не простой IP-адрес, а комбинацию адреса и сетевой маски, сетевому стеку удалось извлечь из него информацию о маршрутизации. Каждый пакет, предназначенный для сети 172.18.0.0/16, будет отправлен через устройство ceth0. Но все остальные пакеты будут отброшены. Точно так же есть новый маршрут в корневом пространстве имен:

# From root namespace:$ ip route# ... omitted lines ...172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11

На этом этапе мы ответили на первый вопрос. Теперь мы знаем, как изолировать, виртуализировать и подключать сетевые стеки Linux.

Объединение контейнеров с помощью virtual network switch (bridge)

Вся идея контейнеризации сводится к эффективному совместному использованию ресурсов. То есть мы крайне редко запускаем на хосте единственный контейнер. Вместо этого стараемся запустить как можно больше изолированных процессов на одном хосте. Итак, чтобы произошло, если бы мы разместили несколько контейнеров на одном хосте, следуя описанному выше подходу? Добавим второй контейнер:

# From root namespace$ sudo ip netns add netns1$ sudo ip link add veth1 type veth peer name ceth1$ sudo ip link set ceth1 netns netns1$ sudo ip link set veth1 up$ sudo ip addr add 172.18.0.21/16 dev veth1$ sudo nsenter --net=/var/run/netns/netns1$ ip link set lo up$ ip link set ceth1 up$ ip addr add 172.18.0.20/16 dev ceth1

Проверим доступность:

# From netns1 we cannot reach the root namespace!$ ping -c 2 172.18.0.21PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.From 172.18.0.20 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.20 icmp_seq=2 Destination Host Unreachable--- 172.18.0.21 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 55mspipe 2# But there is a route!$ ip route172.18.0.0/16 dev ceth1 proto kernel scope link src 172.18.0.20# Leaving netns1$ exit# From root namespace we cannot reach the netns1$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 172.18.0.11 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.11 icmp_seq=2 Destination Host Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 23mspipe 2# From netns0 we CAN reach veth1$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 172.18.0.21PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.64 bytes from 172.18.0.21: icmp_seq=1 ttl=64 time=0.037 ms64 bytes from 172.18.0.21: icmp_seq=2 ttl=64 time=0.046 ms--- 172.18.0.21 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 33msrtt min/avg/max/mdev = 0.037/0.041/0.046/0.007 ms# But we still cannot reach netns1$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 172.18.0.10 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.10 icmp_seq=2 Destination Host Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 63mspipe 2

Что-то пошло не так... По какой-то причине мы не можем подключиться из netns1 к root namespace. А из root namespace мы не можем подключиться к netns1. Однако, поскольку оба контейнера находятся в одной IP-сети 172.18.0.0/16, есть доступ к veth1 хоста из контейнера netns0. Интересно...

Возможно, мы столкнулись с конфликтом маршрутов. Давайте проверим таблицу маршрутизации в root namespace:

$ ip route# ... omitted lines ...172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21

После добавления второй пары veth в таблице маршрутизации root namespace появился новый маршрут 172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21, но маршрут до этой подсети уже существовал! Когда второй контейнер пытается проверить связь с устройством veth1, используется первый маршрут и мы видим ошибку подключения. Если бы мы удалили первый маршрут sudo ip route delete 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11 и перепроверили подключение, то увидели бы обратную ситуацию, то есть подключение netns1 будет восстановлено, но netns0 останется в подвешенном состоянии.

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

Рассмотрим Linux Bridge - еще один виртуализированный сетевой объект! Linux Bridge ведёт себя как коммутатор. Он пересылает пакеты между подключенными к нему интерфейсами. А поскольку это коммутатор, то он работает на уровне L2 (то есть Ethernet).

Чтобы предыдущие этапы нашего эксперимента в дальнейшем не вносили путаницы, удалим существующие сетевые пространства имён:

$ sudo ip netns delete netns0$ sudo ip netns delete netns1# But if you still have some leftovers...$ sudo ip link delete veth0$ sudo ip link delete ceth0$ sudo ip link delete veth1$ sudo ip link delete ceth1

Заново создаём два контейнера. Обратите внимание, мы не назначаем IP-адреса новым устройствам veth0 и veth1:

$ sudo ip netns add netns0$ sudo ip link add veth0 type veth peer name ceth0$ sudo ip link set veth0 up$ sudo ip link set ceth0 netns netns0$ sudo nsenter --net=/var/run/netns/netns0$ ip link set lo up$ ip link set ceth0 up$ ip addr add 172.18.0.10/16 dev ceth0$ exit$ sudo ip netns add netns1$ sudo ip link add veth1 type veth peer name ceth1$ sudo ip link set veth1 up$ sudo ip link set ceth1 netns netns1$ sudo nsenter --net=/var/run/netns/netns1$ ip link set lo up$ ip link set ceth1 up$ ip addr add 172.18.0.20/16 dev ceth1$ exit

Убедимся, что на хосте нет новых маршрутов:

$ ip routedefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100

И, наконец, создадим bridge интерфейс:

$ sudo ip link add br0 type bridge$ sudo ip link set br0 up

Теперь подключим к нему veth0 и veth1:

$ sudo ip link set veth0 master br0$ sudo ip link set veth1 master br0

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

$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.259 ms64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.051 ms--- 172.18.0.20 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 0.051/0.155/0.259/0.104 ms
$ sudo nsenter --net=/var/run/netns/netns1$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.037 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.089 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 36msrtt min/avg/max/mdev = 0.037/0.063/0.089/0.026 ms

Прекрасно! Все отлично работает. При этом мы даже не настраивали интерфейсы veth0 и veth1. Мы назначили только два IP-адреса интерфейсам ceth0 и ceth1. Но поскольку они оба находятся в одном сегменте Ethernet (подключены к виртуальному коммутатору), существует возможность подключения на уровне L2:

$ sudo nsenter --net=/var/run/netns/netns0$ ip neigh172.18.0.20 dev ceth0 lladdr 6e:9c:ae:02:60:de STALE$ exit$ sudo nsenter --net=/var/run/netns/netns1$ ip neigh172.18.0.10 dev ceth1 lladdr 66:f3:8c:75:09:29 STALE$ exit

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

Настраиваем сетевой доступ из контейнера во внешний мир (IP routing and masquerading)

Сейчас контейнеры могут подключаться друг к другу. Но будут ли удачны подключения к хосту, то есть к корневому пространству имён?

$ sudo nsenter --net=/var/run/netns/netns0$ ping 10.0.2.15  # eth0 addressconnect: Network is unreachable

Интерфейс eth0 не доступен. Всё очевидно, в netns0 отсутствует маршрут для этого подключения:

$ ip route172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

Корневое пространство имён также не может взаимодействовать с контейнерами:

# Use exit to leave netns0 first:$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.From 213.51.1.123 icmp_seq=1 Destination Net UnreachableFrom 213.51.1.123 icmp_seq=2 Destination Net Unreachable--- 172.18.0.10 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 213.51.1.123 icmp_seq=1 Destination Net UnreachableFrom 213.51.1.123 icmp_seq=2 Destination Net Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms

Чтобы установить связь между корневым пространством имён и пространством имён контейнера, нам нужно назначить IP-адрес сетевому интерфейсу моста:

$ sudo ip addr add 172.18.0.1/16 dev br0

Теперь после того, как мы назначили IP-адрес интерфейсу моста, мы получили маршрут в таблице маршрутизации хоста:

$ ip route# ... omitted lines ...172.18.0.0/16 dev br0 proto kernel scope link src 172.18.0.1$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.036 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.049 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 11msrtt min/avg/max/mdev = 0.036/0.042/0.049/0.009 ms$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.059 ms64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.056 ms--- 172.18.0.20 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 4msrtt min/avg/max/mdev = 0.056/0.057/0.059/0.007 ms

Контейнер, вероятно, также получил возможность пинговать интерфейс моста, но они все ещё не могут связаться с хостом eth0. Нам нужно добавить маршрут по умолчанию для контейнеров:

$ sudo nsenter --net=/var/run/netns/netns0$ ip route add default via 172.18.0.1$ ping -c 2 10.0.2.15PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.036 ms64 bytes from 10.0.2.15: icmp_seq=2 ttl=64 time=0.053 ms--- 10.0.2.15 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 14msrtt min/avg/max/mdev = 0.036/0.044/0.053/0.010 ms# And repeat the change for netns1

Теперь наш хост является маршрутизатором, а интерфейс моста стал шлюзом по умолчанию для контейнеров.

Отлично, нам удалось добиться сетевой связности контейнеров с корневым пространством имён. Теперь давайте попробуем подключить их к внешнему миру. По умолчанию переадресация пакетов (ip packet forwarding), то есть функциональность маршрутизатора в Linux отключена. Нам нужно её включить

# In the root namespacesudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'

Теперь самое интересное - проверка подключения:

$ sudo nsenter --net=/var/run/netns/netns0$ ping 8.8.8.8# hangs indefinitely long for me...

Всё равно не работает. Мы что-то упустили? Если бы контейнер отправлял пакеты во внешний мир, сервер-получатель не смог бы отправлять пакеты обратно в контейнер, потому что IP-адрес контейнера является частным и правила маршрутизации для этого конкретного IP-адреса известны только в локальной сети. К тому же многие контейнеры в мире имеют один и тот же частный IP-адрес 172.18.0.10. Решение этой проблемы называется преобразованием сетевых адресов (NAT). Принцип работы, следующий - перед отправкой во внешнюю сеть пакеты, отправленные контейнерами, заменяют свои исходные IP-адреса (source IP addesses) на адрес внешнего интерфейса хоста. Хост также будет отслеживать все существующие сопоставления (mapping) и по прибытии будет восстанавливать IP-адреса перед пересылкой пакетов обратно в контейнеры. Звучит сложно, но у меня для вас хорошие новости! Нам нужна всего одна команда, чтобы добиться требуемого результата:

$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o br0 -j MASQUERADE

Команда довольно проста. Мы добавляем новое правило в таблицу nat цепочки POSTROUTING с просьбой выполнить MASQUERADE всех исходящих пакетов из сети 172.18.0.0/16, но не через интерфейс моста.

Проверьте подключение:

$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 8.8.8.8PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=43.2 ms64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms--- 8.8.8.8 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms
$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 8.8.8.8PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=43.2 ms64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms--- 8.8.8.8 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms

Помните, что политика iptables по умолчанию - ACCEPT для каждой цепочки, она может быть довольно опасной в реальных условиях:

sudo iptables -S-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT

В качестве хорошего примера Docker вместо этого ограничивает все по умолчанию, а затем разрешает только для известных маршрутов:

$ sudo iptables -t filter --list-rules-P INPUT ACCEPT-P FORWARD DROP-P OUTPUT ACCEPT-N DOCKER-N DOCKER-ISOLATION-STAGE-1-N DOCKER-ISOLATION-STAGE-2-N DOCKER-USER-A FORWARD -j DOCKER-USER-A FORWARD -j DOCKER-ISOLATION-STAGE-1-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT-A FORWARD -o docker0 -j DOCKER-A FORWARD -i docker0 ! -o docker0 -j ACCEPT-A FORWARD -i docker0 -o docker0 -j ACCEPT-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5000 -j ACCEPT-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2-A DOCKER-ISOLATION-STAGE-1 -j RETURN-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP-A DOCKER-ISOLATION-STAGE-2 -j RETURN-A DOCKER-USER -j RETURN$ sudo iptables -t nat --list-rules-P PREROUTING ACCEPT-P INPUT ACCEPT-P POSTROUTING ACCEPT-P OUTPUT ACCEPT-N DOCKER-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 5000 -j MASQUERADE-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER-A DOCKER -i docker0 -j RETURN-A DOCKER ! -i docker0 -p tcp -m tcp --dport 5005 -j DNAT --to-destination 172.17.0.2:5000$ sudo iptables -t mangle --list-rules-P PREROUTING ACCEPT-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT-P POSTROUTING ACCEPT$ sudo iptables -t raw --list-rules-P PREROUTING ACCEPT-P OUTPUT ACCEPT

Настроим сетевой доступ из внешнего мира в контейнеры (port publishing)

Публикация портов контейнеров для некоторых (или всех) интерфейсов хоста - популярная практика. Но что на самом деле означает публикация порта?

Представьте, что у нас есть сервис, работающий внутри контейнера:

$ sudo nsenter --net=/var/run/netns/netns0$ python3 -m http.server --bind 172.18.0.10 5000

Если мы попытаемся отправить HTTP-запрос этому сервису с хоста, все будет работать (ну, есть связь между корневым пространством имён и всеми интерфейсами контейнера, почему бы и нет?):

# From root namespace$ curl 172.18.0.10:5000<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"># ... omitted lines ...

Однако, если бы мы получили доступ к этому серверу из внешнего мира, какой IP-адрес мы бы использовали? Единственный IP-адрес, который мы можем знать, - это адрес внешнего интерфейса хоста eth0:

$ curl 10.0.2.15:5000curl: (7) Failed to connect to 10.0.2.15 port 5000: Connection refused

Таким образом, нам нужно найти способ перенаправить все пакеты, поступающие на порт 5000 интерфейса eth0 хоста, на адрес172.18.0.10:5000. Или, другими словами, нам нужно опубликовать порт 5000 контейнера на интерфейсе eth0 хоста.

# External trafficsudo iptables -t nat -A PREROUTING -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000# Local traffic (since it doesn't pass the PREROUTING chain)sudo iptables -t nat -A OUTPUT -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000

Кроме того, нам нужно включить iptables intercepting traffic over bridged networks (перехватывать трафик bridged networks):

sudo modprobe br_netfilter

Время проверить!

curl 10.0.2.15:5000<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"># ... omitted lines ...

Разбираемся в работе Docker network drivers

Но что же вам сделать теперь со всеми этими бесполезными знаниями? Например, мы могли бы попытаться разобраться в некоторых сетевых режимах Docker!

Начнем с режима --network host. Попробуйте сравнить вывод следующих команд ip link и sudo docker run -it --rm --network host alpine ip link. Сюрприз, они совпадут! Таким образом host mode Docker просто не использует изоляцию сетевого пространства имён и контейнеры работают в корневом сетевом пространстве имён и совместно используют сетевой стек с хост-системой.

Следующий режим, который нужно проверить, - это --network none. Вывод команды sudo docker run -it --rm --network none alpine ip link показывает только один сетевой интерфейс обратной loopback. Это очень похоже на наши наблюдения за только что созданным сетевым пространством имен. То есть до того момента, когда мы добавляли какие-либо veth устройства.

И последнее, но не менее важное: режим --network bridge (по умолчанию), это именно то, что мы пытались воспроизвести в этой статье.

Сети и rootless контейнеры

Одной из приятных особенностей диспетчера контейнеров podman является его ориентация на rootless контейнеры. Однако, как вы, вероятно, заметили, в этой статье мы использовали много эскалаций sudo и без root-прав настроить сеть невозможно. При настройке сетей rootful контейнеров Podman очень близок к Docker. Но когда дело доходит до rootless контейнеров, Podman полагается на проект slirp4netns:

Начиная с Linux 3.8, непривилегированные пользователи могут создавать network_namespaces (7) вместе с user_namespaces (7). Однако непривилегированные сетевые пространства имен оказались не очень полезными, потому что для создания пар veth (4) в пространствах имен хоста и сети по-прежнему требуются привилегии root (иначе доступ в Интернету будет отсутствовать).

slirp4netns позволяет получить доступ из сетевое пространства имен в Интернет непривилегированным пользователям, подключая устройство TAP в сетевом пространстве имен к стеку TCP/IP usermode (slirp).

Сеть rootless контейнера весьма ограничена: технически сам контейнер не имеет IP-адреса, потому что без привилегий root невозможно настроить сетевое устройство. Более того, проверка связи (ping) из rootless контейнера не работает, поскольку в нем отсутствует функция безопасности CAP_NET_RAW, которая необходима для работы команды ping.

Заключение

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

Подробнее..

Что будет после Docker

12.06.2021 08:11:13 | Автор: admin

Такой мыслью я задался, обдумывая и перечитывая свою статью про Docker для одного внутреннего корпблога. Этот материал -- продолжение этой крайне, крайне субъективной мысли, оформленное во что-то, что может понять и оценить не только я и моя мама-редактор. Это, конечно, не избавляет текст от моих предубеждений, впечатлений от тех или иных технологий, восторга юного, впечатлительного ума от всего нового и блестящего. Но главное, что кто-нибудь да сможет извлечь из неё что-то своё, описать своё мнение, или сделать свой новый инструментарий для контейнеризации.

Кроме того, отмечу, что у меня нет особых претензий к Docker. Просто хорошая прорывная технология, которая стала стандартом индустрии разработки и администрирования. Как bash или Python у сисадминов. Я постараюсь описывать недостатки докера только при сравнении с другими технологиями, и всё.

Почему вообще должно появиться что-то после Docker?

Окей, насчёт запрета на недостатки Docker я соврал, они будут. Но только пара пунктов -- остальные можете почитать в посте "Исповедь docker хейтера", с которыми я, в принципе, согласен. Так вот.

Отсутствие развития. Я не вижу, чтобы в Docker появлялось что-то принципиально новое в последние пару лет. Может, это из-за не очень понятной финансовой модели и вкладывания ресурсов в не то, чтобы перспективный Swarm. Вы сможете вспомнить хайлайты из последних ченжлогов докера? То-то же.

Спорные и местами даже предвзятые решения разработчиков. Дело в том, что Docker в современном дистрибутиве Linux это система в системе, которая активно перетягивает на себя одеяло остальных инструментов: правила фаерволла в iptables (firewalld? Не, не слышали!), инициализация и жизненный цикл приложений (другими словами, докер стремится стать systemd для контейнеров, то есть это сегодня нечто systemd-like в дистрибутиве Linux, где уже есть systemd? шта?), работа от непривелегированных пользователей (и я не про системную группу docker, через которую можно лезть в чужие контейнеры и хостовую систему вообще!), не очень удобный механизм сборки (Dockerfile, конечно, хорошо, но вы попробуйте построить замысловатый пайплайн с пробросом хостовых томов, например), в общем, список наберётся.

Так что же будет после Docker?

Во-первых, будет новый докер. Перефразирую слова из песни одной музыкальной группы: вначале был докер, в конце будет докер, всё повторяется снова, природа сурова. Вполне вероятно, что докер эволюционирует, что его команда и сообщество решат архитектурные и технические болячки, прикрутят сотню крутых фич, сохраняя при этом дистанцию от кубера, выкинут Docker Swarm и всё станет хорошо. Но это, разумеется, не точно. Честно говоря, я сомневаюсь в таком сценарии.

Во-вторых, будет иной докер. Пример сегодня живёт хорошей жизнью и вполне себе развивается -- Podman. С одной стороны, он создан по образу и подобию Docker: CLI мимикрирует под Docker CLI, есть режим эмуляции Docker API для совместимости с docker-compose, технологии под капотом примерно те же, да и написан он тоже на Go.
Но дальше идут различия: например, Podman построен на стандартах OCI, на которые ориентируются остальные инструменты, вроде Harbor container registry. Когда как Docker позволяет от себе отклоняться от стандартов в различных мелочах, просто потому что докер появился до этих стандартов и имеет право.
Это расхождение в стандарте и реализации иногда порождает проблемы. Так, например, я при работе с Podman столкнулся с тем, что Sonatype Nexus отказывается принимать подмановские docker-образы, и надо было использовать особый параметр, чтобы подман собирал образы с конкретно докеровским форматом, а не стандартом OCI. Так что стандарты -- круто, своя имплементация -- не всегда практично.

В-третьих, будет антидокер. Нет, это не аллюзия на антикафе, только где вы платите деньги за время, которое проводите в контейнере. Да, речь о Kubernetes, который расширяет Docker кучей абстракций, концепций, API и всего причитающегося.

В конце-концов, может, я преувеличил касательно Docker как bash современной разработки, и после Docker будет что-то совершенно иное. Может, это будет Serverless?

Каким мог бы быть совершенный Docker?

Для меня критерии совершенного инструмента для разработки и управления контейнерами следующие:

  • Он должен вбирать в себя всё хорошее, что уже есть в Docker, а именно -- отличные REST API и CLI.

  • В нём должны быть результаты уроков, извлечённые из горького опыта Docker, то есть большинство из того, что сделано в Podman.

  • Он должен быть ещё более тесно интегрирован с дистрибутивом Linux. Не так тесно, как systemd-nspawn, который вообще неотделим от systemd, но всё же.

  • Он должен выполнять только определённый круг задач: собирать контейнеры, запускать контейнеры, и ещё всякое по мелочи. Не надо кластеризации, не надо поддерживать сложные сценарии со множеством сетей или томов, максимум -- пробрасывать папки в хостовую файловую систему. Ну и какой-то функционал Docker Compose тоже не помешает.

  • Каким-то образом решить архитектурный косяк с dangling images. Типа, серьёзно, 2021-й год, а мне приходится в кронтабы плейбуков или systemd-таймеры вписывать задачу на docker image prune -f! Это должен делать или сам сервис, то есть самостоятельно маскировать проблему, или это надо решить на уровне дизайна системы работы с образами.

  • Удобная расширяемость через плагины! К сожалению, язык Go не имеет возможности писать расширяемые через классические плагины системы, как умеет Python или Java. Как фанат Kotlin, я нахожу расширяемость через плагины или скрипты (я имею ввиду Kotlin Script) очень клёвой.

У меня на этом всё.

С-спасибо за внимание, в-вы отличная публика (c)

Подробнее..

Как я сделал Discord бота для игровой гильдии с помощью .NET Core

05.06.2021 18:10:05 | Автор: admin
Батрак предупреждает о том что к гильдии присоединился игрокБатрак предупреждает о том что к гильдии присоединился игрок

Вступление

Всем привет! Недавно я написал Discord бота для World of Warcraft гильдии. Он регулярно забирает данные об игроках с серверов игры и пишет сообщения в Discord о том что к гильдии присоединился новый игрок или о том что гильдию покинул старый игрок. Между собой мы прозвали этого бота Батрак.

В этой статье я решил поделиться опытом и рассказать как сделать такой проект. По сути мы будем реализовывать микросервис на .NET Core: напишем логику, проведем интеграцию с api сторонних сервисов, покроем тестами, упакуем в Docker и разместим в Heroku. Кроме этого я покажу как реализовать continuous integration с помощью Github Actions.

От вас не потребуется никаких знаний об игре. Я написал материал так чтобы можно было абстрагироваться от игры и сделал заглушку для данных об игроках. Но если у вас есть учетная запись в Battle.net, то вы сможете получать реальные данные.

Для понимания материала, от вас ожидается хотя бы минимальный опыт создания веб сервисов с помощью фреймворка ASP.NET и небольшой опыт работы с Docker.

План

На каждом шаге будем постепенно наращивать функционал.

  1. Создадим новый web api проект с одним контроллером /check. При обращении к этому адресу будем отправлять строку Hello! в Discord чат.

  2. Научимся получать данные о составе гильдии с помощью готовой библиотеки или заглушки.

  3. Научимся сохранять в кэш полученный список игроков чтобы при следующих проверках находить различия с предыдущей версией списка. Обо всех изменениях будем писать в Discord.

  4. Напишем Dockerfile для нашего проекта и разместим проект на хостинге Heroku.

  5. Посмотрим на несколько способов сделать периодическое выполнение кода.

  6. Реализуем автоматическую сборку, запуск тестов и публикацию проекта после каждого коммита в master

Шаг 1. Отправляем сообщение в Discord

Нам потребуется создать новый ASP.NET Core Web API проект.

Создание нового проекта - это одна из фундаментальных вещей которые я не буду подробно расписывать. При работе над проектом используйте сервис Github для хранения кода. В дальнейшем мы воспользуемся несколькими возможностями Github.

Добавим к проекту новый контроллер

[ApiController]public class GuildController : ControllerBase{    [HttpGet("/check")]    public async Task<IActionResult> Check(CancellationToken ct)    {        return Ok();    }}

Затем нам понадобится webhook от вашего Discord сервера. Webhook - это механизм отправки событий. В данном случае, то это адрес к которому можно слать простые http запросы с сообщениями внутри.

Получить его можно в пункте integrations в настройках любого текстового канала вашего Discord сервера.

Создание webhookСоздание webhook

Добавим webhook в appsettings.json нашего проекта. Позже мы унесем его в переменные окружения Heroku. Если вы не знакомы с тем как работать с конфигурацией в ASP Core проектах предварительно изучите эту тему.

{"DiscordWebhook":"https://discord.com/api/webhooks/****/***"}

Теперь создадим новый сервис DiscordBroker, который умеет отправлять сообщения в Discord. Создайте папку Services и поместите туда новый класс, эта папка нам еще пригодится.

По сути этот новый сервис делает post запрос по адресу из webhook и содержит сообщение в теле запроса.

public class DiscordBroker : IDiscordBroker{    private readonly string _webhook;    private readonly HttpClient _client;    public DiscordBroker(IHttpClientFactory clientFactory, IConfiguration configuration)    {        _client = clientFactory.CreateClient();        _webhook = configuration["DiscordWebhook"];    }    public async Task SendMessage(string message, CancellationToken ct)    {        var request = new HttpRequestMessage        {            Method = HttpMethod.Post,            RequestUri = new Uri(_webhook),            Content = new FormUrlEncodedContent(new[] {new KeyValuePair<string, string>("content", message)})        };        await _client.SendAsync(request, ct);    }}

Как видите, мы используем внедрение зависимостей. IConfiguration позволит нам достать webhook из конфигов, а IHttpClientFactory создать новый HttpClient.

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

Не забудьте что новый класс нужно будет зарегистрировать в Startup.

services.AddScoped<IDiscordBroker, DiscordBroker>();

А также нужно будет зарегистрировать HttpClient, для работы IHttpClientFactory.

services.AddHttpClient();

Теперь можно воспользоваться новым классом в контроллере.

private readonly IDiscordBroker _discordBroker;public GuildController(IDiscordBroker discordBroker){  _discordBroker = discordBroker;}[HttpGet("/check")]public async Task<IActionResult> Check(CancellationToken ct){  await _discordBroker.SendMessage("Hello", ct);  return Ok();}

Запустите проект, зайдите по адресу /check в браузере и убедитесь что в Discord пришло новое сообщение.

Шаг 2. Получаем данные из Battle.net

У нас есть два варианта: получать данные из настоящих серверов battle.net или из моей заглушки. Если у вас нет аккаунта в battle.net, то пропустите следующий кусок статьи до момента где приводится реализация заглушки.

Получаем реальные данные

Вам понадобится зайти на https://develop.battle.net/ и получить там две персональных строки BattleNetId и BattleNetSecret. Они будут нужны нам чтобы авторизоваться в api перед отправкой запросов. Поместите их в appsettings.

Подключим к проекту библиотеку ArgentPonyWarcraftClient.

Создадим новый класс BattleNetApiClient в папке Services.

public class BattleNetApiClient{   private readonly string _guildName;   private readonly string _realmName;   private readonly IWarcraftClient _warcraftClient;   public BattleNetApiClient(IHttpClientFactory clientFactory, IConfiguration configuration)   {       _warcraftClient = new WarcraftClient(           configuration["BattleNetId"],           configuration["BattleNetSecret"],           Region.Europe,           Locale.ru_RU,           clientFactory.CreateClient()       );       _realmName = configuration["RealmName"];       _guildName = configuration["GuildName"];   }}

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

Кроме этого, нужно создать в appsettings проекта две новых записи RealmName и GuildName. RealmName это название игрового мира, а GuildName это название гильдии. Их будем использовать как параметры при запросе.

Сделаем метод GetGuildMembers чтобы получать состав гильдии и создадим модель WowCharacterToken которая будет представлять собой информацию об игроке.

public async Task<WowCharacterToken[]> GetGuildMembers(){   var roster = await _warcraftClient.GetGuildRosterAsync(_realmName, _guildName, "profile-eu");   if (!roster.Success) throw new ApplicationException("get roster failed");   return roster.Value.Members.Select(x => new WowCharacterToken   {       WowId = x.Character.Id,       Name = x.Character.Name   }).ToArray();}
public class WowCharacterToken{  public int WowId { get; set; }  public string Name { get; set; }}

Класс WowCharacterToken следует поместить в папку Models.

Не забудьте подключить BattleNetApiClient в Startup.

services.AddScoped<IBattleNetApiClient, BattleNetApiClient>();

Берем данные из заглушки

Для начала создадим модель WowCharacterToken и поместим ее в папку Models. Она представляет собой информацию об игроке.

public class WowCharacterToken{  public int WowId { get; set; }  public string Name { get; set; }}

Дальше сделаем вот такой класс

public class BattleNetApiClient{    private bool _firstTime = true;    public Task<WowCharacterToken[]> GetGuildMembers()    {        if (_firstTime)        {            _firstTime = false;            return Task.FromResult(new[]            {                new WowCharacterToken                {                    WowId = 1,                    Name = "Артас"                },                new WowCharacterToken                {                    WowId = 2,                    Name = "Сильвана"                }            });        }        return Task.FromResult(new[]        {            new WowCharacterToken            {                WowId = 1,                Name = "Артас"            },            new WowCharacterToken            {                WowId = 3,                Name = "Непобедимый"            }        });    }}

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

Сделайте интерфейс и подключите все что мы создали в Startup.

services.AddScoped<IBattleNetApiClient, BattleNetApiClient>();

Выведем результаты в Discord

После того как мы сделали BattleNetApiClient, им можно воспользоваться в контроллере чтобы вывести кол-во игроков в Discord.

[ApiController]public class GuildController : ControllerBase{  private readonly IDiscordBroker _discordBroker;  private readonly IBattleNetApiClient _battleNetApiClient;  public GuildController(IDiscordBroker discordBroker, IBattleNetApiClient battleNetApiClient)  {     _discordBroker = discordBroker;     _battleNetApiClient = battleNetApiClient;  }  [HttpGet("/check")]  public async Task<IActionResult> Check(CancellationToken ct)  {     var members = await _battleNetApiClient.GetGuildMembers();     await _discordBroker.SendMessage($"Members count: {members.Length}", ct);     return Ok();  }}

Шаг 3. Находим новых и ушедших игроков

Нужно научиться определять какие игроки появились или пропали из списка при последующих запросах к api. Для этого мы можем закэшировать список в InMemory кэше (в оперативной памяти) или во внешнем хранилище.

Если закэшировать список в InMemory кэше, то мы потеряем его при перезапуске приложения. Поэтому позже мы подключим базу данных Redis как аддон в Heroku и будем кешировать туда.

А пока что подключим InMemory кэш в Startup.

services.AddMemoryCache(); 

Теперь в нашем распоряжении есть IDistributedCache, который можно подключить через конструктор. Я предпочел не использовать его напрямую , а написать для него обертку. Создайте класс GuildRepository и поместите его в новую папку Repositories.

public class GuildRepository : IGuildRepository{    private readonly IDistributedCache _cache;    private const string Key = "wowcharacters";    public GuildRepository(IDistributedCache cache)    {        _cache = cache;    }    public async Task<WowCharacterToken[]> GetCharacters(CancellationToken ct)    {        var value = await _cache.GetAsync(Key, ct);        if (value == null) return Array.Empty<WowCharacterToken>();        return await Deserialize(value);    }    public async Task SaveCharacters(WowCharacterToken[] characters, CancellationToken ct)    {        var value = await Serialize(characters);        await _cache.SetAsync(Key, value, ct);    }        private static async Task<byte[]> Serialize(WowCharacterToken[] tokens)    {        var binaryFormatter = new BinaryFormatter();        await using var memoryStream = new MemoryStream();        binaryFormatter.Serialize(memoryStream, tokens);        return memoryStream.ToArray();    }    private static async Task<WowCharacterToken[]> Deserialize(byte[] bytes)    {        await using var memoryStream = new MemoryStream();        var binaryFormatter = new BinaryFormatter();        memoryStream.Write(bytes, 0, bytes.Length);        memoryStream.Seek(0, SeekOrigin.Begin);        return (WowCharacterToken[]) binaryFormatter.Deserialize(memoryStream);    }}

Теперь можно написать сервис который будет сравнивать новый список игроков с сохраненным.

public class GuildService{    private readonly IBattleNetApiClient _battleNetApiClient;    private readonly IGuildRepository _repository;    public GuildService(IBattleNetApiClient battleNetApiClient, IGuildRepository repository)    {        _battleNetApiClient = battleNetApiClient;        _repository = repository;    }    public async Task<Report> Check(CancellationToken ct)    {        var newCharacters = await _battleNetApiClient.GetGuildMembers();        var savedCharacters = await _repository.GetCharacters(ct);        await _repository.SaveCharacters(newCharacters, ct);        if (!savedCharacters.Any())            return new Report            {                JoinedMembers = Array.Empty<WowCharacterToken>(),                DepartedMembers = Array.Empty<WowCharacterToken>(),                TotalCount = newCharacters.Length            };        var joined = newCharacters.Where(x => savedCharacters.All(y => y.WowId != x.WowId)).ToArray();        var departed = savedCharacters.Where(x => newCharacters.All(y => y.Name != x.Name)).ToArray();        return new Report        {            JoinedMembers = joined,            DepartedMembers = departed,            TotalCount = newCharacters.Length        };    }}

В качестве возвращаемого результата используется модель Report. Ее нужно создать и поместить в папку Models.

public class Report{   public WowCharacterToken[] JoinedMembers { get; set; }   public WowCharacterToken[] DepartedMembers { get; set; }   public int TotalCount { get; set; }}

Применим GuildService в контроллере.

[HttpGet("/check")]public async Task<IActionResult> Check(CancellationToken ct){   var report = await _guildService.Check(ct);   return new JsonResult(report, new JsonSerializerOptions   {      Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.Cyrillic)   });}

Теперь отправим в Discord какие игроки присоединились или покинули гильдию.

if (joined.Any() || departed.Any()){   foreach (var c in joined)      await _discordBroker.SendMessage(         $":smile: **{c.Name}** присоединился к гильдии",         ct);   foreach (var c in departed)      await _discordBroker.SendMessage(         $":smile: **{c.Name}** покинул гильдию",         ct);}

Эту логику я добавил в GuildService в конец метода Check. Писать бизнес логику в контроллере не стоит, у него другое назначение. В самом начале мы делали там отправку сообщения в Discord потому что еще не существовало GuildService.

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

await _warcraftClient.GetCharacterProfileSummaryAsync(_realmName, name.ToLower(), Namespace);

Я решил не добавлять в статью больше кода в BattleNetApiClient, чтобы статья не разрослась до безумных размеров.

Unit тесты

У нас появился класс GuildService с нетривиальной логикой, который будет изменяться и расширяться в будущем. Стоит написать на него тесты. Для этого нужно будет сделать заглушки для BattleNetApiClient, GuildRepository и DiscordBroker. Я специально просил создавать интерфейсы для этих классов чтобы можно было сделать их фейки.

Создайте новый проект для Unit тестов. Заведите в нем папку Fakes и сделайте три фейка.

public class DiscordBrokerFake : IDiscordBroker{   public List<string> SentMessages { get; } = new();   public Task SendMessage(string message, CancellationToken ct)   {      SentMessages.Add(message);      return Task.CompletedTask;   }}
public class GuildRepositoryFake : IGuildRepository{    public List<WowCharacterToken> Characters { get; } = new();    public Task<WowCharacterToken[]> GetCharacters(CancellationToken ct)    {        return Task.FromResult(Characters.ToArray());    }    public Task SaveCharacters(WowCharacterToken[] characters, CancellationToken ct)    {        Characters.Clear();        Characters.AddRange(characters);        return Task.CompletedTask;    }}
public class BattleNetApiClientFake : IBattleNetApiClient{   public List<WowCharacterToken> GuildMembers { get; } = new();   public List<WowCharacter> Characters { get; } = new();   public Task<WowCharacterToken[]> GetGuildMembers()   {      return Task.FromResult(GuildMembers.ToArray());   }}

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

Первый тест на GuildService будет выглядеть так:

[Test]public async Task SaveNewMembers_WhenCacheIsEmpty(){   var wowCharacterToken = new WowCharacterToken   {      WowId = 100,      Name = "Sam"   };      var battleNetApiClient = new BattleNetApiApiClientFake();   battleNetApiClient.GuildMembers.Add(wowCharacterToken);   var guildRepositoryFake = new GuildRepositoryFake();   var guildService = new GuildService(battleNetApiClient, null, guildRepositoryFake);   var changes = await guildService.Check(CancellationToken.None);   changes.JoinedMembers.Length.Should().Be(0);   changes.DepartedMembers.Length.Should().Be(0);   changes.TotalCount.Should().Be(1);   guildRepositoryFake.Characters.Should().BeEquivalentTo(wowCharacterToken);}

Как видно из названия, тест позволяет проверить что мы сохраним список игроков, если кэш пуст. Заметьте, в конце теста используется специальный набор методов Should, Be... Это методы из библиотеки FluentAssertions, которые помогают нам сделать Assertion более читабельным.

Теперь у нас есть база для написания тестов. Я показал вам основную идею, дальнейшее написание тестов оставляю вам.

Главный функционал проекта готов. Теперь можно подумать о его публикации.

Шаг 4. Привет Docker и Heroku!

Мы будем размещать проект на платформе Heroku. Heroku не позволяет запускать .NET проекты из коробки, но она позволяет запускать Docker образы.

Чтобы упаковать проект в Docker нам понадобится создать в корне репозитория Dockerfile со следующим содержимым

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS builderWORKDIR /sourcesCOPY *.sln .COPY ./src/peon.csproj ./src/COPY ./tests/tests.csproj ./tests/RUN dotnet restoreCOPY . .RUN dotnet publish --output /app/ --configuration ReleaseFROM mcr.microsoft.com/dotnet/core/aspnet:3.1WORKDIR /appCOPY --from=builder /app .CMD ["dotnet", "peon.dll"]

peon.dll это название моего Solution. Peon переводится как батрак.

О том как работать с Docker и Heroku можно прочитать здесь. Но я все же опишу последовательность действий.

Вам понадобится создать аккаунт в Heroku, установить Heroku CLI.

Создайте новый проект в heroku и свяжите его с вашим репозиторием.

heroku git:remote -a project_name

Теперь нам необходимо создать файл heroku.yml в папке с проектом. У него будет такое содержимое:

build:  docker:    web: Dockerfile

Дальше выполним небольшую череду команд:

# Залогинимся в heroku registryheroku container:login# Соберем и запушим образ в registryheroku container:push web# Зарелизим приложение из образаheroku container:release web

Можете открыть приложение в браузере с помощью команды:

heroku open

После того как мы разместили приложение в Heroku, нужно подключить базу данных Redis для кэша. Как вы помните InMemory кэш будет исчезать после перезапуска приложения.

Установите для нашего Heroku приложения бесплатный аддон RedisCloud.

Строку подключения для Redis можно будет получить через переменную окружения REDISCLOUD_URL. Она будет доступна, когда приложение будет запущено в экосистеме Heroku.

Нам нужно получить эту переменную в коде приложения.

Установите библиотеку Microsoft.Extensions.Caching.StackExchangeRedis.

С помощью нее можно зарегистрировать Redis реализацию для IDistributedCache в Startup.

services.AddStackExchangeRedisCache(o =>{   o.InstanceName = "PeonCache";   var redisCloudUrl = Environment.GetEnvironmentVariable("REDISCLOUD_URL");   if (string.IsNullOrEmpty(redisCloudUrl))   {      throw new ApplicationException("redis connection string was not found");   }   var (endpoint, password) = RedisUtils.ParseConnectionString(redisCloudUrl);   o.ConfigurationOptions = new ConfigurationOptions   {      EndPoints = {endpoint},      Password = password   };});

В этом коде мы получили переменную REDISCLOUD_URL из переменных окружения системы. После этого мы извлекли адрес и пароль базы данных с помощью класса RedisUtils. Его написал я сам:

public static class RedisUtils{   public static (string endpoint, string password) ParseConnectionString(string connectionString)   {      var bodyPart = connectionString.Split("://")[1];      var authPart = bodyPart.Split("@")[0];      var password = authPart.Split(":")[1];      var endpoint = bodyPart.Split("@")[1];      return (endpoint, password);   }}

На этот класс можно сделать простой Unit тест.

[Test]public void ParseConnectionString(){   const string example = "redis://user:password@url:port";   var (endpoint, password) = RedisUtils.ParseConnectionString(example);   endpoint.Should().Be("url:port");   password.Should().Be("password");}

После того что мы сделали, GuildRepository будет сохранять кэш не в оперативную память, а в Redis. Нам даже не нужно ничего менять в коде приложения.

Опубликуйте новую версию приложения.

Шаг 5. Реализуем циклическое выполнение

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

Есть несколько способов это реализовать:

Самый простой способ - это сделать задание на сайте https://cron-job.org. Этот сервис будет слать get запрос на /check вашего приложения каждые N минут.

Второй способ - это использовать Hosted Services. В этой статье подробно описано как создать повторяющееся задание в ASP.NET Core проекте. Учтите, бесплатный тариф в Heroku подразумевает что ваше приложение будет засыпать после того как к нему некоторое время не делали запросов. Hosted Service перестанет работать после того как приложение заснет. В этом варианте вам следует перейти на платный тариф. Кстати, так сейчас работает мой бот.

Третий способ - это подключить к проекту специальные Cron аддоны. Например Heroku Scheduler. Можете пойти этим путем и разобраться как создать cron job в Heroku.

Шаг 6. Автоматическая сборка, прогон тестов и публикация

Во-первых, зайдите в настройки приложения в Heroku.

Там есть пункт Deploy. Подключите там свой Github аккаунт и включите Automatic deploys после каждого коммита в master.

Поставьте галочку у пункта Wait for CI to pass before deploy. Нам нужно чтобы Heroku дожидался сборки и прогонки тестов. Если тесты покраснеют, то публикация не случится.

Сделаем сборку и прогонку тестов в Github Actions.

Зайдите в репозиторий и перейдите в пункт Actions. Теперь создайте новый workflow на основе шаблона .NET

В репозитории появится новый файл dotnet.yml. Он описывает процесс сборки.

Как видите по его содержимому, задание build будет запускаться после пуша в ветку master.

on:  push:    branches: [ master ]  pull_request:    branches: [ master ]

Содержимое самого задания нас полностью устраивает. Если вы вчитаетесь в то что там происходит, то увидите что там происходит запуск команд dotnet build и dotnet test.

    steps:    - uses: actions/checkout@v2    - name: Setup .NET      uses: actions/setup-dotnet@v1      with:        dotnet-version: 5.0.x    - name: Restore dependencies      run: dotnet restore    - name: Build      run: dotnet build --no-restore    - name: Test      run: dotnet test --no-build --verbosity normal

Менять в этом файле ничего не нужно, все уже будет работать из коробки.

Запуште что-нибудь в master и посмотрите что задание запускается. Кстати, оно уже должно было запуститься после создания нового workflow.

Отлично! Вот мы и сделали микросервис на .NET Core который собирается и публикуется в Heroku. У проекта есть множество точек для развития: можно было бы добавить логирование, прокачать тесты, повесить метрики и. т. д.

Надеюсь данная статья подкинула вам пару новых идей и тем для изучения. Спасибо за внимание. Удачи вам в ваших проектах!

Подробнее..
Категории: C , Net , Api , Docker , Dotnet , Discord , Bot , Бот , Heroku , Микросервис , Wow

Как сделать кластерный сервер на ARM процессоре и тестирование VPS на AWS Graviton2

28.04.2021 12:07:29 | Автор: admin
Cluster Server ARM

В предыдущей публикации рассматривались преимущества использование ARM серверов для хостинг провайдеров. В этом посте рассмотрим практические варианты создания кластерного сервера на ARM процессоре и протестируем инстанс Amazon EC2 T4g работающий на процессоре ARM AWS Graviton2, посмотрим на что он способен.

Создание серверного кластера на Raspberry/Banana/Orange Pi


На данный момент существует множество различных вариантов сборки кластера на базе различных одноплатных компьютеров. Можно собрать самостоятельно или купить готовую коробку (CASE) для наполнения модулями CoM Raspberry Pi.

Cluster Server ARM
Кластер на Raspberry/Banana/Orange Pi

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

  1. Малая производительность используемых ARM-процессоров.
  2. В большинстве случаем хранение данных только на eMMC или microSD.
  3. Большой набор лишней периферии, такой как модуль связи Wi-Fi и Bluetooth, порты видеовыхода HMDI и т.д., что приводит к лишней стоимости платы и увеличению энергопотребления.
  4. Невозможна плотная компоновка модулей.
  5. Большое количество лишних проводов для подключения линий электропитания и Ethernet.

Для решения выше перечисленных недостатков необходимо перейти на концепцию Компьютер-на-Модуле (Computer on Module, CoM).

Компьютер на модуле (CoM)


Компьютер на модуле (CoM) это плата, которая объединяет в себе основные элементы системы обработки данных и позволяет подключить их к несущей плате с помощью разъёмов или пайки. На такой плате размещается основной процессор, оперативная память, дополнительные модули чипы. Плата обычно содержит периферийные функции, а несущая плата позволяет реализовать специфические функции устройства, например, соединение с внешними устройствами посредством различных интерфейсов.

Cluster Server ARM
Компьютер на модуле (CoM)

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

Cluster Server ARM
Подключение модуля CoM к несущей плате

Стартап miniNodes разрабатывает недорогие ARM сервера, предназначенные для небольших облачных сервисов, веб-сайтов и приложений Интернета вещей. Они разработали микро-сервер состоящий из несущей платы и 5 модулей Raspberry Pi 3 CoM. Несущая плата Carrier Board содержит встроенный гигабитный коммутатор, который обеспечивает подключение ко всем 5 модулям. С противоположной стороны платы размещен модуль питания для обеспечивания питание CoM. Каждый из вычислительных модулей также имеет отдельный переключатель включения/выключения питания. Модули Raspberry Pi CM3 + поставляются с 8 ГБ, 16 ГБ или 32 ГБ eMMC на борту, поэтому нет необходимости использовать SD-карты, как на обычных платах Raspberry Pi. Таким образом, при полной загрузке есть 5 узлов, состоящих из 4 ядер, 1 ГБ ОЗУ и до 32 ГБ eMMC каждый, всего 20 ядер, 5 ГБ ОЗУ и до 160 ГБ хранилища.

Cluster Server ARM
5 Node Raspberry Pi 3 CoM Carrier Board

Такой подход построения микро-серверов на базе Raspberry Pi безусловно хорош, но вряд ли будет серьезно воспринят бизнесом. Любой бизнес в первую очередь интересует длительность поддержки купленной системы, поэтому решения стартапов не вариант для бизнеса. Дополнительно, закрытость процессоров Broadcom для Raspberry Pi не добавляет популярности в этом сегменте рынка.

Блейд-сервер от Firefly на процессорах Rockchip


Среди доступных решений выделяются процессоры компании Rockchip. По сравнению с процессорами от других производителей, для процессоров Rockchip доступно больше драйверов под Linux и есть даташиты в публичном доступе. Компания Firefly разрабатывает модули CoM на процессорах Rockchip и комплектует из них блейд-серверы. В каталоге компании доступно 9 различных по производительности модулей CoM, которые можно использовать для комплектации сервера. Модули для подключения имеют стандартный интерфейс SODIMM.

Cluster Server ARM
Модули COM от компании Firefly

Компания разработала сервер Cluster Server R1 в 1U форм-факторе который может содержать до 11 модулей CoM.

Cluster Server ARM
Сервер Cluster Server R1

Сервер предназначен для запуска приложений на Linux, облачных игр, виртуальных рабочих столов, тестирования мобильных приложений (до 110 виртуальных телефонов на Android). Возможен запуск ОС: Linux и Android.

Для комплектации доступны модули CoM:

  • RK3399(AI) Core Board (Core-3399-JD4): два ядра A72 + четыре ядра A53, частота до 1.8GHz
  • RK3328 Core Board (Core-3328-JD4): четы ядра A53, частота до 1.5GHz
  • RK1808(AI) Core Board (Core-1808-JD4): два ядра с A35, частота до 1.6GHz

На лицевой панели блейд-сервера размещено: 4 порта Gigabit Ethernet, HDMI, два порта USB2.0, OTG, дополнительно для модулей доступен 3.5-дюймовый жесткий диск SATA/SSD с горячей заменой, слот для SIM карт модуля 4G-LTE.

Cluster Server ARM
Лицевая панель сервера Cluster Server R1

Для управления узлами используется BMC (Baseboard Management Controller) с помощью которой можно управлять узлами: включать/выключать, удаленный доступ, мониторинг состояния, управление аппаратной конфигурацией.

В начале этого года была представлена вторая версия кластерного сервера Cluster Server R2. Cluster Server R2 поставляется в 2U форм-факторе и содержит:

  • 9 блейд-узлов (каждый узел содержит 8 модулей CoM).
  • Два 3.5-дюймовых жестких дисков SATA/SSD.
  • 4 порта Gigabit Ethernet.
  • два порта USB 3.0, USB 2.0, порт HDMI.

Cluster Server ARM
Cluster Server Cluster Server R2

Кластерный север так же работает под управлением ОС: Android, Ubuntu или некоторых других дистрибутивов Linux. Варианты использования сервера: облачный телефон, виртуальный рабочий стол, облачные игры, облачное хранилище, блокчейн, декодирование многоканального видео, и т. Д. Наличие AI (NPU-нейронный процессор) делает кластер похожим на Solidrun Janux GS31 Edge AI. Сервер, предназначенный для вывода в режиме реальном времени нескольких видеопотоков для мониторинга умных городов и инфраструктуры, интеллектуального корпоративного/промышленного видеонаблюдения, обнаружения, распознавания и классификации объектов, интеллектуального визуального анализа и т. д.

Что можно запустить на ARM процессоре?


Запуск приложений с помощью Docker и Kubernetes давно стал де-факто стандартом для Linux. Поэтому рассмотрим какие наиболее популярные контейнеры можно запустить под ARM:

  1. Portainer.io управление и мониторинг контейнерами с помощью web-интерфейса.
  2. OpenVPN самый популярный бесплатный VPN сервер
  3. SoftEther VPN мультипротокольный VPN-сервер с графическим интересом под Windows.
  4. Базы данных все официальные Docker-образы так же собраны для ARM архитектуры: PostgreSQL, Mariadb, MongoDB.
  5. Nginx-proxy Nginx прокси-сервер, образ на базе Alpine
  6. Traefik обратный прокси-сервер, альтернатива Nginx
  7. Wordpress популярная CMS-система
  8. Elasticsearch поисковая система на Java
  9. Asterisk PBX компьютерная телефония (в том числе, VoIP) с открытым исходным кодом от компании Digium
  10. Zabbix система мониторинга и отслеживания статусов различных сервисов компьютерной сети, серверов и сетевого оборудования

Отдельно необходимо отметить проект linuxserver.io, которые собирает контейнеры на базе наиболее популярных приложения для Linux, сборки готовятся и для архитектуры ARM. Наиболее популярные приложений для Linux представлены в виде контейнеров для ARM-систем, поэтому можно уже начинать тестировать.

Тестирование VPS на AWS Graviton2


Amazon для тестирования предоставляет инстансы на базе процессора ARM AWS Graviton2. Это отличная возможность бесплатно протестировать ПО на совместимость с ARM архитектурой и просто получить опыт работы, эксплуатации системы на ARM процессоре. Бесплатно предоставляется инстанс t4g.micro до 30 июня 2021 года в режиме 24x7. Для тестирования достаточно зарегистрироваться и развернуть инстанс на Ubuntu Server 20.04 LTS.
Конфигурация инстанса t4g.micro:

  • 2 vCPUs 2.5 GHz
  • 1 GiB memory
  • 8 GB SSD

Инстанс t4g.micro доступен для развертывания на различных площадках. Ближайшая к нам площадка Europe (Frankfurt) eu-central-1, пинг от Питера в среднем составляет 78 ms.

Команда lscpu выдает следующую информацию:

ubuntu@host:~$ lscpuArchitecture:                    aarch64CPU op-mode(s):                  32-bit, 64-bitByte Order:                      Little EndianCPU(s):                          2...Vendor ID:                       ARMModel:                           1Model name:                      Neoverse-N1Stepping:                        r3p1BogoMIPS:                        243.75L1d cache:                       128 KiBL1i cache:                       128 KiBL2 cache:                        2 MiBL3 cache:                        32 MiBNUMA node0 CPU(s):               0,1...Flags:                           fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimdd                                 p ssbs

Железо:

root@host:/home/ubuntu# inxi -bSystem:    Host: host Kernel: 5.4.0-1038-aws aarch64 bits: 64 Console: tty 1           Distro: Ubuntu 20.04.2 LTS (Focal Fossa)Machine:   Type: Other-vm? System: Amazon EC2 product: t4g.micro v: N/A serial: ec21ba8c-f1b0-3f47-74e0-c648a84383c4           Mobo: Amazon EC2 model: N/A serial: N/A UEFI: Amazon EC2 v: 1.0 date: 11/1/2018CPU:       Dual Core: Model N/A type: MCP speed: 0Graphics:  Message: No Device data found.           Display: server: No display server data found. Headless machine? tty: 130x42           Message: Advanced graphics data unavailable for root.Network:   Device-1: Amazon.com Elastic Network Adapter driver: enaDrives:    Local Storage: total: 8.00 GiB used: 2.52 GiB (31.5%)Info:      Processes: 145 Uptime: 2d 23h 46m Memory: 952.5 MiB used: 336.2 MiB (35.3%) Shell: bash inxi: 3.0.38

Тест CPU

Тестируем процессор sysbenchем:

root@host:/home/ubuntu# sysbench --test=cpu --cpu-max-prime=20000 --num-threads=1 runsysbench 1.0.18 (using system LuaJIT 2.1.0-beta3)Running the test with following options:Number of threads: 1Initializing random number generator from current timePrime numbers limit: 20000...CPU speed:    events per second:  1097.02General statistics:    total time:                          10.0002s    total number of events:              10972Latency (ms):         min:                                    0.91         avg:                                    0.91         max:                                    0.95         95th percentile:                        0.92         sum:                                 9998.11Threads fairness:    events (avg/stddev):           10972.0000/0.00    execution time (avg/stddev):   9.9981/0.00

Тест ОЗУ:

root@host:/home/ubuntu# sysbench --test=memory --num-threads=4 --memory-total-size=512MB runsysbench 1.0.18 (using system LuaJIT 2.1.0-beta3)...Total operations: 524288 (3814836.42 per second)512.00 MiB transferred (3725.43 MiB/sec)General statistics:    total time:                          0.1360s    total number of events:              524288Latency (ms):         min:                                    0.00         avg:                                    0.00         max:                                    8.00         95th percentile:                        0.00         sum:                                  315.89

Тест диска

Посмотрим что покажет dd. Скорость до полноценного SSD недотягивает, но уже полноценный SATA:

root@home:/home/ubuntu# dd if=/dev/zero of=test bs=64k count=16k conv=fdatasync16384+0 records in16384+0 records out1073741824 bytes (1.1 GB, 1.0 GiB) copied, 7.03664 s, 153 MB/s

Результаты теста бенчмарка 7-zip:
root@host:/home/ubuntu# 7za b7-Zip (a) [64] 16.02 : Copyright (c) 1999-2016 Igor Pavlov : 2016-05-21p7zip Version 16.02 (locale=C.UTF-8,Utf16=on,HugeFiles=on,64 bits,2 CPUs LE)LECPU Freq: - - - - - - 512000000 - -RAM size:     952 MB,  # CPU hardware threads:   2RAM usage:    441 MB,  # Benchmark threads:      2                       Compressing  |                  DecompressingDict     Speed Usage    R/U Rating  |      Speed Usage    R/U Rating         KiB/s     %   MIPS   MIPS  |      KiB/s     %   MIPS   MIPS22:       6957   167   4054   6768  |       8353   199    358    71323:        757   172    448    772  |      22509   200    975   194824:       7073   185   4118   7606  |      80370   200   3535   705625:       6831   185   4227   7800  |      77906   199   3480   6934----------------------------------  | ------------------------------Avr:             177   3212   5736  |              199   2087   4163Tot:             188   2649   4950


Сравнение стоимости инстанса t4g.micro с VPS на x86


Воспользуемся калькулятором calculator.aws и рассчитаем на сколько ARM дешевле x86 VPS на AWS. Расчет будем производить для площадки Europe (Frankfurt) eu-central-1. Ближайшей аналог x86 по характеристикам это инстанс t2.micro.

Конфигурация инстанса t2.micro:
  • 1 vCPUs
  • 1 GiB memory
  • 8 GB SSD

Допустим инстанс будет работать 365 дней * 24 часа * 1 год = 8760.0000 часов.

Ежемесячный платеж за аренду инстанса по требованию без учета трафика составит:
  • t4g.micro (ARM): 5.33 USD ~ 400 р. (по курсу 1$ = 75 р.);
  • t2.micro (x86): 7.96 USD ~ 597 р. (по курсу 1$ = 75 р.).

Получает что сервер на ARM стоит на 33% дешевле аналога на x86. Отдельно необходимо рассчитывать объем сетевого трафика, стоимость не зависит от типа архитектуры инстанса. Первый гигабайт трафика в месяц будет бесплатным, далее объем до 10 ТБ оплачивается по цене $0.09 за каждый Гб. Входящий трафик не тарифицируется. Если будем исходить из средней сетевой нагрузки в 150 Гб исходящего трафика в месяц, то стоимость за трафик будет:

  • 150 Гб * $0.09 = $13.5 ~ 1000 р. (по курсу 1$ = 75 р.);

В итоге VPS инстанс t4g.micro (ARM) с объемом трафика в 150 Гб в месяц будет стоить 1 400 р./месяц.

Если для сравнения взять VPS от VDSina.ru за 330 р./месяц (1 ядро, 30 ГБ NVMe, 32 ТБ трафика), то для конкурентного преимущества серверам на ARM еще расти и расти.

Вывод: Отсутствие нативного ПО под ARM архитектуру еще долго будет останавливающим фактором массового перехода. Но так или иначе частичный переход на ARM сервера это уже тренд. Основными двигателями перехода выступают три мощных фактора. Первый фактор независимость от основных поставщиков процессоров x86. Можно выбрать решение максимально подходящее под себя. Второй фактор возможность максимальной оптимизации под себя, исключение всего лишнего и добавление специализированных блоков, таких как NPU, FPGA, и т.д. Третий фактор открытость и доступность Linux. Если сравнивать ARM сервера для массового потребительского сектора, то в этом сегменте еще очень долго будет господствовать x86 архитектура. Скорее всего мы увидим создание нового сегмент рынка для специальных задач ориентированных на преимущества ARM архитектуры, например сервера с FPGA модулями или NPU.



На правах рекламы


Наша компания предлагает в аренду серверы с современными процессорами от Intel и AMD под самые разнообразные задачи. Эпичные серверы это VDS с AMD EPYC, частота ядра CPU до 3.4 GHz. Максимальная конфигурация 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe. Создайте свой собственный тариф самостоятельно в пару кликов!

Подробнее..

Есть ли корпоративная жизнь на удаленке и как ее обеспечить интеграция внутренней системы аутентификации

30.04.2021 12:16:20 | Автор: admin

В digital-агентстве Convergent, где я работаю, в потоке множество проектов, и у каждого из них может быть собственная админка. Есть несколько окружений (дев, стейдж, лайв). А ещё есть разные внутрикорпоративные сервисы (как собственной разработки, так и сторонние вроде Redmine или Mattermost), которыми ежедневно пользуются сотрудники.

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

В данной статье я хочу поделиться опытом создания собственной внутренней системы аутентификации на основе OpenResty, а также спецификации OAuth2. В качестве основного языка программирования мы используем PHP, а фреймворк Yii 2.

Суммирую необходимый функционал:

  • Единое место управления всеми доступами. Здесь происходит выдача и отзыв доступов в административные панели сайтов и списки доменов;

  • По умолчанию весь доступ запрещён, если не указано обратное (доступ к любым доменам контролируется с помощью OpenResty);

  • Аутентификация для сотрудников;

  • Аутентификация для клиентов.

Закрытый доступ к сайтам и инфраструктуре

Начну со схемы, как мы организовали инфраструктуру доступов.

Упрощенная схема взаимодействия между пользователями и серверамиУпрощенная схема взаимодействия между пользователями и серверами

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

Фронт-контроллером в данном случае выступает OpenResty это модифицированная версия nginx, которая в т. ч. поддерживает из коробки язык Lua. На нём я написал прослойку, через которую проходят все HTTP(s)-запросы.

Вот так может выглядеть код скрипта аунтентификации (в упрощенном варианте):

auth.lua
function authenticationPrompt()    ngx.header.www_authenticate = 'Basic realm="Restricted by OpenResty"'    ngx.exit(401)endfunction hasAccessByIp()    local ip = ngx.var.remote_addr    local domain = ngx.var.host    local port = ngx.var.server_port    local res, err = httpc:request_uri(os.getenv("AUTH_API_URL") .. "/ip.php", {        method = "GET",        query = "ip=" .. ip .. '&domain=' .. domain .. '&port=' .. port,        headers = {          ["Content-Type"] = "application/x-www-form-urlencoded",        },        keepalive_timeout = 60000,        keepalive_pool = 10,        ssl_verify = false    })    if res ~= nil then        if (res.status == 200) then            session.data.domains[domain] = true            session:save()            return true        elseif (res.status == 403) then            return false        else            session:close()            ngx.say("Server error: " .. res.body)            ngx.exit(500)        end    else        session:close()        ngx.say("Server error: " .. err)        ngx.exit(500)    endendfunction hasAccessByLogin()    local header = ngx.var.http_authorization    local domain = ngx.var.host    local port = ngx.var.server_port    if (header ~= nil) then        header = ngx.decode_base64(header:sub(header:find(' ') + 1))        login, password = header:match("([^,]+):([^,]+)")        if login == nil then            login = ""        end        if password == nil then            password = ""        end        local res, err = httpc:request_uri(os.getenv("AUTH_API_URL") .. '/login.php', {            method = "POST",            body = "username=" .. login .. '&password=' .. password .. '&domain=' .. domain .. '&port=' .. port,            headers = {              ["Content-Type"] = "application/x-www-form-urlencoded",            },            keepalive_timeout = 60000,            keepalive_pool = 10,            ssl_verify = false        })        if res ~= nil then            if (res.status == 200) then                session.data.domains[domain] = true                session:save()                return true            elseif (res.status == 403) then                return false            else                session:close()                ngx.say("Server error: " .. res.body)                ngx.exit(500)            end        else            session:close()            ngx.say("Server error: " .. err)            ngx.exit(500)        end    else        return false    endendos = require("os")http = require "resty.http"httpc = http.new()session = require "resty.session".new()session:start()if (session.data.domains == nil) then    session.data.domains = {}endlocal domain = ngx.var.hostif session.data.domains[domain] == nil then    if (not hasAccessByIp() and not hasAccessByLogin()) then        session:close()        authenticationPrompt()    else        session:close()    endelse    session:close()end

Алгоритм работы скрипта довольно простой:

  • Поступает HTTP-запрос от пользователя;

  • OpenResty запускает скрипт auth.lua;

  • Скрипт определяет запрашиваемый домен и отправляет два запроса на внешний бэкенд;

  • Первый на проверку IP-адреса пользователя в базу;

  • Если IP отсутствует, выводит браузерное окно для ввода логина и пароля, отправляет второй запрос на проверку доступа;

  • В любой другой ситуации выводит окно Вход.

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

Немного расскажу о том, как выглядит управление в нашей системе.

Отмечу, что IP-адреса попадают в базу при аутентификации пользователя. Это сделано специально для сотрудников. Такие адреса помечают как временные и доступные ограниченное время, после чего они удаляются.

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

Для клиентов же создаются простые доступы по паре логин-пароль. Такие доступы ограничиваются в пределах определенных доменных адресов.

Управление доступами по паролю и по IP-адресуУправление доступами по паролю и по IP-адресу

Управление базой сотрудников подтягивается в ID через синхронизацию с другой внутренней системой. Каждый новый сотрудник при добавлении в нашу CRM автоматически получает учётную запись в ID, а также все нужные письма с доступами и инструкциями для последующей настройки и работы.

Форма аутентификации для сотрудниковФорма аутентификации для сотрудников

Каждый день мы начинаем работу с данной аутентификации, таким образом добавляя свой IP-адрес в белый список для получения доступа к инфраструктуре.

Двухфакторная аутентификация

Для обеспечения двухфакторной аутентификации для сотрудников решено было добавить Google Authenticator. Такой механизм защиты позволяет больше обезопасить себя от утечки доступов. Для PHP есть готовая библиотека sonata-project/GoogleAuthenticator. Пример интеграции можно посмотреть здесь.

Интересный нюанс, с которым мы столкнулись в процессе, это зависимость генерируемого кода от времени на устройстве пользователя. Выяснилось, что у некоторых сотрудников время на смартфоне немного отличалось от реального.

OAuth и OpenID

Третье, и не менее важное для нас, создание OAuth-сервера. За основу мы взяли модуль thephpleague/oauth2-server. Для Yii 2 готового решения не было, поэтому написали собственную имплементацию сервера. OAuth2 достаточно обширная тема, расписывать её работу в данной статье не буду. Библиотека имеет хорошую документацию. Также она поддерживает различные фреймворки, включая Laravel и Symfony.

Таким образом, любой сторонний сервис, который поддерживает кастомные OAuth2 конфигурации, достаточно просто подключается к нашей системе. Значимой фишкой такой интеграции стало подключение нашего ID к Mattermost. Последний в бесплатной версии поддерживает только аутентификацию с помощью GitLab, которую удалось эмулировать через наш сервис.

Также для всех наших проектов на Yii был разработан модуль для подключения ID. Это позволило вынести всё управление доступами в админпанели для сотрудников в централизованное место. Кстати, если интересно, я писал статью о модульном подходе, который мы применили в нашем digital-агентстве.

Заключение

Процесс адаптации компании под новую систему был относительно сложный, т. к. это потребовало доработки инфраструктуры и обучения работе с системой всех сотрудников компании. Просто так сказать вот у нас теперь ID, пользуйтесь им не получится, конечно, поэтому весь процесс миграции был очень тщательно задокументирован, а я выступал в качестве технической поддержки первые пару месяцев. Сейчас, спустя время, все процессы наладились, были внесены корректировки и добавлены новые фичи (например, автоматические письма новым сотрудникам с доступами и инструкциями).

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

Ссылки по теме

Подробнее..

JVM в Docker контейнере. Сборник диаграмм по управлению памятью

07.05.2021 12:20:51 | Автор: admin

План статьи:

1. Мотивация
2. Развлекательная часть (можно пропустить):
2.1. Теоретическая ситуация
2.2. Теоретическая проблема
2.3. Теоретическое решение
3. Основная часть:
3.1. Диаграммы
3.2. Заключение

Мотивация

В интернете огромное количество статей и диаграмм на эту тему, однако, по моему мнению, ни одна из них не позволяет сформировать общее представление об использовании памяти приложения на основе JVM, работающего в Docker контейнере. А без общего представления довольно сложно находить решения конкретных проблем в сложных системах. В результате, я решил выложить свои диаграммы. Уверен, они могут быть полезны в целях устранения пробелов в знаниях, в качестве отправной точки для изучения материала глубже и в помощь для решения конкретных задач и проблем на проекте.

Развлекательная часть (Можно пропустить)

Ситуация
Вы ведете разработку ПО, к примеру, для автоматизированного управления плантацией картофеля на планете Марс. ПО должно работать на легких, энерго эффективных портативных устройствах, типа Raspberry Pi. Также данное ПО не должно выходить за рамки выделенных ресурсов, так как на одном устройстве запущены иные ресурсоёмкие процессы. (Все требования перечислять не буду)... В результате изучения различных вариантов и проведения сравнительных испытаний, Вы приняли решение начать разработку ПО, которое будет запускаться на JVM в Docker контейнерах, с установленными ограничениями на использование ресурсов системы.

Проблема
В процессе тестов производительности Вы столкнулись с ситуацией, когда Docker контейнер крашится и перезапускается по причине превышения установленных лимитов по потреблению памяти, несмотря на то что размер Java Heap остается в допустимых пределах.

Решение
Необходимо разобраться, что может быть причиной потребления памяти в данном Docker контейнере, помимо Java Heap, настроить мониторинг, найти и устранить данную причину. В этом нам помогут диаграммы указанные ниже.
(В данном теоретическом примере, причинами утечки памяти были неверно сконфигурирована embedded RocksDB, вызываемая через rocksdbjni и самописная библиотека на C++, которая вызывалась через JNI)

Основная часть

Диаграммы

Упрощенная диаграмма использования памяти JVM в Docker контейнере:

Упрощенная диаграмма использования памяти JVM в Docker контейнереУпрощенная диаграмма использования памяти JVM в Docker контейнере

Детализированная диаграмма использования памяти JVM в Docker контейнере с некоторыми параметрами:

Заключение

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

Конструктивная критика, предложения и замечания приветствуются.

Подробнее..
Категории: Devops , Java , Docker , Jvm , Memory management , Jvm options

Уютный VPS-сервер для маленьких проектов за минимум денег как настроить

30.05.2021 16:05:01 | Автор: admin

Мне всегда хотелось иметь в сети личное пространство. Место, где все было бы устроено как мне удобно. Наилучшим решением мне виделся недорогой VPS, который я мог бы обустраивать в соответствии со своими потребностями. Долго я не мог подступиться к решению этой задачи, но как-то незаметно подобрался набор инструментов, который позволил организовать именно такую программную среду, как мне хотелось.

Если вам тоже хочется создать в сети свое личное пространство, но вы не знаете, с чего начать, или вам просто интересны такие замечательные программные продукты как Docker, Portainer, Traefik добро пожаловать под кат.

Введение


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

Когда я учился в институте (это то время, когда у меня в городе только появился интернет по карточкам и первое интернет-кафе), мой друг, который к тому времени умудрился устроится лаборантом на кафедру информатики, активно пытался подсадить меня на Линукс. И вот я, считающий себя продвинутым компьютерщиком, имеющий опыт, наверное, сотни установок винды, прихожу домой после института и уверенным движением вставляю в дисковод диск с дистрибутивом линукс И что? И ничего.

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

Были и еще попытки освоить линукс, жизнь раз за разом сводила меня с его фанатами, которые пытались меня перетянуть в свой лагерь, я даже как-то попытался поставить генту (неудачно естественно, хотя у меня была распечатка с пошаговым руководством как это сделать), но проклятье консоли витало надо мной и с каждой неудачей то самое чувство беспомощности все усиливалось. После графического интерфейса винды, где все настройки перед тобой как на ладони, сталкиваясь с консолью я чувствовал себя ослепшим, пытающимся найти дорогу на ощупь в незнакомом месте. Так что я очень долго оставался приверженцем технологического стека Microsoft, программировал на .net, используя VisualStudio (она шикарна, и никакой консоли). Прошу не судить строго, возможно, если бы в то время у меня была возможность поставить линукс на виртуалку, чтобы спокойно с ней разбираться и приличный интернет под рукой, эта история приняла бы совсем другой оборот.

Сейчас все поменялось. С приходом в нашу жизнь веба в его современном виде я пересел на Node.js и волей-неволей стал проникаться идеологией консоли и Linux. Для хостинга своих разработок мне понадобился VPS, и тут оказалось, что VPS с виндой стоят гораздо дороже, так что моя жаба перевесила мои фрустрации относительно консоли, да и наличие скоростного интернета позволило мгновенно находить ответы на возникающие вопросы.

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

Подготовка


Я создал VPS минимальной конфигурации (vCPU: 1 core, RAM: 1 ГБ, NVMe: 20 ГБ) на macloud. Для установки на VPS я выбрал дистрибутив Debian 10. При установке я сразу добавил через панель управления SSH-ключ, чтобы было удобно заходить в консоль с помощью SSH-клиента. Для дальнейших экспериментов понадобится следующее:

  1. Обновить систему
  2. Установить docker и docker-compose
  3. Включить файл подкачки.

Обновить операционную систему можно следующими командами:

# apt updateОбновит список доступных пакетов# apt upgradeОбновит систему, установив свежие версии ПО.

Для установки docker следуем официальной инструкции https://docs.docker.com/engine/install/debian/

Устанавливаем необходимые зависимости:

# apt-get install apt-transport-https ca-certificates curl gnupg lsb-release

Добавляем официальный GPG ключ Dockerа:

# curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

Добавляем репозиторий:

# echo deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable | tee /etc/apt/sources.list.d/docker.list > /dev/null

Теперь осталось обновить список пакетов:

# apt-get update

и установить докер:
# apt-get install docker-ce docker-ce-cli containerd.io

Проверить, что все корректно установилось можно, запустив команду:

# docker run hello-world

Если появилось приветствие, значит все прошло хорошо. У меня на момент написания данной статьи установилась следующая версия:

# docker -vDocker version 20.10.6, build 370c289

Теперь надо установить docker-compose. Для этого воспользуемся официальной инструкцией:

https://docs.docker.com/compose/install/

Для начала нам нужно получить ссылку на свежий релиз docker-compose. Список релизов можно посмотреть тут: https://github.com/docker/compose/releases

На момент написания статьи наиболее свежей была версия 1.29.2. Нам нужна версия для linux, поэтому выбираем файл с названием docker-compose-Linux-x86_64. Скопируем ссылку на него. Теперь надо в консоли ввести следующую команду (вставьте в нужное место ссылку, полученную выше):

# curl -L "<ссылка на релиз>" -o /usr/local/bin/docker-compose

В моем случае получилось:

# curl -L https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64 -o /usr/local/bin/docker-compose

Запустим команду и загрузится бинарный файл docker-compose.

Последний шаг надо установить для него разрешение на исполнение. Это можно сделать следующей командой:

# chmod +x /usr/local/bin/docker-compose

Если всё прошло нормально, то на этом процесс установки окончен, теперь можно проверить версию docker-compose. У меня получилось вот так:

# docker-compose -vdocker-compose version 1.29.2, build 5becea4c

Последний шаг это включение файла подкачки, учитывая малый объем памяти он точно не будет лишним:

Создадим файл размером 4 Гб

# fallocate -l 4G /swapfile

Назначим ему необходимые права:

# chmod 600 /swapfile

Инициализируем его как файл подкачки:

# mkswap /swapfile

Наконец активируем:

# swapon /swapfile

Проверить, появился ли файл подкачки в системе можно командой free. У меня все получилось:

# freetotal    used    free   shared buff/cache  availableMem:    1010900   150500   143788    2892   716612   714916Swap:    4194300      0   4194300

Осталось последнее действие: для того, чтобы файл подкачки оставался активным после перезагрузки надо добавить в файл /etc/fstab следующую строку:

/swapfile swap swap defaults 0 0


Для подобных задач мне нравится использовать файловый менеджер Midnight Commander. Это консольный файловый менеджер с классическим интерфейсом. Если вы сталкивались с Norton Commander/FAR/TotalCommander вы легко поймете, как им пользоваться. Установить его можно следующей командой:

# apt-get install mc


А запустить с помощью команды

# mc

Увидим до боли знакомую картину:


Теперь найдем нужный файл и отредактируем его:


На этом предварительная настройка закончена, можно двигаться дальше.

Portainer


Первый инструмент, с которым мне бы хотелось вас познакомить это Portainer. Portainer это инструмент для управления контейнерами в Docker, Swarm, Kubernetes и Azure ACI. Как написано в документации Portainerа:

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

Для моих целей Portainer подходит просто идеально. Давайте познакомимся с ним поближе. В соответствии с документацией https://documentation.portainer.io/v2.0/deploy/ceinstalldocker/ проще всего это сделать следующим образом:

Создадим том для хранения данных:

# docker volume create portainer_data

Теперь можно запустить Portainer следующей командой:

# docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce

После запуска Portainer будет доступен по адресу http://<ip серевера>:9000/

При первом запуске Portainer попросит вас указать пароль администратора, после чего спросит, каким образом подключится к окружению:


Я выбрал вариант подключится к локальному Dockerу, после чего появился домашний экран Portainerа:


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

Зайдем в него, и увидим удобный дэшбоард со сводной информацией:


Здесь мы можем управлять всеми аспектами работы Dockerа. Давайте посмотрим, что у нас в контейнерах:


Здесь пока один контейнер это сам Portainer. Интерфейс очень удобно позволяет просматривать и управлять контейнерами, а также есть возможность добавлять новые. Можете пройтись по остальным вкладкам и убедится, насколько удобный и продуманный интерфейс имеет Portainer.

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

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


Для определения стека используется формат docker-compose, именно он работает здесь под капотом. Мне при помощи этой функции удобнее всего построить из контейнеров необходимую инфраструктуру.

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

У меня периодически возникает потребность в разработке быстрых проектов. Я так называю проекты, которые не требуют глубокой проработки. Это могут быть эксперименты, небольшие сайты, боты, сборщики информации и т.п. Все то, что не требует выстраивания цепочки CI/CD. Мне было бы крайне удобно разрабатывать и отлаживать такие приложения прямо там, где они будут работать. Также у меня довольно часто бывает ситуация, когда я нахожусь вдали от своего рабочего места с настроенной средой разработки, и, если что-то в этих моих проектах начинает работать не так как надо, хотелось бы иметь возможность быстро разобраться в ситуации и пофиксить проблему, или просто поработать над проектами пока есть свободная минутка с любого компьютера, даже если на нем нет ничего, кроме браузера и доступа в интернет.

Для удобства доступа у меня есть купленное доменное имя, и я хочу настроить для различных инструментов домены третьего уровня, так, чтобы например portainer был доступен по адресу portainer.example.com. Также я хочу, чтобы всё работало через https, и, чтобы не заморачиваться с покупкой сертификатов SSL, воспользоваться Lets Encrypt. Еще одно требование, чтобы всё, к чему не предполагается публичный доступ было закрыто аутентификацией.

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

Traefik


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

  • обновление конфигурации на лету
  • поддержка dockerа в качестве провайдера конфигурации
  • работа с Lets encrypt из коробки

Для того, чтобы запустить traefik совместно с portainerом воспользуемся примером docker-compose.yml, приведенном в документации portainer: https://documentation.portainer.io/v2.0/ad/traefik/rp-traefik/

version: 3.9services:traefik:container_name: traefikimage: traefik:latestcontainer_name: traefikcommand:- --entrypoints.web.address=:80- --entrypoints.websecure.address=:443- --providers.docker=true- --providers.docker.exposedbydefault=false- --log.level=ERROR- --certificatesresolvers.leresolver.acme.httpchallenge=true- --certificatesresolvers.leresolver.acme.email=user@mymail.com- --certificatesresolvers.leresolver.acme.storage=./acme.json- --certificatesresolvers.leresolver.acme.httpchallenge.entrypoint=web- --entrypoints.web.http.redirections.entryPoint.to=websecure- --entrypoints.web.http.redirections.entryPoint.scheme=https- --metrics.prometheus=trueports:- 80:80- 443:443volumes:- /var/run/docker.sock:/var/run/docker.sock:ro- ./acme.json:/acme.jsonnetworks:- intranetlabels:- traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)- traefik.http.routers.http-catchall.entrypoints=web- traefik.http.routers.http-catchall.middlewares=redirect-to-https- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=httpsportainer:image: portainer/portainer-ce:2.5.0-alpinecontainer_name: portainercommand: -H unix:///var/run/docker.sockrestart: alwaysvolumes:- /var/run/docker.sock:/var/run/docker.sock- portainer_data:/datanetworks:- intranetlabels:- traefik.enable=true- traefik.http.routers.frontend.rule=Host(`portainer.example.com`)- traefik.http.routers.frontend.entrypoints=websecure- traefik.http.services.frontend.loadbalancer.server.port=9000- traefik.http.routers.frontend.service=frontend- traefik.http.routers.frontend.tls.certresolver=leresolvervolumes:portainer_data:networks:intranet:name: intranet

Чтобы процесс получения сертификата Lets encrypt прошел успешно, перед запуском надо убедится, что записи DNS корректно настроены. Я для этих целей использую cloudflare, и в нем конфигурация должна выглядеть примерно так:


В графе Content должен находится адрес нашего VPS.

Если теперь скопировать получившийся docker-compose.yml на сервер выполнить следующую команду:

# docker-compose up -d

То после запуска по адресу portainer.example.com будет такая картина:


Причем он уже будет защищен сертификатом Lets Encrypt:


У traefik есть очень удобный дашборд, который помогает понять, правильно ли применились настройки конфигурации. Для того, чтобы его активировать, надо добавить в docker-compose.yml следующие строки:

services:traefik:...command:...- --api.dashboard=truelabels:...- traefik.enable=true- traefik.http.routers.traefik.entrypoints=websecure- traefik.http.routers.traefik.rule=Host(`traefik.example.com`)- traefik.http.routers.traefik.tls=true- traefik.http.routers.traefik.service=api@internal- traefik.http.routers.traefik.tls.certresolver=leresolver- traefik.http.services.traefik.loadbalancer.server.port=8080

Также надо в настройках DNS добавить домен третьего уровня traefik.example.com. Это можно сделать по аналогии с тем, как ранее был добавлен домен для portainer. После применения настроек по адресу traefik.example.com увидим дашборд:


Как видите, Traefik оказался замечательным инструментом. Он работает в тесной связке с dockerом, а его возможности динамической конфигурации позволяют полностью отказаться от использования конфигурационных файлов для настроек роутинга. Вместо этого мы просто прописываем все необходимые параметры в качестве labels для настраиваемого сервиса, так что все настройки оказываются в одном месте.

Теперь предлагаю немного отвлечься от настройки базовых сервисов и добавить в систему среду разработки. Это будет

Visual Studio Code Server


Для меня всегда среда разработки или IDE, была чем-то таким серьезным. Мощный программный пакет, который устанавливается на машине разработчика, занимает много гигабайт на диске и в оперативной памяти. Пример такой IDE, которой я пользовался много лет, и до сих пор считаю, что это лучший выбор, если вы работаете со стеком технологий Microsoft, это Microsoft VisualStudio. Когда я начал изучать Node.js, я открыл для себя VSCode, и, несмотря на родственные названия, это совсем другая IDE, с совершенно иной концепцией и возможностями. Тот факт, что для отображения своего интерфейса VSCode использует движок Chrome, позволяет разнести интерфейс и сам VSCode. Благодаря такой архитектуре возник Visual Studio Code Server, который может работать на VPS, при этом интерфейс VSCode будет доступен через браузер. И нет, это не очередной онлайн редактор кода, это полноценная IDE VSCode, которая обладает всеми ее замечательными возможностями.

Чтобы добавить VSCode Server на свой VPS я в Portainerе создам новый stack, назову его code-server и добавлю туда следующую конфигурацию:

version: 3.9volumes:codeserverdata:codeappdir:networks:intranet:external: trueservices:code-server:image: ghcr.io/linuxserver/code-servercontainer_name: code-serverenvironment:- PUID=1000- PGID=1000- TZ=Europe/London#    PASSWORD=password #optional- SUDO_PASSWORD=password #optional- PROXY_DOMAIN=code.example.comvolumes:- codeserverdata:/config- codeappdir:/appextra_hosts:host.docker.internal: host-gatewayrestart: alwaysnetworks:- intranetlabels:- traefik.enable=true- traefik.http.routers.code.rule=Host(`code.example.com`)- traefik.http.routers.code.tls=true- traefik.http.routers.code.tls.certresolver=leresolver


Также перед запуском надо не забыть добавить в записи DNS домен третьего уровня code.example.com. Теперь осталось нажать кнопку Deploy the stack.

После окончания процесса в portainerе появится новый stack, и, если мы зайдем по адресу code.example.com, то увидим такую картину (я сразу включил dark theme):


Здесь можно вести разработку также как в десктопной версии VSCode. По большому счету, единственное отличие, с которым я столкнулся, это разница в дефолтных биндингах некоторых сочетаний клавиш, но к этому быстро привыкаешь в процессе использования.

Для удобства я подключил к контейнеру code-servera том, который смонтировал в директорию /app, так что создавать или клонировать из репозитория проекты лучше всего там, в таком случае данные не пропадут даже при пересоздании контейнера.

В данном образе уже установлен Node.js, так что мне не пришлось ничего делать, и я могу сразу приступить к работе над своими проектами. Если же вам в работе нужны другие ЯП, то обратите внимание, что создатели данного образа поддерживают каталог модов, которые позволяют добавить поддержку различных платформ. О том, как их использовать, можно почитать в описании образа на Docker Hub, а со списком официальных модов можно ознакомиться здесь: mods.linuxserver.io/?mod=code-server

Обратите также внимание на то, что в настройках контейнера указан пароль sudo. Для демонстрации я оставил его простым, но на практике лучше сделать его сложным, а еще лучше использовать свойство SUDO_PASSWORD_HASH, чтобы не хранить пароль в открытом виде. Как это сделать, можно почитать в описании образа здесь: hub.docker.com/r/linuxserver/code-server.

Как вы наверное уже заметили на данный момент доступ к code-server никак не защищен и сейчас любой, кто зайдет по адресу code.example.com получит доступ. Это очень плохой вариант, и, хотя в настройках образа можно включить доступ по паролю, мне бы хотелось иметь единый логин для доступа ко всем ресурсам, расположенным на сервере. Для этого предлагаю познакомиться со следующим инструментом. Это будет

KeyCloak


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

Для того, чтобы добавить его в мою систему, я создал в portainerе новый стек с названием auth и добавил в него следующую конфигурацию:

version: '3.9'networks:intranet:external: trueservices:keycloak:image: jboss/keycloakcontainer_name: keycloakrestart: alwaysnetworks:- intranetenvironment:KEYCLOAK_PASSWORD: passwordPROXY_ADDRESS_FORWARDING: truelabels:- traefik.enable=true- traefik.http.routers.keycloak.rule=Host(`auth.yourdomain.com`)- traefik.http.routers.keycloak.tls=true- traefik.http.routers.keycloak.tls.certresolver=leresolver


После нажатия кнопки Deploy the stack, KeyCloak будет доступен по адресу auth.example.com. Если мы зайдем туда, нас встретит приветственное окно KeyCloak:



Зайдем в консоль администратора:


Имя пользователя будет admin, а начальный пароль тот, что мы установили в конфигурации стека переменной окружения KEYCLOAK_PASSWORD. После логина попадем в админку KeyCloak:


Рекомендации по первоначальной настройке KeyCloak для dockerа можно подчерпнуть в официальной документации вот здесь www.keycloak.org/getting-started/getting-started-docker.

Вкратце, надо создать новый пользовательский Realm, в нем создать пользователя и добавить нового клиента.

Для клиента необходимо установить Access Type: confidential и добавить в Valid Redirect URIs наши домены, пока это будут https://traefik.example.com/* и code.example.com*:



После установки Access Type: confidential появится вкладка Credentials, в которой можно будет забрать Secret, он нам пригодится далее при настройке.

На этом пока закончим настройку KeyCloak. Теперь нам надо подружить его с Traefikом. Напомню, что мы хотим закрыть от неаутентифицированных пользователей доступ к узлам code.example.com и traefik.example.com. Для этих целей у traefik есть ForwardAuth middleware, которое позволяет организовать авторизацию через внешний сервис. Чтобы обеспечить его взаимодействие KeyCloak нам понадобится промежуточный сервис, я буду использовать github.com/thomseddon/traefik-forward-auth. Он доступен также в качестве образа на Docker Hub, поэтому я просто дополню конфигурацию стека auth в portainerе таким сервисом:

traefik-forward-auth:image: thomseddon/traefik-forward-authcontainer_name: traefik-forward-authenvironment:- DEFAULT_PROVIDER=oidc- PROVIDERS_OIDC_ISSUER_URL=https://auth.example.com/auth/realms/example- PROVIDERS_OIDC_CLIENT_ID=traefik- PROVIDERS_OIDC_CLIENT_SECRET=d7fb86f0-71a9-44f7-ab04-967f086cd89e- SECRET=something-random- LOG_LEVEL=debuglabels:- traefik.enable=true- traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181- traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User- traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181restart: always

Здесь в переменной PROVIDERS_OIDC_ISSUER_URL должен быть путь к ранее созданному нами Realm в Keycloak, PROVIDERS_OIDC_CLIENT_ID должен содержать имя клиента, созданного мной ранее в данном realmе, а PROVIDERS_OIDC_CLIENT_SECRET надо взять из вкладки Credentials данного клиента. В переменной SECRET надо забить рандомную строку.

Теперь, чтобы закрыть сервис, роутинг в который обеспечивает Traefik достаточно добавить к нему в labels следующую строку:

- "traefik.http.routers.<имя роутера>.middlewares=traefik-forward-auth"


Для начала я решил закрыть аутентификацией code server, для чего зашел в его стек и дополнил конфигурацию его конфигурацию. Получилось следующее (для краткости привожу только секцию labels):

labels:- traefik.enable=true- traefik.http.routers.code.rule=Host(`code.example.com`)- traefik.http.routers.code.tls=true- traefik.http.routers.code.tls.certresolver=leresolver     - traefik.http.routers.code.middlewares=traefik-forward-auth


Нажмем кнопку Update Stack и попробуем зайти по адресу code.example.com. Если все сделано правильно, то появится окно логина:


После ввода корректных имени пользователя и пароля (которые до этого настроил в KeyCloak) я попал в интерфейс Code server. Все работает!

Подобным образом я закрыл от посторонних глаз дашборд traefikа. Для этого пришлось сходить в консоль (Portainer не может вносить изменения в конфигурацию стека, который был создан не им, а Traefik я поднимал из консоли) и аналогичным образом отредактировать docker-compose.yml:

labels:- traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)- traefik.http.routers.http-catchall.entrypoints=web- traefik.http.routers.http-catchall.middlewares=redirect-to-https- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https- traefik.enable=true- traefik.http.routers.traefik.entrypoints=websecure- traefik.http.routers.traefik.rule=Host(`traefik.example.com`)- traefik.http.routers.traefik.tls=true- traefik.http.routers.traefik.service=api@internal- traefik.http.routers.traefik.tls.certresolver=leresolver- traefik.http.routers.traefik.middlewares=traefik-forward-auth- traefik.http.services.traefik.loadbalancer.server.port=8080


Для проверки я зашел по адресу traefik.example.com. Чтобы удостоверится, что все работает как надо, мне пришлось открыть окно браузера в режиме инкогнито, иначе система меня узнавала и не спрашивала пароль, поскольку я ранее уже логинился для доступа к code-server и поэтому KeyCloak аутентифицировал меня автоматически.

Таким образом, при помощи связки KeyCloak и Traefik мне удалось защитить от несанкционированного доступа чувствительные элементы моей системы. Преимущество данного подхода в том, что он позволяет сделать это даже там, где не предусмотрены собственные механизмы аутентификации. Есть конечно и недостатки KeyCloak достаточно тяжелый, занимает много ресурсов, в первую очередь памяти, да и по возможностям это явный overkill, мне скорее всего не потребуется большая часть того, что он умеет. В качестве альтернативы можно бы было использовать облачный сервис, например traefik-forward-auth имеет встроенную поддержку Google OAuth.

Выводы


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

  • Подключение доменных имен
  • Настройка TLS
  • Получение и установка сертификатов
  • Аутентификация

делаются в несколько строчек прямо в конфигурации сервиса.

Конечно, при настройке мне пришлось многое делать из консоли, но в дальнейшем, при повседневном использовании построенная система позволит мне сократить ее использование, что для меня несомненно плюс.

Когда я писал эту статью, главной сложностью было собрать всю необходимую информацию воедино. Многие вещи из документации неочевидны и зачастую чтобы добиться того, чтобы система заработала нужным образом приходилось искать решение проблемы по различным issues на github и вопросам со stackoverflow. Поэтому я постарался ряд моментов осветить более подробно, и надеюсь, кому-нибудь мои изыскания помогут лучше разобраться в описываемых продуктах.

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

Для удобства файлы конфигураций я разместил в репозитории вот тут: https://github.com/debagger/vps-docker-workspace

Благодарю за внимание!



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Уютный VPS-сервер для маленьких проектов как настроить

30.05.2021 18:13:46 | Автор: admin

Мне всегда хотелось иметь в сети личное пространство. Место, где все было бы устроено как мне удобно. Наилучшим решением мне виделся недорогой VPS, который я мог бы обустраивать в соответствии со своими потребностями. Долго я не мог подступиться к решению этой задачи, но как-то незаметно подобрался набор инструментов, который позволил организовать именно такую программную среду, как мне хотелось.

Если вам тоже хочется создать в сети свое личное пространство, но вы не знаете, с чего начать, или вам просто интересны такие замечательные программные продукты как Docker, Portainer, Traefik добро пожаловать под кат.

Введение


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

Когда я учился в институте (это то время, когда у меня в городе только появился интернет по карточкам и первое интернет-кафе), мой друг, который к тому времени умудрился устроится лаборантом на кафедру информатики, активно пытался подсадить меня на Линукс. И вот я, считающий себя продвинутым компьютерщиком, имеющий опыт, наверное, сотни установок винды, прихожу домой после института и уверенным движением вставляю в дисковод диск с дистрибутивом линукс И что? И ничего.

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

Были и еще попытки освоить линукс, жизнь раз за разом сводила меня с его фанатами, которые пытались меня перетянуть в свой лагерь, я даже как-то попытался поставить генту (неудачно естественно, хотя у меня была распечатка с пошаговым руководством как это сделать), но проклятье консоли витало надо мной и с каждой неудачей то самое чувство беспомощности все усиливалось. После графического интерфейса винды, где все настройки перед тобой как на ладони, сталкиваясь с консолью я чувствовал себя ослепшим, пытающимся найти дорогу на ощупь в незнакомом месте. Так что я очень долго оставался приверженцем технологического стека Microsoft, программировал на .net, используя VisualStudio (она шикарна, и никакой консоли). Прошу не судить строго, возможно, если бы в то время у меня была возможность поставить линукс на виртуалку, чтобы спокойно с ней разбираться и приличный интернет под рукой, эта история приняла бы совсем другой оборот.

Сейчас все поменялось. С приходом в нашу жизнь веба в его современном виде я пересел на Node.js и волей-неволей стал проникаться идеологией консоли и Linux. Для хостинга своих разработок мне понадобился VPS, и тут оказалось, что VPS с виндой стоят гораздо дороже, так что моя жаба перевесила мои фрустрации относительно консоли, да и наличие скоростного интернета позволило мгновенно находить ответы на возникающие вопросы.

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

Подготовка


Я создал VPS минимальной конфигурации (vCPU: 1 core, RAM: 1 ГБ, NVMe: 20 ГБ) на macloud. Для установки на VPS я выбрал дистрибутив Debian 10. При установке я сразу добавил через панель управления SSH-ключ, чтобы было удобно заходить в консоль с помощью SSH-клиента. Для дальнейших экспериментов понадобится следующее:

  1. Обновить систему
  2. Установить docker и docker-compose
  3. Включить файл подкачки.

Обновить операционную систему можно следующими командами:

# apt updateОбновит список доступных пакетов# apt upgradeОбновит систему, установив свежие версии ПО.

Для установки docker следуем официальной инструкции https://docs.docker.com/engine/install/debian/

Устанавливаем необходимые зависимости:

# apt-get install apt-transport-https ca-certificates curl gnupg lsb-release

Добавляем официальный GPG ключ Dockerа:

# curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

Добавляем репозиторий:

# echo deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable | tee /etc/apt/sources.list.d/docker.list > /dev/null

Теперь осталось обновить список пакетов:

# apt-get update

и установить докер:
# apt-get install docker-ce docker-ce-cli containerd.io

Проверить, что все корректно установилось можно, запустив команду:

# docker run hello-world

Если появилось приветствие, значит все прошло хорошо. У меня на момент написания данной статьи установилась следующая версия:

# docker -vDocker version 20.10.6, build 370c289

Теперь надо установить docker-compose. Для этого воспользуемся официальной инструкцией:

https://docs.docker.com/compose/install/

Для начала нам нужно получить ссылку на свежий релиз docker-compose. Список релизов можно посмотреть тут: https://github.com/docker/compose/releases

На момент написания статьи наиболее свежей была версия 1.29.2. Нам нужна версия для linux, поэтому выбираем файл с названием docker-compose-Linux-x86_64. Скопируем ссылку на него. Теперь надо в консоли ввести следующую команду (вставьте в нужное место ссылку, полученную выше):

# curl -L "<ссылка на релиз>" -o /usr/local/bin/docker-compose

В моем случае получилось:

# curl -L https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64 -o /usr/local/bin/docker-compose

Запустим команду и загрузится бинарный файл docker-compose.

Последний шаг надо установить для него разрешение на исполнение. Это можно сделать следующей командой:

# chmod +x /usr/local/bin/docker-compose

Если всё прошло нормально, то на этом процесс установки окончен, теперь можно проверить версию docker-compose. У меня получилось вот так:

# docker-compose -vdocker-compose version 1.29.2, build 5becea4c

Последний шаг это включение файла подкачки, учитывая малый объем памяти он точно не будет лишним:

Создадим файл размером 4 Гб

# fallocate -l 4G /swapfile

Назначим ему необходимые права:

# chmod 600 /swapfile

Инициализируем его как файл подкачки:

# mkswap /swapfile

Наконец активируем:

# swapon /swapfile

Проверить, появился ли файл подкачки в системе можно командой free. У меня все получилось:

# freetotal    used    free   shared buff/cache  availableMem:    1010900   150500   143788    2892   716612   714916Swap:    4194300      0   4194300

Осталось последнее действие: для того, чтобы файл подкачки оставался активным после перезагрузки надо добавить в файл /etc/fstab следующую строку:

/swapfile swap swap defaults 0 0


Для подобных задач мне нравится использовать файловый менеджер Midnight Commander. Это консольный файловый менеджер с классическим интерфейсом. Если вы сталкивались с Norton Commander/FAR/TotalCommander вы легко поймете, как им пользоваться. Установить его можно следующей командой:

# apt-get install mc


А запустить с помощью команды

# mc

Увидим до боли знакомую картину:


Теперь найдем нужный файл и отредактируем его:


На этом предварительная настройка закончена, можно двигаться дальше.

Portainer


Первый инструмент, с которым мне бы хотелось вас познакомить это Portainer. Portainer это инструмент для управления контейнерами в Docker, Swarm, Kubernetes и Azure ACI. Как написано в документации Portainerа:

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

Для моих целей Portainer подходит просто идеально. Давайте познакомимся с ним поближе. В соответствии с документацией https://documentation.portainer.io/v2.0/deploy/ceinstalldocker/ проще всего это сделать следующим образом:

Создадим том для хранения данных:

# docker volume create portainer_data

Теперь можно запустить Portainer следующей командой:

# docker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce

После запуска Portainer будет доступен по адресу http://<ip серевера>:9000/

При первом запуске Portainer попросит вас указать пароль администратора, после чего спросит, каким образом подключится к окружению:


Я выбрал вариант подключится к локальному Dockerу, после чего появился домашний экран Portainerа:


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

Зайдем в него, и увидим удобный дэшбоард со сводной информацией:


Здесь мы можем управлять всеми аспектами работы Dockerа. Давайте посмотрим, что у нас в контейнерах:


Здесь пока один контейнер это сам Portainer. Интерфейс очень удобно позволяет просматривать и управлять контейнерами, а также есть возможность добавлять новые. Можете пройтись по остальным вкладкам и убедится, насколько удобный и продуманный интерфейс имеет Portainer.

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

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


Для определения стека используется формат docker-compose, именно он работает здесь под капотом. Мне при помощи этой функции удобнее всего построить из контейнеров необходимую инфраструктуру.

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

У меня периодически возникает потребность в разработке быстрых проектов. Я так называю проекты, которые не требуют глубокой проработки. Это могут быть эксперименты, небольшие сайты, боты, сборщики информации и т.п. Все то, что не требует выстраивания цепочки CI/CD. Мне было бы крайне удобно разрабатывать и отлаживать такие приложения прямо там, где они будут работать. Также у меня довольно часто бывает ситуация, когда я нахожусь вдали от своего рабочего места с настроенной средой разработки, и, если что-то в этих моих проектах начинает работать не так как надо, хотелось бы иметь возможность быстро разобраться в ситуации и пофиксить проблему, или просто поработать над проектами пока есть свободная минутка с любого компьютера, даже если на нем нет ничего, кроме браузера и доступа в интернет.

Для удобства доступа у меня есть купленное доменное имя, и я хочу настроить для различных инструментов домены третьего уровня, так, чтобы например portainer был доступен по адресу portainer.example.com. Также я хочу, чтобы всё работало через https, и, чтобы не заморачиваться с покупкой сертификатов SSL, воспользоваться Lets Encrypt. Еще одно требование, чтобы всё, к чему не предполагается публичный доступ было закрыто аутентификацией.

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

Traefik


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

  • обновление конфигурации на лету
  • поддержка dockerа в качестве провайдера конфигурации
  • работа с Lets encrypt из коробки

Для того, чтобы запустить traefik совместно с portainerом воспользуемся примером docker-compose.yml, приведенном в документации portainer: https://documentation.portainer.io/v2.0/ad/traefik/rp-traefik/

version: 3.9services:traefik:container_name: traefikimage: traefik:latestcontainer_name: traefikcommand:- --entrypoints.web.address=:80- --entrypoints.websecure.address=:443- --providers.docker=true- --providers.docker.exposedbydefault=false- --log.level=ERROR- --certificatesresolvers.leresolver.acme.httpchallenge=true- --certificatesresolvers.leresolver.acme.email=user@mymail.com- --certificatesresolvers.leresolver.acme.storage=./acme.json- --certificatesresolvers.leresolver.acme.httpchallenge.entrypoint=web- --entrypoints.web.http.redirections.entryPoint.to=websecure- --entrypoints.web.http.redirections.entryPoint.scheme=https- --metrics.prometheus=trueports:- 80:80- 443:443volumes:- /var/run/docker.sock:/var/run/docker.sock:ro- ./acme.json:/acme.jsonnetworks:- intranetlabels:- traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)- traefik.http.routers.http-catchall.entrypoints=web- traefik.http.routers.http-catchall.middlewares=redirect-to-https- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=httpsportainer:image: portainer/portainer-ce:2.5.0-alpinecontainer_name: portainercommand: -H unix:///var/run/docker.sockrestart: alwaysvolumes:- /var/run/docker.sock:/var/run/docker.sock- portainer_data:/datanetworks:- intranetlabels:- traefik.enable=true- traefik.http.routers.frontend.rule=Host(`portainer.example.com`)- traefik.http.routers.frontend.entrypoints=websecure- traefik.http.services.frontend.loadbalancer.server.port=9000- traefik.http.routers.frontend.service=frontend- traefik.http.routers.frontend.tls.certresolver=leresolvervolumes:portainer_data:networks:intranet:name: intranet

Чтобы процесс получения сертификата Lets encrypt прошел успешно, перед запуском надо убедится, что записи DNS корректно настроены. Я для этих целей использую cloudflare, и в нем конфигурация должна выглядеть примерно так:


В графе Content должен находится адрес нашего VPS.

Если теперь скопировать получившийся docker-compose.yml на сервер выполнить следующую команду:

# docker-compose up -d

То после запуска по адресу portainer.example.com будет такая картина:


Причем он уже будет защищен сертификатом Lets Encrypt:


У traefik есть очень удобный дашборд, который помогает понять, правильно ли применились настройки конфигурации. Для того, чтобы его активировать, надо добавить в docker-compose.yml следующие строки:

services:traefik:...command:...- --api.dashboard=truelabels:...- traefik.enable=true- traefik.http.routers.traefik.entrypoints=websecure- traefik.http.routers.traefik.rule=Host(`traefik.example.com`)- traefik.http.routers.traefik.tls=true- traefik.http.routers.traefik.service=api@internal- traefik.http.routers.traefik.tls.certresolver=leresolver- traefik.http.services.traefik.loadbalancer.server.port=8080

Также надо в настройках DNS добавить домен третьего уровня traefik.example.com. Это можно сделать по аналогии с тем, как ранее был добавлен домен для portainer. После применения настроек по адресу traefik.example.com увидим дашборд:


Как видите, Traefik оказался замечательным инструментом. Он работает в тесной связке с dockerом, а его возможности динамической конфигурации позволяют полностью отказаться от использования конфигурационных файлов для настроек роутинга. Вместо этого мы просто прописываем все необходимые параметры в качестве labels для настраиваемого сервиса, так что все настройки оказываются в одном месте.

Теперь предлагаю немного отвлечься от настройки базовых сервисов и добавить в систему среду разработки. Это будет

Visual Studio Code Server


Для меня всегда среда разработки или IDE, была чем-то таким серьезным. Мощный программный пакет, который устанавливается на машине разработчика, занимает много гигабайт на диске и в оперативной памяти. Пример такой IDE, которой я пользовался много лет, и до сих пор считаю, что это лучший выбор, если вы работаете со стеком технологий Microsoft, это Microsoft VisualStudio. Когда я начал изучать Node.js, я открыл для себя VSCode, и, несмотря на родственные названия, это совсем другая IDE, с совершенно иной концепцией и возможностями. Тот факт, что для отображения своего интерфейса VSCode использует движок Chrome, позволяет разнести интерфейс и сам VSCode. Благодаря такой архитектуре возник Visual Studio Code Server, который может работать на VPS, при этом интерфейс VSCode будет доступен через браузер. И нет, это не очередной онлайн редактор кода, это полноценная IDE VSCode, которая обладает всеми ее замечательными возможностями.

Чтобы добавить VSCode Server на свой VPS я в Portainerе создам новый stack, назову его code-server и добавлю туда следующую конфигурацию:

version: 3.9volumes:codeserverdata:codeappdir:networks:intranet:external: trueservices:code-server:image: ghcr.io/linuxserver/code-servercontainer_name: code-serverenvironment:- PUID=1000- PGID=1000- TZ=Europe/London#    PASSWORD=password #optional- SUDO_PASSWORD=password #optional- PROXY_DOMAIN=code.example.comvolumes:- codeserverdata:/config- codeappdir:/appextra_hosts:host.docker.internal: host-gatewayrestart: alwaysnetworks:- intranetlabels:- traefik.enable=true- traefik.http.routers.code.rule=Host(`code.example.com`)- traefik.http.routers.code.tls=true- traefik.http.routers.code.tls.certresolver=leresolver


Также перед запуском надо не забыть добавить в записи DNS домен третьего уровня code.example.com. Теперь осталось нажать кнопку Deploy the stack.

После окончания процесса в portainerе появится новый stack, и, если мы зайдем по адресу code.example.com, то увидим такую картину (я сразу включил dark theme):


Здесь можно вести разработку также как в десктопной версии VSCode. По большому счету, единственное отличие, с которым я столкнулся, это разница в дефолтных биндингах некоторых сочетаний клавиш, но к этому быстро привыкаешь в процессе использования.

Для удобства я подключил к контейнеру code-servera том, который смонтировал в директорию /app, так что создавать или клонировать из репозитория проекты лучше всего там, в таком случае данные не пропадут даже при пересоздании контейнера.

В данном образе уже установлен Node.js, так что мне не пришлось ничего делать, и я могу сразу приступить к работе над своими проектами. Если же вам в работе нужны другие ЯП, то обратите внимание, что создатели данного образа поддерживают каталог модов, которые позволяют добавить поддержку различных платформ. О том, как их использовать, можно почитать в описании образа на Docker Hub, а со списком официальных модов можно ознакомиться здесь: mods.linuxserver.io/?mod=code-server

Обратите также внимание на то, что в настройках контейнера указан пароль sudo. Для демонстрации я оставил его простым, но на практике лучше сделать его сложным, а еще лучше использовать свойство SUDO_PASSWORD_HASH, чтобы не хранить пароль в открытом виде. Как это сделать, можно почитать в описании образа здесь: hub.docker.com/r/linuxserver/code-server.

Как вы наверное уже заметили на данный момент доступ к code-server никак не защищен и сейчас любой, кто зайдет по адресу code.example.com получит доступ. Это очень плохой вариант, и, хотя в настройках образа можно включить доступ по паролю, мне бы хотелось иметь единый логин для доступа ко всем ресурсам, расположенным на сервере. Для этого предлагаю познакомиться со следующим инструментом. Это будет

KeyCloak


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

Для того, чтобы добавить его в мою систему, я создал в portainerе новый стек с названием auth и добавил в него следующую конфигурацию:

version: '3.9'networks:intranet:external: trueservices:keycloak:image: jboss/keycloakcontainer_name: keycloakrestart: alwaysnetworks:- intranetenvironment:KEYCLOAK_PASSWORD: passwordPROXY_ADDRESS_FORWARDING: truelabels:- traefik.enable=true- traefik.http.routers.keycloak.rule=Host(`auth.yourdomain.com`)- traefik.http.routers.keycloak.tls=true- traefik.http.routers.keycloak.tls.certresolver=leresolver


После нажатия кнопки Deploy the stack, KeyCloak будет доступен по адресу auth.example.com. Если мы зайдем туда, нас встретит приветственное окно KeyCloak:



Зайдем в консоль администратора:


Имя пользователя будет admin, а начальный пароль тот, что мы установили в конфигурации стека переменной окружения KEYCLOAK_PASSWORD. После логина попадем в админку KeyCloak:


Рекомендации по первоначальной настройке KeyCloak для dockerа можно подчерпнуть в официальной документации вот здесь www.keycloak.org/getting-started/getting-started-docker.

Вкратце, надо создать новый пользовательский Realm, в нем создать пользователя и добавить нового клиента.

Для клиента необходимо установить Access Type: confidential и добавить в Valid Redirect URIs наши домены, пока это будут https://traefik.example.com/* и code.example.com*:



После установки Access Type: confidential появится вкладка Credentials, в которой можно будет забрать Secret, он нам пригодится далее при настройке.

На этом пока закончим настройку KeyCloak. Теперь нам надо подружить его с Traefikом. Напомню, что мы хотим закрыть от неаутентифицированных пользователей доступ к узлам code.example.com и traefik.example.com. Для этих целей у traefik есть ForwardAuth middleware, которое позволяет организовать авторизацию через внешний сервис. Чтобы обеспечить его взаимодействие KeyCloak нам понадобится промежуточный сервис, я буду использовать github.com/thomseddon/traefik-forward-auth. Он доступен также в качестве образа на Docker Hub, поэтому я просто дополню конфигурацию стека auth в portainerе таким сервисом:

traefik-forward-auth:image: thomseddon/traefik-forward-authcontainer_name: traefik-forward-authenvironment:- DEFAULT_PROVIDER=oidc- PROVIDERS_OIDC_ISSUER_URL=https://auth.example.com/auth/realms/example- PROVIDERS_OIDC_CLIENT_ID=traefik- PROVIDERS_OIDC_CLIENT_SECRET=d7fb86f0-71a9-44f7-ab04-967f086cd89e- SECRET=something-random- LOG_LEVEL=debuglabels:- traefik.enable=true- traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://traefik-forward-auth:4181- traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User- traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181restart: always

Здесь в переменной PROVIDERS_OIDC_ISSUER_URL должен быть путь к ранее созданному нами Realm в Keycloak, PROVIDERS_OIDC_CLIENT_ID должен содержать имя клиента, созданного мной ранее в данном realmе, а PROVIDERS_OIDC_CLIENT_SECRET надо взять из вкладки Credentials данного клиента. В переменной SECRET надо забить рандомную строку.

Теперь, чтобы закрыть сервис, роутинг в который обеспечивает Traefik достаточно добавить к нему в labels следующую строку:

- "traefik.http.routers.<имя роутера>.middlewares=traefik-forward-auth"


Для начала я решил закрыть аутентификацией code server, для чего зашел в его стек и дополнил конфигурацию его конфигурацию. Получилось следующее (для краткости привожу только секцию labels):

labels:- traefik.enable=true- traefik.http.routers.code.rule=Host(`code.example.com`)- traefik.http.routers.code.tls=true- traefik.http.routers.code.tls.certresolver=leresolver     - traefik.http.routers.code.middlewares=traefik-forward-auth


Нажмем кнопку Update Stack и попробуем зайти по адресу code.example.com. Если все сделано правильно, то появится окно логина:


После ввода корректных имени пользователя и пароля (которые до этого настроил в KeyCloak) я попал в интерфейс Code server. Все работает!

Подобным образом я закрыл от посторонних глаз дашборд traefikа. Для этого пришлось сходить в консоль (Portainer не может вносить изменения в конфигурацию стека, который был создан не им, а Traefik я поднимал из консоли) и аналогичным образом отредактировать docker-compose.yml:

labels:- traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)- traefik.http.routers.http-catchall.entrypoints=web- traefik.http.routers.http-catchall.middlewares=redirect-to-https- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https- traefik.enable=true- traefik.http.routers.traefik.entrypoints=websecure- traefik.http.routers.traefik.rule=Host(`traefik.example.com`)- traefik.http.routers.traefik.tls=true- traefik.http.routers.traefik.service=api@internal- traefik.http.routers.traefik.tls.certresolver=leresolver- traefik.http.routers.traefik.middlewares=traefik-forward-auth- traefik.http.services.traefik.loadbalancer.server.port=8080


Для проверки я зашел по адресу traefik.example.com. Чтобы удостоверится, что все работает как надо, мне пришлось открыть окно браузера в режиме инкогнито, иначе система меня узнавала и не спрашивала пароль, поскольку я ранее уже логинился для доступа к code-server и поэтому KeyCloak аутентифицировал меня автоматически.

Таким образом, при помощи связки KeyCloak и Traefik мне удалось защитить от несанкционированного доступа чувствительные элементы моей системы. Преимущество данного подхода в том, что он позволяет сделать это даже там, где не предусмотрены собственные механизмы аутентификации. Есть конечно и недостатки KeyCloak достаточно тяжелый, занимает много ресурсов, в первую очередь памяти, да и по возможностям это явный overkill, мне скорее всего не потребуется большая часть того, что он умеет. В качестве альтернативы можно бы было использовать облачный сервис, например traefik-forward-auth имеет встроенную поддержку Google OAuth.

Выводы


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

  • Подключение доменных имен
  • Настройка TLS
  • Получение и установка сертификатов
  • Аутентификация

делаются в несколько строчек прямо в конфигурации сервиса.

Конечно, при настройке мне пришлось многое делать из консоли, но в дальнейшем, при повседневном использовании построенная система позволит мне сократить ее использование, что для меня несомненно плюс.

Когда я писал эту статью, главной сложностью было собрать всю необходимую информацию воедино. Многие вещи из документации неочевидны и зачастую чтобы добиться того, чтобы система заработала нужным образом приходилось искать решение проблемы по различным issues на github и вопросам со stackoverflow. Поэтому я постарался ряд моментов осветить более подробно, и надеюсь, кому-нибудь мои изыскания помогут лучше разобраться в описываемых продуктах.

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

Для удобства файлы конфигураций я разместил в репозитории вот тут: https://github.com/debagger/vps-docker-workspace

Благодарю за внимание!



Облачные серверы от Маклауд быстрые и безопасные.

Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru