💭

KubeconformをGitLab CIに組み込んで、k8sのマニフェストがAPIの仕様に沿うか検査する

2023/06/12に公開

はじめに

k8sマニフェストを普段管理していないメンバーがマニフェストのファイルを変更する場面があります。
その際のレビューを出来るだけ自動化したくkubeconformを導入しました。

Kubeconform

マニフェストがAPIの仕様に沿うか検査してくれます。
https://github.com/yannh/kubeconform
自分でスキーマを用意すればIstio、Argo Rollouts、Argo Workflowsのような外部のAPIも検査できます。

スキーマの生成

スキーマの生成はpythonのスクリプトが用意されているので、これをCRDを引数で渡し実行します。

下記はGitLab CI上でスキーマを生成してartifactsに保存しています。
バージョンを上げた時にだけ再実行すれば良いので、manualにしています。
環境変数FILENAME_FORMATでファイル名の形式を決めることができ、実行の際はこの形式を指定します。

generate_k8s_json_schema:
  image: python:3.11-alpine
  variables:
    ISTIO_VERSION: 1.17.0
  before_script:
    - pip install pyyaml
    - wget -q https://github.com/mikefarah/yq/releases/download/v4.32.2/yq_linux_amd64 -O yq
    - chmod +x yq
    - wget -q https://raw.githubusercontent.com/yannh/kubeconform/master/scripts/openapi2jsonschema.py
    - chmod +x openapi2jsonschema.py
    - export FILENAME_FORMAT='{kind}_{version}'
  script:
    - wget -q https://raw.githubusercontent.com/istio/istio/${ISTIO_VERSION}/manifests/charts/base/crds/crd-all.gen.yaml
    - ./yq e 'select(.spec.names.kind == "VirtualService")' crd-all.gen.yaml > virtual_service.yaml
    - ./yq e 'select(.spec.names.kind == "Gateway")' crd-all.gen.yaml > gateway.yaml
    - mkdir schemas
    - cd schemas
    - ../openapi2jsonschema.py https://raw.githubusercontent.com/argoproj/argo-rollouts/master/manifests/crds/rollout-crd.yaml
    - ../openapi2jsonschema.py https://raw.githubusercontent.com/argoproj/argo-workflows/master/manifests/base/crds/minimal/argoproj.io_workflowtemplates.yaml
    - ../openapi2jsonschema.py ../virtual_service.yaml
    - ../openapi2jsonschema.py ../gateway.yaml
  artifacts:
    paths:
      - schemas
  when: manual

Kubeconformの実行

前提としてマニフェストはhelm及びhelmfileで管理されていて、以下のディレクトリ構成になっています。

.
├── kubeconform.sh
├── charts
│   ├── apps
│   │   ├── Chart.yaml
│   │   ├── templates
│   │   │   ├── _helpers.tpl
│   │   │   ├── svc.yaml
│   │   │   ├── gateway.yaml
│   │   │   ├── ns.yaml
│   │   │   ├── rollout.yaml
│   │   │   ├── sa.yaml
│   │   │   └── virtual_service.yaml
│   │   └── values.yaml
│   └── argo-workflows
│       ├── Chart.yaml
│       └── templates
│           ├── _helpers.tpl
│           ├── rbac.yaml
│           ├── sa.yaml
│           └── workflow_templates.yaml
└── values
    ├── argo-workflows
    │   └── sample
    │        ├── helmfile.yaml
    │        ├── dev
    │        │   └── values.yaml
    │        ├── prd
    │        │   └── values.yaml
    │        └── values.yaml
    └── apps
        └── sample
            ├── helmfile.yaml
            ├── dev
            │   └── values.yaml
            ├── prd
            │   └── values.yaml
            └── values.yaml

GitLab CI

kubeconform:
  image: ghcr.io/yannh/kubeconform:latest-alpine
  variables:
    K8S_VERSION: "1.25.0"
  only:
    refs:
      - merge_requests
    changes:
      - charts/**/*
      - values/**/*
  script:
    - ./kubeconform.sh

変更されたファイルに関連するreleaseだけ検査したいので、スクリプトを書いて選別しています。

#!/bin/ash

set -euCo pipefail

# artifactsに保存したスキーマを取得
function fetch_third_party_schemas() {
  wget -q --header PRIVATE-TOKEN:\ ${TOKEN} "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=generate_k8s_json_schema" -O schemas.zip
  unzip schemas.zip
}

# helm, helmfile, yqをインストール
function install_tools() {
  apk add --no-cache unzip jq

  wget -q https://get.helm.sh/helm-v3.11.2-linux-amd64.tar.gz -O - | tar xz
  mv linux-amd64/helm /usr/bin

  wget -q https://github.com/helmfile/helmfile/releases/download/v0.152.0/helmfile_0.152.0_linux_amd64.tar.gz -O - | tar xz
  mv helmfile /usr/bin

  wget -q https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64.tar.gz -O - | tar xz
  mv yq_linux_amd64 /usr/bin/yq
}

# 向き先のブランチとの差分を見て変更されたファイルの一覧を取得
function fetch_changed_files() {
  api_url="https://gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/repository/compare?from=${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}&to=${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}"
  wget -q --header PRIVATE-TOKEN:\ ${TOKEN} ${api_url} -O - \
    | jq -r '.diffs[].new_path'
}

# kubeconformにはhelmfile -e env -f path/to/helmfile.yaml templateの結果を渡すので、変更されたファイルの一覧から-eと-fの引数を作ります
# valuesディレクトリ配下が変更されたとき
#   helmfile.yamlと共通のvalues.yamlが変更された時は該当のhelmfile.yamlと全envを出力
#   env(prd,dev)ディレクトリ配下が変更されたときはそのvalues.yamlを使っているhelmfile.yamlと該当のenvを出力
# chartsディレクトリ配下が変更されたときは該当のchartが使われているhelmfile.yamlと全envを出力
function generate_helmfile_args() {
  while read -r changed_file; do
    if $(echo ${changed_file} | grep -q "^values"); then
      # busyboxのdirnameは複数ファイルに対応していないので、
      # 渋々rev | cut -d/ -f2- | revでdirnameする
      for root in $(find -name helmfile.yaml | rev | cut -d/ -f2- | rev | cut -d/ -f2-); do
        echo ${changed_file} | grep -q "^${root}" || continue
        local base=$(echo ${changed_file} | sed "s@${root}/@@")
        if $(echo ${base} | grep -q /); then
          local env=$(echo ${base} | cut -d/ -f1)
          echo ${root}/helmfile.yaml ${env}
        else
          yq '.environments' ${root}/helmfile.yaml | tr -d : | xargs -i echo ${root}/helmfile.yaml {}
        fi
        break
      done
    elif $(echo ${changed_file} | grep -q "^charts"); then
      for helmfile in $(grep $(echo ${changed_file} | cut -d/ -f1,2) $(find -name helmfile.yaml) | cut -d: -f1 | cut -d/ -f 2-); do
        yq '.environments' ${helmfile} | tr -d : | xargs -i echo ${helmfile} {}
      done
    fi
  done
}

install_tools
fetch_third_party_schemas

fetch_changed_files \
  | generate_helmfile_args \
  | sort \
  | uniq \
  | sed 's@\([^ ]*\) \(.*\)@helmfile -q -f \1 -e \2 template@' \
  | xargs -i ash -c "eval {}" \
  | /kubeconform -schema-location default \
    -schema-location 'schemas/{{ .ResourceKind }}_{{ .ResourceAPIVersion }}.json' \ # FILENAME_FORMATで指定した形式
    -summary \
    -kubernetes-version ${K8S_VERSION}
Summary: 9 resources found parsing stdin - Valid: 9, Invalid: 0, Errors: 0, Skipped: 0

もっと高度な検査しようとするとkicsやtirvyを導入しないといけないですが、ローカルにhelmやhelmfileがない人で環境変数くらいしか変更しない場合などの簡易レビューにはうってつけでした

Discussion