MLOps基盤のフルマネージド化に向けたVertex AI Pipelinesへの移行

こんにちは。エンジニアの竹田です。
BUYMAの検索システムやMLOps基盤の開発・運用を担当しております。

こちらはEnigmo Advent Calendar 2023の21日目の記事です 🎄

弊社では2021年頃よりMLOps基盤をGoogle Cloud PlatformのAI Platform Pipelines上に構築して開発・運用を行っています。
この度、Vertex AI Pipelinesへの移行を全面的に進めることになりましたので、ご紹介も兼ねて記事にしたいと思います。

背景

2024/07にAI Platform Pipelinesが非推奨になるという通知を受けたことがきっかけです。
AI Platform Pipelines deprecations  |  Google Cloud

非推奨の通知が移行を開始するトリガーではあったものの、かねてからGKEクラスタの運用をどうにかできないかなと考えていました。
Vertex AI Pipelinesへの移行によりフルマネージド化できるのは大きなモチベーションとなっています。

移行における課題

この機会にKubeflow Pipelines SDK(以下、kfp) v1からv2へのアップグレードを進めています。
移行ドキュメントも提供されているため、それほど苦労はしないものと考えておりました。
が、結果としてこの選択が多くの苦労を抱えることになってしまいました 😢
実際に kfp v2 利用してみて、良かった点・苦労している点を交えてご紹介いたします。

※下位互換で動作させることも可能でしたが、kfp v1での機能拡張は行われない、そのうち書き換えが必要になる、という点を考慮してkfp v2への書き換えに踏み切りました。

kfp v1からv2へ

kfpは、kubernetes上で機械学習パイプラインを動作させるためのツールキットです。
コードベースはPythonです。
An introduction to Kubeflow

パイプラインを作成してVertex AI Pipelines上で動作させると、動作フローが視覚的に分かりやすく表現されます。

Pythonで記述したコードをコンパイルして利用する性質上、kfp v1の頃からかなりクセが強いなとは感じていました。
実際に利用してみた上での良い点、苦労している点を列挙し、所感を書いていこうと思います。

kfp v2の利用バージョン

# pip list | grep kfp
kfp                              2.3.0
kfp-pipeline-spec                0.2.2
kfp-server-api                   2.0.3

kfp v2の良い点

  • 入出力に利用するInput[xxx] / Output[xxx] が便利
  • ParallelForによる並列処理の結果をCollectedで受け取れるようになった
  • @dsl.componentset_accelerator_typeが直感的
入出力に利用するInput[xxx] / Output[xxx] が便利

kfp v2では入出力に利用するオブジェクトがこの形(基本的に Input[Artifact] / Output[Artifact] の利用)にほぼ統一されており、GCS上のパスを意識せず利用できるため非常に便利です。
https://www.kubeflow.org/docs/components/pipelines/v2/data-types/artifacts/

ParallelForによる並列処理の結果をCollectedで受け取れるようになった

kfp v1でもParallelForは利用できましたが、fan-in(複数の入力を一つにまとめること)が厄介でした。
kfp v2では最近になってCollectedが利用可能となり、ParallelForの後に呼び出すことで結果をリスト形式でfan-inできるため、コードの可読性も飛躍的に向上します。

@dsl.componentset_accelerator_typeが直感的

個人的な好みの部類かもしれませんが、kfp v2は定義周りが直感的になった印象があります。
https://www.kubeflow.org/docs/components/pipelines/v2/migration/#create_component_from_func-and-func_to_container_op-support

kfp v2で苦労している点

  • 変数展開がおかしくなることがある
  • 型指定の厳密化により、何を渡せばよいのか分からなくなることがある
変数展開がおかしくなることがある

kfp v2への移行で最も困っている点です。以下のようなissueも挙がっています。
https://github.com/kubeflow/pipelines/issues/10261
変数内に何らかの文字列や数値を入れているはずが、実際に利用する場合に以下のような展開がされてしまいます。
{{channel:task=;name=g;type=String;}}
コンポーネントの出力結果をうまく展開できない場合は以下のような内容です。 {{channel:task=term-calc;name=list_date;type=typing.List[str];}}
機械学習の初回実行プロセスの多くがBigQueryからのデータ取得を行っており、データ取得期間や特徴量を変数で管理しているため、既存のパイプラインコードではPythonのformatメソッドによる書式変換を多用しています。
この書式変換のほとんどが正常に動かなくなってしまい、試行錯誤を繰り返すことになってしまいました。
以下、期待した変数展開とならずにエラーとなるパターンの一部です。

パイプライン引数やコンポーネントの返却結果をdictに加えてコンポーネントに渡した場合

@dsl.component
def convert_str(tmpl: str, value: dict, output: Output[Artifact]):
    with open(output.path, "w") as f:
        f.write(tmpl.format(value))
  
@dsl.pipeline(name="test", description="test prediction")
def test_pipeline(table_name: str = "sample"):
    value = {
        "table_name": table_name
    }
    sql = "SELECT * FROM {0[table_name]}"
    convert_str_op = convert_str(tmpl=sql, value=value)

コンパイル時のエラー内容

ValueError: Value must be one of the following types: str, int, float, bool, dict, and list. Got: "{{channel:task=;name=table_name;type=String;}}" of type "<class 'kfp.dsl.pipeline_channel.PipelineParameterChannel'>".

パイプライン引数をパイプライン本体で利用しようとした場合

@dsl.pipeline(name="test", description="test prediction")
def test_pipeline(periods: str = "1m"):
    target_file = f"periods_{periods}.yaml"

    with open(target_file, mode="r") as f:
        periods_conf = yaml.safe_load(f)

コンパイル時のエラー内容

FileNotFoundError: [Errno 2] No such file or directory: 'periods_{{channel:task=;name=periods;type=String;}}.yaml'
型指定の厳密化により、何を渡せばよいのか分からなくなることがある

パイプライン引数を利用しようとして以下のようなエラーが出たり

TypeError: PipelineParameterChannel is not a parameter type.

特定コンポーネントにinputを渡した場合に以下のようなエラーが出たり

ValueError: Constant argument inputs must be one of type ['String', 'Integer', 'Float', 'Boolean', 'List', 'Dict'] Got: <kfp.dsl.pipeline_task.PipelineTask object at 0x7f8a03f89880> of type <class 'kfp.dsl.pipeline_task.PipelineTask'>.

といったことが割と発生します。
自分としては正しい型での引き渡し、および参照をしているつもりのため、どう対処してよいか分からなくなることが多いです。
事前にキャストすることで正常に動作することもあれば、そもそもデータ型の扱いを見直す必要があったりします。

所感

上述の苦労している点での引っ掛かりが多いのが難点で、残念ながら使いやすさは感じられていません。
ですが、一度形としてできてしまえばテンプレート化できると思われるため試行錯誤しながら進めている、というのが現状です。

コンテナイメージ化しているコンポーネント内部の挙動はほぼ変更なしで動作しており、ほとんどが「変数展開がおかしくなる」部分の障壁により思うように進捗していないといった状態です。
具体的にこうすれば良い、といったアプローチが見つけられたら何かしらの形で記事にできればと考えております。

引き続き、Vertex AI Pipelines移行による機械学習基盤のフルマネージド化を目指して邁進していく所存です 💪

おわりに

明日の記事の担当はインフラチームの福田さんです。EKS周りのお話です。お楽しみに!!


株式会社エニグモ すべての求人一覧

hrmos.co

外部キー制約が使えない場合のRailsの実装方法

こんにちは、エンジニアの川本です。
主にBUYMAの決済・配送を担当しているチームでバックエンドの開発をしています。

この記事は Enigmo Advent Calendar 2023 の 20 日目の記事です。

個人開発でPlanetScaleというMySQL互換のサーバーレスデータベースを使用しているのですが、特筆すべき仕様として外部キーのサポートがありません。

planetscale.com

外部キー制約はDBレベルで強い整合性を担保できる便利な手段ですが、PlanetScaleではその機能が利用できないので、アプリケーションレベルで整合性を担保する必要があります。

MySQLの外部キーのオプションにはいくつか種類がありますが、これらが使えない場合にアプリケーション側ではどのように担保すればよいのでしょうか?

今回は、Railsを例にしてアプリケーション側でMySQLの外部キーに相当する機能をどのように担保できるのかを検証してみようと思います。

余談:PlanetScaleについて

最近PlanetScaleはベータ版で外部キーをサポートし始めましたが、残念ながらHobbyプランではまだサポートされておりません。

PlanetScaleの基盤であるVitessはOnline DDLの機能を提供しており、それが原因で外部キーのサポートが長らく難しかったようです。

以下のドキュメントやブログには、PlanetScaleが外部キーをサポートできるようになるまでの背景や課題、そしてその克服方法についての詳細な情報が記載されています。興味がある方はぜひ読んでみてください。

外部キーのサポートが難しかった理由

外部キーをサポートするための取り組み

親子関係のテーブルを作成

まず親子関係にある、Parent, Childテーブルを作成してサンプルデータを入れる。

-- テーブル作成
mysql> CREATE TABLE parent (
    ->     id INT NOT NULL,
    ->     PRIMARY KEY (id)
    -> ) ENGINE=INNODB;
mysql> CREATE TABLE child (
    ->     id INT NOT NULL,
    ->     parent_id INT NOT NULL,
    ->     PRIMARY KEY (id)
    -> ) ENGINE=INNODB;
    
-- テストデータをインサート
mysql> INSERT INTO parent (id) VALUES (1), (2);
mysql> INSERT INTO child (id, parent_id) VALUES (1, 1), (2, 1), (3, 2);

-- データ構造の確認
mysql> SELECT * FROM parent p JOIN child c ON p.id = c.parent_id;
+----+----+-----------+
| id | id | parent_id |
+----+----+-----------+
|  1 |  1 |         1 |
|  1 |  2 |         1 |
|  2 |  3 |         2 |
+----+----+-----------+

MySQLの外部キー制約

MySQLでは以下4つのON DELETE副次句で指定できる参照アクションがあります。 ON UPDATE副次句もありますが、今回はON DELETEに限定することにします。

dev.mysql.com

ON DELETE CASCADE

親テーブルから行を削除し、子テーブル内の一致する行を自動的に削除する。

-- ON DELETE CASCADEを指定して外部キー制約を設定
mysql> ALTER TABLE child
    -> ADD CONSTRAINT fk_parent
    -> FOREIGN KEY (parent_id)
    -> REFERENCES parent(id)
    -> ON DELETE CASCADE;

-- parentのid = 1のレコードを削除する
mysql> DELETE FROM parent WHERE id = 1;

-- parent_id = 1のchildのレコードも削除されていることを確認できる
mysql> SELECT * FROM child;
+----+-----------+
| id | parent_id |
+----+-----------+
|  3 |         2 |
+----+-----------+

ON DELETE SET NULL

親テーブルから行を削除し、子テーブルの外部キーカラムをNULLにする。
※ この設定をするときは、childparent_idNOT NULLにしない。

-- ON DELETE SET NULLを指定して外部キー制約を設定
mysql> ALTER TABLE child
    -> ADD CONSTRAINT fk_parent
    -> FOREIGN KEY (parent_id)
    -> REFERENCES parent(id)
    -> ON DELETE SET NULL;

-- parentのid = 1のレコードを削除する
mysql> DELETE FROM parent WHERE id = 1;

-- parent_id = 1のchildのレコードのparent_idはNULLになっていることを確認
mysql> SELECT * FROM child;
+----+-----------+
| id | parent_id |
+----+-----------+
|  1 |      NULL |
|  2 |      NULL |
|  3 |         2 |
+----+-----------+

ON DELETE RESTRICT or ON DELETE NO ACTION or 指定なし

親テーブルに対する削除操作は拒否されます。また、ON DELETE RESTRICT or ON DELETE NO ACTION or ON DELETE 指定なしは同じ挙動になります。以下の例ではON DELETE 指定なしで例を示します。

-- ON DELETE指定なしで外部キー制約を設定
mysql> ALTER TABLE child
    -> ADD CONSTRAINT fk_parent
    -> FOREIGN KEY (parent_id)
    -> REFERENCES parent(id)
    
-- parentのid = 1のレコードを削除する
-- childにはparent_id = 1のレコードがあるので削除拒否される
mysql> DELETE FROM parent WHERE id = 1;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`myapp_development`.`child`, CONSTRAINT `fk_parent` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`))

-- parentもchildも削除されていない
mysql> SELECT * FROM parent p JOIN child c ON p.id = c.parent_id;
+----+----+-----------+
| id | id | parent_id |
+----+----+-----------+
|  1 |  1 |         1 |
|  1 |  2 |         1 |
|  2 |  3 |         2 |
+----+----+-----------+

Rails側の実装方法

Railsでは、Active Recordのdependentオプションを使用して、MySQLの外部キー制約に相当する機能を実現できます。

dependentオプションは親レコードに対してActiveRecord::Persistence#destroyが実行されたときに、紐ずいている子レコードに対して実行されるメソッドのことです。

ON DELETE CASCADE

ON DELETE CASCADEに相当することは、delete_all, destory, destory_asyncのいずれかで実現することができます。これら3つは全て最終的に実現できることは同じですが、それぞれで以下のように挙動の違いがあります。

delete_all

delete_allは、parentに関連付けられたchildが一括で1つのSQLで削除します。

また、childに対してActiveRecord::Persistence#deleteが実行されるので、ActiveRecord::Persistence#destroy実行時に作用するbefore_destroyやafter_destroyといったコールバックや孫クラスのdependentオプションが実行されません。

そのため、単純に削除SQLを実行するだけなので関連するchildが多い場合にはdestroyよりパフォーマンスが向上する可能性があリます。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :delete_all
end
irb(main):002> parent = Parent.find(1)
irb(main):054> parent.destroy
  TRANSACTION (0.7ms)  BEGIN
  Child Delete All (1.2ms)  DELETE FROM `child` WHERE `child`.`parent_id` = 1
  Parent Destroy (0.7ms)  DELETE FROM `parent` WHERE `parent`.`id` = 1
  TRANSACTION (1.9ms)  COMMIT
=> #<Parent:0x0000ffffaf032050 id: 1>

destroy

destroyは、parentに紐づくchildを全て取得して1件ずつ削除します。

ActiveRecord::Persistence#destroyが実行されるため、before_destroyやafter_destroyなどのコールバックも実行され、孫クラスにあるdependentオプションも実行されます。

そのため、関連するchildが多いと発行されるSQLも増え、コールバックの実行や孫クラスのdependentオプションの実行が多くなり、delete_allよりもパフォーマンスが低下する可能性があります。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :destroy
end
irb(main):002> parent = Parent.find(1)
irb(main):062> parent.destroy
  TRANSACTION (0.3ms)  BEGIN
  Child Load (1.0ms)  SELECT `child`.* FROM `child` WHERE `child`.`parent_id` = 1
  Child Destroy (1.3ms)  DELETE FROM `child` WHERE `child`.`id` = 1
  Child Destroy (1.1ms)  DELETE FROM `child` WHERE `child`.`id` = 2
  Parent Destroy (0.9ms)  DELETE FROM `parent` WHERE `parent`.`id` = 1
  TRANSACTION (1.2ms)  COMMIT
=> #<Parent:0x0000ffffafdad888 id: 1>

destroy_async

destroy_asyncは、parentに関連する全てのchildを非同期で1件ずつ削除します。

紐づくchildが非常に多く、即時での削除を求められない場合に有効です。紐づくchildが多いと処理が最悪の場合はタイムアウトする可能性もあります。そのような場合、まずparentを削除してクライアントにレスポンスを速やかに返し、残りの紐づくchildは非同期で削除することで問題を解決できます。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :destroy_async
end
irb(main):002> parent = Parent.find(1)
irb(main):070> parent.destroy
  TRANSACTION (0.3ms)  BEGIN
  Child Load (0.8ms)  SELECT `child`.* FROM `child` WHERE `child`.`parent_id` = 1
  Parent Destroy (0.8ms)  DELETE FROM `parent` WHERE `parent`.`id` = 1
  TRANSACTION (2.0ms)  COMMIT
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: 63fc4528-934a-405c-9311-7bee9fb706b1) to Async(default) with arguments: {:owner_model_name=>"Parent", :owner_id=>1, :association_class=>"Child", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
=> #<Parent:0x0000ffffae761700 id: 1>
irb(main):071> Performing ActiveRecord::DestroyAssociationAsyncJob (Job ID: 63fc4528-934a-405c-9311-7bee9fb706b1) from Async(default) enqueued at 2023-12-16T09:16:43Z with arguments: {:owner_model_name=>"Parent", :owner_id=>1, :association_class=>"Child", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
  Parent Load (3.0ms)  SELECT `parent`.* FROM `parent` WHERE `parent`.`id` = 1 LIMIT 1
  Child Load (5.1ms)  SELECT `child`.* FROM `child` WHERE `child`.`id` IN (1, 2) ORDER BY `child`.`id` ASC LIMIT 1000
  TRANSACTION (0.3ms)  BEGIN
  Child Destroy (0.9ms)  DELETE FROM `child` WHERE `child`.`id` = 1
  TRANSACTION (2.0ms)  COMMIT
  TRANSACTION (0.3ms)  BEGIN
  Child Destroy (1.0ms)  DELETE FROM `child` WHERE `child`.`id` = 2
  TRANSACTION (1.7ms)  COMMIT
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: 63fc4528-934a-405c-9311-7bee9fb706b1) from Async(default) in 63.81ms

ON DELETE SET NULL

nullify

ON DELETE SET NULLに相当することはnullifyで実現できます。

parentに紐づくchildのparent_idをnullに更新して、parentを削除しています。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :nullify
end
irb(main):088> parent = Parent.find(1)
irb(main):090> parent.destroy
  TRANSACTION (0.3ms)  BEGIN
  Child Update All (5.0ms)  UPDATE `child` SET `child`.`parent_id` = NULL WHERE `child`.`parent_id` = 1
  Parent Destroy (3.3ms)  DELETE FROM `parent` WHERE `parent`.`id` = 1
  TRANSACTION (1.3ms)  COMMIT
=> #<Parent:0x0000ffffae66f9a0 id: 1>

ON DELETE RESTRICT or ON DELETE NO ACTION or 指定なし

ON DELETE RESTRICTまたはON DELETE NO ACTIONに相当することは、 restrict_with_exceptionまたはrestrict_with_errorのいずれかで実現することができます。これら2つは全て最終的に実現できることは同じですが、それぞれで以下のように挙動の違いがあります。

restrict_with_exception

parentに紐づくchildが存在することを確認して、処理をロールバックしてActiveRecord::DeleteRestrictionErrorという例外を発生させます。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :restrict_with_exception
end
irb(main):088> parent = Parent.find(1)
irb(main):094> parent.destroy
  TRANSACTION (0.6ms)  BEGIN
  Child Exists? (1.0ms)  SELECT 1 AS one FROM `child` WHERE `child`.`parent_id` = 1 LIMIT 1
  TRANSACTION (0.5ms)  ROLLBACK
/usr/local/bundle/gems/activerecord-7.0.8/lib/active_record/associations/has_many_association.rb:16:in `handle_dependency': Cannot delete record because of dependent child (ActiveRecord::DeleteRestrictionError)

restrict_with_error

parentに紐づくchildが存在することを確認して、処理をロールバックしてfalseを返します。

class Parent < ApplicationRecord
  self.table_name = 'parent'
  has_many :child, dependent: :restrict_with_error
end
irb(main):088> parent = Parent.find(1)
irb(main):098> parent.destroy
  TRANSACTION (0.5ms)  BEGIN
  Child Exists? (0.6ms)  SELECT 1 AS one FROM `child` WHERE `child`.`parent_id` = 1 LIMIT 1
  TRANSACTION (0.4ms)  ROLLBACK
=> false

最後に

ここまでの紹介で、RailsアプリケーションでMySQLの外部キー制約の参照アクションを実現する手段が理解できました。

ただし、データ整合性が担保されるのは、外部キー制約に準拠したアプリケーションからの実行時に限られます。もし、同じDBを参照するが外部キー制約に準拠していないアプリケーションが存在する場合、どのような影響が生じますでしょうか?

外部キー制約のないアプリケーションからの実行により、データ整合性が維持されなくなる可能性があります。このような事態を避けるためには、できるだけDBレベルで整合性を担保する方が望ましいです。

Planet Scaleのような外部キー制約をサポートしていないDBでは、今回紹介したようなアプリケーションの実装が有効であるかもしれません。しかし、外部キー制約がサポートされているDBでは、DBレベルでの制御が安全であると言えるでしょう。

明日の記事担当はデータエンジニアリングチームです!お楽しみに!

株式会社エニグモ すべての求人一覧

hrmos.co

エニグモにおける開発生産性分析の取り組み

こんにちは、サービスエンジニアリング本部の平井です。

こちらはEnigmo Advent Calendar 202320日目の記事です。

私は、エンジニア部門で取り組んでいる開発生産性分析について紹介します。

開発生産性分析を試みた経緯

現在、エニグモでは開発組織体制の変更、メンバー増強など様々な組織強化を目指した動きが加速してきています。ただ、そのような施策が開発組織のパフォーマンスを向上させているのか定量的な指標で測ることができませんでした。 また、開発組織としては、開発を通して一定のスピードでユーザーに十分な価値を届ける責任を負っているものの、開発スピードを測る良い方法がありませんでした。 このように組織パフォーマンス向上施策の結果を確認するため、開発組織としての開発スピードを図るために開発生産性分析の取り組みが始まりました。

また、この取り組みは開発生産性に興味がある有志のメンバーが集まり進めていきました。

指標選定

分析する指標としては、Googleが提案しているFour Keysを参考にしました。 Four Keysを参考にした理由は以下になります。

  • 開発スピードを測る指標が含まれている。
  • 自分達で計測、分析の基盤を整えられそう。
  • 一般的な指標である。

そして、 指標の中で速度に関わる指標であり、計測が容易そうな変更のリードタイムデプロイ頻度を計測することが決まりました。 デプロイ頻度に関しては、営業日や開発メンバーの増減による影響を少なくするために、 @hiroki_daichiさんが紹介されていたd/d/dというやり方で 1日あたりの1開発者あたりのデプロイ回数を計算することにしました。

また、現在BUYMAは基幹システムと複数のマイクロサービスで構成されていますが、どの開発チームも修正することが多い基幹システムにおけるこれらの指標を計測していくことになりました。

各指標の定義

前提として、BUYMAの基幹システムはGitlabをホスティングサービスとして利用しています。 本番環境へのデプロイは内製アプリケーションを通して行われ、開発者が各々デプロイ依頼を作成します。 本番環境へのデプロイプロセスは1日3回実行されるため、各開発者は作成した依頼をそのどれかのプロセスに乗せて本番化します。

Four Keysを参考にしつつ、このようなBUYMAの特徴を考慮して各指標の定義をチームで相談して決めました。

その上でリードタイムは以下の定義で計算しています。

MR内の最初のコミットからそのMRが本番環境に反映されるまでの時間

各開発者によって開発手法は異なるため最初のコミットタイミングが微妙にずれる可能性はありますが、 開発 -> レビュー -> QA -> デプロイという一連の開発プロセスを計測できるためこのような定義にしました。

d/d/dに関しては、デプロイ回数/営業日/開発者数となっていて、それぞれ以下のような定義になっています。

デプロイ回数: 本番化された本番化依頼の数

BUYMAの本番化の特徴として、1デプロイに複数の修正が含まれるためその一つ一つの修正を個別に数えたかったためこのような定義にしました。

営業日: 土日祝日を抜いた平日

営業日は特に一般的に使われているものです。

開発者数: マージされたMRに参加したユニークGitLabユーザー数

エンジニアリング部門に属していても「基幹システムに関わっていない人は含めたくない」、「過去の特定期間の開発者数も出したい」など考慮する点が多く難しかったのですが、「MRに関わった人数は開発に関わる人数と等しいだろう」という考えのもとチームで相談してこのような定義になりました。

データ収集、可視化の仕組み

次に、指標の計測と可視化の仕組みについて説明します。

Airflowにワークフローを構築して、MRに関わる情報をGitLabのAPIから収集しBigQueryに格納しています。 デプロイに関する情報は内製本番化アプリケーションが利用しているデータベースに永続化されているため、BigQueryに連携してGitLabの情報とJOINできるようにしました。 可視化に関しては、BigQuery上のレコードをSQLで加工してLookerのダッシュボードを使うことで実現しています。

開発性生産性分析システム構成図

リードタイムに必要なデータ収集

リードタイムを計測するために以下のGitLab APIを利用しています。 基本的にはAPIのレスポンスから日次の差分データを取得して、そのままBigQueryのテーブルに保存しています。

List all projects API

GitLabのプロジェクトのマスターデータを収集するために利用しています。 収集したデータはLookerで可視化する際にプロジェクト名を表示するなどに利用しています。

List group merge requests API

MRデータを収集するために利用しています。 マージ済みMRの情報のみ必要なのでAPIパラメータを使ってマージ済みMRの情報のみ収集しています。

Get single merge request commits API

MRに紐づくコミットの情報を収集するために利用しています。 ここで取得したデータを利用してMRに紐づく最初のコミットを特定して、リードタイムを計算します。

d/d/d に必要なデータ収集

リードタイムを計測するために以下のGitLab APIを利用しています。 こちらも日次の差分データをそのままBigQueryのテーブルに保存しています。

List a Project’s visible events API

GitLabのイベント情報を収集するために利用しています。 マージされたMRに参加したユニークGitLabユーザー数を計算する際にここで収集したデータを利用しています。

可視化について

先述したように可視化にはLookerのダッシュボードを利用しています。 LookMLSQLを組み立てて指標を計算しています。 エニグモではBUYMAをメインの機能で分割して開発チームを組織していて、それらをドメインチームと呼んでいます。指標の監視や分析は各ドメインチームにやってもらっているため各ドメインチーム毎にダッシュボードを作成しています。

可視化したあるチームのリードタイム

運用について

指標の監視や分析は、細かいやり方を指定せずにドメインチームに依頼しています。 例えば私が所属しているチームでは毎週の振り返り時にリードタイムとd/d/dを確認して、何か問題があれば改善案を考えるという運用をしています。 また、開発生産性指標の計測にあたり、 開発者にMRへのラベル付与を依頼しました。MRに付与されたラベル情報をもとにドメインチーム毎のリードタイムを計測しています。

今後の課題

今後の課題としては以下になります。

  • BUYMA基幹システム以外でまだデータ収集できていないマイクロサービスがあるため正確に開発組織全体の生産性を測れていない。
  • 要件定義、仕様決定などのディスカバリーフェーズにかかっている時間を図れていない。
  • 変更障害率、サービス復元時間など安定性の指標を図れてない。
    • 現状だとリードタイムやd/d/dが向上した際に、それが障害発生による細かい修正が増えたのが原因なのかわからない。

終わりに

今回はエンジニア部門で取り組んでいる開発生産性分析について紹介しました。自分自身、データ収集処理の開発、可視化を担当し、自分達の開発活動が定量データとして表現される面白さを感じました。 最後までご覧頂きありがとうございました。明日の記事の担当は検索エンジニアの竹田さんです。お楽しみに。

現在、エニグモではこのような開発生産性に関わる取り組みを行っています。 興味のある方は以下の求人をご参照ください!!

株式会社エニグモ すべての求人一覧

hrmos.co

マルチカルチャーな職場におけるコミュニケーション

こんにちは、グローバルチームのエンジニアのFernandです。

この記事はEnigmo Advent Calendar 2023の19日目の記事です。私は、15年近くフルスタックエンジニアとして働いてきました。さまざまなプロジェクトに関わってきましたが、、新しいチームに参加するたびに、技術的なスキルだけでなく、特にソフトスキルについても新しく学ぶことが多くあります。今回は、職場のミーティングで外国人として直面した課題を共有したいと思います。

ある日、ITエンジニアの妻が彼に頼んで、「スーパーで牛乳を1パック買ってきて。もし卵があれば1ダース買ってきて」と言いました。そこでエンジニアは買い物に行き、言われた通りに行動し、帰宅しました。手には牛乳パックが12個ありました。
妻は驚愕しました。何が間違いだったのでしょうか?


プロジェクト・マネジメント協会(PMI)の報告によると、ビジネス目標を達成しないプロジェクトの半数以上は、効果的なコミュニケーションの欠如によるものとされています。実際に、不十分なコミュニケーションにより、1500億円を投じたプロジェクトの場合には約100億円がリスクとなるのです。ビジネスでは、12個の牛乳パックが大きな損害を生み出してしまうかもしれないのです。
コミュニケーションの齟齬による経済的リスク
ソース: ©2013 Project Management Institute, Inc. Pulse of the Profession In-Depth Report: The High Cost of Low Performance: The Essential Role of Communications, May 2013. PMI.org/Pulse
私が日本の企業で働き始めた当初、さまざまなチームとのコミュニケーションに苦労しました。私と同じように、日本に住む外国人の多くが直面する経験です。
外国人が日本で働くうえでぶつかる壁(言語の壁が半数以上)
ソース:NikkeiAsia
私が最初に入社したスタートアップ企業では、お客様の満足度が最も重要であり、彼らを神様のように対応すべきということを教わりました。そのため、ミーティングに参加する際、私は静かに座り、より理解するために一層耳を傾ける必要がありました。私は日本語に自信がなかったため、質問をすることや間違いを犯すことを恐れていました。自分が話すことで、クライアントから私たちの会社がネガティブに評価されるのではないかという不安がありました。
しかしそれでも、各ミーティングの後に反省点を見直して、気づいたことを改善してから自信を持って質問をするようにしました。そして、効果的なコミュニケーションを大事にする日本のチームに参加する機会を得たおかげで、私はミーティング中に質問をする勇気を持ち、プロジェクトやクライアントの要件をより理解できるようになりました。日本人の同僚は忍耐と理解力をもって複雑な概念を簡素化するために時間をかけて説明を繰り返してくれました。
しかし、言語の壁は、誤解を招く障害となることもあることも私は理解しています。それにより、会話の流れが乱れ、自分の意見を正確に表現することが難しくなります。思いを正確に表現することへの取り組みは、話す人と聞く人の双方にとっても苦悩の源となっています。できるだけこれを避けるために、事前に読み込んで準備し、質問をまとめて整理することが大事です。
外国人のメンバーがミーティングをする場合、一人だけに発言の責任が押し付けられないように、協力し合いながら取り組みます。通常、そこでは、生産的に結果を出すために、話の途中で割り込むことが歓迎され、許容されます。曖昧さを解消するために、アイデアを効果的に共有して質問をするこの戦略は時に混沌としたものになりますが、コミュニケーションの理解を促進し、より協力的で生産的な働き方を促進するのに非常に効果的だと感じています。誤解や心配事が生じた際に即座にそれらを明確化することは、コミュニケーションの質を向上させ、より協力的かつ生産的な職場環境を生み出します。参加者全員が議論に貢献することを促すことで、創造性が高まり、アイデアが高められ、より豊かでダイナミックな成果が生まれます。
しかし、文化的な習慣やエチケットにも配慮することが重要です、特に話し手が日本人の場合です。このような場合、割り込むことは失礼とされ、質問はミーティングの最後まで取っておくことが通例です。そういったミーティングでは、私はより受動的なアプローチを取る傾向があります。ただし、質問や懸念事項を早急に解決させたい場合、日本語の能力に自信がなくても話すように努めています。日本語の能力に自信がなくても、ただ聞くだけに頼るのではなく、積極的な参加こそが助けを求めて理解を深める唯一の方法であることを心に留めています。言語の壁を乗り越え、異文化間のコミュニケーションの複雑さをより効果的に乗り越えるためには、日本語の勉強を続けることが重要です。
私は現在、さまざまな国のメンバーから成るチームに所属しています。私たちの主要なコミュニケーション手段は日本語ですが、必要に応じて英語も使用しています。さまざまな時差の課題にも取り組んでおり、私たちのマネージャーはその違いがミーティングのスケジュールに影響しないよう柔軟に対応してくださいます。彼女は流暢な英語と日本語を活かし、私たちの質問や懸念事項をサポートし、チーム内の円滑かつ効果的なコミュニケーションを容易にしてくれます。私は定期的に他のチームとも交流しなければなりません。日本語を使ってのコミュニケーションは難しいですが、貴重な機会であり、コミュニケーションスキルを実践し向上させるチャンスでもあります。しかし、通常業務に追われて、学習に費やす時間の確保がなかなか難しくなっています。プライベートと仕事、そして言語学習のバランスを取ることは大変な努力を要します。まるでジャグリングをするようなもので、集中して言語の練習だけに時間を割くことはなかなか難しいものです。
言語の不完全さや課題があっても、ミーティングでの同僚たちはそれを判断することはありません。むしろ、言語の壁にとらわれず、積極的に参加して貢献する姿勢は評価されるでしょう。しかし、質問をする際には、ただ質問を投げる、自己アピールのためにするということは避けるべきです。そのような行動は、ビジネス上での評判に悪影響を及ぼすだけでなく、私たちが維持しようとしている協力的な雰囲気にも支障をきたす可能性があります。
さらに、会議を進行する際は、コミュニケーション能力や自信のレベルに関係なく、全参加者が積極的に参加できる包括的で支援的な雰囲気を育むことも重要です。間違った質問や関連性のない質問をする人がいても批判するべきではないでしょう。そのような場合に他者を批判すると、プロジェクトが始まる前から失敗したも同然です。前述したように、効果的なコミュニケーションが生産性を向上させるという認識はとても重要です。研究によれば、効果的なコミュニケーションは生産性を最大25%向上させることができます。最終的には、プロジェクトの成功は効果的かつ効率的なコミュニケーション能力にかかっています。卵と牛乳の個数を間違えることもないことでしょう。不確実な点が生じた場合には、積極的に話して明確化することが重要です。積極的なリスナーであり、共感を持って接することを心がけましょう。英語や日本語の流暢さだけに焦点を合わせるのではなく、彼らの質問やアイデアの価値がプロジェクトを前進させ成功させる上でとても重要です。

(English version)
Communication in Workplace

An IT engineer was asked by his wife to go to the store and buy a carton of milk, and if there are eggs, buy a dozen. So the engineer goes shopping, does as she says, and returns home with 12 milk cartons.

Oh boy , But what went wrong?


According to the Project Management Institute (PMI) report, over half of projects that fail to meet business goals can be attributed to ineffective communication. In fact, poor communication puts approximately $75 million at risk for every $1 billion spent on projects. Well, it seems that one dozen cartons of milk can potentially result in a substantial financial loss.

The amount at risk for every US$1 billion spent on a project.
Source: ©2013 Project Management Institute, Inc. Pulse of the Profession In-Depth Report: The High Cost of Low Performance: The Essential Role of Communications, May 2013. PMI.org/Pulse
When I first started my journey working at Japanese companies, I often struggled to communicate effectively with various teams. Unfortunately, this burden is not unique to me but a shared experience among many foreign residents in Japan.
Barriers foreigners encounter when working in Japan. Language barrier is the most prominent obstacle.
Source:NikkeiAsia
Working in startup companies, I quickly learned that client satisfaction was held in the highest regard, often considering them as king or god (o-kyaku-sama wa kamisama desu) whose every request must be fulfilled. Therefore, when I joined meetings, I had to sit quietly and listen harder to understand the discussions. I feared asking questions and making mistakes because of my Japanese language proficiency. My company might be judged by the client whenever I attempt to speak up. I was able to manage this by researching sample implementations after each meeting and creating prototypes before confidently raising a question.
Then, I had the opportunity to join a Japanese team that fostered effective communication. Encouraged by my supportive environment, I mustered the courage to ask questions during meetings, improving my comprehension of projects and clients' requirements. My Japanese colleagues displayed immense patience and understanding, taking the time to reiterate their explanations and simplify complex concepts, ensuring everyone understood the information.
But don't get me wrong, despite the positive environment, the language barrier often presents obstacles that could lead to misunderstandings and misinterpretations. It disrupts the conversation flow and makes it challenging to express oneself fully. The struggle to articulate thoughts accurately has become a source of frustration for both the speaker and the listener. In order to avoid this as much as possible, I read and prepared beforehand, collating and organizing my questions.
In situations where the speaker is a non-Japanese teammate, I collaborated, ensuring that the onus of speaking does not solely rest on one person. Usually, in meetings that consist of non-Japanese participants, interruptions are welcomed and accepted to foster a productive environment where ideas can be effectively shared, questions can be asked, and clarification can be provided. While this strategy may seem chaotic at times, I find that it is incredibly effective in facilitating comprehension and generating deeper insights. Obtaining immediate clarity on any misunderstandings or concerns enhances the quality of communication and promotes a more collaborative and productive work environment. By establishing a culture where all participants are encouraged to contribute to the discussion, creativity surges, and ideas are elevated, creating a richer and more dynamic outcome.
However, I am also mindful of cultural norms and etiquette, particularly when the speaker is Japanese. In these situations, it would be considered rude to interrupt, and it is customary to reserve questions until the end of the meeting. I tend to adopt a more passive approach during such meetings. Nevertheless, if I do have pressing clarifications or concerns, I try to speak up regardless of my lack of confidence in my Japanese proficiency. I remind myself that the only way to seek assistance and gain a better understanding is by actively participating in the conversation rather than solely relying on attentive listening. However, continuing to study Japanese remains paramount in overcoming these language barriers and navigating the intricacies of cross-cultural communication more effectively.
Currently, I am fortunate to be a part of a diverse team with members from different parts of the world. Although our primary mode of communication is Japanese, we also embrace the use of English when needed. Despite the challenges of various time zones, our manager has done an exceptional job ensuring that these differences do not impede our meeting schedules. With her fluency in English and Japanese, she adeptly accommodates our questions and concerns, facilitating smooth and effective communication within the team. I also have to interact with other teams periodically. Communicating using the Japanese language is challenging, but it also gives me valuable exposure and the chance to practice and enhance my communication skills. Though I have to admit that the demanding nature of daily life and work commitments has restricted the amount of time available for study. Balancing work, personal responsibilities and language learning can be a juggling act, making it quite challenging to allocate uninterrupted time for focused language practice.
Ultimately, the success of our projects hinges on our ability to communicate efficiently and effectively. If uncertainty arises, it is crucial to speak up and seek clarification. Rest assured that in meetings, colleagues will not judge any language imperfections or challenges you may encounter while expressing yourself in Japanese or English. Instead, your willingness to participate and contribute despite any linguistic limitations will be appreciated and acknowledged. However, it is also essential to avoid asking questions solely for the sake of asking or to showcase your skills. Engaging in such behavior may have a detrimental impact on your professional reputation and hinder the collaborative atmosphere we strive to maintain.
In addition, it is equally important for the meeting host to take accountability in fostering an inclusive and supportive meeting environment that encourages active participation from all attendees, regardless of their communication skills or confidence levels. It is critical to refrain from criticizing individuals who may ask questions that might seem incorrect or irrelevant. Criticizing others in such instances, the project had already failed even before it started. Be an active listener and practice empathy. Fluency in English or Japanese should not be the sole focus; instead, the value of their questions and ideas truly matters in moving the project forward and achieving success.

明日の記事の担当はSellチームの平井さんです。お楽しみに。


株式会社エニグモ すべての求人一覧

hrmos.co

細かいことの組み合わせで日々の開発を快適にする

こんにちは、iOSエンジニアの池田です。

この記事は Enigmo Advent Calendar 2023 の 18 日目の記事です。 この記事では担当のプロジェクトにおいて日々の開発を快適にするために実施している細々したことについて紹介します。

私はBUYMAiOSアプリ開発を担当しています。 iOSアプリ開発に限らずプロジェクトを進める中では、コアタスク(ここではiOSアプリの開発作業と定義)とノンコアタスク(コアタスク以外の作業)が発生します。

開発の効率を上げるため、開発者の開発の快適性を上げるためにノンコアタスクは極力削っていきたいものです。 ここではノンコアタスクの中でもチケット管理、Pull Requestの管理に関わる部分について触れていければと思います。

現在のプロジェクト環境


現在の担当プロジェクト内では、チケット管理にJIRA、開発用のプラットフォームにはGitHubを利用しているので、それらに関わる内容について触れていきます。

また、コミュニケーションツールとしてSlackを利用しているため、こちらとの連携も少し触れます。

JIRA


自動化

JIRAのチケット管理では、各チケットの開始日や終了日、担当者、チケットの作業状況など様々な情報の日々の更新が必要になってきます。

一つ一つの作業は作業時間も短く単純作業ではあるのですが、数が多くなると作業時間ももちろん累積されますし、何にも考えずにできるわけではないため、じわじわ地味に体力を削られます。

そういった作業を減らし、少しでも快適性を上げるためにはJIRAの「自動化」が有効です。

以下は私が担当するプロジェクトで設定している「自動化」のルール一覧です。

様々設定されていますが、この中でも「プルリクエストがマージされる → 課題を完了にする」が結構気に入っています。

Pull Request のコードレビューを受けた後自分でマージする運用をしている場合、このルールが設定されていないと、以下のような手順が必要になります。

  1. ブラウザでGitHubを開く
  2. GitHubの画面上でPull Requestの一覧を開く
  3. 該当のPull Requestを開く
  4. Pull Requestをマージする
  5. ブラウザでJIRAを開く
  6. JIRAのボード等でチケットを完了にトランジションさせる

これらの手順の中で、4.から 5.では別サービスへの切り替えが必要で、6.の作業ではドラッグ&ドロップかオプションメニューからの選択が必要だったりで、キーボードからマウスへの操作の切り替えも余分に必要になります。

4.で作業に満足して 5.以降忘れることもあり、JIRAのカンバン管理者から完了/未完了の問い合わせが来たとしたらコミュニケーションコストもかかります。

これだけでも意外と積み重ねるとコストになるので、設定することによる快適度は上がっている感じがします。

Gitブランチ作成

もう一つJIRAの機能で細かいけどよく使っている機能がチケットのブランチ作成機能です。

以下チケットの画面キャプチャです。

チケットの「開発 > ブランチを作成 > GITで新しいブランチを作成してチェックアウト」の欄で、チケットIDが入ったブランチ名を自動生成してくれます。

また、入力欄の右側のボタンでクリップボードにコピーできるため、CLIでブランチ作成の際にコピー&ペーストでブランチを作成することができます。

こちらを利用することでブランチ名を考える手間や、CLIで入力する手間を省くことができます。

名前を考えるのって決めの問題なのですが、意外と考えるコストがかかる作業だと思っているので個人的にはすごく助かっています。

GitHub


PULL_REQUEST_TEMPLATE.md

Pull Request を作成して開発されている場合、設定しているリポジトリが多いかと思います。

本プロジェクトでは、以下のようなトピックをフォーマット化して記載するようになっており、記載時の手間を少し減らしています。

  • 関連チケット
  • Pull Request でやったこと
  • 懸念点・注意点
  • 相談事項

また、JIRAとGitHubが連携されるようになっているため、チケットのURLを記載するとJIRAのチケットにGitHubのPull Requestが連動するようになっています。

Slack


SlackとJIRA、SlackとGitHubも連携されており、チケットやPull Requestの条件に応じた更新があったタイミングで通知が来るようになっています。

JIRAの通知

JIRAの通知設定は以下のようになっており、コメントやステータスの更新等通知が来るように設定されています。

GitHubの通知

GitHubの通知設定は以下のようになっており、コメントの見落としが少なくなるようにcomments:'channel'の設定をしているところが個別に設定した部分だったかと思います。

まとめ


チケット管理、Pull Requestの管理に関わる部分についてノンコアタスクを削減するため細々やっていることをご紹介しました。

プロジェクトの環境によって合う、合わない部分があったりしますが、見て頂いた方の何かのご参考になれば幸いです。

日々の変化している状況や、気づいたちょっとした不要な手間など、アンテナを高くしつつさらなる改善、日々の開発の快適度を上げていければと思います。

株式会社エニグモ すべての求人一覧

hrmos.co

enigmo(BUYMA運営企業)のコーポレートIT(社内SE・情シス)運営方法と将来像

こんにちは、コーポレートエンジニア(コーポレートIT[CO-IT]チーム) の 横川 です。 この記事は Enigmo Advent Calendar 2023 の 17 日目の記事です。

この記事では社内ITサービスを支えるチームの組織作りをテーマにどのような観点でチーム運営を行っているかをご紹介したいと思います。

コーポレートIT領域は、年々取り扱うサービスや技術が広くなり、様々なスキルセットが要求されてきていますが、本記事は特定技術の話ではなく組織運営にフォーカスした内容となります。 これから社内SEを目指される方や現在コーポレートエンジニアとして活躍されている方々に少しでも参考にしていただける内容となっていれば幸いです。

はじめに

自己紹介とCO-ITチームの紹介

私はITベンダーでネットワークエンジニアとしてキャリアをスタートし、その後セキュリティ製品の代理店にてプリセールスエンジニアを担当してきました。その後、キャリアチェンジし、デベロッパーでの社内SE(情シス)、SaaS提供企業でのコーポレートIT担当を経て、昨年10月よりenigmoにjoinしました。 コーポレートITという大きな領域全体を俯瞰して自社の最適なコーポレートITを自由に作ることができるという環境に惹かれて入社を決断しており、実際にそのミッションに日々向き合える環境で働いています。

CO-ITチームはコーポレートオペレーション本部の人事総務グループに属し、社内ITサービス全般を担当しています。 ブログ執筆時点(2023年12月)では、マネージャ[人事総務グループ部長]を除くとチームメンバーは私を含め3名で現在4人目のエンジニアを募集している状態です。 最終的な意思決定はマネージャが担いますが、当社のコーポレートIT領域の対応は現在私がリードする役割を担っています。

コーポレートエンジニア・社内SE・情シスについて

近年、コーポレートエンジニアやコーポレートITといったワードを見かけることも多くなってきており、従来の社内SEや情シスという表現との明確な違いを説明するWebページ等も増えてきているかと思います。 ざっくり両者の違いは、「見る範囲・視点の違い」とされていることが多く、コーポレートエンジニアは従来の社内SEや情シスのスコープよりも広範囲に及ぶ(企業全体を視野に入れ、ITスキルを活用して提案・企画を行う)と定義されています。*1

コーポレートエンジニアはバックオフィス部門に所属し、会社の売上に直接貢献することはできません(プロダクト部門メンバーの業務効率向上に資することで間接的に貢献する)。企業によっては、プロダクト側に強い優遇(力が強い傾向)があったり、バックオフィス側は利益を生まない部門として不遇な環境になっている場合も見聞きしますが、プロダクト側・バックオフィス側どちらかに優劣があってはならないし、どちらも(会社の成長に貢献する部門として)リスペクトし合える企業風土が醸成できていることが望ましいと感じます。*2

そして、シンプルにコーポレートIT部門が会社から求められることを表現すれば、

自社に最適なコーポレートITをいかに低コスト・最速で実現し、安定稼働を継続できるか

になります。

予算が無限であれば、何も考えず最新で優れたサービスをフルライセンスで導入し、外部の専門ベンダーに要件定義〜運用まで全て外注して、トラブル対応や今後のアップデートも含めて24h365dでフルサポートされるオプションを付帯すれば他社を圧倒するコーポレートITの運用が可能となります。*3しかしながら、自社の社内ITサービスに実プロダクト以上の予算を割ける企業がどれだけあるでしょうか?この点にコーポレートエンジニアの存在意義があるものと考えられます。コーポレートエンジニアは、しっかり自社の状況を俯瞰して会社のフェーズに合わせて、必要十分なサービス提供に日々向き合うことが求められる職種です。

1.組織運営の前提を整理する

私自身が実際に実施していることを含め、組織運営にあたりクリアすべきと考えていることについて記載していきます。

ミッションを理解する

以下にMVVを掲載します。当社のミッションは こちら の通りですが、CompanyのMVVに応じてCO-ITチームでもMVVを定義しています。

MVV(ミッション・ビジョン・バリュー)
このMVVは、私が入社前から定義されていたものですが、現時点で変更の必要性はないと判断し、そのまま継続利用しています。 近年、特にスタートアップ企業では、MVVを掲げている企業が多くなっている印象を受けますが、会社側がただ掲げているというよりは、従業員がそのMVVを自分事として意識するようにMVVに向き合う時間を業務時間の中に組み込んでいる企業も増えてきているのではないでしょうか。MVVというのは、非常に重要であり、各自の担当タスクは全てMVVに紐づいたものと考えることができます。

会社の方針と現在のフェーズをすり合わせる

ワーディングは違っても、多くの企業のコーポレートIT部門のMVVは当社のものと重なる部分が多いのではないでしょうか。 それは企業活動における基本的な役割が同じであることに起因しますが、MVVが同じでも各企業・担当者の業務内容は例えば下記のような理由によって大きく異なります。

  • 会社の規模や方針によって対応範囲やチーム構成が様々である
  • 企業規模の拡大や事業成長に伴って縦割りが進み、サービスや製品毎にチームや担当者が限定されている
  • 要件定義だけ・設計構築だけ・運用だけのようにスコープが限定されている

したがって、具体的なチームビルディングに移る前にまず現在のフェーズを整理し、組織運営方針についての合意形成が必要になります。その大前提として、社内ITサービスを外注するのか内製するのかの判断を行います。一昔前までは専門のエンジニアが担当せず総務担当者が兼務や片手間で社内システム(情シス)を担当することも多かったように感じますが、昨今では求められるスキルや要件の複雑化によってそういった体制では対応しきれなくなってきているのではないでしょうか。事業規模の比較的小さな企業やスタートアップ企業などプロダクト側にフルコミットする必要がある場合は、社内ITサービスの外注化を行う場合もあるかと思いますが、以下のようなリスクがあるため慎重な判断が求められます。

  • 顧客伴走型と謳っているサービスであっても顧客視点の小回りがきくサービスにはなりにくく、他企業共通のテンプレ利用による対応となるリスク
  • システム化や仕組み化のベースとなるプラットフォームを顧客側ではなく発注先ベンダー側環境で構築され、顧客側では関知できないリスク
  • 設計や設定、ナレッジ自体が顧客側に蓄積されず基本的にブラックボックス化してしまうリスク
  • 各種リスクを解決するような契約を試みた場合、自社で専門のエンジニアを採用するよりコストが高くなるリスク
  • 発注先都合で突然サービス終了となり、自社の社内ITサービスが停止してしまうリスク

また、社内ITサービスというのは、自社独自の要件を年々積み重ねて構築していく要素も大きいため、例えばIDaaS製品の設計だけ、社内NWの構築だけを切り出すのではなく、社内ITサービス全体のバランスや連携も視野に入れた高い視座で最適なサービス運営を行う必要があります。加えて、各種サービスの発注先ベンダー等との価格交渉や要件すり合わせについても一定の知識が必要になりますので、その意味でも自社で専門のエンジニアを採用して運営していくことは重要であると言えます。

CO-ITチームは、私が入社する直前まで暫定的にマネージャ自身がプレイヤーを兼務し、多くの実務は(ヘルプデスク業務を外注できるような)外部サービスを利用することで社内ITサービスを維持管理している状態でしたが、私は業務状況を確認した上でチーム運営を再設計するフェーズ(初期フェーズ)と判断しました。なお、マネージャとは定期的に「As is」/「To be」について大枠の目線合わせを行っています。*4

プロダクト、スコープ、役割を整理する

大枠の方針がフィックスした後は、少しずつ具体化していきます。まず、プロダクトとスコープの定義です。 一般的には、事業活動において顧客に提供するものをプロダクト(当社の場合、BUYMAというサービス)と呼び、コーポレートITがプロダクト?と違和感を感じられるかもしれませんが、顧客(従業員)に対してサービスを提供する部門である以上当然プロダクトがあって良いと思いますし、意識すべきと考えています。そして、そのプロダクトを提供するためのスコープの定義も併せて必要です。

プロダクト/Job Titleとスコープのマッピング

上記がCO-ITチームのプロダクトと大枠のスコープになります。スコープは大項目のみ表記していますが、例えば今回のような「techブログへの投稿」業務は「ブランディング貢献」の中に含まれるという感じです。 また、スコープは視座や技術的な難易度等々によってピラミッド型になるので、メンバーの役割の大雑把なプロットも行っています。*5

メンバー構成(チームの総リソース)を整理する

ここまででチームが目指す方向性が見えてきたので、次に具体的なメンバー構成や必要リソースの整理に移ります。 なお、この項目については、各企業の人員計画・予算・業績・既存のメンバー構成等々複雑な要素を踏まえた経営判断になるかと思います。 結果だけの記載となりますが、CO-ITチームは、4名体制を目指すべく現在4人目のエンジニアを募集している状態です。

まとめますと、CO-ITチームは以下のように年間の投下リソースで表現されるValue(アウトプット)が会社から求められる期待値をアウトパフォームすることを意識しなければなりません。

チーム(総リソース[年間約7,680h*6])のValue > 会社からCO-ITチームへの期待値

裁量と責任、働き方

採用業務を行っているとよく質問を受ける項目ですので、ここで触れたいと思います。 CO-ITチームは、各担当者毎にスコープを限定するようなことはなく、基本的に各自のミッションサイズ(会社が各自に期待すること)に応じてコーポレートIT領域全てに携わる方針としております。*7 ミッションサイズの中で最大限の裁量と責任を持っていただき、自由に業務時間を使うことで最大のValueを発揮いただくのが最良という考えに基づいています。

また、当社はオフィスワーク・リモートワークの選択も一定の裁量が許容されていますので、CO-ITチームでもその範囲内で自由選択としています。 基本的にオフィスワークが必要な業務(デバイスのキッティング等)を中心に行うメンバーは出社頻度が高くなりますが、リモートワークすることでオフィスワークする以上のアウトプットが出せるのであればリモートワークの頻度が高くても良いという方針です。なお、業務スコープの特性上(ITサービスデスクを担当しているため)、必ず一人以上のチームメンバーがオフィスワーク(本社に出社)することで物理的なインシデントやユーザサポート対応を可能にしています。

2.現状を俯瞰する

チーム運営の前提の整理が終わった後は、現状の把握に移ります。

課題を整理する

サービスを整理する

プロダクトを理解した上で顧客に提供するサービスの整理が必要です。近年のコーポレートIT領域はオンプレからクラウド化の流れが急速に進んでいるため、SaaSのことばかりに目が行きがちですが、従来の社内NW運用業務をはじめ、企業によってはオフィスファシリティやセキュリティ領域も含まれます。 また、SaaSによっては他のバックオフィス部門(経理等)やプロダクト側が慣例的に運用しているサービスもあり、各サービス毎で主管している部門は企業によって差異があり、正確な把握が必要です。

コストを整理する

主管するサービスが洗い出せた後は、具体的なコスト感の把握です。 自チームが提供するサービスのコスト感がパッと答えられないというのは問題外であり、そのコスト感に見合うメリットを会社にもたらしているのかを常に意識する必要があります。 また、SaaSであったとしても、単純にライセンス費用や月額利用料といった費用だけでなく、導入から日々の維持管理に必要な人的リソースのコスト(1人日の単価を具体的に設定した上で年間何人日、何人月の工数が掛かりどのような金額になるのかまで想定)を含めて該当サービスのコスト妥当性が判断される必要があります。*8

CO-ITチームでは、下記のような主管サービスを網羅した一覧を作成・運用しており、提供サービスの総合計を従業員数で割ったサービスコストの算出を行っています。 この一覧には、サービスのコストをはじめ、サポート窓口の情報や稟議・請求書対応、更新のタイミング等々を含め、サービス別に串刺しして比較でき、これだけを確認すれば運用に必要な情報が取得できるように一元管理しています。

提供サービス一覧

なお、現時点では見える化したところまでしか到達しておらず、コストの最適値はどのくらいなのか、今のコストは妥当なのかという分析や評価を実施するフェーズには至っておりません。 近い将来、このコストを一つのパラメータとして適切なサービス導入のガイドラインを策定することを見据えております。

手続き・運用を整理する

次に見積・稟議・発注・請求のような一連のサービス導入手続き、導入後の実運用について把握します。実現したいことは各企業大体同じであるにも関わらず、企業によって利用しているワークフローは様々であり、実現するまでに理解すべきことのボリュームに大きな違いがあります。

会議コストを考え、会議体を整理する

ファシリテーションや会議に関するビジネス書で数多く述べられていますが、「会議コストは非常に大きなもので意味のある会議をすべき(無駄な会議は廃止すべき)である」といった主旨のトピックが記載されています。

例えば、4人のエンジニアが1hのMTGをする場合、そのコストは超ざっくり¥25,000程度になります。*9ですので、その1hのMTGのアウトプットに会社はその金額を払うことになるので、その期待値を超えるValueが必要になります。特に会議は複数人で行うものなので、各自が単独で行うTodoよりも大きな人件費が計上されてしまうことになります。また、とりあえず参加するだけで仕事をした気になってしまうので注意が必要です(会議に参加すること自体にValueはなく、その会議でどのようなアウトプットを出したかが重要)。

ということで、CO-ITチームでは必要な会議体を整理し、チームのフェーズに合わせた会議運営を行っています。

情報共有・ナレッジ化を意識する

まだまだ続きますが、続いてはチームレベルのナレッジマネジメントです。一人情シスのような状態でなければ、基本的に業務を進めていくとチームメンバーへの具体的な手順共有は当然として、各種設計指針やそもそもの要件定義内容等々、チーム独自のナレッジを複数人で共有するシーンが多々あるかと思います。ナレッジ共有を全く行わない(何もかも属人化)のような組織はないと思いますが、共有の仕方に課題感が残っている組織は少なくないかと思います。CO-ITチームでは以下のポイントを実行し運用しています。

  • 共有の仕方を定義する
  • 共有すべき粒度を定義する
  • ナレッジ化に惜しみなくリソースを投下する

タスクを整理する、バックログ化する

そして、最後にタスクの整理です。PJ管理やタスク管理を実施するためのツールや方法は様々なので省略しますが、当社では私が入社前の段階からAsanaを利用していたため一旦そのまま継続利用しています。タスク整理にあたり、ポイントとしたことは、以下の通りです。

  • 必ず各タスクの目的を明確化する
    • 不明瞭な(誰も自分事としていない)タスクが管理ツール上に表示されていない状態
    • マネージャは意思決定のみを担当(タスク担当者にマネージャをプロットしない)
  • 全体を同じ箱の中に含め他メンバーの業務状況も見える化する*10
    • スポット、割り込みタスクが入ってもチーム全体のタスク一覧に起票する
    • 個人のローカルTodoで処理しない、オープンにする
    • 基本的に起票する(よほど数分でクローズするようなタスクは例外)

本来であれば、アジャイルの考え方に沿って、チーム全体でスクラムを用いてスプリント毎に同じMissionに向かって組織運営(バックログ消化)を行いたいのですが、CO-ITチームではまだまだそういったフェーズにないと判断し、現状はバックログ化することまでに留まっています。

CO-ITチームの課題

以上の課題整理を行った結果、CO-ITチームでは以下のような課題が見えてきました。 既にある程度クリアできてきているものもありますが、日々最適化すべく現在も業務推進中です。 何をどのように解決したかの話は、また機会があればご紹介させていただきます。

  • 資産管理の精度が低い、管理できていないものが存在する
  • サービスのスコープ、責任分界点が不明瞭
  • ナレッジが点在している、不足している、陳腐化している
  • ブラックボックス化(実務の外注サービス依存にも起因)、属人化している
  • 業務分析できる基盤が整っていない(定量化できていない)
  • 意思決定した背景・経緯・ポイントが体系化されていない
  • 中長期的な視点で要件定義・設計・設定が行われていない
  • 暫定対応でタスククローズした内容のまま形骸化している
  • ユーザ要望を単純に解決することにフォーカスされている

3.将来を想像する

前項の通り、課題は山積みであったため、各課題に対してできる範囲(新規サービスの導入やシステム化・インテグレーションは後回し)で将来を見据えた改善を行う方針としました。

見える化定量化に向けた(KPI・KGI策定を見据えた)足固め

冒頭で記載したようにコーポレートIT領域というのは取り扱うサービスや技術が広く、様々なスキルセットが求められると言えますが、個々のPJやタスクサイズは小さいことが多々あります。 しかしながら、タスクサイズが小さいからと言って、個々のタスクを全て点で捉えて個別にクローズし続けるといつまで経っても組織運営が最適化されません。 この観点において、CO-ITチームでは将来の対応リソースの削減も見据え、タスクを定量化することに注力しています。

見える化定量化は多くの企業で実施されているかと思いますが、その粒度や精度については様々かと思います。 CO-ITチームも私が入社する前から全くできていないということではなく、粒度や精度に課題があったという話になります。

私が特に重要視しているのは提供サービス全体での体系化と最小限のリソースを目指す運用サイクルの構築になります。 この観点で言えば、CO-ITチームではまだまだ足固めが必要な状態であり、新規サービスの導入やSaaS間のインテグレーションを考えるフェーズにないと判断しました。 ですので、CO-ITチームではこの一年立ち止まって当たり前のことを実現できる最低限の準備を進めてきました(導入済みサービスはリプレース検討フェーズに移らず基本的に一旦継続利用して精査する)。その中のほんの一例が下記になります。

  • 資産管理の精緻化
  • 問い合わせ対応のワークフロー改善、見える化定量
  • 提供デバイスのラインナップ整理とユーザへのガイドライン提供
  • キッティング業務のワークフロー化
  • 備品貸与ガイドラインの策定
  • バイス提供スピードの向上、提供コストの削減
  • 入退社復職休職のような人事イベントに伴うアカウントフローの整理
  • 社内手続き・稟議の整理
  • サービスの保守期限の統一、サポート窓口の整理
  • ナレッジ共有方法の統一、ナレッジの質・量の向上

業務改善の話(SaaSのインテグレーションだけが全てじゃない!)

採用業務で様々なエンジニアの方とお話する機会があるのですが、コーポレートエンジニアを目指されている方には「業務改善」にやりがいや面白さを強く感じられていて、プログラミングを行って自動化処理の仕組みを作ることが全て(若干誇張していますが・・・)という志向をお持ちである方(そういった印象を受ける方)にお会いすることがあります。確かにコーポレートエンジニアの業務の中でSaaSのインテグレーションというのは、より技術的な要素が強く自身の満足につながる、かつ他の方(特に非エンジニア職の方)からの評価が高くなる傾向がありますので合理的ではあります。

しかしながら、一度立ち止まって以下の質問に向き合うことも必要ではないでしょうか。

  • 本当にシステム化、自動化することだけが正なのでしょうか?
  • 本当に自動化することでコストは下がっていますでしょうか?

参考:運用自動化、不都合な真実

CO-ITチームでは、SaaS運用だけでなく全てのコーポレートIT領域に含まれるサービスにおいて業務改善を行うことが必要であり、実現する方法(コードを書くことが全てではない)に優劣はなく、アウトプットに対する評価は常にフラットでありたいと考えています。例えば、CO-ITチーム内の一例を紹介させていただきますが、以下のような対応も十分一つの業務改善であり、同等に評価されるものと考えています。

業務改善の一例

  • 左側の写真:会議室のモニタスタンドを壁寄せモニタで可動式に変更
    • レイアウト変更を容易にするという要件の充足と机上スペースの拡張を実現
  • 右側の写真:管理するサーバルーム内の棚に共通の収納ボックスを導入
    • 備品管理を最適化(必要な備品に最短でアクセスできる導線の確保とコストの最適化)

サービス導入・施策を開始することについて考える

CO-ITチームではこの一年立ち止まって足固めをしているため主だった新規サービスの導入は見送っている(他にもっとやるべきことが多い)のですが、新規サービスの導入(リプレース含む)業務に魅力を感じるエンジニアの方は多いのではないでしょうか。逆に既存のブラックボックス・複雑化した環境を考慮して体系的に再整理するような業務は(やりたくないとは口には出さないものの)敬遠される傾向が多いように感じます。

この傾向は、自身の直接的なスキルアップや評価へのつながりやすさに起因するものと思います。加えて、ゼロベースでの設計難易度に比べ、複雑な既存環境の再設計は難易度が非常に高く、ボリュームが大きくなる割にはアウトプットが(新規サービス導入に比較すれば)地味になります。また、実際に稼働しているサービスの場合が多く、放置してもスポットでは課題感が多少あるかもしれないが何とかやり過ごせることが多いことも要因の一つにあるように感じます。

ビジネス書(amazonのすごい会議)の中にもまさに同じような視点のコラムが掲載されていましたのでご紹介させていただきます。 PDCAサイクルにおいてAmazon社ではPDだけの人(立ち上げ屋[企画業務])は重視されないが、日本企業ではPDが重視されがちというものです。まさにコーポレートIT領域でも同じように感じます(下記のような状態です)。

  • サービス導入することにだけ大きなコスト・アテンションが払われる*11
  • 導入担当者の主観・感覚のままローンチされることがある
  • 肝心の設計・設定・運用はレビューが薄い*12
  • 問題が顕在化した後に再設計を検討する(再設計の中心人物がヒーローになる。。。) *13

CO-ITチームでは、近い将来新規サービスの導入を必要に応じて行いますが、上記のような課題が残らないようなPJ運営を行うつもりですし、一部の声の大きなメンバー*14だけが新規サービス導入に携わるような組織にならないよう最適な仕組みづくりを日々模索していきます。

チームの安定した運営を考える

チーム運営を安定化するポイントとしては、システム化(人に依存しない運用を構築する)やリソースの安定化(メンバーの頻繁な入れ替わりを防ぐ)といったことが挙げられます。前者については各所で述べられているように感じますが、後者はそれに比較すると情報量が少ない印象です。ここでは後者について深掘りします。

各種サービスのシステム化・自動化が非常に重要であることは自明ですが、それ以上にリソースの安定化は非常に重要です。以下にチームメンバー入れ替わりに関するデメリットを整理してみます。

  • 一時的にリソース不足に陥る
  • 新規メンバーの採用コストが必要となる
  • 新メンバーの立ち上がりに1ヶ月〜半年程度を見込む必要がある

では、逆にメリットがないかと言えばそうではなく、以下のようなチームに新陳代謝が起こるメリットはあります。

  • 旧メンバーより更に良い人材がチームに加入するかもしれない
  • これまで慢性化や形骸化していた業務に良い影響を与えるかもしれない

しかしながら、安定したリソースでの運用を目指す方がチームとしての総合戦闘力は高いと感じますし、各自のスキルアップやチームの新陳代謝はチーム運営方法によりカバーできるものと考えますので、メンバーの不要な入れ替わりは避けたいものです。メンバーが離脱する理由を考えると、

  1. 待遇面(給与やワークライフバランス[会社の規定])の問題
  2. 会社や部署方針との根本的な価値観の不一致
  3. 会社の業績、業種の市場動向
  4. ジョブチェンジを目指す、またはプライベートな問題
  5. 部署内でのコミュニケーションの問題
  6. 志向の問題(自社ではスキルアップが見込めないと感じる)
  7. 足元の担当業務の問題(同じ業務ばかり、丸投げばかりされる、不公平感を感じる)

といったところでしょうか。 この中で1-4まではチーム運営ではどうすることもできないものになりますが、5-7を理由にメンバーの入れ替わりが発生するというのは、当該チームのマネージャやリード担当者に責任があると感じますし、こういった理由が発生しないよう常に改善を続ける必要があります。CO-ITチームでは、将来的なチームの安定運営を目指すために日々 カイゼン に向き合っています(具体的な話は機会があれば・・・)。

終わりに

今回は具体的なサービスの設計や設定内容、サンプルコード等のご紹介にフォーカスできませんでしたが、CO-ITチームの将来に向けた足固めは着々と進みつつあります。 来年以降ようやく新規サービス導入や施策などのコーポレートITサービス拡充をスコープとしていくフェーズを見込んでおりますので、(個人的には)ようやく楽しい業務にリソースを大幅に割けるタイミングとなります。

2023年12月現在、一緒にコーポレートITを推進いただける方を募集中です。 上記チーム運営に共感いただける方や同じような視座で物事を捉えられる方とともに一緒に当社のコーポレートITを作っていけることを楽しみにしています♪

今回の記事は以上になります。 最後まで読んでいただき、ありがとうございました。


株式会社エニグモ すべての求人一覧

hrmos.co

*1:当社でも数年前まで「情シス」というワードが各所で用いられていましたが、少なくとも私が入社後は意図的に情シスというワードは使用せず、コーポレートITという表現を使用するようにしています。

*2:私個人の視点ですが当社はフラットな環境に感じます。

*3:当然ベンダーには自社の独自要件を踏まえたオーダーメイドの最適な設計でチューニングを行うように追加コストを支払う

*4:CO-ITチームの場合、マネージャと私の方向性が元々大枠で一致していたことと、経営陣とマネージャが既に同様のコミュニケーションを行っていたことで非常にスムーズにすり合わせが完了しています。企業によっては、このすり合わせが非常に難航し、そもそもすり合わせせずに実務に移ることがありますが、後々重大な課題につながるのでオススメできません。

*5:あくまで目安であり、ピラミッドのベースにある項目が頂点にある項目に劣るというような優劣は全くありません。全ての項目がコーポレートIT領域には重要です。また、これは一般的に定義されたものではなく、私自身の経験その他から表現したものとなりますので一意見としてご理解ください。

*6:8時間 × 20営業日 × 12ヶ月 × 4名

*7:ミッションサイズは、全社共通の指標があり、入社時を含め半期毎に行われる評価によって適宜決定されます

*8:あくまで私の個人的な意見ですが、業務として責任を負っている(報酬(給与)を獲得している)以上は合理的な説明責任があります(定量化することは必要です)。

*9:SE費¥50,000/人日 × 0.5人日[0.125人日 × 4名]

*10:チームは同じMissionに向き合っているため、各タスクを項目分けすることはあってもブラックボックス化しない(隠さない)。各メンバーのValueをそれぞれが参考にしたり、評価するために必須。

*11:導入すること自体にバリューはなく、そのサービスで何を実現するかにバリューがある。

*12:マネージャやリードエンジニアは、実担当でなくても高い視座でシステムの重要トピックや仕様を明確化してレビューを行う必要がある

*13:そもそも新規導入時の考慮不足・レビュー不足に起因して問題が発生する場合が多い。問題が発生する前にチームで再設計が議論され、バックログ化されているのが正しい運営であり、ヒーロー(救世主)のような特定個人のスキルセット頼みになるのは問題です。

*14:誤解を恐れずに記載すると、いいとこ取りだけして面倒事は他メンバーにぶん投げるような自分本位のようなメンバーのことを指しますが、業務への慣れ等によって知らず知らずの内に意図せずそのような振る舞いになることもあるかもしれません。お互いリスペクトし合える組織を目指したいものです。

Rails7でHotwireのTurboを使う

こんにちは、エンジニアの太田です。

この記事は Enigmo Advent Calendar 2023 の16日目の記事です。

はじめに

TurboはRails7からデフォルトで搭載されており、VueやReactなどjavascriptの記述が必要だったDOMの更新をjavascriptを(あまり)書かずに実現させてくれます。 フロントエンドにあまり触れない方にとっては、SPA風のwebアプリへのとっつきやすさが出たと思います。
本記事では、私が初めてTurboに触れて使い方を覚える際に作成したサンプルの一部と使った感想を備忘録的にまとめたものになります。
各公式ドキュメントでも使い方を確認できます。
Rails で JavaScript を利用する - Railsガイド *1
Turbo Handbook *2

サンプルコード

以下は私が主に使用した形です。 ransack*3とkaminari*4を使ったリストの更新と追加・編集・削除をしてみます。

controllers

class CountriesController < ApplicationController
  def index
    @q = Country.ransack(params[:q])
    @q.sorts = 'name asc'
    @countries = @q.result.page(params[:page])

    # Request HeadersにTurbo-Frameが設定されているとpartialがreturnされて、
    # 対象の要素がpartialに置換される
    if turbo_frame_request?
      render partial: 'list', locals: { countries: @countries }
    end
  end

  def show
    @country = Country.find(params[:id])
  end

  def edit
    @country = Country.find(params[:id])
  end

  def create
    @country = Country.create!(country_params)
  end

  def update
    @country = Country.find(params[:id])
    @country.update!(country_params)
  end

  def destroy
    @country = Country.find(params[:id])
    @country.destroy!
  end

  private
    def country_params
      params.require(:country).permit(:code, :name)
    end
end

models

class Country < ApplicationRecord
  # ransackで検索項目にしたカラムを記載したのみ
  def self.ransackable_attributes(_auth_object = nil)
    %w[name]
  end
end

views

index.html.erb

# このフォーム内からのリクエストはヘッダーにTurbo-Frameを設定する
# レスポンスのpartialによってid属性がlistのturbo-frame要素が置換される
<%= search_form_for @q, html: { data: { turbo_frame: 'list' } } do |f| %>
  <div>
    <%= f.label :name_cont, 'name' %>
    <%= f.text_field :name_cont %>
  </div>
  <div>
    <%= f.submit '検索' %>
    <%= link_to 'リセット', countries_path, data: { turbo_frame: "_top" } %>
  </div>
<% end %>

<%= render 'list', countries: @countries %>

<%= render 'form' %>


_list.html.erb

# id属性がlistのturbo-frame要素
<%= turbo_frame_tag :list, autoscroll: true, data: { autoscroll_block: 'start' } do %>
  <ul id="countries">
    <li>
      <div>国コード</div>
      <div>国名</div>
      <div></div>
      <div></div>
    </li>
    <%= render countries %>
  </ul>
  <div>

    # turbo_frame_tag内は自動的に直近の親を対象としたTurbo-Frameのリクエストになる
    # なのでkaminariにTurbo用の設定は不要
    <%= paginate countries %>

  </div>
<% end %>


_country.html.erb

# id属性がcountry_{country.id}のturbo-frame要素
<%= turbo_frame_tag country do %>
  <div><%= country.code %></div>
  <div><%= country.name %></div>

  # 直近の親 (id属性がcountry_{country.id}のturbo-frame要素)が対象のTurbo-Frameのリクエストになる
  # edit.html.erbに置換される
  <div><%= button_to '編集', edit_country_path(country), method: :get %></div>

  # GET以外のメソッドではTurbo-Streamのリクエストになる
  # destroy.turbo_stream.erbの処理を実行
  <div><%= button_to '削除', country_path(country), method: :delete %></div>
<% end %>


edit.html.erb

# id属性がcountry_{@country.id}のturbo-frame要素
<%= turbo_frame_tag @country do %>
  <div>
    <%= form_with model: @country do |form| %>
      <%= form.text_field :code %>
      <%= form.text_field :name %>

      # 直近の親 (id属性がcountry_{@country.id}のturbo-frame要素)が対象のTurbo-Streamのリクエストになる
      # destroy.turbo_stream.erbの処理を実行
      <div><%= button_to '保存', action: :update %></div>
    <% end %>

    # GETメソッドなのでTurbo-Frameのリクエストになる
    # show.html.erbに置換される
    <div><%= button_to '中止', country_path(@country), method: :get %></div>
  </div>
<% end %>


show.html.erb

<%= render 'country', country: @country %>


update.turbo_stream.erb

# id属性がcountry_{@country.id}のturbo-frame要素をupdate後に置き換える
<%= turbo_stream.replace @country %>


create.turbo_stream.erb

# id属性がcountriesの要素の末尾に_country.html.erbを追加
<%= turbo_stream.append 'countries', @country %>

# 登録フォームを入力をリセット
<%= turbo_stream.replace 'register' do %>
  <%= render 'form' %>
<% end %>


_form.html.erb

# 登録フォームのid属性registerを設定しておく
<%= form_with model: Country.new, id: 'register' do |form| %>
  <div><%= form.label :code %><%= form.text_field :code %></div>
  <div><%= form.label :name %><%= form.text_field :name %></div>
  <div><%= form.submit %></div>
<% end %>


destroy.turbo_stream.erb

# id属性がcountry_{@country.id}のturbo-frame要素を削除
<%= turbo_stream.remove @country %>

turbo-frame

turbo-frameは画面内のturbo-frameタグを一つだけを対象として置換することができます。id属性必須です。
リストの更新が主な使用ケースでした。

turbo-stream

turbo-streamは画面内の複数要素をid属性で指定して対象とすることができ、それぞれに対して下記の7つの処理*5を実行できます。

  • 先頭追加
  • 末尾追加
  • 直前追加
  • 直後追加
  • 置換(対象要素含む)
  • 更新(対象要素含まず中身だけ)
  • 削除

この時、対象とする要素はturbo-frameタグではなくて普通のdivタグなども指定可能です。注意点はGET以外のリクエストにする必要があることです。
登録時にリストへ要素を追加すると同時にフォームをリセットするなどが主な使用ケースでした。フラッシュを表示したりで複数箇所の更新が必要になりがちなDB操作が絡むリクエストでの使用機会が多いと思います。

動きのイメージ

リストを更新
検索をリセットして初期化
編集フォームに置換
編集を保存して反映
編集をやめる
データを削除
データを登録

細かい話

Hotwireとは、Turboとは

下記の公式の説明やimport文からHotwireという開発アプローチがあって、それを実現させるためのパッケージにTurboというパッケージがあるという感じのようです。

Hotwire is an alternative approach to building modern web applications without using much JavaScript by sending HTML instead of JSON over the wire. *6

import * as Turbo from "@hotwired/turbo"

HotwireにはTurbo以外にもStimulusとStradaがあり、この三つの要素から成ります。 Rails7以降ではデフォルトでTurboとStimulusが使えるようになっています。

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"

# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"

ReactやVueとの違い

前述の公式説明には「without using much JavaScript by sending HTML instead of JSON over the wire.」とありますが、これがHotwireがReactやVueと異なるポイントです。
RailsでReactやVueを使う場合は、サーバサイドのrailsがcontrollerでデータをreturnし、そのデータを取得するようにクライアントサイドのReactやVueの実装をするかと思います。
一方でHotwireでは、上に挙げたサンプルコードのようにリクエストに対してcontrollerがHTML(render partial)をreturnするだけになります。
確かに、JavaScriptを使わずにJSONではなくてHTMLを送信するようになっています。

Turboを使った感想

ransackやkaminariといったviewsを構成するファイルに対するgemをそのまま使えて、わざわざフロントエンド用にnpm installとかしなくて済むのがとてもよかったです。
DOM更新はほとんどJavaScriptを記述しなくてもよかったのもあり、作業工数もそこそこ少なく済むのではないかと思います。
使いづらいところとしては、turbo-frameタグで全体を囲わなければならないのでスタイルの当て方が少し面倒になる場面がありました。cssフレームワークを使う場合は結構気を使う必要があるかもしれません。
また、各アクションでhtmlを返す必要があるので、必然的にviewsディレクトリのファイルが多くります。jsの記述が必要無くなった分という感じです
。 一般公開する大きなサービスではちょっと頼りなさそうな感じはしましたが、他の業務もしながら開発する社内ツールくらいの規模であれば十分なものだと思いました。


最後までご覧いただきありがとうございました。

明日の記事の担当は、コーポレートエンジニア(コーポレートIT[CO-IT]チーム) の横川さんです。お楽しみに!


株式会社エニグモ すべての求人一覧

hrmos.co