新マスタデータ管理システムakashicの開発

こんにちは、各位忘年してますか。弊社では新年会は1年に1回しか出来ないが、忘年会は1年に何度も出来るという説が出回っています。僕も8月頃に1度忘年してますが、まだまだやっていきましょう。ソーシャルゲーム事業部ゲーム技研の谷脇です。

この記事はTech KAYAC Advent Calendar 2019 Migration Trackの18日目の記事です。17日目はPush 通知送信エージェント Gunfish に FCM v1 API 対応を追加したでした。

この記事で話すこと

  • モバイルゲームのマスタデータの管理ツールを今年作り直したよ
  • やりたいことが複雑だからいい感じに設計したよ
  • CLIでも動くしサーバレスでも動くよ
  • この記事がakashicの概要が書かれたドキュメントじゃ!(社内向けの業務連絡)

マスタデータの管理とは

そういえば先日、マスタデータNight #1というイベントを開催しました。私は運営と発表をやったのですが、各社・各サービスのマスタデータに関する様々なオナヤミーや、解決方法について共有された素晴らしいイベントになったと自負しております。

そんなマスタデータNightで私が発表した資料の中に、モバイルゲームのマスタデータで起こる問題と、問題をどうやって解決するのかという話をしました。

speakerdeck.com

問題を解決する手法を「マスタデータの管理」とこの記事では言います。

詳しくは上記の内容を見てほしいのですが、要約すると以下のような感じで管理しています。

マスタデータで考えなければならないこと

  • 種類が多い。入力の仕方を考えなければならない
  • 更新頻度が高く、並列開発に耐える管理方法を採用しなければならない
  • 間違ったらサービス運営上の事故につながる

解決方法

  • 入力はGoogle Sheetsをフル活用して自動化する
  • Google Sheets上のデータにバージョンラベル的なのを付けて、CSVなどに落とすときに抽出する
  • マスタデータのレビューとテストを実施

というようなことがスライドで書かれています。この記事では2番目をやるために作ったシステムakashicの話をします。

akashicとは

f:id:mackee_w:20191218134004p:plain:w300

Icon made by Freepik from www.flaticon.com

「アカシック」と呼びます。名前の由来は「アカシックレコード」というスピリチュアルな分野の単語からの引用で、プログラムの中にakashic.Recordって書きたかったから名付けました。

akashicの目的は、ゲームで使われるマスタデータを安全かつわかりやすく、そして試行錯誤がしやすい仕組みで管理することです。

やっていることを一言で言えば、Google Sheets上のデータを核にして、様々な変形や加工、多くの形式への出力を行う内製システムです。なかなか想像がつきにくいと思いますので、図にしてみます。

f:id:mackee_w:20191218134111p:plain

akashicがやっていることとしては、

  • Google Sheets API v4を用いてSheets内のデータを取り込む
  • 取り込んだデータをルールに則って加工する
    • 1行目をカラム名/キー名(JSON出力場合)とみなす
    • _が先頭についているカラム名の列は省く
    • _#tagが付いたカラムはタグカラムというバージョン識別用の特殊カラムとみなす
    • idカラムの中で重複する行が出た場合は最後に出たものを採用する
    • カラム名に.[]が含まれていると構造化データとみなしてJSONのObjectやArrayに変換する(JSON出力時のみ)
  • 同じ種類とみなしたシートを結合する
  • タグ間の依存性を定義したシートを認識して自動で依存する行も含めて取り込みを行う
  • JSON Schemaによる型・値域のバリデーション
    • バリデーションの結果エラーが出た場合は対象にシートに書き戻す。セルのメモとして書き戻すか、シートとして書き戻すかを選択可能
    • JSON Schemaの定義ファイルはGitHubのリポジトリからAPIで取得するか、ローカルファイルからの読み込みに対応
  • JSONもしくはCSVでの出力
  • 出力したファイルのアップロード
    • Google Driveへのアップロード。単体ファイルかシートをまとめてZIPに固めてアップロードするか選択できる
    • 社内で使っているmBaaSへのアップロード
    • GitHubのリポジトリにコミットしてPull Requestを作成する(開発中)

こんな感じです。マスタデータのファイル出力に関連することならすべてやるソフトウェアです。

従来のマスタデータ管理と試行錯誤

akashicは今年の頭から開発しているソフトウェアですが、当然カヤックのゲームは昔からマスタデータを管理しているので、当然同種のソフトウェアはありました。Perl版のKG::Spreadsheet::MasterやGo版のss2csvが存在します。Perlのプロジェクトでは前者を今も使っています。

これらのライブラリ/ソフトウェアが行っていたのは、

  • Google Sheets APIを使うか、xlsxをダウンロードしてCSVに変換する
  • シートとマスタデータの種類の対応表が書かれたシートを見て、同じ種類のマスタデータを結合する
    • マスタデータを編集する場合は必ず新しいシートを作って、行を追加するルール
  • idカラムのセルの内容の先頭に、指定した文字列が入る場合はマスタデータから除外するなどのフィルタ機能

などなど、ありました。これでマスタデータを管理していました。追加する差分を記述して結合して取り込む仕組みなので、社内では「マスタデータの差分管理」と呼ばれていました。

しかし、実際にマスタデータを入力する方々から以下のような声が挙がっていました。

  • 従来のやり方だと同一IDの内容の上書きができない
    • 事故を防ぐために仕組み上禁じていた
  • マスタデータを編集する際は必ず新しいシートを作って追加する仕組みが作っているものにそぐわなくなってきた
    • 昔のモバイルゲームはマスタデータを上書きせずに行追加だけで更新ができるような機能が多かった
    • 今のモバイルゲームはパラメータ調整など上書きが頻発するので今の仕組みでは難しい
  • シートの多段取り込みに対応していない
    • 今までの仕組みは「本番と同内容のマスタデータのシート」+「追加したい行が書かれたシート」の2つを結合する原理
    • 2つまでのシートしか結合できなかった
    • 今後公開する予定のイベントAで配布するアイテムを使ったイベントBのように2つのシートに変更がわかれるが、同時に開発したいイベントがある場合の対処が難しい

今の仕組みが考え出されたのは2014年ごろです。あれから約5年ほど経っているので、それぐらいすると作っているものも変わるし、作るための道具も合わなくなってくるということですね。

というわけで、新しいマスタデータ管理システムを作るぞとなりました。で、さっき言ったような機能がモリモリ追加されていきました。

サーバ(エンジニア)レス環境での運用

akashicを使っているプロジェクトの中には、mBaaSを使っているためにサーバサイドアプリケーションを書くエンジニアがいないチームもあります。サーバサイドアプリケーションを作らないので、汎用の開発サーバのようなものがなく、他のチームでやっているようなBotでakashicを動かしてSlack経由で取り込み実行、のようなことが出来ません。

この場合、普通に考えたらakashicをスタンドアロンのアプリケーションとしてチームの人たちに配布する方法を取るケースが多いかと思います。ですが、チーム内に配布する場合は更新の手間であったり、取り込みをする人が偏ってワークフロー上にボトルネックが生じる可能性があります。

というわけで、以下の図のようにAWS Lambdaでakashicを動かして、Google Driveに成果物をアップする仕組みも構築しました。

f:id:mackee_w:20191218134148p:plain

Google SheetsとLambdaの間には実際にはALBが挟まっています。

Lambdaを起こすのは取り込み対象のGoogle Sheetsのメニューに埋め込んだGoogle App Scriptからです。いじったらすぐ出力できるようにこのようにしています。

なぜ「全部」やるのか

akashicの開発スローガンは「全部やるぞ!」です。

akashicは1つのプログラムの中にモリモリ機能を入れたファットなアプリケーションです。普段、僕がお手本にしているUNIX哲学ですと、仕事を細かいパーツに分けて、それぞれのパーツごとにツールを作って最終的につなげるのが良いとされています。ですが、akashicのやり方は真逆です。

なぜ全部やるのかというと、現場での要望を最速で達成するためです。だいたいの要望は、既存の仕組みにプラス何かが出来るようにしたい、ということなので、既存の仕組みであるakashicに機能を足すほうが、独立したパーツを作るよりは早く作れ、そして早くデプロイ出来ます。これはakashicを開発しているのが主に私だけという事情もあります。

また、1つのプロジェクトの要望のためにパーツを作ると、そのパーツは身動きが取りやすいのでプロジェクト固有の事情によって独自進化してしまいます。ですが、一つのプログラムに内蔵されてそれが他のプロジェクトでも使われているとなれば、より汎用的に機能を考えないといけません。あと1人で複数のパーツを管理するのは大変なんですよね。まさにコンウェイの法則です。

開発言語にGo言語を採用しているので、プラグイン機構を作りにくいのも、動機としてはあると思います。

全部やるための「設計」

ただ、いろんな機能を1つのソフトウェアに収める、しかも追加の要望に答えていくためにはそれなりの戦略が必要です。ここでいう戦略は具体的に言うと、ソフトウェアの設計です。

設計上の機能の間が密結合すぎると、別に機能を入れる隙間がなくなってしまいます。

うまくソフトウェアを設計するにはどうすればいいか。まずソフトウェアがやる仕事を抽象化しつつ分割します。

akashicがやることは、

  • 何らかのソースからのシート(概念)の取り込み
    • シートはレコード(概念)の集合に名前をつけたもの
    • レコードはIDとカラムに対応した名前を持つ
  • シートを一定のルールによって加工する
  • シートを一定のルールでバリデーションする
  • シートをどこかに出力する

に抽象化出来ます。なのでこれらをGoのinterfaceで抽象化します。

type Sheet interface {
  Name() string
  Records() []Record
  // ...
}

type Record interface {
  ID() string
  Columns() []string
  Value(column string) string
  // ...
}

// FiltererはSheetを破壊的に変更します
type Filterer interface {
  Filter(s Sheet) (n int, err error)
}

// Checker は 入力された sheet が valid であるかどうかを検査します
type Checker interface {
  Check(ctx context.Context, s Sheet) ([]CheckResult, error)
}

// CheckResult は Checker の結果を表現します
type CheckResult interface {
  String() string
  Column() string
  CellIndex() string
  Reason() string
  // ...
}

// CheckOutputter は CheckResult の結果をどこかに表示します
type CheckOutputter interface {
  Output(context.Context, []CheckResult) error
}

// Outputter は sheet の内容を任意の形で出力します
type Outputter interface {
  Output(shs ...Sheet) error
}

各機能はこれらのinterfaceを満たすように実装しています。そして、これらの機能を結びつけるRunnerstructが、コマンドライン引数(standaloneモード)やHTTPリクエスト(serverモード)を元に使用すべきFiltererOutputterを見つけてパイプラインを構築し、実行する形にしています。

現在GitHubのPull Requestとして出力する機能を作っていますが、この機能はOutputterとして作っています。

このように最初に内部の設計とinterfaceを決めたことで、ソフトウェアの守備範囲が定まって、「これは他のツールでやろう」「これはakashicに追加します」というような判断がすぐにできるようになりました。

設計思想を明確にする

interfaceを決めることにも通じますが、こういう内製ツールを作るときでも「設計思想」というのは明確にしたほうが良いでしょう。

akashicの方針の一つに、「シートに書かれた内容以外で出力する結果を変えることはしない」というのがあります。何らかの設定ファイルを読むとか、シート以外のリソースも組み合わせてよしなに判断して出力、というようにするとマスタデータをいじる人が少なくなりますし、出来ることが格段に増えてしまいます。

akashicはいろんなことをやりつつも、あくまで「Google Sheetsの内容を加工して他のところに出力する」と一言で言えるソフトウェアにおさめています。一言で言えたほうが役割がはっきりし、プロジェクト側で採用もしやすいですし、機能追加の方針もはっきりしてやりやすくなります。大きな意味で「一つのことをうまくやる」ソフトウェアなのかもしれません。

まとめ

  • なんでこれをmigration trackに書いたかというと、今年ss2csvという従来のソフトウェアからakashicに運用中に移行したプロジェクトがあったからでした
    • それぞれの移行時の事情とか追加した機能とかも書くと面白そうだと思ったけれど長くなるのでやめました
  • Google Sheetsから何かしらしたいという方は社内でも多いと思うのでよろしくおねがいします
  • 社外向けにはこういう仕組みを作って安全に運用しようとしていますアピール
  • 物作る時の思想は大事にしましょう。私からは以上です