この記事は個人ブログと同じ内容です
GraphQL サーバと gRPC サーバを Go で実装し、それらを連携させた簡単なローカル環境を構築しました。この記事では、その手順と体験をシェアしていきます。
全体アーキテクチャの概要
全体のアーキテクチャは以下のような構成になります。
- フロントエンド(クライアント)からのリクエストは、GraphQL サーバのエンドポイントに送信されます。
- GraphQL サーバは受け取ったクエリを解析し、必要なデータを取得するために内部で gRPC クライアントとして動作します。
- gRPC サーバは、GraphQL サーバからのリクエストを受け取り、実際のビジネスロジックを実行してレスポンスを返します。
- GraphQL サーバは、gRPC サーバから受け取ったレスポンスを、GraphQL のスキーマに従った形式に変換してクライアントに返します。
この構成を採用する利点としては以下が挙げられます。
- フロントエンドは柔軟なデータ取得が可能なGraphQLを利用できる
- バックエンド間の通信には、型安全で高速な gRPC を使用できる
- Go の強力な型システムとコード生成ツールにより、安全で保守性の高いコードを実現できる
この構成を図にすると以下のイメージです。
+------------------------+ | | | Frontend (Client) | | | +------------------------+ | GraphQL (HTTP/HTTP2) | v +------------------------+ | | | GraphQL Server (Go) | | - Schema | | - Resolver | | [gRPC Client] | | | +------------------------+ | gRPC (HTTP/2) | v +------------------------+ | | | gRPC Server (Go) | | - Proto | | - Service | | - Business Logic | | | +------------------------+ | v +------------------------+ | | | Database | | | +------------------------+
Frontend (Client) GraphQL Server (Go) gRPC Server (Go) | | | | | | | GraphQL Request | | |--------------------->| | | | | | | gRPC Request | | |--------------------->| | | | | | gRPC Response | | |<---------------------| | | | | GraphQL Response | | |<---------------------| | | | |
なお、まずは基本的な機能実装に焦点を当てるため、ディレクトリ構成やファイル分割、ネーミングなどは簡易的です。
また、実験的な環境であり、本番用には適切なエラーハンドリングやセキュリティ対策の追加が必要です。
開発環境
端末は Mac(M2)で、Go がインストール済みの状態です。
%go version go version: go1.23.2 darwin/arm64
プロジェクトの作成
まずは、開発を進めていくプロジェクトを作成します。新しいディレクトリを作成し、以下のコマンドで Go モジュールとして初期化します。
mkdir <新しいディレクトリ> cd <新しいディレクトリ> go mod init <プロジェクト名>
GraphQL サーバ構築
インストールとセットアップ
GraphQL サーバ構築には 99designs/gqlgen を利用します。このパッケージは、GraphQL スキーマから Go のコードを自動生成してくれる便利なツールです。
まず、必要なパッケージをインストールします。
go get -u github.com/99designs/gqlgen
次に、gqlgen の初期設定を行います。プロジェクトのルートディレクトリで以下コマンドを実行します。
go run github.com/99designs/gqlgen init
ルート配下に graph ディレクトリが作成され、配下に以下のファイルが作成されます。
graph ├── model │ └── models_gen.go ├── generated.go ├── resolver.go ├── schema.graphqls └── schema.resolver.go gqlgen.yml
gqlgen.yml
: gqlgen の設定ファイルgraph/schema.graphqls
: GraphQLスキーマ定義ファイルgraph/generated/generated.go
: 自動生成されたコードgraph/model/models_gen.go
: スキーマから生成されたモデルgraph/resolver.go
: リゾルバーの実装ファイルserver.go
: GraphQLサーバのエントリーポイント
デフォルトの設定が既に生成されているため、この時点で GraphQL サーバを起動できます。以下のコマンドを実行して GraphQL サーバを起動してみます。
go run server.go
http://localhost:8080/
にアクセスすると、GraphQL Playground が表示されたことが確認できました。
GraphQL Playground は、GraphQLクエリのテストや、スキーマの確認ができる Web UI ツールです。クエリの入力、実行、レスポンスの確認が簡単にでき、開発時に便利です。また、スキーマのドキュメントも自動生成されるため、API の仕様確認にも活用できます。
スキーマの定義
次に、スキーマを定義します。スキーマは、GraphQL の API で利用可能な型、クエリ、ミューテーションを定義するものです。graph/schema.graphqls
ファイルを編集します。
今回は例として、よくあるブログシステムを想定し、User(ユーザー)と、そこにひも付く Post(記事)で定義します。また、ユーザーの一覧を取得するリゾルバ(GraphQL のクエリを受け取って実際のデータを返す関数)users
を定義します。
type Query { users: [User!]! } type User { id: ID! name: String! posts: [Post!]! } type Post { id: ID! title: String! }
スキーマの定義ができたら、gqlgen というツールを使用しこのスキーマに基づいて Go のコードを自動生成します。この自動生成では、スキーマで定義した型の構造体や、GraphQL のクエリを処理するためのリゾルバの雛形が作成されます。手作業でこれらのコードを書く必要がなく、開発効率を高めることができます。
以下のようなコードが自動的に生成されます。
コード生成を行う前に、まず gqlgen ツールの設定として tools.go
を作成し、以下を記述しておきます。
//go:build tools // +build tools package tools import ( _ "github.com/99designs/gqlgen" _ "github.com/99designs/gqlgen/graphql/introspection" )
この tools.go
は、プロジェクトで使用するツールの依存関係を管理するためのものです。後ほど go mod tidy
を実行する際に、これらの開発ツールの依存関係が go.mod
ファイルに適切に含まれるようになります。
ここまでできたら、以下のコマンドを実行してコードを自動生成します。
go run github.com/99designs/gqlgen generate
graph/model/models_gen.go
が生成されます。ファイルを見てみると、先程定義したスキーマが出力されていることが確認できます。
type Post struct { ID string `json:"id"` Title string `json:"title"` } type Query struct { } type User struct { ID string `json:"id"` Name string `json:"name"` Posts []*Post `json:"posts"` }
また、graph/schema.resolvers.go
にリゾルバの雛形が生成されます。
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) { panic(fmt.Errorf("not implemented: Users - users")) }
これに仮の実装を行い、結果を返すようにしてみましょう。
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) { return []*model.User{ { ID: "1", Name: "たろう", Posts: []*model.Post{ {ID: "101", Title: "サンタさんは本当にいるかもしれない"}, }, }, { ID: "2", Name: "はなこ", Posts: []*model.Post{ {ID: "102", Title: "近所に KFC があってよかった"}, }, }, }, nil }
ここまでできたら、GraphQL Playground に以下を入力してリクエストを送信してみます。
query { users { id name posts { id title } } }
レスポンスが返ってきたことが確認できました。スキーマ定義通りにユーザー情報と、各ユーザーの投稿が取得できていることが分かります。GraphQL サーバの構築はここまでで完了です。
gRPC サーバの構築
次に、gRPC サーバを構築していきます。
インストールとセットアップ
Protocol Buffers Compiler と、gRPC と Protocol Buffers 用の Go プラグインをインストールします。
Protocol Buffers Compiler インストール
Protocol Buffers Compiler のインストールは OS によって変わりますので下記の公式ドキュメントを参考にしてください。
Protocol Buffers Compiler インストール
Mac は homebrew でいけます。
brew install protobuf
gRPC と Protocol Buffers 用の Go プラグインをインストール
gRPC と Protocol Buffers 用の Go プラグインをインストールします。
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
以下をプロジェクトにインストールします。
go get -u google.golang.org/grpc go get -u google.golang.org/protobuf
proto ファイル作成
proto ファイルは、Protocol Buffers(プロトコルバッファ)という Google が開発したデータ記述言語で使用されるスキーマ定義ファイルです。このファイルをもとに、さまざまなプログラミング言語でデータのシリアライズとデシリアライズを効率的に行うコードを自動生成できます。
また、gRPC では proto ファイルを使用して、RPC(リモートプロシージャコール)サービスやメッセージ構造を定義します。
proto/user/service.proto
を作成して、以下を定義していきます。
package
: プロトコルバッファ定義の名前空間option go_package
: Go コード生成時のパッケージ名service
: gRPC サービスの定義message
: リクエストやレスポンスのデータ構造
syntax = "proto3"; package user; option go_package = "proto/user"; // Query サービスの定義 service QueryService { rpc Users (UsersInput) returns (UsersResponse); } // ユーザー情報の定義 message User { string id = 1; string name = 2; repeated Post posts = 3; } // 投稿情報の定義 message Post { string id = 1; string title = 2; } // Users メソッドのリクエスト(今回は何も指定しない) message UsersInput {} // Users メソッドのレスポンス message UsersResponse { repeated User users = 1; }
コード生成
以下の protoc
コマンドで .proto ファイルから Go コードを生成します。
protoc --go_out=<出力ディレクトリ> --go-grpc_out=<出力ディレクトリ> <protoファイルのパス>
今回の例だとコマンドは以下になります。
protoc --proto_path=proto --go_out=. --go-grpc_out=. proto/**/*.proto
--proto_path=proto
:.proto
ファイルを検索する基準ディレクトリを指定。- 今回は
proto/
配下を基準にして.proto
ファイルを検索します。 - これにより、
proto/
配下でインポートされる他の.proto
ファイルも自動的に解決されます。
--go_out=.
:--go-grpc_out=.
:proto/**/*.proto
:- コンパイル対象の
.proto
ファイルのパス。- ワイルドカード (
**/*.proto
) を使用してproto/
配下のすべての.proto
ファイルを対象とします。
- ワイルドカード (
protoc
コマンドを実行すると、proto ディレクトリに service.pd.go
と service_grpc.pd.go
ファイルが生成されます。
proto/ └── user/ ├── service.pd.go ├── service.proto └── service_grpc.pd.go
gRPC メソッドの実装
次に、ユーザー一覧を返却する gRPC メソッド Users
を実装します。
grpcserver/server.go
を作成し、Users
を実装します。次工程で gRPC サーバを起動するため、StartGRPCServer
関数も一緒に定義しています。
package grpcserver import ( "context" "graphql-dataloader-demo/proto/user" "log" "net" "google.golang.org/grpc" ) // gRPC サーバの実装 type userServiceServer struct { user.UnimplementedQueryServiceServer } // Users ユーザーの一覧を返却します func (s *userServiceServer) Users(ctx context.Context, req *user.UsersInput) (*user.UsersResponse, error) { // 仮のユーザーリストを返す users := []*user.User{ {Id: "1", Name: "ミギ"}, {Id: "2", Name: "ダリ"}, } return &user.UsersResponse{ Users: users, }, nil } // StartGRPCServer gRPC サーバを起動します func StartGRPCServer(port string) error { listener, err := net.Listen("tcp", ":"+port) if err != nil { return err } grpcServer := grpc.NewServer() user.RegisterQueryServiceServer(grpcServer, &userServiceServer{}) log.Printf("gRPC server is running on port %s", port) return grpcServer.Serve(listener) }
gRPC サーバの起動
ここで一度、gRPC サーバを起動してみます。
server.go
に gRPC サーバを起動させるための記述を行っていきます。これまでは GraphQL サーバの起動のみ定義していましたが、ここに gRPC サーバの起動も追記していきます。
package main import ( "graphql-dataloader-demo/graph" "graphql-dataloader-demo/grpcserver" "log" "net/http" "os" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler/extension" "github.com/99designs/gqlgen/graphql/handler/lru" "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" "github.com/vektah/gqlparser/v2/ast" ) const ( defaultPort = "8080" defaultGRPCPort = "50051" // 追加 ) func main() { // HTTP サーバのポート設定 port := os.Getenv("PORT") if port == "" { port = defaultPort } // [追加] gRPC サーバのポート設定 grpcPort := os.Getenv("GRPC_PORT") if grpcPort == "" { grpcPort = defaultGRPCPort } // [追加] GraphQL リゾルバを gRPC クライアントとともに初期化 grpcAddr := "localhost:" + grpcPort resolver := graph.NewResolver(grpcAddr) // GraphQL サーバの設定 srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: resolver})) srv.AddTransport(transport.Options{}) srv.AddTransport(transport.GET{}) srv.AddTransport(transport.POST{}) srv.SetQueryCache(lru.New[*ast.QueryDocument](1000)) srv.Use(extension.Introspection{}) srv.Use(extension.AutomaticPersistedQuery{ Cache: lru.New[string](100), }) http.Handle("/", playground.Handler("GraphQL playground", "/query")) http.Handle("/query", srv) // [追加] gRPC サーバを起動 go func() { log.Printf("Starting gRPC server on port %s", grpcPort) if err := grpcserver.StartGRPCServer(grpcPort); err != nil { log.Fatalf("failed to start gRPC server: %v", err) } }() log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) log.Fatal(http.ListenAndServe(":"+port, nil)) }
以下のコマンドを実行して GraphQL サーバに加えて gRPC サーバを起動します。
go run server.go
% go run server.go connect to http://localhost:8080/ for GraphQL playground Starting gRPC server on port 50051 gRPC server is running on port 50051
gRPC サーバも無事に起動しました。
gRPC クライアントのセットアップ
最後に、GraphQL リゾルバで gRPC メソッドを使えるようにするために、gRPC クライアントのセットアップを行っていきます。
gRPC クライアントの追加
graph/resolver.go
にて、リゾルバに gRPC クライアントを追加します。
package graph import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "graphql-dataloader-demo/proto/user" "log" ) type Resolver struct { GRPCClient user.QueryServiceClient // gRPC クライアントを格納するフィールド } // NewResolver は gRPC クライアントを初期化して Resolver を返します func NewResolver(grpcAddr string) *Resolver { // gRPC の接続設定 // 非セキュア接続(plaintext)を使用しています。 // 本番環境では適切な証明書を使用してセキュア接続に切り替えてください。 opts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), // 非セキュア接続(開発用) } // gRPC サーバに接続 conn, err := grpc.NewClient(grpcAddr, opts...) if err != nil { log.Fatalf("gRPC サーバへの接続に失敗しました: %v", err) } // Resolver に gRPC クライアントを設定して返却 return &Resolver{ GRPCClient: user.NewQueryServiceClient(conn), } }
GraphQL リゾルバで gRPC を呼び出す
最後に、GraphQL の Users
リゾルバで gRPC メソッドを呼び出します。
graph/schema.resolvers.go
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) { // gRPC メソッドへリクエスト res, err := r.Resolver.GRPCClient.Users(ctx, &user.UsersInput{}) if err != nil { return nil, err } // gRPC の User を GraphQL の User に変換 users := make([]*model.User, len(res.Users)) for i, u := range res.Users { users[i] = &model.User{ ID: u.Id, Name: u.Name, } } return users, nil }
go run server.go
でサーバを起動後、GraphQL Playground に以下を入力してリクエストを送信します。
query { users { id name } }
すると、gRPC サーバからのレスポンスを受け取って返したことが確認できました。
{ "data": { "users": [ { "id": "1", "name": "ミギ" }, { "id": "2", "name": "ダリ" } ] } }
ここまでで、GraphQL サーバと gRPC サーバの連携は完了です。
まとめ
本記事では、GraphQL サーバと gRPC サーバを連携させたローカル開発環境を構築しました。
- GraphQLサーバ(99designs/gqlgen)
- gRPCサーバ
- Protocol Buffers によるスキーマ定義
- サービス実装とサーバの起動
- 型安全なコード生成の活用
- GraphQLサーバとgRPCサーバの連携
- gRPC クライアントの実装
- GraphQL リゾルバからの gRPC 呼び出し
- データ変換処理の実装
実験的な構成ではありますが、GraphQL と gRPC を組み合わせることで、フロントエンドに柔軟な GraphQL インターフェースを提供しながらも、バックエンド間では効率的な gRPC 通信を実現し、Go の強力な型システムで開発するというそれぞれの強みを活かしたシステムが構築できる可能性を感じました。
特に、コードの自動生成は大きな強みです。品質が一定に保たれるだけでなく、実装コストの削減にもつながります。
これからこれらのツールを色々試していこうと思います。