Carpe Diem

備忘録

CELでASTを外部に保存する

概要

CELのキーコンセプトでは

  • Control PlaneでCEL式をParse & Checkし、生成されたASTを保存
  • Data Planeで保存したASTを読み取り、インプット値を評価する

と説明されていました。

主に管理ツール等で前者のControl Planeを実装し、オペレーターに自由に評価式を入力してもらいます。
Control PlaneではCheckも行われるので評価式がおかしければその時点でvalidationしてくれます。
そしてData Planeでは起動時にASTを読み込んで入力値を評価できるようにします。

今回は具体的に外部に保存する手順を説明します。

環境

  • go v1.22.1
  • cel-go v0.20.1

方法

Control Plane、Data Planeそれぞれで説明します。

Control Plane

ref: https://codelabs.developers.google.com/codelabs/cel-go#1

cel.AstToCheckedExprを使ってASTをprotocol buffersに変換します。
そしてprotobufをシリアライズして外部に保存します。

package main

import (
    "log"
    "os"

    "github.com/google/cel-go/cel"
    "google.golang.org/protobuf/proto"
)

func main() {
    env, err := cel.NewEnv(
        cel.Variable("name", cel.StringType),
    )
    if err != nil {
        log.Fatalf("Failed to create CEL environment: %v", err)
    }

    ast, iss := env.Compile(`"Hello, " + name + "!"`)
    if iss.Err() != nil {
        log.Fatalf("Failed to compile expression: %v", iss.Err())
    }
    expr, err := cel.AstToCheckedExpr(ast)
    if err != nil {
        log.Fatalf("Failed to convert an Ast to an protobuf: %v", err)
    }

    // Serialize the AST to Protocol Buffers binary format
    astBytes, err := proto.Marshal(expr)
    if err != nil {
        log.Fatalf("Failed to serialize AST: %v", err)
    }

    // Save the serialized AST to a file
    if err := os.WriteFile("ast.pb", astBytes, 0644); err != nil {
        log.Fatalf("Failed to write AST to file: %v", err)
    }
}

今回は簡単のためローカルファイルとして保存しますが、実際の運用ではGCSやデータベースに保存するのが良いでしょう。

動作確認

$ go run main.go
$ ls
ast.pb  go.mod  go.sum  main.go

ast.pbが生成されました。

Data Plane

ローカルにあるast.pbを読み込みデシリアライズし、cel.CheckedExprToAstで*cel.Astにします。

package main

import (
    "log"
    "os"

    "github.com/google/cel-go/cel"
    exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
    "google.golang.org/protobuf/proto"
)

func main() {
    env, err := cel.NewEnv(
        cel.Variable("name", cel.StringType),
    )
    if err != nil {
        log.Fatalf("Failed to create CEL environment: %v", err)
    }

    // Read the serialized AST from the file
    astBytes, err := os.ReadFile("./gen/ast.pb")
    if err != nil {
        log.Fatalf("Failed to read AST from file: %v", err)
    }

    // Deserialize the AST from Protocol Buffers binary format
    var astPb exprpb.CheckedExpr
    if err := proto.Unmarshal(astBytes, &astPb); err != nil {
        log.Fatalf("Failed to deserialize AST: %v", err)
    }

    // Recover the AST structure
    ast := cel.CheckedExprToAst(&astPb)

    // Create a Program from the AST
    prg, err := env.Program(ast, cel.EvalOptions(cel.OptTrackState, cel.OptExhaustiveEval))
    if err != nil {
        log.Fatalf("Failed to create program: %v", err)
    }

    // Evaluate the Program with a given variable
    out, _, err := prg.Eval(map[string]interface{}{
        "name": "World",
    })
    if err != nil {
        log.Fatalf("Evaluation failed: %v", err)
    }

    log.Printf("Result: %v\n", out)
}

動作確認

$ go run main.go
2024/03/31 15:04:48 Result: Hello, World!

期待通り評価式に入力値が適用されたアウトプットが表示されました。

その他

サンプルコード

今回のサンプルコードはこちらです。

github.com

まとめ

CELのキーコンセプトに合わせた運用方法を紹介しました。

参考