Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

OpenTelemetry Collector導入のPoCと今後に向けて

この記事は、Gaudiyが実施している「Gaudiy Advent Calendar 2022」の20日目の記事です。

はじめまして、Web3スタートアップのGaudiyでエンジニアをしているsato(@yusukesatoo06)です。

GaudiyではGoでのBE開発をメインで担当しており、前職ではNode.jsでのBE開発やFlutterでのモバイルアプリ開発を行っていました!

今回は、Gaudiyのインフラ環境でOpenTelemetry Collector導入のPoCを行ったので、その調査経過を書こうと思います(残念ながらまだ導入には至っていません…)

OpenTelemetry Collectorの記事はあまりないと思うので、導入を考えている方の手助けになれば幸いです🙇

1. Gaudiyのアーキテクチャが抱える課題

さて、本題に入る前にGaudiyのアーキテクチャをご紹介します。

https://speakerdeck.com/winor30/gaudiy-fanlink-architecture?slide=5

このように、BEに関してはGCP Cloud Runベースのマイクロサービス構成となっています。

そのため、マイクロサービスが抱える一般的な課題を、Gaudiyも漏れなく抱えていました。具体的には以下のような課題がありました。(マイクロサービスを扱ってる組織ならおそらく一度は悩むはず…)

  • マイクロサービスに分散したログをリクエスト単位で追うのが難しい
  • エラーが起きた際にどこのサービスが原因でエラーが起きたかわかりずらい
  • レスポンス遅延が起きた際に、どのサービスがボトルネックなのか把握しづらい etc.

その解決に向けた活動の一環が、今回のCollector導入の背景になっています。

2. 解決策としてのObservability

そして、このなかで話題に上がったのがObservability(オブザーバビリティ)です。

オブサーバビリティとは、システムを監視するのではなく「システムの内部で何が起きているのかを説明できる状態を作ること」を指し、クラウドベースのシステムやマイクロサービスを運用するにあたって注目が集まってる領域です。

(参考文献:https://pages.awscloud.com/rs/112-TZM-766/images/20221027_23th_ISV_DiveDeepSeminar_Observability.pdf

このオブザーバビリティは、以下の3本柱で構成されています。

  • メトリクス
    • 特定期間のデータの集計値 (何が起きたか)
  • トレース
    • リクエスト、トランザクションの経過観察 (どこで起きたか)
  • ログ
    • 特定地点の記録を示す (なぜ起きたか)

これを実現する方法はいくつかありますが、GaudiyではOpenTelemetryというプロジェクトのライブラリを利用することに決定しました。

3. OpenTelemetryとは

OpenTelemetryとは、Cloud Native Computing Foundation(CNCF)でKubernetesに次いで2番目に活発なプロジェクトです。

以下がCNCF内のプロジェクト毎のvelocityなのですが、OpenTelemetryが活発に開発されているのがわかると思います。

https://github.com/cncf/velocity

https://docs.google.com/spreadsheets/d/1JdAZrQx52m3XVzloE7KK5ciI-Xu-P-swGxdV3T9pXoY/edit#gid=976519966

OpenTelemetryを簡単に説明すると、メトリクス、ログ、トレーシングを包括的に扱うためのプラットフォームで、様々な言語に対応したOSSのライブラリが用意されています。

また元々存在していたOpenMetricsOpenCensusというプロジェクトが統合されたプロジェクトでもあります。

このOpenTelemetryの使い方については、andoさん(@Andoobomber)が24日目の記事で紹介してくれます。

4. OpenTelemetry Collectorとは

4-1. 概要

OpenTelemetryのライブラリは言語毎に用意されており、すごく便利に扱える一方で、プロジェクトの開発者は言語毎にライブラリの実装が必要となります。そのため、メトリクスを送る先のバックエンドサービス x プログラミング言語の数だけ対応が必要でした。

またOpenTelemetryを利用する側も、取得データに共通する加工やバックエンドサービスの変更をする際にすべてのアプリケーションに手を加える必要があり、一定のコストがかかってしまいます。

上記課題を解決するのがOpenTelemetry Collectorです。

以下がOpenTelemetry Collectorの概要図となっています。このような形で、一度OpenTelemetry Collectorに情報を集約し、その後Collectorがバックエンドサービスにデータを送る形となっています。

https://www.splunk.com/ja_jp/solutions/opentelemetry.html

共通の変換処理を行いたい場合や、バックエンドサービスを切り替えたい場合は、Collector側で設定を追加、変更することで共通的に対応することが可能です。

この辺りの設定については次のセクションで詳しく説明します。

4-2. アーキテクチャ

OpenTelemetry Collectorはデータの受信、加工、データの送信を1つのパイプラインとして定義し、処理を行っていきます。またCollectorは1つ以上のパイプラインから構成されます。

アーキテクチャは以下のようになっています。

https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md

<Pipeline>

後述するReceiver、Processor、Exporterを組み合わせたもので、メトリクス、トレース、ログそれぞれに対してPipelineを定義することができます。 またそれぞれに対して複数のPipelineを設定することも可能です。

<Receiver>

Receiverとはメトリクス等を受け取るためのパーツです。アプリケーション用にはgRPCとHTTPのインターフェースが用意されており、ライブラリを通じてやり取りすることができます。それ以外にもクラウドサービス用の専用Receiverもあり、クラウドサービス固有の情報も取得することが可能です。

※提供されているReceiver一覧はこちらで確認できます。

<Processor>

Processorは名前の通りReceiverから受け取った値を加工するためのパーツです。メトリクスやログの変換や、トレースデータにラベルを付与したり等いくつかの処理を行うことができます。また、Exporterに流す時の方法だったり流量の制御や、Collectorのメモリ利用量等の制御も可能です。

※提供されているProcessor一覧はこちらで確認できます。

<Exporter>

Exporterはバックエンドサービスにデータを送信するためのパーツです。PrometheusのようなTime Series Databaseや、GCP、DataDogのようなクラウドサービスなど幅広いバックエンドサービスをサポートしています。ここのExporterには複数のバックエンドサービスを設定できるため、一度に複数箇所に書き込むことができます。

またバックエンドサービスを切り替えたい場合は、Exporterの設定を変更するだけでアプリケーションコードに手を加えず変更することができるため、非常に便利です。

※提供されているExporter一覧はこちらで確認できます。

では次のセクションで、実際に動かすための設定を見ていきたいと思います。

5. OpenTelemetry Collectorを設定してみる

5-1. Collectorの入手方法

まずCollectorの入手方法ですが、こちらはいくつか方法があります。

一番簡単な方法はDistributeされているDocker Imageを使う方法です、以下のコマンドでイメージを取得できます。

docker pull otel/opentelemetry-collector:0.67.0

一方で自身でカスタマイズしたCollectorを使いたい場合は、こちらの専用のビルダーを使ってビルドすることが可能です。

5-2. Configuration

まず、今回のPoCで利用したConfigの全体です。(少し記事用に変更してる部分はあります。)

各フィールドの説明は以降のセクションで説明します。

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: localhost:4317 # Default

processors:
  batch:

exporters:
  logging:
    verbosity: detailed
  googlecloud:
    retry_on_failure:
      enabled: true
    project: <プロジェクト名>

service:
  telemetry:
    logs:
      level: debug
      initial_fields:
        service: poc-instance
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [googlecloud, logging]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [googlecloud, logging]

<Receivers>

ここではReceiverの設定を行います。アプリケーションからの情報取得であれば、以下のように設定するだけで完了します。

※他のReceiverの設定に関してはこちらをご確認ください。

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: localhost:4317 # Default

<Processors>

ここではProcessorの設定を行います。今回は複数データをまとめて書き込むためのbatchのみ設定しています。

※他のProcessorの設定に関してはこちらをご確認ください。

processors:
  batch:

<Exporters>

ここではExporterの設定を行います。今回はPoCということもあり、Collectorの実行環境に出力するための logging と、GCPに出力するための設定を行っています。

※他のExporterの設定に関してはこちらをご確認ください。

exporters:
  logging:
    verbosity: detailed
  googlecloud:
    retry_on_failure:
      enabled: true
    project: <プロジェクト名>

<Service / Pipelines>

Pipelineの設定はserviceフィールドで行います。以下のようにメトリクスやトレース毎に、それぞれどの定義を使うかを選択することでPipelineを組み立てます。

ここの組み合わせを変えることで、メトリクスだけ別のバックエンドサービスに流したり、トレースだけ別のProcessorを挟むことが可能です。

service:
  pipelines:
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [googlecloud, logging]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [googlecloud, logging]

<Debug用>

またserviceフィールドではログを出力する設定もあるため、Debugの際などに使ってみてください。

service:
  telemetry:
    logs:
      level: debug
      initial_fields:
        service: poc-instance

<その他>

Collectorの設定の中で同じ種類のフィールドに対して複数の定義を行うこともできます。

例えばotlpのreceiverを複数定義したい場合は、以下のように type[/name] で一意の識別子をつけることで複数定義が可能です。

receivers:
  otlp:
    protocols:
      grpc:
      http:
  otlp/2:
    protocols:
      grpc:
        endpoint: 0.0.0.0:55690

また利用する際は、以下のように定義した名前をそのまま使ってあげればOKです。

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: []
      exporters: []
    traces/2:
      receivers: [otlp/2]
      processors: []
      exporters: []

6. 実際に動かしてみる

「では実際に、上記設定でCollectorを動かしていきましょう」と言いたいところなのですが、GCPへのExporter設定をしている関係で上記設定を手元で動かすことができないため、別の環境と設定を使いたいと思います…

自分でデモ環境を用意しようと思ったのですが、OpenTelemetry内のプロジェクトに素晴らしいデモ用のコードサンプルがあったため、こちらを活用させていただこうと思います。

上記リンクからディレクトリをダウンロードして、docker-compose up -d を実行すると以下の環境が立ち上がります。

※ここにあるZipkin、Jaegerは分散トレーシング用のシステムです

https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/examples/demo

この環境は全ての情報がCollector経由でExportされているため、このディレクトリ内にあるCollectorの設定を色々編集しながらぜひ遊んでみてください。

7. 今後やっていきたいこと

PoCの中でCollectorの有用性は評価できたのですが、Gaudiyのインフラ環境がCloud Runベースのため、どのような構成でCollectorを導入するのがよいかが一番の課題となっています。

まずCollectorをCloud Runでホスティングする案が上がったのですが、Cloud Runの特性上、常時立ち上げておく必要があるCollectorとの相性が悪く断念しました。(ここはもう少し調査の余地があります...)

またインフラ環境を全てGKE上にホストする案や、Collectorだけ別の形でホスティングする案等色々出ています、がCollectorの利用が目的となる案のため保留となりました。

ですので実際の導入方法に関しては、今後継続的に模索していきたいと思っています。

8. さいごに

今回のPoCですが、実はGaudiyの以下の制度を使った取り組みでした!

note.com

Gaudiyでは毎週水曜の午後、「EMPOWER-DAY」と称して、チームや会社全体の中長期の成長につなげる様々な取り組みを行っています。直近、この時間の使い方をより効果的にするための変更が行われましたが、基本的に「チームの底上げに時間を使う」という目的は変わらずに取り組んでいます。

このような形でGaudiyには新しい技術の導入を積極的に受け入れてくれる組織文化があるので、おもしろい環境かなと思います。Gaudiyの課題と自分の技術チャレンジをリンクさせて解決してくれる仲間も募集しています!

recruit.gaudiy.com

Gaudiy Advent Calendar 2022の21日目は、同じチームの @hassey_11 さんが担当してくれます!

gRPC × Go × Node.js におけるエラーハンドリングの実現方法

この記事は「Gaudiy Advent Calendar 2022」の19日目の記事です。

Web3 スタートアップの Gaudiy でソフトウェアエンジニアをしている Namiki ( @ruwatana ) です 🙂

ワールドカップを楽しみすぎていたら(日本代表もそして数時間前の決勝戦も本当に最高でした 🙌)、あっという間に自分の番が来てしまったわけですが、先月個人にフォーカスした 入社エントリ を書かせていただいたのもあり、今回は直近取り組んでいた gRPC周りのエラーハンドリングで得た技術的なナレッジ を共有できればと思います。

みなさんエモさ全開の記事を書いているところ、ただ実直にTechでコアな話をしていきます。

ちなみに弊社 Tech Blog への寄稿は初となりますので、お手柔らかにお願いします。誤ったことを書いていましたらこそっとご指摘いただけますと幸いです 🙇

1. gRPC について

現在、Gaudiy のプロダクトにおけるバックエンドサーバー間の API 通信のプロトコルとして gRPC を採用し始めています。

そもそも gRPC とは?の詳細については今回は割愛させていただきますが、簡単にいうとインターフェースを定義した言語(IDL)である Protocol Buffers をベースとしたエコシステムを用いることで、クライアント・サーバー間のメッセージのやり取りを効率的に実現することができる仕組みです。

詳しくは、公式 docs をご参考いただければと思います。 grpc.io

Gaudiy のプロダクトにおけるシステム構成は執筆現在では以下の図のようになっており、Go 製のマイクロサービス間での一部やりとりや、Node.js 製の BFF と Go サービス間の一部やりとりなどに gRPC を徐々に採用していっています。

この辺りの初期導入は、Dev Enabling チームのメンバーを中心に行っています。取り組み内容の詳細については、また別途記事になった時にご覧いただければと思います(誰か書きましょう)。

GaudiyのTech stack

2. 独自のパターンを用いたエラーハンドリングの実現

自分の所属している Stream Aligned な開発チームでは、直近で決済基盤のリニューアル開発を行ってきましたが 、その実装をしていく上で、gRPC サーバーにて複数の独自エラーパターンの返却を行い、フロントエンドで表示を制御したいというユースケースに直面しました。

これはさまざまなシステムにおいても、ごく一般的なユースケースかと思うのですが、gRPC でどのように実現したのかを詳しく説明していきたいと思います!

2-1. gRPC で推奨されているエラーモデル

まず、gRPC 公式の docs を見ると、Error handling についての Guide が提供されていることがわかります。2つの Error model についての説明がなされていますが、どちらかに沿って実現していくことが提唱されています。 grpc.io

2-1-1. Standard error model

抽象的かつシンプルなエラーハンドリングに関しては、docs にある通り Standard error model での表現が可能です。

codemessage という文字列が参照可能となっており、サーバー側で任意の情報を詰め、クライアント側でそれをもとにハンドリングするといったやり方で実現が可能です。

詳しい実装方法については、公式 docs に記載されている下記リポジトリにて言語ごとにサンプルが公開されているので割愛します(とても参考になります)。 github.com

2-1-2. Richer error model

非常にシンプルなエラーハンドリングであれば Standard error model で事足りますが、オブジェクトの中にデータも詰めてエラー返却をしたいなどのケースを考慮すると、Richer error model での表現がベターになってきます。

今回、Gaudiy で実際に直面したユースケースでは、カスタムのエラーコードとそれに付随する任意の情報を含めたいといったものでした。その点、Standard error model でも上手くやりくりすれば実現自体はできたかもしれないのですが、より拡張性や共通フォーマットの利用を意識して、こちらのモデルにて実装を行いました。

2-2. Richer error model の実装について

それでは、実際にどのように実装していったか、サンプルコードとともに順に見ていきます。

2-2-1. Protocol Buffers の定義

まずは、エラーの汎用的な独自の message を Protocol Buffers に定義し、protoc で各々の言語のコードを自動生成します。

今回は以下のような感じで、独自の message を定義し、サーバー側ではこの型を用いて任意の情報を詰め、クライアント側でデシリアライズ (パース) して受け取るという流れで実装しました。

独自 message の各フィールドには docs でも推奨されている、google が提供している error_details.proto の各messageを採用し、汎用的かつ条件によってさまざまな情報を詰め込むことができるようにしています。

syntax = "proto3";

package gaudiy.apierror;

import "google/api/field_behavior.proto";
import "google/protobuf/any.proto";
import "google/rpc/error_details.proto";
import "google/rpc/status.proto";

~~~

message Details {
  google.rpc.ErrorInfo error_info = 1 [(google.api.field_behavior) = REQUIRED];
  google.rpc.BadRequest bad_request = 2;
  google.rpc.DebugInfo debug_info = 3;
  google.rpc.LocalizedMessage localized_message = 4;
  google.rpc.PreconditionFailure precondition_failure = 5;
  
   ...

また、任意のカスタムエラーコードなどの情報も Protocol Buffers で enum などを用いて定義しておくと、クライアント・サーバー双方から参照できるので良いと思います。

先述した通り、Gaudiy ではこの辺りの整備や方針は Dev Enabling チームが導入から利用までをサポートしてくれたので、現在は非常にシームレスな利用が可能になっています。フィーチャー開発がメインの我々にとっては本当に頼もしい限りです🙌 (大感謝)

2-2-2.サーバー側の実装

さて、サーバー側の実装ですが、Gaudiy ではシステム構成的にも基本的に Go での実装となります。

エラーの詰め方としては、 grpc/status パッケージの error インターフェースの実装型である status.Status ( google/rpc/status.proto の message に相当 ) の details フィールドに先ほど作成した独自の message をセットすることで実現しています。

下の例では、Required なフィールドとなる ErrorInfo の Reason に Protocol Buffers で定義しておいたカスタムエラーコード (enum) の文字列を詰めるような実装にしています。

もちろん、Metadata フィールドに customErrorCode のような key で格納しても良いと思いますし、カスタムエラーコードに依存した message を作ってしまうというのも良いと思います。この辺りは、開発メンバーの中で合意形成が必要でしょう。

以下が実装サンプルのコードになります。

import (
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
  "google.golang.org/protobuf/types/known/emptypb"

  "gaudiy/apierror"
  hogepb "gaudiy/hoge"
)

func (s *Server) Hoge() (*emptypb.Empty, error) {
  res, err := s.service.Hoge()
  if err != nil {
    // status.Statusを任意のcodeとerrから作成
    st := status.New(codes.InvalidArgument, err.String())

    // Status.detailsに定義した型を詰める
    st, _ = st.WithDetails(&apierror.Details{
      ErrorInfo: &errdetails.ErrorInfo{
        // pbに定義したカスタムのエラーコードをReasonに詰める
        Reason: hogepb.CustomErrorCode.Hoge.String(),
        Domain: "your.domain",
        Metadata: map[string]string{
          // その他任意の情報があれば詰める
          "key": "value",
        },
      },
      // その他の error_details フィールドにも任意の情報を詰められる
    })

    // statusからerrorを生成して返却
    // (wrapしてしまうとstatusが正しくセットされないのでその場合はUnwrapが必要)
    return nil, st.Err()
  }

  return &emptypb.Empty{}, nil
}

2-2-3.クライアント側の実装

Gaudiy ではクライアント側となりうるのは Go 製のマイクロサービスまたは Node.js 製の BFF です。それぞれの実装を見ていきます。

まず、Go で受け取る場合は先ほどの逆を同じパッケージを用いつつ行うだけなので非常にシンプルです 🙆

import (
  "log"

  "google.golang.org/grpc/status"
  "google.golang.org/protobuf/types/known/emptypb"

  "gaudiy/apierror"
)

func (c *Client) Hoge() (*emptypb.Empty, error) {
  res, err := c.service.Hoge()
  if err != nil {
    // errからstatus型への変換を試みる
    st, ok := status.FromError(err)
    if !ok {
      return nil, err
    }

    // status.detailsを取り出して独自の型へのキャストを試みる
    details := st.Details()
    if len(details) > 0 {
      switch d := st.Details()[0].(type) {
      case *apierror.Details:
        // expected case
        log.Printf("reason: %v", d.ErrorInfo.Reason)
      default:
        // unexpected case
      }
    }

  ~~~

さて、問題は Node.js での実装です。

gRPC は、各言語のライブラリがそれぞれ独立された思想で実装・提供されているため、status という構造1つとっても、Go と同じように Node.js でも簡単に扱えるというわけではありません。

Richer error model の説明にも下記のようにあり、Node.js ライブラリは非対応だったりします。

This richer error model is already supported in the C++, Go, Java, Python, and Ruby libraries, and at least the grpc-web and Node.js libraries have open issues requesting it.

Go の gRPC 公式ライブラリである grpc-go では、status.details の情報を gRPC HTTP Client / Server どちらの実装にも serialize / deserialize してくれるロジックが埋め込まれており、利用する側はあまり HTTP 通信の規格までを意識せずに非常にシームレスに扱うことができます。

しかし、Node.js の公式パッケージである grpc-js にはそうした実装が現在なく、Go 製のサーバーから status の情報が HTTP 通信に含まれてきたとしても、自分でクライアントのデシリアライズロジックを書く必要があります。

上記の grpc-go の実装の通り、status.details の情報は grpc-status-details-bin という HTTP header key に格納されていることがわかるので、そちらを取得しつつ手動でデシリアライズする必要があります。

この辺りを行ってくれる Node.js の OSS が下記のようにいくつか公開されていますが、そこまでヘビーなものではないので Gaudiy では自前で実装することにしました。

それでは、サンプルコードとともに紹介していきます。 やってることは Go と同じでリクエストのエラーオブジェクトから status オブジェクトとその中の details に格納された独自の message の型情報を取り出しています。

import { credentials } from '@grpc/grpc-js';

// protobufから自動生成された各種コードをimport
import * as GaudiyApiError from './gaudiy/apierror/apierror_pb';
import * as grpc from './gaudiy/hoge/hoge_grpc_pb';
import { HogeRequest, HogeResponse } from './gaudiy/hoge/hoge_pb';
import { Status } from './google/rpc/status_pb';

const client = new grpc.HogeClient('localhost:5001', credentials.createInsecure());

client.hoge(
  new HogeRequest(),
  function (error: ServiceError | null, response: HogeResponse) {
    if (!error) {
      // 正常系処理
      return;
    }

    // grpc-goのサーバー側で埋め込まれたkeyからstatus.detailsのバイナリ情報を取り出す
    const detailsBinary = error.metadata.get('grpc-status-details-bin');
    if (!detailsBinary) {
      throw error;
    }

    const buffer = detailsBinary[0];
    if (!Buffer.isBuffer(buffer)) {
      throw error;
    }

    // binaryをdeserializeして該当の型だった場合のみunpackする
    const details = Status.deserializeBinary(new Uint8Array(buffer))
      .getDetailsList()
      .map((element) => {
        // protobufで定義したtype名で格納されているので判定
        if (element.getTypeName() === 'gaudiy.apierror.Details') {
          return element.unpack(GaudiyApiError.Details.deserializeBinary, element.getTypeName())?.toObject();
        }
        return null;
      })
      .find((element) => element);

    if (!details) {
      throw error;
    }

    // detailsの中身が取れる
    console.log(details?.errorInfo?.reason);
  }
);

かなり取り回しにくい感じではありますが、これでようやく Go のように Node.js でも Richer error model の情報を取得することができました(Go に比べるとコード量が多いですね😅)。

いかがでしたでしょうか。
gRPC と Go の記事は多いものの、Node.js を駆使したエラーの実装方法やナレッジはなかなか探しても見つからなかったため、改めて全体感をまとめました。

サンプルで紹介したような実装に行き着く際にも、文献や実装を漁ったり、デバッグやトライアンドエラーを繰り返しして実現したので、苦労したポイントでした。
この周辺で悩む方がいましたらその助けになれば幸いです 🙏

3. おわりに

Gaudiy では今回紹介した gRPC をはじめ、新しい技術に対して積極的に投資・チャレンジしていくというスタイルで開発を行っています!

一緒に働くエンジニアも積極的に募集しておりますので、こうした技術や開発スタイルに興味をお持ちの方がいらっしゃいましたら、選考前にカジュアル面談も可能ですので、ぜひ採用ページからお気軽にご応募いただければと思います!

recruit.gaudiy.com

Gaudiy Advent Calendar 2022の20日目は、@yusukesatoo06 さんが担当です!
今年も残り少なくなってきましたね、身体にはお気をつけて良い年末をお過ごしください🎄
それでは、また 👋

Gaudiyのいちエンジニアが代表Dev(技術責任者)になって感じたこと

この記事は「Gaudiy Advent Calendar 2022」の18日目の記事です。

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyでエンジニアをしている勝又(@winor)です。11月初めごろから、いちエンジニアをしていた自分が、社内で「代表Dev」と呼ばれている技術責任者になりました。

今まで明確なマネジメント経験がない自分がストレッチなロールを任されたところもあり、苦戦しながらも日々を過ごしているので、今回はその振り返りができればと思います。

かなりリアルでハードな話になってはいるのですが、同じような境遇の方やこれから遭遇するかもしれないどなたかの参考になれば幸いです。

1. 代表Devが新設された背景

これまでのGaudiyは、CEO以外の役職や階層がないフラットな組織でした。プロダクト開発組織は、Stream aligned teamが複数存在するのみで、チーム内のロールはPdM、UI・UXデザイナー、エンジニアとシンプルなメンバー構成でした。この状況でもメンバーのオーナーシップは非常に高く、他職能メンバーとのコラボレーションを頻繁に行い、個々人が効率的に成果を生む自律的な組織だったと思います。

(その頃の組織体制については、こちらの記事をご参考ください。)

techblog.gaudiy.com

しかし、シリーズBを迎え急激に組織も拡大していった結果、頻繁なコラボレーションが認知負荷の増大を招いたり、合意形成に時間がかかったり、生産性が低下したりする問題が発生してきました。

この問題を解決するために、完全フラットな組織から、各チームの代表者を立てて意思決定の所在と責任を明確にし、よりスピーディな意思決定ができるような自律分散型組織へと移行しました。

この結果として、Stream aligned team以外にもDevやDesignなどの職能横断の組織と、責任者である「代表」という役割が誕生しました。その開発組織の代表が、自分が担う「代表Dev」になります。

また、この組織設計のGaudiyらしいユニークなところは、各組織の代表は各組織にいるメンバーから「選挙」で決定される点です。こうすることで、成果だけでなくメンバーからの信頼を得ないと続投ができない設計になっています。

この理由も詳しくあるのですが、少し本筋とズレてしまうので興味ある方は下記の記事を参考にしていただければと思います。

type.jp

2. 代表Devになる前はどんな役割だったか?

振り返りをする前に、前提情報として、代表Devになる前の自分はどんな役割だったか?について触れることで、なぜ後述する課題にぶち当たってしまったのかの参考になればと思います。

基本的には前述したStream aligned teamに所属をし、アウトカムのためのデリバリーを達成するためにはなんでもするようなエンジニアでした。他のメンバーからは、「不確実性に対して推進力がある」みたいなところは評価いただくこともありました。

チームでは暗黙的に開発のリーダーのような役割を担うことはありましたが、明確にマネージャー的な役割を経験したことは、前職も含め今までのキャリアでは一度もありませんでした。

3. 代表Devになって最初に躓いたこと

3-1. ボールを打ち返すことに精一杯になっている

最初に躓いた点は、様々な課題や要望に対するボールを打ち返すことに精一杯になってしまうことでした。

最初はなにかあったときの一次ボール受けになっていて、スピーディーに打ち返していくように頑張っていたのですが、当たり前ですがわりとすぐにこれだとスケールできないなということに気づきました。

この課題は、今では役割を分けて権限委譲したことによりある程度は解決できています。とはいえ、リソースが限られた開発組織ですべてのボールを打ち返せるかというとそれは難しく、ボールに対して優先順位をつけたり、まとめて解決できるようにしていくことなども大切でした。

3-2. ピープルマネジメントを重視せず、問題が大きくなって気づく

元々の意思決定では、ピープルマネジメントに時間を割かないようにしていました。理由としては、今思うと自分目線の考えだったなと思うのですが、Gaudiyの開発組織でそういった問題が発生しないだろうと考えていたためです。

ただ結果的には、自分が観測できていなかった開発組織内の課題が大きくなり、問題が発生してから気づくケースが複数件ありました。この結果を踏まえて、開発のメンバーと1on1を行うようになり、継続的に開発組織の課題を検知することに対して優先度を上げるようにしています。

失敗をして気づいたことでもありますが、「エンジニアリングマネジメントトライアングル」という、エンジニアリングマネジメントで必要な3つの役割(Technology、Product、Team)の軸をつないだ図から考えても、TeamとProductをつなぐTeam Development的な部分を軽視していたということがわかりました。

また、これを参考にすると、今なにが足りていないのか? 本当にそこが足りていなくて良いのか?という視点が得られ、それ以降の開発組織や仕組みの改善への参考となりました。

steam.place

4. 代表Devになって変化したこと

4-1. 開発組織と隣接組織の成果の総和という考え方を持てた

本や記事を読んで学んだことですが、EM的なポジションになりたての方などでよくありがちなのが、コードを書く時間が減って自分のアウトプット量が減ることにより、フラストレーションが貯まることです。この点に関しては、会社や事業の目標を達成するためにできることを行うという考え方を元から持っていたのでそこまで問題はなく、開発組織全体の成果を上げていくことが重要だと考えていました。

一方で、HIGH OUTPUT MANAGEMENTという本を読み、少し考えが変わったことがあります。それは開発組織だけでなく、開発に影響力が及ぶ隣接する組織の成果を挙げることも、代表Devのロールに対する成果として大事だということです。

最終的に会社として達成すべきことは、事業目標を達成することであり、Gaudiyで言えばミッションである「ファンと共に、時代を進める」ことが最も大事です。そして、この事業目標の達成に必要な価値は、各組織から直接的に生まれることはほぼなく、ある組織で生まれた価値を他の組織がまた違う形に変換をしていき、ユーザーに届くことによって生まれます

例えばGaudiyの開発組織でいえば、社内の管理画面開発を怠ることでコミュニティ内での施策を回すスピードが落ちてしまい、CS組織の成果を落とすような影響があったり、他にも、デザインシステムの運用を怠ることでUIデザインとプロダクトの乖離が発生し、デザインのチェックコストなどにリソースを使ってしまい、デザイン組織の成果を落とすような影響などがあったりします。

開発組織の影響は、直接的な事業への貢献と隣接する組織への貢献の2種類あり、これらを最大化していくことが大事であるという考え方を持てたし、課題が発生したときの優先順位の軸として重要な要素だと考えることができました。

4-2. 開発の役割を明確化し、様々な開発施策をクオリティ高く実行できるようになった

以前はボールが集中する課題もあったため、開発組織内の役割を新設して、開発メンバーを頼るような形に変えていきました。

プロセスとしては、開発、組織、プロダクトなどの課題などから必要な役割を逆算していきました。例えば、Stream aligned teamのデリバリーに責任をもつ開発リーダーや、BE・FEの各リーダーなどの役割を明示して、各々のメンバーに責任の範囲で自律的に動いてもらっています。

彼らのおかげで開発の施策や仕組みづくりがクオリティ高く行われるようになり、最もうまくいった改善点ではないかとも感じています。

5. 今後の課題と向き合い方

5-1. 中長期的な意思決定ができていない

代表Devとして改善しなくてはと思っている一番の課題は、中長期的な意思決定がうまくできていないという点かなと思います。この理由はいくつかあるのですが、

  1. 改善すべき大きな課題はありつつも、その中でどこがボトルネックなのか?が特定できていない
  2. ヒアリング過多になっていて、意思決定の収束までが遅い

が主な要因だと考えています。

ここは自分の力量が本当になかったなと思うところはありつつ、他の方からフィードバックをもらって変えていこうと思ったのは、不安なところがあると、逃げのヒアリングを行ってしまい、とりあえず課題を探ろうとしすぎている点です。

マーケティングやR&Dなどでも、「仮説を持たずに調査やヒアリングばかりを行うのは、すべての課題を揃えて解決策を出していくことになり、効率が悪い意思決定になってしまう」というアンチパターンが実際にあるといいます。

課題を揃えていくことも重要だとは思いますが、事業やプロダクトの不確実性が非常に高いスタートアップの環境では、意思決定を素早く行い、実際に検証をして改善していくことも非常に大切です。そのため、今後はこの考え方も取り入れて意思決定をしていくことが大事だと痛感しました。

5-2. 副業メンターの方がいることで、自分の行動やモヤを振り返られる

代表Devとして今までとだいぶ違った動きをしているところもあり、モヤや躓きが多かったのですが、こういった点を他の方に相談して意見をもらえるのは本当にありがたかったです。飲み会で率直に色々とダメ出ししてもらったりしたことは本当に感謝しています笑。

また、Gaudiyで副業をしていただいてるEMの@yowrb_0905さんと週1で1on1を行っているのですが、メンタリングをしていただいたり、EMとして働かれている経験から新しい考え方に気づかせていただくことが多く、1on1をしてもらう重要性も再認識できました。

6. まとめ

今回は代表Devになってみての振り返りをまとめてみました。良かったところもありましたが、全体的に反省点や自分の至らなさで失敗してしまったところはやはり多いなと感じました。

ただ個人的には、Gaudiyの「ファンと共に、時代を進める」という壮大なMissionに共感しており、この実現にはエンジニアリングの力が本当に大切だと考えています。そのためにも、選挙で変わる可能性はありますが、どんな形でも強い開発組織を作っていき、少しでもVisionの実現に近づけていきたいと思っています。

また、強い開発組織を作っていく文脈でもエンジニアの採用は非常に力を入れていますので、ぜひ興味ある方は下記リンクか自分(@winor)へ連絡でもいただければと思います!

recruit.gaudiy.com

明日のアドベントカレンダーはエンジニアの並木さん(@ruwatana)が担当します。

アウトカムの最大化へ。開発組織の変遷と向き合い方

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyでプロダクトマネージャーをしている@kaa_a_zuです。

開発組織は、ITサービスを提供している企業にとって「エンジン的な存在」であり、プロジェクトや各メンバーの生産性に大きな影響を及ぼします。そんなエンジンは、事業の成長に伴って柔軟に変化させていく必要があると考えています。

Gaudiyでも、これまでに数度、開発組織のアップデートを重ねてきました。今のエンジンは、「仕様策定から開発、リリース、効果測定までをひとつのチームが行い、そのチームメンバー全員が責任を持ってアウトカムの最大化を図ることができる」ものになっています。

そこで今回は、2022年7月に行われた開発組織の体制移行を中心に、これまでの開発組織の変遷から今回の体制に至った背景、体制移行にあたって考慮した点などについて書こうと思います。

事業や人員拡大に伴い開発組織をアップデートする必要性を感じている方や、メンバーが最大限の能力を発揮できていないという課題感のあるエンジニアの方のご参考になれば幸いです。

※少し長いので、現在の開発組織の設計・運用だけ知りたい方は、「3. 開発組織体制の変遷」の終わりまで読み飛ばしてください。

1. Gaudiyのプロダクトと技術

まずはじめに、Gaudiyについて簡単にご紹介します。何をやっているかわかりやすく言うと、Web3のコミュニティSaaSをつくっている企業です。

Web3時代のファンプラットフォーム「Gaudiy Fanlink」というプロダクトを、エンタメIP(知的財産コンテンツ)を有する企業に提供して、クライアントの方々と一緒にファンの熱量を最大化させるコミュニティ運営に日々勤しんでいます。

service.gaudiy.com

Gaudiyが提供しているのはこのワンプロダクトですが、各IPファンの熱量を最大化させるための手段として NFT(代替不可能なトークン)やDID(分散型ID)といったブロックチェーン技術を利用しています。

とはいっても、下図のように、いわゆるWeb2の企業が使っている技術と大きな差はあるわけではありません。

Gaudiyが利用している技術スタックについての説明です。Frontendでは Next.js Typescript, ApolloClientが使われていてVercelにホスティングされてます。BackendではKotlinとGolangが使われていてCloudRunにホスティングされています。FrontendとBackendの間にBFF層として、TypescriptとApolloServerがあります。BlockchainはEthreumとPolygonが使われています。
Gaudiy Fanlinkを構成している技術スタック

よく「ブロックチェーンのことをよく知らなくても大丈夫ですか?」と聞かれますが、Gaudiyでは多くのWeb2企業出身のエンジニアが活躍しています。

techblog.gaudiy.com

2. Gaudiyの開発組織

そんなGaudiyですが、昨今のWeb3やNFTへの注目もあいまって、下図のようにエンジニアメンバーがどんどん増えています。

先日、シリーズBの資金調達が完了したこともあり、今後さらにエンジニア採用を加速させていく予定です。

エンジニアの採用計画についての説明です。2018年は2人、2020年は9人、2022年は19人となっています。そして2023年に50人、2024年に100人を予定しています。
2020年時点では9人だったエンジニアが現在では2倍以上になっている

このような事業フェーズの前進や組織拡大に伴って、開発組織を新たな形に移行する必要がでてきました。

3. 開発組織体制の変遷

ここで一度、創業期からの開発組織の体制について振り返ります。

Gaudiyではこれまでに二度、下図のように組織をアップデートしてきました。それぞれ、どのような背景や意図で組織体制を変えてきたのかについて説明します。

Gaudiyの開発組織の変遷(人数はPO・PM・Designer含む)

3-1. 【第1フェーズ】創業〜2021年5月

がむしゃらにプロダクトをつくる開発組織

<特徴>

  • Engineerは必要最小限のスコープである「プロジェクト」という単位にわかれる
  • POとDesignerが仕様をつくり、各プロジェクトでそれらを開発する

この頃は、まさに0→1のフェーズ。創業初期はブロックチェーンの顕在的なニーズが世の中にあまりなく、先行事例も少なかったため、PMFをめざす上で重要だったのは「いかに関係者に価値を理解してもらえるか」でした。

そこでまずは、クライアント企業に深く入り込み、個別の状況に対して理解と共感を深め、エンタメ企業に共通するペインを見つけながら、それを解決するプロダクトをつくる。そのプロセスが必要であり、戦略的に「Sales-Led Growth(セールス主導の成長)」でのプロダクト開発をしていました。

プロダクトが小さくメンバーも少なかったため、開発スタイルとしては、POとメンバーが密にコミュニケーションを取りながら、アウトプットを重視する形です。不確実な課題に対しての突破力が高いメンバーで構成されており、早いスピードで確実にアウトプットを積み上げていました

この開発体制によって、複数のクライアント企業様と一緒にコミュニティを提供できたことで、プロダクトの大枠をつくることができました。同時に、後に想定される「SaaSとしての急成長に備えた地盤を整えること」が課題となってきました。

3-2. 【第2フェーズ】2021年5月〜2022年7月

その課題に対応するために、2021年5月頃から、「継続的にコミュニティの機能をつくるExperienceチーム」「決済システムや他のプラットフォームと連携するためのSDK、IDシステムなどの基盤をつくるPlatformチーム」の2チームに分かれました。

地盤を整えるための開発組織

<特徴>

  • Engineerは機能をつくるチーム(Frontend Engineer中心)と、基盤をつくるチーム(Backend Engineer中心)にわかれる
  • POとDesignerが仕様をつくり、各チームでそれらを開発する
  • 各チームの開発スケジュールを管理するProject Managerのポジションが生まれる

この時代も、継続して戦略的なSales-Led Growthでの開発をしていました。セールスのニーズにあわせた開発をしていたため、運用するコミュニティの数も着実に増えていきました。また、増えてきたメンバーをまとめるため、プロジェクトマネージャーのポジションが新設され、個の集合からチーム感が強くなっていきました。

ここで課題となってきたのが、「SaaSとしての汎用性」と「1つ1つの機能の質」です。

Product-Led Growth(プロダクト主導の成長)ではなく、スピードとアウトプットを重視したSales-Led Growthでの開発を戦略的に採用してきたために、すべてのコミュニティに汎用的な最大のアウトカムは出せていない状態でした。

また他にも、以下のような課題が浮き彫りになってきました。

  • Designerのリソース不足で、仕様策定者のPOとDesignerの間で機能の細部要件が定まらず、開発に手戻りが生まれる
  • メンバーの増加に伴い、コミュニケーションが複雑になり、よりトップダウン開発の色が濃くなってしまった
  • なぜその仕様でその機能を作るのかの共通認識をメンバー全員が取れていない

3-3. 【第3フェーズ】2022年7月〜現在

こうした課題に対応するために、個々のチームが仕様策定からユーザーへの提供までを一貫して担い、それらが並存する組織体制に移行しました。これが現在の開発組織体制です。

アウトカムを最大化する開発組織

<特徴>

  • 仕様策定からユーザーに体験を届けるまでを一貫して担うため、EngineerはFrontend、Backendという分かれ方はせず、クロスファンクショナルなチーム構成になる。
  • POが事業進捗をふまえた開発の優先順位を決定し、PdMを中心にチーム全員で仕様をつくり、それらを開発/提供/効果検証する。
  • 特定の開発領域をもたないため、チーム名のつけかたを「寿司ネタ」にする。(社内公募で決めました。)

ひとつのプロダクトに対して、2022年9月時点で開発チームは3つです。現在は、横断的なチームは存在していません。

Gaudiyは、2022年の5月にシリーズBの1st、8月に2ndの調達をしました。次はいよいよシリーズCになります。

次のマイルストーンとして、私たちは「シリーズCでユニコーン企業になる」を本気で目指しており、その目標達成のために今までの開発の仕方を大きく変更する必要が出てきました。

ここからは、どんなことを考えて現在の開発組織を設計したか、そしてどのように組織運営をしているかについてお伝えしていきます。

4. 今回の組織体制移行において特に意識したこと

メンバーが増え、組織が拡大するにつれて、組織体制を変えることにはリスクが生じます。社員数が50人に満たないGaudiyでさえ、私はこの難易度が高いと感じていました。大前提、すべての理想を叶えられる組織体制は存在しないと思っています。

また基本的に、恩恵を享受する時には痛みも伴います。このようなトレードオフの関係になり得る重要な意思決定においては、特に意識しないといけないことが2つあると考えています。

1つ目に、絶対に譲れないコアな考え方を決めること、2つ目に関わるメンバーの納得感を得ることです。それぞれ具体的にお伝えします。

4-1. 譲れないコアな考え方を決める

今回の開発組織の体制移行に際して、以下3つをコアな考え方として定めました。

  1. アウトカムに責任が持てるチームであること
  2. すぐに新たなポジションは作らないこと
  3. 文化が継承されること

これらについて、順番に説明します。

4-1-1. アウトカムに責任が持てるチームであること

以前までは、アウトカムよりもアウトプットを重視した開発をしていました。ここからのフェーズでは多少のスピードを犠牲にしてでも、アウトカムを重視した開発をしていく必要があります。

ユーザーに対するアウトカムを最大化するためには、ユーザーが本当にやりたいことを探り、提供し、改善していくことが必要だと考えています。つまり、Whyを意識しながら仕様策定や微小な軌道修正を繰り返すことのできる開発組織の体制が必要です。

そのためには、POが細部まで仕様策定したものをチームが開発するといった社内受託感をなくし、開発チームが能動的にそれらについて考えることができ、アジリティ高く、タスク完結性を持って遂行可能な状況をつくる必要がありました。

  • アジリティ…企業のビジョンと一致している方向に、各々が意思決定ができている度合い
  • タスク完結性…各々が作業工程の一部ではなく最初から最後までに取り組めること

この状況を作るために、変更したのは3つです。

  1. 今までのPjM(Project Manager)というポジションをPdM(Product Manager)に変え、チーム内での最終的な意思決定の責任者に。
  2. 各チームが開発するプロダクトバックログを決める会議体を新設。
  3. 仕様策定からリリースまでを1つのチームだけで一貫して行えるクロスファンクショナルなメンバー構成に変更。

図示すると下図のようになります。

能動的に仕様部分を考えることができ、アジリティ高く、タスク完結性を持って遂行できる開発組織体制

当然かもしれませんが、タスク完結性がなければその決定に対してチームや個人が感じる責任は小さくなり、考えるプロセスにおいて他責感を生んでしまいます。

新しい体制は、チームがタスク完結性を持つ構造にしているため、否が応でも決定に対しての責任を持たなくてはいけない構造です。結果的に、アウトカムについてチームや個人がより意識をするため、ユーザーにとってより良いものが提供できるという状況が生まれています。

4-1-2. すぐに新たなポジションは作らないこと

組織体制を変えるタイミングでやってしまいがちなことのひとつが、新たなポジションを作ることだと思っています。

特定の課題を解決することの単純解が、ポジションの新設です。今回の議論でも「QAエンジニア」「Engineering Manager」「スクラムマスター」などの新たなポジションをつくりたいという声が上がりました。

結論、今回の体制移行に伴って新たなポジションをつくることはしませんでした。なぜかというと、ポジションをつくることは簡単でも、減らすことはとても難しいと考えているからです。

一度ポジションをつくってしまうと、そこにアサインされた人が「今まで持っていた裁量を失うこと」を恐れたり、アサインから外れることがモチベーションを落とす要因にもなり得ます。そのため、ポジションの新設による課題解決は、安易に選択すべきではないという結論にいたりました。

この辺りの「裁量とモチベーションの関係」については、米・ハーバード大学で心理学の教授をされている ダニエル・ギルバート氏 の著書で分かりやすく説明されています。

www.amazon.co.jp

改めてになりますが、ポジションをつくりたいという議論が出てきた時には、第一に仕組みで解決することができないかを模索し、試行錯誤をした後で本当に必要だったらポジションを新しく設ける、という流れが大事だと思っています。

4-1-3. 文化が継承されること

スタートアップの成長過程でよく言われているのが「30人の壁」「50人の壁」「100人の壁」です。

これらは組織規模がおおよそその人数になった時に、組織に対する満足度が低下し、結果として離職率が高まることを指します。これらの障壁の原因は、「文化の希薄化」と「1人当たりが持つ影響力の低下」だと言われています。

創業まもない少人数の時はプロダクトも小さく、ステークホルダーも少ないため、全員で意思決定をしてつくることに必死になります。その後、企業が有名になり組織が拡大していくと、途中で入社してきた経験豊富な優秀な人中心の構造になり古参メンバーがモヤを感じてしまったり、今まで知れていたことが知れなくなったり、以前と比べて組織に対する影響力が小さくなってしまいます。

Gaudiyの場合、まだエンジニアメンバーが20人未満だということもあり、そのまま進めることも可能でした。しかしながら、拡大スピードを考えると、早めに次の一手を打たないといけないような状況でした。

そのために各チームに企業文化を持っていて浸透させることができる古参メンバーが在籍する構成にしたり、全ての情報をアクセス可能にしたり、チーム間でのコミュニケーションが取れる(極論、取らなければいけない)開発の流れにしています。

このあたりは、開発組織だけでなく、GaudiyのバリューのひとつでもあるDAO(Decentralized Autonomous Organization:自律分散型組織)がベースになっています。以下の記事で詳しく説明しているので、よければご覧ください。

seleck.cc

note.com

4-2. 関わるメンバーの納得感を得る

組織体制の変更は、今まで慣れ親しんだ開発手法や今後のキャリアにも影響が出ます。そして、メンバーはロボットではなく、感情をもつ "人" であり、その誰しもがコンフォートゾーン(居心地の良い環境)を探してしまうものです。

その前提のもと、今回の移行に際しては、全開発メンバーにヒアリングをして意見を聞き、できる限りのwillを反映させました

組織体制の変更について考えている時は、どうしても視点が組織に向いてしまいがちですが、その視点だけではロマン(表層)的にしか描くことができません。チームや個人といった現場(本質)的視点を入れることで、より現実的で納得感が得られる体制を描けるのだなと学びました。

実際に、全員と数回の1on1を行うことはかなり大変でした。ただ、これから強い組織が成長するためには、今までのGaudiyを築いてきたメンバーがとにかく大切です。そのメンバーが持つモヤをなくし、組織基盤となる文化や考え方を新たなメンバーに浸透させることは、1on1を行う大変さに比べて何十、何百倍も大事だと思っていました。

それらを経て、最終的には社内AMA(Ask Me Anything)も開き、納得感を得て体制移行を完了できたのではないかと思っています。

5. 組織をワークさせるための工夫

すでに3ヶ月弱、この開発組織体制で動いていますが、実際にうまくワークしているのか?について最後に触れておきます。

結論、課題は山積みですが、今のところうまくワークしていると感じています。今までは開発組織の体制づくりに着目して説明をしてきましたが、ここからはこの組織体制における運用の工夫についてご紹介します。

5-1. 定期的に組織課題を改善するMTGを開く

組織課題は業務を重ねるにつれて顕在化し、日々更新されていきます。そこで、約2週間に1回の頻度で組織課題を改善をするミーティングを開いています。

この会議体の責任者はPdMですが、実際にそれぞれの解決に責任を持って旗を振る人は、その課題を挙げた人になることが多いです。今後も継続的にこのミーティングを行っていく必要があるなと感じています。

組織課題の管理DB

5-2. いつどこのチームが何に着手するのかを可視化する

ワンプロダクトを複数チームで開発する体制なので、他のチームが取り組んでいる開発領域を知り、お互いにコミュニケーションを取りながら開発をしていく必要があります。

そこで、他のチームがやっていること/今後やることをタイムライン形式で表示し、簡単に知ることができる状態にしています。

タイムライン形式でのプロダクトバックログ

また下図のように、弊社のプロダクトバックログには、今後開発をしたい機能(正確にはアウトカムベースのストーリー)があります。

これらをカンバン形式でも見ることができるため、開発メンバーが将来的にどういった機能が載るのかをイメージしながら、現在の開発ができるような状態にしています。(= 拡張性の考慮)

カンバン形式でのプロダクトバックログ

5-3. 通常業務禁止DAYで横断的にDX改善を行う

繰り返しになりますが、Gaudiyでは現在、横断的なチームは存在しません。

一般的に、横断的なチームがやることとしては、技術選定基準や開発方針の決定、難易度が高い開発、横断的に行う必要があるメトリクス計測の開発などがあると思います。

横断的なチームには、上述したようなことにフォーカスして着手できるメリットがある一方で、生じ得るデメリットとしては、組織として階層構造になってしまったり、横断的なチームが社内受託的な組織になってしまうなどが考えられます。

今のフェーズにおいては、できるだけ全員がオーナーシップを持ってレバレッジの効くDX改善に取り組むことが大事だと思っています。これを実現可能にしているのが、毎週水曜日にある「EMPOWER-DAY」です。

この日は午前が休み、午後は通常業務を原則してはいけない決まりになっています。この曜日を使って、チームに関係ない開発メンバーとのDX向上に取り組んでいます。具体的には以下のような取り組みが行われています。

開発チームが抱えている課題管理DB

なお、事業フェーズが進み組織が拡大する中で、横断的なチームが今後誕生する可能性はあります。Empower-Dayが誕生した背景や具体的な内容については、以下の記事で詳しく説明しています。

note.com

6. 現状の課題

上述したように、様々な仕組み化によって改善はしているものの、まだまだ多くの課題があります。

ここでは、今後解決していきたい課題についてお伝えします。

6-1. 意思決定責任の所在が明確化されないこと

開発組織が階層構造になっていないことや、特定のことを行う横断的な組織がないことにはデメリットがあります。

そのひとつが、全員がオーナーシップを持っているが故に、開発時の意思決定スピードが落ちることです。これについては、取り組みごとに最終意思決定者(≒責任者)を明確にするような形で推進しており、徐々に解消している最中です。

6-2. 開発時のコミュニケーション過多、コンフリクトが生じる

現在のプロダクトは小さく、機能もそこまで多くないため、チームごとの担当領域を定めていません。つまり、ワンプロダクトにおける体験ごとにタスク完結性を持って開発する運用になっています。

この運用(体制)では、他のチームが取り組んでいる開発領域を知り、お互いにコミュニケーションを取る必要がありますが、これには一定のコストがかかります。そして、これらの課題はチームが増えるごとにさらに大きくなっていくことが予想されます。

このコミュニケーションコストが許容できなくなるタイミングでは、逆コンウェイの法則に従って、担当する領域を明確化した機能単位のチームに移行することが望ましいのかなと現時点では考えています。

7. 今後に向けて

やはり、プロダクトと組織は切っても切り離せない関係にあると思っています。つまり、強い組織をつくることができれば、おのずと良いプロダクトをつくれる可能性が高くなるはずだと考えています。

ここからGaudiyは、さらに大きくなっていきます。2022年時点で20人ほどの開発組織も、2023年には50人、2024には100人に大きくしていき、グローバルに向けた挑戦をしていきます。

すごくおもしろいフェーズだと思うので、少しでも興味持ってくださった方はぜひカジュアルにお話ししたいです。

meety.net

エンジニア向けの採用ページもできたので、貼っておきます!

recruit.gaudiy.com

Server-Driven UIの採用背景と実装について

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyで、フロントエンドエンジニアをしているkodai(@r34b26)です。

Gaudiyでは、Airbnbが採用していることで有名な「SDUI(Server-Driven UI)」という設計手法を取り入れています。

先月のTech Blogでは、ユーザーに対してファンダムな体験を届けるために実践している、スキーマ駆動開発についてお伝えしました。

techblog.gaudiy.com

今回は少し視点を変えて、顧客やユーザーと対峙する社内メンバーに対して、ファンダムな体験を届けるために実践している、SDUIについてまとめてみます。

GaudiyでSDUIを取り入れた理由や、その実装方法なども書いてみたので、一事例としてよければご参考ください。

1. SDUI(Server-Driven UI)とは

SDUIはおそらく、Airbnbが使用する設計手法として、その名称とともに広がったのが始まりです。

medium.com

UIの表現方法をすべてサーバーサイドで決定し、クライアントに返す、という手法になります。

通常のアプリケーション開発では、レンダリングロジック、ルーティングなどをクライアント側で決めますが、SDUIの手法ではこれらをサーバー側で決定しています。

日本語だとこちらのnoteがわかりやすいです。

note.com

2. SDUIのメリット

先述したAirbnbの記事や、一般的に説明されているメリットとしては、以下になります。

  1. UIの変更にアプリケーションのデプロイを必要としない

    サーバー側のロジックや設定を変更することで、クライアント側の表示を変更できるため、クライアントアプリのデプロイが必要ありません。そのため、デプロイまでの社内プロセスや実行時間を待つことなく、UIに対する変更が可能です。

  2. アプリケーションの審査を必要としない

    とくにネイティブアプリでのメリットになりますが、UI更新にデプロイを必要としないため、Apple, Googleの審査を毎回待つことなく細かい変更や巻き戻しができます。これにより、社内でコントロール不可能なタイムロスを圧縮することが可能です。

  3. 複雑な表示切り替えに拡張可能

    レコメンドエンジンを用いたユーザーごとの表示切り替えによる、行動促進や広告配信ができます。また、ABテストや限定リリースなど、リッチなロジックによる制御を組みやすくなります。

つまり、クライアントの都合による変更反映までのタイムラグをできる限り減らすことができる。それがSDUIの最大のメリットです。

3. なぜGaudiyでSDUIを採用したのか

Gaudiyの話をすると、少なくとも現状はWebアプリケーション(PWA)のみで実装を進めており、ネイティブアプリの実装はしていません。またレコメンドエンジンに関しても、将来展望としてはありますが、現在は実装を行っていません。

それなのに、なぜSDUIを取り入れたのか。それは、顧客に提供するアプリケーションのカスタマイズ性を担保するためです。

こちらは、実際にGaudiyが提供している、各アプリケーションのホーム画面です。

実際のアプリケーション画面

同一ページ、同一ソースコードで実現していますが、表現がだいぶ異なっているのがわかると思います。

Gaudiyでは、ブロックチェーンを活用した「Gaudiy Fanlink」というプロダクトを通じて、さまざまなファン体験を総合的に提供していますが、そのコミュニティアプリは基本的に個別カスタマイズを行っていません。

プロダクトのめざす方向性や汎用性などの観点から、必要と判断された機能を追加する形で開発を進めていますが、クライアント企業によってはすべての機能を必要としていなかったり、段階的に機能を増やしていきたかったりするため、一部機能のみを提供する場合があります。

たとえば、

  • 提供する機能を増やしたい/減らしたい
  • ホームに表示するコンテンツを施策に合わせて変更したい
  • IPの世界観を壊さないような形で表現したい

といったクライアントの要求に対して、Bizサイドのメンバーがエンジニアを常に頼ることなく扱えるようにしたい。それを実現するための手段として、SDUIを実践することに決めました。

4. SDUIの実装方法

ここからは、GaudiyでどのようにSDUIを実装しているかをお伝えしていきます。

元々、BFF-Client間通信に使用していたApollo GraphQLを生かしつつ、Airbnbの実装例を参考にしながら弊社のユースケースに沿うように、レイアウト、UIパーツをGraphQL Schemaに落とし込んで表現しました。

これによって、

  • Fragment Colocationによるフェッチ最適化
  • 宣言的データフェッチによるReactの宣言的UIとの対応
    • 将来的に、宣言的なフレームワークを用いてネイティブに拡張可能

といった、データ効率とDXが高い開発が可能になりました。

では実際に、どのようなSchemaになったかを数点ご紹介していきたいと思います。

4-1. レイアウトのSchema

Gaudiyの現状のアプリでは、モバイルとデスクトップで、大きくレイアウトを変えることがあります。

具体的には、「モバイルで1列」「デスクトップで1列」「デスクトップで2列」の3パターンと、それぞれのレイアウトに対してトップに特別なコンテンツを置く・置かないの出し分けがあり、すべてのレイアウトは3 x 2=6パターンで表現されています。

デスクトップ・2列レイアウト

モバイル・1列レイアウト

これらを表現でき、また将来的にパターンが増えたり、タブレットなどに細かく対応する余地を持たせたSchemaとして、以下のように表現しました。

type LayoutsPerFormFactor {
  mobile: Layout!
  desktop: Layout!
}

union Layout = SingleColumnLayout | TwoColumnLayout

type SingleColumnLayout {
  mainTop: SingleSectionPlacement
  main: MultipleSectionsPlacement...
}

type TwoColumnLayout {
  mainTop: SingleSectionPlacement
  main: MultipleSectionsPlacement!
  sub: MultipleSectionsPlacement!
    ... 
}

抽象化したパターンがSchemaから把握できる形になっていて、わかりやすいかと思います。

4-2. UIパーツのSchema

前述したコミュニティごとの特性の違いから、ある機能単位での表示・非表示と、UI上の表現の違いを数パターンで表したいという要望がありました。

たとえば、以下のような同じ”トピック”機能に対して、ランキング形式でユーザー行動をより強く促す形と、フラットに表現する形で使い分けています。

ランキング形式で表現するパターン

フラットに表現するパターン

ここでは、機能ごとに出し分けする点と、機能ごとのpreview的に複数項目を表現する点から、機能ごとにセクションを分け、Arrayで子を持つように表現しました。

type SectionContainer {
  id: ID!
  sectionComponentType: SectionComponentType
  section: Section
    ...
}

enum SectionComponentType {
  TOPIC_CAROUSEL
  TOPIC_TRENDING
  ...
}

union Section =
    ChatSection
  | TopicSection
  ...

type TopicSection {
  title: String!
  items: [Topic!]!
    ...
}

type Topic {
  id: ID!
  label: String!
  description: String!
  imageUrl: String!
  createdAt: Int!
  isDisabled: Boolean!
  ...
}

機能の増減は Sectionで受け止め、機能ごとの見た目や並びのブレに関しては SectionComponentTypeで受け止めることで、unionが膨大に増えることを抑制しています。また、UIごとに必要になるfieldの違いに関しては、clientでのcolocationで定義することによって、over/under fetchが起きないようにしています。

このように、プロダクトの性質や、設計の採用目的によって重きを置くところが違うので、SDUIの思想をベースにしつつ、ユースケースにあった調整をした上で使用すると効果的だと考えています。

5. まとめ

今回はSDUIに関して、Gaudiyで実際にどう取り組んでいるかをまとめてみました。

特にフロントエンジニアの視点で考えると、Schema駆動でバックエンドと協力して設計・実装するのと同じくらい、もしくはそれ以上に、宣言するUIがデザイナーと認識の合った形になるように、こちらからヒアリングして提案することが大事になると思いました。

これから運用と拡張のフェーズを経て、引き続きSchemaを育てていきたいと思っています。

興味ある人がいれば、ぜひお話ししたいですー。

meety.net

Gaudiyの技術選定について知りたい方は、以下の記事をご参考ください。

techblog.gaudiy.com