初めに
前回の記事では、Crossplane の抽象 API を XRD、XR、Composition の三つで整理しました。
XRD は利用者向け API の型とスキーマを定義し、XR は利用者の要求を表し、Composition はその要求を実装 resource へ変換するレシピ、という話でした。
今回は、その Composition の中身に入ります。
中心になるのは、次の三つです。
Patch & Transform
Composition Functions
Pipeline mode
Composition を初めて読むと、compositeTypeRef、mode: Pipeline、functionRef、input、resources、patches が一気に出てきます。
ここを YAML の細部から読むと迷いやすいです。
まずは、Composition が何をしていて、どこから先は Provider の仕事なのかを分けて見ると理解しやすくなります。
この記事のゴール
この記事のゴールは、次の流れを自分の言葉で説明できるようになることです。
XR の spec
-> Composition pipeline
-> Function が desired resources を作る
-> Crossplane が composed resources を apply する
-> Provider が Managed Resource を reconcile する
-> OCI resource が作られる、または更新される
特に、次の判断軸を持てるようにします。
単純な field mapping
-> Patch & Transform で十分
条件分岐、繰り返し、fan-out、複雑な生成 logic
-> Composition Functions を検討する
ここで重要なのは、Composition Functions が OCI API を直接操作するわけではない、という点です。
Function は desired resources を作ります。
その後、Managed Resource を Provider が reconcile します。
Composition は「外部 API を呼ぶ処理」ではない
まず、Composition の位置づけを確認します。
Composition は、XR を受け取り、そこからどの Kubernetes resource を作るかを決めます。
たとえば、利用者が次のような XR を作ったとします。
apiVersion: platform.example.org/v1alpha1
kind: AppBucket
metadata:
name: demo
spec:
team: team-a
tier: standard
objectStorageNamespace: my-oci-namespace
compartmentId: ocid1.compartment.oc1..example
この XR は、利用者向けの小さな要求です。
これを OCI Object Storage Bucket の Managed Resource に変換するのが Composition の役割です。
ただし、Composition が OCI API を直接呼ぶわけではありません。
Composition が作るのは、あくまで Kubernetes resource としての desired state です。
Composition
-> Managed Resource の desired state を作る
Provider
-> Managed Resource を見て OCI API と状態合わせする
この境界を分けておくと、debug するときにも迷いにくくなります。
Composition で見るのは「どんな resource を作ろうとしているか」です。
Provider で見るのは「その resource が外部 API と同期できているか」です。
Pipeline mode は Function を順番に呼ぶ形
現在の Composition では、mode: Pipeline を使って Function を呼び出す形が中心です。
概念的には、次のようになります。
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: appbucket-oci
spec:
compositeTypeRef:
apiVersion: platform.example.org/v1alpha1
kind: AppBucket
mode: Pipeline
pipeline:
- step: patch-and-transform
functionRef:
name: function-patch-and-transform
input:
apiVersion: pt.fn.crossplane.io/v1beta1
kind: Resources
resources:
- name: bucket
base:
apiVersion: objectstorage.oci.upbound.io/v1alpha1
kind: Bucket
spec:
forProvider:
compartmentId: ""
name: ""
namespace: ""
ここで最初に見るべきなのは、compositeTypeRef です。
compositeTypeRef は、この Composition がどの XR に対応するかを示します。
次に、mode: Pipeline と pipeline を見ます。
pipeline の各 step は Function を呼び出します。
functionRef.name が、呼び出す Function の名前です。
XR
|
v
step 1: function-patch-and-transform
|
v
desired resources
Function が複数ある場合は、定義された順番に呼び出されます。
各 Function は、前の Function が作った desired state を受け取り、必要に応じて追加や更新をして次に渡します。
Function は事前に install する
Composition で functionRef を書くだけでは、Function が使えるようになるわけではありません。
Function は Crossplane の package として install します。
Patch & Transform を使う場合は、たとえば次のような Function resource を用意します。
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-patch-and-transform
spec:
package: xpkg.crossplane.io/crossplane-contrib/function-patch-and-transform:<version>
実際に使う version は、利用している Crossplane と公式ドキュメントに合わせて確認してください。
ここで大事なのは、Composition の functionRef.name と Function resource の metadata.name が対応していることです。
Composition 側
functionRef.name: function-patch-and-transform
Function 側
metadata.name: function-patch-and-transform
Composition を読んでいて functionRef が出てきたら、まずその Function が install 済みか、HEALTHY になっているかを確認します。
Patch & Transform は XR の値を Managed Resource へ写す
Patch & Transform は、XR の field を composed resource の field へコピーし、必要なら値を変換するための仕組みです。
公式ドキュメントでも、patch は一つの resource から別の resource へ値をコピーし、transform は patch 前に値を加工すると説明されています。
たとえば、XR の spec.compartmentId を OCI Bucket の spec.forProvider.compartmentId へ渡す場合は、次のように読みます。
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.compartmentId
toFieldPath: spec.forProvider.compartmentId
読み方は、そのままです。
Composite Resource の spec.compartmentId から値を取り、
composed resource の spec.forProvider.compartmentId へ入れる
OCI Bucket の例にすると、次のような対応になります。
| XR 側 | Managed Resource 側 |
|---|---|
spec.compartmentId |
spec.forProvider.compartmentId |
spec.objectStorageNamespace |
spec.forProvider.namespace |
spec.team、metadata.name
|
spec.forProvider.name |
Bucket 名のように、複数の値を組み合わせたい場合は CombineFromComposite が使えます。
patches:
- type: CombineFromComposite
combine:
variables:
- fromFieldPath: spec.team
- fromFieldPath: metadata.name
strategy: string
string:
fmt: "%s-%s"
toFieldPath: spec.forProvider.name
この例では、team-a と demo から team-a-demo のような名前を作り、Bucket の name に渡すイメージです。
実際の命名規則では、長さ、利用可能文字、重複、環境名なども考慮します。
そのため、ここでは仕組みを理解するための例として見てください。
Transform は値を整えるために使う
Patch は値を渡す操作です。
Transform は、渡す前に値を整える操作です。
代表的には、次のような使い方があります。
| Transform | 使いどころ |
|---|---|
map |
standard を具体的な設定値へ変換する |
match |
文字列や pattern に応じて値を選ぶ |
string |
format を使って名前や tag を作る |
convert |
型を変換する |
math |
数値を加工する |
たとえば、利用者には standard、premium のような tier だけを選んでもらい、実装側では provider 固有の値に変換したい場合があります。
そのような場面では、map transform が読みやすいです。
次の YAML は、Transform の考え方を示すための概念例です。
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.tier
toFieldPath: spec.forProvider.exampleTier
transforms:
- type: map
map:
standard: standard-value
premium: premium-value
このように、利用者向け API の言葉と provider 側の field 値を分けられるのが Patch & Transform の良いところです。
ただし、provider の実際の field 名や許容値は release によって変わる可能性があります。
実際の manifest では、必ず provider の CRD、examples、quickstart を確認してください。
Patch & Transform が向いている場面
Patch & Transform は、単純な対応関係を宣言的に書く場合に向いています。
たとえば、次のような処理です。
XR の region を Managed Resource の region に渡す
XR の team と name から resource 名を作る
XR の tier を provider 固有の値に map する
Managed Resource の値を XR の status に戻す
この範囲であれば、YAML を読めば何が起きているか追いやすいです。
platform team 以外の人が review するときも、field path と transform を見れば意図を確認できます。
一方で、Patch & Transform に何でも詰め込むと、Composition が読みづらくなります。
特に、次のような兆候が出てきたら注意が必要です。
条件分岐が増える
配列を見て resource 数を変えたい
複数 resource をまたいだ判断が増える
同じ pattern の resource を大量に生成したい
validation だけでは表現しにくい business rule が増える
このあたりから、Composition Functions を検討する価値が出てきます。
Composition Functions は複雑な生成 logic の逃がし先
Composition Functions は、Composition の処理を pipeline step として拡張する仕組みです。
Crossplane は Function に RunFunctionRequest を送り、Function は RunFunctionResponse で desired state を返します。
公式ドキュメントでは、Function が受け取る主な情報として、observed state、desired state、Function input、pipeline context が説明されています。
イメージは次のようになります。
Crossplane
-> observed state
-> desired state
-> input
-> context
Function
-> updated desired state
ここでいう observed state は、XR と既存の composed resources の現在状態です。
desired state は、pipeline が最終的に作りたい resource 群です。
Function の主な仕事は、この desired state を更新して Crossplane に返すことです。
その後、Crossplane が desired state を Kubernetes API Server に apply します。
OCI API と状態合わせするのは、その後の Provider です。
Function
-> desired resources を作る
Crossplane
-> desired resources を apply する
Provider
-> Managed Resource を OCI API と reconcile する
この切り分けを押さえておくと、Function を「クラウド API を直接叩く plugin」と誤解しにくくなります。
Pipeline では desired state が積み上がる
Function pipeline では、各 step が desired state を受け取り、必要な変更を加えて次に渡します。
たとえば、次のような構成が考えられます。
step 1: resource template を作る
step 2: naming や labels を補う
step 3: readiness を調整する
公式ドキュメントでは、pipeline の desired state は積み上がるものとして説明されています。
そのため、Function を自作する場合は、自分が追加したい resource だけでなく、前の step から来た desired state をどう扱うかも重要です。
ただし、最初に学ぶ段階では、細かい SDK 実装よりも次の理解で十分です。
Pipeline は Function を順番に呼ぶ
各 Function は desired state を更新する
最後に残った desired state が apply 対象になる
Patch & Transform も、この pipeline の中で動く Function の一つとして見ると分かりやすいです。
Reference、Selector、Connection Details も設計対象になる
Composition が一つの Bucket だけを作る場合は、field mapping を見れば理解しやすいです。
しかし、実際の Platform API では、複数の resource が関係することが多いです。
たとえば、OCI Bucket を中心に考えても、次のような要素が出てきます。
Bucket
Compartment
IAM policy
Secret
tag
利用者へ返す出力
このとき、単に resource を作るだけでは足りません。
どの resource がどの resource を参照するのか、既存 resource をどう選ぶのか、利用者にどの情報を返すのかを決める必要があります。
Managed Resource の CRD には、xxxRef や xxxSelector のような field が用意されていることがあります。
OCI Bucket の CRD でも、compartmentIdRef や compartmentIdSelector のような field が確認できます。
直接値を渡す
compartmentId
別 resource を名前で参照する
compartmentIdRef
label などで対象を選ぶ
compartmentIdSelector
Connection Details は、利用者に返す接続情報や出力を整理するための出口です。
たとえば endpoint、username、password、bucket 名、namespace など、利用者が後続処理で必要とする値をどう渡すかを考えます。
ここを曖昧にすると、XR は作れても、利用者が次に何を使えばよいのか分からなくなります。
Platform API では、resource の作成だけでなく、利用までの流れを含めて Composition を読むことが重要です。
Patch & Transform と Functions の使い分け
使い分けは、次のように考えると分かりやすいです。
| やりたいこと | 向いている方法 |
|---|---|
| XR の field を Managed Resource へ渡す | Patch & Transform |
| tier や region を provider 固有値へ変換する | Patch & Transform |
| 複数 field から名前や tag を作る | Patch & Transform |
| 条件分岐が多い | Composition Functions |
| 配列から resource を複数生成する | Composition Functions |
| business rule を code で表現したい | Composition Functions |
| 複数 step に処理を分けたい | Composition Functions |
最初から Function に寄せすぎると、YAML だけでは挙動が見えにくくなります。
逆に、Patch & Transform に複雑な logic を詰め込みすぎると、Composition が巨大な DSL のようになってしまいます。
おすすめは、単純な mapping は Patch & Transform に置き、複雑さが出てきたら Function に分けることです。
単純な値の受け渡し
-> Patch & Transform
複雑な生成 logic
-> Composition Functions
この境界を持っておくと、Composition の保守性を落としにくくなります。
Composition を読む順番
Composition YAML を読むときは、次の順番がおすすめです。
1. compositeTypeRef
どの XR に対応する Composition かを見る
2. mode と pipeline
Pipeline mode か、どの Function を呼ぶかを見る
3. Function input
Function に渡している設定を見る
4. base resource
どの Managed Resource を作るのかを見る
5. patches / transforms
XR のどの値が、どの field に渡るかを見る
6. reference / selector / connection details
resource 間のつながりと利用者への出力を見る
この順番で読むと、Composition を「長い YAML」としてではなく、「XR から composed resources を作る流れ」として追えます。
debug するときも同じです。
XR の spec は正しいか
Composition は正しい XR を対象にしているか
Function は install されているか
patch の field path は存在するか
Managed Resource は期待通り作られているか
Provider の READY / SYNCED はどうなっているか
Composition の問題なのか、Provider の問題なのかを分けることが大事です。
crossplane render で事前に見る
Composition を書くときは、実際に cluster に apply する前に、crossplane render で出力を確認できます。
公式ドキュメントでも、XR、Composition、Functions を渡して、生成される resources をローカルで preview する方法が紹介されています。
crossplane render xr.yaml composition.yaml functions.yaml
crossplane render は、Function を実行して、Composition からどの resource が出力されるかを確認するために便利です。
ただし、Function の実行には Docker が必要になる場合があります。
また、render は composed resources の形を確認するためのものです。
OCI API との実際の状態合わせ、IAM、ProviderConfig、認証、外部 resource の状態までは、実 cluster 上で確認する必要があります。
crossplane render
-> Composition が何を作ろうとしているかを見る
kubectl get / describe
-> cluster 上で実際に何が起きているかを見る
OCI Console / OCI CLI
-> 外部 resource の状態を見る
この三つを分けると、調査の順番がかなり明確になります。
よくある混乱
最後に、Composition 実装で混乱しやすい点を整理します。
一つ目は、Patch & Transform は古い mode: Resources そのものではない、という点です。
現在は、mode: Pipeline の中で function-patch-and-transform を呼び出す形として見るのが自然です。
二つ目は、Function が外部クラウドを直接作るわけではない、という点です。
Function は desired resources を返し、その desired resources を Crossplane が apply し、Managed Resource を Provider が reconcile します。
三つ目は、Composition は利用者 API の裏側にある実装だ、という点です。
利用者が見るのは XR です。
Composition は、platform team が利用者向け API をどう実装するかを表す場所です。
この三つを押さえると、Composition の YAML を読んだときに、どこを見ればよいかが分かりやすくなります。
まとめ
今回は、Composition の実装として、Patch & Transform、Composition Functions、Pipeline mode を整理しました。
ポイントは、次の三つです。
Patch & Transform は単純な field mapping に向いている
Composition Functions は複雑な生成 logic を分離するために使う
Pipeline mode では Function が desired state を順番に組み立てる
Composition は OCI API を直接呼ぶ処理ではありません。
Composition は desired resources を作り、その後の Managed Resource を Provider が reconcile します。
この境界を押さえると、Composition の問題、Function の問題、Provider の問題を切り分けやすくなります。
次は、この考え方を OCI Provider の構成、認証、Bucket demo に当てはめて見ていきます。