KubeLinterを動かすKustomizeのプラグインを作る

# はじめに

最近、自動化に関する話題を多く聞きます。私も何か使えるようになりたいと思い、k8sリソースの静的な解析ツールであるKubeLinterを使ってみました。Kustomizeのプラグインとしたかったのですが、方法が載っていなかったので作ることにしました。慣れないGoで作りましたので、つたない部分はご了承ください。

# KubeLinterとは

KubeLinterはk8sのマニフェスト（yaml形式）をベストプラクティスに則ってチェックするツールです。チェックするルールはカスタマイズすることができ、ルールを守っていなければエラーでデプロイしないようにできます。ルールを守らなければ以下のようなエラーになります。

```bash
pod.yaml: (object: <no namespace>/security-context-demo /v1, Kind=Pod) container "sec-ctx-demo" does not have a read-only root file system (check: no-read-only-root-fs, remediation: Set readOnlyRootFilesystem to true in your container's securityContext.)

pod.yaml: (object: <no namespace>/security-context-demo /v1, Kind=Pod) container "sec-ctx-demo" has cpu limit 0 (check: unset-cpu-requirements, remediation: Set your container's CPU requests and limits depending on its requirements. See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for more details.)

pod.yaml: (object: <no namespace>/security-context-demo /v1, Kind=Pod) container "sec-ctx-demo" has memory limit 0 (check: unset-memory-requirements, remediation: Set your container's memory requests and limits depending on its requirements.    See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for more details.)

Error: found 3 lint errors
```

（引用：https://docs.kubelinter.io/#/?id=usage）

同じようなツールを調べてみると、kube-scoreも有名のようでした。今回はRedHatが管理しているKubeLinterにしました。

https://www.redhat.com/ja/topics/containers/what-is-kubelinter

https://github.com/zegl/kube-score

# 出来上がり

今回作るものは以下の通りです。
* KubeLinterを実行するGoファイル
* Goファイルをビルドしたコンテナイメージ
* プラグインを実行するyamlファイル

ベストプラクティスに則っていないk8sリソースをKustomizeを使って作成しようとすると、エラーとなります。

```bash
$ kustomize build --enable-alpha-plugins | kubectl apply -f -
KubeLinter development

/tmp/209011040: (object: <no namespace>/nginx /v1, Kind=Pod) container "nginx" has cpu request 0 (check: unset-cpu-requirements, remediation: Set CPU requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)

/tmp/209011040: (object: <no namespace>/nginx /v1, Kind=Pod) container "nginx" has memory request 0 (check: unset-memory-requirements, remediation: Set memory requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)

processing filter: found 2 lint errors
Error: couldn't execute function: exit status 1
error: no objects passed to apply
```

さらにTerraformでも実行できました。

```
$ TF_LOG=WARN terraform apply
2023-02-28T12:47:04.088Z [WARN]  ValidateProviderConfig from "provider[\"registry.terraform.io/kbst/kustomization\"]" changed the config value, but that value is unused
data.kustomization_overlay.resources: Reading...
2023-02-28T12:47:04.982Z [WARN]  unexpected data: registry.terraform.io/kbst/kustomization:stderr="KubeLinter development

/tmp/447357866: (object: <no namespace>/nginx /v1, Kind=Pod) container "nginx" has cpu request 0 (check: unset-cpu-requirements, remediation: Set CPU requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)

/tmp/447357866: (object: <no namespace>/nginx /v1, Kind=Pod) container "nginx" has memory request 0 (check: unset-memory-requirements, remediation: Set memory requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)

processing filter: found 2 lint errors"
2023-02-28T12:47:05.098Z [ERROR] provider.terraform-provider-kustomization_v0.9.2: Response contains error diagnostic: diagnostic_summary="buildKustomizeOverlay: Kustomizer Run for path '.' failed: couldn't execute function: exit status 1 " tf_data_source_type=kustomization_overlay tf_provider_addr=provider tf_req_id=67f53c55-bc6f-64b0-7712-418feb6406d4 tf_rpc=ReadDataSource @module=sdk.proto diagnostic_detail= diagnostic_severity=ERROR tf_proto_version=5.3 @caller=github.com/hashicorp/terraform-plugin-go@v0.14.1/tfprotov5/internal/diag/diagnostics.go:55 timestamp=2023-02-28T12:47:05.097Z
2023-02-28T12:47:05.098Z [ERROR] vertex "data.kustomization_overlay.resources" error: buildKustomizeOverlay: Kustomizer Run for path '.' failed: couldn't execute function: exit status 1
2023-02-28T12:47:05.098Z [ERROR] vertex "data.kustomization_overlay.resources (expand)" error: buildKustomizeOverlay: Kustomizer Run for path '.' failed: couldn't execute function: exit status 1
╷
│ Error: buildKustomizeOverlay: Kustomizer Run for path '.' failed: couldn't execute function: exit status 1
│
│   with data.kustomization_overlay.resources,
│   on terraform.tf line 14, in data "kustomization_overlay" "resources":
│   14: data "kustomization_overlay" "resources" {
│
```

# （補足）Kustomizeのプラグインの種類と実行方法

2023年2月27時点のKustomizeのプラグインの種類と開発ステータスは以下の通りです。

* Containerized KRM Functions：Alpha（recommended style of plugin）
* Exec KRM functions：Alpha（may change significantly）
* Exec plugins：Deprecation
* Go Plugins：Deprecation

詳細は以下のページに書いてあります。

https://kubectl.docs.kubernetes.io/guides/extending_kustomize/

今後使うため、Containerized KRM Functionsを使います。プラグインの作成方法は以下のページを基にしています。

https://kubectl.docs.kubernetes.io/guides/extending_kustomize/containerized_krm_functions/

また、プラグインをKustomizeで使うには有効化する必要があります。以下のようなコマンドでプラグインを有効にしてk8sリソースを作成できます。

```bash
$ kustomize build --enable-alpha-plugins ＜manifest-dir＞ | kubectl apply -f -
# enable-alpha-pluginsはプラグインを有効にするオプションです。
# manifest-dirはkustomization.yamlが存在するディレクトリです。
```

さらに、様々なプラグインが使えるTerraformでも実行できるか確認します。TerraformではKustomize用のプラグインがあります。
https://registry.terraform.io/providers/kbst/kustomization/latest/docs
ドキュメントにて明言されていませんが、v0.9.1からプラグインの有効化のオプションも使えるようになっています。

# KubeLinterを実行するGoファイルの作成

以下のGoファイルを作成しました。Goのバージョンは1.18を使用しています。
スクリプトの書き方はドキュメントがないため、以下のような*_test.goを参考にしました。
https://github.com/stackrox/kube-linter/blob/0.4.0/pkg/command/root/command_test.go

```go:main.go
package main

import (
  "os"
  "fmt"

  "golang.stackrox.io/kube-linter/pkg/command/root"
  _ "golang.stackrox.io/kube-linter/pkg/templates/all"

  "sigs.k8s.io/kustomize/kyaml/yaml"
  "sigs.k8s.io/kustomize/kyaml/fn/framework"
  "sigs.k8s.io/kustomize/kyaml/fn/framework/command"
  "sigs.k8s.io/kustomize/kyaml/kio"
)

func main() {
  fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) {
    //Kustomizeで出力された複数のyamlを1つにまとめるための一時的なファイルを作成します。
    f, _ := os.CreateTemp("","")
    defer os.Remove(f.Name())

    for _, v := range items {
      stdin_str := v.MustString() //yaml形式に変換します。
      stdin_data := []byte(stdin_str) //ファイルに書き込めるようにbyteに変換します。
      f.Write([]byte("\n---\n")) //リソースのyaml間の区切りとして「---」を入れます。
      f.Write(stdin_data)
    }
    c := root.Command()
    c.SetArgs([]string{
      "lint",
      f.Name(), //作成したファイルを指定します。
    })
    err := c.Execute()
    if err != nil {err = fmt.Errorf("%w\n",err)} //見栄えが悪かったので、改行を追加しました。

    return items, err
  }
  p := framework.SimpleProcessor{Filter: kio.FilterFunc(fn)}
  cmd := command.Build(p, command.StandaloneDisabled, false)
  command.AddGenerateDockerfile(cmd)
  if err := cmd.Execute(); err != nil {
    os.Exit(1)
  }
}
```

KubeLinterのモジュールはgo.modに書いてGoファイル中で使えるようにしています。

```go:go.mod
module MyKubeLinter

go 1.18

require (
  golang.stackrox.io/kube-linter v0.4.0
)

replace (
  golang.stackrox.io/kube-linter => ./kube-linter
)
```

上記go.modにもあるように、KubeLinterはGithubにあるソースファイルを編集して使用しています。
編集する目的は以下の通りです。
* 依存モジュールで存在しないバージョンを使用しているため。
以下のモジュールのダウンロードにてエラーが起きたため、go.modを再作成しています。
https://github.com/openshift/api
* ログの出力を標準出力から標準エラーに変更する。
KubeLinterではログの出力は標準出力に固定されています。Kustomizeが標準出力を管理しているのでログが出なくなってしまいます。そのためソースファイルを編集して出力先を変えます。
ここで、KubeLinterはCobraを使っているのですが、SetOutputを使っても変更できません。

以下のようにGithubリポジトリをクローンして編集しています。

```bash
$ git clone https://github.com/stackrox/kube-linter.git -b v0.4.0
#goのバージョンに合わせてv0.4.0にしました。
$ cd kube-linter/
$ grep -rl "os\.Stdout" | xargs sed -i "s/os\.Stdout/os\.Stderr/g"
$ module=$(cat go.mod | grep module | awk '{print $2}')
$ rm go.mod go.sum
$ go mod init $module
$ go mod tidy
```

# Goファイルをコンテナイメージにビルドする

作成したGoファイルはコンテナイメージとしてビルドします。
Goファイル中にDockerfileを作成するコマンドが含まれており、実行できます。

```bash
$ go run main.go gen .
```

しかし、そのままではビルドできません。
ビルド中のコンテナ内にKubeLinterのGithubリポジトリが配置されていないためです。
以下のDockerfileを作成して、ローカルのKubeLinterをコンテナ（golang:1.18-alpine）内に配置します。

```Dockerfile:Dockerfile
FROM golang:1.18-alpine as builder
ENV CGO_ENABLED=0
WORKDIR /go/src/
#コンテナ内にKubeLinterを配置する
COPY /path/to/kube-linter /go/src/kube-linter
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags '-w -s' -v -o /usr/local/bin/function ./

FROM alpine:latest
COPY --from=builder /usr/local/bin/function /usr/local/bin/function
ENTRYPOINT ["function"]
```

イメージをビルドします。

```bash
$ docker build . -t my-kubelinter:1.0.0
```

# プラグインを実行するyamlファイルを作成する

ビルドしたコンテナイメージを指定して、プラグインを実行するyamlファイルを作成します。以下の通りです。

```yaml:mykubelinter.yaml
apiVersion: transformers.example.co/v1
kind: MyKubeLinter
metadata:
  name: myKubeLinter
  annotations:
    config.kubernetes.io/function: |
      container:
        image: my-kubelinter:1.0.0
```

これを他のk8sリソースが書かれたyamlファイルと一緒にデプロイされるようにします。
例として以下のようにします。

```yaml:kustomization.yaml
resources:
- ...
transformers:
- mykubelinter.yaml
```

# プラグインを使ってKustomizeを実行する

Kustomizeを実行すると、KubeLinterがルールに従っているか確認します。

```bash
$ kustomize build --enable-alpha-plugins | kubectl apply -f -
KubeLinter development

/tmp/209011040: (object: <no namespace>/nginx /v1, Kind=Pod) container "nginx" has cpu request 0 (check: unset-cpu-requirements, remediation: Set CPU requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)

/tmp/209011040: (object: <no namespace>/nginx /v1, Kind=Pod) container "nginx" has memory request 0 (check: unset-memory-requirements, remediation: Set memory requests and limits for your container based on its requirements. Refer to https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#requests-and-limits for details.)

processing filter: found 2 lint errors
Error: couldn't execute function: exit status 1
error: no objects passed to apply
```

期待した通りkustomizeを実行した際にKubeLinterのエラーを起こすことができました。
エラーの通りにyamlを修正すると、エラーは解消されます。

```bash
$ kustomize build --enable-alpha-plugins | kubectl apply -f -
KubeLinter development

No lint errors found!
pod/nginx created
```

# Terraformでもプラグインを実行する

以下のtfファイルを作りました。

```tf:terraform.tf
terraform {
  required_providers {
    kustomization = {
      source  = "kbst/kustomization"
      version = "0.9.2"
    }
  }
}

provider "kustomization" {
  kubeconfig_path = "~/.kube/config"
}

data "kustomization_overlay" "resources" {
  resources = [
    "pod.yaml",
  ]
  transformers = [
    "mykubelinter.yaml",
  ]

  kustomize_options {
    load_restrictor      = "none"
    enable_alpha_plugins = true
  }
}

resource "kustomization_resource" "resources" {
  for_each = data.kustomization_overlay.resources.ids
  manifest = data.kustomization_overlay.resources.manifests[each.value]
}
```

TerraformのKustomizeはv0.9.1からプラグインを有効にできるようになっています。
定義はソースファイルから読み取れます。
https://github.com/kbst/terraform-provider-kustomization/blob/v0.9.1/kustomize/data_source_kustomization_overlay.go#L646-L649

実行すると、同じようにKubeLinterを動かせます。ただし、エラーログはWARNとして出力されます。
Terraformの場合はDockerのプラグインがあるため、さらに処理をひとまとめにできると思います。

# おわりに

今回はドキュメントにないものを作ってみました。使ったことのない言語を読み解き自身が使うのは大変だと思いました。。ただ自分で作るのは楽しいのでこれからも勉強します。