ゲーム内お知らせをHugo+Netlify CMS+CircleCIで作りました

鎌倉は寒いです。みなさんはいかがですか。ソーシャルゲーム事業部のゲーム技研チームの谷脇です。

この記事はTech KAYAC Advent Calendar 2019 Migration Trackの10日目の記事です。9日目はデーモン管理をdaemontoolsからsystemdに移行させるでした。

ゲーム内お知らせとは

みなさんは、スマートフォンのゲームをされますか。ええ、そこのあなたはよくされる。しかし、そちらの方はあんまりされない。なるほどなるほど。

では分かる人にはおさらいとして、あまりピンとこない方にはそんなことがあるのか〜となってもらうために説明させていただきます。

我々カヤックでゲームを運営している人々が「お知らせ」を指した場合、ゲーム内のある機能を示しています。他のゲームではニュースなどとも呼ばれています。

カヤックで作っているゲーム「ぼくらの甲子園!ポケット」(以下ぼくポケ)であれば、以下の画面で提供される機能を指します。

f:id:mackee_w:20191210123458j:plain:w300

運営側から全ユーザに対して知らせたいことが書かれている画面です。このアプリの例ですと、お知らせのタイトルが書かれたリスト型のメニューがあり、タップするとお知らせの詳細画面に遷移します。

f:id:mackee_w:20191209191134j:plain:w300

このお知らせでは、アプリのアップデートが告知されているようですね。

お知らせでは他にも、

  • 新しいイベントの予告・告知
  • ゲーム内のイベント結果の発表
  • オフラインイベントの告知
  • 不具合情報の公表

などがされます。運営型のゲームではユーザ様とのコミュニケーションが欠かせないですが、お知らせはそういったユーザ様との大事な接点一つです。

お知らせ機能に必要なこと

お知らせは、頻繁に更新されるコンテンツです。また、ユーザ様に対してわかりやすく伝えるために、それなりの表現力を求められます。

他にも、時限公開機能も必要ですし、入力画面も必要です。入力画面はしばしばゲーム運営のための管理画面の中に組み込まれています。また、公開できる人を限定するためのACLのような機能も必要です。

お知らせは、ゲームの中のマスタデータなどとは違い、縛りがないテキストであるため、入力の際にミスが起こりやすいコンテンツでもあります。

ゲームによっては、正確に固有名詞を記述するためにマスタデータから引っ張ってきた名前をお知らせに埋め込んだり、日付などもゲーム内マスタデータから埋め込むなどの工夫をしている方もいるでしょう。

埋め込みやバリデーション以外の解決方法として、公開前にレビューを入れるというのもあります。レビューを入れる場合は、GitHubのPull Requestのように、他の人がお知らせ記事に対して承認をするとやっと公開できる、といった機能があると、厳格にルールを運用できるでしょう。

そういったわけで、

  • 頻繁に更新される
  • 幅広い表現力が必要

かつ

  • 事故らないための工夫が必要

というこれらの要件を満たしたシステムを作らないといけません。

WebView vs Native

ぼくポケのお知らせは、管理画面で投稿した独自のXMLをUnityでレンダリングしている方式なのですが、アプリがリリースされた頃のWebViewは他のブラウザとの互換性の問題(特にAndroid)などがあり、WebViewを使わずにUnity側でやる判断になったようです。

ですが、現在ではWebViewでの表示も安定してきて、ノウハウも溜まってきたため、WebViewでお知らせを配信することが多くなってきています。

WebViewでお知らせってそれブログ/CMSじゃない?

そうなんです、定期的にテキストを配信する用途ってまさにブログなんですが、今までそれを再発明し続けてきました。ACLの管理や管理画面と同じところにあったほうがいいだろうということからですね。

ただ、最近はブログを流用すればいいんじゃんということで、名前は伏せますが、とあるブログサービスを使用していました。ですが、複数人で管理ができなかった点や、サービスの安定性などに課題があり、また仕様変更で機能を満たさなくなったため、何らかのCMSに移行しようとなりました。

移行先の技術の検討

そもそも、CMSを作ったり運用するのは避けたいところです。

まず、自分でシステムを構築せずに外部のサービスを使う方向で検討を行いました。しかし、普通のブログサービスとは違って、ゲーム内のお知らせなので、見る方法を何らかの手法で限定しなければなりません。例えば、公開された記事を見る際に認証をかけたり、予測が難しいURLで公開するといったやり方が考えられます。

様々なサービスを検討したのですが、そもそも実現できなかったり、トリッキーな運用をしなければいけないサービスが多く、この時点で自ら運用することに切り替えました。

自分たちでブログサービスを運用する際に、まず候補に上がってくるのはWordPressですが、WordPressは複雑なソフトウェアであり、カスタマイズをして安全に運用する自信がありませんでした。

静的サイトジェネレーター

次に検討したのは、静的サイトジェネレーターと呼ばれるソフトウェアです。JekyllHugoGatsby.jsが挙げられます。

今回導入するサービスのサーバサイドアプリケーションはGo言語で書かれています。そのため、Go製のソフトウェアを用いたほうがなにかあったときに対処しやすいのではと考え、Hugoを採用しました。

編集方法と公開方法

CMSではなく、静的サイトジェネレーターを用いる場合は、少しフローが変わります。

  1. Markdownを何らかで記述する
  2. 何らかをトリガーにして誰かがMarkdownから静的サイトジェネレーターを使ってHTMLに変換する
  3. 2で生成されたHTMLを誰かが、インターネットから見れるどこかにアップロードする

CMSは編集から公開までの機能を一括で提供しますが、静的サイトジェネレーターはMarkdownをHTMLに変換するところしか提供しません。なので、前後の編集と公開に関しては何らかの手を考える必要があります。

Netlify CMS

まずは編集方法です。編集方法に使用したのはNetlify CMSです。

Netlify CMSは、動的なサーバを用いずに、ブラウザ上で完結する静的サイトジェネレーター向けのCMSです。

Netlify CMSは、MarkdownがGitHub上のリポジトリに収められているのであればでGitHub API、その他の一般的なgit rpeositoryに収められているのであれば、別途git gatewayというサーバを介してリポジトリの操作を行い、Markdownの編集をします。

今回の方法では、Hugoが使う原稿のMarkdownと出力されるデザインが収められたthemeと、画像などはGitHub上のリポジトリに格納しています。

Netlify CMSを採用した理由としては、

  • 認証以外にサーバを使わない。管理コストが低い
  • リアルタイムプレビュー機能がある
  • 下書き -> レビュー中 -> 公開前 -> 公開というワークフローの仕組みが使える

が、挙げられます。プレビュー機能のカスタムは後ほど紹介します。

また、Netlify CMSという名前ではありますが、Netlify以外でも使用することができます。今回は認証が諸々あるため、API Gateway or ALBを介したAWS Lambdaから配信しています。

CircleCI

次に、MarkdownからHTMLへの変換と、アップロードをする人を考えます。

ここはCircleCIを使うことにしました。過去にCircleCIは使ったことがあったため、こちらの導入はすんなりできました。

CircleCI API v2で自由自在に業務ワークフローのタスクを実行する

CircleCIには、よくCIで行われるタスクをまとめたものを自由に利用できる、Orbsという仕組みがあります。今回はHugoのビルドをCircleCIで行いたいのですが、Hugoを簡単に使うOrbsも存在するので、こちらを利用します。

また、アップロード先ですが、サービスにはすでに静的ファイルを配信するためにAmazon CloudFrontと、その参照先のAmazon S3のバケットが用意されていたので、そちらを流用することにしました。

なので、ビルドした後にS3にアップロードするということですね。S3にアップロードするのもOrbsがあるので、こちらを使いました。

というわけで、.circleci/config.ymlはこちら。

version: 2.1

orbs:
  hugo: circleci/hugo@0.4.1
  aws-s3: circleci/aws-s3@1.0.11

jobs:
  deploy:
    docker:
      - image: 'circleci/python:2.7'
    steps:
      - attach_workspace:
          at: news
      - aws-s3/sync:
          from: news/public
          to: 's3://hogehoge.example.com/foobar/news'
          arguments: |
            --acl public-read \
            --exclude "*.gitkeep" \
            --cache-control "max-age=30"

workflows:
  main:
    jobs:
      - hugo/build:
          html-proofer: false
          source: ./
          version: '0.59'
          filters:
            branches:
              only: master
      - deploy:
          filters:
            branches:
              only: master
          requires:
            - hugo/build

masterにpushされたらビルドとデプロイを行うようにしました。

システム全体像

システムの全体像を図に表すとこちらです。

f:id:mackee_w:20191210123218p:plain

  • Netlify CMSでGitHubにMarkdownをpush
  • pushをトリガーにしてCircleCIのジョブが起動
  • CircleCIがHugo使ってビルド
  • CircleCIがビルドした成果物をS3にアップロード
  • S3のアップロードしたものがCloudFront経由で見れるようになる

上記の図でこれまで説明していない部分、特にNetlify CMSの認証にまつわる部分は追って紹介します。

また、上の図ではAPI Gatewayを使っていますが、管理の都合上、現在ではALBに置き換わっています。

細かいTips

Hugoのテーマ

今回は、themeのベースとしてBlank themeをベースにして、ネイティブアプリのUIなどをCSSで再現した独自のthemeを組み込みました。

イチから作るのは、何がどうなっているのか把握するのが難しいのでこういったテンプレートをベースにするのが良いと思われます。

Netlify CMSのリアルタイムプレビューのカスタム

ドキュメントはこちら Creating Custom Previews

これでHugoの出力で使われているCSSや、Hugoの個々の記事の設定部分に記述されている見出しや見出し用の画像、日付などをヘッダに埋め込むなどの処理を記述しています。

Netlify CMSの認証

先程の図のここの部分を説明します。

f:id:mackee_w:20191210123239p:plain

認証・認可は、GoogleのOpenIDとGitHubのOAuthとそれぞれで行っています。

あえて、認証・認可とOAuth・OpenIDというようにかき分けましたが、実際にGoogleのOpenIDでNetlify CMSを触ることが出来るかを判断し(認証)、GitHubのOAuthで編集画面を触る人のGitHubアカウントでリポジトリの操作をしています。

どちらもIdP(GitHub/Google)の認証画面に一旦リダイレクトをかけて、認証画面のコールバックとして戻ってくるフローで行われます。コールバックを受けてアクセストークンを発行したり、ユーザ情報を見て通してはいけないユーザであれば弾くなどの処理を行わないといけないため、何らかのサーバアプリケーションが必要です。

ここまで、サーバを運用せずにシステムを構築しようと努力してきたため、ここもサーバレス的な物を使うことにしました。

サーバレスといえば、API Gateway + Lambdaですね。リダイレクトをかけたり、コールバックを受けたり、Netlify CMS自体を配信するLambda関数をGoでガッと書いてデプロイしました。

この構成で認証といえばCognitoも考えられるんですが、Netlify CMS独自の事情もあり、自分で実装しました。github.com/markbates/gothを使ったので、すぐに実装ができました。

Netlify CMSがGitHubのtokenを受け取る仕組み

Netlify CMSはもともとNetlifyのCDNを用いる前提で動くため、GitHubのアクセストークンを受け取る仕組みが少し変わったものになっています。

フローを列挙すると、

  1. Netlify CMSのトップページのGitHubのボタンを押す
  2. 別ウィンドウ(GitHub Backendsとして設定されたURL)が開いてGitHub OAuthの認証が行われる
  3. 別ウィンドウ側がコールバックURLまで戻ってくると、JavaScriptでwindow.openerに登録された関数を実行する
  4. window.openerは現在のウィンドウを開いた元のウィンドウのこと。つまりNetlify CMS
  5. https://developer.mozilla.org/ja/docs/Web/API/window.opener
  6. つまりここで、ブラウザのそれぞれ別のウィンドウ間でメッセージを送っている
  7. Netlify CMS側でアクセストークンを受け取れたら認証を行ったウィンドウを閉じて、編集画面に移動する

Netlifyでやる場合はこれをNetlify側のサービスが行ってくれるようですが、今回のように独自にやる場合は自分でこれらを行わないといけません。

実際にどのようにやっているかは以下のサードパーティ製の実装を参照すると良いでしょう。

Netlify CMS側の設定については次のドキュメントに記述があります。

Authentication & Backends

予測が難しいURLで配信する

Hugoはファイル名がそのままURLの一部になる仕組みなのですが、Netlify CMSでそのままMarkdownのファイルを作ってしまうと日付になったり、タイトルが入ったりと、予測がしやすい記事になってしまいます。

なので、Netlify CMSで新しく記事を作る際にランダムな文字列を埋め込むことにしました。ここでいうランダムな文字列は、例えばUUIDなどです。

Netlify CMSの記事入力フォームはWidgetsと呼ばれるものが複数組み合わさってできてます。そして、記事のタイトルはWidgetsの内容を埋め込むことができます。そこで、ランダムな文字列をデフォルト値として持つWidgetsがないかなと探したところ、このissueで紹介されていた、こちらのリポジトリ@ncwidgets/idを一部改造して使っています。

Netlify CMSの設定としては、

collections:
  - label: "お知らせ"
    name: "news"
    folder: "xxxxxxxxxxx"
    slug: "{{fields.uuid}}"
    fields:
      - {label: "ID", name: "uuid", widget: "uuid", hint: "URLの一部に使われるランダム生成のIDです"}

このように設定すると、タイトルがランダムな文字列に置きかわりました。これである程度予測ができないURLとしてデプロイすることができます。

まとめ

  • Hugo + Netlify CMS + CircleCIで運用がある程度不要なお知らせ管理の仕組みを作りました
    • ピタゴラになって職人が必要で引き継ぎ困難では? という声も聞かれるが社内の具体的な実装+この記事で引き継ぎできてほしい
  • ゲームを作るときもどんどんWebの成果を取り入れて省力化したい。これが俺たちのオフ・ザ・シェルフだ

いかがでしたか。他にも画像がうまくでないとか、そういう細かいのがあったんですが、リクエストがあれば詳細に報告させていただきます。