静的解析とUIの自動生成を駆使して
モバイルアプリの運用コストを
大幅に下げた話
The Go gopher was designed by Renee French.
The gopher stickers was made by Takuya Ueda.
Licensed under the Creative Commons 3.0
Attributions license. 1
2017/08/05(土)
@Builderscon Tokyo 2017
自己紹介
メルカリ/ソウゾウ
上田拓也
twitter: @tenntenn
■ コミュニティ活動
Google Cloud Platform User Group (GCPUG) Tokyo
Goビギナーズ
golang.tokyo
Go Conference
■ 業務
GAE/Goでメルカリカウルを作ってます
GoやGCPコミュニティを盛り上げる仕事
Gopherを描く仕事(LINEスタンプ)
2
アジェンダ
● モバイルアプリとコンテンツ管理
● バナーツール開発の背景と実践
● 汎用性の維持と機能拡張
● [バナーツールを支える技術]
3
モバイルアプリとコンテンツ管理
4
モバイルアプリとコンテンツ管理
5
■ 多様なコンテンツを扱う
● アプリ内バナー
● ゲーム内ショップ
● イベントやキャンペーン
● 全体お知らせ
■ コンテンツの特徴
● 数が多い
● 更新頻度が高い
● 配信箇所が複数
● 配信期間がある
● 表示の優先度がある
● ユーザごとに出し分けが必要
○ OSやユーザの状態による
メルカリアッテの例
■ アプリ内バナー
● タイムラインにバナーを表示させている
● バナーをタップするとWebViewなどに遷移
● タブごとやユーザの状態によってバナーが変わる
6
バナーツール
■ バナーの入稿を管理するツール
● 式で配信条件を設定
● 1ソースで複数の環境に対応
7
バナーツール
・配信条件
・レスポンス
バナー取得
OS, APIバージョン などの変数
条件に当てはまるバナー
画像URL, 遷移先URL
アプリ
バナーツールの特徴と使われどころ
■ 複数のアプリで使える
● メルカリ、メルカリ アッテ、メルカリ カウル
● 個人環境も簡単につくれる
■ コンテンツ管理全般に使える
● バナーの管理
● 全体お知らせの管理
● その他、変化の激しいコンテンツの管理
8
デモ1:バナーの設定と配信
9
https://youtu.be/L61IaI-zqyk
バナーツール開発の背景と実践
10
これまでのバナー管理の問題点
■ PHPやTOMLファイルで管理されていた
● 実装は簡単だが運用が大変
● バナーを設定する度にエンジニアが必要
● どの設定が使われているのか分からない
11
[[banners]]
tab_id=2
url=https://example.com/campaign?id=10
image_url="https://example.com/campaing10.png
start_time=2016-10-27T08:00:00Z
end_time=2016-11-07T06:00:00Z
os="ios"
バナー設定ファイルの例
これからの課題になるであろう点
■ アプリごとに管理方法が違う
● 新規アプリは増える
● 案件特化になると使いまわせない
● 学習コストが高くなる
12
PHPで管理 TOMLで管理 新しい管理方法?
New!!
バナーツールに求められること
■ 非エンジニアが運用できること
● GUIで設定できる
● 設定の手順が少ない
● バリデーションがしっかり行える
■ 案件が増えても使えること
● 共通機能を提供する
● 案件ごとにうまくカスタマイズできる余地がある
● 案件ごとに環境をいくつも用意しなくて良い
■ 工数がかからないこと
● 短期間・少人数で開発できる
● アプリのリリースサイクルをブロックしない
13
バナーツールの構想
■ コアとなるエンジンは共通
● 複数のアプリで使えるようにする
● 出来る限り汎用的に作る
■ UIは案件に特化したものにしたい
● 案件固有の事情をUIで吸収
● 運用者が使いやすいUIを提供
● 最悪UIは案件ごとに作っても構わない
14
汎用性 v.s. 特化型
15
VS
100徳ナイフ
パイナップルむき機
使いやすいツールとは?
■ 用途に特化したツール
● 使用者が迷わない
● 余計な入力項目がない
● 学習コストが低い
16
汎用的なツールとは?
■ 使い回しが効くツール
● 案件が増えても対応できる
● カスタマイズしやすい
● 対応できる幅が大きい
17
使いやすくて汎用的なツールを作る
■ 表面と基盤で汎用性を分ける
● UIは案件特化
● コアとなる部分は汎用的にする
18
汎用的な基盤
使いやすいUI 使いやすいUI 使いやすいUI
配信条件の汎用化
■ 配信条件を式で書けるようにする
● os == "iOS" のように書ける
● 変数が使える
● 複雑な条件でも表現ができる
19
汎用的な基盤
条件式の評価エンジン
配信条件式の設定の問題点
■ 条件式の設定の難易度が高い
● バナーの条件はそんなに単純ではない
● 非エンジニアに入力させるのは大変
● 入力ミスがあった場合どうするのか?
20
os == "iOS" && (tab == 1 || tab == 2) && is_beginer
例:iOSユーザのビギナー向けにタブ1とタブ2にバナーを出す
条件式の保存
■ 条件式の構成要素を保存する
● 設定される条件の種類は多くない
● 多くの場合共通の条件の組み合わせ
● 予め使う条件の構成要素を保存しておく
● 選択式にする
21
os == "iOS" && (tab == 1 || tab == 2) && is_beginer
OSの種類 表示タブ ビギナー
UIの自動生成
■保存されている式からUIを自動生成する
● 保存されている式をUIで選択する
● ANDやORで組み合わせる
22
汎用的な基盤
条件式の評価エンジン
使いやすいUI 使いやすいUI 使いやすいUI
自動生成 自動生成 自動生成
開発コストの問題
■ 汎用的にするのはいいがいつできるのか?
● 条件式の評価器
● UIの自動生成エンジン
● どちらも重たい機能
23
開発コストの軽減
■ 条件式の評価器
● Goの式としてパースする
● Goの標準パッケージを利用
● イチから作る必要はない
■ UIの自動生成
● JSON Schema + JSON Editorの利用
● JSON SchemaからWeb UIを自動生成
● 式からJSON Schemaを生成する
24
デモ2:配信条件の設定
25
https://youtu.be/Xjpw9DOEe9g
案件ごとに異なる設定項目の吸収
■ レスポンスをJSON Schemaで定義
● JSON Schemaで予め記述
● JSON EditorでUIの自動生成
26
バナーツール
・配信条件
・レスポンス
バナー取得
OS, APIバージョン などの変数
条件に当てはまるバナー
画像URL, 遷移先URL
アプリ
デモ3:レスポンススキーマの設定
27
https://youtu.be/L61IaI-zqyk
運用コストの問題と解決
■ 利用案件の数は増えていく
● 案件が増える度に環境を用意する?
● 機能を追加する度に反映する?
● 案件間でバージョンが乖離していく
■ マルチテナント型にする
● 1ソース+2環境で運用する
● QA環境+本番環境
● GAEのNamespace APIを使う
● 案件ごとにデータだけ別に保存
28
デモ4:Namespaceの追加
29
https://youtu.be/S9r4_0nyo8o
汎用性の維持と機能拡張
30
機能拡張の課題
31
■ 機能拡張は必要になる
● 案件ごとに違う機能要件
● 影響範囲が大きい
● どういう機能を足していけば良いのか
■ 汎用性が壊れないようにする
● 特化しすぎた機能は作らない
● JSON Schemaでだいたい対応できる
● 要望のもう1つ下の基盤から作る
● 機能を最小にする
安全に機能拡張を行うには?
■ 機能追加は多くの案件に影響する
● 影響範囲を小さくする
● 破壊的な変更はしない
● できるだけ新規追加のみにする
● コアの部分には手を入れない
● 管理画面の機能拡張に留める
32
機能追加の動機づけ
■ どんな機能をいつ追加するのか?
● 利用者から要望のある機能を追加する
● 利用者の問題を解決する機能を追加する
● 利用者がテストしてくれる
● 親切心で100徳アーミーナイフは作らない
33
機能拡張の例
■ 一括アップロード
● 数百件の項目を入力したい
● 一気に入力できるようにしたい
● CSVをアップロードできる
● 複数の案件で利用できる
34
一括アップロードの実現
■ どう実現するのか
● CSVは1行で1件を表す
● JSONはオブジェクトの入れ子もあり得る
● 案件ごとにレスポンスの形式は違う
● JSON Schemaを元にCSVをパースする
35
url image.background image.main
http://example.com http://example.com/b.png http://example.com/m.png
{
"url": "http://example.com",
"image": {
"background": "http://example.com/b.png",
"main": "http://example.com/m.png"
}
}
バナーツールを支える技術
36
条件式と静的解析
37
配信条件式の評価
■ バナーの配信条件に条件式を用いる
● Goの式としてパースできる独自形式
● 型定義を式で表現できるようにしてある
38
バナー取得
GET /banner/?os=1
String(os) == "1"
バナー画像URL
配信条件:
バナーツール
goパッケージ
■ 標準パッケージとして静的解析の機能を提供
go/ast 抽象構文木(AST)を提供
go/build パッケージに関する情報を集める
go/constant 定数に関する型を提供
go/doc ドキュメントをASTから取り出す
go/format コードフォーマッタの機能を提供
go/importer コンパイラに適したImporterを提供
go/parser 構文解析の機能を提供
go/printer ASTの表示機能を提供
go/scanner 字句解析の機能を提供
go/token トークンに関する型を提供
go/types 型チェックに関する機能を提供
39
条件式評価器の実装
40
■ goパッケージを用いて式の評価器を作る
● go/parserパッケージで式のASTを取得
● ASTを再帰的に評価していく
■ 定数評価器をうまく使う
● go/constantパッケージを使う
● constant.Valueに変換する
==
"A" "B"
パース 定数評価
参考:goパッケージで簡単に静的解析して世界を広げよう
"A" == "B" false
定数評価器の利用
■ go/constantパッケージ
● 定数同士の演算ができる
41
constant.Valueに変換できれば、
簡単に演算をやってくれる
func BinaryOp(x Value,op token.Token,y Value) Value
例:2項演算を行う関数
型付き変数の実装
■ 式の中で型を表す
● 関数呼び出しを変数として扱う
● 大文字で始まる関数呼び出しのみ
● 引数は識別子のみでそれを変数名とする
■ 変数を評価する
● 式の評価時には値が決まっている
● 評価器から見ると名前付きの定数に近い
● constant.Valueにして評価
42
型 変数名
Int(n) == 10
型をバリデーションに利用する
■ バリデーションを行う
● JSON Schemaを使えばJSONがスキーマを
満たしているかチェックできる
● 変数に型を導入することで、型情報を基にス
キーマを定義できる
● 型に沿ったバリデーションができる
■ オリジナル型を作る
● 組込み型としてオリジナルの型を作れる
○ URL, IntRange など
● バリデーションも行える
43
ゼロ値をバリデーションに利用する
■ 式を評価するとboolになるか判定する
● 条件式中に変数があると評価できない
● 変数にはゼロ値がある
● 変数にゼロ値を設定してみて評価する
○ 評価結果がboolになればOK
44
Int(n) == 10
例1:
0 == 10 false
Int(n) + 10
例2:
0 + 10 10
関数の実現
■ 関数を使えるようにする
● 小文字から始まる関数呼び出しを対象
● 引数は任意の式
● オリジナルの組込み関数のみ使用できる
○ cond, until, hours, days,...
■ 関数呼び出しを評価する
● const.Valueを引数と戻り値に取る関数
● reflect.MakeFuncの思想と似ている
● 関数呼び出しごとにスコープを作る
○ 別の式を呼び出すcond関数のため
45
関数の例
■ 引数と戻り値をconstant.Valueにする
46
※スペースの都合上、エラー処理は省いてある
func add(args []constant.Value) []constant.Value {
x, _ := strconv.ParseInt(args[0].String(),10,64)
y, _ := strconv.ParseInt(args[1].String(),10,64)
return []constant.Value{
constant.MakeInt64(x + y),
}
}
例:足し算
条件式を保存して呼び出す
■ cond関数
● よく使う式をDBに保存しておける
● cond関数を使って呼び出せる
● 呼び出し時に変数に値を束縛できる
47
String(os) == Enum(__os, "0,1", "iOS,Android")
保存された式 (id:1)
cond(1,"__os=1")
呼び出す式
列挙型の変数
変数の値
呼び出す式のID
String(os) == "1"
展開した式
ASTの操作とあいまい検索
■ 条件式を検索する
● 変数の一部を与えてtrueになる式を検索する
● condを展開する必要がある
● 与えられなかった変数を使っている部分を無視して評価
する
48
&&
== ==
v1 1 v22
&&
== true
v1 1
&&か||がくるまで
上にのぼる
&&ならtrue
||ならfalseに置き換える
v1が与えられた場合
UIの自動生成
49
JSON SchemaとUIの自動生成
■ JSON Editor
● JSON SchemaからWeb UIを作る
● 保存されたデータからJSON Schemaを自動
生成してやればよい
50
※画像はJSON Editorのリポジトリより
条件式からUIを自動生成する
■ 保存された式を組み合わせる
● __で始まる変数をUIで設定する
● UIで設定した変数を束縛する形で
cond関数を用いてバナーの条件式とする
51
String(os) == Enum(__os, "0,1", "iOS,Android")
os: iOS ▼
Android
保存された式 (id:1)
cond(1,"__os=1")JSON Schema
バナーに設定される式
作業者は保存された条件を
ANDやORで組み合わせるだけで済む
UIが生成される 選んだ値を設定する
GAEによるマルチテナントな
Webアプリの構築
52
マルチテナント型のWebアプリとは?
■ マルチテナント型
● 1つのシステムを複数のユーザ(企業)に提供する
53
Datastore
mercari atte kauru
Memcache
mercari atte kauru
アプリ バナーツールGAEプロジェクト
mercari
atte
kauru
GAEでNamespace APIを使ってマルチテナント型を実現
Namespace APIとは?
54
■ Namespace API
● Namespaceを分けることのできるAPI
● 利用可能なAPI
○ Datastore, Memcache, Task Queue, Search
■ Namespaceを分ける理由
● Namespace間でデータの干渉を防ぐ
● マルチテナント型のWebアプリを作れる
URLとルーティングルール
■ GAEのURLは以下のようにアクセスできる
● Inst: インスタンス(数値)
● Ver: バージョン
● Serv: サービス
● AppID: アプリケーションID
■ ゆるいルーティング
● インスタンス、バージョン、サービスが存在しないとデ
フォルトのものにルーティングされる
● インスタンスorバージョンの代わりにNamespaceを書く
55
<Inst>-dot-<Ver>-dot-<Serv>-dot-<AppID>.appspot.com
<NS>-dot-<Ver>-dot-<Serv>-dot-<AppID>.appspot.com
マルチテナント型にしときの課題
■ Namespace間の設定の移行
● 開発環境を用意する際にほしい
● 移行するAPIを用意する必要がある
■ バグが出ると全滅する
● すべて同じソースコードを使ってる
● バージョンをうまく使って移行する
■ ローカルでのデバッグが面倒
● ローカルのコンソールのNamespaceの対応が貧弱
● MemcacheはNamespaceを指定できない
56
まとめ
● モバイルアプリとコンテンツ管理
○ モバイルアプリのコンテンツ管理は複雑
○ 共通化が難しい
● バナーツール開発の背景と実践
○ 汎用性と使いやすいツールの実現
● 汎用性の維持と機能拡張
○ 案件ファーストで破壊的な変更をいれない
● バナーツールを支える技術
○ 静的解析
○ JSON EditorによるUI自動生成
○ GAEによるマルチテナント型Webアプリ
57
Thank you!
twitter: @tenntenn
Qiita: tenntenn
connpass: tenntenn
58

静的解析とUIの自動生成を駆使してモバイルアプリの運用コストを大幅に下げた話