CUE言語触ってみた

Hikaru Sasamoto
Eureka Engineering
Published in
10 min readApr 4, 2023

Table of Contents

はじめに
対象読者
CUEとは
CUEの使い方
−− Types and Values
−− Definitions
−− Imports
−− `_|_` : CUE独自のエラー型
−− Module
終わりに
参考文献

はじめに

Kubernetesマニフェスト管理のためにCUEを取り入れました。
導入に際して、チームへの伝承のための資料を作成しました。どうせならブログにまとめて公開しようと思い立ったのが、本ポストの動機です。
目新しいところはないかと思いますが、参考になればと。

対象読者

  • CUEの使い方を知りたい方
  • 設定ファイルに制約を入れてみたい方

CUEとは

公式
構成管理を目的とした言語です。
構成管理言語の代表的なところだと、JSONやYAML, Jsonnetがあたりが挙げられます。
CUEは、Googleで全てのアプリケーションをデプロイするために利用されていた言語(BCL)の作者が作った言語です。
Ref: What is CUE? | Dagger

CUEは、構成ファイルに関する問題を解決するために設計されています。
構成ファイルのバリデーション、構成のマージ、設定のデフォルト値を持たせることができることが特徴です。
データの定義と制約をファイルにまとめて記載できます。

CLIで、YAML,JSONへの/からのコンバートがビルトインで提供されているのも個人的に気に入っている点です。

CUEの使い方

Types and Values

設定の型と設定を定義することができます。

# type.cue
Hoge: {
Name: string
Age: int
}

Hoge: {
Name: "Hoge Fuga"
Age: 21
}

実行結果

$ cue export type.cue — out yaml
Hoge:
Name: Hoge Fuga
Age: 21

YAMLと同じように単一オブジェクト内で同一keyで複数の値を持つことはできません。
https://yaml.org/spec/1.2-old/spec.html#id2760844
JSONも推奨はしていないので、この辺りの思想は一緒のようです。
https://www.rfc-editor.org/rfc/rfc8259#section-4


# example_ng.cue
Hoge: {
Name: “Hoge”
Age: 22
}

Hoge: {
Name: "Fuga"
}
$ cue export example_ng.cue — out yaml
Hoge.Name: conflicting values “Fuga” and “Hoge”:
./example.cue:2:8
./example.cue:7:8

field名を `_`から始めることで、field名が出力されなくなります。

# hidden.cue
_hoge: 10
fuga: 20
$ cue export hidden.cue
{
“fuga”: 20
}

これは変数を定義したいときに便利です。

# hidden_var.cue
_var: 10
hoge: _var
$ cue export hidden_var.cue
{
“hoge”: 10
}

Definitions

Go言語のstructのようにスキーマを定義します。
実行時に、スキーマのフィールドが定義通りの値かを評価し、出力します。

# definitions.cue

#Definition: {
address: string | *"0.0.0.0" // default値を定義
port: int & > 10 // 10より大きいint型
protocol: "tcp" | "udp" // "tcp"か"udp"のみを許容
host?: string // optional
}
Hoge: #Definition & {
address: "127.0.0.1"
port: 80
protocol: "udp"
host: "hogehoge"
}
Fuga: #Definition & {
port: 443
protocol: "tcp"
}
$ cue export definitions.cue — out yaml
Hoge:
address: 127.0.0.1
port: 80
protocol: udp
host: hogehoge
Fuga:
address: 0.0.0.0
port: 443
protocol: tcp

イテレート処理も簡単に書くことができます


# definitions.cue

_definitions: […#Definition]
_definitions: [
{
address: 127.0.0.1, port: 80, protocol: "udp", host: "hogehoge"
},
{
port: 443, protocol: "tcp", host: "fuga"
},
{
address: 127.0.0.1, port: 8080, protocol: "tcp"
}
]
hogefuga: […] // 型を指定しない配列の定義も可能
hogefuga: [ for i, val in _definitions
{
key: "def-\(i)" // 文字列の中に変数を埋め込める
val
}]


$ cue export definitions.cue — out yaml
hogefuga:
— key: def-0
address: 127.0.0.1
port: 80
protocol: udp
host: hogehoge
— key: def-1
address: 0.0.0.0
port: 443
protocol: tcp
host: fuga
— key: def-2
address: 127.0.0.1
port: 8080
protocol: tcp

Imports

CUEファイルではbuiltinされているパッケージだけでなく、ユーザが定義したパッケージから処理をインポートして使うことができます。

例えば、入力がstringであってもstringの配列であっても、1次元のstring配列で出力したい場合に、`list`が使えます。


# list.cue
import “list”

_word: string
_word: "hoge"
_words: […string]
_words: ["fuga","piyo"]
words1: list.Flatten([_word])
words2: list.Flatten([_words])
$ cue export list.cue — out yaml
words1:
— hoge
words2:
— fuga
— piyo

`_|_` : CUE独自のエラー型

プログラム実行時に発生するエラーを表現するために使用されます。
(Bottom / Error | CUE)

例えば、以下のような制約/値を考えると、

name: string
age: int

name: "hoge"
age: "invalid"

「name」フィールドは`string`型であるという制約を満たしていますが、「age」フィールドは`int`型であるとういう制約は満たされていません。
この場合、「age」フィールドはエラー型つまり `_|_`と表現されます。

また存在しないフィールド・関数を表す時にも `_|_`は使われます。


# list_bottome.cue
import “list”

_word: string
_word: "hoge"
words: list.HogeFunc([_word])

$ cue export list_bottom.cue — out yaml
words: cannot call non-function list.HogeFunc (type _|_):
./list_bottom.cue:6:8

Module

moduleとして使うには制約があるので注意が必要です。
- domain.com または github.com ディレクトリ配下に配置
(Modules and Packages | First Steps | Cuetorials)


.
├── cue.mod
│ └── pkg
│ ├── domain.com
│ │ └── example
│ │ └── thank_you.cue
│ └── github.com
│ └── base
│ └── hello_world.cue
├── fuga
│ └── main.cue
└── hoge
└── main.cue
```

# thank_you.cue
package example

msg: "thank you"
# hello_world.cue
package base
msg: "hello world"
# fuga/main.cue
package fuga

import (
"github.com/base"
"domain.com/example"
)
text: "Hey, fuga! \(base.msg)"
response: "\(example.msg), hoge"
# hoge/main.cue
package fuga

import (
"github.com/base"
"domain.com/example"
)
text: "Hey, hoge! \(base.msg)"
response: "\(example.msg), fuga"
$ cue export fuga/main.cue — out yaml 
text: Hey, fuga! hello world
response: thank you, hoge

$ cue export hoge/main.cue - out yaml
text: Hey, hoge! hello world
response: thank you, fuga

cue.modがあるディレクトリでなくても実行可能でした。

# ./fuga
$ cue export main.cue — out yaml
text: Hey, fuga! hello world
response: thank you, hoge

# ./hoge
$ cue export main.cue - out yaml
text: Hey, hoge! hello world
response: thank you, fuga

終わりに

今回初めてCUEに触れてみましたが、言語体系の理解にはそこまで時間がかからないかなと感じました。

設定とその制約を1ファイルにまとめることができるというのは良い体験と感じました。
大規模な構成管理となったときにどういった分割をしていくかというのは考えていく必要はあるかと思います。

YAMLやJSONで書かれた構成管理のvalidation用途としても活躍できそうですので、サクッと導入してみることも簡単そうです。
Kubernetesのマニフェストの管理で使われる場面は今後増えていくだろうなと思います。kustomizeがcueで書かれたファイルをそのままbuildできるようになると嬉しいですね。

機会があれば、導入背景やマニフェスト管理・運用方法についてもブログ書いてみたいと思います。

参考文献

--

--