Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

Studyplusのとある画面でYouTubeの動画再生に対応し、やらかしたお話

こんにちは、Studyplus iOSチームの明渡(ID: m_yamada1992)です。

今回は、今年3月にリリースしたStudyplusアプリにて大学の情報を表示する画面でYouTubeの再生に対応したお話、およびiOSアプリ側の実装にて盛大にやらかした話をつづっていきます。

YouTubeをStudyplusのアプリで表示する

動画を表示することになった背景

Studyplusは現状受験生のユーザーが非常に多く、「同じ志望校を目指す仲間と励まし合いたい!」というニーズに応えるため大学にまつわる情報を取り扱う機能が手厚く揃っております。志望大学を探す機能、同じ大学を志望しているユーザーの勉強時間ランキングなど。

そうすると、「Studyplusで志望大学を探すユーザーに自校をもっとアピールしたい!」という大学さんも少なからず存在するわけです。アピールする新しい手段の1つとして、動画を掲載もできるように対応することとなりました。

実現方法

YouTubeの動画サムネイルを表示して、サムネイルがタップされたらウェブビューのみの画面を表示して動画を表示するようにしました。実装イメージが以下のとおりです。

f:id:m_yamada1992:20190513212011p:plain:w300f:id:m_yamada1992:20190513212017p:plain:w300

左がサムネイル表示画面、右が遷移先の動画再生のみを目的とするWKWebView画面

なお、公式ライブラリの利用は真っ先に検討したのですが、iOSに関してはとうの昔に非推奨のUIWebViewを挟んで動作させることが前提だったので断念しました。 AndroidはきちんとメンテされているYouTube Android Player APIを採用してます。

YouTubeの動画サムネイルを取得する

YouTubeにて動画の情報を取り扱う手段として正式に提供されているYouTube Data API (v3)のうち、Videosリソースにて動画のサムネイル画像URLが取得できるので、そちらを使用しました。

ひとくちに動画情報といっても動画の投稿者やタイトル、説明文など今回不要な情報もたくさんあるので、クエリパラメータのpartfieldsで必要なレスポンスの値を絞り込みました。

let queryItems = [
    URLQueryItem(name: "id", value: id),
    URLQueryItem(name: "part", value: "snippet"),
    URLQueryItem(
        name: "fields",
        value: "items(snippet(thumbnails(standard(url),medium(url))))"
    )
]

過去に外部のAPIを使用する場面はそこそこあったのですが、「返ってくるレスポンスのうち○○と△△しか使わない」といったコメントを残さないと分かりにくいことがありました。 このように不要な値はレスポンスに含まないよう実装で制御し、使っている値が一目瞭然にできるのはすごく画期的に思えます。

YouTube独自のオーバーレイ表示へ対応

サムネイル画像をタップしたあと表示するWKWebViewの画面なのですが、Studyplusのアプリで素直な実装にて表示してみるとちょっとした問題が発生しました。

Studyplusアプリで実際にYouTubeの動画を再生した際、以下のキャプチャのような表示が可能です。

f:id:m_yamada1992:20190513162321p:plain

問題点

  • 端末の縦方向表示のみをサポートしているのに、YouTubeの動画プレイヤーを横方向で表示できてしまう
    • 端末を横にした状態で動画プレイヤーを閉じると、もとの画面でステータスバーが消えてしまう

なお、消えた状態のまま前の画面に戻ってもステータスバーは復帰せず、通常のナビゲーションバー下辺までの高さがステータスバー分縮んだままとなります。

キャプチャからも見て取れる通り、オーバーレイ表示上でステータスバーのみ標準UIを流用していることが影響している挙動のようです。

今回選んだ解決方法

記事冒頭のキャプチャでお察しかもしれませんが、YouTubeを表示するためのWKWebView画面でのみステータスバーの表示を諦めました。画面を表示時にステータスバーは非表示へ変更、画面を閉じるときに元通り表示すると他の画面は影響しないことが確認できたためです。

YouTube的にはプラットフォームの標準仕様より、ユーザーがいかに快適に動画を視聴できるかを追求した結果の挙動なのかしらとは思います。が、なぜステータスバーだけ標準OSで表示しているものを引っこ抜いていくのだろうか・・・?

こちらの手段に落ち着くまでにそこそこの試行錯誤や調査を要して戸惑ったので、そこまで独自の挙動をするのだったら全部独自のビューを表示すればいいのにと思ってしまいます。

やらかしたお話

結論から申し上げますと、YouTubeの動画サムネイル取得失敗のパターンが無限ループする実装を施してしまいました。

起こったこと

GCPのAPIなので、クォータを食い切ります。以下は弊社開発環境でのYouTube Data APIクォータ利用状況のGoogle Cloud Consoleの様子です。

GoogleCloudコンソール上で確認した、開発環境でクォータ上限突破した際のキャプチャ

キャプチャのうち、赤い点線が1日のクォータ上限です。現実的に上限を使い切ることはまずないだろうという値を設定しております。

1回目の時点で-8時間表記であることを見落として、夜中の3時に起こったアプリの操作では起こりえないという判断をしてアプリ担当としてはスルー。2回目に再発してこれはおかしいぞと調べたら無限ループを仕込んでいたことが発覚。

リリースを予定していたバージョンを数日前に審査提出・通過完了しており、翌日のリリース準備万端! というタイミングで2回目の再発でした。 同じ環境に接続されているうち1個の端末でエラーが発生するとクォータを一気に1日の上限まで使い切っちゃいます。

同じ事象を本番環境で発生させてしまうとサムネイルの取得が全ユーザーで最大24時間できなくなります。存在する動画はサムネイルが取得できるものとして実装しているため、YouTubeの動画をアプリで表示できなくなります。致命的です。

原因

対象の画面で既存バージョンから存在する、ほとんど同じ形で単純に画像の表示を行うセルにて取得した画像サイズに応じて高さを変更していました。そしてYouTubeのサムネイルを表示するセルを新しく作成するときに流用したのですが、その際に本来不要な画像サイズに応じた高さ変更処理諸共流用して放置して開発を進めたのが直接の原因です。

ディレクターチームにリリースを予定している機能を一通り動作確認してもらったあと、iOSチーム内でのソースコードレビューにて指摘をもらって上記の処理をリファクタリングしたのが事の発端でした。

実装の詳細

まず、動画を表示するセルにて表示する内容を設定するメソッドは以下のようなイメージです。

func setup(text: String, completion: @escaping (_ loadImageRatio: CGFloat?) -> Void) {
    // ...省略...
    if let thumbnailUrlString = videoThumbnailUrl {
        // YouTubeのサムネイルのURLをすでに取得済みの場合、画像を表示
        loadImage(urlString: thumbnailUrlString)
    } else {
        // 動画サムネイルURLの取得に1回失敗していた場合、このタイミングでリトライ
        guard let videoId = youtubeId else { return }
        VideoRepository.getVideoData(id: videoId, success: { thumbnailUrl in
            /* このセル用のコンテキストインスタンスへサムネイルのURLを保持 */
            // 取得したYouTubeのサムネイルのURLから画像を取得して表示
            self.loadImage(urlString: thumbnailUrl)
        }, failure: { _ in
            /* 動画を表示できない旨を表記 */
        }, finally: {
            // 成功したときは取得した画像の縦横比率を、失敗時は失敗したとき用の画像の縦横比率を渡す
            completion(imageRatio)
        })
    }
    // ...省略...
}

YouTubeから動画のサムネイルURLをすでに取得して保持済みの場合はその画像を表示、何らかの影響でサムネイルURLを取得できずに動画が表示できていなかった場合はリトライしてみるという実装です。

なお、この画面で表示する動画IDは弊社側で大学から頂戴したデータを管理するので、まず存在しないものはないはずという前提です。一瞬ネットワークの調子が悪くて取得失敗したなら、スクロールして戻ってきた際にリトライしたほうが親切だろうと。

問題が発生した動画を表示するセル生成処理

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ...省略...
    let cell = tableView.dequeueReusableCell(withIdentifier: "VideoCell", for: indexPath)
    cell.setup(text: text, completion: { imageRatio in
        /* このタイミングで取得完了した画像の縦横比率をこのセル用のコンテキストインスタンスへ保持し、以降画面へセルを再表示しても同じ高さで表示 */
        tableView.reloadRows(at: [indexPath], with: .none)
    })
    // ...省略...
}

上記のうち reloadRows(at:with:) メソッドを呼んでいる箇所で、リファクタリング前にperformBatchUpdates(_:completion:)メソッドを引数nilで呼ぶという一見なぜこの処理をこのタイミングで呼ぶか分かりにくい状態でした。ですので、そもそも reloadRows(at:with:) メソッドでも同じ挙動が期待できる()から、こっちのほうがシンプルでいいよね! という結論にいたり修正してこの実装に落ち着きました。

※厳密にはreloadRows(at:with:) メソッドを実行時にはtableView(_:cellForRowAt:)メソッドが呼ばれ、 引数nilのperformBatchUpdates(_:completion:)メソッド実行時は呼ばれないという違いがあります

起こったこと

  1. セルを生成する
  2. 指定した動画IDがなんらかの原因で存在しない、またはアクセス不可なので動画のサムネイルURL取得を再度試みる
  3. 共通の完了処理を呼ぶ
  4. 完了処理でreloadRows(at:)を呼ぶ
  5. 動画を表示するセルを再生成する(1に戻る)

という流れでの無限ループが完成しました。 動画を表示するためのセルが見える範囲内に存在する限り、無限に処理が走り続けます。

対応

さらにそもそもの話で、YouTubeの動画サムネイルはサイズ指定して取得できるため、画像が空の状態で一旦表示したあとに高さが変わることへの考慮は一切必要ございませんでした。 なので、サムネイル取得の試行完了後に行なっていた再読み込み処理をばっさり削除するかたちで事象は解消しました。

対応後の実装

セルのsetupメソッドからcompletion引数を削除しました。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // ...省略...
    let cell = tableView.dequeueReusableCell(withIdentifier: "VideoCell", for: indexPath)
    cell.setup(text: text)
    return cell
    // ...省略...
}

なんて無駄で弊害のある処理をしていたのでしょう! 驚きのシンプルさ。😭

修正版のリリース

リリース予定前日であり、その日修正版で審査を再提出してリジェクトされずに翌日リリースできるという保証はありません。ですので、社内のディレクターチームへ事象の説明や修正をいつリリースするかの対応を相談しました。

結果的な対応としては、もともとのバージョンを一旦予定通りリリースした直後に修正版を審査に提出、審査が通った次の日にリリースしました。

事象が発生したとしてもユーザー影響が限定的であることを鑑みての判断です。今回の事象が発生するのはケース入稿元のデータがおかしくないと滅多なことで発生し得ないこと、リリース日当日から動画の表示を始める大学がまだそれほど多くなかったこともあり。

さいごに

至極当たり前でたいへん初歩的ですが、以下を肝に銘じました・・・

  • こまかな仕様検討が開発と並行で走らざるおえない場合、未決定の状態で極力実装を進めない
    • 動画の取得が失敗した際、失敗したとき用画像を代わりに表示するかサムネイルの表示領域自体を非表示にするか開発フェーズ終盤まで確定させていなかった
      • 決まったあと不要と確定した処理を削れば良いかと思っていた節もある
    • 後回しにしがちだったエラーケースも早めに決めてもらえるよう、ディレクターへ積極的に声がけする
  • 既存の類似UIの処理を流用する際は、その時点で絶対に使用するとわかっている処理のみ絞り込んで利用する
    • "大は小を兼ねる"という概念はソースコードに当てはまらない、当てはめちゃいけない
  • リファクタリング先の影響範囲動作確認を徹底する
    • 全く同じように動くだろうと確認漏らすのはダメ絶対

現在担当しているプロジェクトの実装の品質は、前に担当したものよりかは安定しつつあるかなと自負してます。いや、安定していかないと不味いんですが。

ただし、新規事業開発でお忙しいところiOSチームリーダーの須藤さんにかなりの助力を頂いております。 今回の記事に記載したYouTubeのオーバーレイ表示に対する対応やサムネイルURL取得処理無限ループへの対応をはじめ、現在進行中のプロジェクトも。

まだまだ知識経験共に足りないひよっ子ですが、少しでも早くより力になれるよう、ひよっ子なりに学び取ったことは血肉として今後に活かしてゆく所存です。

そして、やらかしが発覚したあとの対応を決めるまでのスピード感や、「ブログ当番時に今回やらかした内容したためます〜」という具合にアウトプットすることへの寛容さにも驚いたり。

実は事象が発覚したリリース前日に自宅からのリモート勤務をしていて、これは1回会社に出勤して各方面へ直接相談しないと不味いかなと肝を冷やしました。 が、ディレクターチームへの対応の相談はMTGすら発生せず全部Slackのチャットで完結しました。ちょっと感動しました・・・良い環境に身を置けているなぁとしみじみ思います。

弊社のSSO事情について

こんにちは、サーバーサイドエンジニアの金澤です。

みなさんSSOしてますか?

今日は弊社の管理ツールなどで導入しているSSOについてお話しします。

現状

大きく分けて二通りの方法で実現しています。

  • awsのalbでopenid connect(以下oidc)
  • google G suiteのsaml

よくあることですがなぜ二つあるのかについては歴史的経緯以外の理由はありません。

利用する側から見た使い勝手もほぼ変わらないので、実装面などでの差をお話ししたいと思います。

導入方法

それぞれの導入方法について軽くご紹介します。

両者に共通することですが、ヘッダに認証情報が入っているのでhttpsのみの運用が強く推奨されています。

samlの設定例

googleのドキュメントはこちらにあります。

railsアプリケーションなので、OmniAuthとそのsaml strategyを使いました。

admin consoleからappを追加する

google G suiteのadmin権限が必要です。 admin画面から登録しましょう。 このアイコンが目印です。 Screen Shot 2019-05-08 at 18.35.04.png (22.0 kB)

  • SSO URLEntity IdというのがOmniAuthの設定に出てくるidp_sso_target_urlissuerなのでメモしておきましょう
  • 証明書もダウンロードしましょう
  • ユーザーの情報が欲しいときは、attribute mappingに追加
    • 弊社はメアドと名前が欲しかったので以下のようにしました。

Screen Shot 2019-05-08 at 18.33.55.png (84.4 kB)

また、googleからcallbackを呼ぶ必要があるのでそのurlを登録する必要があります。 デプロイする環境が決まったら設定しましょう。

Screen Shot 2019-05-08 at 18.39.08.png (60.8 kB)

entity idはユニークであればなんでもいいのですがurlを入れておくのが分かりやすいし必ずユニークになるので便利かと思います。

有効化する

作った時点ではAppの設定がOff for everyoneなのでonに設定する必要があります。 特定の人にだけ公開したい場合は、下記画面から手で設定することができます。

Screen Shot 2019-05-08 at 20.11.55.png (220.9 kB)

rails newする

いつものやつです。

GemfileにOmniAuthを足す

    gem 'omniauth'
    gem 'omniauth-saml'

initializerを足す

こちらの設定をします。 middlewareでもinitializerでもいいですが、弊社はinitializerにしました。 大体以下のような内容になります。

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :developer unless Rails.env.production?
  provider :saml,
    :issuer                             => "",
    :idp_sso_target_url                 => "",
    :idp_cert                           => ""
end

それぞれに入れるべきものはG suiteのadmin consoleへ登録するときにわかります。

callbackメソッド作る

先ほど出てきたcallbackメソッドを実装する必要があります。 以下サンプルコードです。

post "/auth/:provider/callback" => "welcome#callback"

def callback
    if request.env['omniauth.auth'].extra.response_object.present?
      attributes = request.env['omniauth.auth'].extra.response_object.attributes
      email = attributes[:email] || ''
      first_name = attributes[:first_name] || ''
      last_name = attributes[:last_name] || ''
      name = last_name + first_name
    else
      # OmniAuthが提供する開発用の認証方法
      # http://localhost:3000/auth/developer
      auth = request.env['omniauth.auth']
      name = auth[:info][:name]
      email = auth[:info][:email]
    end

    session[:email] = email
    redirect_to '/'
  end

大体このような感じでヘッダからユーザー情報を取り出せます。 あとはそれをお好みでセッションなどお好きなものに放り込んでそれの有無でログイン状態を判断できます。

albの設定例

awsのalbとoidcを使った設定例です。

oidcクレデンシャルの追加

  • こちらCreate credentials => OAuth client ID からクレデンシャルを追加する。

    • こちらはこちらで上記とは別の権限が必要になります。
  • Authorized JavaScript origins は適宜設定しましょう。

  • redirect urihttps://{{ ホスト名 }}/oauth2/idpresponse を登録しましょう。
    • /oauth2/idpresponse のハンドリングはalbが勝手にやってくれるのでここの実装は必要ありません。
  • client idclient secret をメモする。

アプリケーションの公開範囲制限

社内ツールなどは外部に公開する必要がないので、「OAuth同意画面」から「アプリケーションの種類」を「内部」に変更しましょう。

albに設定追加

  • awsコンソールから認証を追加したいalbを選び、 Listeners タブを選ぶ。
  • Rules のところに View/edit rules とあるので選ぶ。

全てのパスに認証をかけたい場合はこのような感じで設定できます。

Screen Shot 2019-05-08 at 20.00.46.png (218.5 kB)

  • Issuerと3種のendpointはgoogleという事業者で一つなので固定です。
    • 厳密には時期によって変わる可能性はあるはず
  • これらの情報はgoogleのOpenID Connect Discoveryから取得できます。仕様についてはこちら

ユーザー情報

上記設定のすぐ下にAdvanced settingsというものがあります(Timeoutはテストのため適当に変えたのでデフォルトでいいと思います)。

Screen Shot 2018-11-08 at 16.13.03.png (88.1 kB)

Scopeというのはsamlでいうattribute mappingsのようなもので、上記のOpenID Connect Discoveryにも scopes_supported という項目で何が指定できるか書いてあります。

最後に、認証が通ったらどこにforwardするか設定します。

実装

amazonによる実装例がこちらにあります。 Rubyで同じ内容に書き起こしましょう。 認証をかけたい機能ではrequestのヘッダを確認して、ユーザー情報が取れない場合は401を返すなり別のところにredirectするなり実装すれば終わりです。

それぞれのまとめ

saml

  • albよりは書くことが多い
    • とはいえOmniAuthが相当な部分吸収してくれているので大きな手間ではない
  • 証明書の期限があるので数年で使えなくなる
    • スパンが数年になるとどうしても優先度下がるし忘れそう
  • samlはRFCに仕様があるのでamazonにロックインされていないという自己満足

alb

  • 設定はsamlより楽
  • 証明書をアプリで意識しなくてもいい
  • ローカルで開発しながら動かすためにはログイン状態にするためちょっとアドホックなコードが必要
    • samlの方には開発用のログイン方法がOmniAuthにあるので楽
    • もしかしたらいい方法があるのかもしれない
  • amazonにロックインされているという被害妄想
    • oidc自体はOAuth 2.0をベースにした規格なのでopen
    • とはいえ現実的にはamazonから動かすことってあまりなさそう

終わりに

どちらが明確に優れているということは無いと思いますが、仮に自分がまたSSOのアプリを社内向けに用意するとしたらalbを使う気はします。

とはいえalbは当然amazonでしか使えないので、読んでくださっている方の中には使えない場合もあるかもしれません。

どちらの方法でもemailは取れるので(設定は必要)、「認証の結果セッションの中などどこかにemailが入っている」というぐらいまで責務を薄くして作っています。 そうすればあとで切り替えたくなったときも変更は最小限で済むかと思います。

権限についてですが、弊社は今の所誰が何をしたかは記録しているけれど権限制御をガチガチにしているわけではありません。 もしやることになったとしても「Gmailなどの組織図」と「管理ツールの権限の分布」が一致するということは希だと思うので、どのみちそこは手で頑張るしかないと感じています。となると外部に依存するよりは弊社のデータベースなどでemailと紐づけて管理、という形になるのかなとぼんやり思っています。

とはいえ凝りすぎるとSSOの楽さという長所が失われてしまうのでよっぽどの重大な機能(個人情報に関わるなど)でなければ権限を複雑にするつもりは今の所ありません。

何はともあれ、ssoを入れておくと本当に楽です。弊社は少し前に営業の方の入社ラッシュがあったのでそのときに導入できていなかったら多分すごく面倒だったろうなと思っています。

この記事が皆さんのSSO導入の第一歩になればこれに勝る喜びはありません。

ありがとうございました。

RubyKaigi 2019にPlatinumスポンサーとして参加しました!

こんちにちは、スタディプラスCTOの島田です。

スタディプラスはRubyKaigi 2019Platinum Sponsorsとして参加させて頂きました。

こちらで投稿したとおり4/18~19の3日間スポンサーブースを出展しました。
ブースでは学習記録サービス「Studyplus」にちなんで「ORB(推し Ruby Book)選抜総選挙」と銘打って、好きなRuby本の投票を実施いたしました。
RubyWorld Conference 2018での投票結果と、その時に投票頂いた方の意見を元に5つの書籍を選定しました。

選定書籍一覧 - パーフェクトRuby - プロを目指す人のためのRuby入門 - たのしいRuby - ゼロからわかる Ruby 超入門 - Ruby逆引きハンドブック

スポンサーブースにて

数量限定で準備したサコッシュでしたが多くの方に投票を頂き、あっと言う間に配布が終わってしまいました。

技術書典6にて頒布された「マンガでわかるRuby」「RubyとRailsの学習ガイド」を献本頂きました。

著者の方にサインも頂きました。

matzさんもいらっしゃいました。

3日間の投票結果はこちらになります。

感想

参加メンバーの印象に残った発表です。

島田

Pattern matching - New feature in Ruby 2.7

https://speakerdeck.com/k_tsj/pattern-matching-new-feature-in-ruby-2-dot-7 @k_tsjさんによるRuby2.7のパターンマッチングの発表。

Pattern matchingとは

  • Ruby 2.7で実験的に導入された機能
  • パターンによるマッチ処理をして、マッチした値を複数の変数に代入できる
  • case/in を利用する

構文

  • パターンが上から順番に、最初に一致するまで走査される
  • マッチしない場合はelseが実行される
  • パターンにマッチせずelseがない場合は、NoMatchingPatternErrorがraiseされる
case expr
in pattern [if|unless condition]
 ...
in pattern [if|unless condition]
 ...
else
 ...
end

設計で意識した事

  • 下位互換
  • Ruby的な文法

実装のパターンを抜粋

リテラル値を利用したパターン

case 0
  in 0
  in -1..1
  in Integer
end

_を利用したパターン

case [0,1]
in [_,_]
  :reachable
end

Pin Operator。変数の値に対してパターンマッチングを行いたい場合は^を使用

a = 0
case 1
in ^a
  :reachable
end

ASパターン

case 0
in Integer => a
  a #=> 0
end

Arrayパターン

class Array
  def deconstruct
    self
  end
end

case [0, 1, 2]
in Array(0, *a, 2)
in Object[0, *a, 2]
in 0, *a, 2
end

p a #=> [1]

Hashパターン

case {a: 0}
in a: 0
  true
end

# => true

まだ実験段階のパターンマッチですが、その幅広さにRubyの柔軟性をより広げていくものだと感じました。

花井

All bugfixes are incompatibilities

ruby trunk changesでおなじみの、@nagachikaさんによる、安定版Rubyのメンテナンスについてのお話です。 1年に1回歳をとる人間の脆弱性を念頭に、これからメンテナになる人への申し送り事項の伝達というスタイルの発表でした。 trunkからバックポートしたさいに起きた実際のバグを例に、メンテナンスのキモになる「何を取り込むか判断すること」について

  • パフォーマンス改善は不具合修正と言えばそうだが、バックポート対象にはすべきでなかった。trunkがよくなればいいだけで、安定版は安定性を損なわないようにすべきだった。
  • メソッド/定数探索周りはエッジケースも多く、文法周りは組み合わせが膨大で全てテストで網羅して事前検知するのは難しい。
  • 長い間放置されていた(気づかれていない)SyntaxErrorや、リリースした当時の状況的に使われてる量が少ないものは慌てて直さなくてもよかった。

「あるアプリケーションで困っている人がいても、もっと大きなアプリケーションを壊してしまう場合はよりユーザーが多い方を優先しなければいけないという判断をしないといけないのは心苦しい。しかし、それが安定版メンテナのお仕事なのです」 という結びの言葉が印象にのこりました。

Crystalball: predicting test failures

@p0dejeさんによるcrystalballを利用した回帰テストの選択実行について内部実装のお話とデモです。 小さな変更に対して全てのテストを実行するとどうしても時間がかかってしまうので、変更したコードに関係するテストをカバレッジベースで解析し、マッピングしたうえで変更箇所に関係しているテストだけが走るようにできるというものです。 コードの変更はもちろん、schemaファイルやFactoryBotの変更にも対応していて感動しました。セッション後にridgepoleのSchemafileにも対応してるのか聞いてみましたが、ridgepoleをご存じなくデフォルトでは対応していないとのことでした。 @p0dejeさんもGraphQL周りでやっている解決策として、自前でPredictorを書くという手があるそうなので、実際に試してみようと思いました。

Best practices in web API client development

キュアエンジニア@sue445さんによる、APIクライアント開発のベストプラクティスの紹介です。

APIクライアントの責務と、作成者(自分/他人)、公開範囲(public/private)の組み合わせによるケースごとにベストプラクティスが紹介されました。

まずAPIクライアントに作るべき機能として以下をあげています。 - パラメータの変換(型変換、スネークケース・キャメルケースの変換) - エラーコード1のラップ(サンドイッチメソッドの抽出2) - OAuth2アクセストークンの自動更新

逆に作るべきでない機能としては、 - アプリの責務である、APIレスポンスのキャッシュ - 必須や空白チェック以外のリクエストパラメータの詳細なバリデーション

をあげていました。そのほかGood Patternとして以下の7つの紹介がありました。

  • runtime dependencyをできるだけ使わない(できるだけRuby標準機能でつくる)
  • Getだけならopen-uriで十分
  • Faradayは銀の弾丸
  • CRUD全部必要なケースで有用
  • 主題を目立たせる
  • 共通で使うならインスタンス変数にしておく
  • Hash引数よりkeyword引数
  • メソッドの定義が必要なものを物語るので実装しやすい・テストしやすい
  • 新しいサービスなどで頻繁に更新されるならHashでもよい
  • パラメータオブジェクト
  • たくさんの引数を受ける場合に有効
  • パラメータオブジェクト自体のテストもかける
  • 導入基準はYARDでコメントを書きやすくなるか
  • メソッドアクセス可能なレスポンス
  • mashifyを入れえると簡単に実現可能
  • curlは万国共通
  • バグレポートしたいとき、API提供者がRubyを読めるとは限らないのでcurlでレポートを書く
  • faraday-curlを使うとラク

どの内容もすぐに使える実践的な内容であるばかりか、普段の開発でも心がけた方がいい内容も含まれており、 APIクライアント作成の知見が詰まった素晴らしい発表でした。

冨山

印象に残った講演

How to use OpenAPI3 for API developer

twitter.com committeeのOpenAPI3.0対応をしたota42yさんのお話でした.
弊社でもAPI開発の際にcommitteeを使用しており内部実装の話は大変為になりました.
committeeは抽象化されたvalidatorを用意して,OpenAPI2.0, OpenAPI3.0, JSON Schemaなど複数のスキーマに対応していて, OpenAPI3.0のスキーマを理解する部分はopenapi_parserで行っているようです.またバリデーションを行う際にはパスに対応するAPIの定義が含まれるオブジェクトを参照する必要がありここの実装にも工夫がありました.
パスの階層をノードとしたパトリシア木というデータ構造をRubyオブジェクトとして持っているという設計は階層ごとにネストしたHashで定義したり,パスを全件探索するような実装より優れていて印象に強く残りました.

Pragmatic Monadic Programing in Ruby

twitter.com こちらはjoker1007さんによるRubyでmonadをする話でした. Procは手続きを持った関数オブジェクトであるとすれば関数型言語的にかけるようにできるはずだというもので, RubyVM::ASTやTracePointなどを使って悪事を働くコードを披露しつつもスタックトレースは追えるよう実装されていたりと終始会場が賑わう楽しい発表でした.

またDay1のアフターパーティは商店街を貸し切って行われるという今までに無い形の懇親会で,商店街のあちこちで日本酒,ビール,食事などなどが提供されていて,多くの人が歩きながらお酒を飲んで談笑していたため多くの方と交流ができて楽しめました. 弊社メンバーと歩いていたらメタプログラミングRuby3の著者Paolo Perrottaがいたので一緒に写真を取っていただきました! f:id:atomiyama:20190509102457j:plain

最後に

昨年度に続きRubyKaigiの協賛をさせていただき、今回は初めてスポンサーブースの出展をさせていただきました。 ブースへは本当に多くの方に足を運んで頂き、投票への協力をしていただいた事に感謝です。

スタディプラスでは、今後もRubyコミュニティの貢献を考えております。 RubyKaigi 2020は長野県松本市という事なので、何かの形で協力を出来ればと思います。


  1. APIの通信にFaradayを使うとAPIクライアントのユーザーがFaradayを意識しないといけなくなるので、Rubyのエラーに変換するという例をあげていました

  2. 『リファクタリング:Rubyエディション』

  3. メタプログラミングRuby

便利に使えるAnsibleのAWSモジュール11選

こんにちは、インフラエンジニアの菅原です。

皆さんはAWSをどのように管理されてますか?
Ansibleでしょうか?ChefやTerraform、CloudFormationでしょうか?  

弊社ではAnsibleのAWSモジュールを使って、構成をコード化し、管理しております。
そのため最近関わったプロジェクトでも一人で手早く対応することができました。

今回は便利に使い回せるAWSモジュールを厳選して紹介したいと思います。  

AnsibleでAWSを管理する理由

弊社では以下の理由でAnsibleをメインに構成管理を行っています。

  • 構成変更の影響範囲をできるだけ小さくしたい(破壊的変更を避けたい)
  • エージェントレスで導入コストが少なく、手元で設定を完結したい
  • 使うツールは最小限にしたい(新規メンバー参画時の学習コストを減らしたい)
  • 前任者の好み

Infrastructure as Codeを挫折してしまう理由が「ちょっと変更したいけどコード化に時間がかかる」や「ここだけ変更したいけど影響範囲が大きい」という要望に簡単に答えられないときだと個人的に思ってます。「手でやった方が早い」という気持ちになってしまうと再現性のない環境が次々とできてしまいかねません。
その点Ansibleであれば、変更箇所を変えて実行するだけですし、変更したい箇所以外はskipされるため影響範囲も最小限に抑えられます。

この記事の前提

この記事の前提は以下となります。

  • Ansible2.7系で利用することを前提に記載してます。
  • モジュールは順不同になります。
  • 以下の図のような構成を目的としたAWSモジュールの紹介になります。

f:id:ksugahara08:20190416080142p:plain
AWSモジュールでのsample構成

便利AWSモジュール

iam_role

- name: Create IAM roles
  iam_role:
    state: present
    name: "sample-iamrole"
    assume_role_policy_document: "{{ lookup('file', 'sample-role.json') }}"

iam_roleはIAMロールを作成、変更することができます。
初回実行時にAWS環境にJSONファイルで用意した内容でIAMロールを作成してくれます。
JSONファイルは以下のようにfilesディレクトリ配下に配置します。

├── files
│   ├── sample-policy.json
│   └── sample-role.json
└── tasks
    └── main.yml

IAMロールの設定内容を変更したい場合はJSONファイルを書き換えて実行すれば上書き変更できます。
一つ作ってしまえば使い回しが効くのですごく便利です。

iam_policy

- name: Set policy
  iam_policy:
    iam_type: role
    iam_name: "sample-iamrole"
    policy_name: "sample-policy"
    policy_json: "{{ lookup('file', 'sample-policy.json') }}"
    state: present

iam_policyはIAMのポリシーを作成、変更することができます。
弊社ではtrusted entitiesをiam_roleで設定し、policyをiam_policyで付与しています。

route53

- name: Set Route53
  route53:
    command: create
    zone: "sample.com"
    record: "new.sample.com"
    type: "A"
    ttl: "7200"
    value: "dualstack.sample-1111111111.ap-northeast-1.elb.amazonaws.com."
    overwrite: "{{ DNS_OVERWRITE | default(false) }}"
    alias: "True"
    alias_hosted_zone_id: "ZXXXXXXXXXXXXX"

route53はRoute53にDNS設定を追加、変更することができます。
上記はロードバランサー(ELB)に設定する例です。ホスト名の変更を簡単に行うことができるため重宝してます。
既存のレコードを変更する場合はoverwritetrueにします。その際は変数を実行時に渡してあげるとroles配下のファイルを書き直さなくても良くなります。

ansible-playbook main.yml -e DNS_OVERWRITE=true

ec2_vpc_net

ec2_vpc_net, ec2_vpc_igw, ec2_vpc_subnetは基本的に新規プロジェクトや環境追加するときにしか実行しませんが、手間が省けたり、環境設定をコードで管理することができるため利用しています。

- name: Create vpc
  ec2_vpc_net:
    state: present
    name: "sample-vpc"
    cidr_block: "10.10.0.0/16"
    dns_hostnames: yes
    dns_support: yes
  register: vpc_net

ec2_vpc_netはCIDRブロックを指定してVPCを作成、変更することができます。
変更する対象はcidr_blocknameで一意に決まります。

ec2_vpc_igw

- name: Create igw
  ec2_vpc_igw:
    state: present
    vpc_id: "{{ vpc_net.vpc.id }}"

ec2_vpc_igwはInternet Gatewayを設定できるモジュールです。
vpc_idec2_vpc_netで作成したidを渡すことで設定することができます。
例のようにregistervpc_idを渡してあげるとスムーズです。

ec2_vpc_subnet

- name: Create subnets
  ec2_vpc_subnet:
    state: present
    vpc_id: "{{ vpc_net.vpc.id }}"
    cidr: "{{ item.cidr }}"
    az: "{{ item.az }}"
    tags: { "Name": "{{ item.name }}" }
  with_items:
    - { cidr: "10.10.0.0/20", az: "ap-northeast-1a", name: "sample-vpc-subnet1" }

ec2_vpc_subnetはサブネットを設定するモジュールです。
例ではCIDRブロック、Availability Zone、nameタグをwith_itemsに変数として入れて実行しています。
サブネットを複数設定する場合はwith_items構文でループ実行します。

ec2_group

- name: Create security group
  ec2_group:
    name: "sample-sg"
    description: "sample security group"
    vpc_id: "{{ vpc_id }}"
    rules:
      - proto: tcp
        from_port: 22
        to_port: 22
        cidr_ip: "10.10.0.0/20"
      - proto: tcp
        from_port: 80
        to_port: 80
        cidr_ip: "0.0.0.0/20"

ec2_groupはセキュリティグループを作成し、対象のVPCに設定することができます。
2回目以降の実行ではセキュリティグループの変更(上書き)を行うことができます。
ただし、AWSのマネジメントコンソールから直接入れた設定があるとロールバックさせてしまうので、実行する前は--checkを付けて変更箇所を確認した方が良いです。

elb_target_group

- name: Create Target Group
  elb_target_group:
    state: present
    name: "sample-alb-target-group"
    protocol: http
    port: 80
    vpc_id: "{{ vpc_id }}"
    health_check_protocol: http
    health_check_path: /health_check
    health_check_interval: 10
    health_check_timeout: 5
    healthy_threshold_count: 2
    unhealthy_threshold_count: 2
    deregistration_delay_timeout: 0
    successful_response_codes: 200

elb_target_groupはロードバランサーのターゲットグループを作成、変更できます。
例ではリクエストをhttpで80番ポートに受け渡すように設定しています。
ヘルスチェックは要件に合わせて設定値を変えてください。
ターゲットグループにAWSのマネジメントコンソールから追加したインスタンスがあると実行時にターゲットから外れるので注意してください。

elb_application_lb

- name: Create ALB
  elb_application_lb:
    state: present
    name: "sample-alb"
    scheme: internet-facing
    subnets: "sample-vpc-subnet1"
    security_groups:
      - "sample-sg"
    listeners:
      - Protocol: HTTP
        Port: 80
        DefaultActions:
          - Type: forward
            TargetGroupName: "sample-alb-target-group"
      - Protocol: HTTPS
        Port: 443
        DefaultActions:
          - Type: forward
            TargetGroupName: "sample-alb-target-group"
        Certificates:
          - CertificateArn: "arn:aws:acm:ap-northeast-1:123456789:certificate/aaaa-bbbb-cccc-dddd-eeee"
        SslPolicy: ELBSecurityPolicy-2016-08

elb_application_lbはアプリケーションロードバランサーを作成、変更できます。
今まで作成してきたサブネットやセキュリティグループを指定して作成します。
例ではロードバランサーに証明書を設定しています。Certificatesにarnを記述すれば証明書を設定する事ができます。
ALBとして利用するのであればlistenersタグの中にRulesタグを設ければパスで振り分けることができます。(詳しくは公式ドキュメントを確認してください。)

ec2_lc

- name: Create launch configuration
  ec2_lc:
    state: present
    name: "sample-lc"
    image_id: "ami-123456789"
    key_name: galaxy
    security_groups: "sample-sg"
    instance_profile_name: "sample-iamrole"
    instance_type: "t3.small"
    assign_public_ip: yes
    volumes:
      - device_name: /dev/sda1
        volume_size: 32
        volume_type: gp2
        delete_on_termination: true
  register: lc

ec2_lcはAuto Scalingの起動設定を作成、変更をすることができます。
AMIを用意してあればimage_idで指定することで任意のイメージを利用することができます。
弊社ではAMIをAnsibleとPackerで作成しています。
また、EC2のインスタンスサイズやボリームはここで管理する形にしています。

ec2_asg

- name: Set Auto-scaling group
  ec2_asg:
    state: present
    name: "sample-asg"
    launch_config_name: "{{ lc.name }}"
    health_check_period: 500
    health_check_type: EC2
    replace_all_instances: yes
    min_size: "1"
    max_size: "3"
    desired_capacity: "2"
    vpc_zone_identifier: "sample-vpc-subnet1"
    tags:
      - Name: "sample-web"
      - role: "web"
      - env: "cage"

ec2_asgはAuto Scaling グループの作成、変更をすることができます。
ec2_lcregister: lcを指定しておけばlc.nameを受け渡せます。
弊社ではEC2の起動/停止はec2_asgを使って管理しています。

最後に

今回は弊社でもよく使っているAnsibleのAWSモジュールを紹介しました。
AWSモジュールを利用することでインフラの自動化を進めていますが、Ansibleでは対応していない設定項目やAWSの最新の変更に追従できないこともあります。しかし、うまく使うことで運用をかなり楽にできるができます。

Rails Developers Meetup 2019に参加してきた

こんにちは!
For School事業部でサーバーサイドエンジニアをしているatomiyama(id:atomiyama)です.
今回3/22, 3/23に開催されたRails Developers Meetup 2019 Day2参加してきました!
弊社はDay2でビールスポンサーとして参加させて頂いたのですが,こういったカンファレンスに参加させてくれて感謝です🙇
railsdm.github.io

Railsの生みの親であるDHHをはじめとしたそうそうたる面々の発表が聞けて学び多くあったので,
中でも印象に残った発表の感想などレポートしたいと思います.

印象にのこった発表

今回Day2のみ参加だったので2日目の発表の中からの選出ですが特に下の2つの発表はRailsを書いているすべての人に読んでもらいたい程に有益な発表でした.

Clean Test Code, Revised @willnetさん

f:id:atomiyama:20190415161400j:plain 可読性の高いコードを書くためのコツというお話でした.
describe以下に大量に並ぶletやDRYで書かれスコープを飛び出したlet, FactoryBotにおける初期値の話など,脳への負担が少ない可読性の高いテストコードを書くTipsを アンチパターンを交えつつ発表されていました.ついついやってしまうような覚えのある話もあり胸に刺さる話が多かったです.

twitter.com

Ruby on Railsの正体と向き合い方 @yasaichiさん

f:id:atomiyama:20190415161550j:plain そして会場から人がはみ出す程に大盛況だったこちらの発表です.
DHHがRailsを生み出す上でした妥協の背景,内容をHanami(クリーンアーキテクチャ)との比較を通して認識した後,その妥協(Railsの強みでもある)からくる限界をどう倒して行くかをコードレベル(ApplicationModelやForm, Serviceなどの導入)と,アーキテクチャレベル(BFF, MicroServiceなど)で発表されていた100点満点中300点の発表でした.
特に前半のRuby on Railsの正体に迫る部分の話は他のフレームワークのコードまで出しての説明だったので大変わかりやすく感動しました!

twitter.com

まとめ

今回弊社はビールスポンサーとして参加しましたが,参加者の方々と様々なお話をできて大変楽しい1日を過ごすことができました!
弊社はRailsをサービスの中で積極的に使用しており今後もこのような機会があれば是非参加したいと思いつつ,こういった場で発表できるようにより精進しなければと思わせられる貴重な機会でした.
今回でrailsdmは完結となり次回からはrails kaigiとして開催されるようです.
このような貴重な会を開催してくださった運営メンバーの方々への感謝とともに今回のrailsdm2019のレポートとします!

f:id:atomiyama:20190415161926j:plain
懇親会の様子

RubyKaigi2019でお待ちしております!

スタディプラスCTOの島田です。

いよいよ今週RubyKaigi2019が開催されます。

今回、スタディプラスではスポンサーブースを出展させていただきます。 ブーススポンサーはRubyWorld Conference 2018(以下RWC)に続き2回目となります。

ブースでは学習管理サービスStudyplusとRubyにちなんで、「推しRuby本投票」を実施いたします。
多くのRubyistが集うRubyKaigiで、Rubyを学習するにあたって、どのRuby本を薦めたい(推したい)かの投票していただこうという企画です。
(著者の皆様サインお待ちしてます 🙇🏻‍♂️)

f:id:yo-shimada:20190415154627p:plain

RWCで実施した企画の続編で、候補のRuby本を変えての実施となります。

投票していただいた方には、スタディプラスの缶バッジとサコッシュをプレゼントさせていただきます。
※ サコッシュの数には限りがありますので、なくなり次第終了となりますのでご了承下さい。

f:id:yo-shimada:20190415154822j:plain

その他にもスタディプラスではノベルティをご用意しておりますので、是非スタディプラスのブースまでお立ち寄り下さい。

f:id:yo-shimada:20190415154843j:plain

当日の状況については随時、スタディプラスの公式Twitterにてお知らせさせていただきます。

Flutterもくもく自習室 in スタディプラス #2 を開催しました。

スタディプラスでiOSと新規事業を兼務している須藤(id:kurotyann)です。

今回のブログでは、4月6日(土)に開催した「Flutterもくもく自習室 in スタディプラス #2」の結果についてまとめます。

Flutterもくもく自習室 in スタディプラス #2 - connpass

Flutterもくもく自習室とは?

第1回のブログを参照してください。

Flutterもくもく自習室をはじめました - Studyplus Engineering Blog

運営スタッフから見た第2回の様子

今回も運営スタッフは、弊社の社員の2名で行いました。

参加者の様子は当日のTwitterのハッシュタグ #flutter_studyplus で見れます。

須藤(id:kurotyann)

4月初旬はイベントの多い時期です。

connpass.comで調べると都内では、もくもく学習会の当日に48件のイベントが開催されていました。

この状況だと、参加者ゼロ人もありえると覚悟していましたが、3名の方が参加してくれました。

当日は、前回と同様にもくもくと開発できる良い学習環境が維持できました。

私は、新規事業のアプリをFlutterで開発しており、主にListViewをつかった画面を実装してました。

素晴らしい学習環境の維持に貢献してくれた参加者の皆さま、本当にありがとうございました!

若宮(id:D_R_1009)

"Flutterもくもく自習室"はFlutterではない好きな言語の好きなものをもくもくする場として設定しています。 そこで今回はAndroidの簡単なライブラリ開発に取り組んでみました。

https://github.com/koji-1009/FailureStatus

「なにか手をつけてみよう」と考えて取り組む場として、なかなかいい環境なのではないかなと感じています。 気負わずに、軽い気持ちで参加してもらえる場として取り組み続ければなと考えています。

参加者のみなさま、ありがとうございました!

次回の開催日は?

次回のもくもく学習会の開催日は6月を予定しています。

5月は弊社オフィスにてFlutter Meetup Tokyo #9を開催するので、5月のもくもく学習会はお休みです。

Flutter Meetup Tokyo #9の詳細な日程は、4月末ごろに公開しますので、しばしお待ちください。