GS2 Blog

Game Server Services(https://gs2.io/) の最新情報をお届けします

Vue + TS に触れて2週間でシングルページアプリケーションを作ってリリースした話

みなさん、こんにちは。GS2の丹羽です。

本日 Game Server Services は次世代版のベータテストを開始しました。
これまで提供していた GS2 を GS2 Classic と呼び、次世代版 GS2 を単に GS2 と呼びます。

gs2.io

GS2 のリリースにあたっては非常に多くの新しいチャレンジを行いました。
それについては少しずつブログや登壇を通じて皆さんにお伝えしていきたいと思います。

今回は、GS2 のマネージメントコンソールをシングルページアプリケーションとして作りました。
しかも、Vue + TypeScript に触れて2週間で。というトピックで振り返ってみようと思います。

なぜ Vue + TypeScript にいたったか

GS2 Classic のマネージメントコンソールは Google App Engine を使用していました。
弊社はフルサーバレスアーキテクチャでサービスを作ることに並々ならぬ努力を重ねてきました。
そのため、マネージメントコンソールを作るにあたっても採用できる技術は非常に限られています。

GS2 Classic を GAE で運用してみて感じた一番のツラミはコールドスタート時の スピンアップ速度 でした。
GS2 も GS2 Classic も Function as a Service(いわゆる AWS Lambda) を計算基盤として開発しています。
こちらもスピンアップ速度は課題にはなるのですが、弊社では AWS SDKOSS で提供されているものではなくよりフットプリントが小さく、高速に動作するものを用意して使用するなど、サービスのレスポンスタイムを短くする努力をしてきました。

一方で、そこまで大量にアクセスが来るわけではないマネージメントコンソールは開発リソースをあまり割けていないのと、アクセス数がサービスのエンドポイントより圧倒的に少ない。というダブルパンチでコールドスタート時の応答速度がストレスを感じるレベルになっていました。
やはり、開発効率重視で Web Application Framework(WAF) を使ったアプリケーションを GAE に乗せてしまったのは大きな過ちだったのだと思います。
コンテナの起動速度だけでなく、ランタイムに使用しているJava仮想マシン(GS2 Classic はサービスでは Python を使用していますが、マネージメントコンソールは Java(Kotlin) で作っています)。そして、WAFの起動とレスポンスタイムを遅くする要素が満載です。

FaaS でも WAF を使った開発をしようというアプローチもありますが、スピンアップ速度が遅くなるので私はおすすめしません。
GS2 Classic も GS2 も非常に薄いレイヤーで設計されており、スピンアップ速度も最小になるよう心がけています。

というわけで、GS2 のマネージメントコンソールを開発するにあたっては割と早期に
『HTML + JS でサービスと同じ REST API エンドポイントを使って提供する』ということを決めていました。

なぜ Vue なのか

React や Angular といった WAF もあります。その中で Vue を選んだ最大の理由は、学習コストが低そうだったことです。
そう、タイトルにもあるように開発にかけられる時間が非常に限られていたのです。

なぜ TypeScript なのか

単に型が厳密な言語が好みだからです。

開発期間は2週間なのはわかったけど、人数は?

基本的に1人。
しかも大量に他の仕事も兼務してる。実質5営業日くらいしかかけられない。

で、できたものは?

f:id:kazutomo:20190716232113p:plain
Bootstrap 臭いUIなのは気にしないで

f:id:kazutomo:20190716232131p:plain
Vue コンポーネントを活用して複雑なUIもパズルのように組み合わせて実現してます

f:id:kazutomo:20190716232142p:plain
いたって普通の入力フォームはこんな感じ

f:id:kazutomo:20190716232154p:plain
ビューによってはこんなとんでもない入れ子構造のデータもあるけど、Vueコンポーネントならなんとかなった

f:id:kazutomo:20190716232210p:plain
当然、入れ子構造は入力側でも…

ページ数・コンポーネント数は?

正確に数えてないけど、1マイクロサービスあたりページ数で10。
ビュー用のパネルや入力用のパネルのコンポーネント数で50〜80くらいあると思う。
そのマイクロサービスが30個くらいあります。ひいふうみい…。おう…。

物理的に無理じゃない?

感のいいひとは気づいてしまったでしょう。2週間で 1500〜2000 のコンポーネントを実装するとか無理ゲーだということに。

GS2 はマイクロサービスを DDL で定義していて、そこから色々なものを自動生成しています。
FaaS で動くサービスの雛形とか、サービスにアクセスするための各プログラミング言語向けのSDKとか。
TypeScript でアクセスするための SDK もそうやって作ってます。

@data_structure(
    parent=Namespace.Namespace,
    data_owner=DataOwner(
        owner_holder=DataOwner.OwnerHolderSelf,
        owner_type=GamePlayer(),
    ),
    primary_key=String('accountId'),
    alternate_keys=[
        AlternateKey(
            field_name='userId',
        ),
    ],
    description='''
    ゲームプレイヤーアカウント
    
    Account はゲームプレイヤーを特定するためのユーザIDとパスワードを記録します。
    ''',
)
class Account(BaseModel):

    accountId = GenericGrn(
        primary_key=True,
        require=True,
        updatable=False,
    )
    userId = UUID(
        description='アカウントID',
        require=True,
        primary_key=True,
    )
    password = String(
        description='パスワード',
        require=True,
        updatable=True,
        default=Default.Value(),
    )
    createdAt = CreateTimestamp()

モデルの定義はこんな感じ。

@result(
    items=Array(Account),
    nextPageToken=String(
        description='リストの続きを取得するためのページトークン',
    ),
)
class Describe(BaseDescribeAction):

    namespaceId = Namespace.Namespace.namespaceId
    pageToken = PageToken()
    limit = database.AccountDefaultLimit()

    @path(GET, '/{namespaceName}/account')
    @policy('DescribeAccounts')
    @description(
        '''
            {model_name}の一覧を取得
        '''
    )
    @request(
        (namespaceId, [Request.Default()]),
        (pageToken, [Request.Default()]),
        (limit, [Request.Default()]),
    )
    def describeAccounts(self):
        pass

メソッドの定義はこんな感じ。

で、マネージメントコンソールようにこんな DDL を作った。

@description('{model_name}の新規作成')
@path(GET, '/namespace/:namespaceName/account/:userId/take_over/create')
class CreatePage(BaseConsolePage):

    def __init__(self):
        super().__init__(
            parent=Account.Console.DetailPage,
            name='create_take_over',
            panels=[
                Console.Panel.CreatePanel,
            ],
        )

class CreateInputPanel(BaseGeneralInputPanel):
    def __init__(self):
        super().__init__(
            properties=TakeOver.Create.createTakeOver.request,
        )

class CreatePanel(BaseCreatePanel):
    def __init__(self):
        super().__init__(
            method=TakeOver.Create.createTakeOver,
            input_panel=Console.Panel.CreateInputPanel,
        )

CreatePage はどういうページを作るかを定義してあって、
CreateInputPanel はどういうフィールドの入力フォームを用意するかが定義してあって、
CreatePanel は入力パネルとその送信先メソッドを定義している。

CreateInputPanel と CreatePanel が分離しているのは CreateInputPanel を入れ子構造の入力フォームで再利用できるように Submit ボタンの挙動を分離している。

というわけで、すべてのモデルデータに対応するDDL宣言を追加していって ジェネレータで一気に Vue コンポーネント を生成するようにした。

つらかったこと

これだけページやパネルがあるとビルド時に node のメモリが不足してなかなか動かなかった。
結局、最終的に32GB割り当ててビルドすることになった。
GS2 ではメモリ 64GB のマシンをみんなに割り当ててるのでよかったけど、みんなどうしてるんだろう。

gs2.hatenablog.com

最後に

めっちゃ厳しいスケジュールの中、なんとかリリースできました。
サービスの内容は 先日の Game Tool & Middleware Forum で発表した資料をみてね。

speakerdeck.com

まだまだ突貫工事感あるので、少しずつ改善していきます。

あと、ベータテストのフィードバックを GitHub Issue で受け付けて対応予定とかも載せてるので、興味があれば見てみてください。

github.com

それでは、また。

(C) Game Server Services, Inc.