🎃

Cloud Run サイドカーでメトリクス・トレースデータを可視化する

2023/10/05に公開

導入

先日、Cloud Run でのサイドカーコンテナがサポートされました。
https://cloud.google.com/blog/products/serverless/cloud-run-now-supports-multi-container-deployments?hl=en

今までは Cloud Run は 1 つのコンテナしか利用できませんでしたが、2 つ以上の複数のコンテナをデプロイできるようになりました。
これによりサイドカーとして Envoy を利用したり、OpenTelemetry collector を配置し分散トレーシングの設定をアプリケーションから外出しすることができるようになりました。

ゴール

今回はこの仕組みを用いて、Cloud Run アプリケーションの Prometheus 互換のメトリクストレース情報をサイドカーの OpenTelemetry collector 経由で、ローカルでは Victoria Metrics / Grafana Tempo に、 Cloud Run では Cloud Monitoring / Cloud Trace に送信して可視化するまでの流れを整理したいと思います。

最終的な構成は以下のような構成を想定しています。

今回利用した検証用コードは以下のレポジトリで管理しています。
https://github.com/tetsuya28/cloud-run-multiple-containers-observability

設定

事前準備

まずアプリケーション側に Prometheus のメトリクスとトレース情報を吐き出す実装を追加します。

Echo を使ったサンプルアプリケーションを利用しています。
https://echo.labstack.com/

Echo の実装自体は今回のスコープからは外れるので軽く抜き出しだけします。

メトリクス

Prometheus 用のメトリクスを吐き出す設定は Echo のライブラリとして存在するため今回はそちらを利用します。
https://echo.labstack.com/docs/middleware/prometheus

import (
	"github.com/labstack/echo-contrib/echoprometheus"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	e := echo.New()
	e.Use(echoprometheus.NewMiddleware(DefaultComponentName))
	e.GET("/metrics", echoprometheus.NewHandler())
}

上記のような実装を追加することで HTTP 関連のメトリクスを吐き出すことができます。

# HELP app_request_duration_seconds The HTTP request latencies in seconds.
# TYPE app_request_duration_seconds histogram
app_request_duration_seconds_bucket{code="200",host="app:8080",method="GET",url="/metrics",le="0.005"} 0
app_request_duration_seconds_bucket{code="200",host="app:8080",method="GET",url="/metrics",le="0.01"} 4
app_request_duration_seconds_bucket{code="200",host="app:8080",method="GET",url="/metrics",le="0.025"} 35
app_request_duration_seconds_bucket{code="200",host="app:8080",method="GET",url="/metrics",le="0.05"} 36
app_request_duration_seconds_bucket{code="200",host="app:8080",method="GET",url="/metrics",le="0.1"} 37
app_request_duration_seconds_bucket{code="200",host="app:8080",method="GET",url="/metrics",le="0.25"} 37
app_request_duration_seconds_bucket{code="200",host="app:8080",method="GET",url="/metrics",le="0.5"} 37
app_request_duration_seconds_bucket{code="200",host="app:8080",method="GET",url="/metrics",le="1"} 37

トレース情報

トレースに関しては OpenTelemetry の実装に沿っています。
今回はエクスポーターとして OpenTelemetry Collector をサイドカーとして利用します。
OpenTelemetry Collector は HTTP / gRPC 両方でリクエストを受け付けることができますが今回は gRPC を利用しています。

以下のように exporter を作成し

func NewExporter(ctx context.Context, cfg *config.Config) (sdktrace.SpanExporter, error) {
	client := otlptracegrpc.NewClient(
		otlptracegrpc.WithInsecure(),
		// ここで OpenTelemetry Collector のエンドポイントを指定します
		// ローカルで docker-compose を利用する場合は otel:4317
		otlptracegrpc.WithEndpoint(cfg.OtelCollectorEndpoint),
		otlptracegrpc.WithDialOption(grpc.WithBlock()),
	)

	exporter, err := otlptrace.New(ctx, client)
	if err != nil {
		return nil, err
	}

	return exporter, nil
}

作成した exporter を用いて trace provider を作成します。

	r := resource.NewWithAttributes(
		semconv.SchemaURL,
		semconv.ServiceNameKey.String(DefaultComponentName),
	)

	traceProvider := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		sdktrace.WithResource(r),
	)

	otel.SetTracerProvider(traceProvider)

生成した trace provider を任意の関数の中で呼び出し、スパンを生成します。

func home(c echo.Context) error {
	_, span := tracer.Start(c.Request().Context(), "home")
	defer span.End()
	return c.JSON(http.StatusOK, nil)
}

OpenTelemetry Collector

メトリクスとトレース情報を受け取るコンポーネントとして OpenTelemetry Collector を構築します。
https://opentelemetry.io/docs/collector/

ローカル

ローカルで動かす際の OpenTelemetry Collector の設定を記載しています。

config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: "0.0.0.0:4317"
  prometheus:
    config:
      global:
        external_labels: {}
      scrape_configs:
        - job_name: cloud-run-otel
          scrape_interval: 10s
          static_configs:
            - targets:
                - localhost:8888
        - job_name: cloud-run
          scrape_interval: 10s
          metrics_path: /metrics
          static_configs:
            - targets:
                - app:8080
exporters:
  prometheusremotewrite:
    endpoint: http://victoriametrics:8428/api/v1/write
  otlp:
    endpoint: http://tempo:4317
    tls:
      insecure: true
service:
  telemetry:
    logs:
      level: WARN
      encoding: json
  extensions:
    - health_check
  pipelines:
    metrics:
      receivers:
        - prometheus
      exporters:
        - prometheusremotewrite
    traces:
      receivers:
        - otlp
      exporters:
        - otlp
extensions:
  health_check: null

Cloud Run

Cloud Run で動かす際の OpenTelemetry Collector の設定を記載しています。

config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: "0.0.0.0:4317"
  prometheus:
    config:
      global:
        external_labels:
          service: ${K_SERVICE}
          revision: ${K_REVISION}
      scrape_configs:
        - job_name: cloud-run-otel
          scrape_interval: 10s
          static_configs:
            - targets:
                - localhost:8888
        - job_name: cloud-run
          scrape_interval: 10s
          metrics_path: /metrics
          static_configs:
            - targets:
                - localhost:8080
exporters:
  googlemanagedprometheus:
    project: ${PROJECT_ID}
  googlecloud:
    trace:
      endpoint: cloudtrace.googleapis.com:443
service:
  telemetry:
    logs:
      level: WARN
      encoding: json
  extensions:
    - health_check
  pipelines:
    metrics:
      receivers:
        - prometheus
      processors:
        - batch
        - resourcedetection
        - resource
      exporters:
        - googlemanagedprometheus
    traces:
      receivers:
        - otlp
      exporters:
        - googlecloud
processors:
  batch:
    send_batch_max_size: 200
    send_batch_size: 200
    timeout: 5s
  resourcedetection:
    detectors:
      - env
      - gcp
  resource:
    attributes:
      - key: service.name
        value: ${K_SERVICE}
        action: upsert
      - key: service.instance.id
        from_attribute: faas.id
        action: insert
extensions:
  health_check: null

各種設定

まずは OpenTelemetry Collector がメトリクスとトレーシング情報を受け取るための receiver の設定を記載します。
トレーシング情報は先ほど記載したように gRPC で受け取るための設定を記載しています。

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: "0.0.0.0:4317"

メトリクスは Prometheus の設定と同じような書き方でアプリケーションコンテナの /metrics をスクレイピングする設定を記載しています。
サービス運用していくと Cloud Run のリビジョンによってメトリクスを区別できた方がモニタリングを行いやすいため全てのメトリクスに対して global.external_labels で Cloud Run の予約環境変数である K_SERVICE / K_REVISION をラベルに追加しています。

receivers:
  prometheus:
    config:
      global:
        external_labels:
          # Cloud Run のリビジョン情報をメトリクスのラベルとして追加しています
          service: ${K_SERVICE}
          revision: ${K_REVISION}
      scrape_configs:
        - job_name: cloud-run-otel
          scrape_interval: 10s
          static_configs:
            - targets:
                - localhost:8888
        - job_name: cloud-run
          scrape_interval: 10s
          metrics_path: /metrics
          static_configs:
            - targets:
                - app:8080

Cloud Run で動かす際は、アプリケーションコンテナと OpenTelemetry Collector が同一ネットワーク上に所属するため static_configs を以下に変更する必要があります。

          static_configs:
            - targets:
                - localhost:8080

続いて、 OpenTelemetry Collector が受け取ったメトリクスとトレース情報を実際のデータストアに投げるための exporter の設定を記載します。
今回はローカルではメトリクスのデータストアとして Victoria Metrics
https://victoriametrics.com/

exporters:
  prometheusremotewrite:
    endpoint: http://victoriametrics:8428/api/v1/write

Cloud Run で動かす際は Google-managed Prometheus を利用するため exporter は以下の設定を利用しています。
※ Cloud Run の OpenTelemetry Collector コンテナの環境変数に PROJECT_ID を設定しています。

exporters:
  googlemanagedprometheus:
    project: ${PROJECT_ID}

ローカルのトレース情報のデータストアとして Grafana Tempo を利用しています。
https://grafana.com/oss/tempo/

exporters:
  otlp:
    endpoint: http://tempo:4317
    tls:
      insecure: true

Cloud Run で動かす際は Cloud Trace を利用するため exporter は以下の設定を利用しています。

exporters:
  googlecloud:
    trace:
      endpoint: cloudtrace.googleapis.com

そしてこれらの設定を流し込むためのローカル用の service を記載します。

service:
  telemetry:
    logs:
      level: ERROR
      encoding: json
  extensions:
    - health_check
  pipelines:
    metrics:
      receivers:
        - prometheus
      exporters:
        - prometheusremotewrite
    traces:
      receivers:
        - otlp
      exporters:
        - otlp
extensions:
  health_check: null

Cloud Run で動かす際は以下のような設定を記載します。

service:
  telemetry:
    logs:
      level: WARN
      encoding: json
  extensions:
    - health_check
  pipelines:
    metrics:
      receivers:
        - prometheus
      processors:
        - batch
        - resourcedetection
        - resource
      exporters:
        - googlemanagedprometheus
    traces:
      receivers:
        - otlp
      exporters:
        - googlecloud

また、 Cloud Run の場合は Google-managed Prometheus に必要な設定などを自動で挿入するための processors の設定を追加しています。
resourcedetection に関してはこちらのページを参考にしています。
https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/resourcedetectionprocessor/README.md#google-cloud-run-services-metadata

processors:
  batch:
    send_batch_max_size: 200
    send_batch_size: 200
    timeout: 5s
  resourcedetection:
    detectors:
      - env
      - gcp
  resource:
    attributes:
      - key: service.name
        value: ${K_SERVICE}
        action: upsert
      - key: service.instance.id
        from_attribute: faas.id
        action: insert

データストア

今回は Victoria Metrics や Tempo の設定はスコープ対象外とするため詳細には記載しませんが、 GitHub レポジトリにローカルで動かすための設定が記載されていますので気になる方は参考にしてみてください。

確認

ローカルでの確認

上記設定を全てまとめてローカルで実行できるように docker compose 環境作成しているのでこちらを立ち上げて確認を行っていきます。

以下のコマンドで環境の立ち上げを行います。

docker compose up -d

立ち上がったのち、 localhost:3000 にて Grafana へアクセスします。
初期ログイン情報はユーザ名・パスワードともに admin となっています。

ログイン後に Explore にてメトリクスとトレース情報の確認を行います。

メトリクスはデータソースを VictoriaMetrics にした状態で以下のようなクエリを叩くことで Echo のメトリクスが可視化されていることが確認できます。

promhttp_metric_handler_requests_total

トレース情報はデータソースを Tempo にすることでアプリケーションから吐き出されたトレース情報を確認することができます。

Cloud Run での確認

ここまででローカルでの動作確認が完了したので実際に Cloud Run で実行するための設定と構成を記載していきます。

Cloud Run の設定は Knative のマニフェストを利用しています。

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: cloud-run-mco
  annotations:
    run.googleapis.com/launch-stage: BETA
    run.googleapis.com/ingress: all
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/maxScale: '1'
    spec:
      containerConcurrency: 1
      timeoutSeconds: 300
      serviceAccountName: "" # roles/cloudtrace.agent, roles/monitoring.metricWriter 権限を持った Google Service Account を指定します
      containers:
      - name: app
        image: "" # ビルドしたアプリケーションコンテナを指定します
        env:
        - name: OTEL_COLLECTOR_ENDPOINT
          value: localhost:4317
        ports:
        - name: http1
          containerPort: 8080
        resources:
          limits:
            cpu: 500m
            memory: 256Mi
        startupProbe:
          timeoutSeconds: 5
          periodSeconds: 5
          failureThreshold: 3
          httpGet:
            path: /
            port: 8080
      - image: "" # ビルドした OpenTelemetry Collector コンテナを指定します
        env:
        - name: PROJECT_ID
          value: "" # 動かしている GCP プロジェクトを指定します
        resources:
          limits:
            cpu: 200m
            memory: 128Mi
        startupProbe:
          initialDelaySeconds: 10
          timeoutSeconds: 10
          periodSeconds: 30
          failureThreshold: 3
          httpGet:
            path: /

Cloud Monitoring と Cloud Trace でそれぞれメトリクスとトレース情報が可視化されていることが確認できます。


まとめ

アプリケーションの実装からメトリクスとトレース情報の送信や回収を OpenTelemetry Collector に委譲することにより Cloud Run だけではなく任意の環境で同じようなモニタリングの仕組みを実現することができるようになりました。

OpenTelemetry Collector の設定は yaml で管理する必要があり、またローカルと Cloud Run で設定も違うことも多いですが今回の記事では紹介しませんでしたが cue を用いて共通化しながら環境ごとに差分を適用することでかなり簡単に構成ファイルを管理することもできるようになっています。
気になる方は GitHub のレポジトリをご覧ください。

ご意見やご指摘などありましたらコメント / X ( Twitter ) での連絡お待ちしております。

Discussion