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

Docker-compose

Из песочницы Как мы выбирали VPN-протокол и сервер настраивали

23.08.2020 18:21:00 | Автор: admin

Зачем всё это и для чего?


У нас было: 10 самых простых конфигураций серверов на DigitalOcean, мобильные устройства на базе iOS, сервер для сбора статистики, никакого опыта в настройке VPN-серверов, а также неукротимое желание сделать быстрый, надёжный и простой в использовании VPN-сервис, которым будет приятно пользоваться. Не то, чтобы всё это было категорически необходимо, но если уж начали, то к делу надо подходить серьёзно.


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


Я думаю, что в IT сфере уже не найти человека, который не знал бы, что такое VPN и зачем он нужен.


Но если тезисно объяснять, зачем нужен VPN современному человеку, то получится примерно следующее:


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

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


Как мы выбирали VPN-протокол


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


Мы выбрали самые известные реализации протоколов и отсеяли не подходящие по условиям исходной задачи.


А условия всего два, я напомню:


  • Стабильное и надёжное подключение.
  • Без установки стороннего программного обеспечения на устройство клиента.

Пробегусь по протоколам и кратко расскажу о них + расскажу причины, почему тот или иной протокол нам не подошёл.


PPTP (Point-To-Point Tunneling Protocol)


Один из самых старейших VPN протоколов, разработанный компанией Microsoft. Из-за солидного возраста протокол поддерживается большинством операционных систем, но в то же время не может обеспечить стабильное и надёжное соединение. Компания Microsoft советует использовать L2TP или SSTP на замену PPTP.


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


L2TP/IPSec


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


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


IKEv2/IPSec


Был разработан Microsoft совместно с Cisco, существуют реализации протокола с открытым исходным кодом (например, OpenIKEv2, Openswan и strongSwan).


Поддерживает Mobility and Multi-homing Protocol (MOBIKE), что обеспечивает устойчивость к смене сетей.


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


IKEv2 имеет нативную поддержку в большинстве операционных систем.


Вот этот вариант нам уже больше подходит, т.к. поддержка Mobility and Multi-homing Protocol будет очень большим плюсом при использовании на мобильных устройствах.


OpenVPN


Разработан компанией OpenVPN Technologies.


Протокол с открытым исходным кодом, который прошёл все возможные проверки безопасности.
Протокол OpenVPN стабилен и может обеспечить хорошую скорость передачи данных. Ещё одно преимущество протокола в том, что он использует для работы стандартные протоколы TCP и UPD, а также может работать на любом из портов. Это усложняет блокировку VPN сервиса провайдерами.


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


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


Wireguard


На данный момент это самый свежий протокол VPN. Его часто сравнивают с IPSec и OpenVPN, и называют его их заменой, но он всё ещё слишком сырой, чтобы использовать его в больших масштабах.


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


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


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


В итоге мы решили остановится на IKEv2/IPSeс, по следующим причинам:


  • Поддержка Mobility and Multi-homing Protocol (MOBIKE).
  • Нативная поддержка в большинстве операционных систем.
  • Обеспечивает высокую скорость соединения.
  • Не требователен к ресурсам сервера.

Перейдём от теории к практике.


Настраиваем VPN-сервер


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


Самый простой критерий выбора расположения сервера это удалённость от вас, т.е. если будет выбор между размещением сервера в Германии или в США, то своё предпочтение следует отдать Германии (если вы находитель в России), т.к., в теории, ваш трафик будет проходить через меньшее кол-во магистралей и будет идти по более короткому маршруту.


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


От слов к практике, каких-то особых навыков и тайных знаний для настройки VPN-сервера не понадобится.


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


  • Docker + docker-compose.
  • strongswan реализацию IPSec сервера.
  • Let's Encrypt для генерации сертификатов.
  • Radius для мониторинга и отправки статистических данных.

Начнём с Docker контейнера, в котором и будет запускаться vpn-сервер.


Docker.file (полная версия)


FROM alpine:latest #сервер будем собирать на основе образа alpine-linux...# strongSwan VersionARG SS_VERSION="http://personeltest.ru/aways/download.strongswan.org/strongswan-5.8.2.tar.gz" #версию можете выбрать сами, исходя из того когда вы читаете данную статью....COPY ./run.sh /run.shCOPY ./adduser.sh /adduser.shCOPY ./rmuser.sh /rmuser.shRUN chmod 755 /run.sh /adduser.sh /rmuser.shVOLUME ["/usr/local/etc/ipsec.secrets"]EXPOSE 500:500/udp 4500:4500/udpCMD ["/run.sh"]

Для управления пользователями мы создаём два скрипта adduser.sh, rmuser.sh для добавления и удаления пользователя соответственно.


adduser.sh


#!/bin/shVPN_USER="$1"if [ -z "$VPN_USER" ]; then  echo "Usage: $0 username" >&2  echo "Example: $0 jordi" >&2  exit 1ficase "$VPN_USER" in  *[\\\"\']*)    echo "VPN credentials must not contain any of these characters: \\ \" '" >&2    exit 1    ;;esacVPN_PASSWORD="$(openssl rand -base64 9)" #генерируем пароль для пользователяHOST="$(printenv VPNHOST)"echo "Password for user is: $VPN_PASSWORD"echo $VPN_USER : EAP \"$VPN_PASSWORD\">> /usr/local/etc/ipsec.secrets # сохраняем имя пользователя и пароль в файл /usr/local/etc/ipsec.secretsipsec rereadsecrets

rmuser.sh


#!/bin/shVPN_USER="$1"if [ -z "$VPN_USER" ]; then  echo "Usage: $0 username" >&2  echo "Example: $0 jordi" >&2  exit 1ficp /usr/local/etc/ipsec.secrets /usr/local/etc/ipsec.secrets.baksed "/$VPN_USER :/d" /usr/local/etc/ipsec.secrets.bak > /usr/local/etc/ipsec.secrets # удаляем строку и с пользователем из файла /usr/local/etc/ipsec.secretsipsec rereadsecrets

На сервере все пользователи будут храниться в файле ipsec.secrets.


Для запуска нашего сервера подготовим следующий скрипт:


run.sh полная версия


#!/bin/bashVPNIPPOOL="10.15.1.0/24" # указываем из какого диапазона будут выдаваться IP нашим клиентам, которые будут подключаться к VPN-серверу.LEFT_ID=${VPNHOST}       # домен нашего vpn-сервераsysctl net.ipv4.ip_forward=1sysctl net.ipv6.conf.all.forwarding=1sysctl net.ipv6.conf.eth0.proxy_ndp=1if [ ! -z "$DNS_SERVERS" ] ; then # можем указать свои DNS сервера, которые будут использоваться в vpn сервере.DNS=$DNS_SERVERSelseDNS="1.1.1.1,8.8.8.8"fiif [ ! -z "$SPEED_LIMIT" ] ; then # для того, чтобы один пользователь не "съел" весь канал, можем ограничить скорость пользователя до указанной величины.tc qdisc add dev eth0 handle 1: ingresstc filter add dev eth0 parent 1: protocol ip prio 1 u32 match ip src 0.0.0.0/0 police rate ${SPEED_LIMIT}mbit burst 10k drop flowid :1tc qdisc add dev eth0 root tbf rate ${SPEED_LIMIT}mbit latency 25ms burst 10kfiiptables -t nat -A POSTROUTING -s ${VPNIPPOOL} -o eth0 -m policy --dir out --pol ipsec -j ACCEPTiptables -t nat -A POSTROUTING -s ${VPNIPPOOL} -o eth0 -j MASQUERADEiptables -L# Здесь мы генерируем сертификат сервераif [[ ! -f "/usr/local/etc/ipsec.d/certs/fullchain.pem" && ! -f "/usr/local/etc/ipsec.d/private/privkey.pem" ]] ; then    certbot certonly --standalone --preferred-challenges http --agree-tos --no-eff-email --email ${LEEMAIL} -d ${VPNHOST}    cp /etc/letsencrypt/live/${VPNHOST}/fullchain.pem /usr/local/etc/ipsec.d/certs    cp /etc/letsencrypt/live/${VPNHOST}/privkey.pem /usr/local/etc/ipsec.d/private    cp /etc/letsencrypt/live/${VPNHOST}/chain.pem /usr/local/etc/ipsec.d/cacertsfirm -f /var/run/starter.charon.pid# Настройка непосредственно ipsec сервераif [ -f "/usr/local/etc/ipsec.conf" ]; thenrm /usr/local/etc/ipsec.confcat >> /usr/local/etc/ipsec.conf <<EOFconfig setup    charondebug="ike 1, knl 1, cfg 1"    uniqueids=never    conn ikev2-vpn    ...    eap_identity=%identityEOFfiif [ ! -f "/usr/local/etc/ipsec.secrets" ]; thencat > /usr/local/etc/ipsec.secrets <<EOF: RSA privkey.pemEOFfi.....EOFfisysctl -pipsec start --nofork

Чтобы было проще запустить весь сервер одной командой, завернём всё в docker-compose:


version: '3'services:  vpn:    build: .    container_name: ikev2-vpn-server    privileged: true    volumes:      - './data/certs/certs:/usr/local/etc/ipsec.d/certs'      - './data/certs/private:/usr/local/etc/ipsec.d/private'      - './data/certs/cacerts:/usr/local/etc/ipsec.d/cacerts'      - './data/etc/ipsec.d/ipsec.secrets:/usr/local/etc/ipsec.secrets'    env_file:      - .env    ports:      - '500:500/udp'      - '4500:4500/udp'      - '80:80'    depends_on:      - radius    links:      - radius    networks:      - backend  radius:    image: 'freeradius/freeradius-server:latest'    container_name: freeradius-server    volumes:      - './freeradius/clients.conf:/etc/raddb/clients.conf'      - './freeradius/mods-enabled/rest:/etc/raddb/mods-enabled/rest'      - './freeradius/sites-enabled/default:/etc/raddb/sites-enabled/default'    env_file:      - .env    command: radiusd -X    networks:      - backendnetworks:  backend:    ipam:      config:        - subnet: 10.0.0.0/24

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


Пробрасываем порты, необходимые для подключения к серверу, а также для генерации сертификатов через Let's Encrypt.


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


VPNHOST=vpn.vpn.com # домен нашего vpn-сервераLEEMAIL=admin@admin.com # адрес почты, который будет использован для генерации сертификатов Let's EncryptSPEED_LIMIT=20 # если нужно, то указываем как лимит скорости в mbitDNS_SERVERS= # если нужно то указываем собственные DNS сервераRADIUS_SERVER= # адрес radius сервера, в нашем случае это будет radiusRADIUS_SERVER_SECRET= # секретный ключ, с помощью которого проходит авторизация на radius сервереREMOTE_SERVER= # в эту переменную вынесли endpoint, на который отправлялась статистика из radius сервера, об этом расскажу далее.

Выполняя команду docker-compose up -d, мы запускаем наш vpn-сервер, а также radius сервер (если он вам нужен).


Вот так выглядит весь проект в сборке


Сбор статистики с VPN-сервера


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


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


FreeRadius сервер можно использовать и для авторизации пользователей, но мы остановились только на сборе статистики.
Для включения radius необходимо добавить следующее в конфигурацию ipsec:


FreeRADIUS сервер для мониторинга подключений к серверу и сбора статистики


if [[ ! -z "$RADIUS_SERVER" && ! -z "$RADIUS_SERVER_SECRET" ]]; thenrm /usr/local/etc/strongswan.d/charon/eap-radius.confcat >> /usr/local/etc/strongswan.d/charon/eap-radius.conf <<EOFeap-radius {    accounting = yes # включаем аккаунтинг, ipsec будет отправлять данные о том что пользователь подключился/отключился, а также отправлять данные о объёме используемого трафика    accounting_close_on_timeout = no    accounting_interval = 300 # интервал через который radius сервер будет отправлять данные нам.    close_all_on_timeout = no    load = yes    nas_identifier = $VPNHOST    # Section to specify multiple RADIUS servers.    servers {        primary {            address = $RADIUS_SERVER            secret = $RADIUS_SERVER_SECRET            auth_port = 1812   # default            acct_port = 1813   # default        }    }}

Чтобы наши данные уходили на наш endpoint, включаем модуль rest. Для этого в файле /etc/raddb/mods-enabled/rest настраиваем блок accounting, получится что-то вроде:


accounting {    uri = "${..connect_uri}/vpn_sessions/%{Acct-Session-Id}-%{Acct-Unique-Session-ID}"method = 'post'tls = ${..tls}body = jsondata = '{ "username": "%{User-Name}", "nas_port": "%{NAS-Port}", "nas_ip_address": "%{NAS-IP-Address}", "framed_ip_address": "%{Framed-IP-Address}", "framed_ipv6_prefix": "%{Framed-IPv6-Prefix}", "nas_identifier": "%{NAS-Identifier}", "airespace_wlan_id": "%{Airespace-Wlan-Id}", "acct_session_id": "%{Acct-Session-Id}", "nas_port_type": "%{NAS-Port-Type}", "cisco_avpair": "%{Cisco-AVPair}", "acct_authentic": "%{Acct-Authentic}", "tunnel_type": "%{Tunnel-Type}", "tunnel_medium_type": "%{Tunnel-Medium-Type}", "tunnel_private_group_id": "%{Tunnel-Private-Group-Id}", "event_timestamp": "%{Event-Timestamp}", "acct_status_type": "%{Acct-Status-Type}", "acct_input_octets": "%{Acct-Input-Octets}", "acct_input_gigawords": "%{Acct-Input-Gigawords}", "acct_output_octets": "%{Acct-Output-Octets}", "acct_output_gigawords": "%{Acct-Output-Gigawords}", "acct_input_packets": "%{Acct-Input-Packets}", "acct_output_packets": "%{Acct-Output-Packets}", "acct_terminate_cause": "%{Acct-Terminate-Cause}", "acct_session_time": "%{Acct-Session-Time}", "acct_delay_time": "%{Acct-Delay-Time}", "calling_station_id": "%{Calling-Station-Id}", "called_station_id": "%{Called-Station-Id}"}' }

Здесь мы можем как угодно комбинировать данные и отправлять на наш сервер.


При настройке VPN сервера столкнулись с некоторыми нюансами, вроде таких, что устройства Apple не могут подключить к серверу, если на нём будет самоподписанный сертификат, всё заработало только после того, как сертификат начали генерировать через Let's Encrypt.


Какие недостатки присутствуют в нашей сборке?


  • Если radius сервер не отвечает, то пользователь не сможет подключиться к VPN.
  • Если закончится срок действия сертификата, пользователь не сможет авторизоваться на VPN-сервере.

Что можно было бы добавить в следующих версиях VPN-сервера?


  • Авторизацию пользователей вынести в radius.
  • Добавить автоматическое обновление сертификата.
  • Провести рефакторинг скриптов, а также файлов конфигураций.
  • Добавить health-check для впн сервера.

Что же у нас получилось в итоге?


Методом проб и ошибок мы пришли к варианту, который описан в статье, прежде всего мы придерживались принципа "делай проще", не стали изобретать свои велосипеды и воспользовались уже готовыми инструментами типа Docker, FreeRadius. Да, скорее всего тут есть место для оптимизации, ужесточения политик безопасности, автоматизации. Но наш вариант отлично подойдёт для личного использования и для использования в небольших компаниях, если вам нужно организовать доступ к приватной (закрытой) информации.

Подробнее..

Перевод Создание собственной Headless CMS и интеграция с блогом

05.08.2020 08:12:42 | Автор: admin

Hero image


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


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


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


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


Исходный код проекта вместе с пошаговым руководством по пользовательскому интерфейсу и API (рекомендую не читать, пока не закончите статью):


Vidzhel/Bluro


Преамбула


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


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


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


Общая структура


Воодушевленный возможностями, я не мог дождаться, чтобы начать работать над новым проектом. Идея состояла в том, чтобы создать Headless (распределенную) CMS, которую я назвал Bluro. Систему я решил дополнить расширенным Hello world приложение, блогом TechOverload и панелью администрирования для него.


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


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


Подсумировав вышесказанное, я выписал полный список функций:


  • Регистрация, вход, изменение и удаление профиля
  • Публикация, сохранение в черновиках, модификация, удаление статей
  • Создание, модификация, удаление комментариев
  • Возможность подписаться, отписаться от автора
  • Создание, чтение, удаление уведомлений

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


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

Проблема была в том, что я даже не знал с чего начать, кроме того у меня было всего два месяца. Вы можете сказать, что это не проблема для программиста, а скорее типичное состояние. Да, я согласен, за исключением одной маленькой вещи: я мог найти сотни статей, которые объясняют, что означает Headless CMS, но разве что намеки на то, как создать простую версию для начинающих.


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


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


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


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


Диаграмма компонентов системы

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


Поскольку я хотел углубить свой опыт работы с JavaScript, я выбрал NodeJS для бэкэнда и React для фронтендов. Это было бы отличным дополнением к моему портфолио, подумал я тогда.


Разрабатываем Bluro CMS


Headless CMS похожа на обычную, но не имеет уровня представления (UI). Таким образом, она перекладывает задачу отображения графического интерфейса на плечи других компонентов. Вместо этого CMS предоставляет API (REST API в нашем случае, подробнее об этом здесь), чтобы другие могли общаться с ней.


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


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


Частый способ построения такой системы использование шаблона проектирования MVC (Model View Controller). Таким образом мы разделяем структуру на контроллеры и модели данных (у нас нет представлений).


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


Внедрение дополнительного функционала в нашу CMS требует отделения контроллеров от других компонентов.


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


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


Диаграмма компонентов Bluro CMS


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


Main является главным модулем, который загружает систему и другие модули.


ORM


Самый большой и одновременно сложный модуль, являющийся абстракцией между базой данных и другими компонентами ORM (Object Relational Mapper).


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


Центральная сущность модуля Модель. Модели дают нам методы для запроса данных, не вдаваясь в детали, такие как написание SQL запросов.


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


Архитектура уровня данных (первый вариант)


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


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


Я позволю себе немного отклониться от темы. Не всегда будет возможно найти туториал, объясняющий то что вам нужно, что может раздражать. Но не забывайте о миллионах проектов с открытым исходным кодом. Учитывая такую возможность, вы можете узнать гораздо больше, чем следуя какому-то уроку. Помимо того, что вы можете найти ответы на свои вопросы или обнаружить необычный паттерны, вы можете научиться читать чей-то код, что является важным навыком для программистов. Кто знает, вы можете найти что-то интересное и написать в блоге об этом: ).


Итак, вернемся к делу. Я посмотрел в библиотеку Sequelize и пересмотрел API Django, чтобы имитировать его. Вот как я реализовал ORM.


Архитектура ORM


Самые верхние Entities это сущности, которые используются для запроса данных из базы (обычно они имеют то же имя, что и таблицы). Класс Model использует QuerySet для фильтрации, сортировки и извлечения данных. В свою очередь, QuerySet зависит от Statement, который предоставляет удобный API для построения запросов. StatementsBuilder это абстрактный класс, используемый Statement для создания кусочков запроса. Затем у нас есть несколько реализаций, которые вводят определения типов данных и операторов после чего реализуют методы для работы с конкретным диалектом.


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


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


Вот пример использования моей ORM. Не идеально, но для первой попытки сойдет.




Расширение системы


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


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


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


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


Пример модулей вы можете найти здесь.


Создание API


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


Схема базы данных


Auth


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


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


Когда пользователь успешно входит в систему, генерируется JWT (JSON Web Token) после чего он отправляется в виде cookie. Токен является закодированным объектом со всем необходимым для идентификации данными. Последующие запросы будут использовать его для авторизации пользователя.


Кроме того, модуль предоставляет два правила:


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

Article


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


Comment


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


Notifications


Модуль отвечающий за управление уведомлениями. Он использует
NotificationService для дублирования уведомления на электронную почту.


Вот пример стандартного запроса и ответа от API:



fetchData это сага, которая использует Fetch API для выполнения запроса. Если запрос занимает больше времени, чем TIMEOUT, мы отменяем его. makeRequest использует предыдущую сагу для запроса некоторых данных, а затем обрабатывает их. Остальные саги используют эти утилиты для получения необходимых данных или выполнения каких-либо действий. Например, здесь вы можете увидеть, как я выполняю открытие статьи:



Используя Docker Compose, я создал контейнеры из которых собрал сервисы. Здесь вы можете найти конфиги со скриптами, которые облегчают развертывание (у меня есть единственная версия сервиса для разработки).




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


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

Подробнее..

Шаблон Kotlin микросервисов

27.02.2021 20:23:19 | Автор: admin

Для разработчиков не секрет, что создание нового сервиса влечет за собой немало рутиной настройки: билд скрипты, зависимости, тесты, docker, k8s дескрипторы. Раз мы выполняем эту работу, значит текущих шаблонов IDE недосточно. Под катом мои попытки автоматизировать все до одной кроссплатформенной кнопки "сделать хорошо" сопровождаемые кодом, примерами и финальным результатом.
Если перспективы создания сервисов в один клик с последующим автоматическим деплоем в Digital Ocean звучат заманчиво, значит эта статья для вас.

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

plugins {  kotlin("jvm") version "1.4.30"  // Чтобы собрать fat jar  id("com.github.johnrengelman.shadow") version "6.1.0"  // Чтобы собрать self-executable приложение с jvm  id("org.beryx.runtime") version "1.12.1"}

Из зависимостей, в качестве серверного фреймворка был выбран "родной" для Kotlin Ktor. Для тестирования используется связка testNG + Hamkrest с его выразительным DSL, позволяющим писать тесты таким образом:

assertThat("xyzzy", startsWith("x") and endsWith("y") and !containsSubstring("a"))

Собираем все вместе, ориентируясь на Java 15+

dependencies {  // для парсинга аргументов командой строки  implementation("com.github.ajalt.clikt:clikt:3.1.0")  implementation("io.ktor:ktor-server-netty:1.5.1")  testImplementation("org.testng:testng:7.3.0")  testImplementation("com.natpryce:hamkrest:1.8.0.1")  testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")}application {  @Suppress("DEPRECATION") // for compatibility with shadowJar  mainClassName = "AppKt"}tasks {  test {    useTestNG()  }  compileKotlin {    kotlinOptions {      jvmTarget = "15"    }  }}

В исходный код генерируемый шаблоном по умолчанию добавлен entry-point обработки аргументов командой строки, заготовка ддя роутинга, и простой тест (заодно служащий примером использоования testNG с Hamkrest).
Из того что следует отметить, позволил себе небольшую вольность с официальным Kotlin codestyle чуть-чуть поправив его в .editorsconfig:

[*.{kt, kts, java, xml, html, js}]max_line_length = 120indent_size = 2continuation_indent_size = 2

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

gradle clean test shadowJar

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

Осталось написать Dockerfile, его привожу целиком. Сборка и запуск разделены и производятся в два этапа:

# syntax = docker/dockerfile:experimentalFROM gradle:jdk15 as builderWORKDIR /appCOPY src ./srcCOPY build.gradle.kts ./build.gradle.ktsRUN --mount=type=cache,target=./.gradle gradle clean test shadowJarFROM openjdk:15-alpine as backendWORKDIR /rootCOPY --from=builder /app/*.jar ./app

Работать сервис будет в контейнере с jdk (а не jvm), ради простоты ручной диагностики с помощью jstack/jmap и других поставляемых с jdk инструментов.
Сконфигурируем запуск приложения при помощи Docker Compose:

version: "3.9"services:  backend:    build: .    command: java -jar app $BACKEND_OPTIONS    ports:      - "80:80"

Теперь мы можем запускать наш сервис на целевой машине, без дополнительных зависимостей в виде Jdk/Gradle, при помощи простой команды

docker-compose up

Как деплоить сервис в облако? Выбрал Digital Ocean по причине дешевой стоимости и простоты управления. Благодаря тому что мы только что сконфигурировали сборку и запуск в контейнере, можно выбрать наш репозиторий с проектом в разделе Apps Platform и... все! Файлы конфигурации Docker будут подцеплены автоматически, мы увидим логи сборки, а после этого получим доступ к веб адресу, логам приложения, консоли управления, простым метрикам потребления памяти и процессорного времени. Выглядит это удовольствие примерно так и стоит 5$ в месяц:

При последующих изменениях в master ветке репозитория, передеплой запустится автоматически. Очень удобно. И наконец, все описанное в статье, подробно задокументировано в README.md файле шаблона проекта, чтобы после создания последующая сборка и деплой не вызывали сложностей.

Исползьовать описаный шаблон чтобы получить готовый репозиторий, можно просто нажав кнопочку "Use this template"на GitHub:
github.com/demidko/Projekt

Или, если вам нужен варинт с портабельным jvm:
github.com/demidko/Projekt-portable

Интересно услышать предложения, комментарии и критику.

Подробнее..

Перевод Дебажим PHP-контейнер с помощью Xdebug и PhpStorm

24.06.2020 16:09:31 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Backend-разработчик на PHP.




Инструкция Docker #9: xdebug

Я создам очень простую php-страницу и подебажу ее с помощью xdebug и PhpStorm.

Исходные файлы можно найти здесь:
github.com/ikknd/docker-study в папке recipe-09

1. Создайте файл Dockerfile в папке docker:

FROM php:7.2-fpm#Install xdebugRUN pecl install xdebug-2.6.1 && docker-php-ext-enable xdebugCMD ["php-fpm"]

Выполните эту команду из папки docker для создания образа:

docker build -t php-xdebug-custom -f Dockerfile .

2. Создайте файл docker-compose.yml в папке docker:

version: "3.7"services:  web:    image: nginx:1.17    ports:      - 80:80    volumes:      - /var/www/docker-study.loc/recipe-09/php:/var/www/myapp      - /var/www/docker-study.loc/recipe-09/docker/site.conf:/etc/nginx/conf.d/site.conf    depends_on:      - php  php:    image: php-xdebug-custom    volumes:      - /var/www/docker-study.loc/recipe-09/php:/var/www/myapp      - /var/www/docker-study.loc/recipe-09/docker/php.ini:/usr/local/etc/php/php.ini

Здесь я использую образ php-xdebug-custom вместо php:7.2-fpm

3. Внесите в файл php.ini следующие настройки:

[xdebug]zend_extension=xdebug.soxdebug.profiler_enable=1xdebug.remote_enable=1xdebug.remote_handler=dbgpxdebug.remote_mode=reqxdebug.remote_host=host.docker.internalxdebug.remote_port=9000xdebug.remote_autostart=1xdebug.remote_connect_back=1xdebug.idekey=PHPSTORM

4. Настройте сервер в PhpStorm:

File -> Settings -> Languages и Frameworks -> PHP -> Servers
Добавьте новый сервер с помощью иконки + и настройте его, как показано на следующем скриншоте:



Убедитесь, что вы отметили Использовать сопоставление путей (Use path mappings) и сопоставили папку php с /var/www/myapp.

5. Настройте удаленный дебагер PHP в PhpStorm:

Run -> Edit configurations -> PHP Remote Debug

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



6. Выберите конфигурацию дебага на панели дебага PhpStorm



7. Перейдите в /var/www/docker-study.loc/recipe-09/docker/ и выполните:

docker-compose up -d

Если я сейчас введу myapp.loc/ в браузере, я увижу результаты из файла index.php.

Я могу установить точку останова, начать прослушивание соединений в панели дебага PhpStorm и перезагрузить страницу.

Удачного дебага!



Узнать о курсе подробнее.



Еще по теме


Подробнее..

Настройка Xdebug3 для Laravel-приложения в Docker

30.01.2021 18:12:04 | Автор: admin

Начнём пожалуй, со структуры, в которой всё будет:

docker/ docker-compose.yml .env .env.example .gitignore services     database      dump      .gitignore     nginx      site.conf     php         Dockerfile         php.ini

А теперь обо всём по порядку:

Файл .gitingore содержит только одну строчку /.env

Файл .env.example в начале проекта такой же, как и .env

В файле docker-compose.yml содержится информация про все наши сервисы:

version: "3.7"services:  php:    build:      args:        uname: ${PHP_UNAME}        uid: ${PHP_UID}        gid: ${PHP_GID}      context: ./services/php    container_name: ${PROJECT_NAME}_php    image: ${PROJECT_NAME}_php    restart: unless-stopped    working_dir: /var/www/    volumes:      - ./services/php/php.ini:/usr/local/etc/php/php.ini      - ../:/var/www    environment:      COMPOSER_MEMORY_LIMIT: 2G      XDEBUG_CONFIG: client_host=${XDEBUG_REMOTE_HOST} client_port=${XDEBUG_STORM_PORT} remote_enable=1      PHP_IDE_CONFIG: serverName=${XDEBUG_STORM_SERVER_NAME}    networks:      - main_network    depends_on:      - db  db:    image: mysql:5.6    restart: unless-stopped    container_name: ${PROJECT_NAME}_db    command: --default-authentication-plugin=mysql_native_password    environment:      MYSQL_DATABASE: ${DB_DATABASE}      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}      MYSQL_PASSWORD: ${DB_PASSWORD}      MYSQL_USER: ${DB_USERNAME}    ports:      - ${DB_LOCAL_PORT}:3306    volumes:      - ./services/database/dump:/var/lib/mysql    networks:      - main_network  nginx:    image: nginx:1.17-alpine    restart: unless-stopped    container_name: ${PROJECT_NAME}_nginx    ports:      - ${NGINX_LOCAL_PORT}:80    volumes:      - ../:/var/www      - ./services/nginx:/etc/nginx/conf.d    networks:      - main_network    depends_on:      - phpnetworks:  main_network:    driver: bridge    name: ${PROJECT_NAME}_main_network    ipam:      driver: default      config:        - subnet: ${SUBNET_IP}/${SUBNET_MASK}

Из важного здесь стоит отметить ./services/php/php.ini:/usr/local/etc/php/php.ini - наш локальный файл php.ini (там некоторые конфиги дебаггера) будет намаплен на тот что внутри контейнера. XDEBUG_CONFIG - будет задана переменная окружения внутри контейнера php, которую потом будет испльзовать xdebug вместо значений по-умолчанию. Здесь мы задаем client_host - хост, к которому xdebug будет пытаться подключиться при инициации отладочного соединения. Этот адрес должен быть адресом машины, на которой ваш PhpStorm прослушивает входящие отладочные соединения. Получается так, что наша локальная машина находится в одной подсети с запущеными контейнерами, а её адресс будет первым в этой подсети. Таким образом, мы всегда можем знать каким будет адресс нашей машины, и позже зададим это значение в переменную XDEBUG_REMOTE_HOST . В CLIENT_PORT нужно будет задать порт, установленный на прослушивание в IDE (9003). XDEBUG_STORM_SERVER_NAME - имя сервера, который мы создадим в IDE позже. Этот параметр нужен, чтобы сообщить PhpStorm, как сопоставлять пути при подключении с докера (ведь у вас же открыты локальные файлы в редакторе, а код работает на удалённых; хотя при испльзовании volumes это не совсем так).

Вот, как выглядит файл окружения .env :

PROJECT_NAME=my_projectDB_DATABASE=my_project_dbDB_USERNAME=my_projectDB_PASSWORD=p@$$w0rdDB_ROOT_PASSWORD=toorPHP_UNAME=devPHP_UID=1000PHP_GID=1000DB_LOCAL_PORT=3377NGINX_LOCAL_PORT=8077XDEBUG_STORM_SERVER_NAME=DockerXDEBUG_REMOTE_HOST=192.168.227.1XDEBUG_STORM_PORT=9003SUBNET_IP=192.168.227.0SUBNET_MASK=28

На счёт подсети для проекта, то здесь мы задали 192.168.227.0 с маской 28, то-есть для всех устройств остаётся 32 - 28 = 4 бита, что равносильно 2 ** 4 - 1 = 15 контейнеров. Не 16 потому что в подсеть входит также наша локальная машина, которая, кстати, будет иметь адресс 192.168.227.1. Именно это значение мы задали в переменную XDEBUG_REMOTE_HOST.

Настройки веб-сервера site.conf:

server {    listen 80;    server_name 127.0.0.1 localhost;    client_max_body_size 5m;    error_log  /var/log/nginx/error.log;    access_log /var/log/nginx/access.log;    root /var/www/public;    index index.php;    location / {        try_files $uri $uri/ /index.php?$query_string;        gzip_static on;    }    location ~ \.php$ {        try_files $uri =404;        fastcgi_split_path_info ^(.+\.php)(/.+)$;        fastcgi_pass php:9000;        fastcgi_index index.php;        include fastcgi_params;        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;        fastcgi_param PATH_INFO $fastcgi_path_info;    }}

В нашем Dockerfile (тот что для php) нужно не забыть установить и включить xdebug. Для этого добавляем 2 строчки:

RUN pecl install xdebugRUN docker-php-ext-enable xdebug

Стоит отметить, что будет испльзована последняя (то-есть 3) версия (есть различия в конфигурации по сравнению с 2).

Полный Dockerfile:

FROM php:7.4-fpm# Arguments defined in docker-compose.ymlARG unameARG gidARG uid# Install system dependenciesRUN apt-get update \    && apt-get install -y \        git \        curl \        dpkg-dev \        libpng-dev \        libjpeg-dev \        libonig-dev \        libxml2-dev \        libpq-dev \        libzip-dev \        zip \        unzip \        cronRUN pecl install xdebugRUN docker-php-ext-enable xdebugRUN docker-php-ext-configure gd \  --enable-gd \  --with-jpegADD ./php.ini /usr/local/etc/php/php.ini# Clear cacheRUN apt-get clean && rm -rf /var/lib/apt/lists/*# Install PHP extensionsRUN docker-php-ext-install pdo pdo_mysql pdo_pgsql pgsql mbstring exif pcntl bcmath gd sockets zip# Get latest ComposerCOPY --from=composer:latest /usr/bin/composer /usr/bin/composer# Create system user to run Composer and Artisan CommandsRUN groupadd --gid $gid $unameRUN useradd -G www-data,root -s /bin/bash --uid $uid --gid $gid $unameRUN mkdir -p /home/$uname/.composer && \    chown -R $uname:$uname /home/$uname# Set working directoryWORKDIR /var/wwwUSER $uname# Expose port 9000 and start php-fpm serverEXPOSE 9000CMD ["php-fpm"]

Также, зададим некоторые параметры исплнения в php.ini :

max_execution_time=1000max_input_time=1000xdebug.mode=debugxdebug.log="/var/www/xdebug.log"xdebug.remote_enable=1

Запустим наше приложение:

docker-compose up -d

И дальше пойдём в PhpStorm для настройки:

Создадим сервер с названием Docker и cделаем маппинг локального корня проекта (/var/www/quizzy.loc) на путь, по которому он лежит в докере (/var/www):

Дальше, нам нужно будет настроить использование интерпретатора php из докера:

Настраиваем php interpreterНастраиваем php interpreterВыбираем сервис, в котором находится php и указываем путь к интерпретаторуВыбираем сервис, в котором находится php и указываем путь к интерпретаторуЗадаём параметры запуска (через exec)Задаём параметры запуска (через exec)

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

Настраиваем дебаггерНастраиваем дебаггер

Проверим, работает ли Xdebug:

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

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

Перезагружаем страницу, и в PhpStrom должен поймать подключение:

Подробнее..

Рефакторинг пет проекта докеризация, метрики, тесты

17.02.2021 20:19:33 | Автор: admin

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

Предыстория

Пару лет назад я решил тряхнуть стариной и поиграть в LineAge II на одном из популярных пиратских серверов. В этой игре есть один игровой процесс, в котором требуется "поговорить" с ящиками после смерти 4 боссов. Ящик стоит после смерти 2 минуты. Сами боссы после смерти появляются спустя 24 +/- 6ч, то есть шанс появится есть как через 18ч, так и через 30ч. У меня на тот момент была фуллтайм работа, да и в целом не было времени ждать эти ящики. Но нескольким моим персонажам требовалось пройти этот квест, поэтому я решил "автоматизировать" этот процесс. На сайте сервера есть RSS фид в формет XML, где публикуются события с серверов, включая события смерти босса.

Задумка была следующей:

  • получить данные с RSS

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

  • если есть разница данных - сообщить об этом в телеграм канал

  • отдельно сообщать если босса не убили за первые 9ч сообщением "осталось 3ч", и "осталось 1,5ч". Допустим вечером пришло сообщение, что осталось 3ч, значит смерть босса будет до того, как я пойду спать.

Код на php был написан быстро и в итоге у меня было 3 php файла. Один был с god object классом, а другие два запускали программу в двух режимах - парсер новых, или проверка есть ли боссы на максимальном "респе". Запускал я их крон командами. Это работало и решало мою проблему.

Другие игроки замечали, что я появляюсь в игре сразу после смерти боссов, и через 10 дней у меня на канале было около 50 подписчиков. Так же попросили сделать такое же для второго сервера этого пиратского сервиса. Задачу я тоже решил копипастой. В итоге у меня уже 4 файла с почти одинаковым кодом, и файл с god object. Потом меня попросили сделать то же самое для третьего сервера этого пиратского сервиса. И это отлично работало полтора года.

В итоге у меня спустя полтора года:

  • у меня 6 файлов, дублируют себя почти полностью (по 2 файла на сервер)

  • один god object на несколько сотен строк

  • MySQL и Redis на сервере, где разместил код

  • cron задачи, которые запускают файлы

  • ~1400 подписчиков на канале в телеграм

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

Ожидаемый результат после рефакторинга

  1. Отрефакторить код так, чтобы легче было вносить изменения. Важный момент - отрефакторить без изменения бизнес логики, по сути раскидать god object по файлам, сам код не править, иначе это затянет сроки. Следовать PSR-12.

  2. Докеризировать воркера для удобства переноса на другой сервер и прозрачность запуска и остановки

  3. Запускать воркера через supervisor

  4. Внедрить процесс тестирования кода, настроить Codeception

  5. Докеризировать MySQL и Redis

  6. Настроить Github Actions для запуска тестов и проверки на code style

  7. Поднять Prometheus, Grafana для метрик и мониторинга работоспособности

  8. Сделать докер контейнер, который будет отдавать метрики на страницу /metrics для Prometheus

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

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

Шаг 1. Рефакторинг приложения

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

<?phpdeclare(strict_types=1);namespace AsteriosBot\Core\Support;use AsteriosBot\Core\Exception\DeserializeException;use AsteriosBot\Core\Exception\SerializeException;class Singleton{    protected static $instances = [];    /**     * Singleton constructor.     */    protected function __construct()    {        // do nothing    }    /**     * Disable clone object.     */    protected function __clone()    {        // do nothing    }    /**     * Disable serialize object.     *     * @throws SerializeException     */    public function __sleep()    {        throw new SerializeException("Cannot serialize singleton");    }    /**     * Disable deserialize object.     *     * @throws DeserializeException     */    public function __wakeup()    {        throw new DeserializeException("Cannot deserialize singleton");    }    /**     * @return static     */    public static function getInstance(): Singleton    {        $subclass = static::class;        if (!isset(self::$instances[$subclass])) {            self::$instances[$subclass] = new static();        }        return self::$instances[$subclass];    }}

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

Вот так, например, выглядел класс подключения к базе данных

<?phpdeclare(strict_types=1);namespace AsteriosBot\Core\Connection;use AsteriosBot\Core\App;use AsteriosBot\Core\Support\Config;use AsteriosBot\Core\Support\Singleton;use FaaPz\PDO\Database as DB;class Database extends Singleton{    /**     * @var DB     */    protected DB $connection;    /**     * @var Config     */    protected Config $config;    /**     * Database constructor.     */    protected function __construct()    {        $this->config = App::getInstance()->getConfig();        $dto = $this->config->getDatabaseDTO();        $this->connection = new DB($dto->getDsn(), $dto->getUser(), $dto->getPassword());    }    /**     * @return DB     */    public function getConnection(): DB    {        return $this->connection;    }}

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

Шаг 2: Докеризация воркеров

Запуск всех контейнеров я сделал через docker-compose.yml

Конфиг сервиса для воркеров выглядит так:

  worker:    build:      context: .      dockerfile: docker/worker/Dockerfile    container_name: 'asterios-bot-worker'    restart: always    volumes:      - .:/app/    networks:      - tier

А сам docker/worker/Dockerfile выглядит так:

FROM php:7.4.3-alpine3.11# Copy the application codeCOPY . /appRUN apk update && apk add --no-cache \    build-base shadow vim curl supervisor \    php7 \    php7-fpm \    php7-common \    php7-pdo \    php7-pdo_mysql \    php7-mysqli \    php7-mcrypt \    php7-mbstring \    php7-xml \    php7-simplexml \    php7-openssl \    php7-json \    php7-phar \    php7-zip \    php7-gd \    php7-dom \    php7-session \    php7-zlib \    php7-redis \    php7-session# Add and Enable PHP-PDO ExtenstionsRUN docker-php-ext-install pdo pdo_mysqlRUN docker-php-ext-enable pdo_mysql# RedisRUN apk add --no-cache pcre-dev $PHPIZE_DEPS \        && pecl install redis \        && docker-php-ext-enable redis.so# Install PHP ComposerRUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer# Remove CacheRUN rm -rf /var/cache/apk/*# setup supervisorADD docker/supervisor/asterios.conf /etc/supervisor/conf.d/asterios.confADD docker/supervisor/supervisord.conf /etc/supervisord.confVOLUME ["/app"]WORKDIR /appRUN composer installCMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

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

Шаг 3: Настройка supervisor

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

Код файла worker.php

<?phprequire __DIR__ . '/vendor/autoload.php';use AsteriosBot\Channel\Checker;use AsteriosBot\Channel\Parser;use AsteriosBot\Core\App;use AsteriosBot\Core\Connection\Log;$app = App::getInstance();$checker = new Checker();$parser = new Parser();$servers = $app->getConfig()->getEnableServers();$logger = Log::getInstance()->getLogger();$expectedTime = time() + 60; // +1 min in seconds$oneSecond = time();while (true) {    $now = time();    if ($now >= $oneSecond) {        $oneSecond = $now + 1;        try {            foreach ($servers as $server) {                $parser->execute($server);                $checker->execute($server);            }        } catch (\Throwable $e) {            $logger->error($e->getMessage(), $e->getTrace());        }    }    if ($expectedTime < $now) {        die(0);    }}

У RSS есть защита от спама, поэтому пришлось сделать проверку на секунды и посылать не более 1го запроса в секунду. Таким образом мой воркер каждую секунду выполняет 2 действия, сначала проверяет rss, а затем калькулирует время боссов для сообщений о старте или окончании времени респауна боссов. После 1 минуты работы воркер умирает, и его перезапускает supervisor

Сам конфиг supervisor выглядит так:

[program:worker]command = php /app/worker.phpstderr_logfile=/app/logs/supervisor/worker.lognumprocs = 1user = rootstartsecs = 3startretries = 10exitcodes = 0,2stopsignal = SIGINTreloadsignal = SIGHUPstopwaitsecs = 10autostart = trueautorestart = truestdout_logfile = /dev/stdoutstdout_logfile_maxbytes = 0redirect_stderr = true

После старта контейнеров супервизор стартует воркера автоматически. Важный момент - в файле основного конфига /etc/supervisord.confобязательно нужно указать демонизация процесса, а так же подключение своих конфигов

[supervisord]nodaemon=true[include]files = /etc/supervisor/conf.d/*.conf

Набор полезных команд supervisorctl:

supervisorctl status       # статус воркеровsupervisorctl stop all     # остановить все воркераsupervisorctl start all    # запустить все воркераsupervisorctl start worker # запустить один воркера с конфига, блок [program:worker]

Шаг 4: Настройка Codeception

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

# Codeception Test Suite Configuration## Suite for unit or integration tests.actor: UnitTestermodules:    enabled:        - Asserts        - \Helper\Unit        - Db:              dsn: 'mysql:host=mysql;port=3306;dbname=test_db;'              user: 'root'              password: 'password'              dump: 'tests/_data/dump.sql'              populate: true              cleanup: true              reconnect: true              waitlock: 10              initial_queries:                - 'CREATE DATABASE IF NOT EXISTS test_db;'                - 'USE test_db;'                - 'SET NAMES utf8;'    step_decorators: ~

Шаг 5: Докеризация MySQL и Redis

На сервере, где работало это приложение, у меня было еще пара других ботов. Все они использовали один сервер MySQL и один Redis для кеша. Я решил вынести все, что связано с окружением в отельный docker-compose.yml, а самих ботов залинковать через внешний docker network

Выглядит это так:

version: '3'services:  mysql:    image: mysql:5.7.22    container_name: 'telegram-bots-mysql'    restart: always    ports:      - "3306:3306"    environment:      MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"      MYSQL_ROOT_HOST: '%'    volumes:      - ./docker/sql/dump.sql:/docker-entrypoint-initdb.d/dump.sql    networks:      - tier  redis:    container_name: 'telegram-bots-redis'    image: redis:3.2    restart: always    ports:      - "127.0.0.1:6379:6379/tcp"    networks:      - tier  pma:    image: phpmyadmin/phpmyadmin    container_name: 'telegram-bots-pma'    environment:      PMA_HOST: mysql      PMA_PORT: 3306      MYSQL_ROOT_PASSWORD: "${DB_PASSWORD}"    ports:      - '8006:80'    networks:      - tiernetworks:  tier:    external:      name: telegram-bots-network

DB_PASSWORD я храню в .env файле, а ./docker/sql/dump.sql у меня лежит бекап для инициализации базы данных. Так же я добавил external network так же, как в этом конфиге - в каждом docker-compose.yml каждого бота на сервере. Таким образом они все находятся в одной сети и могут использовать общие базу данных и редис.

Шаг 6: Настройка Github Actions

В шаге 4 этого туториала я добавил тестовый фреймфорк Codeception, который для тестирования требует базу данных. В самом проекте нет базы, в шаге 5 я ее вынес отдельно и залинковал через external docker network. Для запуска тестов в Github Actions я решил полностью собрать все необходимое на лету так же через docker-compose.

name: Actionson:  pull_request:    branches: [master]  push:    branches: [master]jobs:  build:    runs-on: ubuntu-latest    steps:      - name: Checkout        uses: actions/checkout@v2      - name: Get Composer Cache Directory        id: composer-cache        run: |          echo "::set-output name=dir::$(composer config cache-files-dir)"      - uses: actions/cache@v1        with:          path: ${{ steps.composer-cache.outputs.dir }}          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}          restore-keys: |            ${{ runner.os }}-composer-      - name: Composer validate        run: composer validate      - name: Composer Install        run: composer install --dev --no-interaction --no-ansi --prefer-dist --no-suggest --ignore-platform-reqs      - name: PHPCS check        run: php vendor/bin/phpcs --standard=psr12 app/ -n      - name: Create env file        run: |          cp .env.github.actions .env      - name: Build the docker-compose stack        run: docker-compose -f docker-compose.github.actions.yml -p asterios-tests up -d      - name: Sleep        uses: jakejarvis/wait-action@master        with:          time: '30s'      - name: Run test suite        run: docker-compose -f docker-compose.github.actions.yml -p asterios-tests exec -T php vendor/bin/codecept run unit

Инструкция onуправляет когда билд триггернётся. В моем случае - при создании пулл реквеста или при коммите в мастер.

Инструкция uses: actions/checkout@v2 запускает проверку доступа процесса к репозиторию.

Далее идет проверка кеша композера, и установка пакетов, если в кеше не найдено

Затем в строке run: php vendor/bin/phpcs --standard=psr12 app/ -nя запускаю проверку кода соответствию стандарту PSR-12 в папке ./app

Так как тут у меня специфическое окружение, я подготовил файл .env.github.actionsкоторый копируется в .env Cодержимое .env.github.actions

SERVICE_ROLE=testTG_API=XXXXXTG_ADMIN_ID=123TG_NAME=AsteriosRBbotDB_HOST=mysqlDB_NAME=rootDB_PORT=3306DB_CHARSET=utf8DB_USERNAME=rootDB_PASSWORD=passwordLOG_PATH=./logs/DB_NAME_TEST=test_dbREDIS_HOST=redisREDIS_PORT=6379REDIS_DB=0SILENT_MODE=trueFILLER_MODE=true

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

Затем я собираю проект при помощи docker-compose.github.actions.ymlв котором прописано все необходимое для тестирвания, контейнер с проектом и база данных. Содержимое docker-compose.github.actions.yml:

version: '3'services:  php:    build:      context: .      dockerfile: docker/php/Dockerfile    container_name: 'asterios-tests-php'    volumes:      - .:/app/    networks:      - asterios-tests-network  mysql:    image: mysql:5.7.22    container_name: 'asterios-tests-mysql'    restart: always    ports:      - "3306:3306"    environment:      MYSQL_DATABASE: asterios      MYSQL_ROOT_PASSWORD: password    volumes:      - ./tests/_data/dump.sql:/docker-entrypoint-initdb.d/dump.sql    networks:      - asterios-tests-network##  redis:#    container_name: 'asterios-tests-redis'#    image: redis:3.2#    ports:#      - "127.0.0.1:6379:6379/tcp"#    networks:#      - asterios-tests-networknetworks:  asterios-tests-network:    driver: bridge

Я закомментировал контейнер с Redis, но оставил возможность использовать его в будущем. Сборка с кастомным docker-compose файлом, а затем тесты - запускается так

docker-compose -f docker-compose.github.actions.yml -p asterios-tests up -ddocker-compose -f docker-compose.github.actions.yml -p asterios-tests exec -T php vendor/bin/codecept run unit

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

Шаг 7: Настройка Prometheus и Grafana

В шаге 5 я вынес MySQL и Redis в отдельный docker-compose.yml. Так как Prometheus и Grafana тоже общие для всех моих телеграм ботов, я их добавил туда же. Сам конфиг этих контейнеров выглядит так:

  prometheus:    image: prom/prometheus:v2.0.0    command:      - '--config.file=/etc/prometheus/prometheus.yml'    restart: always    ports:      - 9090:9090    volumes:      - ./prometheus.yml:/etc/prometheus/prometheus.yml    networks:      - tier  grafana:    container_name: 'telegram-bots-grafana'    image: grafana/grafana:7.1.1    ports:      - 3000:3000    environment:      - GF_RENDERING_SERVER_URL=http://renderer:8081/render      - GF_RENDERING_CALLBACK_URL=http://grafana:3000/      - GF_LOG_FILTERS=rendering:debug    volumes:      - ./grafana.ini:/etc/grafana/grafana.ini      - grafanadata:/var/lib/grafana    networks:      - tier    restart: always  renderer:    image: grafana/grafana-image-renderer:latest    container_name: 'telegram-bots-grafana-renderer'    restart: always    ports:      - 8081    networks:      - tier

Они так же залинкованы одной сетью, которая потом линкуется с external docker network.

Prometheus: я прокидываю свой конфиг prometheus.yml, где я могу указать источники для парсинга метрик

Grafana: я создаю volume, где будут храниться конфиги и установленные плагины. Так же я прокидываю ссылку на сервис рендеринга графиков, который мне понадобиться для отправки alert. С этим плагином alert приходит со скриншотом графика.

Поднимаю проект и устанавливаю плагин, затем перезапускаю Grafana контейнер

docker-compose up -ddocker-compose exec grafana grafana-cli plugins install grafana-image-rendererdocker-compose stop  grafana docker-compose up -d grafana

Шаг 8: Публикация метрик приложения

Для сбора и публикации метрик я использовал endclothing/prometheus_client_php

Так выглядит мой класс для метрик

<?phpdeclare(strict_types=1);namespace AsteriosBot\Core\Connection;use AsteriosBot\Core\App;use AsteriosBot\Core\Support\Singleton;use Prometheus\CollectorRegistry;use Prometheus\Exception\MetricsRegistrationException;use Prometheus\Storage\Redis;class Metrics extends Singleton{    private const METRIC_HEALTH_CHECK_PREFIX = 'healthcheck_';    /**     * @var CollectorRegistry     */    private $registry;    protected function __construct()    {        $dto = App::getInstance()->getConfig()->getRedisDTO();        Redis::setDefaultOptions(            [                'host' => $dto->getHost(),                'port' => $dto->getPort(),                'database' => $dto->getDatabase(),                'password' => null,                'timeout' => 0.1, // in seconds                'read_timeout' => '10', // in seconds                'persistent_connections' => false            ]        );        $this->registry = CollectorRegistry::getDefault();    }    /**     * @return CollectorRegistry     */    public function getRegistry(): CollectorRegistry    {        return $this->registry;    }    /**     * @param string $metricName     *     * @throws MetricsRegistrationException     */    public function increaseMetric(string $metricName): void    {        $counter = $this->registry->getOrRegisterCounter('asterios_bot', $metricName, 'it increases');        $counter->incBy(1, []);    }    /**     * @param string $serverName     *     * @throws MetricsRegistrationException     */    public function increaseHealthCheck(string $serverName): void    {        $prefix = App::getInstance()->getConfig()->isTestServer() ? 'test_' : '';        $this->increaseMetric($prefix . self::METRIC_HEALTH_CHECK_PREFIX . $serverName);    }}

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

        if ($counter) {            $this->metrics->increaseHealthCheck($serverName);        }

Где переменная $counter это количество записей в RSS. Там будет 0, если получить данные не удалось, и значит метрика не будет сохранена. Это потом понадобится для alert по работе сервиса.

Затем нужно метрики опубликовать на странице /metric чтобы Prometheus их спарсил. Добавим хост в конфиг prometheus.yml из шага 7.

# my global configglobal:  scrape_interval:     5s # Set the scrape interval to every 15 seconds. Default is every 1 minute.  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.  # scrape_timeout is set to the global default (10s).scrape_configs:  - job_name: 'bots-env'    static_configs:      - targets:          - prometheus:9090          - pushgateway:9091          - grafana:3000          - metrics:80 # тут будут мои метрики по uri /metrics

Код, который вытащит метрики из Redis и создаст страницу в текстовом формате. Эту страничку будет парсить Prometheus

$metrics = Metrics::getInstance();$renderer = new RenderTextFormat();$result = $renderer->render($metrics->getRegistry()->getMetricFamilySamples());header('Content-type: ' . RenderTextFormat::MIME_TYPE);echo $result;

Теперь настроим сам дашборд и alert. В настройках Grafana сначала укажите свой Prometheus как основной источник данных, а так же я добавил основной канал нотификации Телеграм (там добавляете токен своего бота и свой chat_id с этим ботом)

Настройка GrafanaНастройка Grafana
  1. Метрика increase(asterios_bot_healthcheck_x3[1m]) Показывает на сколько метрика asterios_bot_healthcheck_x3 увеличилась за 1 минуту

  2. Название метрики (будет под графиком)

  3. Название для легенды в пункте 4.

  4. Легенда справа из пункта 3.

  1. Правило, по которому проверяется метрика. В моем случае проверяет что за последние 30 секунд проблем не было

  2. Правило, по которому будет срабатывать alert. В моем случае "Когда сумма из метрики А между сейчас и 10 секунд назад"

  3. Если нет данных вообще - слать alert

  4. Сообщение в alert

Выглядит alert в телеграм так (помните мы настраивали рендеринг картинок для alert?)

Alert в ТелеграмAlert в Телеграм
  1. Обратите внимание, alert заметил падение, но все восстановилось. Grafana приготовилась слать alert, но передумала. Это то самое правило 30 секунд

  2. Тут уже все упало больше чем на 30 секунд и alert был отправлен

  3. Сообщение, которое мы указали в настройках alert

  4. Ссылка на dashboard

  5. Источник метрики

Шаг 9: Телеграм бот

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

Итоги

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

Ссылки на проекты

Подробнее..

Перевод Вы неправильно используете docker-compose

09.04.2021 08:10:47 | Автор: admin

Вот краткое изложение некоторых кардинальных "грехов", которые я совершил при использовании docker-compose.

Вы познакомились с docker-compose либо по собственному выбору, либо по необходимости. Вы используете его некоторое время, но считаете его неуклюжим. Я здесь, чтобы сказать вам: "Вы, вероятно, неправильно его используете".

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

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

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

Грех 1: Вы используете сеть хоста

Одна из первых вещей, которые новички находят обременительными - это использование сетей Docker. Это еще один уровень знаний, который нужно добавить в свой багаж после того, как вы привыкнете к основам docker build и docker run ... и, честно говоря, зачем вам вообще разбираться в этих сетях Docker? Все нормально работает через сеть хоста, правда? Неправильно!

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

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

Эта сеть сразу дает вам ряд преимуществ:

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

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

  • Сервисы смогут общаться, используя их имена в качестве имен хостов. Итак, если у вас есть служба с именем db и в ней есть служба, прослушивающая порт 5432, вы можете получить к ней доступ из любой другой службы через db: 5432. Обычно это более понятно, чем localhost: 5432. А поскольку нет риска конфликта портов localhost, у нас больше шансов избежать ошибок при использовании портов в разных проектах.

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

Грех 2: Вы привязываете порты к 0.0.0.0 хоста

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

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

Но я использую ufw, мои порты по умолчанию недоступны.

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

Исправить очень просто: просто добавьте 127.0.0.1: впереди. Так, например, 127.0.0.1:8080:8080. Это просто указывает докеру, чтобы он открывал порт только для петлевого сетевого интерфейса и ничего больше.

Грех 3: Вы используете helthy для координации запуска служб

Я хотел бы в кое-чем признаться. Я на 100% виноват в этом.

Основная причина, по которой эта проблема такая важная, заключается в том, что Docker или Docker Compose не поддерживают ее. Версия 2.1 формата docker-compose имела параметр depends_on для которого можно было установить значение service_healthy. Кроме того, каждая служба может иметь команду проверки работоспособности, которая может сообщать docker-compose - healthy. Что ж этого больше нет в версии 3.0 и никакой замены для него не предлагается.

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

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

В таких случаях вам нужно что-то, что ждет готовности сервисов. Docker рекомендует использовать wait-for-it, Dockerize или wait-for. Однако обратите внимание, что готовность порта не всегда является признаком того, что служба готова к использованию. Например, в интеграционном тесте с использованием определенной базы данных SQL с определенной схемой порт становится доступным при инициализации базы данных, однако тест может работать только после применения определенной миграции схемы. Сверху могут потребоваться проверки для конкретного приложения.

Проблема 4: Вы запускаете БД в режиме docker-compose, а тест - на хосте

Вот ситуация: вы хотите запустить несколько модульных тестов, но эти тесты зависят от некоторых внешних служб. Может быть база данных, может быть Redis, может быть другой API. Легко: давайте поместим эти зависимости в docker-compose и подключим к ним unit test.

Это здорово, но учтите, что ваши тесты больше не являются просто модульными. Теперь это интеграционные тесты. Следует принять во внимание еще одно важное различие: вам нужно будет учесть настройку тестовой среды и teardown. Обычно лучше всего выполнять setup/teardown вне тестового кода - основная причина в том, что может быть несколько разных пакетов в зависимости от этих внешних служб

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

Контейнерные тесты означают:

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

  • Интеграционный тест не зависит от какой-либо другой конфигурации локальной системы или настройки среды, например ваших учетных данных JFrog или каких-либо зависимостей сборки. Контейнер изолирован.

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

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

Совет по использованию контейнерных интеграционных тестов - использовать для них отдельное определение docker-compose. Например, если большинство ваших сервисов существует в docker-compose.yml, вы можете добавить docker-compose.test.yml с настройками интеграционных тестов. Это означает, что docker-compose up вызывает ваши обычные службы, а docker-compose -f docker-compose.yml -f docker-compose.test.yml up запускает ваши интеграционные тесты. Полный пример того, как этого добиться, можно найти в этом отличном репозитории для интеграционного тестирования docker-compose от Ardan Labs.

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

Заключение

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

Анонсы других публикаций и возможность комментирования (если тут не можете) втелеге.

Подробнее..

Из песочницы Webstorm NodeJs интерпретатор из Docker контейнера

17.06.2020 12:21:27 | Автор: admin

В этой статье описан процесс развертывания NodeJs приложения, используя NodeJs интерпретатор, находящийся в Docker контейнере, в качестве проектного (по умолчанию). Решение скорее всего будет актуально только для пользователей ОС Linux системы, так как в итоге интерпретатор будет взят из официального образа node на DockerHub.


Зачем?


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


Решение


Для примера было создано приложение на ReactJs + Typescipt, так как при некорректной настройке интерпретатора как минимум сломается подсветка кода.


1. Создадим приложение из готового шаблона:


$ npx create-react-app reactjs-typescript-custom-nodejs --template typescript

2. Напишем Dockerfile:


FROM node:14WORKDIR /appCMD cp /usr/local/bin/node /app/docker/node \    && npm install \    && npm run start

Здесь можно подробнее описать команду CMD, интерпретатор копируется из контейнера Docker в папку с приложением /app/docker/node, сама папка /app является корневой директорией приложения в контейнере. Далее выполняется установка зависимостей и старт приложения в режиме разработки.


3. Создадим в проекте директорию node с путем docker/node и поместим туда файл .gitignore, который игнорирует все в только что созданной папке:


*

4. Далее выполним команду в терминале из корня проекта:


docker build -t client . && docker run -it --rm --volume=${PWD}:/app -p 3000:3000 --name client client

Здесь мы собираем образ из Dockerfile и задаем ему тег, после чего запускаем контейнер в интерактивном режиме -it с последующим удалением контейнера --rm, создаем привязку директории проекта на хост-машине к корневой директории приложения в контейнере--volume=${PWD}:/app, открываем порт в сети -p 3000:3000 и задаем имя контейнеру --name client.


Таким образом при запуске контейнера экземпляр интерпретатора копируется на хост машину по пути docker/node/node, осталось указать его путь в настройках idea:



Итог


Приложение разворачивается в dev режиме, корректная подсветка кода, hot-reload и прочие функции, работающие из коробки в react-scripts, при смене версии официального образа NodeJs в Dockerfile дополнительная настройка idea не потребовалась.


Исходный код готового решения доступен на github.

Подробнее..
Категории: Node.js , Linux , Devops , Nodejs , Frontend , Docker , Docker-compose

NestJS. Загрузка файлов в S3 хранилище (minio)

13.08.2020 02:13:49 | Автор: admin
NestJS фреймворк для создания эффективных, масштабируемых серверных приложений на платформе Node.js. Вы можете встретить утверждение, что NestJS является платформо-независимым фреймворком. Имеется в виду, что он может работать на базе одного из двух фрейморков по Вашему выбору: NestJS+Express или NestJS+Fastify. Это действительно так, или почти так. Эта платформо-независимость заканчивается, на обработке запросов Content-Type: multipart/form-data. То есть практически на второй день разработки. И это не является большой проблемой, если Вы используете платформу NestJS+Express в документации есть пример работы для Content-Type: multipart/form-data. Для NestJS+Fastify такого примера нет, и примеров в сети не так уж и много. И некоторые из этих примеров идут по весьма усложненному пути.

Выбирая между платформой NestJS+Fastify и NestJS+Express я сделал выбор в сторону NestJS+Fastify. Зная склонность разработчиков в любой непонятной ситуации вешать на объект req в Express дополнительные свойства и так общаться между разными частями приложения, я твердо решил что Express в следующем проекте не будет.

Только нужно было решить технический вопрос с Content-Type: multipart/form-data. Также полученные через запросы Content-Type: multipart/form-data файлы я планировал сохранять в хранилище S3. В этом плане реализация запросов Content-Type: multipart/form-data на платформе NestJS+Express меня смущала тем, что не работала с потоками.

Запуск локального хранилища S3



S3 это хранилище данных (можно сказать, хотя не совсем строго, хранилище файлов), доступное по протоколу http. Изначально S3 предоставлялся AWS. В настоящее время API S3 поддерживается и другими облачными сервисами. Но не только. Появились реализации серверов S3, которые Вы можете поднять локально, чтобы использовать их во время разработки, и, возможно, поднять свои серверы S3 для работы на проде.

Для начала нужно определиться с мотивацией использования S3 хранилища данных. В некоторых случаях это позволяет снизить затраты. Например, для хранения резервных копий можно взять самые медленные и дешевые хранилища S3. Быстрые хранилища с высоким трафиком (трафик тарифицируется отдельно) на загрузку данных из хранилища, возможно, будут стоить сравнимо с SSD дисками того же объема.

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

Для поднятия реализации серверов S3 minio локально нужен только установленный на компьютере docker и docker-compose. Соответсвующий файл docker-compose.yml:

version: '3'services:  minio1:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data1-1:/data1      - ./s3/data1-2:/data2    ports:      - '9001:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio2:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data2-1:/data1      - ./s3/data2-2:/data2    ports:      - '9002:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio3:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data3-1:/data1      - ./s3/data3-2:/data2    ports:      - '9003:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3  minio4:    image: minio/minio:RELEASE.2020-08-08T04-50-06Z    volumes:      - ./s3/data4-1:/data1      - ./s3/data4-2:/data2    ports:      - '9004:9000'    environment:      MINIO_ACCESS_KEY: minio      MINIO_SECRET_KEY: minio123    command: server http://minio{1...4}/data{1...2}    healthcheck:      test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']      interval: 30s      timeout: 20s      retries: 3


Запускаем и без проблем получаем кластер из 4 серверов S3.

NestJS + Fastify + S3



Работу с сервером NestJS опишу с самых первых шагов, хотя часть этого материала отлично описана в документации. Устанавливается CLI NestJS:

npm install -g @nestjs/cli


Создается новый проект NestJS:

nest new s3-nestjs-tut


Инсталлируются необходимые пакеты (включая те что нужны для работы с S3):

npm install --save @nestjs/platform-fastify fastify-multipart aws-sdk sharpnpm install --save-dev @types/fastify-multipart  @types/aws-sdk @types/sharp


По умолчанию в проекте устанавливается платформа NestJS+Express. Как установить Fastify описано в документации docs.nestjs.com/techniques/performance. Дополнительно нам нужно установить плагин для обработки Content-Type: multipart/form-data fastify-multipart

import { NestFactory } from '@nestjs/core';import {  FastifyAdapter,  NestFastifyApplication,} from '@nestjs/platform-fastify';import fastifyMultipart from 'fastify-multipart';import { AppModule } from './app.module';async function bootstrap() {  const fastifyAdapter = new FastifyAdapter();  fastifyAdapter.register(fastifyMultipart, {    limits: {      fieldNameSize: 1024, // Max field name size in bytes      fieldSize: 128 * 1024 * 1024 * 1024, // Max field value size in bytes      fields: 10, // Max number of non-file fields      fileSize: 128 * 1024 * 1024 * 1024, // For multipart forms, the max file size      files: 2, // Max number of file fields      headerPairs: 2000, // Max number of header key=>value pairs    },  });  const app = await NestFactory.create<NestFastifyApplication>(    AppModule,    fastifyAdapter,  );  await app.listen(3000, '127.0.0.1');}bootstrap();


Теперь опишем сервис, загружающий файлы в хранилище S3, сократив код по обработке некоторых видов ошибок (полный текст есть в репозитарии статьи):

import { Injectable, HttpException, BadRequestException } from '@nestjs/common';import { S3 } from 'aws-sdk';import fastify = require('fastify');import { AppResponseDto } from './dto/app.response.dto';import * as sharp from 'sharp';@Injectable()export class AppService {  async uploadFile(req: fastify.FastifyRequest): Promise<any> {    const promises = [];    return new Promise((resolve, reject) => {      const mp = req.multipart(handler, onEnd);      function onEnd(err) {        if (err) {          reject(new HttpException(err, 500));        } else {          Promise.all(promises).then(            data => {              resolve({ result: 'OK' });            },            err => {              reject(new HttpException(err, 500));            },          );        }      }      function handler(field, file, filename, encoding, mimetype: string) {        if (mimetype && mimetype.match(/^image\/(.*)/)) {          const imageType = mimetype.match(/^image\/(.*)/)[1];          const s3Stream = new S3({            accessKeyId: 'minio',            secretAccessKey: 'minio123',            endpoint: 'http://127.0.0.1:9001',            s3ForcePathStyle: true, // needed with minio?            signatureVersion: 'v4',          });          const promise = s3Stream            .upload(              {                Bucket: 'test',                Key: `200x200_${filename}`,                Body: file.pipe(                  sharp()                    .resize(200, 200)                    [imageType](),                ),              }            )            .promise();          promises.push(promise);        }        const s3Stream = new S3({          accessKeyId: 'minio',          secretAccessKey: 'minio123',          endpoint: 'http://127.0.0.1:9001',          s3ForcePathStyle: true, // needed with minio?          signatureVersion: 'v4',        });        const promise = s3Stream          .upload({ Bucket: 'test', Key: filename, Body: file })          .promise();        promises.push(promise);      }    });  }}


Из особенностей следует отметить, что мы пишем входной поток в два выходных потока, если загружается картинка. Один из потоков сжимает картинку до размеров 200х200. Во всех случаях используется стиль работы с потоками (stream). Но для того, чтобы отловить возможные ошибки и вернуть их в контроллер, мы вызываем метод promise(), который определен в библиотеке aws-sdk. Полученные промисы накапливаем в массиве promises:

        const promise = s3Stream          .upload({ Bucket: 'test', Key: filename, Body: file })          .promise();        promises.push(promise);


И, далее, ожидаем их разрешение в методе Promise.all(promises).

Код контроллера, в котором таки пришлось пробросить FastifyRequest в сервис:

import { Controller, Post, Req } from '@nestjs/common';import { AppService } from './app.service';import { FastifyRequest } from 'fastify';@Controller()export class AppController {  constructor(private readonly appService: AppService) {}  @Post('/upload')  async uploadFile(@Req() req: FastifyRequest): Promise<any> {    const result = await this.appService.uploadFile(req);    return result;  }}


Запускается проект:

npm run start:dev


Репозитарий статьи github.com/apapacy/s3-nestjs-tut

apapacy@gmail.com
13 августа 2020 года
Подробнее..
Категории: Node.js , S3 , Nodejs , Docker , Docker-compose , Nestjs , Express , Expressjs , Fastify , Aws-s3 , Minio

Парсинг логов при помощи Fluent-bit

25.03.2021 18:04:40 | Автор: admin

Не так давно передо мной встала задача организации логгирования сервисов, разворачиваемых с помощью docker контейнеров. В интернете нашел примеры простого логгирования контейнеров, однако хотелось большего. Изучив возможности Fluent-bit я собрал рабочий пайплайн трансформации логов. Что в сочетании с Elasticsearch и Kibana, позволило быстро искать и анализировать лог-сообщения.

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

Кому интересно, добро пожаловать под кат)

Необходимы базовые знания bash, docker-compose, Elasticsearch и Kibana.

Обзор используемого стека

Тестовое приложение будем запускать с помощьюdocker-compose.

Для организации логгирования воспользуемся следующими технологиями:

  • fluent-bit- осуществляет сбор, обработку и пересылку в хранилище лог-сообщений.

  • elasticsearch- централизованно хранит лог-сообщения, обеспечивает их быстрый поиск и фильтрацию.

  • kibana- предоставляет интерфейс пользователю, для визуализации данных хранимых в elasticsearch

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

Подготовка тестового приложения

Для примера организуем логгирование веб-сервера Nginx.

Подготовка Nginx

  1. Создадим директорию с проектом и добавим в нее docker-compose.yml, в котором будем задавать конфигурацию запуска контейнеров приложения.

  2. Определим формат логов Nginx. Для этого создадим директорию nginx c файлом nginx.conf. В нем переопределим стандартный формат логов:

    user  nginx;worker_processes  1;error_log  /var/log/nginx/error.log warn;pid        /var/run/nginx.pid;events {    worker_connections  1024;}http {    include       /etc/nginx/mime.types;    default_type  application/octet-stream;log_format  main  'access_log $remote_addr "$request" '                  '$status "$http_user_agent"';access_log  /var/log/nginx/access.log  main;sendfile        on;keepalive_timeout  65;include /etc/nginx/conf.d/*.conf;}
    
  3. Добавим сервисwebв docker-compose.yml:

    version: "3.8"services:  web:    container_name: nginx    image: nginx    ports:      - 80:80    volumes:      # добавляем конфигурацию в контейнер      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    

Подготовка fluent-bit

Для начала организуем самый простой вариант логгирования. Создадим директорию fluent-bit c конфигурационным файлом fluent-bit.conf. Про формат и схему конфигурационного файла можно прочитатьздесь.

  1. Fluent-bit предоставляет большое количество плагинов для сбора лог-сообщений из различных источников. Полный список можно найтиздесь. В нашем примере мы будем использовать плагинforward.

    Плагин выводаstdoutпозволяет перенаправить лог-сообщения в стандартный вывод (standard output).

    [INPUT]    Name              forward[OUTPUT]    Name stdout    Match *
    
  2. Добавим в docker-compose.yml сервисfluent-bit:

    version: "3.8"services:  web:    ...  fluent-bit:    container_name: fluent-bit    image: fluent/fluent-bit    ports:      # необходимо открыть порты, которые используются плагином forward      - 24224:24224      - 24224:24224/udp    volumes:      # добавляем конфигурацию в контейнер      - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
    
  3. Добавим настройки логгирования для сервисаweb:

    version: "3.8"services:  web:    ...    depends_on:      - fluent-bit    logging:      # используемый драйвер логгирования      driver: "fluentd"      options:        # куда посылать лог-сообщения, необходимо что бы адрес         # совпадал с настройками плагина forward        fluentd-address: localhost:24224        # теги используются для маршрутизации лог-сообщений, тема         # маршрутизации будет рассмотрена ниже        tag: nginx.logs  fluent-bit:    ...
    
  4. Запустим тестовое приложение:

    docker-compose up
    

    Сгенерируем лог-сообщение, откроем еще одну вкладку терминала и выполним команду:

    curl localhost
    

    Получим лог-сообщение в следующем формате:

    [    1616473204.000000000,    {"source"=>"stdout",    "log"=>"172.29.0.1 "GET / HTTP/1.1" 200 "curl/7.64.1"",    "container_id"=>"efb81a754706b1ece6948072934df85ea44466305b326cd45",    "container_name"=>"/nginx"}]
    

    Сообщение состоит из:

    • временной метки, добавляемой fluent-bit;

    • лог-сообщения;

    • мета данных, добавляемых драйвером fluentd.

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

 docker-compose.yml fluent-bit    fluent-bit.conf nginx     nginx.conf

Кратко о маршрутизации лог-сообщиний в fluent-bit

Маршрутизация в fluent-bit позволяет направлять лог-сообщения через различные фильтры, для их преобразования, и в конечном итоге в один или несколько выходных интерфейсов. Для организации маршрутизации используется две основные концепции:

  • тег (tag) - человеко читаемый индикатор, позволяющий однозначно определить источник лог-сообщения;

  • правило сопоставления (match) - правило, определяющее куда лог-сообщение должно быть перенаправлено.

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

  1. Входной интерфейс присваивает лог-сообщению заданные тег.

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

Подробнее можно прочитать вофициальной документации.

Очистка лог-сообщений от мета данных.

Мета данные для нас не представляют интерес, и только загромождают лог сообщение. Давайте удалим их. Для этого воспользуемся фильтромrecord_modifier. Зададим его настройки в файле fluent-bit.conf:

[FILTER]    Name record_modifier    # для всех лог-сообщений    Match *    # оставить только поле log    Whitelist_key log

Теперь лог-сообщение имеет вид:

[    1616474511.000000000,    {"log"=>"172.29.0.1 "GET / HTTP/1.1" 200 "curl/7.64.1""}]

Отделение логов запросов от логов ошибок

На текущий момент логи посылаемые Nginx можно разделить на две категории:

  • логи с предупреждениями, ошибками;

  • логи запросов.

Давайте разделим логи на две группы и будем структурировать только логи запросов. Все логи-сообщения от Nginx помечаются тегом nginx.logs. Поменяем тег для лог-сообщений запросов на nginx.access. Для их идентификации мы заблаговременно добавили в начало сообщения префикс access_log.

Добавим новый фильтрrewrite_tag. Ниже приведена его конфигурация.

[FILTER]    Name rewrite_tag    # для сообщений с тегом nginx.logs    Match nginx.logs    # применить правило: для лог-сообщений поле log которых содержит строку    # access_log, поменять тег на nginx.access, исходное лог-сообщение отбросить.    Rule $log access_log nginx.access false

Теперь все лог-сообщения запросов будут помечены тегом nginx.access, что в будущем позволит нам выполнять фильтрацию логов описанным выше категориям.

Парсинг лог-сообщения

Давайте структурируем наше лог-сообщение. Для придания структуры лог-сообщению его необходимо распарсить. Это делается с помощью фильтраparser.

  1. Лог-сообщение представляет собой строку. Воспользуемся парсеромregex, который позволяет с помощью регулярных выражений определить пары ключ-значение для информации содержащейся в лог-сообщении. Зададим настройки парсера. Для этого в директории fluent-bit создадим файл parsers.conf и добавим в него следующее:

    [PARSER]    Name   nginx_parser    Format regex    Regex  ^access_log (?<remote_address>[^ ]*) "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<status>[^ ]*) "(?<http_user_agent>[^\"]*)"$    Types  status:integer
    
  2. Обновим конфигурационный файл fluent-bit.conf. Подключим к нему файл с конфигурацией парсера и добавим фильтр parser.

    [SERVICE]    Parsers_File /fluent-bit/parsers/parsers.conf[FILTER]    Name parser    # для сообщений с тегом nginx.access    Match nginx.access    # парсить поле log    Key_Name log    # при помощи nginx_parser    Parser nginx_parser
    
  3. Теперь необходимо добавить файл parsers.conf в контейнер, сделаем это путем добавления еще одного volume к сервису fluent-bit:

    version: "3.8"services:  web:    ...  fluent-bit:    ...    volumes:      - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
    
  4. Перезапустим приложение, сгенерируем лог-сообщение запроса. Теперь оно имеет следующую структуру:

    [  1616493566.000000000,  {    "remote_address"=>"172.29.0.1",    "method"=>"GET",    "path"=>"/",    "status"=>200,    "http_user_agent"=>"curl/7.64.1"  }]
    

Сохранение лог-сообщений в elasticsearch

Теперь организуем отправку лог-сообщений на хранения в elasticsearch.

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

    [OUTPUT]    Name  es    Match nginx.logs    Host  elasticsearch    Port  9200    Logstash_Format On    # Использовать префикс nginx-logs для логов ошибок    Logstash_Prefix nginx-logs[OUTPUT]    Name  es    Match nginx.access    Host  elasticsearch    Port  9200    Logstash_Format On    # Использовать префикс nginx-access для логов запросов    Logstash_Prefix nginx-access
    
  2. Добавим в docker-compose.yml сервисы elasticsearch и kibana.

    version: "3.8"services:  web:    ...  fluent-bit:    ...    depends_on:      - elasticsearch  elasticsearch:    container_name: elasticsearch    image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2    environment:      - "discovery.type=single-node"  kibana:    container_name: kibana    image: docker.elastic.co/kibana/kibana:7.10.1    depends_on:      - "elasticsearch"    ports:      - "5601:5601"
    

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

 docker-compose.yml fluent-bit    fluent-bit.conf    parsers.conf nginx     nginx.conf

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

Результаты

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

  • показать только лог-сообщения запросов;

  • показать лог-сообщения запросов с http статусом 404;

  • отображать не все поля лог-сообщения.

Пример фильтрации логов. Выполнена фильтрация по значению поля "status", так же выбраны только необходимые в данный момент поля.Пример фильтрации логов. Выполнена фильтрация по значению поля "status", так же выбраны только необходимые в данный момент поля.

Всем спасибо! Надеюсь туториал был полезен.

Подробнее..

Мониторинг Tarantool логи, метрики и их обработка

28.12.2020 18:17:25 | Автор: admin

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


Мониторинг Tarantool


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


Настройка логов в Tarantool


Базовое конфигурирование и использование логов


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


Каждое сообщение лога имеет свой уровень детализации. Уровень логирования Tarantool характеризуется значением параметра log_level (целое число от 1 до 7):


  1. SYSERROR.
  2. ERROR сообщения log.error(...).
  3. CRITICAL.
  4. WARNING сообщения log.warn(...).
  5. INFO сообщения log.info(...).
  6. VERBOSE сообщения log.verbose(...).
  7. DEBUG сообщения log.debug(...).

Значение параметра log_level N соответствует логу, в который попадают сообщения уровня детализации N и всех предыдущих уровней детализации < N. По умолчанию log_level имеет значение 5 (INFO). Чтобы настроить этот параметр при использовании Cartridge, можно воспользоваться cartridge.cfg:


cartridge.cfg( { ... }, { log_level = 6, ... } )

Для отдельных процессов настройка производится при помощи вызова box.cfg:


box.cfg{ log_level = 6 }

Менять значение параметра можно непосредственно во время работы программы.


Стандартная стратегия логирования: писать об ошибках в log.error() или log.warn() в зависимости от их критичности, отмечать в log.info() основные этапы работы приложения, а в log.verbose() писать более подробные сообщения о предпринимаемых действиях для отладки. Не стоит использовать log.debug() для отладки приложения, этот уровень диагностики в первую очередь предназначен для отладки самого Tarantool. Не рекомендуется также использовать уровень детализации ниже 5 (INFO), поскольку в случае возникновения ошибок отсутствие информационных сообщений затруднит диагностику. Таким образом, в режиме отладки приложения рекомендуется работать при log_level 6 (VERBOSE), в режиме штатной работы при log_level 5 (INFO).


local log = require('log')log.info('Hello world')log.verbose('Hello from app %s ver %d', app_name, app_ver) -- https://www.lua.org/pil/20.htmllog.verbose(app_metainfo) -- type(app_metainfo) == 'table'

В качестве аргументов функции отправки сообщения в лог (log.error/log.warn/log.info/log.verbose/log.debug) можно передать обычную строку, строку с плейсхолдерами и аргументы для их заполнения (аналогично string.format()) или таблицу (она будет неявно преобразована в строку методом json.encode()). Функции лога также работают с нестроковыми данными (например числами), приводя их к строке c помощью tostring().


Tarantool поддерживает два формата логов: plain и json:


2020-12-15 11:56:14.923 [11479] main/101/interactive C> Tarantool 1.10.8-0-g2f18757b72020-12-15 11:56:14.923 [11479] main/101/interactive C> log level 52020-12-15 11:56:14.924 [11479] main/101/interactive I> mapping 268435456 bytes for memtx tuple arena...

{"time": "2020-12-15T11:56:14.923+0300", "level": "CRIT", "message": "Tarantool 1.10.8-0-g2f18757b7", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/main.cc", "line": 514}{"time": "2020-12-15T11:56:14.923+0300", "level": "CRIT", "message": "log level 5", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/main.cc", "line": 515}{"time": "2020-12-15T11:56:14.924+0300", "level": "INFO", "message": "mapping 268435456 bytes for memtx tuple arena...", "pid": 5675 , "cord_name": "main", "fiber_id": 101, "fiber_name": "interactive", "file": "\/tarantool\/src\/box\/tuple.c", "line": 261}

Настройка формата происходит через параметр log_format так же, как для параметра log_level. Подробнее о форматах можно прочитать в соответствующем разделе документации.


Tarantool позволяет выводить логи в поток stderr, в файл, в конвейер или в системный журнал syslog. Настройка производится с помощью параметра log. О том, как конфигурировать вывод, можно прочитать в документации.


Обёртка логов


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


local log = require('log')local context = require('app.context')local function init()    if rawget(_G, "_log_is_patched") then        return    end    rawset(_G, "_log_is_patched", true)    local wrapper = function(level)        local old_func = log[level]        return function(fmt, ...)            local req_id = context.id_from_context()            if select('#', ...) ~= 0 then                local stat                stat, fmt = pcall(string.format, fmt, ...)                if not stat then                    error(fmt, 3)                end            end            local wrapped_message            if type(fmt) == 'string' then                wrapped_message = {                    message = fmt,                    request_id = req_id                }            elseif type(fmt) == 'table' then                wrapped_message = table.copy(fmt)                wrapped_message.request_id = req_id            else                wrapped_message = {                    message = tostring(fmt),                    request_id = req_id                }            end            return old_func(wrapped_message)        end    end    package.loaded['log'].error = wrapper('error')    package.loaded['log'].warn = wrapper('warn')    package.loaded['log'].info = wrapper('info')    package.loaded['log'].verbose = wrapper('verbose')    package.loaded['log'].debug = wrapper('debug')    return trueend

Данный код обогащает информацию, переданную в лог в любом поддерживаемом формате, идентификатором запроса request_id.


Настройка метрик в Tarantool


Подключение метрик


Для работы с метриками в приложениях Tarantool существует пакет metrics. Это модуль для создания коллекторов метрик и взаимодействия с ними в разнообразных сценариях, включая экспорт метрик в различные базы данных (InfluxDB, Prometheus, Graphite). Материал основан на функционале версии 0.6.0.


Чтобы установить metrics в текущую директорию, воспользуйтесь стандартной командой:


tarantoolctl rocks install metrics 0.6.0

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


dependencies = {    ...,    'metrics == 0.6.0-1',}

Для приложений, использующих фреймворк Cartridge, пакет metrics предоставляет специальную роль cartridge.roles.metrics. Включение этой роли на всех процессах кластера упрощает работу с метриками и позволяет использовать конфигурацию Cartridge для настройки пакета.


Встроенные метрики


Сбор встроенных метрик уже включён в состав роли cartridge.roles.metrics.


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


local metrics = require('metrics')metrics.enable_default_metrics()

Достаточно выполнить её единожды на старте приложения, например поместив в файл init.lua.


В список метрик по умолчанию входят:


  • информация о потребляемой Lua-кодом RAM;
  • информация о текущем состоянии файберов;
  • информация о количестве сетевых подключений и объёме сетевого трафика, принятого и отправленного процессом;
  • информация об использовании RAM на хранение данных и индексов (в том числе метрики slab-аллокатора);
  • информация об объёме операций на спейсах;
  • характеристики репликации спейсов Tarantool;
  • информация о текущем времени работы процесса и другие метрики.

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


Для пользователей Cartridge также существует специальный набор встроенных метрик для мониторинга состояния кластера. На данный момент он включает в себя метрику о количестве проблем в кластере, разбитых по уровням критичности (аналогична Issues в WebUI Cartridge).


Плагины для экспорта метрик


Пакет metrics поддерживает три формата экспорта метрик: prometheus, graphite и json. Последний можно использовать, например, в связке Telegraf + InfluxDB.


Чтобы настроить экспорт метрик в формате json или prometheus для процессов с ролью cartridge.roles.metrics, добавьте соответствующую секцию в конфигурацию кластера:


metrics:  export:    - path: '/metrics/json'      format: json    - path: '/metrics/prometheus'      format: prometheus

Экспорт метрик в формате json или prometheus без использования кластерной конфигурации настраивается средствами модуля http так же, как любой другой маршрут.


local json_metrics = require('metrics.plugins.json')local prometheus = require('metrics.plugins.prometheus')local httpd = require('http.server').new(...)httpd:route(    { path = '/metrics/json' },    function(req)        return req:render({            text = json_metrics.export()        })    end)httpd:route( { path = '/metrics/prometheus' }, prometheus.collect_http)

Для настройки graphite необходимо добавить в код приложения следующую секцию:


local graphite = require('metrics.plugins.graphite')graphite.init{    host = '127.0.0.1',    port = 2003,    send_interval = 60,}

Параметры host и port соответствуют конфигурации вашего сервера Graphite, send_interval периодичность отправки данных в секундах.


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


Добавление пользовательских метрик


Ядро пакета metrics составляют коллекторы метрик, созданные на основе примитивов Prometheus:


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

Cоздать экземпляр коллектора можно следующей командой:


local gauge = metrics.gauge('balloons')

В дальнейшем получить доступ к объекту в любой части кода можно этой же командой.


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


local gauge = metrics.gauge('balloons')gauge:set(1, { color = 'blue' })gauge:set(2, { color = 'red' })

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


gauge:inc(11, { color = 'blue' }) -- increase 1 by 11

Лейблы концепт, который также был вдохновлён Prometheus используются для различения измеряемых характеристик в рамках одной метрики. Кроме этого, они могут быть использованы для агрегирования значений на стороне базы данных. Рассмотрим рекомендации по их использованию на примере.


В программе есть модуль server, который принимает запросы и способен сам их отправлять. Вместо того, чтобы использовать две различных метрики server_requests_sent и server_requests_received для хранения данных о количестве отправленных и полученных запросов, следует использовать общую метрику server_requests с лейблом type, который может принимать значения sent и received.


Подробнее о коллекторах и их методах можно прочитать в документации пакета.


Заполнение значений пользовательских метрик


Пакет metrics содержит полезный инструмент для заполнения коллекторов метрик коллбэки. Рассмотрим принцип его работы на простом примере.


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


local metrics = require('metrics')local buffer = require('app.buffer')metrics.register_callback(function()    local gauge = metrics.gauge('buffer_count')    gauge.set(buffer.count())end)

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


Мониторинг HTTP-трафика


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


Чтобы добавить HTTP-метрики для конкретного маршрута при использовании пакета http 1.x.x, вам необходимо обернуть функцию-обработчик запроса в функцию http_middleware.v1:


local metrics = require('metrics')local http_middleware = metrics.http_middlewarehttp_middleware.build_default_collector('summary', 'http_latency')local route = { path = '/path', method = 'POST' }local handler = function() ... endhttpd:route(route, http_middleware.v1(handler))

Для хранения метрик можно использовать коллекторы histogram и summary.


Чтобы добавить HTTP-метрики для маршрутов роутера при использовании пакета http 2.x.x, необходимо воспользоваться следующим подходом:


local metrics = require('metrics')local http_middleware = metrics.http_middlewarehttp_middleware.build_default_collector('histogram', 'http_latency')router:use(http_middleware.v2(), { name = 'latency_instrumentation' })

Рекомендуется использовать один и тот же коллектор для хранения всей информации об обработке HTTP-запросов (например, выставив в начале коллектор по умолчанию функцией build_default_collector или set_default_collector). Прочитать больше о возможностях http_middleware можно в документации.


Глобальные лейблы


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


Для того чтобы прибавить к каждой метрике какой-то общий для процесса или группы процессов Tarantool (например, имя машины или название приложения) лейбл, необходимо воспользоваться механизмом глобальных лейблов:


local metrics = require('metrics')local global_labels = {}-- постоянное значениеglobal_labels.app = 'MyTarantoolApp'-- переменные конфигурации кластера (http://personeltest.ru/aways/www.tarantool.io/ru/doc/latest/book/cartridge/cartridge_api/modules/cartridge.argparse/)local argparse = require('cartridge.argparse')local params, err = argparse.parse()assert(params, err)global_labels.alias = params.alias-- переменные окружения процессаlocal host = os.getenv('HOST')assert(host)global_labels.host = hostmetrics.set_global_labels(global_labels)

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


Роль cartridge.roles.metrics по умолчанию выставляет alias процесса Tarantool в качестве глобального лейбла.


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


Мониторинг внешних параметров


Tarantool не только позволяет организовывать сбор внутренних метрик о работе приложения, но и способен выступать как агент мониторинга внешних параметров. Подмодуль psutils пакета metrics является примером реализации такого поведения.


С помощью psutils можно настроить сбор метрик об использовании CPU процессами Tarantool. Его информация основывается на данных /proc/stat и /proc/self/task. Подключить сбор метрик можно с помощью следующего кода:


local metrics = require('metrics')metrics.register_callback(function()    local cpu_metrics = require('metrics.psutils.cpu')    cpu_metrics.update()end)

Возможность писать код на Lua делает Tarantool гибким инструментом, позволяющим обходить различные препятствия. Например, psutils возник из необходимости следить за использованием CPU вопреки отказу администраторов со стороны заказчика "подружить" в правах файлы /proc/* процессов Tarantool и плагин inputs.procstat Telegraf, который использовался на местных машинах в качестве основного агента.


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


Визуализация метрик


Пример из tarantool/grafana-dashboard


Хранение метрик в Prometheus


Настройка пути для экспорта метрик Tarantool в формате Prometheus описана в пункте "Плагины для экспорта метрик". Ответ запроса по такому маршруту выглядит следующим образом:


...# HELP tnt_stats_op_total Total amount of operations# TYPE tnt_stats_op_total gaugetnt_stats_op_total{alias="tnt_router",operation="replace"} 1tnt_stats_op_total{alias="tnt_router",operation="select"} 57tnt_stats_op_total{alias="tnt_router",operation="update"} 43tnt_stats_op_total{alias="tnt_router",operation="insert"} 40tnt_stats_op_total{alias="tnt_router",operation="call"} 4...

Чтобы настроить сбор метрик в Prometheus, необходимо добавить элемент в массив scrape_configs. Этот элемент должен содержать поле static_configs с перечисленными в targets URI всех интересующих процессов Tarantool и поле metrics_path, в котором указан путь для экспорта метрик Tarantool в формате Prometheus.


scrape_configs:  - job_name: "tarantool_app"    static_configs:      - targets:         - "tarantool_app:8081"        - "tarantool_app:8082"        - "tarantool_app:8083"        - "tarantool_app:8084"        - "tarantool_app:8085"    metrics_path: "/metrics/prometheus"

В дальнейшем найти метрики в Grafana вы сможете, указав в качестве job соответствующий job_name из конфигурации.


Пример готового docker-кластера Tarantool App + Prometheus + Grafana можно найти в репозитории tarantool/grafana-dashboard.


Хранение метрик в InfluxDB


Чтобы организовать хранение метрик Tarantool в InfluxDB, необходимо воспользоваться стеком Telegraf + InfluxDB и настроить на процессах Tarantool экспорт метрик в формате json (см. пункт "Плагины для экспорта метрик"). Ответ формируется следующим образом:


{    ...    {        "label_pairs": {            "operation": "select",            "alias": "tnt_router"        },        "timestamp": 1606202705935266,        "metric_name": "tnt_stats_op_total",        "value": 57    },    {        "label_pairs": {            "operation": "update",            "alias": "tnt_router"        },        "timestamp": 1606202705935266,        "metric_name": "tnt_stats_op_total",        "value": 43    },    ...}

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


[[inputs.http]]    urls = [        "http://tarantool_app:8081/metrics/json",        "http://tarantool_app:8082/metrics/json",        "http://tarantool_app:8083/metrics/json",        "http://tarantool_app:8084/metrics/json",        "http://tarantool_app:8085/metrics/json"    ]    timeout = "30s"    tag_keys = [        "metric_name",        "label_pairs_alias",        "label_pairs_quantile",        "label_pairs_path",        "label_pairs_method",        "label_pairs_status",        "label_pairs_operation"    ]    insecure_skip_verify = true    interval = "10s"    data_format = "json"    name_prefix = "tarantool_app_"    fieldpass = ["value"]

Список urls должен содержать URL всех интересующих процессов Tarantool, настроенные для экспорта метрик в формате json. Обратите внимание, что лейблы метрик попадают в Telegraf и, соответственно, InfluxDB как теги, название которых состоит из префикса label_pairs_ и названия лейбла. Таким образом, если ваша метрика имеет лейбл с ключом mylbl, то для работы с ним в Telegraf и InfluxDB необходимо указать в пункте tag_keys соответствующего раздела [[inputs.http]] конфигурации Telegraf значение ключа label_pairs_mylbl, и при запросах в InfluxDB ставить условия на значения лейбла, обращаясь к тегу с ключом label_pairs_mylbl.


В дальнейшем найти метрики в Grafana вы сможете, указав measurement в формате <name_prefix>http (например, для указанной выше конфигурации значение measurement tarantool_app_http).


Пример готового docker-кластера Tarantool App + Telegraf + InfluxDB + Grafana можно найти в репозитории tarantool/grafana-dashboard.


Стандартный дашборд Grafana


Для визуализации метрик Tarantool с помощью Grafana на Official & community built dashboards опубликованы стандартные дашборды. Шаблон состоит из панелей для мониторинга HTTP, памяти для хранения данных вместе с индексами и операций над спейсами Tarantool. Версию для использования с Prometheus можно найти здесь, а для InfluxDB здесь. Версия для Prometheus также содержит набор панелей для мониторинга состояния кластера, агрегированной нагрузки и потребления памяти.



Чтобы импортировать шаблон дашборды, достаточно вставить необходимый id или ссылку в меню Import на сервере Grafana. Для завершения процесса импорта необходимо задать переменные, определяющие место хранения метрик Tarantool в соответствующей базе данных.


Генерация дашбордов Grafana с grafonnet


Стандартные дашборды Grafana были созданы с помощью инструмента под названием grafonnet. Что это за заморский зверь и как мы к нему пришли?


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


Одним из первых способов решить большинство возникающих проблем было использование механизма динамических переменных (Variables) в Grafana. Например, он позволяет объединить дашборды с метриками из разных зон в одну с удобным переключателем. К сожалению, мы слишком быстро столкнулись с проблемой: использование механизма оповещений (Alert) не совместимо с запросами, использующими динамические переменные.


Любой дашборд в Grafana по сути представляет собой некоторый json. Более того, платформа позволяет без каких-либо затруднений экспортировать в таком формате существующие дашборды. Работать с ним в ручном режиме несколько затруднительно: размер даже небольшого дашборда составляет несколько тысяч строк. Первым способом решения проблемы был скрипт на Python, который заменял необходимые поля в json, по сути превращая один готовый дашборд в другой. Когда разработка библиотеки скриптов пришла к задаче добавления и удаления конкретных панелей, мы начали осознавать, что пытаемся создать генератор дашбордов. И что эту задачу уже кто-то до нас решал.


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


grafonnet opensource-проект под эгидой Grafana, предназначенный для программной генерации дашбордов. Он основан на языке программирования jsonnet языке для генерации json. Сам grafonnet представляет собой набор шаблонов для примитивов Grafana (панели и запросы разных типов, различные переменные) с методами для объединения их в цельный дашборд.


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


Начав с форка проекта и сборки нашей дашборды на основе этого форка, впоследствии мы оформили несколько Pull Request-ов в grafonnet на основе наших изменений. Например, один из них добавил поддержку запросов в InfluxDB на основе визуального редактора.


Визуальный редактор запросов InfluxDB


Код наших стандартных дашбордов расположен в репозитории tarantool/grafana-dashboard. Здесь же находится готовый docker-кластер, состоящий из стеков Tarantool App + Telegraf + InfluxDB + Grafana, Tarantool App + Prometheus + Grafana. Его можно использовать для локальной отладки сбора и обработки метрик в вашем собственном приложении.


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


На что смотреть?


В первую очередь, стоит следить за состоянием самих процессов Tarantool. Для этого подойдёт, например, стандартный up Prometheus. Можно соорудить простейший healthcheck самостоятельно:


httpd:route(    { path = '/health' },    function(req)        local body = { app = app, alias = alias, status = 'OK' }        local resp = req:render({ json = body })        resp.status = 200        return resp    end)

Рекомендации по мониторингу внешних параметров ничем принципиально не отличаются от ситуации любого другого приложения. Необходимо следить за потреблением памяти на хранение логов и служебных файлов на диске. Заметьте, что файлы с данными .snap и .xlog возникают даже при использовании движка memtx (в зависимости от настроек). При нормальной работе нагрузка на CPU не должна быть чересчур большой. Исключение составляет момент восстановления данных после рестарта процесса: построение индексов может загрузить все доступные потоки процессора на 100 % на несколько минут.


Потребление RAM удобно разделить на два пункта: Lua-память и память, потребляемая на хранение данных и индексов. Память, доступная для выполнения кода на Lua, имеет ограничение в 2 Gb на уровне Luajit. Обычно приближение метрики к этой границе сигнализирует о наличии какого-то серьёзного изъяна в коде. Более того, зачастую такие изъяны приводят к нелинейному росту используемой памяти, поэтому начинать волноваться стоит уже при переходе границы в 512 Mb на процесс. Например, при высокой нагрузке в наших приложениях показатели редко выходили за предел 200-300 Mb Lua-памяти.


При использовании движка memtx потреблением памяти в рамках заданного лимита memtx_memory (он же метрика quota_size) заведует slab-аллокатор. Процесс происходит двухуровнево: аллокатор выделяет в памяти ячейки, которые после занимают сами данные или индексы спейсов. Зарезервированная под занятые или ещё не занятые ячейки память отображена в quota_used, занятая на хранение данных и индексов arena_used (только данных items_used). Приближение к порогу arena_used_ratio или items_used_ratio свидетельствует об окончании свободных зарезервированных ячеек slab, приближение к порогу quota_used_ratio об окончании доступного места для резервирования ячеек. Таким образом, об окончании свободного места для хранения данных свидетельствует приближение к порогу одновременно метрик quota_used_ratio и arena_used_ratio. В качестве порога обычно используют значение 90 %. В редких случаях в логах могут появляться сообщения о невозможности выделить память под ячейки или данные даже тогда, когда quota_used_ratio, arena_used_ratio или items_used_ratio далеки от порогового значения. Это может сигнализировать о дефрагментации данных в RAM, неудачном выборе схем спейсов или неудачной конфигурации slab-аллокатора. В такой ситуации необходима консультация специалиста.


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


Заключение


Как этот материал, так и пакет metrics назвать "всеохватными" и "универсальными" на данный момент нельзя. Открытыми или находящимися на данный момент в разработке являются вопросы метрик репликации, мониторинга движка vinyl, метрики event loop и полная документация по уже существующим методам metrics.


Не стоит забывать о том, что metrics и grafana-dashboard являются opensource-разработками. Если при работе над своим проектом вы наткнулись на ситуацию, которая не покрывается текущими возможностями пакетов, не стесняйтесь внести предложение в Issues или поделиться вашим решением в Pull Requests.


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


Полезные ссылки:


Подробнее..

Что будет после 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)

Подробнее..

Из песочницы Использование HAProxy и Docker на машине разработчика при разработке сайтов

13.09.2020 20:11:44 | Автор: admin

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


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


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


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


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


Конфигурация docker


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


Создаем docker сеть с указанием подсети. Указать подсеть необходимо для направления запросов с HAProxy на определенные ip-адреса чтобы не было проблем с разрешением имен доменов, если какой-то из контейнеров не запущен.


docker network create develop --subnet=172.20.0.0/16

Чтобы запускаемые контейнеры работали в нужной нам сети и получали ip адреса из нее в docker-compose.yml будем напрямую указывать сеть:


networks:  default:    external:      name: develop

а в конфигурации контейнеров, на которые будут перенаправляться запросы с HAProxy, жестко задавать ip-адрес.


    networks:      default:        ipv4_address: 172.20.1.1

Сертификат для работы https


Для работы HAProxy по https необходимо создать самоподписанный сертификат или подключить готовый от удостоверяющего центра.


Создание самоподписанного сертификата


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


  1. Создаем приватный ключ (key)

sudo openssl genrsa -out site.key 2048

  1. Создаем Certificate Signing Request (csr)

sudo openssl req -new -key site.key -out site.csr

  1. Создаем сам самоподписанный сертификат (crt)

sudo openssl x509 -req -days 365 -in site.csr -signkey site.key -out site.crt

  1. Объединяем ключ и сертификат (pem)

sudo bash -c 'cat site.key site.crt >> site.pem'

Путь к полученному файлу сертификата необходимо будет прописать в конфигурации HAProxy.


Подготовительны шаги закончены, дальше объединяем все в конфигурациях HAProxy и Docker.


Ниже приведены примеры файлов конфигурации haproxy.cfg и docker-compose.yml.


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


Конфигурация HAPRoxy


Приведенная конфигурация для HAProxy перенаправляет все запросы с порта 80 на порт 443, а затем, основываясь на имени запрошенного хоста, направляет на один из сайтов которые работают по 80 порту. Для сайтов нет необходимости настраивать работу по https.


Запросы пришедшие на 443 порт будут сразу направлены на соответствующие сайты.


В понятиях HAProxy разделы описанные как frontend представляют внешние интерфейсы, куда приходят запросы.

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

Backend, соответственно, интерфейсы куда запросы перенаправляются.

Раздел defaults задает общие параметры для всех разделов описанных в конфигурации.

По умолчанию в официальных docker образах HAProxy файл конфигурации располагается в /usr/local/etc/haproxy/haproxy.cfg

defaults    mode http    timeout connect 5000ms    timeout client 50000ms    timeout server 50000msfrontend http_frontend    bind *:80    redirect scheme https if !{ ssl_fc }frontend https_frontend    bind *:443 ssl crt /etc/ssl/certs/site.pem    acl is_microbase hdr_end(host) -i microbase.localhost    use_backend microbase if is_microbase    acl is_coordinator hdr_end(host) -i coordinator.localhost    use_backend coordinator if is_coordinatorbackend microbase    server microbase 172.20.1.1:80 checkbackend coordinator    server coordinator 172.20.1.2:80 check

Конфигурации docker-compose.yml


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


Приведенная конфигурация запускает контейнеры для сайтов microbase.localhost и coordinator.localhost на которые будут направлены запросы с HAProxy.


Так же в конфигурации задан контейнер c самим HAProxy который будет обрабатывать запросы.


По умолчанию docker-compose будет использовать файл docker-compose.yml в папке из которой запущен.

Передать имя другого файла можно при помощи параметра -f.

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

version: "3"services:  microbase:    image: "inblank/php7.4-apache"    volumes:      - ./microbase:/var/www    networks:      default:        ipv4_address: 172.20.1.1  coordinator:    image: "inblank/php7.4-apache"    volumes:      - ./coordinator:/var/www    networks:      default:        ipv4_address: 172.20.1.2  haproxy:    image: "haproxy:2.2-alpine"    ports:      - 80:80      - 443:443    volumes:      - ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg      - ./cert.pem:/etc/ssl/certs/site.pemnetworks:  default:    external:      name: develop

Тестирование


Тестирование производилось утилитой siege с настройками по умолчанию и с 25 конкурирующими подключениями. Каждый тест был ограничен по времени 1-ой минутой.


siege coordinator.localhost -t 1m

В качестве теста использовался php файл со следующим кодом:


<?phpecho "Hello World!";

Сайты работали под управлением apache 2.4 и php как модуль.


Запуск производился на ноутбуке с процессором Intel Core i5-8250U 1.60GHz, оперативной памятью 8Гб и SSD диском. В качестве системы использовался Linux Mint 20 Cinnamon


Ниже приведены результаты тестирования.


  • Запросы на 80 порт без переадресации
    Через HAProxy Прямое подключение
    ** SIEGE 4.0.4** Preparing 25 concurrent users for battle.The server is now under siege...Lifting the server siege...Transactions:             258084 hitsAvailability:             100.00 %Elapsed time:             59.39 secsData transferred:         2.95 MBResponse time:            0.01 secsTransaction rate:         4345.58 trans/secThroughput:               0.05 MB/secConcurrency:              24.72Successful transactions:  258084Failed transactions:      0Longest transaction:      0.04Shortest transaction:     0.00
    
    ** SIEGE 4.0.4** Preparing 25 concurrent users for battle.The server is now under siege...Lifting the server siege...Transactions:             314572 hitsAvailability:             100.00 %Elapsed time:             59.18 secsData transferred:         3.60 MBResponse time:            0.00 secsTransaction rate:         5315.51 trans/secThroughput:               0.06 MB/secConcurrency:              24.64Successful transactions:  314572Failed transactions:      0Longest transaction:      0.11Shortest transaction:     0.00
    

Разница в производительности ~18%.

  • Запросы на 80 порт с переадресацией на 443 порт
    Через HAProxy Прямое подключение
    ** SIEGE 4.0.4** Preparing 25 concurrent users for battle.The server is now under siege...Lifting the server siege...Transactions:             114804 hitsAvailability:             100.00 %Elapsed time:             59.44 secsData transferred:         0.66 MBResponse time:            0.01 secsTransaction rate:         1931.43 trans/secThroughput:               0.01 MB/secConcurrency:              24.78Successful transactions:  114824Failed transactions:      0Longest transaction:      1.03Shortest transaction:     0.00
    
    ** SIEGE 4.0.4** Preparing 25 concurrent users for battle.The server is now under siege...Lifting the server siege...Transactions:             134364 hitsAvailability:             100.00 %Elapsed time:             59.80 secsData transferred:         19.99 MBResponse time:            0.01 secsTransaction rate:         2246.89 trans/secThroughput:               0.33 MB/secConcurrency:              24.74Successful transactions:  134374Failed transactions:      0Longest transaction:      0.08Shortest transaction:     0.00
    

Разница в производительности ~14.5%.

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


Ссылки


HAProxy


  1. Официальный сайт HSProxy
  2. Документация по HAProxy
  3. The Four Essential Sections of an HAProxy Configuration
  4. Официальный Docker образ HAProxy

Docker


  1. Официальный сайт Docker
  2. Официальная страница docker-compose
  3. Docker Compose file version 3 reference
Подробнее..

FrontEnd разработка в Docker

01.04.2021 04:04:20 | Автор: admin

Легенда

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

К счастью - эта проблема решена в современном мире разработки, если не полностью, то в большей мере. Нам на выручку пришел Docker.

Данная статья сделана для FrontEnd разработчиков, которые не знакомы с Docker. Мы разберем некоторые вопросы связанные с оптимизацией трафика для запуска и затронем немного вопросы безопасности.

По итогу, мы:

  1. Научимся работать с Docker для локальной разработки

  2. Создадим артефакт для production, который в будущем сможет использовать DevOps.

  3. В конце немного поговорим о безопасности

Установка Docker

Установка докер довольна простая и лучше всего описана в официальной документации.

Также нам для работы понадобится docker-compose, например для MacOS при установке Docker Desktop он также автоматом установится, в linux системах же его придется ставить отдельно.

Справка по CLI Docker

docker и docker-compose поддерживают флаг --help для корня и для команд

docker --helpdocker ps --helpdocker-compose --helpdocker-compose up --help

Особенности проекта в статье

Пусть наш FE проект имеет 2 скрипта в package.json: "dev" для разработки и "build" для создания production кода.

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

GitHub: тут

TL;DR; Сразу код

docker-compose.dev.yml
version: "3.9"services:  web:    image: node:15.8-alpine3.11    ports:      - "3000:3000"    volumes:      - ".:/app"    environment:      NODE_ENV: development      HOST: 0.0.0.0    working_dir: /app    command: sh -c "cd /app; yarn install; yarn run dev --host 0.0.0.0"
docker-compose.yml
version: "3.9"services:  web:    build:      context: .      dockerfile: Dockerfile    ports:      - "80:80"    environment:      NODE_ENV: production
Dockerfile
FROM node:15.8-alpine3.11 AS buildWORKDIR /appCOPY package.json package.jsonRUN yarn installCOPY . .RUN yarn buildFROM nginx:1.19-alpineCOPY --from=build /app/dist /opt/siteCOPY nginx.conf /etc/nginx/nginx.conf
nginx.conf
worker_processes auto;events {    worker_connections 8000;    multi_accept on;}http {  include       /etc/nginx/mime.types;  default_type  application/octet-stream;  server {      listen   80;      listen   [::]:80 default ipv6only=on;      root /opt/site;      location / {          try_files $uri $uri/ /index.html;      }  }}

Давайте разбираться, что произошло:

Этап разработки:

docker-compose.dev.yml - Этого фала достаточно для разработки, остальные файлы используются для создания production артефакта

1 шаг.Объявляем сервис web. Выбираем образ который будет делать сборку: node:15.8-alpine3.11Лучше всего детализировать используемые версии, стоит их держать точно такими же, как и у сборщика production build.

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

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

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

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

6 шаг.Устанавливаем зависимости и стартуем разработку (webpack-dev-server с явным заданием хоста): command: sh -c "yarn install; yarn run dev --host 0.0.0.0

Запускаем разработку с помощью команды: docker-compose -f docker-compose.dev.yml up

Этап production:

Хочу особо отметить, что приведенная конфигурация работает для отдачи простой статики. Если используется SSR, нужно будет внести изменения: поднимать сервер на node и т.д.

Что отличается в конфигурации docker-compose.ymlот develop версии?

  1. Указана сборка из Dockerfile, вместо использования образа image

  2. Поменялась переменная окружения NODE_ENV: development-> production

  3. Нет секции command потому что статика будет раздаваться с помощью nginx

Конфигурация nginx максимально простая и не обременяет процесс раздачи файлов и fallback на /index.html в случае, если пытаются получить какой-то файл, которого нет.

Самое интересное кроется в Dockerfile: multi-stage building, который используется для уменьшения результирующего артефакта.

Dockerfile

Первая стадия это сборка

1 строка. Для этого указываем тот же исходный образ, который использовали для develop FROM node:15.8-alpine3.11 AS build

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

2 строка. Указываем рабочую директорию /app

3-5 строки. Здесь останавливаемся подробнее:

Вопрос: почему сначала копируем только package.json и производим установку?
Ответ (не заставляет себя долго ждать): при первом запуске разница не будет заметна, но уже со следующей попытки сборки разница будет очевидна.

Если не было изменений в package.json, то слои, по которым собирается docker не изменятся, и данные шаги будут просто взяты из кэша. Это многократно ускорит процесс и снизит сетевую нагрузку в разы. Нам как раз это и надо.

6 строка. Копируем оставшиеся файлы и запускаем build.

Вторая стадия формирование артефакта

По своей сути артефакт в нашем случае это контейнер nginx со статикой.

8 строка. Указываем образ nginx который возьмем за основу.

9 строка. Копируем файлы из первой стадии в папку из которой будем раздавать статику.

10 строка. Копируем в артефакт файл конфигурации nginx

Запускать артефакт можно например так: docker-compose up

Немного о проблеме безопасности для deveopment

Не стоит использовать volumes с директориями на компьютере, которые не хотим отдать в доступ всем пользователям docker.

Как это проявляется? Давайте рассмотрим на примере:

Создаем обычную папку ~/project. Добавляем в нее файл secret.txt, который содержит текст: secret text

Под пользователем создаем docker-compose.ymlи подключаем директорию, к которой не имеет доступ другой пользователь.

docker-compose.yml
version: "3.9"services:  web:    image: alpine:latest    volumes:      - "./project:/app"    command: sh -c "sleep 3600

Строка 7. Контейнер будет жить 1 час.

Запускаем: docker-compose up -d

Заходим в систему под другим пользователем, который имеет доступ к docker.

Смотрим имя контейнера: docker ps

Заходим в контейнер: docker-compose exec -it {имя_контейнера} /bin/sh

Теперь можно спокойно зайти в папку: cd /app

В которой лежит секретный файл sectret.txt
Файл можно просматривать и редактировать его содержимое.

Итоги

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

GitHub: тут

Подробнее..

Гибриды побеждают или холивары дорого

11.01.2021 02:05:19 | Автор: admin

Мотивом для написания данной статьи послужил тот факт, что на habr.com участилось появление материалов маркетингового характера про Apache Kafka. А также тот факт, что из статей складывается впечатление что пишут их немного далекие от реального использования люди это конечно же только впечателение, но почему-то в большинстве своем статьи обязательно содержат сравнение Apache Kafka с RabbitMQ, причем не в пользу последнего. Что самое интересное читая подобные статьи управленцы без технического бэкграунда начинают тратить деньги на внутренние исследования, чтобы ведущие разработчики и технические директора выбрали одно из решений. Так как я очень жадный/домовитый, а также так как я сторонник тезиса "В споре НЕ рождается истина" предлагаю вам ознакомится с другим подходом почти без сравнения разных брокеров.


Без сравнения никуда


Вообще, по правильному, я должен был сделать статью в формате Kafka+RabbitMQ+Nats+ActiveMQ+Mosquito+etc, но мне кажется, что для Вас дорогие читатели это будет перебор, хотя обычно в моих архитектурных решениях присутствуют все вышеуказанные сервисы (и не только). И это я еще не рассказываю про AzureServiceBus/AmazonServiceBus которые также участвуют в "гибридах" при крупных программах проектов. Поэтому пока остановимся на связке Kafka+RabbitMQ и далее вы поймете почему: по аналогии можно подключить любой сервис с его протоколом. Потому что:


сравнивая Apache Kafka и RabbitMQ вы сравниваете 2 (два) бренда, а точнее 2 коммерческие компании Confluent и vmWare, и немножко Apache Software Foundation (но это не компания)

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


  • RabbitMQ мультипротокольный и расширяемый брокер сообщений
  • Apache Kafka платформа для распределенной потоковой передачи событий
  • Confluent Platform платформа потоковой передачи событий с возможностью создания высокопроизводительных конвейеров обработки данных для целей аналитики и интеграции в бизнес-сценариях

Я не зря третьим пунктом выделяю наработки компании Confluent те кто собирается использовать Apache Kafka в продуктиве должны хотя бы видеть какую функциональность дополнительно добавляет Confluent к Apache Kafka. А это SchemeRegistry, RestProxy, kSQL и еще несколько интересных штук, о одной из которых мы поговорим ниже, она называется Kafka-Connect.


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


Итак чтобы было совсем понятно, куда я веду.


  • ключевая особенность RabbitMQ мультипротокольность и расширяемость. (основной язык якобы Erlang)
  • ключевая особенность экосистемы Kafka потоковая передача с обработкой (основной язык якобы Scala/Java)

Отсюда и возникают минусы каждого из решений


  • для RabbitMQ мы не сможем построить нормального решения для потоковой обработки. Точнее сможем, но НЕ штатно.
  • а для Kafka мы не сможем сделать мультипротокольность, точнее сможем но НЕ штатно.

Сократ не говорил, что в споре рождается истина


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


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


  • ODBC
  • AMQP
  • MSMQ
  • XMPP
  • IP over Avian Carriers

так как тогда наша задача была интегрировать всякое (python, C#, java) и 1С был придуман проект One-S-Connectors (https://code.google.com/archive/p/one-c-connectors/source/default/source). Сейчас он имеет сугубо академический интерес (так как в 1С мире моя персона достаточно известна и на Хабре много 1С специалистов из сообщества "воинствующих 1С-ников" эта ссылка специально для них).
Однако уже тогда (в 2006 году) стало понятно, что по большому счету конечному разработчику придется менять/выбирать протокол под бизнес-задачу. А инфраструктурщикам придется обеспечить максимально широкий спектр интеграционных протоколов. От ODBC до Kafka/NATs/ModBus.


Но вернемся к дню сегодняшнему когда я начал использовать в проектах уровня ГИС (госсударственные информационные системы) различные транспорта данных внезапно выяснилось, что универсальные адаптеры это не только концепт воинствующих 1С-ников, но и соседей. Поэтому многие идеи при внедрении черпались из еще двух интересных проектов



маленькое примечание для менеджеров про Kombu как то так получилось, что имплементация протокола Apache Kafka до сих пор открыта https://github.com/celery/kombu/issues/301 и почему-то перешла в разряд "Дайте денег", поэтому для Python проектов приходится использовать дополнительно https://github.com/confluentinc/confluent-kafka-python

Когда вы дочитаете до этого момента предполагаю, что вы зададите вопрос про остальные языки: Java, GoLang, RUST, etc. Но во первых я не зря выше указал что по серьезному в наш обсуждаемый сегодня гибрид нужно добавить историю про NATs и ActiveMQ и внезапно JMS поэтому просьба дочитать до конца: Java будет, а во вторых мы переходим к еще трем полезным ссылкам



Прокоментируем их? Дело в том, что как бы вы не хотели, а для полноценного использования "в длинную" вам придется подписаться на историю релизов как сервера RabbitMQ и самое главное на те самые расширения (лежат в каталоге /deps) которые постоянно добавляются в ядро RabbitMQ, так и на портал компании Confluent где она публикует приложения полезные для конечного бизнеса использующего Apache Kafka в продуктиве.


подход к расширяемости за счет активируемых расширений также используется в экосистеме PostgreSQL тот который CREATE EXTENSION hypopg, так что подход реализованный компанией Pivotal/vmWare далеко не новый в нашем чудесном мире архитектуры программного обеспечения

Дополнительно на чудесном рынке облачных услуг в формате "Серьезная штука как сервис" есть еще один игрок это компания 84Codes https://github.com/84codes. Когда в рамках проектов внедрения нет нормальных инженеров по инфраструктуре именно 84Codes спасает пилотные проекты, потому как у них можно легко арендовать бесплатные/сильнодешевые контура CloudAMQP и CloudKarafka.


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


  • компания vmWare зарабатывает известно на чем, поэтому RabbitMQ ей развивается как часть своей платформы то есть они инвестируют в открытый проект не особо занимаясь его монетизацией. Возврат их инвестиций происходит в других местах, ну и также за счет контрибьторов на GitHub.
  • а вот компания Confuent собирается монетизировать свою платформу через Enterprise лицензию в которую включает те самые коннекторы Enterprise-Kafka-Connect, а также GUI для управления платформой.

Когда-то давно существовал https://github.com/jcustenborder/kafka-connect-rabbitmq, примечателен тот факт что товарищ Джереми его скрыл, оставив только свои наработки для Java разработчиков в виде Maven Archetype https://github.com/jcustenborder/kafka-connect-archtype еще раз обращаю Ваше внимание, что компания Confluent будет и дальше пытаться монетизировать свою деятельность, так что переводить всю интеграцию только на Kafka я бы на вашем месте поостерегся.

Поэтому когда вам топят за Kafka учитывайте, что вы либо изучаете Java, либо платите за Enterprise лицензию. А когда вам топят за RabbitMQ учитывайте, что либо вы изучаете системное администрирование (Erlang накладывает особенности системного администрирования), либо покупаете сервис у провайдеров типа 84Codes. Кодить на Erlang никогда не придется там это не нужно, если только вы не контрибьюторы OpenStack.


Поставил и забыл уже не работает


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


  • использование только одного протокола интеграции приводит к появлению ProtocolLock и как следствие к VendorLock я же не зря выше написал, что за каждым открытым продуктом, стоит какой-то ключевой комплект вендоров как они себя поведут: мы не знаем.
  • в мире ИТ больше нет серьезных продуктов, которые бы представляли собой монолитную службу все приложения давно стали композитными.
  • все нормальные вендоры сокращают свои релизные циклы по ключевым продуктам нормальной практикой стало выпускать редакции раз в 3 месяца TDD, BDD, CICD, ScallableAgile и DevOps (DocOps, DevSecOps) эти инженерные практики и методики управления не просто так развиваются. Всем очень хочется сокращать себестоимость и TimeToMarket.

Абзац выше важен, как финальный аккорд, прежде чем мы перейдем к Docker-Compose. А именно к нему я вел чтобы и разработчики и инфраструктурщики понимали что такое гибридная инфраструктура в режиме мультипротокольности (с) нужно сделать так, чтобы каждый мог поэкспериментировать с предлагаемым контуром. Как я уже указал выше первично подобное применительно к Kafka+RabbitMQ было подсмотрено именно у коллег из 84Codes (хорошие ребята всем советую https://www.84codes.com/).


Чтобы вы смогли поэкспериментировать сами


Итак подходим к примерам, так как обоснования и вводных уже хватит. Предположим вы уже поняли, что вам также нужна мультипротокольность, однако мы же помним, что все рекламные материалы про Apache Kafka нам рассказывают что это единственное решение с реализацией exactly-ones доставки сообщений от отправителя получателю. Собственно на самом деле нам и нужен гибрид, чтобы сделать из связки ТочкаОбмена->Очередь журнал Kafka (это тот который Topic) чтобы возникла сущность под называнием Offsets у нашей очереди событий.


exactly-ones

проверка на внимательность читающего exactly-ones это шутка в формате "Хотя бы один раз из 1С", а имеется в виду концепт Exactly once строго однократная доставка сообщений получателю, без необходимости повторной отправки от отправителя.


Предлагаю попробовать. Концепт для проверки Вашими руками будет состоять из:


  • Zookeper
  • KafkaBroker
  • RabbitMQ
  • KafkaConnect

и трех приложений приложений


  • отправитель на Python по протоколу AMQP 0.9
  • получатель на С# по протоколу AMQP 1.0
  • получатель на C# по протоколу Kafka

Еще интересное замечание: когда вы смотрите на всякие обучающие видео по Apache Kafka авторы часто (но не всегда) старательно пишут примеры на Java, это они делают скорее всего для того, чтобы скрыть от вас особенности использования librdkafka C++ библиотеки на основе которой сделаны многие не-джава адаптеры,. Я же наоборот предлагаю вам начинать исследование интеграции с Kafka именно с неё, чтобы четко оценивать риски "куда вы ввязываетесь": очень примечательно что там работает фактически один разработчик, формально в одиночку https://github.com/edenhill/librdkafka/pulse/monthly, а допустим wmWare старается поддерживать свою линейку клиентов под своим брендом https://github.com/rabbitmq

ну и самое главное и тяжелое:


контур содержит открытый форк старого RabbitMQ-Kafka-Sinc-Connector того самого который товарищи из Confluent в своё время скрыли с Github.


Докер контура для экспериментов


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


Развертываем RabbitMQ и Kafka


контур инфраструктуры который нам понадобится запускается достаточно просто


docker-compose -f dockers/infra.yml up -d

Если вам интересно что же там внутри, нашего композитного приложения, то в конце статьи дается ссылка на полный комплект исходников, наиболее интересен в нем Kafka-UI и непосредственно RabbitMQ-Sinc, все остальное обычно и штатно для всех известных примеров по Kafka или RabbitMQ


    image: provectuslabs/kafka-ui:latest    ports:      - 8080:8080    depends_on:      - kafka-broker      - zookeeper    environment:      KAFKA_CLUSTERS_0_NAME: local      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: broker:29092      KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181      KAFKA_CLUSTERS_0_JMXPORT: 9101

Но самое главное кроется в репозитории Java


    <parent>        <groupId>com.github.jcustenborder.kafka.connect</groupId>        <artifactId>kafka-connect-parent</artifactId>        <version>1.0.0</version>    </parent>

Если подробно изучить pom.xml то выяснится, что существует заглавный проект для всех конекторов к Кафка https://github.com/jcustenborder/kafka-connect-parent, в котором используется Java-Kafka-Adapter


И непосредственно синхронизацией c RMQ занимается штатный Java клиент https://www.rabbitmq.com/java-client.html


            <groupId>com.rabbitmq</groupId>            <artifactId>amqp-client</artifactId>            <version>${rabbitmq.version}</version>

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


  • собрать из исходников java синхронизатор -1-build-connect-jar.bat
  • собрать контейнер с синхрозатором 00-build-connect-image.sh

и уже потом запустить полный инфраструктурный контур


  • стартуем полный инфраструктурный контур 01-start-infra.sh

обратите внимание так как Docker использует разное поведение при работе с PWD для Windows и Linux приходится делать дубликаты скриптов. В остальных случаях под обоими операционными системами используется интерпретатор sh

В итоге вы получите следующий комплект сервисов



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


Назначение портов:


  • 9092 будет использоваться для Kafka протокола
  • 8080 используется для отображения красивой картинки состояния Apache Kafka UI
  • 5672 будет использоваться для протокола AMQP 0.9 и он же будет работать и как AMQP 1.0
  • 15672 используется для красивой картинки управления RabbitMQ
  • 28082 отладочный порт для управления через curl трансформатором протоколов

В этот момент нужно остановится и прокомментировать особенность развертывания RabbitMQ в Docker:


  • хорошей практикой является версионирование включенных плагинов расширений enabled-rmq-plugins

[    rabbitmq_management,     rabbitmq_amqp1_0,     rabbitmq_mqtt,     rabbitmq_federation,     rabbitmq_federation_management,    rabbitmq_shovel,    rabbitmq_shovel_management,    rabbitmq_prometheus].

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

     "bindings":[        {           "source":"orders-send",           "vhost":"/",           "destination":"orders-amqp-10-consumer",           "destination_type":"queue",           "routing_key":"",           "arguments":{

Запускаем наши приложения


Остается только запустить наши приложения эмулирующие подключения


docker-compose -f dockers/infra.yml restart protocol-connect-syncdocker-compose -f applications.yml builddocker-compose -f applications.yml up

Топология наших тестовых приложений достаточно простая



Исходный код также максимально упрощён:


  • отправляется как-будто бы заказ Васи с периодичностью в 2 секунды

        producer = conn.Producer(serializer='json')        producer.publish({'client': 'Вася', 'count': 10, 'good': 'АйФончик'},                      exchange=order_exchange,                      declare=[kafka_queue, amqp10_queue])        time.sleep(2)

RUN python -m pip install \    kombu \    librabbitmq

причем используется для этого максимально производительная библиотека на Си для AMQP 0.9 librabbitmq наследуется именно от неё https://github.com/alanxz/rabbitmq-c


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

            Attach recvAttach = new Attach()            {                Source = new Source()                {                    Address = "orders-amqp-10-consumer",                    Durable = 1,                },

            ReceiverLink receiver =                 new ReceiverLink(session,"netcore_amqp_10_consumer", recvAttach, null);            Console.WriteLine("Receiver connected to broker.");            while (true) {                Message message = receiver.Receive();                if (message == null)                {                    Console.WriteLine("Client exiting.");                    break;                }                Console.WriteLine("Received "                   + System.Text.Encoding.UTF8.GetString((byte[])message.Body)

Причем в качестве драйвера выбран


  <ItemGroup>    <PackageReference Include="AMQPNetLite.Core" Version="2.4.1" />  </ItemGroup>

именно его https://github.com/Azure/amqpnetlite Microsoft использует для маркетинга своей реализации сервисной шины. Собственно именно AMQP 1.0 как протокол они и рекламируют https://docs.microsoft.com/ru-ru/azure/service-bus-messaging/service-bus-amqp-overview


Ну и финально


  • создан подписчик по протоколу Kafka который при каждом старте перечитывает с нуля журнал отправленных заказов Васи. Тот самый Exactly once.

                AutoOffsetReset = AutoOffsetReset.Earliest

                c.Subscribe("orders-from-amqp");

                    while (true)                    {                        try                        {                            var cr = c.Consume(cts.Token);

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


  • 5 инфраструктурных контейнеров


  • 3 контейнера с приложениями


  • готовый журнал транзакций заказов который можно посмотреть через Kafka-Ui


  • и готовый контур связей для RabbitMQ


А где же Java ?


Не волнуйтесь при таком гибридном подходе, без неё никуда, для того чтобы всё вышеуказанное заработало пришлось сделать форк и актуализировать версии Kafka-Connect-Base


[submodule "dockers/rabbitmq-kafka-sink"]    path = dockers/rabbitmq-kafka-sink    url = https://github.com/aliczin/kafka-connect-rabbitmq

Но самое интересное не это, самое интересное что в этом самом Kafka-Connect нет по сути никакой магии только код трансформации.


По сути нам предлагают:


  • создать наследника абстрактной задачи Источника

public class RabbitMQSourceTask extends SourceTask {

  • выполнить подписку на очередь сообщений

        this.channel.basicConsume(queue, this.consumer);        log.info("Setting channel.basicQos({}, {});", this.config.prefetchCount, this.config.prefetchGlobal);        this.channel.basicQos(this.config.prefetchCount, this.config.prefetchGlobal);

  • трасформировать полученные сообщения в абстрактные записи причем с буфером.

  @Override  public List<SourceRecord> poll() throws InterruptedException {    List<SourceRecord> batch = new ArrayList<>(4096);    while (!this.records.drain(batch)) {

Отдельно можно выделить чудесный трансформатор сообщений из AMQP 0.9 в Кафка. У несведующего в Java глаз может задергаться. У автора чувствуется многолетный опыт работы в J2EE.


  private static final Logger log = LoggerFactory.getLogger(MessageConverter.class);  static final String FIELD_ENVELOPE_DELIVERYTAG = "deliveryTag";  static final String FIELD_ENVELOPE_ISREDELIVER = "isRedeliver";  static final String FIELD_ENVELOPE_EXCHANGE = "exchange";  static final String FIELD_ENVELOPE_ROUTINGKEY = "routingKey";  static final Schema SCHEMA_ENVELOPE = SchemaBuilder.struct()      .name("com.github.jcustenborder.kafka.connect.rabbitmq.Envelope")      .doc("Encapsulates a group of parameters used for AMQP's Basic methods. See " +          "`Envelope <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html>`_")      .field(FIELD_ENVELOPE_DELIVERYTAG, SchemaBuilder.int64().doc("The delivery tag included in this parameter envelope. See `Envelope.getDeliveryTag() <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html#getDeliveryTag-->`_").build())      .field(FIELD_ENVELOPE_ISREDELIVER, SchemaBuilder.bool().doc("The redelivery flag included in this parameter envelope. See `Envelope.isRedeliver() <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html#isRedeliver-->`_").build())      .field(FIELD_ENVELOPE_EXCHANGE, SchemaBuilder.string().optional().doc("The name of the exchange included in this parameter envelope. See `Envelope.getExchange() <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html#getExchange-->`_"))      .field(FIELD_ENVELOPE_ROUTINGKEY, SchemaBuilder.string().optional().doc("The routing key included in this parameter envelope. See `Envelope.getRoutingKey() <https://www.rabbitmq.com/releases/rabbitmq-java-client/current-javadoc/com/rabbitmq/client/Envelope.html#getRoutingKey-->`_").build())      .build();

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


Итоги


Все что здесь продемонстрировано естественно лежит на Github.


В репозитории https://github.com/aliczin/hybrid-eventing. Лицензия выставленна простая до невозможности Creative Commons Attribution 4.0 International.


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


Схема коммуникации в итоге для "разработчика интеграционных потоков" (с) выглядит следующим образом для источника и брокеров


orderEventsApp->Amqp09: send orderAmqp09->Amqp10: fanout\n copy eventAmqp09->KafkaQ: fanout\n copy eventKafkaQ->KafkaConnect: consume\n on messageKafkaConnect->KafkaConnect: transform\n messageKafkaConnect->Kafka: publish to topic


а для приемников все упрощается


Amqp10->orderEventSubApp: subcribe\n for eventorderJournalApp->Kafka: read kafka journal


Приемники берут нужные им данные только по нужному им протоколу

Ключевые посылы


Ключевые моменты которые я хотел расскрыть данной статьей


  • стройте эксперименты и продуктивы с Apache Kafka не со штатным Java клиентом, а librdkafka и базирующихся на ней адаптерах это позволит вам отладить сценарии разных версий протоколов KafkaAPI. Java вам пригодится в другом месте.


  • не ввязывайтесь с священные войны, что лучше RabbitMQ/Kafka/Nats/ActiveMQ просто развертывайте сервисы и публикуйте протоколы и пробуйте свои бизнес-сценарии.


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


  • реальный ИТ ландшафт почти всегда будет мультипротокольным



Примечание для понимающих


чтобы гибриды развивались дальше:


  • Mosquito очень удобен как встраиваемый брокер на уровне контролера SCADA для преобразования из ModBus/OPC-UA. Хотя как вы уже поняли из статьи интересны реализации "мостов из протокола в протокол" пример https://github.com/mainflux/mainflux


  • ActiveMQ удобен для Java разработчиков, потому что у них есть боязнь Erlang, но как мы выше уже сказали мост RabbitMQ AMQP 1.0 -> ActiveMQ легко организуется средствами RabbitMQ, кстати также как и JMS.


  • NATs интересен как часть OpenFaaS платформы, при внедрении "своего маленького" Amazon Lambda с преферансом. И опять же подход будет всё тот же мультипротокольные мосты с трансформацией: https://github.com/nats-io/nats-kafka если Вам не страшно посмотрите эксперименты с OpenFaaS веселых 1С-ников 2.5 часа примеров https://youtu.be/8sF-oGGVa9M



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


Функциональность: Мультипротокольный адаптер    Как разработчик я хочу иметь абстракцию Produser/Consumer    С возможность изменения протокола интеграции    Чтобы под каждую задачу выбирать разные протоколы     и единый интерфейс вызова для обеспечения независимости от вендора предоставляющего транспортСценарий: vmWare реализует протокол Stream средствами RabbitMQ     Когда vmWare закончит свой плагин для потоков    Тогда я активирую новый протокол     И быстро воткну его в приложение    И так как у меня есть продуктивный кластер RabbitMQ    И мне нужно будет просто поменять канал для отдельных бизнес сценариевСценарий: Завтра придут 1С-ники со своим ActiveMQ из Шины для 1С    Когда мне нужно быстро включить очереди 1С в общий контур    И чтобы на Питоне использовать старые наработки с Kafka API    Тогда я добавляю трансформацию ActivemeMQ2Kafka    и живу по старому а события ходят уже и из 1Сetc

А чтобы вы не думали, что данный подход это нечто уникальное вот Вам еще интересная ссылка: https://github.com/fclairamb/ftpserver/pull/34 это когда нужен FTP сервер, а хочется S3.


Ну и в качестве финального момента обратите внимание: есть и риски данного подхода: но они я думаю Вам и так понятны.


  • Придется оркестрировать такой комплект сервисов и вручную это почти невозможно. Придется использовать DevOps штуки типа k8s, OpenShift, etc но если вы уже решились на интеграцию в режимах слабой связаности приложений в режиме онлайн, у вас что-то на эту тему уже скорее всего есть.
  • Трансформаторы между протоколами приходится дорабатывать ничего готового открытого и PRODUCTION-READY на данный момент найти почти невозможно.

Финальное примечение для любителей писать ТЗ по ГОСТу


так как Хабр читают любители цифровой трансформации (чтобы кто не понимал под этим словом) советую в техническое задание добавлять не упоминание конкретных реализации серверов, а что-то примерно следующее:


комплект программ для интеграции должен реализовывать коммуникацию конечных приложений по открытым протоколам HTTP, AMQP 0.9, AMQP 1.0, Apache Kafka не ниже версии 23, MQTT, WebSockets, <ЛюбойДругойХотьSOAPХотяЭтоЖуть> с возможность преобразования между протоколами дополнительными средствами администрирования

Надеюсь моя публикация после долгого перерыва Вам будет полезна в ваших интеграционных проектах. Предполагаю что будет вопрос про 1С и тут у меня совет только один. Используйте Google по ключевым словам 1С+RabbitMQ или 1С+Kafka или 1С+OpenFaas и RabbitMQ и Kafka "в 1С" давно и непринужденно используются. Потому что 1С это не только язык, но и несколько сообществ где уже давно сделаны все возможные адаптеры и платные и бесплатные. Собственно как и в Java/C#/Python/C++/Rust/etc.


Данная статья написана с применением расширения https://shd101wyy.github.io/markdown-preview-enhanced для Visual Studio Code за что автору летят дополнительные лучи добра.


Ну и в качестве финального момента хотел бы заметить, что выбор Cunfluent Inc в качестве платформы разработки Kafka-Connect экосистемы JDK выглядит все таки странно. Не удивлюсь если их конкуренты сделают такое же, но на GoLang, NodeJS (что-нибудь типа Kafka-Beats-Hub)



Красивые картинки в формате GraphViz я делаю при помощи хитрого проекта Docker2GraphViz помогает поддерживать актуальный контур и техническую документацию в формате Markdown


set CURPATH=%~dp0set DOCKER_DIR=%CURPATH%\dockersdocker run --rm -it --name dcv -v %DOCKER_DIR%\:/input pmsipilot/docker-compose-viz render -m image --force --output-file=infra-topology.png infra.ymldocker run --rm -it --name dcv -v %CURPATH%\:/input pmsipilot/docker-compose-viz render -m image --force --output-file=apps-topology.png applications.ymlcopy /b/v/y dockers\infra-topology.png content\assets\infra-topology.pngcopy /b/v/y apps-topology.png content\assets\apps-topology.png
Подробнее..

Traefik, docker и docker registry

11.04.2021 16:04:19 | Автор: admin

Под катом вы увидите:

  1. Использования Traefik в качестве обратного прокси для маршрутизации трафика внутрь docker контейнеров.

  2. Использование Traefik для автоматического получения Lets Encrypt сертификатов

  3. Использование Traefik для разграничения доступа к docker registry при помощи basic auth

  4. Все перечисленное выше будет настраиваться исключительно внутри docker-compose.yml и не потребует передачи отдельных конфигурационных файлов внутрь контейнеров.

Актуальность вопроса

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

Помимо этого в интернете мало информации на тему использования traefik для контроля доступа к docker registry. Описанную ниже технику можно использовать для контроля доступа к любому приложению, реализующему Rest API.

Поиск решения

Вот ссылка на официальную статью по развертыванию docker registry. Крутим страницу вниз и видим пример развертывания через docker-compose. Я перепечатаю пример ниже:

registry:  restart: always  image: registry:2  ports:    - 5000:5000  environment:    REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt    REGISTRY_HTTP_TLS_KEY: /certs/domain.key    REGISTRY_AUTH: htpasswd    REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd    REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm  volumes:    - /path/data:/var/lib/registry    - /path/certs:/certs    - /path/auth:/auth

Нам предлагают терминировать https трафик прямо внутри сервиса registry, чего мы делать не будем. Мы не станем усложнять себе жизнь и копировать сертификаты внутрь сервиса. Кроме того у нас есть другие https сервисы, которым также нужны сертификаты, так что у нас уже есть единая точка входа, где происходит автоматическая генерация сертификатов для новых сервисов с помощью let's encrypt - это контейнер с traefik.

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

В интернете мы находим идеальный пример использования nginx для авторизации и разделения прав доступа. Мы сделаем то же самое, но через traefik.

Итак, приступаем.

Базовая конфигурация Registry

Сперва запустим сервис registry через отдельный compose файл. Создадим новую папку registry", в которой создадим compose файл:

mkdir registrycd registrynano docker-compose.yml

Вставим в файл следующее содержимое и сохраним:

version: '2.4'services:  registry:    restart: always    image: registry:2    ports:      - 5000:5000

Запустим сервис

docker-compose up -d

Откроем в браузере страницу http://<IP>:5000/v2/_catalog , где <IP> - это ip адрес докер машины.
В ответ увидим страницу с текстом:

{"repositories":[]}

Значит, все работает. Если это не так - проверьте firewall.

Базовая конфигурация Traefik

Знакомство с traefik начнем с базовой конфигурации.
Позже мы добавим SSL, сжатие трафика и авторизацию с аутентификацией.

Сперва запустим сервис registry через отдельный compose файл. Создадим новую папку registry", в которой создадим compose файл:

mkdir traefikcd traefiknano docker-compose.yml

Вставим в файл следующее содержимое и сохраним:

version: "2.4" services:   traefik:    image: "traefik:v2.4"    container_name: "traefik"    command:      - "--api.insecure=true"    ports:      - "8080:8080"
Разберем каждую новую строку (нажать)
command:- "--api.insecure=true"

Через command передаются параметры запуска нашего приложения.
Включаем доступ к dashboard в insecure режиме. Это означает, что dashboard будет доступен напрямую в точке входа с названием traefik. Если указанная точка входа traefik не настроена, она будет автоматически создана на порту 8080.

- "8080:8080"

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

Запускаем наш сервис:

docker-compose up -d

Открываем страницу с IP адресом докер машины и портом 8080:

Подключение Registry к Traefik (настройка домена)

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

Новый compose для Traefik:

Version: "2.4" services:   traefik:    image: "traefik:v2.4"    container_name: "traefik"    command:      - "--api.insecure=true"      - "--providers.docker=true"      - "--providers.docker.exposedbydefault=false"    ports:      - "80:80"      - "8080:8080"    networks:      - registry_default    volumes:      - "/var/run/docker.sock:/var/run/docker.sock:ro"networks:  registry_default:    external: true
Разберем каждую новую строку (нажать)
- "--providers.docker=true"

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

- "--providers.docker.exposedbydefault=false"

Запрещаем автоматическое добавление HTTP сервисов и HTTP маршрутов в traefik. Если этого не сделать, то traefik опубликует все docker контейнеры, в которых есть expose порта наружу автоматически. В качестве доменного имени он будет использовать имя контейнера.

Таким образом, без этой строки мы неявно открываем публичный доступ ко всем своим контейнерам! Все, что нужно для атаки - угадать имя контейнера! Я проверил эту теорию на практике: после добавления в файл hosts строки IP_докер_машины имя_контейнера, страница http://имя_контейнера" открылась в браузере.

- "80:80"

Перенаправление стандартного веб порта 80 (http) на docker машине в аналогичный порт контейнера traefik. Это нужно для обработки и маршрутизации полезного трафика.

    networks:      - registry_defaultnetworks:  registry_default:    external: true

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

volumes:  - "/var/run/docker.sock:/var/run/docker.sock:ro"

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

Новый compose для Registry:

version: '2.4'services:  registry:    restart: always    image: registry:2    ports:      - 5000:5000    labels:      - "traefik.enable=true"      - "traefik.http.routers.registry.rule=Host(`<REGISTRY.FQDN>`)"
Разберем каждую новую строку (нажать)
- "traefik.enable=true"

Эта метка оповещает traefik, что данный сервис нужно опубликовать

- "traefik.http.routers.registry.rule=Host(`<REGISTRY.FQDN>`)"

Эта метка указывает traefik, что доменное имя <REGISTRY.FQDN> необходимо связать с данным сервисом. Запросы, в заголовках которых будет указано данное имя, будут перенаправлены в текущий контейнер.

- "traefik.http.services.registry.loadbalancer.server.port=5000"

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

Перезапустим оба наших сервиса:

docker-compose up -d

Откроем в браузере страницу http://<REGISTRY.FQDN>:5000/v2/_catalog , где <REGISTRY.FQDN> - это полное доменное имя нашего сервиса, описанное в метке compose файла.

В ответ увидим страницу в текстом:

{"repositories":[]}

Значит все работает.

Добавление SSL (настройка https)

Мы будем автоматически получать и продлять SSL сертификаты через Let's Encrypt.

Новый compose для Traefik:

version: "2.4" services:   traefik:    image: "traefik:v2.4"    container_name: "traefik"    command:      - "--api.insecure=true"      - "--providers.docker=true"      - "--providers.docker.exposedbydefault=false"      - "--entrypoints.web.address=:80"      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"      - "--entrypoints.websecure.address=:443"      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"      - "--certificatesresolvers.myresolver.acme.email=<EMAIL>"      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"    ports:      - "80:80"      - "443:443"      - "8080:8080"    networks:      - registry_default    volumes:      - "letsencrypt:/letsencrypt"      - "/var/run/docker.sock:/var/run/docker.sock:ro"volumes:  letsencrypt:networks:  registry_default:    external: true
Разберем каждую новую строку (нажать)
- "--entrypoints.web.address=:80"

Меняем имя стандартного entrypoint с http на web для удобства.

- "--entrypoints.web.http.redirections.entryPoint.to=websecure"

Добавляем автоматическое перенаправление трафика с entrypoint web на websecure. Другими словами перенаправление с HTTP на HTTPS

- "--entrypoints.websecure.address=:443"

Создаем новый entrypoint на 443 порту с именем websecure

- "--certificatesresolvers.myresolver.acme.httpchallenge=true"

Настраиваем режим выдачи сертификатов Lets Encrypt через http challenge

- "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"

Настраиваем entrypoint для http challenge

- "--certificatesresolvers.myresolver.acme.email=<EMAIL>"

Настраиваем <email> адрес для регистрации в центре сертификации

- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"

Меняем стандартное расположение файла acme.json. В этот файл будут записываться выданные сертификаты. Дело в том, что стандартное расположение файла /acme.json" в корне не позволяет хранить этот файл на подключенном томе.

- "443:443"

Перенаправление стандартного веб порта 443 (https) на docker машине в аналогичный порт контейнера traefik. Это нужно для обработки и маршрутизации полезного трафика.

volumes:- "letsencrypt:/letsencrypt"volumes:letsencrypt:

Подключаем именованный том для постоянного хранения SSL сертификатов. Теперь даже после пересоздания контейнера нам не придется заново получать все сертификаты.
Именованный том будет храниться здесь: /var/lib/docker/volumes/<имя тома>

Новый compose для Registry:

version: '2.4'services:  registry:    restart: always    image: registry:2    ports:      - 5000:5000    labels:      - "traefik.enable=true"      - "traefik.http.routers.registry.rule=Host(`<REGISTRY.FQDN>`)"      - "traefik.http.routers.registry.entrypoints=websecure"      - "traefik.http.routers.registry.tls.certresolver=myresolver"
Разберем каждую новую строку (нажать)
- "traefik.http.routers.registry.entrypoints=websecure"

Меняем entrypoint со стандартного http (web) на websecure

- "traefik.http.routers.registry.tls.certresolver=myresolver"

Задаем имя резолвера для работы SSL сертификатов

Перезапустим оба наших сервиса:

docker-compose up -d

Откроем в браузере страницу http://<REGISTRY.FQDN>:5000/v2/_catalog , где <REGISTRY.FQDN> - это полное доменное имя нашего сервиса, описанное в метке compose файла.

Мы увидим, что:

  • схема сменилась с http на https автоматически

  • соединение защищено сертификатом выданным Let's Encrypt

В случае проблем с получением сертификата, traefik будет использовать само-подписанный сертификат. Если это произошло, следует использовать команду docker logs traefik для просмотра логов.

Настройка домена и SSL для dashboard

Как ни странно, маршрутизация трафика во встроенный dashboard отличается от маршрутизации трафика во внешние сервисы, запущенные в докер контейнерах. Я потратил не мало времени, пытаясь использовать те же правила, что и раньше.

Новый compose для Traefik:

version: "2.4" services:   traefik:    image: "traefik:v2.4"    container_name: "traefik"    command:      - "--api.insecure=true"      - "--providers.docker=true"      - "--providers.docker.exposedbydefault=false"      - "--entrypoints.web.address=:80"      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"      - "--entrypoints.websecure.address=:443"      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"      - "--certificatesresolvers.myresolver.acme.email=<EMAIL>"      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"    ports:      - "80:80"      - "443:443"      - "8080:8080"    networks:      - registry_default    volumes:      - "letsencrypt:/letsencrypt"      - "/var/run/docker.sock:/var/run/docker.sock:ro"    labels:      - "traefik.enable=true"      - "traefik.http.routers.traefik.rule=Host(`<TRAEFIK.FQDN>`)"      - "traefik.http.routers.traefik.entrypoints=websecure"      - "traefik.http.routers.traefik.tls.certresolver=myresolver"      - "traefik.http.routers.traefik.service=api@internal"volumes:  letsencrypt:networks:  registry_default:    external: true
Разберем каждую новую строку (нажать)
labels:  - "traefik.enable=true"  - "traefik.http.routers.traefik.rule=Host(`<TRAEFIK.FQDN>`)"  - "traefik.http.routers.traefik.entrypoints=websecure"  - "traefik.http.routers.traefik.tls.certresolver=myresolver"

Регистрируем роутер для направления трафика домена <traefik.fqdn> во внутренний dashboard.

- "traefik.http.routers.traefik.service=api@internal"

Явно указываем имя сервиса в который мы будем перенаправлять трафик.

api@internal - это зарезервированное имя сервиса. Перенаправление в dashboard не будет работать без этой строки.

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

Перезапустим traefik:

docker-compose up -d

Откроем в браузере страницу http://<TRAEFIK.FQDN>, где <TRAEFIK.FQDN> - это полное доменное имя нашего для доступа к traefik dashboard, описанное в метке compose файла.

Добавление сжатия трафика

Сжатие сильно ускоряет загрузку сайтов на клиенте. Обязательно нужно включать.

Новый compose для Traefik:

version: "2.4" services:   traefik:    image: "traefik:v2.4"    container_name: "traefik"    command:      - "--api.insecure=true"      - "--providers.docker=true"      - "--providers.docker.exposedbydefault=false"      - "--entrypoints.web.address=:80"      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"      - "--entrypoints.websecure.address=:443"      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"      - "--certificatesresolvers.myresolver.acme.email=<email>"      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"    ports:      - "80:80"      - "443:443"      - "8080:8080"    networks:      - registry_default    volumes:      - "letsencrypt:/letsencrypt"      - "/var/run/docker.sock:/var/run/docker.sock:ro"    labels:      - "traefik.http.middlewares.traefik-compress.compress=true"      - "traefik.enable=true"      - "traefik.http.routers.traefik.rule=Host(`<TRAEFIK.FQDN>`)"      - "traefik.http.routers.traefik.entrypoints=websecure"      - "traefik.http.routers.traefik.tls.certresolver=myresolver"      - "traefik.http.routers.traefik.service=api@internal"      - "traefik.http.routers.traefik.middlewares=traefik-compress"volumes:  letsencrypt:networks:  registry_default:    external: true
Разберем каждую новую строку (нажать)
- "traefik.http.middlewares.traefik-compress.compress=true"

Регистрируем новый middleware с именем traefik-compress и функцией сжатия трафика. Этот middleware мы затем сможем использовать в любом стороннем докер контейнере.

- "traefik.http.routers.traefik.middlewares=traefik-compress"

Добавляем middleware с именем traefik-compress в цепочку обработки трафика для сервиса traefik

Новый compose для Registry:

version: '2.4'services:  registry:    restart: always    image: registry:2    ports:      - 5000:5000    labels:      - "traefik.enable=true"      - "traefik.http.routers.registry.rule=Host(`<REGISTRY.FQDN>`)"      - "traefik.http.routers.registry.entrypoints=websecure"      - "traefik.http.routers.registry.tls.certresolver=myresolver"      - "traefik.http.routers.registry.middlewares=traefik-compress"
Разберем каждую новую строку (нажать)
- "traefik.http.routers.registry.middlewares=traefik-compress"

Добавляем middleware с именем traefik-compress в цепочку обработки трафика для сервиса registry

Добавление basic авторизации для доступа к Dashboard

Сначала мы собираемся сгенерировать комбинацию пользователя и пароля для базовой аутентификации с использованием htpasswd. Если он у вас не установлен, вам нужно сначала сделать это (например, для сервера Ubuntu):

apt-get install apache2-utils

Мы должны экранировать каждый символ $ в нашем зашифрованном пароле (заменить $ на $$), если мы используем пароль напрямую в docker-compose.yml

echo $(htpasswd -nbB USER "PASS") | sed -e s/\\$/\\$\\$/g

Пример вывода команды (результат будет разный при каждом запуске команды):

USER:$$2y$$05$$iPGcI0PwxkDoOZUlGPkIFe31e47F5vewcjlhzhgf0EHo45H.dFyKW

Вывод команды нужно поместить в наш docker-compose.yml внутрь traefik метки, заменив <USER-PASSWORD-OUTPUT> в примере ниже.

Новый compose для Registry:

version: "2.4" services:   traefik:    image: "traefik:v2.4"    container_name: "traefik"    command:      - "--api.insecure=true"      - "--providers.docker=true"      - "--providers.docker.exposedbydefault=false"      - "--entrypoints.web.address=:80"      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"      - "--entrypoints.websecure.address=:443"      - "--certificatesresolvers.myresolver.acme.httpchallenge=true"      - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"      - "--certificatesresolvers.myresolver.acme.email=<EMAIL>"      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"    ports:      - "80:80"      - "443:443"      - "8080:8080"    networks:      - registry_default    volumes:      - "letsencrypt:/letsencrypt"      - "/var/run/docker.sock:/var/run/docker.sock:ro"    labels:      - "traefik.http.middlewares.traefik-compress.compress=true"      - "traefik.http.middlewares.auth.basicauth.users=<USER-PASSWORD-OUTPUT>"      - "traefik.enable=true"      - "traefik.http.routers.traefik.rule=Host(`<TRAEFIK.FQDN>`)"      - "traefik.http.routers.traefik.entrypoints=websecure"      - "traefik.http.routers.traefik.tls.certresolver=myresolver"      - "traefik.http.routers.traefik.service=api@internal"      - "traefik.http.routers.traefik.middlewares=traefik-compress,auth"volumes:  letsencrypt:networks:  registry_default:    external: true
Разберем каждую новую строку (нажать)
- "traefik.http.middlewares.auth.basicauth.users=<USER-PASSWORD-OUTPUT>"

Регистрируем новый middleware с именем auth и функцией авторизации. Этот middleware мы затем сможем использовать в любом стороннем докер контейнере.

- "traefik.http.routers.traefik.middlewares=traefik-compress,auth"

Добавляем middleware с именем auth в цепочку обработки трафика для сервиса traefik

Внимание: Если вы используете переменные среды (например .env файл) в вашем docker-compose.yml вместо прямого указания <USER-PASSWORD-OUTPUT>, то вы не должны экранировать $. Генерация пароля в этом случае будет выглядеть так:

echo $(htpasswd -nbB <USER> "<PASS>")

После перезапуска docker контейнера (docker-compose up -d) мы увидим окно базовой авторизации, когда откроем dashboard traefik в браузере.

Разделение прав доступа пользователей Registry

Новый compose для Registry:

version: '2.4'services:  registry:    restart: always    image: registry:2    ports:      - 5000:5000    labels:      - "traefik.enable=true"      - "traefik.http.routers.registry.rule=Host(`REGISTRY.FQDN`) && Method(`POST`, `PUT`, `DELETE`, `PATCH`)"      - "traefik.http.routers.registry.entrypoints=websecure"      - "traefik.http.routers.registry.tls.certresolver=myresolver"      - "traefik.http.routers.registry.service=registry"      - "traefik.http.services.registry.loadbalancer.server.port=5000"      - "traefik.http.routers.registry.middlewares=auth-registry,traefik-compress"      - "traefik.http.middlewares.auth-registry.basicauth.users=<ADMIN-PASSWORD-OUTPUT>"      - "traefik.http.routers.guest-registry.rule=Host(`REGISTRY.FQDN`) && Method(`GET`, `HEAD`)"      - "traefik.http.routers.guest-registry.entrypoints=websecure"      - "traefik.http.routers.guest-registry.tls.certresolver=myresolver"      - "traefik.http.routers.guest-registry.service=guest-registry"      - "traefik.http.services.guest-registry.loadbalancer.server.port=5000"      - "traefik.http.routers.guest-registry.middlewares=aguest-registry,traefik-compress"      - "traefik.http.middlewares.aguest-registry.basicauth.users=<USER-PASSWORD-OUTPUT>"

Зарегистрируем на этом контейнере 2 набора роутеров и сервисов:

  • registry - роутер и сервис с таким именем будут предоставлять полный доступ (чтение\запись)

  • guest-registry - роутер и сервис с таким именем будут предоставлять гостевой доступ (чтение)

Так же мы создаем одноименные middleware для basic авторизации и добавляем их в роутеры.

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

Перезапустим registry:

docker-compose up -d
Проведем простую проверку с помощью Postman

Авторизовываемся пользователем с ограниченными правами.

Делаем Get запрос - работает.

Делаем Post запрос - 401.

Авторизовываемся пользователем с полными правами.

Делаем Get запрос - работает.

Делаем Post запрос - работает. Авторизация пройдена, но сам запрос отклоняется registry, так как не является допустимым. Мы не стали подбирать правильный запрос для экономии времени.

Заключение

На мой взгляд traefik гораздо удобнее классического nginx, если мы живем внутри docker контейнеров.

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

Подробнее..

Чиним резолвинг адресов в VPN-локалке (openconnect) для docker и systemd-resolved

19.03.2021 20:17:05 | Автор: admin
Для подключения к корпоративной сети у нас используется CiscoAnyConnect, работает хорошо, но не с докером. Как только докер пытается приподнять свою сеть, утилитка тут же отрубает VPN и переподключает. От этого докер себя плохо чувствует. Поэтому я решил использовать обычный линуксовый openconnect соместно с NetworkManager.

systemd-resolved


Поставил пакеты network-manager-openconnect network-manager-openconnect-gnome
Настроил соединение и оно даже подключилось. Проблема первая, каждый раз когда я подключался снова, он забывал имя пользователя, что раздражает. Я нашел решение и создал
баг. Решение простое, с консоли задаем свое имя в особом виде nmcli con mod prgcvp vpn.secrets 'form:main:username=yourName','save_passwords=yes'
После чего оно будет запомнено. Да, галочку запомнить пароль я ставил и пароль он даже запомнил, но вот в тексте галочки про имя пользователя ничего не сказано, так что он честно его забыл :) Напомню, что настройки менеджера лежат в /etc/NetworkManager/system-connections/
И если параметры знаешь, то можно и руками отредактировать нужное соединение.
Подключаться стало удобно и возникла вторая проблема, имена ресурсов в VPN сети не разрешаются в адреса DNS сервером. Сервер есть, все настройки на месте, но nslookup someserver.local выдает ошибку, а nslookup someserver.local somednsIP выдет правильный ответ. Странно, подумал я, как так, сервер есть, отвечает, а если его конкретно не указать, ошибка? Ответ оказался прост (хоть я и потратил изрядно времени пока разбирался, потому и пишу эту заметку что бы через год не забыть). Когда systemd-resolved пытается найти адрес сервера по имени, он выступает фасадом для других DNS серверов. Делается это так, ссылка /etc/resolv.conf может указывать на несколько мест:
  • /run/systemd/resolve/stub-resolv.conf это опция по умолчанию и этот файл будет содержать примерно такое
    nameserver 127.0.0.53
    options edns0 trust-ad
    search somedomain.local
  • /run/systemd/resolve/resolv.conf это можно использовать если функционал systemd-resolved чем то не устроил когда он прикидывается DNS сервером 127.0.0.53. В итоге это не пригодилось, так для информации пишу.

Подробности тут
Саму ссылку /etc/resolv.conf вы можете сами настроить что бы смотрела в любое из мест.
Так вот, дело в том, что openconnect при подключении к VPN получает таблицу route, DNS сервера, а так же search domain и этот домен от VPN сервера приходил неверный (так неправильно настроен у нас). От сервера приходило somedomain.local, а надо было просто local что бы somesrver.local был распознан.
Когда systemd-resolved прикинулся локальным DNS сервером и через /etc/resolv.conf всех отправил к себе за разрешением имен, логика его работы такова. Для каждого коннекта, которые можно посмотреть командой nmcli connection show (это те коннекты, которые знает NetworkManager) systemd-resolved помнит DNS сервера, которые получил по DHCP. Это можно посмотреть командой:
resolvectl dns
Global:
Link 6 (docker0):
Link 5 (vpn0): 999.999.999.999 999.999.999.999
Link 3 (wlp4s0): 192.168.3.8
Link 2 (enp5s0f2):

Когда в 127.0.0.53 приходит запрос на разрешение имени, systemd-resolved смотрит search domain у каждого из коннектов (эти домены он при подключении от DHCP получил тоже). Домены можно посмотреть командой:
resolvectl domain
Global:
Link 6 (docker0):
Link 5 (vpn0): somedomain.local
Link 3 (wlp4s0): ~.
Link 2 (enp5s0f2):

Далее имя хоста проверяется на частичное совпадение с теми доменами, которые прикреплены к коннектам и самое длинное совпадение и определяет какой конкретно DNS сервер вызвать. Либо все идет в DNS сервер где search domain "~."
От сервера компании мне приходил неверный search domain (somedomain.local) для VPN коннекта и потому когда я пытался разрешить адрес someserver.local, systemd-resolved их не мог найти, так как предполагал, что DNS сервера, полученные из этого соединения нужны что бы распознавать имена someserver.somedomain.local. Поправил я это подменив search domain в NetworkManager командой
nmcli connection modify yourConnectionName ipv4.dns-search local
Доменов может быть несколько через пробел. В итоге все заработало.
Помимо этого я удалил пакет avahi-daemon, так как службы bonjur, которые обслуживает этот демон по умолчанию тоже резолвятся на домене local, а в нашей сетке именно это имя используется для локальной сети и будут конфликты.

docker


Теперь в хост системе работает резолвинг адресов, но при запуске докера резолвинг локальных адресов в VPN может не работать по прежнему. И тут есть несколько вариантов.
Контейнер запущен с network_mode: host в таком случае будет использоваться для резольвинга то, что лежит в /run/systemd/resolve/resolv.conf и там у меня первый же днс сервер выбирается для резольва local и он для этого не подходит. Итог, не работает. Зато все сервисы хост машины видно из контейнера, что еще и неправильно.
Контейнер запущен с network_mode: bridge с созданием отдельной сети. В таком случае сервисы хоста будет не видно, помимо этого будет использован все тот же не работающий у меня /run/systemd/resolve/resolv.conf
Контейнер запущен без настроек сети и использует дефолтный bridge, созданный докером при инсталляции (docker0). В этом случае используется для резольва имен внутренний докеровский DNS, который судя по всему нормально взаимодействует с systemd-resolved и все резолвит как надо. В /etc/resolv.conf будет такое:
bash-5.0# cat /etc/resolv.conf
search local
nameserver 127.0.0.11
options ndots:0

Если надо показать сервисы хост машины для доступа из контейнера, то просто запускаем сервисы слушать на docker0 IP и получаем доступ.
ifconfig|grep -n1 docker0
27-
28:docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
29- inet 192.168.32.1 netmask 255.255.240.0 broadcast 192.168.47.255

Не забываем открыть порты, например у меня ufw зарезал 8080 и пришлось открывать.
Было бы отлично если бы docker просто использовал systemd-resolved dns stub как в последнем описанном варианте всегда. Но к сожалению это не так. В версии systemd-resolved 248, которая только вышла и в дистрах ее нет в документации есть параметр DNSStubListenerExtra, который может задать адрес где слушать для stub. Подробности тут
В итоге можно будет указать адрес где слушать не захардкоженный 127.0.0.53, а доступный изнутри докера и все будет работать, но пока нет.
Есть другое решение, когда контейнер пойдет на порт 53 мы будем перебрасывать его запросы в стаб systemd с помощью чего то вроде
socat UDP-LISTEN:53,fork,reuseaddr,bind=yourInterfaceIP UDP:127.0.0.53:53
Соответственно, этот DNS можно указать докеру при старте и весь функционал systemd будет работать. Но не стал это использовать.
В итоге мы получаем работающий VPN на хосте, который резолвит имена в локалной сети, работающий докер, который может резолвить эти адреса как родные.
Подробнее..

Из песочницы Развертывание и настройка аутентификации node-red на docker-compose

28.06.2020 10:17:00 | Автор: admin

Развертывание и настройка аутентификации node-red на docker-compose


Развертывания node-red на docker-compose с включением авторизации и использованием docker volume.

Создаем файл docker-compose.yml:

version: "3.7"services:  node-red:    image: nodered/node-red    environment:      - TZ=Europe/Moscow    ports:      - "11880:1880" # 11880 - порт для подключения к контейнеру, 1880 - порт на котором работает node-red внутри контейнера.    volumes:      - "node-red:/data" # node-red - каталог который выделит docker для хранения данных, /data - каталог внутри контейнера.    restart: alwaysvolumes:  node-red: # создание каталога node-red на хосте.

Запускаем контейнер в отладочном режиме (первый запуск должен быть без ключа `-d`, для просмотра ошибок, когда такие появятся):

$ docker-compose up node-redCreating node-red_node-red_1_3e3e59f5e044 ... doneAttaching to node-red_node-red_1_bca4cb987984node-red_1_bca4cb987984 |node-red_1_bca4cb987984 | > node-red-docker@1.0.3 start /usr/src/node-rednode-red_1_bca4cb987984 | > node $NODE_OPTIONS node_modules/node-red/red.js $FLOWS "--userDir" "/data"...

Останавливаем контейнер и запускаем команду для просмотра volume:

$ docker volume lsDRIVER              VOLUME NAMElocal               node-red_node-red

Просматриваем детальную информацию по volume:

$ docker volume inspect node-red_node-red[    {        "CreatedAt": "2020-05-02T18:37:33Z",        "Driver": "local",        "Labels": {            "com.docker.compose.project": "node-red",            "com.docker.compose.version": "1.23.0",            "com.docker.compose.volume": "node-red"        },        "Mountpoint": "/var/lib/docker/volumes/node-red_node-red/_data", # расоложение нашего каталога        "Name": "node-red_node-red",        "Options": null,        "Scope": "local"    }]

Переходим в каталог volume. В этом каталоге уже находятся фйлы которые docker создал при развертывании контейнера.

$ sudo ls /var/lib/docker/volumes/node-red_node-red/_datalib  package.json  settings.js

Нас интересует файл settings.js. Открываем его и ищем кусок кода adminAuth. Раскомментируем его.

 // Securing Node-RED    // -----------------    // To password protect the Node-RED editor and admin API, the following    // property can be used. See http://nodered.org/docs/security.html for details.    adminAuth: {        type: "credentials",        users: [{            username: "admin",            password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.",            permissions: "*"        }]    },

В ключ password: надо вставить hash пароля node-red.

Получение hash пароля node-red
На любую машину с node.js установить пакет node-red-admin.

npm i node-red-admin -g

Запустить пакет и задать пароль:

node-red-admin hash-pw


Запускаем контейнер и подключаемся к порту 11880.

http://192.168.0.100:11880/

Должно появиться окно авторизации.

image

Вводим логин-пароль.

Если все работает, перезапускаем контейнер с ключом -d.

$ docker-compose up -d node-red

Как-то так.
Подробнее..
Категории: Devops , Docker , Docker-compose , Node-red , Auth

Из песочницы Настройка docker для django на mysql

20.09.2020 18:23:47 | Автор: admin

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


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


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


  • ubuntu 20.04
  • docker 19.03.12
  • docker-compose 1.26.2
  • python 3.8
  • mysql 5.7
  • django 3.1.1
  • mysqlclient 2.0.1
  • django-mysql 3.8.1

Настройка рабочего пространства


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


  1. Установить docker и docker-compose. Процесс установки подробно описан в документации докера.
  2. Создать директорию, в котором будет размещаться наш проект (в данном примере project). В этой директории создать еще одну, в которой будет храниться конфигурация для запуска образа python (в данном примере web).

./project      /web

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


requirements.txt


В директории web создаем файл под названием requirements.txt.
В данном файле объявляем пакеты для работы нашего django приложения.


/project/web/requirements.txt


Django==3.1.1mysqlclient==2.0.1django-mysql==3.8.1

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


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


Dockerfile


В директории web создаем файл под названием Dockerfile.
Файл Dockerfile настраивает содержимое образа приложения с помощью одной или нескольких команд. После сборки запустим контейнер из настроенного образа.


Добавьте следующее содержимое в этот файл:


/project/web/Dockerfile


FROM python:3.8ENV PYTHONUNBUFFERED 1RUN mkdir /web_djangoWORKDIR /web_djangoCOPY requirements.txt /web_django/RUN pip install --upgrade pip && pip install -r requirements.txtADD . /web_django/ 

Данная настройка создает в образе python директорию с нашим проектом, копирует туда список пакетов, которые нужно установить и устанавливает их. Также папка с кодом из образа привязывается к директории web_django в корне проекта.
Перед установкой пакетов обновляется менеджер пакетов pip во избежании проблем с версией(--upgrade pip), т.к. изначально в моем случае он был устаревшим, и вылезала ошибка.


docker-compose.yml


В корне проекта создаем файл docker-compose.yml.


Данный файл содержит настройки сервисов используемые приложением. В данном примере этими сервисами являются веб-сервер и база данных. Файл compose также описывает, какие образы Docker используют эти сервисы, как они связываются друг с другом, какие тома им нужны внутри контейнеров. Также устанавливаются порты, используемые этими сервисами.


Добавьте следующее содержимое в этот файл.


/project/docker-compose.yml


version: '3'services:  web:    build: ./web    command: python manage.py runserver 0.0.0.0:8000    volumes:      - .:/web_django    ports:      - '8000:8000'    depends_on:      - db  db:    image: mysql:5.7    ports:      - '3306:3306'    environment:      MYSQL_DATABASE: 'db_django'      MYSQL_USER: 'root'      MYSQL_PASSWORD: 'password'      MYSQL_ROOT_PASSWORD: 'password'    restart: always    volumes:      - ./db_django:/var/lib/mysql

В первой части собирается образ согласно нашей инструкции в файле web/Dockerfile и запускается контейнер. Далее запускается сервер django на 8000 порту. Настраивается том нашего джанго проекта и присваивается в зависимость сервис с образом mysql.


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


Настройку докера закончили. Теперь приступим к запуску контейнера и создания django проекта.


Создание проект django


Перейдите в корень проекта (project/) и выполните команду. Данная команда создает django проект с названием web_django.


sudo docker-compose run web django-admin startproject web_django .

После этой команды получится следующая структура:


drwxr-xr-x   6 root  root   4,0K  сен 16 21:49 db_django-rw-rw-r--   1 kafka kafka  466   сен 16 21:31 docker-compose.yml-rwxr-xr-x   1 root  root   666   сен 16 21:34 manage.pydrwxrwxr-x   2 kafka kafka  4,0K  сен 16 21:31 webdrwxr-xr-x   3 root  root   4,0K  сен 16 21:34 web_django

Далее необходимо изменить владельца папки, т.к. контейнер запускается как root пользователь.


sudo chown -R $USER:$USER web_django

Настройка базы данных проекта


Откройте файл настроек django проекта по пути web_django/settings.py
В этом файле заменим настройки подключения к бд, которые мы указывали в docker-compose.yml.


/project/web_django/settings.py


DATABASES = {    'default': {        'ENGINE': 'django.db.backends.mysql',        'NAME': 'db_django',        'USER': 'root',        'PASSWORD': 'password',        'HOST': 'db',        'PORT': '3306',    }}

Теперь все готово и можно запускать docker-compose.


Запуск проекта


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


docker-compose up

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


docker-compose up -d

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


docker-compose build

Миграция базы данных


Для того, чтобы выполнить миграцию бд, выполните команды:


docker-compose run web python manage.py makemigrations

docker-compose run web python manage.py migrate
Подробнее..
Категории: Docker-compose , Mysql , Django , Dоcker

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru