BUYMAのプロダクトマネージャー/ディレクターの役割とは?

「安心して何度も利用したくなるマーケットプレイス」を作る!UXデザイングループを紹介

エニグモでTech職種の採用や、採用広報を担当している廣島です。

この記事は、エニグモで新入社員向けのオンボーディング研修として実施する部署紹介プログラムの中で プロダクトマネージャーやディレクター、UI/UXデザイナーが所属するグループであるUXデザイングループマネージャーの山田さんがグループの説明をした内容をまとめた記事です。

グループのミッションや、チーム体制、カルチャー、どのように他チームと連携しながらプロジェクトを推進しているかについて説明します。最後に、BUYMAサービスやUXデザイングループの今後の展望や、UXデザイングループで仕事を行う魅力についてもお話しします。

※この記事は Enigmo Advent Calendar 2023の25日目の記事です※

BUYMAサービスについて

まずは、エニグモが運営するサービスのBUYMAについて説明します。
BUYMAは累計会員数1000万人超えの海外ファッションNo.1の通販(EC)サイトです。
世界中に在住する約20万人のパーソナルショッパー(出品者)から、ファッションを中心とした世界中のアイテムを購入できる「売り手」と「買い手」が個人であるCtoCのグローバルマーケットプレイスです。

主に個人の海外在住の日本人のネットワークを構築して海外から商品を購入することが可能で、多種多様な購入者の要望に答えています。商品の約半分は海外から届きます。
BUYMAが間に入ることで安心安全に海外の買い物ができるプラットフォームとして運営しています。

出品アイテムの特徴としては高級品×希少品の商品のラインナップが充実しており、海外限定デザインや、国内未上陸ブランド、国内完売入手困難品など高付加価値のアイテムを世界中から購入することができます。
BUYMAはファッションを主力にしつつも、インテリア・アウトドア、旅行などカテゴリーを拡大しています。

BUYMAのビジネスモデルの詳細は下記をご覧ください。
https://enigmo.co.jp/business/

UXデザイングループのミッション・業務内容とは?

エニグモディレクションを担うグループはUXデザイングループ(以下UXDグループ)と呼びます。
(UXとは「ユーザーエクスペリエンス」のことで、ユーザーが商品やサービスを通じて得られる体験を指します)

UXDグループとは名前の通り、ユーザーのPain(痛み)に向き合いながら、サービスをより使いやすく・より良くしていくために解決すべきことを考え、実行するグループです。
上記により、ユーザー体験を向上させ、安心して何度も利用したくなるようなマーケットプレイスBUYMAを成長させることがミッションです。

さらに、UXDグループはBUYMAサービスのプロダクト側(エンジニア・デザイナー)とビジネス側(MD、マーケティング・広告、データ、CS、オペレーション等)のちょうど中間に位置して両者を支えサービスを前進させる役割を担います。

具体的な業務領域は?

実際にどういった業務領域を担当しているかについてお話しします。
BUYMAの新機能・既存機能の改修・新サービスの企画・ディレクションからUI/UXと幅広く担当します。一般的なWeb業界の言葉でいうと具体的には下記のような業務です。

  • プロダクトマネジメント
  • プロジェクトマネジメント
  • 制作ディレクション / 進行管理
  • 各施策・プロジェクトの効果検証
  • UX視点からの企画提案 / サービスデザイン
  • 定性 / 定量調査
  • UIデザイン
  • フロントエンドプログラム(主にインタラクションデザイン部分)の実装

上記からも分かるようにUXDグループはディレクションを行うグループではあるが、UIデザインやフロントエンドプログラムなどの制作も行っていることが特徴的です。

フロントエンドプログラムは、Reactでの実装などはエンジニア部署のフロントエンドエンジニアが行いますが、インタラクションデザインなど実際に画面上で動くCSSJavaScriptの実装はUXDグループがおこないます。
その為、企画から制作までをシームレスにグループ内で行えユーザーの反応や外部環境に合わせてスピーディーに柔軟に対応することが可能です。

色々言いましたがまとめると業務内容は下記となります。

  • BUYMAの様々な課題に向き合い最適なスコープでユーザーに価値を提供する
  • ユーザーの反応を定性、定量の両側面から分析して企画・施策に繋げる
  • スキルとスキル(企画と開発)の間に立ち仲介・翻訳を行い、場合によっては制作も行う

グループの体制について

Webサービスにおける様々なスキルを持ったメンバーが集まっています。特徴としては、ディレクター部門であるが、制作するメンバーもいることです。一般的な職種で言うと、プロダクトマネージャー、ディレクター、SEOディレクター、UI/UXデザイナー、フロントエンドエンジニアが所属します。
前項で説明した業務領域全てを1人のメンバーが網羅しているのではなく、それぞれのメンバーの得意分野を活かしながら多岐にわたる領域をカバーし業務を行っています。

さらにグループは下記2つのチームに分かれています。
プロダクトマネジメントチームマーケティング戦略系の施策を担当するチーム
UI/UXチーム:UX戦略系の施策を担当するチーム

グループの体制を示した図です。
明確にチームが別れているのではなく、UXDグループのマネージャーがUI/UXチームのマネージャーを兼任していることもあり、定例は一緒に行っています。週1回グループの定例の他、プロジェクトベースで進めています。

実際の・プロダクトマネジメントチームとUI/UIXチームの業務は下記のインタビュー記事をご覧いただくと,、よりイメージできるかと思います。ご覧ください。
BUYMAのWebディレクター(PdM)紹介/エニグモの魅力は?UXDグループとは?インタビューしました!
・UI/UXチーム リーダーのインタビュー記事(Coming Soon)

開発フロー・プロジェクトの中でのUXDグループの役割

プロジェクトの開発のフローの中で、UXDグループのメンバーが他職種メンバーと協力しながらどういった役割を担うのかについてお話しします。

プロジェクトや施策に伴う開発はエンジニアやビジネスサイドなど他チームメンバーと連携してプロジェクト型チームで進行します。
開発全体の流れは、UXDグループメンバー(ディレクター)が企画をまとめプロジェクトがスタートします。ディレクターとエンジニアのリードメンバーやビジネスサイドのメンバーで企画をどのようにシステムに落とし込むのかを要件定義します。その後、エンジニアのリードメンバーがシステム設計を担当し、タスクを分解してエンジニアメンバーアサインします。

プロジェクトや施策はディレクターから発案されることもあれば、マーケティングやカスタマーなど他の部門からの提案もあります。
要件定義はプロジェクトの特性により異なりますが、通常は主にディレクターが他のステークホルダーとのヒアリングを通じて要求を洗い出し、システムをどう作るかをエンジニアと共に要件定義を進めるのが主流です。

プロジェクトの目標やKPIの策定もディレクターが中心に行います。各プロジェクトでは、新規機能を開発する際に、「なぜこの新機能を開発するのか」「新機能リリースにより、ユーザーにどのような体験や行動を期待するか」「新機能リリース時の成功の定義は何か」などを、各プロジェクトのステークホルダーと明確にしています。

最後に

サービスの課題や今後の展望、エニグモでUXDにジョインする魅力、どういった方がマッチするかについて、マネージャーの山田さんにインタビューしました。

サービスの課題や今後の展望

今期の重点テーマである「BUYMAサービスをより安心できる体験にする」ことは引き続き進めていきます。安心と一言でいっても様々な側面が存在します。
たとえば、高級ブランドを扱っているため、ユーザーが商品が本物であるか不安に感じることもあります。これに対処するため、来期も偽物の不安を払拭するための施策を推進していきます。

また、ビジネスモデル上、高級品および希少品を扱っていることが強みですが、日用品と比べて購入頻度が低い傾向があります。そのため、サイトに訪れるタイミングが少なくサイトにユーザーが定着しにくく、サービスとユーザーの距離が遠くなることが課題です。
この課題に焦点を当て、ユーザーがサイトに定期的に訪れる動機づけや、サービスとの継続的な関係を築くための取り組みにも注力していきたいと考えています。

UXデザイングループの魅力

プロジェクトごとに各職種のメンバーがアサインされ、小規模チームが結成されます。そのためメンバー、一人ひとりが大きな裁量を持ち、エンジニア、デザイナー、ビジネスメンバーと密に連携し、距離が近く風通しも良く別部門という感じはしません。良い人(互いを尊重し、前向きなメンバー)が多いため、仕事上の人間関係による変なストレスがあまりなく、プロダクト開発方法や体制を共に進化させるマインドが根付いています。

また、グローバル×CtoCサービスという、事例や正解がないBUYMA独自の仕組みやUXを構築するフェーズに携わることができます。出品者側と購入者側の双方のデータやユーザ―行動に基づくデータドリブンな開発が可能です。プロジェクトの成果を数値(売上、CVRなど)やユーザーの声によって直接実感することができます。

どういった方がマッチするか

経験やスキルも大事ですが、熱意やマインド面、カルチャーへの共感、エニグモへのバリューへのマッチを大切にしています。※バリューについての詳細はこちら
業務領域が幅広いため、俯瞰して物事を柔軟にアプローチできる方や、自らイニシアチブを取り推進できる方が活躍できるかと思います。

決まった案件をこなすだけでなく若手から裁量を持って働け、新規機能や新規事業の企画を自ら発案しユーザーに届け、世の中を変える可能性があります。
課題を見つけて積極的にアイデアを提案し進めていく方がマッチすると思います!

BUYMAや関連サービスのサービス品質を常に進化させ、国内および世界各国のユーザーがますますサービスを利用して喜びや満足を得られるような機能を、楽しみながら共に考え抜いていただける方にジョインいただけると嬉しいです。

BigQueryマニュアル「関数のベストプラクティス(Best practices for functions)」を試してみた結果(その1)

こんにちは、エニグモ 嘉松です。

BUYMAのプロモーションやマーケティングを行っている事業部に所属、その中のデータ活用推進室という部署で会社のデータ活用の推進やマーケティング・オートメーションツール(MAツール)を活用した販促支援、CRMなどを担当しています。

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

はじめに

この記事ではGoogleから提供されているBigQueryのオンライマニュアル「関数のベストプラクティス(Best practices for functions)」を試してみた結果を紹介していきます。

「関数のベストプラクティス」では、以下の4つのベストプラクティスが紹介されています。

  1. 文字列の比較を最適化する
  2. 集計関数を最適化する
  3. 分位関数を最適化する
  4. UDF を最適化する

はじめは4つ全てを1つの記事で紹介するつもりでしたが、記事を制作していく中で1つでもそれなりの記事ボリュームがあることが分かったので、読みやすさを重視して1つの記事で1つのベストプラクティスを紹介していくことにしました。 ということで、この記事は「その1」として「文字列の比較を最適化する」を試した結果を紹介していきます。

文字列の比較を最適化する

ベストプラクティス

  • 可能であれば、REGEXP_CONTAINS ではなく LIKE を使用します。

BigQuery では、REGEXP_CONTAINS 関数または LIKE 演算子を使用して文字列を比較できます。 REGEXP_CONTAINS は多くの機能を提供しますが、実行に時間がかかります。 REGEXP_CONTAINS ではなく LIKE を使用すると、処理時間が短くなります。 特に、ワイルドカード一致など、REGEXP_CONTAINS が提供する正規表現をフルに活用する必要がない場合には処理時間が短くなります。

次の REGEXP_CONTAINS 関数の使用を検討してください。

SELECT
  dim1
FROM
  `dataset.table1`
WHERE
  REGEXP_CONTAINS(dim1, '.*test.*');

このクエリは次のように最適化できます。

SELECT
  dim1
FROM
  `dataset.table`
WHERE
  dim1 LIKE '%test%';

なるほど。やはり機能の多い関数、複雑なパターンに対応できる関数よりも、単純なことしか出来ない関数の方が処理は軽い(速い)と。言われてみれば当たり前といえば当たり前ではありますが。単純なことしてできない関数が利用できるケースであれば、そちらを使ったほうが良い、正規表現による検索が必要ない場合は LIKEを使いましょう、ということですね。そもそも REGEXP_CONTAINS より LIKE を使うことを最初に考えるとは思うけどww(REGEXP_CONTAINSの方が汎用性が高いので常にREGEXP_CONTAINSを使っています、みたいな人は注意が必要です!)

試してみた

検証方法

REGEXP_CONTAINSLIKEを使ったクエリを実行して比較していきます。

比較する候補となる値は 経過時間消費したスロット時間の2つ、クエリの「実行の詳細」から取得できます。

それぞれの意味は以下の通りです。

  • 経過時間
    • クエリが開始されてから完了するまでの時間
      • サーバの使用状況などによる待機時間も含まれます
  • 消費したスロット時間
    • 「スロットとは、SQL クエリの実行に必要な演算能力の単位です。」(「BigQueryのコンソールの(?)」より)
    • スロットについての詳細は以下のGoogleのマニュアル「スロットについて」を参照ください。
    • BigQueryスロットは、BigQueryでSQLクエリを実行するために使用される仮想CPUということで、ざっくり言うとクエリ(SQLの実行)に使用したCPU使用量です。

今回の検証では 消費したスロット時間 を比較ます。(経過時間だとGoogleのその時のサーバの負荷といった外部要因が加わるため)

クエリはそれぞれ5回ずつ実行してその平均値を比較します。 クエリを実行するとクエリの結果がキャッシュされるので、キャッシュを使用しないように設定を行います。

なお、対象のテーブルは約1億件のテーブルを対象にしました。

キャッシュクリアの方法

  • 検証中はキャッシュを使用してしまうと正しい計測ができないので、キャッシュを使用しないように設定を行います。

https://cloud.google.com/bigquery/docs/best-practices-performance-functions?hl=ja#optimize_string_comparison

検証結果

消費したスロット時間(時間:分:秒)

試行回数 REGEXP_CONTAINS LIKE
1 0:03:06 0:02:50
2 0:03:22 0:02:29
3 0:03:02 0:02:36
4 0:03:06 0:02:20
5 0:02:56 0:02:18
平均 0:03:06 0:02:31
  • REGEXP_CONTAINSを使用した場合の平均の「消費したスロット時間」は 3分6秒 だったのに対して、LIKEでは 2分31秒 と35秒短縮、減少率は 80.79% と約80%に短縮、約20%改善されました。
  • この20%の改善をどう取るか。かなり短縮できたと取るか、さほど変わらないと取るか。
  • 個人的には、思ったほど変わらないな、という印象でした。
  • といのも、SQLではテーブルの結合方法(JOIN)や、絞り込み条件(WHERE句)をチューニングすると実行時間が半分になったり、場合によっては1/10になったりすることもざら、珍しくないため。
  • ただ、単純に LIKE を使うだけで20%削減されるのであれば、それは価値ありますよね。
  • このクエリで検索の対象としているカラムの平均の文字数は33文字と少なかったため大きな差が出なかった、もう少し文字数が多いカラムを対象にしたらもう少し差が出るのでは、と考え文字数の多いカラムを使って検証を実施しました。

追加検証結果(時間:分:秒)

  • 平均文字数が758文字のカラムを使って比較

消費したスロット時間(時間:分:秒)

試行回数 REGEXP_CONTAINS LIKE
1 0:59:53 0:55:26
2 1:02:00 0:53:43
3 1:02:00 0:51:51
4 1:01:00 0:50:43
5 1:04:00 0:52:02
平均 1:01:47 0:52:45
  • まずカラムの平均文字数が増えたことで「消費したスロット時間」も増加しています。
  • 平均文字数が33文字では約3分だったのに対して、758文字は約60分と約20倍に。
    • 当然ですが検索する文字数が増えればその分として処理の時間も増えますね。
  • REGEXP_CONTAINSを使用した場合の平均の「消費したスロット時間」は 1時間1分47秒 だったのに対して、LIKEでは 52分45秒 と9分2秒短縮、減少率は 85.39% と約85%に短縮、15%改善されました。
  • 平均文字数が33文字のときは約80%に短縮されたのに対して、758文字では約85%と短縮の率は減少しました。
    • この辺りの差は文字数のバラツキや検索する文字によっても差が出るのかもしれません。
  • 文字数の多いカラムでは LIKE を使うことでよりパフォーマンスに差が出ると思いましたが、大きな違いはありませんでした。

まとめ

  • REGEXP_CONTAINSLIKEを使ったクエリを実行して比較してみた結果、LIKEを使った方がクエリのパフォーマンスは確かに改善された!
  • その改善率は今回の検証では約20%だった!!
  • 可能であれば、REGEXP_CONTAINS ではなく LIKE を使用しましょう!!!

本日の記事は以上になります。

エニグモ Advent Calendar 2023もこの記事で最後になります。
最後まで読んでいただきありがとうございました。


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

hrmos.co

元SEがコーポレートエンジニアに転職してみた

こんにちは! 今年7月に中途入社しました、コーポレートエンジニア(コーポレートIT[CO-IT]チーム) のフルセです! 今年も終盤(早いですねぇ、、)ということで、 Enigmo Advent Calendar 2023 の季節になりました!! クリスマスイブである24 日目を担当する私は入社エントリ・振り返りなど中心に自由に書きたいと思います!

なお、この記事が少しでもコーポレートエンジニアに興味がある方や入社を検討してくださっている方の参考になれば幸いです。

Embed from Getty Images

簡単な経歴紹介

私の経歴についてざっくり下記のようにまとめました。

① 新卒でシステムインテグレータ企業にSEとして入社

→ITの基礎知識・技術を獲得! and IT業界の厳しさを痛感、、(大袈裟)

② システムインテグレータ企業を退職

→主にシステム相手の業務だったので、より人と接する仕事がしたいと考え決意

③ 某アパレル商社の情シスへ転職

→培った知識とスキルを活かし新たな業種へチャレンジしたかった!

→情シスが意外と性に合っていることに気づくきっかけに!

④ カナダへ留学

→ずっと挑戦したかった海外留学に挑戦!

→語学はもちろん人との出会いや経験から人生にプラスになった!

エニグモにコーポレートエンジニアとして入社

→帰国後にご縁があり、経験を活かしコーポレートエンジニアとして入社!

そもそもコーポレートエンジニアって何?

コーポレートエンジニアとは?
コーポレートエンジニアとは、企業内のIT活用や運用を担当するエンジニアのことです。

企業のIT環境の変化は著しく、テレワークの普及などによってクラウドサービスの利用も活発になり、SaaS型のID管理や統合認証サービスを利用する企業が増えてきました。そのなかでコーポレートエンジニアは、社内ITの構築・運用をはじめとして、社内業務の課題解決のための企画立案や部門間の調整まで幅広い業務を担います。

もっと詳しく知りたい方は、チームメンバーの記事を読んでみてください! tech.enigmo.co.jp

実際に入社してみて感じたこと

コーポレートエンジニアとして働く以上、社員の方々とのコミュニケーションをかなり重要だと考えていました*1。 しかし、エニグモはリモートワーク中心の会社のため、Face to Face でのお話しする機会が少なくどのように交流の輪を広げていこうか少し不安(そもそも入社直後というのもあり。。)に感じていました。

そんなときに、2M(Monthly Meet-up)*2に参加させていただきました。 そこでは色々な部署や役職の方と分け隔てなくフランクにお話しすることができ、一瞬にして不安が和らぎました。 やはり、新しい環境でのスタートはどうしても孤独感や不安がつきものだと思いますが、こういった交流会があると気持ちよくスタートダッシュが切れますよね!そもそも、こういった交流会を定期的に行なっている企業というのは少ないと思うので、これぞエニグモの良いところだと思っております。

それからというもの、毎月可能なかぎり参加し交流の輪を広げております!(無料で美味しいお酒とご飯が得られることも理由の一つなのですが笑)

2Mについて気になる方は、レポート記事がありますので是非ご覧いただければと! www.wantedly.com

入社してからの仕事とその感想

入社してからのざっくりとした仕事内容とその感想を書かせていただきました!

内容

  • 社内ヘルプデスク対応

    • 内容:PCの故障や不具合、各種ITサービスのトラブル対応等
    • 対応頻度:1〜2件/日
  • アカウント管理

    • 内容:アカウントの付与・削除から数量管理・購入
    • 対応頻度:1〜2件/週
  • IT機器の管理・キッティング

    • 内容:PCやモバイルデバイスの発注・管理・キッティング
    • 対応頻度:10〜20件/月
  • 社内ナレッジの作成・整備

    • 内容:手順書やルールの新規作成・ブラッシュアップ
    • 対応頻度:2回/週
  • 中途入社者向けのオリエン

    • 内容:PCのセットアップ方法の説明、社内ITサービスルールの共有
    • 対応頻度:1〜2回/月
  • 社内不要OA・IT機器廃棄対応

    • 内容:廃棄物回収対応
    • 対応頻度:2回/年
  • カオスマップ作成

    • 内容:社内で利用中サービスを整理と今後導入が必要なサービスの洗い出し
    • 対応頻度(見直し):2回/年

感想

一覧にしてみると、今年は色々なことをやらせていただいたと改めて感じています。 裁量を任せていただいているため、日々にマルチタスクをこなしておりますが、 それもまた自分のタスク管理力や遂行力の向上につながっていると感じます。

個人的にやりがいがあった業務としては、「社内不要OA・IT機器廃棄対応」です。 業者選定から始まり、やりとり(契約締結や回収日の調整など)、社内ナレッジ作成、各方面との連携など、これぞコーポレートエンジニアと感じるようなタスクだったため、とてもやりがいがありました。 それと山積みだった廃棄端末類をスッキリさせることができたという達成感が大きかったですね笑

またタスクとして面白かったものとしては、「カオスマップ作成」です。 初めてカオスマップというものを作成したので、そもそもカオスマップとは?からスタートし、他企業のカオスマップの調査、社内ITサービスの整理、今後導入が必要なITサービスの洗い出しを行いました。その後は、カオスマップをパズルのように作成する大変な作業がスタートし...と、ここでは書ききれないので、詳しい作成プロセスなどは機会があればまた書かせていただきたいと思います。(今回は割愛させていただきます。) それからというもの、試行錯誤を繰り返し、レビューを重ねてやっとの思いで完成させることができました。 結果として、時間をかけて取り組んだからこそ、サービスに対する理解や知見を広くできましたし、社内の課題(足りていないサービス)も炙り出すことができました。とはいえ、まだまだ未熟なカオスマップだと思っているので、これからもっと成熟させていきたいと思います。(楽しみ!)

こんな方に向いているかも(個人見解)

コーポレートエンジニアとしてのキャリアをスタートしたばかりの私ですが、 そんな私だからこそ考えるコーポレートエンジニアにはこんな人が向いているのではないかというのをまとめてみました!

私が思うに下記の3つの要素が肝になってくると思います。

コーポレートエンジニアの必要要素

  • 新しい情報・技術に興味があり探求するのが好きな方(好奇心)

    • 日々進化するIT技術やサービスをいち早く自社へ展開・導入するため
    • 最新の情報に敏感であれば、サービスなどがアップデートされたとしても柔軟に対応することができるため
  • 人とコミュニケーションするのが得意・好きな方(コミュニケーション力)

    • 業務上(問い合わせ対応・情報共有など)何かとコミュニケーションが必要になるため
  • 問題を切り分けて解決することが得意・好きな方(柔軟性)

    • コーポレートエンジニアは企業の多くの課題を与えられるので、一つ一つ解決し柔軟に対応することが求められるため

ここで書かせていただいた3つの要素は、かなり重要なポイントになると考えます。 コーポレートエンジニアの業務は、IT関連の何でも屋のようになりがちだと思っております。 そのためタスクを挙げ出したらキリがないうえ、日々変化します。 そのため、柔軟性はもちろん、社内外連携のためのコミュニケーション力やITサービスに関する知見を広げるための好奇心が重要になってきます。 もちろん、足りてないと感じている要素があったとしても、業務を通じてレベルアップすることは可能だと思います!(私も絶賛レベルアップ中...)

※あくまで個人の見解なのでご了承ください。

最後に

私もまだまだ未熟なので、日々進化するIT技術・情報に置いていかれないよう自己研鑽を続けつつ、 社内の課題を一つずつ着実に解決し、より良い社内環境づくりに励みたいと思います!

最後まで読んでいただき、ありがとうございました!!


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

hrmos.co

*1:情シス時代に業務上のコミュニケーションがかなり重要だと理解していたため

*2:毎月月初に開催される社内交流会

Argo Workflowsを使ったKubernetes(EKS)のアップグレード

こんにちは、インフラグループ Kubernetesチームの福田です。
この記事は Enigmo Advent Calendar 2023 の22日目の記事です。

皆さんはKubernetesのアップグレード、どうしていますか?
Kubernetesは4ヶ月に一回、新しいマイナーバージョンがリリースされ、最新の3つのマイナーバージョンのみサポートされます。
つまり、原則は4ヶ月に一度、アップグレードをやらなければなりません。(最新バージョンであれば最大12ヶ月はサボれるという考え方もありますが。。。)
弊社ではKubernetes環境としてEKSを使っており、Kubernetes本体のリリースと微妙に間隔が違いますが、最新を維持するために大体3〜5ヶ月毎にアップグレード作業をやっています。
このKubernetes(EKS)のアップグレード作業をワークフロー化したのでそれを紹介します。

ワークフローとは

本記事でいうワークフローとはワークフローエンジンを使って自動化された作業のまとまりです。
ワークフローエンジンとは、作業プロセスをいい感じに管理してくれるツールです。
代表的なものとして、Apache Airflow, digdag, Argo Workflowsなどがあります。
我々の場合、Argo Workflowsを利用しています。

EKSアップグレードワークフローの概要

上図は実際のワークフローのキャプチャです。
3段階の構造(上から順に実施される)になっていて、以下を実施する内容となっています。

  1. 廃止されるAPIを使っていないかチェック
  2. EKSアドオンのアップグレードバージョンを特定
  3. CloudFormationテンプレートを更新するPullRequestを出す。

元々、弊社ではAWSリソースをCloudFormationで管理する運用になっていたことから、ワークフローのゴールがCloudFormationコードの修正(Pull Request作成)となっています。

ワークフローの入力

上図の通り、ワークフローには入力パラメータがあります。

  • ticket-id
    • JIRAのチケットIDを指定します。 コード修正時のコミットメッセージやブランチ名のプレフィックスとして利用されます。
  • kubernetes-cluster
    • アップグレードを実施する対象のクラスタをプルダウン形式で選択します。
  • eks-version
    • Kubernetes versionをマイナーバージョンまで指定します。例:1.28
  • ami
    • EKSノードで使用するAMIを指定します。

EKSアップグレードワークフローの詳細

1. 廃止されるAPIが無いかチェック

Kubernetesではマイナーバージョンの更新に伴って、機能追加や古い機能の削除のためにAPI Versionが更新され、既存のものが廃止されることがあります。
そのため、Kubernetesアップグレードを実施する前に廃止されるAPIが無いかチェックしています。
plutoというOSSを使って、クラスタ上にあるリソースで廃止されるものを使っていないかチェックをしています。

2. EKSアドオンのアップグレードバージョンを特定

EKSではKubernetesのマイナーバージョンの更新と併せて、EKSアドオンのアップグレードが必要な場合があります。我々の場合、「EKSアドオンのバージョンはデフォルトバージョン※を指定する」というポリシーで運用しています。
※ EKSではアドオン毎にKubernetesのマイナーバージョンに紐づくデフォルトのバージョンを公開しています。

具体的な方法としてはawsコマンドで、指定したKubernetesのマイナーバージョンに対応するEKSアドオンのデフォルトバージョンを取得しています。

aws eks describe-addon-versions --addon-name ADDON_NAME --kubernetes-version K8S_NAME --query 'addons[0].addonVersions[?compatibilities[0].defaultVersion==`true`].addonVersion | [0]'

3. CloudFormationのコードを管理しているリポジトリKubernetesアップグレードのためのPull Requestを出す

このステップについては前のステップで得られるEKSアドオンのアップグレードバージョンとワークフローへの入力値を元に、CloudFormationのコードを修正して、Pull Requestを出します。
また、Pull Requestに人手で実施する作業内容を記載します。
このステップにおける処理はGoで実装した自前のCLIツールで行っています。

考慮事項

このステップを実装するにあたり、考慮したことを紹介します。

VPC CNIのアップグレード

前のステップでEKSアドオンのアップグレードバージョンを特定していますが、VPC CNIについてここで問題があります。
VPC CNIのアップグレードは一度に1つのマイナーバージョンのみアップグレード可能です。
例えば、Kubernetes 1.27に対応するVPC CNIのデフォルトバージョンがv1.12.6-eksbuild.2Kubernetes 1.28に対応するVPC CNIのデフォルトバージョンがv1.14.1-eksbuild.1の場合、予めVPC CNIのバージョンをv1.13系にアップグレードする必要があります。
実際の実装としてはVPC CNIのアップグレードバージョンのマイナーバージョンが元のバージョンより2つ以上離れている場合は、本ワークフローを実施する前にVPC CNIのマイナーバージョンをアップグレードするように促すメッセージを出してワークフローはそこで失敗するようにしています。

手順書

Kubernetesアップグレードの実施自体の手順としてはCloudFormationの実行コマンドだけですが、実際の作業手順はもう少し複雑です。
我々の場合はアップグレードの実施コマンドに加えて以下を盛り込む必要がありました。

  • 作業前
    • 作業通知の開始アナウンス
    • 作業時の監視アラートのサイレント設定
  • 作業後
    • アップグレードが正しく完了したかの確認
    • 作業時の監視アラートのサイレント設定解除
    • 作業通知の終了アナウンス

yamlをGoで編集する際の副作用

CloudFormationのテンプレートはyamlですが、これをGoで書き換えると発生する副作用があります。
それはGoでyamlを扱うためにyamlファイルをエンコードして、プログラム上で何らかの処理をして、再度yamlファイルに戻してやると空行やコメント等が消失してしまうというものです。
yamlとしては等価なのだからという理由で切り捨ててしまえばそれまでですが、コメントや空行はリーダビリティの観点で有用なものも多いので、これは維持するようにしたいです。

まず、コメントの維持についてはyaml.v3を使うことで解決できます。
具体的には以下のようにyaml中にある書き換えたいプロパティをpathで指定して、それをnewValueで更新するようにしています。

func (e *yamlEditor) updateYamlProperty(node *yaml.Node, path []string, newValue string) error {
    if len(path) == 0 {
        return fmt.Errorf("invalid path")
    }
    for i := 0; i < len(node.Content); i += 2 {
        keyNode := node.Content[i]
        valueNode := node.Content[i+1]
        if keyNode.Value == path[0] {
            if len(path) == 1 {
                valueNode.Value = newValue
                return nil
            } else if valueNode.Kind == yaml.MappingNode {
                return e.updateYamlProperty(valueNode, path[1:], newValue)
            }
        }
    }
    return fmt.Errorf("invalid path")
}

空行についてはdiffコマンドのオプションで空行を無視する差分のdiffを生成して、それをpatchコマンドで元のファイルに修正として適用するようにしています。

## 元々存在するcloudformation template
target.yaml

## プログラムによって書き換えが行われたcloudformation template。空行はなくなってしまっている。
target.changed.yaml

diff -U0 -w -b -B target.yaml target.changed.yaml > target.diff
patch -i target.diff target.yaml

参考: https://github.com/mikefarah/yq/issues/515#issuecomment-830380295

まとめ

今回はArgo Workflowsを使ったKubernetes(EKS)のアップグレードについて紹介させていただきました。
紹介したワークフローはKubernetes1.27とKubernetes1.28のリリースの間で作成したもので、Kubernetes1.28のアップグレードはこのワークフローを使ってアップグレードしました。
これのおかげで面倒だったアップグレード作業を大分楽にすることができました。

明日の記事の担当は早野さんです。お楽しみに。


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

hrmos.co

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