gRPCに入門します

# はじめに

以前、私はBFFアーキテクチャとGraphQLに入門ということで、NestJSとNextjsを使用してGraphQLでクエリが実際に実行される構築してみました。

https://qiita.com/y_inoue15/items/38d183b9806436a16b1c

これらの技術と関連して、gRPCなるものがよく使用されているのを見ました。
なので、今回はgRPCがどのようなものなのか実際に試してみようと思います。

今回作成したものこちら

https://github.com/y-inoue15/grpc-graphql

この記事はTDCソフト株式会社Advent Calendarの5日目です。

https://qiita.com/advent-calendar/2024

# gRPCとは？

gRPCは、Googleが開発したRPC(Remote Procedure Call)システムらしいです。

https://grpc.io/

ではそもそもRPCとは何でしょうか。

> RPC（Remote Procedure Call）とは、ネットワーク上で接続されたほかのコンピュータのプログラムを呼び出して実行させるための技術、またはそのためのプロトコルのことです。RPCは日本語で「遠隔手続き呼び出し」と訳されます。
> 遠隔地にある端末が互換性のあるRPC規格を採用していれば、機種、OS、プログラミング言語が異なる場合でも、手元の端末からコマンドを入力して処理を実行することができます。
また、プログラムの処理結果を遠隔地にいながら受け取ることも可能となります。

https://www.ntt-west.co.jp/business/glossary/words-00229.html

つまりプログラミング言語問わず、クライアントからサーバーのある関数を直接呼び出して結果を受け取る技術がRPCで、gRPCはこのRPCの仕組みをさらに効率化し、モダンなアプリケーションに適した形にしたものになります。

# NestJSとPythonによるgRPCアーキテクチャの構築

今回はBFF（Backend for Frontend）としてNestJSを使用し、バックエンドをPythonで構築する例を紹介します。
GraphQLでNestJSにリクエストを送り、gRPCを使ってNestJSからPythonにリクエスト・結果を取得するまでの流れを試してみます。

# Protoファイルを作成
まずは、NestJSとPythonで共有するProtoファイルを作成します。
Protoファイルについては詳しく説明しませんが、インターフェースを記述しておくファイルです。
Protoファイルから各言語でコードを自動生成することで、言語やプラットフォームに依存することなくアプリケーション間で構造化データを簡単に読み書きすることができるようになります。

今回、NestJSとPythonで共有します。

```proto:posts.proto
syntax = "proto3";

package posts;

// Postデータモデル
message Post {
  string id = 1;
  string title = 2;
  int32 views = 3;
}

// サービス定義
service PostsService {
  rpc FindAll (Empty) returns (PostList);
  rpc FindOne (PostId) returns (Post);
}

// リクエスト/レスポンスメッセージ
message Empty {}

message PostId {
  string id = 1;
}

message PostList {
  repeated Post posts = 1;
}
```

# Backend(Python)
バックエンドのPythonを構築していきます。

## ライブラリインストールからコード生成まで
まずは、PythonでgRPCを動かすためのライブラリをインストールします。

```bash
pip install grpcio grpcio-tools
```

次に、ProtoファイルからPythonコードを生成します。
posts.protoをプロジェクトルートにおいて実行します。
```bash
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. posts.proto
```

これにより、posts_pb2.pyとposts_pb2_grpc.pyが生成されます。

## gRPCサーバーの実装
PythonでgRPCサーバーを実装します。
実際はDBとのやり取りなどが発生しますが、今回は固定値を返却します。

```python:server.py
from concurrent import futures
import grpc
import posts_pb2
import posts_pb2_grpc

POSTS = [
    {"id": "1", "title": "a title", "views": 100},
    {"id": "2", "title": "another title", "views": 200},
    {"id": "3", "title": "third title", "views": 300},
]

class PostsService(posts_pb2_grpc.PostsServiceServicer):
    def FindAll(self, request, context):
        return posts_pb2.PostList(posts=[
            posts_pb2.Post(id=post["id"], title=post["title"], views=post["views"])
            for post in POSTS
        ])

    def FindOne(self, request, context):
        for post in POSTS:
            if post["id"] == request.id:
                return posts_pb2.Post(id=post["id"], title=post["title"], views=post["views"])
        context.set_code(grpc.StatusCode.NOT_FOUND)
        context.set_details("Post not found")
        return posts_pb2.Post()

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    posts_pb2_grpc.add_PostsServiceServicer_to_server(PostsService(), server)
    server.add_insecure_port("[::]:50051")
    print("gRPC server is running on port 50051")
    server.start()
    server.wait_for_termination()

if __name__ == "__main__":
    serve()
```

## 動作確認
gRPCサーバーを起動して動作を確認します。
以下のコマンドを実行し、gRPCサーバーを起動します。

```bash
python server.py
```

ログに「gRPC server is running on port 50051」と表示されれば完了です。

# BFF(NestJS)
BFFのNestJSを構築していきます。

## ライブラリインストールからコード生成まで
まずは、NestJSでgRPCを動かすためのライブラリをインストールします。
nest cliを使用していきます。

```bash
nest new grpc-nest-bff
cd grpc-nest-bff
npm install @nestjs/microservices @nestjs/graphql @grpc/grpc-js @nestjs/apollo ts-proto
```
次に、ProtoファイルからTypescriptコードを生成します。
posts.protoをsrc/posts/において実行します。

```bash
npx protoc --ts_proto_opt=nestJs=true --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=. ./src/posts/posts.proto  
```

これにより、posts.tsが生成されます。

また、以下のコマンドを実行し、必要なコードを生成します。

```bash
nest g module posts
nest g resolver posts --no-spec
nest g service posts --no-spec
nest g class posts/models/post --no-spec
```

これで必要なファイルが自動で作成されます。

## 各機能の実装
NestJSで必要な機能を実装していきます。

### moduleを定義
モジュールを定義します。
ここにバックエンドの情報やprotoファイルを登録します。

```typescript:src/posts/posts.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { PostsResolver } from './posts.resolver';
import { PostsService } from './posts.service';
import { join } from 'path';
import { POSTS_PACKAGE_NAME } from './posts';
import { ApolloDriver } from '@nestjs/apollo';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: POSTS_PACKAGE_NAME,
        transport: Transport.GRPC,
        options: {
          url: 'localhost:50051',
          package: POSTS_PACKAGE_NAME,
          protoPath: join(__dirname, 'posts.proto'),
        },
      },
    ]),
    GraphQLModule.forRoot({
      driver: ApolloDriver,
      installSubscriptionHandlers: true,
      autoSchemaFile: 'schema/posts.gql',
    }),
  ],
  providers: [PostsResolver, PostsService],
})
export class PostsModule {}
```

### modelを定義

```typescript:src/posts/models/post.ts
import { ObjectType, Field, Int } from '@nestjs/graphql';

@ObjectType()
export class Post {
  @Field(() => String)
  id: string;

  @Field(() => String)
  title: string;

  @Field(() => Int)
  views: number;
}
```

### resolverを定義

```typescript:src/posts/posts.resolver.ts
import { Query, Resolver, Args, Int } from '@nestjs/graphql';
import { Post } from '../posts/models/post/post';
import { PostsService } from './posts.service';
import { map, Observable } from 'rxjs';

@Resolver(() => Post)
export class PostsResolver {
  constructor(private postService: PostsService) {}

  @Query(() => [Post], { name: 'posts', nullable: true })
  posts(
    @Args('id', { type: () => Int, nullable: true }) id?: number,
  ): Observable<Post[]> {
    if (id !== undefined) {
      return this.postService
        .findOne(id.toString())
        .pipe(map((post) => (post ? [post] : [])));
    } else {
      return this.postService.findAll().pipe(map((postList) => postList.posts));
    }
  }
}

```

### serviceを定義
実際にバックエンドとやり取りを行う部分になります。
自動生成されたposts.tsを参照するようにしていきます。

```typescript:src/posts/posts.service.ts
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import {
  POSTS_SERVICE_NAME,
  POSTS_PACKAGE_NAME,
  PostsServiceClient,
} from './posts';
import { ClientGrpc } from '@nestjs/microservices';

@Injectable()
export class PostsService implements OnModuleInit {
  private postsService: PostsServiceClient;

  constructor(@Inject(POSTS_PACKAGE_NAME) private client: ClientGrpc) {}

  onModuleInit() {
    this.postsService =
      this.client.getService<PostsServiceClient>(POSTS_SERVICE_NAME);
  }

  findOne(id: string) {
    return this.postsService.findOne({ id: id });
  }

  findAll() {
    return this.postsService.findAll({});
  }
}
```


## 動作確認
NestJSアプリを起動して動作を確認します。
以下のコマンドを実行し、NestJSを起動します。
```bash
npm run start:dev
```
http://localhost:3000/graphql にアクセスして、正しく画面に表示されれば完了です。


# 全体の動作確認
では、実際にgraphqlのクエリを投げて結果を確認します。
http://localhost:3000/graphql にアクセスして以下のクエリを実行します。

```graphql
query {
  posts {
    id
    title
    views
  }
}
```

結果を取得することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/05f1126e-585c-8ec6-a201-88a553ea9fad.png)

以下のようにidを指定した場合も試してみます。

```
query {
  posts (id:1) {
    id
    title
    views
  }
}
```

こちらも結果を取得することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1240861/ddd6a198-276b-c51f-621d-b0bf8415eea4.png)

# まとめ

今回はgRPCというものに入門してみました。
お作法も多いので、少し敷居は高いのかもしれませんが、この共通で使用するProtoファイルによって言語問わずシームレスに開発が行えることがとても新鮮に感じました。
マイクロサービスごとに別言語となった場合でも問題なく開発がすすめられそうでした。


# 参考

https://qiita.com/S4nTo/items/0ff0445542538ef49a05

https://zenn.dev/hsaki/books/golang-grpc-starting

https://zenn.dev/maronn/articles/nestjs-nextjs-grpc-graphql