CNDTの懇親会の二次会とセッションでも触れられていたCUE言語ですが名前しか知らないが1日に2回も聞くってことはどこかで役に立つかも?ということでメモを置いておきます。
CUE言語とは?
CUE is an open source language, with a rich set of APIs and tooling, for defining, generating, and validating all kinds of data: configuration, APIs, database schemas, code, … you name it.
公式より引用すると上記のように書かれており設定ファイル、データ検証、およびデータ定義タスクに適したオープンソースの言語という理解をしました。JSONのスーパーセットとして設計されており、YAMLやTOMLなど他のデータ記述言語よりも表現力が高く、精密なデータ構造の定義と検証が可能です。CUEは、コード生成、データ検証、アプリケーションの設定、およびAPIスキーマの記述に利用されているらしいです。実装自体はGoで行われているようです。
これも公式のドキュメントにあるのですが特徴は以下の4つがあります。
- 強力な型システム:静的な型付けを提供し、複雑なデータ構造を正確に定義できる。
- 制約に基づく検証:データ構造に対する制約を定義することで、データの整合性を保証できる
- コードの再利用と組み合わせ:共通のデータ構造や検証ルールをモジュール化し、再利用可能なコンポーネントとして組み合わせることができる
- 簡潔な構文:JSONに似た構文を持ちながら、より簡潔で読みやすい形式を提供している
導入事例だとメルカリさんのKubernetes Configuration Management with CUEあたりが有名でしょうか(私はこれで知った)、他にはDaggerというCI/CDツールなどで使われているらしいです。
インストール
asdfがあったので私はそちらを使いますがGoで実装されているのでwgetなりgo installなりでよさそうです。
$ asdf plugin-add cue https://github.com/asdf-community/asdf-cue.git
$ asdf install cue latest
$ cue version
cue version v0.7.0
go version go1.21.5
-buildmode exe
-compiler gc
-trimpath true
CGO_ENABLED 0
GOARCH arm64
GOOS darwin
completionも実装されているので入れておきます。
$ cue completion zsh > ~/.zsh/site-functions/_cue
サンプルコード
// 人物のデータ構造を定義する
#Person: {
name: string
age: int & >= 0 & <= 120
email: string | *""
}
// 実際の人物データ
person: #Person & {
name: "John Doe"
age: 30
email: "johndoe@example.com"
}
この例では、#Personという名前のデータ構造(またはスキーマ)を定義しています。この構造にはname(文字列型)、age(整数で0以上120以下)、email(文字列型、デフォルトは空文字列)というフィールドがあります。その後、このスキーマを使用して具体的な人物データを定義しています。
$ cue eval test.cue
と実行するとエラーもなく終了します。ここでageを121とかにすると
$ cue eval test.cue
person.age: invalid value 121 (out of bound <=120):
./test.cue:4:24
./test.cue:11:11
expressionsを使うことでif文なんかもかけます。結果をexportで見ることができます。
#user: {
name: string
age: int
if age <= 50 {
isOld: false
}
if age > 50 {
isOld: true
}
}
hoge: {
name: "fuga"
isOld: false
age: 20
}
この場合にexportをすると以下のようになります。
{ "hoge": { "name": "fuga", "isOld": false, "age": 20 } }
CUEの定義からGoのコードを生成する
CUEのAPIを使って統合して使うことができます。構造体を以下のように定義します。その状態でcue get goを打つことでスキーマファイルが生成されます。以下の例ではageが120以下にしているので121などを構造体に入れようとするとvalidatePerson()でValidation failed: #Person.Age: invalid value 121 (out of bound <120)というエラーが返るようになります。
package main import ( "fmt" "log" "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/load" ) // Person はGoの構造体で、CUEスキーマに準拠する必要があります。 type Person struct { Name string Age int `cue:"<120"` } func main() { // CUEコンテキストを作成 ctx := cuecontext.New() // CUEファイルをロード instances := load.Instances([]string{"./cue.mod/gen/main/"}, nil) // インスタンスをビルド inst := ctx.BuildInstance(instances[0]) if inst.Err() != nil { log.Fatalf("CUE build error: %v", inst.Err()) } // CUEスキーマに基づいてGoの構造体を検証 person := Person{"John Doe", 121} err := validatePerson(ctx, inst, person) if err != nil { log.Fatalf("Validation failed: %v", err) } fmt.Println("Validation succeeded:", person) } // validatePerson は、GoのPerson構造体をCUEスキーマに対して検証します。 func validatePerson(ctx *cue.Context, value cue.Value, p Person) error { personValue := ctx.Encode(p) // CUEスキーマを取得 personCue := value.LookupDef("#Person") if personCue.Err() != nil { return personCue.Err() } // 検証 unified := personCue.Unify(personValue) return unified.Validate() }
PlayGround
ブラウザ上で気軽にCUEの動作を確かめられるプレイグラウンドもあったのでそちらで試してみるのもよさそうです。
パッと使えそうなもの
メルカリさんの事例にあるようなk8sのyamlの生成なんかはシュッと使えるのかなぁと思った。サービスとかポートのスキーマだけを用意しているが本番だったらHPAとかSAとかPVCとかそれぞれを最小の記述でプロダクションレディにするとかそういう感じのやつ。今はOPAを使ってapplyするタイミングでバリデートされているがそもそも最低限の設定は自動生成させてアプリケーションエンジニアそれに沿ってyamlを書いていくだけで良いという流れ。便利そうに思えるが例えば何か設定を足したい場合なんかはCUEの柔軟性があるが故に書き方が色々あってレビューが大変そうになる未来が見える。
// Serviceの定義
#Service: {
apiVersion: "v1"
kind: "Service"
metadata: {
name: string
namespace?: string
labels?: [string]: string
}
spec: {
type: "ClusterIP" | "NodePort" | "LoadBalancer" | "ExternalName"
selector?: [string]: string
ports: [...#Port]
}
}
// ポートの定義
#Port: {
name?: string
protocol: "TCP" | "UDP" | *"TCP"
port: int
targetPort?: int
nodePort?: int
}
// 実際のServiceインスタンス
exampleService: #Service & {
metadata: {
name: "my-service"
labels: {
"app": "my-app"
}
}
spec: {
type: "ClusterIP"
selector: {
"app": "my-app"
}
ports: [{
name: "http"
port: 80
}, {
name: "https"
port: 443
}]
}
}
これでexportするとyamlが生成されるというやつ。helmよりも柔軟に色々できるがどっちが良いとかは正直決められないかなぁ。(helmくらい制限がある方がレビューしやすいと思ってしまったが果たして...)
exportすればapplyが可能になる。
$ cue export test.cue --out yaml exampleService: apiVersion: v1 kind: Service metadata: name: my-service labels: app: my-app spec: type: ClusterIP selector: app: my-app ports: - name: http protocol: TCP port: 80 - name: https protocol: TCP port: 443
終わりに
今回はどういうものかや活用事例とチュートリアルの触りをやってみましたが色々便利に使えるんだろうなと思う反面JSON/YAMLのスーパセット以上の使い方が無数に存在していそうだなと感じました。