#denatechcon
#denatechcon
10年目の
『エブリスタ』
を支える技術
井田 祐太 / 松尾 卓朗
ゲーム・エンターテインメント事業本部IPプラットフォーム事業部サービス開発部
#denatechcon
開発期間約1年半
#denatechcon
2019年春 エブリスタは
生まれ変わります
#denatechcon
#denatechcon
10年目の
『エブリスタ』リニューアル
を支える技術
井田 祐太 / 松尾 卓朗
ゲーム・エンターテインメント事業本部IPプラットフォーム事業部サービス開発部
#denatechcon
自己紹介
- 井田 祐太
- @da_ponta
- https://note.mu/fablab
- 2017年4月中途入社
- エブリスタの開発全般を担当
- Perl
- Ruby
- node.js
- Android
- Kubernetes 普段はテラリウムなどものづくり全般やってます
#denatechcon
エブリスタとは
だれもが作家になれる
小説投稿サイト
#denatechcon
代表作品
#denatechcon
サービスの歴史
2007年3月 2010年6月 2019年春
モバゲー
小説コーナー
エブリスタ
オープン
システム
リニューアル予定
10年目突入!
#denatechcon
10年めのサービスの負債
- 誰も把握していない仕様
- コピペされたコード
- 独自フレームワーク
- 未知のテーブル、カラム
- 存在しないテスト、ドキュメント
- SJIS
- フィーチャーフォン
- 4,5種類存在するUI
...
#denatechcon
闇が深い…
作り直すしかない
#denatechcon
リニューアル対象:
DB・サーバサイド・インフラ・アプリ
UI・UX・フロントエンド・機能
≒システムすべて
#denatechcon
リニューアルに際しての課題
- エンジニアリソース(3〜4人😰)
- 機能数(コントローラ数換算:900, テーブル数:1000)
- 既存サービスの運用も継続しなければならない
- リリース後も高速な開発サイクルにしたい
#denatechcon
アプリケーション編
#denatechcon
アジェンダ
- GraphQLの活用
- kubernetesをフル活用した開発環境
#denatechcon
サービス群
MAIN API
GraphQL
COMICPAYMENTAUTH
FRONT
NOTIFICATION
USER
OPE
BOX
#denatechcon
GraphQLの採用理由
リリース後も開発サイクルを高速で回したい
#denatechcon
GraphQLのリニューアルにおける利点
- UI要件の変化への対応が不要
→開発速度UP
- ドキュメント作成が不要
→工数DOWN
- コードがクリーンに保てる
→工数増加防止
- モチベーション向上と2年間の維持ができた
→開発速度維持
#denatechcon
UI要件の変化への対応が不要
query($userId: ID!) {
user(userId:$userId) {
nickname
introduction
}
}
※画面は開発中のものです
😅<ここに作品をいれたいんだ
けど…
#denatechcon
UI要件の変化への対応が不要
query($userId: ID!) {
user(userId:$userId) {
nickname
introduction
novels {
title
updated_at
...
}
}
}
※画面は開発中のものです
#denatechcon
ドキュメント作成いらず
# 型定義
class ContestType < Types::BaseObject
# 型の名称
graphql_name "Contest"
# 型の説明文
description "公式イベント"
# 取得できるデータ
field :contest_id, ID,
"イベントID", null: false
end
# Queryの定義
class FindContest < Resolvers::Base
# 返す型
type ContestType, null: false
# クエリの説明
description "公式イベント"
argument :contest_id, ID,
"公式イベントID", required: true
...
end
#denatechcon
GraphiQL
# Queryの定義
class FindContest < Resolvers::Base
# 返す型
type ContestType, null: false
# クエリの説明
description "公式イベント"
argument :contest_id, ID,
"公式イベントID", required: true
...
end
#denatechcon
GraphiQL
#denatechcon
GraphiQLだと少し足りない
query($userId: ID!) {
user(userId:$userId) {
nickname
introduction
novels {
title
updated_at
...
}
}
}
novelsはどの型が持
っているんだ?
#denatechcon
GraphQL Voyager
#denatechcon
Railsあるある(?)
- アソシエーションとビジネスロジックでファットなモデル
- ビュー処理とモデルの呼び出しでファットなコントローラ
▶ graphql(-ruby)だとスッキリ!
#denatechcon
エブリスタにおけるアーキテクチャ
models モデルに閉じたビジネスロジック
policies 権限の判定ロジック
repositories DB以外も含めた外部リソース操作ロジック
services 何らかの一連のビジネスロジック
graphql コントローラ or ViewModel
#denatechcon
Modelの関連をGraphQLが吸収
- 返却するデータ構造の構築が不要
- model・controllerに複雑なロジックを書く必要がない
- 意識せずにテーブルと対応したnull安全なデータに
# User has a novel
class UserType
field :novel, NovelType,
null: false
...
end
# Novel belongs to user
class NovelType
field :user, UserType, null: true
...
end
#denatechcon
モチベーションについて①
- Github、Facebookが採用
- 採用事例増加中
- 新しい技術は楽しい
#denatechcon
モチベーションについて②
開発効率が良くないと乗り切れない!!
- プロジェクト期間:2年
- エンジニア数:3〜4人
- 機能数:コントローラ数ベース:900
- テーブル数:1000
- 作りながら要件は変化・増加する
#denatechcon
わずか2,3行でAPIにデータが追加される
class Types::UserType < Types::BaseObject
description "ユーザ"
...
field :followers, Types::UserType.connection_type,"フォロワー",
null: false, connection: true
def followers(**args)
@object.follower_users
end
end
class User < ApplicationRecord
self.primary_key = :user_id
has_many :followers
has_many :follower_users,
through: :followers
...
end
#denatechcon
②novel経由で取得するクエリ
①user経由で取得するクエ
リ
#denatechcon
ハマりどころも
- エラーハンドリングはどう行うか?
- フロントエンドから無制限にデータを参照されるのでは?
- パフォーマンス面(N+1問題など)
#denatechcon
デフォルトのエラー形式
query {
user(userId:"存在しないID") {
nickname
}
}
{
"data": null,
"errors": [
{
"message": "User not found.", # システムメッセージ
"locations": [ # クエリにおける発生行
{
"line": 2,
"column": 3
}
],
"path": [ # クエリにおける発生階層
"user"
],
}
]
}
→メッセージとエラーの発生した階層だけなのでクライアントでのハンドリングが困難
#denatechcon
graphql-errors + エラーカスタム
class ApiError < GraphQL::ExecutionError
def initialize(message, type = nil)
@type = type
super(message)
end
def to_h
super.merge("type" => @type)
end
end
graphql-errors https://github.com/exAspArk/graphql-errors
GraphQL::Errors.configure(schema) do
rescue_from ActiveRecord::RecordNotFound do |e|
raise ApiError.new(“#{e.model} not found”,
"NotFound")
end
# 独自エラークラス
rescue_from Errors::PermissionDenied do |e|
raise ApiError.new(e.message, e.class.to_s)
end
end
#denatechcon
拡張されたエラー形式
query {
user(userId:"存在しないID") {
nickname
}
}
{
"data": null,
"errors": [
{
"message": "User not found.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"user"
],
"type": "NotFound"
}
]
}
→パターン化された`type`によりエラーハンドリングが可能に
#denatechcon
クエリの実行制限
{query: “findNovel.gql”}
#denatechcon
開発環境
#denatechcon
本番と最小限の差異の開発環境
- minikubeを使ったローカルkubernetes
- helmによるテンプレート化
- ドメインの割当
- SSL化
#denatechcon
kubernetesを活用した開発環境
- minikubeを利用
- minikubeからNFSマウント
- minikubeからk8sにコード共有
#denatechcon
podへのソースコードマウント(抜粋)
$ sudo nfsd start
$ IP=$(minikube ip | awk -F"." '{print $1"."$2"."$3".1"}')
$ minikube ssh -- sudo mount 
-t nfs $IP:/path/to/src /src 
-o rw,async,noatime,rsize=32768,wsize=32768,proto=tcp
containers:
- name: api
image: "api:v1"
volumeMounts:
- mountPath: "/opt/api"
name: "api-volume"
volumes:
- name: "api-volume"
hostPath:
path: "/src/api"
その他の手法
Tilt,Skaffold:ビルドが発生するのでその分の遅延有り
Docker for Mac:マウントするファイル数が多くなると参照エラーが発生し断念
Dockerイメージは/opt/apiを
WORKDIRに指定します
#denatechcon
Helmによるテンプレート化(抜粋)
- レプリカ数
- Dockerイメージのリポジトリ
- 起動コマンドの引数
- configMapの数、種類
spec:
replicas: {{ .Values.replica }}
template:
spec:
containers:
- name: estar-api
image: “{{.Values.REPO}}/api:latest"
args: [
{{- range .Values.args }}
"{{ . }}",
{{- end }}
]
envFrom:
{{- range .Values.args }}
- configMapRef:
name: {{ . }}
{{- end }}
#denatechcon
まとめ
- エンジニアリソースの課題
→ GraphQLにより実装が簡易化されエンジニア体制が最適化
→ minikubeにより環境整備の工数削減
- 900の機能数
→ GraphQLによりAPI開発数の減少(150クエリに)
- リリース後も開発サイクルを高速で回したい
→ フロントエンド主導の開発プロセスが実現
#denatechcon
❕注意事項
minikubeを利用するには高スペックなPCが必要です
必要要件(エブリスタチームの場合)
- memory 16GB
- クアッドコア
#denatechcon
クラウド活用とサービス開発
#denatechcon
自己紹介 松尾卓朗
•2017年新卒 DeNA入社(2年目)
• IPPF事業部
• サーバーサイド、クラウドを担当
#denatechcon
自己紹介
気づいたら、DevOpsの魅力に惹
かれる
• SRE
• カオスエンジニアリング
• CI/CD
#denatechcon
役割の分割
コミック
Web小説サービス
小説
認証
通知
課金
コミック
スゴ得
コンテンツ
ステークホルダーとのサービス
#denatechcon
役割の分割
コミック
Web小説サービス
小説
認証
通知
課金
コミック
開発進めたいのはココ
スゴ得
コンテンツ
ステークホルダーとのサービス
#denatechcon
サービス群
MAIN API
GraphQL
COMICPAYMENTAUTH
BFF
NOTIFICATION
USER
OPE
BOX
#denatechcon
モノリスとサービシーズ
MAIN API
GraphQL
COMICPAYMENTAUTH
FRONT
NOTIFICATION
OPE
BOX開発者は小説の開発に
集中できる。
モノリス
サービシーズ
#denatechcon
クラウド
#denatechcon
今までの運用体制
サービスエンジニア
インフラエンジニア
インフラは
全てお任せ
#denatechcon
これからの運用体制
サービスエンジニア
インフラエンジニア
インフラはほぼ全て
事業部エンジニアが
#denatechcon
ですが
#denatechcon
リソースが足りない、、、
事業部エンジニア
● アプリケーション開発
● インフラ構築
● ネイティブアプリ開発
#denatechcon
最小限の工数でクラウドを活用する。
• インフラの管理コストを削減
• アプリケーションエンジニアがクラウド業務を
兼務できるように
• 実行環境差分を少なくする
#denatechcon
クラウド環境
Amazon
SES
Amazon
ElasticSearch
Service
#denatechcon
Amazon
SES
Amazon
ElasticSearch
Service
クラウド環境
kubernetes が分かっていれば、
クラウド環境も理解できる
を目指す
#denatechcon
最小限の工数でクラウドを活用する。
• インフラの管理コストを削減
• アプリケーションエンジニアがクラウド業務を
兼務できるように
• 実行環境差分を少なくする
#denatechcon
クラウドの管理
GCP
• メモリー
• CPU
• アプリケーションサービスのノード数
• インスタンスのライフサイクル
• ロードバランサ
• ネットワーク周り
• Cluster nodeの管理
• 各種ミドルウェア
#denatechcon
Infra as a codeはどこまで?
すでにcode化されている
code化しない
GCP
• メモリー
• CPU
• アプリケーションサービスのノード数
• インスタンスのライフサイクル
• ロードバランサ
• ネットワーク周り
• Cluster nodeの管理
• 各種ミドルウェア
#denatechcon
マネージド以外は使わない
Prometheu
s
prometheus
#denatechcon
最小限の工数でクラウドを活用する。
• インフラの管理コストを削減
• アプリケーションエンジニアがクラウド業務を
兼務できるように
• 実行環境差分を少なくする
#denatechcon
アプリケーションエンジニアのインフラ兼務
Local
MiniKube
GKE GCP
Production
…etc
この差分をカバー
さえすれば、
ほぼ運用できる。
2 1.5学習コスト比
#denatechcon
クラウド環境
• コンテナ
• ログの収集
• 監視
• ネットーワーク
• デプロイ
• データコンバート
#denatechcon
コンテナ
●1コンテナ1プロセス
●プロセスの管理を抽象化
●Dockerの管理= Railsの管理
●各環境でも同じコンテナ
●Ubuntu ベース
#denatechcon
Pod(kubernetes)
#denatechcon
ログの収集
• Cluster nodeのメモリ,CPU ..etc
• コンテナのメモリ,CPU ..etc
#denatechcon
外形監視
• 基本はすべて、
stackdriverで監視を行う
• 外形監視のみmonitis
監視
#denatechcon
GCP ネットワーク
USER
Cloud NAT
#denatechcon
CI/CD
Deploy Server
デプロイ
DeNA
#denatechcon
データのコンバート
onPremis
Mysql
Perl
GCS転送スクリプト
JSON
gsutil
rsync
GCS受信スクリプト
受信 処理済み
Rubyコンバート
スクリプト
gsutil
rsync
差分
#denatechcon
2019年春
新しいエブリスタ始まる
#denatechcon
#denatechcon

10年目の『エブリスタ』を支える技術