【AWS】ELBがProxy Protocolをサポート 〜 RubyでEchoサーバ作って確かめてみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは植木和樹です。AWSで提供しているロードバランサーサービス Elastic Load Blancing でProxy Protocolがサポートされました。

【AWS発表】 Elastic Load BalancingがProxy Protocolをサポート

本日は新機能の使用レポートになります。またEC2インスタンスからみてどのように通信内容が変わるのか知りたかったので、簡易Echoサーバを用意して比較的低レベル層で通信をみてみました。

Proxy Protocol対応でなにがうれしいのか?

Proxy Protocolがサポートされたことにより、どのようなメリットがあるのでしょうか?上記AWSブログから抜粋すると、こういうことのようです。

本日までは、ELBはHTTP(S)のロードバランシングに使用される場合のみ、クライアントのIPアドレスを取得できました。(中略)ELBがTCPのロードバランシング用に設定されている場合は、クライアントのIPアドレスを取得できませんでした。

本日より、Elastic Load Balancing (ELB)がProxy Protocol version 1 をサポートしています。 これにより、TCPロードバランシングを使用して、サーバーに接続するクライアントの発信元IPアドレスを識別することができます。

つまりHTTPやHTTPS以外の用途でELBを使う場合にも、接続元のIPアドレスとポート番号が取得できるようになったということです。障害には、いつ・どこからの通信が影響したのかを知りたいことが多々ありますので、今回の機能追加はうれしいですね。

ELBをProxy Protocol対応にする

さて実際にELBを作成してみたのですが、単純に作っただけでは今回の機能を利用することはできませんでした。どうやら追加設定をすることで機能が有効になるようです。設定手順はAWSのドキュメント「Enable Proxy Protocol Support」で解説されています。

ドキュメントをザッと読んでみたのですが、aws-apitoolsを利用してELBに「ポリシー」を設定することで機能が有効になると書かれています。Proxy Protocolを有効にするためにコマンドラインは使いたくない・・・、ということで、やはりCloudFormationの出番ですね!

テンプレート:tcp-proxy-elb.template

{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "AWS CloudFormation Sample Template",

  "Parameters" : {
    "InstanceType" : {
      "Description" : "Echo-Server EC2 instance type",
      "Type" : "String",
      "Default" : "t1.micro"
    },

    "ServerPort" : {
      "Description" : "TCP/IP port of the tcp echo server",
      "Type" : "String",
      "Default" : "8888"
    },
    
    "KeyName" : {
      "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instances",
      "Type" : "String",
      "Default" : "ueki_keypair"
    }
  },

  "Resources" : {
    "EC2Instance" : {
      "Type" : "AWS::EC2::Instance",  
      "Properties" : {
        "ImageId" : "ami-39b23d38",
        "UserData" : { "Fn::Base64" : { "Ref" : "ServerPort" }},
        "SecurityGroups" : [ { "Ref" : "InstanceSecurityGroup" } ],
        "InstanceType" : { "Ref" : "InstanceType" },
        "KeyName" : { "Ref" : "KeyName" }
      }
    }, 

    "ElasticLoadBalancer" : {
      "Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
      "Properties" : {
        "AvailabilityZones" : { "Fn::GetAZs" : "" },
        "Listeners" : [ {
          "LoadBalancerPort" : { "Ref" : "ServerPort" },
          "InstancePort" : { "Ref" : "ServerPort" },
          "Protocol" : "TCP"
        } ],
        "HealthCheck" : {
          "Target" : { "Fn::Join" : [ "", ["TCP:", { "Ref" : "ServerPort" }]]},
          "HealthyThreshold" : "3",
          "UnhealthyThreshold" : "5",
          "Interval" : "30",
          "Timeout" : "5"
        },
        "Instances" : [ { "Ref" : "EC2Instance" } ],
        "Policies" : [{
          "PolicyName" : "EnableProxyProtocol",
          "PolicyType" : "ProxyProtocolPolicyType",
          "Attributes" : [{ "Name" : "ProxyProtocol", "Value" : "true" }],
          "InstancePorts" : [ { "Ref" : "ServerPort" } ]
        }]
      }
    },
    
    "InstanceSecurityGroup" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupDescription" : "Enable SSH access and TCP access on the inbound port",
        "SecurityGroupIngress" : [ {
          "IpProtocol" : "tcp",
          "FromPort" : { "Ref" : "ServerPort" },
          "ToPort" : { "Ref" : "ServerPort" },
          "CidrIp" : "0.0.0.0/0"
        },
        {
          "IpProtocol" : "tcp",
          "FromPort" : "22",
          "ToPort" : "22",
          "CidrIp" : "0.0.0.0/0"
        }]
      }
    }
  }
}

InstancePortsを指定しないとポリシーが有効にならないで注意してください。また"ProxyProtocolPolicyType"以外に、Policiesで指定できるPolicyTypeになにがあるのか、一覧をネットで探したのですがみつかりませんでした。aws-cliで調べることができるので掲載しておきます。

$ aws elb describe-load-balancer-policy-types | jq -r ".PolicyTypeDescriptions[].PolicyTypeName"
ProxyProtocolPolicyType
BackendServerAuthenticationPolicyType
PublicKeyPolicyType
AppCookieStickinessPolicyType
LBCookieStickinessPolicyType
SSLNegotiationPolicyType

Proxy Protocolが有効になっていることの確認はマネージメントコンソールではできないのでaws-cliを使用します。EnableProxyProtocolが表示されていれば有効になっています。

$ aws elb describe-load-balancers --load-balancer-names ProxyELB-ElasticLo-XXXXXXXX | jq ".[].Policies"
{
  "OtherPolicies": [
    "EnableProxyProtocol"
  ],
  "AppCookieStickinessPolicies": [],
  "LBCookieStickinessPolicies": []
}

EchoサーバでProxyヘッダ文字列を確認する

Echoサーバを準備する

CloudFormationで作成されたEC2インスタンスにsshでログインし、下記のrubyスクリプトを作成しました。コマンドライン引数でポート番号を指定すると、そのポートに接続してきたクライアントの入力をそのまま返してくれる単純なEchoサーバです。

ファイル:echosrv.rb

#!/usr/bin/env ruby

require "socket"
require "pp"

port = ARGV[0].to_i
gs = TCPServer.open(port)
addr = gs.addr
addr.shift
printf("server is on %s\n", addr.join(":"))

Signal.trap(:INT){
  puts "server was terminated."
  Thread.list.each do |t| t.kill end
}

while true
  Thread.start(gs.accept) do |s|       # save to dynamic variable
    print(s, " is accepted\n")
    pp s.peeraddr
    while s.gets
      line = $_
      s.write(line)
      line.sub!(/\r/, "<CR>")
      line.sub!(/\n/, "<LF>")
      print("IN> ", line, "\n")
    end
    print(s, " is gone\n")
    s.close
  end
end

Echoサーバを起動する

それではスクリプトを実行しましょう。ELB作成時にインスタンス側ポート番号は8888番で設定していますので、コマンドライン引数で指定してあげます。

$ chmod +x echosrv.rb
$ ./echosrv.rb 8888
server is on 8888:0.0.0.0:0.0.0.0

ELBからのヘルスチェック

Echoサーバが起動すると、早速ELBからのヘルスチェックが行われ30秒毎に画面にログが出力されてくるかと思います。IN> の後に出力されているPROXY TCP4 .... がProxy Protocolが有効になった際にELBから渡ってくる、実際の接続元のIPアドレスとポート番号です。「Elastic Load Balancing Developer Guide」によると、以下の並びで出力されるようです。CLIENT_IPCLIENT_PORTが実際のクライアントのIPアドレスとポート番号ですね。

PROXY_STRING + single space + INET_PROTOCOL + single space + CLIENT_IP + single space + PROXY_IP + single space + CLIENT_PORT + single space + PROXY_PORT + "\r\n"
#<TCPSocket:0x7f1151f61148> is accepted
["AF_INET",
 11871,
 "ip-172-31-10-61.ap-northeast-1.compute.internal",
 "172.31.10.61"]
IN> PROXY TCP4 172.31.10.61 10.0.8.154 11871 8888<CR><LF>
#<TCPSocket:0x7f1151f61148> is gone

ローカルPCから接続してみる

それではローカルPCからELBの8888番ポートへ接続してみましょう。telnetコマンドはホスト名の後にポート番号を指定すると、そのポート番号で接続してくれます。(CloudFormationで作ったのでELBのホスト名がやたらと長くなってしまっていますね)

$ telnet ProxyELB-ElasticLo-XXXXXXXX-XXXXXXXX.ap-northeast-1.elb.amazonaws.com 8888
Trying 54.249.111.111...
Connected to proxyelb-elasticlo-XXXXXXXX-XXXXXXXX.ap-northeast-1.elb.amazonaws.com.
Escape character is '^]'.
PROXY TCP4 124.100.123.123 10.0.8.154 53971 8888(接続するとすぐに表示される)
Hello(入力)
Hello(エコー)
Quit(入力)
Quit(エコー)
(キーボードの Ctrl と ] を同時に押す)
telnet> quit
Connection closed.

接続すると、EC2インスタンス側の画面には以下のような文字列が出力されます。124.100.123.123というのが私が接続しているプロバイダのIPアドレスで、53971が実際のポート番号ですね。

##<TCPSocket:0x7f1151f661e8> is accepted
["AF_INET",
 11862,
 "ip-172-31-10-61.ap-northeast-1.compute.internal",
 "172.31.10.61"]
IN> PROXY TCP4 124.100.123.123 10.0.8.154 53971 8888<CR><LF>
IN> Hello<CR><LF>
IN> Quit<CR><LF>
#<TCPSocket:0x7f1151f661e8> is gone

まとめ

今回はELBでProxy Protocolを有効にした際に設定される接続元情報が、どのようにEC2に渡ってくるかを調べました。実際にサーバプログラムで利用する際には、PROXY文字列パース後の値を取得するAPIが、言語や環境毎に提供されていると思います。各環境ごとにどのようにPROXYヘッダー文字列内のIPアドレスやポート番号を取得すればよいのかについても、いずれまとめてみたいですね。