connect-go+dockerでサーバー開発環境を構築する

本記事 `Cocone Advent Calendar 2023` 9日目の記事となります。

## はじめに

最近では様々な事情もあり、connect-goの利用を始めています。
今回は簡単な利用方法と、protocol bufferのコンパイルもDockerを利用し、すぐに開発開始できるようにしたいよね、ということで、その為に必要な作業をまとめました。
`※golang及びDockerの利用が可能、という前提で進めて行きます`

## connect-goについて

gRPC互換で、よりスリムに、よりスマートに利用できるHTTP APIライブラリです。今回はgolangで利用しますが、web/kotlin/swift/nodeなど、様々な環境への対応も進んでいます。
`Unary RPC` `Client Streaming RPC` `Server Streaming RPC` `Bidirectional Streaming RPC` へも対応しており、今回は触れませんが、各種interceptorも利用できます。

既にgRPCで構築されているシステムでも比較的容易に切り替えできますし、もし「アプリでリリース予定だが、webでのリリースも考えて行きたい」という状況でも、一つのコードでそのまま対応可能なため、クロスプラットフォームがもはやデファクトになりつつある昨今ではとても魅力的かと思います。

また後述しますが、最近ではクライアント側(Unity)のHTTP/2対応の話も出ているので、その点においても切り替え検討の余地はあると思います。

https://connectrpc.com/docs/go/getting-started/

## ディレクトリ構成

```
.
├─ protos
|  └─ echo.proto
├─ server
│  └─ main.go
├─ buf.gen.yaml
├─ docker-compose.yaml
├─ Dockerfile.proto
└─ generate-proto.sh
```

## proto

```protobuf:protos/echo.proto
syntax = "proto3";

package pb;
option go_package = ".;pb";

service EchoService {
    rpc Echo(EchoRequest) returns (EchoResponse) {}
}
  
message EchoRequest { string msg = 1; }

message EchoResponse { string msg = 1; }
```

## protoのビルドコンテナを準備

次にprotoがビルドできる環境を構築していきます。

### docker-compose.yaml
今回composeを利用して手軽に管理したかったため、docker-composeを利用しています。やっていることは単純で、必要なファイルをマウントし、ビルド用のスクリプトを立ち上げているだけです。

```yaml:docker-compose.yaml
version: "3.7"

services:
  proto_builder_connect_go:
    build:
      context: ./
      dockerfile: ./Dockerfile.proto
    volumes:
      - ./protos:/builder/protos
      - ./server/gen/pb:/builder/gen
    command: bash generate-proto.sh
```

### Dockerfile

次にDockerfileです。connect-goの利用に必要な各種モジュールをインストールしています。
```Dockerfile:Dockerfile.proto
FROM golang:1.20.10-bullseye

WORKDIR /builder

RUN apt update && apt install -y unzip rsync clang-format vim

COPY ./generate-proto.sh ./
COPY ./buf.gen.yaml ./

RUN go install github.com/bufbuild/buf/cmd/buf@latest
RUN go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN go install github.com/bufbuild/connect-go/cmd/protoc-gen-connect-go@latest

ENV PATH $PATH:/root/.local/bin/:/go/bin/
```

### buf.gen.yaml
ビルドの設定です。詳細は参考リンクを見ていただければともいますが、サンプルは `server/gen/pb` へ成果物を出力するものとなっています。

```yaml:buf.gen.yaml
version: v1
managed:
  enabled: true
  go_package_prefix:
    default: server/gen/pb
plugins:
  - name: go
    out: gen
    opt: paths=source_relative
  - name: connect-go
    out: gen
    opt: paths=source_relative
```
https://buf.build/docs/configuration/v1/buf-yaml

### generate-proto.sh
最後にビルドスクリプトです。`buf generate` を呼び出すことで、作成した protoファイル からgolangで利用できるコードを生成しています。

```bash:generate-proto.sh
#!/bin/bash

set -e -o pipefail

PROTO_DIR='/builder/protos'
OUT_TMP='/builder/gen'

buf generate --template buf.gen.yaml protos
exit 0
```

### 実行
```sh
docker-compose -f docker-compose.yaml up
```
実行が完了すると、 `server/gen/pb` ディレクトリに下記ファイルが生成されます。
これで準備が完了したので、実際にgolangで利用してみます。
* echo.pb.go
* pbconnect/echo.pbconnect.go

## golang側の準備

```sh
# モジュール初期化
cd server
go mod init server

# 必要なパッケージを取得
go get google.golang.org/protobuf
go get github.com/bufbuild/connect-go
go get github.com/bufbuild/connect-grpcreflect-go
go get golang.org/x/net

# メインファイル作成
touch main.go
```

サーバーの実装です。

```go:main.go
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"server/gen/pb"
	"server/gen/pb/pbconnect"

	"github.com/bufbuild/connect-go"
	grpcreflect "github.com/bufbuild/connect-grpcreflect-go"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
)

func main() {
	log.Println("setup server")
	// EchoServiceの作成
	echoService := EchoService{}

	// サービス登録
	mux := http.NewServeMux()
	handle := mux.Handle
	handle(pbconnect.NewEchoServiceHandler(echoService))

	// テスト用にリフレクションサービスを利用
	reflector := grpcreflect.NewStaticReflector(
		pbconnect.EchoServiceName,
	)
	handle(grpcreflect.NewHandlerV1(reflector))

	// サーバー起動
	log.Println("start server")
	if err := http.ListenAndServe(
		":8090",
		h2c.NewHandler(mux, &http2.Server{}),
	); err != nil {
		log.Fatal(err)
	}
}

// EchoService: EchoServiceの実装
type EchoService struct{}

// Echo: 送られてきたメッセージをそのまま返す
func (s EchoService) Echo(ctx context.Context, req *connect.Request[pb.EchoRequest]) (*connect.Response[pb.EchoResponse], error) {
	log.Println("EchoService.Echo:", req.Msg.Message)
	return connect.NewResponse(&pb.EchoResponse{
		Message: fmt.Sprintf("%s from server", req.Msg.Message),
	}), nil
}
```

## 実際に呼び出してみる

サーバーを起動し、 `grpcurl` または `curl` でアクセスしてみます。
```sh
cd server
go run main.go

>2023/12/06 09:42:44 setup server
>2023/12/06 09:42:44 start server
```

```sh
# curl
curl --header "Content-Type: application/json" --data '{"message": "hello"}' http://localhost:8080/pb.EchoService/Echo
>{"message":"hello from server"}

# grpccurl
grpcurl -plaintext -d '{"message": "hello"}' localhost:8080 pb.EchoService/Echo
>{
>  "message": "hello from server"
>}
```

## 最後に

Unity側のgRPCサポート問題が浮上する中、Cysharp様がUnityでHTTP/2を利用できる `YetAnotherHttpHandler` をリリースし、クライアント側もgRPCからHTTP/2へ切り替える場面が出てきています。

そのような場合に、より円滑に進行できる状況を先んじて作っておく事はとても重要で、その積み重ねがより良い開発環境・良いサービスに繋がっていくと思うので、現状gRPCを利用している方も是非この機会に切り替えを検討してみてはいかがでしょうか。

https://github.com/Cysharp/YetAnotherHttpHandler