東京でウェブオペレーションエンジニアをしている id:dekokun です。
本記事ではAWSでELBを使用してHTTP/2 or SPDYを運用する上で直面する問題としてのクライアントのIPアドレスが分からなくなる問題の紹介を行い、その後に解決策としてのPROXY protocolの紹介・PROXY protocolの設定方法について記載します。
この記事は先日公開しましたAWS EC2でのHTTP/2 or SPDY導入方法 - Hatena Developer Blog (以下、"前回の記事"と呼びます) の続編となります。
ELBのTCPモードにおける問題点
さて、前回の記事にてELBをTCPモードで動かすことによってHTTP/2 or SPDY対応を行う方法を紹介しましたが、この方法では1つ大きな問題点があります。 それは、nginxがクライアントのIPアドレスを取得することができなくなってしまう(nginxはELBのIPアドレスを取得してしまう)ということです。
クライアントのIPアドレスを取得することができないことによって、具体的には以下のような問題が起きます。
- アクセスログに記載されるIPアドレスがクライアントのIPアドレスではなくなるため何か調査の必要が発生した際に詳しい調査を行うことができなくなります
- IPアドレスによるアクセス制御ができなくなります
- 具体的には「特定のIPアドレスからのアクセスだけ許可したい」「ngx_http_limit_req_moduleなどを使用してあるIPからの接続数が急増した際に一時的なアクセス制限を入れたい」などができなくなります
解決策としてのPROXY protocol
PROXY protocolとは、ロードバランサがクライアントのIPをupstreamサーバに伝えることができるものです。TCPレイヤで動く、X-Forwarded-Forのようなものともいえます。 PROXY protocolを使うことで、upstreamサーバがクライアントのIPを取得することができるようになり、上記問題を解決することができます。
PROXY protocolが必要となる技術的背景及びPROXY protocolの動作
この節では、なぜTCPモードのロードバランサがクライアントのIPアドレスを取得できないのか及び、ELB等関係なくそもそもPROXY protocolがどのようにしてその問題を解決しているのかを説明します。
なんらかのロードバランサ(ELBやhaproxy等)からTCPモードでupstreamのサーバに対してパケットを転送する時、基本的にはupstreamサーバ(今回の例だとnginx)に届けるパケットはTCP/IP上でのアクセス元IPアドレスをロードバランサのIPに変換することになります。それにより、upstreamのサーバが認識する"アクセス元のIPアドレス"は、上記ELBやhaproxyのIPアドレスとなってしまい、upstreamのサーバはロードバランサと通信しているクライアントのIPアドレスを取得することができません。
[クライアント(PCとかスマートフォンとか)] <- upstreamサーバは本当ならこいつのIPアドレスを知りたい ↓ ↓ ↓ [ロードバランサ] クライアントから来たパケットを転送する際にTCP/IPレイヤでアクセス元IPアドレスを自分のIPアドレスに変換する ↓ ↓ ↓ [upstreamサーバ] パケットのアクセス元IPアドレスがロードバランサのIPアドレスに変換されているためクライアントのIPアドレスが分からない
ここで、「アクセス元のIPをupstreamのサーバに伝えてあげる」と聞いてパッと思い浮かぶのは、例えばhttpだと"X-Forwarded-For"を付与する等の、L7のプロトコルを使ってupstreamに対してIPアドレスを伝えてあげる手法かと思います。ELBのhttpモード使用等のL7レイヤのロードバランサであれば特に問題なくその方法を使えばいいかと思います。 しかしTCPモードで"httpだった場合はX-Forwarded-Forを付与する"というようなことを行おうとすると、以下2つを実施する必要が出てきます。
- TCP上で流れているL7レイヤのプロトコルを判別
- それぞれのプロトコルに応じて、必要なヘッダを挿入する(HTTPだったらX-Forwarded-Forを挿入)などしてアクセス元のIPアドレスを伝える
1.
は、世の中に数多あるL7レイヤのプロトコルのどれであるかを正確に判断することになるので、非常に困難であることが容易に想像できます。
"このロードバランサはHTTPしか考えなくて良い"という制約を課して1.
の問題を回避したとしても、2.
の問題が依然として残ります。
2.
を解決するにはL7レイヤのパーサをロードバランサが用意しなくてはならず実装が大変になってしまいますし、動作時の負荷も非常に大きく増えてしまいます。
というわけで上記のような問題を解決するために、TCPレイヤでの"X-Forwarded-For"的なものとして出てきたのがPROXY protocolです。ロードバランサがPROXY protocolでアクセス元の情報を伝えることにより、upstreamのサーバがアクセス元のIPアドレスやポートを知ることができるようになります。
この一連の流れ及びPROXY protocolの仕様についてはThe PROXY protocol により詳しく記載してあります。
なお、上記で"基本的にはupstreamサーバ(今回の例だとnginx)に届けるパケットはTCP/IP上でのアクセス元IPアドレスをロードバランサのIPアドレスにすることになります"と書きましたが、そうならない例としては、LVSでDSRを使用するなど(追記 ブログ下部にも記載しましたが、LVSでNATを使用した場合でもアクセス元IPは変換されないようでした)アクセス元IPアドレスを変換しないようなロードバランサを使う場合が考えられます。そのような場合はPROXY protocolなしでupstreamサーバはクライアントのIPアドレスを取得することができます。
ELB + nginx構成でのPROXY protocolの設定方法
以下では実際にどのように設定を行うことでPROXY protocolを使うことができるのかを記載します。
ELBの設定
PROXY protocolに対応させるにはELB側でも設定が必要です。前回の記事 にてTCPモードの設定について記載しましたが、その上でAWS CLIを使用して以下のようにPROXY protocol対応させる必要があります
以下は nginx-test
という名前のELBで443 portに対して設定する場合の設定方法です。
$ aws elb create-load-balancer-policy --load-balancer-name nginx-test --policy-name EnableProxyProtocol --policy-type-name ProxyProtocolPolicyType --policy-attributes AttributeName=ProxyProtocol,AttributeValue=true $ aws elb set-load-balancer-policies-for-backend-server --load-balancer-name nginx-test --instance-port 443 --policy-names EnableProxyProtocol
設定が完了したかどうかは、jqコマンドを使用して以下のように確認することができます。
$ aws elb describe-load-balancers --load-balancer-name nginxtest | jq '.LoadBalancerDescriptions[].BackendServerDescriptions[] | select(.InstancePort == 443)' { "InstancePort": 443, "PolicyNames": [ "EnableProxyProtocol" ] }
nginxの設定
前回の記事にて「TLS対応さえ済めば後はlistenディレクティブにて"spdy"または"http2"と記述するだけです」と記載しましたが、PROXY protocol対応は更にその上に以下3点を行う必要があります。
proxy_protocol
という記述を追加real_ip_header proxy_protocol;
という記述を追加set_real_ip_from
にロードバランサのIPレンジを追加
~略~ server { set_real_ip_from x.x.x.x/x; real_ip_header proxy_protocol; listen 443 ssl http2 proxy_protocol; ~略~
あとがき
前回の記事にも記載しましたが、この記事は以下3本についての連載の2作目となります。
- AWS EC2上の環境へのHTTP/2 or SPDY対応の導入方法
- HTTP/2 or SPDY運用の課題とその解決方法としてのPROXY protocolについての解説
- PROXY protocol自体の運用の課題と解決案
3作目は監視周りについて書く予定です。3作目もお楽しみに!!
はてなでは、HTTP/2を導入していくぜ!というエンジニアを募集しています。 あと、私、東京でインフラエンジニアしています。東京で働きたいぜ!というエンジニアも絶賛募集中です。
追記/修正事項
- "IPと略さないほうがいいのではないか"というブコメでのご指摘いただきました。確かに、インターネット・プロトコルの意味での"IP"という用語もこの記事内に存在することもあることから略さないほうが良いですね。IPアドレスの意味で"IP"としている部分を"IPアドレス"に書きかえました。ご指摘ありがとうございます
- ブコメでLVS/NATでもIPアドレスが取得できるのではないかという点、ご指摘頂き調べたところ、LVSについてはNATでもhalfnatとして動作するためアクセス元IPアドレスを書き換えないようだということがわかりました。LVSの記述について訂正してあります。また実際に構築するなどして詳しく調べてブログにしたいところです。ご指摘ありがとうございます