LoginSignup
18
13

More than 3 years have passed since last update.

NLB配下でgRPC通信するときに考えるALPN対応状況

Last updated at Posted at 2019-12-09

この記事を書いている背景

ステージング環境上でgRPCサーバの動作確認をしている中で、踏み台サーバからgrpcurlを実行するとリクエスト成功するのですが、システム間のgRPCリクエストが通らなくて困りました。
その原因自体は大したものではないのですが、もう少し突き詰めていくと面白い内容だったので、その時調べたことなどを残しておきます。
主にインフラ寄りの話になります。

インフラ前提

ECS/FargateでgRPCクライアントとgRPCサーバは別クラスタです。
システム(クラスタ)間の負荷分散はNLBを使用しています。
なぜ、NLBかというと、ALBはリスナーしかHTTP2対応しておらず、ターゲットはHTTP1.1のため、負荷分散できません。
NLBならL4なので負荷分散できます。

下はインフラ構成概要図となります。
System1はRESTの受け口を持ち、System2のgRPCクライアントでもあります。
踏み台サーバからNLB経由でgrpcurlなどのクライアントツールを利用し、System2のgRPCサーバのAPIを実行することもできます。

インフラ構成概要図

システム間連携の失敗原因

単なるチーム間のコミュニケーションミスです。

バックエンドチームは、平文通信を想定して実装していました。
以下の通り、WithInsecure関数を利用。言語はGoです。

grpc.Dial(fmt.Sprintf("%s:%s", os.Getenv(gRPCHostEnvKey), os.Getenv(gRPCPortEnvKey)), grpc.WithInsecure())

一方SREチームは、暗号化通信を想定し、NLBのリスナーをssl/tlsのみに絞っていました。

平文用のリスナーに変更したところ、システム間でgRPCリクエストが成功しました。
内部通信なので平文でもセーフです。

本題(ALPNを巡る)

ここからが面白いところです。
上記の原因を発見するまでに、色々寄り道をして、「ALPN」という単語を知りました。
今回は上述の通り、ALPNは直接的な問題になり得なかったのですが、今後のことも考えて色々調べました。

ALPNとは

ALPN(Application-Layer Protocol Negotiation)とは、プロトコルネゴシエーションを行うためのTLS拡張です。
クライアントは自身が使用可能なプロトコル一覧をサーバに渡し(ClientHello)、サーバ側はその中から選択し(ServerHello)、TLSハンドシェイクが完了。あとはその上で、通信します。
TLS上でのプロトコルネゴシエーションの仕組み、NPNとALPN

HTTP2でSSL/TLSを使う場合は、NPNかALPNのいずれかを使用します。
HTTP/2 プロトコルネゴシエーション方法と ATS での実装

ただ、NPNよりもALPNの方が本命のようです。
理由はこちらです。

NPNではクライアントがプロトコルを選択するが、ALPNではサーバがプロトコルを選択する。サーバ側が選択権を持つのは他のセキュリティ技術(暗号種類の決定等)で行われているやり方なので、そのポリシーに従う。
NPMは3回クライアント、サーバ間でやり取りが発生するのに対し、ALPNは2回で済むので効率的である。

HTTP/2.0のALPN利用に伴うSSL負荷分散装置の不具合にご注意下さい

NLBはALPNに対応していない?

NLBはALPN未対応のようです。
gRPC と HTTP/2 と ALPN

別プロジェクトで実際に検証した結果からも、NLBがALPN未対応であることが分かりました。

grpc-goはALPNに対応していない?

仮にNLBのリスナーがssl/tlsのみだとして、grpc-goのWithTransportCredentials関数を利用してtls有効でgRPCリクエストを実行するとどうなるのでしょうか?

// WithTransportCredentials returns a DialOption which configures a connection
// level security credentials (e.g., TLS/SSL). This should not be used together
// with WithCredentialsBundle.
func WithTransportCredentials(creds credentials.TransportCredentials) DialOption {
    return newFuncDialOption(func(o *dialOptions) {
        o.copts.TransportCredentials = creds
    })
}

時間が足らず、試せていなくて恐縮なのですが、おそらく成功します。
理由は、grpc-goがALPNに未対応だからです。
issueはこちら。
https://github.com/grpc/grpc-go/issues

java-grpcの開発者がissueを立てているあたり、JavaはALPN対応済みなのでしょう。
また、Node.jsもALPN対応済みのようですね。

grpcurlはALPNに対応していない?

grpcurlのオプション

grpcurlにはtls関連で3つ選択肢があります。

  1. plaintext(tls無効方式)
  2. insecure(形式としてはtls有効だけど、証明書の中身は検証しない方式)
  3. オプション無し(tls有効方式)

-plaintext
Use plain-text HTTP/2 when connecting to server (no TLS).

-insecure
Skip server certificate and domain verification. (NOT SECURE!) Not
valid with -plaintext option.

蛇足ですが、grpcurlの「insecure」はtls有効だけど、go-grpcの「grpc.WithInsecure」はtls無効で、同じ「Insecure」でも意味がことなるため、少し混乱します。
開発団体が全然違うからしょうがないけど。

実行結果

insecureオプションあるいはオプション無しのどちらも(つまりtls有効)リクエスト成功しました。
NLBがALPN未対応で、grpcurlがALPN対応ならば、これらは成功しないはずです。
つまり、grpcurlはALPN未対応なのかと考えました。

ところが、issueあげてみたところ、grpcurlはALPN対応とのことでした。
grpcurl(クライアント)はALPNを使うように要求するが、grpc-go(サーバ)がALPN未対応のため、ALPN自体は成功しない。
ただし、grpc-goの仕様によりTLSハンドシェイクは成功するため、tlsでお話ができるようになるようです。
https://github.com/fullstorydev/grpcurl/issues/125

振り返り

今回、内部通信なのでssl/tlsでなくても良いし、たとえssl/tls有効必要な外部通信だとしても、grpc-goならALPN未対応だからNLBでも問題なさそうだということがわかりました。

しかしながら、ALPN対応のgrpcライブラリが用意されているJavaやNode.jsなどの言語で、かつ外部通信用にgRPCのAPIを公開(NLB使う)する方式だと苦しいなと思いました。
この場合は、EnvoyやNginxにTLS終端させて、ACMは諦めるという方向になるでしょうか。。。

通信通ったからいいやではなく、気になったところをどんどん調べていくことで勉強になりました。
最後に、この問題に付き合って色々調べてくれた弊社SREメンバのいっちーさんに感謝です。

18
13
2

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
18
13