この投稿では、Kubernetes の CRD でフィールドのバリデーションを行う際に活用できる CEL format ライブラリについて紹介します。metadata.name に使える形式や、ラベルの値として許容される形式を検証したいとき、正規表現を自前で組む代わりにこのライブラリを使うと、車輪の再発明を避けられます。
この投稿で知れること
- CEL format ライブラリとは何か
- 利用可能なフォーマットの種類と用途
- CRD の
x-kubernetes-validationsでの具体的な使い方
CEL format ライブラリとは
Kubernetes 1.30 以降では、CRD の x-kubernetes-validations(CEL ルール)から利用できる format ライブラリ が提供されています。このライブラリを使うと、DNS ラベルや UUID、日付といった一般的な文字列フォーマットのバリデーションを、Kubernetes 本体と同じルールで簡単に行うことができます。
公式ドキュメントでは簡単な説明しかないため、「具体的にどんな文字列が OK で、どんな文字列が NG なのか」が分かりづらいところがあります。この投稿では、ソースコードの実装を踏まえながら、各フォーマットの仕様を整理していきます。
基本的な使い方
フォーマットの取得
フォーマットを取得するには2つの方法があります。
// 方法1: 直接フォーマット関数を呼び出す
format.dns1123Label()
// 方法2: format.named() で名前から取得
format.named("dns1123Label") // Optional<Format> を返す
バリデーションの実行
validate() メソッドを呼び出すと、検証結果が Optional 型で返ってきます。
format.dns1123Label().validate("my-name")
// → 有効な場合: optional.none
// → 無効な場合: optional([エラーメッセージのリスト])
CRD での実践的な使用例
バリデーションルールでは、hasValue() を使って結果を判定します。
// バリデーションルールでの使用例
rule: !format.dns1123Label().validate(self.metadata.name).hasValue()
// ^否定演算子を忘れずに!
// messageExpression でエラーメッセージを表示
messageExpression: format.dns1123Label().validate(self.metadata.name).orValue([]).join("\n")
validate() が optional.none を返す(つまり hasValue() が false になる)場合が「有効」であることに注意してください。
Kubebuilder でのコメントアノテーション
Kubebuilder を使って CRD を開発している場合は、Go の構造体にコメントアノテーションを記述することでバリデーションルールを定義できます。+kubebuilder:validation:XValidation マーカーを使用します。
type MyResourceSpec struct {
// Name は DNS-1123 ラベル形式である必要があります
// +kubebuilder:validation:XValidation:rule="!format.dns1123Label().validate(self).hasValue()",message="must be a valid DNS-1123 label"
Name string `json:"name"`
// Host は DNS-1123 サブドメイン形式である必要があります
// +kubebuilder:validation:XValidation:rule="!format.dns1123Subdomain().validate(self).hasValue()",message="must be a valid DNS-1123 subdomain"
Host string `json:"host"`
}
利用可能なフォーマット一覧
format ライブラリで利用可能なフォーマットは以下の13種類です。
dns1123Labeldns1123Subdomaindns1035Labeldns1123LabelPrefixdns1123SubdomainPrefixdns1035LabelPrefixqualifiedNamelabelValueuriuuidbytedatedatetime
以下では、よく使うフォーマットについて詳しく見ていきます。
DNS 関連フォーマット
DNS 関連のフォーマットは、Pod 名や Namespace 名、Service 名などのリソース名を検証する際に使用します。共通の特徴として、すべて ASCII の小文字(a-z)のみを前提としており、大文字やアンダースコアは使用できません。
dns1123Label
Kubernetes で最も一般的に使われるフォーマットです。RFC 1123 に基づいており、最大 63 文字まで許容されます。
使用可能な文字は、小文字英字(a-z)、数字(0-9)、ハイフン(-)です。先頭と末尾は英小文字または数字である必要があり、ドット(.)は使用できません。
!format.dns1123Label().validate(self.metadata.name).hasValue()
my-service や nginx、abc-123 といった値は有効ですが、My-Service(大文字)、-nginx(先頭がハイフン)、my.service(ドットを含む)といった値は無効になります。
dns1123Subdomain
ドメイン名のような形式で、ドットで区切られた複数のラベルを含むことができます。最大 253 文字まで許容されます。
!format.dns1123Subdomain().validate(self.spec.host).hasValue()
example.com や api.example.com といった値は有効です。
dns1035Label
RFC 1035 に基づくより厳格なラベル形式です。dns1123Label との違いは、先頭が必ず英小文字でなければならないという点です。数字で始まる値は許可されません。
!format.dns1035Label().validate(self.metadata.name).hasValue()
nginx や my-service1 は有効ですが、1service(先頭が数字)は無効になります。Pod や Service の名前など、リソースの種類によっては dns1035Label が要求されるケースがあります。
dns1123Label と dns1035Label の違い
これらは混同しやすいので、違いを整理しておきます。
dns1123Label は先頭に英小文字または数字を許容するため、123-abc のような値が有効です。一方、dns1035Label は先頭に英小文字のみを許容するため、123-abc は無効になります。
どちらを使うべきか
基本的には dns1123Label を使えば問題ありません。Kubernetes の多くのリソース(ConfigMap、Secret、Namespace など)は dns1123Label の形式を採用しています。
では、dns1035Label はどのような場面で使うべきでしょうか。主に以下のケースが該当します。
dns1035Label を使うべき代表的なケースは、そのフィールドの値が Service の metadata.name として使われる場合です。Service 名はクラスタ内 DNS でホスト名として解決されるほか、環境変数名のプレフィックスとしても使われるため、RFC 1035 に従って先頭が英字である必要があります。CRD のフィールドが最終的に Service 名になるような設計であれば、dns1035Label でバリデーションしておくと安全です。
Prefix 系フォーマット
dns1123LabelPrefix、dns1123SubdomainPrefix、dns1035LabelPrefix は、generateName のように「接頭辞 + 自動生成サフィックス」という形式で使われることを想定したフォーマットです。通常のフォーマットでは末尾のハイフンが許可されませんが、Prefix 系では末尾のハイフンが許容されます。
!format.dns1123LabelPrefix().validate(self.metadata.generateName).hasValue()
my-app- のような値がプレフィックスとして有効になります。
ラベル関連フォーマット
labelValue
Kubernetes のラベルの値として使用できる形式です。最大 63 文字で、空文字列も有効であるという点が特徴的です。
使用可能な文字は、英字(大文字・小文字)、数字、ハイフン(-)、アンダースコア(_)、ドット(.)です。空でない場合は、先頭と末尾が英数字である必要があります。
!format.labelValue().validate(self.metadata.labels['app']).hasValue()
空文字列、value、my.value_1、v1-2-3 といった値は有効です。-value(先頭がハイフン)や value-(末尾がハイフン)は無効になります。
qualifiedName
アノテーションやラベルのキー名で使用される形式です。オプションで DNS サブドメインのプレフィックスを持つことができ、[prefix/]name という構造になっています。
!format.qualifiedName().validate(key).hasValue()
my-name、My.Name_1(大文字も OK)、example.com/MyName といった値は有効です。/my-name(プレフィックス部分が空)や example.com/(名前部分が空)は無効になります。
その他のフォーマット
uri
Go の url.ParseRequestURI を使用して URI を検証します。スキーム(http:// や https:// など)が必須です。
!format.uri().validate(self.spec.callbackURL).hasValue()
uuid
UUID の形式を検証します。大文字・小文字は区別せず、ハイフンはあってもなくても有効です。
!format.uuid().validate(self.spec.id).hasValue()
date / datetime
date は YYYY-MM-DD 形式の日付を検証します。datetime は RFC 3339 形式の日時を検証しますが、タイムゾーン指定(Z や +09:00 など)が必須である点に注意が必要です。
!format.date().validate(self.spec.startDate).hasValue()
!format.datetime().validate(self.spec.expireAt).hasValue()
2021-01-01T00:00:00Z は有効ですが、2021-01-01T00:00:00(タイムゾーンなし)は無効になります。
format.named の使いどころ
format.named(name) は、文字列から動的にフォーマットを選びたい場合に便利です。
format.named(self.spec.formatName).
map(f, f.validate(self.spec.value)).
orValue(optional.of(["unknown format"])).
value()
サポートされていない名前の場合は optional.none になるため、orValue などでフォールバック処理を書くことができます。
所感
CRD のバリデーションを書くとき、正規表現を自前で組むのは意外と手間がかかりますし、Kubernetes 本体の実装と微妙にずれてしまうリスクもあります。CEL format ライブラリを使えば、Kubernetes 本体と同じルールを再利用する形になるため、挙動の一貫性を保つことができます。
特に dns1123Label と dns1035Label の違いや、labelValue が空文字列を許容するといった細かい仕様は、自分で実装すると見落としがちです。このライブラリを活用して、車輪の再発明を避けていきたいところです。
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローしてもらえると嬉しいです
→Twitter@suin