Crossplane: function-extra-resourcesで通常取得不能なカスタムリソースを取得する方法

[Crossplane]の[`function-extra-resources`]は、Composition内で追加のリソースを取得するための強力なツールです。しかし、デフォルトではCrossplane関連のリソース（XRなど）しか取得できません。この記事では、この制約を回避し、任意のカスタムリソースを取得する方法を紹介します。

[Crossplane]: https://www.crossplane.io/
[`function-extra-resources`]: https://marketplace.upbound.io/functions/crossplane-contrib/function-extra-resources/v0.0.3

## 前提条件

- Kubernetesクラスター
- Helm
- [KCL (Kubernetes Configuration Language)](https://www.kcl-lang.io/)
- kubectl

このチュートリアルはKCLを用いますが、YAMLでも同じようなことができます。

## セットアップ

### 1. Crossplaneのインストール

まず、Helmfileを使用してCrossplaneをインストールします。

```yaml
# helmfile.yaml
repositories:
  - name: crossplane-stable
    url: https://charts.crossplane.io/stable

releases:
  - name: crossplane
    namespace: crossplane-system
    chart: crossplane-stable/crossplane
    version: 1.16
```

以下のコマンドを実行してCrossplaneをインストールします：

```bash
helmfile sync --wait
```

### 2. カスタムリソースへのアクセス権限の設定

Crossplaneがカスタムリソースにアクセスできるように、特別なClusterRoleを作成します。

```python
# 01-cluster-roles.k
import manifests
import k8s.api.rbac.v1 as rbac

manifests.yaml_stream([
    rbac.ClusterRole {
        metadata.name = "suin:crossplane:additional"
        rules = [
            {
                apiGroups = ["suin.jp"]
                resources = ["clusterresources"]
                verbs = ["*"]
            },
            {
                apiGroups = ["suin.jp"]
                resources = ["namespacedresources"]
                verbs = ["*"]
            }
        ]
    },
    rbac.ClusterRoleBinding {
        metadata.name = "suin:crossplane:additional"
        roleRef = {
            apiGroup = "rbac.authorization.k8s.io"
            kind = "ClusterRole"
            name = "suin:crossplane:additional"
        }
        subjects = [{
            kind = "ServiceAccount"
            name = "crossplane"
            namespace = "crossplane-system"
        }]
    }
])
```

このClusterRoleを適用します：

```bash
kcl run 01-cluster-roles.k | kubectl apply -f -
```

これによって、`crossplane`サービスアカウントにカスタムリソースを操作する権限がつきます。これをしないと、デフォルトでは`function-extra-resources`はカスタムリソースにアクセスできません。本チュートリアルで最も重要なステップのひとつです。

ちなみに、`function-extra-resources`を導入すると、この関数用のPodが作られます。このPodは、`
function-extra-resources-fa66e8ab9f79`のような固有のサービスアカウントに紐づきます。そう考えると、サービスアカウント`function-extra-resources-fa66e8ab9f79`にカスタムリソースのアクセス権限をつけるのが良さそうに思えますが、それではうまく動作しません。理由は、この関数はCrossplaneのAPIを経由してKubernetesのオブジェクトを取得しているからです。したがって、CrossplaneのAPIを提供しているPodのサービスアカウントに権限を付与する必要があります。

### 3. カスタムリソース定義（CRD）のインストール

テスト用のカスタムリソースを定義します。

```python
# 02-crds.k
import manifests
import k8s.apiextensions_apiserver.pkg.apis.apiextensions.v1

manifests.yaml_stream([
    v1.CustomResourceDefinition {
        metadata.name = "clusterresources.suin.jp"
        spec = {
            group = "suin.jp"
            scope = "Cluster"
            names = {
                kind = "ClusterResource"
                plural = "clusterresources"
                singular = "clusterresource"
            }
            versions = [{
                name = "v1"
                served = True
                storage = True
                schema.openAPIV3Schema = v1.JSONSchemaProps {
                    type = "object"
                    properties = {
                        spec = {type = "object"}
                    }
                }
            }]
        }
    },
    v1.CustomResourceDefinition {
        metadata.name = "namespacedresources.suin.jp"
        spec = {
            group = "suin.jp"
            scope = "Namespaced"
            names = {
                kind = "NamespacedResource"
                plural = "namespacedresources"
                singular = "namespacedresource"
            }
            versions = [{
                name = "v1"
                served = True
                storage = True
                schema.openAPIV3Schema = v1.JSONSchemaProps {
                    type = "object"
                    properties = {
                        spec = {type = "object"}
                    }
                }
            }]
        }
    }
])
```

CRDを適用し、確立されるまで待ちます：

```bash
kcl run 02-crds.k | kubectl apply -f -
kubectl wait --for=condition=Established=true customresourcedefinition.apiextensions.k8s.io/clusterresources.suin.jp
kubectl wait --for=condition=Established=true customresourcedefinition.apiextensions.k8s.io/namespacedresources.suin.jp
```

ここで定義したkindのリソースを後で作り、`function-extra-resources`でそのオブジェクトを取ってくるということをやる予定です。`function-extra-resources`がクラスタースコープとネームスペーススコープどちらのカスタムリソースも取得可能であることを確かめるために、ここでは2つのCRDを作成しています。

### 4. Crossplane Functionsのインストール

必要なCrossplane Functionsをインストールします。

```python
# 03-functions.k
import manifests

manifests.yaml_stream([
    {
        apiVersion = "pkg.crossplane.io/v1beta1"
        kind = "Function"
        metadata.name = "function-kcl"
        spec.package = "xpkg.upbound.io/crossplane-contrib/function-kcl:v0.9.0"
    },
    {
        apiVersion = "pkg.crossplane.io/v1beta1"
        kind = "Function"
        metadata.name = "function-auto-ready"
        spec.package = "xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.2.1"
    },
    {
        apiVersion = "pkg.crossplane.io/v1beta1"
        kind = "Function"
        metadata.name = "function-extra-resources"
        spec.package = "xpkg.upbound.io/crossplane-contrib/function-extra-resources:v0.0.3"
    }
])
```

Functionsをインストールし、準備が整うまで待ちます：

```bash
kcl run 03-functions.k | kubectl apply -f -
kubectl wait --for=condition=Healthy=true function.pkg.crossplane.io/function-kcl
kubectl wait --for=condition=Healthy=true function.pkg.crossplane.io/function-auto-ready
kubectl wait --for=condition=Healthy=true function.pkg.crossplane.io/function-extra-resources
```

### 5. Composite Resource Definition (XRD)の作成

テスト用のXRDを作成します。

```python
# 04-xrds.k
import manifests
import crossplane.v1 as xp
import k8s.apiextensions_apiserver.pkg.apis.apiextensions.v1

manifests.yaml_stream([xp.CompositeResourceDefinition {
    metadata = {name = "xr1.suin.jp"}
    spec = {
        names = {
            kind = "XR1"
            plural = "xr1"
        }
        group = "suin.jp"
        versions = [{
            name = "v1"
            served = True
            referenceable = True
            schema.openAPIV3Schema = v1.JSONSchemaProps {}
        }]
    }
}])
```

XRDを適用し、確立されるまで待ちます：

```bash
kcl run 04-xrds.k | kubectl apply -f -
kubectl wait --for=condition=Established=true compositeresourcedefinitions.apiextensions.crossplane.io/xr1.suin.jp
```

### 6. Compositionの作成

`function-extra-resources`を使用してカスタムリソースを取得するCompositionを作成します。

```python
# 05-compositions.k
import crossplane.v1 as xp
import outdent

xp.Composition {
    metadata = {name = "test.suin.jp"}
    spec = {
        compositeTypeRef = {
            apiVersion = "suin.jp/v1"
            kind = "XR1"
        }
        mode = "Pipeline"
        pipeline = [
            {
                step = "pull-cluster-resources"
                functionRef.name = "function-extra-resources"
                input = {
                    apiVersion = "extra-resources.fn.crossplane.io/v1beta1"
                    kind = "Input"
                    spec.extraResources = [
                        {
                            apiVersion = "suin.jp/v1"
                            kind = "ClusterResource"
                            into = "cluster_resources"
                            type = "Selector"
                            selector.matchLabels = [{
                                type = "Value"
                                key = "some-label-name"
                                value = "some-label-value"
                            }]
                        },
                        {
                            apiVersion = "suin.jp/v1"
                            kind = "NamespacedResource"
                            into = "namespaced_resources"
                            type = "Selector"
                            selector.matchLabels = [{
                                type = "Value"
                                key = "some-label-name"
                                value = "some-label-value"
                            }]
                        }
                    ]
                }
            },
            {
                step = "kcl"
                functionRef.name = "function-kcl"
                input = {
                    apiVersion = "krm.kcl.dev/v1alpha1"
                    kind = "KCLInput"
                    spec.source = outdent.outdent("""
                    import yaml
                    extra_resources = option("params")?.ctx["apiextensions.crossplane.io/extra-resources"]
                    print("=== found resources by function-extra-resources ===")
                    print("Found ${len(extra_resources.cluster_resources)} cluster resources and ${len(extra_resources.namespaced_resources)} namespaced resources.")
                    print(yaml.encode({
                        cluster_resources: [r.metadata.name for r in extra_resources.cluster_resources]
                        namespaced_resources: [{ 
                            name = r.metadata.name
                            namespace = r.metadata.namespace
                        } for r in extra_resources.namespaced_resources]
                    }))
                    print("===")
                    items = []
                    """)
                }
            }
        ]
    }
}
```

このCompositionは2つのステップからなります。

1. `function-extra-resources`で2種類のkindのカスタムリソースを取得する
1. KCLを使って上で取得したリソースをPodのログに出力する。要するにこのステップはデバッグ要員です。

Compositionを適用します：

```bash
kcl run 05-compositions.k | kubectl apply -f -
```

### 7. テスト用のリソースの作成

テスト用のNamespaceとカスタムリソースを作成します。

```python
# 06-namespace.k
import k8s.api.core.v1

v1.Namespace {metadata.name = "my-namespace"}
```

```python
# 07-crs.k
import manifests

manifests.yaml_stream([
    {
        apiVersion = "suin.jp/v1"
        kind = "ClusterResource"
        metadata.name = "cluster-resource-1"
        metadata.labels = {"some-label-name" = "some-label-value"}
        spec = {}
    },
    {
        apiVersion = "suin.jp/v1"
        kind = "NamespacedResource"
        metadata.name = "namespaced-resource-1"
        metadata.namespace = "default"
        metadata.labels = {"some-label-name" = "some-label-value"}
        spec = {}
    },
    {
        apiVersion = "suin.jp/v1"
        kind = "NamespacedResource"
        metadata.name = "namespaced-resource-1"
        metadata.namespace = "my-namespace"
        metadata.labels = {"some-label-name" = "some-label-value"}
        spec = {}
    }
])
```

Namespaceとカスタムリソースを適用します：

```bash
kcl run 06-namespace.k | kubectl apply -f -
kubectl wait --for=jsonpath=status.phase=Active namespace/my-namespace
kcl run 07-crs.k | kubectl apply -f -
```

### 8. Composite Resource (XR)の作成

テスト用のXRを作成します。

```python
# 08-xrs.k
import manifests

manifests.yaml_stream([{
    apiVersion = "suin.jp/v1"
    kind = "XR1"
    metadata.name = "object1"
    spec = {}
}])
```

XRを適用し、Readyになるまで待ちます：

```bash
kcl run 08-xrs.k | kubectl apply -f -
kubectl wait --for=condition=Ready=true xr1.suin.jp/object1
```

### 9. 結果の確認

`function-kcl`のログを確認して、カスタムリソースが正しく取得されていることを確認します：

```bash
kubectl logs -n crossplane-system -f $(kubectl get pods -o name -n crossplane-system | grep function-kcl)
```

ログに次のような出力が出てくれば成功です！

```
=== found resources by function-extra-resources ===
Found 1 cluster resources and 2 namespaced resources.
cluster_resources:
- cluster-resource-1
namespaced_resources:
- name: namespaced-resource-1
  namespace: default
- name: namespaced-resource-1
  namespace: my-namespace

===
```

## まとめ

このチュートリアルでは、Crossplaneの`function-extra-resources`を使用して、通常はアクセスできないカスタムリソースを取得する方法を紹介しました。この技術を使用することで、Crossplaneの機能を拡張し、より柔軟なリソース管理が可能になります。

## 参考資料

このチュートリアルで使用したコードとリソースは、以下のGitHubリポジトリで参照できます
https://github.com/suinplayground/kubernetes-playground/blob/main/crossplane/12-function-extra-resources-to-pull-custom-resources/README.md

Crossplaneとfunction-extra-resourcesの詳細については、公式ドキュメントを参照してください。

https://www.crossplane.io/

https://marketplace.upbound.io/functions/crossplane-contrib/function-extra-resources/v0.0.3

## 一緒に働きませんか？

僕の会社(株式会社クラフトマンソフトウェア)では、Kubernetesエンジニアを募集しています！興味がある方はぜひ↓も御覧ください😌

https://craftsman-software.com/recruit/