Elastic Beanstalk + DockerでのCloudwatch logsの設定

# 前提
下記のガイドに従い、Elastic Beans + Docker で何かしらのアプリケーションはDeployできている前提
http://aws.typepad.com/aws_japan/2014/04/aws-elastic-beanstalk-for-docker.html

# Elastic Beanstalk Docker に Deploy するファイル構成

下記のファイルを一つのZipにまとめて、Elastic Beanstalk, Dockerにて Deployします
```
.
|-- .ebextensions
|   |-- cwl-myjob.config
|   |-- cwl-setup.config
|   |-- cwl-webrequest-metrics.config
|   `-- eb-logs.config
|-- Dockerfile
|-- Dockerrun.aws.json
`-- testlog.sh
```

* Dockerfile :Docker container 定義file(今回は一般的なサンプル＋下記testlog.shを動かす設定をしている)
* testlog.sh :log を定期的に書き出すサンプルスクリプト
* Dockerrun.aws.json : ElasticBeanstalk 固有の設定ファイル(今回はDocker containerのログDirectoryとHostMacineのDirectoryをMountする)
* .ebextensions 以下:  Cloudwatch logs 関連の設定ファイル


# ElasticBeanstalk + CloudwatchLog用サンプル config ファイルのDL
まず下記の記事を参考に
docs.aws.amazon.com/elasticbeanstalk/latest/dg/AWSHowTo.cloudwatchlogs.html

以下から　EB + Docker 用のnginx 設定ファイルを取得
https://s3.amazonaws.com/elasticbeanstalk-samples-us-east-1/cloudwatchlogs/aws_eb_cloudwatchlogs-nginx-us-east-1.zip
取得,展開したファイルを.ebextensions　というディレクトリに格納

この.ebextensionsというDirectoryをDockerfileと並列において、zipでかためて
EB 上のDashboadで"Upload & Deploy" すればサンプルは確認可能。

サンプルはcwl-webrequest-metrics.config というファイルで
nginx 用のrequest の4XX, 5XX 応答の数をカウントする

cwl-setup.config, eb-logs.config は　特に編集の必要がなく、そのまま使用する


# カスタムログ用　config 作成

上記　nginx request用configを参考に、任意のログ上の値をカウントするサンプルを作成する。
*.config という名前にして、上記.ebextensionsというDirectoryに格納しておくと
ElasticBeanstalk deploy実行時に読み込んでくれる。

HotTopic

* Resourceとして　Cloudwatch logs で連携させたい　(LogGroup,MetricFilter, Alarm)　を定義する。
* CWLogsAgentConfigSetup で監視するファイルの設定をする
* CloudWatchLogsではTimestampとそのときのログ本文という形で保持されるので、datetime_formatを設定する
* MetricFilter のFilterPattern,MetricValue でログの書式と抜き出したい値を定義する

## CWLogsAgentConfigSetup
http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/AgentReference.html
上記に定義されているものの中で重要なものは下記
### file
監視するファイルを以下のように設定する。サンプルのconfigでは最上部に定義いしているMappingsで切り替えられるようにしている。(Mappingsで定数にあたるものの宣言を上部に持ってくるのはCloudformationでもよく使われるパターンの一つ。)
````
Mappings:
  CWLogs:
    MyJobLogGroup:
      LogFile: "/var/log_docker/test.log"
...
 file = `{"Fn::FindInMap":["CWLogs", "MyJobLogGroup", "LogFile"]}`
````

### datetime_format
これも上記のリンクに詳細説明はあるが、下記の例をみればだいたい理解できる。
これを指定しておくと、以下のMetricFilterで特に指定しなくても、一行の中でdatetimeが入っているところを自動で検出してくれて、それをログの発生時刻としてCloudWatch上であつかってくれる点が、優秀
。

```
Syslog: '%b %d %H:%M:%S', e.g. Jan 23 20:59:29

Log4j: '%d %b %Y %H:%M:%S', e.g. 24 Jan 2014 05:00:00

ISO8601: '%Y-%m-%dT%H:%M:%S%z', e.g. 2014-02-20T05:20:20+0000

今回のサンプル：    TimestampFormat: "%Y/%m/%d %H:%M:%S.%f"　( 2014/08/13 19:12:03.839)

```

## AWS::Logs::MetricFilter
Logの出力フォーマットを定義してその中から、値として記録したいValueを定義する。
CloudWatch上のUIからも設定可能だが、複数環境で複数作成する場合などは再現性があるのでconfigファイルとしてsrc code としてコントロールできることが利点となる。

### FilterPattern
下記を参考にログの中で抜き出したい箇所を定義する。
このログ定義に一致したときだけMetricValueとしてカウントされる。
http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/FilterAndPatternSyntax.html

* ログは空白区切りになっていることが前提
* 抜き出したい箇所だけ、変数を定義して幾つはいるかわからないところは...と記述することが可能
  [..., status_code, bytes]
  [ip, user, ..., status_code, bytes]
  [ip, user, ...]

* 文字列は = , != 演算子と * が使用可能
* 数値は  >, <, >=, <=, =, !=  比較演算子が使用可能


今回は Kinesis から読み込んだメッセージ数、書き込んだメッセージ数をCloudWatch上で表示したいと思い、下記のようなログを出力したという前提でLogFilterを設定した。

```
SERVICE #  INFO  2014/08/13 19:12:03.839  0.926.552  TestService  TestSender  0:0(0x00000000) +MSG  TestLog-01 kineGet 250 kineWrite 0

    FilterPatterns:
      TestLogFilter: "[..., logtype=TestLog-01, logtag1, logval1, logtag2, logval2]"  


```
### MetricValue
上記で定義したFilterPatternの中で、使いたい変数を書くことが可能
例:
  MetricValue : $logval2

4xx, 5xx 応答の数などをカウントしたい場合はそのログがあったことで+1すればよいので下記のように定義する
  MetricValue : 1


## Sample config : cwl-myjob.config

``` yaml:cwl-myjob.config
# Sample Myjob access log pattern:
# SERVICE #  INFO  2014/08/13 19:12:03.839  0.926.552  TestService  TestSender  0:0(0x00000000) +MSG  TestLog-01 kineGet 250 kineWrite 0
# $category # $level $date $time ..., logtype=TestLog-01, logtag1, logval1, logtag2, logval2]"
Mappings:
  CWLogs:
    MyJobLogGroup:
      LogFile: "/var/log_docker/test.log"
      TimestampFormat: "%Y/%m/%d %H:%M:%S.%f"
    FilterPatterns:
      TestLogFilter: "[..., logtype=TestLog-01, logtag1, logval1, logtag2, logval2]"  

Outputs:
  MyJobCWLogGroup:
    Description: "The name of the Cloudwatch Logs Log Group created for myjob. You can specify this by setting the value for the environment variable: MyJobCWLogGroup. Please note: if you update this value, then you will need to go and clear out the old cloudwatch logs group and delete it through Cloudwatch Logs."
    Value: { "Ref" : "AWSEBCloudWatchLogs8832c8d3f1a54c238a40e36f31ef55a0MyJobLogGroup"}


Resources :
  AWSEBCloudWatchLogs8832c8d3f1a54c238a40e36f31ef55a0MyJobLogGroup:    ## Must have prefix:  AWSEBCloudWatchLogs8832c8d3f1a54c238a40e36f31ef55a0
    Type: "AWS::Logs::LogGroup"
    DependsOn: AWSEBBeanstalkMetadata
    DeletionPolicy: Retain     ## this is required
    Properties:
      LogGroupName: 
        "Fn::GetOptionSetting":
          Namespace: "aws:elasticbeanstalk:application:environment"
          OptionName: MyJobCWLogGroup
          DefaultValue: {"Fn::Join":["-", [{ "Ref":"AWSEBEnvironmentName" }, "MyJobLogs"]]}
      RetentionInDays: 14


  ## Register the files/log groups for monitoring
  AWSEBAutoScalingGroup:
    Metadata:
      "AWS::CloudFormation::Init":
        CWLogsAgentConfigSetup:
          files:
            ## any .conf file put into /tmp/cwlogs/conf.d will be added to the cwlogs config (see cwl-agent.config)
            "/tmp/cwlogs/conf.d/myjob-log.conf":
              content : |
                [myjob-log_log]
                file = `{"Fn::FindInMap":["CWLogs", "MyJobLogGroup", "LogFile"]}`
                log_group_name = `{ "Ref" : "AWSEBCloudWatchLogs8832c8d3f1a54c238a40e36f31ef55a0MyJobLogGroup" }`
                log_stream_name = {instance_id}
                datetime_format = `{"Fn::FindInMap":["CWLogs", "MyJobLogGroup", "TimestampFormat"]}`
              mode  : "000400"
              owner : root
              group : root

      
  #######################################
  ## Cloudwatch Logs Metric Filters

  AWSEBCWLMyJobLogTestVal1MetricFilter :
    Type : "AWS::Logs::MetricFilter"
    Properties :
      LogGroupName: { "Ref" : "AWSEBCloudWatchLogs8832c8d3f1a54c238a40e36f31ef55a0MyJobLogGroup" }
      FilterPattern : {"Fn::FindInMap":["CWLogs", "FilterPatterns", "TestLogFilter"]}
      MetricTransformations :
        - MetricValue : $logval1
          MetricNamespace: {"Fn::Join":["/", ["ElasticBeanstalk", {"Ref":"AWSEBEnvironmentName"}]]}
          MetricName : CWLTestVal1KinessisRead

  AWSEBCWLMyJobLogTestVal2MetricFilter :
    Type : "AWS::Logs::MetricFilter"
    Properties :
      LogGroupName: { "Ref" : "AWSEBCloudWatchLogs8832c8d3f1a54c238a40e36f31ef55a0MyJobLogGroup" }
      FilterPattern : {"Fn::FindInMap":["CWLogs", "FilterPatterns", "TestLogFilter"]}
      MetricTransformations :
        - MetricValue : $logval2
          MetricNamespace: {"Fn::Join":["/", ["ElasticBeanstalk", {"Ref":"AWSEBEnvironmentName"}]]}
          MetricName : CWLTestVal2KinessisWrite

  ######################################################
  ## Alarms

  AWSEBCWLTestVal1Alarm :
    Type : "AWS::CloudWatch::Alarm"
    DependsOn : AWSEBCWLMyJobLogTestVal1MetricFilter
    Properties :
      AlarmDescription: "Kinesis read count is too many (count too high)."
      MetricName: CWLTestVal1KinessisRead
      Namespace: {"Fn::Join":["/", ["ElasticBeanstalk", {"Ref":"AWSEBEnvironmentName"}]]}
      Statistic: Sum
      Period: 60
      EvaluationPeriods: 1
      Threshold: 200
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - "Fn::If":
            - SNSTopicExists
            - "Fn::FindInMap":
                - AWSEBOptions
                - options
                - EBSNSTopicArn
            - { "Ref" : "AWS::NoValue" }
```

# サンプル用　Dockerfile, Script
AWSで紹介されている"2048" Gameのサンプルファイルに定期的にログを出力するスクリプトを追加した。　

## Dockerfile
気を付けるところ

* ADDでログファイルを該当Directoryにコピー配置するだけだと、下記Dockerrun.aws.jsonで指定したDirectoryにコピーされない。なので、CMDでスクリプトを実行するようにした。
* スクリプトが終了してしまうと、ElasticBeansTalk上でエラーとして扱われてしまうので、終了しないようにwhile loop で処理している

```
FROM dockerfile/nginx

RUN apt-get update
RUN apt-get install -y zip curl

RUN curl -o /usr/share/nginx/html/master.zip -L https://codeload.github.com/gabrielecirulli/2048/zip/master
RUN cd /usr/share/nginx/html/ && unzip master.zip && mv 2048-master/* . && rm -rf 2048-master master.zip

EXPOSE 80

RUN mkdir /var/log_out
ADD ./testlog.sh /var/
RUN chmod 755 /var/testlog.sh 
CMD /var/testlog.sh
```


## testlog.sh
定期的にログを出力するスクリプト（60分以上動かすとおかしくなります）

```shell-session:testlog.sh
#!/bin/sh
a=0
while true
do
a=`expr $a + 1`
if test ${a} -lt 10 
then
echo "SERVICE #  INFO  2014/08/13 20:0${a}:03.839  0.926.552  TestService  TestSender  0:0(0x00000000) +MSG  TestLog-01 kineGet 1${a}0 kineWrite 0" >> /var/log_out/test.log 
else
echo "SERVICE #  INFO  2014/08/13 20:${a}:03.839  0.926.552  TestService  TestSender  0:0(0x00000000) +MSG  TestLog-01 kineGet 10 kineWrite 1${a}" >> /var/log_out/test.log 
fi
sleep 10
done
```



# サンプル用　Dockerrun.aws.json
Docker Container 上のログ出力ディレクトリと、
Host (ElasticBenastalk で起動されたEC2 instance)上のDirectoryのマウント設定を定義
下記で定義したDirectoryを上記 ClooudWatch Logs のconfig CWLogsAgentConfigSetupで指定する

```json:Dockerrun.aws.json
{
  "AWSEBDockerrunVersion": "1",
  "Volumes": [
    {
      "HostDirectory": "/var/log_docker",
      "ContainerDirectory": "/var/log_out"
    }
  ]
}

```

# Cloudwatch logs 出力例
![WS000001.JPG](https://qiita-image-store.s3.amazonaws.com/0/41631/44a3c0d2-c38d-e38c-85b7-40413744d717.jpeg)
![WS000002.JPG](https://qiita-image-store.s3.amazonaws.com/0/41631/724c3bbd-a034-04c5-7da6-a004366dd898.jpeg)