in docker golang ~ read.

Аудит запуска docker-контейнеров на Go

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

Первая половина дела

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

  • Мониторить запуск и остановку отдельного взятого контейнера
  • Отправлять сообщения о событии в некий storage
  • Иметь удобный инструмент для просмотра событий и их последующего анализа

Первую задачу решает registrator. Это прекрасное решение от ребят из GliderLabs, которое позволяет автоматически регистрировать контейнеры в Service Registry решения, такие как consul или Netflix Eurika. К сожалению, эти решения заточены под совсем другую задачу: сказать какие сервисы сейчас доступны, и где расположены контейнеры, которые их реализуют. Нам нужен другой тип storage-а.

Если рассмотреть каждое событие (запуск или смерть контейнера) как запись некоего лога, с которым мы можем делать всё что нам нужно. Для хранения этих записей возьмем 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-а создаётся адаптер (модуль), который был указан при запуске приложения, в который уже передаётся детальная информация о контейнере для её последующей обработки. Таким образом достаточно добавить новый модуль, который будет пересылать данные в ES.

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"
)

Обратите внимание, что пакет bridge ведёт сразу на github, а не в локальную папку. Для работы с elasticsearch воспользуемся уже разработанной библиотекой.

Чтобы обрабатывать события, нужно реализовать интерфейс

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() проверяем, что экземпляр elasticsearch всё ещё жив

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  

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

PS. Файл с реализацией лежит тут: https://github.com/aatarasoff/registrator/blob/master/elastic/elastic.go

comments powered by Disqus