Grafana LokiをGKEで安価に使う

0. はじめに

三菱総研DCSデジタル企画推進部の山内です。最近は、クラウドネイティブなアプリケーションの基盤について調査、検証を行っています。

マイクロサービスアーキテクチャを採用したアプリケーションのモニタリング環境について検証する中で、ログ収集ツールとしてGrafana LokiというOSSに注目しました。
今回は検証環境ということでコストを抑えた構成を取ったため、その構成をご紹介します。

    目次

  1. Grafana Lokiとは
  2. コストを抑えた構成
  3. おわりに

1. Grafana Lokiとは

Grafana Lokiはログ収集ツールで、Grafana Labs主体で開発されているOSSです。
Grafana Lokiでは、収集するログデータを保管するデータストアを2種類用意する必要がありました。
・index用:ログデータのインデックスを保管する。
・chunk用:ログデータ自体を保管する。
モニタリング対象のマイクロサービスはGKE(Google Kubernetes Engine)上で稼働しています。データストアにクラウドサービスを活用する場合、index用とchunk用の組み合わせは以下のようになります。
・index用:BigTable
・chunk用:GCS(Google Cloud Storage)

loki-bigtable

※Grafana Lokiがログ収集する仕組みを簡易的に表現しています。


BigTableの利用料としては、①クラスタ利用料と②ストレージ利用料の2種類が発生します。
仮に100GBのindexが生成されるとした場合、BigTableの利用料を試算したところ、月額7万円程度かかることがわかりました。

Bigtable-Estimate

Grafana Lokiの検証のためだけにBigTableを利用することは費用がかかりすぎると判断し、boltdbというツールを使ってindexもGCSに保管するように構成しました。

indexの保管において、BigTableを利用した構成では約7万円かかるところ、boltdbを利用した構成ではGCSの利用料だけとなり、月額200円弱(0.016USD*100GB*110円)におさえることができます。
このように、boltdbを利用することで大幅な費用削減ができると考えたのが今回の構成の背景となります。

2. コストを抑えた構成

boltdbは組み込み型のKVS(キーバリューストア)です。boltdbを使ってindexをローカルに保持することができます。
今回はindexを一旦boltdbに保持し、boltdb-shipperを使いindexをGCSに転送することで、データ永続化を実現します。

lokiwithbolt

※boltdb-shipperはGrafana LokiのV1.5.0でリリースされたオプションです。

これまでindexはCassandraやBigTableといったNoSQLデータベースを使う必要がありましたが、boltdb-shipperのおかげでchunk保管用のオブジェクトストレージにindexも保管できるようになり、環境を簡易的にしコストを削減することができるようになりました。

環境構築する手順を具体的に見ていきます。
今回は、モニタリング対象のマイクロサービスが稼働するGKE上にGrafana Lokiを相乗りさせます。
Grafana Lokiの導入にあたって、Helmを利用します。
Helmとは、Kubernetesにおけるパッケージマネージャのようなものです。
様々なKubernetesマニフェストのテンプレート(Helmチャート)がアップロードされており、コマンド操作でテンプレートに具体値を与えて、その内容でマニフェストをKubernetesクラスタへ適用できます。

Helmチャートダウンロード

Grafana LokiがGCSにアクセスする際の認証情報を設定する必要がありますが、Helmチャートがその設定に対応していないため、一部テンプレートを修正するためにHelmチャートをダウンロードします。

helm fetch loki/loki-stack


values.yamlの編集

まずはvalues.yamlを編集し、利用するデータストアを設定します。

values.yaml

helmテンプレートの編集

GCSにアクセスするための認証情報をsecretリソースとして定義します。

apiVersion: v1
kind: Secret
metadata:
  name: loki-access-gcs
type: Opaque
data:
  key.json: XXXXX


定義したsecretリソースをGrafana Lokiにマウントし、認証情報を環境変数から読み出せるようにします。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ template "loki.fullname" . }}
  namespace: {{ .Release.Namespace }}
  labels:
    app: {{ template "loki.name" . }}
    chart: {{ template "loki.chart" . }}
    release: {{ .Release.Name }}
    heritage: {{ .Release.Service }}
  annotations:
    {{- toYaml .Values.annotations | nindent 4 }}
spec:
  podManagementPolicy: {{ .Values.podManagementPolicy }}
  replicas: {{ .Values.replicas }}
  selector:
    matchLabels:
      app: {{ template "loki.name" . }}
      release: {{ .Release.Name }}
  serviceName: {{ template "loki.fullname" . }}-headless
  updateStrategy:
    {{- toYaml .Values.updateStrategy | nindent 4 }}
  template:
    metadata:
      labels:
        app: {{ template "loki.name" . }}
        name: {{ template "loki.name" . }}
        release: {{ .Release.Name }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      serviceAccountName: {{ template "loki.serviceAccountName" . }}
    {{- if .Values.priorityClassName }}
      priorityClassName: {{ .Values.priorityClassName }}
    {{- end }}
      securityContext:
        {{- toYaml .Values.securityContext | nindent 8 }}
      initContainers:
        {{- toYaml .Values.initContainers | nindent 8 }}
      {{- if .Values.image.pullSecrets }}
      imagePullSecrets:
      {{- range .Values.image.pullSecrets }}
        - name: {{ . }}
      {{- end}}
      {{- end }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          args:
            - "-config.file=/etc/loki/loki.yaml"
          {{- range $key, $value := .Values.extraArgs }}
            - "-{{ $key }}={{ $value }}"
          {{- end }}
          volumeMounts:
            {{- if .Values.extraVolumeMounts }}
              {{ toYaml .Values.extraVolumeMounts | nindent 12}}
            {{- end }}
            - name: config
              mountPath: /etc/loki
            - name: storage
              mountPath: "/data"
              subPath: {{ .Values.persistence.subPath }}
            - name: loki-access-gcs  # secretをボリュームマウントする
              mountPath: /etc/secrets
          ports:
            - name: http-metrics
              containerPort: {{ .Values.config.server.http_listen_port }}
              protocol: TCP
          livenessProbe:
            {{- toYaml .Values.livenessProbe | nindent 12 }}
          readinessProbe:
            {{- toYaml .Values.readinessProbe | nindent 12 }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          securityContext:
            readOnlyRootFilesystem: true
          env:
            {{- if .Values.env }}
              {{- toYaml .Values.env | nindent 12 }}
            {{- end }}
            {{- if .Values.tracing.jaegerAgentHost }}
            - name: JAEGER_AGENT_HOST
              value: "{{ .Values.tracing.jaegerAgentHost }}"
            {{- end }}
            - name: GOOGLE_APPLICATION_CREDENTIALS  # GCS認証情報を、マウントしたsecretの内容から環境変数に設定する。
              value: /etc/secrets/key.json
{{- if .Values.extraContainers }}
{{ toYaml .Values.extraContainers | indent 8}}
{{- end }}
      nodeSelector:
        {{- toYaml .Values.nodeSelector | nindent 8 }}
      affinity:
        {{- toYaml .Values.affinity | nindent 8 }}
      tolerations:
        {{- toYaml .Values.tolerations | nindent 8 }}
      terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
      volumes:
        - name: config
          secret:
            secretName: {{ template "loki.fullname" . }}
        - name: loki-access-gcs  # secretをボリュームとして定義する
          secret:
            secretName: loki-access-gcs
{{- if .Values.extraVolumes }}
{{ toYaml .Values.extraVolumes | indent 8}}
{{- end }}
  {{- if not .Values.persistence.enabled }}
        - name: storage
          emptyDir: {}
  {{- else if .Values.persistence.existingClaim }}
        - name: storage
          persistentVolumeClaim:
            claimName: {{ .Values.persistence.existingClaim }}
  {{- else }}
  volumeClaimTemplates:
  - metadata:
      name: storage
      annotations:
        {{- toYaml .Values.persistence.annotations | nindent 8 }}
    spec:
      accessModes:
        {{- toYaml .Values.persistence.accessModes | nindent 8 }}
      resources:
        requests:
          storage: {{ .Values.persistence.size | quote }}
      storageClassName: {{ .Values.persistence.storageClassName }}
      {{- if .Values.persistence.selector }}
      selector:
        {{- toYaml .Values.persistence.selector | nindent 8 }}
      {{- end }}
  {{- end }}


このHelmチャートを使って、Kubernetesマニフェストを生成します。

helm template --release-name sample --namespace monitoring loki-stack --set loki.persistence.enabled=true,loki.persistence.storageClassName=standard,loki.persistence.size=5Gi


Kubernetesマニフェスト適用

生成したマニフェストをGKEに適用し、Grafana Lokiをデプロイして完了です。

3. おわりに

Grafana Lokiは、メトリクス監視のPrometheusや分散トレーシングのJaeger、ダッシュボードツールのGrafanaと連携できることを期待して導入しています。

monitoringstack

詳細な動きは未検証ですが、マイクロサービスのオブザーバビリティ向上にむけて、検証を進めていきたいと思います。