GoでのDockerコンテナーの起動の監査を書いたように

ユニバーサルコンテナ化が世界を支配します。 この流行は私を回避しませんでした、そして今、過去6ヶ月間、私は一般的に流行語DevOpsと呼ばれるものをやっています。 私が行うプロジェクトでは、 Dockerを使用することを決定しました。これにより、アプリケーションをわいせつに展開するプロセスが単純になり、文字どおり、今日の流行に劣らない別のトレンド、つまりマイクロサービスアーキテクチャに追随することになります。 ある時点で、完全に危険な生息地で生と死の統計を収集することは良いことだと気づきます。 おまけとして、仕事で使用するツールを学び、メインプログラミング言語ではなく何かを書いて、オプションであるが便利なことをしてください。



この記事では、コンテナのライフサイクル統計を監査および収集するためのプロジェクトが3晩と1晩でどのように開発されたかを説明します。



前半



Googleで簡単に検索しても、既成のソリューションを見つけることはできなかったので、自分で解決します。

必要なもの:



最初のタスクは、 登録者によって解決されます。 これは、GliderLabsのソリューションです。ConsulやNetflix Eurikaなどの構成ストレージシステムにコンテナーを自動的に登録できます。 残念ながら、後者は完全に異なるタスクのために投獄されています:現在利用可能なサービスとそれらを実装するコンテナはどこにあるかを言うことです。



各イベント(コンテナの起動または停止)を必要なすべての操作を実行できる特定のログのレコードと見なす場合、ElasticSearchを使用してこれらのレコードを保存し、Kibanaをリアルタイムで表示および分析できます。



2番目のポイントを解決すること、つまり、レジストラとエラスティックの間にバンドルを作成することは残ります。



レジストラはどうですか



エンターテインメントはフォークで始まるため、リポジトリのGitHubのボタン(https://github.com/gliderlabs/registrator)をクリックしてください。 ローカルマシンでクローンを作成し、内容を確認します。



registrator.go //     modules.go //    (consul, etcd  ..) Dockerfile //   docker- Dockerfile.dev //    dev-  /bridge //     /consul //     consul
      
      





スキームは単純です。 registrator.goでは、ソケットをリッスンするDockerクライアントが作成され、イベントが発生すると(コンテナーの開始、停止、または終了)、コンテナーの識別子とそれに関連付けられたイベントをブリッジに渡します。 アダプタ(モジュール)は、アプリケーションの起動時に指定されたブリッジ内に作成され、コンテナに関する詳細情報は、後続の処理のために既に送信されています。 したがって、ElasticSearchにデータを送信する新しいモジュールを追加するだけで十分です。



開発する



コードを書く前に、プロジェクトをビルドして実行してみましょう。 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コンテナーに配置します。



 $ 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つのコンテナがありました。 そのうちの1つにはポートがありませんでしたが、もう1つにはポート443が公開されていませんでした。 サービスが実際に追加されたことを確認するには、digユーティリティを使用できます



 dig @localhost nginx-80.service.consul
      
      





nginxはいくつかのポートを公開するため、コンテナ名に-80を追加する必要があり、Consulの観点からは、これらは異なるサービスです。



そのため、レジストラを立ち上げました。つまり、コードの記述を開始するときです。



行け行け



さまざまなバックエンド用のプロジェクトのアダプターは、個別のモジュールとして実装されます。 一般に、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 }
      
      





登録解除機能は、前の機能を完全に繰り返しますが、異なるアクションが必要です。



Makefileでconsulをelasticに変更し、modules.goにモジュールを登録します。



すべて一緒に今



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では、新しいコンテナインデックスを設定する必要があります。その後、nginxの実行記録を確認できます。



最終的な実装のファイルはここにあります



Logstashがバーに突入



誰もが私たちのソリューションに満足していますが、彼の仕事のために、別の自己記述インデックスを保持する必要があり、それでも正しいテンプレートをマッピングでロールすることを忘れないでください。 人々がそのような質問に煩わされないように、膨大な数のソースから情報を収集する方法を知っているだけでなく、ログを単一の形式にするという点ですべての汚い仕事をするログアグリゲーターがあります。 実験にはlogstashを使用します。



伝統的に、コンテナでlogstashを実行します。 logstashの公式Dockerイメージにはソースファイルが含まれていませんが、これは私の意見では多少奇妙です( 注意深い読者が指摘しているように、 Dockerfileへのリンクはまだ存在しています )。 ちなみにgithub-eで見つかった2番目に人気のある唯一のイメージは、何らかの理由でElasticSearchとKibanaの両方を起動します。これは、「1つのコンテナー-1つのプロセス」という概念に矛盾します。 もちろん、魔法のようなフラグの組み合わせを伝える機会がありますが、最初は著者のサイトからいくつかのキーを取得する必要がありました。 DockerHubには見知らぬ人のコンテナが約12個あったので、ニーズに合わせてコンテナを組み立てる方が良いでしょう。 必要なのは、この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
      
      





jcpをtcp経由で渡した場合にログが書き込まれることを確認します。



実装番号2



すでに2番目のモジュールを実行しているので、実装を別のプロジェクトに入れる価値があります。これをAuditorと呼びます。 まず、レジストラの既存の「肉」を仕上げる必要があります。 したがって、フォークを取り、コードをプロジェクトに素直にコピーします。



コマンドmake devを実行して、すべてがまだ進行していることを確認します。



regitrator.goファイルでは、ブリッジモジュールが外部依存関係として接続されているため、このフォルダーを安全に削除できます。 繰り返しますが、すべてが機能することを確認します。



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の既製のクライアントを使用します。これはコルクのように愚かで、実際には標準のネットライブラリ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)を適用することにより取得できます。 この関数は2つのパラメーターを返すため、アンダースコアが必要です。2番目のパラメーターは処理できないエラーです。



Pingメソッドの実装は非常に簡単であることがわかりました。



 func (r *LogstashAdapter) Ping() error { _, err := r.client.Connect() if err != nil { return err } return nil }
      
      





実際、tcpを介してlogstashに接続できることを確認します。 接続機能では、現在の接続が使用できなくなった場合にのみ再接続が発生します。



登録方法を実装するために残ります:



 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 }
      
      





コードは十分に明確であり、コメントを必要としないものは1つを除きます。 Writelnの前にConnectを呼び出すと、動作中の接続が受信されます。



Deregisterメソッドは、上記のメソッドの完全なコピーです。



エラスティックローンチラインのDockerfile.devを変更して、logstashを開始し、ElasticSearchのエントリを確認します。



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







...自分の幸せを他の人と共有する



GitHubで変更をコミットし、DockerHubのイメージを収集します。 hub.docker.comで 、ページに移動して[+リポジトリを追加]ボタンをクリックします。 logstash-aのイメージが収集されたときに、リポジトリサブアイテムを選択しました。これにより、イメージを手動で入力できますが、別の方法-自動ビルドがあります。 クリックすると、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。 プロジェクトは本番環境では使用されず、私の重要な観点からは終了する必要がありますが、記事を読んだ人は誰でもそれを試して、必要に応じて改善できます。



All Articles