SAML Group Mappingを使ったEntra ID+Datadogのロール自動割り当て

プラットフォームチームの菅原です。

カンムではサービスのモニタリングにDatadogを利用しており、その管理はプラットフォームチームが担っています。

DatadogへのログインはMicrosoft Entra ID(旧AzureAD)を使ったシングルサインオンで行うため、DatadogのアカウントはEntra IDで一元管理されているのですが、Datadogのロールの割り当ては自動化されておらず、引き続き依頼を受けてプラットフォームチームが割り当てを行う状態でした。

カンムで使っているDatadogのロールはEntra IDのグループから一意に割り当てられるため、機械的マッピングすることができます。 調べたところ、サインオン時に任意のSAML属性からロールを割り当てるSAML Group Mappingという機能が提供されていたので設定してみました。

docs.datadoghq.com

Entra IDの設定

SAML Group Mappingを利用するにはユーザーが所属するグループに応じてSAML属性(クレーム)の値を動的に変更する必要があります。

最初、user.assignedrolesというソース属性を使ってエンタープライズアプリケーションのグループに割り当てられたロールでクレームの値を変更することを想定していたのですが、user.assignedrolesからはユーザーに割り当てられたロールを取得することができても、グループに割り当てられたロールを取得することができなかったため、「グループごとにクレームの値を変える」という要件を満たすことができず、また、設定も煩雑になってしまうためこちらを使うことができませんでした。

代わりに「条件に基づいてクレームを出力する」機能で要求条件(Claim conditions)で、グループごとの値を設定することでクレームの値を動的に変更することができました。以下はその設定画面です。

要求条件では、最終的に条件にマッチした値がクレームの値になります。

上記の例の場合、プラットフォームグループにマッチした場合、DatadogRoleクレームの値は adminに、開発者グループにマッチした場合はstandardに、どちらにもマッチしなかった場合はreadonlyになります。

(残念ながらterraformのazureadプロバイダでterraformingすることはできなかったため、手動で設定を行いました)

Datadogの設定

Datadog側の設定はterraformで行いました。 DatadogRoleクレームの値に応じて、ロールが設定されるようになっています。

data "datadog_role" "admin" {
  filter = "Datadog Admin Role"
}

data "datadog_role" "standard" {
  filter = "Datadog Standard Role"
}

data "datadog_role" "readonly" {
  filter = "Datadog Read Only Role"
}

# NOTE: Enable Mappings manually
# cf. https://my-org.datadoghq.com/organization-settings/mappings/role-mappings
resource "datadog_authn_mapping" "admin" {
  key   = "DatadogRole"
  value = "admin"
  role  = data.datadog_role.admin.id
}

resource "datadog_authn_mapping" "standard" {
  key   = "DatadogRole"
  value = "standard"
  role  = data.datadog_role.standard.id
}

resource "datadog_authn_mapping" "readonly" {
  key   = "DatadogRole"
  value = "readonly"
  role  = data.datadog_role.readonly.id
}

# NOTE: Entra ID SAML Mapping config
# https://portal.azure.com/#view/Microsoft_AAD_IAM/SamlClaimsEditClaimBladeV2/federatedSsoConfigurationIdentifier/...

まとめ

SAML Group Mappingを使うことでDatadogのアカウントの管理を完全にEntra IDで一元化することができました。

この手の基盤チームありがちな「○○の権限をください」というタスクは一つ一つはたいしたことはないのですが、積み重なると地味に作業の時間を奪われるので、なるべく自動化を進めていきたいところです。

Poolにおける残高管理の設計

こんにちは、エンジニアのpongzuです。 今日は、カンムが提供するプロダクト「Pool」の残高管理設計について書きます。

本記事では、まずPoolがどのような仕組みで動いているのか、投資やウォレットといったサービス仕様を簡単にご説明します。その後、それらをどのようにDB管理するかについて、設計の考え方を順を追って解説していきます。

仕様

Poolは簡単にいうと投資とVisaカードを組み合わせたプロダクトです。 アプリ上で口座開設を行い、ウォレットと呼ばれる機能に入金後、資産運用やカード決済に利用できます。 また、同時にウォレット残高と投資資産がカード利用可能額となり、Visaの加盟店でカード決済をすることができます。以下に仕組みを簡単に図解したものを貼ります。

※この仕様はシステム設計の説明のために簡易化したものです。正式な仕様はサービス紹介・利用規約をご参照ください。

ウォレット・投資・カードの機能について、もう少し詳しく説明します。

ウォレット

ウォレットは、入金の受け皿となる機能です。入金処理が行われると、その金額がウォレット残高として反映されます。また、入金以外にも、キャンペーン対象者へのポイント付与やカード支払いによるポイント還元などもウォレット残高に加算されます。

投資

ユーザーは、ウォレット残高から募集中のファンドへ投資申込を行うことができます。運用開始時にウォレット残高から投資資産へ資金が移行します。運用終了後には、資産(以下「償還額」と呼びます)を次の方法で利用できます。

  • ユーザーが指定する銀行口座への出金
  • 次の募集中ファンドへの再投資

カード

ウォレット残高と投資資産がカード利用可能額となり、Visa加盟店でカード決済を利用できます。 カード利用額は、利用月の月末に確定し翌月末にウォレット残高から支払われます。いわゆる「月末締め・翌月払い」のクレジットカード方式です。ただし、支払い額がウォレット残高を上回る場合に限り、カンムがファンドの持ち分を買い取る形で支払いに充てられます。これにより、ウォレット残高だけでなく、投資資産を活用したカード支払いが可能となっています。

以上のようにしてPoolは資産運用と預け入れた資金の流動性を両立させています。

DB設計

ここからは本題であるデータベース設計について説明します。 Poolの残高管理には、入金履歴・投資運用・カード支払いの3つが関係します。これらのテーブルがどのように関わり合い、残高管理を実現しているのかを順を追って解説します。 まず、主要なテーブルを抜き出してみると、deposit, investment_application, investment, card_payment, wallet_snapshotの5つが挙げられます。

※説明の便宜上すべてのテーブルにuser_idを持たせていますが、実際のリレーションとは異なります。

                                        Table "deposit"
     Column              |           Type           | Collation | Nullable |             Default
-------------------------+--------------------------+-----------+----------+----------------------------------
 id                      | bigint                   |           | not null | nextval('deposit_id_seq'::regclass)
 user_id                 | bigint                   |           | not null | 
 wallet_snapshot_id      | bigint                   |           | not null | 
 amount                  | numeric                  |           | not null | 

depositは入金履歴を管理するためのテーブルです。振込入金口座から入金通知を受けて入金額がウォレット残高に反映されます。 wallet_snapshot_idについては後述します。

                                        Table "investment_application"
     Column     |           Type           | Collation | Nullable |             Default
----------------+--------------------------+-----------+----------+----------------------------------
 id             | bigint                   |           | not null | nextval('investment_application_id_seq'::regclass)
 user_id        | bigint                   |           | not null |
 amount         | numeric                  |           | not null |

investment_application は投資申込を管理するためのテーブルです。募集中ファンドへの投資申込を行った際に作成され、運用開始日を迎えるまではウォレット残高に影響を与えません。また、その期間はキャンセルや金額の変更を行うことも可能です。

                                        Table "investment"
     Column     |           Type           | Collation | Nullable |             Default
----------------+--------------------------+-----------+----------+----------------------------------
 application_id | bigint                   |           | not null |
 user_id        | bigint                   |           | not null |
 amount         | numeric                  |           | not null |

investmentは運用開始後に作成される運用資産を表すテーブルです。investment_applicationのIDをPKとします。このレコードの作成時にウォレット残高から運用資産に残高が移行します。

                                        Table "card_payment"
     Column     |           Type           | Collation | Nullable |             Default
----------------+--------------------------+-----------+----------+----------------------------------
 id             | bigint                   |           | not null | nextval('card_payment_id_seq'::regclass)
 user_id        | bigint                   |           | not null |
 amount         | numeric                  |           | not null | 

card_paymentはカード支払いを管理するためのテーブルです。カード利用額の支払い処理で作成されます。詳しい内容は割愛しますが、実際にはcard_paymentが作成される前段にVisaから届いた決済確定電文を表すテーブルとそれを集計するテーブルが存在し、それを元にcard_paymentが作成されます。支払い額は、まずウォレット残高から差し引き、不足分は投資資産から相殺するという順番で充当されます。

                                         Table "wallet_snapshot"
     Column                 |           Type           | Collation | Nullable |             Default
---------------------------+--------------------------+-----------+----------+----------------------------------
 id                        | bigint                   |           | not null | nextval('card_payment_id_seq'::regclass)
 user_id                   | bigint                   |           | not null |
 previous_snapshot_id      | bigint                   |           |          | 
 amount                    | numeric                  |           | not null |

wallet_snapshot は、ウォレット残高を管理するためのテーブルです。ウォレット残高に変動があるイベントが発生すると、その時点の最新残高を反映した wallet_snapshot が作成されます。 previous_snapshot_idには前回のwallet_snapshotのIDを持たせて残高の推移を表現するようにしました。

以上が主要なテーブルの概要です。次に、ウォレット残高を投資やカードとどのように関連付けるかについて説明します。 ウォレット残高が必ず動くイベントが発生する際にはイベント管理用テーブルにwallet_snapshot_idをFKとして持たせて、そうではない場合は中間テーブルを介してウォレット残高とイベントを紐つけるようにしています。 例えば、depositは入金時に必ずウォレット残高に反映されるため、wallet_snapshot_idをFKとして持ちます。投資とカード支払いについてはウォレット残高から充当された場合のみ、中間テーブルを介してイベントとウォレット残高の変更が紐つけられます。

このようにすることで、資金の流れや原資の管理が容易となりました。 例えば、ウォレット残高や再投資から充当された運用資産の内訳は、investment テーブルとそれに紐づく中間テーブルを辿るクエリで算出可能です。

ここで簡単に実装例も書いておきます。 ウォレット残高から投資を開始するコードはこんな感じです。

※カンムではGoを使っています。

// トランザクションを開始
tx, err := db.Begin()
// 必ずロールバックするように設定
defer tx.Rollback()

// ユーザーIDでロックを取得
if err := LockUser(tx, userID); err != nil {return}

// 申込を取得してInvestmentテーブルに登録
application, err := GetInvestmentApplication(tx, userID)
if err != nil {return}

investment := Investment{
    ApplicationID: application.ID,
    Amount:        application.Amount,
}
if err := investment.Create(tx); err != nil {return}

// 最新のウォレットスナップショットを取得
previousSnapshot, err := GetLatestWalletSnapshotByUserID(tx, userID)
if err != nil {return}

// 新しいウォレット残高を計算し、ウォレットスナップショットを登録
newWalletAmount := previousSnapshot.Sub(investment.Amount)
walletSnapshot := WalletSnapshot{
    UserID:             userID,
    WalletAmount:       newWalletAmount,
    PreviousSnapshotID: previousSnapshot.ID,
}
if err := walletSnapshot.Create(tx); err != nil {return}

// 中間テーブルの作成
r := WalletSnapshotInvestment{
   WalletSnapshotID: walletSnapshot.ID,
   InvestmentID: investment.ID,
}
if err := r.Create(tx); err != nil {return}

// コミット
if err := tx.Commit(); err != nil {return}

スナップショットを用いた他機能への接続は、ロックのとり忘れやスナップショットとイベント管理用テーブルの作成忘れなどに注意する必要がありますが、以下のメリットもあるように思います。

  • ウォレット残高の参照を最新のwallet_snapshotを取得すれば良いので計算量が抑えられる
  • 過去のある時点のウォレット残高の算出が容易となった
  • 残高を都度更新する必要がない
    • たとえば、「ウォレット」というテーブルに残高(balance)を保持し、入出金や支払いのたびにアップデートをかけるといった実装が不要

設計思想

最後に、ウォレット残高をこのような設計で管理することに至った背景についてお話しします。 ウォレットは、ユーザーに必ず1つ割り当てられる、いわばリソース型のエンティティです。 しかし、ウォレット残高自体は入金、投資運用、カード支払いといったイベントの履歴から計算可能であり、以下のようなクエリで表現できます。

SELECT COALESCE(SUM(amount), 0)
    FROM (
      SELECT amount
      FROM deposit
      WHERE user_id = $1
    UNION ALL
      SELECT -amount
      FROM investment
      WHERE user_id = $1
    UNION ALL
      SELECT -amount
      FROM card_payment
      WHERE user_id = $1
  ) AS _

このようにイベントの履歴のみを記録しておく設計では、専用の残高管理用テーブルを用意する必要はありません。このアプローチは実際に弊社の別プロダクト「バンドルカード」でも採用しており、詳細はこちらでご紹介しています。

一方で、 Pool はスナップショット形式ではあるものの残高を実体として管理する設計を採用しています。背景としてはウォレット残高がPoolというサービスを支える基盤的な機能であり、他の機能と疎結合な状態である必要があると考えたからです。 投資やカードといった機能は、法律上ウォレットを介することで初めて成立します。この構造は、Poolというサービスが存在する限り変わることのない重要な要件です。現在は投資とカードが資金の移動先として存在していますが、将来的に「保険」など新しい機能が追加される可能性もあれば、「カード」機能を削除する必要が生じることも考えられます。こうした変化に柔軟に対応するためには、ウォレット残高を他の機能から独立させた疎結合な設計が求められました。

さらに、ウォレット残高はサービスの中心として、単に機能を支えるだけでなく、多様な資金の流れを生み出す役割も果たします。この資金の流れを適切に管理することは、会計上の重要な課題でもあります。そのため、ウォレット残高を独立したエンティティとして定義し、各イベントをそこに紐づけることで、資金の流れを管理できる設計を目指しました。

要するに、ウォレットを他の機能から切り離すことで、法的・会計的な要件に柔軟に対応しつつ、拡張性の高い設計を実現することが今回のポイントでした。 こうした考えを踏まえ、スナップショットを活用して他の機能と接続する設計を選択するという結論に至りました。

まとめ

以上、この記事では、Poolが提供する「ウォレット」「投資」「カード」という機能の概要と、それらをDBでどのように管理しているかについて説明しました。 特に、スナップショットを用いた管理は、残高の推移や他機能への資産の流れを把握しやすくする上で大きな利点がありました。もちろん、残高の設計は一筋縄ではいかず、メリットとデメリットの両面がありますが、現時点ではこの設計を採用して正解だったと感じています。

最後に

カンムではソフトウェアエンジニアを募集しています。ご興味ある方はぜひご連絡ください!

herp.careers

エンジニアによるFintech法律勉強会を開きました

はじめに

ソフトウェアエンジニアの hata です。先日、カンムが提供するプロダクトの一つ、 Pool を取り巻くFintech法律勉強会を開きました。

pool-card.jp

金融は最も規制が厳格で複雑な分野です。金融サービスは、複数の法律から最適な組み合わせを選ぶことが競争優位になりえます(ルールに内在する「余白」を正しく理解し、自らのビジネスに最大限有効・有利に活用するという発想)。

Poolは、他にあまり見ない、投資・決済が一つになったサービスを提供しており、その法的構成も非常にユニークです。

ただし、その法的構成のユニークさから、Poolがどのような法律を組み合わせたプロダクトであるのか、非リーガル関連職のメンバーにとってはとっつきにくい内容となっています。社内に勉強できる資料はいくつかあるのですが、初学者向けにまとまった内容になっていませんでした。

そこで、Poolをとりまく法規制を整理し、非リーガル関連職でもスムーズに理解できるように勉強会を開くことにしました。

なぜソフトウェアエンジニアが法規制について学ぶ必要があるのか

  • 法規制の知識がないと、画期的なプロダクトの改善施策を考えたとしても、その案が今の法的構成では実現不可能な場合があります。法規制について良く知ることで、やってはいけないことを回避できることはもちろん、逆に規制を活用したアイデアが議論の中で生まれるかもしれません。
  • ソフトウェアエンジニアリングは、与えられた制限の中での最適解を見つける活動でもあります。法規制やレギュレーションはその制限の一つであり、それを理解しソフトウェアに落とし込んでいくことでエンジニアとしてのスキルも上げられると考えています。

つまるところ、エンジニアがドメイン知識を身につけることで賢くなると、エンジニア自身もチームも嬉しい、というわけですね。

社内にあるホワイトボードの図を借りると、Fintechに関わる法律をプロダクトに関わるメンバー全員が勉強することで「いこうぜ」と「わーい」の第一象限に当てはまるということです。

社内ホワイトボードに描かれたMECEについての図

勉強会の開催にあたり、気をつけたこと

法律分野の用語や法的整理は非常にナイーブです。JavaJavaScript が異なるように、曖昧なまま理解をすると非常に危険です。金融サービスを作っている以上、法律の誤用は最悪の場合ユーザーの金融資産への侵害につながります。

私はあくまでソフトウェアエンジニアなので、間違った解釈を広めないよう、勉強会に使う資料は事前にリーガルチームにチェックしてもらいました。また、勉強会にも同席してもらい、私が誤ったことを口走ろうものならツッコミを入れてもらうようにしました。

実際の様子

勉強会の様子。弊社はリモート組織なので、Google Meetにて開催。

内容的に全てを公開することはできませんが、以下のような構成で行いました。

  • Pool の各機能(ウォレット・決済・投資)を取り巻く法規制(例:資金決済法、割賦販売法、金融商品取引法)の整理
  • 各法律に関するライセンス(前払式支払手段、第二種金融商品取引業)の解説
  • サービス全体を取り巻く法規制(犯罪収益移転防止法)
  • Poolの機能に関する法的な観点をQ&A形式で掘り下げ

以下は、勉強会の資料の一部です。

おわりに

元々は、Pool のメンバーが増えてきたこともあってオンボーディング的にやろうとしたのがきっかけでした。勉強会参加者の方々からは、好意的な感想をもらえたのでよかったです。

単発の勉強会で限られた時間で全てを理解するのは難しいとは思うのですが、今までは勉強するためのとっかかりすらなかったので、後学のために資料を残す機会を作れたのは個人的に良かったなと思っています。

カンムではソフトウェアエンジニアを募集しています。ソフトウェアエンジニアリングにとどまらず、Fintech最前線に立ってあらゆる知識に触れる機会があるおもしろポジションです。少しだけでも話を聞いてみたいという方でも大歓迎なので、ぜひカジュアル面談でお話ししましょう。

herp.careers

Tauri と Pixoo-64 でオンラインミーティングのカメラの映り込みリスクを軽減する

こんにちは、リモートワークしてますか?私は週7家にいます。エンジニアの岡田です。

この記事はカンム Advent Calendar 2024の24日目です。1 23日目は teshiken さんの「成果を出しつづけるための行動として意識していること」でした。 adventar.org

さて、カンムはフルリモートを導入していまして2、自分はもうかれこれ4年ほどずっと家で仕事をしています。 ミーティングではカメラを ON にすることもままあるのですが、家族も家で仕事をしているので、お互いカメラへの映り込みに注意して生活しています。 ただ、お互いにミーティングの予定は何らかの形で共有しているものの、突発的なミーティングもあったりするので、つい意識せずにフラっと映り込んでしまいそうになることも稀によくある状態です。

…ということで、今回はこのフラっと映り込みリスクを軽減することを目指して、ちょっとした仕組みを作った話をしようと思います。

先人の知恵にのっかる

当然のごとく同じようなことを考えている方がいました。 yoshiori.hatenablog.com

とてもシンプルで良さそうだったので、この考え方にのっかって以下の方針とすることにします。

  • カメラの ON / OFF を検知する
  • カメラの ON / OFF に連動して何か表示する

ただ、記事内のカメラ ON / OFF 検知方針だと、自分の環境の MacBook でうまくいかなかったので、そこはまた以下の知見にのっかって回避することにしました。 ON / OFF のイベントログを監視するだけなので、現在の使用状況まではリアルタイムでわかりませんが、常駐監視する前提ならいったんはこれでも十分そうです3

stackoverflow.com

log stream --predicate '(eventMessage CONTAINS "AVCaptureSessionDidStartRunningNotification" || eventMessage CONTAINS "AVCaptureSessionDidStopRunningNotification")'

つくる

先人とまったく同じものでは面白みが薄いので、少しだけアプローチを変えてみます。

我が家にはピクセルアートフレームの Divoom Pixoo-64 がある4ので、これを活用することにします。 Pixoo-64 には API もあるのでちょうど良さそうです。

また自分以外の家族が使うことを想定して GUI を提供することにします。メニューバーに常駐するアプリだとそれっぽいですし。

そして、せっかくなので使ったことないものをってことで、今回は Tauri を使ってみることにします5。 ついでに初めての Rust 体験もできるので一石二鳥です。

v2.tauri.app

ということで、方針を以下のように定めることにします。

  • カメラの ON / OFF を検知する
  • カメラの ON / OFF に連動して Pixoo-64 にそれっぽい GIF を表示する
  • メニューバーに常駐するアプリとして提供する (Tauri で実装する)

そして、できあがりがこれです。 github.com

つかう

こんな感じで動きます6

カメラをONにすると海苔巻文鳥がON AIRに切り替わります

部屋に入った瞬間に目に飛び込んでくるので、疲れていてもカメラの存在に多少気付きやすくなったんじゃないでしょうか7

ドアをバーンしました

おわりに

久しぶりに個人でそれっぽいものを作った気がします。 家でいつも目にするもの、動きがダイレクトに返ってくるものはやっぱりいいですね。

Rust も今回完全に初めて触ったのですが、そこそこ楽しく開発することができました。 公式ドキュメントをろくに読まず、AIのひねり出すコードに質問しながら知識のつまみ食いをしつつ実装してしまったので、次はちゃんと公式ドキュメントを読んで Rust らしさを学んでからコードをリファクタしていこうと思います。

趣味プログラミングはいいですね。

おわり


  1. 完全に遅刻しました。やっちまいです。
  2. https://team.kanmu.co.jp/8de05f77ac8d487e8cf44f0e74da2c23
  3. 改めて調べてみると Objective-C 経由で検知する方法もありそうなので、objc2 クレートを使ってワンチャンもっと良い感じにできるかもしれません。というか最初から Swift で作っとけばよかった説まであります。
  4. 2台あります。購入数ヶ月で液晶に縦線が入ってしまい、新品交換 & 故障品返送不要となって手元に2台が残りました。故障品には縦線が入っているだけなので普通に使っています。
  5. 他の選択肢として Electron / React Native macOS / Swift でふつうのアプリ開発、もしくはワンチャンExpoもいけたりするかな…とか考えてはいたのですが、初めて度が一番大きいやつが Tauri (w/Rust) だったので、テンションが上がるか否かだけで選びました。
  6. 本当は「ワシじゃよ」な某博士が「カメラがオンじゃよ」と語りかけてくる感じの GIF を作りたかったのですが、諦めて Divoom コミュニティ内に公開されている ON AIR アニメーションを使いました。
  7. 実際は複数人で使用するとたぶんうまく動きません。カメラの ON / OFF イベントオンリーでのトリガーかつ状態の同期をしていないので、複数台で同時にカメラを使っていると、1人がカメラを OFF にすると残りの人がカメラ ON のままでも GIF が表示されなくなってしまいます。…が、めったにないのでいったん気にしないことにします。

バンドルカード開発チームの現在地

カンム 2024年アドベントカレンダーの22日目です。
adventar.org

昨日はAwesomeさんの「銀行員が転職した話」でした。 Awesomeさんは今年カンムへ転職してきてくれたのですが、新規事業である「サクっと資金調達」の立ち上げに関わっていただいています。カンムは今までtoCサービスを中心に事業展開してきましたがこれからはtoBに関しても課題解決をしていきます。
note.com

今回の私の記事ではバンドルカードのプロダクト開発が1年前からどのように変化してきたかを書きます。

私について

私については入社した年に書いた紹介記事があるのでこちらをご覧いただければ幸いです。 michiomochi.com

ここからのアップデートとしては2024年10月にもう少し範囲の広いB2C Divの開発全体を統括する立場のディレクターに役割変更しました。
今の役割で取り組んでいること等について最近書いてもらった記事があるのでこちらもご覧いただければ嬉しいです。
note.com

バンドルカード開発チームとは?

カンムは現在以下の3つのプロダクトを運営しています。 vandle.jp pool-card.jp lp.sakutto-funding.jp

今回の記事ではこの中のバンドルカードを開発するチームについてお話します。

2023年末まではどのようにプロダクト開発を行っていたか?

バンドルカード開発チームでは2023年末まで以下のような形でプロダクト開発を行っていました。

2023年末までのプロダクト開発体制

  • プロジェクト毎にチームを作り、プロジェクトが完了したらチームを解散する
  • どんなプロジェクトを行うかは事業企画チームで立案や検証が行われ、やることが決まった段階でプロジェクトチームが作成される
    • プロジェクトチームはやることが決まった施策を実施することが責任範囲
    • プロジェクトによる効果検証については効果検証を行う環境を整えるところまでがプロジェクトチームの責任範囲で、実際に効果検証を行いレポートする責任を持つのは事業企画チーム
      • 効果検証の結果に応じて事業企画チームの判断の元、追加で新しくプロジェクトを行ったりする
  • プロジェクト完了後はプロジェクトに関する情報は各職能チーム(e.g. バックエンドチーム, CSチーム)に共有され、トラブルや効果検証といったタスクについてはカテゴリー毎に適当な職能チームで対応を行う

良かった点

この開発スタイルで良かった点としては以下の点が挙げられます。

  • やることが明確で議論の余地がないものについて最速で実行できる
    • 何をやるのかを決めるチームとそれを実行するチームを明確にわけることで各領域においての専門性を高め、明確な作業分担ができることで最速で実行が可能になる
    • なので「プロダクト設立初期」や「やることが明確なもの」とは非常に相性が良い

課題に感じていた点

前述した通りこの開発スタイルには良い点があり、実際に今までうまくいっていて会社としても売上を順調に積み上げることができていました。
ただこれからチーム規模や事業規模を拡大していこうとしている現在において、以下の点の課題が大きくなってきました。

  • 何をやるかを決めるプロセスに事業企画チーム以外の視点が入りにくいため、現時点では未知なものであったり多角的な視点での施策策定が難しい
  • 事業企画チーム以外の他メンバーが何を行うかのプロセスに介入するのが難しいので「やらされてる感」を持ちやすい
  • プロジェクト毎にチームが作成され解散するので、各メンバーはどのKPIに対して注力すればよいのかが明確ではない
    • なので各メンバーの目標や評価指標としてはプロジェクトを最速で完了させられたかに収束しがちである
      • 本来評価したいものとしてはプロダクト開発においてのアウトカムに寄与できたのかということなので少し乖離する

2024年からどのようにプロダクト開発を行っているか

2024年からは以下のような形でプロダクト開発を行うよう体制変更を行いました。

2024年からのプロダクト開発体制

  • 職能を横断したチームを作り、そのチームでプロダクト開発を行う
  • 各チームには追うKPIを定める
  • 各チームはプロジェクト毎に解散されるといったことはなく、長期的に存続し定められたKPIを向上させていくことに責任を持つ
    • 短期間でチームが解散することはありませんがメンバーの異動等は適宜発生する
  • 各チームがKPIを向上させるためにやることの決定権は各チームが持つ
    • 何をやるかはチーム内のメンバー全員で意見を出し合い決める
    • 各チームにはPdMがアサインされ、主にPdMがチームのリードを努める
    • 何をどのような優先順位でやるかはPdMがリードして決めるが、決めるための意見やフィードバックについては各メンバーが積極的に行う
    • チームの持つKPIがどれだけ向上したかが各メンバーの評価に繋がる
    • メンバーの目標は所属するチームが持つ目標を上位目標とし、紐づかせて設定する

なぜこのような体制変更を行ったのか

前述した通り明確にやることが見えている状態でそれを実行するにおいては2023年末までの体制のほうが都合が良い点が多くあったと思います。
ですが今後チーム規模を拡大し機能開発や新規事業の立ち上げを複数実施していく中においては、各チームが自律的に動き重要なKPIを向上していける体制を作ることが必要でした。このような体制変更をすることで仮説検証を素早く回していかないとたどり着けないような未知の課題に対しての解決も行っていけるようになるはずです。
具体的にはこの体制変更を行うことで以下のような良い効果を期待しました。

  • プロダクトにおける各KPIの向上について各チームが各メンバーの多角的な視点で主体的に動くことで、改善や計測を継続的に行えるようになり自立したプロダクトチームとなる
  • バンドルカード全体のミッションや目標が各チームの目標に繋がると同時に、各チームの目標が各メンバーの目標に繋がるようになる
    • そうなることでメンバーがどのように動くことを期待されているかが明確となり成果を出すための動きがしやすくなる

この開発体制で1年プロダクト開発を行った結果

この体制で1年ほどプロダクト開発を行ってきた結果ですが成功だったのではないかと思っています。
前述した期待する効果としては概ねその通りになったと感じています。
各チームが持っているKPIが何なのかが明確になり、開発チーム全体がそれを前提としたコミュニケーションができるようになりました。またチームが持つKPIが明確になったためそれぞれのチームがKPI向上に向けて自律的に行動ができるようにもなりました。
全体へ進捗を共有し合うような定例において各チームがKPIの現在地であったり向上施策や向上に至るまでのロードマップ等を共有する動きが見られるようになり全体が前進しているように感じます。

いい例として、先日バンドルカードでボーナスタウンという大きめな機能のリリースを行ったのですが、以前であればリリース後にチームは解散し、誰がその後の向上施策を行うかといったことが明確ではない状態になっていたと思いますが、現在はリリース後もシームレスに同じチームで課題の整理やロードマップが能動的に作成されリリース後にどのようにすればより使ってもらえるようになるかといったKPI向上のための動きがなされています。 kanmu.co.jp

これから改善していきたいこと

ただ全てがうまくいったということはもちろんなく、まだうまくいっていないこともあります。

期待していた効果として挙げた「バンドルカード全体のミッションや目標が各チームの目標に繋がると同時に、各メンバーの目標が各チームの目標に繋がる 」については各チームの目標をどのように各メンバーへ接続するのかを模索している状態で未だ運用に上手く乗せることができていません。
各チームが持つ目標は決められていますが、その目標に対し各メンバーがどのような形で貢献するか、そしてそれをどのように目標として置くかをどのように決めるかが難しく感じています。各チームをリードするのはPdMですがPdMは必ずしも各職種に関する深い知識を持ち合わせているわけではないのでそれぞれのメンバーの目標設定までを担当するのは難しいのではないかという課題があります。
なので現状は評価をする際には各チームのリード(主にPdM)が各職種のマネージャーへチームでの動きをレポートし、そのレポートを元に職種のマネージャーが評価を行うという形をとっています。

今後どのように目標設定を行っていくかといった部分に関しては改善していきたいと考えています。

さいごに

カンムではこのようにその時々に応じてどのようなプロダクト開発体制が最適なのかを考え、柔軟に体制変更が行える会社です。
このような変化は非常に大きいものでありこんなに早く適用し運用できるとは思っていませんでした。
ただこれがいつまでも最適な形だとは思っていなく今後もうまく行っていない点を継続的に見つけ、チューニングしていくことが必要だと思っています。カンムはそれができる組織です。
カンムはソフトウェアエンジニア、デザイナー、PdMといった職種を中心に積極的に採用を行っています。
もし少しでもご興味があればぜひカジュアル面談でお話できれば幸いです。 team.kanmu.co.jp

BCD を取り回すライブラリを Rust で書いた

エンジニアの佐野です。文字列および数値を BCD と相互変換するライブラリを Rust で書きました。

github.com

crates.io にも公開しました https://crates.io/crates/bcd-convert

BCD

BCD は Binary-coded decimal の略で日本語では二進化十進表現などとも呼ばれます。

カンムの業務で言うとクレカ決済のデータを扱っているとたびたび出現します*1。例えばカード決済のデータをやりとりする ISO8583 というプロトコルではカード番号は 0x40, 0x19, 0x24, 0x99, 0x99, 0x99, 0x99, 0x99 のようなバイト列で表現されたりするのですが、このような処理をする際は1バイトの上位4ビットと下位4ビットに現れる数字列を分解して 4019249999999999 と解釈してカード番号を得ます*2

このライブラリを使うと BCD <-> 文字列は次のように相互変換できます。

#[test]
fn test_bcd_to_str() {
    let bcd = BcdNumber(vec![0x40, 0x19, 0x24, 0x99, 0x99, 0x99, 0x99, 0x99]);
    let s = bcd.to_string();
    assert_eq!(s, "4019249999999999");
}

#[test]
fn test_str_to_bcd() {
    let s = "4019249999999999";
    let bcd = s.parse::<BcdNumber>().unwrap();
    assert_eq!(
        bcd,
        BcdNumber(vec![0x40, 0x19, 0x24, 0x99, 0x99, 0x99, 0x99, 0x99])
    );
}

詳しい使い方は Docs.rsGitHub を見ていただくとして、 u64 や [u8] のような生バイト列との相互変換もサポートしています。

なぜ Rust

カンムのメイン言語は Go, TypeScript, Python なのですがたまには新しい言語を学びたくなってくるものです。その一環として自分は Go で書かれた決済システムの一部を Rust で書き換えるという個人プロジェクトを始めました*3。ゴールは Rust への書き換えと私自身の満足です。

チュートリアルを1.5周ほどやったあとに AI に手伝ってもらいながら書いたのですがまだ勘所がわからず四苦八苦している状態です。もっとうまい書き方や「らしい」書き方があるかもしれません。

おわり

*1:このような形式で送受信するケースがある

*2:これはもちろん存在しないカード番号で今後も発行されることもないカード番号です

*3:カンムでは今のところ Rust をプロダクションに導入する計画はなく、あくまでも私が自分の目標のために始めました。忙しくなったを言い訳にして頓挫する可能性もあります。

技術者倫理のキホンという本を読みました

はじめまして。プラットフォームチームの高山です。

この記事は カンム Advent Calendar 2024 の18日目の記事です。

adventar.org

最近読んだ本がなかなか興味深かったので、ちょっとご紹介できればと思います。

「技術者・研究者のための 技術者倫理のキホン」と言う本です。

この世の中、様々な方の研究があり、それに付随する形で技術の発達・進展があります。そして、それを実装していく際に「倫理」みたいなものは大変大事だなぁと思っています。

エンジニアとしても「それはちょっと…」みたいな企画・仕様に出くわす時はたまにあります。その際、自分がその気持ちをきちんと共有し、線引きをすることも大事な仕事だと思っています。改めて知識を整理する上でも、この本を読んでみることにしました。

全体的にはタイトルどおり、技術者が持つべき倫理観について様々な視点から書かれているのですが、ここでは「4章のモラル」について抜粋して書きたいと思います。 ただ、この章の内容は倫理観というより、チームとしてどのようにしていくのが良いか、というところに繋がる感じだったので、その点を念頭に置いてお読みくださいませmm

モラルには8種類ある

そもそもモラルとは、手元のMacに付属しているスーパー大辞林

道徳。倫理。また,人生・社会に対する精神的態度。

とあります。

そのモラルを、この本では「〜であってはいけない」という意味で以下の8つ示しています

  • 利己主義:他人よりも自分を優先し、個人的な利益を追求すること

  • 自己欺瞞:自分の言い訳を信じ込み、現実を歪めて自己を欺くこと

  • 意思薄弱:正義や正しい判断を実行する勇気が欠け、他人の意見に流されること

  • 無知:知識や知恵を得る努力を怠り、適切な判断を下すことができないこと

  • 自己本位:他人の視点や意見を無視し、自分だけの立場や利益を優先すること

  • 狭い視野:物事の一面のみに固執し、全体的な視野を持たずに判断すること

  • 権威追従:自らの判断力を放棄し、権威や上司の意向に従うこと

  • 集団思考(浅慮):集団内の価値観や意見を優先し、独自の判断を怠ること

田中和明. (2023). 技術者・研究者のための 技術者倫理のキホン. 秀和システム. p.52

このように8つのモラルが挙げられているのですが、このうち、利己主義〜権威追従までの7つは自分自身のお話です。 自分が何か行動をしよう(した)時に、このような事に陥っていないか?陥っているとしたら、上のモラルと照らし合わせてどのようにギャップを埋めて行けばいいか?を心に置きつつ、自分の改善を少しづつでも続けるのが大事だなと思います。(とはいえ、人間なので難しい面はいっぱいありますが💦)

ただ、8つ目にある「集団思考(浅慮)」に関しては自分自身だけではありません。集団(組織 or チーム)全体の話になってくると思います。

集団思考(浅慮)とは何だろう?

みなさんも経験があるかもしれませんが、組織やチーム内で意思決定をする際、一人ひとりは冷静に考えられるはずなのに、なぜか全体ではおかしな方向に進んでしまうことはないでしょうか。

これが「集団思考(浅慮)」と呼ばれる現象です。組織の結束力が高まるほど、メンバーは内輪の雰囲気に流され、十分な検証や異なる意見の考慮を省略してしまう傾向が強まってしまうことです。

主な特徴として、下記のような事があげられています。

  • 過大な自己評価とオールマイティ幻想:集団内では、組織やグループに対する過大な自己評価や、自分たちの判断力があらゆる状況に対応できるというオールマイティ幻想が広がる。

  • 集団への自己弁護と外部への偏見:集団内では自己弁護が行われ、集団外部への偏見が生じることがある。他の意見や視点を排除し、集団内の考えが優越的であると信じ込むことがある。

  • 組織の均一性の維持:集団浅慮の集団は、組織内での均一性を維持しようとする。外れた意見や行動がないかを監視し、集団の結束を保つために誘導することがある。

  • 代替案の不考慮と情報の偏り:集団内では、代替案を考慮せず、目標や対策の選択肢の危険性を無視することがある。情報の収集が不十分で、偏りのあるデータを元に判断がされることがある。

  • 事態に対する計画の不足:集団思考の状態では、非常事態に対する十分な計画が策定されず、適切な対応ができない可能性がある。

田中和明. (2023). 技術者・研究者のための 技術者倫理のキホン. 秀和システム. p.68

エンジニア視点で集団思考(浅慮)を考える

先ほどの特徴として挙げられている事をエンジニア視点で考えた時に、どこかあてはまる事はないでしょうか?

あくまで個人的意見にはなりますが「全てそのまま」はあてはまらずとも、形を変えて1つ、ないしは2つは経験としてはあるのでは?と思っています。

インフラ寄りの例にはなるのですが、よくありがちな例として

  • 障害発生時の1次対応が、思い込みからあらぬ方向にいってしまう
  • 作業手順や障害対応手順などのドキュメントに関するレビューが機能していない(レビューによる指摘がされない、もしくは指摘しづらい)
  • 外部の人から指摘を受けた改善すべき点に大きな抵抗力が出てきてしまう

というようなことがあるかなと思います。

もちろん、上記の例において、だれも悪意はもっている訳ではないと思います。ただ、そのような場合でも意図せずとも発生することはあると思います。

できるだけ集団思考(浅慮)を避けるには

ではでは、どのようにすれば、集団思考(浅慮)から避けれるのでしょうか?

個人的には「Team GeekGoogleギークたちはいかにしてチームを作るのか」で書かれているHRTの原則が大事だと思っています。

  • H(Humility:謙虚)
    • 世界の中心は君ではない。君は全知全能ではないし、絶対に正しいわけでもない。常に自分を改善していこう。
  • R(Respect :謙虚)
    • 一緒に働く人のことを心から思いやろう。相手を1人の人間として扱い、その能力や功績を高く評価しよう。
  • T(Trust:信頼)
    • 自分以外の人は有能であり、正しいことをすると信じよう。そうすれば、仕事を任せることができる。

Brian W. Fitzpatrick (著), Ben Collins-Sussman (著), 及川 卓也 (解説), 角 征典 (翻訳). (2013). Team GeekGoogleギークたちはいかにしてチームを作るのか. オライリージャパン p.15

この原則の上で、心理的安全性を保ち、多様な意見を促す仕組みを作ることが集団思考を避けることに繋がると感じています。

たとえば、今まで、自分がよくやっていた取り組みとしては下記のようなものがあります。

  • ファシリテーターのローテーション
    • 常に同じ人がファシリテーションを行うと、その人が「進行役としての権威」を持ちやすくなり、参加者も無意識にその人の期待する方向へ発言を寄せてしまう傾向がどうしても生じやすくなります。これを避けるために、ファシリテーターをローテーションすることにより、チーム内でよりフラットな関係性を築きやすくなります。
  • 非難なきポストモーテム文化(Blameless Postmortems)
    • 障害発生後の振り返りは、「誰が悪いか」ではなく、「何が起こり、なぜそうなったか」を冷静に明らかにする場の空気づくりをしましょう。批判や個人攻撃を避けることで、メンバーは安心して失敗や問題点を共有しやすくなるかなと思います。
  • 小さく始め、成功体験を積む
    • 大きな改善提案よりも、小さく改善して、少しづつ成功体験を繰り返していくことで、多様な意見をどのように改善に結びつけていくのが良いか、という仕組み作りをチームで行なっていけるようになっていきます。

その組織(チーム&メンバー)によって最適な取り組みは様々ですし、何が正解!というのは一概に言えるものではないです。が、ちょっとした仕組みと気遣いで、色々なことが少しづついい方向に変わっていくような取り組み方はきっとあるはずなので、一気に変えずに、一つ一つ少しづつ進めるのがいいかなと思います。

まとめ

当初の本のテーマから、少しエンジニアチームの例に目線を移した話になってしまいましたが、「技術者・研究者のための 技術者倫理のキホン」には品質管理からコンプライアンスBCP、安全対策といったITエンジニアにとっても割と身近なテーマが扱われていますので、興味がある方は一度書店で手に取ってみて、年末年始の帰省など移動の際に読んでみてはいかがでしょうか〜

採用について

カンムではこんな僕たちと一緒に働いてくれるエンジニアを募集しています! カンムテックブログを見て、少しでも興味を持たれた方、ぜひぜひ応募してください!一緒に働きましょう!

team.kanmu.co.jp