本記事はJapan AWS Jr. Champions Advent Calendar 2024の記事です。
クラウドインテグレーション部の井川です。
本記事は、AWS Lambda を Python で実装する際に知っておいた方が良い JSON データの取り扱いやログの出力方法について解説した記事です。
特に、Lambda で初めてコーディングに触れたような方におすすめしたい内容になっています。
とある日、Lambda を呼び出すとエラーが発生したので、原因を探るために CloudWatch Logs で処理対象の JSON を確認することにしました。
(コード内で JSON を出力する処理を入れていて良かった...。)
print(invoking_event['configurationItem'])
出力を見よう。
CloudWatch だと 1 行になっていて見づらい...。
VSCode にコピペして整形させるか...。
JSON の形式を満たしていないようですね。
JSON として認識させるには、'
は "
に置換して、None
も "None"
にして...
確認のたびにこんな作業をするのは恐ろしいですね。
json.dumps()
する。
print(json.dumps(invoking_event['configurationItem']))
json.dumps()
しておけば、CloudWatch でもシンタックスハイライトや整形が適用され、見やすく表示してくれます。
もちろん VSCode でも JSON として認識、整形してくれます。
悲劇を防ぐことができました。
json.dumps()
によって、「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 オブジェクトを表示するという意味で、もちろん 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 では、ログのフォーマットとして従来からあった「テキスト形式」と、新しく追加された「JSON 形式」が利用できます。
ここまで取り上げていた例は、全てテキスト形式でのログ出力でした。
先程説明した通り、json.dumps()
を使用して JSON に変換すると良いのですが、1 つ注意点があります。
それは、json.dumps()
で見た目を整えるための indent
を指定しないことです。
indent
を指定してしまうと、JSON を整形した際の行ごとに分かれて記録されてしまいます。
indent
を指定すると、JSON に変換する際にインデントや改行を挿入して見た目を整えてくれます。
ただし、CloudWatch Logs ではログを 1 行ごとに認識して JSON を整えて表示する機能があるようで、上記のような表示になってしまいます。
その他、ログレベルについてはコード内で logger.setLevel()
等で設定することになりますが、コードを更新しなくてもレベルを変えられるよう、環境変数を使用して渡すと良いかもしれません。
JSON 形式のログ出力は、1 年程前からサポートするようになりました。
コンソール上で確認すると、JSON 形式をおすすめするようなメッセージが表示されています。
ログ全体を JSON として扱うことで、データ操作等がしやすくなるというメリットがあります。
また、画像に表示されている通りログレベルを設定することもできるようになります。
「アプリケーションログ」は先程説明した Python コード内で定義したログ、「システムログ」は Lambda サービスで定義されるログです。
JSON 形式のログ出力ではいくつかメリットがありますが、logging
で設定したメッセージはテキスト扱いで表示されるという点には注意です。
logger.debug()
に JSON を渡した場合、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 の前処理と後処理について簡単に触れておきます。
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 関数の構築」あたりのドキュメントに分かりやすくまとめられていますので、一度目を通してみると良いです。