こんにちは。
先日、CloudFormationのカスタムリソースをLambdaで作成していた時にハマったので共有です。
プロローグ
CloudFormationには、対応していないリソースタイプを実現するためのカスタムリソースという仕組みがあるのはご存じのことと思います。CloudFormationテンプレートでカスタムリソースを作成する場合、カスタムリソースの定義であるAWS::CloudFormation::CustomResource
(外箱)と、その動作を実装する実体(中身)を作成することになります。このとき、外箱と中身が連携する必要がありますが、外箱から中身への情報連携は中身を指し示すARNおよび入力パラメータで、中身から外箱への結果報告はサービストークンにより行われます。
中身に該当する部分については、Amazon SNSトピックもしくはAWS Lambda関数により実装することとなっており、今回、自分はLambda関数によりその処理を実装していました。
Lambda関数は用意されているさまざまなランタイムで記述することができますが、今回はPythonを使用して作成していました。
その時に起こったのです。「cfnresponse
が見つからない」って言われる...。
何が起こったのかよくわからなかったので、調べるついでにちょっと実験してみました。
仕様を知る
ドキュメントによればこのように書いてあります。
cfn-response モジュールは、ZipFile プロパティを使用してソースコードを作成した場合にのみ使用できます。Amazon S3 バケットに保存されたソースコードには使用できません。バケットのコードでは、独自の関数を作成してレスポンスを送信する必要があります。
つまり、ロジックをZipファイル化してS3バケットに配置してのデプロイでは利用できない、ということですね。
使い方は同じくドキュメントより。
Python の場合は、次の例に示すように、import ステートメントを使用して cfnresponse モジュールをロードします。
1 import cfnresponseNote: Use this exact import statement. If you use other variants of the import statement, CloudFormation doesn't include the response module.
注記:この完全インポートステートメントを使用します。インポートステートメントの他の形式では、CloudFormation では応答モジュールが含まれません。
最後の文章がよくわかりませんが、「まあつまりインポートすれば使えるんでしょ」というふうにとらえておきます。(これが落とし穴だったと知ることは当時は知る由もなく...)
試してみる
いつも通りCloudFormationテンプレートで作成します。
Pythonコード部分以外は共通ですね。IAMロールなどは別途作ってありますのでパラメータ化して省略しています。
また、コードをZipFileプロパティで埋め込む必要がありますので、コードの編集などはテンプレートを直接いじる方法で行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{ "AWSTemplateFormatVersion": "2010-09-09", "Parameters": { "RoleArn": { "Type": "String" } }, "Resources": { "Function": { "Type": "AWS::Lambda::Function", "Properties": { "Handler": "index.lambda_handler", "Runtime": "python3.12", "Role": { "Ref": "RoleArn" }, "Timeout": 10, "Code": { "ZipFile": { "Fn::Join": [ "\n", [ "import boto3, cfnresponse", "import json", "", "def lambda_handler(event, context):", " print('Hello, world!')", "" ] ] } } } } } } |
以降の各パターンではPythonコード部分のみを示していきます。
モジュールをカンマで列記するPart1
pythonは複数のモジュールをすべて普通にインポートする場合、モジュール名のカンマ列記が可能なので試してみます。
1 2 3 4 |
import json, boto3, cfnresponse def lambda_handler(event, context): print('Hello, world!') |
エラーになりました。
cfnresponseモジュールが見つからない、とお怒りです。
一回言ってくれればいいところ、initフェーズとinvokeフェーズの両方で二倍怒られています。
ここだけは一粒で二度おいしくなくて結構です。
1 2 3 4 |
[ERROR] Runtime.ImportModuleError: Unable to import module 'index': No module named 'cfnresponse' Traceback (most recent call last):INIT_REPORT Init Duration: 368.96 ms Phase: init Status: error Error Type: Runtime.ImportModuleError [ERROR] Runtime.ImportModuleError: Unable to import module 'index': No module named 'cfnresponse' Traceback (most recent call last):INIT_REPORT Init Duration: 4221.60 ms Phase: invoke Status: error Error Type: Runtime.ImportModuleError |
モジュールをカンマで列記するPart2
最後に置いたからダメだったんですか?
最初に書いてあげましょう。
1 2 3 4 |
import cfnresponse, json, boto3 def lambda_handler(event, context): print('Hello, world!') |
動いちゃいました。
成功パターンを知っている前提でこの記事を書いてはいるのですが、これは正直予想外でした。
1 |
Hello, world! |
モジュールをひとつずつ読み込む
基本に立ち返った書き方も試しておきましょう。
1 2 3 4 5 6 |
import cfnresponse import json import boto3 def lambda_handler(event, context): print('Hello, world!') |
これは予想通りです。当然動きますよね(笑)
1 |
Hello, world! |
おまけ:NGパターンをOKパターンに変えられるか
ここまでのテストは、すべて「新規に作成したLambda関数」で試しています。
おまけとして、「NGパターンでデプロイしちゃったけど、後からの上書きで動くようになるのか」を試してみます。
最初は下記のコードでLambda関数を作成します。
先に書いた列記型Part1ですね。
1 2 3 4 |
import json, boto3, cfnresponse def lambda_handler(event, context): print('Hello, world!') |
想定通りエラーになります。
同じ実験をしてますので当然ですが、ちゃんと再現性がありますね。
1 2 3 4 |
[ERROR] Runtime.ImportModuleError: Unable to import module 'index': No module named 'cfnresponse' Traceback (most recent call last):INIT_REPORT Init Duration: 318.86 ms Phase: init Status: error Error Type: Runtime.ImportModuleError [ERROR] Runtime.ImportModuleError: Unable to import module 'index': No module named 'cfnresponse' Traceback (most recent call last):INIT_REPORT Init Duration: 3832.00 ms Phase: invoke Status: error Error Type: Runtime.ImportModuleError |
ここから、マネジメントコンソール上でLambda関数のコードを下記のように変更します。
import
部分をいじるだけです。
1 2 3 4 5 6 |
import cfnresponse import json import boto3 def lambda_handler(event, context): print('Hello, world!') |
これで実行してみると...同じエラーが出ました。
つまり、書き方を間違えたからと言って後からいじっても後の祭りで、新規作成時に認識してもらえないとダメなことがわかります。
1 2 3 4 |
[ERROR] Runtime.ImportModuleError: Unable to import module 'index': No module named 'cfnresponse' Traceback (most recent call last):INIT_REPORT Init Duration: 136.60 ms Phase: init Status: error Error Type: Runtime.ImportModuleError [ERROR] Runtime.ImportModuleError: Unable to import module 'index': No module named 'cfnresponse' Traceback (most recent call last):INIT_REPORT Init Duration: 1420.06 ms Phase: invoke Status: error Error Type: Runtime.ImportModuleError |
まとめ
PythonランタイムでのLambda関数におけるcfnresponse
モジュールは、書き方にうるさいことがよくわかりました。「完全インポートステートメント」というのも、結果を知ればなるほど、という感じですね。(でも英語版ドキュメントでは"Use this exact import statement"となっていたので、英語版ドキュメントを読んでいればもう少し早く正解にたどり着けていたかも...というのは反省点ですね)
また、「正確なステートメント」についてですが、最後の改行文字まで込みで見られているのかと思いきや、後ろに続く分には問題なく、「import cfnresponse」の連続する17バイトが存在していればOKというのは新たな発見でした。
それではまた!
投稿者プロフィール
- 根っこはインフラ屋な古いおじさん。
最新の投稿
- AWS2024年11月1日【小ネタ】cfnresponseが見つからない?
- CloudFormation2024年10月23日スポットインスタンスな起動テンプレートのインスタンスサイズを可変にしたい
- AWS2023年11月14日DLQを積み重ねる
- SQS2023年10月2日1分より短いサイクルで定期的にLambdaを実行する