はじめに
初めまして。夏より冬に食べるアイスの方が好きです。AI事業本部のエンジニアとして所属している佐藤 (@Rintarooo) と申します。
AIクリエイティブディビジョンのデジタルツインレーベル事業部にて、広告素材の制作用の社内ツールの開発・運用に取り組んでいます。
本記事では、gRPCのスキーマ定義であるprotoファイルと生成されたコードの管理用のリポジトリを作成し、git submoduleから移行した際の内容についてまとめています。特に、社内に閉じたプライベートなリポジトリで実施する上で躓く点や学びが多くあったため、この度得られた知見を共有いたします。
移行前の構成
私が所属するチームでは、Webフロントエンド(以下、フロント)とバックエンドとの主な通信にConnectを採用し、Protocol Buffersによるスキーマ定義とコード生成を活用した開発を行っていました。
protoファイルの管理は、バックエンド側のリポジトリのみで行い、フロント側のリポジトリでは、そのprotoファイルをgit submoduleを用いて参照していました。
課題点
一方、この構成で開発を進めていく中で、いくつかの課題が見受けられるようになりました。
- git submoduleの複雑・煩雑さ
- protoファイルに修正を加える開発メンバーが、git submoduleの仕組み・コマンドについて理解する必要がある。
- ブランチを切り替える度に、参照先の更新などのgit操作が発生する。
- 参照先のコミットのハッシュ値しか表示されないため、そもそもどのコミットのprotoファイルを参照しているのか把握しづらい。
- protoファイルの修正PRにかかる負担の重さ
- フロントとバックエンド間で参照しているprotoファイルの差異を無くすため、バックエンドのリポジトリでPRをマージした同じタイミング、つまり直後にフロントのリポジトリにPRをマージさせる必要がある。
- バックエンドのリポジトリにprotoファイルの修正PRを出す開発メンバーが、フロントのリポジトリにCIのテストを通してPRを出すことが望ましいため、フロントのソースコードにも精通している必要がある。
上記の課題から、git submoduleを使用しない、且つprotoファイルのバージョン管理・参照を柔軟に出来るような構成にしたいと考えました。
移行後の構成
protoファイルと生成したコードを一元的に管理するprotoリポジトリを新しく作成して、git submoduleを用いない構成に移行しました。
開発メンバーは以下の手順で、protoリポジトリを利用します。
① protoファイルの修正を行い、pushしてPRを出す。
② workflowを手動実行することで、①で生成されたコードをリリースする。
まず、①のPRのマージをトリガーに、GitHub Actions側で、protoファイルの整形とコードの自動生成、及び自動コミットを行います。
一方、CIだけでなく開発マシン上でも、protoファイルの整形やコードの生成をしたい場合があります。docker-compose.ymlに、Buf CLIを用いたprotoファイルの整形・コード生成用のサービスを定義しておき、CI側で差分がある場合にのみ自動コミットさせることで、開発マシン上でのコード生成にも対応しています。
次に、②で実行されたworkflowでは、メジャー、マイナー、パッチ更新のいずれかの指定した更新方法に従って、CI側でタグが打たれてリリースされます。
①で生成されたTypeScriptのソースコードは、npmパッケージとしてGitHub Packagesに配置します。一方、Goのソースコードは、protoリポジトリのgo.modファイルを置いているディレクトリのバージョンタグを付与することで、リリースします。
移行して良かった点
protoファイル・生成コードの管理場所を他のリポジトリと分離して、セマンティックバージョニングでリリースすることで、参照するprotoファイル・生成コードを柔軟に切り替えることが出来るようになりました。
また、protoファイルを参照するリポジトリ・生成言語が今後増えた場合にも、対応しやすい構成になっています。
移行で苦労した点
一方、移行する際に苦労した点として、生成されたGoのモジュールをバックエンドのリポジトリから取得する際の権限をいかに付与するか、が挙げられます。
protoリポジトリは、開発チーム内での使用を想定しているため、誰もが見れるpublicなリポジトリではなく、社内のOrganizationのinternalなリポジトリとして設定しています。
プライベートなGoのモジュールの取得
本記事では、protoリポジトリで生成されたGoのモジュールを、バックエンドのリポジトリにおける以下の4つの場合で、それぞれどのようにダウンロードしたか説明します。
- ローカル環境
- GitHub Actions環境
- ローカル環境でのDockerfileのビルド
- GitHub Actions環境でのDockerfileのビルド
1. ローカル環境
開発マシンのローカル環境では、go getコマンドにより、Goのモジュールをダウンロードすることが可能です。一方、このgo getコマンドは、モジュールのダウンロード時に、go.modファイルに記載されているモジュール間の依存関係の更新を行います。
依存関係が更新される際に、Goの開発元であるGoogleが公開しているチェックサムデータベース(sum.golang.org)を参照して、ダウンロードしたモジュールのチェックサムと照合することで、インストールしたモジュールが改ざんされていないか検証します。
当然ながら、社内に閉じたprotoリポジトリのモジュールは、公開されているチェックサムデータベースに登録されていないため、go getコマンドを叩くと、404 Not Foundのエラーが出ます。
そこで、GOPRIVATE
と呼ばれる環境変数を以下のように設定してgo getコマンドを叩くことで、モジュールの取得が可能になります。
$ export GOPRIVATE=github.com/organization_name/private_repository_name
2. GitHub Actions環境
次に、GitHub Actions上で、Goモジュールを取得する場合を考えます。この場合、上記の環境変数を設定しても、エラーが出ます。GitHub Actions側で、プライベートリポジトリの読み取り権限がそもそも不足しているからです。そこで、アクセス権限を発行して付与する必要があります。
個人に紐づく個人用アクセストークン(PAT)を発行する代わりに、Organizationに紐づくGitHub Appsによる発行を行う安全な運用が推奨されます(GitHub Appsを用いるには、GitHubの同一Organization内でリポジトリが管理されていることが前提です)。
また、Github Appsのアクセストークンの発行のactionsについては、個人で公開されているものではなく、公式のactionsを用いました。
以下に、アクセストークンを発行後に、git configコマンドで権限を付与して、モジュールをダウンロードするGitHub Actionsの記述例を示します。
- name: Set up git config to fetch private repo
run: git config --global url."https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/".insteadOf "https://github.com/"
- name: Download Go modules
run: go mod download
また、GitHub Actionsで使用しているgo mod downloadコマンドは、モジュールのダウンロードを行うのみで依存関係を更新しないので、GOPRIVATE
の環境変数の設定は不要になります。
3. ローカル環境でのDockerfileのビルド
今度は、開発マシン上でDockerfileをビルドする際の、go buildコマンドによるモジュールのダウンロード方法について考えます。この場合にも、fetchする際に読み取り権限不足が発生します。
そのため、Dockerfileをビルドする際に、ホスト側である自身のローカル環境にある権限を共有します。開発マシン上のSSHキーをssh-agentに追加した上で、以下のように記述したDockerfileをビルドします。
RUN mkdir -p ~/.ssh && chmod 0600 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts && \
git config --global url."ssh://git@github.com/".insteadOf "https://github.com/"
RUN --mount=type=ssh CGO_ENABLED=0 go build -tags release -o /go/bin/app ./cmd/app/main.go
4. GitHub Actions環境でのDockerfileのビルド
最後に、GitHub ActionsでDockerfileをビルドする場合です。この場合、既に上述したGithub Appsによるアクセストークンを発行して、CI上でビルド時に変数として渡すようにします。
Dockerfileは、以下のようになります。
ARG TOKEN
RUN git config --global url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/"
おわりに
本記事では、protoリポジトリに移行するに至った経緯や構成から、苦労した点であるGoのモジュールの取得方法について、書かせていただきました。
ネット上の記事には古い情報も混在しており、どのように権限を付与するのがより良いか試行錯誤しながら移行を進めました。
この構成がベストプラクティスであるというわけではなく、改善点を洗い出しながら、チームメンバーがより開発しやすい構成を今後とも探りたいと思います。
また、設計から移行まで進めるにあたり、そして本記事を執筆する上で、相談やご協力していただいた開発チームのメンバーに、この場を借りて感謝申し上げます。