Lambda 関数のログをLambda Extensionsを利用してS3に出してみた

# はじめに
AWS Lambda関数を実装して動かすとログはCloudWatch Logsに出力されます。長期的で安価な保存や、ログデータを利用してAmazon Athena を利用した解析やAmazon Quick Sightを利用した可視化に使いたいと考えられる方もいるかもしれません。その場合、最終的にAmaonz Simple Storage Service(Amazon S3)に格納することが第一歩になるかと思います。その後、S3のライフサイクルポリシーの活用や、各種サービスへの連携が可能となります。CloudWatch Logsに出力されたログデータはSubscription Filterを利用してAWS Lambda経由で別の場所にExportし保存することができますが、当記事では2020年に登場したLambda ExtensionsのExternal extensions の機能を利用してAmazon S3にログを出力する方法について試した結果を共有したいと思います。なお、今回はGitHubのAWS Samplesを動かしてみた結果をもとに書いた記事となります。

Internal extensionsについては、[こちら](https://qiita.com/_kensh/items/ce620ec5b53c623775ae)で解説がされていますのでご確認ください。
# サマリ
- AWS Lambda Logs APIを利用することでLambda 関数のログを制御可能
- ログの種類は、Platform/Function/Extensionsの三種類があり任意のログを処理可能
- Lambda Extensionsのサンプルを利用すると容易にS3にログ出力が可能(Production用ではない）

# AWS Lambdaのログ
## AWS Lambda が出力するログ
AWS Lambda関数を実行したときに出力されるログについて整理します。

|種類|概要|出力のための実装|
|:--|:--|:--|
|開始|Lambda 関数の実行開始を示すログ|不要。自動出力|
|終了|Lambda 関数の実行完了を示すログ|不要。自動出力|
|レポート|Lambda 関数の実行完了後に使用した時間、リソース等を出力するログ|不要。自動出力|
|アプリケーション|関数内で標準出力・標準エラーに出力されたログ|コーディングが必要|

## サンプルコード

```python:app.py
import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

## CloudWatch Logsに出力されたログ
以下の画像にある通り、AWS Lambdaでは、アプリケーション開発者がログを出力するコーディングを一切しなくても1リクエストごとに3つのログが出力されます。
3つのログに加えて、アプリケーション内で出力する記載をするとログがさらに出力されます。

AWS Lambda関数から出力される標準出力のログは全てCloudWatch Logsに出力されます。
CloudWatch Logsに出力するためには、AWS Lambda 関数が CloudWatch Logsに出力する権限が必要となります。CloudWatch Logsに出力する権限を持っていないと以下のログは一切出力されません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/9fd7c3ec-c58a-a08f-f111-777981cc5744.png)

# Lambda Extensionsを利用したログの出力先制御
## Lambda Extensions
Lambda Extensionsは、既に利用中のモニタリング、オブザーバビリティ、セキュリティ、ガバナンス用ツールとLambdaとの統合を簡単にする新しい方法で、Lambda Extensionsの動作については、[こちらのブログ](https://aws.amazon.com/jp/blogs/news/introducing-aws-lambda-extensions-in-preview/)をご確認ください。なお、最大10個のExtensionsを登録可能です。


### Extension API
Extension APIを使うことで、開発者はAWS Lambda 関数の実行環境のライフサイクル(Initフェーズ, Invokeフェーズ, Shutdownフェーズ)に合わせて処理を実行することが可能となります。
Extension APiは、AWS Lambda Serviceから呼び出されるAPIの一つで、それ以外には、Runtime APIとLogs APIがあります。当記事ではLogs APIについても触れます。

Extension APIには、二種類の動作モードがあり、詳細はAWS ドキュメントで解説されていますが、簡単にまとめてみました。

## Internal extensions /External extensions

|種類|Internal extensions| External extensions|
|:--|:--|:--|
|実行時のプロセス|ランタイププロセスの一部として動く。つまり、Lambda 関数と同一プロセス内で実行|Lambda 関数のランタイムとは別個のプロセスを実行|
|使用するリソース|関数と同一実行環境を利用するためCPU/Memory等のリソースはLambda関数と共有する。|同左|
|初期化タイミング|Lambda 関数と同じタイミングで初期化。Invoke時に呼び出す関数を登録。Shutdownはランタイムの中で自動的に呼ばれるため登録不要|Lambda 関数の初期化よりも前に初期化の呼び出し。Invoke/Shutdown時に呼び出す関数を登録することに加え、ランタイムの初期化が動く前に実施したい処理を組み込み可能（例：常駐プロセスを起動する等の処理など)|
|起動タイミング|実装された関数のInvokeよりも前に起動|ランタイムや実装された関数よりも前に起動|
|実装方法|利用するライブラリや他の依存関係を内包したZIPアーカイブである Lambda レイヤーとしてデプロイ。|同左


### AWS Lambda 関数の実行環境のライフサイクル
前述した通り、三段階の実行フェーズがあります。
#### Init
Initは、Lambda ServiceからExtensions APIを通して呼び出され、External extensionsの初期化が最初に動き、次にRuntime APIが呼び出されの初期化が行われ、その中で、Internal Extensionsの初期化がされます。その後、関数の初期化がされます。Lambda は実行環境の /opt ディレクトリextensionsを含むLambda Layerを抽出します。Lambda は /opt/extensions/ ディレクトリ内のextensionsを探し、各ファイルをextensionsを起動するための実行可能なブートストラップとして解釈します。つまり、extensionsフォルダ内の実行ファイルをLambda Layerとして準備しておくことで、自動的に起動されます。

#### Invoke
InvokeはLambda関数の呼び出しとともに、Internal/ External のそれぞれのExtensionsがInitで登録した機能が呼び出されます。ExternalはRuntimeとは別のプロセスで動き、並行して動作します。Internal extensionsはRuntime、つまり関数と同じプロセスで動きます。

#### Shutdown
Lambda 関数が終了する際には、まず、Runtimeの終了処理が呼び出され、その後、Extensionsの終了処理が呼び出されます。なお、終了処理の実行時間には制限があります。AWSドキュメントをご確認ください。


## AWS Lambda Logs API
Lambda extensionsでは、[AWS Lambda Logs API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-logs-api.html)を利用して関数の実行環境のログストリームをサブスクライブしてログを処理（別のリソースへのログの転送、ログの変換、その他の処理）をすることが可能です。Kinesis Firehose経由でS3に出力することや他のAWSサービスや外部サービスにも転送可能です。つまり、任意の宛先にログを送りたい場合は、AWS Lambda extensionsを利用し、そこで、AWS Lambda Logs APIを利用することで、当記事ので実現したいことが可能となります。

### ログの種類
Logs APIを利用することで以下の3種類のログのログストリームをサブスクライブすることができます。なお、サブスクライブはInitフェーズで実施する必要があります。初期化が完了した後では、サブスクライブ処理は受け付けられません。また、1つのExtensionsからのサブスクライブリクエストは冪等です。つまり、１つのExtensioからは重複した複数のサブスクライブはできない仕様のようです。

- 標準出力・標準エラーのログ
- START, END, REPORT ログ。Lambda のプラットフォームログ
- Lambda Extensionsログ

### Logs APIの利用時に必要なこと
Logs APIの詳細は[AWS Lambda Logs API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-logs-api.html)に記載されています。必要なこととしては、Logs APIを利用してログを受け取るプロトコル(HTTP/TCP）やログをバッファリングするために利用するメモリの最適化のためのバッファリング設定などとなります。

## 利用したLambda Extenstionsのサンプル
今回は、GitHubのAWS Samplesにある[s3-logs-extension-demo-zip-archive](https://github.com/aws-samples/aws-lambda-extensions/tree/main/s3-logs-extension-demo-zip-archive)を利用して動作確認しました。サンプル構成は以下の通りです。サンプルを利用するだけのため構成手順は簡易ですが、私が検証したときには、READMEから漏れている手順があり、`chmod +x extensions/logs_api_http_extension.py` を実行せずデプロイすると以下のエラーログが出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/482f1595-0d9f-d33e-87a0-7e061901d240.png)

```text:構築手順

git clone https://github.com/aws-samples/aws-lambda-extensions
cd s3-logs-extension-demo/
chmod +x extensions/logs_api_http_extension.py
sam build
sam deploy --stack-name s3-logs-extension-demo --guided
```

なお、画像のログは、Platformのログ（START,End）とExtensionのログが出ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/41f4e16a-3a3b-ccfd-69cc-e6ac45a725d1.png)

権限エラーを解消することで、以下のログがでます。なお、`sample sample sample`は私が print 文でログを出したものです。

```python:lambda_function.py
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0

import json
import os

def lambda_handler(event, context):
    print(f"Function: Logging something which logging extension will send to S3")
    print(f"sample sample sample ")
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535037/6223c0b6-b458-9f13-debe-94398fcfce92.png)

### 実装概説
まず、このサンプルは本番環境用で利用する場合は十分にテストし、必要な改善を実施する必要があります。（本番環境用ではないと書かれてます）

今回のサンプルでは、extensionssrc以下がLambda Layerとしてデプロイされるように構成されており、直下に`logs_api_http_extension.py`があります。
前述した通り、Init時にLambdaはこのプログラムを実行します。

#### Initフェーズで実行する以下の内容を実装しています。

1. Extension APIを利用してInvoke時とShutdown時のイベントを受け取るように登録する
2. Logs APIを利用したLog Streamのサブスクリプション(受け取るポート、受け取るログの種類(Platform/Function)、バッファリング構成）
3. HTTPサーバを起動し、Logs APIのサブスクリプションを受け取れる準備をする（今回は4243ポートをListen)
4. Extensionsとして、Invoke/Shutdownへ登録(実際には、単にnextを呼び出しているだけでInvoke/Shutdown時に特に処理は何もしていない）
5. 無限ループでキューに格納されたログをS3に出力する処理
#### Invoke/Shutdown
特に何もしていません。今回は、Logs APIをSubscribeした際に登録したポート（HTTP ServerでListenしているポート）にログデータが送信されてきたとき、キューにデータを格納し、順次ログを出力するように実装しています。Invoke/Shutdownで独自処理をしているわけではないため何も実装・実行されないです。


# まとめ
AWS Lambdaのログは今までCloudWatch Logsに出すことが標準でしたが、これからは、Lambda Extensionsを利用しLogs APIを利用することで任意の場所に"も"ログを出力できるようになります。
独立したプロセスとして並行で動かす必要がないケース、例えば、Lamnbda関数に対して関数実行前後に処理を組み込みたい場合はExternal extensionsだけではなく、Internal Extensionsを利用することも可能です。
ぜひ、サンプルを利用して検証してみてください。
なお、その分、実行時間、リソース消費は発生しますのでご注意ください。