CloudFormationでLambdaを実行する

実行例

AWS Lambda-backed カスタムリソースを使います。
以下のテンプレートでは、CloudFormationの入力パラメータとしてArg1、Arg2の2つの数を受取り、足し算した結果を出力します。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "lambda test",

"Parameters" : {
"Arg1" : {
"Type": "Number"
},
"Arg2" : {
"Type": "Number"
}
},

"Resources" : {
"LambdaTest": {
"Type": "Custom::LambdaTest",
"Properties": {
"ServiceToken": { "Fn::GetAtt" : ["LambdaTestFunction", "Arn"] },
"Arg1": { "Ref": "Arg1" },
"Arg2": { "Ref": "Arg2" }
}
},
"LambdaTestFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile" : { "Fn::Join" : ["\n", [
"import cfnresponse",
"",
"def handler(event, context):",
"",
" print(event)",
" print(vars(context))",
"",
" arg1 = event['ResourceProperties']['Arg1']",
" arg2 = event['ResourceProperties']['Arg2']",
"",
" responseData = {}",
" responseData['Sum'] = int(arg1) + int(arg2)",
" cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)"
]]}
},
"Handler": "index.handler",
"Runtime": "python3.6",
"Timeout": "30",
"Role": { "Fn::GetAtt" : ["LambdaExecutionRole", "Arn"] }
}
},
"LambdaExecutionRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": ["lambda.amazonaws.com"]},
"Action": ["sts:AssumeRole"]
}]
},
"Path": "/",
"Policies": [{
"PolicyName": "lambdatest",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"],
"Resource": "arn:aws:logs:*:*:*"
}]
}
}]
}
}
},
"Outputs" : {
"Sum" : {
"Description" : "Sum of Arg1 and Arg2.",
"Value" : { "Fn::GetAtt": [ "LambdaTest", "Sum" ] }
}
}
}

上記の例ではLambdaのソースコードを直接テンプレート内に書いています。
AWS ユーザーガイドによると、インラインで書けるのは現在nodejs4.3、python2.7、nodejs6.10、および python3.6 ランタイム環境の場合のみです。
該当環境でもインラインで書けるソースコードの長さに制限(最大4096文字)があるため、長いソースコードの場合は別ファイルに分ける必要があります。

ソースコードを別ファイルに分けると以下のようになります。
cfnresponseモジュールはテンプレート内でしか使えないとのことなので、その処理相当のこと書かなれければなりません。
AWS ユーザーガイド内にソースコードが公開されているのでそれを使います。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from botocore.vendored import requests
import json

SUCCESS = "SUCCESS"
FAILED = "FAILED"

def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False):
responseUrl = event['ResponseURL']

print(responseUrl)

responseBody = {}
responseBody['Status'] = responseStatus
responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name
responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name
responseBody['StackId'] = event['StackId']
responseBody['RequestId'] = event['RequestId']
responseBody['LogicalResourceId'] = event['LogicalResourceId']
responseBody['NoEcho'] = noEcho
responseBody['Data'] = responseData

json_responseBody = json.dumps(responseBody)

print("Response body:\n" + json_responseBody)

headers = {
'content-type' : '',
'content-length' : str(len(json_responseBody))
}

try:
response = requests.put(responseUrl,
data=json_responseBody,
headers=headers)
print("Status code: " + response.reason)
except Exception as e:
print("send(..) failed executing requests.put(..): " + str(e))

def lambda_handler(event, context):

print(event)
print(vars(context))

arg1 = event['ResourceProperties']['Arg1']
arg2 = event['ResourceProperties']['Arg2']

responseData = {}
responseData['Sum'] = int(arg1) + int(arg2)

send(event, context, SUCCESS, responseData)

上記ソースコードをファイル名「lambda_test.py」で保存し、zip圧縮します。zipファイル名「program.zip」とし、S3の任意のバケットへアップロードします。
この場合、CloudFormationテンプレート内のLambdaリソース部分は以下の通りになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
"LambdaTestFunction": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": "S3 のバケット名",
"S3Key": "program.zip"
},
"Handler": "lambda_test.lambda_handler",
"Runtime": "python3.6",
"Timeout": "30",
"Role": { "Fn::GetAtt" : ["LambdaExecutionRole", "Arn"] }
}
}

例外処理について

例外がハンドルされなかった場合、どのような動きになるでしょうか。
0除算で試してみると、CloudFormationが処理中(状況がCREATE_IN_PROGRESS)のまま進行しません。
CloudWatchのログを確認すると、例外発生後約1分後に再実行、その後約2分後に再度実行されていることが分かります。

処理中のまま放置しておくと約1時間くらいでスタック作成に失敗します。
スタック作成失敗後、又はスタック作成失敗を待ちきれずにスタック作成中の状態でスタックの削除を実行すると削除時に再度Lambdaが実行されるので約1時間待つことになります。
スタック削除に失敗(状況がDELETE_FAILED)した後、再度スタック削除を実行するとようやく削除出来ます。

どうしてこのような挙動になるのか調べてみると、AWS ユーザーガイドに非同期でLambdaを実行した場合処理しきれない場合2回自動で再試行するとの記述がありました。
再試行でも駄目な場合、CloudFormationだと処理中のまま約1時間ほど待つことになることについての記述は見つけられませんでした。
LambdaリソースにDeadLetterConfigなるプロパティがあり、これは使えば何かしら制御できるかも知れませんが詳しくは調べていません。

スタック更新、削除時の動作

最初に示した実行例ではLambdaはスタックの更新、削除時にも実行されます。
CloudWatchのログを確認すると引数eventはプロパティとしてRequestTypeを持っていることが分かり、このプロパティを使って制御できます。
削除時は何も実行せずに即リターンさせるにはソースコードのはじめに以下のような処理を入れます。

テンプレートにインラインの場合

1
2
3
4
if event['RequestType'] == 'Delete':
responseData = {}
  cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
return

別ファイルの場合

1
2
3
if event['RequestType'] == 'Delete':
send(event, context, SUCCESS, {})
return

例外のハンドル

例外をキャッチした場合にリソース作成失敗とするには、cfnresponse.FAILEDを返せばよいです。
スタックのロールバック時に削除で再度Lambdaが実行されるので注意します。前述のようにevent[‘ResponseType’] で制御しましょう。

1
2
3
4
5
6
try:
1 / 0
except Exception:
print(\"error occurred.\")
cfnresponse.send(event, context, cfnresponse.FAILED, {})
return

まとめ

  • CloudFormationではLambdaのソースコードはテンプレート内に書く方法と別ファイルで用意する方法の2種類がある
  • 例外がハンドルされなければ自動で2回再実行される、再実行でも駄目な場合スタックの状況が処理中から他の状況へ遷移するまで約1時間ほどかかる
  • 更新、削除時も実行される、ソースコード内で引数をもとに処理内容を分けることができる
  • 例外をキャッチ時にリソース作成失敗とするにはcfnresponse.FAILEDを返せばよい

参考

AWS Lambda-backed カスタムリソース
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html

AWS Lambda 関数コード
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

再試行動作について
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/retries-on-errors.html

AWS::Lambda::Function
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html

デッドレターキュー
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/dlq.html