CUE

CUE(cuelang)総合ガイド — 型システム・単一化・設定管理の全容

本ガイドは、設定記述言語 CUE の全体像を、実際に評価した具体例とともにまとめたものである。掲載したコードと出力はすべて cue v0.14.1(Go 1.24.6 / darwin-arm64)で実行して確認している。

目次

  1. はじめに — CUEとは何か
  2. 中核概念 — 値の格子(lattice)
  3. 基本構文
  4. 定義(definition)と閉じた構造
  5. 参照・スコープ・エイリアス
  6. パターン制約とテンプレート
  7. 内包表記(comprehension)
  8. 標準ライブラリ
  9. モジュールとパッケージ
  10. CLI
  11. 検証(validation)の設計
  12. スクリプティング層 tool/
  13. 他ツールとの比較
  14. 実践パターン
  15. 落とし穴12選
  16. まとめ
  17. 参考情報

1. はじめに — CUEとは何か

1.1 一言で言うと

CUEは「型と値を区別しない」設定記述言語である。

普通のプログラミング言語では int は型、3 は値であり、両者は別の世界に属する。CUEにはその区別がない。int3 も同じ空間の点であり、3int より「具体的な」点にすぎない。この一点から、CUEの性質のほとんどが導かれる。

a: int      // 型を書く
a: 3        // 同じフィールドに値も書く → 矛盾しない
$ cue eval a.cue
a: 3

int3 を「単一化」すると 3 になる。型注釈と値代入が同じ演算になっている。

1.2 名前の由来と系譜

CUE = Configure, Unify, Execute。

系譜はGoogleの内部設定言語 GCL(Generic Configuration Language、Borgの設定を書いていた言語)にある。CUEの作者 Marcel van Lohuizen はGoogle時代にGCLとその後継の設計に関わり、その経験から「継承とオーバーライドを中心に据えた設定言語は大規模化すると破綻する」という結論を得た。CUEはその反省から出発している。

理論的な土台は1980年代の特性構造(feature structure)型付き素性構造の単一化にあり、これは計算言語学(自然言語の構文解析)で使われていた枠組みである。設定管理の文脈へ持ち込んだのがCUEの独自性である。

世代言語中心概念
第1世代YAML / JSONデータのみ。抽象化機構なし
第2世代GCL、Jsonnet継承とオーバーライド
第2.5世代Helmテキストテンプレート + YAML
第3世代CUE、Dhall、Pkl型システムと制約

1.3 CUEが解こうとしている問題

大規模な設定管理で繰り返し起きる問題は、おおむね4つに整理できる。

① 「この値は最終的に何になるのか」が分からない

継承とオーバーライドを持つ言語では、値は「最後に書いた人が勝つ」。5層のオーバーレイがあれば、実際の値を知るには5層すべてを読んで評価順を追う必要がある。

② 設定の誤りが実行時まで発覚しない

YAMLは構文が正しければ通る。replicas: "3"(文字列)と replicas: 3(整数)の違いは、デプロイしてから判明する。

③ テンプレートが構造を理解しない

Helmのテンプレートは文字列を組み立てているだけで、出力がYAMLとして妥当かどうかを知らない。インデント1つで壊れる。

④ スキーマとデータが別の言語で書かれる

JSON SchemaはJSONで書かれ、対象のデータもJSONだが、両者は別の語彙を使う。二重管理になる。

CUEの回答はそれぞれこうである。

問題CUEの回答
① 最終値が分からないオーバーライドを持たない。 値は狭まるだけで、矛盾すればエラー
② 誤りが遅く分かる型と制約を同じ言語で書き、cue vet で検証する
③ テンプレートが構造を知らないテキストではなく値を組み立てる。 出力は常に構造的に妥当
④ スキーマとデータが別言語同じ言語で書く。 int3 が同じ空間の値

1.4 3つの用途

CUEは3つの層で使える。それぞれ独立に採用できる。

┌─────────────────────────────────────────────────────┐
│ ③ スクリプティング層  tool/                          │
│    設定を「使う」— ファイル出力、HTTP、コマンド実行     │
├─────────────────────────────────────────────────────┤
│ ② 設定生成層  cue export / eval                      │
│    設定を「作る」— YAML / JSON を生成                  │
├─────────────────────────────────────────────────────┤
│ ① 検証層  cue vet                                    │
│    既存の YAML / JSON を「検査する」                   │
└─────────────────────────────────────────────────────┘

①だけの採用が最も導入コストが低い。 既存のYAMLを1行も書き換えずに、スキーマだけをCUEで書いてCIで cue vet を回せる。

1.5 検証環境

$ cue version
cue version v0.14.1

go version go1.24.6
   -compiler gc
     GOARCH arm64
       GOOS darwin
cue.lang.version v0.14.1

インストールは Homebrew か Go で行う。

$ brew install cue-lang/tap/cue
# または
$ go install cuelang.org/go/cmd/cue@latest

バージョンについての注意 — CUEは2026年時点でまだ v0.x である。言語仕様は安定してきているが、CLIのフラグや標準ライブラリには非互換変更が入ることがある。本ガイドの内容は v0.14.1 で確認したものであり、古いバージョンでは動かない例がある(§15.12 参照)。


2. 中核概念 — 値の格子(lattice)

2.1 型と値を区別しない

CUEのすべての値は、**1つの半順序集合(格子)**の要素である。順序は「より具体的か」で決まる。

                          _  (top / ⊤ — 任意の値)
                          │
          ┌───────────────┼───────────────┐
          │               │               │
        number         string           bool
          │               │               │
       ┌──┴──┐        ┌───┴───┐       ┌───┴───┐
      int  float    =~"^a"  !=""     true   false
       │                │
   >5 & <10          "abc"
       │
       7
       │
       └───────────────┐
                       │
                     _|_  (bottom / ⊥ — エラー)

上に行くほど抽象的(多くの値を含む)、下に行くほど具体的である。 _(top)はあらゆる値を許し、_|_(bottom)はいかなる値でもない = エラーを表す。

2.2 半順序と2つの極

記号名前意味
_top(⊤)制約なし。あらゆる値を受け入れる
_|_bottom(⊥)エラー。両立しない制約の結果

_|_ は「エラー値」として第一級に扱われる。これが後述する型テストのイディオム(§7.5)を可能にしている。

2.3 単一化 & — 最大下界

& は2つの値の最大下界(greatest lower bound、meet)を取る演算である。 「両方の制約を同時に満たす、最も抽象的な値」を返す。

a: int
a: 3           // ← 同じフィールドへの複数の記述は暗黙に & される

b: >5 & <10
b: 7

c: "hello" | "world"
c: "hello"

e: {x: 1} & {y: 2}
$ cue eval a.cue
a: 3
b: 7
c: "hello"
e: {
    x: 1
    y: 2
}

同じフィールドを2回書くことは、上書きではなく & である。 これがCUEの最も重要な性質である。

具体値どうしが食い違えばエラーになる。

port: 8080
port: 9090
$ cue eval conflict.cue
port: conflicting values 9090 and 8080:
    ./conflict.cue:1:7
    ./conflict.cue:2:7

「後に書いたほうが勝つ」ではなく「両方を満たす値が存在しない」というエラーになる。 エラーメッセージが両方の出現位置を示すため、どこで衝突したかが即座に分かる。

構造体の & は、フィールドごとの再帰的な & である。

{x: 1} & {y: 2}         →  {x: 1, y: 2}          (共通フィールドなし → 合併)
{x: 1} & {x: int}       →  {x: 1}                (int と 1 の meet は 1)
{x: 1} & {x: 2}         →  _|_                   (衝突)
{x: int} & {x: string}  →  {x: _|_}              (型が両立しない)

2.4 論理和 | — 最小上界

| は最小上界(least upper bound、join)を取る。 「どちらかを満たせばよい」を表す。

env:      "dev" | "stg" | "prod"     // 列挙型に相当
port:     int | string               // どちらでもよい
maybe:    string | null              // nullable

論理和は & によって絞り込まれる。

("dev" | "stg" | "prod") & "stg"        →  "stg"
("dev" | "stg" | "prod") & "xxx"        →  _|_
("dev" | "stg" | "prod") & ("stg"|"prod") →  "stg" | "prod"

列挙型の実装に特別な構文が要らない。 | と文字列リテラルだけで表現される。

2.5 順序に依存しない — 可換・結合・冪等

& は数学的に良い性質を持つ。

法則意味
可換律 a & b = b & a書く順序が結果に影響しない
結合律 (a & b) & c = a & (b & c)グループ化が結果に影響しない
冪等律 a & a = a同じことを2回書いても無害

実際に確認する。

x: {a: 1} & {b: 2} & {a: int}
y: {a: int} & {b: 2} & {a: 1}
$ cue export commute.cue --out json
{
    "x": {
        "a": 1,
        "b": 2
    },
    "y": {
        "a": 1,
        "b": 2
    }
}

xy は書く順序が違うが、結果は同一である。

この性質の実務上の意味は大きい。

  • ファイルの読み込み順を気にしなくてよい。 cue eval a.cue b.cuecue eval b.cue a.cue は同じ結果になる
  • 設定の「層」に優先順位を定義する必要がない。 ベース・環境別・ホスト別を単に & で合成できる
  • 並列評価が安全である

一方で | は可換・結合的だが冪等ではない側面があり、既定値(§3.4)が絡むと注意が必要になる。

2.6 「オーバーライドがない」ことの帰結

CUEに overridemerge に相当する操作は存在しない。これは制限だが、同時に保証でもある。

保証されること:

あるフィールドの最終値は、そのフィールドに関する
すべての記述の & である。1つでも矛盾すればビルドが落ちる。

したがって「本番のこの値、どこで決まっているのか」という調査で、評価順を追う必要がない。 そのフィールドに言及しているすべての箇所を集めれば、それが答えである。

引き換えに失うもの:

// ベース
base: timeout: "30s"

// 「本番だけタイムアウトを延ばしたい」→ これはエラーになる
prod: base & {timeout: "60s"}
//    ^^^^ conflicting values "60s" and "30s"

上書きしたい値は、上書きされる側が「上書き可能」と宣言していなければならない。 その宣言が既定値マーカー * である。

base: timeout: string | *"30s"      // 既定は30s、ただし上書き可
prod: base & {timeout: "60s"}       // → "60s"
dev:  base                          // → "30s"

「どの値が環境ごとに変わりうるか」を、スキーマ設計時に明示的に決める必要がある。 これはCUEを採用するときに最初にぶつかる設計上の負荷であり、同時にCUEの主要な価値でもある。設定の可変点が型として文書化される。


3. 基本構文

3.1 構造体とフィールド

CUEのファイルはトップレベルが暗黙の構造体である。

// 行コメント
name: "api"
port: 8080

// ネストは3通りの書き方がある
a: {b: {c: 1}}
d: b: c: 1              // 短縮形(コロン連鎖)
e: {
	b: {
		c: 1
	}
}

// 引用符が必要なフィールド名
"with-hyphen":     true
"with.dot":        true
"日本語":           true

カンマは省略できる。 改行が区切りになる。1行に複数書くときだけカンマが必要である。

inline: {a: 1, b: 2}
multi: {
	a: 1
	b: 2
}

3.2 型と制約

組み込み型は次の通りである。

nullnull
booltrue, false
string"abc", '''...'''
bytes'\x00'
int42, 0x2a, 0b101010, 1_000_000
float3.14, 1e-3
numberintfloat の和
struct{...}
list[...]
_top

制約は型と同じ位置に書ける。

// 境界
age:      int & >=0 & <150
ratio:    float & >=0.0 & <=1.0
nonEmpty: string & !=""

// 正規表現
email:  =~"^[^@]+@[^@]+$"
notTmp: !~"^tmp-"

// 列挙
level: "debug" | "info" | "warn" | "error"

// 組み合わせ
port: int & >=1024 & <=65535
演算子意味
>, >=, <, <=数値・文字列の境界
!=不一致
=~正規表現に一致(RE2構文)
!~正規表現に一致しない

境界は文字列にも使える(辞書順)。>="a" & <"b"a で始まる文字列ではなく、辞書順で a 以上 b 未満の文字列である。

3.3 任意フィールドと必須フィールド

#Config: {
	required:  string      // 通常フィールド — 値が必要
	optional?: string      // 任意 — 無くてもよい
	mandatory!: string     // 必須 — 具体値が必ず必要
}
記法具体化の要求省略可
f: Texport 時に具体値が必要不可(ただし定義内では制約のみでもよい)
f?: T存在すれば T を満たす
f!: T必ず具体値が必要不可

f!: は v0.5 で導入された。「スキーマの利用者が必ず埋めなければならない」ことを明示できる。

3.4 既定値 *

* は論理和の「既定の枝」を示すマーカーである。

a: int | *1
b: a + 1
c: *"x" | "y" | "z"
f: (>=1 & <=5) | *3
g: int & (*3 | 4 | 5)
$ cue eval def2.cue
a: 1
b: 2
c: "x"
f: 3
g: 3

* は論理和の要素位置にしか置けない。 制約に直接付けるとエラーになる。

e: >=1 & <=5 & *3
$ cue eval def.cue
e: preference mark not allowed at this position:
    ./def.cue:6:16

これは初学者が最もよく踏む構文エラーである。「1〜5の範囲で既定は3」を表現したいなら次のように書く。

e: (>=1 & <=5) | *3      // 3 は範囲内だが、範囲との整合は保証されない
e2: >=1 & <=5            // 制約
e2: int | *3             // 既定(別の行で与える)

既定値が競合すると、既定が消える。

d: (*"x" | "y") & (*"y" | "x")
$ cue eval def3.cue
d: "x" | "y"

"x""y" のどちらも既定を主張しているため、CUEはどちらも選ばず既定を持たない論理和を返す。この値を export しようとすると「具体的でない」というエラーになる。

複数の層がそれぞれ * で既定を与える設計は危険である。 既定は1箇所(最も抽象的な層)でのみ宣言するのが安全である。

3.5 リスト

closed: [1, 2, 3]           // 閉じたリスト — 長さ3で固定
open:   [1, 2, ...]         // 開いたリスト — 先頭が1,2で、以降は任意
typed:  [...int]            // 任意長のintリスト
mixed:  [int, string, ...bool]

リストの単一化は要素ごと、かつ長さが一致する必要がある。

import "list"
a: [1, 2, 3] & [1, 2, 3]
b: [...int] & [1, 2]
d: [...string] & ["x", ...]
r: list.Repeat([0], 3)
$ cue export list3.cue --out json
{
    "a": [1, 2, 3],
    "b": [1, 2],
    "d": ["x"],
    "r": [0, 0, 0]
}

長さが違うとエラーになる。

c: [1, 2] & [1, 2, 3]
$ cue eval list.cue
c: incompatible list lengths (2 and 3):
    ./list.cue:3:13

これは構造体との決定的な違いである。 構造体は & で合併するが、リストは合併しない。§15.7 で改めて扱う。

注意 — 古い資料に出てくるリストの繰り返し記法 3*[int] は、v0.14 では動かない。* が乗算として解釈され invalid right-hand value to '*' (type int) になる。代わりに list.Repeat を使う。

3.6 文字列

simple: "hello"

// 補間
name: "api"
url:  "https://\(name).example.com"

// 複数行(3連クォート)
script: """
	#!/bin/sh
	echo hello
	"""

// 生文字列(バックスラッシュを解釈しない)
regex: #"^\d+$"#

// バイト列
raw: '\x00\x01'

複数行文字列のインデントは、閉じる """ の位置を基準に剥がされる。 上の script の内容は #!/bin/sh\necho hello になり、タブは含まれない。

3.7 数値

CUEの数値は任意精度である。

big:   123456789012345678901234567890
exact: 1.0000000000000000001
div:   1 / 3          // 有理数として保持される

intfloat は別の型であり、11.0 は単一化できない。

x: int
x: 1.0        // → エラー

浮動小数点への変換は明示的に行う。

import "math"
y: math.Trunc(3.7)     // 3
z: float & 1           // 1 を float として扱う

4. 定義(definition)と閉じた構造

4.1 #Name の意味

# で始まるフィールドは「定義(definition)」である。 3つの性質を持つ。

_priv:  "not exported"
#Def:   {k: string}
pub:    "exported"
derived: _priv + "!"
$ cue export hidden.cue --out json
{
    "pub": "exported",
    "derived": "not exported!"
}

$ cue eval hidden.cue
#Def: {
    k: string
}
pub:     "exported"
derived: "not exported!"
性質説明
① 出力されないcue export の結果に含まれない。スキーマがデータに混ざらない
② 既定で閉じている定義に無いフィールドを追加するとエラー
③ 再帰的に閉じる入れ子の構造体も閉じる

cue eval では表示され、cue export では消える点に注意したい。eval は「CUEの値」を見るコマンド、export は「データ」を出すコマンドである(§10.2)。

4.2 開いた構造体と閉じた構造体

#Person: {
	name: string
	age?: int
}
p: #Person & {name: "Ada", email: "a@b.c"}
$ cue eval closed.cue
p.email: field not allowed:
    ./closed.cue:5:28

... を書くと明示的に開く。

#Loose: {
	name: string
	...
}
p: #Loose & {name: "Ada", extra: true}
$ cue export open.cue --out json
{
    "p": {
        "name": "Ada",
        "extra": true
    }
}

通常の構造体(# なし)は開いている。

Person: {name: string}          // 定義ではない
p: Person & {extra: true}       // → 通る
書き方出力される閉じている
Name: {...}✗(開いている)
#Name: {...}
#Name: {..., ...}✗(明示的に開く)
_name: {...}

4.3 閉じた定義は単一化で拡張できない

これはCUEで最もよく踏まれる落とし穴である。

#Base: {kind: string, meta: name: string}
#Pod: #Base & {kind: "Pod", spec: containers: [...{name: string}]}
$ cue def d1.cue
#Pod.spec: field not allowed:
    ./d1.cue:2:29

「基底定義を & で拡張する」という直感は通らない。 #Base は閉じているので、spec という未知のフィールドを足せない。

一方、既存のフィールドを狭めるのは通る。

#Base: {kind: string, meta: name: string}
#Pod: #Base & {kind: "Pod"}          // OK
$ cue def ext1.cue
#Base: {
	kind: string
	meta: name: string
}
#Pod: #Base & {
	kind: "Pod"
}

この区別が要点である。

#Base & {既にあるフィールドをより具体的に}   →  OK(これが単一化)
#Base & {新しいフィールドを追加}             →  field not allowed

拡張したい場合の選択肢は2つある。

方法A: 基底を開いておく

#Base: {kind: string, ...}
#Pod: #Base & {kind: "Pod", spec: {replicas: int}}
$ cue def ext3.cue
#Base: {
	kind: string
	...
}
#Pod: #Base & {
	kind: "Pod"
	spec: replicas: int
}

代償として、#Base を使うすべての場所で未知フィールドの検査が効かなくなる。

方法B: 埋め込み(embedding)を使う ← 推奨

4.4 埋め込み(embedding)

構造体リテラルの中に値を単独で書くと「埋め込み」になる。

#Base: {kind: string, meta: name: string}
#Pod: {
	#Base                              // ← 埋め込み。コロンがない
	kind: "Pod"
	spec: replicas: int
}
ok:  #Pod & {meta: name: "a", spec: replicas: 2}
bad: #Pod & {meta: name: "a", spec: replicas: 2, typo: true}
$ cue eval emb.cue
bad.typo: field not allowed:
    ./emb.cue:8:50

ok は通り、bad は弾かれた。 埋め込みなら新しいフィールドを足せて、しかも結果の #Pod は閉じたままである。

#Base & {spec: ...}     →  エラー(閉じた定義に足せない)
{#Base, spec: ...}      →  OK、かつ結果も閉じている

「継承」を表現したいなら埋め込みを使う。 これがCUEにおける唯一の正しい拡張手段である。

埋め込みは定義以外にも使える。

common: {region: "us-east-1"}
svc: {
	common                 // 埋め込み
	name: "api"
}
// → {region: "us-east-1", name: "api"}

4.5 隠しフィールド _name

アンダースコアで始まるフィールドは「隠しフィールド」で、出力されないが閉じてもいない。

_intermediate: {a: 1, b: 2}
result: _intermediate.a + _intermediate.b     // 3

用途は中間計算である。定義(#)はスキーマ、隠しフィールド(_)は計算の作業領域、と役割が分かれる。

#_name は両方の性質を持つ(隠された定義)。パッケージ外から見えない内部スキーマに使う。

記法出力閉じるパッケージ外から参照
name
#Name
_name
#_Name

4.6 まとめ

                 出力される?
                  /      \
               はい        いいえ
                │           │
             name       閉じる?
                       /     \
                    はい       いいえ
                     │          │
              #Name / #_Name   _name
              (スキーマ)    (中間計算)

実務上の判断基準:

  • 外部に公開するスキーマ → #Name
  • パッケージ内部だけのスキーマ → #_Name
  • 計算の途中結果 → _name
  • 出力したいデータ → name

5. 参照・スコープ・エイリアス

5.1 レキシカルスコープ

参照は**レキシカルに(構文上の入れ子で)**解決される。最も内側の同名フィールドが優先される。

x: 1
outer: {
	x: 2
	inner: {
		v1: x        // レキシカルに最も近い x
	}
}
$ cue export scope.cue --out json
{
    "x": 1,
    "outer": {
        "x": 2,
        "inner": {
            "v1": 2
        }
    }
}

inner.v1outer.x(= 2)を見る。トップレベルの x(= 1)を明示的に参照する方法は用意されていないため、シャドーイングが起きうる名前の付け方は避けるべきである。 隠しフィールドやエイリアスで明示的に束縛するのが安全である。

5.2 3種類のエイリアス

CUEには紛らわしいことに3種類のエイリアスがある。すべて = を使うが、意味が違う。

// ① フィールドエイリアス — フィールドの値に別名を付ける
X=svc: {name: "api", port: 8080}
ref: X.port

// ② ラベルエイリアス — パターン制約のキーに別名を付ける
m: [Key=string]: {id: Key}
m: alpha: {}
m: beta:  {}

// ③ let — 式に別名を付ける(値エイリアス)
let base = {retries: 3}
withLet: base & {timeout: "5s"}
$ cue export alias.cue --out yaml
svc:
  name: api
  port: 8080
ref: 8080
m:
  alpha:
    id: alpha
  beta:
    id: beta
withLet:
  retries: 3
  timeout: 5s
種類記法束縛する対象出力される
フィールドエイリアスX=field: valueそのフィールドのfield は出力される。X は識別子で出力されない
ラベルエイリアス[X=string]: ...フィールド名(キー)
letlet X = expr任意の

ラベルエイリアスが最も実用的である。 マップのキーを値の中で再利用できるため、「名前を2回書く」という定型的な冗長を消せる。

// キーを id として自動的に埋める
services: [Name=string]: {
	name: Name
	port: int
}
services: api: {port: 8080}       // name: "api" が自動で入る
services: web: {port: 8443}       // name: "web" が自動で入る

フィールドエイリアスは、深いパスを繰り返し書くのを避けるために使う。

X=configuration: environments: production: {...}
elsewhere: X.someField            // configuration.environments.production.someField

let は評価が1回であることを保証しない(CUEは純粋なので結果は同じ)が、可読性のために使われる。ローカルスコープを持つため、外に漏れない。

5.3 循環参照

CUEはすべての循環を禁止しているわけではない。 3種類に分かれる。

① 数値の循環 — 未解決のまま残る(エラーではない)

a: b + 1
b: a - 1
$ cue eval cyc1.cue
a: b + 1
b: a - 1

エラーにならず、単に「不完全な値」として残る。 cue export すると具体的でないというエラーになるが、cue eval は通る。CUEは代数的に解こうとはしない。

② 再帰的な定義 — 許される

#List: {
	head: int
	tail?: #List
}
x: #List & {head: 1, tail: {head: 2}}
$ cue export cyc2.cue --out json
{
    "x": {
        "head": 1,
        "tail": {
            "head": 2
        }
    }
}

任意フィールド(?:)による再帰は問題なく動く。 有限の深さで具体化されれば評価が止まる。連結リストや木構造のスキーマが書ける。

③ 構造的な循環 — エラー

a: {b: c}
c: {d: a}
$ cue eval cyc3.cue
c.d: structural cycle

無限に深い構造を要求する参照はエラーになる。 a を評価するには c が必要で、c を評価するには a が必要、というループが構造として閉じない場合である。

種類挙動対処
数値の循環不完全な値として残るどちらか一方を具体化する
再帰的な定義(?: 経由)正常に動く
構造的な循環structural cycle エラー片方を任意フィールドにする、または設計を見直す

6. パターン制約とテンプレート

6.1 [string]: T

パターン制約は「名前がパターンに一致するすべてのフィールドに制約を課す」機構である。 他言語のマップ型や、JSON Schema の additionalProperties に相当する。

// すべてのフィールドが string
labels: [string]: string
labels: app:  "api"
labels: tier: "backend"

// すべてのフィールドが構造体
services: [string]: {
	port:     int
	replicas: int | *1
}
services: api: {port: 8080}

パターンには制約を書ける。

// 名前が小文字英数字とハイフンのみのフィールドに制約
resources: [=~"^[a-z0-9-]+$"]: {size: string}

// 特定のプレフィックスだけ
envVars: [=~"^APP_"]: string

パターン制約は「そのフィールドが存在すれば」という条件付きである。 フィールドを1つも書かなくてもエラーにならない。

6.2 ラベルエイリアスとキーの再利用

§5.2 で触れた通り、キーを値の中で使える。

deployments: [Name=string]: {
	metadata: name: Name
	metadata: labels: app: Name
	spec: selector: matchLabels: app: Name
}
deployments: "my-api": {}
deployments:
  my-api:
    metadata:
      name: my-api
      labels:
        app: my-api
    spec:
      selector:
        matchLabels:
          app: my-api

Kubernetesマニフェストで同じ名前を4〜5箇所に書く定型が、1箇所に減る。 これがCUEをKubernetes設定に使う最大の実利の1つである。

6.3 動的フィールド名

フィールド名を式から作れる。

prefix: "app"
items: ["a", "b"]
out: {
	for i in items {
		"\(prefix)-\(i)": {index: i}
	}
}
$ cue export dynfield.cue --out yaml
prefix: app
items:
  - a
  - b
out:
  app-a:
    index: a
  app-b:
    index: b

補間を含むフィールド名は、必ず引用符で囲む必要がある。

6.4 条件フィールド if

構造体の中に if を書くと、条件が真のときだけフィールドが現れる。

env: "prod"
config: {
	replicas: int | *1
	if env == "prod" {
		replicas: 5
		monitoring: true
	}
}
$ cue export ifcond2.cue --out yaml
env: prod
config:
  replicas: 5
  monitoring: true

重要な注意点 — 条件の中で既存フィールドの値を変えたい場合、そのフィールドは既定値で宣言されていなければならない。

env: "prod"
config: {
	replicas: 1                     // ← 具体値
	if env == "prod" {
		replicas: 5                 // ← 衝突する
	}
}
$ cue eval ifcond.cue
config.replicas: conflicting values 5 and 1:
    ./ifcond.cue:3:12
    ./ifcond.cue:5:13

if は「上書き」ではない。 条件が真なら、そのフィールドへの追加の制約として & される。§2.6 の原則がここでも一貫している。

「条件で値を差し替える」を実現する2つの書き方:

// 方法A: 既定値にする
config: {
	replicas: int | *1
	if env == "prod" { replicas: 5 }
}

// 方法B: 論理和で分岐を明示する
config: {
	replicas: [
		if env == "prod" {5},
		1,
	][0]
}

// 方法C(推奨): 論理和と条件式
config: replicas: [if env == "prod" {5}, 1][0]

実務では方法A(既定値)が最も読みやすい。 「この値は上書き可能である」という意図がスキーマに現れる。


7. 内包表記(comprehension)

7.1 構造体内包とリスト内包

envs: ["dev", "prod"]

// リスト → 構造体
out: {
	for e in envs {
		"\(e)": {name: e, replicas: 1}
	}
}

// 構造体 → リスト(`if` ガードつき)
items: {a: {n: 1}, b: {n: 5}, c: {n: 9}}
big: [ for k, v in items if v.n > 4 {key: k, n: v.n} ]

// let で中間値
sum: {
	let doubled = [ for x in [1, 2, 3] {x * 2} ]
	result: doubled
}
$ cue export comp.cue --out yaml
envs:
  - dev
  - prod
out:
  dev:
    name: dev
    replicas: 1
  prod:
    name: prod
    replicas: 1
items:
  a:
    "n": 1
  b:
    "n": 5
  c:
    "n": 9
big:
  - key: b
    "n": 5
  - key: c
    "n": 9
sum:
  result:
    - 2
    - 4
    - 6

内包表記の形は2通りである。

記法結果
構造体内包{ for k, v in X { "\(k)": ... } }構造体
リスト内包[ for k, v in X { ... } ]リスト

for はリストと構造体の両方を走査できる。

対象1変数2変数
リストfor v in listfor i, v in listi はインデックス)
構造体for v in structfor k, v in structk はフィールド名)

7.2 if ガード

for の直後に if を置くとフィルタになる。

// 名前が "prod" で始まる環境だけを抽出
all: {dev: {}, prod-us: {}, prod-eu: {}, stg: {}}
import "strings"
prodOnly: {
	for k, v in all if strings.HasPrefix(k, "prod") {
		"\(k)": v
	}
}

for を伴わない単独の if は §6.4 の条件フィールドである。両者は同じキーワードだが役割が違う。

7.3 let

内包表記の中で中間値を束縛できる。

services: {
	for name, cfg in input {
		let fullName = "\(prefix)-\(name)"
		"\(fullName)": {
			metadata: name: fullName
			spec:     cfg
		}
	}
}

let は内包表記の各周回で評価される。 ループ変数に依存する計算を1度だけ書ける。

7.4 マップとリストの相互変換

実務で頻出する変換を挙げる。

import "list"

// ① マップ → リスト(Kubernetesの env 形式へ)
envMap: {LOG_LEVEL: "info", PORT: "8080"}
envList: [ for k, v in envMap {name: k, value: v} ]
// → [{name: "LOG_LEVEL", value: "info"}, {name: "PORT", value: "8080"}]

// ② リスト → マップ
recordList: [{id: "a", n: 1}, {id: "b", n: 2}]
recordMap: {for r in recordList {"\(r.id)": r}}
// → {a: {id: "a", n: 1}, b: {id: "b", n: 2}}

// ③ 平坦化
nested: [[1, 2], [3, 4]]
flat:   list.FlattenN(nested, 1)      // [1, 2, 3, 4]

// ④ 集約
counts: [1, 2, 3]
total:  list.Sum(counts)              // 6

マップ → リストの変換順序は、フィールド名の辞書順である。 CUEの構造体は宣言順を保持するが、内包表記の走査順は保証されていないため、順序に依存する出力を作るときは明示的にソートする(list.Sort)。

7.5 型テスト — _|_ を使ったイディオム

CUEには instanceof に相当する演算子がない。 代わりに「試しに単一化して bottom にならないか」を見る。

#Secret: {path: string, version?: int}
#Cert:   {profile: string, cn: string}

input: {
	dbPassword: {path: "secret/db"}
	tlsCert:    {profile: "server", cn: "api.example.com"}
	timeout:    "30s"
}

// 種類ごとに振り分ける
secrets: {
	for k, v in input if (v & #Secret) != _|_ {
		"\(k)": v
	}
}
certs: {
	for k, v in input if (v & #Cert) != _|_ {
		"\(k)": v
	}
}
plain: {
	for k, v in input
	if (v & #Secret) == _|_ && (v & #Cert) == _|_ {
		"\(k)": v
	}
}
イディオム意味
(x & #T) != _|_x#T と両立する(#T として扱える)
(x & #T) == _|_x#T と両立しない

これは実務で非常によく使われる。 1つの平坦な入力を、種類ごとに複数の出力へ振り分ける処理がこれで書ける。設定基盤では「プロパティ」「シークレット参照」「証明書要求」を1つの記述から分離する、といった用途に使われる。

注意点 — 閉じた定義(#T)と両立するかどうかの判定は、x#T に無いフィールドがあれば偽になる。逆に #T が開いていれば(...)、ほとんどの構造体が「両立する」と判定されてしまう。型テストの精度は定義の閉じ方に依存する。


8. 標準ライブラリ

8.1 パッケージ一覧

CUEの標準ライブラリはGoの標準ライブラリを踏襲した構成である。

パッケージ内容
strings文字列操作、MinRunes / MaxRunes などのバリデータ
strconv数値・文字列変換
listリスト操作、MinItems / UniqueItems などのバリデータ
math数学関数
math/bitsビット操作
regexp正規表現の抽出・置換
structMinFields / MaxFields
encoding/jsonJSON のマーシャル・アンマーシャル・検証
encoding/yamlYAML の同上
encoding/csvCSV
encoding/base64Base64
encoding/hex16進
encoding/pemPEM
netIP / FQDN / Host / Port などの検証
time時刻・期間の検証とフォーマット
uuidUUID の生成・検証
crypto/*md5 / sha1 / sha256 / sha512 / hmac
pathパス操作(OSごとの流儀を選べる)
text/templateGoテンプレート
text/tabwriter整列
htmlエスケープ
tool/*スクリプティング層(§12)

8.2 よく使う関数

import (
	"strings"
	"list"
	"math"
	"encoding/json"
	"encoding/yaml"
	"net"
	"strconv"
)
s1: strings.Join(["a", "b", "c"], "-")
s2: strings.ToUpper("cue")
s3: strings.Split("a,b,c", ",")
s4: strings.HasPrefix("cuelang", "cue")
l1: list.Contains([1, 2, 3], 2)
l2: list.Sort([3, 1, 2], list.Ascending)
l3: list.Sum([1, 2, 3])
l4: list.FlattenN([[1, 2], [3]], 1)
m1: math.Pow(2, 10)
j1: json.Marshal({a: 1})
y1: yaml.Marshal({a: 1, b: [2, 3]})
n1: net.IP & "10.0.0.1"
n2: net.FQDN & "api.example.com"
c1: strconv.Atoi("42")
$ cue export bi.cue --out json
{
    "s1": "a-b-c",
    "s2": "CUE",
    "s3": ["a", "b", "c"],
    "s4": true,
    "l1": true,
    "l2": [1, 2, 3],
    "l3": 6,
    "l4": [1, 2, 3],
    "m1": 1024,
    "j1": "{\"a\":1}",
    "y1": "a: 1\nb:\n  - 2\n  - 3\n",
    "n1": "10.0.0.1",
    "n2": "api.example.com",
    "c1": 42
}

yaml.Marshal / json.Marshal が文字列を返す点に注意したい。 これはConfigMapの中にYAMLを埋め込むような場面で使う。

import "encoding/yaml"
configMap: data: "app.yaml": yaml.Marshal(appConfig)

8.3 バリデータ関数

標準ライブラリの一部の関数は「制約」として使える。 引数を1つ取り、値と & することで検証になる。

import "strings"
import "list"
name: string & strings.MinRunes(3) & strings.MaxRunes(10)
tags: [...string] & list.MinItems(1) & list.UniqueItems()
name: "api"
tags: ["a", "b"]
$ cue export val.cue --out json
{
    "name": "api",
    "tags": ["a", "b"]
}

違反すると詳細なメッセージが出る。

import "list"
tags: [...string] & list.UniqueItems()
tags: ["a", "a"]
$ cue vet val2.cue
tags: invalid value ["a","a"] (does not satisfy list.UniqueItems): equal value ("a") at position 0 and 1:
    ./val2.cue:2:21
    ./val2.cue:2:7
    ./val2.cue:3:7

「どの要素が重複しているか」まで示す。 JSON Schema の uniqueItems: true より診断が具体的である。

主なバリデータ関数を挙げる。

関数対象意味
strings.MinRunes(n) / MaxRunes(n)string文字数(バイト数ではない)
strings.HasPrefix(s) / HasSuffix(s)string前方・後方一致
list.MinItems(n) / MaxItems(n)list要素数
list.UniqueItems()list重複なし
list.Contains(v)list特定要素を含む
struct.MinFields(n) / MaxFields(n)structフィールド数
net.IP / net.IPv4 / net.IPv6stringIPアドレス
net.FQDN / net.Host / net.Portstringホスト名・ポート
time.Durationstring"30s" などの期間
time.Format(layout)string時刻の書式
uuid.ValidstringUUID
encoding/json.Validstring/bytesJSONとして妥当

net.*time.* は設定検証で特に有用である。 「タイムアウトは期間文字列」「エンドポイントはFQDN」といった制約を1語で書ける。


9. モジュールとパッケージ

9.1 cue.mod/

$ cue mod init example.com/demo
$ find . -type f | sort
./cue.mod/module.cue

$ cat cue.mod/module.cue
module: "example.com/demo"
language: {
	version: "v0.14.1"
}

cue.mod/ ディレクトリがモジュールのルートを示す。Goの go.mod と同じ役割である。

myproject/
├── cue.mod/
│   ├── module.cue        ← モジュール名と言語バージョン
│   ├── pkg/              ← 依存モジュール(ベンダリング先)
│   ├── gen/              ← cue get go で生成した定義
│   └── usr/              ← ユーザーによる上書き
├── schema/
│   └── schema.cue
└── main.cue
ディレクトリ用途
pkg/外部モジュールの取り込み先。cue mod が管理
gen/cue get go などが生成したコード
usr/gen/ の内容を手で上書きする場所

language.version が重要である。 CUEはこの値を見て言語の振る舞いを決めるため、CUE本体をアップグレードしても既存モジュールの評価結果が変わらないよう保護される。

9.2 パッケージ宣言とimport

// schema/schema.cue
package schema

#Service: {
	name:     string
	port:     int & >=1024 & <=65535
	replicas: int | *1
}
// main.cue
package demo

import "example.com/demo/schema"

services: [string]: schema.#Service
services: api: {name: "api", port: 8080}
services: web: {name: "web", port: 8443, replicas: 3}
$ cue export ./... --out yaml
services:
  api:
    name: api
    port: 8080
    replicas: 1
  web:
    name: web
    port: 8443
    replicas: 3
---
{}

末尾の ---\n{} に注意したい。 ./... は各パッケージを別々のインスタンスとして評価するため、schema パッケージ(具体値を持たない)も1つの空のドキュメントとして出力される。実務では対象を明示するほうがよい。

$ cue export . --out yaml          # カレントパッケージのみ

同じディレクトリ内の同名パッケージのファイルは、暗黙にすべて & される。

config/
├── base.cue        package config
├── prod.cue        package config
└── secrets.cue     package config

この3ファイルは1つの値として評価される。ファイル分割に意味論的な影響はない(§2.5 の可換性)。

9.3 モジュール依存

v0.9 以降、CUEはOCIレジストリベースのモジュール機構を持つ。

$ cue mod init example.com/myapp
$ cue mod get github.com/example/schemas@v1
$ cue mod tidy
コマンド動作
cue mod init <path>モジュールを初期化
cue mod get <mod>@<ver>依存を追加
cue mod tidy未使用の依存を削除、不足を追加
cue mod publish <ver>レジストリへ公開
cue mod resolve依存の解決結果を表示

CUE Central Registryregistry.cue.works)が既定のレジストリである。プライベートなOCIレジストリも指定できる。

$ export CUE_REGISTRY=my-registry.example.com/cue-modules
$ cue mod get my.org/schemas@v1

バージョン解決は Go の Minimal Version Selection(MVS)と同じ方式である。 依存の依存が要求する最小バージョンのうち最大のものが選ばれる。

9.4 ベンダリング

外部モジュールを cue.mod/pkg/ へ物理的に配置する運用も広く行われている。

cue.mod/pkg/
└── example.com/
    └── schemas/
        └── v1/
            ├── types.cue
            └── validators.cue

この方式の利点はビルドの再現性である。 レジストリに到達できない環境(エアギャップされたCI)でも評価できる。

欠点は2つある。

  • スキーマの更新が明示的な操作になる。 上流の修正が自動では入らない
  • cue.mod/pkg/ を直接編集する誘惑が生じる。 応急処置としては機能するが、次回の更新で失われる

大規模な設定基盤では後者が実際に問題になる。「上流のスキーマに不具合があるのでベンダリングされたファイルを直接直す」という運用が定着すると、モジュールのバージョンアップが困難になる。ベンダリングされたディレクトリは読み取り専用として扱い、修正は上流へ送るのが原則である。


10. CLI

10.1 コマンド一覧

$ cue --help
Available Commands:
  cmd         run a user-defined workflow command
  completion  Generate completion script
  def         print consolidated definitions
  eval        evaluate and print a configuration
  export      output data in a standard format
  fix         rewrite packages to latest standards
  fmt         formats CUE configuration files
  get         add non-CUE dependencies to the current module
  import      convert other formats to CUE files
  login       log into a CUE registry
  mod         module maintenance
  trim        remove superfluous fields
  version     print CUE version
  vet         validate data

追加のヘルプトピックも用意されている。

cue help commands       ユーザー定義コマンド
cue help embed          ファイル埋め込み
cue help environment    環境変数
cue help filetypes      対応ファイル形式と修飾子
cue help flags          パッケージ合成の共通フラグ

cue help filetypes は覚えておく価値がある。 --out に指定できる形式と、yaml:, json:, text: などのファイル修飾子が一覧できる。

10.2 eval / export / vet / def の違い

この4つの区別がCUEを使う上で最も重要である。

コマンド入力出力具体値を要求するか
cue evalCUECUEの値(制約を含む)
cue exportCUEデータ(JSON/YAML等)
cue vetCUE + データエラーのみ用途により
cue defCUE定義を含むCUE
name: string
port: int & >1024
$ cue eval incomplete.cue
name: string
port: int & >1024

$ cue eval -c incomplete.cue
name: incomplete value string:
    ./incomplete.cue:1:7
port: incomplete value >1024 & int

$ cue export incomplete.cue
name: incomplete value string:
    ./incomplete.cue:1:7
port: incomplete value >1024 & int

cue eval は制約をそのまま表示する。 スキーマを確認したいときに使う。-c--concrete)を付けると具体値を要求する。

cue export は常に具体値を要求する。 データを出すコマンドなので、制約が残っていれば失敗する。

cue def は定義(#)を保持したまま整理して出す。 スキーマの正規化に使う。

#Base: {kind: string, meta: name: string}
#Pod: #Base & {kind: "Pod"}
$ cue def ext1.cue
#Base: {
	kind: string
	meta: name: string
}
#Pod: #Base & {
	kind: "Pod"
}

式を指定して一部だけ取り出せる。

$ cue eval -e 'a' a.cue
3
$ cue export -e 'e' a.cue --out yaml
x: 1
"y": 2

-e はデバッグで多用する。深いパスの一部だけ評価できる。

10.3 具体性(concreteness)

CUEの値は「具体的(concrete)」か「不完全(incomplete)」のどちらかである。

具体的か
3, "abc", true, [1,2], {a: 1}
int, string, >5, =~"^a"
"a" | "b"✗(既定がない論理和)
"a" | *"b"(既定 "b" に定まる)
{a: int}✗(a が不完全)

export が通るかどうかは、この判定で決まる。 「エクスポートできない」というエラーが出たら、どのフィールドが不完全かを cue eval -c で特定するのが定石である。

10.4 import — 既存ファイルの取り込み

# good.yaml
name: api
replicas: 3
image: registry.example.com/api:1.2.3
env:
  LOG_LEVEL: info
$ cue import -f -o - good.yaml
name:     "api"
replicas: 3
image:    "registry.example.com/api:1.2.3"
env: LOG_LEVEL: "info"

既存のYAML/JSONをそのままCUEへ機械変換できる。 移行の第一歩がこれである。

主なオプション:

オプション意味
-o <file>出力先(- で標準出力)
-f既存ファイルを上書き
-p <pkg>パッケージ名を付ける
-l <expr>各ドキュメントをパスの下に配置する
--list複数ドキュメントをリストにまとめる
--with-contextファイル名などのコンテキストを使える

Kubernetesマニフェストの取り込みでは -l が有用である。

$ cue import -p k8s -l 'strings.ToLower(kind)' -l 'metadata.name' *.yaml

これで deployment: myapp: {...} のような階層に自動的に配置される。

cue import は YAML / JSON 以外にも対応する。

形式備考
JSON / YAML標準
JSON Schemacue import jsonschema: schema.json
OpenAPIcue import openapi: api.yaml
Protobuf.proto から定義を生成
Gocue get go <pkg>(別コマンド)

10.5 trim — 冗長性の削除

cue trim は「他の場所から導出できるフィールド」を削除する。

package trim

deployments: [string]: {
	replicas: int | *1
	image:    string
}
deployments: api: {replicas: 1, image: "api:1"}
deployments: web: {replicas: 3, image: "web:1"}
$ cue trim ./...
$ cat t.cue
package trim

deployments: [string]: {
	replicas: int | *1
	image:    string
}
deployments: api: {image: "api:1"}
deployments: web: {replicas: 3, image: "web:1"}

apireplicas: 1 が消えた。 パターン制約の既定値と同じなので冗長だったためである。webreplicas: 3 は既定と違うので残る。

cue importcue trim の組み合わせが、既存YAML群のリファクタリングの基本手順である。

① cue import *.yaml           既存YAMLをCUEへ機械変換(この時点では冗長そのまま)
② 共通部分をパターン制約と既定値で書く
③ cue trim ./...              ①の冗長を機械的に削除
④ cue export で元のYAMLと差分が無いことを確認

④の差分確認が重要である。 trim はセマンティクスを保つが、手で書いた共通部分に誤りがあれば出力が変わる。移行時は必ず往復確認する。

10.6 タグによる注入

@tag() を付けたフィールドは、コマンドラインから値を注入できる。

env:      *"dev" | "prod" @tag(env)
replicas: int | *1        @tag(replicas,type=int)
debug:    bool | *false   @tag(debug,type=bool)
$ cue export tagged.cue --out json
{
    "env": "dev",
    "replicas": 1,
    "debug": false
}

$ cue export -t env=prod -t replicas=5 -t debug=true tagged.cue --out json
{
    "env": "prod",
    "replicas": 5,
    "debug": true
}

type= を指定しないと文字列として注入される。 数値や真偽値を注入するときは必須である。

関連する機構が2つある。

@if() — ファイル単位の条件付き取り込み

@if(debug)
package config

debugSettings: {verbose: true}
$ cue export -t debug ./...       # このファイルが取り込まれる
$ cue export ./...                # 取り込まれない

-T / --inject-vars — システム変数の注入

now:  string @tag(now,var=now)
user: string @tag(user,var=username)
$ cue export -T config.cue

注意 — システム変数の注入は評価結果を非決定的にする。cue export の出力をコミットする運用(GitOps)では避けるべきである。

10.7 他形式への出力

package oa

#Service: {
	// The service name
	name: string
	// Listening port
	port:     int & >=1024 & <=65535
	replicas: int | *1
}
$ cue def oa.cue --out openapi
{
    "openapi": "3.0.0",
    "info": {
        "title": "Generated by cue.",
        "version": "no version"
    },
    "paths": {},
    "components": {
        "schemas": {
            "Service": {
                "type": "object",
                "required": ["name", "port", "replicas"],
                "properties": {
                    "name": {
                        "description": "The service name",
                        "type": "string"
                    },
                    "port": {
                        "description": "Listening port",
                        "type": "integer",
                        "minimum": 1024,
                        ...

ドキュメントコメントが description になり、境界制約が minimum / maximum になる。 CUEの定義がそのままOpenAPIスキーマとして公開できる。

主な出力形式:

--out内容
cueCUE(既定、def / eval
jsonJSON
yamlYAML
text文字列そのまま(-e と併用)
openapiOpenAPI 3.0 スキーマ
jsonschemaJSON Schema
protobufProtocol Buffers

--out text は、CUEでテキストファイルを生成する用途に使う。

$ cue export -e 'nginxConf' --out text config.cue > nginx.conf

10.8 fmt

$ cat ugly.cue
a:    1
b: {c:   2}
list: [1,2,3,]

$ cue fmt ugly.cue
$ cat ugly.cue
a: 1
b: {c: 2}
list: [1, 2, 3]

cue fmtgofmt と同じ思想で、設定項目がない。 コロンの位置を揃え、末尾カンマを消す。--simplify を付けると冗長な構造をさらに簡略化する。

CIには cue fmt --check(差分があれば失敗)を入れておくのが定石である。


11. 検証(validation)の設計

11.1 スキーマとデータの分離

CUEでの検証は3つのファイルに分かれる。

// schema.cue — スキーマ
#Config: {
	name:     string & !=""
	replicas: int & >=1 & <=10
	image:    =~"^[a-z0-9./-]+:[a-zA-Z0-9._-]+$"
	env?: [string]: string
}
# good.yaml — 正しいデータ
name: api
replicas: 3
image: registry.example.com/api:1.2.3
env:
  LOG_LEVEL: info
# bad.yaml — 誤りのあるデータ
name: ""
replicas: 42
image: api
env:
  LOG_LEVEL: 3
$ cue vet -d '#Config' schema.cue good.yaml
$ echo $?
0

$ cue vet -d '#Config' schema.cue bad.yaml
env.LOG_LEVEL: conflicting values 3 and string (mismatched types int and string):
    ./bad.yaml:5:14
    ./schema.cue:5:18
name: invalid value "" (out of bound !=""):
    ./schema.cue:2:21
    ./bad.yaml:1:7
    ./schema.cue:2:12
replicas: invalid value 42 (out of bound <=10):
    ./schema.cue:3:24
    ./bad.yaml:2:11
image: invalid value "api" (out of bound =~"^[a-z0-9./-]+:[a-zA-Z0-9._-]+$"):
    ./schema.cue:4:12
    ./bad.yaml:3:8
$ echo $?
1

4つの誤りが同時に報告される。 1つ直して再実行、を繰り返す必要がない。そしてデータ側とスキーマ側の両方の位置が示されるため、「どの制約に、どの行が違反したか」が一目で分かる。

11.2 -d を忘れると黙って通る

これはCIで最も危険な落とし穴である。

$ cue vet schema.cue bad.yaml
$ echo $?
0

エラーが1つも出ず、終了コードも0である。

理由は明快で、#Config定義であり、cue vet はデータをトップレベルの値と単一化する。schema.cue のトップレベルには #Config という定義しか無く、データの name / replicas などに対応する制約が存在しない。定義は「使われなければ何も検証しない」。

対処は2通りある。

方法A: -d でスキーマを明示する(推奨)

$ cue vet -d '#Config' schema.cue bad.yaml

方法B: スキーマをトップレベルに書く

// schema_top.cue
name:     string & !=""
replicas: int & >=1 & <=10
$ cue vet schema_top.cue bad.yaml
name: invalid value "" (out of bound !=""):
    ./schema_top.cue:1:20
    ./bad.yaml:1:7
    ./schema_top.cue:1:11
replicas: invalid value 42 (out of bound <=10):
    ./schema_top.cue:2:23
    ./bad.yaml:2:11
$ echo $?
1

方法Bは -d なしで動くが、スキーマを他の場所から再利用しにくくなる。 実務では方法Aを採り、CIに「-d が指定されていること」を保証する仕組み(Makefileのターゲットに固定するなど)を入れるべきである。

.PHONY: validate
validate:
	cue vet -d '#Config' ./schema.cue ./environments/*.yaml

「CIが緑なのに設定の誤りが本番へ流れた」という事故の典型的な原因がこれである。 検証の仕組みを入れたあと、必ず意図的に壊したデータで exit 1 を確認することを推奨する。

11.3 エラーメッセージの読み方

CUEのエラーは4つのパターンに分類できる。

conflicting values A and B — 具体値の衝突

port: conflicting values 9090 and 8080:
    ./conflict.cue:1:7
    ./conflict.cue:2:7

両方の位置が示される。どちらかを消すか、既定値(*)にする。

field not allowed — 閉じた構造体への追加

p.email: field not allowed:
    ./closed.cue:5:28

定義にフィールドを追加した。 綴り間違いか、スキーマの更新漏れか、... が必要かのいずれかである(§4.3)。

invalid value X (out of bound C) — 制約違反

replicas: invalid value 42 (out of bound <=10):

値が制約の範囲外である。 最も分かりやすいパターン。

incomplete value — 具体化されていない

name: incomplete value string:

export に必要な具体値がない。 値を与えるか、既定値を設定する。

structural cycle — 構造的な循環

§5.3 参照。

メッセージ原因典型的な対処
conflicting values具体値の衝突既定値 * にする、片方を消す
field not allowed閉じた定義への追加埋め込みを使う、... を足す、綴りを直す
out of bound制約違反値かスキーマを直す
incomplete value具体化不足値を与える、* で既定を与える
structural cycle構造的循環片方を ?: にする
mismatched types型の不一致データ側の型を直す(YAMLの引用符など)

mismatched types はYAMLの型推論と衝突していることが多い。 LOG_LEVEL: 3 は整数、LOG_LEVEL: "3" は文字列である。CUEはこの区別を厳格に扱う。


12. スクリプティング層 tool/

12.1 _tool.cuecue cmd

CUEは設定を「生成する」だけでなく「使う」層も持つ。

ファイル名が _tool.cue で終わるファイルにワークフローコマンドを書く。

// gen_tool.cue
package demo

import (
	"tool/cli"
	"tool/file"
	"encoding/yaml"
)

command: dump: {
	print: cli.Print & {
		text: yaml.Marshal(services)
	}
	write: file.Create & {
		$after:   print
		filename: "out.yaml"
		contents: yaml.Marshal(services)
	}
}
$ cue cmd dump
api:
  name: api
  port: 8080
  replicas: 1
web:
  name: web
  port: 8443
  replicas: 3

$ cat out.yaml
api:
  name: api
  port: 8080
  replicas: 1
web:
  name: web
  port: 8443
  replicas: 3

ファイル名が _tool.cue で終わっていないと認識されない。 これは最初につまずく点である。

12.2 タスクと依存関係

公式ドキュメントの説明が仕組みを端的に述べている。

Each command consists of one or more tasks. ... Inputs of tasks may refer to outputs of other tasks. The cue tool does a static analysis of the configuration and only starts tasks that are fully specified. Upon completion of each task, cue rewrites the instance, filling in the completed task, and reevaluates which other tasks can now start, and so on until all tasks have completed.

タスクの実行順序は、データ依存から自動的に決まる。 あるタスクの入力が別のタスクの出力を参照していれば、後者が先に走る。§2 の格子の性質(不完全な値は評価が進まない)が、そのままスケジューリング機構になっている。

明示的に順序を付けたいときは $after を使う。

command: deploy: {
	build: exec.Run & {cmd: "make build"}
	test:  exec.Run & {cmd: "make test", $after: build}
	push:  exec.Run & {cmd: "make push", $after: test}
}

12.3 主なタスク

パッケージタスク用途
tool/cliPrint, Ask標準出力、対話入力
tool/execRun外部コマンド実行
tool/fileRead, Create, Append, Glob, Mkdir, RemoveAllファイル操作
tool/httpDo, Get, PostHTTPリクエスト
tool/osGetenv, Environ, Clearenv, Setenv環境変数

実務的な例を挙げる。

package k8s

import (
	"tool/exec"
	"tool/cli"
	"encoding/yaml"
)

// cue cmd apply
command: apply: {
	for name, obj in objects {
		"apply-\(name)": exec.Run & {
			cmd:    ["kubectl", "apply", "-f", "-"]
			stdin:  yaml.Marshal(obj)
			stdout: string
		}
	}
}

// cue cmd diff
command: diff: {
	for name, obj in objects {
		"diff-\(name)": exec.Run & {
			cmd:   ["kubectl", "diff", "-f", "-"]
			stdin: yaml.Marshal(obj)
		}
	}
}

// cue cmd ls
command: ls: cli.Print & {
	text: strings.Join([for name, _ in objects {name}], "\n")
}

設定の生成と適用が同じリポジトリの同じ言語で書ける。 ただし tool/ 層は Makefile やシェルスクリプトと機能が重複するため、採用するかどうかはチームの好みに依る。

採用の判断基準:

状況推奨
生成した設定を単にファイルへ出したいcue export + Makefile で十分
設定の一部だけを対象に反復処理したいtool/ が有効
タスク間の依存が複雑tool/ が有効(自動スケジューリング)
既存のCIが成熟している無理に置き換えない

13. 他ツールとの比較

13.1 YAML + アンカー

# YAML のアンカーとマージキー
defaults: &defaults
  replicas: 1
  image: api:latest

prod:
  <<: *defaults
  replicas: 5
観点YAML
抽象化アンカー(&/*)とマージキー(<<)のみ
検証なし(JSON Schemaを別途)
制約表現できない
合成の順序依存あり<< は上書き)
学習コスト最小

マージキーはYAML 1.1の拡張で、1.2では標準ではない。 パーサによって挙動が違う。またアンカーは同一ファイル内でしか使えないため、環境ごとにファイルを分ける運用と相性が悪い。

13.2 Helm

# Helm — テキストテンプレート
replicas: {{ .Values.replicas | default 1 }}
{{- if .Values.monitoring.enabled }}
annotations:
  prometheus.io/scrape: "true"
{{- end }}
観点Helm
抽象化Goテンプレート(テキスト置換)
検証values.schema.json(JSON Schema、任意)
構造の理解なし — 文字列を組み立てているだけ
エコシステム極めて大きい(Artifact Hub)

Helmの根本的な性質は「出力がYAMLとして妥当かどうかを知らない」ことである。 {{- if }} の前後の空白やインデントで壊れる。helm template で目視確認する運用が必要になる。

一方でエコシステムの規模はCUEと比較にならない。 サードパーティのチャートを使う場面では、Helmを置き換える選択肢は現実的でない。

現実的な組み合わせ — Helmのチャートはそのまま使い、values.yaml をCUEで生成・検証する。

$ cue export -e 'helmValues' --out yaml > values.yaml
$ helm upgrade --install myapp ./chart -f values.yaml

13.3 Jsonnet

// Jsonnet
local defaults = {replicas: 1, image: "api:latest"};
{
  prod: defaults + {replicas: 5},
}
観点Jsonnet
抽象化関数、オブジェクト合成(+)、継承(::, super
検証なし(アサーションのみ)
なし
合成の順序依存あり+ は右が勝つ)
チューリング完全はい

JsonnetとCUEの最大の違いは、+& の意味である。

Jsonnet:  {a: 1} + {a: 2}  →  {a: 2}    (右が勝つ = オーバーライド)
CUE:      {a: 1} & {a: 2}  →  _|_       (衝突 = エラー)

Jsonnetは関数を持つのでCUEより手続き的な表現力が高い。 一方で「最終値を知るには評価を追う必要がある」という問題を引き継いでいる。

13.4 Dhall / Pkl

Dhall — 完全な型システム(依存型に近い)を持つ関数型言語。チューリング完全ではないことを保証している(正規化が必ず停止する)。表現力は高いが、Haskell風の構文と学習コストがCUEより高い。

Pkl(Apple、2024年公開) — オブジェクト指向的な設定言語。クラス、継承、amends による拡張を持つ。Java/Kotlin/Swift/Go向けのコード生成が充実している。CUEとは思想が対照的で、Pklは継承とオーバーライドを積極的に採用している。

CUEDhallPkl
型システム格子(型と値が同一空間)関数型(依存型に近い)クラスベース
オーバーライドなしなし(関数適用)ありamends
チューリング完全✗(正規化が停止)
検証組み込み型検査組み込み
コード生成Go中心多言語Java/Kotlin/Swift/Go
学習コスト中〜低

13.5 JSON Schema / OpenAPI

JSON Schemaは検証専用であり、設定を生成できない。 CUEは両方できる。

$ cue import jsonschema: schema.json       # JSON Schema → CUE
$ cue def schema.cue --out jsonschema      # CUE → JSON Schema
$ cue def schema.cue --out openapi         # CUE → OpenAPI

双方向の変換ができるため、既存のJSON Schema資産を捨てずに移行できる。 既にJSON Schemaを持っているなら、それをCUEへ取り込んで出発点にするのが最短である。

13.6 総合比較表

観点YAMLHelmJsonnetCUEDhallPkl
データ記述
スキーマ記述
検証
制約(範囲・正規表現)
合成が順序非依存
関数
チューリング完全でない保証
出力形式の多様さYAMLJSON多数多数多数
エコシステム最大最大小〜中
学習コスト最小中〜高

CUEの弱点は関数がないことと、エコシステムの規模である。

CUEに「関数」はないが、パターン制約と単一化で多くの用途は代替できる。

// 関数のように使える定義
#Deployment: {
	_name:    string
	_image:   string
	_replicas: int | *1

	apiVersion: "apps/v1"
	kind:       "Deployment"
	metadata: name: _name
	spec: replicas: _replicas
	spec: template: spec: containers: [{name: _name, image: _image}]
}

// 「呼び出し」
api: #Deployment & {_name: "api", _image: "api:1.0"}
web: #Deployment & {_name: "web", _image: "web:1.0", _replicas: 3}

隠しフィールド(_name など)を「引数」として使うのが定石である。 出力されないので、生成されたYAMLには現れない。


14. 実践パターン

14.1 スキーマ・ポリシー・データの3層

CUEの設計で最も有用な分割がこれである。

schema/     — 構造の定義(何のフィールドがあるか、型は何か)
policy/     — 組織のルール(本番はレプリカ3以上、イメージは社内レジストリのみ)
data/       — 具体値(環境ごとの実際の設定)
// schema/service.cue
package schema

#Service: {
	name:     string
	image:    string
	replicas: int
	env:      "dev" | "stg" | "prod"
	resources: {cpu: string, memory: string}
}
// policy/policy.cue
package policy

import "example.com/cfg/schema"

// 本番の追加要件
#ProdService: schema.#Service & {
	env:      "prod"
	replicas: >=3
	image:    =~"^registry\\.internal\\.example\\.com/"
	resources: {
		cpu:    !="" 
		memory: !=""
	}
}
// data/prod.cue
package data

import "example.com/cfg/policy"

services: [string]: policy.#ProdService
services: api: {
	name:     "api"
	image:    "registry.internal.example.com/api:1.2.3"
	replicas: 5
	resources: {cpu: "500m", memory: "1Gi"}
}

3層に分ける利点:

変更する人変更頻度レビュー基準
schema/プラットフォームチーム低い後方互換性
policy/セキュリティ / SRE低い組織の規約
data/アプリチーム高い値の妥当性

変更頻度とレビュー基準が層ごとに違うため、分けておくとレビューが軽くなる。 data/ のPRは「値が妥当か」だけを見ればよく、スキーマとポリシーは機械が保証する。

14.2 環境オーバーレイ

CUEにオーバーライドがないため、オーバーレイは「既定値を狭める」形で書く。

// base.cue
#Config: {
	replicas: int | *1
	logLevel: "debug" | "info" | "warn" | *"info"
	timeout:  string | *"30s"
	features: [string]: bool
}
// dev.cue
dev: #Config & {
	logLevel: "debug"
	features: newUI: true
}
// → replicas: 1, timeout: "30s"(既定のまま)
// prod.cue
prod: #Config & {
	replicas: 5
	timeout:  "60s"
	features: newUI: false
}
// → logLevel: "info"(既定のまま)

設計上の要点は「どのフィールドを既定値付きにするか」である。

既定値を付ける     →  環境ごとに変わりうる(可変点)
既定値を付けない   →  すべての環境で明示が必要(重要な値)
具体値で固定する   →  変更を禁止(不変)

これが設定の可変点をスキーマとして文書化することになる。「この値は環境ごとに変えてよいのか」という問いに、コードが答える。

14.3 Kubernetesマニフェスト

package k8s

#Deployment: {
	_name:     string
	_image:    string
	_port:     int
	_replicas: int | *1

	apiVersion: "apps/v1"
	kind:       "Deployment"
	metadata: {
		name: _name
		labels: app: _name
	}
	spec: {
		replicas: _replicas
		selector: matchLabels: app: _name
		template: {
			metadata: labels: app: _name
			spec: containers: [{
				name:  _name
				image: _image
				ports: [{containerPort: _port}]
			}]
		}
	}
}

#Service: {
	_name: string
	_port: int

	apiVersion: "v1"
	kind:       "Service"
	metadata: name: _name
	spec: {
		selector: app: _name
		ports: [{port: _port, targetPort: _port}]
	}
}

// アプリごとに1箇所書けばDeployment + Serviceが揃う
apps: [Name=string]: {
	image:    string
	port:     int | *8080
	replicas: int | *1
}
apps: api: {image: "api:1.0", replicas: 3}
apps: web: {image: "web:1.0", port: 8443}

objects: {
	for name, a in apps {
		"\(name)-deploy": #Deployment & {
			_name: name, _image: a.image, _port: a.port, _replicas: a.replicas
		}
		"\(name)-svc": #Service & {
			_name: name, _port: a.port
		}
	}
}
$ cue export -e 'objects' --out yaml

アプリ1つの定義(3行)から、DeploymentとServiceの完全なマニフェストが生成される。 名前は1箇所にしか書かない。

エコシステムのツールも存在する。

ツール内容
TimoniCUEベースのKubernetesパッケージマネージャ(Helmの代替)
DaggerCI/CDエンジン。初期はCUEをDSLに採用していた
Istio内部の設定スキーマ管理にCUEを使用
Grafanaダッシュボードスキーマ(Thema)にCUEを採用
KubeVela一部のスキーマ定義にCUEを採用

設定基盤の内部実装としてCUEを採用する事例も増えている。 CUEを直接書くのはプラットフォームチームだけで、アプリチームはより狭いDSLやYAMLを書き、その検証と生成にCUEを使う、という構成が現実的である。

14.4 アンチパターン

① 既定値を複数の層で宣言する

// base.cue
timeout: string | *"30s"
// region.cue
timeout: string | *"45s"     // ← 既定が競合して両方消える

§3.4 で見たとおり、既定が競合すると既定が消える。既定は最も抽象的な1箇所でのみ宣言する。

② 定義を & で拡張しようとする

§4.3 の通り、field not allowed になる。埋め込みを使う。

③ すべてを1つの巨大な定義にする

CUEの評価は格子の探索であり、巨大な閉じた定義は評価コストが高い。関心ごとに分割し、必要な箇所だけ単一化する。

cue vet-d なしでCIに入れる

§11.2 の通り、黙って通る。必ず壊れたデータで exit 1 を確認する。

⑤ リストを「合成」しようとする

base: tags: ["a", "b"]
prod: base & {tags: ["a", "b", "c"]}    // ← incompatible list lengths

リストは合成されない(§3.5)。追加が必要なら list.Concat を明示する。

import "list"
base: tags: ["a", "b"]
prod: tags: list.Concat([base.tags, ["c"]])

+ によるリスト連結は v0.11 で廃止された。 使うとエラーになる(§15.6)。

⑥ 生成物をコミットせず、常に評価する

CUEの評価は決定的だが速くはない。数千のマニフェストを生成する場合、CIの各段階で cue export を繰り返すと時間がかかる。1度生成してアーティファクトとして受け渡す。


15. 落とし穴12選

本節はすべて cue v0.14.1 で実際に再現した挙動である。

15.1 cue vet-d を忘れると黙って通る

$ cue vet schema.cue bad.yaml      # #Config は定義
$ echo $?
0                                   # ← エラーなし、終了コード0

最も危険な落とし穴。 §11.2 参照。CIに検証を入れたら、必ず壊れたデータで exit 1 を確認する。

15.2 閉じた定義は & で拡張できない

$ cue def d1.cue
#Pod.spec: field not allowed:
    ./d1.cue:2:29
#Pod: #Base & {spec: ...}     // ✗ field not allowed
#Pod: {#Base, spec: ...}      // ✓ 埋め込み

既存フィールドを狭めるのは通り、新しいフィールドの追加は通らない。 §4.3 / §4.4 参照。

15.3 * は論理和の要素位置にしか置けない

$ cue eval def.cue
e: preference mark not allowed at this position:
    ./def.cue:6:16
e: >=1 & <=5 & *3        // ✗
e: (>=1 & <=5) | *3      // ✓

15.4 既定値が競合すると既定が消える

d: (*"x" | "y") & (*"y" | "x")
$ cue eval def3.cue
d: "x" | "y"

エラーにならないので気づきにくい。 後で cue export した時点で「具体的でない」と言われる。既定は1箇所でのみ宣言する。

15.5 リストは合成されない

$ cue eval list.cue
c: incompatible list lengths (2 and 3):
    ./list.cue:3:13

構造体は & で合併するが、リストは長さが一致しなければエラーになる。追加したいなら list.Concat を使う。+ は v0.11 で廃止されており、使うとエラーになる(§15.6)。

[...int] & [1, 2]            // ✓ → [1, 2](開いたリストは受け入れる)
[...string] & ["x", ...]     // ✓ → ["x"]
[1, 2] & [1, 2, 3]           // ✗ incompatible list lengths

15.6 リストの + 連結と繰り返し記法 3*[int] は使えない

+ によるリスト連結は v0.11 で list.Concat に置き換えられ、現在はエラーである。

a: [1, 2] + [3]
$ cue export concat3.cue --out json
a: Addition of lists is superseded by list.Concat; see https://cuelang.org/e/v0.11-list-arithmetic:
    ./concat3.cue:1:4
import "list"
a: list.Concat([[1, 2], [3]])          // → [1, 2, 3]
b: list.Concat([[1,2],[3],[4,5]])      // → [1, 2, 3, 4, 5]

cue.mod/module.cuelanguage.version を古く宣言しても回避できない。 v0.9.0 と宣言しても同じエラーになる(実測)。なお文字列の + はこれまで通り使える。

同様に、リストの繰り返し記法も使えない。

$ cue export list2.cue --out json
e: invalid right-hand value to '*' (type int): 0: incomplete value int:
    ./list2.cue:4:4

古い資料に頻出する記法だが、現在は * が乗算として解釈される。list.Repeat を使う。

import "list"
r: list.Repeat([0], 3)      // [0, 0, 0]

15.7 if は上書きではない

config: {
	replicas: 1
	if env == "prod" { replicas: 5 }
}
$ cue eval ifcond.cue
config.replicas: conflicting values 5 and 1:
    ./ifcond.cue:3:12
    ./ifcond.cue:5:13

条件付きフィールドも & される。 差し替えたいなら既定値にする。§6.4 参照。

15.8 _# の混同

_name: "hidden"      // 出力されない、閉じない、パッケージ外から見えない
#Name: {...}         // 出力されない、閉じる、パッケージ外から見える

# はスキーマ、_ は中間計算。 _ をスキーマに使うとパッケージ外から参照できず、# を中間計算に使うと意図せず閉じてしまう。§4.6 の判断基準を参照。

15.9 cue export ./... が余分な空ドキュメントを出す

$ cue export ./... --out yaml
services:
  api: {...}
---
{}                     # ← schema パッケージ(具体値なし)

./... は各パッケージを別インスタンスとして評価する。 対象を明示する。

$ cue export . --out yaml

15.10 ツールファイルは _tool.cue で終わらなければならない

gen_tool.cue    ✓ 認識される
tool.cue        ✗ 認識されない
tools.cue       ✗ 認識されない

cue cmd <name> が「コマンドが見つからない」と言うときは、まずファイル名を確認する。

15.11 構造的な循環はエラー、数値の循環は無言で残る

a: b + 1
b: a - 1
$ cue eval cyc1.cue
a: b + 1
b: a - 1                   # ← エラーではない。ただ解決されない
a: {b: c}
c: {d: a}
$ cue eval cyc3.cue
c.d: structural cycle       # ← こちらはエラー

数値の循環はエラーにならないので、export の段階まで気づかない。 §5.3 参照。

15.12 バージョン間の非互換

CUEは v0.x であり、以下は実際に変わってきた項目である。

項目変化
リストの連結 [a] + [b]廃止(v0.11、list.Concat へ)
リストの繰り返し n*[T]廃止(list.Repeat へ)
モジュール機構v0.9 でOCIレジストリベースに刷新
cue.mod/module.cuelanguage.version フィールドが追加
f!:(必須フィールド)v0.5 で追加
@embed()(ファイル埋め込み)比較的新しい機能
クローズネスの詳細な意味論複数回調整されている

対策は2つある。

cue.mod/module.cuelanguage.version を明示する。 CUEはこの値を見て振る舞いを決めるため、本体をアップグレードしても評価結果が変わらない。

module: "example.com/demo"
language: {
	version: "v0.14.1"
}

② CIでCUEのバージョンを固定する。 cue version の出力をCIログに残しておくと、挙動が変わったときの切り分けが早い。


16. まとめ

16.1 CUEを1枚にまとめる

CUEの中核 = 「値の格子」+「単一化」

  すべての値は1つの半順序集合の点である
     _(top)        ← あらゆる値
     int             ← 整数すべて
     >5 & <10        ← 6〜9
     7               ← 具体値
     _|_(bottom)   ← エラー

  & は「両方を満たす最も抽象的な値」を返す
     → 可換・結合・冪等 → 書く順序が結果に影響しない
     → オーバーライドが存在しない
     → 衝突はエラーになる

  | は「どちらかを満たす」を返す
     → 列挙型になる
     → * で既定の枝を選べる = 唯一の「上書き可能」宣言

  # は定義(definition)
     → 出力されない
     → 既定で閉じている = 未知フィールドを弾く
     → 拡張は埋め込みで行う(& では拡張できない)

16.2 採用の判断

CUEが向いている状況:

状況理由
環境・リージョン・テナントで設定が多次元に増えている単一化で層を合成でき、順序を気にしなくてよい
設定の誤りが本番障害につながるスキーマと制約で機械的に検証できる
「この値はどこで決まっているのか」の調査に時間を取られているオーバーライドがないので、言及箇所を集めれば答えになる
既にJSON SchemaやOpenAPIの資産がある双方向に変換できる
設定基盤を自前で作っているプラットフォームの内部実装として適している

CUEが向いていない状況:

状況理由
設定が小規模で単純学習コストが見合わない
サードパーティのHelmチャートが中心エコシステムの差が大きい
手続き的な生成ロジックが必要関数がないため無理が出る
チームに言語を学ぶ余力がない単一化の理解が前提になる

16.3 段階的な導入手順

リスクの低い順に3段階ある。

第1段階: 検証だけ導入する(既存ファイルを1行も変えない)
  ① 既存YAMLに対するスキーマをCUEで書く
  ② cue vet -d '#Schema' schema.cue **/*.yaml をCIに入れる
  ③ 壊れたデータで exit 1 になることを確認する   ← 必須
     → この時点で「型の誤り」「範囲外の値」が本番に出なくなる

第2段階: 一部の生成をCUEに移す
  ④ cue import で既存YAMLをCUEへ機械変換
  ⑤ 共通部分をパターン制約と既定値で書く
  ⑥ cue trim で冗長を削除
  ⑦ cue export の出力が元のYAMLと一致することを確認   ← 必須
     → この時点で「同じ値を何箇所にも書く」がなくなる

第3段階: スキーマ・ポリシー・データの3層に整理する
  ⑧ 組織のルールを policy/ として切り出す
  ⑨ 層ごとにレビュー基準とオーナーを決める
     → この時点で「規約違反がレビューをすり抜ける」がなくなる

第1段階だけでも十分な価値がある。 既存の資産を変更せず、CIに1コマンド足すだけで型の誤りが止まる。

16.4 学習の順序

CUEを学ぶ際、概念の理解順序が効率を大きく変える。

順序学ぶことなぜこの順か
1& は上書きではないすべての誤解の根源がここにある
2|*(既定値)「上書きしたい」という要求への唯一の答え
3# と閉じた構造検証の強さがここで決まる
4埋め込み& で拡張できないことへの答え
5パターン制約 [string]: T実務で最も使う
6内包表記データ変換に必要
7eval / export / vet / def の違い「なぜエラーになるか」の理解に必要
8モジュールと標準ライブラリ規模が大きくなってから
9tool/任意。必要になったら

1〜4を理解すれば、CUEのエラーメッセージのほとんどが読めるようになる。


17. 参考情報

項目場所
公式サイトhttps://cuelang.org
言語仕様https://cuelang.org/docs/reference/spec/
チュートリアルhttps://cuelang.org/docs/tour/
標準ライブラリhttps://pkg.go.dev/cuelang.org/go/pkg
tool/ パッケージhttps://cuelang.org/go/pkg/tool
リポジトリhttps://github.com/cue-lang/cue
Central Registryhttps://registry.cue.works
コミュニティhttps://cuelang.org/community/(Slack、Discourse)
Timoni(K8sパッケージマネージャ)https://timoni.sh
Daggerhttps://dagger.io
Pkl(比較対象)https://pkl-lang.org
Dhall(比較対象)https://dhall-lang.org

検証に使った環境

$ cue version
cue version v0.14.1

go version go1.24.6
   -compiler gc
     GOARCH arm64
       GOOS darwin
cue.lang.version v0.14.1

本ガイドに掲載したすべてのCUEコードと、$ から始まるコンソール出力は、上記の環境で実際に実行して得たものである。 バージョンが違う場合、特に §15.6(リストの繰り返し記法)と §9.3(モジュール機構)の挙動は異なる可能性がある。