【GitHub Actions】Actions Runner Controllerのメトリクスを可視化してみた

# やること

Actions Runner Controller(以降、ARC)のmetricsを有効化して、Grafanaで可視化してみます。
ARCとは、GitHub Actions Self-hosted runnerを自動でスケーリングするための Kubernetes オペレーターです。

# 前提

- Kubernetesクラスタ（Linux）が構築済み
  - Azure以外のマネージドな Kubernetes サービスやOpenShift上での実行は[サポートしていない](https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-support-for-actions-runner-controller)ようです（頑張れば動かないことはない）
- ARCがインストール済み

私はローカル端末に構築したKubernetesクラスタで検証しました。

# 概要

OpenTelemetry Collector で`arc-systems`内のPodが持つmetrics取得用エンドポイントをScrapeして、Prometheusに送信します。 Prometheusに保存したmetricsをGrafanaで可視化します。
Namespaceは適宜読み替えてください。

<div align="center">

![スクリーンショット 2024-12-08 14.21.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621075/1c9c8ed6-37c0-9c19-2263-3b2b875fb586.png)

</div>

# ARCのメトリクスを有効化

ARCのメトリクスを有効化することで、ランナー数やワークフローの実行時間等のデータが Prometheus 形式で出力されるようになります。
公式ドキュメントにある通り`gha-runner-scale-set-controller`の values.yml を用意してHelmChartを更新すればOKです。

https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/deploying-runner-scale-sets-with-actions-runner-controller#enabling-metrics

Podへ通信できるようにするため、Serivceをデプロイします。

```yaml:
apiVersion: v1
kind: Service
metadata:
  name: controller-service
  namespace: arc-systems
spec:
  selector:
    app.kubernetes.io/instance: <gha-runner-scale-set-controllerのチャート名>
    app.kubernetes.io/name: gha-rs-controller
    app.kubernetes.io/namespace: arc-systems
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: listener-service
  namespace: arc-systems
spec:
  selector:
    actions.github.com/scale-set-name: <gha-runner-scale-setのチャート名>
    actions.github.com/scale-set-namespace: arc-runners
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
```

# ARCのメトリクスを保存

OpenTelemetry Collectorをクラスタにデプロイして、収集したARCのmetricsをPrometheusに送信します。

## OpenTelemetry Collectorイメージのビルド

まずは、OpenTelemetry Collectorのコンテナイメージを作成します。[公式ドキュメント](https://opentelemetry.io/docs/collector/custom-collector/)を参考にDockerfileと`builder-config.yaml`を作成します。

```Dockerfile:Dockerfile
FROM golang:1.23-bullseye AS build
WORKDIR /app
RUN go install go.opentelemetry.io/collector/cmd/builder@v0.115.0
COPY builder-config.yaml .

ARG CGO_ENABLED 0
RUN CGO_ENABLED=${CGO_ENABLED} builder --config=builder-config.yaml

FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=build --chown=nonroot:nonroot /app/otelcol-custom /app

USER nonroot

ENTRYPOINT ["/app/otelcol-custom"]
CMD ["--config", "/etc/otelcol/config.yml"]
```

Prometheus系のreceiver, exporterをインストールします。他のコンポーネントが使いたい場合は、[OpenTelemetry Registry](https://opentelemetry.io/ecosystem/registry/?language=collector&s=+Prometheus)から検索するか[GitHubリポジトリ](https://github.com/open-telemetry/opentelemetry-collector-contrib)の中から探してみましょう。

```yaml:builder-config.yaml
dist:
  name: otelcol-custom
  description: Custome OTel Collector distribution
  output_path: .

receivers:
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.115.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusremotewritereceiver v0.115.0
processors:
  - gomod: go.opentelemetry.io/collector/processor/batchprocessor v0.115.0
  - gomod: go.opentelemetry.io/collector/processor/memorylimiterprocessor v0.115.0
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.115.0
exporters:
  - gomod: github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusremotewriteexporter v0.115.0
```

ビルドしたイメージをクラスタから疎通可能なイメージレジストリに格納してください。具体的にインターネットの公開されているDockerhub、GitHub Packages...に格納するか、ローカルで完結したい場合はDocker社が提供する[registryイメージ](https://hub.docker.com/_/registry)を利用するのも手です。

## OpenTelemetry Collectorのデプロイ

OpenTelemetry CollectorをKubernetesクラスタにデプロイします。

<details><summary>マニフェスト実装例</summary>

```yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
  name: collector
  namespace: monitoring
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: otel-collector
rules:
  - apiGroups:
      - ''
    resources:
      - events
      - namespaces
      - namespaces/status
      - nodes
      - nodes/spec
      - nodes/stats
      - pods
      - pods/status
      - replicationcontrollers
      - replicationcontrollers/status
      - resourcequotas
      - services
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - apps
    resources:
      - daemonsets
      - deployments
      - replicasets
      - statefulsets
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - 'extensions'
    resources:
      - daemonsets
      - deployments
      - replicasets
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - batch
    resources:
      - jobs
      - cronjobs
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - autoscaling
    resources:
      - horizontalpodautoscalers
    verbs:
      - get
      - list
      - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: otel-collector
subjects:
  - kind: ServiceAccount
    name: collector
    namespace: monitoring
roleRef:
  kind: ClusterRole
  name: otel-collector
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-cm
  namespace: monitoring
data:
  config.yml: |
    receivers:
      prometheus:
        config:
          scrape_configs:
            - job_name: arc-controller
              scrape_interval: 60s
              metrics_path: /metrics
              static_configs:
                - targets:
                    - controller-service.arc-systems.svc:8080
            - job_name: arc-listner
              scrape_interval: 60s
              metrics_path: /metrics
              static_configs:
                - targets:
                    - listener-service.arc-systems.svc:8080
    processors:
      batch:
      memory_limiter:
        check_interval: 1s
        limit_percentage: 80
        spike_limit_percentage: 50
    exporters:
      prometheusremotewrite:
        endpoint: http://prometheus-server.monitoring.svc/api/v1/write
        tls:
          insecure: false
        resource_to_telemetry_conversion:
          enabled: true
    service:
      pipelines:
        metrics:
          receivers: [prometheus]
          processors: [memory_limiter, batch]
          exporters: [prometheusremotewrite]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-deployment
  namespace: monitoring
  labels:
    app: otel
spec:
  replicas: 1
  selector:
    matchLabels:
      app: otel
  template:
    metadata:
      labels:
        app: otel
    spec:
      serviceAccountName: collector
      containers:
      - name: otel-collector
        image: xxx/opentelemetry-collector:1.0.0
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        volumeMounts:
        - name: otel-config
          mountPath: "/etc/otelcol"
          readOnly: true
      volumes:
      - name: otel-config
        configMap:
          name: otel-cm
          items:
          - key: "config.yml"
            path: "config.yml"
```

</details>

# Prometheusのインストール

PrometheusのHelmChartをインストールします。
書き込み用のエンドポイントを有効化するための[起動オプション](https://grafana.com/docs/k6/latest/results-output/real-time/prometheus-remote-write/#send-test-metrics-to-a-remote-write-endpoint)を設定する必要があります。[values.yaml](https://github.com/prometheus-community/helm-charts/blob/main/charts/prometheus/values.yaml#L249)に起動オプションを追加してください。

```yaml:prometheus-values.yaml
server:
  extraFlags:
    - web.enable-lifecycle
    - web.enable-remote-write-receiver

```

https://artifacthub.io/packages/helm/prometheus-community/prometheus

# Grafanaのインストール

GrafanaのHelmChartをインストールします。クラスタ外からGrafanaにアクセスしたいのでvalues.yamlでNodePortを指定しています。他の項目については[公式のvalues.yaml](https://github.com/grafana/helm-charts/blob/main/charts/grafana/values.yaml)を参照してください。

```yaml:grafana-values.yaml
service:
  type: NodePort
  nodePort: 32300
image:
  repository: grafana/grafana
  tag: 11.4.0
persistence:
  type: pvc
  enabled: true
  storageClassName: <デフォルトのStorageClass名>
  accessModes:
    - ReadWriteOnce
  size: 1Gi

# 環境変数でconfigをオーバーライド
# https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#override-configuration-with-environment-variables
env:
  GF_AUTH_ANONYMOUS_ORG_ROLE: "Viewer"
  GF_AUTH_ANONYMOUS_ENABLED: true
  GF_SECURITY_ALLOW_EMBEDDING: true

datasources:
 datasources.yaml:
   apiVersion: 1
   datasources:
    - name: "Prometheus"
      type: prometheus
      access: proxy
      url: http://prometheus-server.monitoring.svc.cluster.local
```

https://artifacthub.io/packages/helm/grafana/grafana

# GitHub Actionsワークフローの実行

何でも良いので、ARCを指定してワークフローを実行してください。実行結果をGrafanaで確認します。

<details><summary>ワークフロー実装例</summary>


```yaml:.github/workflows/linter.yml
name: Lint workflows

on:
  pull_request_target:
    branches:
      - main
    paths:
      - ".github/workflows/*.yml"

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

defaults:
  run:
    shell: bash

permissions:
  contents: read

jobs:
  lint:
    name: Lint workflows
    runs-on: <gha-runner-scale-setのチャート名>
    timeout-minutes: 10

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          persist-credentials: false

      - name: Check workflow files
        uses: docker://rhysd/actionlint:latest
        with:
          args: -color
```

```yaml:.github/actionlint.yml
self-hosted-runner:
  labels:
    - <gha-runner-scale-setのチャート名>
```

</details>


# Grafanaダッシュボードの作成

`gha_job_execution_duration_seconds`を選択すると、Time列と秒数が表示されました。秒数の列はラベルに色々情報が入っているようです。また、同じようなレコードが何行も表示されていることが確認できます。

<div align="center">

![スクリーンショット 2024-12-08 23.03.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621075/70bb026e-6af6-4d16-d6f7-f0c264f29b07.png)

</div>

Transformationsタブから`Join by labels`を選択してValueに`__name__`を設定すると、ラベルにあったデータがテーブルに表示されます。

<div align="center">

![スクリーンショット 2024-12-08 23.10.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621075/48dce6b4-739c-3118-1e3b-e227c04e78ad.png)

</div>

`Group by`を追加して、以下の項目に`Group by`を設定します。

- job_name
- job_result
- runner_id
- gha_job_execution_duration_seconds_sum

RunnerのID毎にグルーピングされ重複行が表示されなくなります。

<div align="center">

![スクリーンショット 2024-12-08 23.14.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621075/32444b8b-151f-5eb1-0582-edf394c1b0b1.png)

</div>

`Rename fields by regex`を追加して、gha_job_execution_duration_seconds_sumをdurationにリネームします。

<div align="center">

![スクリーンショット 2024-12-08 23.20.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621075/a2e58184-7d61-bcbe-adec-64305aedd059.png)

</div>

Bar chartに変更して整形すると、実行したワークフローの完了までにかかった時間が表示できました。

<div align="center">

![スクリーンショット 2024-12-08 23.21.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621075/804160d2-0140-0734-d1ce-f7dcf636efdf.png)

</div>

# まとめ

ARCのmetricsをOpenTelemetry Collectorで収集して、Grafanaのダッシュボードに可視化してみました。
Platform提供者がObservabilityを導入する際に役に立ちそうなmetricsが数多く存在するので、ARCを導入する際はmetricsも保存しておくのがお勧めです。

# 補足

ARCのGitHub issuesを見ると、一部取れないmetricsがあるようです。

https://github.com/actions/actions-runner-controller/issues/3546

また、[こちらのjson](https://github.com/actions/actions-runner-controller/blob/master/docs/gha-runner-scale-set-controller/samples/grafana-dashboard/ARC-Autoscaling-Runner-Set-Monitoring_1692627561838.json)をGrafanaでimportすれば、基本的なパネルは簡単に作成できました。

https://github.com/actions/actions-runner-controller/blob/master/docs/gha-runner-scale-set-controller/samples/grafana-dashboard/README.md

# 参考リンク

- https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners-with-actions-runner-controller/about-actions-runner-controller
- https://docs.github.com/ja/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners
- https://github.com/distribution/distribution
- https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/prometheusreceiver
- https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/prometheusremotewriteexporter
- https://www.kenmuse.com/blog/enabling-github-arc-metrics/