Cloudflare Tunnel + NginxでローカルgRPCサーバーを無料で公開する

# やりたいこと
ローカルに立てたgRPCサーバーを無料で公開し、指定ドメインからアクセスできるようにします。

:::note warn
はじめに
ローカルサーバーの公開は自己責任でお願いします。

また、この記事で紹介する方法は、cloudflare公式で下記の通り、
https://developers.cloudflare.com/network/grpc-connections/
>Cloudflare Tunnel supports gRPC traffic via private subnet routing. Public hostname deployments are not currently supported.

パブリックホストでのgRPCは現在サポートしていないと明記されています。
突然利用できなくなる可能性がありますのでご注意ください。
:::

目次は次のとおりです。

- Cloudflare Tunnelの設定
- gRPCサーバーの構築
- Nginxの構築
- 動作確認

# 前提
<dl>
    <dt>マシン</dt>
    <dd>- macOS 14.6.1（筆者が試した環境）</dd>
    <dt>使用するツール</dt>
    <dd>- brew</dd>
    <dd>- cloudflared</dd>
    <dd>- Docker (Compose)</dd>
    <dd>- grpcurl (動作確認用)</dd>
    <dt>ドメイン</dt>
    <dd>- ご自身で作成しておいてください</dd>
    <dd>※当記事では「example.com」を所有している程で書いています。</dd>
    
</dl>

# Cloudflare Tunnelの設定
アカウントを持っていない方は適宜作成してください

https://www.cloudflare.com/

また、作成した後は、gRPCを受け付けられるように下記の設定を更新してください
左ペインの[ネットワーク]から設定できます。
![スクリーンショット 2024-12-29 19.23.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951986/186ce87d-2a31-1cd5-dfb2-428f5111a284.png)


## Cloudflared(cli)でtunnelの設定

1. **Cloudflare Tunnel CLIのインストール**  
   Cloudflareのトンネルを作成するために、CLIツール`cloudflared`をインストールします

https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/

2. **トンネルの作成**  
   以下のコマンドを実行してトンネルを作成します。`<トンネル名>`はなんでも良いです。
   この記事では`my-grpc-server`という名前で作成して進めます。
   ```bash
   cloudflared tunnel create <トンネル名>

   例：
   cloudflared tunnel create my-grpc-server
   ```
   実行結果の例：
   ```
   Tunnel credentials written to ~/.cloudflared/<トンネルID>.json. cloudflared chose this file based on where your origin certificate was found. Keep this file secret. To revoke these credentials, delete the tunnel.

   Created tunnel my-grpc-server with id <トンネルID>
   ```
   - 上記の実行後、トンネルの認証情報がJSONファイルとして自動生成されます
   - `<トンネルID>`はuuid形式で出力されます
　
3. **DNSにトンネルを登録**  
   トンネルIDをDNSに登録します：
   ```bash
   cloudflared tunnel route dns <トンネルID> <追加したいサブドメイン名>.<所有しているドメイン名>

   例：
   cloudflared tunnel route dns <トンネルID> my-grpc-server.example.com
   ```
   <p>
4. **`config.yml`の作成**  
   `/usr/local/etc/cloudflared/config.yml`に以下の内容を記述します：
   ```yaml
   tunnel: <トンネルID>
   credentials-file: ~/.cloudflared/<トンネルID>.json
   ingress:
     - hostname: my-grpc-server.example.com
       service: https://localhost:443
       originRequest:
         noTLSVerify: true
         http2Origin: true
     - service: http_status:404
   ```
    :::note info
    serviceのスキーマは**https**を指定すること。
    また、portはデフォルトでは次の中から設定することができます。
    
    - 443
    - 2053
    - 2083
    - 2087
    - 2096
    - 8443
    
    参考：[Cloudflare Network ports](https://developers.cloudflare.com/fundamentals/reference/network-ports/)
    ※上記リンクにも記載されていますが、443以外のポートはキャッシュが無効になっています
   :::
   <p>
5. **トンネルの実行**  
   以下のコマンドを実行してトンネルを起動します：
   ```bash
   cloudflared --protocol http2 tunnel --loglevel debug run my-grpc-server
   ```
    `--loglevel debug`は付けなくても良いです。
    付けていれば後の動作確認でリクエストを受信できているか確認できます。

# Docker Composeの用意
grpcとnginxのコンテナをそれぞれ定義します。
```:ディレクトリ構成
.
├── Dockerfile
├── compose.yml
├── main.go
├── helloworld
│  ┗─ 各pbファイル
├── nginx
│  ┗─ nginx.conf
┗── cert
  ├─ server.crt　<- Nginx構築時に作成します
  ┗─ servrt.key　<- Nginx構築時に作成します
```

```docker:Dockerfile
FROM golang:1.23

RUN go mod download
COPY . .

RUN go build main.go
```

```dockerfile:compose.yml
services:
  grpc-server:
    build: .
#    ports:            # grpc単体で動作確認する場合はコメントアウトを外す
#      - "50051:50051" # grpc単体で動作確認する場合はコメントアウトを外す
    networks:
      - network-grpc

  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - grpc-server
    networks:
      - network-grpc

networks:
  network-grpc:
    driver: bridge
```

# gRPCサーバの構築
 [こちら](https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go)のサンプルを借ります
    動作確認できるようにリフレクションを追加します。
```diff_go:main.go
import (
~~~
	"google.golang.org/grpc"
+	"google.golang.org/grpc/reflection"
~~~
)

~~~ 中略

func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
+   reflection.Register(s)
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
```
このサンプルサーバーは`localhost:50051`で動作します。
pbファイルやパッケージ名の解決は適宜行なってください。

# Nginxの構築
1. nginx.confの編集
    ```nginx:nginx.conf
    http {
        server {
            listen 443 ssl;
            server_name my-grpc-server.example.com;
            http2 on;
        
            # SSL証明書の設定
            ssl_certificate /etc/nginx/certs/server.crt;
            ssl_certificate_key /etc/nginx/certs/server.key;
        
            location / {
                grpc_pass grpc://grpc-server:50051; # docker-composeで利用しているコンテナ名
                
                # エラーハンドリング
                error_page 502 = /error502grpc;
            }
        
            location = /error502grpc {
                internal;
                default_type application/grpc;
                return 204;
            }
        }
    }
    ```
    <p>

2. 証明書はcloudflareコンソールから取得します
     下記の「証明書を作成」から証明書を作ります。
     左ペインの[SSL/TLS] > [オリジンサーバー]からアクセスできます。
    
    ![スクリーンショット 2024-12-29 18.15.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951986/17e8119b-1077-9c78-d584-c4ddc66bb784.png)
    
    遷移先の画面で、証明書と秘密鍵をコピーして、それぞれ分かりやすいファイル名で保存します。
    - sercer.crt: 証明書（`-----BEGIN CERTIFICATE-----`から始まる）
    - server.key: 秘密鍵（`-----BEGIN PRIVATE KEY-----`から始まる）



# 動作確認
1. docker-coposeを実行します: `docker-compose up`
2. まずはlocalhost経由でつながるか確認（`-insecure`で証明書をスキップ）
    ```
    grpcurl -insecure -d '{"name": "HELLO!"}' localhost:443 helloworld.Greeter/SayHello
    ```
    <p>
3. ドメイン経由で繋がるか確認
    ```
    grpcurl -d '{"name": "HELLO!"}' my-grpc-server.example.com:443 helloworld.Greeter/SayHello
    ```

以上です
# 参考
[Cloudflare Network ports](https://developers.cloudflare.com/fundamentals/reference/network-ports/)
https://github.com/cloudflare/cloudflared/issues/491
https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go

# おまけ
gRPCサーバーの方に証明書を読み込ませてportを443にすれば、Nginxをプロキシしなくても動作可能です。