MackerelのカスタムメトリックをNew Relicに移行する時はNew Relic Flexという機能がおすすめ

システム開発部Misocaチームエンジニアの id:mizukmb です。

Misocaチームでは監視ツールとしてNew Relic Oneの採用を決定し、現在はインフラの監視に使用しているMackerelからの移行作業を進めています。

newrelic.com

Mackerelにはカスタムメトリックという独自のメトリック投稿機能がありますが、こちらをNew Relicに移行する方法について書きたいと思います。

Mackerelカスタムメトリックの代替候補

MackerelカスタムメトリックからNew Relicに移行するには 任意の数値をメトリックとしてNew Relicに送信できる 機能が必要です。私が調査した限りでは以下の機能が代替できそうでした。

  • Custom metrics
    • APMの機能。アプリケーションコード内で専用のメソッドが呼ばれる事でメトリックを送信できる
  • Metrics API
    • 専用のAPIエンドポイントにメトリックとして渡ししたい数値をJSONペイロードに入れてPOSTリクエストすると送信できる
  • New Relic Flex
    • New Relic Infrastructureエージェントに同梱された機能。専用のymlファイルを用意すると定期的にコマンドを実行し、標準出力で得られた値をNew Relicにメトリックとして送信できる

今回はMackerelのカスタムメトリックに機能的に最も近いNew Relic Flexを採用しました。

New Relic Flexの設定・動作確認方法について

New Relic FlexはInfrastructure agentがインストールされたサーバーであればすぐに使う事ができます。

docs.newrelic.com

github.com

/etc/newrelic-infrastructure/integrations.d/ 以下にNew Relic Flex用のymlファイルを置き、agentを再起動する事でメトリックの送信が開始されます。

基本的な使い方はGitHub READMEのbasic tutorial から読み始めると理解しやすいと思います。また、ymlファイルの設定については、 configureexamples が役立ちました。

動作確認には /var/db/newrelic-infra/newrelic-integrations/bin/nri-flex コマンドが便利です。このコマンドはInfrastructure agentがインストールされたサーバーであれば一緒にインストールされます。チュートリアルの4. How to add more Flex integrationsに使い方が記載されていますが、以下のようにymlファイルを指定する事でNew Relicにはメトリックを送信することなく、New Relicにどのような形式でメトリックが送信されるのか、意図した通りのメトリックになっているのかといった事が確認できます。

# myconfig.yml を対象に動作確認する。結果はJSON形式で標準出力として出力される
$ sudo /var/db/newrelic-infra/newrelic-integrations/bin/nri-flex --verbose --pretty --config_file ./myconfig.yml

例) delayed jobのキュー毎のジョブ数をカウントするメトリックを送信する

delayed jobはRailsアプリで非同期処理を実現するためのRuby gemです。RDBをバックエンドとして利用してジョブの登録や取り出しを行います。今回はこちらのキューに登録されたジョブ数をメトリックとしてNew Relic Flexを使ってNew Relicに送信する実装例を紹介します。

delayed jobキュー毎のジョブ数をRDBに問い合わせて標準出力に表示すれば良いので手段は複数考えられます (DBに接続して直接SQLを発行する、rakeタスクにする等) 。今回は実装を簡単にするため rails runner をシェルスクリプトから実行する方法にします。

#!/bin/bash

# ファイル名は delayed_job.sh とする

cd /path/to/rails_root

bundle exec rails runner '%w(default foo bar baz).each {|q| puts "#{q} #{Delayed::Job.where(queue: q).count}"}'

Rails runnerで実行するスクリプトは以下の内容を一行にまとめたものになっています。

# キュー名 `default`, `foo`, `bar`, `baz` に登録されたジョブ数をカウントして出力する
%w(default foo bar baz).each do |q|
  puts "#{q} #{Delayed::Job.where(queue: q).count}"
end

シェルスクリプトを実行するとキュー名とジョブ数がスペース区切りで出力されます。

default 10
foo 5
bar 0
baz 0

最後に設定用のymlファイルを用意します。さきほど作成したシェルスクリプトのファイルを指定しつつ、メトリック名や実行間隔もymlファイルに書いていきます。New Relic Flexでは得られた出力をどのようにパースするかを指定できます。今回はスペース区切りで出力しているのでそのようにymlファイルにも指定します。正規表現で指定することもできるみたいです。

# /etc/newrelic-infra/integrations.d/delayed_job.yml
integrations:
  - name: nri-flex
    interval: 5m # 実行間隔
    config:
      name: delayedJob
      apis:
        - name:  delayedJob # メトリック名
          commands:
            - run: ./delayed_job.sh # 実行するコマンド (今回はシェルスクリプトを実行)
              split: horizontal # 行指向で分割する
              set_header: [queueType, count] # 項目名
              split_by: \s # データをどのような文字で区切っているか。 \s はスペース区切りを意味している
              timeout: 30000 # タイムアウト (ms)

細かい設定項目については configureexamples を参照ください。

あとは New Relic Infrastructure のプロセスを再起動させれば上記ymlファイルが読み込まれメトリックの送信がはじまります。

メトリックは delayedjobSample という Sample サフィックス付きの名前で保存されます。以下はNRQLで実際に可視化させた例です。

f:id:mizukmb:20210405110818p:plain
New Relic Flexで送信したメトリックをNRQLで可視化した

おわりに

MackerelのカスタムメトリックをNew Relicに移行する時の方法としてNew Relic Flexを紹介しました。どのくらいニーズがあるかわからない内容でしたが参考になれば幸いです。

採用

弥生株式会社ではエンジニアを積極採用中です。

www.yayoi-kk.co.jp

よいミーティングの作り方

こんにちは、Misoca開発チームの黒曜(@kokuyouwind)です。
ついにECS execできるようになったことに咽び泣いていますが、今日の記事は全然関係ない話です。

社内向けに「どうすれば質の高いミーティングを作れるか」を検討した読み物記事を書いていたのですが、社外に出しても問題ない内容だったので開発者ブログに載せることになりました。
割と社内では評判が良かったので、参考になる部分があれば幸いです。

目次

はじめに

複数人で何かをしようとしたり、何かを決めようとしたとき、あるいは認識を揃えようとしたとき、業務では様々なタイミングで「ミーティング」が必要になります。 しかしながら、ミーティングというのは「無駄な仕事」と感じられることも多く、また正しくミーティングを実施しないと「1時間かけて、得られたのは参加者の徒労感だけ」ということになってしまうこともあります。

本記事では筆者の考える「よいミーティング」の作り方を、いくつかの観点からまとめたものです。 もちろん「このやり方が正しい!」と主張するものではありませんが、一つの参考としていただければ幸いです。

要点

長くなってしまったので、最初に要点だけをまとめます。 各節にはより細かい話や具体例などを書いていますので、興味のある題材の節をご参照ください。

  • 良いミーティングとは
    • 「短時間・少人数」で、「ミーティングの目的をよりよく達成する」もの
    • この2つはある程度トレードオフの関係にあるが、工夫して「効率の良いミーティング」にすることで両方を高める事ができる
  • ミーティングの準備
    • ミーティングの目的を明確にする。参加者全員に誤解なく伝わる端的な名前をつける
    • ミーティングの参加者を決める。必要十分なメンバーを選ぶ
    • ミーティングの前提情報を洗い出し、参加者に共有する。議論の前提となる知識が何であるかをはっきりさせる
    • ミーティングの進行方法を選び、時間を見積もる。簡単なこと・前提となるものを前に、難しいこと・時間のかかることを後にする
  • ミーティングの実施
    • ファシリテーターは「ミーティングの目的」を達成するため、参加者の意見を引き出し整理させる。適切なタイミングで落とし所が見つけられるよう誘導する
    • タイムキーバーは「時間内でのミーティング」を達成するため、進行の区切り時刻を通知する
    • 参加者は「ミーティングの目的」を達成するため、各々の観点から意見を出し議論を進展させる。他の参加者やファシリテーターが「敵」ではなく「同じ目的を達成するための別ロール」であることを認識し、互いを尊重しながら協力して議論を行う
  • ミーティングの後
    • 議事録を作り、知るべき関係者に周知する
    • 次のアクションがあった場合、それが適切に実施されるようにする

よいミーティングとは

ミーティングとは

そもそも、ミーティングとは何でしょうか。

辞書で「ミーティング」という語を調べると、「比較的少人数の集会。会合。」といった定義が出てきます。 これに間違いはありませんが、業務において使われる「ミーティング」はもう少し狭い範囲のものを指しているような気がします。

Misocaチームでは複数の定期ミーティングを開催しています。 これらはそれぞれ、何らかの 目的 を持って、参加者の時間を使っているものです。

こうしてみると、少なくともMisocaチームにおいては「何らかの目的を達成するために参加者が集まる場」をミーティングと呼んでいる、といえそうです。
おそらく、一般的な会社でもこういった意味合いで使われることが多いのではないでしょうか。

よいミーティングの条件

例えば、Misocaチームで毎週行っている「開発チームふりかえり結果共有」というミーティングであれば、目的は以下のように書かれています。

Misocaで動いてるプロジェクトは定期的にふりかえりを行っている。 開発チームが関わるプロジェクトで得られた知見や経験を、開発チーム全体で共有して他のプロジェクトへ展開する。

つまり、このミーティングを経て「開発チームがかかわるプロジェクトで得られた知見や経験が、他のプロジェクトへ展開されている(業務でその知見や経験が生かされる)」ことが期待されています。
目的を持って開催されている以上、 ミーティングの目的が達成されている のはよいミーティングの条件として適切でしょう。

ところで、同じことを決めるのに、一方のミーティングでは2時間かかっていたのに、他方のミーティングでは30分で終わったとしましょう。
この場合、30分で終わった参加者は残りの1時間半を自由に使うことができることになります。
あるいは、同じことを決めるのに、一方のミーティングでは10人で話しており、もう一方のミーティングではそのうち5人で話していたとしましょう。
この場合、前者では全員が会議しかできなかったのに対し、後者では5人が別の作業を自由に行うことができます。

こうしてみると、複数人の時間を使うものである以上、目的が達成されるのであれば より短時間・少人数で終わる というのも大事な観点だと言えそうです。
ただし後者の「少人数」については、参加しなかったメンバーへの情報共有や、そのメンバーの意見抽出が行われないことに注意が必要です。

目的の達成度

注意が必要なのは、目的の達成は「できたか、できなかったか」だけではなく、「どの程度よく達成できたか」という曖昧さがあることです。
先の「開発チームふりかえり結果共有」でいえば、「開発チームがかかわるプロジェクトで得られた知見や経験が、他のプロジェクトへ展開されている(業務でその知見や経験が生かされる)」というのには、少し考えるだけでも以下のような結果がありえます。

  • どのメンバーも、互いのプロジェクトで得られた知見を「各自が体感した」レベルで細部まで完全に把握した
  • どのメンバーも、互いのプロジェクトでどのような知見が得られたか一切わからなかった
  • どのメンバーも、プロジェクトXでの知見は完全に把握したが、プロジェクトYでの知見は一切わからなかった
  • メンバーAは各プロジェクトでの知見を完全に把握したが、メンバーBはどのプロジェクトの知見も一切わからなかった
  • どのメンバーも、互いのプロジェクトで得られた知見を大まかに把握したが細部まではわからなかった
  • メンバーAはプロジェクトXの知見が完全に、プロジェクトYの知見は大まかにわかった。メンバーBはプロジェクトXの知見が大まかに、プロジェクトYの知見がほんの少しわかった

これだけでも、「誰が」「何を」「どれだけ」理解したか、と3種類の尺度が登場しています。

情報共有以外でも、例えばブレインストーミングであれば「どれだけ多くの意見が出たか」「どのくらい多方面からの意見が出ているか」「重要な意見をどれだけ掘り下げられたか」など、決議であれば「各参加者がどれだけ背景を理解したか」「考慮漏れの検討などがどれだけ正確に行えたか」「参加者のうちどれくらいが納得できる結論をだせたか」など、様々な観点から「達成度」を考えることができるでしょう。

もちろん、可能な限り目的を100%達成するのが望ましいのは言うまでもありません。
しかしながら、目的を完全に達成するのが時間の制約上難しかったり、そもそも現実的に不可能である、ということもありえます。

達成度と時間のバランス

別の例として、以下の3つのミーティングを考えてみましょう。

  • 10分で目的の5割を達成できたミーティングA
  • 30分で目的の8割を達成できたミーティングB
  • 2時間で目的の99%を達成したミーティングC

この3つのミーティングでは、どれが一番良いミーティングだと言えるでしょうか?

極論は「目的による」といえますが、筆者の個人的な感覚では以下のようになります。

  • 情報共有が目的であれば、短時間で大枠のみ目的を達成したミーティングAが優れている
  • 何らかの議論やブレインストーミングが目的であれば、議論の大半を終わらせることができるミーティングBが優れている
  • 大きな決定を下すのであれば、ほぼ完全に目的を達成できるミーティングCが優れている

「目的を達成すれば良い」「時間が短ければ良い」のは確かですが、実際には上記のようなトレードオフの関係になることが多いでしょう。
このため、ミーティングを実施するときには目的に応じて適切な達成度が得られるよう開催時間を調整する必要があります。

効率の良いミーティング

達成度と時間のバランスを踏まえた上で、より良いミーティングにするためにできることはないでしょうか。

ひとつの考え方として、「より密度の高いミーティングを行う」ということが考えられます。
すなわち、「同じ時間でより高い達成度を得られるよう工夫する」のです。

みんなが知っていることの確認に時間をかけすぎていないでしょうか?
参加者が議論に集中できる説明の流れになっているでしょうか?
ミーティングの目的と関係のない議論に時間をかけていないでしょうか?

こうした点に注意することで、短い時間で目的をより達成する 効率の良いミーティング にすることができるかもしれません。

ミーティングの準備

ミーティングを行う際は、以下のことを事前に行うのが望ましいでしょう。

  • ミーティングの目的ゴールを明確にする
  • ミーティングの参加者を決める
  • ミーティングの前提情報が何かを洗い出し、参加者に共有する
  • 目的達成のために必要なミーティングの進行方法を検討し、必要な時間を見積もる

ミーティングの目的とゴールを明確にする

ミーティングを開催する以上、そこにはなんらかの「目的」があるはずです。 よくあるのは以下のようなものでしょう。

  • 一人では決定できない事柄について、複数人で協議して決めたい
  • ある事柄について、いろいろな意見を集めて掘り下げたい
  • ある情報を参加者全員に周知したい
  • 複数人の親睦を深めたい

これらが入り混じったミーティングもありえますが、その場合でも 主たる目的を明確にする ほうが望ましいでしょう。 そうすることで、「最低限何を達成しなければいけないか」という優先度づけがしやすくなります。

目的を明確にして人に伝えるためには、言葉で表現する必要があります。

「会議の目的」を、端的な1~3文程度でまとめましょう。
「何をしたいか」「なぜミーティングをする必要があるのか」「何が足りないのか」などを明確にしましょう。
読んだ人が「なるほど、確かにこのためにはミーティングが必要だ」と思うようにしましょう。

また、それにあわせて「会議名」をつけましょう。
目的の中から重要な語を拾い上げ、目的がイメージしやすいようにしましょう。
文字数が限られるため、文字数が少なくなるよう言い換えたほうが伝わりやすくなるかもしれません。

さらに、「会議のゴール」も端的な1~3文程度でまとめるとよりよいでしょう。
ミーティングを経て「何が得られていればよいか」「どうなっていればよいか」を明確にしましょう。
「ゴールを達成したとき」のイメージができるようにしましょう。

これらは誰でも見られるよう、カレンダー予定の会議名と詳細に書いておくと良いでしょう。

例えば、「この記事を全体に共有するミーティング」であれば以下のようになるでしょう。

会議名: 「よいミーティングの作り方」の読み合わせ

目的:
 ミーティングを効率よく進行するための自分の考えについて、「よいミーティングの作り方」という記事をまとめました。
 この内容を展開して今後のミーティングの質を高めるため、読み合わせによって全員に確認していただきたいです。
 また時間があれば、これを各ミーティングで意識できるようにプロセスへの組み込みなどができないかを議論したいです。

ゴール: メンバー全員が記事に書かれた内容を理解し、各ミーティングの前提事項として共有された状態にする

この例では、目的を「背景」「主目的」「副目的」の3文で構成しています。 このうち、「背景」と「主目的」で なぜ ミーティングが必要なのかを、「主目的」と「副目的」で 何をしたいかを明示しています。

  • なぜ: 新しく作った記事が重要な内容なので、全員に確認してもらいたい
  • 何をしたいか: 記事の読み合わせと、プロセスへの組み込みに関する議論

記事の読みあわせが主目的なので、会議名はそれが明確なようにします。 一方で、ゴールについては「記事の内容を確認する」だけではなく「それを会議に活かす」ほうがより達成度が高いので、それを含めた1文にまとめています。

以下は、よりよい目的設定をするための検証観点です。

  • 目的は端的な表現になっているでしょうか? 誰が読んでも誤解なく伝わるでしょうか?
  • 会議名から目的を想像できるでしょうか?
  • 目的とゴールは正しく繋がっているでしょうか? そのゴールを満たすことで、目的は達成されているでしょうか?
  • 専門的な用語が使われていないでしょうか? 誰が読んでもその内容を理解できるでしょうか? 平易な言い換えはないでしょうか?
  • 参加者一人を具体的にイメージして、その人が目的やゴールを正しく認識できそうでしょうか?

ミーティングの参加者を決める

目的とゴールが定まったら、ゴールを達成するのに必要なメンバーを列挙します。

情報周知であれば、周知したい対象全員に参加してもらうだけでよいでしょう。
ただし、大人数になるほど「本当にミーティングが必要か」や「事前の情報共有で時間を短縮できないか」をより吟味して開催する必要があります。

議論や決議が目的の場合、参加者はよく吟味する必要があります。
人数が少なすぎると十分な意見の吸い上げができませんが、多すぎると議論が発散してしまったり発言する時間が取れず、参加する意味のない参加者が発生しやすくなるからです。

以下は、適切な参加者を洗い出すための検証観点です。

  • 目的に含まれる題材について、担当していたり作業をしているメンバーはいないでしょうか?
  • ゴールを達成する上で、参加者に不足しているロールはないでしょうか?
    • 意思決定が目的であれば、意思決定を行える人はいるでしょうか?
    • 議論が目的であれば、多方面からの意見が出るようなメンバーになっているでしょうか?
  • 重複するロールの人はいないでしょうか? いずれか一名にした場合、どのような影響があるでしょうか?
    • 影響が少ないのであれば、いずれか一名にしたほうが効率が良い可能性があります

ミーティングの前提情報を洗い出す

ある事柄を議論するためには、その事柄のことを十分に知っておく必要があります。
議論のために何を知っていればいいかを検討し、参加者が確認できるようにしましょう。
おおむね全員が理解しているのであれば、情報へのリンクなどを張るだけでよいでしょう。
もし前提情報を把握しきっていない参加者がいるようであれば、事前に目を通すようにお願いしたり、ミーティングの最初に前提確認の時間を長く取ると良いでしょう。

情報共有を目的とするミーティングでも、前提が揃っているかは重要です。
また情報共有は「その情報についての質疑応答による掘り下げ」を副目的とすることが多いため、主目的の資料を事前に共有しておくのはよいプラクティスです。
すべてのメンバーが目を通すとは限りませんが、「参加者全員に一定以上理解してもらう」上で「事前に読んだメンバーはより高いレベルで理解し、質問事項を予め検討することで深いレベルの掘り下げを行うことができる」というのは十分な効果でしょう。

ミーティングの進行方法を決める

最後に、ミーティングの進め方と時間配分を検討します。

典型的な流れは以下のとおりです。

  • 議論であれば「前提の確認」→「議題の確認」→「意見の吸い上げ」→「個別意見の掘り下げ」→「得られた知見の整理」
  • 決議であれば「前提の確認」→「議題の確認」→「質疑応答」→「決議」
  • 情報共有であれば「前提の確認」→「情報共有」→「質疑応答」

ただし、目的や題材に応じて変わる部分も多いでしょう。前提確認は必要ないこともあれば、長めに取る必要があることもあります。

重要なのは以下の2点です。

  • 参加者が自然に参加できるか
  • その流れでゴールにたどり着けるか

いかなる時でも、前提の確認はミーティングの最初にあるべきでしょう。
また参加者が意見を出しやすいよう、理解しやすい流れで進行するべきです。

一方で、時間が足らなくなってしまったり、そもそもゴールとは全く違う方向へ進んでしまっては本末転倒です。 このため、極力「時間のかからないこと」を先に済ませ、「主目的」をその次に置き、「時間のかかること」や「副目的」は最後になるようにするのが望ましいです。 とはいえ主目的やその前提となる議論に時間がかかることも多いので、このあたりはケースバイケースとなります。

進行が決まったら、それぞれのステップの時間目安を添えて、参加者全員が見られるようにまとめておきましょう。

ミーティングの実施

ミーティングでは目的を達成するために、以下のようなロールが協力して進行します。

  • ファシリテーター
  • タイムキーパー
  • 参加者

以下の役割は、議論に主眼を置いたミーティングについて記述しています。 決議や情報共有を目的としたミーティングでも各々の役割は変わりませんが、自由な発言の余地が少ないためそこまで意識しなくても問題なく進行することがほとんどです。

ファシリテーターの役割

ファシリテーターの役割は「議論を促進し、ミーティングの目的を達成できるように参加者を導く」ことです。
参加者から意見を引き出し活発な議論になるようにしつつ、議論が収束するよう交通整理を行います。

  • 議題について、参加者が平等に発言できるよう配慮して意見を求めましょう
    • 発言の少ない人には「◯◯さんはどうでしょう?」などという形で意見を求めましょう
    • 話の長すぎる人がいる場合は、適度な長さで中断してもらうよう促しましょう
    • 人の話を遮ってしまう人には、議論のルールを守ってもらうよう注意しましょう
    • 議論が発散してきた場合はそれまでに出た意見を整理し、議論の焦点を確認しましょう
    • 議論がミーティングの目的から逸れてきた場合は、目的を再度周知しましょう
  • 時間内に結論が出るよう、参加者を促しましょう
    • 「残り10分ですが、どうしましょうか」「直近で行うアクションはありますか?」など、極力オープンクエスチョンの形で参加者の発言を促しましょう
    • 議論が発散している場合、それまでの議論を整理して採りうる選択肢を列挙したうえで、参加者の意見を募っても良いでしょう
    • 意見が大きく対立している場合、双方の譲歩できるポイントを確認してみましょう

ファシリテーター自身が意見を出したりミーティングの結論をまとめるのではなく、「参加者が意見を出して結論を出すのをサポートする」役割である、ということが重要です。
ファシリテーターの詳しい役割やポイントについては「会議ファシリテーション」の基本がイチから身につく本などが参考になるでしょう。

タイムキーパーの役割

タイムキーパーの役割は「ミーティングの目安となる時間を参加者に周知する」ことです。
名前と反し、「ミーティングの時間を守る」のは参加者全員の責任であるため、時間を守らせることはタイムキーパーの責務ではありません。

ミーティングの進行予定を元に、議題ごとの予定時刻が近づいていることを知らせます。予定時刻の何分前に伝えるかはミーティング開始時に確認するか、チーム全体で目安を決めておくと良いでしょう。
なお筆者個人の目安として、「10分の議論なら2分前」「30分の議論なら5分前」など議論時間の1/5 ~ 1/6程度を残したタイミングで時刻を知らせるようにしています。 このくらいの残時間ならば「もうまとめに入らないと終わらないな」という意識になりやすく、なおかつそれまでの議論をまとめられる程度の余裕時間があるように感じます。

タイムキーパーが時刻を伝える際、できれば呼び出しベルのようなものを用意しておくと良いでしょう。
口頭で「◯分前です」と伝えても良いのですが、誰かが話している最中だと気後れしてしまうこともありますし、声が交じると聞き取りづらくなってしまいます。
ベルの機械的な音であればボタンを押すだけなので気後れせず押しやすいですし、話を遮ることもなくなります。

参加者の役割

参加者の役割は「ミーティングの時間内にミーティングのゴールを達成すること」です。
議論であれば自分の意見を積極的に出し、他者の意見を吟味し、全員が納得できる結論を得られるように協調します。
情報共有であれば受け取った情報を確認し、疑問点があれば質疑応答の時間内で許される限り確認します。

重要なポイントは「時間を守ったりゴール達成を目指すのは参加者全員の責任」というところでしょう。
とくに議論を目的とするミーティングでは、しばしば議論が横道に逸れてしまったり、意見が対立してしまうことも起こります。
こういった場合でも、「議題以外の話を取り下げ、議論を収束させること」「お互いの意見をすり合わせ、落とし所となる結論を見つける」といった振る舞いが参加者全員に求められます。

もちろん意図せず話がそれてしまった場合などはファシリテーターや周囲の参加者からも働きかけるべきですが、少なくとも「自分がミーティングの目的達成に協力する」という意識は全員が持っておくべきでしょう。
これが抜け落ちると、本来の議題とはかけ離れた話で時間を使い切ってしまったり、互いの意見を主張し合うだけで落とし所が見つけられなかったり、といったことが起こることになります。

ミーティングの後

議事録をまとめる

ミーティングで決まったことや話したことは議事録にまとめ、関係者に周知します。

個人情報や情報機密などの問題がない限り、全ての議事録は極力チームメンバー全員が見られるようにしましょう。
情報をオープンにしておくことで、関係者以外からのフィードバックも得やすくなり、場合によっては新たな関係者が見つかることにも繋がります。

議事録の形式はミーティングの内容に応じて調整するべきですが、「ミーティングの結論」「直近のアクションと担当者」「ミーティング中の議論の流れ」といった形で重要な情報を先にしておくと、読み手が概要を掴んだ上で必要に応じて詳細を追うことができるようになります。

議事録は可能であればミーティング中に並行してまとめ、ミーティング最後に全員で「ミーティングの結論」「直近のアクションと担当者」を合意すると効率が良いです。
とはいえ話しながら議事録を取るのは慣れないと難しいため、重要な情報のみミーティング内にまとめて合意をとった上で、議論の大まかな流れなどは後追いでまとめても良いでしょう。

なお、音声録音からの文字起こしは補佐的に利用しても良いものの、全文を読まないと結論やそれに至る流れが掴めないという点で読者に不親切になります。
後追いで議事録を作る際の参考情報としては有用なため、そういった用途で使うのがおすすめです。

「次のアクション」を実施する

議論の結果なんらかのアクションを起こすことになった場合、それが適切に実行されるようにしましょう。

チームのタスクになるのであれば、そのチームのタスクボードで管理するため理想的です。
一方で個人のタスクになってしまうと、その個人がうっかり忘れてしまった場合にアクションが止まったままになってしまいます。
「共通のタスクボードにアクションの置き場所を作る」「アクションの関係者が定期的に確認する」など、チームとして放置されたアクションを作らない仕組みが構築できるとよいでしょう。

まとめ

ミーティングは油断すると大人数の時間と気力を奪うだけに終わってしまいますが、うまく実施できれば短時間で多くの人の意見を反映した素晴らしい結論を得ることもできます。

この記事が「よいミーティング」を開催する一助になれば幸いです。

参考リンク

Misoca開発者ブログでは他にもミーティングに関する記事がいくつか投稿されていますので、そちらもぜひご覧ください。

tech.misoca.jp

tech.misoca.jp

またミーティングのための資料準備については、質の高い技術文書を書く方法が非常に参考になると思います。

blog.riywo.com

宣伝

弥生株式会社ではよいミーティングをしたいエンジニアを募集しています!

www.yayoi-kk.co.jp

AWS ECRのクロスアカウントレプリケーションを設定してみた

システム開発部Misocaチームエンジニアの id:mizukmb です。

今回は最近追加されたAWS ECRのクロスアカウントレプリケーション機能を実際に設定してみた話を書きたいと思います。

AWS ECRのクロスアカウントレプリケーション機能とは

ECRにDockerイメージがプッシュされたタイミングで異なるAWSアカウントのECRプライベートリポジトリに同じイメージを複製するといった機能です。アカウントだけでなく、同一アカウント内の異なるリージョンの複製もできるようです。

詳しくはAWS公式のブログを参照ください。

aws.amazon.com

これまでは異なるリージョン・アカウントに同じDockerイメージを配置するためにはレプリケーションを行うスクリプトを用意するといった運用が必要でしたが、AWS側でサポートされることでこうした運用が必要なくなります。

Misocaチームの課題

Misocaチームでは、本番環境と開発環境はAWSアカウントを分けて運用しています。 開発環境のECSリプレイスとterraformでのコード化 - 弥生開発者ブログ by Misocaチーム にある通り開発環境はECSで動いていて、本番環境でも一部Dockerコンテナを利用してるところがあります。両環境共に同じDockerイメージを利用する箇所があり、下図のようにそれぞれの環境でCodeBuildを使ってDockerイメージのビルドとECRプライベートリポジトリへのプッシュを行っています。見てわかると思いますがほぼ同じ処理をするCodeBuildを二重に動かすことで両環境にDockerイメージを配置しています。この複雑なビルド方法に対しては以前から課題に感じており、もっとシンプルにできないかと頭を悩ませていました。

f:id:mizukmb:20210305113439p:plain
同じDockerイメージを異なるAWSアカウントで作成していて煩雑になってしまっていた

そしてこのタイミングでクロスアカウントレプリケーション機能が使えるようになりました。この機能によってCodeBuildをどちらかの環境で動かすだけでビルドしたDockerイメージを両環境に配置できるため、ビルド構成が今よりシンプルになりました。

送信元アカウントで機能を有効にする

それでは設定していきます。送信元アカウントと送信先アカウントの両方で設定が必要です。まずは送信元アカウントでクロスアカウントレプリケーション機能を有効にします。

ECRのページから Registries をクリックし、レジストリページに遷移します。こんなページです

f:id:mizukmb:20210303183838p:plain

次に プライベート をクリックし、ラジオボタンを有効にした後、 編集 ボタンをクリックして、編集ページに遷移します。

クロスアカウントレプリケーションの有効・無効を切り替えるトグルボタンがあるので有効にします。送信先アカウント設定のフォームが出現するので送信先アカウントとリージョンを入力します。入力したら保存ボタンをクリックして保存をします。

f:id:mizukmb:20210303184801p:plain

保存すると、クロスアカウントレプリケーションの欄に送信先のAWSアカウントIDが表示されるようになります。これで、送信元アカウントの設定は完了です。次は送信先アカウントの設定になります。

f:id:mizukmb:20210303185451p:plain
送信先のAWSアカウントIDが表示されていたら設定完了

送信先アカウントでレジストリポリシーを設定する

レジストリポリシーを設定しますが、設定は 送信先アカウント で行いますので間違えないように気をつけてください。

まずは先ほどと同様にレジストリページに遷移します。次に プライベート をクリックし、ラジオボタンを有効にした後、今度は アクセス許可 ボタンをクリックして、プライベートレジストリのアクセス許可ページに遷移します。以下のような画面になります。

f:id:mizukmb:20210304145459p:plain

ステートメントを生成 ボタンをクリックして、ポリシーステートジェネレーターのモーダル画面を開きます。

ポリシーステートジェネレーターでは、ポリシーステートメント用のJSON文字列を自動生成し、設定します。直接JSONを書き込むこともできますがこちらの方が誤字等の設定ミスが少なくできるので今回はこちらを利用して設定します。以下のようにステートメントIDと送信元のAWSアカウントIDを入力し、 ポリシーに追加 ボタンをクリックして保存します。

f:id:mizukmb:20210304150829p:plain
ポリシーステートメントジェネレーター入力例

これで全ての設定が完了しました。

実際にレプリケーションするか確かめるには

レプリケーションは送信元のECRリポジトリに docker push されたタイミングで一度だけ実行されます。レプリケーションには数分の時間がかかっており、瞬時にレプケーションが完了するわけではない事に注意してください。

また、レプリケーション時にタグは上書きされることにも注意してください。例えば latest 運用していた場合にレプリケーションによって意図せずベースイメージが変わってしまう可能性があります。

その他レプリケーションに関する挙動については公式ドキュメントの『プライベートイメージのレプリケーションに関する考慮事項』をご確認ください。

docs.aws.amazon.com

おわりに

AWS ECRのクロスアカウントレプリケーションの設定に関する一連の流れを書きました。弊チームのようにDockerイメージのレプリケーション作業を自前で運用していた方は、こちらの機能を検討してみてはいかがでしょうか。

参考文献

EOL な Elasticsearch クラスターのバージョンアップを実施しました

はじめに

こんにちは、狩野と申します。 平沢進 氏の24曼荼羅(不死MANDALA)ライブ開催が決定されました。また第9曼荼羅のライブDVDの発売決定しましたので、ありがたく発売日を待っております。

⬆️ EOLなElasticsearchのバージョンアップ

先日Misocaで利用しているElasticsearchクラスターのバージョンアップと関連Gemのアップデートをリリースしました。

それまでのMisocaではEOLとなってしまった5系のバージョンを使用しておりました。 最新に追従できていない現状と、今後の機能追加・改善にElasticsearchを採用しにくくなっている懸念があったためElasticsearchのメジャーバージョンアップと関連Gemのアップデートを行いました。

アップグレード方針として5系から7系に直接上げず、一旦6系の最新にアップグレードしてから7系に上げました。

これはGemとElasticsearch本体のChangelogを調査した時に想像以上に変更が多く、問題発生時の原因切り分けが難しくなることが予想できたためです。 結果的な話ではありますが、6から7へアップグレードする際に切り戻し対応を行いましたので、その際に原因究明のスコープが小さくなり早急な対処に繋がったため良い方針だったと感じました。

本記事ではバージョンアップに際して工夫した以下の2点について記載します。

  • サービス無停止でのバージョンアップが可能なリリース方式の検討
  • 切り戻しを含めた詳細なリリース手順の作成とリハーサル

🦍 サービス無停止でのバージョンアップが可能なリリース方式の検討

MisocaではAmazon Elasticsearch Serviceを利用しています。 リリース方式の策定のために2つの方法を検討しました。

  • Amazon ESの「サービスソフトウェア更新」機能を利用する方式
  • 新旧クラスタを準備し切り替える方式

今回は「新旧クラスタを準備し切り替える方式」を採用しました。選んだ理由としては

「サービスソフトウェア更新」機能を利用する場合ESクラスターがアップグレードされるタイミングはAWS側に委ねられてしまいます。アプリケーションのGemバージョンとESクラスターのバージョンを一致させつつサービス無停止を実現するには「新旧クラスタを準備し切り替える方式」が必要という結論になりました。

🔄 新旧クラスタを準備し切り替える

リリース方式をかいつまんで説明します。リリース前の状態が下図になります。

現行

リリース事前準備として、通常時の検索であればElasticsearchからデータを取得しますが、一時的にMySQLを参照する機能をfeature flagによって実現しました。

feature flagの説明はこちらのMisocaのブログ記事をぜひ御覧ください!

またES6.xのクラスタを準備し事前に同期できるデータについては同期を実施しました。

移行直前

Elasticsearch側の準備はできたのでアプリケーションサーバ側の更新を行います。 またサーバ更新中に発生した差分を同期させます。

移行中

feature flagによって一時的に検索参照先をMySQLにしていたのを元に戻すことでバージョンアップ完了です。

移行仕上げ

上記の作業では5から6へアップグレードしていますが、同様のことを6から7へとアップグレード時にも実施し、現状ではAmazon Elasticsearch Serviceが提供している7系の最新に更新できました 🎉

📑 切り戻しを含めた詳細なリリース手順の作成とリハーサル

上記の方式ではリリースフェーズ毎にどちらのESクラスターにアプリが接続しているかや不整合なデータをユーザーに表示させていないかが分かりづらくなります。

なので当たり前のことですが入念に作業手順書をしっかりと書いておきリハーサルも行いました。

リリース時に想定しなかったエラーが起こってしまいましたので切り戻しを行いました。*1

作業手順書には切り戻し時の対応も準備しておきましたし、切り戻しのリハーサルも行っていたためユーザーへの影響無く切り戻しできました。

🌈 現状と今後

Amazon ESは今後のESバージョンのリリース予定日や古いバージョンのEOL日を提供していないため、Elastic社側のEOLやAmazon ESが新しいバージョンを提供したかを確認する日を半年に1回のペースでカレンダーで登録し、人力で気付けるようにしました。

ただもっと良い方法があると嬉しいな〜というきもちです。情報いただければ幸いです 🙏

今後は、よりよいチューニングやログ監視をしたいね、Amazon ESの「サービスソフトウェア更新」機能使えるなら使いたいね、という計画をしています。

📡 宣伝

Misoca開発チームではElasticsearchのことなら俺に任せろ!なエンジニアを募集しています! https://www.wantedly.com/projects/28443

*1:切り戻しの原因としては、インデックスへのデータインポート時にTimeoutエラーを起こしてしまうことでした。データインポート処理のバッチサイズを削減したり、非推奨とされているT2インスタンスを使わずT3インスタンスに切り替えることで解決しました。

Misocaメンバーの新年の抱負2021

はじめに

コロナ禍の厳しい年末年始でしたが、皆さんいかがお過ごしでしたでしょうか。 id:RKTMです。

私は子どもを連れてスキーをしていました。 良い雪があるのに、コロナのせいでスキー場もなかなか厳しいようです。

f:id:RKTM:20210110110501j:plain
長野県阿智村ヘブンスそのはらスキー場から登る富士見台高原の霧氷

今年のMisocaメンバーの抱負は?

弥生全体で大きなプロジェクトがスタートし、Misocaメンバーも何人かはそのプロジェクトに参戦中です。

www.talent-book.jp

そんなMisocaチームですが、今年の抱負を聞いてみたいと思います。

@suer

  • SwiftUI と Firebase でアプリを作る
  • Androidアプリもなにか作りたい
  • アウトプットを増やしたい(気持ちだけがある)
  • 去年は自粛続きで運動不足なので健康を取り戻す

@hidakatsuya

  • 今年こそ Thinreports v1.0.0 を出して、SectionReport 機能 を世に出す
  • Android アプリを作って公開する
  • TOEIC を受験する

@KawamataRyo

  • 今年こそGitHubに一面の草を生やす(去年は360/365)
  • SwiftUIで、AppleWatchのプランクアプリを作る
  • AtCoder始める
  • 懸垂以外の趣味を見つける

daaaaahara

  • 運動(リングフィットアドベンチャー)を週5日以上継続して行う。
  • Discordのbotの機能を増やす。
  • Androidアプリの勉強がてら、輝度を限界より暗くするアプリを作ってみる。
  • 平和に過ごす。

@RKTM

  • 去年はVueとTypeScriptをサポート受けながら書けるようになったので、今年はもう少しスッと書けるようになる。
  • 今年こそRustでなにか作りたい…。
  • バックカントリースキーで未圧雪の(緩い斜面を)気持ち良く滑られるようになる。
  • (マルチピッチ)クライミング頑張る。

id:mizukmb

  • 毎日同じ時間に起きる
  • 毎朝体重を測定する
  • AWSソリューションアーキテクトアソシエイト(SAA)試験に合格する

@kokuyouwind

  • リングフィットアドベンチャーとフィットボクシングを継続してやる
  • 死なない

@mugi_uno

  • Next.js完全に理解する
  • 2022年を元気に迎える

まとめ

全体的な傾向として、運動・健康に関心があるようです。 メンバー皆無事にこの1年を過ごせるよう願っています。

そんなMisocaを2021年もよろしくおねがいします!

新メンバーも募集しています!

www.wantedly.com

受注管理機能を支える技術 〜 VueCompositionAPIとGraphQLとAtomicDesignとScopedStyle〜

こんにちは、 @mugi_uno です。

少し前に背骨の手術を受けたら身長が伸びました。

🎉 受注管理機能をリリースしました

2020/12/10に、Misocaに新しく「受注管理機能」をリリースしました。

www.misoca.jp

f:id:mugi1:20201217165947p:plain

いままでは、請求書・見積書・納品書といった単位でのステータス管理が主でしたが、新たに追加された受注管理機能を使うことで、案件単位でステータスを管理しつつ、各文書への変換も簡単に行えるようになりました。

そして、同時にこの受注管理機能は、開発面においても様々な新しい技術面でのトライもありました。

  • Vue.js & Vue Composition API
  • GraphQL
  • Apollo Client & Vue Apollo
  • Atomic Design
  • Scoped Style

今回は、これらについてどういった対応をしたのかと、リリース後にふりかえってみてそれぞれよかった点、大変だった点についてのお話です。

前提 / 以前から利用している技術要素と受注管理機能の基本構成

受注管理に限らないMisoca全体の代表的な技術要素として、次のものを以前から利用しています。

  • バックエンド: Ruby on Rails
  • フロントエンド: TypeScript & Vue 2.x
  • API: REST
  • デザイン: SCSS (*.scss ファイルに記述)

新たに作成した受注管理機能はSPAで構成されており、次のような機能を実現しています。

  • ステータスの追加・変更・削除
  • 受注情報の新規作成・複製
  • 受注情報のステータス変更
  • 履歴の確認
  • コメントの追加
  • 検索

Vue.js (Vue Composition API)

受注管理機能の開発着手時点で、すでに Vue 3.x 系のリリースも見えている状態で、当時の選択肢としては2つ考えられました。

  • 従来の Vue 2.x 系の記法のまま実装する
  • Vue Composition API を利用する

Vue 3.x 以降も従来の記法はサポートされるので、無理に新しい記法とする必要はなかったのですが、「どうせ近い将来にすべて書き換えるなら、最初から Vue3フレンドリーな形で実装していこう」と、Vue Composition API を利用した形を主軸に実装しました。

github.com

結果としては、2020年9月時点で Vue3 が正式リリースされました。MisocaではIE対応の必要性から未導入ですが、後々必要となるマイグレーションを考えると、良い決断だったかなと思います。

👍 良かったこと

共通処理を Vue Composion API を使って簡単に切り出せるのは使い勝手が良かったです。

例として「新規作成と編集と複製って、ロジックがほぼ同じなんだけどちょっとだけ違うんだよな〜」といった際に、共通ロジックを useEditForm.ts として切り出す、といったことが簡単にできます。

また、Composition API は TypeScript との親和性が高く、機能追加・変更時も安全に手を加えていける点も良かったです。

🙈 大変だったこと

開発までの慣れ

いままでは Vue 2.x + Class Component で開発していたため、書き方が大きく変わる Composition API に開発メンバーが慣れるまでに少し時間がかかりました。ただ最初でこそ大変でしたが、慣れてくると「こっちのほうが書きやすい」という意見が多くなりました。

なお、開発と並行して @kawamataryoの主催で Vue Composition API のドキュメントを対象にした輪読会も開催されていました。そちらも、レビュアー・レビュイー双方の観点で良い効果があったと思います。

setup関数の肥大化

Composition API は setup 関数が肥大化しがちで、意識せずに処理を詰め込んでいくと、全体的に何をやってるか見通しが悪くなるのが課題でした。対策として、「大きくなりそうなら、処理は意味のある単位でコンポーネント内のローカル関数に切り出して、setup は小さく保つ」といったルールを共有していました。

<script lang="ts">

...

const useA = () => {
   // Aに関連する処理
}

const useB = () => {
   // Bに関連する処理
}

const useC = () => {
   // Cに関連する処理
}

export default defineComponent({
  setup() {
    return {
      ...useA(),
      ...useB(),
      ...useC()
    }
  }
})
</script>

関連ライブラリの影響

Vue Router や Vue Apollo などで一部機能が未対応だったり、Vue Composition API 側の変更の煽りを受けて挙動が壊れるというのを何度か踏みました。こればかりは新しい技術を突っ込んだ以上逃げられないものですので、ライブラリ側に修正パッチを送るなどして、わりと泥臭く対応してました。

GraphQL

SPAなのでデータの取得・更新はAPI経由ですが、すべてGraphQLで実装しています。

バックエンド(Rails)では、GraphQL Ruby でエンドポイントの実装とスキーマファイル出力を行い、フロントエンドでは、スキーマファイルを元に GraphQL Code Generator を利用して TypeScript の型定義ファイルを生成しています。

GraphQLを導入した背景としては、すでにMisocaでメイン利用されていた TypeScript との親和性が非常に高いことや、将来的にモバイルなど他の領域からの利用を考えた場合に柔軟に対応できる点が挙げられます。(※ただ正直なところ、エンジニアチームとして新しい技術にトライしたかったというのもあったかなと思います。)

👍 良かったこと

GraphQL による開発体験はとても良く、特に GraphQL Code Generator 経由で生成した型定義をフロントで利用できる点が非常に快適でした。

Misocaでは開発中の仕様変更も柔軟に受け入れていくスタイルですので、DB定義も含めてガンガン変更が入ります。その際に、GraphQL スキーマの変更から連動して型定義も変更されるため、ビルド時の静的検査の時点で検知でき、簡単かつ安全に対応していくことができました。

「バックエンドが変わったのにフロントエンドの変更が漏れてる」といったこともほぼ無く、品質面でも大きなメリットがあったように感じます。

ふりかえりなどでも GraphQL 超便利といった声が多数聞けました。導入して良かったです。

🙈 大変だったこと

ついうっかり N+1 を踏んでしまうことが何度かありました。「履歴データが多くなってくると一覧がタイムアウトします!!」みたいな感じです。

根本的な対処としては、Dataloader を利用してバッチ的にデータを取得する方法などが例として挙げられるかと思います。

しかし、Misocaの場合は発行されるGraphQLクエリがある程度は予測可能でしたので、単純に ActiveRecord 側で includes を足して対処しました。それでも、可能な限り無駄なくincludes を足すため、GraphQL クエリに基づいて自動的に includes に渡す引数を生成するヘルパーを自作しました。以下はヘルパーの利用イメージです。

RELATION_MAP = {
  comments: {
    user: {}
  },
  histories: {},
  items: {},
  status: {}
}.freeze

# クエリ内容とマッピング定義から includes に渡すべきパラメータを得る
includes_params = IncludesHelper.create_includes(
  lookahead, # GraphQLRubyが提供するクエリ検査用インタフェース
  RELATION_MAP
)

orders.includes(includes_params)

これにより、あるクエリ内で comments だけ取得している場合は comments のみが includes に含まれ、別クエリで itemsstatus を取得する場合には itemsstatus のみが includes に含まれるようになっています。

状態管理

Vue & TypeScript の組み合わせで迷いやすいのが状態管理の方法かなと思います。

今回のケースでは GraphQL のために Apollo Client を利用しており、基本的なデータについては Apollo Client のキャッシュ機構を利用して状態管理の代替としています。

同時に Vue Apollo も組み合わせているので、たとえば

  1. ユーザーが何か入力する
  2. 1 を Vue のリアクティブシステムで検知して GraphQL クエリを自動発行
  3. Apollo Client のキャッシュが更新
  4. 同一 GraphQL クエリに依存する箇所すべてが自動的に再描画

といった挙動もわりと簡単に実現できます。

ただ、画面上のすべての情報が Apollo Client で集約できるかというとそんなこともなく、UI操作上一時的にどこかに保持したい かつ 様々なコンポーネントから参照が必要になる値も存在します。モーダルの表示状態やドラッグ&ドロップの一時的なデータ保持などが代表例です。

Apollo Client に local stateとして管理させる方法もありましたが、シンプルさ重視で Vue Composition API の reactive を利用した簡易的なストアを作成しました。

👍 良かったこと

リフレッシュや多重リクエスト制御が自然といい感じになる

複数コンポーネントが Apollo Client のクエリ・ミューテーションと連動して自動的に再描画されていくのはなかなか便利です。

独自でクエリ発行→再描画を制御している場合、適宜処理の影響を受ける箇所でリフレッシュが必要になりますが、Vue Apollo がそのあたりを吸収してくれて、とても簡単に実装できました。

また、データが必要な箇所ではあまり深く考えずにクエリを実行しても、Apollo Client側で多重リクエストは自動的に抑制して捌いてくれます。(※設定によります)

開発時の「こうやって動いてくれたらいいのにな〜」という思いを、ほどよく汲みとって動作してくれるのは体験として良かったです。

reactive での簡易ストアは楽だった

reactive での簡易的な状態管理を作成しましたが、単純なオブジェクトを変更・参照していく形なのでとてもシンプルでした。TypeScriptとの相性も良かったです。最低限のガードとして参照側は Readonly 型としていたため、意図しないストア書き換えによる事故も発生しませんでした。

ただし、これは常にベストな選択肢では無さそうにも思います。一定以上の規模感になってくると、独自実装を増やすよりも、Vuexなどを利用して王道に沿った形にしていくほうが、長期的なメンテナンスコストは低くなるかもしれません。

🙈 大変だったこと

テストが書きづらい

Vue Test Utilsを利用したコンポーネントのテストを書く際に、Apollo Client のキャッシュに依存したテストが非常に厄介でした。

graphql-toolsを使うことでモック化自体は容易ですが、キャッシュを前提とした挙動のテストを書く場合、セットアップ段階でキャッシュを生成する必要があります。

ただ、実際にテストを実行していると、「キャッシュが無くてテストに落ちている」というのがエラーから気付くのがなかなか難しく、長時間ハマってしまうことも何度かありました。

暗黙的な挙動が増える&対処が複雑化しがち

Vue Apollo を組み合わせることで「Vueでの値の変更や別の箇所でのクエリ結果に連動して自動的に再描画してくれる」というのは、便利である反面、暗黙的な挙動となる箇所が増えていきます。また、クエリ発行のタイミング制御も難しくなり、画面上で複雑な操作・更新が発生する場合、それを実現するためのコードも同時に複雑になっていく傾向がありました。

Vue Apolloを利用せず、クエリ結果をストアに記録して明示的に制御するように書き換えるほうがシンプルになるかもしれないな..と思っている部分もありますが、もちろん恩恵を受けている箇所もあるため、今後様子を見ての判断になりそうです。

コンポーネント設計&デザイン

Atomic Design

コンポーネント設計は Atomic Design を採用しました。これは、全力で死守するものというより「判断基準を最初に設けたかった」というのが目的です。

以前からMisocaのフロントエンド開発時には、コンポーネントをどういった粒度で切るべきか / どこに配備すべきか、といった点が曖昧で迷ってしまい、フロントエンドチームが相談を受けることも度々ありました。

受注管理の機能開発時も同じ状況に陥ることが予想できたため、予め何らかの基準を設けるのが必要と考えました。

Scoped Style (Vue)

CSSは Vue.js の Scoped Style で書いていきました。

もともとMisocaのデザインは *.scss ファイル内に SCSS で書かれていますが、デザイナーの皆様の尽力もあり、いまではかなり整理されて、ファイル単位での影響範囲が明確になっていました。

tech.misoca.jp

さらに将来的な展望として Scoped CSS を利用した Vueコンポーネント化もあったため、まず手始めに今回の受注管理機能で実践してみた形です。

開発時のルールとしては、外部のCSSとの干渉や無駄な重複を避けるため、

  • Scoped Style を適用するためのクラスにはすべて _ を prefix として付与する
  • 色やフォントサイズ定義は既存の定義をimportして利用する

などがありました。

👍 良かったこと

迷いにくかった

「Atomic Design ベースでいきましょう」と合意をとったことで、当初の目論見どおり、コンポーネントの粒度・配備で迷うことは少なかったです。また、あくまでも判断基準のためで良くも悪くも緩い導入だったので、ルールに縛られすぎずに程よい付き合いができたのも良かった点でした。

CSSの変更が楽だった

Scoped Style ではコンポーネントに対応するスタイルは、当然ではありますが、そのコンポーネントの中に書いてあります。

整理されていたとはいえ、外部化されているCSSを触るときは「これってどこか他を壊しちゃうんじゃ…」という不安が心の片隅にありましたが、Scoped Style のように影響範囲がそのファイル内で閉じているのはなかなかに快適で、修正時の負担がかなり軽減されたように感じます。

🙈 大変だったこと

Scoped Style のスタイルの親子継承が厄介

影響範囲がファイル内で閉じていると書きましたが、実は Vue の Scoped Style には一部例外があり、子コンポーネントのルート要素だけは親要素のCSSを引き継いでしまうケースがあります。

スコープ付き CSS · vue-loader

たとえば、親コンポーネント側で _foo というクラスに対してスタイリングした場合に、子コンポーネントのルート要素に _foo というクラスを付与すると、親コンポーネント側で定義したスタイルが適用されてしまいます。

レイアウト用のスタイリング時に利用できる仕組みのようですが、どちらかというと意図しないCSS適用を引き起こすケースのほうが多かったです。

調べた限りではこれを無効化する仕組みは無いようで、コンポーネントのルート要素に Scoped Style 用のクラスを付与する場合には、できるだけ他で利用されにくい冗長な名前をつけて回避していました。

まとめ

今回は新しくリリースされた受注管理機能で利用した技術の話でした。

今後もずっと使い続けていけそうなノウハウも得られましたし、逆に「もっとこうすればよかったなぁ」という反省も勿論ありました。

いずれにしても試してみない限りは得られることのなかった知見かなと思うので、今後も(将来の開発者に迷惑をかけない範囲で)新しいことにチャレンジしていけるといいな〜と思います。

AndroidアプリにViewModelを導入しました

はじめまして。Misocaモバイルチームのtijinsです。

この記事は、弥生アドベントカレンダー14日目の記事です。

MVPからMVVMへ

Android版MisocaはModel-View-Presenter構造で作られていたのですが、Model-View-ViewModel構造へのリファクタリングが完了しました。

コードがスッキリしたので、リファクタリングの内容を紹介します。

MVPパターンとMVVMパターン

Model-View-Presenter(MVPパターン)

MVPパターンでは、PresenterがViewの参照(intrefaceで)を保持し、結果をViewに表示します。

Viewを直接参照せずにInterfaceにしておくことで、PresenterからViewの依存を排除しています。

@startuml
  interface PresenterResult
  class View implements PresenterResult
  {
    表示用データ
  }
  class Presenter
  class Model

  View --> Presenter: 読み込み・新規作成等の操作
  Presenter --> PresenterResult : 結果の表示
  Presenter --> Model
@enduml

Model-View-ViewModel(MVVMパターン)

MVVMパターンでは、ViewModelからViewの参照は不要です。

表示用のデータはViewModelが保持しており、ViewがViewModel上のデータを監視(Observe)して表示します。

@startuml
  class View
  note right of ViewModel : ViewModelは、操作結果により、\n保持する表示用データを更新する
  class ViewModel{
    表示用データ
  }
  class Model

  View --> ViewModel: 読み込み・新規作成等の操作\n変更の監視(変更があれば表示を更新する)
  ViewModel --> Model
@enduml

ViewModel化のメリット

ViewModelにも色々あるみたいなのですが、ここでのViewModelはデータを保持できるPresenterといった感じです。

PresenterからViewの参照を排除できる

ViewModel化前は、Presenterに非同期処理完了のコールバックとして、Viewの参照を(interfaceとしてですが)渡していました。

ViewModelを利用すると、ViewModelからViewへの参照は不要になります。

AndroidアプリでのViewModelのメリット

  • FragmentでもActivityライフサイクルでのデータ保持が可能

Fragmentにデータを保持していると、画面の回転等でFragmentが破棄された際に、データの読み込みが必要でした。

ActivityライフサイクルのViewModelを利用する事で、Fragmentが破棄されても、データの引き継ぎが可能になります。

  • ライフサイクル管理の単純化

Presenterが行う非同期処理の結果をFragmentで表示する場合、先にFragmentが破棄されていると、クラッシュしてしまう問題がありました。

ViewModelのライフサイクルはFragmentから分離されている為、Fragmentの状態を気にせずに、非同期処理が可能です。

リファクタリング前のコード

最初にリファクタリング前のコードです。

Viewを直接参照しないのは、PresenterからViewの依存を排除する為です。

こうしておくと、テスト用のダミークラスに差し替えたりできます。

@startuml

  interface ItemsResult
  {
    onItemLoaded(items)
    onFailed(message)
  }
  
  class ItemsFragment implements ItemsResult
  {
    items:List<Item>
  }
  class ItemsPresenter
  {
    loadItems(page)
  }
  class UseCase

  ItemsFragment --> ItemsPresenter: itemsの読み込み操作
  ItemsPresenter --> ItemsResult : 結果(items)
  ItemsPresenter --> UseCase


@enduml

ItemsViewInterface.kt

interface ItemsResult
{
    fun onItemsLoaded(items:List<Item>)
    fun onFailed(message:String)
}

ItemsPresenter.kt

class ItemsPresenter(
  private val context:Context,
  private val coroutineScope: CoroutineScope,
  private val view: ItemsResult) {

    private val useCase = ItemsUseCase()

    fun loadItems(page:Int) {
        coroutineScope.launch{
            try {
                val result = useCase.loadItems(context, page)
                view.onLoadItems(result)
            } catch (ex: Exception) {
                view.onFailed(ex.message)
            }
        }
    }
}

ItemsFragment.kt

class ItemsFragment:Fragment,ItemsResult
{
    // 請求書一覧のアダプターです
    private var adapter: ListAdapter<Item, RecyclerView.ViewHolder>?
    private var presenter: ItemsPresenter?
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        presenter = ItemsPresenter(requireContext(), viewLifecycleOwner.lifecycleScope, this)
        presenter?.loadItems(1)
    }
    
    override fun onItemsLoaded(items:List<Item>){
        adapter?.submitList(items)
    }
    
    override fun onFailed(message: String){
        // エラー表示する
    }
}

リファクタリング後のコード

ViewModelの実装

ViewModelからFragmentへの参照が無くなり、すっきりしていると思います。

@startuml

  class ItemsViewModel
  {
    - itemsBuffer:List<Item>
    + items:MutableLiveData<List<Item>>
    + networkState:MutableLiveData<NetworkState>
    laodItems(page)
  }

  note right of Fragment: Items, networkStateの変更を監視している
  class Fragment
  class UseCase

  Fragment --> ItemsViewModel : itemsの読み込み
  ItemsViewModel --> UseCase

@enduml

ItemsViewModel.kt

MisocaのAndroid版では、APIとの通信にContextが必要な為、AndroidViewModelを継承しています。

AndroidViewModelを継承することで、ApplicationContextをgetApplication()で参照可能になります。

ただし、ApplicationContextには言語設定や画面向きの変更が反映されない為、ViewModel内でUIに関する処理を行わないよう注意が必要です。

class ItemsViewModel(context: Context) : AndroidViewModel(context.applicationContext as Application){
    // 通信中状態、エラーの通知用のobserve可能なプロパティ
    val networkState = MutableLiveData<NetworkState>()    
    // 表示するデータの実体(LiveData経由でFragmentに渡す)
    private val itemsBuffer = ArrayList<Item>()
    // observe可能なプロパティ
    val items = MutableLiveData<List<Item>>()
    
    // データの一覧を読み込みます
    fun loadItems(page: Int) {
        viewModelScope.launch {
            try {
                networkState.postValue(NetworkState.LOADING)
                
                // loadItemsはDispatchers.IOで動作するsuspend functionです。
                val result = useCase.loadItems(getApplication(), page)
                itemsBuffer.addAll(result)
                
                // observeしているViewに更新を通知する
                items.postValue(itemsBuffer)
                networkState.postValue(NetworkState.LOADED)
            } catch (ex: Exception) {
                // エラーメッセージを表示
                networkState.postValue(NetworkState.error(ex.message))
            }
        }
    }
}

NetworkStateの実装は、下記のコードを参考にしました。

https://github.com/android/architecture-components-samples/tree/master/PagingWithNetworkSample

ViewModel上のデータ変更を監視する

ViewModelをFragmentから使う場合は、fragment-ktxにあるExtension(viewModels,activityViewModels)を使用すると便利です。

build.gradle

dependencies {
    implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion"
}

ItemsFragment.kt

class ItemsFragment:Fragment
{
    // fragment-ktxのactivityViewModelsでviewModelを取得します
    private val viewModel: ItemsViewModel by activityViewModels()
    // 請求書一覧のアダプターです
    private var adapter: ListAdapter<Item, RecyclerView.ViewHolder>?

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)

     observeViewModel()
     // itemsの読み込みを開始します
     viewModel.loadItems(1)
   }

    private fun observeViewModel() {
        // itemsの更新を監視します
        viewModel.items.observe(viewLifecycleOwner) { items ->
            // List<Item>をそのまま通知しても、DiffUtilが効率的に再描画してくれるので気にしない
            adapter?.submitList(items)
        }
        
        // 通信中状態の更新を監視します
        viewModel.networkState.observe(viewLifecycleOwner) {
            when (it.status) {
                Status.RUNNING ->{
                    // プログレスバーを表示
                }
                Status.FAILED -> {
                    // エラー表示
                }
                Status.Success -> {
                    // プログレスバーを消す
                }
            }
        }
    }
}

おまけ

この画面は2つのタブがそれぞれFragmentになっているのですが、ViewModelを通じて2つのタブがデータを共有しています。

タブ間(Fragment間)でのデータの移動も、ViewModelを使用する事で、簡単に実現できています。

スクリーンショット 2020-11-13 10.37.55.png (47.6 kB)

まとめ

Viewの操作が不要になって、スッキリしました

宣伝

Misoca 開発チームでは、モバイルアプリエンジニア(iOS, Android)を募集しています。

www.wantedly.com