GCPのサービスアカウントを簡単かつ安全に利用するための方法

こちらはmikanのアドベントカレンダー16日目の記事になります。

15日目は @yirszk から プロジェクトオーナー制を突き詰めてチームの成果を最大化した話でした。チーム全体の生産性を高めるためにぜひ参考にしてみてください!

こんにちは。mikanでBackendエンジニアをしております。 @hoshitocat です。

最近は急に寒くなってきましたね。自宅で灯油ストーブを出したのですが、久しぶりの給油で絨毯に灯油をこぼしてしまい、絨毯を自宅のお風呂場で洗うのが大変でした。灯油の扱いには注意したい季節ですね・・・

さて、この記事では、GCPとFirebaseメインでサービスを構成している弊社mikanがどのようにしてサービスアカウントを利用しているかについて紹介します。

サービスアカウントの基本的な利用方法、応用編としてGKEを使う上で本番環境におけるサービスアカウントの利用方法がわかると思います。

はじめに

サービスアカウントとは、アプリケーションからGoogle Cloudのサービスを利用するときに使用するアカウントになります。

Firebaseを使ったことがある方はご存知かと思いますが、以下のようにプロジェクト設定から、サービスアカウントの鍵を発行し、Firebaseの機能をアプリケーションから利用できることが示されています。

⚙️ → プロジェクトの設定 → サービスアカウント

🆖 サービスアカウントの推奨されていない使い方

当初、私はサービスアカウントの鍵を自分で生成し、それをローカル開発環境やステージング(QA)、本番環境で利用していました。

しかし、サービスアカウントキーはできるだけ自分で管理しないことが推奨されています。自分で管理するのは非常に面倒です。不正利用されないために、漏洩対策や漏洩時の被害を最小限にするために鍵のローテーションなどが必要になってきます。この話は サービス アカウント キーを管理するためのベスト プラクティス にも記載されています。

弊社でサービスアカウントキーを利用する際には鍵のローテーションJobを作成し、毎朝10時に鍵をローテーションしGCSへアップロードしていました。

🆗 サービスアカウントの推奨されている使い方

サービス アカウントを操作するためのベスト プラクティスに公式からサービスアカウントの推奨されている使い方が記載されています。今回はこのベストプラクティスに従って、弊社がどのようにサービスアカウントを利用しているのか?具体例を交えながらご紹介していきます。

ローカルの開発環境

いきなりですが、ローカルの開発環境ではサービスアカウントを使用しないことが推奨されています。サービスアカウントではなく、開発者自身のユーザを使用します。そのために、 gcloud CLI が必要になります。

以下を実行すると、ユーザのログインが求められます。

gcloud auth application-default login

ログイン後、以下に認証情報が保存されます。

$HOME/.config/gcloud/application_default_credentials.json

この認証情報はサービスアカウントキーとは別物になるのですが、Cloud SDKなどの公式が出しているライブラリを使っている場合で、ADC(Application Default Credentials)を使った認証であれば区別する必要はありません。

ADCは以下の場所を探索してくれるため、アプリケーションコード上の初期化時には認証情報を明示的に渡す必要はありません。

  1. 環境変数 GOOGLE_APPLICATION_CREDENTIALS
  2. gcloud auth application-default login の任意の場所
  3. 接続されたサービスアカウント

ローカルのホストマシン上は、これで特に2をやっていれば何もやることないのですが、Docker環境の場合はリモートマシン上で、1~3を満たす必要があります。弊社の例は以下になります。環境変数に入れることもできるのですが、結局リモートにマウントすることになるので、この方法にしました。

version: '3'
services:
  app:
    volumes:
      - ~/.config/gcloud/application_default_credentials.json:/root/.config/gcloud/application_default_credentials.json
      # 省略

本番環境

サービスアカウントを使用します。公式の以下の図から何を使えばよいのか?がわかります。図からわかるようにほとんどの場合で、一番右下 Create service account key に該当することがありません。つまり、ユーザ自身がサービスアカウントキーを発行してそれを使う場面がほとんどないということです。

Google側で鍵の管理をしてくれて、安全にサービスにアクセスする仕組みが整えられていてありがたいですね。

「サービス アカウントを使用するタイミングを選択する」から抜粋

弊社では、GKEを使っていますが、最初は default の設定のまま使っていました。

GKEのノードプールにはサービスアカウントが紐づけられます。これは何もしないと、Compute Engineのデフォルトサービスアカウントになります。

こんな感じのやつ 👇

{プロジェクト番号}-compute@developer.gserviceaccount.com

上記は 編集者 という割と結構強めのロールが付与されています。そのため、本来ワークロードから使うことがないAPIも呼び出せてしまう権限を持っています。また、ノードプール中のノードおよびPodに関しても同様のサービスアカウントを使用して、Google Cloud APIが実行できるため、過剰なアクセス権を付与してしまうことになります。

Workload Identityを利用する

じゃあどうすれば良いのか?ということですが、弊社では、推奨されているWorkload Identityを使うようにしました。

基本的には公式の Workload Identity を使用する 方法が示されているので、その通りに実行していくだけです。

弊社では、すでに本番環境で動作中のため、既存のクラスタがある場合についての事例となります。

  1. クラスタWorkload Identity を有効化

     gcloud container clusters update {{ クラスタ名 }} \
         --zone=asia-northeast1-a \
         --workload-pool={{ GCPプロジェクトID }}.svc.id.goog
    
  2. Workload Identity を有効化したノードプールを新規作成

    既存のノードプールを更新することもできますが、現在ワークロードが本番環境で動作している状態で、Workload Identityを有効化すると、動作中のワークロードがCompute Engineのデフォルトのサービスアカウントを使えなくなり、一時的に停止してしまう可能性があるようです。

    そのため、今回は新しいノードプールを追加 → その後古いノードプールを削除するといった手順を取ります。

    これからGKEでインフラを構成しようと考えている方は、新規で作る段階から有効化することをオススメします!

     gcloud container node-pools create {{ ノードプール名 }} \
         --cluster={{ クラスタ名 }} \
         --workload-metadata=GKE_METADATA
    
  3. Kubernetes サービスアカウントを作成

     kubectl create sa {{ Kubernetesサービスアカウント名 }} \
         --namespace {{ ネームスペース(defaultネームスペースでも可) }}
    
  4. 利用するサービスアカウントを作成

     gcloud iam service-accounts create {{ サービスアカウント名 }} \
         --project={{ GCPプロジェクトID }}
    
  5. ④のサービスアカウントに必要なロールを付与

     gcloud projects add-iam-policy-binding {{ GCPプロジェクトID }} \
         --member "serviceAccount:{{ サービスアカウント名 }}@{{ GCPプロジェクトID }}.iam.gserviceaccount.com" \
         --role "{{ 付与したいロール }}"
    
  6. サービスアカウントとKubernetes サービスアカウントを紐づける(アノテーションを付与する)

    saserviceaccount の略なので、 kubectl create serviceaccount とやっても同じです。以降長いので sa の方を使います。

     kubectl annotate sa {{ Kubernetesサービスアカウント名 }} \
         --namespace {{ ネームスペース(defaultネームスペースでも可) }} \
         iam.gke.io/gcp-service-account={{ サービスアカウント名 }}@{{ GCPプロジェクトID }}.iam.gserviceaccount.com
    
  7. ワークロードのマニフェストアノテーション付きのKubernetes サービスアカウントを使用するように変更を加える

    ※このときに、 nodepool の指定が必要な場合は、既存に影響が出ないように nodeSelector に条件を追加( cloud.google.com/gke-nodepool=ノードプール名

     spec:
       template:
         spec:
           serviceAccountName: {{ Kubernetesサービスアカウント名 }}
           nodeSelector:
             iam.gke.io/gke-metadata-server-enabled: "true"
    
  8. 適用する(ここは各環境で、弊社は Kustomize を使っているため、以下のような感じ)

     kubectl apply -k {{ path/to/dir/ }}
    

これで無事にワークロード上で、Google Cloud APIの使用が確認でき、デフォルトサービスアカウントでの認証をしなくても良くなりました。

ちょっとだけセキュアになりました👮(Workload Identityが有効になっているノードプール以外のノードプールを忘れずに削除しましょう 🙆‍♂️)

今回はGKEに関してだけですが、デフォルトサービスアカウントが使われている箇所は他にもあるので、今後そこへの対応やっていきたいと思います。

🙂 まとめ

今回は、サービスアカウントの簡単かつ安全に利用するための基本的な方法と、実際に弊社でどう使っているのかについてご紹介しました。

サービスアカウントは非常に便利で、簡単にGoogle Cloud APIを利用をスタートすることができます。しかし、私がミスしたように、使い方を誤ってしまうと大きなセキュリティリスクにつながることになりますので、気をつけましょう。

今回は、開発環境とGKEについてのみの紹介となってしまいましたが、基本的にサービスアカウントキーを自分で発行して管理しないということが重要になってくるかと思います。少しでも、誰かの参考になれば幸いです。

さいごになりますが、弊社では一緒に働く仲間を募集しています。もし、ご興味のある方は以下のリンクからでも良いですし、私にDMしていただいても良いので、お気軽に声かけてください!

01. バックエンドエンジニア - 株式会社mikan

特にデザイナーは足りていません 😭

ご興味ある方は何卒お声がけください!!社内のデザイナーにもお繋ぎします 🧑‍🎨

04. デザイナー - 株式会社mikan

それでは、また次回もお楽しみに。

mikan Advent Calendar 2022 - Adventar

ExoPlayerとComposeでオーディオプレーヤーを作った際のTips

これはmikanのアドベントカレンダー 10日目の記事です。

9日目は@mizorinからmikanのフルリモート環境を支える工夫でした。 そういえば、Gatherを使ってみようかという話が結構前にあったなーと懐かしい気持ちで当時を振り返りながら、気づけば今では当たり前の習慣になっていてもはや生活の一部となっています。こんな便利な仕組みをどうやって作ったのかという背景が知れるのでぜひぜひこちらもみていってください。

はじめに

こんにちは。 mikanでAndroidエンジニアをしております、zukkeyといいます。

今回は、今年リリースされた機能の一つで、特定の教材のみでmikanで利用できる 「オーディオプレーヤー」 の機能を実装する際に、学び得た知見を書いていこうと思います。

本記事で、対象となる読者は次のとおりです。

対象読者

  • ComposeとExoPlayerを組み合わせた場合のハマったことや実装のちょっとしたTipsについて知りたい方
  • ある程度Composeを触ったことがあるという方

オーディオプレーヤーとは

mikanでは、オーディオ再生が可能な一部教材で使える聞き流し機能のことを「オーディオプレイヤー」と定義しています。

ExoPlayerとは

ExoPlayerとは、動画や音声を扱う際に利用するライブラリです。 ExoPlayerに関しては、詳細な公式ドキュメントが用意されているので、もしわからないことがあればこちらを参照すると良いです。 初めて実装する方はまずコードラボを、サンプルも用意されており、ExoPlayerの使用方法についてはこちらの方が丁寧かと思いますので割愛します。

今回、オーディオプレーヤーを実装するにあたって、Composeと組み合わせた際に4つTipsがあったのでそれらについて紹介させていただこうと思います。

目次

画面を跨いでオーディオを操作したい場合のTips

現在のmikanのオーディオプレーヤー部分の画面は次のとおりです。 トラック一覧画面からプレーヤー詳細画面への動きを載せてみました。

今回機能を実装する際に、Composeで作ってはいるものの、一部Composeに置き換えることができないという状態でした。

現状、mikanのアプリのトップ画面はComposeを使わない実装のままなので、プレーヤー詳細画面を開く時には、別途BottomSheetDialogFragmentを利用しなくてはなりませんでした。

プレーヤー詳細画面のBottomSheetDialogFragment内部では、Composeを利用しています。

この実装の場合だと、画面を跨いで再生位置、再生状態、どのトラックを選択しているかを保持する必要があります。

そのため、mikanではactivityViewModelsとして、オーディオ状態を管理するViewModelを用意するようにしています。

一部でかつ画面を跨いで状態を保存したい時は、activityViewModelsで状態を管理すると簡潔に実装できます。

トラック再生の落とし穴にハマらないためのTips

今回のオーディオプレーヤーを実装する際に、要件として、3秒早送りと巻き戻し、トラックの切り替え部分がありました。

実際にmikanアプリの挙動は次のとおりです。

この挙動を実装する際に、ExoPlayer側で用意されているseekTo関数を利用すると実現できます。

今考えると初歩的なミスではあるのですが、トラックの切り替え時にseekTo(windowIndex, positionMs)を利用している際にハマってしまっていたことがありました。

トラックはトラックに関する情報を持っていて、その中にTrackNumberがあるので、最初のうちはTrackNumberでseekTo(trackNumber, 0)のように呼び出すようにしていました。

しかし、教材によっては構造が異なることから次のようなパターンになっています。

  • 教材
    • アルバム1
      • トラック
    • アルバム2
      • トラック
    • アルバム3
      • トラック

mikanアプリでは、この構造の場合にTrackNumberは必ずしも同じではなく0 ~ 10、11 ~ 21のように変わります。

ExoPlayerにセットしている時には、アルバムそれぞれ全てに設定するのではなくトラックごとにセットしているので、アルバム1では0 ~ 11でたまたまindexがあっていたとしても、アルバム2では11 ~ 21のようになるので、TrackNumberでトラック切り替えを行おうとするとズレが出てきてIllegalSeekPositionExceptionが起きてしまいます。

そのため、seekTo(windowIndex, positionMs)を使う際には、必ずExoPlayerにセットしたリストのindexと合うようにしましょう。

この画面ではオーディオ処理を止めたいのだけど、Composeではどうやったらいいのと困った時のTips

mikanアプリでは、トラック一覧画面では再生してていいけど、アルバム一覧画面では音声を止めて欲しい、という要件がありました。

実際のアプリの挙動はこちらです。これは、トラック一覧画面からアルバム一覧画面へと戻る時の挙動です。

実はこの2画面、Composeで作っています。1つのFragmentにてComposeのスクリーンを切り替えているという感じです。

Composeでの遷移周りは、NavHostとrememberNavControllerで実装すると思います。

特定の画面で、挙動を変えたいということがユースケースとして出てくるのではないかと思います。mikanアプリでもこの音声を止めてほしいという要件がそうでした。

こういった場合に使うのが、addOnDestinationChangedListenerです。これは、rememberNavControllerの現在の表示しているスクリーンがどこであるかを検知できます。

実際にコードで書くと次のとおりです。

val navController = rememberNavController().apply {
    if (currentDestination?.route == AudioScreenType.ALBUM_SCREEN.routeName) {
        // 一番最初の初期状態の時に処理を変えたい場合はこちらに書く
    }
}
navController.addOnDestinationChangedListener { _, destination, _ ->
    if (destination.route == AudioScreenType.ALBUM_SCREEN.routeName) {
        // アルバム一覧画面へきた時の処理を書く
        // ここでExoPlayerの操作をしてしまうのがおすすめ
    }
}

destination.routeは、NavHostにてNavGraphBuilder.composableで指定したroute名と同じですので、単純にdestination.routeと同じであるかどうかの分岐を書くだけでよいです。

ただ、こちら注意点としましては、切り替えるたびに実行されることになりますので、適宜要件に応じてフラグを使うなり、ロジックを持っている側で弾いて早期リターンするなどが必要になってきます。

YouTubeっぽいボトムプレーヤーを作りたい場合のTips

YouTubeのアプリを再生して、バックキーなどの戻る操作をすると、下にミニプレーヤーが表示されると思います。

mikanでは、「ボトムプレーヤー」と呼んでいて、今回この挙動を実現して欲しいという要件がありました。

この挙動、実はComposeでswipeable修飾子を使うことで簡単に作れます。

実際にmikanで実装してみたパターンはこちらになります。

公式ドキュメントにも記載されてある通り、swipeable修飾子を利用することで「スワイプして閉じる」パターンを実装することができます。

swipeable修飾子は、rememberSwipeableStateを用いて状態を作成し、記憶することができます。

この挙動を実装するための全体のコードはこちらです。

val swipeableState = rememberSwipeableState(initialValue = States.EXPANDED, confirmStateChange = { state ->
    if (state == States.COLLAPSED) {
        // ボトムプレーヤーを閉じた時の処理を書く
    }
    true
})

BoxWithConstraints(
        modifier = Modifier
            .fillMaxWidth()
            .align(Alignment.BottomCenter)
    ) {
        val constraintsScope = this
        val maxHeight = with(LocalDensity.current) {
            constraintsScope.maxHeight.toPx()
        }
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .swipeable(
                    orientation = Orientation.Vertical,
                    state = swipeableState,
                    thresholds = { _, _ -> FractionalThreshold(0.5f) },
                    anchors = mapOf(
                        0f to States.EXPANDED,
                        maxHeight to States.COLLAPSED,
                    )
                )
                .offset {
                    IntOffset(
                        x = 0,
                        y = if (swipeableState.offset.value.roundToInt() < 0) 0 else swipeableState.offset.value.roundToInt()
                    )
                }
                .clickable(
                    interactionSource = remember { MutableInteractionSource() },
                    indication = null
                ) {
                    // タップした際の処理
                }
        ) {
            // ボトムプレーヤーのViewのComposableをここに入れる。今回は割愛。
        }
}

ポイントとなる点は、2つあります。

1つ目は、スワイプの状態を定義するのに、rememberSwipeableStateでスワイプの状態を作成し、状態とアンカーのペアを設定する必要があります。

まず、BoxWithConstraintsを利用して、ボトムプレーヤー自体のViewの最大の高さを取得します。次のコードの部分になります。

BoxWithConstraints(
        modifier = Modifier
            .fillMaxWidth()
            .align(Alignment.BottomCenter)
    ) {
        val constraintsScope = this
        val maxHeight = with(LocalDensity.current) {
            constraintsScope.maxHeight.toPx()
        }
}

次に、アンカーとswipeableの状態をペアに設定します。

ボトムプレーヤーのViewをBoxで囲い、swipeable修飾子で、anchorsにて初期位置を0と拡大を示すスワイプ状態(Expanded)と、最大の高さつまり下に移動した際にボトムプレーヤーの高さ分動いて欲しいので最大の高さを下に動かした時のスワイプ状態(COLLAPSED)をペアにして設定します。

今回は縦方向に動いて欲しいので、orientationでは、Orientation.Verticalを設定します。

thresholdsは状態がどこで変わるかの閾値を設定します。今回は半分で良いということだったので、0.5fを指定しました。

こうすることで、rememberSwipeableStateが記憶している縦方向の値をもとにスワイプの状態が紐づきました。

BoxWithConstraints(
        // 省略
    ) {
        // 省略
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .swipeable(
                    orientation = Orientation.Vertical,
                    state = swipeableState,
                    thresholds = { _, _ -> FractionalThreshold(0.5f) },
                    anchors = mapOf(
                        0f to States.EXPANDED,
                        maxHeight to States.COLLAPSED,
                    )
                )
            // 省略
        ) {
            // ボトムプレーヤーのViewのComposableをここに入れる。今回は割愛。
        }
}

2つ目は、offset修飾子で要素となるViewの位置を設定し、制限をかけることです。

今回は、縦方向つまり、yを可変な値に設定することで縦スワイプの移動を実現できます。しかし、1点だけ注意点があります。

次のように書いてしまった場合は、挙動が若干異なってしまいます。

BoxWithConstraints(
        // 省略
    ) {
        // 省略
        Box(
            modifier = Modifier  // わかりやすいように一部省略
                .offset {
                    IntOffset(
                        x = 0,
                        y = swipeableState.offset.value.roundToInt()
                    )
                } 
        // 省略
        ) {
            // ボトムプレーヤーのViewのComposableをここに入れる。今回は割愛。
        }
}

一瞬、うまくいったように見えるのですが、この場合だと上方向の制限がかかっていないので、上に引っ張ると隙間が開いてしまうという不具合が出てしまいます。

そのため、正確には次のように上方向、つまり0より小さい場合は初期位置から移動しないように0を指定して書くことで、期待しているスワイプの挙動を実現することができます。

BoxWithConstraints(
        // 省略
    ) {
        // 省略
        Box(
            modifier = Modifier  // わかりやすいように一部省略
                .offset {
                    IntOffset(
                        x = 0,
                        y = if (swipeableState.offset.value.roundToInt() < 0) 0 else swipeableState.offset.value.roundToInt()
                    )
                } 
        // 省略
        ) {
            // ボトムプレーヤーのViewのComposableをここに入れる。今回は割愛。
        }
}

おわりに

今回は、オーディオプレーヤーをExoPlayerとComposeで作った際に学んだTipsの紹介でした。

オーディオプレーヤーは、音声を繰り返し聞き流しているだけで頭に残ってくるので、覚えられるいい機能なのではないかと思います。音声を聞いて自分でも発音すると覚えるスピードが上がりそうです。ただ、私の場合は、実装するときのハマってつらい記憶も思い出しそうというのが、難点です。

同じようにして、プレーヤーを作ろうとしている方の参考になれば幸いです。

さいごに、mikanでは、一緒に英語学習をより良いものにしていくべくサービスを作っていく仲間を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!他の職種の方もぜひぜひ、ご応募ください〜〜

03. Android エンジニア

現在デザイナーを特に募集中です!ぜひぜひご応募いただけますと嬉しいです〜〜!

04. デザイナー

Androidエンジニアとしてmikanに入社し、あっという間に2年が過ぎちゃいました。

はじめに

こちらはmikanのアドベントカレンダー 4日目の記事になります!

1つ前の3日目の記事は@zukkeyGASとHARMOSとSlackではじめる勤怠Botでした!エンジニア問わず誰にでも自動化の一歩を踏み出させてくれる、頑張ってやってみようかな…!!と思わせてくれるそんな記事となっております!

こんにちは!mikanでAndroidエンジニアをしている、gumiojiです! twitter.com

今年も残すこと1ヶ月を切ってしまいました….!!

約2年ほど前に僕はmikanに転職してきました。今となってはすごく懐かしいですね。

当時の気持ちを綴った入社エントリーがあるので、そもそもなんで~を知りたい方はこちらへ note.com

この記事では、入社してもう2年も経ったのか…と思ったので、大まかに自身の振り返りをしながら、mikan Androidの変化というところを紹介してみようかなと思います!

入社直後

このときはほんとにいい意味で何も整ってない状態でのスタートだったので、どういうふうにしていこうかをあれこれ考えていました。

  1. 今後入ってくる人などが安心して開発を進めていけるような状態づくり
  2. iOSとの機能差分を進めて追いつく

主にこの2点を軸に動いていました。この当時のmikan Androidはmikan iOSと1年数ヶ月分くらいは差ができていた記憶がありますw

このときの主な対応は以下の通りになります

  • GithubのPRテンプレート改善
  • DebugのためにHyperion追加
  • Library周りの依存関係を集約して管理するためにDep.ktの設置
  • 隙あらば、Kt化とButterKnifeからviewBindingに変える

GithubのPRテンプレート改善

元々mikanにはGithubのPR時のテンプレートが設置されてあったのですが、成果物のBeforeとAfterを比較する箇所がなかったので、reviewerに優しくしようということでBefore / Afterを並べるように以下の部分を追加しました

Before | After
:--:|:--:
<img src="" width="300" /> | <img src="" width="300" />

DebugのためにHyperion追加

こちらも元々mikanには簡易的なDebugMenuはあったのですが、奥深くの設定に行かないと辿り着けないような場所にあったので、いつでも呼び出せるように、その他のDebug機能も触れるように。ということでHyperionを追加しました

詳しくはこちらへ

qiita.com

Library周りの依存関係を集約して管理するためにDep.ktの設置

依存関係の設定が直に殴り書きされていたので、Dep.ktを用意したいなと思った感じです。

ちなみに今はDep.ktはversion検知するのめんどくさいのと対応しないとアップデート忘れてしまうため、Gradle v7.0から追加されたversion catalogを使用するのが良いと思います。

star-zero.medium.com

また、renovateというこういう依存関係差分を自動で検知し、アップデートをPRで投げてくれる君があるのですが、上記のversion catalogに対応しているので、相性がよさそうですね…!!(まだ移行できてない)

github.com

隙あらば、Kotlin化とButterKnifeからviewBindingに変える

ほんとに余裕があれば、Kotlin化 + ButterKnifeを使用していたらviewBindingにセットで置き換えを行ってましたw 基本の姿勢は不具合や機能追加などで画面を少しでも弄る際に関連も含めて、やるという精神でやってました!

当初はKotlin化率が大体20%くらいの状態でした(!)

今は88.6%。ほとんどJavaを見る機会がなくなりましたね…. = 改修する機会が減って残りがしぶとく生きてるという状態

2021/01~05: リデザインPJ

最初に行ったPJがメインの教材一覧, 教材詳細, ホーム画面…etc.といったメイン画面のリデザインです。色んな画面を大きくデザインを変える、これは完全新規で作ってしまった方が速いですし、チャンスだなと思い、リデザイン対象画面の使用技術をガラッと変えました。

ちなみに当時のmikan Androidの構成はほぼMVCの状態でした。要件的には複雑な設計を用いる必要性もないと判断し、当時のGoogleさんがAAC使うならこんな感じにするといいよ!という推奨の形として構築しました。

このとき導入したのは以下でした!技術採用は提案ベースで積極的に使ってきましょ!と新しく入社される方、業務委託の方々含め、話してました。

  • AAC
  • Navigation
  • Dagger Hilt
  • Coroutine
  • Coroutines Flow
  • Groupie…etc.

ほんとは既存でDagger等々を色々導入してたら、もう少し移行などがややこしかったのでしょうが完全新規導入だったため、恐ろしくスマートに導入できたので何も書けません()

強いて述べるとしたら、新しい技術は新しい画面で使っていきたいですね!!(それはそうだ)

2021/06 ~ 2022/04: DB刷新PJ

上記リデザインPJを頑張ってる中、横目でiOSがDB刷新を行っているのを見ていたのですがこのPJを超えるものは今後出てくるのだろうか。。。そのくらい大変で印象的に残っています。期間も10ヶ月ほど使いました。

→ 余裕を持って技術調査などの期間もしっかりスケジュールに組み込ませてもらいました

DB刷新って何をするの??

iOS verでのDB刷新をiOSDCの場で発表してくれた @aviciidaによるスーパー分かりやすいスライドがここにあります!

speakerdeck.com

地獄への片道切符

このDB刷新PJで実はABテストを行いました。何を言ってるかわからないと思いますが、僕にも分かりません。当時に戻れるなら本間にやるんか!? やらへんよりもやる価値のが高いんか!? と言ってあげたいですね。そしたらまだ多少は楽で済んだのではないでしょうか….。 今戻るなら、ABテストはしないと思います…w

iOS先行で実施した際はABをやってなかったのですが、結果的にDB刷新はもうクローズしてよいのだろうか…..??? 何を見てそう判断しよう…??? 分からないから画面の表示速度をログとして飛ばすようにし、そこから分析で判断するようになっていたのがきっかけです。Androidでも同じようにデータ取得までの速度という観点でそれにKPIを紐付けてみるようにしてました。結果としては良くなってたので、すぐに判断できたのはABやっててよかったなとは思ってます。

こういった超絶大規模な変更などはABをやっても良いとは思うのですが、大前提として戻すことはない片道切符だったので、仮に数値影響が出たとしてもそこから改善をまわしていくという判断にしてもよかったのかなと思ってます。それでもこんな機会は二度あるかないかレベルなので、興味が止まらないわけですね。やってしまいましたね!!!!!

設計のお話

先行したiOSからヒアリングした上で実施したため、Androidでは結果的に以下の意見を踏まえ、実験的な構成にしました。

「firestoreのcache、ハンドルできなくない?? こっち側でオフライン対応を持ってしまえば、今後remoteDataSource何になってもオフライン利用できるんじゃないかなと。ついでに通信節約、パフォーマンス向上にもならないかな??」

こうした話から、Androidではキャッシュ層としてRoom(SQLite)を利用しています。一度Roomに問い合わせ、なかったらfirestoreに問い合わせ、Roomに保持するというようなよくある形です。

そのため、mikan Androidではfirestoreのcache機能をoffにしてます。なんでfirestore使ってんの??というツッコミはお控えください!

何が大変だった?

一番はやっぱり今までコアなデータをLocalDataとして持っていたRealmの引き剥がしでした。引き剥がしさえしてしまえば、後は楽だったのですが1つ剥がせば10箇所くらいから悲鳴があがるあがる…。そんな印象でしたねw

それにユーザーさんの大事な今までのデータがうっかりミスなどで吹っ飛んだり、変な間違ったデータになったりしないか(不整合と呼んでました)、神経質になりながら作業してるのが辛かったですね。PJを始める半年前、僕が入社する辺りにダブルライトPJというLocalDataにも移行先のfirestoreにも同じ内容を書き込むという手順を踏んでいたのですが、実装を通じてテストしていく中でLocalだけに書き込まれるパターン、Remoteだけに書き込まれるパターン..etc.が出てくる&出てくることで、これはあかん…。ってなったためです。

それに追い打ちをかけるのがABテストによる、確認の多さです。本来1パターンで済むものを何倍にも膨れ上がらしていたので、出来たてホヤホヤのQAチームとAndroidチームは悲鳴をあげていましたw

もちろんリリースまでの開発も大変だったのですが、リリース後も大変でした…。

リリース後は速度が遅い問題と向き合う

大前提としてABの結果からも全体的なパフォーマンスはよくなってはいたのですが、限られた環境下でパフォーマンス面にかなり問題があり、一部のユーザーさんには恐ろしく学習体験を低下させているという状況になり、お問い合わせがかなり来て大変でした。

→ リリース後すぐに速度改善PJが立ち上げるほどでした。

全員がそうなっているというわけではないのが大変ポイントで、原因はブラックボックスな状態からスタートしました。様々な検証や仮説を立てるためにログをたくさん仕込むなどを主に行い、パフォーマンス改善に取り組みました。

主な大きな原因は以下でした

  • Localからデータ取得してくる想定なのにRemoteにアクセスし続けている
  • 最新情報をfetchしてくる際の効率が悪い

1つはネットワーク接続状態を常に監視して、状態を変えていたのですが期待しない挙動をするときがあった。もう1つは別端末などで更新があったり、マスターデータといった書籍のデータを新しく保つために起動時にfetchしているのですが、それが遅すぎたというのがありました。

まだまだベストとは言えない

当時と比べるとかなりパフォーマンスは良くなったのですが、かなり快適か?といわれるとまだまだ改善ポイントはあると思っています。現在も速度改善は隙間時間を活用して、取り組んでいます。

とはいえ隙間時間で改善できる範囲はしれており、これ以上高速にするには現状の構成から大きく変更する必要があると思っており、今は現状の形で運用を続けています。

まとめ

  • 結果的にはRoomを合間に挟んでみる方式をやってよかったなとは思ってます
  • こういったデータ移行作業に立ち会える機会は同じ会社に居続けても中々ないことだと思いますので、いい経験になった
  • ほんとに繰り返しになりますが、アプリ全体に変更が加わるようなABテストをやるなら明確な目的をもって行いましょう(戒め)
  • パフォーマンス大事

2021/10 ~ 11: 突然のBilling Libのupdate期限に追われる

これはほんとに盲点で、課金系をあまり触ってこなかったという言い訳もするのですが上記のDB刷新を実装してる際にGoogle Playから、「お前らそろそろBilling Libraryを新しくしろよw」という通知が来てしまって、冷や汗でした。

なんでアップデート?

Google Playの課金ライブラリである、Billing Libraryは毎年Google IOのタイミングで、メジャーバージョンが上がる傾向があるのですが、新しいバージョンがリリースされてから、2年間がサポート期間となっており、移行期間に2年ほどの猶予が与えられます。

mikan Androidの当時はBilling Library? ナニソレオイシイノ? という状態で、BL2が宣言だけされてはいるものの、課金アクセスはAIDLが使用されてました…OH

少なくともBL3に上げないといけない!でも来年すぐにまたBL4に上げないといけない!という状態になるので、余裕を持つために最新のBL4にアップデートするプチPJを差し込みました。

しゃりふ神タイミング良すぎる

Android界隈で課金といえば〜?

しゃりふ氏〜!

twitter.com

はい。このときmikanでは最高のタイミングでしゃりふさんを業務委託としてお迎えしてましたので、超絶感謝の気持ちを込めて対応をしていただきました(ほんとにありがとうございます)

クライアント管理からバックエンドに移行

AIDL → BL4にぶっ飛んだわけなのですが、完全クライアント完結の課金管理をしていたため、このタイミングでDB刷新後にバックエンドに寄せるための準備対応を入れました。

  • 購入後の承認はバックエンドのAPI
  • 承認完了後のユーザーデータと課金情報を紐づけれるように裏側でレシート保存だけしておく

いつでも移行やステータスを引き継げるようにこのような状態のみに削って対応を済ませました。

削って準備だけに留めたのは、DB刷新を達成することが最優先だったのと、DB刷新後じゃないと好きなアカウントにアクセスしたりすることができないため、課金ステータスを引き継いだりさせるということができなかったため

アップデートはお早めに

今回のBilling Libraryだけに限らず、色んなLibraryやOSといったものは即座にアップデートすることを心がけましょう…!!!

mikanだったから即座に差し込み対応ができただけで、組織によっては差し込めないときもあると思いますので、お早めに。

2022/04 ~ 06: アカウント管理

一番大変なDB刷新PJが無事にリリースされ、本来のDB刷新の目的、どのPFからでもデータにアクセスできるように。を達成するためにアカウント機能を用意しました。

この時から、るんるん気分で実装をしていました。もうDB刷新を超えるものなんていないからねって。あれに比べたら~を口癖にしてた気がしますw

mikanではfirebaseを採用してるため、firebase authを利用し、以下のプロバイダで用意しました

  • Twitter
  • Google
  • メールアドレス
  • Custom Token = mikan for school

mikanではtoB向けの事業、mikan for schoolというのをやっており、通常のアカウント機能 + こちらの生徒さん向けのアカウント機能も同時に作成していました。

forschool.mikan.link

余談: Apple認証はあまり需要を感じなかったので用意していなかったのですが、皆さんはどうでしょう?? あったほうがよい!とかあったりしますか??

初めましてJetpack Compose

このタイミングでめでたくComposeデビューを果たしました。

Jetpack ComposeはAndroidが提供してるUI宣言型のUIツールキットです。

iOSでいうなら、SwiftUIですね。

developer.android.com

採用しない理由はなかったので、この頃からmikan Androidでは新規で作成するUIに関してはComposeを使用すること。業務委託の方にはちょこちょこ既存の画面をComposeに移行するタスクなどをお願いするようにしていました。

結論、Compose最高ですね!!!!!!!

ほんとに楽です。もうXMLは読めません。読みません。よろしくお願いします!!

これでめでたく、firestore上のデータにいつでもアカウント連携・ログインすることができるようになりました!さあ、mikan Androidの基盤完成まであと一歩です….!!!

2022/07 ~ 11: PRO PLUS対応

今までmikanは英単語アプリとして英単語の学習などを効率よくさせるために機能などを提供していたのですが、これからは英語アプリとして進化していくために英単語以外も1機能として提供していくことをiOS先行で始めてました。

こうした新たなリッチな機能の提供を既存の課金プランのmikan PROではなく、より上位の課金プランとしてPRO PLUSを用意することになりました。

PRO PLUSのことをもっと知りたい方はこちら!! mikantechnology.notion.site

この時点でiOSもPRO PLUSをリリースし、落ち着いたのが6月中旬でした。

皆さん僕が最初に言ってたことを覚えてますでしょうか!?!?

| この当時のmikan Androidはmikan iOSと1年数ヶ月分くらいは差ができていた記憶がありますw

そうなんです、もうiOSの背中がほぼ見えてきました!!!!

課金つらい….色々つらい….

Androidで課金実装をちゃんとしたのは今回が初めてでしたが、ほんとに考えることが多いんだなと思わされました。単なる上位プランの導入、アップグレード、ダウングレードのみだったらまだよかったのですが、先程あげたように課金ステータスをバックエンドで管理することが完了してません!

なのでこのPJでは僕が兼PMっぽいこともしていたこともあり、上位プランを導入 + 課金ステータスをバックエンドに寄せるという実質2つの対応が含まれたものになっていたため、スコープを細かく切って、段階的に開発、QA、リリースをするように進めてました。

同じことは繰り返さないということで、すぐさまBilling Libraryをv5にアップデートしたのはいいのですが、リリースしたてで情報が全然ない故に試行錯誤で検証することも多かったのがツラミポイントでした。この辺りの細かな話は18日のアドカレ記事で1本使って話そうかなと思っていますので、ぜひお楽しみにです!

まとめ

  • 課金って考慮することたくさんで大変だねb
  • テスト環境を用意しても確認が大変だねb
  • リリースほやほやのバージョンは全然情報がなくて大変だねb

宣伝

ありがたいことに11月中旬にリリース完了し、何も問題なく今を過ごしています!ほんとに何もなさすぎて怖いくらいです!何も起きないくらいmikanのQAチームは優秀です!

さて、今回僕が担当していたPRO PLUSでブロッキングをくらっていたPRO PLUSの機能や、それに付随したたくさんの教材たちが一気に開放されました!ぜひ一度iOSでもAndroidでも構いませんので、手にとって触ってもらえたらなと思います。よろしくお願いします!

現在進行形

そしてこの記事を書いている今に至るわけですが、仕事に夢中になっていたら2年が過ぎちゃいました…。ほんとにあっという間の2年でした。

mikan Androidの基盤はLegacyな部分もまだ残っていたりはしますが、2年前の自分が思っていたくらいには整ったと思っています。これから先はユーザーさんにより価値のあるプロダクトとしていい体験を届けれるように頑張っていこうと思います!

mikan Androidはこれからも常にモダンな形を模索しながら、チャレンジングに楽しみながらアップデートさせていこうと思います!

さいごに

いかがだったでしょうか? 最後までお読みいただき、ありがとうございます!

あまり役に立つことは今回書いていないのですが、誰かのためになればいいかなと思ってますw

そんなmikanでは最高の英語アプリを共に作っていける仲間を常に探しております!(特にデザイナー…!!)

お気軽にTwitterでも下記のHERP経由でもご連絡ください。主はSplatoonが大好きなのでSplatoon面談もWelcomeです!

herp.careers

明日は@gamiの記事です!お楽しみに!

GASとHARMOSとSlackではじめる勤怠Bot

これはmikanのアドベントカレンダー 3日目の記事です。

2日目は@satoshin21からEvolution of mikan iOS 2022でした。今のmikanのiOSアプリの全体像とこれからについてわかる内容だったので、iOSエンジニアの方はみていただけると嬉しいです。

はじめに

こんにちは。 mikanでAndroidエンジニアをしております、zukkeyといいます。

今回は、社員数が増えてきたこともあって勤怠システムのHRMOSを利用するようにしたが、自分たちで使いやすいように勤怠用のSlack Botを作ってほしいなぁ、という依頼があった際に業務の合間に密かに作っていたので、GASとHARMOSとSlackではじめる勤怠Botの作り方について紹介していこうと思います。

対象となる読者の方はこちらを想定しています

  • GASでSlack Botを作ってみたい方
  • HRMOS APIを使ってよしなに自分のBotを作ってみたい方

GASとは

まずはじめに、GASとはなんなのかということについてです。

GASとは、Google Apps Scriptの略称で、Googleによって開発されたアプリケーション開発のためのGoogleワークスペースプラットフォームです。

どういうことができるかというと、HTML、CSSJavaScriptを使って開発を行うことができます。定期実行も容易に行うことができ、Google Spread Sheetも併用して自動化することもできます。

HRMOSとは

HRMOSは、株式会社ビズリーチの採用管理システム、タレントマネジメントシステムです。

弊社では勤怠管理に利用させていただいております。

HRMOS勤怠では、APIを利用することができ、自分たちでカスタムしたい場合はドキュメントが用意されています。

今回は、このGASとSlackとHRMOS APIを組み合わせて勤怠Botを作成していきます。

HRMOS APIを利用するための準備

まず初めに、HRMOS APIを利用するために準備が必要です。

管理画面より、API KEY の作成を行ってください。

システム管理者でないとできないので、システム管理者に問い合わせましょう。

GASの準備

Google スプレッドシートにアクセスして、空白を選択し無題のスプレッドシートを新規作成します。

作成がおわると、次のような画面にきます。

拡張機能を選択して、Apps Scriptを選択します。

選択した後に、次のような画面にくるとGASの準備は完了です。あとはコードを書いていくだけになります。

一応試しに、console.log("Hello, World!") と入力してみて、実行を行ってみましょう。

次の画面が出たら、初めての実行は成功です。

勤怠情報を取得しよう

Botを作っていく前に、せっかくHRMOS APIも利用できるようになったので、GASとHRMOS APIを使用して勤怠情報を取得する部分を作成しましょう。

勤怠情報取得までの流れとしては、次のとおりです。

  1. 管理画面で発行したAPI KEYのSecret Keyをスクリプト プロパティに保存する
  2. Secret Keyを使用してBasic認証を行ってTokenの発行を行う
  3. 発行したTokenを用いて、会社に所属するユーザーの一覧を取得する

管理画面で発行したAPI KEYのSecret Keyをスクリプト プロパティに保存する

まずは、Secret Keyのような値をベタで書くこともできますが、セキュリティ上好ましくないということでスクリプトプロパティを利用します。

スクリプトプロパティとは、GASで1つのドキュメントに対して、key-value方式で文字列を保存することができる仕組みです。

コードで設定できる方法もありますが、今回は手動で設定してしまいます。

GAS上の歯車のアイコンを選択します。

遷移後、一番下にいくとスクリプトプロパティの項目を確認できると思います。

次に、スクリプトプロパティを追加を選択します。

プロパティにHRMOS_API_KEYと入れて、値には管理画面で発行したAPI KEYのSecret Keyを入れるようにしてください。

最後に、スクリプトプロパティを保存を選択します。

これでスクリプトプロパティの保存は完了です。

Secret Keyを使用してBasic認証を行ってTokenの発行を行う

次は、HRMOS APIv1/authentication/token を利用してTokenの取得を行いましょう。

まず初めに、先ほどスクリプトプロパティに保存した値を読み取る部分から行っていきましょう。 スクリプトプロパティの値を読み取るには、PropertiesServiceを利用します。

GASでエディタに戻って、次のように記載します。

function getToken() {
  const ps = PropertiesService.getScriptProperties()
  const apiKey = ps.getProperty('HRMOS_API_KEY')
  console.log(apiKey)
}

これを実行して、下記のように先ほどスクリプトプロパティに設定した値が表示されていればOKです。

例では、hogehogefugafugaと指定したので、その値がログに表示できていることを確認できました。

次に、Basic認証を行ってHRMOS勤怠のAPIを使用するためのTokenを発行する部分を実装していきます。

Tokenを発行するためのエンドポイントは、https://ieyasu.co/api/company_url/v1/authentication/token であることが、ドキュメントより確認できます。

ここで、company_urlは、企業ごとに異なります。

次のようなHRMOS勤怠にログインする画面をみたときに、表示されているURLの中にあります。 例で挙げると、次のとおりです。

https://f.ieyasu.co/{ここに書かれています}/login

これも、API KEYの場合と同様にして、 HRMOS_COMPANY_URL というキー名でスクリプトプロパティに保存しておきましょう。先程と同じ手順になるので、ここは省略します。

エンドポイントが何かわかったので、GASで通信する部分について実装していきます。

UrlFetchAppというクラスを使うことで、容易に通信することが可能です。

UrlFetchApp.fetch(url, params)で、urlにはエンドポイントを指定し、paramsにオプションとしてリクエストのメソッドの種類やヘッダーなどの詳細情報を含めることができます。

何を指定できるかはドキュメントにも書いてありますが、次のとおりです。

これらを参考にして、実際にコードを書いていきます。これを実行してコンソールにログを出すようにすると、トークンが発行できていることがわかります。

function getToken() {
  const ps = PropertiesService.getScriptProperties()
  const apiKey = ps.getProperty('HRMOS_API_KEY')
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/authentication/token'
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Basic ' + token
  })
  const options = {
    method: 'get',
    headers: headerInfo(apiKey),
    "muteHttpExceptions": true,
  }
  const response = UrlFetchApp.fetch(endPoint, options)
  const json = JSON.parse(response.getContentText())
  return json.token
}

Headerはドキュメントにあるとおりに指定します。

muteHttpExceptionsをtrueにすると、HTTPResponseを返してくれるようになるので指定しましょう。

getContentTextで得られる値は、jsonになっているので、JSON.parseで処理可能なオブジェクトに変更します。

これで、Tokenの発行を行うことができました。

発行したTokenを用いて、会社に所属するユーザーの一覧を取得する

Tokenを発行したので、次に会社に所属するユーザーの一覧を取得するようにします。

ユーザーの取得を行う必要があるのは、打刻登録のAPIを利用するのにユーザーのIDを必要とするからです。

先程とほとんど変わらず、エンドポイントが変わることと、headerInfo部分の Auth部分が先程実装したTokenを利用する形になります。

ユーザーを取得するためのエンドポイントは、 https://ieyasu.co/api/company_url/v1/users であることが、ドキュメントより確認できます。

コードを書いていきます。これを実行してコンソールにログを出すことで、ユーザーの情報を取得できるようになっていると思います。

function getUserInfo() {
  const hrmosToken = getToken()
  const ps = PropertiesService.getScriptProperties()
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/users'
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Token ' + token
  })
  const options = {
    method: 'get',
    headers: headerInfo(hrmosToken),
    "muteHttpExceptions": true,
  }
  const response = UrlFetchApp.fetch(endPoint, options)
  const responseBody = JSON.parse(response)
  console.log(responseBody)
}

取得するページを指定したり、取得件数の制限があったりするので取得したい情報に合わせて変更してみてください。

SlackアプリとGASで連携しよう

Slackアプリの作成部分に関しては、いろんな方が書かれていることもあるので、必要な部分だけ記載してざっくりといきます。

Slack Botを作成するには、Slack appを作る必要があります。こちらより、Create an appで作成しましょう。

Create an app > From scratch を選択します。

App Name を決めて、Pick a workspace to develop your app in のところで、導入したいworkspaceを選んでください。

Slack Appの方はいくつか設定が必要です。下記の項目を有効にしてください。

そして、WorkspaceにSlackアプリをインストールしましょう。

ざっくりと準備はこんな感じです。利用するAPIに応じてScopeの設定が必要になる点が注意です。

弊社の勤怠Botの要件は次のとおりでした。

  • 出勤や退勤などの特定の単語で打刻ができること
  • 打刻が失敗したら何かわかるといい
  • 打刻が成功した場合は、リアクションがついてくれると嬉しい

これらの要件を満たす為には、下記フローが必要でした。

  1. Event Subscriptionsを有効化して、特定のチャンネルに投稿された特定のワードで打刻を行う
  2. 打刻が失敗した場合は、対象のSlackメッセージのスレッドにBotからのエラーメッセージを表示する
  3. 打刻が成功した場合は、対象のSlackメッセージにリアクションを付与する

それぞれに分けて説明していきます。

Event Subscriptionsを有効化して、特定のチャンネルに投稿された特定のワードで打刻を行う

まずは、Slackでメッセージを送信した後にSlackからGAS側にリクエストを送信し、GAS側でSlackからのリクエストかどうか検証する部分から確認していきましょう。

Slack App側で、Event Subscriptionsを有効化すると、doPost関数で受け取ることができます。

doPost関数は次のように実装します。forwardMessage関数は後ほど説明します。

function doPost(e) {
  const ps = PropertiesService.getScriptProperties();
  const json = JSON.parse(e.postData.getDataAsString());
  if (ps.getProperty("VERIFICATION_TOKEN") != json.token) {
    throw new Error("invalid token.");
  }
  if (json.type == "url_verification") {
    return ContentService.createTextOutput(json.challenge)
  }
  const token = ps.getProperty("BOT_TOKEN")
  const text = json.event.text
  const eventType = json.event.type
  const user = json.event.user
  const ts = json.event.ts
  forwardMessage(token, eventType, user, text, ts)
}

例のごとく、スクリプトプロパティに保存した値を利用します。

Slackからのリクエスト情報として、getDataAsStringでjson形式のデータを取得することができます。

受け取ったJsonの中に、tokenがあり、これはSlackからのものであることを検証する為に利用します。

VERIFICATION_TOKENは、Slackアプリ側の Basic Information > App Credentials > Verification Token にて表示されている値を、ここではスクリプトプロパティに保存して読み取っています。

ContentService.createTextOutput(json.challenge)では、Slack のEvents API ドキュメントのURL verification handshakeに記載されているとおり、最初にイベントリクエストURLを確認する必要があるので、その確認に利用しています。

ContentService.createTextOutputは、GAS側で用意されている関数でテキストを返してくれます。

BOT_TOKENは、Slack アプリ側の OAuth & Permissions > OAuth Tokens for Your Workspace > Bot User OAuth Token の値をスクリプトプロパティに保存して読み取っています。

このようにして、まずはGAS側にSlackからの応答であることを確認する部分を最初に実装してしまいます。

次に、特定ワードの検証部分についてです。

特定ワードの検証部分については、forwardMessage関数に記載しているので、そちらをみていきましょう。

function forwardMessage(token, eventType, user, text, ts) {
  const triggerWord1 = "出勤"
  const triggerWord2 = "退勤"
  const triggerWord3 = "休憩"
  const triggerWord4 = "休憩戻り"
  const triggerWord5 = "ヘルプ"
  if (eventType == "message" && text === triggerWord1) {
    registerStamp(user, 1, "in", ts)
  } else if (eventType == "message" && text === triggerWord2) {
    registerStamp(user, 2, "out", ts)
  } else if (eventType == "message" && text === triggerWord3) {
    registerStamp(user, 7, "bi", ts)
  } else if (eventType == "message" && text === triggerWord4) {
    registerStamp(user, 8, "bo", ts)
  } else if (eventType == "message" && text === triggerWord5) {
    postToSlack(token, "<@" + user + "> さん\nこちらがヘルプのNotionです \n https://hogehogefugafuga", ts)
  }
}

registerStampとpostToSlackは後ほど説明するので、割愛します。

メッセージの検証はとても単純で、ベタで決めている文字列と、Slackからのテキストを比較して、条件分岐しているだけです。

もっと汎用性を高くするのであればスプレッドシートに保存して、特定の列にある言葉のリストのどれかにマッチしているか、というようなことをやるといいのかなと考えてはいるのですが、今回は簡易的に済ませました。

特定ワードの検証はこれだけで済むので、次は打刻部分についてみていきましょう。

registerStamp関数にまとめています。詳細は次のとおりです。コード中の...は省略を示します。

function registerStamp(user, stampType, emojiName, ts) {
  const hrmosToken = getToken()
  const ps = PropertiesService.getScriptProperties()
  const endPoint = 'https://ieyasu.co/api/' + ps.getProperty('HRMOS_COMPANY_URL') + '/v1/stamp_logs'
  const headerInfo = token => ({
    'Content-Type': 'application/json',
    'Authorization': 'Token ' + token
  })
  const sheetId = ps.getProperty('SHEET_ID')
  const spreadById = SpreadsheetApp.openById(sheetId)
  const spreadsheet = spreadById.getSheetByName("勤怠管理")
  const values = spreadsheet.getRange("A2:B46").getValues()
  const searchIndex = values.findIndex(value => value[1] == user)
  const targetIndex = searchIndex + 2
  const targetRange = "D" + targetIndex
  spreadsheet.getRange(targetRange).setValue(emojiName)
  const userId = values[searchIndex]
  const query = {
    user_id: userId,
    stamp_type: stampType
  }
  const options = {
    method: 'post',
    headers: headerInfo(hrmosToken),
    payload: JSON.stringify(query),
    "muteHttpExceptions": true,
  }
  const response = UrlFetchApp.fetch(endPoint, options)
  ...
}

打刻部分は、HRMOS APIの/v1/stamp_logsを利用します。

以前説明しているgetToken関数で、HRMOSのトークンを取得するようにして、例のごとく打刻登録用のエンドポイントと、通信を行うのに必要なパラメーターを設定していきます。

今回私が作成しているBotでは、先に説明している所属企業の社員のuser idをスプレッドシート側にまとめるようにしているので、SpreadsheetApp.openByIdで指定したスプレッドシー トを利用して、getSheetByNameにて対象のシートから必要な情報を取得するようにしています。

A列にUser Idを入れているので、searchIndexではSlackのメッセージを送ったuser idとシートに保存しているuser idを比較して対象の位置を割り出し、targetIndexでスプレッドシート上のヘッダー分を+2して実際のリストのindexを取るようにしています。

targetIndexを用いて、そのユーザーの現在の勤怠情報を保持しておきたいので、スプレッドシートに記録する部分をsetValueにて行っています。

あとは、HRMOSドキュメントの定義に合わせて、queryを作ります。

user_idとstamp_typeがrequiredになっているので、それらをforwardMessageでメッセージごとに指定した値を渡してあげるようにします。

あとは、ユーザー一覧を取得した時と同じようにUrlFetchApp.fetchで通信するだけです。

レスポンスを受け取っているので、成功か失敗かに応じて分岐させます。

function registerStamp(user, stampType, emojiName, ts) {
  ...
  const response = UrlFetchApp.fetch(endPoint, options)
  const botToken = ps.getProperty("BOT_TOKEN")
  const responseCode = response.getResponseCode()
  const responseBody = response.getContentText()
  const retryCounterTarget = "E" + targetIndex
  if (responseCode === 200) {
    // 成功した時の処理
  } else {
    // 失敗した時の処理
  }
}

getResponseCodeで、レスポンスコードを取得できます。とりあえずドキュメントに記載がある通り、200であれば成功なので、それだけの分岐を行うようにしています。

打刻が成功した場合は、対象のSlackメッセージにリアクションを付与する

こんな感じに投稿したメッセージ対してリアクションをつけるようにするところを説明していきます。

Slackメッセージで、リアクションをつける場合は、reactions.add APIを利用します。

注意点としましては、作成したSlack App側にScopeの設定がされていないとつきません。 まだ未設定の場合は、reactions:write を指定するようにしてください。

打刻が成功した場合なので、前の節の成功した時の処理のところに、次のaddReactionToSlackを呼ぶようにします。

function addReactionToSlack(token, emojiName, ts) {
  const ps = PropertiesService.getScriptProperties()
  const channelId = ps.getProperty("SLACK_CHANNEL_ID")
  const payload = {
    "token": token,
    "name": emojiName,
    "channel": channelId,
    "timestamp": ts
  }
  const options = {
    "method": "post",
    "payload": payload
  }
  UrlFetchApp.fetch("https://slack.com/api/reactions.add", options);
}

tokenで渡すのは、Slack BotのTokenです。xoxb- から始まる値を指定してください。

SLACK_CHANNEL_IDは、Slackのチャンネルの詳細のAboutの一番下に記載されています。SLACK_CHANNEL_IDというキー名でスクリプトプロパティに保存しているので、そこから読み取るようにしています。

timestampは対象のメッセージより取得できるので、doPostから渡してくるようにします。

nameには、付けたい好きなSlack絵文字の名前を指定するようにしてください。今回のBotでは勤怠に使用しているリアクションのアイコンの名前を指定するようにしています。

それぞれ設定し終えたら、UrlFetchApp.fetchで通信するだけです。今のところ実装が雑になってしまっているのですが、ここもエラーハンドリングする場合はresponseを受け取って分岐させるようにしましょう。

打刻が失敗した場合は、対象のSlackメッセージのスレッドにBotからのエラーメッセージを表示する

こんな感じに投稿したメッセージ対してエラー内容を表示して投稿するところを説明していきます。

Slackメッセージで、メッセージを送信したい場合は、chat.postMessage APIを利用します。

注意点としましては、作成したSlack App側にScopeの設定がされていないと投稿できません。 まだ未設定の場合は、chat:write を指定するようにしてください。

今回は、打刻が失敗した場合なので、前の前の節の失敗した時の処理のところに、次のpostToSlackを呼ぶようにします。

function postToSlack(token, message, ts) {
  const ps = PropertiesService.getScriptProperties()
  const channelId = ps.getProperty("SLACK_CHANNEL_ID")
  const payload = {
    "token": token,
    "text": message,
    "channel": channelId,
    "thread_ts": ts
  }
  const options = {
    "method": "post",
    "payload": payload
  }
  UrlFetchApp.fetch("https://slack.com/api/chat.postMessage", options);
}

前の節と同様、tokenで渡すのは、Slack BotのTokenです。xoxb- から始まる値を指定してください。

SLACK_CHANNEL_IDは、Slackのチャンネルの詳細のAboutの一番下に記載されています。SLACK_CHANNEL_IDというキー名でスクリプトプロパティに保存しているので、そこから読み取るようにしています。

thread_tsでは、タイムスタンプを指定しています。指定することでスレッドに対してメッセージを送信することができるようになります。

今後の改善点と注意点

この勤怠Botの仕組みとしましては、いくつか改善点があります。

一つは、打刻の単語判定部分です。今は固定の文字列で分岐しているので、使いたい言葉を設定してそれぞれの言葉に対して設定できるようにする、といったことができると、より自分好みに打刻することができるようになると思います。

単純に、スプレッドシートに記録しておきそこに含まれているかどうかだけで判定できるようになると思いますので、空き時間が出来次第やっていこうかなと思います。 もう一つは、エラー周りです。

現在は、エラーログをGCP上に記録されるように送っているのですが、詳細がわかりづらいことと、他にはエラーが起きた際に利用者へよりわかりやすいメッセージを出す、といったことがあって良いのかなと思っています。

これも時間をとってどこかでやりたいなと思います。

他に、勤怠Botの改善点でもありつつ、注意点があります。

勤怠Botの今の仕組みの注意点として、HRMOS Tokenの更新の仕組みが必要です。やろうやろうと後回しになってしまっていてまだ実装していないのですが、更新しないとある特定の期間で動かなくなってしまいます。

これは次回更新時にきちんと更新されるように、実装しようと思います。

おわりに

今回は、業務の合間に依頼があったので、GASとHRMOS APIとSlackを連携して作る勤怠Botの紹介でした。

以前はBotを作成するとなるとHerokuを使ったりサーバー立てたりとかいろいろと面倒だったのですが、GASで作ってみるととても簡単に作れるので、非常におすすめです。

今回は勤怠Botとしての実装の紹介だったので、HRMOSのパターンでの紹介でしたが、他のAPIでも同様にして、さまざまな自動化を行うことが容易に可能です。

公式ドキュメントをみて実装していけば、誰でも簡単に自動化が可能だと思いますので、ぜひぜひやってみてください。

さいごに、mikanでは、一緒に英語学習をより良いものにしていくべくサービスを作っていく仲間を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!他の職種の方もぜひぜひ、ご応募ください〜〜

03. Android エンジニア

現在デザイナーを特に募集中です!ぜひぜひご応募いただけますと嬉しいです〜〜!

04. デザイナー

WorkManagerの内部を見てみた

f:id:qurangumio:20210907153258p:plain

はじめに

皆さん、こんにちは!mikanでAndroidエンジニアをしているgumioです。

twitter.com

今回はmikan androidにWorkManagerを導入しようとした際に色々面白かったので、それを紹介しようかなと思います。

こちらの記事は以下のような流れになっています。

  1. WorkManagerを知らない人向けに簡単な概要とメリット
  2. WorkManagerのユースケース
  3. WorkManagerの内部挙動
  4. 検証してtoolなどを使って覗いてみた
  5. さいごに

WorkManagerを知らない人向けに簡単な概要とメリット

Android Jetpackの仲間です。

developer.android.com

あるバックグラウンドタスクを実行するScheduler APIです。

今までバックグラウンドタスクに利用されていたFirebaseJobDispatcherGcmNetworkManagerJobSchedulerなどがあったが、WorkManagerを利用すれば以下の図のように内部で分岐してくれるので、API levelとかを意識する必要がないメリットがある + 電池寿命も考慮されており、電池寿命が伸びるメリットもあると公式は言うております。

f:id:qurangumio:20210907150508p:plain
WorkManagerの内部分岐について

ちなみにWorkManagerはAPI level 14から機能するので、安心してください。

WorkManagerの全体的なメリットとしては

  • API level 14から気にせずに機能する
  • シンプルで一貫性がある
  • 信頼性の高い非同期タスク
  • 電池寿命がよくなる
  • RxとCoroutineのサポートもされている

今回WorkManagerの詳細な使い方などは日本語ドキュメントが充実しているため、紹介しないです。

WorkManagerのユースケース

Coroutineとはどこで使い分けたらいいの?どういう時にWorkManagerを使うべき?とか少なからず思いますよね。

androidドキュメントににバックグラウンドタスクのタスクのガイドが用意されてますので、これに従うでいいと思います。

developer.android.com

僕なりの解釈としてはこんな感じだと思いましたが、どうでしょう?

  • 短時間 or 長時間かかって、裏側で必ず動かしてほしいタスク
    • 厳密には書かれていませんが、10 分以上は実行できるらしい
  • 電源が急に落ちる、いきなりプロセスキルされても必ず実行されたい 延期させたい: ネットワークがオフラインになっている、充電が充分にない場合に延期させて、条件を充分に満たした場合に実行したい

公式でも以下のように述べられてます

WorkManager は、ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業を対象としています。次に例を示しますログやアナリティクスをバックエンド サービスに送信するアプリデータをサーバーと定期的に同期する

mikanでもログを送るときに使用しようとしています。

アプリデータをサーバーと定期的に同期するはfetchしてくるイメージだと思います。アプリのデータをpushすることもできなくはないですが、WorkManagerが入力を受け付けれるをデータ量は10KBまでとなっているので、あまり何かを載せるのは期待できません。

それでは、次に実際の挙動を見ていきましょう!

WorkManagerの内部挙動

さて、今回一番紹介したい部分で、WorkManagerの謳い文句の

「ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業」

これって、結局どうやってるんだろう??ってなりました。 そこで内部の動きを簡易ではありますが、追ってみます。

まず、簡単なWorkManagerの流れのおさらいですが

  1. 行いたいタスク(Worker)を定義する
  2. このタスクの実行方法とタイミングを決めるRequestを作って依頼する
  3. WorkManagerを通じて、システムに2.のRequestを飛ばす(enqueue)

すごくシンプルですね。

このとき、WorkManagerのStateは以下の図のようになっていて、一度きりのRequest(OneTimeWorkRequest)と定期的なRequest(PeriodicWorkRequest)で少し異なりますが、終わりがあるかないかの違いだけで基本は同じです。

以下、処理の状態  |  Android デベロッパー  |  Android Developers より

f:id:qurangumio:20210907150506p:plain
一度きりのRequestのとき

f:id:qurangumio:20210907150503p:plain
定期的なRequestのとき

それでは、enqueueされたあとのRequestたちはどのように扱われてるのでしょうか?

WorkManager basics. Getting started with WorkManager | by Lyla Fujiwara | Android Developers | Medium より

f:id:qurangumio:20210907150513p:plain
enqueueされたあとの流れ

  • Internal TaskExecutorが、requestされたものをWorkManager DB(Room)にWorkRequest情報を保存します
    • Internal TaskExecutorはsingle threadのExecutorで、実際にenqueueされたrequestをさばく役割を持っています
  • WorkRequestのConstraintが満たされると(すぐにでも可能ですが)、Internal TaskExecutorはWorkerFactoryにWorkerを作成するように指示します
  • その後、defaultのExecutorは、mainThreadからWorkerのdoWork()メソッドを呼び出します

developer.android.com

なるほど!RequestされたものがDBに保持されていたんですね。このDBにはRoomが使われてました!! Requestされたものは制約が満たされるまではDBでおとなしくしてるんですね。この制約は端折っていましたが、Requestを作るときに指定することができます。

例えば、ネットワークがオンラインのときかつ、充電中のときのみ実行される制約が設定されます。

val constraints = Constraints.Builder()
  .setRequiredNetworkType(NetworkType.CONNECTED)
  .setRequiresCharging(true)
  .build()

先程のStateでRUNNINGになってたとしても処理が完了する前に電源落とされたり、オフラインになったりするとENQUEUEEDに戻り、またDBに大人しく返ってたんですね。

改めて、最初の謳い文句を再度見たときに、なるほどとなるのではないでしょうか?

「ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、確実に実行する必要がある作業」

検証してtoolなどを使って覗いてみた

最後に少しだけ挙動を確認してみて、終わりにしようと思います! 以下のような1...10を出力する簡単なWorkerを作成します。

→ 例ではWorkerではなく、CoroutineWorkerを使用していますが、こうするだけでdoWorkはsuspend関数となり、内部ではCoroutineが使われるようになりますb

class TestWorker(
  appContext: Context,
  workerParams: WorkerParameters
): CoroutineWorker(appContext, workerParams) {
  override suspend fun doWork(): Result {
    repeat(10) {
      Log.d("TestWorker", it.plus(1).toString())
      delay(1000L)
    }
    return Result.success()
  }
}

次にこのWorkerをRequestします。一度きりでネットワークに繋がってるときのみの制約にしています。

補足: 前述したようにWorkerをRequestするときは、一度きりと定期的で分かれます。

また、実行条件の制約をつける場合はConstraints.Builderで指定し、setConstraintsにsetしてあげる必要があります。

val testWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<TestWorker>()
  .setConstraints(
    Constraints.Builder()
     .setRequiredNetworkType(NetworkType.CONNECTED)
     .build()
   ).build()

最後に適当なボタンを用意して、enqueueするようにして、実行してみましょう 途中でオフラインにしてみます。

f:id:qurangumio:20210907150527p:plain
実行中にオフラインにして、キャンセルされてる

Workerがキャンセルされてることが確認できますね。 このとき、最近機能が充実してAndroid StudioのApp Inspectionで覗くことができるので、見てみましょう!!

App Inspectionとは、RoomやWorkManagerといったdebugをしづらいものをAndroid Studioがdebugしやすいように用意してくれたサポートtool郡のことを指します。

まずはDB Inspector

f:id:qurangumio:20210907150523p:plain
DB InspectorでWorkManagerが作成しているworkdbを見ている

どうやら、内部でWorkManager用にworkdbというのが内部で自動作成されていることがInspectorからわかりますね。これが先程の内部挙動で紹介したRequestされたWorkerを保持しておくために必要なDBということが予測できます。

色んなtableがありますが、試しにWorkSpecというtableを開いてみましょう!

名前の通り、各Workerの詳細情報を持っていることが伝わりますね。

f:id:qurangumio:20210907150534p:plain
workdbのWorkSpecテーブルを見ている

次にArctic Fox Canary 3で新たにWorkManagerのdebug用に追加された、Background Task Inspectorを見てみましょう。

f:id:qurangumio:20210907150538p:plain
Background Task Inspectorを見ている

最後のWorkerが途中でオフラインにされたWorkerなのですが、ENQUEUEDになっていることがわかります。 この状態でアプリをオンラインにして再度立ち上げたら、再度Workerが動き出して無事に処理されました!!

さいごに

いかがだったでしょうか? 今までWorkManagerとは関わる機会がなかったのですが、今回導入することをきっかけにキャッチアップをし、気になるところなど、学んだことを書いてみました。

mikan androidではこのように新しめな技術を気軽に試して、導入できるかどうかを検証するのも業務の一環です。まだまだやりたいことがたくさんあり、メンバーが足りないので少しでも興味を持った方は是非お話しましょうー!

herp.careers

チームでスキーマ設計した時のハマりポイントと勘所

f:id:nixii_squid:20210903185934j:plain

はじめに

こんにちは。mikanでバックエンドエンジニアをしているnixiiです。

今年の7月にmikanにジョインし、そこから1ヶ月ちょっとが経ちました。最近は本格的に開発にもコミットするようになり、本エントリテーマにあるようなスキーマ設計を議論したり、がっつりコードを書いている日々です。

先日出した入社エントリから早くも1ヶ月が経ったと思うと、時の流れは早いな〜と実感しています。

note.com

こちらの入社エントリでは、私の自己紹介や、なぜmikanにジョインしたのかについて色々と深ぼって書いています。是非ちらっと読んで頂けると嬉しいです!

。。と前置きが長くなりましたが、今回は「チームでスキーマ設計した時のハマりポイントと勘所」と題して、私を含めバックエンドチームがスキーマ設計を行った際に出会ったつまづきと、そこから得た学びをシェアしていきます。

そもそもスキーマ設計とは

スキーマ(Schema)は広義には「図式・構造」などと訳され、データベース分野においては「DB(データベース)の構造」を指します。

例えばRDB(リレーショナルデータベース)であれば、現実で扱いたいデータに応じてテーブルを定義し、必要なカラムも考慮した上で、テーブル同士の関連についても考えることになると思います。こういったDBの構造(= スキーマ)の設計を考えることが、即ち「スキーマ設計」です。

特にテーブル間のリレーションなど、実世界をデータモデリングしているものは「概念スキーマ」、それをRDBであればSQL文などで表現したものを「外部スキーマ」と呼びますが、本エントリでは便宜上、両者スキーマの設計を合わせて「スキーマ設計」と呼んでいます。

ハマりポイント

今回mikanでは、英単語アプリで新たな教材をアプリ上で扱うため、その教材に応じたスキーマを新たに設計する必要がありました。

スキーマ設計を進めていくうち、こうした設計タスクなどでよく出会うハマりポイントに、今回バックエンドチームもハマってしまっていました。具体的には以下の2つが挙げられます。

  1. チーム内で設計への見通しが異なっていた

  2. 検証フェーズについて考慮漏れしていた

「チーム内で設計への見通しが異なっていた」点について

スキーマ設計の工数としては、元々チームでは1日程度と見積もっていました。が、実際には素案レベルでのスキーマをチーム内で仕上げ、ビジネスサイドと認識をすり合わせつつ調整するために、結果としてトータルで3, 4日以上かかってしまっていました。

元々1日で完了すると踏んでいた理由として、じつは過去すでに類似したスキーマを作っており、このスキーマが流用出来ると考えていた点があります。しかし実際には、このスキーマは今回RDB向けに設計しようとしていたフォーマットとは異なり、Cloud Firestore向けに設計していたものでした。

Cloud FirestoreはNoSQLのドキュメント指向データベースで、key-value値が入れ子構造のようにドキュメントとして保存されていく構造です。これは各テーブルを必要に応じて正規化したり関連させることでデータモデルを表現するRDBとは大きく構造の異なるデータベースで、当然出来上がるスキーマも自ずと違ってきます。

このように前提の異なるスキーマを再利用するのは実際にはかなり難しく、チーム内で議論しつつ、RDB向けにほぼスキーマを設計し直す形となりました。結果として工数が膨らみましたが、元々資産として参考にする予定だったスキーマについて、事前にチーム内で目線を合わせることで気づけていた点でもあり、走り出し時点でチームで認識を揃える重要さを体感しました。

「検証フェーズについて考慮漏れしていた」点について

本来スキーマ設計をチームで進める場合、議論しつつスキーマ設計の素案を作成するだけでなく、「作成したスキーマが実際に解決したい現実の問題を扱えるのか」を検証する必要もあります。

しかし、設計に着手時には後者について考慮しておらず、着手中に検証フェーズの必要性に気づいた結果、1~2日ほど工数が膨れていました。今回に限らず、新規事業でのPoCや実装におけるテストなど、考えたコト・作ったモノに対して正しく機能するかどうかを担保するための検証フェーズは不可欠なものだと思いますが、常にその点も考慮に入れるべきだと振り返りました。

チーム内で認識を揃え、問題解決にあたる際には解決案の検証まで行うというのは非常に基本的な事ながら、日々それを常に徹底することの大切さと難しさを痛感した週でした。

スキーマ設計の勘所

スキーマ設計に限らないのですが、こうしたチームレベルで考え、検討した上で設計するタスクでは、事前にチーム内で

  • 今明らかになっているものは何か
  • まだ不明瞭なものは何か
  • 現在やろうとしている事は何か

について認識を揃えておくだけでも、予期せず工数が膨らむといった事態は避けやすくなるだろうと振り返りました。

今回の例だと、「明らかに過去の資産を今回の設計に活かせる」と考えていたものの、実はチーム内でその点について触れることで「実は明らかではなく、活かせるかどうかは不明瞭だった」という事が把握出来たためです。

中でも「一見明らかだけど、明らかだと思い込んでいて実際には不明瞭だった」というパターンは結構曲者だと思っていて、上記はこのパターンにチームで早期に気づくための勘所でもあるなと思います。

また、現在やろうとしている事についても認識を揃えることで、本当にそれらを完了すれば設定したゴールに辿り着けるかもチームで確認出来ます。今やっている事をこなすことで設定したゴールに到達出来るのか、最初にチームで目線を合わせるのもこうしたタスクでは重要だと思いました。

mikanでは現在ほぼフルリモートで開発を行なっていますが、今回のリモートでのスキーマ設計の議論ではmiroというオンラインホワイトボードのようなツールを使ってみました。ホワイトボード上で図を書きつつ議論するような使い方が出来る点で、非常に使いやすいな〜と感じています!

f:id:nixii_squid:20210903185650p:plain

(miroを使って概念スキーマの議論をしている画面)

他にも必要に応じてZoomにiPadを繋いでイメージ図を書いたり、チーム内で必要に応じてサッと情報を共有していくのもこうした設計の議論では大切だなと実感しています。

コロナ禍前ではオフラインで実際のホワイトボードを前にワイワイ議論出来ていたのですが、オンラインでこういった設計の話を進める上では正直やっぱりハードルが上がったなと感じています。

一方で、お互いにこうしてツールや通信環境をより良くしたり、進める上でのちょっとした障害が無いかどうか都度フィードバックをし合うなど、ハードルを下げる方法はまだまだ沢山あるな〜と思いました。

さいごに

私がmikanチームにジョインしてから度々思うのが、こうやって自分たちが過去やったこと・現在やっていることへの振り返りと、そこから学びを得ようとするカルチャーが徹底されているなという事です。

良い結果を得るために出来たこと、芳しくない結果になった経緯などを、次により良い仕事をやり切るためにチームでシェアするというmikanのカルチャーは個人的にすごく好きで、他のメンバーの振り返りも見ていて非常に勉強させて貰えたりします。

チームにとって良い影響を与える事を継続し、プロジェクトの進行を妨げる要因になりそうなものを排除する上でも振り返りはとても重要だと考えていて、個人・チームレベルのどちらでも日次や週次で振り返り、かつチーム内から色んなフィードバックをもらえるというのはすごく良い環境です。

最後になりますが、このような環境で働いてみたいと少しでも思った方、英語が好きだったり教育を変えていきたいという想いのある方、是非一緒に働いてみませんか?

以下の私のTwitterに気軽にDMして頂いても構いませんので、ぜひ気軽に声をかけてみてください!

twitter.com

mikan.link

参考にした記事

Firebase Documentation Cloud Firestore

入社して4ヶ月経つのでぶっちゃけてみる

f:id:iiizukkeyiii:20210826134024p:plain

はじめに

こんにちは。 mikanでAndroidエンジニアをしております、zukkeyといいます。

今年の4月に入社して、大体4ヶ月ぐらいが経過しました。 まだ入社して4ヶ月しか経っていないのか、という驚きと共に、4月に入社エントリーを書いたときから、何ができたんだろうかと振り返っていこうと思います。

ちなみに、入社してすぐに書いたエントリーについてはこちらになります。

note.com

入社してからやっていたこと

入社してから次のようなことをやっていました。

  • Android開発
    • 運用
    • 機能実装
    • 設計
  • 業務改善
    • Ktlint、自動レビューアサイン、PRテンプレート見直し
  • デザイン
  • 広報活動(採用)

Androidエンジニアなので、まずはAndroid開発で何をやってきたのかをお話しさせていただこうと思います。

入社する前から副業で手伝っていたリデザインの開発を主に行っていました。

mikan AndroidiOSアプリとは異なり、なかなか置き換えるための人的リソースが少なく、代表の@kazさんや副業メンバーで回している状況でしたので、以前自分が触った時と同じままの古いデザインでした。

そのため、Androidのメイン画面を新しいデザインに変更したいという要望もあり、新デザインに変更する(リデザイン)をやっていました。

f:id:iiizukkeyiii:20210826133555p:plain

リデザインの開発では、具体的に次のようなことをやっていました。

  • 画面を跨いでの作業が必要な箇所の大きい改修
    • Fragmentの状態保持の仕組みを入れる
    • 教材一覧画面でカルーセルのindexが保持されておらずリセットされていたのを保持する仕組みを入れる..etc
  • デザイン改修
    • Proラベルの改修やボタンのBackgroundカラーがMaterialComponentsで変わっていた部分を修正
    • タブレットのデザインが崩れていたところを直す..etc

他にも、業務改善や全体として必要そうなところもやっていました。

  • チームになったことで必要になった業務改善
    • Ktlintを入れてフォーマットを整える
    • GitHub Actionsを用いた自動レビューアサイン機能を導入
    • チームができたことにより、プルリクテンプレートの見直し
  • その他
    • 今まではなかなか取り組めていなかった既存のバグ対応(優先度の高いものから着手して着実に数を減らしてきて、今では残すところ7件)
    • アプリケーションの全体の設計を含めてたたき台を作って導入し、今も今後のやりたいことを見据えてブラッシュアップ

設計に関しての記事もまとめているので、よかったら見ていってください。

mikan-tech.hatenablog.jp

mikanでは、担当領域だけでなく手を挙げれば他の領域もやっていけるので、自分の新たなチャレンジとしてデザインタスクもやっていました。

このエンジニアブログのアイキャッチ画像作成にも自分が関わりました!

f:id:iiizukkeyiii:20210826133618p:plain
作成したアイキャッチ画像集

他にも、mikanの認知を広める活動の一環として、広報ブログを開始したり採用面でもお声がけさせていただくことをやっていたりしています。

note.com

mikanに入社してよかった?

総じていうとよかったなー、と思ってます。

よかったと思う点は次のとおりです。

  • いい人で構成されている
  • 施策を決める前の段階から進められる
  • 何でこれやるの?への納得感が大事にされている
  • 状況に応じて柔軟に対応している
  • ベンチャーでありつつ、ベンチャーという理由を言い訳にしない

一番入ってきてからいいなと思ったことは、Qの初めにやる施策案出しがあります。

分業されているところだと、この機能を作るためにはどう実装するのか、現在の状況から実現可能か、いつまでにやり切れるかにばかり集中していて、これってそもそも何のために作ってるんだっけという部分に声を上げられなかったり、提言してもあまり変わらなかったりします。

最初からどういう課題があるかを探しながら、案を出して納得感を大事にして上流から関わることができるのが、一番楽しいところだなと思っています。

他にも、ベンチャーでありながらも柔軟に対応していて、ベンチャーだからできないというようなことがないのも嬉しいところかなと思っていたりします。

昨今のコロナウイルスの状況を鑑みて、適宜リモートにしたり、ワクチン接種まで用意していただいたりして、無事にワクチン2回目摂取も終えることができました。

また、病気・ケガを事由とした有給休暇を年10日間、年次有給休暇とは別で付与されるSick Leaveを設けたり、「安心キット」という人事制度まで整ってきて驚くばかりです。 ベンチャーだからという理由はなく、正直ここまでやるとは思っていなかったので、すごいなぁと思いました。

mikan.link

逆に、もうちょっとここどうにかしたいなーって思うところもあります。

Androidは、今はまだ複数人で開発するためのアーキテクチャ設計をやっていたり、根幹となる技術選定と検証をしていたりする土台作りの段階で、まだ長期的に見据えて時間を割く必要があり、施策をバンバン打って事業伸ばしていくぞー、というよりも前の段階にいます。

自分としては、開発の成果をユーザーからフィードバックをもらっていけるのがとても楽しく感じるので、もどかしい気持ちが多くあります。

早く土台作りを終わらせて次に進みたい、という気持ちと、長期視点で立つときちんと作らないと後で大変なことになるな、という思いがせめぎ合って、まずは目の前の課題を片付けないといけないので、モヤモヤしているところではあります。

人が増えれば解決しそうな気もしますが、今度はマネジメント部分の問題も出てきそうで、今後どうなるかはまだわかりません。実装に突き抜けていたり、マネジメントの経験が豊富であったり、いろんなバックグラウンドを持った仲間がmikanには必要な気がしており、今後の進展が楽しみでもあります。

今はとにかく目の前の課題を先に終わらせて、次行かなくてはという気持ちで走っています。

入社してから気づいたmikanの文化

副業していたときにはあまり気づいてなかったmikanの文化について紹介させていただこうと思います。

Good&New

mikanでは毎日10時から朝会があります。ラジオ体操を終えた後に、Good&Newについてみんなで雑談する場があります。

Good&Newとは、その場で当てられた人がここ最近で嬉しかったことや楽しかったことを共有して、それについて話すということをやっています。

最近は、自粛期間ということもあってなかなかいい話題が見つかりづらいところあったりしますが、Netflixで見た映画の話とか家での過ごし方を共有しながら和気藹々とやっています。

f:id:iiizukkeyiii:20210826133701p:plain

mikan LT

月に1回程度、好きなこととか趣味とかを話す場があります。

ジャンルは何でもよくて、コミュニケーションの活性化のためにやっている催しです。

準備するのは大変ですが、聞いてる分には結構楽しくやっています。

締め会

月次で一回あり、月の振り返りとプレミアムバリューカードを送る会があります。

中でもプレミアムバリューカードは、mikanにある4つのバリューのうち体現している方に1人1枚だけ送ることができ、日々の感謝の気持ちを伝える場となっています。

日々の感謝を伝える方法として、バリューカードを送るのもあるのですが、月次で贈られるものは1枚だけの限定なので嬉しいところがありますね。

おわりに

いかがだったでしょうか?入社からのmikanの雰囲気が少しでも伝われば嬉しいと思います。

もうちょっとここどうにかできないかなーという部分でも出てきたところで、施策をバンバン打ってグロースさせるという方向に早く持っていくためにも、Androidエンジニアが増えると嬉しいなぁと思っております。

もちろん、他の職種も応募しています!

さいごに、mikanでは、一緒に英語学習をより良いものにしていくべくサービスを作っていく仲間を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!

herp.careers