[CloudFormation]Lambda-backedカスタムリソースを理解する
コンニチハ、千葉です。
CloudFormationを利用する上で、かゆいところに手が届く、それがカスタムリソースです。
例えば、EC2で最新のAMIを毎回検索してAMI IDを指定している!そんなことがあると思いますが、Lambda-backedカスタムリソースを使うと、自動で最新のAMIを検索して指定することができるようになります。これは、便利。因みに最新のAMIの取得は公式ドキュメントチュートリアル: Amazon マシンイメージ ID を参照するがありますので、これをやればすぐに対応できます。
では、早速Lambda-backedカスタムリソースを使いこなす上で必要なことを見ていきます。といっても、そんなに難しくないのでご安心を。
Lambda-backedカスタムリソースの仕組み
CloudFormation <-> Lambdaの連携です。
CloudFormationから必要なパラメータをLambdaへ渡し発火させます。そして、Lambdaで処理を行い成功/失敗のステータスをCloudFormationに返します。とてもシンプルです。
利用する上でやることは、2つです。
- CloudFormationでカスタムリソースを定義する
- Lambda関数を作成する
詳しく見てみます。
CloudFormationでカスタムリソースを定義する
---一部抜粋--- "GetServiceName": { "Type": "Custom::GetServiceName", "Properties": { "ServiceToken": "arn:aws:lambda:ap-northeast-1:123456789012:function:getEcsServiceName", "ServiceArn": "arn" } }
Type
:任意のタイプ名を指定します
ServiceToken
:発火させるLambdaのARNを指定します
ServiceArn
:任意のLambdaに渡したいものを指定します。変数みたいなものですね。なので名前はなんでもいいですし、複数定義しても問題無いです。今回は、ServiceArnと指定したのでLambda側でServiceArnとして受け取れます。
Lambda関数の作成
CloudFormationから呼ばれる関数を定義します。CloudFormationから値を受け取って、AWSのAPIを叩くなり、受け取った文字列を編集しCloudFormationに返したり、または最新のAMIを検索してCloudFormationに返したりと、この部分は任意の処理を実行することができます。
---一部抜粋--- exports.handler = function(event, context) { console.log("REQUEST RECEIVED:\n" + JSON.stringify(event)); var responseStatus = "SUCCESS"; var responseData = {}; responseData["Name"] = returnServiceName(event.ResourceProperties.ServiceArn); sendResponse(event, context, responseStatus, responseData); };
event.ResourceProperties.ServiceArn
:CloudFormationで指定したServiceArn
の値を取得して来て処理を行っています。
あとは、sendResponseにて処理をした結果をLambdaに返します。
この例には記載していませんが、event.RequestType
で、Create、Update、Deleteという値(CloudFormationで何が実行されたか)を取得することもできますので、処理にを場合分けすることもできます。
チュートリアル
チュートリアルとして、CloudFormationにより以下を実行してみます。
- ECSのServiceを起動
- 起動されたECS Service名を取得するLambda-backedカスタムリソースを実行
- 起動したECS ServiceのCloudWatchアラームを設定
CloudWatchアラームには、ECSのサービスを名を指定する必要があるのですが、CloudFormationで作成した場合、ECS Service名にはランダムな文字列が指定されるため事前には分かりません。 なので、ECS Servuce名を取得する必要があるのですが、CloudFormationで取得できるのはECS ServiceのARNとなります。そのため、
- ECS ServiceのARNをLambdaへ渡す(arn:aws:ecs:ap-northeast-1:123456789012:service/dev-sampleWebapp-xxxxxxxxxxxx)
- Lambdaで受け取ったARNからサービス名を取得して返す(dev-sampleWebapp-xxxxxxxxxxxx)
- CloudFormation側でサービス名を受け取ってCloudWatchアラームを作成する
という流れになります。
チュートリアルのゴール
ECSサービスの起動
CloudWatchアラームの作成
ディメンションにECS ServiceNameを指定して、アラームが作成されればokです。
Lambdaサンプルコード
このコードを貼り付けてLambda関数を作成します。
var aws = require("aws-sdk"); exports.handler = function(event, context) { console.log("REQUEST RECEIVED:\n" + JSON.stringify(event)); var responseStatus = "SUCCESS"; var responseData = {}; responseData["Name"] = returnServiceName(event.ResourceProperties.ServiceArn); sendResponse(event, context, responseStatus, responseData); }; // return service name from arn function returnServiceName(ecsArn){ var str1 = ecsArn.split("/"); return str1[1]; } // Send response to the pre-signed S3 URL function sendResponse(event, context, responseStatus, responseData) { var responseBody = JSON.stringify({ Status: responseStatus, Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, PhysicalResourceId: context.logStreamName, StackId: event.StackId, RequestId: event.RequestId, LogicalResourceId: event.LogicalResourceId, Data: responseData }); console.log("RESPONSE BODY:\n", responseBody); var https = require("https"); var url = require("url"); var parsedUrl = url.parse(event.ResponseURL); var options = { hostname: parsedUrl.hostname, port: 443, path: parsedUrl.path, method: "PUT", headers: { "content-type": "", "content-length": responseBody.length } }; console.log("SENDING RESPONSE...\n"); var request = https.request(options, function(response) { console.log("STATUS: " + response.statusCode); console.log("HEADERS: " + JSON.stringify(response.headers)); // Tell AWS Lambda that the function execution is done context.done(); }); request.on("error", function(error) { console.log("sendResponse Error:" + error); // Tell AWS Lambda that the function execution is done context.done(); }); // write data to request body request.write(responseBody); request.end(); }
CloudFormationテンプレート
このサンプルコードでCloudFormationスタックを作成します。
{ "AWSTemplateFormatVersion" : "2010-09-09" , "Description" : "CloudFormation create CloudWatch Alarm", "Resources" : { "sampleWebapp": { "Type": "AWS::ECS::Service", "Properties": { "Cluster": "default", "DesiredCount": "1", "TaskDefinition": "arn:aws:ecs:ap-northeast-1:123456789012:task-definition/console-sample-app-static:1" } }, "GetServiceName": { "Type": "Custom::GetServiceName", "Properties": { "ServiceToken": "arn:aws:lambda:ap-northeast-1:123456789012:function:getEcsServiceName", "ServiceArn": { "Ref": "sampleWebapp" } } }, "CPUAlarm" : { "Type": "AWS::CloudWatch::Alarm", "Properties": { "AlarmDescription": "cpu alarm for ECS Service", "MetricName": "CPUUtilization", "Namespace": "AWS/ECS", "Statistic": "Average", "Period": "300", "EvaluationPeriods": "1", "Threshold": "90", "ComparisonOperator": "GreaterThanThreshold", "Dimensions": [ { "Name": "ServiceName", "Value": { "Fn::GetAtt": [ "GetServiceName", "Name" ]} }, { "Name": "ClusterName", "Value": "default" } ] } } } }
最後に
Lambda-backedカスタムリソースは、ここぞというときに柔軟に対応できます。CloudFormationを使っていて、ある処理をなんとか自動化できないか?という、かゆいところに手が届く機能だと思います。こんな機能もあるんだー!と思ってもらえると嬉しいです。