ちょっと話題の記事

ECSでgRPC+ServiceDiscoveryな構成を試してみました

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

はじめに

先日開催された Mercari Tech Conf 2018 にて発表されたマイクロサービスアーキテクチャの話に触発されてgRPCをさわってみました。また、マイクロサービスといえば...で思い出して、まだ試していなかったECSのServiceDiscoveryもさわってみました。

用語の説明

gRPC

gRPCとは、Googleが開発したOSSで、RPC(Remote Procedure Call)をモダンに使えるようにしたフレームワークです。特徴としては、http/2ベースで様々な通信方法をサポートしており、多数のプログラミング言語に対応したライブラリとprotocol buffersをインターフェイス定義言語として用いることで異なるプログラミング言語間のサービスでも使うことができます。この他にも数多くの機能があるようです。

ServiceDiscovery

ServiceDiscoveryとは、動的に増えたり減ったりするサービスのIPやポート番号などを検知する機能のことです。ECSでも最近サポートされ、従来であればECS Serviceの前にALBなどを置くか自力でServiceDiscovery機能を用意する必要があったものが不要となりました。

Amazon ECS サービスディスカバリ | Amazon Web Services ブログ

概要

構築する構成の概要は以下の通りです。

                     ECS(Fargate)
                     +---------------------------------------+
                     |                                       |
                     |                        server.local   |
                     |    +----------+        +----------+   |
                     |    |          |        |          |   |
              HTTP   |    |          |  gRPC  |          |   |
 Internet ---------------->  Client  +-------->  Server  |   |
                     |    |          |        |          |   |
                     |    |          |        |          |   |
                     |    +----------+        +----------+   |
                     |                                       |
                     +---------------------------------------+

ClientサービスはHTTPリクエストを受け付けServerサービスにgRPCでリクエストを送信します。ServerサービスはgRPCリクエストを受け付け、レスポンスを返します。これはgRPCのサンプルコードをベースに多少の加工したものです。Clientサービスは、ServiceDiscoveryの機能により、プライベートドメイン(server.local)を指定することでServerサービスのIPやポート番号を知ることができます。

プロジェクトの構成は以下のとおりです。中身を詳しく知りたい方は、GitHubのリポジトリを参照してください。

.
├── ClientDockerfile
├── ServerDockerfile
├── greeter_client.js
├── greeter_server.js
├── helloworld.proto
└── package.json

ローカルで動作確認する場合は以下のように行います。

$ yarn

$ node greeter_server.js # localhost:50051で起動

# 別のターミナルで
$ node greeter_client.js # localhost:80で起動

# 別のターミナルで
$ curl -XGET localhost:80
Hello world

Hello world が出力されれば成功です。

構築

1.DockerイメージをPush

Clientサービス、ServerサービスそれぞれのリポジトリをECRに作成しておきます。

$ aws ecr create-repository --repository-name grpc-client
$ aws ecr create-repository --repository-name grpc-server

GitHubからcloneして、それぞれのサービスのイメージを作成してリポジトリにPushします。

# Client
$ docker build -f ./ClientDockerfile -t grpc-client .
$ docker tag grpc-client:latest {アカウントID}.dkr.ecr.us-east-1.amazonaws.com/grpc-client:latest
$ docker push {アカウントID}.dkr.ecr.us-east-1.amazonaws.com/grpc-client:latest

# Server
$ docker build -f ./ClientDockerfile -t grpc-server .
$ docker tag grpc-client:latest {アカウントID}.dkr.ecr.us-east-1.amazonaws.com/grpc-server:latest
$ docker push {アカウントID}.dkr.ecr.us-east-1.amazonaws.com/grpc-server:latest

2.ECSクラスターを作成

今回はFargateを使用しますのでネットワーキングのみを選択します。

クラスター名を入力して作成します。

3.タスク定義を登録

Clientサービスは grpc-client 、Serverサービスは grpc-server として登録します。

イメージのパスはそれぞれ、 {ECRのリポジトリのURL}:latset とします。

grpc-client のみ、環境変数に SERVER_HOST: grpc-server.local を追加します。

4.サービスを作成

Clientサービスは、パブリックIP経由のアクセスのみです。Service Discoveryは使用しません。

Serverサービスは、VPC内のアクセスのみですので、Service Discoveryを使用します。

DNSレコード型は、AレコードとSRVレコードが選択できますが、SRVレコードはDynamic Port Mappingの場合でしか利用できませんので注意してください。

Route53を確認すると local Hosted Zone が作成され、その中に grpc-server.local のAレコードが生成されていることがわかります。サービスの数を増やすと動的にAレコードが増え、サービスを減らすとAレコードも減ることが確認できます。

5.動作確認

curlでClientサービスのグローバルIPにリクエストをして、Hello world が出力されれば成功です。

$ curl -XGET {ClientサービスのグローバルIP}:80
Hello world

トラブルシューティング

検証中に遭遇したトラブルを誰かの役に立つかもしれないので載せておきます。

Service Discoveryの設定をしたがECS Service作成時にエラーが起きる

エラーメッセージ: The Service Discovery instance could not be registered.

原因: DNSレコード型に選択できない型を選択している可能性があります。DynamicPortMappingでない場合はSRVレコードは登録できません。

Service Discoveryの設定をしたがECS Service作成時にエラーが起きる(その2)

エラーメッセージ: Service already exists. (Service: AWSServiceDiscovery; Status Code: 400; Error Code: ServiceAlreadyExists; Request ID: ...)

原因: Serviceがすでに存在します。ここでいうServiceはAWSServiceDiscoveryの中のサービスです。AWSServiceDiscoveryの構造は、 ドメイン(Hosted Zoneに相当) > サービス(ECSのServiceに相当) > インスタンスサービス(Aレコード/SRVレコードに相当) になっています。AWSServiceDiscoveryのサービスはECSのServiceを作成したときに自動的に作成されますが、ECSのServiceを削除したときは自動で削除されません。そのため、一度作成したサービス名で再度ECSのServiceを登録する場合は、「サービスの検出サービスの設定」を「既存のサービスの検出サービスの選択」にして選択するか、既存のサービスを削除することで登録できます。なお、AWSServiceDiscoveryにはマネージメントコンソールがないため、aws-cliでの操作が必要です。

コンテナが起動しない

エラーメッセージ: CannotPullContainerError

原因: コンテナがイメージを取得するためにインターネットに出ていくことができない状態です。VPCからインターネットに出ていくことができるか、またはPublicIPがENABLEDであることを確認してください。

おわりに

サービス間の通信といえばHTTPベースのAPIくらいしか経験がなかったので、gPRCのように通信のレイヤーを意識することなくサービス間の通信ができることと、異なるプログラミング言語でも共有できるインターフェイス定義は良いなと思いました。もっと機能を深掘りしていきたいと思います。

ECSのServiceDiscoveryについては、既存のALBからの置き換えにはとても有用だと思います。ただ, 本格的なマイクロサービスを考えているのなら機能不足は否めないので素直にKubernetes(AWSならEKS)に乗るのがいいと思いました。