Всеобщая контейнеризация захватывает мир. Не обошла эта эпидемия и меня стороной, и теперь, последние шесть месяцев, я занимаюсь тем, что сегодня принято называть модным словом DevOps. В проектах, которыми я занимаюсь, мы решили использовать Docker, ведь он делает процесс развёртывания приложений до неприличия простым, и буквально заставляет вас следовать другому не менее модному сегодня течению — микросервисной архитектуре, которая способствует бурному размножению этих самых контейнеров на его основе. В какой-то момент понимаешь, что было бы неплохо собрать статистику их жизни и смерти в отнюдь небезопасной среде обитания. А в качестве бонуса изучить инструменты, которые используешь в работе, понаписать что-то не на основном языке программирования, да и просто сделать что-то необязательное, но полезное.
В статье я расскажу как за три вечера и кусочек ночи был разработан проект для аудита и сбора статистики жизненного цикла контейнеров.
Беглый поиск в гугле не привёл к нахождению уже готового решения, поэтому будем делать сами.
Что нужно:
Первую задачу решает registrator. Это решение от ребят из GliderLabs, которое позволяет автоматически регистрировать контейнеры в системах хранения конфигурации, такие как Сonsul или Netflix Eurika. К сожалению, последние заточены под совсем другую задачу: сказать какие сервисы сейчас доступны, и где расположены контейнеры, которые их реализуют.
Если рассмотреть каждое событие (запуск или смерть контейнера) как запись некоего лога, с которым мы можем делать всё что нам нужно, то для хранения этих записей можно взять ElasticSearch, а для просмотра и анализа в реальном времени — Kibana.
Нам остаётся решить второй пункт, а именно сделать связку между регистратором и эластиком.
Всякое развлечение начинается с форка, поэтому смело жмём кнопочку на GitHub-е для репозитория (https://github.com/gliderlabs/registrator). Клонируем себе на локальную машину и смотрим содержимое:
Схема простая. В registrator.go создаётся Docker-клиент, который слушает сокет, и, при возникновении какого-либо события (запуска, остановки или смерти контейнера), передаёт в bridge идентификатор контейнера и событие с ним связанное. Внутри bridge-а создаётся адаптер (модуль), который был указан при запуске приложения, в который уже передаётся детальная информация о контейнере для её последующей обработки. Таким образом достаточно добавить новый модуль, который будет пересылать данные в ElasticSearch.
Прежде чем писать код, попробуем собрать и запустить проект. В Makefile-е есть таск, в котором создаётся и запускается новый Docker-образ:
consul намекает нам на то, что это мастер-система по-умолчанию, без которой приложение не будет работать. Поставим его в Docker-контейнере в режиме standalone:
Затем запустим сборку регистратора:
Если всё прошло удачно (к сожалению удача она такая штука), то мы увидим что-то вроде этого:
Как видно у нас было 4 контейнера. У одного из них не было портов, у другого — порт 443 не был опубликован и т.д. Чтобы проверить, что сервисы действительно добавились, можно воспользоваться утилитой dig
Добавить -80 к имени контейнера необходимо, поскольку nginx выставляет наружу несколько портов, и с точки зрения Consul-а это разные сервисы.
Итак, мы запустили регистратор, а это значит, что самое время начать писать код.
Адаптеры в проекте для различных бэкендов реализуются в виде отдельных модулей. Вообще в Go модуль очень занятная штука. Это может быть как локальная папка, так и проект на GitHub-е, разницы в подключении практически нет.
Добавим новую папку в корень проекта: /elastic и разместим в ней файл с нашей будущей реализации: elastic.go.
Дадим имя по-умолчанию для нашего модуля
Заимпортируем необходимые нам сторонние пакеты:
Чтобы обрабатывать события, нужно реализовать интерфейс
Адаптер регистрируется через метод init(), который исполняется при загрузке модуля:
При создании адаптера необходимо создать экземпляр клиента к ElasticSearch:
С помощью метода isRunning() нужно проверить, что экземпляр всё ещё жив
Пусть запись о контейнере будет иметь следующую структуру:
Реализуем метод регистрации контейнера:
Дампим полностью информацию о сервисе в json.
Получаем текущее время. В Go используется забавная нотация для определения формата даты
Создаём новую запись для лога:
И отправляем её в специально созданный индекс
Функция Deregister полностью повторяет предыдущую, только с другим action-ом.
Остаётся поменять в Makefile-е consul на elastic, и прописать модуль в modules.go.
Запускаем ElasticSearch
Чтобы Kibana корректно работала с индексом, нужно добавить чуть переработанный шаблон от logstash-а:
Запускаем Kibana
Запускаем регистратор:
Запускаем контейнер с nginx-ом для тестирования решения
В Kibana нужно настроить новый индекс containers, после чего можно будет увидеть запись о запущенном nginx-е.
Файл с конечной реализацией лежит тут.
Всем хорошо наше решение, но для его работы нам нужно держать отдельный самописный индекс, и ещё не забыть накатить правильный шаблон с mapping-ами. Чтобы люди не заморачивались подобными вопросами существуют агрегаторы логов, которые не только умеют собирать информацию из огромного количества источников, но и сделают за нас всю грязную работу в части приведения логов к единому формату. Мы возьмём для наших экспериментов logstash.
По традиции запускать logstash мы хотим в контейнере. Официальный Docker-образ для logstash-а поставляется без исходных файлов, что на мой взгляд несколько странно (как заметил внимательный читатель grossws, ссылка на Dockerfile всё же присутствует). Второй по популярности и единственный, к слову, нашедшийся на github-e образ зачем-то запускает внутри себя и ElasticSearch и Kibana, что противоречит идее «один контейнер — один процесс». Там конечно есть возможность напередавать волшебную комбинацию флагов, но у меня он всё равно при старте лез брать какие-то ключи с сайта автора. На DockerHub-е было ещё с десяток контейнеров от неизвестных мне лиц, поэтому лучше соберём контейнер сами под наши нужды. Всё что нам понадобится — вот такой вот Dockerfile:
Образ будет очень простым и запустится только при наличии внешнего конфигурационного файла, что для наших развлекательных задач вполне себе норма. Соберём образ и зальём его на Docker Hub:
Создадим конфигурационный файл /mnt/logstash/conf/logstash.conf со следующим содержимым:
type => «audit» сделает так, что все наши логи будут иметь общее значение в поле type, что позволит нам их отличать от других логов по этому дискриминатору. Остальные настройки довольно очевидны. Запустим свежеиспечённый контейнер:
и проверим, что логи будут писаться, если мы по tcp передадим json.
Мы делаем уже второй модуль, поэтому стоит вынести реализацию в отдельный проект, который назовём auditor. Первым делом нам надо накрутить уже имеющееся «мясо» из регистратора. Поэтому берём наш форк и нагло копируем код себе в проект.
Проверяем, что всё у нас по-прежнему собирается, выполнив команду: make dev.
Замечаем, что в файле regitrator.go модуль bridge подключается как внешняя зависимость, поэтому можно смело удалять эту папку. Снова проверяем, что всё работает.
Изменяем Dockerfile.dev:
Аналогично меняем релизный Dockefile. Убираем лишние таски и меняем имя контейнера в Makefile:
Добавим новый модуль /logstash и файл logstash.go к нашему проекту. Возьмём готового клиента для logstash-а, который туп как пробка, и фактически является просто обёрткой над стандартной библиотекой net: github.com/heatxsink/go-logstash.
В этот раз структура контейнера будет несколько отличаться от предыдущего варианта:
Связано это с тем, что теперь нам нужно просто сериализовать объект в json и отправить его как строку в logstash, который сам разберётся со всеми полями в сообщении.
Также как и в прошлый раз регистрируем нашу фабрику:
И создаём новый экземпляр адаптера:
Здесь нам пришлось использовать утильный метод net.SplitHostPort(urls), который умеет вычленять хост и порт из строки, потому что клиент принимает их раздельно, а приходят они вместе в uri.Host.
Числовое представление порта можно получить, применив метод конвертации строки в число: intPort, _ := strconv.Atoi(port). Знак подчёркивания нужен, потому что функция возвращает два параметра, второй из которых ошибка, которую мы можем не обрабатывать.
Реализация метода Ping получилась довольно простой:
Фактически мы проверяем, что можем подключиться по tcp к logstash-у. В функции Connect повторное подключение произойдёт только если текущее уже не может быть использовано.
Осталось реализовать метод регистрации:
Думаю, что код достаточно понятен и не требует комментариев, кроме одного. Вызов Connect перед Writeln гарантирует, что будет получено рабочее соединение.
Метод Deregister полная копия метода выше.
Меняем в Dockerfile.dev в строке запуска elastic на logstash, запускаем и проверяем наличие записей в ElasticSearch:
Коммитим наши изменения на GitHub и идём собирать образ для DockerHub-а. На hub.docker.com, заходим на свою страницу и жмем кнопку +Add Repository. Когда собирался образ для logstash-a, я выбрал подпункт Repository, который позволяет вручную заливать свои образы, но есть и другой путь — Automated Build. Нажав на него, Docker Hub предложит подключить к нему свой аккаунт на GitHub-е или BitBucket-е. После этого остаётся только выбрать свой репозиторий, нужную ветку, и изменить названия образа, если это очень нужно. Всё остальное, включая перенос описания из README.MD возьмёт на себя Docker Hub.
После небольшого ожидания вот он — готовый образ.
Теперь можно протестировать его выполнив простую команду:
PS. Проект не используется в продакшене, и с моей критичной точки зрения требует допила, но каждый прочитавший статью может его попробовать и, при желании, улучшить.
В статье я расскажу как за три вечера и кусочек ночи был разработан проект для аудита и сбора статистики жизненного цикла контейнеров.
Первая половина дела
Беглый поиск в гугле не привёл к нахождению уже готового решения, поэтому будем делать сами.
Что нужно:
- мониторинг запуска и остановки отдельного взятого контейнера
- отправка сообщений о событии в некое хранилище
- удобный инструмент для просмотра событий и их последующего анализа
Первую задачу решает registrator. Это решение от ребят из GliderLabs, которое позволяет автоматически регистрировать контейнеры в системах хранения конфигурации, такие как Сonsul или Netflix Eurika. К сожалению, последние заточены под совсем другую задачу: сказать какие сервисы сейчас доступны, и где расположены контейнеры, которые их реализуют.
Если рассмотреть каждое событие (запуск или смерть контейнера) как запись некоего лога, с которым мы можем делать всё что нам нужно, то для хранения этих записей можно взять ElasticSearch, а для просмотра и анализа в реальном времени — Kibana.
Нам остаётся решить второй пункт, а именно сделать связку между регистратором и эластиком.
Как устроен регистратор
Всякое развлечение начинается с форка, поэтому смело жмём кнопочку на GitHub-е для репозитория (https://github.com/gliderlabs/registrator). Клонируем себе на локальную машину и смотрим содержимое:
registrator.go // основной файл запуска приложения
modules.go // подключение реализованных модулей (consul, etcd и т.д.)
Dockerfile // файл сборки docker-контйнера
Dockerfile.dev // файл для сборки dev-версии контейнера
/bridge // отсылаем данные во вне
/consul // реализация отправки сообщения в consul
Схема простая. В registrator.go создаётся Docker-клиент, который слушает сокет, и, при возникновении какого-либо события (запуска, остановки или смерти контейнера), передаёт в bridge идентификатор контейнера и событие с ним связанное. Внутри bridge-а создаётся адаптер (модуль), который был указан при запуске приложения, в который уже передаётся детальная информация о контейнере для её последующей обработки. Таким образом достаточно добавить новый модуль, который будет пересылать данные в ElasticSearch.
make dev
Прежде чем писать код, попробуем собрать и запустить проект. В Makefile-е есть таск, в котором создаётся и запускается новый Docker-образ:
dev:
docker build -f Dockerfile.dev -t $(NAME):dev .
docker run --rm --net host \
-v /var/run/docker.sock:/tmp/docker.sock \
$(NAME):dev /bin/registrator consul:
consul намекает нам на то, что это мастер-система по-умолчанию, без которой приложение не будет работать. Поставим его в Docker-контейнере в режиме standalone:
$ docker run -p 8400:8400 -p 8500:8500 -p 53:53/udp \
-h node1 progrium/consul -server -bootstrap
Затем запустим сборку регистратора:
make dev
Если всё прошло удачно (к сожалению удача она такая штука), то мы увидим что-то вроде этого:
2015/04/04 19:55:48 Starting registrator dev ...
2015/04/04 19:55:48 Using elastic adapter: consul://
2015/04/04 19:55:48 Listening for Docker events ...
2015/04/04 19:55:48 Syncing services on 4 containers
2015/04/04 19:55:48 ignored: cedfd1ae9f68 no published ports
2015/04/04 19:55:48 added: b4455d0f7d50 ubuntu:kibana:80
2015/04/04 19:55:48 added: 3d598d184eb6 ubuntu:nginx:80
2015/04/04 19:55:48 ignored: 3d598d184eb6 port 443 not published on host
2015/04/04 19:55:48 added: bcad15ac5759 ubuntu:determined_goldstine:9200
2015/04/04 19:55:48 added: bcad15ac5759 ubuntu:determined_goldstine:9300
Как видно у нас было 4 контейнера. У одного из них не было портов, у другого — порт 443 не был опубликован и т.д. Чтобы проверить, что сервисы действительно добавились, можно воспользоваться утилитой dig
dig @localhost nginx-80.service.consul
Добавить -80 к имени контейнера необходимо, поскольку nginx выставляет наружу несколько портов, и с точки зрения Consul-а это разные сервисы.
Итак, мы запустили регистратор, а это значит, что самое время начать писать код.
Go Go Go
Адаптеры в проекте для различных бэкендов реализуются в виде отдельных модулей. Вообще в Go модуль очень занятная штука. Это может быть как локальная папка, так и проект на GitHub-е, разницы в подключении практически нет.
Добавим новую папку в корень проекта: /elastic и разместим в ней файл с нашей будущей реализации: elastic.go.
Дадим имя по-умолчанию для нашего модуля
package elastic
Заимпортируем необходимые нам сторонние пакеты:
import (
"net/url"
"errors"
"encoding/json"
"time"
"github.com/gliderlabs/registrator/bridge"
elasticapi "github.com/olivere/elastic"
)
Чтобы обрабатывать события, нужно реализовать интерфейс
type RegistryAdapter interface {
Ping() error //проверяем жив ли наш бэкенд
Register(service *Service) error
Deregister(service *Service) error
Refresh(service *Service) error // можно не реализовывать :)
}
Адаптер регистрируется через метод init(), который исполняется при загрузке модуля:
func init() {
bridge.Register(new(Factory), "elastic")
}
При создании адаптера необходимо создать экземпляр клиента к ElasticSearch:
func (f *Factory) New(uri *url.URL) bridge.RegistryAdapter {
urls := "http://127.0.0.1:9200"
if uri.Host != "" {
urls = "http://"+uri.Host
}
client, err := elasticapi.NewClient(elasticapi.SetURL(urls))
if err != nil {
log.Fatal("elastic: ", uri.Scheme)
}
return &ElasticAdapter{client: client}
}
type ElasticAdapter struct {
client *elasticapi.Client
}
С помощью метода isRunning() нужно проверить, что экземпляр всё ещё жив
func (r *ElasticAdapter) Ping() error {
status := r.client.IsRunning()
if !status {
return errors.New("client is not Running")
}
return nil
}
Пусть запись о контейнере будет иметь следующую структуру:
type Container struct {
Name string `json:"container_name"`
Action string `json:"action"` //start and stop
Message string `json:"message"`
Timestamp string `json:"@timestamp"`
}
Реализуем метод регистрации контейнера:
func (r *ElasticAdapter) Register(service *bridge.Service) error
Дампим полностью информацию о сервисе в json.
serviceAsJson, err := json.Marshal(service)
if err != nil {
return err
}
Получаем текущее время. В Go используется забавная нотация для определения формата даты
timestamp := time.Now().Local().Format("2006-01-02T15:04:05.000Z07:00")
Создаём новую запись для лога:
container := Container {
Name: service.Name,
Action: "start",
Message: string(serviceAsJson),
Timestamp: timestamp
}
И отправляем её в специально созданный индекс
_, err = r.client.Index().
Index("containers").
Type("audit").
BodyJson(container).
Timestamp(timestamp).
Do()
if err != nil {
return err
}
Функция Deregister полностью повторяет предыдущую, только с другим action-ом.
Остаётся поменять в Makefile-е consul на elastic, и прописать модуль в modules.go.
All together now
Запускаем ElasticSearch
docker run -d --name elastic -p 9200:9200 \
-p 9300:9300 dockerfile/elasticsearch
Чтобы Kibana корректно работала с индексом, нужно добавить чуть переработанный шаблон от logstash-а:
{
"template" : "containers*",
"settings" : {
"index.refresh_interval" : "5s"
},
"mappings" : {
"_default_" : {
"_all" : {"enabled" : true},
"dynamic_templates" : [ {
"string_fields" : {
"match" : "*",
"match_mapping_type" : "string",
"mapping" : {
"type" : "string", "index" : "analyzed", "omit_norms" : true,
"fields" : {
"raw" : {"type": "string", "index" : "not_analyzed", "ignore_above" : 256}
}
}
}
} ],
"_ttl": {
"enabled": true,
"default": "1d"
},
"properties" : {
"@version": { "type": "string", "index": "not_analyzed" },
"geoip" : {
"type" : "object",
"dynamic": true,
"path": "full",
"properties" : {
"location" : { "type" : "geo_point" }
}
}
}
}
}
}
Запускаем Kibana
docker run -d -p 8080:80 -e KIBANA_SECURE=false \
--name kibana --link elastic:es \
balsamiq/docker-kibana
Запускаем регистратор:
make dev
Запускаем контейнер с nginx-ом для тестирования решения
docker run -d --name nginx -p 80:80 nginx
В Kibana нужно настроить новый индекс containers, после чего можно будет увидеть запись о запущенном nginx-е.
Файл с конечной реализацией лежит тут.
В бар врывается logstash
Всем хорошо наше решение, но для его работы нам нужно держать отдельный самописный индекс, и ещё не забыть накатить правильный шаблон с mapping-ами. Чтобы люди не заморачивались подобными вопросами существуют агрегаторы логов, которые не только умеют собирать информацию из огромного количества источников, но и сделают за нас всю грязную работу в части приведения логов к единому формату. Мы возьмём для наших экспериментов logstash.
По традиции запускать logstash мы хотим в контейнере. Официальный Docker-образ для logstash-а поставляется без исходных файлов, что на мой взгляд несколько странно (как заметил внимательный читатель grossws, ссылка на Dockerfile всё же присутствует). Второй по популярности и единственный, к слову, нашедшийся на github-e образ зачем-то запускает внутри себя и ElasticSearch и Kibana, что противоречит идее «один контейнер — один процесс». Там конечно есть возможность напередавать волшебную комбинацию флагов, но у меня он всё равно при старте лез брать какие-то ключи с сайта автора. На DockerHub-е было ещё с десяток контейнеров от неизвестных мне лиц, поэтому лучше соберём контейнер сами под наши нужды. Всё что нам понадобится — вот такой вот Dockerfile:
FROM dockerfile/java:oracle-java8
MAINTAINER aatarasoff@gmail.com
RUN echo 'deb http://packages.elasticsearch.org/logstash/1.5/debian stable main' | sudo tee /etc/apt/sources.list.d/logstash.list && \
apt-get -y update && \
apt-get -y --force-yes install logstash
EXPOSE 5959
VOLUME ["/opt/conf", "/opt/certs", "/opt/logs"]
ENTRYPOINT exec /opt/logstash/bin/logstash agent -f /opt/conf/logstash.conf
Образ будет очень простым и запустится только при наличии внешнего конфигурационного файла, что для наших развлекательных задач вполне себе норма. Соберём образ и зальём его на Docker Hub:
docker build -t aatarasoff/logstash .
docker push aatarasoff/logstash
Создадим конфигурационный файл /mnt/logstash/conf/logstash.conf со следующим содержимым:
input {
tcp {
type => "audit"
port => 5959
codec => json
}
}
output {
elasticsearch {
embedded => false
host => "10.211.55.8"
port => "9200"
protocol => "http"
}
}
type => «audit» сделает так, что все наши логи будут иметь общее значение в поле type, что позволит нам их отличать от других логов по этому дискриминатору. Остальные настройки довольно очевидны. Запустим свежеиспечённый контейнер:
docker run -d -p 5959:5959 -v /mnt/logstash/conf:/opt/conf \
--name logstash aatarasoff/logstash
и проверим, что логи будут писаться, если мы по tcp передадим json.
Реализация №2
Мы делаем уже второй модуль, поэтому стоит вынести реализацию в отдельный проект, который назовём auditor. Первым делом нам надо накрутить уже имеющееся «мясо» из регистратора. Поэтому берём наш форк и нагло копируем код себе в проект.
Проверяем, что всё у нас по-прежнему собирается, выполнив команду: make dev.
Замечаем, что в файле regitrator.go модуль bridge подключается как внешняя зависимость, поэтому можно смело удалять эту папку. Снова проверяем, что всё работает.
Изменяем Dockerfile.dev:
FROM gliderlabs/alpine:3.1
CMD ["/bin/auditor"]
ENV GOPATH /go
RUN apk-install go git mercurial
COPY . /go/src/github.com/aatarasoff/auditor
RUN cd /go/src/github.com/aatarasoff/auditor \
&& go get -v && go build -ldflags "-X main.Version dev" -o /bin/auditor
Аналогично меняем релизный Dockefile. Убираем лишние таски и меняем имя контейнера в Makefile:
NAME=auditor
VERSION=$(shell cat VERSION)
dev:
docker build -f Dockerfile.dev -t $(NAME):dev .
docker run --rm --net host \
-v /var/run/docker.sock:/tmp/docker.sock \
$(NAME):dev /bin/auditor elastic:
build:
mkdir -p build
docker build -t $(NAME):$(VERSION) .
docker save $(NAME):$(VERSION) | gzip -9 > build/$(NAME)_$(VERSION).tgz
Добавим новый модуль /logstash и файл logstash.go к нашему проекту. Возьмём готового клиента для logstash-а, который туп как пробка, и фактически является просто обёрткой над стандартной библиотекой net: github.com/heatxsink/go-logstash.
В этот раз структура контейнера будет несколько отличаться от предыдущего варианта:
type Container struct {
Name string `json:"container_name"`
Action string `json:"action"`
Service *bridge.Service `json:"info"`
}
Связано это с тем, что теперь нам нужно просто сериализовать объект в json и отправить его как строку в logstash, который сам разберётся со всеми полями в сообщении.
Также как и в прошлый раз регистрируем нашу фабрику:
func init() {
bridge.Register(new(Factory), "logstash")
}
И создаём новый экземпляр адаптера:
func (f *Factory) New(uri *url.URL) bridge.RegistryAdapter {
urls := "127.0.0.1:5959"
if uri.Host != "" {
urls = uri.Host
}
host, port, err := net.SplitHostPort(urls)
if err != nil {
log.Fatal("logstash: ", "split error")
}
intPort, _ := strconv.Atoi(port)
client := logstashapi.New(host, intPort, 5000)
return &LogstashAdapter{client: client}
}
type LogstashAdapter struct {
client *logstashapi.Logstash
}
Здесь нам пришлось использовать утильный метод net.SplitHostPort(urls), который умеет вычленять хост и порт из строки, потому что клиент принимает их раздельно, а приходят они вместе в uri.Host.
Числовое представление порта можно получить, применив метод конвертации строки в число: intPort, _ := strconv.Atoi(port). Знак подчёркивания нужен, потому что функция возвращает два параметра, второй из которых ошибка, которую мы можем не обрабатывать.
Реализация метода Ping получилась довольно простой:
func (r *LogstashAdapter) Ping() error {
_, err := r.client.Connect()
if err != nil {
return err
}
return nil
}
Фактически мы проверяем, что можем подключиться по tcp к logstash-у. В функции Connect повторное подключение произойдёт только если текущее уже не может быть использовано.
Осталось реализовать метод регистрации:
func (r *LogstashAdapter) Register(service *bridge.Service) error {
container := Container{Name: service.Name, Action: "start", Service: service}
asJson, err := json.Marshal(container)
if err != nil {
return err
}
_, err = r.client.Connect()
if err != nil {
return err
}
err = r.client.Writeln(string(asJson))
if err != nil {
return err
}
return nil
}
Думаю, что код достаточно понятен и не требует комментариев, кроме одного. Вызов Connect перед Writeln гарантирует, что будет получено рабочее соединение.
Метод Deregister полная копия метода выше.
Меняем в Dockerfile.dev в строке запуска elastic на logstash, запускаем и проверяем наличие записей в ElasticSearch:
curl 'http://localhost:9200/_search?pretty'
… счастьем поделись с другим
Коммитим наши изменения на GitHub и идём собирать образ для DockerHub-а. На hub.docker.com, заходим на свою страницу и жмем кнопку +Add Repository. Когда собирался образ для logstash-a, я выбрал подпункт Repository, который позволяет вручную заливать свои образы, но есть и другой путь — Automated Build. Нажав на него, Docker Hub предложит подключить к нему свой аккаунт на GitHub-е или BitBucket-е. После этого остаётся только выбрать свой репозиторий, нужную ветку, и изменить названия образа, если это очень нужно. Всё остальное, включая перенос описания из README.MD возьмёт на себя Docker Hub.
После небольшого ожидания вот он — готовый образ.
Теперь можно протестировать его выполнив простую команду:
docker run -d --net=host \
-v /var/run/docker.sock:/tmp/docker.sock \
--name auditor aatarasoff/auditor logstash://
PS. Проект не используется в продакшене, и с моей критичной точки зрения требует допила, но каждый прочитавший статью может его попробовать и, при желании, улучшить.