💭
KubeconformをGitLab CIに組み込んで、k8sのマニフェストがAPIの仕様に沿うか検査する
はじめに
k8sマニフェストを普段管理していないメンバーがマニフェストのファイルを変更する場面があります。
その際のレビューを出来るだけ自動化したくkubeconformを導入しました。
Kubeconform
マニフェストがAPIの仕様に沿うか検査してくれます。
自分でスキーマを用意すればIstio、Argo Rollouts、Argo Workflowsのような外部のAPIも検査できます。スキーマの生成
スキーマの生成はpythonのスクリプトが用意されているので、これをCRDを引数で渡し実行します。
- スクリプト
- IstioのCRD
- Argo RolloutsのCRD
- Argo Workflows Workflow Templatesの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