Rust と Cloud Run でサーバレスな (Unary) gRPCサーバを構築する方法

## 概要

**ストリーミング対応版の記事を公開しました。最新情報はそちらをご覧ください。**
https://qiita.com/K-Kachi/items/8aa6c81990b7e8c6a628

Cloud Run とはグーグルが提供する サーバレスコンピューティングサービス の１つで、ステートレスなコンテナを実行可能なサービスである。
ウェブリクエストに応答する任意のコンテナを実行可能なため自由度が高い。

最近（といっても数か月前だが）、新機能としてUnary gRPC プロトコル（非ストリーミングの gRPC）のサポートが[発表された](https://cloud.google.com/blog/ja/products/serverless/a-developers-guide-to-new-features-in-cloud-run)。
そこで、Go言語でのチュートリアルを参考にしつつ、試しにRustでgRPCサーバを構築してみた。

## RustでgRPC

[tonic](https://github.com/hyperium/tonic) はgRPCサーバ・クライアントをRustで実装したライブラリである。
このライブラリのリポジトリにあるExamplesをもとに Cloud Run で実行可能なgRPCサーバを構築する。

まずはソースコードをとってくる。

```bash
git clone https://github.com/hyperium/tonic.git
cp -r tonic/examples tonic-examples
cd tonic-examples
```

続いて`Cargo.toml`をいじる。

```diff:Cargo.toml
106c106
< tonic = { path = "../tonic", features = ["tls", "data-prost"] }
---
> tonic = { version = "0.1", features = ["tls", "tls-roots"] }
131c131
< tonic-build = { path = "../tonic-build", features = ["prost"] }
---
> tonic-build = "0.1"
```
Cloud Run では https での呼び出しが必須のため`tls`関連が必要になってくる。

tonicが外部クレートとして利用されることによる変更も適宜行う。

```bash
sed -i 's/::prost//g' build.rs 
```

この段階でサンプルコードが動作することを確かめておく。

```bash
cargo run --bin helloworld-server
# 別のターミナルで実行
cargo run --bin helloworld-client
```

## Cloud Run で呼び出すための変更

IPアドレスとポート番号を変更する。
ポート番号は環境変数から与える。

```diff:src/helloworld/server.rs
30c30,31
<     let addr = "[::1]:50051".parse().unwrap();
---
>     let port = std::env::var("PORT").unwrap_or("8080".into());
>     let addr = format!("0.0.0.0:{}", port).parse().unwrap();
```

Dockerfileも用意する。証明書関連のパッケージもインストールしていることに注意。
テストが目的なのでコンパイルの最適化を行っていないが、**本番環境で利用する場合は`--release`フラグを付ける**のを忘れずに。その際、バイナリの出力される場所が`./target/debug`ではなく`./target/release`になることに注意。

```Dockerfile
FROM rust:1.41.0 as build-env
WORKDIR /app
ADD . .
RUN rustup update && rustup component add rustfmt && cargo build --bin helloworld-server

FROM debian:stretch-slim
WORKDIR /app
RUN apt-get update && apt-get install -y libgcc1 libgomp1 libstdc++6 ca-certificates &&  update-ca-certificates && rm -rf /var/lib/apt/lists/*

COPY --from=build-env /app/target/debug/helloworld-server /app
ENTRYPOINT ["./helloworld-server"]
```

## ビルド

Dockerfileはローカルでビルドしてもよいが、Cloud Buildを使ったほうが個人的には早くて便利。
`.gcloudignore`でバイナリフォルダを除いておく。

```text:.gcloudignore
target/
```

```bash
gcloud builds submit --machine-type=n1-highcpu-32 --tag gcr.io/$PROJECT_ID/helloworld-grpc-rust
```


## デプロイ

`gcloud`コマンドが使用できることを前提とする。
まずリージョンを設定しておく。以下のコマンドでは東京リージョンを選択している。

```bash
gcloud config set run/region asia-northeast1
```

コンテナイメージを指定してデプロイ。

```
 gcloud run deploy helloworld-grpc-rust --image  gcr.io/$PROJECT_ID/helloworld-grpc-rust
```

途中で質問されるので　Cloud Run (fully managed)　と　Allow unauthenticated invocations [Yes]　と答える。

無事デプロイされるとデプロイ先のURLが出力されるはずである。

## gRPC クライアント

出力されたURLをクライアントのコードに貼り付ける。そのままではHTTPSに対応していないので修正する。

```diff:src/helloworld/client.rs
0a1,2
> use tonic::transport::{Channel, ClientTlsConfig};
>
10c12,19
<     let mut client = GreeterClient::connect("http://[::1]:50051").await?;
---
>
>     let tls = ClientTlsConfig::new()
>         .domain_name("デプロイ先");
>
>     let channel = Channel::from_static("https://デプロイ先")
>         .tls_config(tls)
>         .connect()
>         .await?;
15a25
>     let mut client = GreeterClient::new(channel);
```

## まとめ

Rustは神。Cloud Run は最強。