GameWith Developer Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

GuardDuty(とSecurity Hub)で始めるセキュリティの第一歩 #GameWith #TechWith

初めまして、GameWith のインフラ担当 加我(id:damenaragyouza)です。
GameWith Advent Calendar 2018 の2日目を担当させて頂きます。

ここ最近、外部からの不正なトラフィックによる攻撃が流行しています。
ユーザにサービスを安心して使って頂くために弊社もちゃんとセキュリティを考えていく必要があると感じ、現状の課題と対策を検討しました。

対象となる読者

  • セキュリティ対策を何から始めようか迷ってる方
  • チームにセキュリティの専任がいない状況で開発を行っている方

gamewith.jp におけるセキュリティ上の課題

弊社のゲーム攻略メディアである gamewith.jp は2013年9月にオープンしました。
特定の時間帯でトラフィックが跳ね上がるというサービス上の特性に対し、AWSを活用する事で高トラフィックな環境に対応してきました。

その反面、セキュリティに対しては十分な対策を行えていなかったという事情があり、現時点でどのような課題があるかを調べてみることにしました。

技術的な課題

  • 外部からの不正なトラフィックを検知する仕組みがない
    • ポートスキャンやSSHブルートフォースアタック
    • DDoS攻撃
    • 設定不備によるEC2のインターネット公開状態
  • 内部から外部に対する不正なトラフィックを検知する仕組みがない
    • 外部に向けてのDDoS攻撃やSSHブルートフォースアタック
    • ビットコインのマイニング
  • AWSアカウントに対する不正操作を検知する仕組みがない
    • Credentialの漏洩
    • 各種AWSサービスのAPI不正利用
    • AWS Management Consoleへの不正ログイン

技術的な観点では上記の課題が見えてきました。
これはしっかりと対策をする必要があります。

少し観点を変えてみます。
人的リソースという点でどのような課題があるのかを考えてみました。

組織的な課題

  • インフラの専任は自分のみ
    • そもそもこれまでずっとインフラの専任は不在だった
  • セキュリティの専任はずっと不在である

つまり、インフラとセキュリティに対してガッツリ対策ができる専任のリソースが無かったという状況が続いていました。こういった状況を改善すべく、どのような対策が可能で、より効果的なのかを考えていきます。

理想は「運用に私のリソースをそこまで費やす事なく、いい感じに検知してくれるような都合の良いサービス」です。まぁこんなの無いだろうなぁと思ってたんですがありました。

選ばれたのはAmazon GuardDutyでした

www.slideshare.net

Amazon GuardDutyは AWS環境におけるセキュリティの驚異リスクを検知するAWSマネージドサービス です。
分析する情報として下記の3点が挙げられています。

Amazon GuardDutyの分析ソース

  • VPC Flow Logs
  • AWS CloudTrail Event Logs
  • DNS Logs

上記のソースを機械学習で分析し、脅威(異常)を検知する仕組みとのことです。
Amazon GuardDutyを使用する事で弊社が抱える技術的な課題にアプローチが可能ではと考えました。

VPC Flow Logsからは 外部からの不正なトラフィックを検知内部から外部に対する不正なトラフィックを検知 を、
AWS CloudTrail Event Logsからは AWSアカウントに対する不正操作の検知 を期待します。

導入してみて「やっぱりダメでした」という事もありえるため、30日間のフリートライアルがあるのは個人的に好印象です。

導入

AWS Management ConsoleからAmazon GuardDutyのコンソールに飛び「Get started」をポチるだけです。とても簡単。

f:id:damenaragyouza:20181113155855p:plain
Get started

導入した結果

意図せず外部との通信が可能になっていたSecurity Groupが適用されているEC2インスタンスに対し、外部からポートスキャン(PortProbeUnprotectedPort)が多々行われている事が判明しました。
この結果を踏まえ、Security Groupの全体的な見直しを実施することでGuardDutyが検知した脅威に対する対応ができました。Trusted Advisorを併用するとより効率的です。

f:id:damenaragyouza:20181113170008p:plain
現在はAmazon GuardDutyが検知した脅威をSlackに通知することで運用コストの軽減に成功しています。

[11/29 追記] AWS Security Hubについて

re:Inventで新サービス AWS Security Hub が発表されました!既に東京リージョンでもPreview版が利用可能です。
aws.amazon.com
AWS Security Hubは各セキュリティサービスを一括で管理する事ができるサービスで、文字通りHubとして機能してくれます。
先に導入したAmazon GuardDutyもSecurity Hubから管理する事が可能ですので、今後は全てこちらで管理・操作することになりそうです。

Security Hubの設定ですが、こちらも非常に簡単です。
SecurityHub ConsoleからEnable Security Hubを選択するだけです。
f:id:damenaragyouza:20181129140850p:plain
設定を有効にする際、いくつかのPermissionが必要になるので、内容を確認した上で有効にします。 f:id:damenaragyouza:20181129141149p:plain
StandardsではCIS AWS Foundationsの結果を確認することができます。
設定は簡単で、Enableを選択するだけです。(注意書きを見る限りだと、All resourcesに対してAWS Configを有効にする必要があります)
f:id:damenaragyouza:20181129151926p:plain
Insightsを見ると、様々な観点から問題を報告してくれます。
この観点の中には弊社がこれまで出来ていなかったセキュリティに対する技術的な課題へのアプローチ(Credentialの漏洩やEC2の設定不備/脆弱性)も含まれていました。
GuardDutyと併せて強力にサポートしてくれる事間違いなしです。

f:id:damenaragyouza:20181129142147p:plain
Insightsの一部

コスト

現在トライアルの最中なので最終的なコストはまだ出ていませんがコレくらいです。
この金額は月額のように錯覚しますが日額です。比較的高めではありますが、とても強力かつお手軽なため、トライアル終了後も継続して利用する事にしました。
f:id:damenaragyouza:20181127115730p:plain

終わりに

タイトルに第一歩と書いた通り、Amazon GuardDutyの導入はあくまで「脅威の検知」が目的です。
今後は脅威を未然に防ぐための仕組みづくりや、よりセキュアな運用方法を考えていく必要があります。

このような課題を一緒に考え、一緒に解決していってくれる仲間をGameWithはお待ちしております。

www.wantedly.com

参考資料

Amazon GuardDuty(インテリジェントな脅威検出) | AWS
20180509 AWS Black Belt Online Seminar Amazon GuardDuty
AWS Security Hub | Amazon Web Services (AWS)

GameWithアプリに、iOS12の新機能 Provisional Authorization (お試しプッシュ通知)を導入してみたら通知許諾率が下がった話

どうも皆さんおはこんばんにちは。

スコルパイド3討伐済みのiOSアプリエンジニア @peka3 です。

前回から約3ヶ月ぶりの投稿となります。

先日、GameWithアプリのプッシュ通知許諾率改善施策として、iOS12の新機能 Provisional Authorization(お試しプッシュ) を実装したんですが、 改善されるどころか許諾率が下がってしまいました。

なぜそうなったのか、Provisional Authorization を紹介しつつ原因を深掘りします。

前提

  • GameWithアプリではログインユーザーに向けてプッシュ通知を行っている(当時)
  • 通知許諾率が低い
  • 開封率は悪くないので、通知の質はどうやら問題なさそう

Provisional Authorization とは?

簡単にいうと、ユーザーがお試しでプッシュを受け取れるようになる機能です。

今まではユーザーがアプリの通知許諾ダイアログで「OK」を選択すると、プッシュ通知が受け取れるようになっていたのですが、 Provisional Authorizationでは通知許諾ダイアログを表示することなく、お試しのプッシュを受け取れるようになります。

Apple的には、どんな通知が受け取れるかわからない状態で通知許諾を出されても、ユーザーの混乱を招くということで実装された機能のようです。

参考: 動画の30:00〜

developer.apple.com

これが通知許諾率改善に効果があるのでは!?ということで検証することになりました。

お試しプッシュ通知の制約

通知を受け取っても、

  • 音がならない
  • バッジが表示されない
  • 受信時のアニメーションはない
  • 通知センターを見ることで初めて届いてることが確認できる

という制約があります。

実装方法

すでにプッシュ通知を実装しているアプリであれば簡単に実装できます。

既存の requestAuthorization を実行しているところで option に .provisional を含めるだけです。

var options: UNAuthorizationOptions
if #available(iOS 12.0, *) {
    options =  [.alert, .badge, .sound, .provisional]
} else {
    options =  [.alert, .badge, .sound]
}

UNUserNotificationCenter.current().requestAuthorization(options: options) { granted, error in
   if granted {       
        DispatchQueue.main.async {
            UIApplication.shared.registerForRemoteNotifications()
        }
    }
}

これによって通知許諾ダイアログが出ることなく、デバイストークン が発行されます。

自サーバのDBにデバイストークンを格納してる場合は、レコードが爆増すると思うので注意が必要です。

お試しプッシュ通知受信後

お試しプッシュを受信した状態で通知センターを開くと、以下のような通知が確認できます。

f:id:peka3:20181127120332p:plain:w200

通常のプッシュ通知とは違い、残す(通知をこのまま受け取り続ける)、それともオフにするかのボタンが存在します。

残すを選択した場合は、さらに

f:id:peka3:20181127120622p:plain:w200

  • 目立つように配信してもらうか(ロック画面 通知センター バナー全部出し サウンドとバッジもあり)
  • 目立たないように配信してもらうか(通知センターのみ かつ サウンドとバッジはなし)

をユーザが選択できるようになっています。

これを選択し終えると、ようやくauthorizationStatusauthorized(通知許諾ON状態)となります。

導入後、どういう通知許諾フローにしたのか

当時のGameWithアプリでは、ログインユーザーのみがプッシュ通知を受け取れる仕組みでした(現在は未ログインユーザーもプッシュ通知を受け取れる)。

そのため、アプリ起動時にログイン訴求を表示していました。

f:id:peka3:20181127114759p:plain:w200

それを踏まえて、訴求表示でのボタンを押したときのフローを以下のように変更しました。

  • アプリを初めてDLした人がログイン訴求を見てログインする

    • 今まで: ログイン後に通知許諾ダイアログを出す
    • これから: 今までと同じ
  • アプリを初めてDLした人がログイン訴求を見て「あとで」を選択し、その後どこかのタイミングでログインする

    • 今まで:ログイン後に通知許諾ダイアログを出す
    • これから: ダイアログなしでprovisional にする

このような実装を行ったのですが、

結論としてはプッシュ通知許諾率は向上せず、むしろ下がってしまいました。

なぜ通知許諾率は向上しなかったのか?

前述した制約がやはり大きかったようで、自分から通知センターを見るユーザーが全然いないのではないか?と思われます。

お試しでプッシュ通知を受信できると言っても、気づかれなければ意味がなく、このお試しプッシュ機能はユーザーの邪魔をしないように通知を送るので、結果として通知が届いていてもほとんどの人は気づかずに終わっているようです。。

どうすればもっと通知許諾率が向上していたか?

検証してないので仮説にすぎないのですが、

  • provisionalステータスのまま一定期間が過ぎたユーザーには、通知許諾ダイアログを出す
  • ユーザーがお試しプッシュを受け取って通知センターを見るところまで、アプリ側でチュートリアルとしてサポートする

このあたりまで行えば効果があったかもしれません

闇雲にProvisional Authorizationを実装するだけでは、通知許諾率改善に貢献するのは難しそうです

まとめ

iOS12からの新機能を取り入れてみたのですが、今回のやり方ではうまくできませんでした。

ですが、仮説検証できたという点では大きな収穫でした。

これからもGameWithアプリではこのような仮説検証をどんどん行って、ユーザーの皆様がより満足できるアプリにしていきます!

一緒にやっていっていただける仲間も募集中です!

LINE DEVELOPER DAY 2018 に参加してきました #LINEDevDay #GameWith #TechWith

id:syque です。 2018/11/21 に開催された LINE DEVELOPER DAY 2018 に参加してきました。

linedevday.linecorp.com

f:id:syque:20181121114223j:plain

(ブラウン君割れてた、すまない)

以下の発表を聴講しました:

  • Opening Session - "Next LINE" LINEが創る新たな世界 -
  • LINEが目指す理想の広告プラットフォーム
  • (Lunch Session) パネルディスカッション: グローバルな組織で働くエンジニアから見たLINEのエンジニアカルチャー
  • LINEのインフラプラットフォームはどのように大規模サービスをスケールさせ運用コストの小さなインフラを提供しているのか
  • フロントエンド開発によって進化するLINEの未来
  • (Closing Session) LINEが創る理想のDeveloper Relations

今回はLINEが取り組んでいる技術と、会社やエンジニアチームがどのようにプロダクトに取り組んでいるかということに全体的に触れてみたいという思いからの参加でした。

この先1年で注力していく分野(AI、Blockchain、Fintech)と対応する現行・開発中のプロダクトについて、またそれぞれのプロダクトを取り巻く課題とそれを解決する技術についてが各々のプレゼンで補完的に発表されており、LINEという会社が取り組んでいく事項について大枠から知ることができました。*1

エンジニアが2,100人もいる(3,000人に増やしたいらしい)LINE社と比較すればエンジニアリングで取り組める分野、領域、そして特に規模は大きく異なってしまうとは思います。ですが取り組むべき分野に焦点を絞って組織編成をし、組織を拡大し、新たに表出した課題と向き合って解決策を考えて...といったサイクルを回すという点では一緒であるし、見習うべき点はたくさんあるのではと考えながら聴講していました。

発表内容では以下のようなフレーズ、事柄が印象的でした:

  • LINE Pay の人が会議中に娘から LINE 電話が来て、昼間から何だろう事故か何かか...と思ったら「買い物するので LINE Pay で 5,000 円送って」と。大したことない内容でよかったというのと、電話が終わって会議に戻るまでの間に送金はできてしまって、スマホだけで簡単に送金できる世界が来たなという実感が出たという話
  • 広告プラットフォームの方。広告の配信システムを運用するにあたって「今度こういう広告打つので数万リクエスト増えるけどよろしく」という話が来た時に、インフラどうしようトラフィックは大丈夫かという事項が社内のインフラチームにすぐ相談できる状態だったのはすごく助かった。安心して施策に素早く対応することができていた。
  • アプリは当然のこと、インフラ、フロントエンド、コンテンツデリバリーなど色んなものを独自フレームワークや内部サービス等に内製して運用している
    • (感想: LINE レベルの規模になると既存のサービス・ライブラリでは対応できないので仕方なさそう)
  • LINE の基本理念: Take Ownership, Be Open, Trust and Respect リンク
  • LINE は外国籍のエンジニアが6割
  • 役員がカフェスペースに集まってタウンホールミーティングを行ったりする。厳しめの公開質問が飛んできたりもする
  • 今回の DEVELOPER DAY 含め、ビジョンをエンジニアに伝える施策については CTO が時間を割いて考えている。2,100人というエンジニア全てに接触することはもちろんできないが、全員が自分たちで考えて動けるようになるために共通のビジョンを持つことが必要不可欠なので、それだけ優先すべき事項であると認識して取り組んでいる

LINE 社主催であるゆえの、事業・プロダクトの大きな視点から各々のプロダクトにまつわる細かな技術についてもあり広さも深さもあるカンファレンスだったなという感想でした。 弊社もこのくらい技術発信、プロダクト発信できるといいなと思いました(小並感)。スポンサーシップなどできるところから発信を進めて行きたいです。

最後に

GameWithは、ゲームが大好きで、新しい技術をどんどん使っていきたいという方を大募集中! Wantedly でもよいので是非お気軽にお声がけください!

www.wantedly.com

*1: もちろん内容は発表のためにブラッシュアップされているので、表からは見えない苦労や問題点などもあるのでしょう...

Firebaseをプロダクトに採用して得た知見 #GameWith #TechWith

こんにちは、GameWith iOSエンジニアの@kyamです。

前回こちらのiOSアプリの開発記事を書いて以来の執筆となります。 tech.gamewith.co.jp

今回のブログ内容と関連するので是非一読していただけると今回の記事がスムーズに読めるかと思います。

なぜFirebaseの記事を書こうと思ったか

Firebaseに関する記事は巷にたくさんあると思いますが改めて記事を書こうと思った理由として、主に2つあります。

  • 記事を書く際に改めてブログで扱う技術について再調査する事が多く、知らなかったことを見つけ、実際に既存のプロダクトに活かすという事がよくある
  • 社内で新規プロダクトや既存プロダクトにFirebaseを採用する事例が増えているので、社内に向けても知見共有になれば良いなと思った

上記の理由からブログを書くことにしたのですが、今回の構成としてはiOSとWeb(管理画面)の開発でそれぞれ取り入れたFirebaseのサービスを順に簡単に紹介しつつ、採用して良かった点や、苦労した点、今後より最適化していきたい点などをまとめていきたいと思います。

前回のブログを書いてから今日までプロダクト改善や新規施作のためにいくつかのFirebaseが提供している機能を活用しました。 それぞれ順に説明しつつ、簡単に実装例などを紹介します。

RemoteConfig

Firebase Remote Config  |  Firebase

一つ目の紹介です。

Firebaseの管理画面でサービス側パラメータ値を変更することで、アプリのデフォルトの動作や外観を変更することができます。内容にもよりますが基本的にアプリ起動時にパラメータ値のfetchを行います。

キャッシュの期間なども設定できますが、開発中はすぐさま管理画面上でのパラメーター値の変更をアプリ側で検知したいため isDeveloperModeEnabled を有効にするのがオススメです。

        config = RemoteConfig.remoteConfig()
        config.configSettings = RemoteConfigSettings(developerModeEnabled: false)
        let expirationDuration = config.configSettings.isDeveloperModeEnabled ? 0 : 3600
        config.fetch(withExpirationDuration: TimeInterval(expirationDuration)) { (status, error) in
            if status == .success {
                self.config.activateFetched()
            } else {
                if let e = error {
                    SBLog.error(e)
                }
            }
        }

A/B Testing

Firebase A/B テスト

A/B Testを作成する事ができる機能です。
上記で紹介したRemoteConfigを用いて、どちらのパターンがサービスにとって最適かを継続率や独自に設定したイベントを元に計測する事ができます。

Mippleでは動画の再生に到達するまでに二つのUIフローを用意し、どちらがより再生されるか、継続率の数値が良いかなどを検証することにしました。

    func checkMenuUIStatus() -> Bool {
        let showNewMenu = config[showNewMenuUIKey].boolValue
        if showNewMenu {
            Analytics.setUserProperty("新UI", forName: UserProperty.MENU_UI.rawValue)
        } else {
            Analytics.setUserProperty("旧UI", forName: UserProperty.MENU_UI.rawValue)
        }
        return showNewMenu
    }

ユーザープロパティをセットしてあげると予めセットしてあるイベントに対しても比較することができるので極力セットしたほうが良いです。 Firebase側は下記の添付画像のように自動でテスト結果を表示してくれます。(※数字は伏せています)

f:id:keeetaka:20181107132822p:plain

f:id:keeetaka:20181107135436p:plain

いつでも比率を増加させる事ができるので、AとBのテストの途中で明らかにBの方が良い数値であれば、アプリのアップデートを待たずしてBを100%のユーザーに適用する事なども管理画面上から実行できます。 RemoteConfigと組み合わせると本当にあらゆるテストが簡単にできるので気軽にエンジニアにこれテストできない?と聞いてみましょう。

Dynamic Links

簡単にディープリンクを作成する事ができるサービスです。
ダイナミックリンクを開くと、ネイティブアプリのリンク先のコンテンツに直接移動し、アプリがインストールされていない場合にはストアページに飛ぶといったものです。 管理画面上でリンクを発行することもできるのですが、Mippleではアプリ内で動的にリンクを作成しています。

Mippleで想定したユーザーの使い方は、

  • ユーザーが動画をシェアしようとする(その際に動的にアプリ内でダイナミックリンクを作成)
  • 実際にシェアされたリンクを別のユーザーがクリック、既にアプリをインストール済みのユーザーは再生画面が開き、そうでないユーザーはストアページに飛ぶ

といったものです。

extension DynamicLinker {
    func createDynamicLink(video: Video, completion: ((_ withUrl: URL?) -> Void)?) {
        guard let link = URL(string: "https://mipple.page.link/\(video.videoId)") else { return }

        let dynamicLinksDomain = "mipple.page.link"
        let linkBuilder = DynamicLinkComponents(link: link, domain: dynamicLinksDomain)

        let iOSParameters = DynamicLinkIOSParameters(bundleID: bundleID)
        iOSParameters.appStoreID = appStoreID
        linkBuilder.iOSParameters = iOSParameters
        
        let otherPlatformParameters = DynamicLinkOtherPlatformParameters()
        otherPlatformParameters.fallbackUrl = URL(string: "https://itunes.apple.com/jp/app/id1406664366")
        linkBuilder.otherPlatformParameters = otherPlatformParameters
        
        let socialMetaTagParameters = DynamicLinkSocialMetaTagParameters()
        socialMetaTagParameters.title = video.videoTitle
        socialMetaTagParameters.descriptionText = video.videoDescription
        if let thumbnailUrl = URL(string: video.videoThumbnailUrl) {
            socialMetaTagParameters.imageURL = thumbnailUrl
        }
        linkBuilder.socialMetaTagParameters = socialMetaTagParameters
        
        let navigationInfoParameters = DynamicLinkNavigationInfoParameters()
        navigationInfoParameters.isForcedRedirectEnabled = true
        linkBuilder.navigationInfoParameters = navigationInfoParameters
        
        guard let longDynamicLink = linkBuilder.url else { return }
        
        let options = DynamicLinkComponentsOptions()
        options.pathLength = .short
        linkBuilder.options = options
        
        linkBuilder.shorten { (url, warnings, error) in
            if let shortUrl = url {
                completion?(shortUrl)
            } else {
                completion?(nil)
            }
        }
    }

開発途中のコードになりますが、流れはコードの通りです。
渡された Videoオブジェクトからシェアの際に必要な情報(リンクを作成するのに必要なVideoIDやシェアされた時に表示されるサムネイル情報など)を抜き取り最後に短縮URLにするといった内容です。

ただ、実際には該当するWebページが必要で(Mippleの例でいうとWebの再生画面)、その画面でアプリで開くボタンなどを押した時にアプリ内の該当画面に飛ばすというのが望ましい挙動ですので、一旦Webを用意するまで上記の機能は封印しています。

Cloud Messaging

簡単にPush機能を入れる事ができるサービスです。
配信比率や配信先のグループ(RemoteConfigの対象値で振り分けなど)を指定できるので効果的に運用できれば良さそうです。 Pushサービスは他にもたくさんあるのですが、FirebaseSDKをサービスに導入しているのであれば使って見ましょう。

カスタムデータも送信できるので、アプリ側で値を見てアプリ内の該当ページにディープリンク的に飛ばすなども無料でできます。

Authentication

Firebase Authentication  |  Firebase

管理画面のログイン管理に用いています。 管理画面では任意のメールアドレスとパスワードでのアクセスにしていますが、SNSログインなどにもFirebase Authentication自体は対応しています。

firebase.auth().signInWithEmailAndPassword(email, password).catch(function(error) {
  // Handle Errors here.
  var errorCode = error.code;
  var errorMessage = error.message;
  // ...
});

Database

動画データの管理に Cloud Firestore を採用しています。

今まではアプリ側でYoutubeAPIを叩きMippleアカウントから動画を引っ張ってきていたのですが、今後機能追加などを検討していく中でYouTubeAPIだけでは必要な動画情報が足りないということになりました。 そのため独自にDBを持ち、様々動画データを管理する必要性が出てきました。

AWSのRDSを使って・・API作って・・など色々検討したのですがインフラの構築、管理、API設計などスケールを気にしながら諸々を全部作るのは今のサービスのフェーズ的には時間的にもコスト的にも現実的ではないなと感じました。

そこで以前から興味のあったFirestoreを採用することにしました。 このためFirestoreのDBに動画データを登録し、登録されたデータを確認するための管理画面が必要になったため作成したという流れです。

f:id:keeetaka:20181107182301p:plain

管理画面に関しては Vue.js + Firebase の構成で作ったのですが、なぜ Vue.jsとFirebaseを採用したのかに関してはこちらのFirebase Advent Calendar*1で紹介する予定です!

話は戻り、Firestoreでは単純なクエリ・ソートは可能なものの、複雑なものはできないというのをよく見かけたため、まずデータ構造の持ち方を決め、実際の運用の際にどのようなクエリやソートを行うかを検討し実際にテストしました。

ところがtimeStampでデータをソートしようとしたところデータが取得できないという問題に遭遇しました。

f:id:keeetaka:20181107164211p:plain (一瞬絶望する様子)

2つ以上の別々のフィールドを組み合わせてソートする場合は予めインデックスを作成しておかないと機能しません。 結果的に問題なくプロダクトに使う事ができたのですが、自分たちのサービスでしたい要件をFirestoreが満たすことができるかは投入前に確認するべきだなと感じました。

実装例

動画を日付順に並び替えて10件ずつ取得するケースだと以下のようになります。

        let db = Firestore.firestore()
        var query: Query!
        if dataSource.isEmpty {
            query = db.collection("videos")
                .order(by: "postedDate", descending: true)
                .limit(to: 10)
        } else {
            query = db.collection("videos")
                .order(by: "postedDate", descending: true)
                .start(afterDocument: lastDocumentSnapshot)
                .limit(to: 10)
        }
        isLoading = true
        query.getDocuments { (snapshot, error) in
            if let err = error {
                self.isLoading = false
                completion(err)
            } else {
                guard let snapshot = snapshot else { return }
                snapshot.documents.forEach { doc in
                    let data = doc.data()
                    let jsonData = JSON(data)
                    let video = Video(dataObject: jsonData)
                    self.dataSource.append(video)
                }
                if let lastDocument = snapshot.documents.last {
                    self.lastDocumentSnapshot = lastDocument
                    self.hasMore = true
                } else {
                    self.hasMore = false
                }
                self.isLoading = false
                completion(nil)
            }
        }

limitafterDocument を合わせて使うことで一般的なAPIクライアントで必須のページングの機能を導入することができます。 これによって読み込みの数を制限することで、ロードを早くし、余計なアクセスを減らすことができます。

Storage

動画サムネイルの管理に Cloud Storage を採用しています。 Cloud Storage 用の Firebase SDK があるので、簡単に扱うことができます。

最初動画の登録のためAPI経由でアップロードしていたのですが250KB以上の画像がAPI経由でアップロードに失敗すると言う問題に遭遇しました。 現状サムネサイズはそれを越えるものがないのですが、ここに関しては調査しようと思います。

また実装して気付いたのですが、Cloud Storageからダウンロードリンクを経由してサムネイルをアプリ内で用いると、アプリ内キャッシュがない場合の初期表示が露骨に遅いです。

改善策

初めは新しく用意したサムネイルのサイズが原因かなと思いサイズの削減なども行ったのですが、明らかにそこまでのサイズではないため勿論結果は変わりませんでした。 そこで画像読み込み部分を改善しようと思い、今までは KingFisher をアプリ内で使っていたのですが、これを機に少しでも速度を早めようと表示速度が速いと評判の Nuke を導入してみました。

ですが、体感ではあまり分からないレベルです。

そもそもの問題はGoogle Cloud Storage側にあるのではないかと思いコンソールで設定など色々調べていたところ、バケットのロケーションがUSになっていました。

f:id:keeetaka:20181107183119p:plain

どうやらデフォルトでFirebase側のプロジェクトの設定と紐づくようで、GCP側でバケットを手動で作らない限り自動的にUSになります。 試験的にアジアのバケットに画像データを用意したところスピードが劇的に向上しました。 途中でバケットのロケーションを変更することはできないので、既に本番稼働中のプロジェクトに関しては新しくバケットを作り既存のバケットからデータを転送する作業*2が必要となりますので注意してください。

Cookpadさんのエンジニアブログで先日公開された記事*3でも、Firebase Cloud Storageからの画像取得を改善した例が紹介されていました。 データの持ち方なども工夫されていましたが根本の原因はやはり「US」だったようです。

多くの方がFirebaseとCloud Storageを採用した時に一瞬詰まるポイントかもしれません。

Hosting

管理画面の公開にFirebase Hostingを利用しました。

独自のドメインを設定することもでき、当該ドメインの SSL 証明書が自動的にプロビジョニングされ安全にコンテンツを配信する事ができます。

今回の場合Vueを使っているので、npm run build でdistフォルダに公開用のビルドファイルが生成されます。 後は firebase deploy などといったコマンド一つで簡単にデプロイすることができます。

Functions

Cloud Functions for Firebase  |  Firebase

Cloud Storageにファイルをアップロードした時やFirestoreにデータが登録された時など、任意のタイミングでJSの関数を実行する事ができます。 例としては下記の用途が公式ページで紹介されています。

  • ユーザーにフォローされた際に(Firestoreにフォロワー情報が保存される)、Cloud Messagingでユーザーに通知を送る関数を実行する
  • コメントで不適切なコメントがあった際に、ワードをチェックする関数を実行し、コメントを削除するなど

f:id:keeetaka:20181107131835p:plain

今はFirebaseのSDKが進化しており単体でそれぞれの機能が使いやすいのでFunctionsを使わずともサービス運営も可能だとは思いますが、 Functionsを部分的に採用する事でより効率的なサービス運営が可能になると思います。

Cloud Storage を Cloud Functions で拡張する  |  Firebase

終わりに

Firestore以外は過去にも利用した事が何度かあったのと、プロダクトの構造に大きく影響を与えるものはないため懸念はなかったのですが、Firestoreに関しては正直不安でした。 既存サービスをFirestoreを使ったものにリプレイスするというのは特別な理由がない限り必要ないと思いますが、新規サービスなどバックエンドにあまり工数を割かずスピード重視で開発したいという場合には十分に期待に応えてくれるものだと思いました。

Firebaseのサービスは今回紹介した以外にもいくつかあります。 いずれも導入が容易で使いやすさが特徴なので是非一度試してみてください。

今後

Firestoreを採用したことでアプリで扱うデータを自由に持つことができたので、現在は動画配信部分のインフラを整えている最中です。 これでアプリ・管理画面・動画配信基盤といったベースが完成するので、後はAndroid版の開発をしたり、アプリの質を上げたり、色々チャレンジ出来る事が増えそうです。

会社のStyleとして 新たな挑戦を恐れてはならない というものがありエンジニアの技術的なチャレンジを会社として全面的にサポートしてくれます。
少しでも話を聞いて見たいと思った方は是非Wantedlyで会いに来てください!

www.wantedly.com

今回の記事が少しでも多くの方の参考になれば幸いです。
ここまで読んで頂いてありがとうございました。

参考

以下の記事はプロダクトにFirebaseを導入するにあたって非常に参考になりました。

Firebaseを活用したiOSアプリ開発事例 - クックパッド開発者ブログ
Firebaseでバックエンドエンジニア不在のアプリ開発 クックパッドが体感した、メリットと課題 - エンジニアHub|若手Webエンジニアのキャリアを考える!
Cloud Firestoreの勘所 パート1 — 概要 – google-cloud-jp – Medium

iOSアプリにSiri Shortcutsを実装して露出アップを狙ってみた #GameWith #TechWith

GameWithのiOSアプリエンジニアの chuymaster です!会社ではチャイと呼ばれています。インドのミルクティーのアレです。

今回はGameWithアプリに、Siri Shortcutsっていう機能を実装したので、その裏話を話したいと思います!

Siri Shortcutとは

Siri ShortcutsはiOS 12からできた機能で、Siriを通して音声でいろいろなアクションをさせることができる機能です。

f:id:gwchai:20181102110231p:plain

実装がとても簡単で、Siriだけではなく、Spotlight検索からもアクションを出現させることができて、アプリの露出を増やすのに役に立つ機能です。

また、頻繁にアクションが行われると、検索するまでもなく、Spotlightを出したときやロックスクリーンにショートカットが出現するので、アプリの起動回数アップも狙えます。

Siri Shortcutsの最小実装方法

Siri Shortcuts用のこんなクラスを作りました。

class SiriShortcutsService {
    
    static func createActivity(activityType: String,
                               title: String,
                               keywords: Set<String> = [],
                               contentDescription: String = "",
                               suggestedInvocationPhrase: String = "") -> NSUserActivity {

        // NSUserActivityを作成して付帯情報を設定
        let activity = NSUserActivity(activityType: activityType)
        activity.title = title
        activity.keywords = keywords
        
        let searchItemAttributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
        searchItemAttributeSet.title = title
        searchItemAttributeSet.contentDescription = contentDescription
        activity.contentAttributeSet = searchItemAttributeSet
        
        // Spotlight検索とSiri Shortcutsを有効化
        activity.isEligibleForSearch = true
        if #available(iOS 12.0, *) {
            activity.isEligibleForPrediction = true
            activity.persistentIdentifier = activityType
            activity.suggestedInvocationPhrase = suggestedInvocationPhrase
        }
        return activity
    }
}

これで、Siriに登録するショートカットを作成できます。 ショートカットの付帯情報はコードから分かるように

  • title: タイトル
  • keywords: 検索キーワード
  • contentDescription: ショートカットの説明
  • suggestedInvocationPhrase: Siriで起動するフレーズの提案

が設定できます。

次にショートカットを ViewController で登録する必要があります。

    override func viewDidLoad() {
        super.viewDidLoad()
        
        userActivity = SiriShortcutsService.createActivity(activityType: "jp.co.gamewith.xxxxxx",
                                                           title: "モンストのマルチに参加",
                                                           keywords: ["モンスト", "マルチ", "モンスターストライク"],
                                                           contentDescription: "モンスターストライクのマルチ募集中のクエストに参加できます")
        userActivity?.becomeCurrent()
    }

ViewController が持っている userActivity プロパティにショートカットを設定し、 userActivity?.becomeCurrent() を呼ぶことでショートカットを登録します。これで登録完了です。

ここで登場した activityType パラメータは、ショートカットごとに一意の文字列を設定します。 Siri Shortcutsでアプリが起動された際のハンドリングするための認識記号として持ちます。

ハンドリング処理は、 AppDelegateapplication(_:continueUserActivity:restorationHandler:) でこのようにします。

func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([Any]?) -> Void) -> Bool {

        // activityTypeの文字列を取得
        switch userActivity.activityType {
        case "jp.co.gamewith.xxxxxx":
            
            // モンストのマルチ参加画面を開く
            Route.multiParticipate()
            return true
            
        default:
            return false
        }
    }

留意点

Siri Shortcutsはあくまでも、ユーザーがよく使っているアクションを登録して、簡単に呼び出せるようにする機能なので、 実際にそのアクションをしたときにショートカットを追加するのがAppleのガイドラインに沿って適切です。

Don’t make donations for actions that the user has not completed in your app; if the user never places an order for soup, you should never donate a shortcut for the order soup action.

Donating Shortcuts | Apple Developer Documentation

Siriへの登録方法

GameWithアプリのモンスト攻略にはマルチ掲示板機能があり、 そこでマルチ募集かマルチ参加をした方なら、Siri Shortcutsを使うことができます。

  • まずは設定を開くf:id:gwchai:20181102115743p:plain

  • Siriと検索を選ぶ f:id:gwchai:20181102115812p:plain

  • GameWithを選ぶ f:id:gwchai:20181102115827p:plain

  • ショートカットを選ぶ f:id:gwchai:20181102115846p:plain

  • 使えるショートカット一覧から、「モンストのマルチに参加」を選ぶ f:id:gwchai:20181102120028p:plain

  • Siriで起動するフレーズを登録する f:id:gwchai:20181102120118p:plain

  • 「マルチ参加」で登録したショートカットが表示される f:id:gwchai:20181102120134p:plain

  • Siriに「マルチ参加」と話しかける f:id:gwchai:20181102120208p:plain f:id:gwchai:20181102120215p:plain

  • GameWithアプリのモンストマルチ掲示板が起動する f:id:gwchai:20181102120241p:plain

  • Spotlightからもショートカットが出現するようになる f:id:gwchai:20181102120305p:plain

実装した結果

Siri Shortcutsをリリースして1週間経ったとき

ショートカットを経由してアプリを起動してくれたユーザーがなんと・・・

・・・

・・

40名弱いました!

Siri経由とSpotlight経由を含めた数字です。

アプリの利用者の数から考えると、とてつもなく少ないです。

あまり普及しないSiriを使う機能なので、利用者が少ないのは予想通りとはいえ、少し凹みました(笑)

しかし、GameWithが掲げるビジョン「世界のゲームインフラになる」を実現するには、 こうしたユーザー体験の改善を地道に積み上げることが必要であり、 今後も試行錯誤しながらサービスを作っていきたいと思います。

最後に

社内では、Siriのことをもっと勉強して、Intentsという機能でSiriと会話して何かできないかも検証しています。

Siri Shortcutsのように、新しい技術をエンジニアから積極的に提案して実装できるのも、GameWithの社風ならではです。

そんなGameWithは、ゲームが大好きで、新しい技術をどんどん使っていきたいという方を大募集中!

Wantedly でもよいので是非お気軽にお声がけください!

www.wantedly.com

builderscon tokyo 2018 に参加してきました! #GameWith #TechWith

GameWithのエンジニアの @syque と tiwu です!

少し時間が経ってしまいましたが、今回はbuilderscon tokyo 2018に二人で参加してきたので簡単ですがレポートを書いていきたいと思います!

builderscon.io

GameWith は今回、builderscon tokyo 2018にスポンサーとして協賛させていただきました。

GameWithはバッテリーをノベルティとして提供させていただきました。

f:id:tiwu_gamewith:20181019144151j:plain

9/7 金

@syque タイムテーブル

Electronによるアプリケーション開発事情2018
Building and operating a service mesh at mid-size company
The state of the art of architecting Kubernetes-based infrastructure -Towards Maximum Security and Usability-
Istio: Weaving a Secure Service Mesh
Understanding Microservices with Distributed Tracing
lld − 開発ツールの主要コンポーネントの1つをスクラッチから作成した話

9/7のセッションで特に面白かったのが、「 lld − 開発ツールの主要コンポーネントの1つをスクラッチから作成した話」だったのでピックアップしたいと思います。

lld − 開発ツールの主要コンポーネントの1つをスクラッチから作成した話

https://www.youtube.com/watch?v=vWmqBp3uRsw

発表内容は以下の通りでした。

  1. lldの紹介
  2. 開発を始めた経緯
  3. 速くてシンプルなコードを書くためのポイント
    1. 良いデータ構造にする。データが先、コードが後
    2. 2回書く 1度目の経験を2回目に活かす
    3. 最適化する箇所を最小にとどめる。大半のコードは読みやすさを重視する
    4. プロジェクトのオーナーが責任を持ってコードをキレイに保つ
  4. 具体的な高速化のテクニック

発表内容については、自分はリンカを一度も作ったことのない畑違いなエンジニアであるにも関わらず非常に感銘を受けました。基本的にはリンカについての説明やlldのプロダクトについて成り立ちや具体的なテクニックについてを発表されていたのですが、それぞれのフェーズでのアプローチがとても適切で、正にエンジニアらしい問題を適切に捉えた仕事の仕方であるなと感じたためです。 例として以下のようなことを話されていました。

  • リンカのプロジェクトに携わったのはいいけれど、資料もコードもなくて手かがりがほとんどなかった
  • オブジェクトファイルを結合して空の実行プログラムを作成するところまでを手探りでやった(これだけで3ヶ月)
    • Hello World がリンクできるまでに数ヶ月、大きなプログラムがリンクできるまでは1年かかった
  • クロスプラットフォームなリンカ(Windows, Unix, ...)を作ろうとすると抽象化のためのコードが非常に多くなる
    • でも別々のリンカの機能が使われることはあり得ないので、これは無意味だと思ったので作り直しを決めた
    • メーリングリストでは喧々囂々になったが、結果としてコードがすっきりし、動作も10倍速くなった

内容について1つ1つは当たり前のことだけれど、規模の大きいプロジェクトにも関わらず自分の姿勢を崩さずに問題に対して適切な手法、アプローチで解決を図ろうとしていた植山さん自身の人となりが発表によく現れていて、同じエンジニアとして強く共感しました。 この発表でファンになってしまったので懇親会でも思わず話しかけに行ってしまいました(お邪魔してしまいすみませんでした...!)

tiwu タイムテーブル

Envoy internals deep dive
ランチセッション E
開発現場で役立たせるための設計原則とパターン
Webサービスにて200週連続で新機能をリリースする舞台裏 
実録!ある担当者がみた「謎ガジェット」開発1年史 
ブロックチェーン(DApp)で作る世界を変える分散型ゲームの世界

9/7のセッションで特に面白かったのが、「開発現場で役立たせるための設計原則とパターン」だったのでピックアップしたいと思います。

開発現場で役立たせるための設計原則とパターン

セッションの内容は、最高の設計というものは無く結局ケースバイケースという話をよく聞くけど、じゃあどうしたら問題に対して最適な設計を選べるのか?という話で、詳細を書くと

  • 設計とはごちゃごちゃとひとかたまりになった問題を分割し構造化するアクティビティ
  • だが、どうやって分割するか?テーブルごと?CRUDごと?
  • そこで、設計原則とデザインパターンが武器になる
  • デザインパターンは、手法のカタログ(選択肢が増える)
  • 設計原則は、良いかどうか判断する指針(選択肢を選べる)
  • こういった設計のほうが良いというのを、言語化できないと暗黙知になる
  • 単一責任原則,開放閉鎖原則,凝集度と結合度はとても重要な設計原則
  • 設計をしたらこの設計原則をもとにレビューしてよりよい設計にブラッシュアップしていく
  • (余談)FizzBuzzEnterPriseEditionという最強のFizzBuzzのリポジトリがある

といった内容でした。

エンジニアは全員それぞれ頭の中で良い設計を考えていて、それをコード化したりしていると思いますが 頭の中で考えているものなのでどうしても、レビューの際で上手く伝わらなかったりすることがあると思います。

しかし、こういった設計原則とデザインパターンという共通の言語で会話ができれば互いに良いコミュニケーションができ より、自信を持ってコード化もできるなと感じました。

9/8 土

@syque タイムテーブル

ファミコンエミュレータの創り方
高集積コンテナホスティングにおけるボトルネックとその解法
Lunch Session (VOYAGE GROUP)
遠いようで身近なサウンドエンジニアリング
Using Chrome Developer Tools to hack your way into concerts
Extending Kubernetes with Custom Resources and Operator Frameworks
LT

tiwu タイムテーブル

「Web とは何か?」 - あるいは「Web を Web たらしめるものは何か?」
証券トレーディング業務におけるExcel依存を脱却するプロジェクトで直面した技術的選択とプロジェクト運営の失敗
なぜエンジニアはパフォーマンス計測しないのか
Webアプリケーションエンジニアが知るべきDNSの基本
LT

9/8のセッションで特に面白かったのが、「「Web とは何か?」 - あるいは「Web を Web たらしめるものは何か?」」だったのでピックアップしたいと思います。

「Web とは何か?」 - あるいは「Web を Web たらしめるものは何か?」

セッションの内容は、webの歴史とこれから未来についての話でした。

まとめると

Webが生まれた当初はドキュメントを共有するプラットフォームだったが、最近はアプリのプラットフォームになっている。
Gmailとかがまさにそうで、Windows上で動くアプリからWebで動くアプリに変革していった(スプレッドシートも同じ流れ)
では、次はどこに向かうのか?
それは、「OS」である。
PCのローカルファイルを操作したり、USBにアクセスしたりできるAPIが現在開発中である。
ただ、一番の問題はPermissionで、実はセキュリティとか無視して開発を進めているらしい。
今丁度ターニングポイントで、このまま進めばWebはOSとなる未来がやってくる・・・?

といった内容でした。

PWA、AMPでワクワクしていた自分ですが、それを超える何かを感じました(個人的に)。

今後のWebの動向は要チェックです!

最後に

GameWithではエンジニアを積極的に募集しています。ゲームが好きな方大歓迎です!

(現在、関東圏にお住まいでない方もお気軽に!)

ご興味のある方は、Wantedly でもよいので是非お気軽にお声がけください。

www.wantedly.com

古民家を貸し切ってWebチーム合宿をしてきた話

はじめまして、GameWithのWeb面のディレクターをしているやまぐちです。

だいぶ間が空いてしまいましたが、8/30にWebチームのメンバー8名で合宿を行いました。 第一回なのでゆるい感じですが、その様子をお送りします。

続きを読む