KubernetesにおけるCELの記述方法まとめ

Keigo Kurita

2024.6.12

はじめに

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 : e2etrue と評価される場合は e1 と評価され、 efalse と評価される場合は 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 オブジェクトには getSchemegetHostgetHostnamegetPortgetEscapedPath 、および 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について、同じくポリシー適応ツールであるkyvernoOPAと比較する記事を書く予定です。

参照

ブログ一覧へ戻る

お気軽にお問い合わせください

SREの設計・技術支援から、
SRE運用内で使用する
ツールの導入など、
SRE全般についてご支援しています。

資料請求・お問い合わせ