この記事は 株式会社サイバー・バズ Advent calendar 2021 の記事です。
はじめに
「Protocol Buffersでコード生成して、gRPCを実現してみたよ〜」や
「マイクローサービスアーキテクチャにおけるgRPCの使い所はここだ」
などの記事は見かけるのですが、
「「具体的にどうやってフローを実現するのか」」
について書かれている記事がなかったので、
1つの方法として捉えて頂ければなーと思います。
構成のイメージ図
軽く上図を説明
4つのリポジトリがある
- 全てのProtocol Buffers(以下PBと略す)を集約するリポジトリ(aggregation-pb)
- Protocol Buffersによって生成されたGoのコードを集約するリポジトリ(grpc-go-packages)
- Protocol Buffersによって生成されたPythonのコードを集約するリポジトリ
- 4つのマイクロサービスリポジトリ(例:BFF, 例:Gateway, Stock, Preprocess)
※各マイクロサービスは作成途中で未完成
※Pythonのマイクロサービスは未作成
図の流れ
- BFFを開発するエンジニアがBFFサービスにて新たなAPIを生やしたい
- Protocol Buffers Repositoryで新たな変更をPR→merge
- mergeをhookにProtocol Buffersによってコードを生成する
- ↑のコードを言語毎リポジトリにてPRを投げる
- 各マイクロサービスでGitHubのソースコードをimportして利用する
上の流れを実現したコードの紹介
上記の流れを実現するためのコードを書いてみました。
中々にGitHub Actionsしているので、
解説はさみつつ進めていきます。
aggregation-pb
Protocol Buffersを集約するリポジトリです。
aggregation-pbのリポジトリはコチラ
リポジトリ構成
構成としては以下のようになっています。
root/
├ .github/
│ └ workflows/
│ └ test.yaml
│ └ hook.yaml
├ gateway/
│ └ proto/
│ └ gateway.proto
├ bff/
│ └ proto/
│ └ bff.proto
etc...
ルート直下に.github
と各マイクロサービス
のディレクトリに
分かれていることが分かります。
では.github
で設定した設定を見ていきます。
※各マイクロサービスがどうなっているかは今回は割愛します。
hook.yaml
主に生成された[プログラミング言語]のコードを集約するリポジトリ
にて
GitHub Actionsを走らせるために、hookとなる動作をします。
name: generate
on:
## なぜpushではなく、pull_requestにしたか?
## → ProtocolBuffersからコードを生成する際に、任意のサービスを指定できる
## PRのタイトルを用いて、マイクサービスを判別する
## またタイトル取得には`github.event.pull_request.title`が不可欠
pull_request:
branches:
- main
## なぜclosedか?
## 承認フローを通るため
types:
- closed
jobs:
pr-hooks:
runs-on: ubuntu-20.04
strategy:
matrix:
## ただの環境変数化
repo: ['mocoog/grpc-go-packages']
steps:
- name: checkout
uses: actions/checkout@v2
- name: merged check
## これをしないと、PRを単にCloseした時(mergeしなかった時)にも起動してしまう
if: github.event.pull_request.merged != true
## アーリーリターン
run: exit 1
- name: def checkRunCompile
uses: actions/github-script@v3
env:
## タイトルを環境変数化。onでpull_requestを指定したから使えるevent
PR_TITLE: ${{ github.event.pull_request.title }}
id: checkRunCompile
with:
## 返り値の型
result-encoding: string
## JSのコードを書いている
## コロンがあればservice名を指定している or コード生成以外の目的PR
script: |
const prTitle = process.env.PR_TITLE
return prTitle.match(/:/) ? prTitle : ''
- name: dispatch compile-pb
## コード生成以外のPRでhookを発動させない&↑JSの返り値を取得
if: steps.checkRunCompile.outputs.result != ''
## 以下hookのための処理
uses: peter-evans/repository-dispatch@v1
with:
## tokenが必要
token: ${{ secrets.ACCESS_TOKEN }}
repository: ${{ matrix.repo }}
event-type: compile-pb
## payloadも指定できる
## PRのタイトルを`生成された[プログラミング言語]のコードを集約するリポジトリ`のGitHub Actionsへ送ります。
client-payload: '{"pr_title": "${{ github.event.pull_request.title }}"}'
これによって、aggregation-pb
にて出されたPRのタイトルによって
生成するProtocolBuffersを決定する。
例文: bff: add health check
例Actions: bff: add health check generate #23
test.yaml
気になる方もいらっしゃったと思いますが、
こんなルールしらねーとmergeしてしまう人がいるのではないかというケースに対応!
自動テストも作っておきましたw
name: test
on:
pull_request:
## PRのタイトル変更にも対応
types: [opened, edited, synchronize]
jobs:
test-check-directory:
runs-on: ubuntu-20.04
steps:
- name: Check out
uses: actions/checkout@v2
- name: def checkColon
id: checkColon
uses: actions/github-script@v3
env:
PR_TITLE: ${{ github.event.pull_request.title }}
with:
result-encoding: string
script: |
const prTitle = process.env.PR_TITLE
if (prTitle.match(/:/)) {
return prTitle.split(':')[0] === '' ?
core.setFailed('Not setup service name!')
: prTitle.split(':')[0]
}
return ''
- name: test target service
env:
## ↑JSの呼び出し
SERVICE_NAME: ${{ steps.checkColon.outputs.result }}
if: env.SERVICE_NAME != ''
## PRで指定したタイトルのディレクトリがない時、コード生成できないため
run: |
echo ${{ env.SERVICE_NAME }}
test -d ${{ env.SERVICE_NAME }}
これによって不適切なPRタイトルを防ぐことができました。
もし**「「PRタイトルにてディレクトリに無いマイクロサービス名が指定された時」」**に
事前にテストで落とすことができます。
grpc-go-packages
Protocol Buffersによって生成されたGoのコードを集約するリポジトリです。
grpc-go-packagesのリポジトリはコチラ
リポジトリ構成
構成としては以下のようになっています。
root/
├ .github/
│ └ workflows/
│ └ compile.yaml
├ gateway/
│ └ proto/
│ └ gateway.proto
│ └ gateway.pb.go
│ └ gateway_grpc.pb.go
├ bff/
│ └ proto/
│ └ bff.proto
│ └ bff.pb.go
│ └ bff.pb.go
├ docs/
etc...
各マイクロサービス`のディレクトリ下にて
それぞれProtocolBuffersにより生成されたコードがあることが分かります。
ついでにドキュメントも生成しています。
では.github
で設定した設定を見ていきます。
compile.yaml
aggregation-pbのactionsによるhookで発火させられ、
値を受け取っています。
その値を用いてProtocolBuffersをコンパイル(コード生成)しています。
name: compile aggregation protocol-buffers
on:
## hookにより起動する際のセリフと合言葉
repository_dispatch:
types: [compile-pb]
jobs:
setup:
runs-on: ubuntu-20.04
steps:
- name: Set up
uses: actions/setup-go@v2
with:
go-version: 1.17.1
id: go
- name: Check out
uses: actions/checkout@v2
- name: Cache
uses: actions/cache@v2.1.0
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
pick-service-name:
needs: setup
runs-on: ubuntu-20.04
outputs:
## jobを超えた値を渡すため
pr-title: ${{ steps.pickPR.outputs.prTitle }}
service-name: ${{ steps.pickPR.outputs.serviceName }}
steps:
- name: Check out
uses: actions/checkout@v2
- name: def pickServiceName
uses: actions/github-script@v3
env:
## aggregation-pbからのpayloadが存在しているか判定する
PR_TITLE: ${{ github.event.client_payload.pr_title }}
id: pickServiceName
with:
result-encoding: string
script: |
const prTitle = process.env.PR_TITLE
return prTitle.match(/:/) ?
prTitle.split(':')[0]
: core.setFailed('Not setup service name!')
- name: pickup service name from PR title
id: pickPR
if: steps.pickServiceName.outputs.result != ''
## 三行目: pick-service-nameのlevelからidを元に取り出せるようにセット
## 六行目: サービス名だけを取得する関数を発火して、idを元に取り出せるようにセット
run: |
echo '== pr title =='
echo ${{ github.event.client_payload.pr_title }}
echo '::set-output name=prTitle::${{ github.event.client_payload.pr_title }}'
echo '== service name =='
echo ${{ steps.pickServiceName.outputs.result }}
echo '::set-output name=serviceName::${{ steps.pickServiceName.outputs.result }}'
compile-pb:
needs: pick-service-name
runs-on: ubuntu-20.04
env:
## pick-service-nameのoutputsで指定したものを取り出し(長いので少しでもjobを短くして責務分け)
PR_TITLE: ${{ needs.pick-service-name.outputs.pr-title }}
SERVICE_NAME: ${{ needs.pick-service-name.outputs.service-name }}
steps:
- name: Check out
uses: actions/checkout@v2
- name: clone aggregation-pb
uses: actions/checkout@v2
with:
repository: mocoog/aggregation-pb
path: aggregation-pb
token: ${{ secrets.ACCESS_TOKEN }}
- name: copy target service
## aggregation-pbの対象のサービスディレクトリをコピー(バグり防止で削除処理も込み)
run: |
echo '== ls aggregation-pb/${{ env.SERVICE_NAME }} =='
ls aggregation-pb/${{ env.SERVICE_NAME }}
if [ -d ${{ env.SERVICE_NAME }} ]; then
rm -rf ${{ env.SERVICE_NAME }}
fi
cp -rf aggregation-pb/${{ env.SERVICE_NAME }} ${{ env.SERVICE_NAME }}
- name: clean aggregation-pb
run: |
rm -rf aggregation-pb
- name: ls test
run: |
echo '== ls =='
ls
echo '== ls service name =='
ls ${{ env.SERVICE_NAME }}
## Protocol Buffersのコンパイラー準備
- name: apt-get
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
## go.modを生成
- name: initialize go
run: go mod init github.com/mocoog/grpc-go-packages
## protocするための関連パッケージをゲット
- name: get protoc
run: |
go get -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
go get -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
go get -u github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc
## ドキュメント生成用のディレクトリを生成
- name: make directory docs
run: mkdir -p docs
## compile!!
- name: compile protocol-buffers
run: |
export PATH="$PATH:$(go env GOPATH)/bin"
protoc \
--doc_out=./docs --doc_opt=markdown,${{ env.SERVICE_NAME }}.md \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
${{ env.SERVICE_NAME }}/proto/${{ env.SERVICE_NAME }}.proto
## 不要なものを消去。今思ったけど、消さない方が効率的な気がしてきている。
- name: rm go mod & sum
run: rm go.mod go.sum
## プルリクを作成
- name: create PR
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.ACCESS_TOKEN }}
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
commit-message: compile ${{ env.SERVICE_NAME }}
branch: compile-pb
branch-suffix: timestamp
delete-branch: true
title: 'compile:${{ env.PR_TITLE }}'
めちゃめちゃ長いYAMLさんですが、ヤバそうな所はコメントを付けたので、
読む意思が強めな方は読んでみてください〜!
- 取得した値のtest
- split関数
- jobを超えた変数のやり取り
- 対象サービスディレクトリのコピーコマンド
- Goの環境構築
- protoc実行
- プルリク作成
大体↑こんなことしています。
もっとjob分けできるなーとも思ったんですが、また長くなるので、悩みどころです。
完成例
最後までフローが進むと以下の感じにプルリクが作成されます。
(以下例はマージ済みのやつです)
そして生成されたGoのコードをパッケージとして、
各マイクロサービスでgo get
してimport "github.com/〜"
で呼んであげる訳です。
↑を呼び出す例はまだ作っていないです!
Python-GoのgRPC実装例も作ったことはありますので、見てみてください^^