ALBでgRPCを使う際にターゲット側もTLSしてみた

Application Load Balancingがターゲット側に対してもHTTP2に対応しました。リスナー側はTLSが必須ですが、ターゲット側はTLSが任意です。ターゲット側もTLSするにはどうすれば良いか気になり実際にやってみました。
2020.10.30

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

はじめに

おはようございます、加藤です。?ついに、ALBのターゲット側がHTTP/2、gRPCに対応しましたね。新機能としてのブログは誰かが書くと思うので、タイトルの通りこの図の赤字の部分でをTLSする方法を検証したのでブログとしてまとめます。

前提

下記のAWS News Blogを既に読んでいる方が対象です。

New – Application Load Balancer Support for End-to-End HTTP/2 and gRPC | AWS News Blog

後半、ALBやECSを作成する箇所がありますが詳しくは説明しません。下記を一度は触ったことがある方を想定しています。また、作成するのはドメインを所持している必要があります。

  • ACM
  • ALB
  • ECS(Fargate)
  • ECR
  • Route53

E2EでTLSは必須なのか?

The ALB supports both secure and insecure connections for target groups using the gRPC protocol.AWS News Blogで書かれておりHTTP/2、gRPCプロトコルで使用する場合はターゲット側はHTTP/SどちらでもOKです。

E2EでTLSできるのか?

はい、できます。しかし、ALBは必ずTLSを終端してしまうので、Client - ALBとALB - Backendは別途証明書(ACM発行以外で証明書を購入、独自発行)を用意してTLSする必要があります。

サンプルコードの場合は server.add_insecure_port('[::]:50051')の箇所を下記の様に書き換えると、E2EでTLSができます。合わせてTarget Groupを新規作成し、ProtocolをHTTPSにPortを443に設定してください。

# 検証用コードなので、関数内でimportするなど雑に書いています。
import os
port =  os.environ['PORT']
pkey = open('private_key.pem', 'rb').read()
chain = open('cert.pem', 'rb').read()
creds = grpc.ssl_server_credentials([(pkey, chain)])
server.add_secure_port(f'[::]:{port}', creds)

実際にE2EでTLSしてみる

AWS News Blogの手順とgRPCのサンプルコードをカスタマイズしてE2EでTLSしてみましょう。

最初にgRPCのリポジトリをクローンし、使いたいサンプルコードのディレクトリに移動します。

git clone https://github.com/grpc/grpc.git
cd grpc/examples/python/route_guide

続いて、独自証明書を発行します。

openssl req -x509 -nodes -newkey rsa:2048 -days 3650 -keyout private_key.pem -out cert.pem -subj "/CN=localhost"
openssl  x509 -in cert.pem -out root.crt

route_guide_server.py を編集します。serve関数を下記に書き換えます。

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    route_guide_pb2_grpc.add_RouteGuideServicer_to_server(
        RouteGuideServicer(), server)
    # 検証用コードなので、関数内でimportするなど雑に書いています。
    import os
    port =  os.environ['PORT']
    pkey = open('private_key.pem', 'rb').read()
    chain = open('cert.pem', 'rb').read()
    creds = grpc.ssl_server_credentials([(pkey, chain)])
    server.add_secure_port(f'[::]:{port}', creds)
    server.start()
    server.wait_for_termination()

Dockerfileを作成します。今回は検証なので、証明書もイメージの中に含めてしまいます。

FROM python:3.7
RUN pip install protobuf grpcio
COPY ./ /
CMD python route_guide_server.py
EXPOSE 50051

ECRにリポジトリを作成し、作成したコンテナイメージをPushします。YOUR_AWS_ACCOUNTはAWSアカウントIDに書き換えてください。

AWS_ACCOUNT_ID=YOUR_AWS_ACCOUNT
TAG=v0.1

docker build -t route-guide .
docker tag route-guide:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/route-guide:${TAG}
aws ecr create-repository --repository-name route-guide
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/route-guide:${TAG}

ALBを作成します。ListenersにはHTTPSを設定します。Certificateを求められるので、所有しているドメイン用のCertificateをACMで作成して設定します。

ターゲットグループを作成します。Target typeは必ずIPにします。Protocol versionがHTTP2、gRPCの場合はInstanceとIPをサポートしていますが、Fargateの場合はIPを指定する必要があります。

また、Health checksのProtocolもHTTPSにPathを/に変更してください。

具体的にターゲットを指定せずに作成を完了します。

ECSクラスタ(Network Only)を作成します。

タスク定義を作成します。TCPポートの50051番を開放し、環境変数PORT=50051に設定します。CPU、MEMは最低でOKです。

このタスク定義を使いサービスを定義します。そのときに作成したALBとTarget Groupをアタッチします。Security GroupのIngressで50051/TCPを開放することを忘れないようにしてください。ソースは0.0.0.0/0かALBでのSGを選択してください。

DNSサーバーでALBに対してALIASを設定します。私はドメインをRoute 53に登録しているのでRoute 53で設定を行いました。

最後にクライアントからアクセスしてみましょう。

route_guide_client.py のrun関数を下記に書き換えます。YOUR_DNS_NAMEはALBにALIASしているドメイン名に置き換えてください。

def run():
    # NOTE(gRPC Python Team): .close() is possible on a channel and should be
    # used in circumstances in which the with statement does not fit the needs
    # of the code.
    cred = grpc.ssl_channel_credentials(root_certificates=None, private_key=None, certificate_chain=None)

    with grpc.secure_channel('YOUR_DNS_NAME:443', cred) as channel:
        stub = route_guide_pb2_grpc.RouteGuideStub(channel)
        print("-------------- GetFeature --------------")
        guide_get_feature(stub)
        print("-------------- ListFeatures --------------")
        guide_list_features(stub)
        print("-------------- RecordRoute --------------")
        guide_record_route(stub)
        print("-------------- RouteChat --------------")
        guide_route_chat(stub)

必要なライブラリをインストールしてから、Clientを実行します。私はグローバル環境を汚したくないので、asdfとpipenvを使用し環境を切り替えてから下記のコマンドを実行しました。

pip install grpcio google-api-python-client
python ./route_guide_client.py

無事にレスポンスが帰ってきましたー

-------------- GetFeature --------------
Feature called Berkshire Valley Management Area Trail, Jefferson, NJ, USA at latitude: 409146138
longitude: -746188906

Found no feature at 
-------------- ListFeatures --------------
Looking for features between 40, -75 and 42, -73
Feature called Patriots Path, Mendham, NJ 07945, USA at latitude: 407838351
longitude: -746143763

(中略)

Feature called 3 Hasta Way, Newton, NJ 07860, USA at latitude: 410248224
longitude: -747127767

-------------- RecordRoute --------------
Visiting point latitude: 415830701
longitude: -742952812

(中略)

Visiting point latitude: 405002031
longitude: -748407866

Finished trip with 10 points 
Passed 10 features 
Travelled 704831 meters 
It took 0 seconds 
-------------- RouteChat --------------
Sending First message at 
Sending Second message at longitude: 1

Sending Third message at latitude: 1

Sending Fourth message at 
Sending Fifth message at latitude: 1

Received message First message at 
Received message Third message at latitude: 1

感想

念願のALBのHTTP2、gRPC対応ということで、ターゲット側がTLS必須なのか?どうやって設定するのかを検証しました。gRPC関係なく、独自証明を発行して(ryって手順が面倒だなーってのが素直な思いですね、本番だとコンテナイメージに証明書を含めずに起動時にS3からダウンロードするとか、Secrets ManagerにJSONとして保持していてファイルに書き起こすとか必要で、証明書の期限まで考えるととっても大変そう。ALBは管理下の信頼できるリソースなので、どうしてもっていう要件が無ければALB - Backendは平文で良いかなーって思いました。

今までgRPCは全く触ったことがないのですが、このブログを書くにあたってほんの少しは理解できて良かったです。

まとめ

  • HTTP/2、gRPCプロトコルのターゲットグループをアタッチするならリスナー側はHTTPSが必須です
  • ターゲット側はHTTP/2、gRPCプロトコルだとしてもHTTP/SどちらでもOKです
  • ターゲット側をTLSする場合は別途証明書が必要になります(追加の管理コストが発生します)
  • ALB→BackendへgRPC/TLSでアクセスする場合に証明書のCommon Nameはlocalhostで良い(恐らくなんでも良い)

引用元