はじめに

この記事は CyberAgent Developers Advent Calendar 2024 4日目の記事です。

こんにちは、FANTECH本部の前田(@arabian9ts)です。
私のチームでは複数のプロダクトを数人で同時に開発・運用しています。
これまでのアウトプットは以下のスライドやブログにまとめています。
https://www.cyberagent.co.jp/way/list/detail/id=30678 https://speakerdeck.com/cyberagentdevelopers/cyberagent-inner-sourcing

※2024年12月現在、3事業を横断して7プロダクトを同時開発しており、今後も増えていく予定です。

これまでにも、Developers Blogを通して取り組みを紹介してきましたが、基本的な開発思想はプラガブルであり、使用する技術もプラガブルなものを積極的に選定しています。
プラグインを独自に開発することにより、複数の技術の集約管理を実現することで、開発生産性の向上につなげています。
今回は、私のチームがどのような技術を使用し、どのようなプラグインを開発しているのか、チームの開発思想も含めて紹介したいと思います。
インフラ・データ管理、モニタリング、テストツール、Linterなど、幅広い領域でプラグインを開発しているので、ぜひ参考にしてみてください。

OSSとプラグインと

CNCFのプロジェクトをはじめ、多くのOSSプロジェクトやエコシステムは、プラガブルに設計される傾向があります。
例えば、TerraformはProviderを自作することで、あらゆるリソースをTerraformで集約管理できます。
Grafanaであれば、コンポーネントやデータソースを自作することで、より自由度の高いモニタリングを構築できます。

近年盛り上がりを見せているOpenFeatureも同様に、Feature Flagsを標準化しようとしており、OpenFeature Providerを自作することで、どのFeature Flagsシステムを利用する場合でもクライアントコードの変更が不要になる設計になっています。

自分たちでプラグインを作れば細かいニーズに対応できることに加えて、企業やコミュニティが運用しているOSSは、本体となるプロジェクトは常にメンテナンスが行き届いています。
これは非常にありがたいことで、標準化やプラガブルな仕組みは、手軽に自分たちのニッチなニーズを満たす仕組みとして積極的に活用できます。
(もちろん、OSSの運営主体に対してリスペクトを込めて)

開発しているプラグイン

では、私のチームで実際にどのようなプラグインを開発しているのかご紹介します。

Terraform Provider

これまでに2つのTerraform Providerを開発しています。

  1. wings(Feature Flags)のフラグ管理用Provider
  2. PipeCDのアプリケーション管理Provider

Terraform Providerのアーキテクチャ自体はシンプルで、乱暴な言い方をするとただの外部APIのラッパーであり、Terraformから利用できるようにI/Fを合わせれば完成します。

公式に提供されているFrameworkを利用するとスムーズに実装できますし、サンプル実装も公開されています。
https://github.com/hashicorp/terraform-provider-hashicups

実際に実装した2つのProviderについては、以下のブログに過去にまとめてありますので、ぜひご覧ください。
https://developers.cyberagent.co.jp/blog/archives/43038/
https://developers.cyberagent.co.jp/blog/archives/47225/

Feature FlagsやCI/CDがIaCで集約管理されているって、なんだかワクワクしませんか?

多くのエンジニアがよく利用するのは、AWSやGoogle Cloud、AzureなどのパブリッククラウドのProviderだと思います。 内製ツールや利用しているSaaSのIaC集約管理を諦めていませんか? 意外と簡単に実装できるので、ぜひTerraform Providerの実装を検討してみてください。

Grafana Datasource

Grafana Datasourceも同様に、外部APIのラッパーのように捉えることができ、Grafanaが期待するデータに整形することで独自のモニタリングを構築できます。

公式のドキュメントに実装方法の記載があります。
https://grafana.com/developers/plugin-tools/publish-a-plugin/publish-a-plugin

私のチームでは、リアルユーザーモニタリングのため、試験的にMUX Dataを導入しました。 MUX Dataではメトリクス取得のAPIが公開されており、試しに実装してみたのがこちらのリポジトリです。
https://github.com/arabian9ts/mux-datasource

Grafanaのプラグインは審査があり、既に似たようなプラグインが存在する場合や、セキュリティやパフォーマンスなどコード上に問題がある場合はRejectされます。 私が開発したDatasourceは現在Grafanaのレビュー中ですが、無事に公開されれば誰でも利用できるようになります。

理想とするモニタリングに対して、不足しているDatasourceは開発すれば良いと考えると、あらゆるモニタリングをGrafanaに集約管理する方針も1つの戦略として良いと思っています。

もちろん、他サービスを利用することで、モニタリングを構築する時間を節約する選択肢もありますが、重要なのは集約管理され、管理工数自体が削減できていることだと思います。

Prometheus Exporter

パブリッククラウドによるManaged Prometheusの登場により、モニタリング構築においてPrometheusを選択する場面は増えたと感じています。 私のチームでは、FastlyとMUXのメトリクスをCloud RunサイドカーでPrometheusにExportしています。
https://developers.cyberagent.co.jp/blog/archives/47493/

Cloud Runサイドカーは非常に便利です。 私のチームでも積極的に利用しており、その中で一部不具合を見つけたので、リスペクトを込めて修正のコントリビュートをしました。 ただ、2024年12月現在、開発途上のようでコントリビュートは受け付けていないようなので注意が必要です。
https://github.com/GoogleCloudPlatform/run-gmp-sidecar/pull/22

FastlyのExporterは、Fastlyの公式から提供されています。 一方、MUX DataのExporterは公式から提供されているものはないため、新たに実装しました。 リポジトリをPublicにしていないため実装はお見せできませんが、実装するメトリクス数次第では、Prometheus Exporterは数百行で簡易的な実装が可能です。

Managed PrometheusにExportしたメトリクスは、GrafanaのCloud Monitoringプラグインなどを用いて可視化・アラート設定できるので、Grafanaプラグインを開発するハードルが高い場合や、単純なメトリクスの集計程度であれば、Prometheus Exporterの実装で十分かもしれません。

Scenarigo

Scenarigoは、シナリオテストのためのツールで、以前テストに関してのブログを書いた際にも後半で紹介しました。

簡易的なビジネスロジックのテストは、任意(開発者に委ねる)ことにしており、Scenarigoを利用して最低限の異常系と正常系でテストを完結させることが増えてきました。 これにより、テストの実装量を削減でき、生産性の向上に寄与しています。

Scenarigoの優れている点は、テストステップ自体をプラグインとして実装できることです。 基本的にはブラックボックステストに近い形で、一連の入出力だけをテストすることになりますが、APIによっては副作用を伴うものがあります(例えば、何かの処理を実行したあとに履歴をデータベースに書き込むなど)。 副作用として書き込まれるデータがAPIから取得できるものであれば、一度書き込まれたかどうかデータをGETして確認できますが、API経由では取得が難しいデータもあります。

シナリオテストを実装するためにAPIを開発するのは目的と手段が入れ替わってしまうので、その場合はデータベースに書き込まれたデータを確認するステップを実装します。

プラグインの実装はかなりシンプルです。

package main

import (
	"fmt"
	"time"

	"github.com/zoncoen/scenarigo/plugin"
	"github.com/zoncoen/scenarigo/schema"
)

var Do = plugin.StepFunc(func(ctx *plugin.Context, step *schema.Step) *plugin.Context {
	// データベースへの書き込みを確認するコード
	return ctx
})

ビルドしたプラグインを読み込み、ステップで参照します。

title: example
plugins:
  assertDatabaseEntity: assertDatabaseEntity.so

steps:
  - title: example
    protocol: http
    request:
      method: POST
      url: '{{vars.baseUrl}}/something'
      header:
        Content-Type: application/json
      body:
	message: test
    expect:
      code: OK
    bind:
      vars:
        someId: '{{response.id}}'

  - title: assert database entity
    ref: '{{plugins.assertDatabaseEntity.Do}}'

テストシナリオ内で利用する関数もプラグインとして提供できます。

package main

import (
	"time"
)

func Now() int64 {
	return time.Now().Unix()
}

利用方法もシンプルです。

title: example
plugins:
  utilfunc: utilfunc.so

steps:
  - title: example
    protocol: http
    request:
      method: POST
      url: '{{vars.baseUrl}}/something'
      header:
        Content-Type: application/json
      body:
	id: 'someId'
	timestamp: '{{plugins.utilfunc.Now()}}'
    expect:
      code: OK

go vetool

Goを利用している人にとっては身近なvettoolも開発しており、私のチームのコード規約として活躍しています。

  1. 外部で定義された特定のインターフェースを実装しているか検査するツール
  2. 定数列挙が型付けされていることを検査するツール

静的解析で検知できる実装ミスは多々あると思っており、テストを実装せずに検査できることには、将来的なメンテナンスコストも踏まえて非常に価値があります。

1は、外部で定義されたインターフェースを実装している場合に、処理がフックされる類のものです。 例えば、以下のProcessorを実装している場合に限り、 PreProcessPostProcess がコールされるイメージです。

type Processor interface {
    PreProcess(ctx context.Context) error
    PostProcess(ctx context.Context) error
}

実装されていないと困るケースがあるため、チーム内ではこれらを実装することを義務付けています。

2は、テストのブログでも以前触れましたが、switch文でハンドリング漏れが発生した際に、型エイリアスした型になっていないと、exhaustiveで検知できません。 そのため、型を明示することを義務付けています。
https://developers.cyberagent.co.jp/blog/archives/47920/#customlint

type Device int32

const (
    DeviceUnspecified Device = iota
    DevicePC
    DeviceSP
    DeviceTablet
)

以下のコードは、独自のvettoolによりエラー扱いにしています。

const (
    DeviceUnspecified = iota
    DevicePC
    DeviceSP
    DeviceTablet
)

GitHub Custom Action

Custom Actionを実装している方は非常に多いと感じています。
例: Checkout

- uses: actions/checkout@v4
  with:
    ref: 'main'

Custom Actionは、JavaScriptまたはDocker(Dockerfileまたはビルド済みイメージ)で準備します。 https://docs.github.com/ja/actions/sharing-automations/creating-actions/about-custom-actions

Custom Actionは、DevToolで利用者のハードルを大幅に下げることができると感じています。 私が社内で開発しているreminder-lintも、Custom Actionとして利用できるようにしています。

name: reminder-lint

on:
  schedule:
    - cron: '0 1 * * 1,2,3,4,5'

jobs:
  run:
    runs-on: ubuntu-latest
    name: reminder-lint
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run
        uses: CyberAgent/reminder-lint@main
        with:
          args: run

※reminder-lintは、以前ブログで紹介してからインナーソースプロジェクトとして生まれ変わり、社内で事業を横断して開発・利用されています。ある程度機能が揃ったタイミングでリポジトリも公開します。
https://developers.cyberagent.co.jp/blog/archives/47402/

vettoolなども、再利用性の高いものはCustom Actionとして整備することで、CIを効率的に再利用できると思います。

まとめ

IaC集約管理、モニタリング、テストツール、静的解析と、幅広いツールのプラグインについてご紹介してみました。
みなさんは、どのくらいプラグインを活用して開発生産性を上げられていますか?

私たちのソフトウェア開発を支えているのはOSSであり、リスペクトを込めてプロダクト開発で利用しています。
私自身も、利用しているOSSへの還元として積極的にエコシステムの成長にコントリビュートしたいと考えていますし、より生産性の高い仕組み作りに取り組んでいきたいと考えています。

紹介した中から、何か生産性を上げるヒントが見つかっていれば幸いです。