Amazon API Gateway アクセスログ出力設定とアクセスログステータスコード監視設定

個人的備忘録

# 1. 概要
* 設定内容
 * **Amazon API Gateway のアクセスログを出力する設定**
      * API名: hogehoge
      * CloudWatch Logs ロググループ名: /aws/apigateway/hogehoge-accesslog
 * **アクセスログのステータスコード監視設定 - 方法１**
     * CloudWatch Logs メトリックフィルター ＋ CloudWatch メトリックアラーム ＋ Amazon SNS (AWS Chatbot) + Slack
 * **アクセスログのステータスコード監視設定 - 方法２**
     * Lambda ＋ CloudWatch イベント (Amazon EventBridge) ＋ Slack
     * [参考] https://qiita.com/chii-08/items/e20651e7912596e9a556

* 監視方法
 * 方法１
      * 監視間隔
          * １分おき
      * 監視ワード
          * status = 4* || status = 5* 
      * Slackへの通知条件
          * 上記監視ワードにマッチしたアクセスログのカウント数が閾値を超えたときと、下回ったとき<br />（閾値を超えている間はSlackへの通知はない）
      * Slackへの通知内容
          * カウント数

 * 方法２
      * 監視間隔
          * 5分おき
      * 監視ワード
          * status >= 400
      * Slackへの通知条件
          * 直近５分前のアクセスログが上記監視ワードにマッチしたとき
      * Slackへの通知内容
          * マッチしたアクセスログの行

 * 監視例
     * 方法１で第一報と収束を通知、方法２でエラー中の実際のログを１分毎に通知する

# 2. 設定
## 2.1. API Gateway アクセスログ出力

[参考] https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-logging.html

1. API Gateway が CloudWatch Logs ロググループへアクセスログを書き込むことを許可するIAMロールを作成
2. API Gateway のアクセスログを記録する CloudWatch Logs ロググループ を作成
3. API Gateway のアクセスログ出力を有効にする設定

---

**1. API Gateway が CloudWatch Logs ロググループへアクセスログを書き込むことを許可するIAMロールを作成**

* IAMロール を作成する terraform サンプル

```terraform
data "aws_iam_policy_document" "apigw-assume-role-policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["apigateway.amazonaws.com"]
    }
  }
}
resource "aws_iam_role" "role_AmazonAPIGatewayPushToCloudWatchLogs" {
  name               = "AmazonAPIGatewayPushToCloudWatchLogs"
  assume_role_policy = data.aws_iam_policy_document.apigw-assume-role-policy.json
  path               = "/service-role/"
}
resource "aws_iam_role_policy_attachment" "AmazonAPIGatewayPushToCloudWatchLogs" {
  role       = aws_iam_role.role_AmazonAPIGatewayPushToCloudWatchLogs.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
}
```

* 作成したIAMロール
    ![20201119-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/77be761d-3f6b-a56c-1262-5be9aa273cbb.png)
    ![20201119-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/73f21341-cfe3-0218-f764-9d50ec32bf65.png)


* 作成したIAMロールを設定する

    ![20201119-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/d6658480-54d3-13e5-bb5b-fd4c3f9007bc.png)

---

**2. API Gateway のアクセスログを記録する CloudWatch Logs ロググループ を作成**

* ロググループ を作成する terraform サンプル

```terraform
resource "aws_cloudwatch_log_group" "apigateway-hogehoge-accesslog" {
  name              = "/aws/apigateway/hogehoge-accesslog"
  retention_in_days = "90"
}
```

---

**3. API Gateway のアクセスログ出力を有効にする設定**

[参考]https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/set-up-logging.html

* AWS マネジメントコンソール

    ![20201119-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/34c1023e-14c2-f7a5-b8fd-ee06c5e39b57.png)

  * 「Access Log Destination ARN」は、 **2.** で作成したロググループの ARN を設定
  * ログの形式は **JSON** を選択

* アクセスログ例 (JSON)

```json
{
    "requestId": "※※※※※※※※-※※※※-※※※※-※※※※-※※※※※※※※※※※※",
    "ip": "***.***.***.***",
    "caller": "-",
    "user": "-",
    "requestTime": "18/Nov/2020:05:28:56 +0000",
    "httpMethod": "POST",
    "resourcePath": "/v1",
    "status": "403",
    "protocol": "HTTP/1.1",
    "responseLength": "42"
}
```

## 2.2. アクセスログのステータスコード監視設定 - 方法1

* ディレクトリ構成
<pre>
└── apigateway-accesslog-check.tf
</pre>

* CloudWatch Logs メトリックフィルター を作成する terraform のサンプル

```terraform:apigateway-accesslog-check.tf
resource "aws_cloudwatch_log_metric_filter" "APIGateway_hogehoge_accesslog_4xx5xxError" {
  name           = "APIGateway_hogehoge_accesslog_4xx5xxError"
  pattern        = "{ ($.status = 4*) || ($.status = 5*) }"
  log_group_name = aws_cloudwatch_log_group.apigateway-hogehoge-accesslog.name
  metric_transformation {
    name      = "APIGateway_hogehoge_accesslog_4xx5xxError"
    namespace = "LogMetrics"
    value     = "1"
  }
}
```

* CloudWatch メトリックアラーム を作成する terraform のサンプル

```terraform:apigateway-accesslog-check.tf
resource "aws_cloudwatch_metric_alarm" "APIGateway_hogehoge_accesslog_4xx5xxError" {
  alarm_name          = "APIGateway hogehoge accesslog 4xx5xxError"
  threshold           = "1"
  alarm_description   = ""
  alarm_actions       = [※※※※※]
  ok_actions          = [※※※※※]
  comparison_operator = "GreaterThanOrEqualToThreshold"
  namespace           = "LogMetrics"
  metric_name         = "APIGateway_hogehoge_accesslog_4xx5xxError"
  period              = "60"
  evaluation_periods  = "1"
  statistic           = "Sum"
  treat_missing_data  = "notBreaching"
}
```


## 2.3. アクセスログのステータスコード監視設定 - 方法２

* ディレクトリ構成
<pre>
├── apigateway-accesslog-check.tf
└── source_code
    └── apigateway-accesslog-check
        └── main.py
</pre>

* Lambda にアタッチする IAM ロール を作成する terraform のサンプル

```terraform:apigateway-accesslog-check.tf
data "aws_iam_policy_document" "lambda-assume-role-policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}
resource "aws_iam_role" "role_lambda-apigateway-accesslog-check" {
  name               = "lambda-apigateway-accesslog-check"
  assume_role_policy = data.aws_iam_policy_document.lambda-assume-role-policy.json
  path               = "/service-role/"
}
resource "aws_iam_role_policy_attachment" "lambda-apigateway-accesslog-check_CloudWatchLogsReadOnlyAccess" {
  role       = aws_iam_role.role_lambda-apigateway-accesslog-check.name
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsReadOnlyAccess"
}
resource "aws_iam_role_policy_attachment" "lambda-apigateway-accesslog-check_AWSLambdaBasicExecutionRole" {
  role       = aws_iam_role.role_lambda-apigateway-accesslog-check.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
```

* Lambda を作成する terraform のサンプル

```terraform:apigateway-accesslog-check.tf
data "archive_file" "apigateway-accesslog-check" {
  type        = "zip"
  source_dir  = "./source_code/apigateway-accesslog-check"
  output_path = "./source_code/apigateway-accesslog-check.zip"
}
resource "aws_lambda_function" "apigateway-accesslog-check_hogehoge" {
  filename         = data.archive_file.apigateway-accesslog-check.output_path
  function_name    = "apigateway-accesslog-check_hogehoge"
  role             = aws_iam_role.role_lambda-apigateway-accesslog-check.arn
  handler          = "main.lambda_handler"
  source_code_hash = data.archive_file.apigateway-accesslog-check.output_base64sha256
  runtime          = "python3.8"
  memory_size      = 128
  timeout          = 300
  environment {
    variables = {
      LOGGROUP_NAMES    = "/aws/apigateway/hogehoge-accesslog"
      SLACK_CHANNEL     = "#※※※※※"
      SLACK_TEXT        = "Slack通知テスト"
      SLACK_USER_NAME   = "名無し"
      SLACK_ICON_EMOJI  = ":fearful:"
      SLACK_COLOR       = "warning"
      SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/※※※※※/※※※※※/※※※※※"
    }
  }
}
```

```python:source_code/apigateway-accesslog-check/main.py
import os
import datetime
import boto3
client = boto3.client('logs')
import time
import ast
import json
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

logGroupName    = os.environ['LOGGROUP_NAMES']
slackWebhookURL = os.environ['SLACK_WEBHOOK_URL']
slackChannel    = os.environ['SLACK_CHANNEL']
slackUserNmae   = os.environ['SLACK_USER_NAME']
slackText       = os.environ['SLACK_TEXT']
slackColor      = os.environ['SLACK_COLOR']
slackIconEmoji  = os.environ['SLACK_ICON_EMOJI']

def lambda_handler(event, context):
    five_minutes_ago = datetime.datetime.now() - datetime.timedelta(minutes = 5)
    startTime = five_minutes_ago.replace(second = 0, microsecond = 0)
    endTime = startTime + datetime.timedelta(minutes = 5) - datetime.timedelta(milliseconds = 1)
    queryString = 'fields @timestamp, @message | filter (status >= 400)'

    error_logs = results(logGroupName, startTime, endTime, queryString)
    if len(error_logs) > 0:
        for error_log in error_logs:
            dict_message = ast.literal_eval(error_log[1]['value'])
            text = str(logGroupName) + '\n' + str(dict_message)
            print(text)
            post_slack(text)

def results(logGroupName, startTime, endTime, queryString):
    start_query_res = client.start_query(
        logGroupName=logGroupName,
        startTime=int(startTime.timestamp()),
        endTime=int(endTime.timestamp()),
        queryString=queryString
    )
    queryId = start_query_res['queryId']

    get_results_res = client.get_query_results(
        queryId=queryId
    )

    while get_results_res['status'] == 'Running':
        time.sleep(5)
        get_results_res = client.get_query_results(
           queryId=queryId
        )
    return get_results_res['results']

def post_slack(text):
    print(slackWebhookURL)

    slack_message = {
        'channel': slackChannel,
        'username': slackUserNmae,
        'text': slackText,
        'icon_emoji': slackIconEmoji,
        'attachments': [
            {
                "color": slackColor,
                "text": text
            }
        ]
    }
    req = Request(slackWebhookURL, json.dumps(slack_message).encode('utf-8'))
    response = urlopen(req)
    response.read()
```

* CloudWatch イベントルール を作成する terraform のサンプル

```terraform:apigateway-accesslog-check.tf
resource "aws_cloudwatch_event_rule" "apigateway-accesslog-check" {
  name                = "alarm_apigateway-accesslog-check"
  is_enabled          = true
  schedule_expression = "rate(5 minutes)"
}
resource "aws_cloudwatch_event_target" "apigateway-accesslog-check_hogehoge" {
  rule      = aws_cloudwatch_event_rule.apigateway-accesslog-check.name
  target_id = "apigateway-accesslog-check_hogehoge"
  arn       = aws_lambda_function.apigateway-accesslog-check_hogehoge.arn
}
```

※ terraform で CloudWatch イベントルール を作成した場合、スケジュールが来ても lambda が実行されない時がある。
※ そのときは、AWSマネジメントコンソールから 対象のイベントルールを無編集で更新する。



# 3. Slack通知例

* 方法１

    ![20201119-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/dc1fd6e5-6153-4abf-1c35-cfb13f507300.png)
    ![20201119-09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/49544cea-afd5-3318-ec4b-0123bcd885ba.png)

* 方法２

    ![20201119-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148868/2cc52fb2-7353-7514-42f6-a94045426417.png)