Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

中立的なGraphQLスキーマの管理

こんにちは、id:aerealです。

今回はGraphQLのスキーマ管理を工夫している点について紹介します。

背景

対象となるアプリケーションは先日プレスリリースが出された学習トレーニング機能を裏で支えているコンテンツ管理システム (以下、内部CMS) で、エンドユーザ向けを含む複数のサービスから呼び出されます。またAngularで書かれたWeb UIを備えます。

内部CMSを開発するチーム内には主にサーバサイドを担当するメンバーと、主にクライアントサイドを担当するメンバーとがおり、どちらもGraphQLを用いた開発経験があります。

この内部CMSはスクラッチから開発を始めており、目指すリリース予定日に対してやることは山積みなのでうまくタスクを分担したい状況にありました。

時と場合によってはクライアントサイドのチームの手が空いていたりあるいは逆になったり、状況は目まぐるしく変わります。

ですから手が空いた方がスキーマの設計に手をつけられるような分業体制を整えるモチベーションが強くあります。 *1

つまり スキーマの管理を特定のチームやコンポーネントに寄せず、責任・主導権を関係者の誰もがとれる ようにすることを狙って設計しました。 この点を指してタイトルにあるように「中立的な管理」と表現しています。

これから具体的な手法について紹介しますが、同等のコードを含むリポジトリを公開しましたのでぜひこちらも併せてご覧ください: https://github.com/classi/example-graphql-api-schema

内部CMSの構成

クライアントサイドはフレームワークにAngularを、GraphQLクライアントにはApollo Clientを採用し、TypeScriptで書かれています。

TypeScriptの型生成にはGraphQL Code Generatorを使っています。

サーバサイドはGoで書かれておりフレームワークの類は採用せず、標準のnet/httpのみでHTTPサーバを書いています。 GraphQLのサーバサイド実装およびコード生成はgqlgenを使っています。

スキーマの配置

クライアントサイド・サーバサイドのどちらからも中立させるためにスキーマは独立した単体のリポジトリに配置します。 簡便のため以後の文中では単に「スキーマリポジトリ」と呼びます。

スキーマリポジトリではGitHub Package RegistryのNPM registry (以下、単にGitHub Pacakge Registry) を介してスキーマファイルを含んだNPMパッケージを配布し、スキーマを利用するリポジトリはこのNPMパッケージを利用します。

後述するリリース関連のスクリプトや各種設定ファイルを除けばスキーマファイルのみが置かれています。 以下はファイル配置をtree(1)で出力したものです:

# tree --gitignore
.
├── LICENSE
├── README.md
├── analyzer
│   └── requireauthorize
│       └── checker.go
├── go.mod
├── go.sum
├── graphql.config.yml
├── index.ts
├── package-lock.json
├── package.json
├── schemata
│   └── main.gql
├── tools
│   ├── analyze
│   │   └── main.go
│   └── generate-manifest
│       └── main.go
└── tsconfig.json

開発者はスキーマファイルを変更するPull Requestを送り、関係者のレビューを経てマージされたあと新しいNPMパッケージがリリースされます。

スキーマのリリース

前述の通りスキーマはNPMパッケージとして配布されます。

このリリース手順は自動化されており、変更したい開発者の視点では単にPull Requestを送りマージするだけで新しいスキーマが配布されるようになっています。

既存のOSSを参考にsemantic-releaseを使って新しいバージョンの発行とGitHub Package Registryへの公開を自動化しています。

開発者に求められるのはConventional Commitsに従ったコミットメッセージを書くだけです。

Conventional Commitsを内部向けリポジトリに採用することへの懐疑や批判を抱かれる読者もいるかもしれません。

今回取り上げる内部CMSのスキーマリポジトリでは単に「semantic-releaseによって新しいバージョンを発行するための規約」としてConventional Commitsに従っているものとしており、厳密なsemantic versioningに従うものではないということを合意しています。

つまりごく単純化すれば「新しくバージョンとしてリリースしたい変更を含む場合は fix:, feat: などのタグをつける」「それ以外のリリースフローの改善などのみを含む変更はその他のタグ (e.g. chore:, build:, etc.) を使う」というルールしか強制しません。

いずれにせよシステムに変更を加える際には動作確認をすることを前提としているので「後方互換性を保つかどうか」といった含意を重視してないということです。

スキーマの利用

GitHub Package Registryは対応するリポジトリ自体の公開範囲とは別にパッケージ自身の公開範囲を定義できます。

たとえばリポジトリXからは読み取り (= インストール) のみ許可し、リポジトリYには加えて書き込み (= アップロード、新規リリース) を許可する……といった風です。

我々の内部CMSにおいてはクライアントサイド・サーバサイドそれぞれのリポジトリに読み取り権限を許可し、スキーマリポジトリに管理権限を許可しています。

読み取りたいリポジトリのGitHub Actionsワークフローで packages: read の権限を与えると、リポジトリが読み取りを許可されているパッケージをインストールすることができます。

これら詳細についてはPublishing and installing a package with GitHub Actions - GitHub Docsを参照してください。

もし単にGit submoduleやworktreeを用いてリポジトリを参照しようとすると現時点のGitHub Actionsないし類似のCIサービスでは、より複雑かつ潜在的な危険性を孕んだ構成が求められます。

細やかな権限付与により安全でありながら、利用する際に複雑な設定の既述や手順が求められないためGitHub ActionsとGitHub Pacakge Registryを採用しています。

Goで書かれたサービスでNPMパッケージを利用する?

クライアントサイドはパッケージレジストリのエコシステムとしてのNPMに既に乗っているから良いとして、Goで書かれたサーバサイドのリポジトリではどうするんだ? という疑問をお持ちかもしれません。

答えとしては「Goで書かれたサービスのリポジトリであってもpackage.jsonを置けば良いじゃない」というものになります。

おもむろに npm i -D @classi/example-graphql-api-schema (パッケージ名は例です) すれば node_modules/@classi/example-graphql-api-schema/schemata/main.gql が手に入ります。

gqlgenの設定ファイルでスキーマのパスをこれに揃えておけば、問題なくコード生成もできます。

サーバサイドのリポジトリを扱う際にもNodeやNPM/Yarnが必要になりますが、Classiの開発者はWebアプリケーションエンジニアですから大した支障にはなりません。

ローカル環境にインストールする

各開発者のローカル環境にインストールする際には公式のAuthenticating with a personal access token - Working with the npm registry - GitHub Docsというドキュメントに従います。

Classic Personal Access Tokenを発行し、npmrcに「npm.pkg.github.com を参照する際はこのclassic PATを使え」という設定を既述することで認可されインストールできます。

執筆時点ではGitHub Package Registryの認可にFine-grained PATsは対応していません。

Fine-grained PATsは必要最小限の権限のみを付与できるのに対してclassic PATsは大雑把な権限付与しかできないので漏洩したり誤用した時のリスクは大きくなります。

具体的には「リポジトリコンテンツの読み取り」という権限が少なくとも必要になりますが、対象リソースを絞れないので PATの所有者が権限をもつすべてのリポジトリを読み取れる ということになります。

相対的にリスクはあるものの、classic/Fine-grained共にPATの利用を追跡する仕組みがGitHubにより提供されていることもあり、利用を渋るほどではないと評価して呑んでいます。

Fine-grained PATsは若い機能で継続的に改善されているので、今後の改善に期待したいところです。

今後の改善点

classic PATs脱出

ローカル環境へのインストールにはclassic PATsが必要で、classic PATsはセキュリティ上の懸念があるということは既に述べた通りです。

これはGitHubがFine-grained PATsに対応させるしか本質的解決を望めないので座して待っています。

Renovate対応

依存パッケージの自動更新をしてくれる[Renovate]ですが、開発・提供するMendがホスティングするGitHub Apps版ではGitHub Package Registryでホストされているパッケージの更新にはPATが必要です。 これは前述のclassic PATの懸念に加え、[machine user]のPAT管理という新たな問題も生じます。

これはGitHub Package Registryの認可の仕組み上、仕方のないことではあります。

現時点では、スキーマをインストールする各リポジトリのGitHub Actionsワークフロー内でRenovateを実行するとこの制約を回避できます。

ただSaaS版と共存させようとすると煩雑で、GitHub Actionsで実行するということは、managedにせよself-hostedにせよrunnerの計算機資源を消費することになり、SaaS版と比べてクレジット消費増加やインフラコストの増加に繋がります。

我々はスキーマの利用者と密にコミュニケーションできる体制なのでRenovateによる自動アップデートを諦めています。 スキーマの利用規模が大きい組織ではこの問題はより深刻になるかもしれません。

むすび

我々が開発・運用している内部CMSで扱うGraphQLスキーマの管理・運用について紹介しました。

とても凝っているという印象を受けたでしょうか? あるいは意外と普通だなと感じましたか?

実際、筆者としては特別新規性のあることはやっていないと考えています。実はこうしたワークフローの設計・整備は特にOSSではよく見られるものです。

近年は、semantic-releaseのようにコモンセンスになっていたワークフローをロジックとして実装したソフトウェアや、内部向け用途にカスタマイズされたGitHub Package Registryなどの登場により、内部向け固有の要求を満たしつつ慣れ親しんだワークフローを実現しやすくなったと感じます。

また自動化されたワークフローとはワークフローがコード化されていることとほぼ同義です。

ソフトウェアエンジニアにとってコード化されたソフトウェアほど雄弁な文書はないというのが持論ですが、コード化されていれば変更を提案しそれを直ちに普及させることは、素朴な慣習や自然言語によるルールより、圧倒的に省力で済むのは間違いありません。

この記事で紹介したワークフローを提案し実装したのは主に筆者自身ですが、自動化されて考えることが少ないワークフローが整備されていることによって、実際に慌ただしい時にそのありがたみを感じることができました。

なにより便利さと「やりすぎなさ」のバランスをとりながらワークフローを設計するのはとても楽しい時間です。

エコシステムの成熟に感謝しながら、ぜひ身近なワークフローを自動化してみませんか?

*1:また、今回は深く触れませんがスキーマ駆動開発の実践として、モックレスポンスの生成・利用もしておりサーバサイドの実装を待たずにクライアントサイドの実装を進める余地があったことも手伝っての判断です。

© 2020 Classi Corp.