[CloudFormation]Lambda-backedカスタムリソースを理解する

AWS CloudFormation

コンニチハ、千葉です。

CloudFormationを利用する上で、かゆいところに手が届く、それがカスタムリソースです。

例へば、EC2で最新のAMIを毎回検索してAMI IDを指定している!そんなことがあると思いますが、Lambda-backedカスタムリソースを使うと、自動で最新のAMIを検索して指定することができるようになります。これは、便利。因みに最新のAMIの取得は公式ドキュメントチュートリアル: Amazon マシンイメージ ID を参照するがありますので、これをやればすぐに対応できます。

では、早速Lambda-backedカスタムリソースを使いこなす上で必要なことを見ていきます。といっても、そんなに難しくないのでご安心を。

Lambda-backedカスタムリソースの仕組み

CloudFormation <-> Lambdaの連携です。

CloudFormationから必要なパラメータをLambdaへ渡し発火させます。そして、Lambdaで処理を行い成功/失敗のステータスをCloudFormationに返します。とてもシンプルです。

利用する上でやることは、2つです。

  1. CloudFormationでカスタムリソースを定義する
  2. 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となります。そのため、

  1. ECS ServiceのARNをLambdaへ渡す(arn:aws:ecs:ap-northeast-1:123456789012:service/dev-sampleWebapp-xxxxxxxxxxxx)
  2. Lambdaで受け取ったARNからサービス名を取得して返す(dev-sampleWebapp-xxxxxxxxxxxx)
  3. CloudFormation側でサービス名を受け取ってCloudWatchアラームを作成する

という流れになります。

チュートリアルのゴール

ECSサービスの起動

20160901-cloudformtion-lambda-backed-1

CloudWatchアラームの作成

20160901-cloudformtion-lambda-backed-2

ディメンションに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を使っていて、ある処理をなんとか自動化できないか?という、かゆいところに手が届く機能だと思います。こんな機能もあるんだー!と思ってもらえると嬉しいです。

参考

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html