Studyplus Engineering Blog

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

インフラエンジニアがオフィス移転で考えたこと[前編]

初めまして、インフラエンジニアの菅原です。

弊社では事業拡大に伴うオフィス移転を予定しております。
インフラエンジニアとして社内ネットワーク移設について検討したことを移転前と移転後の2回に分けてまとめたいと思います。

f:id:ksugahara08:20181110143139j:plain:w300

まだ何も物が入っていないまっさらなオフィスの写真です。結構広い!
移転は年末になり、現在は移転準備をしている最中となります。

移転前に検討するべき項目

1.移転後の要件

まず、移転後の社内ネットワークデザインを決める基礎となる情報を収集しました。

  • 社内ネットワークの利用ユーザー数
    弊社では事業拡大も視野に入れて50人~200人規模の利用ユーザー数を想定しました。
    それだけでなく、イベント時はゲストも繋げるため最大200人の同時接続を許容する必要がありました。

  • ネットワーク接続する端末の台数
    アプリの検証端末などもあるため利用ユーザー数+αの接続が見込まれます。接続数には余裕を持たせたほうが良さそうでした。

  • オフィスのレイアウト
    今回は内装業者さんに座席の位置やパーティション等の図面を早い段階で頂いたため、そちらを基にルーターの設置場所、無線アクセスポイントの配置について検討することにしました。

  • オフィスで行う業務
    一般的な事務、カスタマーサポート、WEBアプリ開発がオフィス業務の中心になります。
    AWSとGCPを利用しているため、サーバーの移設については今回検討不要でした。
    オフィス向けプリンター・複合機は現在利用しているものを利用します。

2.現状の確認

現状どのような業者と契約しているか、どのような機器を利用しているかを把握し、移転後も使い続けるのかを確認する必要があります。今回は以下の現状について確認しました。

  • ISP(インターネットサービスプロバイダ)と光回線業者
    過去の経緯からIP電話とインターネット回線を別々の業者と契約している状況でした。こちらを一つにまとめられないか検討しました。しかし、変更してもランニングコストは変わらなかったため、現状のプランのまま移転することにしました。

  • ネットワーク機器とその設定
    ルーター、無線アクセスポイントについて設定内容を確認しました。現状では接続先アクセスポイントによって回線速度が遅いと感じていました。こちらも移転に伴って変更を検討しました。

  • 現状の回線速度
    引越し後の回線速度とも比較を行いたいため、測定しておきます。ツールは「Speedtest by Ookla」を 利用しました。

  • 固定IPが使われている箇所
    社内ツールなどは固定IPでアクセス制限を行なっている箇所があったため、設定を洗い出しました。
    移転に伴って設定を書き換える必要があります。

3.障害・災害時の検討

SLOによって変わってくるとは思いますが、機器の故障に寄る障害にも強い構成を検討するべきでしょう。
今回の移転ではSPOF(単一障害点)になりうる部分をできるだけ排除し、ネットワークの可用性を向上させることにしました。
具体的には現状ルーターが1つしかないので、機器の故障に備えてもう一つ購入し、多重化することにしました。 業者の保険に入ることで故障時の対策とすることもできましたが、ルーターを2つ購入して設置しておくことで、素早く復旧できると考えたからです。

4.スケジュールの策定

ネットワークの動作確認を考えると、移転の1週間前までには移転先でのネットワーク設定作業を終えていたいと考えました。オフィス移転の前日や前々日がネットワーク移転の作業日になるケースは多いと思います。そうするとスケジュールに余裕がなく、もしもの時に対応できません。
こうした要件を満たしたい場合に問題となってくるのが光回線業者の作業です。 「移転」で依頼した場合は工事作業日が前日や前々日になってしまします。しかし、「新規」で契約し直すことで現オフィスと平行契約期間を設けることができます。これで1週間前に動作確認ができるようにしました。

5.移設後のネットワークの構成と機器を検討

  • ルーター
    現状使っているルーターが故障した時のために同じものをもう1台購入することにしました。
    移転後のオフィスでは利用者が増えるためルータのNAPTテーブルエントリ数(IPマスカレードエントリ数)は購入前に確認しました。 PC1台に対して50〜200程度あれば十分だと思います。
    スループットも重要な確認ポイントになります。 こちらが低いと回線速度を遅く感じられてしまいます。 大雑把ですが、実測値で1Gbps以上出るものを選ぶと良いと思われます。

  • 無線アクセスポイント
    移転前のオフィスでは無線アクセスポイントが2つしかなく、接続先によっては遅く感じられました。
    そのため、移転先のオフィスでは無線アクセスポイントを5つに増やすことにしました。 天井に設置し、電波の強い範囲が被るようにする予定です。
    これでインターネット回線が遅いと言われないようになって欲しいです(願望)。

まとめ

今回はオフィス移転前に社内ネットワークについて検討したことを中心にまとめました。
社内要件を行い、業者さんとコミュニケーションを取りながら移転に向けて準備を進めております。
次回は移転後の話を記事にしたいと思います。

弊社では新しいオフィスで一緒に働いてくれる仲間を募集しております!
ご応募お待ちしております!!

RubyWorld Conference 2018に行ってきた

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

はじめに

スタディプラスは、RubyWorld Conference 2018Platinumスポンサーとして協賛をさせて頂きました。 それに伴って、ブースも出展いたしました。

2018.rubyworld-conf.org

島根到着からブース出展までの内容と、同行した各エンジニアの印象に残ったセッションの感想を紹介させて頂きます。

出雲縁結び空港と会場

出雲縁結び空港には、早速RubyWorld Conferenceのポスターが。

カンファレンス当日は晴天に恵まれました。(大橋川にてしじみ漁をしている模様)

企業ブース出展をする大展示場の様子。

ブース出展

スタディプラスのブースでは、Studyplusのサービスにちなんで、「好きなRubyの技術書を投票!!」という企画を実施しました。

投票してもらうRuby技術書を独断と偏見で5冊ピックアップ。

多くの方に投票を頂きました。

ブースにはMatzさんにも来て頂き、著書にサインと記念撮影をしていだきました。

セッションの感想

島田

基調講演 The Power of the Community(まつもとゆきひろ氏)

Rubyコミュニティのこれまでのヒストリーと、コミュニティの力。 個人的には「Rubyは不景気が生んだ言語」というのが刺さった。

花井

RubyによるDBスケーラビリティ

Leonard Chinさんによる、クックパッドの1000万ユーザーを支えるDB周りについての発表でした。 クックパッドさんほどではないですが、弊社もRails 3の時代からRuby on Railsによる開発を続けており、すぐに業務に役立てたいと思うTipsがある発表でした。 特に

  • ユーザーが多いので遅くなっている人を特定するのが難しい
  • データが多いので再現するのも大変

という点は大変共感できました。

発表では、実際にあったトラブルを例に

  • NewRelicのAverageResponstimeでは問題ないようにみえるレスポンスの陰にあるユーザーの体験を95パーセンタイル、99パーセンタイルも見てボトルネックの発見に至った
  • activerecord のexplainメソッドで計測した
  • indexだけで解決できない問題をRubyの積演算で解決した

など具体的な解決の手順と、そこに至る道筋を追体験できるもので、とても参考になりました。

mruby/cを用いたプログラミング教育向けデバイスの開発

牧 俊男さんによる、mruby/cでArduinoの制御をする事例の発表でした。 今回のカンファレンスには現地の高校生も参加しており、最近のプログラミング教育で具体的にどのような取り組みがされているのかを知る機会に恵まれておりました。 そんな中での本セッションは、プログラミング未経験の中高生に8時間で体験してもらうための工夫や、発展途上のmruby/cでの苦労などが紹介されていて個人的に興味深い内容でした。

Cのコードへ変換するか、直接Cのコードを書くというところにハードルを感じていたのですが、irbの感覚でデバイスの制御ができるという点に関心を持ちました。 業務ではまず使うことのないデバイスですが、この発表を見て早速Arduinoを購入しました。

石上

1日目、2日目の中から気になった講演・発表の感想などを書きます。

基調講演 The Power of the Community(まつもとゆきひろ氏)

タイトル通り、Rubyコミュニティの力についての講演でした。Rubyをデザインしたのはまつもとさんですが、Rubyがここまで大きくなるにはコミュニティの力が不可欠だったことを知ることができました。

スモウルビー3.0の開発とRubyを用いたプログラミング学習への活用

ScratchのRuby版、smalruby(スモウルビー)の開発についての発表。私はこの発表を聴くまでsmalrubyについて知りませんでしたが、発表者である島根大学の武本さんの、プログラミングを楽しむことへの情熱が伝わってきてとてもよい発表でした。Githubに公開されているので、実際に手元へcloneしてきて yarn; yarn run start したら動きました。スモウルビーはscratch-guiをforkしていて、Rubyコードの生成以外は本家と同じようです。

Scratchのようなツールは、存在自体は前々から知っていたものの、普段触ろうとする機会がないので新鮮でした。プログラミング教育の盛り上がりと同時に、こういったプログラミング体験ツールも今後いろいろ出てくると面白いなと思いました。

基調講演 Don't Stop Moving(Chad Fowler氏)

エンジニアがモチベーションを保つためにどうするかという話でした。エンジニアリングに限らず、自己啓発系の書籍や、考え方なども紹介されていました。自分が投資している技術カテゴリを意識することなど、今後のキャリアを考える上で参考になる話が多かったです。

CookpadがRubyと歩んできた10年

Cookpadで実際に存在した、過去のおもしろPull Requestが紹介されていて楽しかったです。 終わったあと、一緒に聴いていた自社のエンジニアと、うちもいろいろありそうですねという話をしました。

RubyによるIoTデバイス制御

mruby/c で下記のIoTシステムを作ったことについての発表。

組み込み系のシステムをRubyで書いた実例として面白かったです。mrubyに対して興味がわきました。

田口

一日目に印象に残った2つの発表についての感想を書きます。

Railsチュートリアル×反転授業: 解説動画を用いた能動的な学びによる驚きの効果

https://speakerdeck.com/yasslab/more-interactive-way-of-learning-rails

Railsチュートリアルを公開しているYassLab株式会社の安川さんの発表です。
個人的にRailsチュートリアルは大変お世話になったので、今回の発表は非常に楽しみでした。
発表を聞く前は「反転授業」とはどういうことかわからなかったのですが、「一斉授業」「反転授業」について以下のスライドでわかりやすく説明されています。

https://speakerdeck.com/yasslab/more-interactive-way-of-learning-rails?slide=30 https://speakerdeck.com/yasslab/more-interactive-way-of-learning-rails?slide=31

「一斉授業」は、現在の学校での授業のような体系です。講義外の課題として、宿題を解いたりします。
「反転授業」はまさに一斉授業の反転で、学校の授業でやっているような「知識のインプット」を講義外で行い、講義中は実戦形式で開発していくといった体系です。
反転授業の体系の話を聞いたとき、なるほどと感じました。自分がRailsチュートリアルを実際にやってみたり、エンジニアとして働いてみて感じたことですが、手を動かして作りながら学んでいったほうが、インプットだけするよりも効果的であると思っています。それに通ずる感覚を、講義を通じて養えると考えると非常に有意義だと思います。また、スライドにもありますが、難しくて挫折しがちな初学者が挫折しないようにすることにおいてもかなり効果的なのは素晴らしいことだと思います。
この発表を見て、学生だけでなく、新しいことを学ぶ社会人にも反転授業が有効であると感じました。料金的にお得な法人向けの動画視聴サービスを公開されているようなので、法人での導入も大いにアリなのではないかなと思います。

プログラミング入門をプロジェクトでやってみた -Rubyで取り組むプログラミング実践-

フェリス女学院大学でのRubyを用いたプログラミング授業の実例の発表でした。上記のRailsチュートリアルの次の発表だったのですが、実践的な教育の事例として関連が深い発表だと感じました。
授業の講師の方と、その授業を受けていた生徒2名の合同発表でした。授業の実際の内容の話を聞くと、4〜5人でチームを組み、チームメンバーの得意分野に応じて作業を分担したり、講師の方との頻繁なやり取りで疑問を解決したりしていて、非常に実践的だなと感じました。個人の実際の作業とチームでの作業を同時に体験できるので、かなり良い経験になるのではと思います。
生徒の方々は文系学部で、IT関連の業界や職務には詳しくなかったと聞きました。プログラミングは遠い世界にあるものという感覚を持っていたそうです。そのような人が、授業を通じてプログラミングを実際にやってみて、さらにそれをチーム全体のプロジェクトとして進める経験を積んだというのは、非常に素晴らしいことだと思います。
成果物として、Rubyで図形を描いて作成したというステッカーを配布していたので、ありがたくいただきました。素敵な授業だと思うので、今後もこの授業を受けて少しでもプログラミングに興味を持ってくれる生徒が増えるといいなと思います。

スポンサーLT

コーヒーブレイクで発表した協賛企業のショートプレゼンテーションを紹介します。

speakerdeck.com

最後に

スタディプラスとしてテック系カンファレンスでブースを出すのは初めてのことだったので、不慣れな点や反省する点がいくつかありました。 しかし色々な方に立ち寄って頂き、ユーザーの方とも触れ合う機会も得たりと実りあるものだったと思います。

今後もメンバーの技術への知見を広げる事とOSSコミュニティへの貢献のため、カンファレンスへの協力をしていきたいと考えています。

Kotlin Coroutinesを(SDKに)導入しました!

初めまして。Studyplus開発部の若宮(id:D_R_1009) です。9月よりAndroidアプリの開発を行っています。

Kotlin、いいですよね。Kotlin Coroutines、シビれますよね。
ということで、Kotlin1.3がリリースされたことを記念してStudyplus-Android-SDKにKotlin Coroutinesを早速導入した話をします。

Studyplus-Android-SDKとは

Studyplus APIの詳細はこちらをご確認ください。

Studyplus-Android-SDKはStudyplus APIを簡単にご利用いただくためのSDKです。社内のAndroidチームメンバーにより開発・メンテナンスされています。
2018年の8月にフルスクラッチを行い1系から2系にバージョンが大きく上がりました。なお、2系からKotlinを利用しています。

Githubリポジトリ : https://github.com/studyplus/Studyplus-Android-SDK-V2

SDKは次の2つの機能を提供しています。

  • Studyplus APIによる学習記録の投稿
  • Studyplus AndroidアプリによるStudyplusとの連携

この学習記録の投稿処理をRxJava/RxAndroidからKotlin Coroutinesへ切り替えたv2.1.0系のリリースを、2018年10月31日に行いました。今回はこのRxJava/RxAndroidをKotlin Corutinesへ置き換えた経緯、置き換えの所感について書いていきたいと思います。

なぜRxJava/RxAndroidをやめてKotlin Coroutinesへ置き換えたのか

stable版がリリースされて嬉しかった 開発チーム内で上がっていた「RxJava/RxAndroidの利用は色々と過剰なのでは」という疑念を解消するためになります。

疑念1 : 利用シーンに対して

SDK内で行う通信処理は、主にサーバーのStudyplus APIを叩き学習記録を投稿する、というシンプルなものになります。そのため、本来Streamを扱うためのRxJavaであるのにOne-Shotな処理を行うに止まっていました。
SDKの基本機能としては現在のSDKでほぼ要求は満たされていると考えており、これ以上Streamを必要とする処理が追加される見込みもありません。

疑念2: SDKが持つ依存関係に対して

SDKを使っていただくデベロッパーの視点に立った時、RxJava/RxAndroidへの依存をSDK側で作成してしまうことに対してセンシティブになる必要があると思っています。
もちろんRxJava/RxAndroidは多くのプロダクトで利用されており、近年では当たり前な選択肢になっています。しかし、Androidの標準ライブラリに組み込まれるには至っていません。

Kotlin Coroutinesでは

Coroutinsのasync/awaitは1回の通信処理に対して1つのCoroutinesとなるため、処理と実装が必要十分な関係性になります。その意味で、SDK内の処理を整理し非同期処理をasync/awaitに統一することは、非常に理にかなった行為であると判断しました。

依存関係については少しチーム内で議論するところなり、はっきりとは解決しませんでした。
ご存知の通りKotlin Coroutinesの導入にはGradle上で依存関係を追加する必要があります。その点において、RxJava/RxAndroidを利用している問題は解決していません。
現時点での結論としては、SDK v2ではKotlinを記述言語として採用していることを重視しました。 RxJva/RxAndroid(+RxKotlin)を利用するよりもKotlin Coroutinesを利用する方が、はるかに依存関係がシンプルであると判断しています。

RxJava/RxAndroidとRxKotlinの比較

RxJava

// API通信
internal interface ApiService {
    @Headers(value = [
        "Accept: application/json",
        "Content-type: application/json"
    ])
    @POST("/v1/study_records")
    fun postStudyRecords(
            @Header("Authorization") oauth: String,
            @Body studyRecord: StudyRecord)
            : Observable<PostStudyRecordsResponse>
}

~~~

// 投稿処理
fun postRecord(context: Context, studyRecord: StudyRecord, listener: OnPostRecordListener?) {
    if (!isAuthenticated(context)) {
        throw IllegalStateException("Please check your application's authentication before this method call.")
    }

    ApiClient.apiClient.postStudyRecords(context, studyRecord)
            .subscribeOn(Schedulers.newThread())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(
                    { listener?.onResult(success = true, recordId = it.recordId) },
                    { listener?.onResult(success = false, throwable = it) }
            )
}

Kotlin Coroutines

// API通信
internal interface ApiService {
    @Headers(value = [
        "Accept: application/json",
        "Content-type: application/json"
    ])
    @POST("/v1/study_records")
    fun postStudyRecords(
            @Header("Authorization") oauth: String,
            @Body studyRecord: StudyRecord
    ): Deferred<PostStudyRecordsResponse>
}

~~~

// 投稿処理
fun postRecord(context: Context, studyRecord: StudyRecord, listener: OnPostRecordListener?) {
    if (!isAuthenticated(context)) {
        throw IllegalStateException("Please check your application's authentication before this method call.")
    }

    runBlocking {
        try {
            val deferred = ApiClient.postStudyRecords(context, studyRecord)
            val result = deferred.await()
            listener?.onResult(success = true, recordId = result.recordId)
        } catch (t: Throwable) {
            listener?.onResult(success = false, throwable = t)
        }
    }
}

返りの型が変わるだけなので簡単ですね。Singleを使う程度の通信なら、書き換えのコストは非常に低く済みそうです。

まとめ

Coroutinesの導入

通信処理への導入は低コストであると実感しました。 そのためStudyplus Androidアプリへの導入を進めたいなと感じています。 特に新規に追加する機能はCoroutinesを積極的に導入をしようと話をしています。

ひとまず足がかりとして kotlinx-coroutines-rx2 の導入を着々と進めています。こちら、成果が出たら本ブログで公開いたします。

SDKの今後

より依存関係を減らし、軽量なライブラリを目指していきたいと思っています。
GsonやRetrofit、Retrofit-Kotlin-Coroutines-Adapterの依存関係は早期に解消していきます。

今回の置き換えにおいてはRetrofitによる簡潔なインタフェースがあったから迅速に対応できました。 しかしこれらの利便性と何を引き換えにしているか、常に検討しながら開発をしていくべきだとも感じる機会になりました。 今後とも、Studyplus-Android-SDKをよろしくお願いいたします。

最後に

一緒にKotlin Coroutinesを使って開発をしたい方はこちらからぜひご応募ください! 開発部一同、お会いできる機会を心よりお待ちしております。

Sidekiq Enterpriseを使う

お久しぶりです。サーバーサイドエンジニアの花井です。

みなさんは非同期処理に何をお使いですか? ActiveJobでしょうか? Resqueでしょうか? Sidekiqでしょうか?

弊社では歴史的経緯から、上記全てのgemをプロダクトで使っていますが、 この度Sidekiq Enterpriseを導入して非同期処理の統一に着手しました。 Sidekiq Enterpriseの日本語記事があまりなかったので、利用の検討や実際に使う際の注意点などを紹介できればと思います。

What is Sidekiq

SidekiqはRuby製の非同期処理フレームワークです。 resqueを使うと、ジョブのリトライとユニーク性の担保のために自前で実装が必要でつらいので、sidekiqの採用に至りました。

また、非同期処理の流量を制限したい(特に外部サービスへの問い合わせが発生する所)という意図があり、Enterpriseを採用するに至りました。

今回は以下の2機能を使ったので、その設定方法を紹介します。

  • RATE LIMITING
  • PERIODIC JOBS

How to use

install

ここから申し込みをすると利用に必要なキーの情報が送られてきます。 Gemfileに指定された内容を追記して、$ bundle install します。

Enterpriseを入れるとき、同時にProのインストールもします。 Enterpriseを申し込むとProの機能も使えるようになるというのは、こういうカラクリのようです。

Implementation

Wikiが充実しているので、そちらを参考にすればほとんど迷うことなく設定できます。

ActicveJobなどと併用している場合、Sidekiq単体(include Sidekiq::WorkerしたWorker)に変更する必要があるようです(Wikiに例がなかったのと、resqueueからの置き換えだったこともあり、Sidekiq単体で使うように書き換えていますがもしかしたらできるかもしれません)。

RATE LIMITING

大量の子ワーカーを作るような非同期処理1つ1つの中で、APIを叩くようなケースでは、許容量を超えたリクエストをしてしまうことがあると思います。そんな時に指定した量を超えないように制限できる機能がRate Limitingです。 ※Redis 2.8以上が必要です。

弊社ではサブシステムから大量のアプリ内メッセージを送信する際に、APIサーバーへ負荷をかけないようにすることを目的として使っています。

特別な設定は必要なく、流量を制限したいWorkerに、以下のような設定を記述します。

class HogeWorker
  include Sidekiq::Worker
  DELIVER_LIMIT = Sidekiq::Limiter.concurrent('limitter_name', 50, wait_timeout: 5, lock_timeout: 30)

  def perform
    DELIVER_LIMIT.within_limit do
          # 必要な処理
    end
  end
end

Sidekiq::Limiter.concurrent には、リミッター名、同時実行数、ロック空き待ち時間(秒)、ロックを手放す時間(秒)を指定できます。 Web UIにタブが追加され、そこからどのくらいロック待ちが起きたかなどのメトリクスを確認することができますので、チューニングも簡単にできます。

PERIODIC JOBS

非同期処理の定期実行をcron形式で指定できます。Sidekiqを使う場合、sidekiq-cronを使う方も多いと思いますが、Enterpriseにすると標準装備されています。 弊社では集計処理など、ピークタイムを避けて深夜に行いたい処理の実行に活用しています。

/config/initializer/sidekiq.rb の中に以下のように実行を開始する時間と、対象のWorkerを指定します。 cronに設定する時間は内部的にTimeクラスで操作されるので、システムのタイムゾーンに合わせて記述する必要があります。resque-schedulerを使っているとタイムゾーンも指定できるので、システムのタイムゾーンと異なるタイムゾーンを扱っている場合は注意が必要です。

Sidekiq.configure_server do |config|
  config.periodic do |mgr|
    mgr.register('0 20 * * *', Hoge::HardWorker)
    mgr.register('0 21 * * *', FugaWorker)
    mgr.register('0 23 * * *', Piyo::PiyoWorker)
  end

まとめ

プッシュ通知の送信が重複しないように、次はUnique Jobsを利用予定です。 他にも色々な機能があるので、必要に応じて活用していきたいです。

一緒にプロダクトの改善をやってくれる仲間を募集しています!

おまけ

Enterpriseの効果かわかりませんが、設定中に相談のIssueを立ててたところ早めに返事がもらえました。 また、確認の過程で翻訳のIssueが見つかったらしく、コントリビュートもできました。

Google Cloud Functions を利用して社内コミュニケーションを円滑にしていく

はじめに

こんにちはスタディプラスでCTOしています島田です。

現在スタディプラスには新メンバーが続々と入社して、うれしい悲鳴があがっています。 その悲鳴にほんのり応えた例の紹介です。

課題

  • Slackチャンネルが乱立(186のパブリックチャンネル)。そのため新メンバーが、どのチャンネルでどんな目的の会話をしているかを把握する事が困難
  • 上記の理由により、質問や必要な情報を取得するチャンネルを探すコストが上がっていた

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

理想状態

  • 新メンバーが必要なSlackチャンネルを、迷う事なくすぐに見つけられ発言ができることで業務を円滑に遂行することができる

アプローチ案

理想状態を実現する方法を幅広く考える。

  • A) 全てのチャンネルを把握したメンバーを養成し、新メンバーが必要なチャンネルを聞く
    • =>❌ : 属人性・コストが高い
  • B) 定期的に不要なチャンネルを精査する
    • => ⭕️

定期的に不要なチャンネルを精査する

  • A) 毎月不要なチャンネルかどうかを聞いてまわってアーカイブする
    • =>❌ : コストが高い
  • B) 人力でSlackのアナリティクスから「最後にアクティブだった日」をCSVエクスポートして、そのCSVを利用してプログラマブルなアプローチをする
    • =>❌ : 人力要素がある。CSVだとチャンネルIDがエクスポートされないで、扱いが複雑になりそう
  • C) 不要なチャンネルを「最終投稿から一定期間投稿がない」と一旦定義して、Web APIを利用して過疎化しているチャンネルを探す。それを自動化して定期実行をする
    • => ⭕️

Web APIを利用したアプローチ

このアプローチだけでは「理想状態」へ到達する事はもちろんできないです。しかしそのためのファーストステップとして、まず明らかに不要と思われるチャンネルのアーカイブを自動化するというアイデアは良いかと思いました。

今回やること

  • 不要なチャンネル(30日間投稿がない)をアーカイブする
    • ただしパブリックチャンネルでも_で始まる業務以外のチャンネルは除く。例) #_boardgame
  • アーカイブ処理を自動化して定期実行をする

実現方法

で、実現方法を検討。 - A) チャンネルをアーカイブするプログラムを自分のPCでcrontabを実行する - =>❌ : 属人性が高い - B) チャンネルをアーカイブするプログラムを外部サービスを利用して定期実行 - => ⭕️

大きな課題解決に対する第一歩的でありお試しなので、限りなく運用負荷を低くしたいと思い、以下の技術を利用することにしました。

手順

  1. GCPの事前準備
  2. SalckのTokenを生成
  3. 実装
  4. Cloud Functions デプロイ
  5. Google Apps Scriptで定期実行を登録する

GCPの事前準備

HTTP のチュートリアル の「始める前に」を参考に事前準備。

SalckのTokenを生成

https://api.slack.com/apps より「Creaet New App」からAppをつくりトークンを生成 参考: Slack API 推奨Tokenについて

実装

プログラムの構成

  1. channels.list でチャンネル一覧を取得。
  2. _で始まるチャンネル以外を抽出
  3. 最終投稿を知るために、channels.history で履歴を取得。( channels.info だとAppを作成したユーザーが参加しているチャンネルでないとlatestが取得できなかったため )
  4. 各チャンネルの最終投稿が30日以上経過しているかチェック
  5. 対象となったチャンネルをchannels.archive でアーカイブを実行
  6. chat.postMessage で完了を通知

もろもろ準備

Node Slack SDK をインストール

$ npm init
$ npm install @slack/client -s

アーカイブをする処理を実装

$ vim index.js
const { WebClient } = require('@slack/client');
const token = process.env.SLACK_TOKEN;
const web = new WebClient(token);

function archiveChannels(intervalDays) {
  web.channels.list({ exclude_archived: true })
    .then((res) => {
      const publicChannels = res.channels.filter(c => c.name.indexOf('_') != 0 );
      publicChannels.forEach(c => archiveForlornlyChannel(c, intervalDays));
      // 通知するチャンネルのID
      postMessage('CXXXXXX', 'SlackArchiveを実行しました');
    })
    .catch(console.error);
}

function archiveForlornlyChannel(channel, intervalDays) {
  web.channels.history({ channel: channel.id })
    .then((res) => {
      const ts = res.messages[0].ts;
      const latest = new Date(parseFloat(ts * 1000));
      const now = new Date();
      const diff = now.getTime() - latest.getTime();
      const days = Math.floor(diff / (1000*60*60*24));
      if (days > intervalDays ) {
        console.log(channel.id, channel.name, days);
        archiveChannel(channel);
      }
    })
    .catch(console.error);
}

function archiveChannel(channel) {
  web.channels.archive({ channel: channel.id})
    .then((res) => {
      console.log(res);
    })
    .catch(console.error);
}

function postMessage(channel_id, text) {
  web.chat.postMessage({ channel: channel_id, text: text })
    .then((res) => {
      console.log(res);
    })
    .catch(console.error);
}

exports.ArchiveChannel = (req, res) => {
  archiveChannels(30);
  res.status(200).send('OK');
};

デプロイ

関数をデプロイ

$ gcloud beta functions deploy ArchiveChannel --trigger-http

Slackトークンの環境変数をデプロイ

$ gcloud beta functions deploy ArchiveChannel --set-env-vars SLACK_TOKEN=Slackのトークン

Google Apps Scriptで定期実行を登録する

Google Apps Script(以下GAS)にプロジェクトを作って、定期実行を登録する。

参考:無料でお手軽Cron!Google Apps Scriptを使ってみる

  • GASにプロジェクトを作って、関数をコールするScriptを実装
  • 定期実行をするトリガーを登録
function fetchToSlackArchive() {
  UrlFetchApp.fetch("https://ここに関数URL");
}

実行

  • 設定した時間内で実行がされること(Slackに通知が来ること)を確認

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

  • GCPのログビューアでみると55チャンネルがアーカイブされました

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

  • Slakのアナリティクス

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

まとめ

Cloud Functions を利用して簡単に開発と実行を実現する事ができました。 本質的な課題解決までは遠いですが、まずはその橋頭堡となる仕組みが出来たのではないかと思います。

新しい概念・技術をいきなりプロダクション環境で導入するには腰が重くなってしまいがちですが、社内のプロセスを改善するためにサクッとお試し導入することで、その技術の良さや導入ハードルを経験する事ができたと思います。 スタディプラスでは社内システムやプロダクション環境で積極的に新しい取り組みや技術の活用をして行くことを検討しています。 こういった取り組みに共感ができる方、是非一緒に働きませんか! こちらよりお待ちしております。

elm-upgradeに従ってElmのバージョンを0.18から0.19へ上げる

ForSchool事業部の石上です。ウェブのフロントエンドを中心にStudyplus for Schoolの開発に携わっています。

あるアプリケーションのElmのバージョンを0.18から0.19に上げる対応をしました。今回はこのことについて書きます。

背景

Studyplus for School で新たにちょっとしたサブシステムが必要になり、その小さなSPAのためのウェブフロントエンドの言語として今回、Elmを採用しました。

弊社のプロダクトのほとんどは、サーバーサイドはRuby on Railsで作られており、ウェブフロントエンドはJavaScriptかTypeScriptです。Elmでウェブのフロントエンドを書くことはけっこう挑戦的でしたが、会社としても他の言語に手を伸ばしていきたいという話もあり、CTOとチームリーダーから許しをいただきElmでの実装に至りました。

ある程度実装が終わった段階でElm 0.19のリリースが発表され、これまでのElmの仕様から大きく変更がありました。主要パッケージの移動、関数の仕様変更の数々...。しかしこれはある程度覚悟していたことです。今回実装したものがまだリリース前かつ小規模なアプリケーションだったので、1日くらいガッとやれば対応できるだろうということで、対応しました。

対応手順

  • Elmのバージョンを上げる
  • elm-upgradeを実行
  • elm-upgradeで自動修正されない部分を手で書き換え

Elmのバージョンを上げる

yarn upgrade elm@0.19-bugfix2

elm-upgradeを実行

elm-upgradeというツールがあるので、それを使います。

npx elm-upgrade

elm-upgradeを実行すると、修正の方針について何点か質問されるのでそれに答えながら修正を実行していきます。

INFO: Found elm at node_modules/.bin/elm
INFO: Found elm 0.19.0
INFO: Found elm-format at node_modules/.bin/elm-format
INFO: Found elm-format 0.8.1
INFO: Cleaning ./elm-stuff before upgrading
INFO: Converting elm-package.json -> elm.json
INFO: Detected an application project (this project has no exposed modules)
INFO: Installing latest version of elm-community/list-extra
Here is my plan:

  Add:
    elm-community/list-extra    8.1.0
Would you like me to update your elm.json accordingly? [Y/n]:
  • elm-stuffを消す
  • elm-package.jsonからelm.jsonへ移行する
  • 最新のelm-community/list-extraをインストールする

と言われています。

Elm 0.19からはパッケージの依存関係を記録するファイルが elm-package.json から elm.json になっています。 インストールしたモジュールのコードはelm-stuff から ~/.elm になりました。

elm-upgrade がこの移行をやってくれます。パッケージを入れ直そうとしているので、そのための質問が続きます。全部Yesと答えました。

完了すると、以下のメッセージが表示されます。

SUCCESS! Your project's dependencies and code have been upgraded.
However, your project may not yet compile due to API changes in your
dependencies.

See <https://github.com/elm/compiler/blob/master/upgrade-docs/0.19.md>
and the documentation for your dependencies for more information.

Here are some common upgrade steps that you will need to do manually:

- elm/core
  - [ ] Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate
- undefined
  - [ ] Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/
  - [ ] Replace uses of Date and Time with Time.Posix
- elm/html
  - [ ] If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document
  - [ ] If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox
- elm/browser
  - [ ] Change code using Navigation.program* to use Browser.application
  - [ ] Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward
- elm/url
  - [ ] Changes uses of Navigation.Location to Url.Url
  - [ ] Change code using UrlParser.* to use Url.Parser.*

elm-upgradeで自動修正されない部分を手で書き換え

上記のメッセージの通り、以降はソースを手で直していきます。親切にリストになっているので、これを上から潰していけばいいでしょう。

- elm/core
  - [ ] Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate
- undefined
  - [ ] Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/
  - [ ] Replace uses of Date and Time with Time.Posix
- elm/html
  - [ ] If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document
  - [ ] If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox
- elm/browser
  - [ ] Change code using Navigation.program* to use Browser.application
  - [ ] Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward
- elm/url
  - [ ] Changes uses of Navigation.Location to Url.Url
  - [ ] Change code using UrlParser.* to use Url.Parser.*

Replace uses of toString with String.fromInt, String.fromFloat, or Debug.toString as appropriate

2件ありました。以下のように修正しました。

- toString 1000
+ String.fromInt 1000

Read the new documentation here: https://package.elm-lang.org/packages/elm/time/latest/

https://package.elm-lang.org/packages/elm/time/latest/ を読むと、Elm 0.19においての時間の取扱について書かれています。Daylight Saving Timeの話を出したりしつつ、人間用の時刻をモデルやデータベースに持たせるんじゃない! ということが書かれています。データとしてはPOSIXタイムとタイムゾーンとして扱い、人間用の表現はモデルではなくビューの関数の中でやるんだぞというようなことが書かれています。なるほど。

Replace uses of Date and Time with Time.Posix

0.19ではそのような考えがモジュールに反映されています。これまでの0.18ソースで使っていたDateとTimeをTime.Posixに置き換える必要があります。

Time.everyしか使っていなかったので特に変更は必要ありませんでした。

If you used Html.program*, install elm/browser and switch to Browser.element or Browser.document

使っていないので関係ありませんでした。

If you used Html.beginnerProgram, install elm/browser and switch Browser.sandbox

使っていないので関係ありませんでした。

Change code using Navigation.program* to use Browser.application

今回このアプリケーションはSPAとして実装、つまりページを読み込み直さずに状態とURLを書き換えてページ遷移を行いたいと考えていました。このフロントエンド側でのルーティングの機能は、Elm以外の言語・ライブラリでもたいてい何かしらの形で提供されています(ReactならReact Routerなど)。

Elm 0.18では「クリック時のデフォルト挙動を無効化しつつ、引数でメッセージを渡して副作用を起こす」みたいなヘルパーを書いてこれを実現していました。 こういう感じです

Elm 0.19ではこんなことをしないでもこの機能を実装することができるようになったようです。

Browser.applicationを見ると、関数シグネチャはこうなっています。

application :
    { init : flags -> Url -> Key -> ( model, Cmd msg )
    , view : model -> Document msg
    , update : msg -> model -> ( model, Cmd msg )
    , subscriptions : model -> Sub msg
    , onUrlRequest : UrlRequest -> msg
    , onUrlChange : Url -> msg
    }
    -> Program flags model msg

applicationに渡すレコードの中に、onUrlRequestonUrlChangeがあります。名前からしてこれらを使えば良さそうです。それぞれ説明を読んでみます。

onUrlRequest

When someone clicks a link, like <a href="/home">Home</a>, it always goes through onUrlRequest. The resulting message goes to your update function, giving you a chance to save scroll position or persist data before changing the URL yourself with pushUrl or load. More info on this in the UrlRequest docs!

リンクのクリック時にデフォルトでonUrlRequestメッセージが発行されるとのこと。ありがたい!

onUrlChange

When the URL changes, the new Url goes through onUrlChange. The resulting message goes to update where you can decide what to show next.

↑URLが変更されるとonUrlChangeになるとのこと。

UrlRequest Browser.UrlRequestUrlChange Url.Urlというメッセージを用意してあるとすると、こういう感じになります。

    Browser.application
        { view = view
        , init = init
        , update = update
        , subscriptions = subscriptions
        , onUrlRequest = UrlRequest
        , onUrlChange = UrlChange
        }
update msg model =
        UrlRequest urlRequest ->
            case urlRequest of
                Browser.Internal url ->
                    ( model, Navigation.pushUrl model.nav.key (Url.toString url) )

                Browser.External url ->
                    ( model, Navigation.load url )

        UrlChange url ->
            ({ model | url = url }, Cmd.none)

Use the Browser.Key passed to your init function in any calls to Browser.Navigation.pushUrl/replaceUrl/back/forward

Elm 0.18でページ遷移したいときは、Navigation.newUrl "/hoge" みたいな形でできました。0.19のBrowser.NavigationにはnewUrlは無く、見てみるとpushUrlを使えば良いようです。

しかし遷移先の文字列をただ渡すのではなく、Browser.Keyなるものを渡す必要があるみたいです。

pushUrl : Key -> String -> Cmd msg

このKeyは外から書き換えたり出来ない値で、initで入ってくるものをModelに持っておいて、pushUrl などURL変更の関数に渡して使います。

You only get access to a Key when you create your program with Browser.application, guaranteeing that your program is equipped to detect these URL changes. If Key values were available in other kinds of programs, unsuspecting programmers would be sure to run into some annoying bugs and learn a bunch of techniques the hard way!

Changes uses of Navigation.Location to Url.Url

言われている通り修正しました。

Change code using UrlParser. to use Url.Parser.

言われている通り修正しました。

その他

elm installできない?

Elmのモジュールの依存関係はnpmなどと同様、1つのJSONファイルに記録されます。しかし npm install で全部ダウンロードというようなことはできません。elm 0.19で elm install を実行すると以下のメッセージが表示されます。

~/project (feature/update-elm-to-0.19 *)$ elm install
-- INSTALL WHAT? ---------------------------------------------------------------

I am expecting commands like:

    elm install elm/http
    elm install elm/json
    elm install elm/random

Hint: In JavaScript folks run `npm install` to start projects. "Gotta download
everything!" But why download packages again and again? Instead, Elm caches
packages in /Users/ishigami/.elm so each one is downloaded and built ONCE on
your machine. Elm projects check that cache before trying the internet. This
reduces build times, reduces server costs, and makes it easier to work offline.
As a result elm install is only for adding dependencies to elm.json, whereas
elm make is in charge of gathering dependencies and building everything. So
maybe try elm make instead?

パッケージは./elm-stuff ではなく、 ~/.elm にキャッシュされます。

Variable Shadowing

こんなエラーも出てきました。

./src/Main.elm
[=========================                         ] - 1 / 2-- SHADOWING --------------------------------------------- src/Views/Hoge.elm

The name `hoge` is first defined here:

23| viewHogeName hoge =
                    ^^^^^^^
But then it is defined AGAIN over here:

25|         Just hoge ->
                 ^^^^^^^
Think of a more helpful name for one of them and you should be all set!

Note: Linters advise against shadowing, so Elm makes “best practices” the
default. Read <https://elm-lang.org/0.19.0/shadowing> for more details on this
choice.
Detected errors in 1 module.

怒られている通り、こう直します。

viewHogeName maybeHoge =
    case maybeHoge of
        Just hoge ->
            hoge.name

        Nothing ->
            ""

エラーメッセージの通り、なんでこう直さないと怒られるんだというのは以下に書かれています。

Regex

I cannot find a `Regex.regex` variable:

330|             Regex.regex "hogehoge"
                 ^^^^^^^^^^^
The `Regex` module does not expose a `regex` variable. These names seem close
though:

    Regex.never
    Regex.find
    Regex.replace
    Regex.split

Hint: Read <https://elm-lang.org/0.19.0/imports> to see how `import`
declarations work in Elm.

Regexも変わっていました。今回エラーが出た Regex.regexRegex.fromString になったようです。以下のような変更が必要でした。

removeHogehoge hogehoge =
    let
         regex =
-            Regex.regex "hogehoge"
+            Regex.fromString "hogehoge"
        in
-            Regex.replace Regex.All regex (\{ match } -> "") hogehoge
+            Regex.replace regex (\{ match } -> "") hogehoge

さらにその他

非標準ライブラリにもElm 0.19で変わったものがあり、その対応も必要でした。

所感

変更範囲はかなり大きいので、大きめのアプリケーションを0.19対応するのは相当大変そうです。

しかし、elm-upgradeが最初にやることリストを出してくれたりコンパイルエラーにここ読めリンクが適切に貼られていたりと、とても親切に感じました。

変更点についても、Browser.applicationで簡単にSPAを作れるようになっていたりして、SPAのフレームワークとして改善されていると感じました。

参考記事

UIPickerViewをUIControlを使用してキーボードの様に表示する

こんにちは。 入社して一ヶ月が経過したiOSエンジニアの弘田です。
今回はUIPickerViewをキーボードの様に表示する方法を解説します。

なぜそんなことをするの?

昔のiPhoneでしたら画面の中心などにUIPickerViewを表示しても画面サイズが小さかったので片手で操作できていましたが、最近は大画面化が進み片手での操作が難しくなってきました。
操作性を損なわずにUIPickerViewを使用してもらう為にも今回の方法が役にたつと思います。

Human Interface GuidelinesでもPickerついて触れているページがありこの様な記載があります。

Avoid switching screens to show a picker. A picker works well when displayed in context, below or in close proximity to the field being edited.

翻訳
ピッカーを表示するように画面を切り替えることは避けてください。ピッカーは、編集中のフィールドの下、または近くにコンテキストで表示されたときにうまく機能します。

今回目指すもの

※シミューレーターなのでキーボードを閉じるアニメーションが少しおかしいです

実装

1. UIControlを継承したclassを作る

class pickerKeyboard: UIControl {

}

2.イニシャライザを作成し、自身がタップされた時にinputViewを出す処理を作る

class pickerKeyboard: UIControl {
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        addTarget(self, action: #selector(tappedPickerKeyboard(_:)), for: .touchDown)
    }
    
    @objc private func tappedPickerKeyboard(_ sender: PickerKeyboard) {
        self.becomeFirstResponder()
    }
}

3.canBecomeFirstResponderをtrueで返して自身をFirstResponderにする

canBecomeFirstResponderのデフォルトはfalseになっていて、
trueを返さないと後述のinputViewで指定したViewが表示されません。

class pickerKeyboard: UIControl {
    
    //~~~省略~~~
    
    override var canBecomeFirstResponder: Bool {
        return true
    }
    
}

4.FirstResponderになった上でinputViewをoverrideする

ここでは表示したいViewを返します。
今回はUIPickerViewをaddSubviewしたUIViewを返します。
UIViewを返す理由はSafeAreaに対応するためです。
inputViewについて(Apple公式)

class pickerKeyboard: UIControl {

    //~~~省略~~~

    override var inputView: UIView? {
        let pickerView: UIPickerView = UIPickerView()
        pickerView.delegate = self
        pickerView.dataSource = self
        pickerView.backgroundColor = UIColor.white
        pickerView.autoresizingMask = [.flexibleHeight]
        
        // SafeArea対応をする為にUIViewを挟む
        let view = UIView()
        view.backgroundColor = .white
        view.autoresizingMask = [.flexibleHeight]
        view.addSubview(pickerView)
        
        pickerView.translatesAutoresizingMaskIntoConstraints = false
        pickerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        pickerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        pickerView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor).isActive = true
        
        return view
    }
}

5.inputAccessoryViewをoverrideしてUIPickerViewを閉じるボタンを作る

class pickerKeyboard: UIControl {

    //~~~省略~~~

    override var inputAccessoryView: UIView? {
        
        let view = UIVisualEffectView(effect: UIBlurEffect(style: .extraLight))
        view.frame = CGRect(x: 0, y: 0, width: frame.width, height: 44)

        let closeButton = UIButton(type: .custom)
        closeButton.setTitle("閉じる", for: .normal)
        closeButton.sizeToFit()
        closeButton.addTarget(self, action: #selector(tappedCloseButton(_:)), for: .touchUpInside)
        closeButton.setTitleColor(UIColor(red: 0, green: 122/255, blue: 1, alpha: 1.0), for: .normal)

        view.contentView.addSubview(closeButton)
        
        closeButton.translatesAutoresizingMaskIntoConstraints = false
        closeButton.widthAnchor.constraint(equalToConstant: closeButton.frame.size.width).isActive = true
        closeButton.heightAnchor.constraint(equalToConstant: closeButton.frame.size.height).isActive = true
        closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 5).isActive = true
        closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16).isActive = true

        return view
    }

    @objc private func tappedCloseButton(_ sender: UIButton) {
        resignFirstResponder()
    }
}

6.通常のUIPickerViewと同様にUIPickerViewDelegateとUIPickerViewDataSourceを継承してデータを表示

class pickerKeyboard: UIControl {
    let array:[String] = ["A","B","C","D","E"]
        
    //~~~省略~~~
}

extension PickerKeyboard: UIPickerViewDelegate, UIPickerViewDataSource {
    
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return array.count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return array[row]
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        // delegateなどでViewControllerに選択された情報を渡す
    }
}

7.StoryboradやXibでUIViewのCustomClassとして設定する

まとめ

手順4のUIPickerViewを作った時にpickerView.backgroundColor = UIColor.whiteとしているのでわかり難いですが、 別の色に変更するとSafeArea対応できていることが確認できます。

記事で紹介したコードはGithubで公開しています。
https://github.com/srknra/PickerKeyboard

今回はUIPickerViewをキーボードの様に表示してUXを低下させない様な工夫でしたが、
Studyplusのアプリでは他にもユーザーの事を考えて様々な工夫をしてるので今後もブログで紹介していこうと思います。