AWS Lambda-backed カスタムリソースでクロススタック参照


cloudpack大阪の佐々木です。
Cloudformation(CFn)のAWS Lambda-backedカスタムリソースという機能を試したので、まとめておきます。

# 概要

CFnで  スタック内のリソースは"Ref"で簡単にアクセスできますが、別スタックのリソースには直接アクセスできません。ネットワークを1つのスタックで作って、その上に別のスタックでインスタンスを立ち上げるとかよくあると思います。AWS Lambda-backed カスタムリソースというのを使えば、Lambdaを使って、他のスタックの情報(クロススタック参照と言うらしい)を取得できるらしいので試してみました。
チュートリアルは下記にあります。
http://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/walkthrough-custom-resources-lambda-cross-stack-ref.html

# ネットワークのCFnテンプレート
```json:
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Resources" : {
    "VPC" : {
      "Type" : "AWS::EC2::VPC",
      "Properties" : {
        "CidrBlock" : "10.0.0.0/16"
      }
    },
    "Subnet1A" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "AvailabilityZone" : "ap-northeast-1a",
        "CidrBlock" : "10.0.1.0/24",
        "VpcId" : { "Ref" : "VPC" }
      }
    },
    "Subnet1C" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "AvailabilityZone" : "ap-northeast-1c",
        "CidrBlock" : "10.0.2.0/24",
        "VpcId" : { "Ref" : "VPC" }
      }
    },
    "InternetGateway" : {
      "Type" : "AWS::EC2::InternetGateway"
    },
    "VPCGatewayAttachment" : {
       "Type" : "AWS::EC2::VPCGatewayAttachment",
       "Properties" : {
         "VpcId" : { "Ref" : "VPC" },
         "InternetGatewayId" : { "Ref" : "InternetGateway" }
       }
    },
    "PublicRouteTable" : {
      "Type" : "AWS::EC2::RouteTable",
      "Properties" : {
        "VpcId" : { "Ref" : "VPC" }
      }
    },
    "PublicRoute" : {
      "Type" : "AWS::EC2::Route",
      "DependsOn" : "VPCGatewayAttachment",
      "Properties" : {
        "RouteTableId" : { "Ref" : "PublicRouteTable" },
        "DestinationCidrBlock" : "0.0.0.0/0",
        "GatewayId" : { "Ref" : "InternetGateway" }
      }
    },
    "PublicSubnetRouteTableAssociation1A" : {
      "Type" : "AWS::EC2::SubnetRouteTableAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "Subnet1A" },
        "RouteTableId" : { "Ref" : "PublicRouteTable" }
      }
    },
    "PublicSubnetRouteTableAssociation1C" : {
      "Type" : "AWS::EC2::SubnetRouteTableAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "Subnet1C" },
        "RouteTableId" : { "Ref" : "PublicRouteTable" }
      }
    },
    "PublicSubnetNetworkAclAssociation1A" : {
      "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "Subnet1A" },
        "NetworkAclId" : { "Fn::GetAtt" : ["VPC", "DefaultNetworkAcl"] }
      }
    },
    "PublicSubnetNetworkAclAssociation1C" : {
      "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "Subnet1C" },
        "NetworkAclId" : { "Fn::GetAtt" : ["VPC", "DefaultNetworkAcl"] }
      }
    },
    "BaseSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Base Security Group",
        "VpcId" : { "Ref" : "VPC" },
        "SecurityGroupIngress" : [{
          "IpProtocol" : "tcp",
          "FromPort" : "22",
          "ToPort" : "22",
          "CidrIp" : "0.0.0.0/0"
        }]
      }
    }
  },
  "Outputs" : {
    "VPCId" : {
      "Description" : "VPC ID",
      "Value" :  { "Ref" : "VPC" }
    },
    "Subnet1A" : {
      "Description" : "The subnet ID on ap-northeast-1a",
      "Value" : { "Ref" : "Subnet1A" }
    },
    "Subnet1C" : {
      "Description" : "The subnet ID on ap-northeast-1c",
      "Value" : { "Ref" : "Subnet1C" }
    },
    "BaseSecurityGroup" : {
      "Description" : "Base security group ID",
      "Value" : { "Fn::GetAtt" : ["BaseSecurityGroup", "GroupId" ]}
    }
  }
}
```


Outputsで出力した値をLambdaからとってきます。
ここでは、VPC、サブネット、セキュリティグループのIDを出力しています。
チュートリアルから変更したところは、インスタンスのスタック作成時にAZを指定できるように、AZそれぞれにサブネットをつくっています。

# ネットワークStackの作成

1. CFnのManagementConsoleからCreate stackを実行し、上記のテンプレートを選択します。

![スクリーンショット 2016-05-25 19.24.06.png](https://qiita-image-store.s3.amazonaws.com/0/3246/f8b42018-0b06-36e6-cedd-d89da0ae6c99.png "スクリーンショット 2016-05-25 19.24.06.png")

2. 入力はスタック名だけです。SampleNetworkConfigurationにしときます。あとでインスタンスのスタックを作る時の値と、合わせる必要があります。

![スクリーンショット 2016-05-25 19.27.37.png](https://qiita-image-store.s3.amazonaws.com/0/3246/c4d55584-27ba-d8a8-28e4-134156988fd2.png "スクリーンショット 2016-05-25 19.27.37.png")

3. スタックの作成が完了すると、下記のようにOutputsに表示されます。

![スクリーンショット 2016-05-25 19.33.52.png](https://qiita-image-store.s3.amazonaws.com/0/3246/074f579a-dc40-6ded-30f2-2894240640e6.png "スクリーンショット 2016-05-25 19.33.52.png")



# ネットワークのCFnテンプレート
```json:
{
  "AWSTemplateFormatVersion" : "2010-09-09",

  "Parameters" : {
    "NetworkStackName" : {
      "Type" : "String",
      "MinLength" : 1,
      "MaxLength" : 255,
      "AllowedPattern" : "^[a-zA-Z][-a-zA-Z0-9]*$",
      "Default" : "SampleNetworkConfiguration"
    },
    "AvailabilityZone" : {
      "Type" : "String",
      "Description" : "Input Subnet in which AZ to use. (Subnet1A or Subnet1C)",
      "Default" : "Subnet1A"
    },

    "Keyname" : {
      "Description" : "input EC2 Keyname",
      "Type" : "AWS::EC2::KeyPair::KeyName"
    }  
  },

  "Resources" : {
    "Instance" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "InstanceType" : "t2.micro",
        "ImageId" : "ami-29160d47",
        "KeyName" : { "Ref" : "Keyname" },
        "NetworkInterfaces" : [{
          "GroupSet" : [{ "Fn::GetAtt" : [ "NetworkInfo", "BaseSecurityGroup" ]}, { "Ref" : "HTTPSecurityGroup" }],
          "AssociatePublicIpAddress" : "true",
          "DeviceIndex" : "0",
          "DeleteOnTermination" : "true",
          "SubnetId" : { "Fn::GetAtt" : [ "NetworkInfo", { "Ref" : "AvailabilityZone" } ]}
        }]
      }
    },

    "HTTPSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable HTTP ingress",
        "VpcId" : { "Fn::GetAtt" : [ "NetworkInfo", "VPCId" ]},
        "SecurityGroupIngress" : [{
          "IpProtocol" : "tcp",
          "FromPort" : "80",
          "ToPort" : "80",
          "CidrIp" : "0.0.0.0/0"
        }]
      }
    },

    "NetworkInfo" : {
      "Type" : "Custom::NetworkInfo",
      "Properties" : {
        "ServiceToken" : { "Fn::GetAtt" : ["LookupStackOutputs", "Arn"]},
        "StackName" : {
          "Ref" : "NetworkStackName"
        }
      }
    },

    "LookupStackOutputs" : {
      "Type" : "AWS::Lambda::Function",
      "Properties" : {
        "Handler" : "index.handler",
        "Role" : { "Fn::GetAtt" : ["LambdaExecutionRole", "Arn"] },
        "Code" : {
          "ZipFile" : { "Fn::Join" : ["\n", [
            "var response = require('cfn-response');",
            "exports.handler = function(event, context) {",
            "  console.log('REQUEST RECEIVED:\\n', JSON.stringify(event));",
            "  if (event.RequestType == 'Delete') {",
            "    response.send(event, context, response.SUCCESS);",
            "    return;",
            "  }",
            "  var stackName = event.ResourceProperties.StackName;",
            "  var responseData = {};",
            "  if (stackName) {",
            "    var aws = require('aws-sdk');",
            "    var cfn = new aws.CloudFormation();",
            "    cfn.describeStacks({StackName: stackName}, function(err, data) {",
            "      if (err) {",
            "        responseData = {Error: 'DescribeStacks call failed'};",
            "        console.log(responseData.Error + ':\\n', err);",
            "        response.send(event, context, response.FAILED, responseData);",
            "      }",
            "      else {",
            "        data.Stacks[0].Outputs.forEach(function(output) {",
            "          responseData[output.OutputKey] = output.OutputValue;",
            "        });",
            "        response.send(event, context, response.SUCCESS, responseData);",
            "      }",
            "    });",
            "  } else {",
            "    responseData = {Error: 'Stack name not specified'};",
            "    console.log(responseData.Error);",
            "    response.send(event, context, response.FAILED, responseData);",
            "  }",
            "};"
          ]]}
        },
        "Runtime" : "nodejs",
        "Timeout" : "30"
      }
    },

    "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" : "root",
          "PolicyDocument" : {
            "Version" : "2012-10-17",
            "Statement" : [{
              "Effect" : "Allow",
              "Action" : ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
              "Resource" : "arn:aws:logs:*:*:*"
            },
            {
              "Effect" : "Allow",
              "Action" : ["cloudformation:DescribeStacks"],
              "Resource" : "*"
            }]
          }
        }]
      }
    }
  }
}
```
まず、Parameterでネットワークのスタック名を入力させます。
次にAZの指定ですが、ここではSubnet1A、Subnet1Cというリソース名を入力するようにしました。
Mappingsでやりたかったんですが、MappingsにFn::GetAttとかが使えないっぽいので。
いい方法あれば教えて下さい。
あとSSHのキーを選択します。

Custom::NetworkInfo に LambdaのARN、参照したいスタックの名前を渡して、Lambdaを実行します。
Lambdaのコードについては、チュートリアルそのままなので説明は省略します。(ちゃんと説明できないので)
実行後は、{ "Fn::GetAtt" : [ "NetworkInfo", "VPCId" ]} みたいな感じで、参照元スタックのOutputsの値にアクセスできるようになります。

# インスタンスStackの作成

1. Create stackから上記のテンプレートを選択します。

![スクリーンショット 2016-05-25 19.36.51.png](https://qiita-image-store.s3.amazonaws.com/0/3246/44329e25-50ac-fdae-ab95-aff53b56755f.png "スクリーンショット 2016-05-25 19.36.51.png")

2. Stack nameを適当に入力し、Parametersに下記を入力します。
 
+ AvailabilityZone → Subnet1A or Subnet1Cを入力
+ Keyname → SSHの鍵を選択
+ NetworkStackName → SampleNetworkConfiguration


![スクリーンショット 2016-05-25 19.37.54.png](https://qiita-image-store.s3.amazonaws.com/0/3246/96ea9768-16fe-a9cd-d442-f347d8f9ec19.png "スクリーンショット 2016-05-25 19.37.54.png")


# まとめ

CFnを実環境で使う場合、ネットワークとインスタンスは別スタックにするというのは普通の話かと思うので、使える機能かと思います。
ただ、参照先のスタックの情報を選択できればいろいろできそうなんですが・・・
それができなさそうなので、AZは文字入力にしました。
SecurityGroupも使うものを選択という感じにしたかったのですが、難しそうなので、
ネットワークの方に全台に適用するような設定を入れて、
インスタンスごとに異なるようなセキュリティグループはインスタンスのテンプレートに定義するようにしました。
いい方法があれば教えて下さい。