✏️

Kubernetes のバージョンアップに Conftest を使用した

2020/12/21に公開

本記事は ZOZOテクノロジーズ #3 Advent Calendar 2020 21日目の記事です。

  1. ZOZOテクノロジーズ #1 Advent Calendar 2020
  2. ZOZOテクノロジーズ #2 Advent Calendar 2020
  3. ZOZOテクノロジーズ #3 Advent Calendar 2020
  4. ZOZOテクノロジーズ #4 Advent Calendar 2020

現在、私が所属しているチームでは Kubernetes のマネージドサービスに Amazon EKS を利用しています。
EKS は過去4世代の Kubernetes のマイナーバージョンをサポートしていますが、約3ヶ月おきに新しいマイナーバージョンがリリースされるので、最低でも1年に1度バージョンアップ作業を行うことになります。

Amazon EKS Kubernetes バージョン - Amazon EKS

その際に必要なのが、 Kubernetes のマニフェストファイル内から非推奨または廃止となる API を探し修正する作業です。
最近行った弊社のとあるプロダクトの EKS バージョンアップでは、その作業に Conftest を使用しました。

Conftestとは

Open Policy AgentRego という言語で書かれたポリシーを基に Kubernetes に限らず様々なツールの設定ファイルが、そのポリシーに沿っているかテストを行うためのツールです。
ポリシーは Amazon S3 などへpush、pullして異なるプロジェクト同士で利用可能です。
現在 Conftest は入力として下記のファイルをサポートしています。

  • YAML
  • JSON
  • INI
  • TOML
  • HOCON
  • HCL
  • HCL 2
  • CUE
  • Dockerfile
  • EDN
  • VCL
  • XML
  • Jsonnet

また、出力として下記のタイプをサポートしています。

  • Plaintext
  • JSON
  • TAP
  • Table
  • JUnit

Open Policy Agent(OPA)とは

汎用的なポリシーエンジンで CNCF のプロジェクトの一つです。
API を介してアプリケーションから送られてきたクエリ(JSONなどの構造化データ)とポリシーを評価して結果を返します。これにより各アプリケーションにポリシーの管理または判定処理を実装する手間を省くことができます。
また、ポリシーを定義するための Rego という言語を用いて、 Policy as Code が実現できます。

類似ツール

Conftest を導入する上で類似ツールの調査も行いました。

  • kubeval
  • kubetest
    • 導入にいたらなかった理由: レポジトリがアーカイブ済み。 Conftest に移行したとのこと。
  • kube-lint
  • open-policy-agent/gatekeeper
    • 導入にいたらなかった理由: 自分の調べた範囲ではどうやらクラスタに apply する段階でチェックが行われるようだった。今回はそれ以前のタイミングでチェックしたかった。
  • copper
    • 導入にいたらなかった理由: Copper DSL という独自のDSLを書く必要があり作業コストがかかりそうだと判断した。

ポリシーファイル

少々古くて申し訳ないのですが、下記に v1.15 以前のマニフェストファイルに対して v1.16 での非推奨 API を調査したときのポリシーを掲載します。
こちらの記事 で引用されていた ポリシー を利用させていただいたのですが、 Kubernetes オフィシャルの情報 と改めて過不足がないか確認した後にメッセージを日本語に訳しました。
このファイルを <任意の名前>.rego としてプロジェクトディレクトなどに保存します。

package main

deny[msg] {
  input.apiVersion == "v1"
  input.kind == "List"
  obj := input.items[_]
  msg := _deny with input as obj
}

deny[msg] {
  input.apiVersion != "v1"
  input.kind != "List"
  msg := _deny
}

warn[msg] {
  input.apiVersion == "v1"
  input.kind == "List"
  obj := input.items[_]
  msg := _warn with input as obj
}

warn[msg] {
  input.apiVersion != "v1"
  input.kind != "List"
  msg := _warn
}

# Based on https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.16.md

# All resources under apps/v1beta1 and apps/v1beta2 - use apps/v1 instead
_deny = msg {
  apis := ["apps/v1beta1", "apps/v1beta2"]
  input.apiVersion == apis[_]
  msg := sprintf("%s/%s: API %s はデフォルトで提供されなくなりました。代わりに apps/v1 を使用してください。", [input.kind, input.metadata.name, input.apiVersion])
}

# daemonsets, deployments, replicasets resources under extensions/v1beta1 - use apps/v1 instead
_deny = msg {
  resources := ["DaemonSet", "Deployment", "ReplicaSet"]
  input.apiVersion == "extensions/v1beta1"
  input.kind == resources[_]
  msg := sprintf("%s/%s: %s のAPI extensions/v1beta1 はデフォルトで提供されなくなりました。代わりに apps/v1 を使用してください。", [input.kind, input.metadata.name, input.kind])
}

# networkpolicies resources under extensions/v1beta1 - use networking.k8s.io/v1 instead
_deny = msg {
  input.apiVersion == "extensions/v1beta1"
  input.kind == "NetworkPolicy"
  msg := sprintf("%s/%s: NetworkPolicyのAPI extensions/v1beta1 はデフォルトで提供されなくなりました。代わりに networking.k8s.io/v1 を使用してください。", [input.kind, input.metadata.name])
}

# podsecuritypolicies resources under extensionsDeployment /v1beta1 - use policy/v1beta1 instead
_deny = msg {
  input.apiVersion == "extensions/v1beta1"
  input.kind == "PodSecurityPolicy"
  msg := sprintf("%s/%s: PodSecurityPolicyのAPI extensions/v1beta1 はデフォルトで提供されなくなりました。代わりに policy/v1beta1 を使用してください。", [input.kind, input.metadata.name])
}

# Ingress resources will no longer be served from extensions/v1beta1 in v1.20. Migrate use to the networking.k8s.io/v1beta1 API, available since v1.14.
_warn = msg {
  input.apiVersion == "extensions/v1beta1"
  input.kind == "Ingress"
  msg := sprintf("%s/%s: Ingress用のAPI extensions/v1beta1 は非推奨です。代わりに networking.k8s.io/v1beta1 を使用してください。", [input.kind, input.metadata.name])
}

# PriorityClass resources will no longer be served from scheduling.k8s.io/v1beta1 and scheduling.k8s.io/v1alpha1 in v1.17.
_warn = msg {
  apis := ["scheduling.k8s.io/v1beta1", "scheduling.k8s.io/v1alpha1"]
  input.apiVersion == apis[_]
  input.kind == "PriorityClass"
  msg := sprintf("%s/%s: PriorityClassのAPI %s は非推奨です。代わりに scheduling.k8s.io/v1 を使用してください。", [input.kind, input.metadata.name, input.apiVersion])
}

# The apiextensions.k8s.io/v1beta1 version of CustomResourceDefinition is deprecated and will no longer be served in v1.19. Use apiextensions.k8s.io/v1 instead.
_warn = msg {
  input.apiVersion == "apiextensions.k8s.io/v1beta1"
  input.kind == "CustomResourceDefinition"
  msg := sprintf("%s/%s: CustomResourceDefinitionのAPI apiextensions.k8s.io/v1beta1 は非推奨です。代わりに apiextensions.k8s.io/v1 を使用してください。", [input.kind, input.metadata.name])
}

# The admissionregistration.k8s.io/v1beta1 versions of MutatingWebhookConfiguration and ValidatingWebhookConfiguration are deprecated and will no longer be served in v1.19. Use admissionregistration.k8s.io/v1 instead.
_warn = msg {
  kinds := ["MutatingWebhookConfiguration", "ValidatingWebhookConfiguration"]
  input.apiVersion == "admissionregistration.k8s.io/v1beta1"
  input.kind == kinds[_]
  msg := sprintf("%s/%s: %s のAPI admissionregistration.k8s.io/v1beta1 は非推奨です。代わりに admissionregistration.k8s.io/v1 を使用してください。", [input.kind, input.metadata.name, input.kind])
}

# Based on https://github.com/jetstack/cert-manager/releases/tag/v0.11.0

_deny = msg {
  kinds := ["Certificate", "Issuer", "ClusterIssuer", "CertificateRequest"]
  input.apiVersion == "certmanager.k8s.io/v1alpha1"
  input.kind == kinds[_]
  msg := sprintf("%s/%s: %s のAPI certmanager.k8s.io/v1alpha1 は廃止されました。代わりに cert-manager.io/v1alpha2 を使用してください。", [input.kind, input.metadata.name, input.kind])
}

_deny = msg {
  kinds := ["Order", "Challenge"]
  input.apiVersion == "certmanager.k8s.io/v1alpha1"
  input.kind == kinds[_]
  msg := sprintf("%s/%s: %s のAPI certmanager.k8s.io/v1alpha1 は廃止されました。代わりに acme.cert-manager.io/v1alpha2 を使用してください。", [input.kind, input.metadata.name, input.kind])
}

Conftest のインストールと実行

以下のコマンドで Conftest を各環境にインストールします。

Linux

$ wget https://github.com/open-policy-agent/conftest/releases/download/v0.21.0/conftest_0.21.0_Linux_x86_64.tar.gz
$ tar xzf conftest_0.21.0_Linux_x86_64.tar.gz
$ sudo mv conftest /usr/local/bin

Homebrew

brew tap instrumenta/instrumenta
brew install conftest

Scoop

scoop bucket add instrumenta https://github.com/instrumenta/scoop-instrumenta
scoop install conftest

Docker

$ docker run --rm -v $(pwd):/project openpolicyagent/conftest test deployment.yaml

実行

基本の使い方としては、先ほど用意したポリシーファイルとテスト対象のファイルを引数に指定してコマンドを実行します。

conftest test --policy <ポリシーファイル> <テスト対象のマニフェストファイル>

API に非推奨のものがある場合は次のように出力されます。

conftest test --policy <ポリシーファイル> <テスト対象のマニフェストファイル>
WARN - <ファイル名> - CustomResourceDefinition/<リソース名>: CustomResourceDefinitionのAPI apiextensions.k8s.io/v1beta1 は非推奨です。代わりに apiextensions.k8s.io/v1 を使用してください。

62 tests, 61 passed, 1 warnings, 0 failures

Kustomize 環境の場合

Kustomize を使用している場合は一度マニフェストファイルをビルドしてから Conftest を実行する必要があります。

kubectl kustomize <kustomization.yaml> | conftest test --policy <ポリシーファイル> -

これだと複数ファイルをチェックするのが大変なので、例えば、 /kustomize/overlays 下にエントリーポイントとなる kustomization.yaml があったとして、 /policy 下にポリシーファイルを設置し次のようなシェルスクリプトを適当な名前(今回は manifests_check.sh )を付けて作成します。

#!/usr/bin/env bash

CURRENT_DIR=$(cd $(dirname $0); pwd)

if [ $# = 0 ]; then
  printf "ERROR: kustomization.yaml までのファイルパスもしくは -a を引数にしてください\n"
  exit 1
fi

if [ $1 = "-a" ]; then
  for CLUSTER_DIR in `find ${CURRENT_DIR}/kustomize/overlays -name kustomization.yaml`; do
    printf ${CLUSTER_DIR} | awk -F "/" '{print "NAME: " $(NF-2)"/"$(NF-1)"\n"}'
    kubectl kustomize $(dirname ${CLUSTER_DIR}) | conftest test --policy ${CURRENT_DIR}/policy -
    printf '%s\n' '--------------------------------------------------'
  done
else
  printf $1 | awk -F "/" '{print "NAME: " $(NF-2)"/"$(NF-1)"\n"}'
  kubectl kustomize $(dirname $1) | conftest test --policy ${CURRENT_DIR}/policy -
fi

以下のコマンドでテストできるようになります。

マニフェストファイルを1つだけテストする場合

./manifests_check.sh <エントリーポイントとなるkustomization.yaml>

マニフェストファイルを全てテストする場合

./manifests_check.sh -a

感想

今回は非推奨または廃止となる API のチェックのため Conftest を導入しました。
この場合の懸念点はEKSのバージョンアップに合わせたポリシーファイルのメンテナンスコストだと思います。他のプロジェクトと共有することで、それがどれくらい圧縮できるのか確認が必要そうです。
しかし導入自体は非常に簡単なので、今後は API チェック以外のマニフェストファイルのバリデーションにも Conftest を利用し CI/CD にも組み込んで、より安全なデプロイフローの構築に役立ていこうと思います。

Discussion