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

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

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

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

![インフラ構成概要図](https://gyazo.com/0acfa91bf14dd65bf0faeccbe23afc5a.png)

# システム間連携の失敗原因
単なるチーム間のコミュニケーションミスです。

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

```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](https://asnokaze.hatenablog.com/entry/20130207/1360249692)

HTTP2でSSL/TLSを使う場合は、NPNかALPNのいずれかを使用します。
[HTTP/2 プロトコルネゴシエーション方法と ATS での実装](https://techblog.yahoo.co.jp/infrastructure/http2/ats_http2_pn/)

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

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

[HTTP/2.0のALPN利用に伴うSSL負荷分散装置の不具合にご注意下さい](https://jovi0608.hatenablog.com/entry/20131106/1383724662)

## NLBはALPNに対応していない？
NLBはALPN未対応のようです。
[gRPC と HTTP/2 と ALPN](https://kiririmode.hatenablog.jp/entry/20190707/1562456342)

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

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

```go
// 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メンバの[いっちーさん](https://qiita.com/Ichi0124)に感謝です。