Socket Activated Containers (Unicorn + Systemd)

У клиента есть большое количество медийных спец проектов (~250). Это виджеты, лендинги, апишки и т.д. С 2012 года это все живет на 1 машине со связкой Nginx + Passenger + Ruby.
Все хорошо, за исключением момента обновления ОС, когда все проблемы со старыми/новыми версиями пакетов вылезают и заявляют о себе в полный голос.
Казалось бы, идеальная история для контейнеров, но есть одно но. Passenger или PHP-FPM умеют то, что из коробки нет даже у Kubernetes — это старт по входящему трафику.
На просторах сети это называется (гуглится) — Socket Activation. На Network или Unix сокет приходит пакет, сервис запускается. Покопавшись на Stack Overflow и некоторых админских форумах, понял, что мысль о том, что контейнер, можно только по необходимости поднимать интересует многих. Такие запросы есть у больших ребят — привет велосипеды (читай свои решения).
Оказывается то, что нужно написали уже лет 7 назад. Пост про реализацию функционала в systemd датирован 2011 годом. Ну и в 2013 уже вышел пост про старт контейнера с помощью той же функциональности. Есть одно но, там используются стандартные средства systemd для виртуализации, нас же интересуют более популярные на данный момент Docker.

Во всех статьях говорили о proxy сервисе, который принимает трафик и отвечает за запуск контейнера. Gочему нельзя сокет использовать сразу в контейнере? Вопрос этот возникает, потому что на каждый сервис придется делать 3 конфига systemd и минимум 2 сокета, что как мне казалось много и не слишком красиво. Посмотрел потом на конфиги kubernetes и 3 конфига systemd показались маленькими, простыми и понятными.
Но так что с сокетом то? А вот я не нашел способа как его пробросить в контейнер. Если кто знает, с радостью послушаю. Проблема не в софте (unicorn и puma), который может переиспользовать проброшенный сокет. Есть стандартные процедуры для этого. Через ENV переменные передаются ID процесса (LISTEN_PID) и номер слушающего сокета (LISTEN_FDS), после чего софт не должен пытаться открыть новый, а переиспользовать (подключиться) к сокету с соответствующими координатами.
Проблема заключается в том, что ID процесса и ссылку на сокет будут переданы с host машины, контейнер естественно ничего о них не знает.
Шарить данные с хост машиной конечно можно, но безопасно ли?
require 'bundler/setup'
Bundler.require
require 'rack'
app = Proc.new do |env|
['200', {'Content-Type' => 'text/html'}, ['Simple app for test']]
end
run app
FROM ruby:2.4
RUN mkdir /app
COPY Gemfile Gemfile.lock /app/
WORKDIR /app
RUN bundle install
ADD config.ru /app/
ADD unicorn.rb /app/
CMD ["bundle", "exec", "--keep-file-descriptors", "unicorn", "-c", "unicorn.rb"]
source 'https://rubygems.org'
gem 'unicorn'
gem 'rack'
GEM
remote: https://rubygems.org/
specs:
kgio (2.11.2)
rack (2.0.5)
raindrops (0.19.0)
unicorn (5.4.0)
kgio (~> 2.6)
raindrops (~> 0.7)
PLATFORMS
ruby
DEPENDENCIES
rack
unicorn
BUNDLED WITH
1.16.1
[Unit]
Description=Socket Activation with Unicorn Service
After=network.target sact-test.socket
Requires=sact-test.socket
[Service]
ExecStart=/usr/bin/docker run --user 1001 --rm --name sact-test-n -v '/apps/sact/sact-git/tmp:/app/tmp' -e 'SOCKET_FILE=/app/tmp/sockets/unicorn.sock' sact-test
ExecStartPost=/bin/sleep 1
SyslogIdentifier=sact-test-docker
ExecStop=/usr/bin/docker stop sact-test-n
[Unit]
Requires=sact-test-docker.service
After=sact-test-docker.service
[Service]
SyslogIdentifier=sact-test-docker
ExecStart=/lib/systemd/systemd-socket-proxyd /apps/sact/sact-git/tmp/sockets/unicorn.sock
[Unit]
Description=Socket Activation with Unicorn
PartOf=sact-test.service
[Socket]
SocketUser=sact
SocketGroup=sact
SocketMode=0777
Backlog=2048
ListenStream=/apps/sact/sact-git/tmp/sockets/unicorn-proxy.sock
[Install]
WantedBy=sockets.target
upstream app_sact {
server unix:/apps/sact/sact-git/tmp/sockets/unicorn-proxy.sock;
}
server {
listen 80;
charset utf-8;
set $application_name "sact";
location / {
try_files $uri $uri.html @application;
}
location @application {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-User-Id;
proxy_redirect off;
proxy_pass http://app_$application_name;
break;
}
}
worker_processes 2
timeout 30
listen ENV['SOCKET_FILE'] || 8080, backlog: 2048
working_directory ENV['APP_PATH'] || '/app/
Сделал gist с файлами для тестов. Простейшее Rack приложение и набор сервисов для systemd. Чтобы это все завелось надо завести отдельного пользователя, назвал его sact
, и сложить Gemfile, Gemfile.lock, unicorn.rb, config.ru
файлы в /apps/sact/sact-git
. Установить docker, сложить systemd сервисный файлы в /etc/systemd/system
и перезагрузить демона через systemctl daemon-reload
. Также понадобиться nginx. Но думаю для тех кто это читает не составит большого труда разобраться.
Если обратиться на 80 порт localhost
curl localhost
, то получим Simple app for test
. Небольшая задержка на старте нас не пугает, потому что подразумевается что к сервисы обращаются не часто.
Что я не успел сделать — отслеживание трафика за определенный момент времени с целью остановить сервис. По идее это просто парсинг логов, если за последние N минут ничего не было, то останавливаем сервис с контейнером. Если мы соберемся делать такое решение на наших 250+ проектов, то обязательно напишу.
Stay tuned…
P.S. Где что подсматривал:
- От разработчика systemd про активацию контейнеров.
- Это на Python, но можно посмотреть как пробрасывается Socket из Systemd
- А это пример на Ruby как работать с сокетом Sytemd
- Откуда взял гифку