AWS PrivateLinkのVPCエンドポイントIDをNGINX Plusで取得する

nginxの有償版NGINX PlusがProxy Protocolバージョン2のカスタムフィールドに対応しPrivateLinkのVPCエンドポイントIDを参照できるようになったので、試してみた様子をレポートします。
2018.10.16

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

ども、大瀧です。
AWS PrivateLinkはユーザー独自のエンドポイントサービスを定義し、他のAWSアカウントのVPCやそこから接続するオンプレミス環境にプライベートなネットワークサービスを提供できます。具体的な設定手順は、以下の記事を参考にしてください。

【新機能】PrivateLinkで独自エンドポイントを作ってアプリをプライベート公開する #reinvent

エンドポイントサービスはNLB(Network Load Balancer)を経由して利用するため、トラフィックの接続元IPアドレスはNLBのPrivate IPになってしまいます。接続してくるクライアントの情報を取得する手段としてProxy Protocolバージョン2を有効にし、PrivateLinkが付与するそのヘッダから情報を得ることが出来ます。

Proxy ProtocolはTCPの拡張なので、NLBとTCPセッションを確立するサーバープロセスでそのヘッダを読みこまなければなりません。サーバーアプリケーションであればヘッダの読み込み&パースの実装は容易ですが、管理や運用の都合でリバースプロキシなどのミドルウェアで対応したいケースが多いと思います。Proxy Protocolバージョン2をサポートするリバースプロキシはHAProxyやnginx、Envoyなど複数ありますが、PrivateLinkが付与するVPCエンドポイントIDはTLV(Type-Length-Value)形式のカスタムフィールドであり、これまでそれをサポートするミドルウェアを見つけられていませんでした。

先月、nginxの有償版NGINX PlusがProxy Protocolバージョン2のカスタムフィールドに対応しVPCエンドポイントIDを参照できるようになったので、試してみた様子をレポートします。

NGINX Plusのセットアップ

  • NGINX Plusライセンス : フリートライアル
  • NGINX Plusバージョン : R16(Nginx 1.15.2)
  • Linux OS : Amazon Linux 2

NGINX Plusには30日間のフリートライアルがあるので、今回はそれを利用します。NGINX Plusのサイトでフリートライアルを申請するとアクティベーションのEメールが届くので、メール記載のリンクからカスタマーポータルサイトにアクセス、クライアント証明書の2つのファイルをダウンロードします。

そのあとの手順は以下のドキュメントに従います。

今回はAmazon Linux 2なので、それに対応するyumのリポジトリファイルを配置、先ほどダウンロードしたクライアント証明書をリポジトリから参照してyum経由でライセンス認証が行われる仕組みのようです。

$ sudo mkdir /etc/ssl/nginx
$ sudo yum install -y ca-certificates
(略)
$ sudo wget -P /etc/yum.repos.d https://cs.nginx.com/static/files/nginx-plus-amazon2.repo
(略)
$ sudo mv nginx-repo.crt nginx-repo.key /etc/ssl/nginx/
$ sudo yum install nginx-plus
読み込んだプラグイン:extras_suggestions, langpacks, priorities, update-motd
nginx-plus                                                                                                                                                                                                             | 2.9 kB  00:00:00
nginx-plus/2/x86_64/primary_db                                                                                                                                                                                         |  27 kB  00:00:01
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ nginx-plus.x86_64 0:16-1.amzn2.ngx を インストール
--> 依存性解決を終了しました。

依存性を解決しました

==============================================================================================================================================================================================================================================
 Package                                                  アーキテクチャー                                     バージョン                                                      リポジトリー                                              容量
==============================================================================================================================================================================================================================================
インストール中:
 nginx-plus                                               x86_64                                               16-1.amzn2.ngx                                                  nginx-plus                                               3.4 M

トランザクションの要約
==============================================================================================================================================================================================================================================
インストール  1 パッケージ

総ダウンロード容量: 3.4 M
インストール容量: 8.5 M
Is this ok [y/d/N]: y
Downloading packages:
nginx-plus-16-1.amzn2.ngx.x86_64.rpm                                                                                                                                                                                   | 3.4 MB  00:00:02
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  インストール中          : nginx-plus-16-1.amzn2.ngx.x86_64                                                                                                                                                                              1/1
----------------------------------------------------------------------

Thank you for using NGINX!

Please find the documentation for NGINX Plus here:
/usr/share/nginx/html/nginx-modules-reference.pdf

NGINX Plus is proprietary software. EULA and License information:
/usr/share/doc/nginx-plus/

For support information, please see:
https://www.nginx.com/support/

----------------------------------------------------------------------
  検証中                  : nginx-plus-16-1.amzn2.ngx.x86_64                                                                                                                                                                              1/1

インストール:
  nginx-plus.x86_64 0:16-1.amzn2.ngx

完了しました!
$

一度インストールしてしまえば、オープンソース版Nginxと同様に扱うことができます。

$ sudo service nginx start
Redirecting to /bin/systemctl start nginx.service
$ sudo service nginx status
Redirecting to /bin/systemctl status nginx.service
● nginx.service - NGINX Plus - high performance web server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; vendor preset: disabled)
  Drop-In: /usr/lib/systemd/system/nginx.service.d
           └─php-fpm.conf
   Active: active (running) since 月 2018-10-15 04:13:59 UTC; 5h 17min ago
     Docs: https://www.nginx.com/resources/
  Process: 5335 ExecStop=/bin/kill -s TERM $MAINPID (code=exited, status=0/SUCCESS)
  Process: 5511 ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf (code=exited, status=0/SUCCESS)
  Process: 5495 ExecStartPre=/usr/libexec/nginx-plus/check-subscription (code=exited, status=0/SUCCESS)
 Main PID: 5514 (nginx)
   CGroup: /system.slice/nginx.service
           ├─5514 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
           ├─5515 nginx: worker process
           └─5516 nginx: worker process

10月 15 04:13:59 ip-172-31-15-203.ap-northeast-1.compute.internal systemd[1]: Starting NGINX Plus - high performance web server...
10月 15 04:13:59 ip-172-31-15-203.ap-northeast-1.compute.internal check-subscription[5495]: Your trial subscription will expire in 30 days
10月 15 04:13:59 ip-172-31-15-203.ap-northeast-1.compute.internal systemd[1]: Started NGINX Plus - high performance web server.
$

これでNGINX Plusが起動しました。

VPCエンドポイントIDの参照

まずは、リクエストをProxy Protocolバージョン2で受け取るためにlistenディレクティブにproxy_protocolを指定します。

/etc/nginx/conf.d/default.conf(抜粋)

server {
    listen       80 proxy_protocol;
    #listen       80 default_server;
    server_name  localhost;
    :(略)

あとはヘッダをNGINX Plusが読み込んで変数に格納するので、VPCエンドポイントIDに対応する変数名$proxy_protocol_tlv_0xEAで参照すればOKです。今回アクセスログに以下の様に追記してみます。

/etc/nginx/nginx.conf(抜粋)

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$proxy_protocol_addr" "$proxy_protocol_tlv_0xEA"';
                      #'"$http_user_agent" "$http_x_forwarded_for"';

$proxy_protocol_addrはProxy Protocolヘッダに含まれるクライアントIPアドレスを指します。NLBはX-Forwarded-Forヘッダを操作しないので、その代わりに入れてみました。アクセスログを見てみると。。。

$ tail /var/log/nginx/access.log
172.31.4.8 - - [15/Oct/2018:03:57:33 +0000] "GET /h/ HTTP/1.1" 400 48 "-" "curl/7.55.1" "172.31.1.203" "\x01vpce-0123456789abcdef0"
172.31.4.8 - - [15/Oct/2018:03:57:34 +0000] "GET /h/ HTTP/1.1" 400 48 "-" "curl/7.55.1" "172.31.1.203" "\x01vpce-0123456789abcdef0"
172.31.4.8 - - [15/Oct/2018:03:58:58 +0000] "GET /h/ HTTP/1.1" 400 48 "-" "curl/7.55.1" "172.31.1.203" "\x01vpce-0123456789abcdef0"
  :

172.31.1.203がアクセス元のIPアドレス、vpce-0123456789abcdef0(架空のID)がVPCエンドポイントIDです。きちんとログに残せていますね。

アプリケーションへのVPCエンドポイントIDの伝達

エンドポイントサービスでマルチテナントなサービスを提供するのであれば、顧客ごとの認証やアクセス制限をアプリケーションで実装したいところです。そこでNGINX Plusで取得したVPCエンドポイントIDをリクエストヘッダにセットし、プロキシのアップストリーム(アプリケーション)に渡す構成を考えてみます。まずはproxy_set_headerディレクティブで任意のヘッダに$proxy_protocol_tlv_0xEAをセットしてみたのですが、NGINX Plusが400 Bad Requestを返すようになってしまいました。

$ curl vpce-0123456789abcdef0-XXXXXXXX.vpce-svc-0123456789abcdef0.ap-northeast-1.vpce.amazonaws.com/h/
400 Bad Request: invalid header value$

NGINX Plusのエラーログでエラー内容を拾おうとしたのですが、debugレベルにしてもそれらしいログが出て来ずハマりました。いろいろ試してみたところ変数の値に含まれる\x01が悪さをしている気がしてきたので、以下のように\x01を取り除いてリクエストヘッダにセットするようにしてみました。

/etc/nginx/conf.d/default.conf

server {
    listen       80 proxy_protocol;
    #listen       80 default_server;
    server_name  localhost;
    
    if ($proxy_protocol_tlv_0xEA ~* "\x01(.*)") {
        set $vpce_id $1;
    }

    location /h/ {
        proxy_pass   http://127.0.0.1:3000;
        proxy_set_header X-Forwarded-By $vpce_id;
    }
    :(略)

アプリケーションサーバーはGolangで実装し、ヘッダをレスポンスとして返すようにしてみました。

server.go

package main

import (
  "fmt"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Value: " + r.Header.Get("X-Forwarded-By"))
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":3000", nil)
}

あらためてリクエストを送ってみると...

$ curl vpce-0123456789abcdef0-XXXXXXXX.vpce-svc-0123456789abcdef0.ap-northeast-1.vpce.amazonaws.com/h/
Value: vpce-0123456789abcdef0$

NGINX Plusが読んだVPCエンドポイントIDを、きちんとアプリケーションサーバーで受けて取れていますね!

まとめ

NGINX PlusでPrivateLinkのVPCエンドポイントIDを参照する様子をご紹介しました。VPCエンドポイントIDがわかればAWSアカウントIDに対応づけることもできるので、マルチテナント、マルチアカウントでエンドポイントサービスをするときにはリバースプロキシとしてNGINX Plusを検討してみるのが良いと思います!

参考URL