Gaudiy Tech Blog

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

OpenTelemetry Collector導入の実践編とその後

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

以前、「OpenTelemetry Collector導入のPoCと今後に向けて」という記事を弊エンジニアの sato(@yusukesatoo06)より公開しました。簡単に記事を要約すると、

  1. OpenTelemetry及びOpenTelemetry Collectorの説明
  2. 実際にPoCを作ってみる
  3. 実導入を試みたがOpenTelemetry Collectorのホスティングに悩み、今後の課題として保留となった

といった内容でした。

あれから1年経ち、GaudiyではOpenTelemetry Collectorを本番環境に組み込み、OpenTelemetryの仕様に準拠して計装し、データの分析や監視を行っています。この記事では、前回からの進捗を紹介すると共にOpenTelemetryの導入方法を書きたいと思います。

1. 前回のPoCから変わったこと

1-1. インフラ: Cloud Run → Google Kubernetes Engine(GKE)

前回上がった導入の課題として、「CollectorのホスティングにCloud Runを使用する案が上がったが、Cloud Runの特性上、常時立ち上げておく必要があるCollectorとの相性が悪く断念した」といった課題がありました。(当時はCloud Run SidecarがPreviewでした)

これをどう解決したかというと、Gaudiyのアプリケーション(Gaudiy Fanlink)のインフラ環境をKubernetes(k8s)環境に乗せ替えることで解決しました。

※ただし、OpenTelemetry Collectorの問題を解消するためにk8sに移行した訳ではなく、複合的な要因でk8sに移行しました。詳細については「Kubernetes初学者が担当したGKE移行プロセスの全貌」に書かれているので是非みてください!

1-2. 監視: GCP → Datadog

また、MonitoringツールもGCPの監視サービス(Operations Suite, Cloud Trace/Metrics/Logging)を使っていたのですが、Datadogに乗り換えました。Datadogはとても高機能でGCPやGithubなど多種多様なアプリケーションへのIntegrationがあることや、直感的に操作できるUXが使いやすく乗り換えを決意しました。

元々、全てのアプリケーションはOpenTelemetryの仕様に準拠していたので、Datadogへの移行は、GCP OpenTelemetry Exporterを使ってGCPへExportしていたのを、Collector経由でDatadogにExportするだけで済みました。これがOpenTelemetryの強みであり、予め準拠しておいて本当によかったなと思います。

前回のPoCと現在の構成

2. OpenTelemetry Collectorの導入

2-1. Kubernetesリソースの定義

GaudiyではKubernetesのリソース設定にKustomize × Helmを使っています。以下のKustomize YAMLをArgoCDがGKEに適用しています。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

metadata:
  name: opentelemetry-collector

namespace: opentelemetry-system

helmCharts:
- name: opentelemetry-collector
  repo: https://open-telemetry.github.io/opentelemetry-helm-charts
  releaseName: opentelemetry-collector
    ...

patches:
    - patch: |-
      - op: replace
        path: /spec/template/spec/containers/0/image
        value: ${CUSTOM_OTEL_COLLECTOR_PATH} # カスタムしたOTel Collectorを使用
  - path: ./configmap.yaml # ConfigMapにPatchを充てる
    target:
      version: v1
      kind: ConfigMap
        ~省略~

2-2. Custom OTel Collectorの用意

OpenTelemetry CollectorではDistributionとしてCoreContrib が用意されており、Coreはextensions/connectors/receivers/processors/exporters のベース部分が備わっており、Contribは加えてOTel開発者以外のサードパーティ等が開発した機能(Component)が備わっています。

しかし、OTel Collectorアンチパターンの記事にあるようにCoreもContribも本番環境で使うのは推奨されておらず、アプリケーションに必要なComponentを適時選んで独自でビルドして使うことが勧められています。Gaudiyも独自にビルドしたImageをCUSTOM_OTEL_COLLECTOR_PATH にPushして使っています。

以下はCollectorで使用するComponentのManifestになります。

dist:
  module: github.com/gaudiy/**
  name: otelcol
  description: "Gaudiy OpenTelemetry Collector distribution"
  otelcol_version: "v0.95.0"
  output_path: out
  version: "v0.95.0"

receivers:
  - gomod: go.opentelemetry.io/collector/receiver/otlpreceiver v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.95.0

connector:
  - gomod: go.opentelemetry.io/collector/connector/forwardconnector v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/datadogconnector v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/routingconnector v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/servicegraphconnector v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/connector/spanmetricsconnector v0.95.0

processors:
  - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.95.0
  - gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/attributesprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sattributesprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/probabilisticsamplerprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/transformprocessor v0.95.0

exporters:
  - gomod: go.opentelemetry.io/collector/exporter/debugexporter v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/datadogexporter v0.95.0

extensions:
  - gomod: go.opentelemetry.io/collector/extension/zpagesextension v0.95.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckextension v0.95.0

↑のmanifestをOpenTelemetry Collector Builder(ocb)を使ってビルドしています。

ビルドの手順については公式ドキュメントに書いてあるので、是非そちらを見てみてください。

2-3. CollectorのConfigを設定

ConfigMapにCollectorの設定値を書いています。以前の記事で紹介したreceiverやprocessorなどのフィールドを設定しています。

(一部のパラメータのみ載せています)

apiVersion: v1
kind: ConfigMap
metadata:
  name: opentelemetry-collector-**
data:
  relay: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318

        ~省略~

        processors:
      transform:
        error_mode: ignore
        log_statements:
          - context: scope
            statements:
              # DataDog cannot pick up the span name as resource on
              # Service Entry Spans. We append it here. This might have a
              # side effect to overwrite an application that is instrumented with
              # DataDog SDK. However, as long as OpenTelemetry SDK is used to
              # instrument, this transformation will help.
              - set(attributes["resource.name"], name)
          # - context: log
          #   statements:
          #     - set(attributes["env"], attributes["deployment.environment"])

        ~省略~

        exporters:
        datadog:
          api:
            key: ${env:DD_API_KEY}
            fail_on_invalid_key: true
          host_metadata:
            enabled: true
          traces:
            span_name_as_resource_name: true
            compute_stats_by_span_kind: true
            peer_service_aggregation: true
          metrics:
            resource_attributes_as_tags: true
            histograms:
              mode: counters
              send_aggregation_metrics: true
          logs:
            dump_payloads: true

        extensions:
      health_check: ~
      memory_ballast: ~
      zpages: ~

        service:
      telemetry:
        metrics:
          address: 0.0.0.0:8888

      extensions:
        - health_check
        - memory_ballast
        - zpages

      pipelines:
        traces:
          receivers:
            - otlp
          processors:
            - resourcedetection
            - k8sattributes
            - attributes/trace
            - transform
            - batch
          exporters:
            - datadog

        metrics:
                ~省略~

        logs:
                ~省略~

変わった設定は特にしていないのですが、特筆すべきところは以下の3つです。

2-3-1. exportersにdatadogを設定

DatadogのAPI KeyをSecretで定義しておいて、ENVに渡して設定しています。

そして、.service.pipelines.traces.exportersにもdatadogを使うことを指定します。tracesだけでなくmetrics/logsでも同様に指定しています。

exporters:
  datadog:
    api:
      key: ${env:DD_API_KEY}
      fail_on_invalid_key: true

service:
  pipelines:
    traces:
      exporters:
        - datadog

2-3-2. processorsでDatadog用にAttributeを設定する

DatadogではSpanにresource.nameの属性を設定する必要があるのですが、OTelの仕様ではname という属性で扱われています。解決方法として、processorsでDatadogのspecに合わせて加工しています。

先ほどのCustom OTel Collectorの所でtransformprocessorというcomponentを導入しており、ConfigMapにてtransformの処理を書いています。

processors:
    transform:
      error_mode: ignore
      log_statements:
        - context: scope
          statements:
            - set(attributes["resource.name"], name)

2-3-3. Loggingは社内独自ライブラリを使用

GoのLogging SDKは公式でもまだIn developmentのステータスですが、Javaなどは既にSDKが提供されている関係でProtocol Buffersの定義は存在したので、それをベースに社内でzapのadaptorを実装し先行して使っています。

この記事の公開時点では公式にもGoのSDK実装が徐々に入りつつあり、APIが安定した段階で移行する予定です。

以上の設定で、OTel Collectorを立てています。

2-4. アプリケーションにてExport先を設定する

実際のアプリケーションコードは複雑なので割愛しますが、OpenTelemetryのデモ用のコードサンプルを例にすると以下のようなコードになります。

// Collectorのアドレスを設定
// http://opentelemetry-collector.opentelemetry-system.svc.cluster.local:4317
otelAgentAddr, ok := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
if !ok {
    otelAgentAddr = "0.0.0.0:4317"
}

// Metricsの設定
metricExp, err := otlpmetricgrpc.New(
    ctx,
    otlpmetricgrpc.WithInsecure(),
    otlpmetricgrpc.WithEndpoint(otelAgentAddr))
handleErr(err, "failed to create the collector metric exporter")

meterProvider := sdkmetric.NewMeterProvider(
    sdkmetric.WithResource(res),
    sdkmetric.WithReader(
        sdkmetric.NewPeriodicReader(
            metricExp,
            sdkmetric.WithInterval(2*time.Second),
        ),
    ),
)
otel.SetMeterProvider(meterProvider)

// Traceの設定
traceClient := otlptracegrpc.NewClient(
    otlptracegrpc.WithInsecure(),
    otlptracegrpc.WithEndpoint(otelAgentAddr),
    otlptracegrpc.WithDialOption(grpc.WithBlock()))
traceExp, err := otlptrace.New(ctx, traceClient)
handleErr(err, "failed to create the collector trace exporter")

bsp := sdktrace.NewBatchSpanProcessor(traceExp)
tracerProvider := sdktrace.NewTracerProvider(
    sdktrace.WithSampler(sdktrace.AlwaysSample()),
    sdktrace.WithResource(res),
    sdktrace.WithSpanProcessor(bsp),
)

// set global propagator to tracecontext (the default is no-op).
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
otel.SetTracerProvider(tracerProvider)

OTel Trace/MetricsのGo SDKにExport先としてOTel Collectorのアドレスを渡すだけで設定は完了です。

TraceやMetricsはサンプルコードのようにして、好きな箇所で計装することができます。

// Metrics
meter := otel.Meter("demo-server-meter")
serverAttribute := attribute.String("server-attribute", "foo")
commonLabels := []attribute.KeyValue{serverAttribute}
requestCount, _ := meter.Int64Counter(
    "demo_server/request_counts",
    metric.WithDescription("The number of requests received"),
)
requestCount.Add(ctx, 1, metric.WithAttributes(commonLabels...))

// Trace
span := trace.SpanFromContext(ctx)
bag := baggage.FromContext(ctx)
var baggageAttributes []attribute.KeyValue
baggageAttributes = append(baggageAttributes, serverAttribute)
for _, member := range bag.Members() {
    baggageAttributes = append(baggageAttributes, attribute.String("baggage key:"+member.Key(), member.Value()))
}
span.SetAttributes(baggageAttributes...)

3. (おまけ)opentelemetry-go-instrumentationを試す

OTelではAuto Instrumentation(自動計装)という、組み込むとテレメトリーデータを自動計装してくれるライブラリを出しています。自動計装は言語毎にメカニズムが異なり、各言語毎にライブラリが作られています。

Go言語はまだ開発段階ではありますが opentelemetry-go-instrumentation が出ており、今回は開発環境で試験的に導入してみたのでその詳細について書こうと思います。

※ただし、まだ開発段階なのでLinuxディストリビューションやGoのバージョンによっては動作しない場合があります。

3-1. OpenTelemetry Operatorの利用

OpenTelemetry Operatorは、k8sクラスター内でOpenTelemetryコンポーネントを管理するoperatorです。今回は opentelemetry-go-instrumentation をSidecarで立てるためにoperatorを使います。

まずはhelmを使いリソースの定義をします。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

metadata:
  name: opentelemetry-operator

namespace: opentelemetry-operator-system

helmCharts:
- name: opentelemetry-operator
  repo: https://open-telemetry.github.io/opentelemetry-helm-charts
  releaseName: opentelemetry-operator
  version: 0.x
  namespace: opentelemetry-operator-system
  valuesInline:
    pdb:
      create: true
    manager:
      featureGates: "+operator.autoinstrumentation.go"
    kubeRBACProxy:
      image:
        tag: v0.14.3
  includeCRDs: true

featureGatesとして"operator.autoinstrumentation.go" を有効にしておきます。

3-2. アプリケーション側の設定

アプリケーション側にはSidecarをinjectするアノテーションを付けます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  selector:
    matchLabels:
      app: my-app
  replicas: 1
  template:
    metadata:
      labels:
        app: my-app
      annotations:
        instrumentation.opentelemetry.io/inject-go: "true"
        instrumentation.opentelemetry.io/otel-go-auto-target-exe: "/path/to/container/executable"

また、OTel OperatorがアプリケーションにSidecarをInjectできるようInstrumentationのリソースを作ります。

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: post-service
  namespace: post-service
spec:
  exporter:
    endpoint: http://opentelemetry-collector.opentelemetry-system.svc.cluster.local:4317
  go:
    image: ${AUTO_INSTRUMENTATION_IMAGE_PATH}
    resourceRequirements:
      limits:
        cpu: 500m
        memory: 2Gi
      requests:
        cpu: 500m
        memory: 1Gi
    env:
      - name: OTEL_EXPORTER_OTLP_ENDPOINT
        value: http://opentelemetry-collector.opentelemetry-system.svc.cluster.local:4317

これによりGoにてAuto Instrumentが可能になりました。

4. 今後の方針

以上により本番環境でOTel Collector経由でDatadogにExportすることができました。ただ、まだまだ課題もあるので今後の方針としていくつか挙げたいと思います。

  1. 可用性: Collectorの構成をAgent modeGateway mode両方使って可用性高い構成にしたいと思っているのですが、費用面と現状のアプリケーションの規模を見て意図的に対応していません。将来的には変えていきたいと思っています。(OTel Collectorアンチパターンの記事にも記載されています)
  2. 機能面: 今の所、最低限のSpanしか登録していない & Attributeも上手く使いこなせていないので、目的に合わせた計装の最適化を行っていきたいと思っています。

5. まとめ

今回はGCPからDatadogへの移行、OpenTelemetry Collectorの導入、そしてopentelemetry-go-instrumentationのテスト及びOpenTelemetry Operatorの利用といった一連のステップを通じて、効率的かつ効果的なテレメトリーデータの収集と分析方法を探求しました。この記事を通して、これからOpenTelemetryを導入しようとしている人の参考になると嬉しいです。

またGaudiyとしては、前回の記事が出てから1年経ち、紆余曲折ありましたがOpenTelemetryを実運用に取り込むことができてObservabilityの向上にむけてかなり前進したのではないかと思っています。

始めた当初、DevメンバーがTrace/Metricsを見て能動的にボトルネックの改善やアプリケーション監視に役立ててくれると良いなと思っていたのですが、その兆候が社内でも見られ始めているので、今回の取り組みは本当にやって良かったと思っています。

ボトルネック調査をして下さったSlackの投稿

Observablityの向上に取り組んでいる人や、OpenTelemetryに関心がある人がいたら、ぜひカジュアルにお話ししましょう。

site.gaudiy.com

Gaudiyではエンジニアを積極採用中なので、興味ある人はぜひこちらもご覧ください。

site.gaudiy.com

GoとCobraを用いた新規マイクロサービス用ボイラープレートの自動生成CLIツールでコスト削減した話

こんにちは。ファンと共に時代を進める、Web3スタートアップ Gaudiy でソフトウェアエンジニアをしている ryio1010です。

私は弊社が提供するファンコミュニティプラットフォーム「Gaudiy Fanlink」の開発において、フィーチャーチームの一員として、主にバックエンド開発を担当しています。

バックエンドのアーキテクチャにはマイクロサービスを採用していますが、会社のフェーズ的に試行錯誤の段階であることや、それに伴うチーム体制の変更がよく起きていることもあり、新しいマイクロサービスの立ち上げも頻繁に行われています。

私自身もこれまでの業務で2〜3つの新しいマイクロサービスを立ち上げる経験をしてきました。

今回は、これらのマイクロサービスの立ち上げと運用の経験から、特に立ち上げフェーズにフォーカスし、改善を行った事例をご紹介したいと思います。

1. マイクロサービスの概要とGaudiyのシステム構成について

本記事をお読みの皆さんにとって、マイクロサービスの概念はすでにご存知の内容かもしれません。しかし、後続の内容をより深く理解していただくために、まずはマイクロサービスの基本的な概念と、Gaudiyにおけるシステム構成の現状を簡潔にご説明します。

1-1. マイクロサービスとは?

マイクロサービスは、大規模なアプリケーションを小規模かつ独立したサービス群として構築する設計手法です。このアーキテクチャスタイルでは、各マイクロサービスが特定の機能やビジネス要件に特化し、独立して開発、デプロイ、運用されます。

例えば、Gaudiyでは、ユーザー関連の処理を担う「user-service」、投稿関連の処理を担う「post-service」などが存在します。

マイクロサービスはそれぞれ独立しているため、Go、Kotlin、TypeScriptなど、異なるプログラミング言語での開発も可能です。

実際にGaudiyでは、これらの言語を使用したマイクロサービスを運用しています。

主なメリットは以下の通りです:

  • 開発とデプロイメントの迅速化: 各マイクロサービスは独立して開発・デプロイできるため、迅速な更新が可能。
  • 耐障害性の向上: サービス間が疎結合になるので一つのサービスに障害が発生しても、他のサービスには影響が少ない。
  • 複数技術スタックの利用: 異なるサービスで異なる技術スタックを採用できる。

一方でデメリットも存在するため、考慮に入れておく必要があります。

  • 通信の複雑さ: サービス間の通信が多く、複雑になる傾向がある。
  • データ管理の難しさ: 分散したサービス間でのデータ一貫性の維持が課題。
  • 運用コストの増加: 個別サービスの監視・管理には追加の労力とリソースが必要。

多くのメリットがあるマイクロサービスですが、上記のようなデメリットもあります。 この記事では、マイクロサービスの運用におけるコスト増加の部分、特に新規マイクロサービスの立ち上げコスト削減に焦点を当てています。

1-2. Gaudiyのシステム構成

現在のシステム構成は下記のようになっています。

Fanlinkシステム構成

フロントエンドではReact(Next.js)とTypeScriptを用い、BFFとしてApollo ServerとTypeScriptを採用しています。バックエンドではGoとKotlin・Pythonを使用したマイクロサービス群を構築しています。 これらの技術スタックを基に、ブロックチェーン技術やAI・LLM(Large Language Models)技術を取り入れながら、当社のプロダクト「Fanlink」の開発を進めています。

バックエンドに焦点を当てると、ホスティング環境としてGoogle CloudのGKE(Google Kubernetes Engine)を使用しています。現在、20近くのマイクロサービスをGKE上で運用しています。

2. 既存の新規構築手順と課題感

2-1. これまでのマイクロサービス構築手順

まず、弊社で新規マイクロサービスを立ち上げるために必要な要素と、従来の構築方法についてご説明します。

新規マイクロサービスの立ち上げには、大きく分けて以下の3種類が必要です。

  • マイクロサービス用のgRPCサーバーのコードベース(Go)
  • GCP環境でホストするためのGCPリソースを作成するTerraformファイル群
  • 各環境へのデプロイなどを担うCI/CDやDBマイグレーションを行うGitHub Actions用のyamlファイル群
2-1-1. gRPCサーバー用のコードベース(go)

まずマイクロサービスのサーバー本体となるコードベースについては、一部はKotlinを採用していますが、新規サービスは基本的にGoを使用しています。

Gaudiyにはecho-serviceという、いわゆる新規マイクロサービスの元になるコードベースがあります。

このテンプレートには、ログクライアントの設定、DBクライアントの設定、サーバーの起動など、マイクロサービスに必要な基本機能がGoで書かれています。

これまではこのecho-serviceをコピーする形で新しいマイクロサービスのコードベースを作成していました。コピーしていく中で、コードベースの中の名前を新しく作成するマイクロサービスのドメインに合わせるように手作業で修正していくという形でした。

手作業で修正する必要があり、その量もそれなりになるため、細心の注意を払っていたとしてもヒューマンエラーが発生したり、そもそもecho-serviceからの新規サービス作成が初めてな人も多いので必要以上に時間がかかってしまうことも少なくない状況でした。

2-1-2. terraformファイル

GCP環境でマイクロサービスを動作させるためには、Service AccountやSpanner InstanceなどのGCPリソースが必要です。

これらのインフラ管理はTerraformを用いて行っており、既存のファイルに新規マイクロサービスのリソースを追加する必要があります。

この作業に関しては手順書があるため、他の作業に比べると比較的スムーズに作業できていました。

2-1-3. cicd用yamlファイル

各マイクロサービスには下記のためのgithub actions用のyamlファイルがあります。

  • 各環境へdeploy用
  • SpannerDBのマイグレーション用
  • 各PRの確認用(ユニットテスト実行・lint実行など)

これらに関しては手順書もなく、これまで新規構築したことのあるメンバーに逐一どのファイルが必要かを確認して手動で作成している状況でした。

ファイルの追加漏れなどヒューマンエラーも発生しやすい状況となっており、実際私もDBマイグレーション用のファイル追加がもれており、DBのマイグレーションが実行されず追加し直すといった手戻りが発生してしまった経験がありました。

2-2. 課題感

マイクロサービスは上記でお伝えした利点があり採用しているのですが、会社のフェーズ的にも試行錯誤をしていく中で、実績値で平均すると2-3ヶ月に1回くらいのペースで新しいマイクロサービスを立ち上げていました。

私は新規マイクロサービスの構築に多く関わる機会があり、初めて構築を行う中で以下の課題があると個人的に感じていました。

  • 初期構築手順書はあるものの、十分に更新されておらず有識者に確認する必要があり開発の手が止まってしまう
  • 手順書には書かれていない暗黙知があり、チームメンバーによっては暗黙知を持っておらず新規構築したもののなぜがうまく動かないといったことが起こりうる
  • 基本的に手作業でテンプレートとなるディレクトリをコピーする形でコードベースを作るのでタイポなどのヒューマンエラーが発生し、作業が手戻りしてしまう
  • そもそも手順が多く手作業が大変

上記の状況の中で、実際に新規マイクロサービス基盤の構築が完了しコア機能の開発に入るまでに1-2人日くらい工数がかかっているため、少なからず開発スピードを鈍化させる原因になっていることは明らかでした。

また基本的にどんなマイクロサービスを立ち上げるとしても、そのマイクロサービスが扱うドメインの名称が違うだけで新規立ち上げの作業自体は定型的なものであるため、十分に自動化できうるものでした。

3. 具体的なアプローチ方法と効果

3-1. アプローチ方法

先にご紹介した新規マイクロサービス構築に必要な3種類の作業のうち、Terraformに関しては手順書通りの追記で問題なく、大きく工数を要していなかったため、GoサーバーのコードベースとGitHub Actions用のyaml群の生成を自動化の対象としました。

詳しい実装については割愛しますが、おおまかに下記のような処理を実装している「gauctl」という名前のCLIを作成しています。

  1. GitHubのリポジトリからecho-serviceをクローン(tarファイル)
  2. 取得したtarを解凍
  3. 変更が必要な箇所を新規マイクロサービスのドメイン名で置換
  4. GitHub Actions用のyamlのテンプレートを基に、新規マイクロサービス用のyamlを生成(Go言語のテンプレート機能を利用)
  5. 適切なディレクトリに生成ファイルを出力

3-2. 利用した技術について

利用した技術スタックは以下の通りです。

  • Go言語
  • Cobra(GoのCLI アプリケーションフレームワーク)

Go言語はCLIツールの作成に適しているだけでなく、Gaudiyではバックエンドに広く採用されており、多くのエンジニアが読み書きできるため、将来的なメンテナンスの観点からも採用することにしました。

またCobraはGo製のCLIフレームワークで、容易にコマンド生成やフラグ処理などを直感的なAPIで実装できるため、開発の効率やメンテナンスが容易になるという点から採用しました。

ここではより具体的な実装イメージを持っていただくためにも、yamlの生成で利用したGo言語のテンプレート機能とCLIアプリケーションフレームワークとして利用したCobraについて簡単な使い方をご紹介できればと思います。

3-2-1. Go言語のテンプレート

今回yamlの生成にはGo言語のテンプレート機能を利用しました。

この機能を使うことで動的なデータ(今回の場合は新規マイクロサービスのドメイン名)を静的テキストファイルに組み込むことができ、”text/template”パッケージから利用できます。

ディレクトリ構成は下記の感じです。(一部抜粋)

├── transform
│   └── templates
│       └── sample1.yaml
│       └── sample2.yaml
│   └── embed.go // yamlファイルをbyte配列で読み込む処理を記載
│   └── transform.go // yamlの中身を変換する処理

yamlファイルの中身で変換したい部分には「{{ . }}」と記載します。

(この部分がtemplateの機能によって変換されます。)

// transform/templates/sample1.yaml
name: Check PR for {{ . }}-service

on:
  push:
    branches:
      - "main"
    paths:
      - "{{ . }}-service/**"
  pull_request:
    paths:
      - "{{ . }}-service/**"

まずembed.goで”embed”パッケージを利用して”templates内のファイルをbyte配列で読み込んでいます。そしてそれをTransform関数の引数として渡しています。

// transform/transform.go

// contentにyamlファイルのbyte配列を、replacedStrには変換したいドメイン名が入る
func Transform(content []byte, replacedStr string) ([]byte, error) {
  // New関数でテンプレートを作成して、Parse関数で文字列を解析し、テンプレート定義に追加する
	t, err := template.New("yaml").Parse(string(content))
	if err != nil {
		return nil, fmt.Errorf("failed to parse: %w", err)
	}

	var buf bytes.Buffer
    // Execute関数で、テンプレートの内容を変更し、bufに格納する
	if err = t.Execute(&buf, replacedStr); err != nil {
		return nil, fmt.Errorf("failed to execute: %w", err)
	}

	return buf.Bytes(), nil
}

上記のTransformが実行されると、sample1.yamlは下記のようになり、それを指定したディレクトリに出力しています。

(replacedStrにfooを指定した場合)

// transform/templates/sample1.yaml
name: Check PR for foo-service

on:
  push:
    branches:
      - "main"
    paths:
      - "foo-service/**"
  pull_request:
    paths:
      - "foo-service/**"

このようにtemplate機能を使うことで動的なコンテンツを簡単に作成することができます。

3-2-2. Cobra

CLIのディレクトリ構成は下記の感じです。(一部抜粋)

(cobraのコマンドをインストールして自動生成もできます。)

$ go install github.com/spf13/cobra-cli@latest
$ cobra-cli init
├── cmd
│   └── create.go
│   └── root.go
├── main.go

サブコマンドもコマンドでファイル生成できます。

(↓のコマンドを実行すると、cmd下にcreate.goが生成されます。)

$ cobra-cli add create

Cobraでのサブコマンドの実装はCommandというStructのフィールドに値を定義していく形で作成します。

基本的なフィールドの説明は下記になります。

  • Use: コマンド名とその使用方法(引数など)を定義
  • Short**:** コマンドの短い説明
  • Args**:** コマンドに渡された引数を検証する関数を定義
  • RunE: エラーを返却できる、コマンドが実行された際に呼び出される関数の定義

上記以外にも色々と設定できる項目はあるものの、基本的にRunEにサブコマンドが実行された時に実行したい処理を書くことでCLIを実装することができるようになっています。

// cmd/create.go
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var (
	createExample = `
* Create new micro-service from template
	`

	createCmd = &cobra.Command{
		Use:     "create [domain_name arg]",
		Short:   "Create new micro-service from template",
		Example: createExample,
		Args: func(cmd *cobra.Command, args []string) error {
			// 引数に関する処理(引数の検証など)をここに記述できる
			if len(args) != 1 {
				return fmt.Errorf("requires 1 arg(s), received %d", len(args))
			}
			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			// 以下にコマンドが実行された時に実行したい具体的な処理を記述

			return nil
		},
	}
)

func init() {
	rootCmd.AddCommand(createCmd)
}

CobraはKubernetesやDockerなどでも採用されているとのことで筆者も初めて使用してみましたが、直感的で分かりやすく、素早く軽量のCLIを作成するツールとして優れていると感じました。

3-3. 実際の効果

CLIをリリースしてから、既に4つの新規マイクロサービスが立ち上がっており、全てこのCLIを使用いただいています。

以前は1-2日かかっていた環境構築作業が、定型作業を自動化することができたため、現在ではおよそ半日で完了するようになりました。

手順は以下のように変わりました。 特に時間を要していたecho-serviceのコピーとドメイン名の置換作業、およびGitHub Actions用のyamlファイルの作成作業が、コマンド一つで完了できるようになりました。

3-3-1. これまで
  1. echo-serviceをコピー
  2. echo-serviceのドメイン部分を新規マイクロサービスの名称に置換し、Goサーバーのコードベースを作成(ここに多くの工数がかかっていた)
  3. Terraformファイルに必要なリソースを追加
  4. GitHub Actions用のyamlファイル群を追加(ここも手順書がなく、工数がかかっていた)
  5. PRを出す
  6. レビューし、mainブランチへマージ
  7. CI/CDにより新規マイクロサービスの環境が構築される
3-3-2. CLI導入後
  1. CLIを実行(マイクロサービスのコードベースとGitHub Actions用のファイル群を生成)
    • 実際のコマンド例: foo-serviceを作りたい場合
      • gauctl create foo
  2. Terraformファイルに必要なリソースを追加
  3. PRを出す
  4. レビュー、mainブランチへマージ
  5. CI/CDにより新規マイクロサービスの環境が構築される

主にヒューマンエラーや経験不足(暗黙知だった部分)が原因で発生していた手戻りや、定型的な作業を自動化することで、工数を大幅に削減できたのかなと思います。

4. おまけ(CloudRun→Kubernetes移行について)

少しおまけ的な話にはなりますが、当時、マイクロサービスのホスティングはCloud Runを利用していました。そのため、初回リリース時にはCloud Runで動作するコードベースを自動生成するCLIを作成していました。

その後、全社的にCloud RunからKubernetesへのホスティング基盤の移行が行われました。この移行は、横断的なチームであるenableチームによって進められました。 それに伴い、CLIもKubernetesで使用できるように機能拡張が必要となりました。

詳しい話は別の機会に書かせていただければと思いますが、Cloud Runとは違いKubernetesで動作させるためには、deployment.yamlやservice.yamlなどの設定ファイルが必要になります。 したがってKubernetesに対応するために、これらの設定ファイルを生成するコマンドを追加し、必要なファイルの自動生成が可能になりました。

Kubernetesへの移行経緯や移行方法の詳細は、以下の記事で詳しく書かれています。興味のある方はぜひご覧いただけるとうれしいです! 

techblog.gaudiy.com

5. おわりに

この記事では、マイクロサービスを運用する中でも特に新規立ち上げフェーズにフォーカスし、改善を行った事例についてお伝えさせていただきました。

改善を行う際のコストと効果を比較して改善するべきかを熟考することは重要ですが、定型的な作業の自動化は、ヒューマンエラーによる手戻りの防止や開発スピードの改善に寄与できると思うので、積極的に改善して良い部分になるかなと思います。

また、定型的な作業は自分の所属するチーム内だけではなく、他のチームも同様の課題を抱えがちです。そのため、開発組織全体の開発スピード向上に寄与しやすい部分なのではないかと個人的には感じています。

Gaudiyでは、課題に直面した際、本人が主体的に積極的に解決に取り組む文化があります。これは一人で行うのではなく、周囲を巻き込みながら(アドバイスをもらったり、改善方針のすり合わせなどで相談に乗ってもらえる)課題解決を進めていけるので気軽に改善活動に取り組めると感じています。

今後もフィーチャーチームに在籍しながら、横断的に解決できる課題を見つけ、開発スピードの向上に寄与していければと思います〜!

今回の記事が皆さんの参考になれば幸いです。

Gaudiyでは、一緒に働くエンジニアを積極的に募集しています。当社の技術や開発スタイルに興味を持った方は、選考前のカジュアル面談も可能ですので、ぜひ採用ページからお気軽にご応募ください!

site.gaudiy.com

Kubernetes初学者が担当したGKE移行プロセスの全貌

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

クラウドネイティブ全盛の世の波に乗り、この度 Gaudiy では Cloud Run から Google Kubernetes Engine (GKE) への移行を行いました。

この記事では、その移行プロセスの全体像を共有し、得られた教訓と今後の展望を探ってみたいと思います。

1. Before After: 移行の概観

1-1. Before

Before

以前までの構成は、

  • マイクロサービスの実行環境として Cloud Run を使用
  • 監視ツールにはGCPの Cloud Monitoring, Cloud Logging, Cloud Trace を使用
  • Pub/Sub→マイクロサービス間に API Gateway を経由
  • アプリケーションのデプロイは Github Actions が行う

といった感じでした。

1-2. After

現在の構成は、

  • マイクロサービスの実行環境としてGKEを採用。1つの Cluster 上で全てのサービスを動かしています。
  • 監視ツールにはDatadogを採用し、OpenTelemetry Collector経由でテレメトリーデータを流しています。
  • Pub/Sub→マイクロサービス間にESPv2を経由。
  • アプリケーションのデプロイはArgo CDが行い、Github ActionsArtifact Registryに push した Image を Argo CD Image Updater が Watch し、Cluster にデプロイしています。

といった感じに変わりました。

2. なぜGKE環境に移行したのか

移行に至った経緯は様々あるのですが、主に↓の3つが理由です。

  • クラウドネイティブな技術選択をしたい
    • CNCF Projectsをはじめとする、コンテナ技術の最新トレンドを採択したい
    • Kubernetes Controller で自由に機能拡張できる
  • Ops 起因で重要な目的があった
    • これが大きな理由だったのですが、機密情報に当たるので申し訳ないですが詳細は伏せさせていただきます
  • 機械学習関連でマシンリソースを選びたかった
    • Gaudiyではレコメンドエンジンなど、AIにも注力しており、通常サーバーとは異なるマシンリソースが必要になった

3. 移行のプロセス

普通なら移行計画を立ててKubernetes(k8s)に移行していくところですが、僕はk8s未経験であり前提知識も全くなかったので、k8sを学ぶことから始めていきました。

3-1. Kubernetesを学ぶ (1週間: 2023/10/01~)

Kubernetes完全ガイド 第2版を読んでサンプルアプリを作ってました。これが無かったら移行完了までのスケジュールは大きく変わっていたと思うくらい良書でした。

この時に、DeploymentやServiceなどのk8sオブジェクトを理解するとともに、ライフサイクルやエコシステムなども学んでいきました。

3-2. Dev on GKE環境作成 (2-3週間)

FanlinkのアーキテクチャはFE → Load Balancer → BFF → BE(micro services)という構成になっており、今回は Load Balancer 以降を GKE に全く同じ環境を作成して、FEの向き先を変えて移行することにしました。

GKE環境を作る際に様々な技術を使用しましたが、1つずつ書いていくと書ききれないので、以下にやったことを抜粋して記載します。詳細は割愛しますが、後に別記事としていくつか公開する予定です。

  • Cluster の作成
  • 各マイクロサービスのKubernetesオブジェクト定義
    • サービス: 27個、cronjob: 8個ほどありました。
  • External Secrets の使用
    • Secret Managerを参照するため使用。
  • OpenTelemetry Collector の使用
    • テレメトリーデータをOpenTelemetry Collectorに集約させて、まとめて Datadog にexportしています。
  • Gateway API の使用
    • つい先日GAとなったKubernetes Gateway API を使って、Load Balancerを管理しています。
  • ESPv2 の使用
    • REST から gRPC へのトランスコーディングとして、Envoy ベースの ESPv2 を使用しています。
  • Argo CD の導入
    • k8s リソース管理及びCDとして、Argo CD を採択しました。
  • etc…

3-3. Staging on GKE環境作成 (2日)

基本的には環境変数の変更で、Devで作成したyamlをStagingも同じように作っていくだけなのでそこまで難しくはありませんでした。

この時、開発チームへGKE環境に触ってもらう・受け入れテストのお願いをアナウンスしました。

3-4. Private Clusterへの移行 (1-2週間)

今まではデフォルトの設定で Cluster を作成していましたが、Private Cluster にすることでコントロールプレーンとノードに対して外部アドレスを割り当てなくする(≒よりセキュアにする)ことができると知り、Stating/Prod 環境は Private Cluster で建てることにしました。

  • GCPリソースのTerraform
    • GKE Cluster
    • Network / Subnetwork
    • Firewall
    • etc…
  • Tailscale Operator 導入
    • Private Cluster の操作を Tailscale Network 経由で行うようにしました。
  • Private Clusterでの環境再作成
    • コントロールプレーンのパブリックエンドポイントアクセスを無効にして、よりセキュアな Clusterへと変更しました。

3-5. Prod on GKE環境作成 (2日)

こちらもStaging同様にyamlを複製し、Prod用の環境変数を割り当てる程度なのでそこまで難しくはありませんでした。

ただし、cron処理やoutbox patternが存在したので現行の環境に影響を与えないように気をつけながら作業を行っていきました。

3-6. 負荷試験・受け入れテスト (1週間)

Staging環境を作成した段階で、開発チームへのアナウンスや負荷試験をProd環境作成と並行して行っていきました。

最後の大詰めとして以下のような作業をコツコツと進めていきました。

  • バグの解消
    • 環境変数や外部サービスとの通信部分で特にバグが起きました…
  • Pod/Nodeリソース・スケール値の設定
    • Grafana k6で負荷試験をして、想定負荷に必要なリソースを計算しました。幸い、瞬間的に同時アクセスが大量に走るような複雑なサービスではないので、ある程度の負荷を余裕もって耐えれる位の設定値にしました。
    • ただ、それでも単一Podだと落ちた時が怖かったので複数Podをminimumとし、負荷試験に耐えられる位をmaxとして設定しました。

3-7. リリース(2023/12/04)

2023/12/04の深夜にCloud Run環境からGKE環境にSwitchしました。移行後から1週間くらいは異常が起きないかモニタリングを続けましたが、特段大きな問題はなくアプリケーションが稼働していたのでホッとしました。

4. 移行後の感想

4-1. 良かった点

  • おおよそ2ヶ月でk8s環境へ移行できた。
    • 経験者の知見もいただきながらですが、初学者ながらわりと順調に進めることができたと思います。
  • 最新のクラウドネイティブ技術への追従ができる。
    • CNCF Projectsに登録されているプロジェクトを見て活かせそうなツールがないかみるのが最近楽しいです。
  • リソースの調整
    • 機械学習系のマシンが使えるの嬉しい。
  • レスポンスタイム向上
    • 全体約200msほど
  • Datadogに移行したこと
    • 高機能でかなり使いやすいです。今後のSRE業務に拍車がかかりそうです。

4-2. 課題点

  • k8sの学習コストが高い
    • k8sへの完全移行は達成したものの、k8s APIやらeBPFなど学ぶことがいっぱい。まだまだこれからだなと感じています。
  • 運用コストも高い
    • 学習コストに近いですが、定期的なk8sのアップデートやCSP(クラウドサービスプロバイダ)の知識がないとアプリケーションを落としかねないので、運用は簡単とは言えなさそうです。
    • また、適切なリソース設定をしないと費用面でも無駄なコストがかかってしまいます。

5. 今後の展望

現在は最小構成のk8s環境で動いているので、様々なクラウドネイティブ技術を積極的に試していきたいと考えています。

  • Ciliumの導入
  • eBPFの調査
  • Flux (GitOps)
  • Kubernetes Custom Controller 作成
  • Config Connector

6. 結論

Kubernetes移行はかなり大変でしたが、Gaudiyのビジョンに向けたソリューションと技術的な野心を反映した、意義深い変革の一歩だったと思います。

クラウドネイティブ技術の進化に伴い、より良いサービスとソリューションを提供するために、今後もアップデートを続けていきたいと考えています。

この領域に知見のある方や、Gaudiyでのプロダクト開発に興味のある方がいたらぜひお話ししましょう!

site.gaudiy.com

site.gaudiy.com

Authlete を活用して OAuth 認可サーバの構築期間を短縮した

こんにちは、Gaudiyでソフトウェアエンジニアを担当しているsato(@yusukesatoo06)です。

弊社が提供するファンコミュニティプラットフォーム「Gaudiy Fanlink」において、外部サービスにAPI提供をする必要があったことから、外部連携について色々と調べて実装しました。

そこで今回は、調査からサーバ構築までのプロセスと、そこで得た学びや気づきを共有できればと思います。

1. OAuthとは

1-1. OAuthの概要

OAuthとは、Webやモバイルアプリケーションなどのクライアントアプリが、ユーザーに代わって他のウェブアプリがホストしているリソースにアクセスできるよう設計されたプロトコルです。(みなさんご存知かもしれないですが...)
参考) OAuth 2.0 とは何か、どのように役立つのか? - Auth0

OAuthを使用することで、ユーザーはクライアントアプリに対して、自身のID/パスワードを直接提供せず、他サービスとの連携を可能にします。そのため、複数のサービス間で連携を行うために非常に重要な技術となっています。

※OAuthが解決する課題については、LayerXさんの記事が参考になります。

1-2. OAuthのフロー

OAuthには認可フローがいくつか存在し、大きく以下の4つで構成されます。

  • 認可コード
  • インプリシット
  • リソースオーナー・パスワード・クレデンシャルズ
  • クライアント・クレデンシャルズ

それぞれのフローには、異なる用途やセキュリティ要件があるため、適切なフローを選択する必要があります。ここに関しては、以下の記事が詳しいです。

kb.authlete.com

僕たちは、上記記事でも推奨されている認可コード + PKCE(※)パターンを採用しました。

※ 認可コード横取り攻撃に対する対策
参考) Proof Key for Code Exchange (RFC 7636) - Authlete

2. OAuthが必要な背景

2-1. 外部サービス連携

今回、OAuthが必要になった背景として、外部サービスからGaudiyのAPIをコールしたいという要求がありました。また実装に関しても、1, 2ヶ月程度で行う必要があり、かなりタイトなスケジュール前提でのAPI連携となりました。

(※ ただしビジネス的な要件でAPI連携が後ろ倒しになったため、まだ実運用には至っていません)

API連携を行う手段はいくつかあります。

  1. APIキーでの連携
  2. 独自方式での連携
  3. OAuthでの連携 など

今回は工期の関係や、後述する少し特殊な環境下での連携だったため、最初は1か2を選択しようと考えていました。

しかし最終的には、3のOAuth形式を採用することとなりました。その主な理由は以下です。

  • クライアントサイドの実装も独自となり、エコシステムを利用できない
  • 今後、別のサービスと連携する際の拡張性が乏しい

2-2. 他の連携方式との比較

1. APIキーでの連携

まず最初に検討にあがったのがAPIキーでの連携方式でした。

こちらはIP制限等と併用することで、一定のセキュリティを担保することができるかつ工数のかからない方式となります。

ただし今回、Fanlink APIをコールするクライアントが、サーバサイドだけではなくクライアントアプリも含まれる前提でした。そのため、早々に本手段は選択できないことが決定しました。

2. 独自方式での連携

前述した通り、今回の連携環境が少し特殊な関係で、独自方式の連携を検討していました。

具体的に説明すると、Fanlinkと連携サービスが同一のIdPとOIDC連携をしており、同じIdPのアカウントで作られたサービス間を連携する環境でした。

そのため、IdPで発行されたID Tokenを流用することでサービス間の連携ができないかを検討していました。

一般的な方式ではないですが、工期を考えるとOAuthをゼロから作ることが難しいため、本方式を検討しました。またセキュリティの監査会社に相談した結果、セキュリティホールとなる部分もありませんでした。ただし前述の通り、クライアントサイドも独自実装となるため、本手段も選択しませんでした。

そのため、僕たちは腹を括って短期間でOAuthの仕組みを構築することにしました。

3. OAuthの提供

3-1. 提供方式

短期間でOAuthサーバを提供する必要があるため、まずはどういった方式で提供できるかを検討しました。

その中で提供方式は大きく3つ出てきました。

  • フルマネージド
    • サービスプロバイダー側で提供されるOAuthサービスを利用する方式
    • 例) Auth0
  • ハイブリッド
    • サービスプロバイダー側で提供されるAPI群を利用し、自社で認可サーバを構築し提供する方式
    • 例) Authlete
  • フルスクラッチ
    • ライブラリ等を用いて、自社で認可サーバを0から構築し提供する
    • 例) fosite, hydra など

それぞれ以下のPros/Consがありました。 (Gaudiyの開発環境に依存する理由も含まれます)

サービス Pros Cons
フルマネージド 導入までの期間が圧倒的に短い。フルマネージドでの提供のため運用工数がかからない。Auth0等であればグローバルでの実績があり、セキュリティ面の担保もされている Firebase Authをすでに導入しており、IdP機能が既存サービスと競合してしまう。
ハイブリッド OAuth/OIDCに特化したサービスで、既存のIdPと競合しない。フルスクラッチに比べ運用工数がかからない。コアな部分はAPIとして提供されており、セキュリティ監査等をクリアした機能を利用できる。 フルマネージドに比べ、一定の開発/運用工数がかかってしまう。
フルスクラッチ フルマネージド / ハイブリッドと比較し、ランニングコストがサーバ費用のみ。 開発/運用工数がかなりかかる。セキュリティ監査など周辺工数/コストもかなりかかる。実装に問題があった際に、会社として大きなリスクを抱える。

3-2. 今回の選定方式

今回は前述した3つの方式のうちハイブリッド型を選択し、サービスとしてはAuthleteを採用しました。

すでに導入しているFirebase Authと競合しない点、フルスクラッチに比べると開発工期を大きく圧縮できる点が理由となります。

また開発者向けのドキュメントが充実していることや、Goで利用するライブラリも用意されていた点も決め手となりました。

4. OAuthサーバの構築

4-1. Authleteについて

では実際に、OAuthサーバを構築した際の内容に移ろうと思います。

本来であればここで苦労話を記載したいのですが、Authleteを活用したこともあり、1 ~ 2ヶ月の工期に対して意外とすんなり構築できました笑

今回採用したAuthleteは、RFCに準拠するエンドポイントをAPIとして提供していて、利用者はクライアントからのリクエストをバイパスするのみでOAuthサーバを提供できるサービスとなっています。 (詳しくはサービス概要をご覧ください)

https://www.authlete.com/ja/developers/overview/

そのため、まずは今回必要なエンドポイント群を整理するところから始めました。

4-2. 必要なエンドポイント

今回のOAuthサーバの提供にあたり、外部向けには大きく以下のエンドポイントを用意しました。

  • 認可エンドポイント
  • トークンエンドポイント
  • リソースサーバエンドポイント

それぞれ簡単に説明していきます。

認可エンドポイント

認可エンドポイントは、連携するサービスの認証/認可処理を開始するためのエンドポイントです。一般的には連携サービスの認証画面でログイン処理を行い、OAuthの処理を開始した後に、指定したリダイレクトURLが認可コードを付与された状態で戻ってきます。

仕様についてはRFC6749 3.1に仕様が記述されており、わりと柔軟に実装することができます。

例えばパスについては、フラグメントを含めなければどのような値でも利用することができます。

詳しい仕様はAuthleteさんの記事が分かりやすいです。

トークンエンドポイント

トークンエンドポイントでは、認可エンドポイントで取得した認可コードを用いてリクエストに利用するトークンを発行します。

またリフレッシュトークンを利用してリクエストすることで、トークンのリフレッシュも行うことが可能です。

仕様についてはRFC6749 3.2に記述されています。

リソースサーバエンドポイント

リソースサーバエンドポイントは、実際に認可されたリソースにアクセスするためのエンドポイントで、提供機能に応じて用意するエンドポイントとなります。

トークンエンドポイントで発行したトークンを用いてアクセスを行い、スコープに準拠したリソースであればOAuth連携したサービスから情報取得することができます。

上記以外にも、内部で利用するためのエンドポイントをいくつか用意しておりますが、今回は割愛させていただきます。

4-3. システム構成

今回、OAuthサーバを構築した際のシステム構成は以下です。

OAuthはHTTP/1.1をベースにしたプロトコルである一方で、GaudiyのバックエンドエコシステムがgRPCベースの構成となっているため、以下の形で実現しました。

構成図の通りではありますが、ポイントは以下です。

  • API Gatewayを構築し、OAuthに関する外部通信は基本API Gatewayでハンドリングすることで、プロトコル準拠の通信を実現
  • 後続サービスとの通信に関してはgRPCベースで通信を行い、Gaudiyのエコシステムを流用する
  • Authleteとの通信は基本的にパススルー方式とし、クライアントからのリクエストをほぼ加工せず送信することで、レスポンスも可能な範囲でそのままクライアント側に返却する

また外部公開するURL仕様やバージョニングも悩む部分が多かったです。

大手サービスが提供しているエンドポイントを調査すると以下のような仕様でした。

  1. Google

     https://accounts.google.com/o/oauth2/vX/auth
    
  2. Facebook

     https://www.facebook.com/vXX.X/dialog/oauth
    
  3. GitHub

     https://github.com/login/oauth/authorize
    
  4. Twitter

     https://api.twitter.com/oauth/authorize
    

上記のうちGoogleの形式を参考に、僕たちは以下のような設計にしました。

https://<domain>/oauth/vX/<method or resource>
例) https://<domain>/oauth/v1/authorize

API仕様が変わるケースは少ないと思いますが、外部連携を行う前提に立つとクライアント側との調整/修正が必要なため、後方互換性を担保しつつ切り替え可能にしました。

5. 開発を通じて

5-1. 開発を通じた学び/気づき

今回、OAuthサーバというRFCに厳密に準拠すべきサーバを開発して、色々と学び/気付きがありました。

まずRFCの内容が意外と解釈の余地があり、何かしらの指針がないと解釈に差が生まれる恐れがあるなと感じました。そのためAuthleteという、開発するための指針があったのは非常に助かりました。その一方で、RFCに準拠した自前の認可サーバを作る選択肢を取っていたら、途方に暮れる工数がかかっていただろうなと思います。

またこのようなプロトコルを策定した過去の技術者達のすごさを改めて感じました。Webサービスを開発しているとRFCを読む機会は少ないですが、一度RFCに準拠するサービスを作ることは非常に学びが多いなと感じました。

個人/チームとしては、GitHubのやり取りの中でRFCについて触れるなど、開発者としての成長を感じた場面もありました笑

5-2. フルスクラッチとの比較

フルスクラッチで開発した場合を想定し比較すると、半分以下の工数で実装できたのではないかと思います。工数を圧縮できた大きなポイントは以下です。

  • 基本的にAuthleteの設定後、Authlete提供のAPIをコールするだけでよいため実装方針が明確
  • RFCに準拠するために必要となる知識が圧倒的に少ない(とはいえ開発者としては理解しておくべきですが)
  • 自分たちの開発範囲が少ないため、セキュリティ監査などの対象/工数も削減できる
  • シンプルなものであればすぐに構築出来るため、動くものを確認しながら試行錯誤ができる

5-3. 今後の対応

これまでAuthleteを活用したOAuthサーバの構築に触れてきましたが、冒頭でも説明した通り、残念ながら実運用にはまだ至っていません。

本来であればOAuthスコープの設計をAPI毎に行う必要があり、スコープ設計もかなり重たい内容となる想定です。ただし今回は実運用を行うエンドポイントがなくなってしまったため、スコープ設計は後ろ倒しとなりました。

OAuthサーバの実運用を行った際は、再度スコープ周りの知見を共有できればと思います。

6. まとめ

今回はOAuthサーバの実装について、その採用背景から構築方法まで紹介させていただきました。調査した内容や実際に感じたこと含めてまとめてみたので、これからOAuthサーバの実装を検討している人の参考になれば嬉しいです。

また今後の実運用に向けて、OAuthのスコープ設計周りに知見がある人がいたらぜひお話ししてみたいです。

Gaudiyに興味を持っていただいた方は、ぜひ以下のサイトも覗いてみてください!

site.gaudiy.com

不確実性や心理的安全性に向き合い自己組織化するチームを作る実践プラクティス

こんにちは。Gaudiyでソフトウェアエンジニア兼スクラムマスターをしている Namiki ( @ruwatana ) です。

チームが向き合う不確実性が大きいと手戻りが増えて価値提供のリードタイムが遅くなる
チーム内の心理的安全性の低さや認知負荷の高さによってエンゲージメントが低下して従業員がオンボード・定着しにくい
...

などなど、昨今のチーム開発はこうした課題で溢れかえっていることかと思います。

結局のところ、我々は具体的にどんなプラクティスを行うことで、こうした課題を解決できていくのでしょうか?

本稿では、筆者と筆者が4ヶ月ほど前に配属することになったチームがこうした問題に対して執ったアプローチおよびその効果をより具体的に示すことができればと考えています。

プロダクトチーム開発を行う皆様に何かしらの参考になれば幸いです。

1. チーム構成と特性

Gaudiyでは、複数のIP(知的財産)に関するコミュニティプラットフォーム「Fanlink」を提供しています。

service.gaudiy.com

現在、Gaudiyにはプロダクト開発チームは複数存在しますが、筆者が所属するチームは「特定のIP向けの連携機能やエンハンス開発に注力したチーム」となっています。

そのため、下記のような特性があります。

  • IPプロバイダーとの協業をより求められ、チーム開発の中でIPの影響力が大きい
  • 連携機能はリッチで新規の機能開発が求められやすい
  • マイルストーンやスコープが変化しやすい

また、メンバー構成は以下の通りです。

  • 👥 プロダクトマネージャー 1名 (+ UXデザイナー1名も兼任)
  • 👥 UI/UXデザイナー 3名
    • お試し入社者を含む ※
  • 👥 フルサイクルエンジニア 4名 ← 筆者はここに所属しています
    • お試し入社者を含む
  • (その他、BizDev・コミュニティマネージャーとも連携)

※ お試し入社の制度に関しては採用情報をご覧ください
site.gaudiy.com

ピザ2枚ルール🍕にギリギリ収まるような人数構成です(これ以上増えても減ってもバランスが崩れそうなちょうど良い状態です)。

チームの中にデザイナーも所属しており、デザイン(ディスカバリー)と開発(デリバリー)のどちらの進捗も統合的に管理するデュアルトラックアジャイルのような開発手法をとっています。

弊チームだけでなくGaudiyのプロダクト開発チームは基本的にこの構成をとっているのが特徴です。

デュアルトラックな開発チームのプロセス例

2. 特性が生み出しうるリスクや課題

さて、こうした特性が以下のような問題を生み出すと考えられます。

2-1. プレッシャーが高くストレスフルな環境に陥りやすい

IPに密着したチーム特性ということで、特定IP向けのプロダクトが生み出すアウトカムが会社というよりも特にチーム(メンバー)に直結する構造となりえます。

完全内製なプロダクトなどと比較すると、先方と握った納期・スケジュールに間に合わせなければならず、何か問題が発生した場合に社内だけでなくIPにも影響が出てしまいかねないですし、先方の要望に叶った要件を着実にデリバリーしなければならないといった外部からのプレッシャーを比較的受けやすく、一概にはいえませんがストレスフルな環境に陥りやすい構造と考えられます。

2-2. 技術的な複雑性や負債が高くなりやすい

Fanlinkは、マルチテナント(IP)向けのプロダクトですが、IPからはIP特有の要望を受けることも少なくありません(要望をいただくことは率直にありがたいことです)。

その中で、機能追加したものが特定IP専用の機能にならないように極力、汎用的に要件を落とし込むという正解があるようでない難易度の高い判断が求められることもあります。

専用の機能が増えてしまうと、固有のロジックや他チームからしたら知りもしない機能が増え、複雑性や技術的負債が増加してしまいます。もしかすると、他チームが勝手に消しにかかってしまうかもしれません。

2-3. さまざまな不確実性が高い

また、さまざまな不確実性が高いということもいえます。

Gaudiyの推奨図書にもなっている広木大地さんの「エンジニアリング組織論への招待 不確実性に向き合う思考と組織のリファクタリング」によれば、不確実性は大きく下記の3つがあるとされています。例とともに分類します。

  • 目的不確実性
    • プロダクトにおけるマーケットに何が求められているのかの正解が見えない
    • 他の競合サービスなどの取り巻く環境の変化に適応しなければいけない
  • 方法不確実性
    • スケジュールに対して要件が決まりきらずに開発を始めなければならない
    • ある大きな機能を開発中に方針・優先度が変わってペンディング・ピボットする
    • チームにプラクティスが存在しない新しい技術を用いた実現方法を求められる
  • 通信不確実性
    • PdMやデザイナーが考えた要件がデリバリーする際に適切に伝わらず手戻りを起こす
    • お試し入社者が目の前のタスクをこなす上での情報やコンテキストの不足
    • リモート主体の開発で、メンバーに聞きにくい・意見しにくい

3. それぞれの課題に対するアプローチ

ここまでは、特に目新しい情報はあまりなかったと思いますが、もしかすると「うんうん」と頷いていただけた方もいらっしゃるのかなと思います。

ここからは、より具体的なアプローチをご紹介できればと思います。

3-1. チームの成長実感や自己肯定感の醸成

もしかしたら、目的不確実性によって価値提供をしてもマーケットのアウトカムを得られないということもあるかもしれません。

そんなときもプロセスに注目し、人やチームの良かった動きやことに対してねぎらいを与えるきっかけを作ることはとても重要であると考えます。

弊チームでは、1週間1スプリントとしてスクラム開発を行っており、レトロスペクティブ(振り返り)やスプリントレビューを定期的に実施して改善を続けてきましたが、その中のHowに工夫を加えていく動きが加速しているので一部をご紹介します。

3-1-1. レトロスペクティブにSpeed boatを導入

まず、レトロスペクティブでは従来 Win-Learn-Try のようなフォーマットを用いていましたが、割と個人の内省に使われることが多く、チームや他人に対してのフィードバックが欠如してきていました。これではみんなでやる理由があまりありません。また、ずっと同じ手法を使っていると飽きも来てしまうというのもあったかもしれません。

こうした問題に気づき、チームのメンバーが立候補して、レトロスペクティブの改善のオーナーを担ってくださいました。これによって、新フォーマットの Speed boat (国内ではSail boatという方がなじみが多いかもしれません)を行うことになりました。
※ちなみに現在は、また別のフォーマットを試すなど改善がどんどん進んでいます。

Speed boatでは、そのスプリントでのチームの動きを事象別に振り返ります。

テンションが上がったアイスブレイク的な内容(太陽)、チームが進むのに助けになった要因(追い風)、逆に止めた要因(錨)、わかった潜在的なリスク(岩)を書いて内容をシェアし、最後に次に取るべきアクションをボート上に乗せて投票でネクストアクションを決めるというシンプルな内容です。

チームのSpeed boatの様子 ※抽象化してます

これによって、他者の動きを褒めたり助かったなどの声が圧倒的に増えました。また、個人よりもチームに向いた振り返りができるようになり、みんなが納得感のある振り返りの場になったと思います。

自分の成果を自分であげるよりも、他者からフィードバックをもらえる方が自己肯定感を醸成できると思いますし、何より褒められたら嬉しいですよね?

Gaudiyは評価という仕組みがない会社なため、正も負もフィードバックを他者からもらえるシチュエーションが欠如しているという特性もあり、そういった部分にもアプローチできているのかなと考えています。

3-1-2. スプリントレビューにてPdMからの今週のスポットライト選出

スプリントレビューはプロダクトマネージャーが主導していますが、ビジネス周りのメンバーも含めてディスカバリー・デリバリーのスプリント内での成果を共有する場というのが基本的な立ち位置です。

そこに、毎週チームの中で良い動きをしたメンバーにスポットライトを当て、選出の理由とともに発表いただくアジェンダを追加するという動きがありました。

こちらもレトロスペクティブの改善と同様、チームメンバーのエンゲージメントを向上する要因となっていると思います。PdMからの視点をもらえるという意味でも、たとえ選出されなかったとしても選出理由を聞けるだけで学びが大きいなと感じます。

今週のスポットライトの例 ※

※ UNIFORMな人とは弊社のクレド(行動指針)の模範となっている人のことです
note.gaudiy.com

我々は、いつもPdMの方々にスポットライトをあてていただく側なので、この場を借りて弊チームのPdMの皆様にも素晴らしいアクションをとってくださったことを改めて讃えたいと思います。ありがとうございます!

3-2. チームとしての共有知やコンテキストレベルの底上げ

特定のメンバーだけが知っていること、複数人で会話する時に相互のコンテキストの違いによって前提が合わなくてコストがかかるといったことが起きないように、短いスパンでコンテキストの共有を行っていくことが必要と考えています。特にエンジニアは4名在籍しており、ここのコンテキストレベルのズレが大きく進捗にも影響しかねないです。

下記にそれらを実現するアプローチを示します。

3-2-1. SlackのハドルミーティングやGatherに常駐する

弊チームでは地方に住むメンバーもいたりするなどリモートが主体の開発を行っています。そこで、朝から晩まで、基本的にはSlackのハドルミーティングに常駐していつでも口頭での相談や同期ができることを実現しています。

話す時以外は大体ミュート状態にしている運用で、話しかける時に「〇〇さん今いけます?」的な形で質問や相談、ペア作業などがすぐにできるような形にしています。

これには、心理的安全性を高める効果があると考えています。

最近では、試験的に Gather というサービスを用いてバーチャルオフィスを構築し、チームのミーティングなどを全てこのサービス上で行ってみるような取り組みを全社的に行なっています。

隣り合うだけでプライベートの会話空間が作り出せるのでペアプロがしやすかったり、他の人が話している場合は吹き出しマークが出て視覚的にも様子がわかるなど、会話のし始めや既に会話しているところへのジョインのハードルを下げるような狙いもありそうです。

Slackハドルミーティングとの大きな違いとして、後から会話にこっそり入室しても何もフィードバックがない(気付きにくい)ので、リアルタイム空間のような他チームのミーティングの盗み聞きみたいなこともカジュアルにできるのが斬新だなと思います(笑)

Gatherでのチームミーティングの様子
(モザイクやアバターも相まって怪しさMAXですが健全な議論をしています)

3-2-2. QAをチーム全員参加で行う

Gaudiyでは、トランクベースの開発プロセスを取っており、ほぼ毎日リリースを行なっています。そのため、前日の夕方に各チームにてデイリーQAを行って、その日にマージされた改修の品質をE2Eでチェックしています。時間としてはその日のマージした分量にもよりますが、早い場合は5分で終わる場合もあれば、30分以上かかる場合もあります。

弊チームでは基本的には、Pull Requestにスクラムのアイテム(JIRAチケットで管理)を紐づけるような運用となっていて、マージすると自動的にQAレーンに移動するようにしており、チケットに書かれた受け入れ条件に沿ってQAを行うという流れで開発を行っています。

JIRAの自動化設定

一見、エンジニアや受け入れ条件を確認するPO/PdM向けの作業に見られがちですが、ここにはデザイナーのメンバーにも参加してもらっています。

これによって、ディスカバリーとデリバリーの認知の分断を防ぐ役割や、エンジニアが気付きにくいデザイン目線でのデザイン仕様との乖離を検出しフィードバックしてもらえるなどの効果が得られています。

また、エンジニアメンバーが有志で行なったチームのコンテキストとは関係のないバグ修正や改善などに関しても、レビューからQAをチームとして担当・チケット管理するようにしており、属人化や個人への責任を緩和できるような効果も期待しています。

ちなみにコードレビューにおいても、弊チームではすべての領域の改修に関してチームのエンジニア全員のApproveを原則もらう形としており、チームメンバーが行なった改修内容のコンテキストを必ず得られるように工夫しています。(日課として、溜まったレビューを捌いたり、連携するところから業務が開始します)

3-2-3. チームで判断しきれない問題はチーム外を積極的に頼る

チームのエンジニアメンバーにおいて、どういうアーキテクチャで価値提供するかを調査・検証しレビューするというプロセスがありますが、迷った挙げ句結論がなかなか出なかったり、そもそも判断するための確度が高くないといったことが往々にして起こります。

その場合は、積極的にチーム外を頼るようにしています。
チーム外とは、社内のEnabling Teamなどの他エンジニアはもちろん、外部のコンサルタントなども含んでいます。

例えば、チーム内で認可サーバを実装する必要が出た時は、社内に詳しい知見を持つ人がいなかったため、利用するSaaSの窓口から外部の有識者を紹介していただいて仕様のレビューを受けるなどを行なってきました。

もちろん、チーム内で考えうる方針案を複数用意しつつ比較し、最適と思う案をピックアップするところまでは行い、相手のレビュー負荷を下げてから依頼することを怠ってはいけません。

このような外からの知見を獲得するプロセスを経ることによって、チームの中にも知見やコンテキストが増えていくことを期待しています。

3-3. コンテキストフォーカスによる高速デリバリーの実現

コンテキストのレベルを底上げしたり、共有したりすることも重要ですが、フォーカスすることもとても重要です。

3-3-1. フロー効率とリソース効率を意識して使い分ける

例えば、「エンジニアが4人いるなら、半分ずつに割ってそれぞれで大きなコンテキストを抱えて並行稼働すれば、二つもアウトカムを同時に得られるのでは?」というのは誰しもが考えうるかなと思います。

果たして、本当にそうでしょうか・・・?

その2ラインのレビューやQAは誰がやるのでしょうか?お互いに1人しかレビュアーがいないと質の部分に問題が生じるリスクがあり、結局相互に行わないといけないとなると設計や要件、直面した問題の背景なども十分に理解する必要が出てきてしまいます。
それを知るための時間や不確実性(ここでは先ほど挙げた通信不確実性)が暗黙的には増えてしまうわけです。

また、2つの価値提供がともに2人月ずつかかる見積もりだった場合、どちらの機能も提供まで4人2ラインでの開発では単純計算で1ヶ月かかってしまいます。

こうした半分に分けて生産性をあげようという考え方はリソース効率と呼ばれます。

もちろん適切に使えば、この考え方も大きな成果を挙げられる手法であるといえます。例えば、だれもがやり方を熟知していて誰がやっても一定のスピードでできることであれば、ペア作業で行うよりも、それぞれが分散して並行に着手する方が効率が良いに決まっています。

しかし、実装方法も定まっていない・仕様も詳しく知らないような不確実なものでは、コンテキストレベルを合わせたり、異なる視点を入れながら検討して前に進む方が手戻りも少ない可能性があります。コンテキストスイッチも不要なので、頭がパンクせずにしっかりコトに向き合えるというメリットもあるでしょう。

こうした、一つの物事を優先的に着手から完成まで行う考え方をフロー効率といいます。

例えば先ほどの価値提供の例も、全員で取り掛かったら一つの機能は半月でリリースできるかもしれません。もう半月経つと次の機能を出せて1ヶ月後から見ると価値提供完了のスピード自体は変わらないですが、一つの機能を早く価値提供でき仮説検証できるというアドバンテージが生まれます。

リソース効率とフロー効率

デュアルトラックアジャイルを敷いている我々にはこの早く検証を回せる状態はとても相性が良いと考えています。

リソース効率とフロー効率の両者をケースバイケースで使い分けていくことが、より早いリードタイム(価値提供)を生み出せるといえるのではないでしょうか。

3-3-2. ペア/モブプログラミングをカルチャーとして根付かせる

不確実なものにはフロー効率という考え方が良さそうということが先ほどの結論ではありますが、弊チームでは積極的に2人でのペアプログラミングやさらに多い人数でのモブプログラミング(モブ作業)を行なって手戻りリスクを抑えたり、同時に把握が必要なコンテキストの量を減らしレベルを揃えやすいような取り組みをしています。

プログラミングに精通しない人からすると、ペアプロする(2人で1つのことをやる)わけだから単純に速度も1/2になるのでは?と考えるかもしれませんが、下記の理由で十分に有効であると考えています。

  • ソロだとコーディング時にわからないことが出て、調査に時間がかかるかもしれない
  • 各々が分散し目前のコンテキストに集中した結果、他のメンバーのコンテキストに対してのフォローが行き届かなかったり、あまり理解しないままコードレビューまで問題に気づかず、後から手戻りするコストが発生するかもしれない
  • 従来のペアプロのドライバ・ナビゲータという役割を超えた並行的なペアプロが実現可能になったため、何もしていない時間は実は少ない
  • お試し入社者を置いてけぼりにせず、ハマりやすい罠を共有することで、早期に立ち上がってもらえる

XP(エクストリームプログラミング)などで提唱されている従来のペアプロ手法では、ドライバとナビゲータに分かれて指示を出す人とコーディングする人がそれぞれ存在しました。

同じPC上で一つのエディタを使ってお互いに操作しなければならなかったので必ず待つ人が発生してしまうという物理的な問題もあったと思われます。

しかし現在は、Visual Studio CodeのLive ShareやJetBrains製IDEのCode With Meといった主要なエディタにて、複数のPCから同時並行作業ができるソリューションが登場したことにより、双方が手を止めずに同一のブランチを並行で修正できるようになりました。もちろん、リモートにも対応していて、ホストのローカルホストポートの共有やコマンドラインの実行なども可能になっています。

これを生かし、弊チームでもペアプロ中にある程度のやるべきことだけ最初に認識を合わせたら、あとは別れてコーディングをすることも多いです。例えば、テストケースの名前だけ先に書いてその実装を分担したりするといったイメージです。一人がミーティングになっても、コネクションを繋げておけば残りの一人が作業を続行することも可能です。

頻繁にチーム内でペアプロされている様子

Gaudiyでは、どんどん新しい仲間が増えており、ほとんどのチームがお試し入社のメンバーを迎えながら開発をしています。お試し入社者の中には、特定の技術スタックは強いけど特定のスタックは経験がない・少ないというメンバーもいらっしゃいます(何を隠そう、自分が完全にそれでした)。

そうしたメンバーと作業をする場合は特にペアプロは大きな威力を発揮すると考えています。技術スタックや既存フィーチャーの仕様・コンテキストが不足していると、その時点で心理的安全性がかなり低くなってしまいます(自分も入社時はソロタスクが多く、わからないことを聞くためにも中々人が捕まりにくいなどの問題があり不安なこともありました)。自走ができる状態に持っていくための最短手段としても素晴らしいソリューションだと思います。

一方で、常にサポートが入っている状態だとアウトプットのスピードと質は保証できるかもしれませんが、個人としてのインプットや納得感・達成感は得られにくいといった問題は大いに考えられます。

時には、「一人でトライしてみたい」や、「最近あまり技術的なチャレンジできていない」といった意思やモヤをできるだけ表明しやすくし、他のメンバーはそれを尊重できるような関係性の構築も重要だといえるでしょう。

3-4. USMからバックログアイテムを転用

主にデザイナーが行うディスカバリーの手法としてはリーンXPやMVP仮説検証などを用いており、新たな価値を提供するときはユーザーストーリーマッピングという主にエンドユーザーの行動をベースとした体験を時系列などにまとめたものを制作しています。

現在はオンラインホワイトボードツールのMiroでUSM(ユーザーストーリーマップ)を作っていますが、JIRA連携の機能を利用してディスカバリーフェーズで作成されたユーザーストーリーをそのままデリバリーのバックログアイテムとして転用しています。

Miroで作ったUSMとJIRAチケットを連携し可視化するイメージ

これによって、わざわざスクラムにおけるプロダクトオーナー(PO)がバックログアイテムを作るという作業をスキップできるようになりました。

さらに、USMがユーザの行動ベースかつ比較的粒度自体も小さく設計されているため、チケットの要件記述も非常にシンプル(時にはタイトルで自明なので詳細不要)になり、要件の手戻りもしにくくなりました。

USMの例としては「ログインしていると〇〇が表示される」、「〇〇を押したら〇〇に遷移する」といった非常に小さな粒度となっています。

チーム構成のところでも紹介しましたが、メンバー数も比較的多く、デザインと開発をチーム内で抱えており、IPと密着したチーム特性もあることで、特にPdMの負荷が高くなっており、ビジネスなどのより本質的な仕事にフォーカスできるようにすべく、この手法を取り入れたという背景がありました。

チームの声としては、「チケットがわかりやすくなった」などのポジティブな反応もあったため、やって良かったと思います。

この取り組みに関しては、絶賛PDCA中になりますので、それなりに形になったタイミングでまたその内容を詳しく共有できればと考えています。

4. まとめ

いかがでしたでしょうか。最後に抽象的にはなってしまいますが、まとめです。

  1. チームの取り巻く状況や環境、特性を理解する
  2. 生まれうる/生まれた課題をしっかりと捉える
  3. 課題に対して適切なアプローチと改善のサイクルを早期に回す

この特性や課題の設定は、設定をする人のスキルセット(いわゆるメタ認知と構造化する力)に依存してしまうとは思います。そのため、その設定自体がブレていると、ただ自分がやりたいことを改善しているだけになってしまう可能性があり、本質を解決できていないことがあります。

ブレていないかは、チーム内外のメンバーに雑談ベースでも良いので壁打ちしてもらい、ブラッシュアップするのが良いかなと感じています。

筆者も、チームへのジョイン時に既存メンバーと1on1を設定させていただき、チームが置かれている状況や課題などの解像度をより鮮明にするところからアプローチしました。

この課題設定の確度を上げることが、結果的に短期での解決や改善を生む要因になったのではないかと考察しているので、非常に重要なステップだと考えています。

また、こうした取り組みがどんどん回せているのは、筆者が主導・管理したからというわけでもなく、チーム全体で一人ひとりが問題に向き合い、自律的に改善することができている結果だと考えています。

一人が主導するよりも、このように自己組織化するチームになる方が、並行してたくさんの改善が回せると思うので、チームのカルチャー創りというのも非常に重要かなと思います。

--

以上になります、お読みいただきありがとうございます!

ぜひこの記事に対しても感じたことなどありましたら、率直なフィードバックをいただけますと幸いです 🙏 

site.gaudiy.com

11/16(木)の自社イベントでも開発組織についてお話しするので、ご興味ある方はぜひ!

gaudiy.connpass.com