ELB(ALB,NLB,CLB)アクセスログを、CloudWatch Logsに出力させてみた(Lambda)

# はじめに
こんにちは、はやぴー（@HayaP）です。
日々のログ監視、皆さんはどうされていますか？

- とりあえず、ログをS3に出力している
- CloudWatchで見たいな、、と思いつつ諦めている

なんて人は、少なくないはずです。

そこで、今回は
ELBアクセスログを、CloudWatch Logsに出力した
経験を記したいと思います。

## 対象読者
- ログ監視設計者
- CloudWatch Logsに、ログを集めたい
- 毎回S3に、ログを見に行くのがしんどい

この記事が、クライドネイティブな運用の参考になれればと願っています。

# 概要
## ELB(ALB,NLB,CLB)アクセスログとは
ロードバランサーに送信されるリクエストの、詳細情報が記されているログです。

- リクエストを受け取った時刻
- クライアントの IP アドレス
- サーバーレスポンス
などの情報が含まれます。

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-access-logs.html

## 要件
ELB(ALB,NLB,CLB)アクセスログを、CloudWatch Logsに出力することです。

## 問題
残念ながら、ELB(ALB,NLB,CLB)アクセスログは
S3にしか出力することができません。

Amazon Athenaなどを使う事で、ある程度検索はできます。
しかし、使い慣れたCloudWatch Logsのコンソールを使いたいところです。

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-log-file-format:~:text=%E3%81%AE%E7%84%A1%E5%8A%B9%E5%8C%96-,%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%83%AD%E3%82%B0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB,-Elastic%20Load%20Balancing

## 解決策
そのため、Amazon S3 イベント通知 + Lambda(CloudWatch API)を用いる事で
解決をしました。
![ELB_Access_log_to_CWL.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/9eff802e-c116-ecc0-3918-362393ca5a99.png)

では、具体的な方法を記していきます。

# 詳細
リソース毎に説明をします。順序ではないので、注意してください。

## ELB(ALB,NLB,CLB)
今回は、ALBを例に記していきます。

ALBを作成し、
**Edit load balancer attributes** -> **Monitoring**から、
Access Logsを有効化します。（事前にS3バケットを作成する必要あり）


![MicrosoftTeams-image (12).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/5feb6368-7199-5fe0-39a9-680b997284dd.png)

## S3
アクセスログが出力している事を確認します。
![MicrosoftTeams-image (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/73a704a9-6832-1fc1-4db9-c84bfa1418a0.png)
S3イベント通知を設定します。（事前にLambda関数を作成する必要あり）
![MicrosoftTeams-image (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/2e7cf05b-594e-72ea-7bce-4bc2b5fa07f7.png)

## Lambda
言語はPythonを使用し、boto3を利用しています。
下記のようなコードになりました。（動作は保証しません）

ロググループを、先に作成する必要があるので注意です。
（IaCなどで、先にリソースを作る事をお勧めします）

```sample.py
import boto3
import time
import json
import io
import gzip

s3_boto3 = boto3.client('s3')
cw_boto3 = boto3.client('logs')

LOG_GROUP_NAME = "hoge"

def parse_event(event):
    return {
        "bucket_name": event["Records"][0]["s3"]["bucket"]["name"],
        "bucket_key": event["Records"][0]["s3"]["object"]["key"]
    }

def convert_response(response):
    body = response['Body'].read()
    test = gzip.open(io.BytesIO(body))
    return test.read().decode('utf-8')

def put_log(log, put_log_count, sequence_token, log_stream_name):
    try:
        if put_log_count == 1 and sequence_token == None:
            response = cw_boto3.put_log_events(
                logGroupName=LOG_GROUP_NAME,
                logStreamName=log_stream_name,
                logEvents=[
                    {
                        'timestamp': int(time.time()) * 1000,
                        'message': log
                    },
                ]
            )
            return response

        elif sequence_token != None and put_log_count != 3:
            response = cw_boto3.put_log_events(
                logGroupName=LOG_GROUP_NAME,
                logStreamName=log_stream_name,
                logEvents=[
                    {
                        'timestamp': int(time.time()) * 1000,
                        'message': log
                    },
                ],
                sequenceToken=sequence_token
            )
            return response
        else:
            raise Exception
    except cw_boto3.exceptions.InvalidSequenceTokenException as e:
        sequence_token = e.response.get('expectedSequenceToken')
        put_log_count += 1
        response = put_log(log, put_log_count, sequence_token, log_stream_name)
        return response

    except cw_boto3.exceptions.ResourceNotFoundException as e:
        cw_boto3.create_log_stream(
            logGroupName=LOG_GROUP_NAME, logStreamName=log_stream_name)
        response = put_log(log, put_log_count, sequence_token, log_stream_name)
        return response
    except Exception as e:
        print("error", e)
        raise

def lambda_handler(event, context):
    bucket_info_dictionary = parse_event(event)
    response = s3_boto3.get_object(
        Bucket=bucket_info_dictionary["bucket_name"], Key=bucket_info_dictionary["bucket_key"])
    log_stream_name = bucket_info_dictionary["bucket_key"].split(".")[1]
    log_text = convert_response(response)
    log_list = log_text.splitlines()
    sequence_token = None
    for log in log_list:
        put_log_count = 1
        response = put_log(log, put_log_count,
                            sequence_token, log_stream_name)
        sequence_token = response["nextSequenceToken"]
    return {
        "statusCode": 200
    }
```

ポイントは、CloudWatch APIを使用する際に必要な
**sequence_token**です。概念を含め理解をしておくと、デバック時に役に立ちます。

https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html

今回のコードでは、一回目に故意にエラーを発生させることで
sequence_tokenを取得しています。

下記記事を参考にすると、理解が深まります。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/logs.html#CloudWatchLogs.Client.put_log_events

## CloudWatch Logs
指定したロググループ、ログストリームにALBアクセスログが
出力されています。
![MicrosoftTeams-image (15).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478323/27a70db4-9e8a-2643-acad-766d43b8f397.png)

# まとめ
いかがだったでしょうか？

CloudWatch Logsのコンソールは、見やすいので
実は需要があるのでは・・？と思い投稿してみました。

コストに関しても、数日で削除すれば気にならないと思います。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html#:~:text=%E3%83%AD%E3%82%B0%E3%81%AE%E4%BF%9D%E6%8C%81%E6%9C%9F%E9%96%93%20%E2%80%93%20%E3%83%87%E3%83%95%E3%82%A9%E3%83%AB%E3%83%88%E3%81%A7%E3%81%AF%E3%80%81%E3%83%AD%E3%82%B0%E3%81%AF%E7%84%A1%E5%88%B6%E9%99%90%E3%81%AB%E4%BF%9D%E6%8C%81%E3%81%95%E3%82%8C%E3%80%81%E5%A4%B1%E5%8A%B9%E3%81%97%E3%81%BE%E3%81%9B%E3%82%93%E3%80%82%E3%83%AD%E3%82%B0%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%94%E3%81%A8%E3%81%AB%E4%BF%9D%E6%8C%81%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC%E3%82%92%E8%AA%BF%E6%95%B4%E3%81%97%E3%80%81%E7%84%A1%E5%88%B6%E9%99%90%E3%81%AE%E4%BF%9D%E6%8C%81%E6%9C%9F%E9%96%93%E3%82%92%E7%B6%AD%E6%8C%81%E3%81%99%E3%82%8B%E3%81%8B%E3%80%811%20%E6%97%A5%E9%96%93%EF%BD%9E10%20%E5%B9%B4%E9%96%93%E3%81%AE%E4%BF%9D%E6%8C%81%E6%9C%9F%E9%96%93%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%81%8C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82

是非、活用してみてください！