この記事は DeNA Advent Calendar 2021 の10日目です
11/30にリリースした PLAYBACK 9 では、クライアントとサーバーとのやり取りに gRPC を利用しています。
gRPCではIDLを利用してAPIを表現でき、クライアント及びサーバーのスタブコードを自動生成できるため非常に簡単に利用することができます。
しかしながら、生成したコードをどのように管理するかどうかは各自に委ねられており、ベストプラクティスが定まっているわけではありません。
インターネットでも様々な方法が公開されており、一番よく見かけるのは Git の Submodules を使い、共通の proto ファイルから各自コード生成を行うという方法です。
この方法は簡単ではありますが、
- コード生成の責務が各利用者に発生する
- 各言語で、どの時点のコードを利用しているか分かりづらい
といった問題があります。
この問題に対して、GitHub Actions と各言語で提供されているパッケージシステムを利用して解決します。
この解決方法により、クライアントおよびサーバーは通常のパッケージを利用するのと同じように生成されたコードを利用できるようになるため、コード生成の知識が必要ではなくなります。
また、セマンティックバージョニングにてバージョン指定するため、複数の環境があったとしてもバージョン番号によってどの時点で生成されたコードを利用しているかの確認が容易になります。
今回の例では、クライアントに TypeScript、サーバーに Go を利用して紹介していきます。
TypeScriptからサーバーへのアクセスにはgRPC Webを利用しています。
本記事で紹介するコードは以下のリポジトリにて公開しています。
生成されたコードを一括管理するリポジトリを作成する
最初に proto ファイルおよび各言語向けに生成されたコードを保存するリポジトリを作成します。
このリポジトリで、コード生成の責務を全て担うことになります。
リポジトリのディレクトリの構成は以下のようになります。
各言語向けに生成されたコードは各言語の名前の付いたディレクトリに置かれる形になります。
Go 向けに生成されたコードは、パッケージパスをgo
ではなく、pb
にするため、さらに一階層ディレクトリを掘っています。
.
├── Makefile
├── README.md
├── doc
├── go
│ └── pb
├── proto
│ └── service.proto
└── ts
コードの自動生成を行う
GitHub Actions を利用してコードが main ブランチに push された (PRがマージされた) タイミングでコード生成を常に行いコミットするように設定します。
これにより main ブランチでは、常に最新の生成されたコードが反映されていることを保証します。
docker run
で自前の docker image を利用していますが、protoc
が実行できるのであれば、どのイメージでも問題ないと思います。
name: Generate gRPC code
on:
push:
branches: main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Regenerate gRPC code
run: |
docker run --rm \
-v `pwd`:/workdir -w /workdir \
ghcr.io/amothic/protoc --proto_path=proto \
--go_out=go/pb --go_opt=paths=source_relative \
--go-grpc_out=go/pb --go-grpc_opt=paths=source_relative \
--js_out=import_style=commonjs:ts --ts_out=service=grpc-web:ts \
service.proto
- name: Commit
run: |
[[ ! $(git diff --exit-code go ts) ]] && echo "Nothing to commit." && exit 0
git config user.name "gRPC Bot"
git pull
git add go ts
git commit -m "chore: regenerate grpc code"
git push
パッケージとしてリリースするための準備を行う
go
ディレクトリと ts
ディレクトリでパッケージとしてリリースするための準備を行います。
Go
go
ディレクトリ以下に新たにgo.mod
ファイルおよびgo.sum
ファイルを作成します。
$ go mod init github.com/OWNER/your-service-proto/go
$ go mod tidy
作成が終わったら、commit
してpush
しておきます。
TypeScript
ts
ディレクトリ以下に新たにpackage.json
ファイルを作成します。
{
"name": "@OWNER/your-service-proto",
"version": "0.0.0",
"repository": "git@github.com:OWNER/your-service-proto.git"
}
必要なパッケージを追加し、package-lock.json
を作成します
$ npm install --package-lock-only @improbable-eng/grpc-web google-protobuf @types/google-protobuf
また、github packageにアップロードするため以下の.npmrc
ファイルも作成します
@OWNER:registry=https://npm.pkg.github.com
セマンティックバージョンの更新を行う
更新するための最初のバージョンタグを作成します
$ git tag v0.0.1
$ git push origin v0.0.1
その後、バージョン番号を更新する GitHub Actions を追加します
こちらの GitHub Actions は、major
, minor
, patch
のいずれかを渡すことで既存の Git Tag のバージョンをインクリメントして新たな Git Tag を打ってくれます。
major
, minor
, patch
の文字列は、GitHub Actions の workflow_dispatch
を利用して開発者が任意のタイミングで渡すこととします。
name: release
on:
workflow_dispatch:
inputs:
method:
description: |
Which number to increment in the semantic versioning.
Set 'major', 'minor' or 'patch'.
required: true
jobs:
tag:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: kyoh86/git-vertag-action@v1.1
with:
method: ${{ github.event.inputs.method }}
push: true
バージョン番号を参照して、パッケージのリリース作業を行う
更新されたバージョンタグを利用して、各言語のリリース作業を行なっていきます。
Go
Go のリリースは、通常Git Tag を打つだけなのですが、サブディレクトリに生成されたコードが置かれているので、サブディレクトリのパスを Tag に追加する必要があります
jobs:
golang:
needs: tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
# サブディレクトリにGoのモジュールがあるため、サブディレクトリのパスを先頭に付けたTagを追加で発行する
- name: go module version tag
run: |
TAG_VERSION=$(git describe --tags --abbrev=0 --match='v*.*.*')
MODULE_VERSION=go/$TAG_VERSION
git tag $MODULE_VERSION
git push origin $MODULE_VERSION
TypeScript
Git Tag を参照して、package.json
を更新し、リリース作業を実施します。
npm パッケージは、アクセスコントロールが容易な GitHub Packages に公開しています。
jobs:
typescript:
needs: tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: ts package version tag
id: version
run: |
PACKAGE_VERSION=$(git describe --tags --abbrev=0 --match='v*.*.*' | sed -e 's/v//')
echo "::set-output name=package_version::${PACKAGE_VERSION}"
- name: bump version
run: |
jq '.version|="${{ steps.version.outputs.package_version }}"' ts/package.json > tmp
mv tmp ts/package.json
- name: commit package.json
run: |
[[ ! $(git diff --exit-code ts/package.json) ]] && echo "Nothing to commit." && exit 0
git config user.name "gRPC Bot"
git pull
git add ts/package.json
git commit -m "chore: bump version to ${{ steps.version.outputs.package_version }}"
git push
- uses: actions/setup-node@v2
with:
node-version: '14'
registry-url: 'https://npm.pkg.github.com'
scope: '@OWNER'
- run: npm publish
working-directory: ./ts
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
生成したパッケージの利用
生成したパッケージを各言語で利用する方法について紹介します。
Go
protoのディレクトリをclone
できる状態なら、GitHubから直接取得することが可能です
// private repoの場合
$ export GOPRIVATE=github.com/OWNER
$ go get -u github.com/OWNER/your-service-proto/go
TypeScript
-
GitHub にて、
read:packages
の権限を与えた Personal Access Token を発行します (private repoの場合) -
npm login
(private repoの場合)$ npm login --scope=@OWNER --registry=https://npm.pkg.github.com > Username: USERNAME > Password: TOKEN > Email: PUBLIC-EMAIL-ADDRESS
-
package.json
と同じディレクトリに.npmrc
を作成します@OWNER:registry=https://npm.pkg.github.com
-
npm install
$ npm install @OWNER/your-service-proto
終わりに
GitHub Actions と各言語で提供されているパッケージシステムを利用して、生成されたコードをシンプルに利用できるようになりました。
他にも良い方法などあればコメントなどで指摘していただければ幸いです。