シンクロ・フード エンジニアブログ

飲食店ドットコムを運営する、株式会社シンクロ・フードの技術ブログです

Railsアプリケーションで使われているモデルをRSpecを使って自動的に機能ごと一覧化する

こんにちは。開発部の竹内です。
飲食店ドットコムモビマルといったサービスで Ruby on Rails での開発に携わっています。

Railsアプリケーションで使われているモデルをRSpecを使って自動的に機能ごと一覧化する対応を行いましたので、それについてお話ししたいと思います。

背景

コードを修正するにあたり、既存の機能に影響がないか・整合性があるかを調べるのは重要です。ですが私が所属する会員企画開発チームでは影響範囲の確認が漏れてバグや仕様上の不整合が発生することが多く、課題となっていました。
そのため機能ごとにどのモデルが使用されているかが分かるドキュメントを整備し、影響範囲の調査やサービスの理解に役立てようという提案がありました。ドキュメントは人力で作成していたのですが、作成に時間がかかりメンテナンスも大変であることから、自動的に出力する仕組みを作りたいと思いました。弊社には業務時間の10%でサービス改善のためにエンジニアの好きなことをできる制度があるので利用することにしました。

方針

Ruby の言語の特性上、静的に解析を行うのは難しそうです。
そのため動的に情報を得る方針を取りました。
弊社が運営しているモビマルでは Request Spec がほぼ全機能で書かれているのでこれを利用できないかと考えました。
カバレッジツールやプロファイラでも使用されている TracePoint を用いればメソッド呼び出しをフックできるので、これを記録すればどのモデルのメソッドが呼ばれているかが分かります。
また、どの機能が実行されているかは Active Support Instrumentation によって分かるためこれも利用することにしました。

実装

まず、TracePoint を使ってメソッドが呼ばれたクラスを記録するクラスを定義します。

# TracePoint を使ってメソッド呼び出すを記録するためのクラス
class Tracer
  attr_reader :used_classes

  # @param [Proc] filter_proc
  #  TracePoint で記録するメソッド呼び出しを絞り込むためのブロック
  #  引数に TracePoint を受け取り、true を返した場合のみ記録する
  def initialize(&filter_proc)
    @used_classes = Set.new
    @filter_proc = filter_proc || proc { true }
  end

  def trace(&block)
    tr = TracePoint.new(:call, :c_call) do |tp|
      next unless @filter_proc.call(tp)

      @used_classes << tp.self.class
      @used_classes << tp.self if tp.self.is_a?(Class) # クラスメソッドが呼ばれた場合
    rescue NoMethodError, ArgumentError # rubocop:disable Lint/SuppressedException
      # tp.self.class 取得時エラーになることがあるので、それは無視する
    end
    tr.enable(&block)
    self
  end
end

Tracer#trace をブロック付きで呼び出すと、ブロック中で呼ばれたメソッドのレシーバのクラス、またはレシーバがクラスの場合(クラスメソッドが呼ばれた場合)はレシーバ自身を @used_classes に記録します。
コンストラクタにブロックを渡すことができ、それによりクラスを記録するかどうかを判定します。こちらは後ほど説明します。

これを使ってアクションごとに使用されているモデルを取得します。

# frozen_string_literal: true

require 'csv'

MODEL_ANALYZER_TMP_FILE = 'coverage/model_analyzer_tmp.txt'
MODEL_ANALYZER_RESULT_FILE = 'coverage/model_analyzer.tsv'
MODEL_ANALYZER_EXCLUDED_CLASS_NAMES = Set.new([
  'ApplicationRecord',
])

RSpec.configure do |config|
  # テストスイート実行開始時、一時ファイルを削除する。存在しなければ何もしない
  config.before(:suite) do
    FileUtils.rm_f(MODEL_ANALYZER_TMP_FILE)
  end

  config.around do |ex|
    # controller の action が呼ばれた時にその controller と action を記録する
    controller = nil
    action = nil
    subscriber = ActiveSupport::Notifications.subscribe('process_action.action_controller') do |_, _, _, _, payload|
      controller = payload[:controller]
      action = payload[:action]
    end
    tracer = Tracer.new { RequestDurationMiddleware.in_request? }
    tracer.trace { ex.run }
    ActiveSupport::Notifications.unsubscribe(subscriber)

    # 使われたクラスを行ごとの JSON 形式で 一時ファイルに出力する
    # ディレクトリが存在しない場合は作成する
    FileUtils.mkdir_p(File.dirname(MODEL_ANALYZER_TMP_FILE))
    File.open(MODEL_ANALYZER_TMP_FILE, 'a') do |f|
      break unless controller && action

      f.puts JSON.generate({
        controller: controller,
        action: action,
        classes: tracer.used_classes.map { |klass|
          next if klass.name.nil? || MODEL_ANALYZER_EXCLUDED_CLASS_NAMES.include?(klass.name)

          source_location =
            begin
              Object.const_source_location(klass.name).first
            rescue NameError
              nil
            end

          # models 下で定義されたものが対象
          next unless source_location && source_location.start_with?('/mobimaru/rails/app/models/')

          klass.name
        }.compact,
      })
    end
  end
end

RSpec の config.around で、各ケース実行時に処理を追加します。
Active Support Instrumentation によりテスト対象のコントローラとアクションを取得します。
Tracer#trace に渡したブロック内でケースを実行し、使用されたクラスを記録します。
Tracer.new に渡しているブロック内の RequestDurationMiddleware.in_request? は実行タイミングがリクエスト中かを判定します。詳しくは後述します。
ケース実行後は、coverage/model_analyzer_tmp.txt にコントローラ・アクション・使用クラスの情報を行ごとの JSON 形式で追加します。この時 /mobimaru/rails/app/models/ 下以外のクラスやトップの ApplicationRecord は除外しています。

RequestDurationMiddleware の定義は以下となります。

# frozen_string_literal: true

# リクエスト中かを保持するためのミドルウェア
class RequestDurationMiddleware
  class << self
    def in_request?
      Thread.current[:request_duration_middleware_in_request]
    end
  end

  def initialize(app)
    @app = app
  end

  def call(env)
    self.in_request = true
    @app.call(env)
  ensure
    self.in_request = false
  end

  def in_request=(value)
    Thread.current[:request_duration_middleware_in_request] = value
  end
end

Rails.application.config.middleware.use RequestDurationMiddleware

Rack ミドルウェアでリクエスト中かを判定します。
RSpec のケース実行中には準備段階でのデータ作成などがあり、機能内で使われないモデルのメソッドが呼ばれることがあるので、除外するために行なっています。

全てのケース実行後は結果をファイルに出力します。

# ... 省略

RSpec.configure do |config|
  # ... 省略

  # テストスイート終了時、request spec で使用されたモデル一覧を集計する
  config.after(:suite) do
    controller_and_actions = {}

    File.open(MODEL_ANALYZER_TMP_FILE) do |f|
      f.each_line do |line|
        used = JSON.parse(line)
        key = [used['controller'], used['action']]
        (controller_and_actions[key] ||= Set.new).merge(used['classes'])
      end
    end

    # controller_and_actions から、| controller | action | used_classes(カンマ区切り) | の形式のTSVを作成する
    # controller と action の昇順でソートする
    File.open(MODEL_ANALYZER_RESULT_FILE, 'w') do |f|
      # ヘッダを出力
      f.puts(%w[controller action used_classes].to_csv(col_sep: "\t"))

      controller_and_actions.sort_by(&:first).each do |(controller, action), used_classes|
        f.puts([controller, action, used_classes.to_a.sort.join(',')].to_csv(col_sep: "\t"))
      end
    end
  end
end

一時ファイルに保存した内容を読み取った後、コントローラ・アクションごとに集計し、TSVで出力します。

実行

spec_helper.rbrequest_spec_model_analyzer_helper.rb を require し、RSpec を実行すると TSV が出力されます(一部抜粋)。

controller    action  used_classes
# ...
Home::MagazinesController   index   Contact,Magazine,MagazineCategory
# ...
Home::NewsController    show    Contact,News,NewsTarget,NewsTargetManage
# ...

まとめ

Request Spec を用い、機能ごとに使われているモデルを一覧化することができました。
これを使ってチーム内でドキュメントを作成するのに役立てています。
普段の Web 開発では馴染みの薄い TracePoint や Active Support Instrumentation を使うことができ、作っていて面白い取り組みになりました。
シンクロ・フードでは他にも自動化など改善の余地がある箇所がたくさんあるので、関わってみたい!という方をお待ちしています。

Aurora MySQL v3 (MySQL 8.0 互換) へのアップグレード対応

はじめまして、SRE チームの村山です。

今回、弊社の主要サービスで使用している Aurora MySQL のアップグレードを行いましたので、対応内容や得られた知見についてご紹介したいと思います。

背景

弊社で使用している Aurora は、2022 年 10 月に Aurora MySQL v1 (MySQL 5.6 互換) から Aurora MySQL v2 (MySQL 5.7 互換) へとアップグレードしております。その際の対応については、以下の記事をご覧ください。

tech.synchro-food.co.jp

その後も v2 を使用していましたが、2024 年 10 月 31 日 をもって v2 の標準サポートが終了になるため、Aurora MySQL v3 (MySQL 8.0 互換) へのアップグレードが必要になりました。

docs.aws.amazon.com

変更点の影響調査

アップグレードに向けて、まずはドキュメントを中心に変更点と影響範囲の調査を進めました。 これは SRE チームではなく、Web サービスの基盤部分を横断的に見るアプリケーション基盤チームを中心に実施して頂きました。

この中で対応が必要だと判断し、実施した主な内容を以下に記載します。

デフォルトの COLLATION が utf8mb4_0900_ai_ci になる

MySQL 8.0 では、utf8mb4 のデフォルトの COLLATION が utf8mb4_general_ci から utf8mb4_0900_ai_ci に変更になりました。

Each character set has a default collation. For example, the default collations for utf8mb4 and latin1 are utf8mb4_0900_ai_ci and latin1_swedish_ci, respectively.

これまで DB のテーブル定義 (マイグレーションファイル) には明示的に COLLATION を設定していなかったので、今後新規にテーブルを作成する際は COLLATE=utf8mb4_general_ci を設定するように開発ルールを変更しました。
DB アップグレードのタイミングに合わせ、既存のテーブルも COLLATE を設定するように変更しています。

FLOAT, DOUBLE, DECIMAL タイプのカラムに対して UNSIGNED 属性が非推奨になる

MySQL 8.0.17 で非推奨になったため、ALTER TABLE で対象のカラムから UNSIGNED 属性を削除するようにしました。対象のカラムが多いため、稼働中の DB には ALTER TABLE を適用せず、後述するクローンの DB にだけ適用するようにしています。詳しくは「アップグレード方法」の項に記載します。

As of MySQL 8.0.17, the UNSIGNED attribute is deprecated for columns of type FLOAT, DOUBLE, and DECIMAL (and any synonyms);

SQL_CALC_FOUND_ROWS, FOUND_ROWS() が非推奨になる

MySQL 8.0.17 で非推奨になったため、使用箇所を修正しました。アップグレード前に修正できる内容でしたので、事前に対応しています。

The SQL_CALC_FOUND_ROWS query modifier and accompanying FOUND_ROWS() function are deprecated as of MySQL 8.0.17;

utf8mb3 が非推奨になる

MySQL 8.0 で非推奨になったため、ALTER TABLE で対象のテーブル・カラムを utf8mb4 に変換しました。こちらも後述するクローンの DB にだけ適用するようにしています。

The utf8mb3 character set is deprecated. utf8mb3 remains supported for the lifetimes of the MySQL 8.0.x and following LTS release series, as well as in MySQL 8.0.

予約語利用箇所の対処

MySQL 8.0 より RANK や LEAD の単語が予約語に追加されました。

Reserved words are permitted as identifiers if you quote them as described in Section 11.2, “Schema Object Names”:

既存のテーブルに同名のカラムがあることを下記クエリで確認しましたが、アプリのライブラリにより引用符で囲む処理をするため、対応は不要の想定でした。

select table_schema, table_name, column_name 
from information_schema.columns 
where table_schema not in ('information_schema', 'mysql', 'sys') 
  and column_name in (select WORD from information_schema.KEYWORDS where reserved = 1)
and TABLE_NAME NOT IN ('ar_internal_metadata');

しかし、後の段階で一部のアプリではその処理がされず、エラーになることが判明したため、最終的にはカラム名を変更する対応をとりました。

GROUP BY 句での ASC/DESC の廃止

MySQL 8.0 より GROUP BY 句で ASC/DESC をつけることができなくなりました。後述する「テスト」の段階で、各サービスの開発担当者にて使用箇所を修正して頂きました。(昔からある一部の機能で利用されていました)

The deprecated ASC or DESC qualifiers for GROUP BY clauses are removed. Queries that previously relied on GROUP BY sorting may produce results that differ from previous MySQL versions. To produce a given sort order, provide an ORDER BY clause.

上記のほか、アップグレードにより変更されたパラメータグループの設定値の調査や検討なども行いました。

アップグレード方法

アップグレードの方法は、前回のメジャーバージョンアップ時と同じく、binlog レプリケーションを使った Blue-Green Deployment を採用しました。採用の理由は、実績があり手順が確立できていること、リリース当日のメンテナンス作業の時間を短くできること、切り戻しを容易にできることです。

具体的な手順は以下の記事を参考に検討しました。

aws.amazon.com

最初に検討した手順の大まかな流れは以下のとおりです。
※ テストで不備がみつかり修正したため、実際の手順とは異なります。詳しくは「アップグレード手順のテスト」の項に記載します

検討時のアップグレード手順

  1. 既存の Aurora MySQL v2 からクローンを作成する
  2. クローンを v3 へインプレースアップグレードする
  3. v2 から v3 へレプリケーションする
  4. v3 へ ALTER TABLE を実行する
  5. v2 から v3 へのレプリケーションを停止する
  6. アプリケーションの接続先を v3 へ切り替える
  7. v3 から v2 へレプリケーションする

手順 1 ~ 4 は事前に実施し、5 ~ 7 をリリース当日のメンテナンス作業時に実施します。そうすることで、リリース当日の作業時間を短くすることができます。
「変更点の影響調査」の項に記載した ALTER TABLE は、手順 4 のタイミングで v3 にだけ実行します。この時点でアプリケーションは v2 へ接続しているため、ALTER TABLE を安全に適用できます。
手順 7 は、アップグレード後に v2 へ切り戻せるようにするため実施します。

なお、RDS がサポートする Blue-Green Deployment の方法も候補にありましたが、切り戻しの容易さから前述の方法をとることにしました。

docs.aws.amazon.com

テスト

開発環境にてアップグレード手順のテストと、アップグレード後のアプリケーションのテストを行いました。

アップグレード手順のテスト

「アップグレード方法」の項で述べた手順で進めましたが、手順 4 の ALTER TABLE 実行後、v2 から v3 へのレプリケーションでエラーが発生してしまいました。

Column 17 of table 'テーブル名' cannot be converted from type 'varchar(765(bytes))' to type 'varchar(1020(bytes) utf8mb4)'

utf8mb4 へ変換する ALTER TABLE を v3 側だけに実行していたため、データ型の不一致が原因のようです。これを許容してレプリケーションできるようにするため、v3 側のパラメータグループで replica_type_conversions を設定するようにしました。
今回は source (v2) よりも target (v3) 側の型が大きいため、非不可逆変換を許可する ALL_NON_LOSSY の値を設定します。設定値の詳細は以下をご参照ください。

設定後、v2 から v3 へのレプリケーションがエラーにならないことを確認できました。
ただここで、手順 7 の v3 から v2 へのレプリケーションにおいても同じ問題に遭遇することが予想できました。型の大小関係が逆になるため、不可逆変換を許可する ALL_LOSSY を設定する必要があります。しかし、値が切り捨てられデータの不整合が生じる可能性があるため、他の方法を検討することにしました。

考えた方法は、v2 切り戻し用のクラスターを別に作成し、クラスター 3 台でレプリケーションする構成です。

実際のアップグレード手順

具体的な手順は以下になります。

  1. 既存の Aurora MySQL v2 からクローンを 2 つ作成する (v3 用と切り戻し用)
  2. 片方のクローンを v3 へインプレースアップグレードする
  3. v3 から切り戻し用の v2 へレプリケーションする
  4. v2 から v3 へレプリケーションする
  5. v3 へ ALTER TABLE を実行する
  6. v2 から v3 へのレプリケーションを停止する
  7. アプリケーションの接続先を v3 へ切り替える

切り戻し用の v2 には、v3 からのレプリケーションにより ALTER TABLE が反映されているため、型の不一致が起こりません。そのため、ALL_LOSSY を設定することなくレプリケーションすることができました。
また副産物として、v3 から切り戻し用の v2 へのレプリケーションを事前に実施できるため、リリース当日の作業を 1 つ減らすこともできています。

アプリケーションのテスト

上記の手順で開発環境を v3 へアップグレードした後、各サービスの開発担当者にてアプリケーションのテストを実施して頂きました。テストコードや、前回のバージョンアップ時に作成した実行計画を比較する機能を活用してテストをしています。

テストにより修正した内容の 1 つに、パラメータグループの optimizer_switch の変更があります。
実行結果が変わってしまうクエリが見つかったため、実行計画を確認するなどして調査を進めました。試行錯誤の結果、セミジョイン最適化の duplicateweedout の動きが原因ではないかと考え、開発環境で optimizer_switch の duplicateweedout を off にしたところ、問題が解消しました。
バグレポートをみると、duplicateweedout 含めセミジョイン関連のバグがいくつか報告されていました。ただ今回のテストでは、セミジョインに起因する他の問題は起こらなかったので、duplicateweedout だけ off にする方針にしています。

その他にも問題のあった箇所は、各開発者にて修正をして頂きました。

リリース

テストが完了した後、リリースへ向けた対応を進めました。
まずリリースの前週に「アップグレード手順のテスト」の項に記載した手順の 5 までを実施します。v3 へのインプレースアップグレードは約 15 分、ALTER TABLE の実行は約 15 分で完了し、作業全体は 70 分程で完了しました。

そしてリリース当日のメンテナンス中に手順 6, 7 を実施しました。正確にはレプリケーションの同期確認やアプリのデプロイ作業も含むため、メンテナンス時間は 2 時間設けていましたが、ほぼ予定時間内に作業を完了することができました。
ちなみにレプリケーションの同期確認では、各 DB のレコード件数が一致することの確認をしています。テーブルごとに SELECT COUNT をしているのですが、処理を並列化することにより 10 分程で完了しました。 直列で行なうと 1 時間以上かかることがわかっていたため、作業時間の短縮に繋がりました。

リリース後の修正

v3 へのアップグレード後、v2 へ切り戻すほどの致命的なエラーはありませんでしたが、いくつか問題が発生しました。
ひとつは、パフォーマンスが極端に劣化した SQL の問題です。実行時間が著しく伸び、DB の負荷も増大したため急ぎ修正する必要がありました。
その主な原因は、「テスト」の項でも述べたセミジョインによるものでした。
MySQL 8.0 では内部のオプティマイザに変更が多く、サブクエリを使う SQL において v2 ではセミジョインが使われなくても、v3 だとセミジョインが使われるケースが多くあります。
オプティマイザの不具合なのか、セミジョインを使うと速度が大きく劣化するようなケースにもかかわらず、セミジョインを利用してしまうようなこともありました。
具体的な例を示します。以下は同一の SQL について、セミジョインを on(デフォルト) または off にしたときの、EXPLAIN FORMAT=TREE の実行結果になります。(最初の 2 行のみ抜粋)

optimizer_switch: semijoin=on のケース

-> Aggregate: avg((db_1.table_1.column_1 / (case when (((to_days(db_1.table_2.column_1) - to_days(db_1.table_2.column_2)) / 30) < 1) then 1 else round(((to_days(db_1.table_2.column_1) - to_days(db_1.table_2.column_2)) / 30),0) end)))  (cost=7839673608.28 rows=6050893371)
    -> Nested loop semijoin  (cost=7234584271.20 rows=6050893371)

optimizer_switch: semijoin=off のケース

-> Aggregate: avg((db_1.table_1.column_1 / (case when (((to_days(db_1.table_2.column_1) - to_days(db_1.table_2.column_2)) / 30) < 1) then 1 else round(((to_days(db_1.table_2.column_1) - to_days(db_1.table_2.column_2)) / 30),0) end)))  (cost=302837.05 rows=618503)
    -> Nested loop inner join  (cost=240986.72 rows=618503)

セミジョインを利用する場合だと、rows=6050893371 という内部テーブルができてしまっています。実際にこれが原因で、SQL の実行時間が著しく伸びてしまっていました。
このようなケースに対処するためには、SQL を書き換えたりして局所的にセミジョインを回避する必要がありました。オプティマイズヒントを使って無効化する方法も把握していましたが、フレームワーク経由で SQL が実行される都合上、SQL を書き換えることで対応し、解決することができました。

他には、バッチ処理で以下のエラーが発生した問題がありました。

The table '/rdsdbdata/tmp/#sqlxxx_xxx' is full

これは MySQL 8.0 から一時テーブルの動作が変更されており、それに起因するものでした。リソースの制限値を超えてしまったことによるエラーのため、パラメータグループから temptable_max_ram と temptable_max_mmap の値を調整することで解決することができました。

docs.aws.amazon.com

その他にもいくつかの問題が発生しましたが、内容に応じて各サービスの開発者や SRE チームにて修正を行っております。

リリース後のパフォーマンス

v3 へのアップグレード後、主要サービスの全体的なレスポンスタイムは大きな変化がありませんでした。
先に述べたパフォーマンスが劣化する SQL の影響があり、リリース日は悪化していましたが、修正によりその後は元に近い値となっています。
現状、v3 の仕様や追加機能にあわせたチューニングはしていないので、大きく改善することもなく変化は少なかったのかなと考えています。

レスポンスタイムの状況

まとめ

各チームの開発者とチーム横断で協力して対応した結果、無事に Aurora MySQL v3 へのアップグレードを完了することができました。
本記事が Aurora アップグレードへ取り組む方にとって、少しでもお役に立てれば幸いです。

RubyKaigi 2024に参加しました

はじめまして、開発部の大庭です。
普段はRuby on Railsを用いたアプリケーション開発を行なっています。

今回、那覇で開催されたRubyKaigi 2024に参加してきましたので、印象に残ったセッションや感想をお伝えしたいと思います。

なお、弊社はSilver SponsorとしてRubyKaigi2024に協賛させて頂きました。

rubykaigi.org

印象に残ったセッション

Day1 Keynote 「Writing Weird Code」

発表資料はこちらから確認できます。

初日の最初のセッションで、初参加の私はRubyKaigiの洗礼を受けました。
一見して複雑で実行不可能に見えるコードが、実際には文法として有効で実行可能であること、さらにはQuine(自身のプログラムを出力する性質)であるプログラムが紹介されました。

普段の業務では、読みやすく簡潔なコードを目指していますが、このセッションではその対極にある世界を垣間見ることができ、Rubyの奥深さに衝撃を受けました。

このセッションは衝撃の連続であり、正規表現を用いて連立方程式の解を返すプログラムや、Rubyのプログラムとして実行可能な画像など、Rubyの高い表現力や拡張性に大変驚きました。

帰宅してから、セッションで取り上げられていたプログラムを実行してみたのですが、実行結果を見てただただ驚くばかりでした。 改めてまじまじと見ても、何が書いてあるのか、何故動くのかがまったく理解できませんでした。

github.com

次回のRubyKaigiでは、このようなコードが集まるTRICK(超絶技巧 Ruby 意味不明コンテスト)というも催しも開催されるようで、非常に楽しみです。

Day1 「Unlocking Potential of Property Based Testing with Ractor」

発表資料はこちら確認できます。

speakerdeck.com

こちらは、Rubyの並列処理機能であるRactorを、テスト手法であるProperty Based Testingに適用し、実行速度の向上を試みたというセッションになります。

Property Based Testingとは、RSpecでテストするようなExample Based Testingに対して、ランダムな値を大量に生成しテストするという手法です。 ランダム値を大量に生成して試行するため、プログラマが予期しなかったバグを発見可能という利点があります。 この手法ではテストを大量に実行する必要があるのですが、それらは独立した処理であるということに注目し、Ractorの適用を試みたという内容でした。

RubyKaigi 2024を通して、個人的に非常に興味を惹かれたセッションでした。
Property Based Testing自体が魅力的な手法で、開発時に手軽に素早く実行できるとより良い開発体験になりそうと感じました。 Integerなどの型をもとに入力データを生成していることから、(近年のRubyのトピックの1つである)型推論と組み合わせることでさらなる手軽さを導入できるのではと思いました。

RubyKaigi 2024を通しての感想

印象に残ったセッション以外にも、言語処理系など普段あまり関わる機会のない分野のセッションも聞きました。 これまで勉強してこなかった分野のため、内容のほとんどは理解できなかったものの、「Rubyの1+1は常に2ではない」(Integer#+の定義を容易に上書きできる)などといった話は印象的でした。 このように、Rubyについての幅広い話題が取り上げられており、そこから新たな考えや発見を得られるのは非常に良い経験でした。

私は外部の勉強会などに参加してこなかったのですが、RubyKaigiに参加してブースや懇親会で、他社のエンジニアさんと交流することができて非常に刺激を受けた良い経験となりました。他社のエンジニアさんを勝手に畏怖していたのですが、実際に交流してみるとそんなことはなく、会社の違いはあれど同じエンジニアなんだなと感じることができました。 また、弊社に在籍していた先輩がLTに登壇されており、こちらも刺激を受けるとともに、登壇(者)を少しだけ身近に感じ、RubyKaigiに限らず機会があれば何か発表してみたいなと思いました。

さらに、各社ブースを巡ってみて、使用しているツールなどのアンケート結果に驚いたり、掲示されているプログラムに付箋でコードレビューする催しなど、セッション以外も非常に面白かったです。

おわりに

非常に熱気のある発表に対して、自分は知らないことだらけで、学習欲・プログラミング欲が刺激される3日間でした。 いつか発表の内容が理解できるように精進していきたいです。

中途入社モバイルアプリエンジニアが入社後4ヶ月で感じたこと

自己紹介

こんにちは、開発部モバイルアプリチームの横山です。

普段は求人飲食店ドットコムのAndroidアプリの開発をしている私ですが、今年の2月に中途入社しました。
社会人になって10年目になります。
新卒でIT業界とは関係のない業界に就職し、コロナの影響など紆余曲折を経て前職でエンジニアとしてのキャリアをスタートしました。
前職は会社ではWebサイト開発とFlutterを使ってiOS・Androidアプリケーションの開発をしていました。

そんな私が入社して4ヶ月を経とうとしている今、入社エントリーとして、転職活動をしていた当時や、入社してからのシンクロ・フードでの経験を振り返り、感じたことを今回お伝えできればと思っています。

なぜシンクロ・フードに転職したか

ネイティブでのアプリ開発に徐々に興味が出てきていた事と、社内でのアプリ開発者が私一人で、自分よりも知見や知識が豊富なエンジニアと共に働き、自身の技術を向上させたいと考え転職を決意しました。

転職の軸として「成長できそうか」「アプリ開発に携われるか」という観点で会社を探していました。
弊社とのカジュアル面談での説明が、他社と比べて技術スタックや会社の評価制度について詳しく説明をしていただき好印象だったことや、アプリ開発の方針や、アプリ開発をクロスプラットフォームのFlutterやReact Nativeで開発するのではなく、Kotlin、Swiftで開発を行っていることなど、今後エンジニアとして働いていくなかで技術力を伸ばしていける理想的な環境が整っていると思ったのでシンクロ・フードを選びました。

また、評価制度がしっかりしてると感じたので、納得して業務を取り組んでいける環境でもあると思いました。

入社前の不安と実際に入社した後の心境

弊社の開発部はフルリモート制度が2023年5月より採用されており、フレックスタイム制で働くことができます。
コアタイムの11:00〜16:00に出勤していれば、フレキシブルタイムの7:00〜22:00の間で好きな時間に業務を行うことができ、月の労働時間が所定の時間を満たすように気をつければ出勤時間と退勤時間は自由にすることができます。
このように、柔軟に働くことができるので、とても魅力的な環境だと思っていたのですが、フルリモート勤務、フレックタイム制どちらとも未経験の私には不安のもとでした。
仕事とプライベートの区別が難しいのではないか、オフラインで直接相談して聞けないことで、チームメンバーとのコミュニケーションや業務の進め方、社内ツールの使い方など上手くやっていけるかなど様々なことに不安を感じていました。

しかし、実際に働き始めてみると、仕事とプライベートの切り替えにはそれほど問題がなく、フルリモート勤務ならではのメリットを感じています。
腰痛があるため前職ではずっと座っていることが辛かったのですが、これを機会に自宅のデスクを昇降式デスクに変更するなど作業環境の改善に取り組み、身体的ストレスが軽減されるようになりました。
私は本社がある東京ではなく関西圏からフルリモート制度を利用して勤務をしてるので、おかげで自宅でも集中して業務に取り組めるようになり作業効率は良くなったと感じています。

現在のデスク

コミュニケーションに関しても、Slack内のチームチャンネルや1on1の場を通じて、気軽に質問や雑談ができる環境が整っているので、不安を感じることなく業務に取り組めています。週1回チームのミーティングでは、雑談の時間があり好きな雑談テーマや、近況などを話すことができます。最近あった話題は「よく見るYoutubeチャンネルやテレビ番組」、「雑学やトリビアなど、ちょっとした知識や発見、タメになったり面白い情報」など業務とは関係ないことなどを業務委託の方々含め談笑していたりします。

また、社内のドキュメントが整備されており、必要な情報をすぐに見つけることができるため、仕事に取り組む上での情報に困ることは今のところないです。あと、オンボーディング期間も十分に設けられ、社内の業務フローやツールの使い方が丁寧に教えてもらえたので、スムーズに業務に取り組むことができる環境だと感じています。

■ オンボーディング期間中の1日の流れ

  • 09:00:メール確認、昨日の復習など
  • 11:00:チームMTG
  • 12:00:お昼休憩
  • 13:00 ~ 17:00:学習、業務フローやサービスの説明
  • 18:00:1on1、メンターとの振り返り

シンクロ・フードに入って感じたこと

一番の大きな印象は、社内ドキュメントがしっかりしている整理されていることでした。
情報共有サービスなどに社内業務の進めかたや、開発のルール、仕様等がまとまっていて、大体の探したいものは検索したらすぐに見つかるので、とても便利で仕事がしていきやすいと感じています。あと入社間もない頃は、大量に覚えないといけないという気持ちが少しあったのですが、検索したら情報が出てくるので、心理的な負担が軽くなった覚えがあります。

esa環境構築などの一覧

あと、社内アシスタントbotの「ナレット」や「さえずり」の開発を行うなど、業務をより便利に、かつ効率よくしていこうという活動や、前職ではなかったドキュメントを作るだけでなく、アップデートをしていこうというチームとしての方針を感じているので、とてもいい環境だなと感じています。私も何かしらアプリ開発やその他の業務に関することを残して貢献していけたらと思っています。

業務でもネイティブでのアプリ開発がしたくて、入社したので現在の開発業務全般にやりがいを感じています。

課題・改善点について

求人飲食店ドットコムAndroidアプリには、技術的にレガシーな部分やフォルダ構成が機能によってバラバラな点があるのでその改善をしていきたいです。(フォルダ構成に関しては現在リアーキテクチャを進めているので時間の問題で解決をするのかなと思うのですが...)

あと、AndroidOS特有のナビゲーションバーの「戻るボタン」をタップした時やジェスチャーナビゲーションの戻る操作を行った際の動作について、もっとこうしたらいいのにと感じる点があるので、そのことが課題かなと思っています。感じた課題に関しては機能改善として提案していきたいです。

これからについて

まだまだ力不足なところもあり十分には貢献できてはいないのですが、求人飲食店ドットコムAndroidアプリの施策機能の実装を進めて行きたいです。求人飲食店ドットコムiOSアプリと比べて、開発が追いついていない状況なので開発を進めていき、使っていただくユーザーに便利なアプリを提供していきたいです。

機能開発以外ではiOSアプリではSwiftUIについて取り組んでいるので、Android側でも徐々にですがJetpack Composeの導入について考えていければといいなと思っています。

便利な社内アシスタントBotのシステム構成を大公開!

こんにちは、開発部モバイルアプリチームの小関です。

普段は求人飲食店ドットコムのiOS・Androidアプリの開発をしている私ですが、昨年4月に設立された「GPTプロジェクトチーム」にも参加しており、この1年でより一般的にも身近になってきた生成AIをサービスや普段の業務に使えないかと模索する仕事もする日々です。

今回は、そんな「GPTプロジェクト」の一環で作成した社内アシスタントBotによって、ナレッジの検索しづらさを解決しようとした事例をご紹介しようと思います。

GPTプロジェクトとは

弊社では2023年4月から、CTO直下に「GPTプロジェクトチーム」を新設して(*1)、ChatGPTのような生成AIを活用する取り組みを進めています。
このプロジェクトでは、弊社が運営するサービスや社内業務に対して生成AI技術を適用し、飲食店の業務効率化や負荷削減を図ることを目的として活動しています。

チーム発足まもない頃は、そもそも生成AIとはどう動いているのだろうといった部分や、ChatGPTなどの生成AIサービスはどういった形で利用でき、どのような場面に適用することができるのかを探るところから行っていました。

生成AI自体は様々な種類がありますが、手始めに取り組むものとしてはやはりChatGPTからだろうとなり、APIの利用方法やLangChainという文章生成に関わるライブラリを調べるところから行い、この1年間でいくつか、ChatGPTを組み込んだツールや機能の開発を行いました。

今回はその中から、社内アシスタントBotを作ったことでナレッジ検索がしやすくなった事例を紹介していきます。

社内アシスタントBot 第一弾「ナレット」

弊社の社内ルールや労務関連の情報をまとめたナレッジはGoogleサイトで管理をしているのですが、ナレッジが存在するかを調べるときに検索ワードが適切に選定することが難しかったり、それらしいキーワードで調べたときに本来見たいはずのナレッジが上位にでてこなかったりする課題がありました。

そんな中、他社の事例でチャットツールを経由したナレッジ検索のやり方が公開されており(*2)、弊社でもそれを参考にして作ることでこの課題をクリアすることができるのではないかという話がチーム内で挙がり、実際に開発に至りました。

そうして作られた社内アシスタントBotは「ナレット」と名付けられ、ナレットに対してメンションをつけて質問を投げかけると、質問に類似したナレッジ記事から必要な箇所を情報として引っ張ってきて、その情報を元に質問に答えてくれるといった、まさにほしかった仕組みが完成したのでした。

ナレットとのやりとり
ナレットとのやりとり

(ナレッジ+キャットからついた名前のとおりに猫キャラを付与しています)

このBotの良い点は、適切なナレッジを検索するためのキーワードがある程度曖昧でも、自然言語で質問をして検索ができるところにあります。

また、参照したナレッジがリンクで提示されるため、そのナレッジにアクセスして、より詳しい情報を得ることもできますし、正しい情報を言っているのかどうかも、参照したナレッジが適切そうかどうかである程度判断することができます。
本当に正しいのだろうか?という回答の際には、大半が関係のないナレッジを参照してしまったことによるもののため、その際には質問の聞き方を変えてみることで、本来知りたかった情報を得られるということもありました。
回答は必ずしも正しいとはいえないため、最終的には質問者がソースまで見て判断することを一応推奨しているのですが、ある程度は正しいナレッジが参照されていれば適切な回答ができるようになっています。

また、回答にフィードバックボタンをつけて意図どおり動作しているかを把握できるようにしています(適切な回答が得られなかった際にGPTプロジェクトメンバーがサポートを行い、質問の仕方をアドバイスしたり、ナレッジがないことで回答できていなければナレッジを作成・更新してもらうよう働きかける想定でつけた機能です)。

ここからはシステム的な話になりますが、実際に組んだ構成は以下のようになりました。

ナレットシステム構成
ナレットシステム構成

中心となっているのは、Slackが提供する Node.jsベースのフレームワークの Bolt for JavaScript を使って構築したアプリケーションで、実験的に動かしたいということもあり、PaaSのherokuを利用して動かしています。

また、今回はChatGPTを利用するにあたり、「LangChain」というChatGPTなどの大規模言語モデル(LLM)を使いやすくするためのフレームワークも併せて利用しています。
このLangChainはTypeScriptかPythonの形で提供されていましたが、社内的にもGPTプロジェクトメンバー的にもTypeScriptのほうが都合が良かったため、Slackアプリケーションも含めて言語はTypeScriptで統一して、このような構成になりました。

全体的な処理の流れとしては、

① Slackでナレット宛のメンションとともに質問をすると、Webhookによってアプリケーションにその情報が送られる
② 質問文をベクトル化する
③ 質問文に近い内容のナレッジがないかを、②のベクトルを使ってナレッジのベクトルDBに対して問い合わせる
④ ③で得た検索結果上位数件のナレッジの情報と質問文を組み込んだプロンプトでChatGPTに回答を作成してもらう
⑤ Slackの質問のスレッドに対して、④を返信する

となっています。

この中にでてくるナレッジが格納されたDBにはPineconeを利用しており、あらかじめGoogleサイトから取得したナレッジデータを一定の文量で区切って(チャンク化)、それをOpenAIのEmbedding APIにてベクトル化したデータを保存しています。
チャンク化するデータサイズは、大きすぎると最終的にChatGPTに渡す情報量が大きすぎることで当時は最大文字数の関係で扱いづらかったり課金額が増えてしまう課題につながり、小さすぎても情報が限定されることで質問に似ているかどうかの検索マッチ度がブレてしまって適切なデータが参照できないため、ここの調整には時間をかけて実験を重ね、ちょうどよいサイズを模索しました。
(最近では、ChatGPTに渡せる最大情報量(=トークン量)がかなり拡張され、課金額も安くなっているので昔よりもこのサイズは大きくしても問題なくなってきています)

こうして作られたナレットですが、単純にナレットを利用することでナレッジが検索しやすくなったこと以外にも、古いナレッジを放置するリスクがより高まったことで、なるべく更新をしようとする動きが働いたこともプラスの一面でした。

ちなみにChatGPTに詳しい方であればお気づきのとおり、このように大量の文章データを元にChatGPTに質問に回答させるようなことは、現在はChatGPT単体で解決できるようになっており、Assistantsという機能で提供されています。
ただし、柔軟なカスタマイズはできないので、ベクトルデータに対してタグを付与することや、引用元のURLを表示したりすることなどは工夫が必要になりそうではあり、今の構成を変えるほどではないとも考えています。

社内アシスタントBot 第二弾「さえずり」

弊社ではナレッジサービス・ツールをいくつか利用しており、部署などによっても使い分けをしています。
その中でも、エンジニア、デザイナー、ディレクターが利用するesa.ioというナレッジサービスがあり、業務に関するナレッジを保存したり、普段の会議での議事録としても利用しています。

そんなesaには、弊社が運営するサービスの知識や、開発における知見・ルールなどがまとめられており、前述のナレットのナレッジデータをesaにしたバージョンを作れないかという話が挙がったため、作成することになりました。

アプリケーションの全体的な仕組み自体は流用ができ、ナレッジを保存するベクトルDBの内容を社内ナレッジではなくesaの記事に変えるだけで済むといった具合に、比較的に容易に完成に至り、この新しいBotにはesaのサービスキャラクターの鳥から連想して「さえずり」という名前が付けられました。

さえずりとのやりとり
さえずりとのやりとり

ただ、社内ナレッジはGoogleサイトの都合上、手動でデータを定期的に更新しているのですが、社内ナレッジの更新頻度が低いためにある程度許容できた部分でした。
esaに関しては基本的には毎日誰かがいろいろなナレッジを最新化したり、新規記事作成を行っており、更新間隔は短いほうが好ましかったことと、esaそのものにWebhookの機能があったため、更新や新規作成などをリアルタイムに受け取ってDB側も更新できそうだというところで、ナレットよりも少し広がったこのようなシステム構成となりました。

さえずりシステム構成
さえずりシステム構成

やはり、労務などのルールに関する疑問よりも実業務における疑問のほうが発生しやすいこともあり、ナレット以上にさえずりは利用されているのを見かけます。
また、ナレットが任意のチャンネル内でしか利用できないのに対して、さえずりはDMでクローズドに質問ができる点も良いところのため、ナレットも同様にDMで利用できるようにすることを検討しています(DMでのやりとりの際にはログが残らないようにも設定をしています)。

おわりに

GPTプロジェクトではこの1年で他にも、求人@インテリアデザインにおける職務経歴例自動生成機能のリリース(*3)に協力したりと、ChatGPTを活用した試みをいくつか行ってきました。

最近は、Claude 3というLLMがGPT-4を超えているのではないかと話題になっていたり、まだまだこの領域は目まぐるしく進化をしている毎日ですが、プロジェクトチームではChatGPTのみに限らずに多くの生成AIの活用を模索していきたいと考えています。
この1年は小さい改善を中心に検証を行って、社内でのノウハウも少しずつ溜まってきたため、今後はLLMを利用した大きい改善に挑戦していくフェーズになっていきます。
これからも飲食に関わる方々へ、より価値を提供できるような開発を行っていきたいと思います。

Four Keys によるシンクロ・フード開発チームの生産性可視化

はじめまして、シンクロ・フード開発部の眞田です。

今回、シンクロ・フードにおける開発生産性向上活動の一環として開発部 各開発チームのFour Keysを計測しパフォーマンスレベルを比較しました。Four Keysの指標値やデータの分布などを比較していくと各開発チームごとの差異が見られ課題も見えてきましたので、ご紹介しようと思います。

Four Keysの概要

Four Keysは、GoogleのDevOps Research and Assessment(DORA)チームの研究・調査により示されたソフトウェア開発チームのパフォーマンスを表す指標で、4つの指標と指標それぞれに対する4つのレベルで開発チームのパフォーマンスレベルを分類するものです。最近の開発生産性に関する議論ではFour Keysがよく参照されており、さまざまなソフトウェア開発企業でFour Keysが開発生産性の指標として採用されています。

開発チームのパフォーマンスを見るための4つの指標は下記となります。

指標 内容
デプロイ頻度 本番環境への正常なリリースの頻度
変更リードタイム 本番環境稼働への修正着手(First Commit)からリリースまでの所要時間
変更失敗率 本番リリースの内で障害が発生する割合(%)
サービス復元時間 本番リリースで発生した障害から回復するのにかかる時間

上記の各指標の値がどの範囲に含まれるかにより組織・開発チームのパフォーマンスをElite / High / Medium / Lowというレベルに分類します。

Elite High Medium Low
デプロイ頻度 適時(1日複数) 1回/日〜1回/週 1回/月〜1回/6ヶ月 1回 / 6ヶ月以下
変更リードタイム 1時間以内 1日 〜 1週間 1ヶ月 〜 6ヶ月 6ヶ月以上
変更失敗率 0% 〜 15% 16% 〜 30% 16% 〜 30% 16% 〜 30%
サービス復元時間 1時間以内 1日以内 1日〜1週間 6ヶ月以上

Four Keysを用いる理由づけとしては一般的に下記のようなことが言われており、シンクロ・フードでの開発生産性を考えるスタート地点として適切であると判断し、今回Four Keysを計測することとしました。

  • ビジネス面への好影響・関連が示されている
    • Four Keysを提案したレポートにて企業の競争優位性とFour Keysに優れた開発チームを持つことの関連性が確認されているため、開発会社として取り組むべき意味付けや意義がビジネスサイドにも理解してもらいやすい
    • エンジニアにとっても、Four Keysは最近のソフトウェア開発におけるトレンドに沿っているので、改善に取り組むことの意味を理解してもらいやすい
  • 車輪の再発明をしなくて済む
    • 多くの関係者が長期間に携わった(過去 7 年間で3万人を超える世界中の専門家が参加している)成果である
    • Four KeysはアジャイルやDevOpsなど最近のソフトウェア開発に適用しやすい指標である
  • 社内・社外の開発チームとの比較ができる
    • Four Keysのパフォーマンスレベル「Elite」「High」「Medium」「Low」により、他の開発チームと比較して自分達がどのレベルにいるのかを客観的に把握できる
    • それぞれの組織や開発チームに適合する開発生産性指標は様々あると思われますが、まずはFour Keysから始めれば良いのではないかと思われる

今回の計測におけるFour Keys定義

インターネットで調べてもFour Keysの指標値の算出に関する厳密な定義はされておらず、Four Keysを採用する開発企業にて少しずつ異なる定義でFour Keysの計測がされているようです。今回、シンクロ・フードでのFour Keys計測における各指標値の定義は下記としました。(集計期間は2023年4月から2023年12月としています)

指標 定義
変更リードタイム 該当期間に本番リリースされたチケットに紐づく(複数の)PullRequestの中で最も古いCommit(First Commit)のタイムスタンプから本番リリースのタイムスタンプまでの営業日日数
デプロイ頻度 ある週内で、日毎に本番リリースされたチケット件数の中央値
変更失敗率 2023/4 - 2023/12に本番リリースされた全てのチケット件数中でバグ対応と分類されたチケット件数の割合
サービス復元時間 2023/4 - 2023/12に発生したバグ対応チケットについて、修正に要した日数(チケット起票日から本番リリースまでの日数)の中央値
  • ここでの変更失敗率・サービス復元時間はFour Keysとしては参考値として捉えています。 Four Keysの定義に基づくと、該当期間の本番リリースに起因して発生したバグに対する各指標値を算出すべきですが、正確な記録が現状では残っていないためチケット管理システムにバグとして記録されているものから集計できる値を算出しました。

Four Keys計測のためのデータ取得

シンクロ・フード 開発部では、ソースコード管理にはGitHub Enterprise、開発チケット管理には社内で内製した独自システムを使用しており、今回のFour Keysの算出に際してはそれぞれから必要な情報を抜き出し、各指標値を算出しました。

チケット管理システムからの情報取得

チケット管理システムには、チケット登録日、本番環境リリース日などが管理されており、Four Keys計測のためにチケット管理システムからは下記の情報を取得しました。

  • チケットの起票日
  • チケットの本番環境リリース日
  • 対象期間における本番リリースされたチケット数
  • 対象期間にバグとして登録されたチケット数

GitHubからの情報取得

GitHub Enterpriseからは、チケットに紐づけている複数のPullReqestに記録されてるCommitから最も古いものを特定し、そのタイムスタンプをチケットのFirst Commitタイムスタンプとして取得しました。(GitHubからのデータ取り出しにはGitHub API (GraphQL)を用いました)

シンクロ・フードの開発チーム

シンクロ・フードにはサービスの開発・運用保守を行っている開発チームが大きく4チームあり、各チームの概要は下記のようになります。今回のFour Keysの計測では、それぞれのチームそれぞれで指標値を算出しました。

チーム 担当サービス
チームA 求人飲食店ドットコム関連サービスの開発・運用保守
チームB 求人飲食店ドットコムサービスに関連する業務システムの開発・運用保守
チームC 飲食店ドットコムサービスの開発・運用保守
キッチンカーやアパレルカーなど移動販売事業者に、出店場所の提供や出店・運営の支援サービスであるモビマルの開発・運用保守
チームD 店舗デザイン.COMなど飲食店出店時の内装を依頼するデザイン会社を探すサービスの開発・運用保守

シンクロ・フード各開発チームのFour Keysによるパフォーマンス比較

開発チームのFour Keysパフォーマンスレベル

各開発チームのFour Keys指標値を算出した結果は下記となりました。

チーム 変更リードタイム デプロイ頻度 変更失敗率 サービス復旧時間
チームA 9 営業日 1回/日 24% 1 営業日
チームB 5 営業日 1回/日 15% 3 営業日
チームC 7 営業日 3回/日 16% 10 営業日
チームD 7 営業日 1回/日 20% 2 営業日

この各開発チームのFour Keys指標値から判別されたパフォーマンスレベルは、下記のように全開発チームHighとなりました。Four Keysのパフォーマンスレベルをみるだけでは各開発チームの違いが見られませんでした。

Four Keysの定めるEliteパフォーマンスレベル以外の各レベル間の指標値レンジ幅が大きいため、開発チーム間の違いが現れにくくなっているように感じます。加えて、レンジの指標境界に隙間があり、指標値が隙間に位置する場合にはどのレベルかの判断しにくかったです。今回、シンクロ開発チームでも指標値がレベルの隙間に位置する場合もありましたが、下位のパフォーマンスレベル境界値との差を考慮してHighと判断している箇所があります。

チーム 変更リードタイム デプロイ頻度 変更失敗率 サービス復旧時間
チームA High High High High
チームB High High High High
チームC High High High High
チームD High High High High

インターネット上には自社開発チームのFour Keysパフォーマンスを公開されている企業も多くあり、その中にはある指標値に関してはEliteパフォーマンスレベルであったという報告も散見されます。シンクロ・フードはパフォーマンスレベルとしては総じてHighであり悪い結果ではないと考えられますが、まだまだパフォーマンス改善の余地があると感じました。

とはいえ、リードタイムのEliteレベルの定義をみると、修正着手から本番リリースまでの時間が1時間以内となっており、シンクロ・フードの開発フローや開発環境を鑑みるとその実現はあまり現実的ではないと捉えています。このことから、一律にEliteレベルを目指すというのではなく、適宜Four Keysの指標値を改善していくというのがシンクロ・フードとしての適切な改善スタンスだろうという見解になっています。

Foue Keys指標値による比較

パフォーマンスレベルは全ての開発チームでHighレベルとなりましたが、各開発チームでの違いを見てみるためにFour Keys各指標値を直接比較してみました。

各チームのFour Keys指標値(再掲)

チーム 変更リードタイム デプロイ頻度 変更失敗率 サービス復旧時間
チームA 9 営業日 1回/日 24% 1 営業日
チームB 5 営業日 1回/日 15% 3 営業日
チームC 7 営業日 3回/日 16% 10 営業日
チームD 7 営業日 1回/日 20% 2 営業日

Four Keys指標値を横並びで比較すると4チームでの課題が見られます。

チーム Four Keysによる評価
チームA 4チーム中で最もパフォーマンス改善の必要性があるように見える
変更リードタイム・変更失敗率・デプロイ頻度が他のチームの中では低いが、サービス復元平均時間は最良
チームB 4チーム中ではパフォーマンスは良好
変更リードタイム・変更失敗率が4チーム中最良
チームC デプロイ頻度は4チーム中で最良、一方でサービス復元時間については最下位
サービス復元時間についてパフォーマンスが課題と見られる
チームD 4チームの中ではいずれの指標もパフォーマンスとしては中位
ただし、変更失敗率が課題と見られる

より詳細な比較

さらに、Four Keys指標値だけでなく、各開発チームのリードタイム・デプロイ頻度データの分布からチームパフォーマンスを比較しました。

変更リードタイムの度数分布

各開発チームのリードタイムの度数分布は下記のようになりました。横軸は変更リードタイム(営業日)で、縦軸は度数です。変更リードタイム度数分布の評価観点としては、原点付近に度数が集まっている方がパフォーマンスが良好(全体的に変更リードタイムが短い)であると捉えられるため、中央値と80パーセンタイル値がなるべく小さい方がパフォーマンスが良好という評価をしました。

変更リードタイムの度数分布
変更リードタイムの度数分布

デプロイ頻度の週毎の分布

各開発チームのデプロイ頻度の分布は下記のようになりました。横軸は2023年4月から12月までの各週で、縦軸は該当週の各営業日のデプロイ回数の中央値です。評価にはデプロイ頻度の平均値と標準偏差を算出し、平均値がより大きく(デプロイ頻度が多い)、標準偏差の小さい(平均値付近の頻度で安定してデプロイできている)ものをパフォーマンスが良好という評価をしました。

デプロイ頻度(週毎)
デプロイ頻度(週毎)

チーム デプロイ頻度平均値 デプロイ頻度標準偏差
チームA 0.86 0.68
チームB 0.74 0.69
チームC 3.37 1.22
チームD 1.47 0.88

このグラフ分布から変更リードタイム・デプロイ頻度の状況をまとめると下記のようになりました。

チーム 変更リードタイム評価 デプロイ頻度評価
チームA リードタイム2週間以内が全体の半数
1.5ヶ月以上要している要件が10%強
平均的に週0.2〜1.6回程度のデプロイ頻度
チームB リードタイム1週間以内が全体の半数
1.5ヶ月以上要している要件は5%強
平均的に週0〜1.4回程度のデプロイ頻度
チームC リードタイム1週間強が全体の半数
1.5ヶ月以上要している要件は5%
平均的に週2〜4回程度のデプロイ頻度
チームD リードタイム1週間強が全体の半数
1.5ヶ月以上要している要件は10%弱
平均的に週0.6〜2.4回程度のデプロイ頻度

変更リードタイムのチーム比較

変更リードタイムのデータの分布から、チームBの中央値が他のチームと比較して最も良く(80パーセンタイルも2番目に良い) 、次いでチームC、チームD、チームAというパフォーマンス順であるように見えます。

チームBの開発内容としては、比較的規模の大きな案件を定常的に数件開発しつつ細かい機能改修や不具合対応などを多く対応しており、前者はリードタイムが長い傾向にありますが後者のパフォーマンスが良好であることが現れていると考えています。チームAに関しては、サービスとしては機能も多くコードベースも大きいことから、影響範囲も大きくなりがちで改修・テストに時間がかかりリードタイム・デプロイ頻度に影響しているように思われます。チームCとチームDは中央値は同じですが、80パーセンタイルはチームCの方が小さいため、チームDよりもチームCの方パフォーマンスが良いと判断しています。

デプロイ頻度のチーム比較

デプロイ頻度からは、チームCが最もパフォーマンスが良い結果となりました、その他のチームはFour Keys指標値のみでの評価では同じように見られますが、週単位でのデプロイ回数データをみると違いが見られ、パフォーマンス順は、チームC>チームD>チームA>チームBであるように見えます。(各チームの標準偏差は 0 .7〜1.2となっており、データの分布は平均値の±1営業日程度の範囲に収まっている状況であることがわかりました)

チームCは、サービスとしては若く多くの新規機能の追加を優先的・精力的に行なっているためリリース頻度が他のチームと比較して多くなっています。また、各機能の開発規模も小さく、細かいサイクルで開発を行なっているということもデプロイ頻度に影響していると思われます(ちなみに、Four Keysのサービス復旧時間が長く(10営業日)なっており他のチームと比較してパフォーマンスとしては良くありませんが、これは影響度の小さい・緊急度の低い不具合対応よりも新機能開発を優先しているという状況からきています)。チームBは2023年度前半はデプロイのない週も多く見られましたが、10月以降デプロイ頻度が上がってきている様子が伺え、チームAと同等レベルになってきています。

注:この比較では、リードタイム・デプロイ頻度の観点のみでシンクロ開発チームのパフォーマンスレベルを考察しています。FourKeysの残りの2つの指標:変更失敗率・サービス復元時間について、今回算出した指標値はFour Keysの定義からは外れていると思われるため、今回はこれらの指標では評価しませんでした。

まとめ

シンクロ・フード開発部 各開発チームのFour Keysを計測した結果、Four Keysの定義するパフォーマンスレベルとしてはどの開発チームもHighレベルと判断されました。ただ、Four Keysの指標値やデータの分布などを細かく比較していくと、各開発チームごとの状況の違いから差異が見られ、それぞれが異なる課題を持っていることが見えてきたと思います。

この結果に基づいて、今後は下記のような活動を予定しています。これらの改善活動を通じて新しい情報・知見などが得られましたら、またこのブログで紹介したいと思います。

1. Four Keys を開発生産性指標とした改善活動

開発チームにおけるFour Keys を開発生産性指標のうち、まずは変更リードタイム・デプロイ頻度にフォーカスして改善活動の実施していく予定です。

2. 企画チームを巻き込んだ改善活動

シンクロ・フードにおける開発では、Four Keysで計測される指標値は開発チームの作業のみならずサービスの企画を立案しているチームの作業も関わってくるため、開発チーム・企画チームが協調して改善活動をしていく必要があります。変更リードタイムは、企画チームの受入テストの時間・企画チームとのコミュニケーション・チケットの作業規模などにも依存していると考えられるため、企画チームを巻き込んだ改善が必要となると考えています。

3. 各開発チームへの横展開

改善活動はまずは一つの開発チームにて実施していく予定ですが、そこで成果が得られた改善活動は他の開発チームにも横展開していきます。

4. Four Keysを継続的・自動的に集計できる仕組みの整備

今回のFour Keys計測では、継続的・自動的に集計する仕組みは用意していません。開発・企画メンバーが常にFour Keysを収集・閲覧できる仕組みを構築していく予定です。

Gmail メール送信者のガイドラインに対応した話

はじめまして。開発部の髙木です。

今回は Google が去年新たに制定したメール送信者のガイドラインに対応したので、具体的な対応方法とその結果をご紹介したいと思います。
Google が制定したメール送信者のガイドラインについて詳しく知りたい方は、公式の記事を確認していただければ理解しやすいと思います。

弊社のメール事情

弊社は飲食店に関わる多種多様なサービスを展開しています。そのため、それぞれのサービスごとにお客様へ販促・トランザクションメールを送信する必要があります。例えば、飲食店の開業を検討しているお客様におすすめの新規物件をお知らせするメールや、求職者に希望条件とマッチした求人をお知らせするメールのようにユーザーがサービスを有効活用できるメールや、飲食店に関する有益な情報を定期的にメールで送信しています。多いときには一日に数十万通の販促メールを送信することもあります。これだけの数のメールがお客様に届かなくなるとサービスに致命的な影響を与えてしまうため、メール送信者のガイドラインへの対応は迅速に行う必要がありました。

対応すべきメール送信者のガイドライン

上記のメール事情でも述べたように弊社は多くのメールを送信しており、中でも Gmail アカウント宛が約半数を占めます。つまり、メール送信者のガイドラインに記載されている Gmail アカウントに 1 日あたり 5000 件以上のメールを送信する送信者 に該当します。よって、以下に記載するガイドラインへの対応が必須という状況でした。

  • メールの暗号化・認証を適切に設定する
    • ドメインに SPF・DKIM・DMARC 認証を設定する
    • メールの送信に TLS 接続を使用する
  • 迷惑メール率を 0.1 % 未満にする
    • 0.3 % 以上に決してならないようにする
  • 販促メールの購読解除を容易にする
    • ワンクリック購読解除を提供する
    • メール本文に登録解除のリンクをわかりやすく表示する

メールの暗号化・認証

最初にメールの暗号化・認証について説明します。
設定した暗号化・認証はガイドラインに記載されている以下の通りです。要するに、送信元の偽装やメールの改ざんがないこと・認証失敗時の対応を明示していること・安全性の高いメール送信方法を適切に設定したということになります。

  • 暗号化
    • TLS
      • より安全性の高いメール送信を実現するため
  • 認証
    • SPF
      • 送信ドメインの正当性を証明するため
    • DKIM
      • 送信元の詐称やメール内容の改ざんがないことを証明するため
    • DMARC
      • SPF や DKIM の認証が失敗した場合の対応策を示すため

弊社は販促メールとトランザクションメールを送信していますが、それぞれ送信方法が異なります。販促メールは弊社で立てたメールサーバーを利用して送信し、トランザクションメールは ESP( Email Service Provider )である SendGrid を利用してメールを送信しています。そのため、暗号化・認証の設定を行うに当たり、対応漏れがないように注意しました。

加えて、今回は Google Workspace から送信しているメールに対しても DKIM の設定を行っています。弊社では独自ドメインを取得して Gmail を利用しており、Google Workspace の DKIM 設定がデフォルトのままだと DKIM アライメントに失敗してしまいます。SPF は従来から対応済みのため DMARC 認証には Pass しますが、ワンクリック購読解除の提供に合わせて DKIM アライメントにも Pass するよう対応しました。

ワンクリック購読解除の提供

続いてワンクリック購読解除の提供について説明します。 その前に、弊社の販促メールの送信方法について簡単に紹介だけしておきます。メール事情でも述べたのですが、弊社は飲食店に関わる多種多様なサービスを展開しています。これらのサービスごとに販促メールを送信するとなるとメール送信処理が複雑化してしまうので、弊社では「一括メールシステム」というものを作成して販促メール送信に利用しています。以下の図に示すように、一括メールシステムは多数のサービスで作成した販促メールデータを一元管理し、一斉送信するような機能になります。

一括メール配信システム

上記のように、弊社は販促メールの送信に SendGrid や Salesforce などの ESP を利用していません。そのため、ワンクリック購読解除の仕組みを独自に実装する必要がありました。そこで、弊社の販促メール送信フローを考慮した上で、以下のようにワンクリック購読解除を実装しました。

  1. 一括メールシステムの送信処理にワンクリック購読解除の設定を組み込む
  2. ワンクリック購読解除リクエストを受け付ける処理を実装する
  3. ワンクリック購読解除に必要な情報をメールデータに追加する

まずは、一括メールシステムの送信処理にワンクリック購読解除の設定を組み込むことから対応しました。Google の「簡単に登録解除できるようにする」や RFC 8058 に詳しく記載されていますが、ワンクリック購読解除を提供するには以下 2 つのヘッダー情報をメールに設定するだけです。

List-Unsubscribe: <https://example.com/unsubscribe.html?opaque=123456789>
List-Unsubscribe-Post: List-Unsubscribe=One-Click

List-Unsubscribe-Post が設定されている場合のみ List-Unsubscribe に記載されている url やメールアドレスに対して、ワンクリック購読解除リクエストが送信できるリンクがメール内に表示されるようになります。上記の仕様から、ワンクリック購読解除に使用できるメールデータがある場合に限り、一括メールシステムのメール送信時にメールヘッダーとして設定するような実装を行いました。

次に、ワンクリック購読解除リクエストを受け付ける処理を実装しました。再三ですが、弊社は販促メールの送信に ESP を利用していないので、ワンクリック購読解除リクエストを受け付ける機能の実装も独自に行なっています。一括メールシステムはほぼ全サービスの販促メールを一元管理して送信しているため、同じようにワンクリック購読解除リクエストを受け付ける機能も一元管理するように実装しています。具体的には、特定の url をメールデータから構築して解析することで購読解除に必要な情報を抽出して、メールを送信しないお客様として登録するような実装となっています。

最後に、ワンクリック購読解除に必要な情報をメールデータに追加しました。一括メールシステムを利用しているサービスの販促メール登録処理を洗い出し、ワンクリック購読解除に必要な情報をメールデータとして一括メールシステムへ渡せるように実装しています。

ワンクリック購読解除

まとめると、「サービス」と「一括メールシステム」でワンクリック購読解除を提供するための基盤を実装したことになります。そのため、お客様にワンクリック購読解除を提供するだけでなく、新規で販促メールを登録するような場合でも容易にワンクリック購読解除を適用できる環境の構築まで完了しています。

Gmail メール送信者のガイドラインに対応した結果

Gmail アカウント宛のメール配信状況は Google が提供する Postmaster Tools を使うことで確認できます。
この Postmaster Tools 上で確認できる情報や個別に集計した情報を用いて、メールの暗号化・認証、ワンクリック購読解除を提供した結果を共有します。

メールの暗号化・認証は今回のメール送信者のガイドラインが規定される前からある程度対応していたこともあり、迅速に対応することができました。そのため、PostmasterTools を確認すると 2023-11 月末から常に認証率 100 % となっております。もちろん、TLS による暗号化も同時期から常に 100 % の使用率です。

TLSによる暗号化

また、ドメインレピュテーションも向上しています。

ドメインレピュテーション

続いてワンクリック購読解除を提供した結果を、迷惑メール率の平均値推移・ワンクリック購読解除リクエスト数を指標として共有します。

迷惑メール率

ワンクリック購読解除を提供する以前は、弊社の迷惑メール率の月間平均値は 0.25 % 前後でした。これは月間平均値なので 0.3 % を超過することはありませんが、日単位で確認すると 0.4 % などの 0.3 % を超過する日もありました。ただ、販促メールに対するワンクリック購読解除を提供開始した 2024-02 月以降は迷惑メール率の月間平均値が 0.05 % 程度と著しく低下しており、メール送信者のガイドラインを満たすことができています。日単位で確認しても 0.0 % か 0.1 % の日が大半を占めており、0.2 % 以上になったのは一日だけでした。これは、実際にワンクリック購読解除のリクエスト数が 2024-02 月だけで 3500 件程度あることからも、販促メールを受信していただいているお客様が迷惑メールとして報告するのではなく、購読解除していただけていると捉えることができます。

ワンクリック購読解除リクエスト数

では、実際に送信されたワンクリック購読解除リクエストの推移を確認してみます。ワンクリック購読解除リクエストを送信するためには Google が提供しているメール内リンクを押下する必要があります。しかし、こちらのリンクは今までワンクリック購読解除を提供していない送信者の場合、Google の判断で表示制限が設けられてしまいます。そのため、提供開始直後はワンクリック購読解除のリンクが表示されるケースが少ない = リクエスト数が少ない( 迷惑メールとして報告される )状態であると考えられます。現状、時間の経過とともにワンクリック購読解除リクエスト数は増加傾向にあるため、ワンクリック購読解除のリンクが多くの受信者で表示されるようになっているのだと捉えています。このワンクリック購読解除のリンクがすべての受信者で表示されるようになれば、さらに迷惑メール率を低下させることが可能になり、0.1 % 未満を維持することができる想定です。

まとめ

期日までに Google が新たに制定したメール送信者のガイドラインに対応することができました。暗号化・認証はすでに設定していたものが多く、迅速に対応することができました。ワンクリック購読解除は全サービスの販促メールを網羅できるように実装しました。また、今後追加される販促メールに対しても容易にワンクリック購読解除を提供できる基盤を作ることもできました。これらの対応によって PostmasterTools で報告される平均迷惑メール率は 0.25 % 程度から 0.05 % 程度へと減少させることができました。

また、今回の対応は弊社の販促メール送信フローを再確認する良い機会だったと捉えています。今までルール化されていなかった曖昧な販促メール送信フローを見直し、全サービスの販促メール送信フローの基盤を構築できたと考えています。

現段階でもメール送信者のガイドラインに対応できてはいます。しかし、日毎の迷惑メール率が 0.1 % となる場合があること・販促メールがトランザクションメールにも影響してしまうことを考慮して、販促メール専用のドメイン分割を試みています。また、迷惑メール率が一定値以上になった場合に通知するような監視体制を整えている段階です。こちらについては、対応完了後に別記事として掲載できればと思います。