初心者がAWS Linux環境にdocker-composeでELK環境を構築してみた

あんまりAWSやDockerを触ってこなかったので、自身の勉強も兼ねてやってみました。
楽勝かと思いましたが意外とハマったので、備忘録的にまとめてます。
※AWSの設定やDocker環境構築から説明しているので、「もう知ってる！ or やってるよ！」という人は、[docker-compose.ymlの作成とコンテナの起動](#docker-composeymlの作成とコンテナの起動)から読んでいただくと良いと思います。

# AWS構築
AWSの初期構築を整理します。

## インスタンススタック

EC2インスタンスの構築はコンソール画面からの操作で簡易に可能かつ[公式リファレンス](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/concepts.html)が充実しているので省略します。
構築したインスタンスは下記の通りです。

* マシンイメージ
  * Amazon Linux 2 AMI (HVM), SSD Volume Type
* インスタンスタイプ
  * t2.medium(t2.microではスペック不足でコンテナが立ち上がりませんでした  
  (詳細は[インスタンスメモリ不足による起動失敗](#インスタンスメモリ不足による起動失敗)を参照))
* ディスクサイズ
  * SSD 30GB(まずはお試し構築ならこの容量でも十分です)
* セキュリティ(外部向け開放ポート)
  * 22(SSHアクセス用)
  * 5601(Kibanaにブラウザからアクセスする用)

## AWSインスタンス初期設定

インスタンスを立ち上げたので、CUIによるインスタンスへのアクセスから必要と思われる操作を記載します。
(もし過不足があれば指摘してもらえれば幸いです)
状況は秘密鍵をダウンロードした状態からの操作イメージです。

1. 秘密鍵の配置(~/.ssh配下に格納)
`mv /Users/ユーザ名/Downloads/秘密鍵名.pem ~/.ssh`

2. 秘密鍵の権限設定
(インスタンスへSSHアクセスするためには秘密鍵が公開されていないことが必須なため)
`chmod 400 ~/.ssh/秘密鍵名.pem`

3. AWSのEC2インスタンスにSSH接続
`ssh -i ~/.ssh/秘密鍵名.pem ユーザ名@パブリックDNS`
(デフォルトのユーザ名はec2-user)

4. パッケージの更新
`sudo yum update -y`

5. パッケージの自動更新設定

```
# yum-cronのインストール
sudo yum install yum-cron -y

# yum-cron有効化確認
sudo chkconfig --list yum-cron

# 有効化
sudo chkconfig yum-cron on

# 自動更新設定
sudo sed -i "s/^apply_updates.*$/apply_updates = yes/g" /etc/yum/yum-cron.conf

# 起動
sudo service yum-cron start

・起動確認
sudo service yum-cron status
```

6.タイムゾーンの変更
(インスタンスのローカルタイムとハードウェアクロックを変更します)  

```
# 現在の設定確認
date

# ローカルタイムを【Japan】に変更
sudo ln -sf /usr/share/zoneinfo/Japan /etc/localtime

# ハードウェアクロックを【Japan】に変更
sudo sed -i "s/\"UTC\"/\"Japan\"/g" /etc/sysconfig/clock

# システム再起動
sudo reboot

# 現在の設定確認
date
```

7.文字コードを日本語対応に変更
`sudo sed -i "s/en_US\.UTF-8/ja_JP\.UTF-8/g" /etc/sysconfig/i18n`

# Docker環境初期構築
初期ではDockerがインストールされていなかったので、初期構築を実施。

1.gitのインストール
`sudo yum install git -y`

2.dockerをインストール
`sudo yum install docker -y docker -v`

3.dockerを起動
`sudo service docker start`
(起動状態を確認する場合は、`sudo service docker status`)

(補足)4.dockerコマンド実行時にsudoをやらないように設定
(利用者のセキュリティ観点で要否は判断してください)

```
dockerグループの確認(ない場合は追加)
$ sudo groupadd docker
任意のユーザーをdockerグループに入れる
$ sudo usermod -a -G docker $USER
```

5.docker-composeのインストール
curlコマンドを使って、GitHub上にある最新版資材を取得します。
(2018/8/18現在の最新版は1.22.0)
[公式サイト手順](https://docs.docker.com/compose/install/#install-compose)
`sudo curl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose`

資材を取得したら、バイナリファイルに権限を付与します。
`sudo chmod +x /usr/local/bin/docker-compose`

正常にインストールされたかを確認します。
`docker-compose --version`
以下のような結果が出力されればOK。
`docker-compose version 1.22.0, build f46880fe`

# docker-compose.ymlの作成とコンテナの起動
初期設定が完了したらdocker-composeを使って、コンテナを作成/起動していきます。
※今回はdockerfileは作りません。

ちなみにDocker-composeとは[公式サイト](http://docs.docker.jp/compose/overview.html)によると以下のことです。
>Compose とは、複数のコンテナを使う Docker アプリケーションを、定義・実行するツールです。Compose はアプリケーションのサービスの設定に、Compose ファイルを使います。そして、コマンドを１つ実行するだけで、設定した全てのサービスを作成・起動します。

実行後のざっくりイメージは以下になります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/265994/ff5e688b-a9d5-55f6-0131-06129c8eedaf.png)

それではやっていきましょう。

## docker-compose.ymlファイルの作成
任意のディレクトリを作成し、その中に`docker-compose.yml`というファイル名で新規ファイルを作成します。
Composeファイル作成には、[公式リファレンス](http://docs.docker.jp/compose/compose-file.html#compose)にお世話になりました。

作成したYAMLファイルは以下になります。

```
# elasticsearch
elasticsearch:
  image: docker.elastic.co/elasticsearch/elasticsearch:6.3.2
  container_name: elasticsearch
  ports:
    - "9200:9200"
    - "9300:9300"
  ulimits:
    nofile:
      soft: 65536
      hard: 65536

# kibana
kibana:
  image: docker.elastic.co/kibana/kibana:6.3.2
  container_name: kibana
  ports:
    - "5601:5601"
  links:
    - elasticsearch

# logstash
logstash:
  image: docker.elastic.co/logstash/logstash:6.3.2
  container_name: logstash
  links:
    - elasticsearch
```

### YAMLファイルの解説コメント
YAMLファイルの記載内容を説明します。
コメントは「コード上部に##で記載します」 

```
# elasticsearch
## elasticsearchの定義を開始
elasticsearch:
　## 公式サイトからdockerイメージの最新版を取得
  image: docker.elastic.co/elasticsearch/elasticsearch:6.3.2
  ## コンテナ名を明示的に付与
　container_name: elasticsearch
　## ポートフォワードで必要なポートを開放
  ## 9200：REST通信用
  ## 9300：ノード間コミュニケーション用
  ports:
    - "9200:9200"
    - "9300:9300"
  ## file descriptorの設定値エラーを防ぐため
  ## 詳細はfile descriptorの設定変更漏れによる起動エラー項目を参照
  ulimits:
    nofile:
      soft: 65536
      hard: 65536

# kibana
## kibanaの定義を開始
kibana:
　## 公式サイトからdockerイメージの最新版を取得
  image: docker.elastic.co/kibana/kibana:6.3.2
  ## コンテナ名を明示的に付与
  container_name: kibana
　## ポートフォワードで必要なポートを開放
  ## 5601：ブラウザからkibanaのGUIにアクセスするためのポート
  ports:
    - "5601:5601"
　## コンテナ間連携の設定
　## elasticserchと情報を授受するため必須
  links:
    - elasticsearch

# logstash
## logstashの定義を開始
logstash:
　## 公式サイトからdockerイメージの最新版を取得
  image: docker.elastic.co/logstash/logstash:6.3.2
  ## コンテナ名を明示的に付与
  container_name: logstash
　## コンテナ間連携の設定
　## elasticserchと情報を授受するため必須
  links:
    - elasticsearch
```

## docker-composeによる起動
docker-compose.ymlファイルを作成したら、以下のコマンドを「YAMLファイルのあるディレクトリ」で実行。
`docker-compose up -d`

※初期実行時は、外部からDockerイメージの取得⇒コンテナの起動を行うため、少し時間がかかります。
`docker-compose up`実行時にエラーが発生した場合は、[`docker-compose up -d`の実行時にエラーが発生](#docker-compose-up--dの実行時にエラーが発生)の対処を実施してみてください。

構築が終わったら以下のコマンドで正常に起動しているかを確認します。
`docker ps`

コマンド実行後は以下のようなイメージとなります。

```
$ docker ps
CONTAINER ID        IMAGE                                                 COMMAND                  CREATED             STATUS              PORTS                                            NAMES
<ハッシュ値>        docker.elastic.co/logstash/logstash:6.3.2             "/usr/local/bin/dock…"   3 days ago          Up 3 days           5044/tcp, 9600/tcp                               logstash
<ハッシュ値>        docker.elastic.co/kibana/kibana:6.3.2                 "/usr/local/bin/kiba…"   3 days ago          Up 3 days           0.0.0.0:5601->5601/tcp                           kibana
<ハッシュ値>        docker.elastic.co/elasticsearch/elasticsearch:6.3.2   "/usr/local/bin/dock…"   3 days ago          Up 3 days           0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp   elasticsearch
```

* 確認ポイント
  * NAMESに指定した3つのコンテナが並んでいること
  * 指定した3つのコンテナが起動していること(STATUSがUpになっていること)
です。
* `docker ps`コマンド自体が正常に起動しているコンテナのみしか一覧表示しないので、この中にコンテナが入ってなければ起動失敗と認識してOKです。  

コンテナが正常に起動していない場合は、以下の項目を参照してみてください。
[コンテナが起動しない時の基本動作](#コンテナが起動しない時の基本動作)
[vm.max_map_countの設定値不足による起動エラー](#vmmax_map_countの設定値不足による起動エラー)

### 動作確認
コンテナが正常に起動していたら、以下の操作で動作確認します。

* Kibanaへのブラウザアクセス
  * http://<パブリックIP>:5601
  * アクセス後にKibanaのTop画面が表示されればOK
* elasticsearchにサーバ内でcurlコマンドを実行
  * `curl localhost:9200`
  * コマンド実行後に以下のような結果が出ればOK

```
{
  "name" : "_2cmTM_",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "L-ftBXNTQJW_iiVW4PTujg",
  "version" : {
    "number" : "6.3.2",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "053779d",
    "build_date" : "2018-07-20T05:20:23.451332Z",
    "build_snapshot" : false,
    "lucene_version" : "7.3.1",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}
```
以上が構築から正常起動確認までの流れになります。

## 構築時発生エラーとその対処
個人的に構築している際にエラーが発生して詰まったポイントとどう対処したかを解説します。

### `docker-compose up -d`の実行時にエラーが発生
* エラー内容

```
  $ docker-compose up
  ERROR: Couldn't connect to Docker daemon at http+docker://localhost - is it running?

  If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
```

* エラー原因
  * docker-compose コマンドを実行したユーザがアクセスできる権限を持っていないため
* エラー対処
  * 以下のコマンドを順に実行し、`docker`というグループにユーザを所属させます。
  * dockerグループの作成：`sudo groupadd docker`
  * 現行ユーザをdockerグループに所属：`sudo gpasswd -a $USER docker`
  * dockerデーモンを再起動する：`sudo service docker restart`
  * 設定反映のためexitして再ログイン：`exit`

### コンテナが起動しない時の基本動作
私がエラー解析などで使った手順です。
(もっと良い手順もあるはずですが、今回はこれで解決したのでメモです)

* 1. 起動原因の切り分け
  * `docker ps`コマンドを実行して、コンテナが起動していなければ、`docker ps -a`コマンドで、全てのコンテナを一覧表示し、「特定のコンテナのSTATUSがEXIT()」になっているかを確認
  * docker-compose.ymlに異常があってそもそもイメージ取得などでこけているのか、起動で失敗しているのかを切り分け
* 2. 起動失敗コンテナの起動ログを確認
  * 起動失敗している場合は、失敗しているコンテナに対して`docker logs <CONTAINER ID>`を実行することで、コンテナ起動時のログと失敗理由を参照できます。

### インスタンスメモリ不足による起動失敗
* エラー内容

```
OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x0000000085330000, 2060255232, 0) failed; error='Cannot allocate memory' (errno=12)
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 2060255232 bytes for committing reserved memory.
# An error report file with more information is saved as:
# /tmp/hs_err_pid1.log
```

* エラー原因
  * インスタンスメモリ不足(t2.microでケチった結果です。。。)
* エラー対処
  * インスタンスタイプを変更し、メモリを増設(t2.micro⇒t2.medium)

### file descriptorの設定変更漏れによる起動エラー
* エラー内容

`[1]: max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]`

* エラー原因
  * BootstrapCheck(設定が推奨値に達していなかったり、変な設定が入っているとエラーになって終了する)により、file descriptorの設定値が既定よりも不足していたため
* エラー対処
  * docker-compose.ymlファイルに`ulimits:`の設定を追加し、再度`docker-compose up -d`を実行。  
  [公式リファレンス](http://docs.docker.jp/compose/compose-file.html#ulimits)
  * ulimits設定は、ホストを変更してもコンテナに設定反映されないため、YAMLファイルに直接設定を書き込む必要がある。

### vm.max_map_countの設定値不足による起動エラー
* エラー内容

`[2]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]`

* エラー原因
  * BootstrapCheck(設定が推奨値に達していなかったり、変な設定が入っているとエラーになって終了する)により、vm.max_map_countの設定値が既定よりも不足していたため
* エラー対処
  * `sudo sysctl -w vm.max_map_count=262144`コマンドを実行し、設定ファイルを書き換え
  * vm.max_map_count設定は、ホストの変更がコンテナにも影響するため、ホストを書き換えればOK

#