Gaudiy Tech Blog

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

Gaudiyのエンジニア組織の「今」を紹介します!(2023年6月時点)

こんにちは!ファンと共に時代を進める、Web3スタートアップのGaudiyでエンジニアをしている勝又(@winor30)です。現在は、代表Dev(技術責任者)として、エンジニア組織全体の体制づくりや技術選定などを担っています。

Gaudiyの開発組織では、ビジョンである「ファン国家」を実現していくために、技術革新と最高のユーザーエクスペリエンスの追求を目指しています。

この記事は、Gaudiyという社名はなんとなく聞いたことがあるけど、エンジニア組織についてはよく知らない人に向けて、組織体制や開発指針、開発カルチャーなどをご紹介してみたいと思います。

Gaudiyに興味を持ってくださった方や、エンジニアの組織づくりを担っている方に、ご参考になれば幸いです!

1. Devチーム(エンジニア組織)概要

Gaudiyの開発組織には現在、フルタイムで約20人のエンジニアが在籍しています。バックグラウンドは多種多様で、フロントエンド(FE)寄り:バックエンド(BE)寄り:Unity = 3:5:1 くらいで構成されています。

チーム内には、Goやデザインエンジニアリングなどの特定領域にスペシャリティを持ったエンジニアや、プロダクト開発のためならなんでもやります!というスタンスのフルスタックなエンジニアまで幅広くいます。

私たちが取り組んでいる技術領域は、主に以下のようなものです。

  • 通常のフロントエンド(FE)Next.js、バックエンド(BE)Go x gRPC & TypeScript x GraphQLを中心としたWeb開発
  • より新しいファン体験を実現するための、以下の技術領域を使った開発
    • Unity、WebGL
    • Blockchain(Solidity、wallet)
    • Generative AI

この規模のスタートアップにしては、幅広い技術領域に携わっている会社だと思います。これらの技術を駆使しながら、ユーザーに新しい体験を提供する「Gaudiy Fanlink」というファンコミュニティプラットフォームを開発しています。

2. 最も大切にしていること:複雑性への挑戦と品質の追求

Gaudiyのエンジニア組織では、複雑性への挑戦と品質の追求を最も重視しています。

なぜなら、中長期的に開発組織の総生産力を最大化するためには、複雑性に立ち向かいながら、ソフトウェアの保守性と堅牢性を向上させることで、質とスピードを両立させることが不可欠だからです。

もちろん、プロダクト開発組織として最も大事なことは「プロダクトへのアウトカム最大化」だと考えていますが、このアウトカムを最大化するためには、エンジニア組織として「開発生産性を最大化すること」が重要です。その実現には、複雑性への挑戦と品質の追求が必要だと考えています。

元々は、プロダクト開発組織という一括りになっており、一定の規模感になるまでエンジニア組織が明確に存在していませんでした。その結果、技術負債や複雑性に対するマネジメントが、エンジニア個々人の努力でなんとなく成り立っている状態でした。

ですが、この複雑性がエンジニアのELTV(Employee Lifetime Value)を下げてしまったり、ソフトウェアの内部・外部品質の低下やDeveloper Experienceを下げてしまったりと、問題になっていました。

そこでエンジニア組織が明確にできたタイミングから、ソフトウェアの複雑性への対処に責任を持ち、ソフトウェアの品質を向上させることをエンジニアの行動指針として取り入れました。

3. 複雑性に立ち向かうための2つの指針

まず、そもそも複雑な前提・仕様だと複雑性が高くなってしまうため、前提を疑い、時には仕様を変えたり、削ったりして、前提となる複雑性を下げることが重要です。本質的な複雑さはドメインの複雑さに起因するため、複雑でないドメインにすることが、結果的にソフトウェアの複雑性を下げることにつながります。

次に、複雑なドメインを保守性・堅牢性の高いソフトウェアに落とし込めるように、エンジニアとしての技術力を高めることが大事です。

前提の複雑性を減らしても、プロダクトのアウトカム最大化を優先に考えると、複雑なドメインをソフトウェアに落とし込むケースは存在します。その場合は、エンジニアとしての技術力や設計力が「質とスピード」の両方を高めていく重要な要素になってくるため、それらを向上させることが重要です。

これらの方針は、エンジニア組織だけではなく、プロダクト開発組織やビジネス組織にもその重要性を理解いただいてるので、会社全体としての優先度も非常に高くなっています。

4. 開発組織のチーム構成

次に、Gaudiyの開発組織の体制についてご紹介します。現在、下図のように「フィーチャーチーム」と「開発横断チーム」の2種類で構成されています。

4-1. Feature Team

Feature Teamは、機能開発を行い、ユーザーに価値を提供することを目的としたチームです。フルサイクルエンジニアをベースとし、設計・開発・運用・QAなどのサービスライフサイクルを1つの開発チームがすべて担当します。

クラウド化やコンテナ技術、開発系ツールの進化、DevOpsの浸透などによって、1人のエンジニアが対処できる範囲が急速に広がっているため、Gaudiyではアジリティの高いチームを増やすために、フルサイクルエンジニアが重要であると考えています。

特に、最近のGitHub Copilotを始めとしたAIを使ったツールによる開発生産性の向上は強烈なポテンシャルがあり、Gaudiyはこのテクノロジーの可能性を信じてフルサイクルエンジニアを推進しています。(よければ、こちらの記事「GitHub Copilot 導入して1ヶ月経ったので振り返ってみた」をご参照ください。)

また、Feature Team内では、サービスライフサイクルを円滑に回すために「役割」を設定しています。各サイクルに対する責任者として、以下の役割を設定しています。

  • Dev Leader:Build(設計)とDeploy(デリバリ)に責任を持ちます。システムの複雑性を増加させ、チームの開発競争力が落ちることを防ぐ責任も持っています。
  • ScM:アジャイルメトリクスの可視化と問題検知、開発プロセス全体の問題検知と改善サポートを行います。
  • QA Lead:QAの結果に基づき、ソフトウェアが仕様通りデプロイされることに責任を持ちます。
  • Metrics & Ops Lead:Opsプロセスで発覚した問題をチームでもれなく処理し、ソフトウェアメトリクス・ログが正しく取得されていることを保証します。

また、各チーム内のサイクルの責任者(Dev Leader、ScM、QA Lead、Metrics & Ops Lead)に加えて、各チーム横断のリーダーも存在しており、各サイクルでの改善や課題がチームにサイロ化しないような組織構造となっています。

4-2. 開発横断チーム

開発横断チームは、開発生産性を向上させることを目的としたチームです。

Feature Teamは、工夫をしないとただ役割をたくさん持ったチームになるため、設計・開発・運用・QAなどの各サイクルをより円滑に回せるようにするツールやプロセスの洗練が必要です。

これらの構築・改善に責任を持つ役割として、開発横断チームが活動しています。

開発横断チームは、以下のような取り組みを実施して、開発生産性を向上させます。

  • 各Feature Teamが使用する共通のツールやライブラリの開発・メンテナンス。これにより、各チームが効率的に開発を進めることができます。
  • 開発プロセスの改善やベストプラクティスの導入のサポート。各チームが円滑に開発を進めることができる環境が整えています。
  • ナレッジ共有を促進し、各チーム間での情報交換の促進。チーム間での連携を向上させ、全体としての開発力を向上しています。
  • パフォーマンス改善やセキュリティ対策など、全チームで共通の課題に取り組むことができます。

開発横断チームの活動によって、各Feature Teamが高い開発生産性を維持し、より質の高いプロダクトをユーザーに提供できるようになります。

このように、Gaudiyのエンジニア組織はFeature Teamと開発横断チームの2つのチームで構成され、それぞれが異なる役割を担っています。これらのチームが連携することで、複雑性への挑戦と品質の追求を目指しています。

5. 開発文化・制度

続いて、開発文化・制度をご紹介します。

Gaudiyではリモートでのペアプログラミングや各技術領域の勉強会をよく実施しています。こうした取り組みを通じて、エンジニアの基礎力を向上させ、複雑なドメインにおいても堅牢性・保守性の高いソフトウェアを開発することを目指しています。

5-1. Gaudiyらしいエンジニア組織カルチャーの構築

Gaudiyには「Fandom」「New Standard」「DAO」という3つのバリューがありますが、エンジニア組織としても、Gaudiyらしい組織文化の構築に注力しています。その取り組みとしては、採用活動やクレドなどが挙げられます。

カルチャーマッチを重視する採用活動では、スキルレベルだけでなくカルチャーマッチも選考基準として考慮しています。また、Gaudiyで特徴的なのが「お試し入社制度」です。Gaudiyのカルチャーとミスマッチがないかを、Gaudiy側と採用候補者側の双方で確認しています。

また、どのような価値観や行動がGaudiyらしいかをクレドとして明文化し、相互理解を深めることを重視しています。

例えば、「たのピンチ」というクレドは、挑戦を続ける中で避けられない大きなピンチや問題、課題に直面した際に、それを楽しんで解決に取り組むことが重要であり、それがより良い結果・成果につながるという考え方を示しています。

5-2. リモートでのペアプログラミング

Gaudiyでは、VSCodeのLive Shareを使用してリモートでペアプログラミングを実践しており、1日中ペアプロを行うチームも存在しています。ペアプロを積極的に使うことで、エンジニア同士のスキル・コンテキストの共有や、コード品質の向上ができる上、チームワークも強くなると感じています。

5-3. FE、BE、Blockchainの勉強会

Gaudiyでは、フロントエンド(FE)、バックエンド(BE)、ブロックチェーン(Blockchain)技術に関する勉強会を、それぞれ週1のペースで開催しています。それぞれ強みの違うエンジニアが、互いに知識やスキルを共有することで、組織の技術力向上を目指しています。

具体的には、業務を通して学んだナレッジを資料やモブプロを通してシェアしたり、新しく調べた技術や手法に関してシェア・ディスカッションなどを行っています。

例えば、「gRPC × Go × Node.js におけるエラーハンドリングの実現方法」という記事は、実際に勉強会で取り上げられた内容を元にしています。

6. Gaudiyのエンジニアとして働く魅力

最後に、エンジニアメンバーに聞いたGaudiyで働くことの魅力をご紹介します。

ひとつめは、エンタメ領域でファン活動を最大化するような革新的なプロダクトを開発できることです。

ファンとクリエイターの間のつながりを強化し、エンタメ体験をより楽しく、魅力的なものにするプロダクトが作れます。特にエンタメ領域が好きで、こういった世界を実現したいと思う方は、この部分に強い魅力を感じて入社を決める人も多くいます。

ふたつめは、様々な技術領域を活用してプロダクトを開発できることです。

Gaudiyでは、Web技術だけでなく、Blockchain、Generative AI、WebGLなどの先端技術を組み合わせて、世の中にまだ存在しない新しいプロダクトを生み出すチャンスがあります。これによって、エンジニアは自分の技術力を高めると同時に、業界に革新的な影響を与えることができます。

3つめに、多様なスペシャリティやバックグラウンドを持った人が、エンジニアだけでなく他職種にもたくさんいることです。

なので、メンバー同士が領域をまたいで互いに学び合い、刺激を受けることができる環境があります。また、異なる視点やアイデアを持ち寄ることで、より優れたプロダクト、ソリューション、制度などを生み出すことができると考えています。

それぞれの興味ポイントにもよると思いますが、エンタメ領域が好きだったり、先端技術や革新的なプロダクトへの興味関心が高かったり、自己成長したい人にはおすすめの環境かなと思います。

また、Gaudiyの環境を通じて、業界に新たな価値を提供し続けるエンジニアになれることが、Gaudiyで働くエンジニアの真の魅力だと考えています。

7. まとめ

今回は、Gaudiyのエンジニア組織について紹介させていただきました。

まだまだ課題だらけではありますが、複雑性への挑戦と品質の追求を指針に、エンジニア1人1人が自律的に改善に動く文化があります。

開発体制や技術、カルチャーなど、少しでも興味を持っていただいた方は、ぜひ一度カジュアルにお話しさせてください!

site.gaudiy.com

site.gaudiy.com

OpenAI API を使ったデザインからコード生成する Figma プラグイン

こんにちは。ファンと共に時代を進める、Web3スタートアップのGaudiyに、6月からお試し入社する seya(@sekikazu01)と申します。

ここしばらく話題になっている、OpenAI が提供する ChatGPT を代表とした LLM。この記事では、そんな OpenAI の API を使って Figma からコード生成するプラグインを作ってみた過程を記していこうと思います。

先に背景をちょっとお伝えしますと、Gaudiy ではPSFに向けて、複数パターンのUI・機能を実際に提供しながら検証を回すことを予定しています。

すでに定義したコンポーネントはある程度使い回せるものの、ページ実装の試行回数の増加が見込まれる状況です。ここの作業効率化のために、コンポーネントをしっかり活用しながらも、ちょっといじればプロダクション利用可能な React コードを Figma から書き出すトライとして開発しています。

現状の肌感としては「デザインがしっかり構造的に作られている」「コンポーネントがデザインとコードで同期されている」状態においては、だいぶ実践投入できるレベルのコードを出してくれる印象です。

ですが、まだまだ課題はたくさんあり、それはこのプラグインの機能のみならず組織のデザインと実装のプロセスにも関わるものです。なので OSS にもしてみたので、ご興味ある方に Contribute いただいたり、働き方の知見の共有をしていけると嬉しいです。

github.com

とは言いつつ現状 GPT-3.5-turbo だとそんなにクオリティの高いコードを出してくれないので、GPT-4 が必要になります。GPT-4 API のアクセスがない方はお待ちいただくか、3.5 でいい結果の出るプロンプトを模索してみてください。

デモ動画

まず、実際どんなものなのかデモ動画を用意したので下記をご覧ください。

youtu.be

※動画ではさも一瞬で生成されたかのように見えますが、実際は1分くらいかかっています。

▼ 忙しい人のための静止画

基本的な作り

はじめに、ざっくりとした生成プロセスをまとめると次のような形です。 Figma のレイヤーを JSON に変換 -> それを OpenAI API に渡してコードを書いていただいてます。

デザインを OpenAI を通じてコード化するフロー図

Figma プラグインでは、Figma のレイヤーやコンポーネントなどの情報を参照することができます。 例えば下図は Frame のレイヤーから取れる情報の型定義です。Auto Layout 関連の情報などが取得できることが見て取れると思います。

Auto Layout の型情報
Auto Layout の型情報

ただ、これはそのままでは余計な情報が多いのと、Figma 専用の語彙になっていることがちらほらあり(primaryAxisSizingMode, counterAxisSizingMode の組み合わせなど)、OpenAI API さんではうまく解釈できないものがあるため、最初にCSSの語彙に直したオブジェクトに変換します。

あまり詳細は書いてもアレなのでざっくり概要のコードだけ貼ると、下記のように単純に Figma の node(レイヤー)の children を辿っていって、必要な情報だけ持ったオブジェクトに変換しています。

また、コンポーネントのインスタンスの場合はコンポーネントとして表示したいので、それ以上 children は深追いせず ComponentNode という個別の型のオブジェクトに変換しています。(https://github.com/kazuyaseki/figma-code-transformer/blob/main/src/figmaNode/buildTagTree.ts)

    node.children.forEach((child) => {
      if (child.type === 'INSTANCE') {
        const props = Object.keys(child.componentProperties).reduce(
          (_props, key) => {
            const value = child.componentProperties[
              key
            ] as ComponentProperties[string];

            // component property keys are named like this: "color#primary"
            // thus we need to split the key to get the actual property name
            const _key = value.type === 'VARIANT' ? key : key.split('#')[0];
            return { ..._props, [_key]: value.value };
          },
          {} as { [property: string]: string | boolean }
        );

        if ('Instance' in props) {
          delete props['Instance'];
        }

        childTags.push({
          name: child.name.replace(' ', ''),
          props,
          isComponent: true,
          children: [],
        });
        if (child.mainComponent) {
          componentNodes.push(child.mainComponent);
        }
      } else {
        const childTag = buildTagTree(child, componentNodes);
        if (childTag) {
          childTags.push(childTag);
        }
      }
    });

getCssDataForTag という関数はこんな感じでひたすら Figma のレイヤーからデータ取って CSS の語彙に変換しているだけです。

export function getCssDataForTag(node: SceneNode): CSSData {
  const properties: CSSData = {};
  if(!node.visible) return;

  if ('opacity' in node && (node?.opacity || 1) < 1) {
    properties['opacity'] = node.opacity || 1;
  }
  if ('rotation' in node && node.rotation !== 0) {
    properties['transform'] = `rotate(${Math.floor(node.rotation)}deg)`;
  }
  ...他にも色々

▼ 詳細なコード

github.com

ちなみに上記のコードは、昔自力で頑張って作ってた React コード生成プラグインから取ってきて拡張したものです。時を超えて役に立つエモい展開です。

github.com

あとはこうして作った JSON を元にプロンプトを作って書いてもらうだけです。

Convert this Figma JSON object into React, TypeScript, Tailwind code.

## Figma JSON
{ここにさっきの Figma の JSON を貼る}

## constraints
- component does not take any props
- do not omit any details in JSX
- Do not write anything besides code
- import components from @/components directory
- if a layer contains more than 1 same name child layers, define it with ul tag and create array of appropriate dummy data within React component and use map method to render in JSX
- use export rather than default export

以上、基本的な動作の解説でした。

余談: LLM はコード生成において何が革新的か

ちょっとここ数年「デザインからコード生成できないかなぁ。見た目の部分のコーディングしたくないよぉ」という思いから色々調べたり作ったりしてきて想いが乗っていることもあり、若干脇道にはそれてしまいますが LLM がその文脈においてどのような変革をもたらしたかを簡単に語っていきます。

以前、私はFigmaからReactコンポーネントを生成するプラグインを作成し、コミュニティに公開しました。 www.figma.com

このプラグインを作ったはいいものの、以下のような課題がありました。

  • 様々なフォーマットへの対応が非常に手間がかかる
    • 例えば上記のプラグインで Tailwind に対応させようとすると、各プロパティに対してクラス名の変換コードを真心込めて書く必要があります。
  • 既存コードとの競合
    • すでに実装されているページのデザインが新しくなった場合、既存のコードを無視して書いてしまうため、そのままでは使えない状態でした。
  • コードのコンポーネントとの違い
    • 一応上記プラグインでもコンポーネントをセットする機能とかは作っていたのですが、プラグイン専用に情報をインプットする手間が大きいのもあり、あまり現実的な解決策とは言い難い状況でした。

総じて頑張れば実装はできたとは思いますが、恐ろしく手間なのでさすがにやってられんなと感じていました。 そんな折に彗星の如く現れた LLM

プロンプトで

  • 「CSS は Tailwind で書いて」
  • 「これが既存コードなので、この部分は改変せずに書いて」
  • 「これがコードのコンポーネントなのでこれ通りに Props書いて」

と適切なデータと共にお願いするだけで、期待通りの品質のコードを書いてくれるようになりました。 ありがとう OpenAI、こんなに実装のハードルを下げてくれたのであれば、実現に漕ぎ着けるのに努力しない手はありません。

また、ユースケースと相性がいいなと思ったこととして、あくまでこの生成されたコードは後にエンジニアが手直しすることが前提です。

LLM のアウトプットは工夫のしようはあるのですが、どうしても回答にブレだったりハルシネーションが起きてしまいます。なので 90 点のアウトプットを出せればよく、あとは人間がそれを元に開発をロケットスタートできる、というコード生成のようなユースケースは、そういった LLM の特性とも相性が良いように思います。

工夫したこと

以上、基本的なロジックについて解説しましたが、これだけではまだまだ実践には足りない部分があります。

例えば次のようなケースには対応できていません。

  • コンポーネントの Props が Figma 上の Variants のプロパティを元に書いているため、コードだけに存在する Props が書かれない / デザインにだけあるプロパティが書かれてしまう
  • 既に実装したページのロジックを消してしまう
  • OpenAI API にはトークン長の制限があり、大きいレイヤーをコード生成してもらおうとするとエラーになる
  • デザイントークンを使った CSS になっていない

このような課題たちを解くために色々実装してみたので、一つ一つ紹介していこうと思います。

Props のサマリを渡してコードの Props と同じように書いてもらう

ゴールとしては、プロンプトに下記のように使っているコンポーネントのコード上の Props のサマリを渡すことによって「コンポーネントの Props が Figma 上の Variants のプロパティを元に書いているため、コードだけに存在する Props が書かれない / デザインにだけあるプロパティが書かれてしまう」という課題を解決します。

### Button component:

Props:
- variant?: 'primary' | 'secondary' | 'outlined' | 'basic' | 'success' | 'dangerous' | 'dangerousOutlined' | 'dangerousPlain' 
- size?: 'xsmall' | 'small' | 'medium' | 'large'
- subText?: string
- loading?: boolean
- disabled?: boolean
- rounded?: boolean
- startIcon?: ReactNode
- endIcon?: ReactNode
- children: ReactNode

そしてプロンプトに次の一文を足します。

- if Props summaries are provided for the components used in JSON, write props that are required in code props and omit props that only exist in Figma JSON

こうするとコードの Props に応じて Variants の値をうまいこと入れてくれるのと、デザインにしか存在しない Variant Property を書いたりしなくなります。

サマリの作り方

今回はこのサマリを作ってもらうのも OpenAI API にお任せしました。

まず GitHub API で該当のコンポーネントのファイルを取得します。 (ここでははデザインとコードでコンポーネント名が統一されていることを前提としています。)

const { data } = await octokit.repos.getContent({
      owner: 'gaudiy',
      repo: 'design-system',
      path: `packages/designed-components/src/components/${componentName}/${componentName}.tsx`,
    });

そして次のようなプロンプトでサマリを書いてもらいます。ちなみにこちらは gpt-3.5-turbo で十分なタスクなのでそちらを利用しています。

export const buildPromptForPropsSummary = (
  componentName: string,
  codeString: string
) => {
  return `Write Summaries of Props of ${componentName} component in the following code

## Code

\`\`\`tsx
${codeString}
\`\`\`

## constraints
- start with a sentence "{ComponentName} component:"
- write list of props with TS type
`;
};

こうやって作った Props のサマリをコード生成のプロンプトにも入れています。 割と端折って説明しましたが、コンポーネントの情報を取るところは、参照先が一つのディレクトリでは済まないケースがありリクエストの数が多くなってしまうため、もう少し柔軟に検索できるようになると嬉しいなと思っています。ここには Embedding を活用するか、もはやプラグインのコードに埋め込んじゃうか(この場合アプデをどう反映するかは未定ですが)を検討したいなと考えています。

大きいレイヤーの時は Chunk に分割してマージするようにする

前提として OpenAI API はトークン長により制限があります。厳密には違いますが、トークン長は文字数のようなもので、プロンプトが長過ぎるとエラーが起きてしまいます。

なのでそのままだと大量のレイヤーが含まれたページのコードを生成することができないのですが、そんな上限を気にしてプラグインを使いたくない...。 そんな気持ちから Chunk に分割して後でマージする実装をしました。

こんな感じで Tree を Chunk の大きい順にノードを並べて、トークン数が上限値に近い順から元の Tree から取り除いていく関数を作りました。(※現状同じツリー内のものを分割してしまうバグがある)

import {
  APPROX_TOKEN_COUNT_PER_CHARACTER,
  MAX_TOKEN_COUNT_FOR_CODE,
} from '../constants';
import { Tag } from './buildTagTree';

function getTokenCount(tag: Tag) {
  const stringLength = JSON.stringify(tag).length;
  return stringLength * APPROX_TOKEN_COUNT_PER_CHARACTER;
}

export const divideTagTreeToChunks = (tag: Tag): Tag[] => {
  let totalTokenCount = getTokenCount(tag);
  if (totalTokenCount < MAX_TOKEN_COUNT_FOR_CODE) {
    return [];
  }

  let result: Tag[] = [];

  // 全部の node を探索して {node, childIndex, parentNodeRef} の配列を作り、トークンカウントでソートする
  const nodes: {
    node: Tag;
    childIndex: number;
    parentNodeRef: Tag | null;
  }[] = [];
  const traverse = (
    node: Tag,
    childIndex: number,
    parentNodeRef: Tag | null
  ) => {
    nodes.push({ node, childIndex, parentNodeRef });
    if ('children' in node) {
      node.children.forEach((child, index) => {
        traverse(child, index, node);
      });
    }
  };
  traverse(tag, 0, null);
  nodes.sort((a, b) => getTokenCount(b.node) - getTokenCount(a.node));

  // トークンカウントがmaxChunkSizeより小さいもので多い順に chunk に追加していく
  nodes.forEach(({ node, childIndex, parentNodeRef }) => {
    if (totalTokenCount < MAX_TOKEN_COUNT_FOR_CODE) {
      return;
    }

    const tokenCount = getTokenCount(node);
    if (
      tokenCount < MAX_TOKEN_COUNT_FOR_CODE &&
      'id' in node &&
      parentNodeRef &&
      'children' in parentNodeRef
    ) {
      result.push(node);
      parentNodeRef.children[childIndex] = {
        nodeId: node.id || '',
        isChunk: true,
      };

      totalTokenCount = getTokenCount(tag);
    }
  });

  return result;
};

そしてプロンプトに子レイヤーが Chunk だった場合に ID だけ JSX に書き込むようにする命令を追加しました。

- if child is chunk, render it as --figmaCodeTransformer=nodeId, it will later be used to replace with another code

そうするとこんな感じで描画してくれます。

<div className="flex flex-col justify-start items-center p-16 gap-16">
          --figmaCodeTransformer=149:20062

そして、Chunk にした部分は JSX だけ書いてもらって、あとは手動で差し替えます。

export function getChunkReplaceMarker(id: string) {
  return `--figmaCodeTransformer=${id}`;
}

export function integrateChunkCodes(
  rootCode: string,
  chunks: { id: string; code: string }[]
) {
  let integratedCode = rootCode;
  chunks.forEach((chunk) => {
    if ('id' in chunk) {
      const replace = getChunkReplaceMarker(chunk.id);
      const regex = new RegExp(replace, 'g');
      integratedCode = integratedCode.replace(regex, chunk.code);
    }
  });

  return integratedCode;
}

// コードをマージするところ抜粋
const codes = await Promise.all([
  createChatCompletion(prompt, []),
  ...chunkPrompts.map((chunkPrompt) => {
    return createChatCompletion(chunkPrompt, []);
  }),
]);

const rootCode = integrateChunkCodes(
  codes[0],
  chunks.map((chunk, index) => ({
    id: 'id' in chunk ? chunk.id || '' : '',
    code: codes[index],
  }))
);

これで動くコードはできあがるのですが、フォーマットが美しくないので後日 prettier 動かす対応は入れようかなと思います。

ちなみにプロンプトでは JSX 部分だけ吐き出してもらう部分はこんな感じで指定しています。Write only JSX だけだと関数宣言の部分も含んじゃったので、例を入れると安定して出力してくれるようになりました。

- Write only JSX
for instance if the result code is like below:
\`\`\`
import { Hoge } from "hoge";
export const ExampleComponent = () => {
    return <div>....</div>
}
\`\`\`
Then output only
\`\`\`
<div>....</div>
\`\`\``;
};

既存のコードを渡すと差分だけ書いてもらう

これはシンプルにページの既存コードを渡して do not change previous code と指定すれば変わった部分だけ追加して出力してくれます。かしこい!

なので「既存コードを選ぶ」という体験をいい感じにするための機能を用意したい気持ちがあります。下記あたりを作れると大体のケースに対応できるのではという予感がしております。

  • テキストエリアに手動コピペ
  • 前に生成したことがあったらファイル名をプラグインに保存しておく
  • レポジトリのファイルをツリー状に表示して選ぶ

デザイントークンをクラス名に変換

今回 Gaudiy 社では Tailwind を使っているので、いわゆるデザイントークンを tailwind.config.js の中で指定している内容通りのクラス名に変換したいなと思っております。

まずはそのデザイントークンの情報を取る方法ですが、Figma ではスタイルという標準の機能Token Studio というスタイルで色やタイポグラフィ以外のものも定義できるプラグインの2パターンが大きくあります。

tokens.studio

とりあえず両方を対応してみます。

スタイルの方は色やタイポグラフィの値が指定されている場合は生の値を渡さず、スタイル名を渡すようにします。

function setColorProperty(
  fills: Paint[],
  colorStyleId: string,
  properties: CSSData,
  colorProp: 'background-color' | 'color'
) {
  if ((fills as Paint[]).length > 0 && (fills as Paint[])[0].type !== 'IMAGE') {
    const style = figma.getStyleById(colorStyleId);
    const paint = (fills as Paint[])[0];

    const color = style ? style.name : buildColorString(paint);
    properties[colorProp] = color;
  }
}

また、TokenStudio はありがたいことに [setSharedPluginData](https://www.figma.com/plugin-docs/api/properties/nodes-setsharedplugindata/) という他のプラグインでも値を参照できる形でデータを保存してくれているので、それを通して取得してみます。

function setFigmaTokens(node: SceneNode, properties: CSSData) {
  const tokenKeys = node
    .getSharedPluginDataKeys('tokens')
    // Omit "version" and "hash" because they are not tokens
    .filter((key) => key !== 'version' && key !== 'hash');

  tokenKeys.forEach((key) => {
    const value = node.getSharedPluginData('tokens', key);
    if (value) {
      // remove css that's represented by token
      if (key === 'itemSpacing') {
        delete properties['gap'];
      }

      properties[key] = value.replaceAll('"', '');
    }
  });
}

こんな感じで取得できたらプロンプトでは「なんかちゃんとした値入ってなかったらそれデザイントークンで Tailwind ではこういうネーミングルールだからよしなに変換してくれ」と指定しておくといい感じになります。

- if string value other than hex or rgb() format is specified for color property, it is design token vairable. it is defined with the name in kebab-case in tailwind.config.js
- if "typography" property is specified, it is defined in tailwind config as typography token that has multiple properties such as font-family, font-size, font-weight, line-height

GraphQL query をレイヤーに保存する

若干「デザインからコード生成」の文脈からは脇道逸れてしまうのですが、Gaudiy では GraphQL を使っており、fragment colocation の文脈でレイヤーやコンポーネントに fragment を保存 -> 生成する時にそれらをまとめた Query を発行、みたいなのできたら良さそうだねという会話があったため、試しに作ってみました。

例えばこうやって二つの違うレイヤーで Fragment を保存して

Fragment を保存する
Fragment を保存する

ルートのレイヤーでプラグインを開くとこのように配下にある Fragment を展開した Query が表示されます。

Fragmentが統合された Query になっている図
Fragmentが統合された Query になっている図

エディタには graphiql-explorer というライブラリを利用しています。

github.com

あまり中身は深く説明してもアレなので概要だけざっくりお伝えすると、この graphiql-explorer はスキーマの内容に応じてポチポチプロパティを選べて便利なのですが、Fragment の記述は対応しておりません。

なのでデータとしては Query として保存しておいて、見た目を表示したりプロンプトにデータを渡す時に Fragment の形に変換してマージするようにしています。

▼ Query を Frament に変換するコード

figma-code-transformer/convertQueryToFragment.ts at main · kazuyaseki/figma-code-transformer · GitHub

▼ Frament をマージするコード

figma-code-transformer/mergeQueryAndFragments.ts at main · kazuyaseki/figma-code-transformer · GitHub

それでレイヤー毎に Fragment や Query を setPluginData で保存して機能させています。 保存する機構は作れたのですが、もう一歩としてはここで書いた gql をプロンプトに使ってデータの繋ぎ込みまで書いてもらったり、ちょっとした工夫としては gql ファイルも一緒に PR 作れるといいのかなと検討しています。

ちょっとした手間を減らす機能

上記以外にもちょっとした手間を減らす工夫を用意したのでサッと紹介していきます。

Storybook も作成

ChatGPT さんはコンポーネントのコードを渡してあげれば Storybook も書いてくれます。

なので下記のようなプロンプトで Storybook も書いていただいてます。(gpt-3.5-turbo で可)

export const buildPromptForStorybook = (
  codeString: string,
  componentName: string
) => {
  return `Write storybook for the following component in Component Story Format (CSF).
\`\`\`tsx
${codeString}
\`\`\`
## constraints
- Do not write anything besides storybook code
- import component from the same directory
- do not have to write stories for componnents used in ${componentName}
`;
};

PR を作るところまでサポート

コードが出力されても、そこから

  • ブランチを切る
  • ファイルを作成
  • そこにコピペ

という手間がまだ存在します。せっかくならそんな手間も減らしたい...。という訳で GitHub API を使って PR を作るところまでサポートしています。

PR を作るコードはこんな感じです。

github.com

ちなみにブランチ名なども下記プロンプトにて ChatGPT さんに考えていただいてます。(gpt-3.5-turbo で可)

export const buildPromptForSuggestingBranchNameCommitMessagePrTitle = (
  codeString: string
) => {
  return `Suggest
- branch name
- commit message
- PR title
- component name
for the following code
\`\`\`tsx
${codeString}
\`\`\`
## constraints
out put as JSON in following property names, do not include new line since this string is going to be parsed with JSON.parse
{ "branchName": "branch name", "commitMessage": "commit message", "prTitle": "PR title", "componentName": "component name" }
`;
};

待ち時間に猫動画を表示する

GPT-4 の API はとても長いです。長いので工夫が必要です。

ただ chatCompletion API は stream で結果を逐次返してくれるので、なんだかんだ ChatGPT 本家のように1文字ずつ表示する方が残りどれくらいかかりそうかもなんとなく分かりますし体験としてはいいのかもと思い始めています。

今後の課題

以上、これまで実現してきたことを書いてきました。

ですがまだまだまだまだ大変なことはありますし、今回作ったものに対して正しい期待値を持っていただくという意味でも、現時点で私が認識している課題を赤裸々に列挙していきます。

パフォーマンス

これは割とどうしようもない感あるのですが、GPT-4 の API はとんでもなく時間がかかります。コード生成に関しては、レイヤーの量にもよりますが 40-120秒ぐらいかかる印象です。猫動画でカバーはしていますが、人は飽きてしまうかもしれません。

こればっかりは OpenAI さんに頑張っていただくしかないのですが、ここが課題として大きくなる場合には

  • 3.5-turbo でも解けるように分解する
  • 同じプロンプトや既に出力したレイヤー構造に対してはキャッシュする
    • これは割とがんばりがいある気がする
  • Web LLM の発展を祈る
  • 待ってる時間を有益に or 楽しく過ごせるような何かを用意する

また、現状はプラグインで実行していますが、デザイナーがフィーチャーのデザインを作り、デザインのリファクタをしたら Figma API を通してまとめて実行しておく、みたいなフローにするとかもありかもしれません。

簡単に構造的なデザインが作られるようにする

本プラグインを使っていく上でネックになるのが「デザインをしっかり作る」ことです。

今回 Figma のレイヤーのデータを元にして OpenAI API さんにコードを書いていただいており、Auto Layout が使われていないとレイアウトの情報が分からなかったり、変なレイヤー構造で作られていると(リストなものが一つのテキストで作られてたりね)、それがそのまま出力されるコードにも反映されてしまいます。

基本的にはデザイン自体のメンテナビリティにも繋がるものなのでしっかり作った方がいいとは思っているのですが、状況によってはそんなに頑張るのかグレーなところもあります。(それもこのコード生成ができるようになった文脈で、しっかり作ることに対するインセンティブが強まったとは思います)(デザイナー目線ではそのメリットは感じられないのでサイロ化されてダメかも)

ちなみに私が思う「しっかり作る」は以下のあたりです

  • Frame 関連
    • No Group, Just use Frame
    • Frame には基本 Auto Layout を使おう(本当に違う場合は除く)
  • コンポーネント関連
    • コンポーネントは命名規則に則って名付ける
    • Variants / Component Properties を全部使っておこう
  • スタイル or/and TokenStudio 全部ちゃんとつけよう
  • レイヤーの構造をコードと同じ形にしよう

この辺りができてたら大体いい感じなると思います。 これをサポートする案をいくつか考えてみます。

DesignLint の拡張

まずはなるべく人間に頼らない方法での解決を考えてみます。

Figma には既に Design Lint という「スタイルがあたってないよ」「規定以外の border-radius の値が入ってるよ」などを教えてくれるプラグインがあります。

www.figma.com

こちらですが OSS として公開されているので、Fork して独自のルールを追加したり機能を足すことができます。

github.com

結構お手軽で、例えば下記は Auto Layout がついていないフレームがあったら怒るルールを足したい場合は下記のような関数を追加して、

export function checkAutoLayout(node, errors) {
  if (node.type === 'FRAME' && node.layoutMode === 'NONE') {
    return errors.push(
      createErrorObject(
        node,
        'frame',
        'Missing Auto Layout',
        'Did you forget it?'
      )
    );
  }
}

controller.ts の Frame に対するチェック関数をまとめているところに追加するだけです。

  function lintFrameRules(node) {
    ...
    checkAutoLayout(node, errors);

    return errors;
  }

また、これだけだと検出しかできないので、eslint fix がごとく勝手に修正できる機能も追加していけば負荷も軽減していけるのかなと思います。

デザインリファクタをワークフローに取り入れる

詳しくは下記記事に記したのですが、デザインはそんなカチカチに作りたくない仮説検証のフェーズと、将来のメンテナンス性のため・コラボレーションのため構造化していくフェーズに分かれると思います。

note.com

後者の時に、しっかりとエンジニアにハンドオフするタイミングでデザインのリファクタをしてから「これは実装 Ready だよ」と伝える決まりにするとかいいかもしれません。

構造的に作らなくてもいい感じのコードを出力してくれるモデルを作る

あとはそもそも Figma をちゃんと作らなくてもちゃんとしたコードが出てくるようになんかできないかという方策です。

一つに OpenAI はいつか画像をインプットにできる、いわゆるマルチモーダルの API を提供するみたいなので、もしかしたら Figma の描画内容を渡してあげたらいい感じになるかもしれません。

また、私はまだ試してはいないのですが、下記のプロダクトが「レイヤー構造を修正することなく」高精度の HTML/CSS を出力することを謳っていたり、

front-end.ai www.figma.com

あとは Hugging Face にも色々あったりするので(下記は試してみましたが期待通りのクオリティではありませんでしたが)オープンのものでも何かしら出てくるかもしれません。

huggingface.co huggingface.co

こういったものを組み合わせると幸せになれそうです。 ちょっと ML 素人の私が自分で作るのを試すのはちょっと時間がかかりすぎるかもしれないので、これを見てる誰か、作ってくれ!!

スタイルレスな UI Kit を使う

Figma Community にはスタイルレスで構造のみを持った UI Kit が色々作られています。 例としては下記のようなものがあります。

www.youtube.com

tokens.studio

一方でこれに則って作る手法を学習するくらいなら、もはや Framer などのコードにエクスポートすることを最初からゴールにしているデザインツールを使った方がいいんじゃないか感がなくもないです。 www.framer.com

Figma で表現できないものはスタイルを付与できない

大体のものが実現できるようになった Figma ですが、頻出 CSS としてまだ足りない感があるものとしては次のものがあります。他にも細かいものは色々あるとは思いますが大きいものだとこれくらいだと思います。

  • flex-wrap
  • table layout
  • grid layout

情報がないものはないのでどうしようもなく、Figma に頑張っていただくしかありません。 幸い flex-wrap は Auto Layout v5.0 なるもので登場しそうで(下記はおそらく早めに権限をもらった方)

また、table に関しては既に FigJam で登場しており、これは Figma にもコピペして使えることからその内 Figma でもリリースされるだろうと予感しています。

www.figma.com

という訳で課題はありますが、デザインツール由来の実装のものとのズレはほぼほぼなくなるんじゃないかなと勝手に期待しています。

同じページで複数のフレームがある場合の対応

実際のデザインでこのプラグインを使い始めて「あーこれどうしよう?」と思ったのが "同じページで複数のフレーム" がある場合です。

例えば下記のように、同じページ内でローディング表示したりモーダルとかがにゅっと出たりして、デザインが複数のフレームで表現されることはよくあることです。

Home画面のデザインとHome画面にモーダルが表示されている図

人間が差分に対してだけプラグインを実行して手動コピペでもいいのですが、せっかくならフレームをまとめて選択したら状態に応じてだし分けるコードも書いてくれたらテンション爆上げだと思うので、何かしら実現方法を考えてみたいと思っています。

より良いデザインプロセスを問い続ける

ここまでガッツリこのプラグイン開発記について語ってきたのですが、正直な気持ちを吐露しますと、これを作っていて「そもそもデザインを Figma でやる必要はあるのか?」という問いはしっかり考えて行った方がいいなと感じました。

より具体的に言うと、「このきっちりデザインを作る作業はコーディングするコストと変わらないのでは」だったり「そもそもデザイン案を直接コードで表現してもいいのではないか?」という問いです。

一点目に関していうと、より良いコード生成のアウトプットのためにはデザインをより構造的に作ったり、メタデータの付与などを頑張りたくなるのですが、独自にデザインに対してルールを覚えるくらいなら諦めてコードで書いた方が良くね?という線引きはどこかで生じると思います。

「デザインからコードを生成する」という目的に捉われすぎて全体のプロセス改善に繋がらなくては本末転倒なので、「デザインをきっちり作る」ことはコード生成の文脈のみならず、デザイン自体のメンテナビリティだったりあるいは他の目的にもプラスに働くものであるかは、常に冷静に判断すべきでしょう。

一方別の話で、 LLM によってデザインプロセス自体が大きく変わる可能性にも柔軟に開いていたいなと考えています。

例えばですが、現状ではデザイナーがスクラッチで(コンポーネントやデザインシステムのルールに則ったりはあると思いますが)デザインを作っていってると思いますが、もしかしたら今後は作りたい仕様・ユースケースと既にあるデザインをインプットにしたら AI がデザイン案を出してくれて、それをちょっとチューニングして実装するUIデザインのプロセスが訪れるかもしれません。

そして、その時、AI がアウトプットするデザインは Figma のようなデザインツールで表現される必要はありません。実際にユーザが触れるコードで出力をしてしまえばいいのです。

Magician などの AI を活用した Figma プラグインの数々を作ってきた jsngr さんは既にそういった AI を前提にした新しい体験のデザインツールを作り始めているように見えます。

こういった新しい体験のデザインプロセスになった時、今この記事で私が解説した Figma を前提にしたコード生成プラグインは価値が無になりますが、サンクコストに縛られず、常により良いプロセスを模索していきたいなと思います。

おわりに

以上、ChatGPT を使ってコードを生成する取り組みについて語ってきました。

私個人としては長年の夢だった見た目部分のコードを書かなくてよくなる体験がグッと近づいてきた感がありワクワクする毎日です。

一方で課題もたくさんあるので Gaudiy で試していきたいのはもちろん、色んな企業の人でこちらを試していただいてより良いデザイン - 実装プロセス作りの知見を共有していけるとなおのこと楽しいかなと思い、こちらのプラグインを OSS として作りました。

ぜひお試しいただいて、良かったこと辛かったことをご共有いただけると大変嬉しいです。

そして最後に宣伝コーナーです。

Gaudiy では絶賛プロダクト開発人材を募集中です。会社としてLLMに投資しており、エンジニアもデザイナーも特に AI を活用した業務改善、所謂 AIOps にもかなり力を入れています。こういった取り組みにワクワクする方は楽しく働けるのではないかなと思います。私はまだ入社してないので多分。

recruit.gaudiy.com

それでは!

参考資料

www.figma.com community.openai.com www.pinecone.io

GitHub Copilot 導入して1ヶ月経ったので振り返ってみた

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

今年に入ってから、AIの話題が尽きることのない、楽しい日々を送っています。それにしても目まぐるしく発展していっていますね〜

Gaudiyもちょうど1ヶ月前にプレスリリースで発表しましたが、全社的にChatGPT PlusとGithub Copilotを導入して、時代の変化に合わせたプロダクト開発スタイルを模索しているところです。

prtimes.jp

(最近AIツールの模索にハマりすぎて3時に寝落ちる日々を送っています。)

Twitterでも、#GitHubCopilot使えます のタグをよく見かけますし、GitHub Copilotを使い始めている企業が続々と増えているようです。

ということで、今回は、GitHub Copilot導入後ちょうど1ヶ月の運用を振り返るためのアンケートを取ってみました。はたして、福利厚生は実際に使われているのでしょうか…? 

まだ導入に踏み切れていなかったり、検討している企業のご参考になれば嬉しいです!

1. みなさんGitHub Copilot使ってますか?

約1ヶ月前、3月16日に、開発メンバー全員のアカウントに対して、Github Copilotの権限を一斉に付与しました。

実際のSlackでの呼びかけ

Slackで全体に告知しただけで、はたして使われているのでしょうか….?

アンケートに回答してくれた12人のうち、11人が使っているようです。

ちなみに「使ってない」と回答したエンジニアの理由は、

とのことでした。(このzcheeさん↓は実際にめちゃくちゃコード書くの速いですw)

2. 使ってみてどうですか?

次に、実際に触ってみた感想をみてみましょう。まずはメリット面から。

Copilot(副操縦士)とは言い得て妙というところで、実装者が本来より頭を捻るべき箇所に対して、コンテキストを集中できるようにサポートしてくれる点が、やはり効果として実感されています。

自分としても、実際にコードをタイピングする量としては、2分の1程度には下がったような感覚があります。そのおかげで、何を、どう作るのが自分たちのプロダクトとして適切なのか?に対して、より考える時間を取れるようになったと思います。

また、Copilotが生成してくれるコードは、自分が出力していながらもコーディングをしていないので、客観的な視点を持つことが可能です。具体的に言うと、レビューするような感覚で、これは適切な出力なのか?を考えられるようになりました。シンプルなコードがゆえに意識が低くなり、質が下がるようなことが少なくなったと考えています。

次に、デメリット面をみてみます。

ほぼ、ない!という意見も多いものの、

このように、Copilotとの付き合い方の面で、調整する必要があるとの意見が多く見られました。

実害のある挙動をすることはないですが、従来の補完機能と競合して操作しずらい部分があったり、使う側の出力の期待値や扱い方が慣れきれていなかったりすることに起因するものが、やはり多いようです。

ここに関しては、ツールとしても自分たちとしても成熟していく必要があるでしょう。

3. 今後も使い続けたいと思いますか?

最後に、今後も使い続けたいか?という回答に関しては、全員がpositiveに回答しています。

多くの人が、Copilotなしの生活に戻れなくなっているようです。

4. AIとこれから

今後、Copilotに関わらず、AIによる自動生成を用いたツールは、どんな形であれ広く活用されていくことになると思います。

どのようなツールが覇権を握るとしても、共通して大事なことは、生成結果の正当性やそれを自分たちのプロダクトや業務に使うことの正当性を、自らの視点で評価をした上で活用することだと考えています。

どの部分を任せ、どの部分を握るべきか?どのように評価し活用すべきか?これに関して答えが出ていないことも多く、人間側として思考をアップデートしていくべき点に溢れていると思います。

このすばやい変化に対して、個人として、組織として、適応できるかできないかが、今後の生産性に大きな差を生んでくると考えています。ここに適応するべく、今後もGithub Copilotの活用だけでなく、他のAIツールやプロセスに対しても検証を回していきます。

デメリット面でもあげたように、まだまだ使いにくさがあるのも事実です。でも、Gaudiyとしては一般的に使いやすい状態になる前から、今、組織的にここに投資していくことが大きな競争優位になると思っていますし、ビジョンの実現に向けて大きく飛躍できるチャンスだと考えています。

これまで50人いなければ開発できなかったものが、3人で開発できるようになる未来ってワクワクしませんか? AIと共創してどんどん業務改善もプロダクト活用も進めていきたいです。

実際に手を動かして、この時代の変化を乗りこなしていきたい方、ぜひ一緒にやっていきましょう!!

recruit.gaudiy.com

Kotlin(Ktor)にOpenTelemetryを導入し、Google Cloud TraceにExportした話

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

はじめまして。Gaudiyでエンジニアをしているあんどう(@Andoobomber)です。

GaudiyではKotlin, Go, Node.jsでのBE開発をしており、前職ではKotlin×Spring BootでC2Cサービスの開発をしていました。

先日、↓の記事でも伝えたように、GaudiyではマイクロサービスにおけるObservabilityの課題に対して、OpenTelemetryの導入を行いました。そこで自分は、Kotlin(Ktor)環境への導入を担当しました。

techblog.gaudiy.com

その中で、実際のアプリケーションでの導入事例や、特定のAPMへのExport方法に関してはまだ情報が少ないかなと感じたので、今回この記事を書くことにしました。

同じような技術スタックの方や、OpenTelemetryを実際に導入しようと考えている方の参考になればと思います。

1. OpenTelemetryって何?

OpenTelemetryとは、テレメトリーデータ(ログ、メトリクス、トレース等)の計装と送信の標準仕様、及びそのライブラリーとエージェントを提供するプロジェクトです。

これだけ聞いてもなんのこっちゃわからんと思いますが、具体的には以下のイメージです。

1-1-1. ログ

アプリケーションに発生したイベントを記録するテキスト。ロガーライブラリによって吐かれるわりとよく見るやつ。

Log_sample

1-1-2. メトリクス

一定期間に測定された数値。 例: アクセス数、レイテンシ、CPU使用率

Metrics_sample

1-1-3. トレース

依存関係のあるシステム間で、リクエストのフローをエンドツーエンドで表すもの。

Trace_sample

OpenTelemetryはこれらを標準仕様化&SDKを言語毎に用意してくれるので、各マイクロサービスに導入することでアプリケーションの健全性を監視したり分析することが容易にできるようになります。

1-2. 計装方法

計装には、手動計装(Manual Instrumentation)と自動計装(Auto Instrumentation)の2パターンがあります。

手動計装

  • 自前で実装する必要がある
  • 柔軟な計装ができる

自動計装

今回は両方の計装を試してみたので、その実装方法について書きたいと思います。

2. Kotlin(Ktor)にOpenTelemetryを導入する

前置きが長くなりましたが、ここからが本題です。

今回検証したのは主にTraceになるのですが、一応Metricsも導入しています。 また、前提情報としてGaudiyのKotlinサーバーではFWにKtor, DIコンテナにKoinを利用しています。

まず、手動計装(Manual Instrumentation)の実装からです。

2-1. 手動計装

2-1-1. 依存関係のダウンロード

// build.gradle

implementation platform('io.opentelemetry:opentelemetry-bom:1.21.0')
implementation 'io.opentelemetry:opentelemetry-api'
implementation 'io.opentelemetry:opentelemetry-sdk'
implementation 'io.opentelemetry:opentelemetry-exporter-jaeger'
implementation 'io.opentelemetry.instrumentation:opentelemetry-ktor-2.0:1.21.0-alpha'
implementation 'com.google.cloud.opentelemetry:exporter-trace:0.23.0'
implementation 'com.google.cloud.opentelemetry:exporter-metrics:0.23.0'
implementation 'com.google.cloud.opentelemetry:propagators-gcp:0.23.0-alpha'

GaudiyではFWとしてKtorを利用しているので、Ktorのinstrumentationを入れています。 jaegerを入れているのはローカル確認用になります。

2-1-2. jaeger用のdocker-compose.ymlを作成

// docker-compose.yml

version: '3.2'

services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "5775:5775/udp"
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
      - "16686:16686"
      - "14268:14268"
      - "14250:14250"
      - "9411:9411"

2-1-3. コード書く

OpenTelemetryをアプリケーション導入する処理を書きます。

// SetOpenTelemetry.kt

fun Application.setOpenTelemetry(env: Env): OpenTelemetry {
  // Resourceの設定.
  // https://opentelemetry.io/docs/reference/specification/resource/sdk/
  val resource: Resource = Resource.getDefault()
    .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "${SERVICE_NAME}")))

  // PropagatorはW3C Trace ContextとGCP Propagator両方利用しています.
  val propagators = ContextPropagators.create(
    TextMapPropagator.composite(
      XCloudTraceContextPropagator(false),
      W3CTraceContextPropagator.getInstance(),
    ),
  )

  // otel build.
  val openTelemetry = OpenTelemetrySdk.builder()
    .setTracerProvider(createTraceProvider(env, resource))
    .apply { if (!env.isLocal) createMeterProvider(resource) }
    .setPropagators(propagators)
    .build()

  // ktor instrumentationのinstall.
  install(KtorServerTracing) {
    setOpenTelemetry(openTelemetry)
  }
  
  return openTelemetry
}

// TraceProviderの作成
fun createTraceProvider(env: Env, resource: Resource): SdkTracerProvider {
    // localはjaeger, その他環境ではGCPへExportするように設定
  val traceExporter = if (env.isLocal) {
    JaegerGrpcSpanExporter.builder().setEndpoint("http://localhost:14250/api/traces").build()
  } else {
    TraceExporter.createWithDefaultConfiguration()
  }
  return SdkTracerProvider.builder()
    .addSpanProcessor(SimpleSpanProcessor.create(traceExporter))
    .setResource(resource)
    .build()
}

// MeterProviderの作成
fun createMeterProvider(resource: Resource): SdkMeterProvider {
  val metricExporter = GoogleCloudMetricExporter.createWithDefaultConfiguration()
  return SdkMeterProvider.builder()
    .registerMetricReader(
      PeriodicMetricReader.builder(metricExporter)
        .setInterval(Duration.ofSeconds(30))
        .build()
    )
    .setResource(resource)
    .build()
}

計装クラス(SampleService)にTracerを生成し、処理の間にspanを生成・記録します。

// SampleService.kt

class SampleService(private val repository: SampleRepository, openTelemetry: OpenTelemetry) {
  
  private val tracer: Tracer = openTelemetry.getTracer("SampleService")

  fun execute() {
    // start span.
    val span = tracer.spanBuilder("execute").startSpan()
    
    val data = repository.get()
    println(data)
    
    // end span.
    span.end()
  }
}

SampleServiceをエンドポイントを叩いて呼び出すようにします。

// SampleRouting.kt

fun Route.sampleRouting(sampleService: SampleService) {
  route("/sample") {
    get {
      sampleService.execute()
      call.respondText(LocalDateTime.now().toString(), status = HttpStatusCode.OK)
    }
  }
}

最後に、作成したOpenTelemetryやSampleService等をシングルトン化し必要なクラスに渡します。

// Application.kt

fun Application.module() {
    ...
    
    // Env is original config class.
  val env = Env.of(environment)
    val openTelemetry = setOpenTelemetry(env)
    
  install(Koin) {
    module(
        org.koin.dsl.module(createdAtStart = true) {
        single { openTelemetry }
        single { SampleService(get(), get()) } // openTelemetry利用クラス
          }
    )
  }
  
    ...
  
  val sampleService by inject<SampleService>()
  routing {
    sampleRouting(sampleService)
  }
}

2-1-4. Jaeger起動&Application起動&エンドポイント叩く

$ docker compose up

$ ./gradlew runShadow
{"time":"2022-12-14T17:43:01.261Z","message":"Autoreload is disabled because the development mode is off.","logger_name":"Application","thread_name":"main","severity":"INFO"}
{"time":"2022-12-14T17:43:04.317Z","message":"Application started in 3.156 seconds.","logger_name":"Application","thread_name":"main","severity":"INFO"}
{"time":"2022-12-14T17:43:04.323Z","message":"Application started: io.ktor.server.application.Application@2eda4eeb","logger_name":"Application","thread_name":"main","severity":"INFO"}
{"time":"2022-12-14T17:43:04.588Z","message":"Responding at http://0.0.0.0:8080","logger_name":"Application","thread_name":"DefaultDispatcher-worker-1","severity":"INFO"}

$ curl http://localhost:8080/sample
2022-12-15T02:46:40.909022

2-1-5. Traceの確認

ブラウザでhttp://localhost:16686にアクセスします。 Trace_manual_local 今回はjaegerで確認しましたが、いい感じに記録されてます。

2-2. 自動計装

続いて自動計装(Auto Instrumentation)になります。

2-2-1. opentelemetry-javaagent.jarのダウンロード

JVM系で自動計装するには、opentelemetryのjavaagentをアプリケーションに入れる必要があります。 Releaseにて最新のopentelemetry-javaagent.jarをダウンロードすることができるのですが、今回はbuild時にgradleで自動ダウンロードするようにしてみました。

// build.gradle

plugins {
  id "com.github.johnrengelman.shadow" version "7.1.2"
    id "de.undercouch.download" version "4.1.1"
}

def agentPath = project.buildDir.toString() + "/otel/opentelemetry-javaagent.jar"

task downloadAgent(type: Download) {
    src "https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.21.0/opentelemetry-javaagent.jar"
    dest agentPath
    overwrite true
}

shadowJar {
    dependsOn("downloadAgent")
      ...
}

2-2-2. opentelemetry-javaagent.jarの適用

fat jar作成時に-javaagent:path/to/opentelemetry-javaagent.jar のようにjavaagentのconfigを記載します。 また、exporterの指定などotelに必要なconfig達を環境に合わせて渡しておきます。 ※configについて詳しくはagent-configを確認。

// build.gradle

def isLocal = !project.ext.has("ENV")

shadowJar {
    dependsOn("downloadAgent")
    ...
    def args = [
            "-javaagent:${agentPath}",
            "-Dotel.service.name=sample-service",
    ]
    if (isLocal) {
        args += [
                "-Dotel.traces.exporter=jaeger",
                "-Dotel.metrics.exporter=none",
        ]
    } else {
          args += [
                "-Dotel.traces.exporter=google_cloud_trace",
                "-Dotel.metrics.exporter=google_cloud_monitoring",
        ]
    }
    applicationDefaultJvmArgs = args
}

2-2-3. 実装

依存関係の追加

// build.gradle

dependencies {
    ...
    implementation 'io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:1.21.0'
    ...
}

手動計装の際にSpan作成した箇所(SampleService)を修正します。OpenTelemetryやTracerを削除し、Span作成するfunctionに対して@WithSpanアノテーションを付与します。

// SampleService.kt

import io.opentelemetry.instrumentation.annotations.WithSpan

class SampleService(private val repository: SampleRepository) {
    @WithSpan
    fun execute() {
        val data = repository.get()
               println(data)
    }
}

2-2-4. Jaeger起動&Application起動&エンドポイント叩く

Jaegerのdockerファイル用意は手動計装と同じなので割愛します。

$ docker compose up

$ ./gradlew runShadow
[otel.javaagent 2022-12-15 03:45:35:551 +0900] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 1.18.0
{"time":"2022-12-14T18:45:42.522Z","message":"Autoreload is disabled because the development mode is off.","logger_name":"Application","thread_name":"main","severity":"INFO"}
{"time":"2022-12-14T18:45:45.724Z","message":"Application started in 3.875 seconds.","logger_name":"Application","thread_name":"main","severity":"INFO"}
{"time":"2022-12-14T18:45:45.726Z","message":"Application started: io.ktor.server.application.Application@44e3f3e5","logger_name":"Application","thread_name":"main","severity":"INFO"}
{"time":"2022-12-14T18:45:47.223Z","message":"Responding at http://0.0.0.0:8080","logger_name":"Application","thread_name":"DefaultDispatcher-worker-2","severity":"INFO"}

$ curl http://localhost:8080/sample
2022-12-15T04:05:28.119532

2-2-5. Traceの確認

アノテーション付けただけでSpanが記録されました。とても簡単で便利ですね。 他にも@SpanAttributeなどでSpanに対して属性を追加できるみたいです。※詳しくはannotationsをご確認ください。

Trace_auto_local

これでKotlinで手動計装・自動計装ができました。次にこのテレメトリーデータをGoogle Cloud Traceに出力してみたいと思います。

3. Google Cloud Traceに出力する

今まではサンプルコードを使ってテレメトリーデータを計装してみましたが、どうせなら色々Spanがみれた方が良いと思うので、Gaudiyのアプリケーションを用いてOpenTelemetryを実装・Google Cloud TraceにExportしてみようと思います。

3-1. 手動計装の場合

手動軽装は上記のSampleの実装で利用することができます。 一応おさらいしておくと、Metrics, Trace共にGCP用Exporterを作成し、MetricsProvider, TraceProviderを作成。それをOpenTelemetryに読み込ませるだけです。

// SetOpenTelemetry.kt

fun Application.setOpenTelemetry(env: Env): OpenTelemetry {
  // Resourceの設定.
  // https://opentelemetry.io/docs/reference/specification/resource/sdk/
  val resource: Resource = Resource.getDefault()
    .merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "${SERVICE_NAME}")))

  // PropagatorはW3C Trace ContextとGCP Propagator両方利用しています.
  val propagators = ContextPropagators.create(
    TextMapPropagator.composite(
      XCloudTraceContextPropagator(false),
      W3CTraceContextPropagator.getInstance(),
    ),
  )

  // otel build.
  val openTelemetry = OpenTelemetrySdk.builder()
    .setTracerProvider(createTraceProvider(env, resource))
    .apply { if (!env.isLocal) createMeterProvider(resource) }
    .setPropagators(propagators)
    .build()

  // ktor instrumentationのinstall.
  install(KtorServerTracing) {
    setOpenTelemetry(openTelemetry)
  }
  
  return openTelemetry
}

// TraceProviderの作成
fun createTraceProvider(env: Env, resource: Resource): SdkTracerProvider {
    // localはJaeger, その他環境ではGCPへExportするように設定
  val traceExporter = if (env.isLocal) {
    JaegerGrpcSpanExporter.builder().setEndpoint("http://localhost:14250/api/traces").build()
  } else {
    TraceExporter.createWithDefaultConfiguration()
  }
  return SdkTracerProvider.builder()
    .addSpanProcessor(SimpleSpanProcessor.create(traceExporter))
    .setResource(resource)
    .build()
}

// MeterProviderの作成
fun createMeterProvider(resource: Resource): SdkMeterProvider {
  val metricExporter = GoogleCloudMetricExporter.createWithDefaultConfiguration()
  return SdkMeterProvider.builder()
    .registerMetricReader(
      PeriodicMetricReader.builder(metricExporter)
        .setInterval(Duration.ofSeconds(30))
        .build()
    )
    .setResource(resource)
    .build()
}

3-2. 自動計装の場合

自動計装はopentelemetry-javaagent.jarがexportを行うので、opentelemetry-javaagent.jarにgcp用exporterを指定する必要があります。

しかし、自動計装用のcustom exporterはPoC段階でありexporter-auto.jarファイルをmaven repositoryから取得、もしくは自分で生成しなければならなそうでした。(2022/12/15現在) ※詳しくはexporter-autoをご確認ください

3-2-1. gcp exporterの生成

GCP opentelemetry-eperations-javaをcloneして、fat jarをbuildし、~/exporters/auto/build/libs/exporter-auto(.*).jarを生成します。

参考: opentelemetry-operations-java/build.gradle at main · GoogleCloudPlatform/opentelemetry-operations-java · GitHub

3-2-2. 生成したjarファイルをアプリケーション内に設置

$ cp ~/exporters/auto/build/libs/exporter-auto(.*).jar path/to/application/exporter-auto(.*).jar

3-2-3. exporterをopentelemetry-javaagent.jarのconfigに設定

GaudiyではプラットフォームとしてCloud Runを利用しているため、Dockerfileを書きます。 otel.javaagent.extensionsに先程のpath/to/application/exporter-auto(.*).jarを指定します。

// Dockerfile

FROM adoptopenjdk/openjdk11:alpine-slim

EXPOSE 8080

ENTRYPOINT java \
  -Dfile.encoding=UTF-8 \
  -javaagent:path/to/otel/opentelemetry-javaagent.jar \
  -Dotel.javaagent.extensions=path/to/application/exporter-auto(.*).jar \
  -Dotel.service.name=sample-service \
  -Dotel.metrics.exporter=google_cloud_monitoring \
  -Dotel.traces.exporter=google_cloud_trace \
  -jar ./build/libs/sample.jar

この時、exporterやservice.nameなどのopentelemetry configを渡します。

3-2-4. Docker build & Docker Push & Cloud Runデプロイ

$ docker image build -t $IMAGE_PATH .
$ docker image push $IMAGE_PATH
$ gcloud beta run deploy sample-service --image $IMAGE_PATH

3-2-5. Traceの確認

少しわかりにくいかもしれませんが、ServiceA → ServiceB → ServiceCとTraceが伝搬されていることがわかります。 自動計装によりGraphQL, Firestore, Spannerの処理も計装できていていい感じですね。

Trace

  • ServiceA: Node.js
  • ServiceB: Kotlin
  • ServiceC: Kotlin

4. まとめ

Kotlin(Ktor)サーバーにOpenTelemetryの導入ができました。ただ、Google Cloud TraceへのExportがPoCであり若干の荒技感あるので、今後はOpenTelemetry Collector経由でテレメトリーデータを扱うようにしたいと考えています。

また、Metrics, Logについても本格的にアプリケーションに適用して、Observabilityの向上に努めたいです。

そういえば今日はクリスマスイブですね。Merry Xmas, Merry OpenTelemetry!

recruit.gaudiy.com

5. 参考文献

opentelemetry.io

cloud.google.com

github.com

github.com

opencensus.io

DevOpsのCAMS原則に従ってアラート/不具合対応プロセスを改善する

この記事は「Gaudiy Advent Calendar 2022」の21日目の記事になります。

こんにちは。Web3スタートアップのGaudiyで、エンジニアをしているhaseyan(@hassey_11)です。

Gaudiyは、この1年で開発組織が倍以上になってきており、来る2023年に向けて組織もプロダクトも加速度的に成長しています。それに伴い、開発〜運用プロセスにおいていくつかの問題が発生していたため、DevOps の CAMS 原則に則ってアラート/不具合対応プロセスの改善に取り組みました

今回は、その改善プロセスについて一連の流れを振り返ってみたいと思います。スケールする開発組織の課題を抱えているチームの方に参考になれば嬉しいです!

1. スケールするプロダクト組織と DevOps の必要性

Gaudiy は、Gaudiy Fanlinkという、IPコンテンツホルダー自身がファンコミュニティ-ECプラットフォームを立ち上げることを可能にするサービスを開発・提供しています。

一般的に、人が増えるのに比例して、プロダクトの成長スピードが上がっていくわけでは必ずしもありません。むしろ、人数やサービスの規模が増えるにつれて、これまでは問題にならなかったことが新たな問題として表出してきます。

開発面では、トラフィック増加によるパフォーマンス問題が発生したり、開発の大規模化に伴うリファクタやシステムのリプレイスが必要になる場合もあります。また運用面では、リリースが複雑になりすぎたり、一部のドメイン/サービス領域が属人化するといった問題も生まれるかもしれません。また当然ですが、開発や運用だけでなく、組織面での問題も発生します。

スタートアップがスケール後もその成長スピードを保つためには、こうした問題を可能な限り防ぎ、スケールしても1人1人がプロダクトのアウトカムに貢献できる総量を高く保ち続けることが重要です。

こうした問題の中でも、運用や組織の開発プロセスに関する問題にアプローチする際に役立つ考え方のひとつが、DevOps です。

2. 実際に起こっていた問題

表題の通り、Gaudiyではアラート/不具合対応プロセスに関して、以下の課題を抱えていました。

  • アラート/不具合が発生した際に、明確な対応フローが定まっておらず、対応が属人化している
  • 不具合の対応状況が運営チームと開発チーム間でうまく同期されていない etc.

これまでは特に仕組みを作らずとも、アラートや不具合が発生した際にメンバーが自発的に対応を行うだけで、問題なく運用できてました。

しかし、開発に関わるメンバーが増えるにつれて対応する/できる領域が属人化していき、いわゆるアジャイルのトラックナンバー 1 が少ない状態になっていきました。また各人が通常のスクラムプロセス外で対応を行うので、報告者及び対応者以外の第三者が状況を把握することが難しくなっていました。

こうした属人化の影響が大きくなると、不具合全体の定量分析によって発見できたはずの、アーキテクチャレイヤの潜在的な問題点を見落としたり、監視として最適なアラートの設計などが遅れるリスクも高まります2

こうした課題を解決するために、DevOps の考え方に基づき、アラート/不具合対応のプロセスについて改善を行いました。

3. DevOps の CAMS 原則に基づく改善策の検討

DevOps の考え方の重要な要素として、CAMS(culture, automation, metrics, sharing)というものがあります。 システム運用アンチパターンによると、CAMS の各要素は以下のようになっています。

  • culture(文化)
    • チームが活動する上での基準に影響するもの
  • automation(自動化)
    • 人的資本を平凡な作業から解放するもの
  • metrics(メトリクス)
    • 物事がうまく機能しているかどうかを判断するために必要なもの
  • sharing(共有)
    • 知識は自由であるという考えのもと、文化を強化するために必要なもの

各要素の詳細や DevOps のより具体的な考え方などは上記の書籍などで学ぶことができますが、大まかには、運用を改善していくには「automation によってアンチパターンを避けながらリソースを効率化し、その結果について metrics を用いて判断・フィードバックを行った上で、得られた知見を sharing することで culture が強化されていく」と捉えると理解しやすいかもしれません。

この CAMS に従って、アラート/不具合対応プロセスの問題に対して、以下の流れで改善策を検討しました。

  1. 関連メンバーに現状のプロセスの概要及び課題感のヒアリング
  2. プロセス改善の why と改善方針のsharing
  3. プロセスの automation の実施
  4. 各種不具合/アラート対応 metrics の可視化と評価
  5. metrics の結果分かったことと更なる改善方針の sharing

4. アラート/不具合対応プロセスの改善でやったこと

上記の検討に沿って、具体的にアクションを実行していきました。

4-1. 関連メンバーに現状のプロセスの概要及び課題感のヒアリング

Gaudiy には「Be Agile に最大効率を徹底しよう」という行動指針があり、今回の改善でも、小さな改善を繰り返し行っていくことを意識していました。

具体的にはヒアリングをもとに、現状のプロセスの整理と各メンバーの課題感の洗い出しを行い、最も取り組むべき問題を特定することを試みました。

この行動指針ではチームをまたいだ「ちょいコラ」(ちょっとコラボするの略、Gaudiyの社内用語)が推奨されており、どのメンバーも「ちょいコラ」に慣れているので、コラボレーションがとてもしやすい環境になっていると感じています。

プロセスと課題感の整理は miro で行い、以下のような形に整理しました。ヒアリングはプロセスを追いながら課題感を付箋でマークしていく形で行い、課題のある場所とその内容が明確になりました。また、各人がプロセスに関してそれぞれ異なった認識をしていたこともわかりました。

プロセスの整理

この結果、アラートがわかりづらくて課題を特定しづらい、情報共有がうまくできてないなど、いくつかの開発/運用/コミュニケーションに関する問題が出てきました。特に、現状の一番大きな課題は 「不具合/アラート発生後の進捗管理・対応方法が属人化しており、特定個人の負荷が高まっていること」であると結論づけました。

4-2. プロセス改善のwhyと改善方針のsharing

軽微な仕組みであっても、プロセスの実行主体は常に受け入れる側であるため、その受け入れる側に納得感や理解がなければ仕組みの導入や浸透が難しくなります。また、担当者がいなくなると仕組みがすぐに陳腐化するといったこともあるので、仕組みを作るだけでなく、それを支える文化を醸成していくことも同時に行う必要があります。

今回の取り組みでは、ヒアリングを通じて把握できた現状の課題感と、将来的にアラート/不具合改善のプロセスを改善した結果、どういったことを実現したいかをドキュメントにしてチームに共有しました。

また、その実現に向けた大まかな方針から紐解き足元における改善後のプロセスを示し議論をすることで、納得感を得られるように努めました。方針については最終的な理想系から逆算する形で提案したことで、将来像の議論と共通認識をすることができ、結果としてとても良い議論ができました。

Why/方針の共有 改善後プロセスの共有
Why の共有 改善後プロセスの共有

具体的な改善後のプロセスとしては、個人単位ではなくサービスのオーナー単位で対応を行い、そのリソースもチーム内で管理するという方針で合意しました3

4-3. プロセスの automation の実施

DevOps の中でも個人的に重要だと考えているのが、この automation の部分です。人力での作業/属人性を減らし、プロダクトのアウトカムにリソースを集中できるかの多くは、このautomationの度合いに依存します。

上述の改善後プロセスの画像内の黒い四角の1つ1つは、各プロセスで出てくる Slack bot のスクリーンショットになっています。このプロセスの図は一見すると少し複雑になっていますが、ほぼ全てがSlack上で完結するようになっています。

不具合・アラートの検知 サービスオーナーのアサイン
解決済みの不具合・アラート 次スプリントで解決 ignore済みの不具合・アラート

この Slack bot は TypeScript/Node.js で作成し、 Cloud Function 上の1つのエンドポイントで動作しています。Slack の API はとてもリッチで、簡単にインタラクティブな bot を作成することができます。DevOpsを推進する上では非常に強力なツールになるので、興味がある方は是非試してみてください。

また、手動による不具合報告とサーバやSentryなどの3rdパーティツールから送られるアラートは、zapierを利用して一つのチャンネルに集約され、全て統一されたプロセスで対応できるようになっています。zapierもかなり自由度が高く、おすすめのツールです。

4-4. 各種不具合/アラート対応 metrics の可視化

上記のプロセスにて運用を開始してからしばらくしたのち、いくつかの指標を用いて当初の課題が解決されているかを確認しました。

1つ1つのアラート/不具合のデータは全て Notion に蓄積されているようにしているため、そのままダウンロードし、Google Spreadsheet で可視化することができました。Notion はドキュメント管理だけでなく、DevOps 観点では簡易 DB としても利用できるため、非常に重宝しています。

【確認したい課題1】個人単位の負荷は減ったか?

こちらの課題は、どれくらいアラート/不具合対応の担当領域が分散しているか、という観点で、発生したアラート/不具合のサービス領域の割合を確認しました。

サービス分散の図

この図から、特定の領域でのみ対応が発生しているということはなく、個人に大幅な負担がかかっている状況は解消されつつあるのかなと判断しています。また、データだけでなく、メンバーにヒアリングした際にも、問題の解消が確認できました。

【確認したい課題2】対応方法は標準化されているか?

こちらは、発生したアラート/報告された不具合のうち、改善後のプロセスでは、属人的に解決される=プロセス外で解決or対応漏れが発生していないことを確認しました。結果としては、約1ヶ月で37件の対応が発生し、そのうちの34件はその日のうちに適切なプロセスでチーム内タスクへの統合や根本対応が行われ、丸2日以上放置される件数は0となっていました。

この結果から、個人で対応するのではなく、チームで対応するという仕組みが実現できたと判断しました。

Gaudiy の社内用語には「チャレオン」(人のチャレンジに乗っかるの意)というものがあり、こうした新しい仕組みにも積極的に乗っかってくれる人が多く、非常にスムーズに導入が進みました。

4-5. metrics の結果わかったことと、さらなる改善方針の sharing

ここまでのCAMSの一周目の取り組みを通して、

  • 不具合/アラートが起きやすい箇所やその原因の分析
  • 発生したときに対処しやすい意味のあるアラート設計の方針

など、metricsを取ったことで得られた新しい知見が得られ、さらなる改善方針などをより解像度高く考えられるようになりました。

これらは現在、teamごとのDevLeader(選挙で選ばれる任期性の開発リーダー)と週次で共有・振り返りをしており、ネクストアクションを制定したり、必要に応じてチームや横断的な改善チームに相談しながら、改善に向けて動いています。

また、このプロセス自体にもいくつかのさらなる改善ポイントがあり(開発JiraチケットとNotionの連携automationなど)、随時2周目となるアップデートを行っていく予定です。

開発チーム全体には、週報として Slack でサマリを展開しています。

週報

5. おわりに

こうした DevOps の取り組みは、やりたいけど時間がない、という方は多いと思います。

ありがたいことにGaudiyでは、平日のアウトカムを高めるために、毎週水曜日に横断的な課題を解決するために時間をとる「EMPOWER-DAY」という仕組みがあります。今回の取り組みはほぼ全てこの水曜日に行ったものであり、 組織全体でこうした取り組みに時間を割くことを許容する文化があるので、非常に進めやすかったです。

あらゆる課題は、代表Dev(選挙で選ばれる任期性の開発全体のリーダー、所謂CTOポジション)などと適切に合意を取れば、手を挙げれば誰でもどんな課題でも取り組むことができ、課題解決が好きな人は楽しい環境かなと思います。

私自身DevOpsの考え方は大好きですし、スケールする開発組織には必須だと考えているので、引き続きプロダクト開発と並行して改善を進めていこうと思います。

recruit.gaudiy.com


  1. 特定の人物がトラックに轢かれるなど何かしらの要因で突発的に離脱することで、プロジェクトが立ちゆかなくなったり、継続して開発が困難になる人数
  2. 不具合の報告の際にはランダムにメンバーがアサインされる形でしたが、FE/BE といった領域や、サービスに関係なくアサインされるため、人によってはキャッチアップコストが高かったり、ユーザ影響が小さく優先度が低い場合に通常のタスクの後回しになり、対応漏れが発生するといった問題もありました。こうした対応漏れもメンバーの自主的な動きによって解決されていましたが、プロダクトのスケールとともに各人が持つコンテキストが複雑になっていき、負荷が高まっていました。
  3. 元々はチーム単位で、という提案でしたが、この改善の最中にサービスオーナー(各ドメインのオーナー)という概念が生まれ、そのほか各種プロセスに利用されたので、今回の改善でもその流れを汲むこととなりました。