HTTPを使ったDocker Registryを作る

基本的にドキュメントに書いてありまが、Docker RegistryでHTTPを使おうとすると少し面倒だったので、まとめておきます。

プライベートなネットワークで使っているぶんにはHTTPで十分ですが、いくつか設定を追加する必要があります。

Docker Registryを起動する

Docker Registryを起動すにはDeploy a registry serverに書かれている通り、以下のコマンドを実行します。
Docker Registryはこれで終わりです。

$ docker run -d -p 5000:5000 --restart=always --name registry registry:2

Docker Registryに接続するクライアントの設定

こちらに書かれている通り以下のようにDockerの設定を追加します。

Docker Registry が10.0.0.2:5000でLISTENしている例です。

/etc/docker/daemon.json

{ "insecure-registries":["10.0.0.2:5000"] }

/etc/default/docker

DOCKER_OPTS="--insecure-registry 10.0.0.2:5000"

設定はこの2つです。

あとはDocker daemonをrestartします。

# systemdの場合
$ sudo systemctl restart docker.service

これでHTTPで接続できるようになります。

設定は簡単ですが、公式ドキュメントから設定方法を見つけられなかったので、設定に少し時間がかかりました。

Docker Swarm内の分散したログをFluentdでまとめてElasticsearchとKibanaでログを確認する

概要

Docker Swarmで複数のコンテナがあると、コンテナが起動している複数のnodeに入ってdocker logsは大変です。2台や3台程度であればdocker logsで十分かもしれませんが、5台やそれ以上になると大変です。
また、一番大きい問題としてコンテナのログはapplication imageを更新するたびに揮発しまうことがあります。

揮発対策のためにtaskを実行しているホストマシン上にファイル出力する方法もありますが、分散したnodeに入ってログを確認しなければいけない問題が解決しません。
そのため、Docker Swarmで複数のコンテナのログを一箇所に収集するようにします。

また一箇所に集めたログをlessなどで見てもいいですが、APIアクセスやエラーの統計を取れるようにしたいので、ElasticsearchとKibanaを使用します。

全体の流れは以下のようになります。

Docker container -> Fluentd -> Elasticsearch -> Kibana

  1. Docker containerからFluentdにログを送信
  2. FluentdでElasticsearchにログを送信するようにする。ここでElasticsearchのフォーマットに合わせてログを変換
  3. Kibanaでログを確認

Docker containerからFluentdにログを送信する

Dockerサービスを作成時に--log-driver=fluentdでFluentdに出力するように指定します。Dockerは標準でFluentdをサポートしています。次に--log-opt fluentd-address=127.0.0.1:24224で送信先とポートを指定します。--log-opt tag='developer_center.clientでFluentdのタグを指定します。

docker service create --replicas 1 --name api --log-driver=fluentd --log-opt fluentd-address=127.0.0.1:24224 --log-opt tag='developer_center.api' api

FluentdからElasticsearchにログを加工して送信する

Fluentdは0.12を使用しています。

NginxのログをElasticsearchに送信してみます。

Docker containerから送信されたログは以下のようにJSON形式になっています。

2017-07-19 11:54:27,165 [application-package-actor-7] INFO  application - make directories: mkdir -p /myapp/example/124223/50001

以下はFluentdでアプリケーションのログをパースしてElasticsearchに送信する設定です。

<filter application>
  @type parser
  format /^(?<time>.*) \[(?<thread>.*)\] (?<level>[\w]*)\s+(?<class>[\w.]*) - (?<message>.*)|\s*(?<system_message>.*)$/
  time_format %Y-%m-%d %H:%M:%S,%L
  key_name log
  keep_time_key true
  reserve_data true
</filter>

<filter application>
  @type record_transformer
  enable_ruby true
  <record>
    timestamp ${ require 'time'; Time.now.utc.iso8601(3) }
  </record>
</filter>

<match application>
  @type copy
  <store>
    @type elasticsearch
    host sample-s18kfo2pa387njeiowbf9km4oe38by.ap-northeast-1.es.amazonaws.com
    port 80
    logstash_format true
    logstash_prefix application
    time_key timestamp
    buffer_type file
    buffer_path /dev/shm/fluentd_buffer/application.buf
    buffer_queue_limit 128
    buffer_chunk_limit 32m
    flush_interval 1s
  </store>
  <store>
    @type file_alternative
    path /home/fluentd/logs/application.log
    time_slice_format %Y%m%d%H00
    time_format %Y-%m-%dT%H:%M:%SZ
    buffer_type file
    buffer_path /dev/shm/fluentd_buffer/application.buf
    buffer_queue_limit 64
    buffer_chunk_limit 32m
    flush_interval 1s
    flush_at_shutdown
    add_newline true
  </store>
</match>

最初のfilterではアプリケーションのログをパースします。formatはパースするフォーマットです。ここではアプリケーションのログフォーマットに合わせています。time_formatも同じように時刻のフォーマットを合わせています。key_name logはどのJSONのkeyをパース対象にするか指定します。ここでは log を対象にしています。reserve_data truekey_name logで指定した元のログをパース後も保持するか指定します。 false にするとkeyが log のレコードが削除されます。

2番目のfilterではパースしたレコードにElasticsearchで使用するタイムスタンプを追加しています。タイムスタンプはすでにあるのですが、Fluentd 0.12はミリ秒に対応していないので、ここでrubyのTimeを使ってログを処理した時刻にミリ秒を含めたタイプムスタンプをレコードに追加しています。

matchではElasticsearchに送信する設定とローカルディレクトリにログを保存する設定です。

1番目の<store>はElasticsearchに送信する設定です。ここでElasticsearchに送信するための設定としてhostport指定しています。次にlogstash_formatlogstash_prefixを指定しています。logstash_formatはKibanaで加工しやすいようにインデックスを付けます。logstash_prefixではインデックスのprefixを指定します。Kibanaでインデックスを指定してログを見ることになるのでわかりやすい名前をつけます。上記の設定ではclient-2017.01.01のようなインデックスが作成されます。また、time_key timestampfilterで追加したタイムスタンプを使用するようにkeyを指定しています。

2番目の<store>はローカルディレクトリにログを保存する設定です。
詳しい設定はこちらを参照してください。

ElasticsearchとKibanaを準備する

ElasticsearchとKibanaを使用するにはAWSで提供されているAmazon Elasticsearch Serviceを使用しています。インスタンスを立ち上げるだけで使用できるようになるのでとても簡単です。もちろん、Elasticsearchにログを流す前にインスタンスを立ち上げておく必要があります。

Kibanaでログを確認する

Kibana上で、Management -> Index Patterns -> [Add New]でindex patternを指定します。

Index name or patternにはlogstash_prefixで指定した値を入力します。
正しくログが入っているとTime-field name@taimestampを選択できるようになるので、選択して[Create]します。

あとは、Discover から先ほど登録したindex patternを指定すればログを確認できるようになります。

課題

複数行になるログ(スタックトレースなど)をKibanaで見るとタイムスタンプが同じになってしまい、本来の行とは異なった並びになってしまうことがあります。複数行のログをうまく一つにまとめられるとログが見やすくなると思います。何か方法はないかな…

Dockerとnet.ipv4.conf.all.forwarding

Docker containerから外部へのアクセスができなくなった時の原因と直し方です。

何が起こったか

centosをリスタートかsystemctl restart network.serviceすると net.ipv4.conf.all.forwarding0 になってしまいます。

$ sysctl net.ipv4.conf.all.forwarding
net.ipv4.conf.all.forwarding = 0

net.ipv4.conf.all.forwarding が0になってしまうとDockerコンテナと外のネットワークのパケットのforwardingができなくなり、外部とのネットワークが繋がらない状態になります。
ドキュメントにも書かれています。

これはDocker imageをbuildするときも同じです。

CIでこれをやってしまい、Docker imageのbuild時にyum installしている部分でネットワークに接続されず、かつ必要なパッケージがインストールされずにDocker imageがbuildされてしまいました。
ログを見ると warning は出ているのですが error ではないためbuildは継続され、正常にDocker imageが作成されます。アプリケーションを実行すると実行に必要なパッケージがインストールされていないため、アプリケーション側でエラーが出ます。

下記はnet.ipv4.conf.all.forwarding が0の状態でDocker buildをした出力の一部です。

3行目で---> [Warning] IPv4 forwarding is disabled. Networking will not work.が出力されていることから、IPv4 forwardingが無効になっていることがわかります。また、 Removing intermediate container c0b08184ced5でもわかるようにbuildは続行されています。

ネットワークエラーでパッケージはインストールされないがDocker buildは続行される

Step 3/5 : RUN yum -y update; yum -y install rsync; yum clean all; yum install -y openssh-clients
---> [Warning] IPv4 forwarding is disabled. Networking will not work.
---> Running in c0b08184ced5
Loaded plugins: fastestmirror, ovl

One of the configured repositories failed (Unknown),
and yum doesn't have enough cached data to continue. At this point the only
safe thing yum can do is fail. There are a few ways to work "fix" this:

1. Contact the upstream for the repository and get them to fix the problem.

2. Reconfigure the baseurl/etc. for the repository, to point to a working
upstream. This is most often useful if you are using a newer
distribution release than is supported by the repository (and the
packages for the previous distribution release still work).

3. Run the command with the repository temporarily disabled
yum --disablerepo= ...

4. Disable the repository permanently, so yum won't use it by default. Yum
will then just ignore the repository until you permanently enable it
again or use --enablerepo for temporary usage:

yum-config-manager --disable
or
subscription-manager repos --disable=

5. Configure the failing repository to be skipped, if it is unavailable.
Note that yum will try to contact the repo. when it runs most commands,
so will have to try and fail each time (and thus. yum will be be much
slower). If it is a very temporary problem though, this is often a nice
compromise:

yum-config-manager --save --setopt=.skip_if_unavailable=true

Cannot find a valid baseurl for repo: base/7/x86_64
Could not retrieve mirrorlist http://mirrorlist.centos.org/?release=7&arch=x86_64&repo=os&infra=container error was
12: Timeout on http://mirrorlist.centos.org/?release=7&arch=x86_64&repo=os&infra=container: (28, 'Resolving timed out after 30544 milliseconds')
Loaded plugins: fastestmirror, ovl

---> d6d1bd8a8271
Removing intermediate container c0b08184ced5

原因

これが発生する原因としては2つあります。

network serviceのscript

起動時のスクリプトでnet.ipv4.ip_forward=0を実行しています。

/etc/init.d/network

...
stop)
sysctl -w net.ipv4.ip_forward=0 > /dev/null 2>&1
...

networkをリスタートした時sysctl -w net.ipv4.ip_forward=0が実行されます。

動作の確認

$ sudo sysctl net.ipv4.conf.all.forwarding=1
net.ipv4.conf.all.forwarding = 1
$ sudo systemctl restart network.service
$ sysctl net.ipv4.conf.all.forwarding
net.ipv4.conf.all.forwarding = 0

/etc/sysctl.conf

/etc/sysctl.confnet.ipv4.ip_forward=0を設定している場合でも0になってしまいます。

/etc/sysctl.conf

net.ipv4.ip_forward=0

動作の確認

$ sudo sysctl net.ipv4.conf.all.forwarding=1
net.ipv4.conf.all.forwarding = 1
$ sudo systemctl restart network.service
$ sysctl net.ipv4.conf.all.forwarding
net.ipv4.conf.all.forwarding = 0

対策

/etc/sysctl.confnet.ipv4.ip_forward=1を設定します。

/etc/sysctl.conf

net.ipv4.ip_forward=1

Docker buildでも問題なくビルドされるので、少し気づきにくい不具合でした。
ホストOSをリスタートしたりnetwork serviceをリスタートするときは設定を確認すると良いかもしれません。