JAWSUG Tokyo #20 でLTしてきた

2014.04.14

こんにちは。望月です。
先週開催されたJAWSUG Tokyo #20で、「VPCとVPC Peeringのおはなし」と題してLTさせてもらいました。発表資料は以下です。

当日は宴会JAWSということで、大量のビールが振る舞われました。私も登壇時点でビール3杯を摂取しており、頭の回転が鈍ってきている状態で突っ走ってしまった感じがあります。反省しています。
それはさておき、VPC Peeringのおはなしについて知りたい方は先日もブログに書いたのでそちらを読んで頂き、あまりLT内で解説できなかったCloudFormation Custom Resourceについて書いておきたいと思います。

CloudFormation Custom Resource

CloudFormationは、AWSの構成をJSON形式で記載することで、そのテンプレートを利用した構成を何個でも作成できるAWSの機能です。
CloudFormationにはCustom Resourceという機能がありまして、以下の様な形で動作します。

  1. Custom Resource作成時に、SNSにメッセージがPublishされる
  2. SNSからHTTPやSQSにNotificationを飛ばす
  3. Notificationを受けたアプリケーションが処理を行い、処理結果を返す

Custom Resourceで何が嬉しいかというと、単純なCloudFormationの実行では作成できないリソースや、複雑な処理を外部のアプリケーションで実行することができる点です。これにより、単体のCloudFormationで複雑な構成も実現可能になります。JAWSのLTで紹介したアプリケーションでは、この機能を利用してVPC Peeringを作成しました。

それでは簡単にCustom Resourceを利用したアプリケーションの書き方をご紹介します。

実行環境

今回は、実行環境としてAmazon Elastic Beanstalkを利用しています。Beanstalkでなくても、Amazon EC2にRuby + Sinatraがあれば動きます。

今回は試しにHTTPでSNSからのNotificationを受けることにします。そのため、Rubyの軽量アプリケーションフレームワークであるSinatraを試しに利用します。必要になる処理は、

  • SNSからの通知を受け、CloudFormationの実行種別(Create,Update,Delete)によって処理を分ける
  • 処理の実施
  • 実行結果を、SNSからの通知に含まれるResponseURLにPUTリクエストで返す

まず、SNS通知を受ける部分は以下のようなコードになります。

  post '/' do
    body = JSON.parse(request.body.read)

    # Custom Resource Request
    message = JSON.parse(body["Message"])
    logger.debug message
    
    begin
      case message["RequestType"].downcase
        when "create" then
          peering_id = vpc_peering.create(message)
          send_result(true, message, {:PhysicalResourceId => peering_id})
        when "delete" then
          vpc_peering.delete(message)
          send_result(true, message)
        else
          send_result(true, message)
      end
    rescue => e
      logger.error "#{e.message}"
      send_result(false, message)
      return 500
    end

SNSからのHTTP通知はPOSTで送信されるため、post '/' doでリクエストを受けます。リクエストボディの中のRequestTypeの部分から、通知元CloudFormationのAction(Create/Update/Delete)が判断できるので、それぞれに応じた処理を実行します。今回はCreateとDeleteに対応したリソースとなっています。

例えばCreateのリクエストが飛んできた場合は、新しくVPC Peeringを作成するアクションを実施します。そのコードは以下になります。

   response = @ec2client.create_vpc_peering_connection(:vpc_id => @vpc_id, :peer_vpc_id => request["ResourceProperties"]["VpcId"])
   peering_id = response.vpc_peering_connection.vpc_peering_connection_id
   res = @ec2client.accept_vpc_peering_connection(:vpc_peering_connection_id => peering_id)


   @logger.debug "vpc_peering_connection_id => #{peering_id}"

    # save Custom Resource data into MySQL
    peering = Peering.new do |p|
      p.vpc_id                = request["ResourceProperties"]["VpcId"]
      p.stack_id              = request["StackId"]
      p.request_id            = request["RequestId"]
      p.logical_resource_id   = request["LogicalResourceId"]
      p.peering_connection_id = peering_id
    end
    peering.save

CreateVPCPeeringConnectionとAcceptVPCPeeringConnectionのAPIを利用して、VPC Peeringの作成と承認を行いました。最後に、リクエストの値をMySQLに保存しておきます。ここで保存しておかないと、Deleteリクエストが飛んできた時にどのPeeringコネクションを削除したらよいかの紐付けができません。

最後に、アプリケーションの処理が成功したかどうかを通知します。通知先のURLはSNSからのメッセージの中に含まれるResponseURLという項目に含まれています。です。成功した場合のレスポンスを返すには、以下の様なコードを書きます。

    data = {
      :Status => "SUCCESS",
      :StackId => message["StackId"],
      :RequestId => message["RequestId"],
      :LogicalResourceId => message["LogicalResourceId"]
    }

    if message["RequestType"].downcase == "create"
      if status
        data[:PhysicalResourceId] = response[:PhysicalResourceId]
      end
    else
      data[:PhysicalResourceId] = message["PhysicalResourceId"]
    end

    hc = HTTPClient.new
    hc.request(:put, message["ResponseURL"], body: data.to_json, header: {"Content-Type" => ""})

レスポンスの仕様は、公式のドキュメントを参照してください。PUTリクエストでデータを送るのですが、リクエストヘッダにContent-Typeが含まれているとSignatureDoesNotMatchのエラーとなってしまうので注意して下さい。

VPC PeeringをCustom Resourceで実現する際のサンプルコードをGithubにて公開していますので、参考にしてください。