6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-25

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テンプレート

{
  "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

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

スクリーンショット 2016-05-25 19.27.37.png

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

スクリーンショット 2016-05-25 19.33.52.png

ネットワークのCFnテンプレート

{
  "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

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

スクリーンショット 2016-05-25 19.37.54.png

まとめ

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?