grpc-elixirでGoと通信してみる #1

（この記事は、「fukuoka.ex Elixir／Phoenix Advent Calendar Advent Calendar 2018」の11日目です）

昨日は @curry_on_a_rice さんの[**Elixirでニューラルネットを実装しようとした話**](https://qiita.com/curry_on_a_rice/items/c657b3cd5a91a8f56550)でした。

# grpc-elixirについて

gRPCはマイクロサービス用の通信プロトコルとして脚光を浴びていますが、今回はそのElixir実装である、[gRPC Elixir](https://github.com/tony612/grpc-elixir)をご紹介します。C++, Node.js, Python, Ruby, Objective-C, PHP, C#, Java, Goと、様々な言語サポートがある一方で、Elixirは蚊帳の外だったのですが、Tony Han氏によりgRPCのElixir実装が進んでいます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/66748/bc2fe6db-3ce7-b305-5760-d66a8f330815.png)



gRPCに関しては[マイクロサービスバックエンドAPIのためのRESTとgRPC](https://www.slideshare.net/disc99_/apirestgrpc)をご覧ください。


~~2018年12月現在はα版ですが、~~( ⇒ 12/11α版表記取れました ! )
他言語との連携を確認する分には問題ないので、今回ご紹介することにしました。ロードマップを見る限りは、圧縮・ロギング・外部エンコーディング以外の実装は済んでます。

gRPC公式には単純な接続を確認するHello Worldと、双方向ストリーミングを行うRoute Guideというサンプルがあります。elixir-grpcでは両方とも実装されているので、２つのサンプルを使った通信をPlay with Dockerを使用して行います。


~~いわゆる「体裁」面はほぼ実装済みで、β版への移行は内部で使用しているgunなどのhttp2の関連ライブラリの安定待ちではないかと、私は推測しています。~~
本コラム投稿直前に正式版となりました！

## 以降の手順で必用とされるスキル
vimでファイルの編集ができること
Dockerコンテナで中と外の居場所の区別がつくこと

# 一般的な実装手順

今回は単項目の呼び出しに限定して、解説します。
導入手順に関しては、[grpc-elixir](https://github.com/tony612/grpc-elixir)のリポジトリに丁寧に書いてあるので、ここではその手順を軽く辿るのみとします。

## 0. プロジェクトへのインストール

mix.exsにパッケージを追加します

```elixir:mix.exs
def deps do
  [{:grpc, github: "tony612/grpc-elixir"}]
end
```

```
mix deps.get
```

## 1. protocol buffer elixirプラグインのインストール

 gRPCでは、.proto拡張子が付くIDL(インターフェス定義言語)を定義します。protocツールを使って、各言語用のスキーマーや構造体を作成します。各言語用のprotocプラグインがあるのですが、なんと、elixirプラグインである[elixir-protobuf](https://github.com/tony612/protobuf-elixir#usage)もtony氏が作られたものです。

以下のコマンドを使ってprotocのプラグインをインストールします。（protocコマンドはOSの[パッケージマネージャーでインストール](https://github.com/protocolbuffers/protobuf/blob/master/src/README.md)してください）

```
mix escript.install hex protobuf --force
```

インストール後は`~/.mix/escripts`にパスを通す必要があります。

```
PATH=~/.mix/escripts:$PATH
```

## 2. protoファイルから、構造体ファイル(.pb.ex)を作成

```
protoc --elixir_out=plugins=grpc:./lib/ helloworld.proto
```

IDLの[hellowold.proto](https://github.com/tony612/grpc-elixir/blob/master/examples/helloworld/priv/protos/helloworld.proto)からは、`.pb.ex`という拡張子で次のようなRequestとReply用のモジュールが生成されます。

```elixir:hellowolrd.pb.ex
defmodule Helloworld.HelloRequest do
  use Protobuf, syntax: :proto3

  @type t :: %__MODULE__{
    name: String.t
  }
  defstruct [:name]

  field :name, 1, type: :string
end

defmodule Helloworld.HelloReply do
  use Protobuf, syntax: :proto3

  @type t :: %__MODULE__{
    message: String.t
  }
  defstruct [:message]

  field :message, 1, type: :string
end

defmodule Helloworld.Greeter.Service do
  @moduledoc false
  use GRPC.Service, name: "helloworld.Greeter"

  rpc :SayHello, Helloworld.HelloRequest, Helloworld.HelloReply
end

defmodule Helloworld.Greeter.Stub do
  @moduledoc false
  use GRPC.Stub, service: Helloworld.Greeter.Service
end
```

## 3.クライアント側コード

クライアント側のコードを対話環境で実行すると以下の通りとなります。

```elixir
iex> GRPC.Server.start(Helloworld.Greeter.Server, 50051)
iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051")
iex> request = Helloworld.HelloRequest.new(name: "grpc-elixir")
iex> {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(request)
```

## 4.サーバー側コード

また、サーバー側のコードは以下の通りです。
上記で定義したモジュールを使いながら記述していきます。

```elixir
defmodule Helloworld.Greeter.Server do
  use GRPC.Server, service: Helloworld.Greeter.Service

  @spec say_hello(Helloworld.HelloRequest.t(), GRPC.Server.Stream.t()) ::
          Helloworld.HelloReply.t()
  def say_hello(request, _stream) do
    Helloworld.HelloReply.new(message: "Hello #{request.name}")
  end
end
```
以下のようにApplicationモジュールのSupervisorツリーにhelloworldサーバーを登録します

```
defmodule YourApp do
  use Application
  def start(_type, _args) do
    import Supervisor.Spec
    children = [
      # ...
      supervisor(GRPC.Server.Supervisor, 
    [{Helloworld.Greeter.Server, 50051}])
    ]

    opts = [strategy: :one_for_one, name: YourApp]
    Supervisor.start_link(children, opts)
  end
end
```

mix.exsにアプリケーションモジュールを登録します


```elixir:mix.exs
  def application do
    [
      mod: {YourApp, []}, # これを登録
      extra_applications: [:logger]
    ]
  end
```

config.exsにアプリケーション設定するか、mixコマンドで待ち受けが可能です。

```
mix grpc.server
```

# Docker-composeで双方向通信環境を作る

ここからは、docker環境を作り相互通信をするフェーズです。

２つ以上の言語を使った通信なので、環境まわりを楽にすべくDocker-composeを使います。といっても、ファイルを３種コピペすれば通常のDocker環境で実行できるので、気軽に読み進めて下さい。

ここでは２種類のコンテナを一気に立ち上げます
- Elixirコンテナ **`elixir-node`**
- Golangコンテナ **`go-node`**

## [Play with Docker](https://labs.play-with-docker.com/)について
無料で４時間Docker環境が使えるサービスです。Docker Hubに登録していれば誰でも使用できます。ホストVMのメモリも4GBでvCPU8コアなので、速度はかなり速いです(個人の感想です)。Docker Hubに登録すればすぐ使用できるので、ご登録をお勧めします。

詳しくは[Docker 入門にはインストールなしで使える「Play with Docker」がいいと思う](https://qiita.com/tag1216/items/ec7228440f6eaf849cd6)をご覧ください。


以下の説明はPlay with Dockerにて進めますが、ご自分のPCにDockerをインストールされている方も、ほぼ変わらない手順で動作します。（touch等必用ない分そちらが楽です）

## コンテナ構築

Play with Dockerで行う場合は、内臓のEditorを使うためにtouchコマンドでまず空ファイルを３種作ります。（Vimも使えますが、コピペに難があります）

コンテナには、elixir-nodeとgo-nodeという名前を付けています。docker-composeだとホスト名として扱えるので生IPアドレスを扱わなくて済むので楽です。

まずは「ADD　NEWINSTANCE」ボタンを押して下さい。ブラウザ内にLinuxコンソールが立ち上がります。そこで以下を入力します。

```
$ touch docker-compose.yml
$ touch Dockerfile-go
$ touch Dockerfile-elixir
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/66748/4c0fcc72-0fb9-cf31-6f2c-ccb24772fa7e.png)


Editorボタンを押すと、数秒後にフォルダ構成が出て来ます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/66748/b12526fd-5f31-b3d5-e3e7-0dbfda38df0f.png)


それぞれのファイルに以下をそれぞれコピペしてSaveボタンを押してください。

### Dockerfile-elixir

公式のaplineコンテナを使用して、grpc-elixirのリポジトリをクローン。protobuf関連の準備をするコンテナです。

```Dockerfile:Dockerfile-elixir
FROM elixir:1.7.4-alpine

RUN apk -U update && apk --update --no-cache add protobuf git vim && \
    cd ~ && \
    git clone https://github.com/tony612/grpc-elixir.git && \
    mix local.hex --force && \
    mix local.rebar --force && \
    mix hex.info && \
    mix escript.install hex protobuf --force

ENV PATH /root/.mix/escripts:$PATH

CMD ["ash"]
```

### Dockerfile-go
gRPCの公式コンテナを引用して、Goのパッケージを準備を行います。
各種サンプルを事前コンパイルして、helloworldのサーバーを実行して待機する内容です。

```Dockerfile:Dockerfile-go
FROM grpc/go:latest

RUN go get -u golang.org/x/net/http2 golang.org/x/net/context && \
    cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_server && \
    go build main.go && \
    cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_client && \
    go build main.go && \
    cd /go/src/google.golang.org/grpc/examples/route_guide/server && \
    go build server.go && \
    cd /go/src/google.golang.org/grpc/examples/route_guide/client && \
    go build client.go

CMD ["/go/src/google.golang.org/grpc/examples/helloworld/greeter_server/main"]

EXPOSE 50051 10000
```

### docker-composer.yml

elixir-nodeとgo-nodeを定義します。２つのコンテナの由来や関連付けを行います。コメント化された一行は次回で使用します。

```yml:docker-compose.yml
version: '3'
services:
  elixir-node:
    build:
      context: .
      dockerfile: Dockerfile-elixir
    command: ash
    tty: true
    environment:
      - MIX_ENV=dev

  go-node:
    build:
      context: .
      dockerfile: Dockerfile-go
    # command: sh -c "cd /go/src/google.golang.org/grpc/examples/route_guide/ && server/server"
```

エディタを閉じコンソールに戻ります。
以下を起動すると、ビルドが始まりますのでしばらくお待ちください。

`docker-compose.yml`を見てわかるように、`elixir-node`と`go-node`という２つのコンテナを同時に起動します。

```
docker-compose up -d
```

## HelloWorld gRPC サンプル

Elixirのコンテナにashを起動します。

### HelloWorld : Elixir -> Golang

```bash
$ docker-compose exec elixir-node ash
```

以下elixir-nodeのコンテナ内での作業です。フォルダ移動と依存関係の取得、コンパイルを行います。

```
# cd ~/grpc-elixir/examples/helloworld
# mix deps.get && mix compile
```

まずはクライアントを実行してみます。
priv/client.exsにクライアント用のコードが用意されているのですが、サーバーがlocalhost固定ですので、ここは動きを理解するためにも、手動で実行します。

```
# iex -S mix
```

まずはGRPC.Stub.connect関数でコンテナgo-nodeのgrpcサーバーとの接続を確立します。

```elixir
iex(1)> {:ok, channel} = GRPC.Stub.connect("go-node:50051")
{:ok,
 %GRPC.Channel{
   adapter: GRPC.Adapter.Gun,
   adapter_payload: %{conn_pid: #PID<0.216.0>},
   cred: nil,
   host: "go-node",
   port: 50051,
   scheme: "http"
 }}
``` 

無事接続できました。protoファイルで定義されているsay_hello関数を実行します。

```elixir
iex(2)> {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir"))
{:ok, %Helloworld.HelloReply{message: "Hello grpc-elixir"}}
```

構造体に`Hello grpc-elixir`という文字列が返ってくることが確認できました。


順調に行き過ぎてるように見えるので、接続先を変えてみます。

```elixir
iex(3)> {:ok, channel2} = GRPC.Stub.connect("localhost:50051")
** (MatchError) no match of right hand side value: {:error, "Error when opening connection: :timeout"}
```

きちんと、タイムアウトでエラーとなりましたね。

ping代わりのテストなので大したことをやってるわけではないのですが、priv/client.exsを見てわかるとおり、スタブに対してsay_hello関数を呼び出すだけでRPCが実行できる簡単な流れとなっているのがおわかり頂けると思います。

Ctrl+C を２回押してiexから抜けてください。

### HelloWorld : Golang -> Elixir

こんどは、Elixir側を待ち受けにしてGolang側からAPIを叩いてみます。

以下でgrpcサーバーが待ち受けとなります。

```
# mix grpc.server
```

Ctrl+p, Ctrl+q でコンテナを抜けて、go-nodeのコンテナに入ります。

```
$ docker-compose exec go-node bash
```

helloworld/greeter_clientフォルダーに移動します

```
cd /go/src/google.golang.org/grpc/examples/helloworld/greeter_client
```

サンプルプログラムはLocalhostのサーバーに対して、リクエストを送るようになっているので宛先をelixir-nodeに変えます。このコンテナにはvimが入ってないので、sedでlocalhostを強制変換して再コンパイルします。

```
# sed -i -e s/localhost/elixir-node/ main.go
# go build main.go
```

実行ファイルを起動します。

```
# ./main
2018/12/10 05:11:17 Greeting: Hello world
```

無事にElixirとの接続が確認できました。

コンテナを抜け、コンテナを終了させましょう。

```
# exit
$ docker-compose down
```

# まとめ

protcのプラグインのインストールと、Stubの作成を乗り越えてしまえば、簡単に多言語と通信できることが理解頂けたのではないかと思います。今回は、HelloWorldのみでしたが、次回は１接続でストリーミングを行うコードを含んだRoute Guildサンプルを試してみます。お楽しみに。

---


　明日のfukuoka.ex Elixir／Phoenix Advent Calendar 2018 12日目の記事は， @tuchiro さんの「[BehaviorとMix.Configで切り替え可能なStubを実装する](https://qiita.com/tuchiro/items/a13592b242009e902b20)」です。こちらもお楽しみに！