DockerでNginx+Rails6+MySQL+Fluentd+Elasticsearch+Kibanaを構築する

はじめに

Dockerはコンテナ仮想化技術のデファクトスタンダードとなっている。
そんなDockerをやるやる詐欺を繰り返し未だ初歩的な知識も欠如している筆者が、夏休みの宿題も兼ねてDockerを使えるようになろうというのが目的。
タイトルがやたら盛りだくさんだが、同じくやるやる詐欺を繰り返していた者達なので個々の詳しいことはこれから学ぶ予定。

とりあえず、Dockerを用いて環境を構築することが今回のゴール。

Docker?

Dockerを勉強とすると言っても何から始めればいいのか?
筆者も苦労したが基本的な概念とイメージを掴むにはKatacodaというeラーニングサイトで学ぶのが良いと感じた。

KatacodaではWebブラウザ上でDockerを直接操作しながら学習することができる。
実際にコマンドを叩いてどういう出力がでて、結果どうなるのかという流れを短時間で準備なく試せるのは非常に快適。


基本的にはKatadcodaのDockerをコースを受講していればよいが、すでに非推奨となったlinkオプションを用いたコースなどもそのまま存在するので公式ドキュメント等で情報を補足しながら利用するのが良い。

幸いドキュメント日本語化プロジェクトが存在しており日本語で読むことができるので英語が辛くても安心。

そして、概念を掴んだ上でここを理解しておけば大きな誤解なく進められるんじゃないか?と思ったのが下記の項目

  • イメージとコンテナの違いを理解すること
    • これがはっきりしていないと後に出てくるDockerfileとdocker-compose.ymlを用いる際わけがわからなくなってしまった。
    • イメージに対しての操作なのか、コンテナに対しての操作なのかを常に意識すれば理解しやすくなった。
  • Dockerはプロセスをフォアグラウンドで動作させないとコンテナが停止してしまう。
    • 当たり前のようにNginx起動のコマンドに"daemon off"がついているけどそういう理由
    • Nginx以外でも同様の対策が必要。
  • Data Volumeについて大まかに理解をしておくことが大事。
  • docker-composeに頼る。
    • ymlを書くことで設定が可視化でき、Data Volumeなどもいい感じにしてくれるので概念さえつかめたらあとはdocker-composeでやったほうが圧倒的に楽だった。
  • DockerHubに書いてあるOSのディストリビューションを理解しておく。
    • Dockerイメージの容量を小さくする為に用いられることが多い「Alpine Linux」
      • alpineと書いてあったらOSはこれ。パッケージ管理は「apk」シェルは「ash」なので注意
    • Debianと書いてなくても「buster」って書いてあればDebian
      • Debian 10のコードネーム。知らなかったので何者だ?と初見で思った。

とりあえず以上。これがわかったのでだいたい大丈夫なんじゃない?と一安心してる感じ。

構築

ようやく本題。これから構築の話を書いていくけど基本的な思想としてはなるべく自分でDockerfileは書きたくなくてDockerHubのOfficialイメージを使っていい感じに構築したいという感じ。

構築したいもの

docker-composeを用いてRailsアプリケーションがMySQLに接続しつつ、Nginxをリバースプロキシとして利用する。
アプリケーションサーバはpuma。
NginxのログファイルをFluentdでElasticsearchに送りKibanaで見ることができるもの。

各バージョン

  • Docker 19.03.1
  • docker-compose 1.24.1
  • MySQL 8.0.17
  • Ruby 2.6.3
  • Rails 6.0.0
  • Nginx 1.17.2
  • Elasticsearch 7.3.0
  • Kibana 7.3.0
  • Fluentd 1.6.3

ディストリビューションは選択肢があればDebianを選んでる。よくわからない事が起きたときコンテナに入って調べるときAlpineだときつかったので。

$ docker-compose run name bash

上記でnameのコンテナにbashで入れるから困ったらこれしてる。

MySQL

基本的にはDockerHubに記載されているdocker-composeを利用するが2箇所変更した。

参考
  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
https://hub.docker.com/_/mysql
docker-compose.yml
version: '3.7'

services:
  db:
    image: mysql:8.0.17
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    volumes:
      - db-data:/var/lib/mysql
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
    ports:
      - 3306:3306

volumes:
  db-data:
変更点
  1. rootのパスワード
    • 開発用ということもありrootのパスワードはなしにしたかったためMYSQL_ALLOW_EMPTY_PASSWORDを設定している。
    • Environment Variablesの説明はDockerHubにあるので用途に応じて設定してほしい。
  2. ポートフォワーディング
    • 加えてホストからアクセスするためにports設定を足した。ホストにMySQLは起動していないので3306同士。
    • ホストにMySQLが起動している場合はホスト側のポートを変えれば両立可能(例:6666:3306)
  3. データボリューム
    • トップレベルにdb-dataという名前付きのボリュームを定義している。
    • オプションが色々設定可能だが、オプションなしだとボリュームが存在しない場合docker-composeが名前付きのボリュームを作成してくれる。
    • これでMySQLのデータの永続化を実現している。
注意点
  1. https://qiita.com/Lio/items/3127685e26081993b51e
    • portsの設定をしているのでホストの3306ポートからアクセスしようとlocalhostでアクセスしようとするとホストで起動してるMySQLと接続しようとしてつながらない罠。
    • hオプションの値を127.0.0.1にすると接続できる。
  2. MySQL8でデフォルトの認証形式になったcaching_sha2_password
    • docker-compose.ymlのcommandを見てもらえばわかるが、起動オプションにdefault-authentication-plugin=mysql_native_passwordが指定されている。
    • これはMySQL8からデフォルトの認証プラグインが変わったためcaching_sha2_passwordにクライアントが対応していないとエラーになるため。
    • caching_sha2_passwordに対応していく必要はあるが、今回の趣旨ではないので従来のmysql_native_passwordにしておいた。

Rails

Docker本家にドキュメントがありそれを参考にRails6 betaを導入する記事を参考させてもらった。
Rails6になり必要になったものなどをインストールする必要があったためDockerfileでイメージをビルドすることに。

参考

手順に則りDockerfile,entrypoint.sh,Gemfile,Gemfile.lockを作成。Gemfile.lockは空ファイルで良い。

Dockerfile
FROM ruby:2.6.3-buster
# nodejs
# https://github.com/nodesource/distributions/blob/master/README.md
RUN curl -SL https://deb.nodesource.com/setup_12.x | bash
# yarn
# https://yarnpkg.com/lang/en/docs/install/#debian-stable
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list

RUN apt-get update -qq && apt-get install -y nodejs yarn

RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

Node.jsとyarnはDockerfile内コメントにあるURLから最新版ぽいバージョンを選択している。

entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
https://docs.docker.com/compose/rails/

そのままの為引用。pidファイルが残っていて起動できないケースを防ぐ為。

Gemfile
source 'https://rubygems.org'
gem 'rails', '6.0.0'
docker-compose.yml

追記部分以外は省略

services:
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/myapp
    ports:
      - 3000:3000
    depends_on:
      - db

rails newをする。

$ docker-compose run web rails new . --force --no-deps --database=mysql

WORKDIRを/myappに設定しているためこの指定で/myapp配下にrails newされる。
そして/myappはホストのカレントディレクトリをbindしてるためホストのカレントディレクトリにもrails newしたファイルが存在する。

Macで作業しているためファイルの所有権は問題ないのでスキップ。

Gemfileが更新された(rails newによって)のでbuildする。Gemfileが更新されたらbuildする必要があるようだ。

$ docker-compose build

database.ymlのhostをdocker-compose.ymlで指定しているサービス名に変更する。
今回の場合はdbなので、host: dbとすればよい。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: db

ようやくupできる。

$ docker-compose up

portsで設定しているのでhttp://localhost:3000でアクセスできるはず。
アクセスするとDBがないと怒られるのでdbを作成する。

$ docker-compose run web rake db:create

これでYay! You’re on Rails!に到達できる。

Nginx

次にpumaのリバースプロキシにNginxを使うための設定を行う。

まず、pumaの設定。unixソケット通信に変更してNginxと連携できるようにする。

config/puma.rb
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -10,7 +10,10 @@ threads min_threads_count, max_threads_count
 
 # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
 #
-port        ENV.fetch("PORT") { 3000 }
+#port        ENV.fetch("PORT") { 3000 }
+
+# unix socket
+bind "unix://#{Rails.root}/tmp/sockets/puma.sock"
 
 # Specifies the `environment` that Puma will run in.
 #
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,13 +13,23 @@ services:
       - 3306:3306
   web:
     build: .
-    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
+    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s"

pやbのオプションがついているとunixソケットを使ってくれないのでこちらも修正。

nginx.confも作る。適切なディレクトリ構成とかは一旦置いといてカレントに作成。

upstream myapp {
    server unix:///myapp/tmp/sockets/puma.sock;
}
server {
  listen 80;
  server_name localhost;

  access_log /var/log/nginx/myapp_access.log;
  error_log  /var/log/nginx/myapp_error.log;

  root /myapp/public;

  try_files  $uri/index.html $uri @myapp;

  location @myapp {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://myapp;
  }
}
docker-compose.yml

追記部分以外は省略

services:
  proxy:
    image: nginx:latest
    command: "nginx -g 'daemon off;'"
    volumes:
      - .:/myapp
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - 80:80
    depends_on:
      - web

unixソケットを利用するのでsockファイルを参照できるようにカレントディレクトリをbind。
nginxデフォルトのconfファイルはひとまず必要ないので上書きしてしまう。

これでhttp://localhostにアクセスするとYay! You’re on Rails!に到達できる。

注意点
  1. Dockerコンテナ上で、ログファイルにしかログを書かないソフトウェアに標準出力・標準エラー出力を使わせるようにする - CLOVER🍀
    • 関連: View logs for a container or service | Docker Documentation
    • docker container logsやdocker-compose upをフォアグラウンドで実行していると見ることのできるログに、各コンテナで起きたログを表示するには標準出力、標準エラー出力に内容を書く必要がある。
    • Nginxはアクセスログをログファイルに書きつつ標準出力に書き出す方法がないので、アクセスログを標準出力にシンボリックリンクすることで実現している。
    • そのため/var/log/nginx/access.logにいくらログを書いてもコンテナ内にログは残らなくなっている。
# ls -l /var/log/nginx/
total 0
lrwxrwxrwx 1 root root 11 Aug 15 21:22 access.log -> /dev/stdout
lrwxrwxrwx 1 root root 11 Aug 15 21:22 error.log -> /dev/stderr

今回はログファイルに書き込み、Fluentdで扱いたかったのでファイル名を変えてログファイルを残すことにした。(myapp_access.log)

Elasticsearch+Kibana

docker-compose.yml
  elasticsearch:
    image: elasticsearch:7.3.0
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
    volumes:
      - elastic-data:/usr/share/elasticsearch/data
  kibana:
    image: kibana:7.3.0
    ports:
      - 5601:5601
    depends_on:
      - elasticsearch
volumes:
  elastic-data:

DockerHubの公式イメージからコンテナを立ち上げるだけ。
データ永続化にはトップレベルにelastic-dataというボリュームを作成して実現。

http://localhost:5601/にアクセスするとKibanaにアクセスできる。データはまだ無い。

Fluentd

ちょっとスマートなやり方ができなかったのでもう少しなんとかしたい課題あり。

  • gemインストールするためだけにDockerfileを書きたくない。
  • 今回の要件はただKibanaでみたいだけなので、ログファイルを残す必要が特に無い。そのためアクセスログを直接Fluentdに渡したかった。
    • Dockerには直接関係ないので今回はやらない。
参考

Fluentdの公式イメージはDockerHubにあるが、Elasticsearchを使用するためにはgemをインストールする必要があるのでFluentd用にDockerfileを作る。

fluentd/Dockerfile
FROM fluent/fluentd:edge-debian
USER root
RUN gem install fluent-plugin-elasticsearch

設定ファイルも同ディレクトリに作る。

fluentd/fluent.conf
<source>
  @type tail
  format ltsv
  path /var/log/nginx/myapp_access.log
  tag nginx
  pos_file /var/log/nginx/myapp_access.log.pos
</source>

<match nginx>
  @type elasticsearch
  host elasticsearch
  buffer_type memory
  port 9200
  index_name fluentd
  type_name nginx
  logstash_format true
  logstash_prefix nginx.access
</match>

細かい意味はわかってない...

docker-compose.yml
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -26,6 +26,7 @@ services:
     volumes:
       - .:/myapp
       - ./nginx.conf:/etc/nginx/conf.d/default.conf
+      - http-log:/var/log/nginx
     ports:
       - 80:80
     depends_on:
@@ -44,6 +45,16 @@ services:
       - 5601:5601
     depends_on:
       - elasticsearch
+  fluentd:
+    build: ./fluentd
+    volumes:
+      - http-log:/var/log/nginx
+      - ./fluentd/fluent.conf:/fluentd/etc/fluent.conf
+    ports:
+      - 24224:24224
+    depends_on:
+      - elasticsearch
 volumes:
   db-data:
   elastic-data:
+  http-log:

nginxのログをfluentdで扱いたいのでログディレクトリをデータボリュームに。

KibanaでNginxのログファイルを見る

  1. http://localhost:5601でKibanaにアクセスする
  2. Management>Index patterns>Create index patternを選ぶ
    • f:id:arcright:20190818224339p:plain
    • f:id:arcright:20190818224504p:plain
  3. index patternを作成する
    • f:id:arcright:20190818224706p:plain
    • 時系列で見たいので@timestampを指定する(?)
    • f:id:arcright:20190818224955p:plain
  4. Discoverに行くとそれっぽいグラフが表示される。
    • f:id:arcright:20190818232812p:plain

最終的なdocker-compose.yml

version: '3.7'

services:
  db:
    image: mysql:8.0.17
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    volumes:
      - db-data:/var/lib/mysql
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
    ports:
      - 3306:3306
  web:
    build: .
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s"
    volumes:
      - .:/myapp
    ports:
      - 3000:3000
    depends_on:
      - db
  proxy:
    image: nginx:latest
    command: "nginx -g 'daemon off;'"
    volumes:
      - .:/myapp
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - http-log:/var/log/nginx
    ports:
      - 80:80
    depends_on:
      - web
  elasticsearch:
    image: elasticsearch:7.3.0
    environment:
      - discovery.type=single-node
    ports:
      - 9200:9200
    volumes:
      - elastic-data:/usr/share/elasticsearch/data
  kibana:
    image: kibana:7.3.0
    ports:
      - 5601:5601
    depends_on:
      - elasticsearch
  fluentd:
    build: ./fluentd
    volumes:
      - http-log:/var/log/nginx
      - ./fluentd/fluent.conf:/fluentd/etc/fluent.conf
    ports:
      - 24224:24224
    depends_on:
      - elasticsearch
volumes:
  db-data:
  elastic-data:
  http-log:

おわりに

開発環境でdocker-composeを使った開発はできるようになったんじゃないかという感じだが、まだまだ解決しなければいけない課題はたくさんありそう。

  1. RailsアプリーケーションログもKibanaでみたい。
  2. 適切なディレクトリ構成やどのようにGit管理すべきか。
  3. docker-composeを作成者以外に利用して貰う場合にはどうするのがいいか
  4. Dockerfileでやるかdocker-compose.ymlでやるか

などなど。だが、ひとまず一定の成果が得られてよかった。
FluentdやElasticsearchについてほぼ運用的な知識がないので、環境も作れるようになったことだし色々と調べないとね。

Docker

Docker

Amazon