![[CloudFormation]Lambda-backedカスタムリソースを理解する](https://devio2023-media.developers.io/wp-content/uploads/2014/05/AWS_CloudFormation.png)
[CloudFormation]Lambda-backedカスタムリソースを理解する
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
コンニチハ、千葉です。
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を使っていて、ある処理をなんとか自動化できないか?という、かゆいところに手が届く機能だと思います。こんな機能もあるんだー!と思ってもらえると嬉しいです。











