CloudFormationのマクロ機能でLambda関数と一緒にCloudWatch LogsのLog Groupを自動作成してみる
はじめに
サーバーレス開発部@大阪の岩田です。 現在サーバーレス開発部では、AWSリソースの管理にやデプロイにAWS SAMを用いることが多いのですが、下記のブログでも紹介されているように普通にAWS SAMでLambda関数を作成すると、CloudWatch LogsのLog Groupが自動生成されません。
【小ネタ】AWS SAMでLambda関数を作成する場合はCloudWatch LogsのLog Groupも同時に作った方がいいという話
SAMテンプレートで明示的にLog Groupを作れば良いのですが、どうしても単調な記述の繰り返しになり、Lambdaの数が増えてくるとテンプレートがゴチャゴチャしてきます。できればServerlessFlameworkのように、勝手にLog Groupを作成して欲しいところです。
先月から使えるようになったCloudFormationのマクロを使うことで、このあたりの作業を省力化できたので、手順をご紹介します。
CloudFormationのマクロ機能とは
CloudFormationのマクロとは、Lambda関数を使ってCloudFormationテンプレートの記法を拡張できる機能です。AWS SAMをイメージして頂ければ分かり良いかと思います。
AWS SAMではテンプレートにTransform: AWS::Serverless-2016-10-31
という記述を追加することで、サーバーレスアプリに必要なリソースを簡易な表現で記述することができます。
実際にはCloudFormationのチェンジセットを作成する際に、生のCloudFormationのテンプレートに変換されるのですが、CloudFormationのマクロを利用すると、自作のLambda関数でテンプレートを変換することができます。
AWSのニュースでは、下記のように記載されていました。
AWS CloudFormation Macros は、CloudFormation テンプレートで、検索や置換などの単純な操作からテンプレート全体の変換までカスタム処理を実行できます。CloudFormation Macros では、AWS::Include 変換および AWS::Serverless 変換の強化と同じテクノロジーを使用しています。CloudFormation 変換は、AWS インフラストラクチャの式をコードとして凝縮し、テンプレートコンポーネントの再利用を可能にすることによって、テンプレート作成を簡素化します。
https://aws.amazon.com/jp/about-aws/whats-new/2018/09/introducing-aws-cloudformation-macros/
動作の詳細についてはAWSのブログが詳しいです。
AWS CloudFormation を AWS Lambda によるマクロで拡張する
マクロの使い方
実際に自作のマクロを使うには、マクロの定義と実装を行った後に、CloudFormationでマクロをデプロイする必要があります。
マクロの定義
マクロを定義するにはCloudFormationのテンプレートでType: "AWS::CloudFormation::Macro"
のリソースを定義します。
YAMLの場合は
Type: "AWS::CloudFormation::Macro" Properties: Description: String FunctionName: String LogGroupName: String LogRoleARN: String Name: String
といった定義になります。
FunctionName
では後述するマクロの実装であるLambda関数名を指定します。
このCloudFormationテンプレートのデプロイが完了すると、Name
で定義された名前を指定することでマクロを呼び出すことが可能になります。
マクロの実装
マクロの実装はLambdaを利用し、イベント引数で渡されたペイロードを基に適切なアウトプットを作成します。
マクロのインプット
マクロが実行される時、イベントとして下記のようなペイロードがLambdaに渡されます。
{ "region": "<リージョン>", "accountId": "<AWS アカウントID>", "fragment": { ... }, "transformId": "<自動採番されたTRANSFORM_ID>", "params": "<インライン変換の場合は、Parametersで渡されたパラメータが設定される>", "requestId": "<自動採番されたREQUEST_ID>", "templateParameterValues": "<テンプレート内で指定されたParameters>" }
マクロのアウトプット
自作のLambda関数で諸々の処理を行った後、最終的に下記のようなJSONを返却します。
{ "requestId": "<イベントで渡されたリクエストID>", "status": "success", "fragment": {...} }
ポイントはfragment
です。
この中身に変換後のCloudFormationテンプレートを設定することで、変換後のテンプレートを用いてチェンジセットが作成されます。
トップレベルでの使用とインライン変換での使用
マクロを使う方法は
- トップレベルで使用する
- インライン変換で使用する
という2通りの使用方法があります。
トップレベルで使用
AWSTemplateFormatVersion: '2010-09-09' Transform: [MyMacro, 'AWS::Serverless-2016-10-31']
のように、テンプレート最上位のTransform
でマクロを指定する方法です。
この場合、テンプレートの全てが変換対象となります。
また、複数のマクロが指定されている場合はTransform
のリストに記載された順(左→右)に変換が実行されます。
Lambdaが受け取るイベント引数は
{ "region": "<リージョン>", "accountId": "<AWSアカウントID>", "params": {}, "fragment": { "AWSTemplateFormatVersion": "2010-09-09", "Outputs": "{テンプレートで定義したOutputs}", "Resources": "{テンプレートで定義したResources}", "Description": "<テンプレートで指定したDescription>", "Parameters": {} }, "transformId": "<AWSアカウントID>::<マクロ名>", "params": {}, "requestId": "<自動採番されたリクエストID>", "templateParameterValues": "<テンプレートのParametersで指定したパラメータ>" }
のような形式になります。
fragment
の中にCloudFormationテンプレートの中身が丸々入るようなイメージです。
インライン変換で使用
個々のリソースを定義する際にFn::TransForm
関数を使ってマクロを呼び出す方法です。
例えば
AWSTemplateFormatVersion: 2010-09-09 Resources: MyS3Bucket: Type: 'AWS::S3::Bucket' Fn::Transform: Name: MyMacro Parameters: Key: Value
のような形式でマクロを呼び出します。
Parameters
でマクロに渡す引数を設定できるのがポイントです。
この場合、Lambdaが受け取るイベント引数は
{ "region": "<リージョン>", "accountId": "<>", "fragment": { "Type": "AWS::S3::Bucket" }, "transformId": "<AWSアカウントID>::MyMacro", "params": { "Key": "Value" }, "requestId": "<自動採番されたリクエストID>", "templateParameterValues": {} }
のようになります。
fragment
の中身にはFn::Transform
を記述したリソースだけが入ります。
また、Fn::TransForm
関数で指定した引数がparams
に渡っているのが確認できるかと思います。
やってみる
実際にマクロを自作してみます。
マクロの実装
今回作成したマクロのコードです。
def handler(event, context): if "Resources" not in event["fragment"]: return { "requestId": event["requestId"], "status": "success", "fragment": event["fragment"], } log_groups = {} for key,resource in event.get("fragment",{}).get("Resources",{}).items(): if resource.get("Type") == "AWS::Lambda::Function": log_groups[f"{key}LogGroup"] = { "Type": "AWS::Logs::LogGroup", "Properties":{ "LogGroupName": { "Fn::Sub": [ "/aws/lambda/${FuncName}", { "FuncName": {"Ref" : key }} ] }, "RetentionInDays": event["templateParameterValues"].get("LogRetentionDays",365), } } event["fragment"]["Resources"].update(log_groups) return { "requestId": event["requestId"], "status": "success", "fragment": event["fragment"], }
fragmentの中にテンプレートで定義した諸々のリソースが入ってくるので、このマクロでは、AWS::Lambda::Function
リソースが指定されている場合に、自動的にAWS::Logs::LogGroup
リソースを作成して、レスポンスのfragmentにマージするようにしています。
また、ログの保持期間はテンプレートのパラメータでLogRetentionDays
が指定されている場合はその値を、指定されていない場合は365日をデフォルト値として採用しています。
※実際に利用する際は、リソース名の重複チェックやエラーハンドリングを適宜追加して下さい。
マクロの定義
上記のLambdaと合わせてAWS SAMでマクロの定義を作成しました
マクロの名前はMyMacro
としています。
Transform: AWS::Serverless-2016-10-31 Resources: Function: Type: AWS::Serverless::Function Properties: Runtime: python3.6 CodeUri: . Handler: index.handler Macro: Type: AWS::CloudFormation::Macro Properties: Name: MyMacro FunctionName: !GetAtt Function.Arn
デプロイします
aws cloudformation package --template-file macro.yml --s3-bucket <デプロイに利用するS3バケット名> --output-template-file macro-output.yml aws cloudformation deploy --template-file macro-output.yml --stack-name <スタック名> --capabilities CAPABILITY_IAM
マクロ無しでAWS SAMのテンプレートをデプロイ
マクロの準備ができたので、AWS SAMのテンプレートをデプロイしてみます。
sam init --runtime python3.6
で作成したpython3.6のSAMアプリをデプロイします。 まずはマクロ無し版です。
AWSTemplateFormatVersion: '2010-09-09' Transform: 'AWS::Serverless-2016-10-31' Description: > sam-app Sample SAM Template for sam-app # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 3 Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: hello_world/build/ Handler: app.lambda_handler Runtime: python3.6 Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object Variables: PARAM1: VALUE Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get Outputs: HelloWorldApi: Description: "API Gateway endpoint URL for Prod stage for Hello World function" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" HelloWorldFunction: Description: "Hello World Lambda Function ARN" Value: !GetAtt HelloWorldFunction.Arn HelloWorldFunctionIamRole: Description: "Implicit IAM Role created for Hello World function" Value: !GetAtt HelloWorldFunctionRole.Arn
aws cloudformation package --template-file template.yaml --s3-bucket <デプロイに利用するS3バケット名> --output-template-file output.yml aws cloudformation deploy --template-file output.yml --stack-name <スタック名> --capabilities CAPABILITY_IAM
blog-HelloWorldFunction-1P6EPXH6K0463
というLambda関数がデプロイされましたが、Cloud Watch Logsには/aws/lambda/blog-HelloWorldFunction-1P6EPXH6K0463
というロググループが作成されていません。
マクロ有りでAWS SAMのテンプレートをデプロイ
先ほどデプロイしたテンプレートを修正、マクロを使うように変更して再度デプロイしてみます。
AWSTemplateFormatVersion: '2010-09-09' Transform: ['AWS::Serverless-2016-10-31', MyMacro] Parameters: LogRetentionDays: Type: Number AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827,3653] Default: 365 Description: > sam-app Sample SAM Template for sam-app # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: Timeout: 3 Resources: HelloWorldFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: hello_world/build/ Handler: app.lambda_handler Runtime: python3.6 Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object Variables: PARAM1: VALUE Events: HelloWorld: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: Path: /hello Method: get Outputs: HelloWorldApi: Description: "API Gateway endpoint URL for Prod stage for Hello World function" Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" HelloWorldFunction: Description: "Hello World Lambda Function ARN" Value: !GetAtt HelloWorldFunction.Arn HelloWorldFunctionIamRole: Description: "Implicit IAM Role created for Hello World function" Value: !GetAtt HelloWorldFunctionRole.Arn
aws cloudformation package --template-file template.yaml --s3-bucket <デプロイに利用するS3バケット名> --output-template-file output.yml aws cloudformation deploy --template-file output.yml --stack-name <スタック名> --capabilities CAPABILITY_IAM
デプロイが完了すると・・・
今度はCloud Watch Logsに/aws/lambda/blog-HelloWorldFunction-1P6EPXH6K0463
というロググループが作成されました!!
ログの保持期間も365日になっています。
CloudFormationのスタック詳細の方にもバッチリ表示されています。
成功です!!
最後に
CloudFormationのマクロ機能について見てきました。 うまく利用することで、SAMテンプレートを簡素化できる良い機能だと感じました。 色んなプロジェクトで共通の困りごとを吸い上げて、サーバーレス開発部標準のマクロを作ってみるのも面白いかもしれません。
誰かのお役に立てば幸いです。