Lamdbaの開発を便利にするPythonのアプリケーションフレームワークJeffyが公開されました

127件のシェア(ちょっぴり話題の記事)

CX事業本部@大阪の岩田です。

下記リンクの通りServerless OperationsさんからJeffyというOSSのアプリケーションフレームワークが公開されました。

AWS Python Lambda用のアプリケーションフレームワークJeffyをリリースしました!

サーバーレスなシステム開発ではServerless FrameworkやSAMを利用することが多いと思いますが、これらのフレームワークは「アプリケーションフレームワーク」ではありません。リソースの管理やデプロイを簡略化することはできても、アプリケーションのコードは開発者が全て実装する必要があり、コード量の削減や保守性の向上には寄与しません。じゃあDjangoのようなフレームワークを使ってLambdaを開発すればいいのか?というと、そういうわけでもありません。DjangoはLambdaの開発向けに設計されたフレームワークではないので、欲しい機能が実装されて無かったり、逆にサーバーレスなシステム開発では不要な機能が多数実装されていたりもします。フレームワーク自体ある程度大きなサイズになるので、コールドスタートへの悪影響も気になるところです。今回ご紹介するJeffyはLambdaの開発向けに設計された薄いフレームワークで、まさに「痒いところに手が届く」フレームワークになっています。

Jeffyとは?

JeffyはLambdaの開発に利用することを目的としたPython向けの「アプリケーションフレームワーク」です。 Jeffyを利用することでLambdaの開発における決まりきった共通処理の実装を簡略化することが可能です。

といったライブラリにインスパイアされており、上記のライブラリが備えているような

  • トレーシング
  • ロギング
  • デコレータによる処理の共通化

にフォーカスした設計になっています。GitHubのリポジトリは以下です。

https://github.com/serverless-operations/jeffy

日本発のOSSということも注目したいポイントですね。海外製のフレームワークと比べると、自分でissueやプルリクを上げるハードルも低いのではないでしょうか?

1つ注意点として2020/1/25現在ではBeta版のステータスになっています。

やってみる

実際にREADMEの内容をいくつか試してみましょう。以後の処理は全て2020/1/25時点で最新のバージョン0.1.3とPython3.8で実装しています。

ロギング

Lambdaからログを出力する際は、付随する情報としてLambdaのバージョンやメモリサイズを出力しておくと便利です。以前のブログではLambda Powertoolsを利用する方法をご紹介しました。

Lambdaのログ出力にオススメしたいPythonのライブラリLambda Powertools

Jeffyのログ出力もLambda Powertoolsと同じような感覚で利用することが可能です。

基本的なログ出力

from jeffy.framework import setup
app = setup()

def handler(event, context):
    app.logger.info({"foo":"bar"})

出力されたログです。

[INFO]	2020-01-25T04:38:08.683Z	d13185aa-6163-41f3-80d1-1fc748fa8a7b	{'message': {'foo': 'bar'}, 'aws_region': 'ap-northeast-1', 'function_name': 'jeffy-test', 'function_version': '$LATEST', 'function_memory_size': '128', 'log_group_name': '/aws/lambda/jeffy-test', 'log_stream_name': '2020/01/25/[$LATEST]b8453c7ac2274c90b97ddbbf81a77291'}

JSON形式ではないですがPythonの辞書オブジェクトが出力されているので、CloudWatch Logs Insightsを使って特定のキーを指定してログをフィルターするといったことが容易に実現できます。

ログに項目を追加する

自動出力される項目以外にもログに残したい項目がある場合はlogger.setupで出力項目を追加できます。以下はユーザー名、メールアドレスを追加出力する例です。

from jeffy.framework import setup
app = setup()

app.logger.setup({
   "username":"user1",
   "email":"user1@example.com"
})

def handler(event, context):
    app.logger.info({"foo":"bar"})

出力されたログです

[INFO]	2020-01-25T04:52:30.127Z	11b79502-7bc4-4356-b5c1-53d6032b80f1	{'message': {'foo': 'bar'}, 'aws_region': 'ap-northeast-1', 'function_name': 'jeffy-test', 'function_version': '$LATEST', 'function_memory_size': '128', 'log_group_name': '/aws/lambda/jeffy-test', 'log_stream_name': '2020/01/25/[$LATEST]0c8ab1778a0a409ba6496e68f6de58ba', 'username': 'user1', 'email': 'user1@example.com'}

CloudWatch Logs Insightsから以下のクエリを発行することでメールアドレスがuser1@example.comのログを抽出することが可能です。

fields @timestamp, @message
| filter email = "user1@example.com"
|sort @timestamp

デコレータ

Lambdaの開発では例外発生時のログ出力やイベントソースからのデータ取り出し・チェック・加工等、決まりきった定型的な処理を実装することが多くなります。こういった処理はPythonのデコレータを使うことで楽をすることができます。

PythonのデコレータでLambdaのコードをクリーンに保つ!!

Jeffyもデコレータベースで色々な機能を提供してくれます。

DynamoDBストリーム起動のLambda

DynamoDBストリームが有効化されたテーブルに以下のアイテムを登録してみます

{
  "Id": "1",
  "key1": "val1"
}

上記アイテムが登録された際にストリームに流れてくるデータを確認してみましょう。

from jeffy.framework import setup
app = setup()

def handler(event, context):
    app.logger.info(event)

ログからイベントデータだけ抜粋すると以下のような形式になります。

{'Records': [{'eventID': 'bb59b726ba8d6ec24c59e96bf54fc76d', 'eventName': 'INSERT', 'eventVersion': '1.1', 'eventSource': 'aws:dynamodb', 'awsRegion': 'ap-northeast-1', 'dynamodb': {'ApproximateCreationDateTime': 1579928744.0, 'Keys': {'Id': {'S': '1'}}, 'NewImage': {'key1': {'S': 'val1'}, 'Id': {'S': '1'}}, 'SequenceNumber': '135872800000000012967605757', 'SizeBytes': 14, 'StreamViewType': 'NEW_AND_OLD_IMAGES'}, 'eventSourceARN': 'arn:aws:dynamodb:ap-northeast-1:944137583148:table/dazn-lambda-powertools/stream/2019-12-28T07:14:26.696'}]}

何か色々とゴチャゴチャしてますね。decorator.dynamodb_streamを付与するとイベントデータから1アイテム分のデータを抽出してhandlerの処理に渡すことができます。

from jeffy.framework import setup
app = setup()

@app.decorator.dynamodb_stream
def handler(event, context):
    app.logger.info(event)

出力されたログです。

{'ApproximateCreationDateTime': 1579929022.0, 'Keys': {'Id': {'S': '1'}}, 'NewImage': {'key1': {'S': 'val1'}, 'Id': {'S': '1'}}, 'SequenceNumber': '135873000000000012967727844', 'SizeBytes': 14, 'StreamViewType': 'NEW_AND_OLD_IMAGES', 'correlation_id': '7e6f06e6-00d1-4058-9e2a-9d6487c87c38'}

handler内の処理には1レコード分のデータだけ渡ってきている事がわかります。デコレータ側でレコード数の分だけループ処理を行っているので、ストリームに複数のレコードが流れてきた場合は流れてきたレコードの分だけhandler内の処理が繰り返し実行されることになります。また、後述するcorrelation_idが元データに付与されていることも分かります。

トレース

サーバーレスなシステム開発では複数のLambdaを組み合わせて1つの処理を実装することも多いです。こういったアーキテクチャでは複数のLambdaを横断的に分析するために、何かしら共通のパラメータをLambda間で取り回すことが望ましく、Node.jsの場合はDAZN Lambda Powertoolsを使うことで複数のLambda間でx-correlation-idという属性値を取り回すことが可能になります。

Lambdaのログ出力にオススメしたいNode.jsのライブラリ DAZN Lambda Powertools

Jeffyも同様の機能を備えており、correlation_idという属性値を自動キャプチャしたり、自動付与する機能を持っています。

オブジェクトのメタデータにcorrelation_idを付与しつつS3にアップしてみる

JeffyのS3クラスを利用するとオブジェクトのメタデータにcorrelation_idを付与しつつS3にアップロードすることが可能です。以下のコードを試してみましょう。

from jeffy.framework import setup
from jeffy.sdk.s3 import S3

app = setup()

@app.decorator.api
def handler(event, context):
    
    with open('/tmp/hoge.txt', 'w') as f:
        f.write('hoge')
        
    S3.upload_file(
        file_path='/tmp/hoge.txt', 
        bucket_name='<適当なS3バケット>',
        object_name='hoge.txt',
        correlation_id=event['correlation_id']
    )

Lambdaのテストイベントを実行した場合もイベントデータにcorrelation_idが自動付与されるようにdecorator.apiのデコレータを付けています。

実行完了後にAWS CLIから確認してみましょう

$ aws s3api head-object --bucket <適当なS3バケット> --key hoge.txt
{
    "AcceptRanges": "bytes",
    "LastModified": "Sat, 25 Jan 2020 05:42:37 GMT",
    "ContentLength": 4,
    "ETag": "\"ea703e7aa1efda0064eaa507d9e8ab7e\"",
    "ContentType": "binary/octet-stream",
    "Metadata": {
        "correlation_id": "122716b1-6960-4189-a1ec-7d6f76fa1f73"
    }
}

メタデータが付与されていることが分かります。

若干余談になりますが、jeffy.sdk配下の各クラスはクラス変数を使ってboto3のクライアントを管理しており、初回実行時はboto3のクライアント生成処理が走りますが、2回目以後はクライアントの生成処理が不要になります。handler外でグローバル変数を定義してLambdaの初期化処理でboto3のクライアントを生成する実装と比較するとグローバル変数を減らせるというメリットがあります。

まとめ

ざっとJeffyの機能を調べてみましたが、大体自分が実装しているのと似たような共通処理が実装されていました。定型的な共通処理に関してはフレームワーク任せにすることで開発を省力化できる可能性があるので、今後もJeffyの開発動向を注目して見守って行きたいと思います。

また、現在はJeffyに実装されていない機能で「こういう機能があれば便利なのに...」という機能もいくつか思い浮かぶので、時間を見つけてissueやプルリクでコントリビューションしたいと思います。