5
2

More than 1 year has passed since last update.

マイクロサービスアーキテクチャにおけるProtocolBuffers管理のGitHubActionsフローを実現した

Last updated at Posted at 2021-12-13

この記事は 株式会社サイバー・バズ Advent calendar 2021 の記事です。

はじめに

「Protocol Buffersでコード生成して、gRPCを実現してみたよ〜」や
「マイクローサービスアーキテクチャにおけるgRPCの使い所はここだ」
などの記事は見かけるのですが、

「「具体的にどうやってフローを実現するのか」」
について書かれている記事がなかったので、
1つの方法として捉えて頂ければなーと思います。

構成のイメージ図

MicroServiceArchby.drawio.png

軽く上図を説明

4つのリポジトリがある

  1. 全てのProtocol Buffers(以下PBと略す)を集約するリポジトリ(aggregation-pb)
  2. Protocol Buffersによって生成されたGoのコードを集約するリポジトリ(grpc-go-packages)
  3. Protocol Buffersによって生成されたPythonのコードを集約するリポジトリ
  4. 4つのマイクロサービスリポジトリ(例:BFF, 例:Gateway, Stock, Preprocess)

※各マイクロサービスは作成途中で未完成
※Pythonのマイクロサービスは未作成

図の流れ

  1. BFFを開発するエンジニアがBFFサービスにて新たなAPIを生やしたい
  2. Protocol Buffers Repositoryで新たな変更をPR→merge
  3. mergeをhookにProtocol Buffersによってコードを生成する
  4. ↑のコードを言語毎リポジトリにてPRを投げる
  5. 各マイクロサービスで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となる動作をします。

hook.yaml
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

test.yaml
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をコンパイル(コード生成)しています。

compile.yaml
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分けできるなーとも思ったんですが、また長くなるので、悩みどころです。

完成例

最後までフローが進むと以下の感じにプルリクが作成されます。
(以下例はマージ済みのやつです)

スクリーンショット 2021-12-14 1.21.16.png

そして生成されたGoのコードをパッケージとして、
各マイクロサービスでgo getしてimport "github.com/〜"で呼んであげる訳です。

↑を呼び出す例はまだ作っていないです!

Python-GoのgRPC実装例も作ったことはありますので、見てみてください^^

5
2
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
5
2