TalentX Tech Blog

Tech Blog

バックエンドAPI開発にスキーマ駆動開発を導入している話

Tech Leadの籔下(@ybalexdp)です。本記事では現在MyReferが取り組んでいるスキーマ駆動開発に関して紹介します。

導入に至った経緯

現在MyReferではフレームワークFuelPHPを採用し、Hack1でプロダクトを開発しています。
しかし、HHVM2PHPのサポート終了を発表したため、FuelPHPを捨てHackでいくか、HHVMを捨てPHP7でいくか、他の言語にリプレイスするか選択に迫られました。さらにFulePHP自体も開発が過疎化している状況でLTSも定められておらず、PHP7にした場合、LaravelなどLTSが定まっているフレームワークに変更するなどの検討も必要でした。
エンジニアチームで色々相談し、

  • Hackは情報量が少ないし、他社の導入事例もあまりないため避けたい
  • PHP7でもよいが、このタイミングでコードを綺麗にするのであればいっそのこと違う言語でいきたい
  • フロントエンドにReactを採用しているためリプレイス後はSPAにして、バックエンドはフルスタックのWebフレームワークではなく軽量にAPIを提供できる構成をとりたい
  • Goやりたい

などの理由より現在Goでのリプレイスを進めています。
その過程でスキーマ駆動開発を導入することになり、紆余曲折しながら検討した記録を残します。

フロントエンドも徐々にjQueryをReactに移行しており、既にReactに移行済みの箇所については、FuelPHPでバックエンドAPIを実装しています。リプレイス作業に伴い、フロントはReactで完全に制御し、SPAとして生まれ変わる予定です。

ただ、今後継続的に開発を進めるにあたり、バックエンドの開発がボトルネックになるのを避けるべく、モックAPIを簡単に準備できる仕組みの導入と、ついでにドキュメントも自動生成しようということで、スキーマ駆動開発を導入しました。

要約

以下ではTwirpでのgRPC to RESTで色々検討した知見を記載していますが、結局Twirpの導入は見送り、gRPC-WEBを採用し、バックエンドAPIはgRPCで提供する方針となりました。理由としては以下の事項があります。

  • Twirpの情報が少ない(メンバーのキャッチアップの阻害、採用時に懸念されそうなど)
  • gRPCはメジャープロトコルなので情報量が多い
  • Twirpの情報が本当に少ない(開発止まったりサポート面なども含め将来的な不安もある)

現在のバックエンドの構成がFuelPHPというフレームワークにHackという個性的(?)な構成のため、情報量が少なく困ることがあり、同じ轍を踏むことを危惧し、Twirpを捨てた感じです。

Protocol buffers

検討開始当初はginを導入し、RESTful APIとして実装していく方針でした。 ただ上記の通り、スキーマ駆動開発導入の話も出てきたので、Protocol buffersを導入することにしました。

また、Protocol buffersを導入するにあたり、gRPC-WEBを導入し、ReactからgRPCでAPIをコールするか、grpc-gatewayを導入し、バックエンドAPIはRESTful APIとして提供するかを選択する必要がありました。

現在のフロントエンドのプロダクトコードにはすでにReactを導入しており、 ReactからコールされるRESTful APIfuelPHPで実装しています。

またMyReferはリクルーター(導入企業様の社員の方)向けにiOS/Androidアプリも提供しており、 こちらもバックエンドで提供しているRESTful APIを利用しています。

gRPCではなくRESTにした場合、SPA化した場合にでもフロントエンド/iOS/Androidは基本(API仕様に変更無ければ)そのまま移行可能ということでgrpc-gatewayを導入し、バックエンドAPIはRESTful APIとして提供する方向で当初検討していました。

Twirp

grpc-gatewayでもよかったのですが、Twirpというフレームワークを発見し、grpc-gatewayに比べて、ルーティングの自動生成が実現できたり、protoファイルへの記述量が少なくすむなどの理由よりTwirpの導入検討を前向きに進めていました。

導入自体はgo getなりなんなりでtwirpとprotocのプラグインをインストールし、protoファイルを記載した後、protoc実行時にtwirpのオプションを付与するだけです。
(protocやその他grpc関連のライブラリなどは事前にインストール済みとします)

# インストール
go get github.com/twitchtv/twirp
go get github.com/twitchtv/twirp/protoc-gen-twirp

今回は紹介者の情報をIDから取得するAPIの例を記載します。

myrefer.proto
syntax = "proto3";

option go_package = "pb";

import "pb/timestamp.proto";

message GetRecruiterRequest {
    int64 recruiter_id = 1;
}

message RecruiterResponse {
    int64 recruiter_id = 1;
    int64 company_id = 2;
    string employee_number = 3;
    string email = 4;
    string first_name = 5;
    string last_name = 6;
    # 以下略
}

service SampleAPI {
    rpc GetRecruiter(GetRecruiterRequest) returns (RecruiterResponse);
}

protocを実行しgoファイルを生成します。

protoc pb/myrefer.proto --proto_path=. --twirp_out=. --go_out=. 

するとpbディレクトリ配下にmyrefer.pb.gomyrefer.twirp.goが生成されます。 これらのファイルを利用してサーバ側の実装を行います。

今回はサンプルのためfirst_nameパラメータに「hello」を付与して返却しています。

main.go
package main

import (
  "context"
  "fmt"
  "net/http"

  "[gitlab]/linkmap/api/backend-service/pb"  // [gitlab]はmyreferのgitlabのホスト名
)

type HelloWorldServer struct{}

func (ua *HelloWorldServer) GetRecruiter(ctx context.Context, params *pb.GetRecruiterRequest) (*pb.RecruiterResponse, error) {
  // 本来は受け取ったrecruiter_idを元に情報を取得し返却する
  return &pb.RecruiterResponse{FirstName: "Hello "}, nil
}

func main() {
  hws := &HelloWorldServer{}
  server := pb.NewSampleAPIServer(hws, nil)
  http.Handle(pb.SampleAPIPathPrefix, server)
  err := http.ListenAndServe(":8080", nil)
  if err != nil {
    panic(err)
  }
}

curlAPIを叩いてみます

$curl -H "Content-Type: application/json" \
         -XPOST http://localhost:8080/twirp/SampleAPI/GetRecruiter \
         -d "{\"recruiter_id\": 1}"
{"first_name":"Hello "}

API仕様書自動生成

protocのtwirp向けswaggerプラグインprotoc-gen-twirp_swaggerの導入を検討していました。 導入検討に至った経緯としては下記のようなモチベーションが主な理由です。

  • MR3のタイミングで、リクエストパラメータやレスポンスデータが実装とずれてることが発覚するケースがちょくちょくある
  • 変更時などAPI仕様書のドキュメントメンテが辛い
  • とは言えswagger書くの辛い

protoc-gen-twirp_swaggerではProtocol buffers形式で作成したスキーマ定義からswagger.jsonを生成してくれます。 なのでスキーマ定義さえ作成すれば、APIのベース実装とAPI仕様書がそれぞれ自動生成される仕組みが出来上がりました。

swaggerのファイル生成もprotocコマンドで生成します。

protoc pb/myrefer.proto --proto_path=. --twirp_swagger_out=. --go_out=. 

myrefer.swagger.jsonが生成されます。このファイルをswaggerに食わせると以下のようにAPI仕様が確認できます。

f:id:myrefer:20190918145215p:plain

swagger自体の構築手順などは省略しますが、MyReferではdocker-composeでswagger用のコンテナを以下のように定義しています。

  swagger_ui:
    image: swaggerapi/swagger-ui:v${SWAGGER_UI_VERSION}
    volumes:
      - ../backend-service/pb:/usr/share/nginx/html/swagger
    environment:
      API_URL: swagger/myrefer.swagger.json
    ports:
      - "9999:8080"

gRPC-WEB

冒頭でも記載していますが、検討していたTwirpによるProtocol buffersベースでのRESTful API提供は止めてクライアント側にgRPC-WEBを導入し、バックエンドAPIはgRPCで提供することにしました。
現状のReactで実装されているフロント及びAndroid/iOSアプリで利用しているAPIがRESTなので、Twirpの導入を見送ったとしても、grpc-gatewayでいくという選択肢もありました。
gRPC-WEBをクライアント側に導入し、RESTful APIを捨てた理由としてはザックリ、以下のような感じでしょうか。

  • プロトコルをgRPCに統一できる
  • protoの記述量が減る(RESTfulにすると、protoのoptionの記載が増えていく)

gRPCでのAPI仕様書自動生成

gRPCにしたためswaggerが使えなくなってしまいました。
API仕様書の自動生成にはprotoc-gen-docの導入を現在検討しています。

さいごに

Goへのリプレイスの過程で現在取り組んでいるスキーマ駆動開発について紹介しました。MyReferでは事業展開やVOC4、現在の事業の課題など様々な観点に対してソフトウェアによる解決を進めながら、技術的負債の返済や新しい技術の導入にも力を入れています。

MyReferでは絶賛エンジニアを募集しています。まだまだ組織を作っている途中なので組織を作るということに興味がある人、最新技術を駆使しながらプロダクトを作っていきたい人、もっと裁量があり事業にも関わっていきたいエンジニアの方などなど、是非カジュアルな場からでもウェルカムなのでお話しに来てください。 以下からお気軽にどうぞ!

myrefer.co.jp

wantedlyからでも!

www.wantedly.com


  1. HackはFacebook社が開発したプログラミング言語でHHVM上で動作する

  2. HHVMはPHPとHackのJIT

  3. 弊社ではGitlabを利用しているためMerge Request

  4. Voice of Customer(お客様の声・要望)