golang × Github ActionsでGOPRIVATEを使って社内ライブラリ配布を効率よくしたかった話

こんにちは、ライクル事業部 エンジニアの菊池@kichionです
去年(2021年)からフロントエンド環境の立ち上げを行い、現在はバックエンドに戻ってきて技術負債の解消などを中心にシステム改善を行っています

ライクルでは早すぎたマイクロサービス化により、コードベースが30近いGithub repositoryに分散しており重複コードが散乱している状態でした
今回はコードベースの共通化策としてgolangで書かれた社内用のライブラリを配布する方法を紹介します

前提

外部API

ライクルでは事業ドメインとしてGoogle Business Profileと呼ばれるGoogle 検索/マップなどにお店の情報を表示するツールを利用します
そのため、各システムからBusiness Profile APIsを使います

開発言語

ライクルでは今回紹介する社内ライブラリと各システムのバックエンド開発言語としてgolangを採用しています

Github Organization

SOTとしてGithub OrganizationのGitHub Team Planを利用しています
基本的にアプリケーションのコードはprivate repositoryで管理されており、Github Actionsの利用もTeamプランの制限内で行っています

解決したかった問題

ライクルでは分散したコードベースがそれぞれ外部のAPI(以下、GBP API)を利用しており、GBP API実行のコードが分散していました
Token利用やリトライ方法(exponential backoffなど)が部分的に重複していたり古いrepositoryではリトライなどが行えないなどの問題がありました

上記、以外にも様々な問題が浮き彫りになりました

  • コードがDRYではない
  • Token利用やリトライ方法が均一ではないのでシステムの認知負荷が高い
  • GBP APIクライアントライブラリがgolangでは存在しない
  • GBP API version upの度にコードのコピペが必要になる

事業要望を叶えるためにシステムの数を増やした結果GBP APIのversion up対応が辛くなり、今回の共通化を行うことになりました

社内ライブラリ配布

実際に取り組んだ社内ライブラリ配布の仕組みを紹介します

配布フロー

tag pushによりgithub actionsを起動して配布先の各repositoryにrepository dispatchを送ります。受け取った配布先はライブラリをアップデートしたpull requestを作成します
private repositoryを使ったgolangのモジュール配布

各項目

  1. golang private repositoryにtag pushを行う
  2. tag pushでGithub Actionsを起動する
  3. Github Actionsから配布先のrepositoryに対してdispatch eventを通知する
  4. 配布を受けるrepositoryのGithub Actionsを起動する
  5. golang packageをversion upしてPull Requestを作成する

配布元の構成

Github上でprivate repositoryを作成し、golangでコードを書きます
実際に配布元として行う設定などはGithub Actionsに集約します

Github Actions workflow

Github Actionsで手動トリガーとして用意されているrepository dispatchを利用したjobを組みます
実際にdispatch eventを臆す際には公開されているActionで利用できるものを見つけたので採用します

  • github/workflows/dispatch-update.yaml
name: dispatch-update
on:
  push:
    tags:
      - 'v*'
jobs:
  release:
    strategy:
      fail-fast: false
      matrix:
        # 配布先が増えるときに追加する
        repo: ['repo1', 'repo2','repo3','repo4']
    name: dispatch
    runs-on: ubuntu-latest
    steps:
      - name: dispatch update module
        uses: peter-evans/repository-dispatch@v1
        with:
          repository: ${{ OrganizationName }}/${{ matrix.repo }}
          token: ${{ secrets.REPO_ACCESS_TOKEN }}
          event-type: update-gbpapi-client

secrets.REPO_ACCESS_TOKENにはPersonal access tokenを利用するかGithub App Tokenを利用するか等様々な方法があります
今回はFull control of private repositoriesなPersonal access tokenを採用しています

※ Github App Tokenの利用方法を知ったのは弊社テックブログの岸田くんの記事だったのは内緒です

Github App Tokenでの設定については下記のブログをご参照ください

developer.so-tech.co.jp

配布先の構成

こちらもGithub上でprivate repositoryを作成しgolangでコードを書きます
配布eventの受け取りとしては以下の2点が必要です

  • dispatchを受け付ける
  • private repositoryのgolang packageをgo getできるようにする

Github Actions workflow

repository dispatchを受け取りつつ、package updateしたPRを作成します
その際にprivate repositoryからgo getできるようにするため、OAuthトークンを使ってgit参照を行います
また、private repositoryのgolang packageを展開するためにGOPRIVATE設定を用います ※ CICDでgo getを行う場合でもGOPRIVATE設定を利用する必要があります

  • .github/workflows/update-gmbapi-client.yml
name: update-gbpapi-client

on:
  repository_dispatch:
    types:
      - update-gbpapi-client
jobs:
  create-pull-request:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-go@v2
        with:
          go-version: 1.17.3
      - name: Set up github token
        run: git config --global url."https://${{ secrets.LYCLE_GO_MODULES_TOKEN }}:x-oauth-basic@github.com/".insteadOf "https://github.com/"
      - name: Set current client version
        id: current
        run: |
          current_version=$(go list -m all | grep -o -E "(github.com/{{ OrganizationName }}/gbpapi-client .*)?" | grep -o -E "([0-9]+\.){1}[0-9]+(\.[0-9]+)?" | head -n1)
          current_major_version=$(echo "${current_version}" | grep -o -E "([0-9]+\.)" | head -n1)
          current_minor_version=$(echo "${current_version}" | grep -o -E "([0-9]+\.)" | tail -n1)
          echo "current_major_version=${current_major_version}" >> $GITHUB_ENV
          echo "current_minor_version=${current_minor_version}" >> $GITHUB_ENV
      - name: Update gmbapi client
        run: GOPRIVATE="github.com/{{ OrganizationName }}/*" go get github.com/{{ OrganizationName }}/gbpapi-client@latest
      - name: go mod tidy
        run: go mod tidy
      - name: Set latest client version
        id: latest
        run: |
          latest_version=$(go list -m all | grep -o -E "(github.com/{{ OrganizationName }}/gbpapi-client .*)?" | grep -o -E "([0-9]+\.){1}[0-9]+(\.[0-9]+)?" | head -n1)
          latest_major_version=$(echo "${latest_version}" | grep -o -E "([0-9]+\.)" | head -n1)
          latest_minor_version=$(echo "${latest_version}" | grep -o -E "([0-9]+\.)" | tail -n1)
          echo "latest_major_version=${latest_major_version}" >> $GITHUB_ENV
          echo "latest_minor_version=${latest_minor_version}" >> $GITHUB_ENV
      - name: Create Pull Request
        id: cpr
        uses: peter-evans/create-pull-request@v3
        with:
          title: "✨ Update GBP API Client"
          base: staging
          branch: update-gbpapi-client
      - name: pull request auto merge
        if: env.current_major_version == env.latest_major_version && env.current_minor_version == env.latest_minor_version
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh pr merge ${{ steps.cpr.outputs.pull-request-url }} --merge

ライクルチームではマイナーアップデートの場合はオートマージしたいという要求もあったため、バージョン差異を確認してPRをマージするロジックを組んでいます

GOPRIVATE

golang v1.13で追加された設定です
golang v1.13からはデフォルトでsum.golang.orgにある公開のGo checksum databaseと照合して検証するようになっています

対象のモジュールがprivate repositoryにある場合はchecksum databaseに記録されないため、検証が失敗してダウンロードができません
GOPRIVATEを設定することで対象のパスを検証外のものとして扱うようにできます

終わりに

上記の配布を行って、問題の解消に向けて大きく前進できました

  • Token利用やリトライ処理などを共通化できた
  • GBP APIのversion upを行いやすくなった
  • 配布先のGBP API version up対応を自動作成のブランチから開始しやすくなった

GOPRIVATEを利用することでAPIのクライアントライブラリ以外の共通処理もライブラリ化しやすくなりました

しかし、pubsubっぽい仕組みの割には配布元のworkflowにリポジトリ名を追加する必要がありそこには課題が残ります
そもそも「Monorepoで良かったよねー」と感じる部分のマージ作業など泥臭い部分の解消は今後も粛々と続けていきます