このチュートリアルは、CUEプロジェクトが提供する原文を、AIアシスタントが日本語に翻訳したものです。技術的な正確性を保ちつつ、日本語として自然な表現になるよう努めました。ただし、翻訳の過程で誤りや不明確な点が生じている可能性があります。もし疑問点や改善点がございましたら、原文を参照いただくか、CUEプロジェクトの公式ドキュメントをご確認ください。
この翻訳が、日本語を母語とする開発者の方々にとって、CUEとKubernetesの理解を深める一助となれば幸いです。
翻訳日:2023年7月22日
はじめに
このチュートリアルでは、マイクロサービスのコレクションに対するKubernetes設定ファイルの変換方法を示します。
設定ファイルは、実際の設定ファイルから機密情報を削除し、名前を変更したものです。
ファイルは、関連するサービスをサブディレクトリにグループ化したディレクトリ階層で整理されています。
これは一般的なパターンです。
cue
ツールはこのユースケースに最適化されています。
このチュートリアルでは、以下のトピックを扱います:
- 与えられたYAMLファイルをCUEに変換する
- 共通のパターンを親ディレクトリに移動する
- ツールを使用してCUEファイルを書き換え、不要なフィールドを削除する
- 異なるサブディレクトリに対してステップ2を繰り返す
- 設定を操作するためのワークフローコマンドを定義する
- Kubernetes GoソースからCUEテンプレートを直接抽出する
- 設定を手動で調整する
- Kubernetes設定を
docker-compose
にマッピングする(TODO)
このチュートリアルのコンテキスト
このチュートリアルのディレクトリに含まれるデータセットは、実際のケースに基づいており、サービスに異なる名前を使用しています。
実際のセットアップのすべての不整合が、実践的にCUEへの変換がどのように動作するかの現実的な印象を得るために、ファイルに複製されています。
コンテキストに慣れる
与えられたYAMLファイルは様々なディレクトリにわたって整理されています。次のようにして、どのようなファイルが存在するかを確認できます:
terminal
find ./original -type f
これにより、以下のような出力が表示されます(ここでは省略しています):
./original/services/frontend/bartender/kube.yaml
./original/services/frontend/breaddispatcher/kube.yaml
./original/services/frontend/host/kube.yaml
./original/services/frontend/maitred/kube.yaml
./original/services/frontend/valeter/kube.yaml
./original/services/frontend/waiter/kube.yaml
./original/services/frontend/waterdispatcher/kube.yaml
./original/services/infra/download/kube.yaml
./original/services/infra/etcd/kube.yaml
./original/services/infra/events/kube.yaml
[ ... 省略 ... ]
各サブディレクトリには、しばしば類似した特性と設定を共有する関連するマイクロサービスが含まれています。
設定には、サービス、デプロイメント、コンフィグマップ、デーモンセット、ステートフルセット、クーロンジョブなど、幅広いKubernetesオブジェクトが含まれています。
最初のチュートリアルの結果は、「quick」(クイック&ダーティ)ディレクトリにあります。
手動で最適化された設定は「manual」ディレクトリにあります。
既存の設定のインポート
データディレクトリのコピーを作成する
構造の変更を開始する前に、データディレクトリのコピーを作成しましょう:
terminal
cp -a original tmp
cd tmp
CUEモジュールを初期化する
terminal
cue mod init
CUEモジュールを初期化することで、サブディレクトリ内のすべての設定ファイルを1つのパッケージの一部として扱うことができます。
後で、すべてに同じパッケージ名を付けることでこれを実現します。
モジュールを作成することで、パッケージが外部パッケージをインポートできるようにもなります。
Goモジュールを初期化する
terminal
go mod init mod.test
Goモジュールを初期化することで、後でk8s.io/api/apps/v1
のGoパッケージ依存関係を解決できるようになります。
試行1:YAMLファイルを単一のCUEパッケージにインポートする
cue import
コマンドを使用して、与えられたYAMLファイルをCUEに変換し、kube
パッケージに含めてみましょう:
terminal
cd services
cue import ./... -p kube
複数のパッケージとファイルがあるため、それらが属すべきパッケージ(-p kube
)を指定する必要がありました。
これにより、以下のようなエラーメッセージが出力されます:
path, list, or files flag needed to handle multiple objects in file ./services/frontend/bartender/kube.yaml
このエラーメッセージは、多くのファイルに複数のKubernetesオブジェクトが含まれているために表示されています。
さらに、すべてのファイルからすべてのオブジェクトを含む単一の設定を作成しようとしています。
試行2:動的オブジェクトアドレスを使用してYAMLファイルをインポートする
単一の設定内で各Kubernetesオブジェクトを個別に識別できるように整理する必要があります。
これを行うには、各タイプに対して異なる構造体を定義し、各オブジェクトをそれぞれの構造体に名前でキー付けします。
これにより、Kubernetesで許可されているように、異なるタイプのオブジェクトが同じ名前を共有できます。
これを実現するために、cue
に各オブジェクトを設定ツリーの"kind"を最初の要素、"name"を2番目の要素とするパスに配置するよう指示します。
terminal
cue import ./... -p kube -l 'strings.ToCamel(kind)' -l metadata.name -f
追加された-l
フラグは、各オブジェクトの値に基づいて各オブジェクトのラベルを定義し、フィールドラベルにCUEの通常の構文を使用します。
この場合、各オブジェクトのkind
フィールドのキャメルケースバリアントと、metadata
セクションのname
フィールドを各オブジェクトの名前として使用します。
また、以前に成功した少数のファイルを上書きするために-f
フラグを追加しました。
何が起こったか見てみましょう。以下のコマンドを実行します:
terminal
find . -type f
これにより、以下のような出力が表示されます:
./frontend/bartender/kube.yaml
./frontend/bartender/kube.cue
./frontend/breaddispatcher/kube.yaml
./frontend/breaddispatcher/kube.cue
./frontend/host/kube.yaml
./frontend/host/kube.cue
./frontend/maitred/kube.yaml
./frontend/maitred/kube.cue
./frontend/valeter/kube.yaml
./frontend/valeter/kube.cue
[ ... 省略 ... ]
各YAMLファイルが対応するCUEファイルに変換されています。
YAMLファイルのコメントは保持されています。
しかし、結果は完全に満足のいくものではありません。
mon/prometheus/configmap.cue
を見てみましょう。
mon/prometheus/configmap.cue
package kube
configMap: prometheus: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: name: "prometheus"
data: {
"alert.rules": """
groups:
- name: rules.yaml
[ ... 省略 ... ]
設定ファイルには、フィールドの1つの文字列値に埋め込まれたYAMLがまだ含まれています。
元のYAMLファイルはすべて構造化されたデータのように見えたかもしれませんが、その大部分は(うまくいけば)有効なYAMLを含む文字列でした。
試行3:埋め込み文字列処理を使用してYAMLファイルをインポートする
cue import
の-R
オプションを使用して、設定ファイルに埋め込まれた構造化YAMLまたはJSON文字列を検出し、再帰的に変換しようとします:
terminal
cue import ./... -p kube -l 'strings.ToCamel(kind)' -l metadata.name -f -R
インポートされたCUEファイルを再確認してみましょう:
mon/prometheus/configmap.cue
package kube
import yaml656e63 "encoding/yaml"
configMap: prometheus: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: name: "prometheus"
data: {
"alert.rules": yaml656e63.Marshal(_cue_alert_rules)
[ ... 省略 ... ]
これでよくなりました!
結果の設定ファイルは、元の埋め込み文字列をyaml.Marshal
の呼び出しに置き換え、構造化されたCUEソースを同等のYAMLファイルを含む文字列に変換しています。
CUE出力を検証する
アンダースコア(_
)で始まるフィールドは、設定ファイルを出力する際に含まれません(フィールド名が二重引用符で囲まれている場合を除く)。
cue import
の-R
オプションを使用する際に追加された_cue_alert_rules
フィールドがcue
の出力に存在しないことを確認しましょう:
terminal
cue eval ./mon/prometheus -e configMap.prometheus
これにより、以下のような出力が表示されます:
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: "prometheus"
}
data: {
"alert.rules": """
groups:
- name: rules.yaml
rules:
[ ... 省略 ... ]
やった!
クイック&ダーティな変換
このチュートリアルでは、一連の設定からボイラープレートを迅速に排除する方法を示します。
手動での調整は通常より良い結果をもたらしますが、かなりの思考を必要とします。一方で、クイック&ダーティなアプローチを取ると、ほとんどの目標を達成できます。
このような迅速な変換の結果は、より思慮深い手動最適化の良い基礎にもなります。
トップレベルテンプレートの作成
上記のYAMLファイルをインポートした後、簡素化プロセスを開始できます。
参照コピーを保存する
構造の変更を開始する前に、完全な評価を保存しましょう(これにより、簡素化が同じ結果をもたらすことを確認できます):
terminal
cue eval -c ./... >snapshot
-c
オプションは、具体的な値(つまり有効なJSON)のみが許可されることをcue
に指示します。
テンプレートの作成を開始する
様々なkube.cue
ファイルで定義されたオブジェクトに焦点を当てます。
簡単な調査により、多くのDeploymentとServiceが共通の構造を共有していることがわかります。
両方を含むファイルの1つをコピーして、テンプレートの作成の基礎とし、ディレクトリツリーのルートに配置します。
terminal
cp frontend/breaddispatcher/kube.cue .
このファイルを以下のように修正します。
./kube.cue
package kube
service: [ID=_]: {
apiVersion: "v1"
kind: "Service"
metadata: {
name: ID
labels: {
app: ID // 慣例による
domain: "prod" // 与えられたファイルでは常に同じ
component: string // ディレクトリごとに異なる
}
}
spec: {
// どのポートも以下のプロパティを持つ
ports: [...{
port: int
protocol: *"TCP" | "UDP" // Kubernetesの定義から
name: string | *"client"
}]
selector: metadata.labels // これらは同じであることを望む
}
}
deployment: [ID=_]: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: ID
spec: {
// 1がデフォルトだが、任意の数を許可する
replicas: *1 | int
template: {
metadata: labels: {
app: ID
domain: "prod"
component: string
}
// 常に1つの同名コンテナを持つ
spec: containers: [{name: ID}]
}
}
}
serviceとdeploymentの名前を[ID=_]
に置き換えることで、定義を任意のフィールドにマッチするテンプレートに変更しました。
その結果、CUEはフィールド名をID
に束縛します。
インポート時にmetadata.name
をオブジェクト名のキーとして使用したので、このフィールドをID
に設定できます。
テンプレートは定義されている構造体のすべてのエントリに適用される(統合される)ので、breaddispatcher
定義に特有のフィールドを削除するか、一般化するか、または削除する必要があります。
Kubernetesのメタデータで定義されているラベルの1つは、常に親ディレクトリ名に設定されているようです。
component: string
を定義することでこれを強制し、つまりcomponent
という名前のフィールドが何らかの文字列値に設定されなければならないことを意味し、後でこれを定義します。
不完全に指定されたフィールドは、たとえばJSONに変換する際にエラーを引き起こします。
したがって、deploymentまたはserviceは、このラベルが定義されている場合にのみ有効になります。
テンプレートをテストする
新しいテンプレートをマージした結果を元のスナップショットと比較してみましょう:
terminal
cue eval -c ./... >snapshot2
このコマンドは失敗し、以下のような(省略された)エラーメッセージが表示されます:
// :kube
deployment.alertmanager.spec.template.metadata.labels.component: incomplete value string:
./kube.cue:36:16
service.alertmanager.metadata.labels.component: incomplete value string:
./kube.cue:11:15
service.alertmanager.spec.selector.component: incomplete value string:
./kube.cue:11:15
// :kube
service."node-exporter".metadata.labels.component: incomplete value string:
./kube.cue:11:15
[ ... 省略 ... ]
おっと。
アラートマネージャーがcomponent
ラベルを指定していません。
これは、制約を使用して設定の不整合を捕捉する方法を示しています。
テンプレートを修正する
このラベルを指定していないオブジェクトはごくわずかなので、すべての場所でこのラベルを含むように設定を変更します。
これを行うには、新しく定義したトップレベルフィールドを各ディレクトリのディレクトリ名に設定し、マスターテンプレートファイルを変更してそれを使用するようにします。
以下のスクリプトを実行して、ファイルをインラインで編集し、各ディレクトリにいくつかのCUEファイルを追加します:
terminal
# componentラベルを新しいトップレベルフィールドに設定する
sed -i.bak 's/component:.*string/component: #Component/' kube.cue
rm kube.cue.bak
# 新しいトップレベルフィールドを以前のテンプレート定義に追加する
cat <<EOF >> kube.cue
#Component: string
EOF
# componentラベルを含むファイルを各ディレクトリに追加する
ls -d */ | sed 's/.$//' | xargs -I DIR sh -c 'cd DIR; echo "package kube
#Component: \"DIR\"
" > kube.cue; cd ..'
# ファイルをフォーマットする
cue fmt kube.cue */kube.cue
テンプレートを再度テストする
テンプレートが修正されたか確認しましょう:
terminal
cue eval -c ./... >snapshot2
diff -w --side-by-side snapshot snapshot2
これにより、snapshot2
のラベルがより一貫性を持ち、一部の順序変更があることを除いて、snapshot
とsnapshot2
の間に実質的な変更がないことがわかります。
結果を新しいベースラインとして保存する
terminal
cp snapshot2 snapshot
ボイラープレートを削除する
テンプレートの内容によって暗示されるようになった多くのボイラープレート設定を、cue trim
を使用して削除できます。
まず、ボイラープレート削除前の全kube.cue
ファイルの総行数を確認します:
terminal
find . | grep kube.cue | xargs wc -l | tail -1
これにより、以下のような結果が表示されます:
1887 total
次に、cue trim
を使用してボイラープレートを削除し、残っている行数を再確認します:
terminal
cue trim ./...
find . | grep kube.cue | xargs wc -l | tail -1
大幅な削減が見られるはずです:
1312 total
cue trim
は、テンプレートや包括表現によってすでに生成されている設定をファイルから削除します。
これにより、500行以上の設定、つまり30%以上が削除されました!
ボイラープレート削除を確認する
意味的に何も変更されていないことを確認します:
terminal
cue eval -c ./... >snapshot2
diff -wu snapshot snapshot2 | wc -l
報告される行数は0になるはずです。
テンプレートを改善する
さらに改善できます。
最初に注目すべき点は、DaemonSetとStatefulSetがDeploymentと類似した構造を共有していることです。
_spec
の必要性を排除するために、トップレベルテンプレートを以下のように一般化します。kube.cue
に追加します:
terminal
cat <<EOF >> kube.cue
daemonSet: [ID=_]: _spec & {
apiVersion: "apps/v1"
kind: "DaemonSet"
_name: ID
}
statefulSet: [ID=_]: _spec & {
apiVersion: "apps/v1"
kind: "StatefulSet"
_name: ID
}
deployment: [ID=_]: _spec & {
apiVersion: "apps/v1"
kind: "Deployment"
_name: ID
spec: replicas: *1 | int
}
configMap: [ID=_]: {
metadata: name: ID
metadata: labels: component: #Component
}
_spec: {
_name: string
metadata: name: _name
metadata: labels: component: #Component
spec: selector: {}
spec: template: {
metadata: labels: {
app: _name
component: #Component
domain: "prod"
}
spec: containers: [{name: _name}]
}
}
EOF
cue fmt
共通の設定が_spec
に抽出されました。
オブジェクトの名前を指定および参照するために_name
を導入しました。
完全を期すために、configMap
をトップレベルエントリとして追加しました。
deployment
の古い定義をまだ削除していないことに注意してください。
これは問題ありません。
新しい定義と同等なので、それらを統合しても効果はありません。
その削除は読者の演習として残しておきます。
テンプレートをさらに改善する
次に、すべてのdeployment、stateful set、およびdaemon setには、多くの同じフィールドを共有する付随するserviceがあることに注目します。
再びkube.cue
のテンプレートに追加します:
terminal
cat <<EOF >> kube.cue
// _exportオプションを定義し、すべてのコンテナで定義されたすべてのポートのデフォルトをtrueに設定する
_spec: spec: template: spec: containers: [...{
ports: [...{
_export: *true | false // ポートをサービスに含める
}]
}]
for x in [deployment, daemonSet, statefulSet] for k, v in x {
service: "\(k)": {
spec: selector: v.spec.template.metadata.labels
spec: ports: [
for c in v.spec.template.spec.containers
for p in c.ports
if p._export {
let Port = p.containerPort // Portはエイリアス
port: *Port | int
targetPort: *Port | int
},
]
}
}
EOF
cue fmt
この例では、いくつかの新しい概念を導入しています。
開放型リストは省略記号(...
)で示されます。
省略記号の後の値は、後続の要素と統合され、追加のリスト要素の"タイプ"またはテンプレートを定義します。
Port
宣言はエイリアスです。
エイリアスはその字句スコープでのみ可視であり、モデルの一部ではありません。
エイリアスは、ネストされたスコープ内で隠れたフィールドを可視にするため、または、この場合、新しいフィールドを導入せずにボイラープレートを減らすために使用できます。
最後に、この例ではリストとフィールドの包括表現を導入しています。
リストの包括表現は、他の言語で見られるリスト内包表記に類似しています。
フィールドの包括表現により、構造体にフィールドを挿入できます。
この場合、フィールドの包括表現は、deployment、daemonSet、およびstatefulSetごとに同名のサービスを追加します。
フィールドの包括表現は、条件付きでフィールドを追加するためにも使用できます。
targetPort
を指定する必要はありませんが、多くのファイルでそれが定義されているため、ここで定義することでcue trim
を使用してそれらの定義を削除できるようになります。
コンテナで定義されたポートに対して_export
オプションを追加し、ポートをサービスに含めるかどうかを指定します。そして、infra/events
、infra/tasks
、およびinfra/watcher
の各ポートに対してこれを明示的にfalseに設定します。
このチュートリアルの目的のために、以下のクイックパッチを適用します:
terminal
cat <<EOF >>infra/events/kube.cue
deployment: events: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
EOF
cat <<EOF >>infra/tasks/kube.cue
deployment: tasks: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
EOF
cat <<EOF >>infra/watcher/kube.cue
deployment: watcher: spec: template: spec: containers: [{ ports: [{_export: false}, _] }]
EOF
実際には、元のポート宣言にこのフィールドを追加するのがより適切な方法です。
テンプレートの改善を確認する
すべての変更が受け入れ可能であることを確認し、別のスナップショットを保存します。
その後、trim
を実行してさらに設定を削減します:
terminal
cue trim ./...
find . | grep kube.cue | xargs wc -l | tail -1
書き直され、今や冗長になったdeployment定義を削除した後、テンプレートを追加したにもかかわらず、さらに約100行を削減しました。
ほとんどのファイルでservice定義が消えていることを確認できます。
残っているのは、追加の設定か、おそらくクリーンアップされるべき不整合です。
単一要素の構造体を折りたたむ
もう一つのテクニックがあります。
-s
または--simplify
オプションを使用すると、trim
またはfmt
に単一の要素を持つ構造体を1行に折りたたむよう指示できます。
例えば、frontend/breaddispatcher/kube.cue
の先頭を確認してください:
terminal
head frontend/breaddispatcher/kube.cue
これは以下のように表示されるはずです:
package kube
deployment: breaddispatcher: {
spec: {
template: {
metadata: {
annotations: {
"prometheus.io.scrape": "true"
"prometheus.io.port": "7080"
}
次に、cue trim
の-s
フラグを使用します:
terminal
cue trim ./... -s
head -7 frontend/breaddispatcher/kube.cue
... これにより、以下のように表示されるはずです:
package kube
deployment: breaddispatcher: spec: template: {
metadata: annotations: {
"prometheus.io.scrape": "true"
"prometheus.io.port": "7080"
}
設定全体での行数の削減を確認します:
terminal
find . | grep kube.cue | xargs wc -l | tail -1
さらに150行ほど削減されました!
このように行を折りたたむことで、かなりの量の句読点を削除し、設定の読みやすさを向上させることができます。
結果を新しいベースラインとして保存する
terminal
cue eval -c ./... >snapshot2
cp snapshot2 snapshot
複数のサブディレクトリに対して繰り返す
前のセクションでは、ディレクトリ構造のルートにserviceとdeploymentのテンプレートを定義して、すべてのserviceとdeploymentの共通の特性を捉えました。
さらに、ディレクトリ固有のラベルを定義しました。
このセクションでは、ディレクトリごとのオブジェクトの一般化について検討します。
frontend
ディレクトリをテンプレート化する
frontend
のサブディレクトリにあるすべてのdeploymentには、1つのポートを持つ単一のコンテナがあり、通常は7080
ですが、時々8080
であることに気づきます。
また、ほとんどが2つのprometheus関連のアノテーションを持っていますが、一部は1つだけです。
ポートの不整合は残しますが、両方のアノテーションを無条件に追加します。
terminal
cat <<EOF >> frontend/kube.cue
deployment: [string]: spec: template: {
metadata: annotations: {
"prometheus.io.scrape": "true"
"prometheus.io.port": "\(spec.containers[0].ports[0].containerPort)"
}
spec: containers: [{
ports: [{containerPort: *7080 | int}] // 7080がデフォルト
}]
}
EOF
cue fmt ./frontend
# 差分を確認する
cue eval -c ./... >snapshot2
diff -wu snapshot snapshot2
これにより、以下のような結果が表示されるはずです...
--- snapshot 2022-02-21 06:04:10.919832150 +0000
+++ snapshot2 2022-02-21 06:04:11.907780310 +0000
@@ -188,6 +188,7 @@
metadata: {
annotations: {
"prometheus.io.scrape": "true"
+ "prometheus.io.port": "7080"
}
labels: {
app: "host"
@@ -327,6 +328,7 @@
metadata: {
annotations: {
"prometheus.io.scrape": "true"
+ "prometheus.io.port": "8080"
}
labels: {
app: "valeter"
唯一の違いは、2行にアノテーションが追加され、一貫性が向上したことです。
trim操作を行い、設定全体での行数の削減を確認します:
terminal
cue trim ./frontend/... -s
find . | grep kube.cue | xargs wc -l | tail -1
さらに40行ほど削減されました。
これまでのような大幅な削減に慣れてしまったかもしれませんが、この時点では削除するものがほとんど残っていません:frontendファイルの一部では、残っている設定はわずか4行だけです。
結果を新しいベースラインとして保存する
terminal
cue eval -c ./... >snapshot2
cp snapshot2 snapshot
kitchen
ディレクトリをテンプレート化する
このディレクトリでは、すべてのdeploymentが例外なく1つのコンテナとポート8080
を持ち、すべて同じliveness probeを持ち、1行のprometheusアノテーションを持ち、ほとんどが類似したパターンで2つまたは3つのディスクを持っていることに気づきます。
ディスク以外のすべてを追加しましょう:
terminal
cat <<EOF >> kitchen/kube.cue
deployment: [string]: spec: template: {
metadata: annotations: "prometheus.io.scrape": "true"
spec: containers: [{
ports: [{
containerPort: 8080
}]
livenessProbe: {
httpGet: {
path: "/debug/health"
port: 8080
}
initialDelaySeconds: 40
periodSeconds: 3
}
}]
}
EOF
cue fmt ./kitchen
差分を取ると、1つのprometheusアノテーションがサービスに追加されたことがわかります。
これは偶然の脱落だと仮定し、差分を受け入れます。
ディスクは、テンプレートのspec部分とそれを使用するコンテナの両方で定義する必要があります。
これら2つの定義を一緒に保持したいと思います。
expiditer
(2つのディスクを持つそのディレクトリの最初の設定)からvolumes定義を取り、一般化します:
terminal
cat <<EOF >> kitchen/kube.cue
deployment: [ID=_]: spec: template: spec: {
_hasDisks: *true | bool
// "if"のみを使用したフィールド包括表現
if _hasDisks {
volumes: [{
name: *"\(ID)-disk" | string
gcePersistentDisk: pdName: *"\(ID)-disk" | string
gcePersistentDisk: fsType: "ext4"
}, {
name: *"secret-\(ID)" | string
secret: secretName: *"\(ID)-secrets" | string
}, ...]
containers: [{
volumeMounts: [{
name: *"\(ID)-disk" | string
mountPath: *"/logs" | string
}, {
mountPath: *"/etc/certs" | string
name: *"secret-\(ID)" | string
readOnly: true
}, ...]
}]
}
}
EOF
cat <<EOF >> kitchen/souschef/kube.cue
deployment: souschef: spec: template: spec: {
_hasDisks: false
}
EOF
cue fmt ./kitchen/...
このテンプレート定義は理想的ではありません:定義は位置依存であるため、設定が異なる順序でディスクを定義した場合、再利用がないか、または競合さえ起こる可能性があります。
また、この制限に対処するために、ほとんどすべてのフィールド値がデフォルト値であり、インスタンスによってオーバーライドできることに注意してください。
より良い方法は、トップレベルのKubernetesオブジェクトを整理したのと同様に、volumesのマップを定義し、そこからこれら2つのセクションを生成することです。
ただし、これにはある程度の設計が必要であり、"クイック&ダーティ"なチュートリアルには適していません。
このドキュメントの後半で、手動で最適化された設定を紹介します。
デフォルトで2つのディスクを追加し、オプトアウトするための_hasDisks
オプションを定義します。
souschef
設定は、ディスクを定義しない唯一のものです。
トリムして差分を確認する
terminal
cue trim -s ./kitchen/...
# 差分を確認する
cue eval -c ./... >snapshot2
diff -wu snapshot snapshot2
cp snapshot2 snapshot
find . | grep kube.cue | xargs wc -l | tail -1
差分を見ると、_hasDisks
オプションが追加されましたが、それ以外の違いは見られません。
また、再び大幅に設定が削減されました。
ただし、残りのファイルをよく見ると、一貫性のない命名の結果として、ディスク指定に多くのフィールドが残っていることがわかります。
このような方法で設定を削減すると、不整合が露呈します。
不整合は、特定の設定でオーバーライドを単に削除することで取り除くことができます。
そのままにしておくと、設定が一貫していないことを明確に示すシグナルになります。
クイック&ダーティなチュートリアル:結論
他のディレクトリでもまだ改善の余地があります。
1000行近く、つまり55%の削減を達成しましたが、残りは読者の演習として残しておきます。
CUEを使用してボイラープレートを削減し、一貫性を強制し、不整合を検出する方法を示しました。
一貫性と不整合の両方に対処できることは、制約ベースのモデルの結果であり、継承ベースの言語では実現が難しいものです。
間接的に、CUEが機械的な操作に適していることも示しました。
これは、構文と、その意味論から導かれる順序独立性の産物です。
trim
コマンドは、この特性によって可能になる多くの自動化されたリファクタリングツールの一つです。
これも、継承ベースの設定言語では実現が難しいでしょう。
ワークフローコマンドの定義
cue export
コマンドを使用して、作成した設定をJSONに変換することができます。
この場合、マップされたKubernetesオブジェクトをリストに戻すために、トップレベルの「emit value」が必要です。
通常、この出力はkubectl
やetcdctl
などのツールにパイプで渡されます。
実際には、これは同じコマンドを何度も繰り返し入力することを意味します。
次のステップは、しばしばラッパーツールを作成することです。
しかし、一般的に一つのサイズですべてに対応する解決策がないため、これは限定的に有用なツールの増殖につながります。
cue
ツールは、頻繁に使用されるコマンドをCUE自体で宣言できるようにすることで、代替手段を提供します。
利点:
- CUEが改善された分析に使用できる追加のドメイン知識
- 学ぶべき言語は1つだけ
- ワークフローコマンドの簡単な発見
- それ以上の設定は不要
- ワークフローコマンド全体で統一されたCLI標準の強制
- 組織全体で標準化されたワークフローコマンド
ワークフローコマンドは、ワークフローコマンドが操作すべき設定ファイルと同じパッケージにある_tool.cue
で終わるファイルで定義されます。
設定のトップレベル値は、ツールファイルのトップレベルフィールドによって隠されない限り、ツールファイルから見えます。
ツールファイルのトップレベルフィールドは設定ファイルからは見えず、モデルの一部でもありません。
ツール定義には、追加の組み込みパッケージへのアクセスもあります。
CUE設定は完全に独立しており、外部からの影響を一切許可しません。
この特性により、trim
コマンドのような自動化された分析と操作が可能になります。
しかし、ツール定義は、コマンドラインフラグや環境変数、乱数生成器、ファイルリストなどにアクセスできます。
この例では、以下のツールを定義します:
-
ls
: 設定で定義されたKubernetesオブジェクトをリストする -
dump
: 選択されたすべてのオブジェクトをYAMLストリームとしてダンプする -
create
: 選択されたすべてのオブジェクトをkubectl
に送信して作成する
準備
Kubernetesを扱うには、Kubernetesオブジェクトのマップを単純なリストに戻す必要があります。
kube_tool.cue
ファイルを作成する
kube_tool.cue
package kube
objects: [ for v in objectSets for x in v {x}]
objectSets: [
service,
deployment,
statefulSet,
daemonSet,
configMap,
]
オブジェクトのリスト
ワークフローコマンドは、ツールファイルのトップレベルのcommand
セクションで定義されます。
cue
ワークフローコマンドは、コマンドラインフラグ、環境変数、および一連のタスクを定義します。
タスクの例としては、ファイルの読み込みや書き込み、コンソールへのダンプ、ウェブページのダウンロード、コマンドの実行などがあります。
ls
ワークフローコマンドを定義する
ls
ワークフローコマンドは、すべてのオブジェクトをダンプします。これをls_tool.cue
ファイルに配置します:
ls_tool.cue
package kube
import (
"text/tabwriter"
"tool/cli"
"tool/file"
)
command: ls: {
task: print: cli.Print & {
text: tabwriter.Write([
for x in objects {
"\(x.kind) \t\(x.metadata.labels.component) \t\(x.metadata.name)"
},
])
}
task: write: file.Create & {
filename: "foo.txt"
contents: task.print.text
}
}
注意:タスク定義のAPIは変更される予定です。
必要に応じて、この形式のサポートを維持する可能性はあります。
ls
ワークフローコマンドをテストする
ls
ワークフローコマンドがcue
ツールで使用可能になったことを確認します:
terminal
cue cmd ls ./frontend/maitred
これにより、以下の出力が表示されます:
Service frontend maitred
Deployment frontend maitred
複数のインスタンスが選択された場合、cue
ツールはそれらを1つずつ操作するか、マージすることができます。
デフォルトはマージです。
パッケージの異なるインスタンスは通常互換性がありません:
異なるサブディレクトリには異なる特殊化がある可能性があります。
マージは、各インスタンスのテンプレートを事前に展開し、その後でルート値をマージします。
結果には、トップレベルの#Component
フィールドのような競合が含まれる可能性がありますが、タイプごとのKubernetesオブジェクトのマップは競合がないはずです(競合がある場合、Kubernetesに問題があります)。
したがって、マージによってすべてのオブジェクトの統一されたビューが得られます。
terminal
cue cmd ls ./...
これにより、以下のような出力が表示されるはずです...
Service frontend bartender
Service frontend breaddispatcher
Service frontend host
Service frontend maitred
Service frontend valeter
Service frontend waiter
Service frontend waterdispatcher
Service infra download
Service infra etcd
Service infra events
[ ... 省略 ... ]
YAMLストリームのダンプ
dump
ワークフローコマンドを定義する
以下は、選択されたオブジェクトをYAMLストリームとしてダンプするワークフローコマンドを追加します。
dump_tool.cue
package kube
import (
"encoding/yaml"
"tool/cli"
)
command: dump: {
task: print: cli.Print & {
text: yaml.MarshalStream(objects)
}
}
MarshalStream
関数は、オブジェクトのリストを---
で区切られたYAML値のストリームに変換します。
オブジェクトの作成
create
ワークフローコマンドを定義する
create
ワークフローコマンドは、オブジェクトのリストをkubectl create
に送信します。
create_tool.cue
package kube
import (
"encoding/yaml"
"tool/exec"
"tool/cli"
)
command: create: {
task: kube: exec.Run & {
cmd: "kubectl create --dry-run=client -f -"
stdin: yaml.MarshalStream(objects)
stdout: string
}
task: display: cli.Print & {
text: task.kube.stdout
}
}
このワークフローコマンドには、kube
とdisplay
という2つのタスクがあります。
display
タスクはkube
タスクの出力に依存します。
cue
ツールは依存関係の静的解析を行い、依存関係が満たされたすべてのタスクを並行して実行し、入力が欠けているタスクをブロックします。
create
ワークフローコマンドをテストする
以下を実行します:
terminal
cue cmd create ./frontend/...
... これにより、以下のような出力が表示されます:
service/bartender created (dry run)
service/breaddispatcher created (dry run)
service/host created (dry run)
service/maitred created (dry run)
service/valeter created (dry run)
service/waiter created (dry run)
service/waterdispatcher created (dry run)
deployment.apps/bartender created (dry run)
deployment.apps/breaddispatcher created (dry run)
deployment.apps/host created (dry run)
[ ... 省略 ... ]
もちろん、実際の本番環境版では--dry-run=client
フラグを省略するべきです。
Kubernetes GoソースからCUEテンプレートを直接抽出する
Kubernetes CUEスキーマを生成する
cue get go
がGoソースからCUEテンプレートを生成するためには、事前にソースをローカルに用意する必要があります。
terminal
go get k8s.io/api/apps/v1@v0.23.4
cue get go k8s.io/api/apps/v1
Kubernetes CUEスキーマを使用する
これでKubernetesの定義がモジュールに含まれたので、それらをインポートして使用できます。k8s_defs.cue
ファイルを作成します:
k8s_defs.cue
package kube
import (
"k8s.io/api/core/v1"
apps_v1 "k8s.io/api/apps/v1"
)
service: [string]: v1.#Service
deployment: [string]: apps_v1.#Deployment
daemonSet: [string]: apps_v1.#DaemonSet
statefulSet: [string]: apps_v1.#StatefulSet
最後に、CUEファイルをフォーマットします:
terminal
cue fmt
手動で調整された設定
「クイック&ダーティ」セクションでは、CUEをすぐに使い始める方法を示しました。
もう少し慎重に検討することで、設定をさらに削減することができます。
また、より汎用的で、Kubernetesに特化しない設定を定義したいと思います。
CUEの順序独立性を大いに活用します。これにより、同じオブジェクトの2つの設定を明確に定義された方法で簡単に組み合わせることができます。
例えば、頻繁に使用されるフィールドを1つのファイルに、より特殊なフィールドを別のファイルに配置し、一方が他方を上書きする心配なくそれらを組み合わせることが容易になります。
このセクションではこのアプローチを採用します。
このチュートリアルの最終結果は、トップレベルのmanual
ディレクトリにあります。
次のセクションでは、そこに至る方法を示します。
概要
設定の基本的な前提は、シンプルで抽象的な設定と、Kubernetesと互換性のある設定の2つを維持することです。
Kubernetes版は、シンプルな設定から自動的に生成されます。
各簡略化されたオブジェクトには、変換時にKubernetesオブジェクトにマージされるkubernetes
セクションがあります。
汎用的な定義を含む1つのトップレベルファイルを定義します。
package cloud
service: [Name=_]: {
name: *Name | string // サービスの名前
...
// 変換時にKubernetesにマージされるKubernetes固有のオプション
kubernetes: {
}
}
deployment: [Name=_]: {
name: *Name | string
...
}
次に、Kubernetes固有のファイルには、汎用オブジェクトをKubernetesに変換するための定義が含まれます。
全体として、サービスをモデル化するコードとKubernetesコードを生成するコードは分離されていますが、一般的なモデルにKubernetes固有のデータを注入することも可能です。
同時に、Kubernetesを壊すことなく、モデルに追加情報を加えることができます。
抽象的な設定と具体的な設定を分離することで、同じ設定に対して異なるアダプターを作成することもできます。
Deployment定義
設計上、すべてのKubernetes Podの派生物が1つのコンテナのみを定義すると仮定します。
これは一般的には明らかに当てはまりませんが、多くの場合そうであり、良い実践です。
都合よく、これによって我々のモデルも簡略化されます。
モデルは、「クイック&ダーティ」セクションで導出したマスターテンプレートを大まかに基にしています。
最初のステップは、statefulSet
とdaemonSet
を排除し、代わりに異なる種類を許可するdeployment
だけを持つことです。
deployment: [Name=_]: _base & {
name: *Name | string
...
kindは、deploymentがstateful setまたはdaemonsetである場合にのみ指定する必要があります。
これにより、_spec
の必要性もなくなります。
次のステップは、image
などの共通フィールドをトップレベルに引き上げることです。
引数はマップとして指定できます。
arg: [string]: string
args: [ for k, v in arg { "-\(k)=\(v)" } ] | [...string]
順序が重要な場合、ユーザーは明示的にリストを指定することもできます。
ポートについては、名前からポート番号へのシンプルなマップを2つ定義します:
// expose portは、サービスで公開される名前付きポートを定義します
expose: port: [string]: int
// portは、サービスで公開されない名前付きポートを定義します
port: [string]: int
両方のマップがコンテナ定義で定義されますが、port
のみがサービス定義に含まれます。
これが最良のモデルではなく、すべての機能をサポートしているわけではありませんが、異なる表現を選択する方法を示しています。
環境変数についても同様です。
ほとんどの場合、文字列から文字列へのマッピングで十分です。
ただし、テストデータは他のオプションも使用しています。
シンプルなenv
マップと、より詳細なケース用のenvSpec
を定義します:
env: [string]: string
envSpec: [string]: {}
envSpec: {
for k, v in env {
"\(k)" value: v
}
}
シンプルなマップは自動的により詳細なマップにマッピングされ、全体像を示します。
最後に、deploymentごとに1つのコンテナがあるという仮定により、volume specとvolume mountの情報を組み合わせた単一のボリューム定義を作成できます。
volume: [Name=_]: {
name: *Name | string
mountPath: string
subPath: null | string
readOnly: bool
kubernetes: {}
}
定義したい他のすべてのフィールドは、生成された他のすべてのKubernetesデータとマージされる汎用的なkubernetes構造体に入れることができます。
これにより、コンテナに追加のフィールドを追加するなど、生成されたデータを拡張することさえ可能です。
Service定義
サービス定義は直接的です。
statefulとdaemon setを排除したので、サービスを自動的に導出するためのフィールド包括表現は少しシンプルになりました:
// deploymentによって暗示されるサービスを定義する
service: {
for k, spec in deployment {
"\(k)": {
// コンテナから公開されたすべてのポートをコピーする
for Name, Port in spec.expose.port {
port: "\(Name)": {
port: *Port | int
targetPort: *Port | int
}
}
// ラベルをコピーする
label: spec.label
}
}
}
完全なトップレベルのモデル定義は、
doc/tutorial/kubernetes/manual/services/cloud.cueにあります。
このプロジェクト固有の調整(ラベル)は
ここで定義されています。
Kubernetesへの変換
サービスの変換は比較的直接的です。
kubernetes: services: {
for k, x in service {
"\(k)": x.kubernetes & {
apiVersion: "v1"
kind: "Service"
metadata: name: x.name
metadata: labels: x.label
spec: selector: x.label
spec: ports: [ for p in x.port { p } ]
}
}
}
Kubernetesのボイラープレートを追加し、トップレベルのフィールドをマッピングし、各サービスの生のkubernetes
フィールドをマージします。
deploymentのマッピングはもう少し複雑ですが、類似しています。
Kubernetes変換の完全な定義は
doc/tutorial/kubernetes/manual/services/k8s.cueにあります。
トップレベルの定義を具体的なKubernetesコードに変換することは、この演習の中で最も難しい部分です。
とはいえ、ほとんどのCUEユーザーは設定を書くためにこのレベルのCUEを使用する必要はありません。
例えば、サブディレクトリ内のファイルには包括表現が含まれていません。これらのディレクトリ内のテンプレートファイル(kitchen/kube.cue
など)でさえそうです。
さらに、リーフディレクトリの設定ファイルには文字列補間が含まれていません。
メトリクス
完全に記述された手動設定はmanual
ディレクトリにあります。
manual
ディレクトリの総行数を確認します:
terminal
find manual | grep kube.cue | xargs wc -l | tail -1
報告される542行には変換テンプレートは含まれていません。
トップレベルのテンプレートが再利用可能であると仮定し、両方のアプローチでそれらをカウントしない場合、手動アプローチはさらに約150行を削減します。
テンプレートもカウントすると、2つのアプローチはほぼ同等です。
手動設定:結論
テンプレートファイルを手動で最適化することで、設定をさらにコンパクトにできることを示しました。
しかし、手動最適化は、クイック&ダーティな半自動削減と比較して、わずかな利点しかないことも示しました。
手動定義の利点は主に、得られる組織的な柔軟性にあります。
設定を手動で調整することで、論理的な定義とKubernetes固有の定義の間に抽象化レイヤーを作成できます。
同時に、CUEの順序独立性により、必要で適切な場所に低レベルのKubernetes設定を簡単に混ぜ込むことができます。
手動調整により、Kubernetesを壊すことなく独自の定義を追加することもできます。
これは、定義に関連するが、Kubernetesとは無関係な情報を、適切な場所に定義する上で重要です。
抽象的な設定と具体的な設定を分離することで、同じ設定に対して異なるアダプターを作成することもできます。