Studyplus Engineering Blog

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

WorkManager + Dagger2によるバックグラウンド処理

こんにちは、Androidチームの若宮(id:D_R_1009)です。 昨年末にAndroidチームが導入した、WorkManagerをDagger2と組み合わせる方法を紹介します。

WorkManagerとは

developer.android.com

WorkManagerは、確実に実行したい非同期処理に対して利用するAndroid Architecture Componentsとなります。 概要についてはDroidKaigi 2019の「実践 WorkManager」をご一読ください。

speakerdeck.com

また、CodeLabも用意されています。

codelabs.developers.google.com

WorkManager + Dagger2(DI)

StudyplusのAndroidアプリではDagger2を活用しているため、WorkManagerとDagger2を組み合わせる必要があります。 検索してみると、次の記事で組み合わせ方が紹介されていました。

proandroiddev.com

もともとAssistedInjectを利用していたこともあり、おおよそこの方法で導入することはできそうでした。 ただ、できれはWorkManagerも provide メソッドによる管理を行いたくなります。

と言うことで、対応していきます。

provide 対応

WorkManagerはインスタンス化する処理が(特に指定しなければ)デフォルトのものが利用されるため、この処理を切り替える必要があります。 対応したのがWorkManager 2.1のため、ドキュメントに従って AndroidManifest.xml に記述を加えます。

developer.android.com

この対応により、 WorkManager.getInscance(context) する前に Configuration.Provider をセットすることができます。

@Provides
@Singleton
fun provideWorkManager(
    context: Context,
    factory: WorkManagerFactory
): WorkManager {
    WorkManager.initialize(context, Configuration.Builder().setWorkerFactory(factory).build())

    return WorkManager.getInstance(context)
}

あとは記事の通り、諸々のモジュールをセットすれば完了です。

@Module(includes = [PresenterModule::class, WorkerBindingModule::class])
object WorkManagerModule {

    @Provides
    @Singleton
    fun provideWorkManager(
        context: Context,
        factory: WorkManagerFactory
    ): WorkManager
}

@Module(includes = [AssistedInject_PresenterModule::class])
@AssistedModule
internal interface PresenterModule

@MapKey
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class WorkerKey(val value: KClass<out ListenableWorker>)

@Module
interface WorkerBindingModule {
    @Binds
    @IntoMap
    @WorkerKey(HogeWorker::class)
    fun bindHogeWorker(factory: HogeWorker.Factory): ChildWorkerFactory
}

利用状況、所感

StudyplusのAndroidアプリでは、WorkManagerをRepository層とViewModel層の間にある概念として利用しています。 このため、DIによりViewModel層からWorkManagerを呼び出し、WorkManagerのWokerにRepositoryをInjectして非同期処理を実行する構成となります。

アプリケーションの各種ライフサイクルに影響を受けずに非同期処理を実行することができ、開発のしやすさが高まったと感じています。 アーキテクチャとしてMVVMを採用しているチームにおいては、有用な選択肢となるのではないでしょうか。

終わりに

簡単ではありますがWorkManagerとDagger2を組み合わせる方法の紹介と、その効果をまとめてみました。 WorkManagerはKotlin Coroutinesを利用することができるため、Repositoryの各メソッドをsuspend関数にしておくだけで、簡単に呼び出すことができます。

2020年も引き続き、よろしくお願いいたします。

EOF 2019に参加しました(イベントレポート)

こんにちは、スタディプラス iOSチームの大石(id:k_oishi)です。 2019/10/31に開催されたEOF 2019に参加しました。

f:id:k_oishi:20191226142847j:plain:w300

EOFとはEngineering Organization Festivalの略で最近役職として注目されているEngineering Manager(略してEM)のためのカンファレンスです。 以前、EM的な役職だったこともあり、最近のEM界隈にも興味がありましたので参加してきました。

オープニングトークではEngineering Managerに興味を持つ参加者同士で自己紹介や現在持っている課題などを共有しましょうという時間がありました。 偶然、隣にいらっしゃった方がプログラミング学習の際に弊社Studyplusを使用しているとのことでうれしい出会いとなりました。

それでは、個人的に気になったセッションの紹介と感想です。

「質とスピード」

和田卓人さん

TDDでおなじみ和田卓人さんによるセッションです。
※詳しい内容はセッションのスライドをご覧ください

このセッションはタイトルのとおり「質とスピード」がテーマになっています。 今回の講演が初演ということで貴重な機会だったかと思います。

初めに、与えられた時間に対しやるべきことが多い場合に品質を犠牲してしまうケースが多いが、品質を犠牲にすればスピードが得られるのか?という問いから始まりました。 結果としては、短期的にスピードは得られますが、長期的には逆効果になるというということでした。

スピードを優先して品質を犠牲にした結果、内部品質といわれる部分が影響を受けます。 内部品質はテスト容易性、理解容易性、変更容易性で構成されています。(これらをまとめて保守性ともいう) これらを犠牲にするとプロジェクトにどのような影響を及ぼすかは、ある程度の経験者であれば容易に想像できると思います。

次にスピードを落とせば保守性は上がるのか?という問いがありました。 作業時間が少なくても品質の高いコードを書く人がいれば、作業時間が十分にあっても品質の低いコードを書く人もいるからです。

つまり、質とスピードはトレードオフではなく、品質をアップするためのコストをかける必要があるということです。 ここでは品質アップの2つの考え方としてコストアップ説とコストダウン説が図解で紹介され興味深いものでした。

結論として長期的に見れば質がスピードを生むのであって、そのスピードがさらなる質を生み、そのループのなかで外部品質を生み、サービスの競争力を生み、売り上げを生むという関係があったのです。 プロジェクトによっては品質を犠牲にしてリリースを優先する場合もあると思います。また、すでにそのような状態のプロジェクトに途中から参加する場合もあると思います。そのような状況をどう改善していくかが1つのポイントでは無いかと思いました。

感想

以前の会社では、通常の開発スプリントを4週実施したら、次の1週はテクニカルスプリントでエンジニア主導での既存コードのリファクタリングや新技術の調査などを行うことができました。 また、現在のStudyplusのiOSチームでは毎週金曜日をリファクタリングと緊急性の無いクラッシュ対応や不具合対応を行う日としてプロダクトの改善を行い、毎週リリースするサイクルを回しています。 このようなことを定期的に行ってはいますが、今後も品質について考えていきたいと思いました。

おまけ

講演内容に引用された書籍は以下のとおりです。

  • アジャイルサムライ
  • ワインバーグのシステム思考法
  • レガシーコードからの脱却
  • エンジニアリング組織論への招待
  • エクストリームプログラミング
  • LeanとDevOpsの科学
  • Experiences of Test Automation
  • A Philosophy of Software Design

すでにご存知のタイトルも多いと思いますが、チェックしてみてはいかがでしょうか。

「レガシーコードからの脱却」

吉羽 龍太郎さん

スライド

書籍「レガシーコードからの脱却」を執筆された吉羽さんによるセッションです。
※詳しい内容はセッションのスライドをご覧ください

この本はタイトルからするとレガシーコードを改善するような内容に受け取れますが、実際はレガシーコードを生み出さないようにする方法論がまとめられているとのことでした。ちなみにタイトルにレガシーとつくと本が売れるそうです。 レガシーコードの定義は様々ですが、このセッションでは修正、拡張、作業が難しいコードと定義され、保守に多額のお金がかかるコードという定義です。

ユーザーに使われるソフトウェアは変更が必要になります。 機能の追加や既存の機能の更新などが想像できると思います。 しかし、これらの更新を事前に予測することは不可能です。 そのため、変更しやすいしておくことが大事であり、その変更に対応できないのはレガシーコードであるということでした。

では、最初からレガシーコードを作らないようにするにはどうすれば良いのでしょうか?

まずは開発プロセスです。 ウォーターフォールはリスクが後半になればなるほど顕在化して取り返しがつかなくなるので、登場してきたのがアジャイルという手法、さらにXPやScrumといった手法が登場しました。 当然、アジャイルでも失敗するときは失敗します。ソフトウェアが生み出す成果に必要な要素は問題設定力、開発力、チーム力です。

次にレガシーコードを作らないための9つのプラクティスの一部が紹介されましたので簡単にまとめます。

  • 1 やり方より先に目的、理由、誰のためかを伝える
    プロダクトオーナーの領域である何をしたいか、なぜしたいか(What)と開発者の領域(How)であるやり方を分離して、双方が創造的に協調してコンテキストを共有、理解することが大事。

  • 2 小さなバッチで作る
    タイムボックスとスコープボックスという概念やケイデンス、リソース効率、プロセス効率などが登場します。 まとめると品質を一定に保ち、間に合わなければスコープを減らす。そして、ソフトウェアの評価として顧客にとっての価値が提供できているのかを小さいバッチでリリースしてフィードバックの回数を増やしてより価値を高めるということでした。

  • 5 Cleanコードを作る
    いわゆる一般的なCleanコードの定義ではありますが、開発の速後向上のために日々の積み重ねが必要で、それによりすばやく働く(= きれいに働く)が実現できるとのこと

  • 8 設計は最後に行う
    ソフトウェア開発は開発中に仕様が追加されたり、あとから分かることがあると思います。それらを随時反映するために、まずコードが動作し、テストがある状態から設計を良くするという考え方です。

感想

全体的に理解は出来るのですが、現実ではそこまでうまくできていない部分が多々あると感じました。1つ1つの考え方や振る舞い方を取り入れるだけでも、少しずつ改善できるのではないでしょうか。 より理解を深めるために「レガシーコードからの脱却」をしっかり読もうと思いました。

最後に

以上、印象に残った発表を紹介させていただきましたが、他にも素晴らしいセッションが多数ありました。 最後に和田卓人さんのツイートを紹介します。

会場の廊下にはスポンサーのブースが設置されていましたが、ある会社さんのブースで自作キーボードのスイッチを交換されている方がいました。弊社の自作キーボード部の部員としてついついキーボード話をしてしまいました。 これもそのような機会だったと思っております。

f:id:k_oishi:20191226142857j:plain:w300

この度このようなイベントに参加することで普段得られない知見を得られ、新しい出会いがありました。 運営スタッフの方々、登壇者の方々に感謝いたします。

スタディプラス SREチームの2019年の取り組みまとめ

SREチームの栗山(id:shepherdMaster)と菅原(id:ksugahara08)です。

年末ということもあり、弊社SREチームが2019年に行ってきた取り組みの中で大きめのトピックを紹介したいと思います。
本来ならもっともっと書きたいことがあるのですが、今回はスタディプラスのSREチームが何をやってきたのか概要がわかるように書いていきたいと思いますのでぜひ最後まで読んで頂けるとありがたいです。

SREチーム発足

2019年はSREチームの発足をしたというのが大きなトピックでした。発足にはインフラを担当していた1名とサーバーサイドから1名が参画し、2名で発足しました。
元々SRE経験者を社外から採用してから発足を考えていたのですが、SREは転職市場でも希少で採用が難航していため、それなら自分たちでSREを始めてしまおう!とSREチームを作ることを決めました。

SREという職種の共有会

www.oreilly.co.jp

発足当初、SREという職種について経験者もいなければ、SREという職責について理解が足りていなかったためとにかく他社事例を見ることからはじめました。
その際に活用したのがYouTubeに上がっているSRE関連の動画でした。動画視聴会は開催しやすく学びも多いということで弊社では割と頻繁に行っております。

次に、オライリー・ジャパンから出版されているSRE サイトリライアビリティエンジニアリング――Googleの信頼性を支えるエンジニアリングチームをSREメンバーそれぞれが読み、30分程度のプレゼン形式で社内のエンジニアにSREという職種はどんなものかを共有する『SRE共有会』を開催しました。

輪読会という形式も選択肢にはあったのですが、

  • SREメンバーが2人で経験者もいないということ
  • スピード重視
  • SREではない周りのエンジニアにも理解してもらいたいという想いがあった

という理由でサクッと概要を掴める共有会形式を取りました。
結果としてSREという職種について理解と協力をしてもらえるようになったと思います。今後も何らかの形で社内への理解を深める活動をしていきたいと考えています。

ポストモーテム導入

前述のSRE本に載っているポストモーテムを弊社でも導入しました。
弊社では以前から障害報告書を書く習慣はあったのですが、障害対応した人がその障害を見るような形でチーム全員で振り返るという事はしていませんでした。
障害報告書をポストモーテムに変えるにあたって内容の変更や振り返りを全員で議論する場を設けるようにしました。
ポストモーテムを導入した結果、弊社では以下のようなメリットがあったと思われます。

  • 時系列で何が起きていたかわかりやすくなった
  • チーム全員で障害を振り返るようになった
  • 障害後、取るべきアクションをきちんとみんなで話し合うようになった
  • 「障害から学ぶ」という意識が広がった

ポストモーテムを導入後の2019年8月23日にはAWSの大きな障害が起きましたが、ポストモーテムで記録に残し、コミュニケーションを取りながら障害対応ができました。導入してすぐにメリットを痛感できたのも機運が高まった要因かもしれません。
今後もポストモーテムを続けていくということだけではなく、振り返ることで障害に強いサービスに変更していきたいと考えています。

脱AWS Elastic BeanstalkとKubernetes移行

弊社ではメインのマイクロサービスはAWS Elastic Beanstalk上で、その他のマイクロサービスはEC2上で動いています。
Elastic Beanstalkはメリットもたくさんあります。しかし、デプロイが遅かったりElastic Beanstalk自体の仕組みが独特で学習コストがかかったりで、他のなにかに移行したいという話が上がっていました。
またEC2のほうは歴史的経緯からEC2インスタンスを増やすためには手動作業(EC2インスタンスを立ち上げてからAnsibleを実行するという作業)が必要という課題がありました。
その他の要望として

  • カナリーリリースを楽にしたい
  • 簡単に言語やライブラリのバージョンアップをするためにコンテナを導入したい
  • インフラ作業コストを下げたい
  • 耐障害性を高めたい

というものがありました。
それらの課題、要望を解消するためにKubernetesに移行することを決めました。現在はまず一部のマイクロサービスのKubernetesへの移行を取り組んでいます。

Terraform移行

www.terraform.io

弊社では前任者がAWSサービスをAnsibleで管理している状況でした。
AWSリソースからEC2の設定まで全てAnsibleで管理するのは、いくつもIaCツールを使うより学習コストを抑えられるという点でメリットがあります。しかし一方でAWSの新規サービスや最新バージョンに追従できないことが多々ありました。
その際は自分たちでPythonのAnsibleモジュールを自作していたのですが、サービス数が増えるに連れ運用コストが比例して増えるようになってきました。
そこで弊社ではAWSリソースの管理をTerraformへ移行することを決めました。
Terraformを選択した理由としては以下があげられます。

  • AWSの新規サービスや最新バージョンに比較的早く追従される
  • 宣言的に記述することができるため直感的にわかりやすい
  • 利用できるProvidersが豊富なこと

もちろんデメリットとしてAWS CloudFormationに比べて新規サービスや最新バージョンに対応するラグありますが、今のところ対応は早く不便に感じていません。
またTerraform学習コストもありますが、新規ツールに対してキャッチアップする姿勢が強いメンバーが揃っていたため問題になっていません。
社内でTerraform共有会を定期開催するなどお互いの知識を教え合う場ができている状態です。
現在はAmazon EKS周りのリソースをTerraformでコード化していますが、これからは現状のリソースもコード化したり、CI/CDの自動化をしていく予定です。

ログ収集基盤改善

ログ収集基盤をAmazon CloudWatch LogsからAmazon S3 + Amazon Athenaに変更しました。
CloudWatch Logsには以下の課題がありました。

  • 料金が高い
  • ログ保存期間が短い
  • UIが使いづらい

それを解決するためにログをS3に保存しAthenaで検索できるように基盤作りをしました。
最初は、Amazon Kinesis Data Firehoseを使ってFirehose自身の変換機能でParquet形式でS3に保存していたのですがFirehoseは料金が高くて諦めました。
JSON形式で保存してAthenaで検索してもそれほど検索速度に影響がなかったので、現在はFluentdから直接ログをS3にJSON形式で保存しています。

Rubyバージョンアップ

弊社では早くからマイクロサービスを採用しており、Rubyを使っているサービスは全部で9サービスあります。その中で2020年3月31日にEOLを迎えるRuby2.4を使っている5つのサービスを2.6にバージョンアップしました。
またついでに使っているGem(Rails含む)のバージョンアップも行いました。

作業自体はどのサービスも概ねスムーズにいったのですが、一部のサービスでPumaのGemのバージョンアップを行ったらゾンビプロセスが発生するバグを踏み抜いてしまい焦りました。

speakerdeck.com

最初は原因が分からなかったのでひとまずGemのバージョンアップをrevertし調査しました。
Pumaの直近のリリースノートに「ゾンビプロセスが発生するバグを修正した」というのがあったのでPumaのGemを最新にしてリリースしたところゾンビプロセスが発生することはなくなりました。

バージョンアップ作業はなかなか大変ですが、非常に重要なので定期的かつ計画的に行っていきます 。
今後コンテナ化を進めていったら新しいバージョンのRubyを入れた新しいサーバーを用意する必要もなくなるのでバージョンアップ作業が楽になるのではないかと期待しています。

jemallocの導入

詳しくは Rubyアプリケーションのメモリ使用量上昇問題をjemallocを使うことで解決しました をご参照下さい。

tech.studyplus.co.jp

勉強会開催

SREチームでは知識、知見をインプット/アウトプットすることを重視しており、定期的に勉強会を開催しています。2019年に行った勉強会を紹介します。

Kubernetesハンズオン

GCPを使ってKubernetes上にアプリケーションを動かしながら、Kubernetesの各機能の説明をするハンズオンを開催しています。Kubernetesを浸透させたいのでエンジニアが新しく入ってくるタイミングでハンズオンを実施しています。

Kubernetetsの各機能の勉強会

Kubernetes完全ガイド本をもとにKubernetesの各機能について紹介する勉強会を7回に分けて開催しました。

コンテナ監視ツール勉強会

Kubernetesを本番サービスで運用していくにはObservabilityを上げていく必要があり、CNCFのTrail Mapにも4番目に載っています。弊社でもKubernetesでの監視をどのようにしていくべきか検討する必要があり、主要な監視ツール9つを比較し、勉強会を行いました。

検討の結果、今後はDatadog等のService Discoveryに対応したツールを利用してKubernetesの監視を設定していきたいと考えています。

サービスメッシュ勉強会

サービスメッシュのIstioの勉強会も開催しました。 その時の資料はこちらです。

qiita.com

CI/CD勉強会

Kubernetesへのデプロイツールとしてどのようなものが最適なのかを知るためにいくつかCI/CDを各自調べ、勉強会を開きました。
以下が調べたツール、サービスになります。

AWS Black Belt動画視聴会

AWSには多くのサービス多くの機能があるため、「自分たちが使っているサービスをより知るため」「使ってないサービスの概要を知るため」という理由でYouTubeに上がっているAWS Black Beltの動画をみんなで視聴する会を定期的に開催しています。(スタディストさんでやっている活動を参考にさせてもらいました)
動画を見ながら「この機能は使えそう」「機会があれば使ってみたい」とみんなでわいわい話しながら見ています。
今年観た動画は Aurora、Athena、Amazon Personalize、Key Management Service、CloudFront、Lake Formation、AI Services、Config です。

監視周りの設定見直し

弊社では主にMackerelを使ってサーバーメトリクスを取得し、Slackにアラートを出したり、Twilioで電話通知を行ったりしていました。
しかしDBが高負荷による障害時に監視やログ取得が足りていないと痛感することがありました。そのため以下のような設定を行い、監視周りの設定見直しを行いました。

  • Amazon Auroraのパフォーマンスインサイト導入
  • Amazon RDS、Auroraのスロークエリを出すようにする
  • 各サービスの死活監視強化

改善はしたものの、まだ現状でも課題があります。CPUやメモリー等のResource Metricsは監視できているものの、スループット等のパフォーマンス周りのWork Metrics、そして設定変更の監視周りであるEventsが弱いと感じています。
今後は Datadog社のMonitoring Modern Infrastructureを参考に監視の改善を行っていきたいと考えています。

深夜メンテナンス手順書の整備

今年何回か深夜メンテナンスを行ったのですが、私たちSREチームが初めて深夜メンテナンスの準備をするときにメンテナンス手順書が存在しないことに気が付きました(あったのは前任者が残した簡単なメモ書き)。
これはメンテナンス手順書を作る機会だと思い、メンテナンスモードにするための必要な手順を1つ1つ社内Wikiに書き起こしました。それ以降の深夜メンテナンスでは毎回メンテナンス手順書を作っています。
事故なく深夜メンテナンスを行うには手順書は非常に重要で、その手順書のレビューやリハーサルもしっかり行っています。

最後に

インフラチームからSREチームに変わりメンバーも増え、様々な改善ができた一年でした。

2020年も弊社サービスのユーザーの皆様により良い価値を提供するためサービス基盤、開発環境、パフォーマンス等の改善など行っていきたいと考えてます。SREチームとして弊社サービスをより愛して頂けるように全力を尽くしていきたいと思います。

Studyplus for SchoolのCSS事情

こんにちは。ForSchool事業部の石上です。最近、弊チームのスクラムマスター id:atomiyama さんから、「伝えにくい事とか相手の注意引きたい時に幼児言葉を使うと有効です。」というテクニックを教えてもらいました。今後はなるべく実践していきたいでちゅ 👶

今日は、Studyplus for SchoolのCSS事情について書かせてもらいます。

以前に書いたやりたいことベースでWebpackにCSS周りの設定をする - Studyplus Engineering Blogとだいぶ重複する部分もありますが、今回はWebpackのloaderの話ではなく、なにが欲しくてなにを使っているのかという形式で整理していきたいと思います。

ちょうど最近 id:tagucch さんがうちのチームに異動してきてくれたので、 id:tagucch さんへの説明も兼ねています。

CSSのためにいろいろ道具が必要になる背景

本題へ入る前に、そもそもなぜCSSを書くために余計な道具が必要となるのでしょう。CSSはブラウザが解釈できる言語なので、そのまま書いて配れば動くはずです。

しかし欲を出して、ちょっとでもCSSを書きやすくするためにはいろんなツールを入れる必要があります。

それらを今回改めて紹介していければと思います。

変数がほしい

CSSでは、セレクタ(画面内の要素)を指定して見た目を当てることができるわけですが、何度も同じスタイルを当てるのは大変です。なので、HTMLのclass属性に名前を指定してうまいこと管理することで繰り返しを避けられるわけですが、その指定だけでは管理しづらいこともあります。

たとえば、色コードです。そのサービスのブランドカラーのような色コードは、いろんなところで使うことがあります。テキストの色だったり、ボタンの背景色だったりします。

.primary-color-text {
  color: #008080;
}
.primary-color-button {
  background: #008080;
  color: #fff;
}

color, backgroundとそれぞれプロパティの種類は違いますが、当てたい色コードは同じです。CSSで変数が使えると以下のように書けます。

:root {
  --primary-color: #008080;
}
.primary-color-text {
  color: var(--primary-color);
}
.primary-color-button {
  background: var(--primary-color);
  color: #fff;
}

これはいいですね。名前がつくことで、色コードの意味を開発者が覚える必要もなくなりますし、変更を入れたいときも一箇所で済みます。

ただ、この記法はまだサポートされていないブラウザもあります。

このコードが使えないブラウザのために代替の手段が必要で、それが今のところはSassというツールです。Sassの変数を使えば、上記とほぼ同じように書けます。

$primary-color: #008080;
.primary-color-text {
  color: $primary-color;
}
.primary-color-button {
  background: $primary-color;
  color: #fff;
}

関数がほしい

変数と同じく、関数もあると嬉しいです。似たようなプロパティをまとめられて、使うところで少しだけ変えられると、とても便利です。ただ、これはCSSでは書けません。

Sassを使うと、以下のようにmixinというものを定義できます。たとえば、スクリーン幅をみて当てるスタイルを変えたいとき、以下のようなものを定義しておくと、各所で長ったらしいメディアクエリを書かずに済んで便利です。

/* mixin */
@mixin max-screen($break-point) {
  @media screen and (max-width: $break-point) {
    @content;
  }
}
/* 使うとき */
@include max-screen(768px) {
  width: 50%;
  /*...*/
}

スコープがほしい

Reactを使ってアプリケーションを作っているとUIの部品をコンポーネントと呼び、汎用的にするべしという風潮があります。実際そうしたほうが、繰り返し同じようなものを実装しなくて済むので幸せになれます。

そうすると、できれば見た目の定義もそのスコープに閉じ込めたくなります。そこで出てくるのがCSS Modulesです。

import * as styles from './styles.scss';

const Button = (props: Props) => {
  return (
    <button className={styles.root}>{props.children}</button>
  )
}
.root {
  background: $primary-color;
  /* ... */
}

Webpackのloader、css-loaderでCSS Modulesの設定をするとこのように書けるようになります。.rootは、ビルド時にアプリケーション内で衝突しない文字列に置き換えられるので、クラス名の衝突を気にせず短いクラス名でCSSを書けます。Button/styles.scssの.rootは、Buttonコンポーネントのスコープに閉じられています。

型がほしい

Studyplus for Schoolのフロントエンド側のコードはTypeScriptで書いているため、エディタ補完のありがたみを感じながら開発をしています。CSS Modulesを利用して、TypeScript側からクラス名を参照できるようになっているので、型もついているとなお嬉しいですね。className={styles.とタイピングしたら、指定できるclass名が補完されてほしい。

これには、Quramy/typed-css-modulesを利用して、CSSの型を生成して使っています。

まとめ

こんな感じで現状だといろいろ使っているのですが、個人的にはなるべく依存するツールは減らしていきたい気持ちがあります。いつかCSSだけで快適に書けるようになったらいいなと思いつつ、そうなるまでは必要なものを選んで使って、いらなくなったら捨ててというのをちゃんと繰り返していければいいかなと考えています。

これもう不要じゃん! とかこっちのほうがいいぞ! みたいものを見つけたときはどんどん変えていきたいのでご指摘もらえると嬉しいです! 👶

CloudNative Days Kansai 2019参加レポート

どーも、SREやってます。菅原です。

先日は11月27,28日にグランフロント大阪のコングレコンベンションセンターで行われたCloudNative Days Kansai 2019に参加して来ました。

f:id:ksugahara08:20191201140016j:plain

せっかくなのでイベントの参加レポートを載せたいと思います。

CloudNative Daysとは

公式サイトから引用させて頂くと、

クラウドネイティブの現状をひとまとめにした開発者のためのイベントです。2018年はJapanContainerDaysとして2度開催し、今年からCloudNativeDaysに名称を変え4月に福岡、7月に東京、11月に大阪で開催します。

ということらしい。

クラウドネイティブ関連の技術を中心としたセッションが行われ、国内インフラ系のイベントでは一番大きいものではないかと思ってます。(クラウドベンダーが主催するものを除き)

CloudNative Days Kansai注目セッション

ここからは私が注目したセッションを紹介していこうと思います。

スライドが上がると思うので私の感想を中心に載せていきます。

コンテナの作り方~Dockerは裏方で何をしているのか~(前佛雅人氏)

コンテナやDockerって何?という疑問に図解で解説するという内容でした。

初心者向けと思いきや、図解でナレッジがまとめられていて、Dockerを使ったことがある人でも頭の中が整理されて本当に良いセッションでした。

また、前佛氏のトークはものすごくわかりやすく「コンテナはデフォルトでisolate」というキャッチフレーズが頭に残ったのは私だけではないでしょう。

メルペイのマイクロサービスとCloud Native(Junichiro Takagi氏)

speakerdeck.com

メルペイのマイクロサービスがどのようにCloud Nativeを取り入れているのか。また、運用してみてどのような課題があるのかを聞くことができた素晴らしいセッションでした。

最後にTakagi氏は「Cloud Nativeで重要なのは技術セットではない、組織体制や開発・運用のスタイルも含めたCultureが重要だ」とまとめていました。

弊社も半年間SREの文化や考え方を社内エンジニアに理解してもらうため共有会や勉強会をしてきたので、組織が大きくなってもこれを継続していくことが必要だなと思いました。

分散システム内のプロセス間の関係性に着目したObservabilityツールの設計と実装(Yuuki Tsubouchi氏)

speakerdeck.com

ゆうきさんのセッションは、Transtracerについての内容でした。

分散アプリケーションにおけるObservabilityの問題を、分散トレーシングで主流のリクエストベースアプローチとは違ったアプローチで問題解決しようというものでした。

正直この分野について詳しくなかったので目から鱗でした。 今後のTranstracerの発展が気になる素晴らしいセッションでした。

Kubernetesの運用を支えるGitOps(藤原峻輝氏)

www.slideshare.net

freeeではWeaveworksのGitOpsを参考にOpsのGit化をしたというセッション内容でした。

バージョン管理やロールバックが容易になるという恩恵を狙ってGitOps導入したところ、集中した権限管理、kubectlでdeployする必要がない安心感、実際に今動いているmanifestがコード上でバージョン管理されるといったメリットが得られたとのことでした。

freeeのGitOpsの導入手法はCloud Nativeを実践していて理想的ないい例だと思いました。弊社でもWeaveworksのGitOpsの考え方の布教活動から始めていきたいと思いました。

Production Ready Kubernetesに必要な15のこと(磯賢大氏)

speakerdeck.com

Kubernetesプラットフォームを構築するにあたって考慮点を15つにまとめており、コロプラでの設定例も紹介している神スライドでした。

Kubernetesを本番環境で運用していくには何を考えておく必要があるのかベストプラクティスがまとまっていたと言っても過言ではないでしょう。 Kubernetes初心者にも、実際に運用している人にも参考になったのではないでしょうか?

弊社もこの15項目についてSREチームで議論して決めていきたいと思います。

最後に

東京から大阪のカンファレンスに参加したのですが、大阪の街並みや関西のエンジニアの熱量を感じることができました。仕事に活かせる話を聞けただけでなく、自分のモチベーション向上につながったと思います。

少ししか大阪に滞在できませんでしたが、最高でした。また機会があれば遠征したいです。

AAC Navigation で Bundleの受け渡しを SafeArgs で行なってハマった話

こんにちは、Studyplus Androidチームの中島です。

以前の話に引き続き、 AACのNavigationについて話したいと思います。

今回は 遷移元・遷移先のFragment間でのBundle受け渡しでちょっとハマったことについてです。

なお、執筆時に利用している Navigation のバージョンは2.2.0-rc02です。

(前回) tech.studyplus.co.jp

初めに、Fragment間受け渡し方法について

まず、Navigation でのFragment間のデータ受け渡し方法について軽く触れておきます。 もう知ってるよという方は、本節を読み飛ばして頂いても構いません。 (遷移方法などの説明は割愛します、公式ドキュメントなどを参照してください)

FactoryメソッドによるsetArgumentsを利用した方法

従来通りの、プリミティブ型やSerializable、ParcelableをBundleに詰めて渡す手法です。

companion object {
    private const val ARG_KEY_NAME = "arg_key_name"
    fun newInstance(arg: Int) = YyyFragment().apply {
        arguments = Bundle(1).apply {
            putInt(ARG_KEY_NAME, arg)
        }
    }
}

ただし、 Navigation ではライブラリ内部で Fragment の生成が行われるため、この従来の生成方法は使えません。

Navigation では navigate メソッドで遷移アクションを指定しますが、その際にBundle型をパラメータに入れることができます。 なので、Fragmentでは Instanceの生成 ではなく Bundleの生成 のメソッドを生やしておくといいと思います。

// 遷移先Fragment
companion object {
    private const val ARG_KEY_NAME = "arg_key_name"
    // Navigation遷移のためInstanceではなくBundleを作成して返している
    fun createBundle(myArg: Int) = Bundle(1).apply {
        putInt(ARG_KEY_NAME, myArg)
    }
}
// 遷移元Fragmentの遷移処理箇所
val args = YyyFragment.createBundle(myArg = 123)
findNavController().navigate(R.id.action_Xxx_to_Yyy, args)

受け取ったBundleの中身は従来通りに取り出せます。

// 遷移先Fragment
val myArg = requireArguments().getInt(ARG_KEY_NAME) ?: 0

SafeArgs

Navigation の便利機能として、SafeArgsがあります。 これはBundleの put~~ get~~ を書かずにArgumentを直接渡せる機能です。 最大の利点としてその名の通りSerializableやParcelableを型安全なかたちで キャスト処理を書かず受け渡し可能になります。 利用するには build.gradle に追加でプラグインを指定する必要があります。 以下公式ドキュメントから抜粋します。

To add Safe Args to your project, include the following classpath in your top level build.gradle file:

buildscript {
    repositories {
        google()
    }
    dependencies {
        def nav_version = "2.1.0"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

To generate Java language code suitable for Java or mixed Java and Kotlin modules, add this line to your app or module's build.gradle file:

apply plugin: "androidx.navigation.safeargs.kotlin"

これらを追記した後にビルドすることで、NavGraph内で設定した各Fragmentごとに~~Directionsというクラスが自動生成されます。 その中に、同じくNavGraphで指定した各actionと対応したメソッドが自動生成されます。 この action のメソッドに引数としてデータを入れるわけですが、その型はnavGraphの遷移先Fragment定義内に<argument />アトリビュートを追加することで指定できます。 以下公式ドキュメントから一部改変して抜粋します。

<fragment android:id="@+id/YyyFragment" >
    <argument
        android:name="myArg"
        app:argType="integer"
        android:defaultValue="0" />
</fragment>

ここまで指定して改めてビルドすることで、以下のようにBundleにput/getせずデータのやり取りをすることができます。

// 遷移元Fragmentの遷移処理箇所
findNavController().navigate(XxxFragmentDirections.actionToYyy(myArg = 123))
// 遷移先Fragmentではフィールドとしてby navArgs()で取得
private val args: YyyFragmentArgs by navArgs()

val myArg = args.myArg // Int型として型安全で受け取れる

例ではintegerで書きましたが、これを用いることでSerializableやParcelableを継承したdata classのやりとりがとても楽になります。

なおYyyFragmentArgsの内部コードを覗いてみますと、変数名をArgumentのkey名としてBundleにput/getしている処理を見ることができます。 put/getのラッパークラスを自動生成してくれる機能、と思っていいかと思います。

ActivityレベルのViewModel

ActivityのLifecycleに合わせたViewModelでデータを保持することで、そのViewModelを通して共有する手法です。 強力な保持期間があり楽にデータを受け渡しできますが、逆にデータがずっと保持されてしまうとも言えます。 そのため、あるFragmentからあるFragmentへ遷移する際に明示的なリセット処理が必要となることがあります。 本記事では主題の外なのでこれ以上触れません。

NavGraphViewModelはNavigation2.1.0-alpha02以上で利用できる機能です。 同一のNavGraphの中にあるFragment間でのみ生存するViewModelでデータを共有する手法です。 Activityレベルより細かくデータ保持期間を指定できますが、NavGraphの分け方に依存する面もあります。 詳しくは公式ドキュメントをご参照ください。 本記事では主題の外なのでこれ以上触れません。

データの受け渡しについてまとめ

Navigation におけるFragment間でのデータ受け渡しは、公式ドキュメントにもある通りデータの量を考慮して選別する必要がありそうです。 また、そのデータを利用する上で生き残るべき期間も考えて、これらの手法を的確に使い分けるべきなのかなという認識です。

今回ハマったこと

手法選択

コードのリファクタリングを行なっていたところ、検索条件指定画面から検索結果表示画面への遷移を行なうコードにおいて Navigation を導入することとなりました。 このケースではFragment間での検索条件データの受け渡しが必要になります。 また、検索条件データは遷移時に生成して検索結果表示画面のFragmentだけで保持すればよく、検索条件指定画面のFragmentに戻る際は破棄したいデータでした。 そのため、ViewModelに依る共有ではなくBundleで渡す方法を選びました。

また、Serializable の data class であったため、型安全のため SafeArgs を利用したいと思いました。

ダメだったこと

以下の様に<argument />を指定したところ、debugビルドではうまくいっていました。

<argument
    android:name="data"
    app:argType="{data classのフルclasspass}" />

しかし、R8による難読化がなされたステージ環境向けのapkでテストしたところ、クラッシュが起きました。

Didn't find class {classpass}

つまり、難読化によって指定したクラス名が見つからないよと言われてしまったのです。 この解決法について調べている時、STAR-ZEROさんのブログ: Navigation SafeArgsで使える型から以下の記述を見つけました。

XMLで直接設定もできます。このとき . から開始することで、アプリのパッケージ名を省略することが可能です。

medium.com

この一文から、「もしかして相対パスで書けばいけるのでは?」と思い、以下の様にxmlを書き直しました。

<argument
    android:name="data"
    app:argType=".{data classの相対classpass}" />

結論から言うとこれでも同じく Didn't find class エラーでクラッシュし、しかも難読化のないdebugビルドですら同様のエラーが起きる様になってしまいました。

この時のエラーメッセージにあったクラスパスですが以下の様になっていました。

Didn't find class {アプリパッケージ名}.debug.{dataクラスの相対classpass}

Studyplus Androidアプリでは開発者環境、ステージング環境、本番環境で同端末内に共存できるよう、ビルドタイプに応じてsuffixをパッケージ名につけています。

アプリのパッケージ名を省略することが可能です。

そう、そのsuffixも当然パッケージ名の一部ですからここに挟まれてしまうわけです。 そのため、debugビルドでもクラスパスがズレてしまっていたわけでした。

加えて、Studyplus Androidアプリでは絶賛マルチモジュール化の促進中であることも思わぬ障壁になっていました。 マルチモジュールではアプリのパッケージ名各モジュールのパッケージ名が異なる形になります。 相対パスで書くと、SafeArgsによる自動生成クラス(~~FragmentArgsクラス)内ではモジュールのパッケージ名に相対パスを繋げてdata classをimportします。 しかし実行時だと、Navigationアプリのパッケージ名の直後に相対パスを繋げて指定したクラスを探しに行きます。 その結果、

  • 自動作成クラスに合わせて相対パスを指定すると実行時クラスが見つからずクラッシュする
  • 実行時の設定に合わせて相対パスを指定するとSafeArgsの自動生成クラスがdata classをimportできずビルドが通らない

となってしまっていました。 suffixがなかったとしても、マルチモジュールとこの相対クラスパスによる記法は相性が非常に悪いわけです。

解決法

まぁなんのことはなく、NavigationSafeArgs を利用せず、Bundle を生成して navigate メソッドに入れる手法で無事解決しました。

公式ドキュメントを改めて見たら…

SafeArgs と難読化の併用問題についての話には続きがあります。

コーディング当時(2019/11/11)は前節にまとめたように、FactoryメソッドによるsetArgumentsを利用した方法で解決しました。

その後(2019/11/22)、公式ドキュメントを改めて確認したところ、 ProGuard に関する考慮事項という記述があることに気付きました。 本節ではこれについて追記します。

keepnames ルールを使用する

公式ドキュメントから引用します。

keepnames ルールを proguard-rules.pro ファイルに追加することもできます。

これについては気づいていました。 確かに難読化によってフルパスが指定できなくなるのであれば、該当data classを難読化対象から外すことでももちろん解決できるはずです。 ですが、Studyplus Androidアプリは R8+Moshi Codegen によって、外部ライブラリ以外のファイルに関してproguard-rulesの表記が不要な状態となっています。 この手法を採用しなかったのは、今更 SafeArgs のためにこのdata classだけをproguard-rulesに追加したくないという判断でした。

@Keep アノテーション

公式ドキュメントから引用します。

    @Keep class ParcelableArg : Parcelable { ... }

    @Keep class SerializableArg : Serializable { ... }

    @Keep enum class EnumArg { ... }

androidx.annotation.Keep アノテーションを利用することで、難読化対象から外すというものです。 これについては恥ずかしながら知らなかったものでした。 試してみたところ、R8で難読化していても問題なく動作しました。 proguard-rulesファイルに影響を及ぼすことなく難読化対象から外せるのであれば、このアノテーションを付けて SafeArgs を利用していくのがベターかと思いました。

まとめ

今回は、NavigationSafeArgs を使おうとしてハマった話を紹介させていただきました。

SafeArgs は非常に便利ですが、現状では難読化と併用する場合もう一工夫必要になります。 まだ調査しきれていませんが、このArgumentの受け渡し周りについてはDeepLinkでの利用もある為、クラス名の取り扱いが内部で色々大変なのかもしれません。 今後のアップデートでこの辺りの利便性がさらに向上していくと嬉しいですね。

最後に反省点として、公式ドキュメントは今後も細かく確認していこうと思います…。

FirebaseとStripe Billingを組み合わせるとき、stripe.customerのdescriptionとmetadataが便利です

こんにちは、スタディプラスの須藤(id:kurotyann)です。

9/17にFirebaseとStripe Billingを使って新しいサービスをリリースしました。 サービスについては先日投稿したブログを参照してください。

tech.studyplus.co.jp

今日は、タイトルどおり「FirebaseとStripe Billingを組み合わせるとき、stripe.customerのdescriptionとmetadataが便利です」について説明します。

FirebaseとStripe Billingを使ってサービスを開発する予定がある人は、知っておけば作業効率が確実に上がる内容です 💪

Stripe Billingとstripe.customerとは?

Stripe Billingとは、定期支払いのビジネスモデルを構築できるStripeの機能のことです。 詳細は、Stripeの公式サイトを参照してください。

stripe.com

そして、stripe.customerとは、Stripe上で管理される顧客オブジェクトのことです。 stripe.customerには、顧客の連絡先や、クレジットカード情報、定期支払いのプランなど、様々な支払い情報が紐付けられています。

stripe.customerはIDをもち、このIDをFirestoreに保存してFirebase Authenticationのユーザーと紐付けることで、サブスクリプションサービスの構築が可能です。

stripe.customerのdescriptionとmetadataとは?

stripe.customerのdescriptionとは、Stripeのダッシュボードで顧客情報の隣に表示される文字列のことです。

customer_object-description

An arbitrary string attached to the object. Often useful for displaying to users. https://stripe.com/docs/api/customers/object#customer_object-description

そして、stripe.customerのmetadataとは、開発者が顧客情報に追加できるhash型の情報のことです。

customer_object-metadata

Set of key-value pairs that you can attach to an object. This can be useful for storing additional information about the object in a structured format https://stripe.com/docs/api/customers/object#customer_object-metadata

APIドキュメントを見た限りでは、よくあるオブジェクトのプロパティですが、これらを活用すると、FirebaseとStripeの開発効率が上がります。

私が開発をしていて、開発効率が上がった(便利だ)と感じた具体的な事例を3つあげて説明します。

descriptionとmetadataが便利な理由

1. descriptionは、Stripeのテストデータを環境別に見分けやすくする

Stripeには本番とテスト(テストデータ)の2つの環境しか用意されていません。 2つしかないので、本番(production)/ ステージング(staging)/ 開発(development)といった3環境構成のときに困ります。

一方、Firebaseで環境別に3つのプロジェクトを作成するのは簡単です。 そして、アプリ側でリクエスト先のプロジェクトを変えることも、今やデファクトスタンダードになっています。

Stripeのテスト環境にステージング環境と開発環境のユーザーを登録する仕様にした場合、テスト環境に2つの環境のユーザーが混じり合うことになります。 これだと、Stripeのダッシュボードでどの環境の顧客データなのか識別しづらくなります。

そこで、descriptionに環境の識別子をいれて見分けやすくしましょう。 具体的には、Cloud Functions for Firebase で process.env.GCLOUD_PROJECT を stripe.customerの新規作成時に保存するのが一番簡単だと思います。

customer = await stripe.customers.create({
  description: process.env.GCLOUD_PROJECT,
  email: email,
  source: token,
});

これでStripeのダッシュボードでテストデータを見たとき、どの環境で作成された顧客データなのか、ひと目でわかります。

Stripeのダッシュボードでテストデータを見たとき
f:id:kurotyann:20191105012821p:plain

さらに、他にも嬉しいことがあります。

  • ダッシュボードの検索バーに process.env.GCLOUD_PROJECT の値を入力すれば、環境ごとの顧客データを絞り込める
  • 顧客データをcsv形式でエクスポートしたとき、顧客データにdescriptionも同時に付与されて出力される

2. metadataにuidを保存して、uidで顧客情報を検索できるようにする

descriptionには環境の識別子を入れたので、metadataにはFirebase Authenticationのuidを保存します。

customer = await stripe.customers.create({
  description: process.env.GCLOUD_PROJECT,
  email: email,
  source: token,
  metadata: {
    uid: uid,
  },
});

これでdescriptionと同様に、Stripeのダッシュボードでどの環境のユーザーでもuidさえわかれば、顧客情報をすぐに絞り込めます。 また、metadataもcsv形式でエクスポートしたとき、顧客データに付与されて出力されるので、csvで分析したいときも便利です。

3. metadataにも環境識別子を保存して、どの環境のwebhookなのか判別できるようにする

最後に、 metadataにも環境識別子を保存しましょう。

customer = await stripe.customers.create({
  description: process.env.GCLOUD_PROJECT,
  email: email,
  source: token,
  metadata: {
    uid: uid,
    env: process.env.GCLOUD_PROJECT,
  },
});

Stripeのwebhookの環境も、本番とテストの2環境しかありません。

これだと、例えば定期支払いのイベント invoice.payment_failed がテスト環境で発生したとき、ステージングと開発の両方のwebhookが対象になってしまいます。

つまり、特に何も準備しなければ、Stripeのテスト環境で起きたイベントが、ステージングと開発のどちらの環境で発生したイベントなのか識別できません。

そこで、metadataの環境識別子を使います。 私の場合、webhookのStripe-Signatureの処理直後に、下記の環境識別子チェックを走らせるようにしました。

const functions = require('firebase-functions');
const stripe = require('stripe')(functions.config().stripe.token);

/*
 * Stripeのテスト環境に開発とステージングの webhookUrl を定義しているため
 * customerのメタデータでどちらの環境の webhook なのか判定している
 * doc: https://stripe.com/docs/api/customers/retrieve?lang=node
 */
module.exports = async function(data) {
  try {
    const customerId = data['customer'];
    if (customerId === 'cus_00000000000000') {
      // Stripe dash boardからイベントをテスト送信した場合
      return {
        isValid: true,
      };
    }

    const customer = await stripe.customers.retrieve(customerId);
    const metadataEnv = customer.metadata['env'];
    return {
      isValid: metadataEnv === process.env.GCLOUD_PROJECT,
      code: 202,
      message: `different project processEnv: ${process.env.GCLOUD_PROJECT}, metadataEnv: ${metadataEnv}`,
    };
  } catch (e) {
    console.error(`🧨 validateProjectEnv: ${JSON.stringify(e)}`);
    throw e;
  }
};
const validateProjectEnv = require('./util/validateProjectEnv');

/* Stripe-Signature の直後 */

const validate = await validateProjectEnv(event.data.object);
if (!validate.isValid) {
  return res.status(validate.code).send(validate.message);
}

/* 任意の処理が走る(例: Firestoreの更新や、slackへの通知など) */

Stripeの現在の仕様だと、 ダッシュボードからテストでwebhookを起動させるとき顧客IDは cus_00000000000000 です。これは無視するようにしています。

ちなみに、descriptionもwebhookのレスポンスに含まれるのでmetadataに保存するのは二度手間のように思いますが、StripeのAPIドキュメントだとdescriptionはダッシュボードで利用されるプロパティだと明記されています。

今後のStripeの仕様変更でAPIのレスポンスにdescriptionが含まれなくなっても大丈夫なように念の為、metadataに保存しておいた方が無難だと私は思います。

おわりに

stripe.customerのdescriptionとmetadataが便利な理由を具体的な3つの事例を交えて紹介しました。

まだまたFirebaseとStripe Billingを使った内容で紹介したいことがあるのですが、それは今年のアドベントカレンダーのどこかで投稿する予定なのでしばしお待ちください 😄