in docker golang ~ read.

Аудит Docker-контейнеров: logstash наносит ответный удар

В предыдущем посте была расширена функциональность проекта registartor, в который был добавлен модуль для сохранения событий запуска/остановки контейнеров в elasticsearch. В этот раз мы попробуем использовать для обработки логов logstash, сделаем отдельный проект на github-е, а также выложим собранный образ на docker hub.

Copy-Paste-Replace

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

Проверяем, что всё у нас по прежнему собирается, выполнив команду: make dev.

Замечаем, что в файле regitrator.go модуль bridge подключается как внешняя зависимость с github-а, поэтому можно смело удалять эту папку. Снова проверяем, что всё работает.

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

go get [-d] [-f] [-fix] [-t] [-u] [build flags] [packages]  

не сразу найдёшь [build flags], который указывает на возможность указания флагов из описания команды build, в которой таки присутствует флаг -v, позволящий увидеть желаемое. Понимание происходящего после этого пришло, но сборка dev-контейнера быстрее не стала.

Изменяем 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

В modules.go оставляем только наш модуль:

package main

import (  
    _ "github.com/aatarasoff/auditor/elastic"
)

Теперь у нас есть отдельный проект, который умеет логировать нужную нам информацию в ES.

В бар врывается logstash

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

По традиции запускать logstash мы хотим в контейнере. Официальный docker-образ для logstash-а поставляется без исходных файлов, что на мой взгляд несколько странно. Второй по популярности и единственный, к слову, нашедшийся на 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  

По ссылке можно посмотреть, что образ действительно добавился: https://registry.hub.docker.com/u/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

Добавим новый модуль logstash и файл logstash.go к нашему проекту. Возьмём готового клиента для logstash-а, который туп как пробка, и фактически является просто обёрткой над стандартной библиотекой net: https://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, запускаем и проверяем наличие записей в ES:

curl 'http://localhost:9200/_search?pretty'  

...счастьем поделись с другим

Коммитим наши изменения на github и идём собирать образ для DockerHub-а. Идём на https://hub.docker.com/, заходим на свою страницу и жмем кнопку +Add Repository. Когда собирался образ для logstash-a, я выбрал подпункт Repository, который позволяет вручную заливать свои образы, но есть и другой путь - Automated Build. Нажав на него, Docker Hub предложит вам подключить к нему свой аккаунт на GitHub-е или BitBucket-е. После этого остаётся только выбрать свой репозиторий, нужную ветку и изменить названия образа, если это очень нужно. Всё остальное, включая перенос описания из README.MD возьмёт на себя Docker Hub.

Пришлось немного подождать и вот он - готовый образ: https://registry.hub.docker.com/u/aatarasoff/auditor/.

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

docker run -d --net=host \  
    -v /var/run/docker.sock:/tmp/docker.sock \
    --name auditor aatarasoff/auditor logstash://
comments powered by Disqus