CloudWatchLogsとSNSを使ってnginxで4xxエラーの際にメールを送信する


CloudWatchLogsを使ったことがなく、何かやってみようと思い、nginxで400系エラーの場合にSNSを使ってメールを送信することをやってみたのでメモ

# 前提

+ Amazon Linux AMI release 2015.03

# 参考

+ [CloudWatch LogsでAmazonLinux上のApacheエラーログを監視する](http://dev.classmethod.jp/cloud/cloudwatch-logs-apache/)
+ [AWS マイスターシリーズ Black belt
AWS CloudTrail & CloudWatch Logs](http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-tech-aws-cloudtrail-cloudwatch-logs)
+ [CloudWatch Logs エージェントのリファレンス](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/DeveloperGuide/AgentReference.html)

# CloudWatchLogsへアクセスできるIAMロールの作成

EC2起動前にCloudWatchLogsへのアクセス権限を持ったIAMロールを作成します。

+ IAM->Roles->Create New Role
+ 「Role Name」の画面では任意の名前を入力
+ 「Select Role Type」の画面では**Amazon EC2**を選択
+ 「Attach Policy」の画面では**CloudWatchLogsFullAccess**を選択

# EC2の起動

AmazonLinuxを起動します。
先ほど作成したCloudWatchLogsへのアクセス権限を持ったIAMロールの付与を忘れないようにします。

# EC2のタイムゾーンをJSTに変更する

起動されたEC2のタイムゾーンがUTCになっているのでJSTに変更します。

```
$sudo cp /usr/share/zoneinfo/Japan /etc/localtime

$date
2015年  6月 28日 日曜日 15:56:25 JST
```

# nginxのインストール、

yumでインストールし、サービス起動を実施します

```
$sudo yum install -y nginx
$sudo service nginx start
```


# CloudWatchLogsエージェントのインストール

yumでインストールします。

```
$sudo yum install -y awslogs
```

# CloudWatchLogsエージェントの設定

デフォルト設定ではリージョンがUSになっているので東京(ap-northeast-1)に変更します。

```lang:/etc/awslogs/awscli.conf
[plugins]
cwlogs = cwlogs
[default]
region = ap-northeast-1
```

次にCloudWatchLogsで送信するログのデフォルト情報を確認してみます。

```lang:/etc/awslogs/awslogs.conf

#
# ------------------------------------------
# CLOUDWATCH LOGS AGENT CONFIGURATION FILE
# ------------------------------------------
#
# --- DESCRIPTION ---
# This file is used by the CloudWatch Logs Agent to specify what log data to send to the service and how.
# You can modify this file at any time to add, remove or change configuration.
#
# NOTE: A running agent must be stopped and restarted for configuration changes to take effect.
#
# --- CLOUDWATCH LOGS DOCUMENTATION ---
# https://aws.amazon.com/documentation/cloudwatch/
#
# --- CLOUDWATCH LOGS CONSOLE ---
# https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logs:
#
# --- AGENT COMMANDS ---
# To check or change the running status of the CloudWatch Logs Agent, use the following:
#
# To check running status: service awslogs status
# To stop the agent: service awslogs stop
# To start the agent: service awslogs start
# To start the agent on server startup: chkconfig awslogs on
#
# --- AGENT LOG OUTPUT ---
# You can find logs for the agent in /var/log/awslogs.log
#

# ------------------------------------------
# CONFIGURATION DETAILS
# ------------------------------------------

[general]
# Path to the CloudWatch Logs agent's state file. The agent uses this file to maintain
# client side state across its executions.
state_file = /var/lib/awslogs/agent-state

## Each log file is defined in its own section. The section name doesn't
## matter as long as its unique within this file.
#[kern.log]
#
## Path of log file for the agent to monitor and upload.
#file = /var/log/kern.log
#
## Name of the destination log group.
#log_group_name = kern.log
#
## Name of the destination log stream. You may use {hostname} to use target machine's hostname.
#log_stream_name = {instance_id} # Defaults to ec2 instance id
#
## Format specifier for timestamp parsing. Here are some sample formats:
## Use '%b %d %H:%M:%S' for syslog (Apr 24 08:38:42)
## Use '%d/%b/%Y:%H:%M:%S' for apache log (10/Oct/2000:13:55:36)
## Use '%Y-%m-%d %H:%M:%S' for rails log (2008-09-08 11:52:54)
#datetime_format = %b %d %H:%M:%S # Specification details in the table below.
#
## A batch is buffered for buffer-duration amount of time or 32KB of log events.
## Defaults to 5000 ms and its minimum value is 5000 ms.
#buffer_duration = 5000
#
# Use 'end_of_file' to start reading from the end of the file.
# Use 'start_of_file' to start reading from the beginning of the file.
#initial_position = start_of_file
#
## Encoding of file
#encoding = utf-8 # Other supported encodings include: ascii, latin-1
#
#
#
# Following table documents the detailed datetime format specification:
# ----------------------------------------------------------------------------------------------------------------------
# Directive     Meaning                                                                                 Example
# ----------------------------------------------------------------------------------------------------------------------
# %a            Weekday as locale's abbreviated name.                                                   Sun, Mon, ..., Sat (en_US)
# ----------------------------------------------------------------------------------------------------------------------
#  %A           Weekday as locale's full name.                                                          Sunday, Monday, ..., Saturday (en_US)
# ----------------------------------------------------------------------------------------------------------------------
#  %w           Weekday as a decimal number, where 0 is Sunday and 6 is Saturday.                       0, 1, ..., 6
# ----------------------------------------------------------------------------------------------------------------------
#  %d           Day of the month as a zero-padded decimal numbers.                                      01, 02, ..., 31
# ----------------------------------------------------------------------------------------------------------------------
#  %b           Month as locale's abbreviated name.                                                     Jan, Feb, ..., Dec (en_US)
# ----------------------------------------------------------------------------------------------------------------------
#  %B           Month as locale's full name.                                                            January, February, ..., December (en_US)
# ----------------------------------------------------------------------------------------------------------------------
#  %m           Month as a zero-padded decimal number.                                                  01, 02, ..., 12
# ----------------------------------------------------------------------------------------------------------------------
#  %y           Year without century as a zero-padded decimal number.                                   00, 01, ..., 99
# ----------------------------------------------------------------------------------------------------------------------
#  %Y           Year with century as a decimal number.                                                  1970, 1988, 2001, 2013
# ----------------------------------------------------------------------------------------------------------------------
#  %H           Hour (24-hour clock) as a zero-padded decimal number.                                   00, 01, ..., 23
# ----------------------------------------------------------------------------------------------------------------------
#  %I           Hour (12-hour clock) as a zero-padded decimal numbers.                                  01, 02, ..., 12
# ----------------------------------------------------------------------------------------------------------------------
#  %p           Locale's equivalent of either AM or PM.                                                 AM, PM (en_US)
# ----------------------------------------------------------------------------------------------------------------------
#  %M           Minute as a zero-padded decimal number.                                                 00, 01, ..., 59
# ----------------------------------------------------------------------------------------------------------------------
#  %S           Second as a zero-padded decimal numbers.                                                00, 01, ..., 59
# ----------------------------------------------------------------------------------------------------------------------
#  %f           Microsecond as a decimal number, zero-padded on the left.                               000000, 000001, ..., 999999
# ----------------------------------------------------------------------------------------------------------------------
#  %z           UTC offset in the form +HHMM or -HHMM (empty string if the the object is naive).        (empty), +0000, -0400, +1030
# ----------------------------------------------------------------------------------------------------------------------
#  %j           Day of the year as a zero-padded decimal number.                                        001, 002, ..., 365
# ----------------------------------------------------------------------------------------------------------------------
#  %U           Week number of the year (Sunday as the first day of the week) as a zero padded          00, 01, ..., 53
#               decimal number. All days in a new year preceding the first Sunday are considered
#               to be in week 0.
# ----------------------------------------------------------------------------------------------------------------------
#  %W           Week number of the year (Monday as the first day of the week) as a decimal number.      00, 01, ..., 53
#               All days in a new year preceding the first Monday are considered to be in week 0.
# ----------------------------------------------------------------------------------------------------------------------
#  %c           Locale's appropriate date and time representation.                                      Tue Aug 16 21:30:00 1988 (en_US)
# ----------------------------------------------------------------------------------------------------------------------

[/var/log/messages]
datetime_format = %b %d %H:%M:%S
file = /var/log/messages
buffer_duration = 5000
log_stream_name = {instance_id}
initial_position = start_of_file
log_group_name = /var/log/messages
```

**[general]**セクションでは共有の設定を記載し、以降の**[/var/log/messages]**などの個別のセクションがCloudWatchLogsで送信する各ログファイルの設定をします。

設定を確認するとデフォルト設定でも**/var/log/messages**を送信する設定になっているのが分かります。

指定されている各パラーメーターの詳細は以下を参照ください。

[CloudWatch Logs エージェントのリファレンス](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/DeveloperGuide/AgentReference.html)


# /var/log/messages送信の確認

デフォルトで指定されている/var/log/messagesの内容が正しく送信されるかサービスを起動して確認します。

```
$sudo service awslogs start
```

上記実施後、マネージメントコンソールで確認します。

+ CloudWatch->Logs

LogGroupsに**/var/log/messages**が存在することが確認できます。

![Screen Shot 2015-06-28 at 4.11.42 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/3fde7787-4b77-045b-2071-a4b6145c2fa6.png "Screen Shot 2015-06-28 at 4.11.42 PM.png")

また、**Never Expire**ということでこのログがずっと残ることが分かります。保存期間を変更する場合には**Never Expire**のリンクを選択することで変更が可能です。

LogGroupsを選択すると**Log Streams**が選択できます。デフォルト設定ではEC2のインスタンスIDとなっています。Log StreamsにインスタンスIDを利用することで具体的にどのインスタンスからのログ情報なのかが確認できます。

![Screen Shot 2015-06-28 at 4.14.42 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/4c75af9d-6e59-28c1-f512-9dda0c37eacd.png "Screen Shot 2015-06-28 at 4.14.42 PM.png")

任意のLog Streamsを選択すると実際のログ情報が見ることが出来る画面となります。また、本画面では任意の文字列と日付時間（日付時間はFromのみでToの指定はできない)によるフィルタリングも可能です。


![Screen Shot 2015-06-28 at 4.19.20 PM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/00b63195-20a1-080b-9217-cdeba495e1f5.png "Screen Shot 2015-06-28 at 4.19.20 PM.png")

細かいフィルタリングなどは後述する**メトリックフィルタ**を使うことで可能です。

# nginxのアクセスログをCloudWatchLogsに対応させる

nginxのアクセスログをCloudWatchLogsに対応させるために**/etc/awslogs/awslogs.conf**に追記を行います。

と、その前にnginxのアクセスログの形式を確認してみます。


```lang:/var/log/nginx/access.log
127.0.0.1 - - [28/Jun/2015:09:13:41 +0900] "GET /hogefuga HTTP/1.1" 404 3696 "-" "Wget/1.16.1 (linux-gnu)" "-"
```

CloudWatchLogsでは日付時刻フォーマットを記載する必要があるので、予めフォーマットを確認しておきます。

どのように日付時刻フォーマットを指定すればよいかについては`date`コマンドを利用して確認します。

**28/Jun/2015:09:13:41 +0900**という形式であればdateコマンドのmanを見つつ、以下のようフォーマット指定できることが分かります。

```
$date +"%d/%b/%Y:%H:%M:%S %z"
28/ 6月/2015:09:19:58 +0900
```

上記で設定ファイルを記載する準備ができたので、末尾に追記を行います。

```lang:/etc/awslogs/awslogs.conf
[/var/log/nginx/access.log]
datetime_format = %d/%b/%Y:%H:%M:%S %z
file = /var/log/nginx/access.log
buffer_duration = 5000
log_stream_name = {instance_id}
initial_position = start_of_file
log_group_name = /var/log/nginx/access.log
```

**datetime_format**には先ほど調べたフォーマットの内容を記載しています。

設定ファイルを反映するためにサービスの再起動を行います。

```
$sudo service awslogs restart
```

サービス起動後、Webサーバーにアクセスし、アクセスログを追記します。

```
$wget http://localhost
```

上記実施後、同じようにマネージメントコンソールを確認し、ロググループに**/var/log/nginx/access.log**が追加され、先ほどアクセスしたログを表示できれば成功です。

うまくいかない場合、CloudWatchLogsのログ(/var/log/awslogs.log)などを確認します。


# nginxのエラーログもCloudWatchLogsに対応させる

nginxのエラーログは以下のような形式となっております。

```lang:/var/log/nginx/error.log
2015/06/28 09:00:45 [error] 2279#0: *3 open() "/usr/share/nginx/html/hogefuga" failed (2: No such file or directory), client: 127.0.0.1, server: localhost, request: "GET /hogefuga HTTP/1.1", host: "localhost"
```

先ほどと同じように`date`コマンドでフォーマットを確認します。

```
$date +"%Y/%m/%d %H:%M:%S"
2015/06/28 09:18:58
```

準備ができたので設定ファイルに追記します。

```lang:/etc/awslogs/awslogs.conf
[/var/log/nginx/error.log]
datetime_format = %Y/%m/%d %H:%M:%S
file = /var/log/nginx/error.log
buffer_duration = 5000
log_stream_name = {instance_id}
initial_position = start_of_file
log_group_name = /var/log/nginx/error.log
```

先ほどと同じようにCloudWatchLogsのサービス再起動、nginxのエラーログ出力を行います。

```
$wget http://localhost/hogefuga
```

マネージメントコンソールを確認し、表示されれば成功です。

# SNSの設定

本項以降ではCloudWatchLogsに登録した内容を利用してSNSと連携して4xxエラーの際にメールが送信されるように設定します。

メールを送信する箇所ではSNSを利用するのでその設定を以下のように行います。

+ SNSでトピックを作成
+ 作成したトピックを選択し、Create Subscriptionを行う。今回の例ではプロトコルはEmailとする
+ 自分のメールアドレスを入力する
+ 登録したメールアドレスのリンクをクリックし、Subscriptionの許可を行う

# メトリックフィルタの作成

LogGroupsからnginxのエラーログを示す/var/log/nginx/error.logにチェックを行い、**Create Metric Filter**を選択します。

すると以下のような画面が表示され、フィルタをするパターンを指定できます。

![Screen Shot 2015-06-29 at 7.56.38 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/121248c8-0dee-6e3c-6c4c-dfa8b2ec8a06.png)


上記では **[remote, host, user, time, request, code = 4\**, size, referer, agent, forward]** と記載することでnginxでステータスコードが4xx系の場合のログをフィルタリングするようにしています。

フィルタの書き方の詳細については以下を参照ください。

[フィルタとパターンの構文](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/DeveloperGuide/FilterAndPatternSyntax.html)

フィルタパターンを作成後、**Test Pattern**ボタンを選択することで指定したファイルパターンが実際のログにどれだけマッチするかも確認できます。

# アラームの作成

アラームを作成して4xx系のエラーが5分で5回を超えた時にメールが送信されるようにします。

LogGroupsのグラフの/var/log/nginx/access行のMetric Filters列の**1filter**となっている部分を選択します。先ほど登録したメトリックフィルタの内容が確認できる画面に遷移します。このフィルタに対してアラームを作成するので**Create Alarm**リンクを選択します。

アラーム画面では以下のように設定しました。

![Screen Shot 2015-06-29 at 8.19.52 AM.png](https://qiita-image-store.s3.amazonaws.com/0/20193/99e253fa-6cb4-22d6-e94f-250b08390838.png)

**Send notification to**の項で先ほど作成したSNSのトピックを選択することでアラーム発生時にメールを送信することができます。

# アラームを発生させる

設定が正しくできているか確認するために対象のサーバーで404(Not Found)を複数回発生させます。対象のサーバーで以下を5回以上実施します。

```
$wget http://localhost/hoge
```

設定が正しくできていればSNSのトピックで指定したメールアドレスに対してアラームが発生したことを示すメールが届くかと思います。