Lambda での Python コーディングにおける必須知識 - JSON とログ出力

Lambda での Python コーディングにおける必須知識 - JSON とログ出力

本記事はJapan AWS Jr. Champions Advent Calendar 2024の記事です。

目次

    はじめに

    クラウドインテグレーション部の井川です。

    本記事は、AWS Lambda を Python で実装する際に知っておいた方が良い JSON データの取り扱いやログの出力方法について解説した記事です。

    特に、Lambda で初めてコーディングに触れたような方におすすめしたい内容になっています。

    こんな悲劇をなくしたい

    とある日、Lambda を呼び出すとエラーが発生したので、原因を探るために CloudWatch Logs で処理対象の JSON を確認することにしました。

    (コード内で JSON を出力する処理を入れていて良かった...。)

    print(invoking_event['configurationItem'])

    出力を見よう。

    CloudWatch だと 1 行になっていて見づらい...。
    python-object-on-cloudwatch

    VSCode にコピペして整形させるか...。python-object-on-vscode

    JSON の形式を満たしていないようですね。

    JSON として認識させるには、'  " に置換して、None  "None" にして...

    確認のたびにこんな作業をするのは恐ろしいですね。

    悲劇を防ぐには

    json.dumps() する。

    print(json.dumps(invoking_event['configurationItem']))

    json.dumps() しておけば、CloudWatch でもシンタックスハイライトや整形が適用され、見やすく表示してくれます。json-on-cloudwatch

    もちろん VSCode でも JSON として認識、整形してくれます。json-on-vscode

    悲劇を防ぐことができました。

    json.dumps() によって、「Python オブジェクトを表示する処理」が「JSON の文字列を表示する処理」に変わっています。

    Python オブジェクトと JSON

    いきなり Python オブジェクトという用語が出てきたので解説します。

    Python オブジェクトとは Python で操作することのできる整数や実数、文字列等のデータのことを指しています。
    Python で扱うことのできるデータは、全て何かしらの Python オブジェクトだと考えて問題ありません。

    一方で、JSON はテキスト形式のデータフォーマットです。
    AWS 上でポリシーやメッセージの定義等、様々な場面で用いられているためこちらは既にご存じかと思います。

    JSON はただのテキストデータなので、そのままでは Python 上で「キーを指定して値を取得する」というようなことはできません。

    Python で JSON 内のキーや値を取得したり、逆に Python で扱っているオブジェクトを JSON として出力したりするためには変換が必要です。
    Python 標準ライブラリである json を使用することで、次のように変換できます。

    • json.dumps():Python オブジェクト → JSON 形式のテキスト
    • json.loads():JSON 形式のテキスト → Python オブジェクト

    Python オブジェクトの出力と JSON の出力は似ている部分もありますが、論理値 True/False  true/false や、値がないことを表す None  null 等、そもそも記法が異なる部分もあります。

    冒頭の例では次のように記載していましたが、厳密には None は JSON では null に対応するので、"None" にしてしまうと元の JSON とは異なるデータになってしまいます。

    JSON として認識させるには、'  " に置換して、None  "None" にして...

    したがって、Python オブジェクトを操作しているのか、JSON のテキストを操作しているのかを意識して、json.dumps()  json.loads() を適切に使用することが必要です。

    基本的な Python ログ出力

    次に、Python でのログ出力について説明します。

    Python オブジェクトを表示するという意味で、もちろん print() を使用することもできます。

    しかし、Python にはログ用の標準ライブラリである logging が用意されているため、ログ出力ではこちらを使用する方が良いです

    logging の最も簡単な使用例を次に示します。

    import logging

    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)

    def lambda_handler(event, context):
    logger.debug('debug message')
    logger.info('info message')
    logger.warning('warning message')
    logger.error('error message')
    logger.critical('critical message')

    logging ではログのレベルが定義されており、logger.debug() 等を使用してレベルごとにログ出力を設定できます。

    logger.setLevel() により出力するログレベルを設定でき、詳細な動作の確認をする際に「DEBUG レベル以上のログを全て出力する」というような制御をすることが可能です。

    例えば、ログレベルに WARNING が設定されている場合、次のように WARNING 以上のレベルのログが表示されます。

    [WARNING]	0000-00-00T00:00:00.000Z		warning message
    [ERROR] 0000-00-00T00:00:00.000Z error message
    [CRITICAL] 0000-00-00T00:00:00.000Z critical message

    Lambda 上での Python ログ出力

    Lambda では、ログのフォーマットとして従来からあった「テキスト形式」と、新しく追加された「JSON 形式」が利用できます。

    テキスト形式

    ここまで取り上げていた例は、全てテキスト形式でのログ出力でした。

    先程説明した通り、json.dumps() を使用して JSON に変換すると良いのですが、1 つ注意点があります。

    それは、json.dumps() で見た目を整えるための indent を指定しないことです。

    indent を指定してしまうと、JSON を整形した際の行ごとに分かれて記録されてしまいます
    json-dumps-indent-cloudwatch

    indent を指定すると、JSON に変換する際にインデントや改行を挿入して見た目を整えてくれます。
    ただし、CloudWatch Logs ではログを 1 行ごとに認識して JSON を整えて表示する機能があるようで、上記のような表示になってしまいます。

    その他、ログレベルについてはコード内で logger.setLevel() 等で設定することになりますが、コードを更新しなくてもレベルを変えられるよう、環境変数を使用して渡すと良いかもしれません。

    JSON 形式

    JSON 形式のログ出力は、1 年程前からサポートするようになりました。

    コンソール上で確認すると、JSON 形式をおすすめするようなメッセージが表示されています。lambda-logging-configs

    ログ全体を JSON として扱うことで、データ操作等がしやすくなるというメリットがあります。
    また、画像に表示されている通りログレベルを設定することもできるようになります
    「アプリケーションログ」は先程説明した Python コード内で定義したログ、「システムログ」は Lambda サービスで定義されるログです。

    JSON 形式のログ出力ではいくつかメリットがありますが、logging で設定したメッセージはテキスト扱いで表示されるという点には注意です。

    logger.debug() に JSON を渡した場合、CloudWatch コンソール上では次のような表示になります。lambda-json-format-cloudwatch

    Python オブジェクトをそのまま表示した場合と見やすさの面ではあまり変わりませんが、データ処理のしやすさを考えると json.dumps() で JSON に変換してから出力しておくべきです。

    コンソール上で JSON として整形してもらうなら、extra に Python オブジェクトを指定する方法があります。

    メッセージとして extra に指定するオブジェクトの概要を記載し、extra には extraData をキーとしたオブジェクトを渡してみました。

        logger.debug(
    'check invokingEvent - configurationItem',
    extra={
    'extraData': invoking_event['configurationItem']
    }
    )

    extra に指定したオブジェクトは、CloudWatch 上で JSON として認識されます。lambda-json-format-with-extra-cloudwatch

    補足情報:Lambda の前処理・後処理

    最後に、Lambda の前処理と後処理について簡単に触れておきます。

    Lambda で定義するメイン処理のコードは、デフォルトでは lambda_function.py ファイル内の lambda_handler 関数で定義します。

    import json

    def lambda_handler(event, context):

    return {
    'statusCode': 200,
    'body': json.dumps('Hello from Lambda!')
    }

    前処理

    event には受け取った JSON イベントデータが格納されているのですが、このデータは既に Python オブジェクトに変換されています。
    これは Lambda の内部で、前処理として json.loads() のような処理が実行されているためです。
    したがって、event から受け取るデータは、既に「キーを指定して値を取得する」といった操作ができる状態になっています。

    次のようなイベントを受け取った場合、event['invokingEvent'] で値が取得できます。

    {
    "invokingEvent": "{\"configurationItem\":{\"configurationItemCaptureTime\":\"2016-02-17T01:36:34.043Z\",\"awsAccountId\":\"123456789012\",\"configurationItemStatus\":\"OK\",\"resourceId\":\"i-00000000\",\"ARN\":\"arn:aws:ec2:us-east-2:123456789012:instance/i-00000000\",\"awsRegion\":\"us-east-2\",\"availabilityZone\":\"us-east-2a\",\"resourceType\":\"AWS::EC2::Instance\",\"tags\":{\"Foo\":\"Bar\"},\"relationships\":[{\"resourceId\":\"eipalloc-00000000\",\"resourceType\":\"AWS::EC2::EIP\",\"name\":\"Is attached to ElasticIp\"}],\"configuration\":{\"param\":null}},\"messageType\":\"ConfigurationItemChangeNotification\"}",
    "ruleParameters": "{\"myParameterKey\":\"myParameterValue\"}",
    "resultToken": "myResultToken",
    "eventLeftScope": false,
    "executionRoleArn": "arn:aws:iam::123456789012:role/config-role",
    "configRuleArn": "arn:aws:config:us-east-2:123456789012:config-rule/config-rule-0123456",
    "configRuleName": "change-triggered-config-rule",
    "configRuleId": "config-rule-0123456",
    "accountId": "123456789012",
    "version": "1.0"
    }

    テキスト化された JSON の中の値("configurationItem" 等)を取得する場合には、改めて json.loads() が必要になります。

        invoking_event = json.loads(event['invokingEvent'])
    configuration_item = invoking_event['configurationItem']

    後処理

    同様に、Lambda が値を返す際には後処理として内部で json.dumps() が実行されています
    そのため、Lambda からレスポンスを返す際には、return に Python オブジェクトをそのまま渡すだけで良いです。

    json.dumps() して渡してしまうと、JSON 全体がテキスト化されて返されるようになります。

    import json

    def lambda_handler(event, context):

    response = {
    'statusCode': 200,
    'body': json.dumps('Hello from Lambda!')
    }

    return json.dumps(response)

    Lambda としてのレスポンスは次のようになり、うまく処理できない可能性が高いです。

    "{\"statusCode\": 200, \"body\": \"\\\"Hello from Lambda!\\\"\"}"

    このように、Lambda では JSON の受け渡しで前処理・後処理が動いていることも認識しておくと良いと思います。

    おわりに

    現在はクラウドインテグレーション部で AWS をメインに対応していますが、もともとコードを書くのも好きなので、今回は少しコーディング寄りの内容を取り上げてみました。

    ログは運用負荷にも効いてくる部分なので、後々苦労しないためにも利用しやすい形で吐いておくことはとても重要です。

    冒頭にも書きましたが、特に、Lambda で初めてコーディングに触れたような方におすすめしたい内容になっています。
    最初は Python を使用することが多いと思いますので、ぜひ参考にしてみてください。

    以下に関連するリンクをいくつか挙げていますが、「Python による Lambda 関数の構築」あたりのドキュメントに分かりやすくまとめられていますので、一度目を通してみると良いです。

    参考

    アジアクエスト株式会社では一緒に働いていただける方を募集しています。
    興味のある方は以下のURLを御覧ください。