こんにちは.Kubernetes班のryota-kaです。皆さんKubernetesやっていますか?*1
Kubernetesをやっていこうとすると,大量のYAMLを書くことになって大変である.大量のYAMLを書くことは大変なので,大抵コピペする.コピペをするが,コピペは怖い.例えば,deploymentを定義するYAMLをコピペしたとして,万一labelを変更するのを忘れて,想定していないserviceからルーティングされたりすると悲惨である.そもそも(個人的な意見ではあるが)YAMLの仕様自体があまりにも複雑かつ難解であり*2,ある種の仕様が余計なお世話だともしばしば感ぜられる*3.
最近ではkustomizeが将来的にkubectlに統合される予定で開発が進められているが,この記事では異なる切り口として,Dhall langを用いてプログラマティックにYAMLファイルを生成するという方法をご紹介したい.
Dhallについて#
Dhallは,かのGabriel Gonzalez氏が中心になって開発している設定記述言語である.READMEによると"tall"/"call"/"hall"と韻を踏むことから,アメリカ英語では/dɔl/,イギリス英語では/dɔːl/と発音するそうだ.カタカナを当てはめてみるとすると「ドール」だろうか.
GitHubのdhall-lang/dhall-langでは以下のような説明がなされている.
A configuration language guaranteed to terminate
Dhall is a programmable configuration language that is not Turing-complete You can think of Dhall as: JSON + functions + types + imports
Dhallの特徴的な性質として,チューリング完全でないことが挙げられる.Dhallは設定ファイルを記述するための言語であり,汎用プログラミング言語ではないので,高い計算能力を必要としないのだ.雑に言ってしまうと,再帰や無限ループを書けない.チューリング完全でないことの嬉しさとして,評価が停止することが保証できる.雑に言ってしまうと,再帰や無限ループが書けないのでちゃんと停止する.理論が好きな方への説明も申し添えておくと,Dhallにおけるすべての式は一意な正規形を持つ.
Nixを使っていれば,以下のコマンドでインストールできる.
$ nix-env -iA nixpkgs.dhall nixpkgs.dhall-json
環境を汚したくなければもちろんnix-shellを利用してもよい.
$ nix-shell -p dhall dhall-json
executableとしてdhall, dhall-to-json, dhall-to-yamlが利用できることを確認してほしい.
dhall replでREPLが起動する.
$ dhall repl
⊢ 1 + 1
2
ghciと同じく,:typeまたは:tで,与えられた式の型を表示する.
⊢ :t 42
Natural
Dhall expression入門#
あまり詳細に解説すると,それだけで一つの記事になってしまうので,この記事中で用いるものの説明のみで済ませる.より詳しく知りたい方はGitHub Wikiを参照のこと.基礎的な文法について詳しく解説することはしないが,このブログの読者であれば,HaskellないしOCamlの基本的な文法を心得ていると思うので,特に問題ないだろう*4.
Bool型#
リテラルFalseおよびTrueはBool型をもつ.
⊢ :t False
Bool
Natural型#
符号なしの数値リテラルはNatural型をもつ.
⊢ :t 42
Natural
ちなみに符号を付ければInteger型.このあたりの表記は最近破壊的変更があった.
Text型#
ダブルクォートで囲む文字列リテラルはText型をもつ.
⊢ :t "Dhall"
Text
Nix形式の複数行文字列リテラルも提供されている.
$ dhall <<EOF
heredoc> ''
heredoc> Dhall is a programmable configuration language that is not Turing-complete
heredoc>
heredoc> You can think of Dhall as: JSON + functions + types + imports
heredoc> ''
heredoc> EOF
Text
''
Dhall is a programmable configuration language that is not Turing-complete
You can think of Dhall as: JSON + functions + types + imports
''
${...}によるinterpolationもサポートされている.
⊢ let name = "ryota-ka" in "Hello, ${name}!"
"Hello, ryota-ka!"
文字列の結合は++演算子.
⊢ "Hello, " ++ "world!"
"Hello, world!"
List型#
ブラケットで囲むとList型になる.
⊢ :t [6, 28, 496]
List Natural
リストの結合は#演算子.
⊢ [0, 1, 2] # [3, 4]
[ 0, 1, 2, 3, 4 ]
Optional型#
Optional型のリテラルは,リストと同じでブラケットを用いる.リテラルを共有している都合上,型注釈が必須となっている.型注釈はvalue : Typeという形式で書く.
⊢ [42] : Optional Natural
[ 42 ] : Optional Natural
⊢ [] : Optional Natural
[] : Optional Natural
関数型#
関数は以下のような構文で表される.引数の型注釈は必須.
⊢ :t λ(n : Natural) → n * 2
∀(n : Natural) → Natural
適用はもちろんスペース.
⊢ let double = λ(n : Natural) → n * 2 in double 42
84
Type型#
BoolやNatural,Listなどの型も型をもつ.「型の型」はHaskellなどでは「族」と呼ばれ区別されているものだが,Dhallではこれらも立派に値の型として作用する.後で見るが,型を値のコンテクストで利用できるからである.
例えば,BoolはType型をもつ.
⊢ :t Bool
Type
Natural → NaturalもType型をもつ.
⊢ :t Natural → Natural
Type
Listは具体型を一つ受け取ると具体型になるので,Type → Typeという型をもつ.
⊢ :t List
Type → Type
「型を値のコンテクストで利用できる」ことを確かめる.以下の例では,値である変数xが型注釈の後ろ,すなわち型として出現していることから,値を型として利用できることがわかる.
⊢ let x = Bool in False : x
False
型を値として扱えるので,もちろん型を引数として取るような関数も定義できる.以下はSomeとでも名付けられそうな関数の例である*5.
⊢ :t λ(t : Type) → λ(x : t) → [x] : Optional t
∀(t : Type) → ∀(x : t) → Optional t
レコード型#
レコード型は0個以上のkey-valueの組である.
⊢ :t { name = "ryota-ka", age = 25 }
{ name : Text, age : Natural }
空のレコードの値は,空のレコードの型{}と区別するため,{=}と書く.
⊢ :t {=}
{}
⫽演算子を用いて,複数のレコードをマージすることができる.同じkeyが存在する場合には,右側のものが優先される.
⊢ { foo = 42 } ⫽ { bar = "Hello", baz = {=} } ⫽ { foo = "Yo" }
{ foo = "Yo", bar = "Hello", baz = {=} }
union型#
任意個の型の和を表す型は次のように記述する.
⊢ <Left = 42 | Right : Text> : < Left : Natural | Right : Text>
< Left = 42 | Right : Text >
⊢ <Left : Natural | Right = "foo"> : < Left : Natural | Right : Text>
< Right = "foo" | Left : Natural >
慣例的にLeft, Rightというタグを用いたが,任意の名前を使うことができる.
⊢ < Top : Natural | Middle : Bool | Bottom : Text>
< Top : Natural | Middle : Bool | Bottom : Text >
少々冗長だが,constructorsキーワードを利用することで記述性が改善されるが,今回は割愛する.
ファイルインポート#
他のファイルに書かれたDhall expressionを読み込みたい場合はどうすればよいだろうか.Dhallでは,絶対パスや相対パスもexpressionとして扱われ,パスが指し示すファイルの内容をDhall expressionとして解釈した式として扱われる.
例えば,cube.dhallに以下の内容が記述されているとする.
$ cat cube.dhall
let cube = λ(n : Natural) → n * n * n in cube
この状態で,./cube.dhallはvalidなDhall expressionであり,上述の関数が式の値となる.
λ(n : Natural) → n * n * n
⊢ ./cube.dhall
λ(n : Natural) → n * n * n
⊢ ./cube.dhall 5
125
また,Dhallはdistributedであることを謳っており,ファイルパスだけではなく,URLを用いた参照も行える.
⊢ https://raw.githubusercontent.com/dhall-lang/Prelude/master/List/all
λ(a : Type)
→ λ(f : a → Bool)
→ λ(xs : List a)
→ List/fold a xs Bool (λ(x : a) → λ(r : Bool) → f x && r) True
⊢ https://raw.githubusercontent.com/dhall-lang/Prelude/master/List/all Natural Natural/odd [3, 5, 7, 11, 13]
True
Kubernetesの設定を記述する - deploymentとserviceを例に#
さて,ここからは実際にDhallを用いて,Kubernetesの設定を記述するYAMLファイルを作っていこう.
まず,記述したいサービスを表現するデータ型を用意する.管理したいサービスは,基本的にすべてこのデータ型で記述することを目標としたい.今回は簡単なデータ型だが,必要に応じて拡張すればよい.
また,この型をもつ値を別のファイルに定義する.今回はNginxを動かすことを想定してみよう.
OurServiceからdeploymentを生成する式は以下のようになる.Deployment型はここで定義されている.
一方serviceはこんな感じ.Service型の定義はここにある.
./deployment.yaml.dhallは∀(svc : OurService) → Deployment,./service.yaml.dhallは∀(svc : OurService) → Service なる関数である.nginx.dhall に定義されているOurService型の値に適用することで,Deployment型及びService型の値を得ることができる.
dhall-to-yamlを用いて,これらの値をYAMLに出力してみよう../deployment.yaml.dhall ./nginx.dhallという式は関数適用であることに注意されたい.
dhall-to-yaml --omitNull <<< './deployment.yaml.dhall ./nginx.dhall' > ./dist/nginx.deployment.yaml
dhall-to-yaml --omitNull <<< './service.yaml.dhall ./nginx.dhall' > ./dist/nginx.service.yaml
以下のようなYAMLが得られる.
これらのYAMLをKubernetesに食わせると,Nginxが動く.私はDocker for MacのKubernetesで試した.
$ kubectl apply -f ./nginx.deployment.yaml -f ./nginx.service.yaml
$ curl localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
これだけの例ではあまり恩恵を感じることができないかもしれないが,管理されるべきYAMLファイルが大量かつ膨大になってしまった際に,独自の型や関数・値の定義,ファイルの分割,型検査などの恩恵が得られるのはありがたい.
おわりに#
今回は煩雑なYAML管理や,それを解決しようとする本流の解法に対するある種のアンチ・テーゼとしてDhallを紹介した.Dhall自体は比較的新しいツールセットであり,現状広く普及しているわけでもない.実際にKubernetesのYAMLの管理をこれだけでまかないきれるのかと問われると,「試していないのでわからない」というのが正直なところである*6.しかしながら,安全性や分散性に気を遣いつつ,設定記述言語に必要なだけの表現力を持ちながらもチューリング完全とではないという分を弁えた言語というコンセプトは目新しく興味深いものであるし,その有用性が十分伺えるものである.しかも,新しいツールセットといえども,今回紹介したdhall-to-yamlの他に,JSONを出力するdhall-to-json,テンプレートエンジンたるdhall-to-text,Bashに変換するdhall-to-bash,Cabalファイルを出力するdhall-to-cabal,Haskellバインディングなどを備え,既に幅広いツールが提供されている.
今回はKubernetesを例に取ったが,設定記述のためにJSONやYAMLを書かされるシチュエーションは世の中に溢れ返っている.とりわけCIのタスクの記述などは,スキーマが存在し,かつプログラマティックに記述できればどれだけありがたいだろうかと常々思うものだが,Dhallはこのような状況を解決する可能性を秘めている.このような有用なツールセットが広く普及することが期待される.
記事中のコードGitHubからどうぞ.
脚注#
*1: この冒頭挨拶は「kubernetesに自分のコードがマージされるまでのフロー」のオマージュです.
*2: 一体どれだけの実装がYAMLの仕様を満たしているというのか!
*3: "yes"を真だとみなしたりだとか.
*4: 問題がある方はやはり公式のGitHub Wikiを読むとよい.
*5: 実際に同名の関数がPreludeで定義されている.
*6: 恵比寿の方のとある会社では実戦に投入しているという噂を耳にした
