はじめに
Kubernetes 1.30でValidating Admission Policyの機能がGAするなど、開発中の新機能にCELが組み込まれるケースが増えています。今後Kubernetesで使われる機会が増えそうなCELについて、使い所や記述方法をまとめてみました。
CELとは?
CEL(Common Expression Language)の概要は以下の通りです。
- 検証ルール、ポリシールール、その他の制約や条件を宣言するために使用される。
- CEL式はKubernetes APIサーバーで直接評価されるため、Admission WebhookのようなKubernetesのコントロールプレーンの外で拡張する仕組みに代わる便利な選択肢となる。
- 構文は、C、C++、Java、JavaScript、Go の式に似ている。
- CEL式は、単一の値に評価される。 CEL 式は通常、Kubernetes リソースの文字列フィールドにインライン化されるワンライナー。
Kubernetesでの使い所
Kubernetesでは次のような場面でCELを使用します。
Validating Admission Policy
Validating Admission Policy とは Validating Admission Webhooksに代わる、宣言的でKubernetes のコア機能として利用できる代替手段です。
Validating Admission Webhookでは、Kubernetes APIサーバーからリクエストを受け取り検証するルールをコードで記載し、Webhookサーバーをデプロイする必要がありました。
一方、Validating Admission Policyは、CELで宣言したポリシーをKubernetes APIサーバーで検証します。
次のValidating Admission Policyの例では spec.validation[0].expression
にCEL式を指定しています。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.example.com"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: "object.spec.replicas <= 5"
Custom Resouce DefinitionsでのValidation rules
Custom Resouce DefinitionsでのValidation rules ではカスタムリソースのフィールドの値を検証します。
これまでもOpenAPIスキーマで検証することは出来ましたが、柔軟性は高くありませんでした。そのため、Validating Admission Webhookを自作するケースが多かったです。
こちらも、CELを使うことでKubernetes APIサーバーで検証できるようになりました。
次の例ではopenAPIV3Schema.properties.spec.x-kubernetes-validations.rule
にCEL式を指定しています。
...
openAPIV3Schema:
type: object
properties:
spec:
type: object
x-kubernetes-validations:
- rule: "self.minReplicas <= self.replicas"
message: "replicas should be greater than or equal to minReplicas."
- rule: "self.replicas <= self.maxReplicas"
message: "replicas should be smaller than or equal to maxReplicas."
properties:
...
minReplicas:
type: integer
replicas:
type: integer
maxReplicas:
type: integer
required:
- minReplicas
- replicas
- maxReplicas
記述方法
CEL式の例と実行結果を示しながら、KubernetesでのCELの記述方法を見ていきます。なお、サンプルコードでは以下のマニュフェストに対してCEL式を実行するものとします。
apiVersion: v1
kind: Pod
metadata:
name: example-pod
spec:
containers:
- name: example-container
image: nginx:latest
env:
- name: ENVIRONMENT
value: production
- name: CUSTOM_VARIABLE
valueFrom:
secretKeyRef:
name: example-secret
key: CUSTOM_VARIABLE
- name: CUSTOM_VARIABLE2
valueFrom:
secretKeyRef:
name: example-secret
key: CUSTOM_VARIABLE2
- name: ALPHANUMERIC_VARIABLE
value: 1 abc 2
- name: URL_VARIABLE
value: https://example.com:80
securityContext:
runAsNonRoot: true
resources:
requests:
memory: "64Mi"
limits:
memory: "128Mi"
また、紹介する記述方法はCEL Playgroundで試すことができます。
なお、詳細な仕様については省略している部分もあるので公式ドキュメントをご確認ください。
CELの基本
まずはCELの基本的な構文を紹介します。
Literals
さまざまな種類のリテラル (数値、ブール値、文字列、バイト、および null) は、それらが表す値として評価されます。
"Literal"
"Literal"
※上段がCEL式、下段が実行結果です。
Variables
変数は識別子と値の対応付け(束縛)です。未定義の変数はエラーと評価されます。
metadata.name
"example-pod"
List, Map
各式が評価され、いずれかの式がエラーになった場合、この式はエラーになります。そうでない場合は、サブ式の結果のリスト、マップが返されます。
[kind, metadata.name]
["Pod", "example-pod"]
{"kind": kind, "name": [metadata.name](<http://metadata.name/>)}
{"kind":"Pod","name":"example-pod"}
Macros
定義済みのマクロをサポートしています。マクロ呼び出しは、関数呼び出しと同じ構文を持ちますが、型チェックのルールや実行時の内部的な挙動が異なります。
has(e.f)
:フィールドが存在するかどうかをテストします。
has(spec.containers[0].securityContext.runAsNonRoot)
true
e.all(x,p)
:リストe
の全要素またはマップe
のキーに対して述語式p
が成り立つかどうかをテストします。all()
マクロは、要素ごとの述語式p
の結果をand(&&
)演算子で結合するので、もし述語が偽と評価されれば、マクロは偽と評価され、他の述語からのエラーは無視されます。
spec.containers[0].env.all(e, has(e.name))
true
e.exists(x, p)
:all()
マクロと似ていますが、述語の結果をor(||
) 演算子で結合します。
spec.containers[0].env.exists(e, has(e.valueFrom))
true
e.exists_one(x,p)
:exists()
マクロと似ていますが、ちょうど1つの要素の述語がtrue
と評価された場合のみtrue
と評価され、他はfalse
と評価されます
spec.containers[0].env.exists_one(e, has(e.valueFrom))
false
e.map(x, t)
: 各要素x
を式t
で指定された関数に渡すことにより、リストe
に変換します。いずれかの要素で評価エラーが発生すると、マクロはエラーを発生します。
[1, 2, 3].map(n, n * n)
[1, 4, 9]
{'one': 1, 'two': 2}.map(k, k)
['one', 'two']
e.map(x, p, t)
: 2 つの引数のマップと同じですが、値が変換される前に条件付きp
フィルターが使用されます。
[1,2,3].map(n, n % 2 == 1, n * n)
[1,9]
e.filter(x, p)
:e
のすべての要素x
のうち、述語式p
(変数x
を使用することができる)において真と評価されるもののサブリストを返します。どの要素も真と評価されない場合、結果は空リストになります。いずれかの要素で評価エラーが発生すると、マクロはエラーを発生させます。
[1,2,3].map(n, n % 2 == 1, n * n)
[1,9]
{'one': 1, 'two': 2}.filter(k, k == 'one')
['one']
Logical operators
条件演算子 e ? e1 : e2
。 e
が true
と評価される場合は e1
と評価され、 e
が false
と評価される場合は e2
と評価されます。
true ? "true" : "false"
"true"
ブーリアン演算子 &&
と ||
では、オペランドのいずれかが結果を一意に決定する場合( &&
では false
、 ||
では true
)、もう一方のオペランドは評価されても評価されなくてもよく、その評価によって実行時エラーが発生した場合は無視されます。
has(metadata.annotations) && has(metadata.name)
false
Functions
標準関数が用意されています。
ユーザーが意識しなくても、入力する型毎に内部の処理が変わります。
size(spec.containers)
1
spec.containers[0].env[0].value.matches("prod")
true
Kubernetes拡張
ここまでCELの基本を見てきましたが、Kubernetes環境ではライブラリが用意されています。
extended strings library
文字列操作関数です。
例:
指定された位置にある文字を返す。
metadata.name.charAt(4)
'p'
セパレータによって分割された文字列のリストを返す。
'hello hello hello'.split(' ')
['hello', 'hello', 'hello']
文字列の先頭と末尾の空白を削除した新しい文字列を返す。
' \\ttrim\\n '.trim()
'trim'
Kubernetes list library
リスト操作の為の関数です。
例:
リストがアルファベット順に保たれていることを確認する。
['a', 'b', 'c'].isSorted()
true
リストの合計を確認する。
[1, 2, 3].sum()
6
リストの最大値最小値を確認する。
[2, 1, 3].max()
3
[2, 1, 3].min()
1
Kubernetes regex library
標準ライブラリによって提供される matches
関数に加えて、regex libraryには find
および findAll
が提供され、より広範囲の正規表現操作が可能になります。
例:
文字列内の最初の数字を見つける。
spec.containers[0].env[3].value.find('[0-9]+')
1
文字列内の数値の合計が5未満であることを確認する。
spec.containers[0].env[3].value.findAll('[0-9]+').map(x, int(x)).sum() < 5
true
Kubernetes URL library
URL の処理をより簡単かつ安全にするために、次の関数が追加されています。
isURL(string)
:文字列が有効な URL かどうかをチェックする。url(string)
:文字列を URL に変換する。文字列が有効な URL でない場合はエラーが発生する。
url
関数を介して解析されると、結果の URL オブジェクトには getScheme
、 getHost
、 getHostname
、 getPort
、 getEscapedPath
、および getQuery
アクセサーが含まれます。
例:
URL のホスト部分を取得。
url(spec.containers[0].env[4].value).getHost()
"example.com:80"
Kubernetes authorizer library
リクエストのプリンシパル (認証されたユーザー) の承認チェックを実行できます。
例:
環境に依存するため、出力結果は省略。
default
名前空間でのpodの作成を許可されている場合は true
を返す。
authorizer.group('').resource('pods').namespace('default').check('create').allowed()
/healthz
API パスへの HTTP GET リクエストを行う権限を持っているかどうかを確認。
authorizer.path('/healthz').check('get').allowed()
サービス アカウントにdeploymentsを削除する権限があるかどうかを確認。
authorizer.serviceAccount('default', 'myserviceaccount').resource('deployments').check('delete').allowed()
Kubernetes quantity library
Kubernetes 1.29以降で使用できます。
数量文字列の操作のサポートです。
isQuantity(string)
:有効な数量であるかどうかを確認。quantity(string)
:文字列を数量に変換する。文字列が有効な数量でない場合は、エラーが発生する。
quantity
を介して解析されると、結果として得られる数量オブジェクトには、メンバー関数のライブラリが含まれます。
例:
整数への変換でエラーが発生するかどうかをテストする。
quantity(spec.containers[0].resources.requests.memory).isInteger()
true
整数への正確な変換。
quantity(spec.containers[0].resources.limits.memory).asInteger()
50000
quantity(spec.containers[0].resources.requests.memory).asInteger() < quantity(spec.containers[0].resources.limits.memory).asInteger()
true
Type checking
Kubernetes APIのフィールドの中には、部分的に型チェックされたCEL式が含まれているものがあります。部分的に型チェックされた式とは、変数の一部が静的に型付けされているが、他の変数は動的に型付けされている式のことです。
例えばValidatingAdmissionPoliciesのCEL式では、 object
, request
, authorizer
などを使うことが出来ます。 request
変数は型付けされていますが、 object
変数は動的に型付けされています。その結果、 request.namex
を含む式は、namex
フィールドが定義されていないため、型チェックに失敗します。しかし、 object.namex
は、 namex
フィールドが object
が参照するリソースの種類に対して定義されていなくても、 object
が動的に型付けされているため、型チェックを通過します。
has()
マクロを使うことで、動的に型指定された変数のフィールドがアクセス可能かどうかをチェックできます。
例:
上記のことが起こり得るCEL式を含んだValidatingAdmissionPolicyのリソースがある。 spec.validations
のCEL式ではdeploymentの反映時のポリシーを記述している。
cat <<EOF | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.uesyn.dev"
spec:
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: request.namex.startsWith("foo")
- expression: object.metadata.namex.startsWith("foo")
EOF
このリソースを kubectl apply
すると、 request.namex.startsWith("foo")
はエラーとなるが、 object.metadata.namex.startsWith("foo")
はエラーとならない。
The ValidatingAdmissionPolicy "demo-policy.uesyn.dev" is invalid: spec.validations[0].expression: Invalid value: "request.namex.startsWith(\"foo\")": compilation failed: ERROR: <input>:1:8: undefined field 'namex'
| request.namex.startsWith("foo")
| .......^
一方で、 request.namex.startsWith("foo")
のCEL式を削除し
cat <<EOF | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.uesyn.dev"
spec:
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: object.metadata.namex.startsWith("foo")
EOF
kubectl apply
するとエラーが発生しない。
validatingadmissionpolicy.admissionregistration.k8s.io/demo-policy.uesyn.dev created
この場合、deploymentを kubeclt apply
すると、
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
EOF
namex
に対するエラーが発生する
expression 'object.metadata.namex.startsWith("foo")' resulted in error: no such key: namex
今回のように型チェックを通過する可能性がある場合は spec.validations
の式を
has(object.metadata.namex) ? object.metadata.namex.startsWith("foo") : request.name.startsWith("foo")
のように has()
マクロを使うことで、deploymentの動的に型指定される変数のフィールドがアクセス可能かどうかをチェックできる。
CEL optional types
Kubernetes 1.29以降で使用できます。
部分的に型チェックされた式に対して、代替値をサポートします。
Optional.Of
与えられた型のoptional typeを作成します。
optional.of(10)
Optional.None
空のoptional typeの値を作成します。
optional.none()
Field Selection
obj.?field
と表記されます。フィールドが設定されていれば optional.of(obj.field)
を返し、そうでなければ optional.none()
を返します。次の式は同等です。
obj.?field.subfield
obj.?field.?subfield
Indexing
list[?0]
map[?key]
フィールド選択と同様に、マップおよびリストのインデックス式で利用できます。
例:
以下のようなCEL式を含んだValidatingAdmissionPolicyのリソースを kubectl apply
する。
spec.validations
のCEL式ではdeploymentの反映時のポリシーを記述している。
cat <<EOF | kubectl apply -f -
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "demo-policy.uesyn.dev"
spec:
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: object.?metadata.?namex == optional.of("nginx-deployment")
EOF
以下のような metadata.namex
フィールドを持たないdeploymentを kubeclt apply
すると、
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
EOF
エラーが発生する。
failed expression: object.?metadata.?namex == optional.of("nginx-deployment")
no such key: namex
のようなエラーではなく、比較演算の結果が false
だったことがわかる。
まとめ
以上がkubernetesでのCELの使い所や記述方法となります。
変数の型付けのタイミングがややこしかったり、なかなか慣れないところはあると思いますが、これまでWebhookしていた処理をAPIサーバーが行ってくれるのは嬉しい事だと思います。
KubernetesでのCEL利用が進んで行きそうなので、この記事を読んで役に立てていただけたら幸いです。
今後は、この記事でも説明したValidating Admission Policyについて、同じくポリシー適応ツールであるkyvernoやOPAと比較する記事を書く予定です。