Kubernetesのmanifest管理をkustomizeに移行した話

今までkubernetesのmanifestはテンプレートエンジンを使って環境毎にビルドして使っていましたが
kubectl1.14にkustomizeが統合された(?)のもあり
kustomizeでビルドする感じに移行したので、やったことをメモ

[Kubernetes 1.14: Production-level support for Windows Nodes, Kubectl Updates, Persistent Local Volumes GA - Kubernetes](https://kubernetes.io/blog/2019/03/25/kubernetes-1-14-release-announcement/)

kustomizeのベスト・プラクティスが知りたい、、、

# 環境

- GKE
  - 1.13系
- kustomize
  - v2.0.3
- sops
  - 3.3.0

# 管理方針

- **ディレクトリ構成**
  - baseとoverlaysで管理する
- **環境変数**
  - configMapGeneratorとsecretGeneratorを使う
  - 環境変数にConfigMapを使うと環境変数を変えた時にdeploymentが自動的にrestartしないのを理由にすべてenvを書いていたのでやっとConfigMapで管理できるようになった
    - env部分だけテンプレートファイルを分けて、同じ環境変数を使いたいdeploymentのテンプレートにincludeするような感じで運用していた
- **secret**
  - [mozilla/sops](https://github.com/mozilla/sops#encrypting-using-gcp-kms)で暗号化してリポジトリに入れる
- **Deploy**
  - CloudBuildでbuild & deploy（to GKE）を行う

# ディレクトリ構成

```
.
├── base (各サービスのベースとなるmanifest)
│   ├── service-a
│   │   ├── kustomization.yaml
│   │   └── deployment.yaml
│   ├── service-b
│   │   └── ...
│   ├── kube-state-metrics
│   │   └── ...
│   ├── newrelic-infra
│   │   └── ...
│   └── etc...
└── overlays (各環境毎のmanifest)
    ├── production
    │   ├── kustomization.yaml
    │   ├── hpa.yaml
    │   └── secret
    │       ├── xxx.enc.env
    │       └── etc...
    └── staging
        ├── kustomization.yaml
        ├── hpa.yaml
        └── secret
            ├── xxx.enc.env
            └── etc...
```

- **base**
  - overlaysのkustomization.yamlから読み込む
  - 必要なマイクロサービス的な単位でディレクトリを分ける
  - baseはbase単体でビルド可能なようにしておく
    - 必要な環境変数はconfigMapGeneratorとsecretGeneratorでダミー値を入れて書いておく
      - overlaysのkustomization.yamlで `behavior: merge` で必要な部分だけ書き換える
  - リソースの種類毎にYAMLファイルを分ける
    - 複数のDeploymentがある場合などは、 `deployments/a.yaml` のようにして分ける場合もある
- **overlays**
  - 環境毎にディレクトリを分ける
  - patch用のyamlはリソースの種類毎に分ける
    - こうしておくと `スケールアウトさせたい時はhpa.yamlを見ればいい` など意外と運用時にわかりやすい
  - secretはsecretGeneratorを使うので、env形式のファイルに分離して暗号化しておく

# 環境変数

- とくに暗号化の必要ない環境変数はconfigMapGeneratorで管理する
  - ConfigMapの名前のsuffixとしてhash値を付けて管理してくれるので、ConfigMapの内容が変わった時にnameが変わることでdeploymentが更新される
  - ただし、不要になったConfigMapがゴミとして残りつづける
      - rollbackした時などに必要になるのである程度しょうがないが、残す数を設定できると良いかも...
- 暗号化が必要な環境変数はsecretGeneratorでenvファイルを読み込むようにする

# Secret

- 読み込むenvファイルは `xxx.enc.env` というファイル名で暗号化して持っておく
  - 暗号化にはsopsとGCPのCloud KMSで管理している鍵を使う
      - https://github.com/mozilla/sops#encrypting-using-gcp-kms
- ビルド時にsopsで復号化するが、復号化したファイルはgitignoreでgit管理下には置かないようにする
  - `envCommand` は使えなくなっていた https://github.com/kubernetes-sigs/kustomize/pull/703
- DeploymentやCronJobなどからはenvFromでConfigMap or Secretを読み込む

**このあたりモヤモヤするのでベストプラクティスが知りたい、、、**


# Build

- sopsでenvの復号化が必要だったので、スクリプトを書くことにした
  - https://github.com/Agilicus/kustomize-sops こんなのもあるらしい

```shell
#!/bin/bash -eu

decrypt_secrets() {
    for f in $(ls overlays/${stage}/secret/*.enc.*); do
        local output=$(echo $f | sed -e 's/\.enc//')
        sops --output $output -d $f
    done
}

usage() {
  echo $1
  cat <<_EOT_
Usage:
  `basename $0` stage

Description:
  build manifests by kustomize

Options:
  stage           ビルドするoverlayを指定します。 production, staging, etc...
  -h --help       HELP

_EOT_
  exit 1
}


stage=
for OPT in "$@"
do
    case "$OPT" in
        # ヘルプメッセージ
        '-h'|'--help' )
            usage "Help"
            exit 1
            ;;
        -*)
            usage "[ERROR] Undefined options."
            exit 1
            ;;
        *)
            # コマンド引数（オプション以外のパラメータ）
            if [[ ! -z "$1" ]] && [[ ! "$1" =~ ^-+ ]]; then
                stage=$1
                shift 1
            fi
            ;;
    esac
done

if [ -z "$stage" ]; then
    echo -e "\033[0;31mplease select stage. production or staging\033[0m" 1>&2
    exit 1
fi

decrypt_secrets \
    && kustomize build overlays/${stage}
```

- getopts使ってないのは特に理由はありません...
- shell scriptあまり書かないので、細かい所は適当です

# Deploy

- deploy実行用のCloudBuildで使うイメージはkustomize入りのイメージを使う
  - [cloud-builders-community/kustomize at master · GoogleCloudPlatform/cloud-builders-community](https://github.com/GoogleCloudPlatform/cloud-builders-community/tree/master/kustomize)
  - CloudBuildのstepでsopsのバイナリをダウンロードして使う
- production
  - gitのpushをトリガーにしてCloudBuildで実行
  - 条件
      - branch: master
      - `base/**/*` or `overlays/production/**/*` に変更がある場合
- staging
  - `gcloud builds submit` でローカルからCloudBuildを実行する
  - ブランチへのpushをトリガーにしてもよかったが、以下の問題があったのでローカルから手動実行する事にした
      - staging環境が複数ある場合に、同じ変更を複数のstagingにデプロイしたい場合がある
      - ブランチ名を条件にしてしまうと、同じ変更なのに複数のブランチを用意する必要がある

```cloudbuild.yaml
steps:
  - id: build
    name: 'gcr.io/$PROJECT_ID/kustomize'
    entrypoint: bash
    args:
      - '-c'
      - |
        gcloud container clusters get-credentials --zone "$$CLOUDSDK_COMPUTE_ZONE" "$$CLOUDSDK_CONTAINER_CLUSTER" \
        && ./scripts/install-sops-on-cloudbuild.sh \
        && ./kube-apply ${_STAGE}
    env:
      - 'CLOUDSDK_COMPUTE_ZONE=xxxx'
      - 'CLOUDSDK_CONTAINER_CLUSTER=${_STAGE}'
```

```install-sops-on-cloudbuild.sh
#!/bin/bash -eu

SOPS_VERSION=3.3.0
curl -L https://github.com/mozilla/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux -o /usr/bin/sops \
    && chmod +x /usr/bin/sops
```

- `gcloud` コマンドで、GKEのkubernetesクラスタに対するcredentialを取得する
    - cloudbuildのサービスアカウントに権限をつけておく必要がある
        - [Cloud Build サービス アカウントの権限の設定  |  Cloud Build のドキュメント  |  Google Cloud](https://cloud.google.com/cloud-build/docs/permissions?hl=ja)
- `kube-apply` は `./build staging | kubectl apply -f -` のような事をやっているだけ
- 環境毎にクラスタを分けているので、 `substitutions` で `_STAGE` を渡すようにしてデプロイ先を切り替える

```shell
$ gcloud builds submit --config=cloudbuild.yaml --substitutions=_STAGE=staging .
```

# kustomizeへの移行でハマったポイント

## selectorがimmutableなので変更できない

### 問題点

- commonLabelsに入れたラベルはDeploymentなどのselectorにも自動的に挿入される
- これによって、既存のselectorと差異が出ると、apiVersionが `apps/v1` でselectorがimmutableなリソースは変更ができなくてエラーになる
  - https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#label-selector-updates
  
```
In API version apps/v1, a Deployment’s label selector is immutable after it gets created.
```

- transformer configでdisableできないか試したができなかった (2019/05時点)
  - [Unable to disable commonLabels injection using transformer config: `Error: conflicting fieldspecs` · Issue #817 · kubernetes-sigs/kustomize](https://github.com/kubernetes-sigs/kustomize/issues/817)

### 解決策

- `apiVersion` を `apps/v1beta1` など古いバージョンに戻すと変更できる
  - ※どのバージョンまで変更できるかまでは試してない

## selectorを変更した事でゾンビpodが発生する

### 問題点と原因

- これもcommonLabelsなどで既存のpodのlabelとselectorに差異が発生することによって起こる
- **例**
  - 既存
      - deployment
          - spec.selector.matchLabels
              - app: hoge
          - spec.template.metadata.labels
              - app: hoge
  - 新規
      - commonLabels
          - stage: staging
      - deployment
          - spec.selector.matchLabels
              - app: hoge
          - spec.template.metadata.labels
              - app: hoge
  - こんな状態になると、新規の方はselectorとpodのlabelに `stage` というのが追加される
  - 前述の方法でselectorを変更すると、旧DeploymentのReplicaSetに紐付いているpodは新Deploymentのselectorと一致しないので、Deploymentが管理してくれなくなる
  - そしてゾンビってしまう
- https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#label-selector-updates

```
  This change is a non-overlapping one, meaning that the new selector does not select ReplicaSets and Pods created with the old selector, resulting in orphaning all old ReplicaSets and creating a new ReplicaSet.
```

### 解決策

- ゾンビpodを手動で削除する
- いい方法あったら知りたかった…

# 感想

- configMapGeneratorとsecretGeneratorはまさに欲しかった機能だったのでありがたい
- kustomization.yaml以外は **ほぼ** kubernetesのmanifestそのものなので、覚えることは少ない
- kustomization.yamlの `images` が地味に便利
  - 環境毎にデプロイするイメージのタグを変えたいので
  - [kustomize/image.md](https://github.com/kubernetes-sigs/kustomize/blob/2796e54540/examples/image.md)
- まだまだ運用の知見が乏しいので、改善の余地が盛りだくさん