12
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

gRPCに入門します

Last updated at Posted at 2024-12-04

はじめに

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

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

今回作成したものこちら

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

gRPCとは?

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

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

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

つまりプログラミング言語問わず、クライアントからサーバーのある関数を直接呼び出して結果を受け取る技術が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で共有します。

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を動かすためのライブラリをインストールします。

pip install grpcio grpcio-tools

次に、ProtoファイルからPythonコードを生成します。
posts.protoをプロジェクトルートにおいて実行します。

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. posts.proto

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

gRPCサーバーの実装

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

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サーバーを起動します。

python server.py

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

BFF(NestJS)

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

ライブラリインストールからコード生成まで

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

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/において実行します。

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

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

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

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ファイルを登録します。

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を定義

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を定義

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を参照するようにしていきます。

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を起動します。

npm run start:dev

http://localhost:3000/graphql にアクセスして、正しく画面に表示されれば完了です。

全体の動作確認

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

query {
  posts {
    id
    title
    views
  }
}

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

image.png

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

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

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

image.png

まとめ

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

参考

12
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?