lokiは絶対にデフォルトの構成でデプロイするな

本記事は [Kubernetes2 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kubernetes2) の ５日目です。何を書くか最後まで迷ってました。

# はじめに

インフラのイの字も無かった私ですが、半年前にwebサービスのインフラ開発をすることになり、そこでk8sに出会いました。インフラ構成をチームメンバーと考え実装してく中でとある問題に行きつきます。

「k8sってアプリログとかアクセスログとかどうやって取ればいいんだ？」

調べていくうちにk8sには多くのログ監視ツールや仕組みがあることを知りました。そこで今回採用したのがGrafana、loki、promtailのログ監視構成です。この構成になった理由ですが

- k8s likeなログ収集（デプロイ簡単だし。ログ取得楽ちんだし。ログ整形しなくていいし）
- Grafanaのダッシュボードがチーム内でかなり評判よかった（かっちょいい）
- 最近結構熱い（？）

という結構単純な理由です（見た目大事ですよね）。promtailやlokiに関してはhelmによるデプロイが推奨されていますし、コマンド一発でk8sにデプロイできるのでhelmを使いました。よく見るlokiのデプロイ方法だと、

```bash
# リポジトリの追加
helm repo add loki https://grafana.github.io/loki/charts
helm repo update

# lokiデプロイ（default設定）
helm upgrade --install loki --namespace={{namespace}} loki/loki
```

みたいな感じですよね。これ実はだいぶ罠でした（理由は後ほど）。当時はそんなことも露知らず、コンテナのデプロイやCPU、メモリ、DIskなどの監視も上手くいき、一先ずは本番ローンチできた訳です。

# 問題発生

運用開始からしばらくした頃、問題が発生します。数あるnodeのうちの1つのDisk使用量が90%を超えたのです。「ん？なんか一つだけ明らかに使用量多いな」と思いつつ色々調べてみると結構えげつないことが判明しました。helmでデプロイするlokiのデフォルト構成、実はPodのVolumeのタイプがemptyDirで作成されていました。

これ、具体的に何がまずいかというと、

・何かの拍子でlokiのPodが落ちた時、収集してきたログが全部消える

・ログによりnodeのストレージが圧迫される

という事態に陥ります。

# 原因

なぜそうなってしまっているのかというと、[lokiのhelmでデプロイするときの設定ファイル](https://github.com/grafana/loki/blob/master/production/helm/loki/values.yaml)、こいつのpersistenceの部分がvolumeの拡張設定になるのですが、デフォルトだと

```bash
## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/
## If you set enabled as "True", you need :
## - create a pv which above 10Gi and has same namespace with loki
## - keep storageClassName same with below setting
persistence:
  enabled: false
  accessModes:
  - ReadWriteOnce
  size: 10Gi
  annotations: {}
  # selector:
  #   matchLabels:
  #     app.kubernetes.io/name: loki
  # subPath: ""
  # existingClaim:
```

という風な設定になっています。そして肝心の[lokiのstatefulsetのyamlファイル](https://github.com/grafana/loki/blob/master/production/helm/loki/templates/statefulset.yaml)ですが、Volumeのログを保存する部分（name: storage）が以下のようになっています。

```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  ~~
spec:
  ~~
  template:
    metadata:
      ~~
    spec:
      ~~
      volumes:
        ~~
  {{- 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 }}
```

そうです、` {{- if not .Values.persistence.enabled }} ` こいつです。設定ファイル(values.yaml)のpersistenceのenabledが `false` だとemptyDirでstatefulsetが作成されます。んでデフォルトは `false` ですね。そういうことでした。

# 正しくログを保存しよう

基本的にログは勝手に消えてはならないので、PersistentVolume を使って安全に保管することが推奨されます。ですので loki の statefulset が PersistentVolume を使うような設定をしていきます。

1. StorageClass と PersistenceVolumeClaim の作成

    まず PersistentVolume を作成するために StorageClass と PersistenceVolumeClaim を作成します。外部ストレージに保存できてバックアップも取れるのが割とベストかなと思い、かつ今回は AKS（Azure Kubernetes Service）を使用していたので、StorageClass の Volume タイプは Azure Files にしています。（複数マウントもできてgeoレプリケーションも組めるのでAKS使ってるなら割とおすすめ）

    ```yaml
    kind: StorageClass
    apiVersion: storage.k8s.io/v1
    metadata:
      name: azurefile-csi-standard-retain-loki-jpeast-gzrs
      namespace: loki
    provisioner: kubernetes.io/azure-file
    reclaimPolicy: Retain
    parameters:
      skuName: Standard_GZRS
      location: japaneast
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: azure-blob-storage-gzrs
      namespace: loki
    spec:
      accessModes:
      - ReadWriteMany
      storageClassName: azurefile-csi-standard-retain-loki-jpeast-gzrs
      resources:
        requests:
          storage: 10Gi
    ```

2. loki のデフォルト構成ファイル（values.yaml）を吐き出す

    次にlokiの構成を変更するために元の構成ファイルを用意します。公式の [GitHub](https://github.com/grafana/loki/blob/master/production/helm/loki/values.yaml)から持ってきても良いですが以下のコマンドでもファイルとして吐き出せます。

    ```yaml
    helm inspect values loki/loki > ./values.yaml
    ```

3. 作成したStorageClass を values.yamlに適用

    2で吐き出した構成ファイルを弄ります。persistenceの部分を以下のように変更します

    ```yaml
    persistence:
      enabled: true ## trueにする
      storageClassName: "azurefile-csi-standard-retain-loki-jpeast-gzrs" ## 作成したStorageClass名
      annotations: {}
      existingClaim: "azure-blob-storage-gzrs" ## 1でPVCを予め作ってる場合はexistingClaimにPVC名を指定
    ```

    1で PersistenceVolumeClaim を作らずにここで動的プロビジョニングしても良いです。その場合は以下のように記述します。

    ```yaml
    persistence:
      enabled: true ## trueにする
      storageClassName: "azurefile-csi-standard-retain-loki-jpeast-gzrs" ## 作成したStorageClass名
      annotations: {}
      accessModes:
      - ReadWriteMany
      size: 10Gi
    ```

4. カスタムのlokiデプロイ

    最後に3でカスタムしたファイルを指定してhelmでlokiをデプロイします。

    ```bash
    helm upgrade --install loki --namespace={{namespace}} loki/loki -f {{3のファイルのパス}}
    ```

これでemptyDirではなく適切なPersistenceVolumeにログを流すことができるようになります。図で表すとこんな感じ。

![スクリーンショット 2020-12-05 4.02.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243744/c82caa48-51b2-1c0e-5fed-e2392b7bf581.png)


# 終わりに

言わずもがなですがログというのはサービスを運用していく上でとっっっっても重要です（エンジニア２年目の当時の私は興味すら持っていなかった...）。アプリのバグ調査や不正アクセスの監視、不正アクセスがあった場合の調査、ユーザーからの問い合わせ対応など様々な場面で役に立ってくれることをここ半年で学びました。インフラに関してはk8sも含めまだまだひよっこな私ですが、今回のアドベントカレンダーでみなさんの記事を見ながら知見を増やせて行けたらなーと思います！

※何か記事内で間違ったことを書いてたらコメントなどでご指摘お願いします...