CrossplaneのUsage機能を使って複数のKubernetesリソースにおける削除の順序性を制御する

# はじめに

<!-- 挨拶文を入れてください -->
みなさん、こんにちは。日立製作所の宮村です。  
今年の8月に開催されたKubeDay Japanにて、「Towards a World Without Dependency Consideration: All Resources Be Deleted Simply」というタイトルで発表しました。  
本記事ではKubeDayの発表でお見せしたデモを実施する手順について共有したいと思います。  前回は、S3バケットを例に誤削除の防止機能について紹介しました。
今回は、複数のKubernetesリソースを削除する際に削除順を制御する手順について紹介します。

発表内容についてもっと詳しく知りたい方は、以下のリンク先から確認できるので、見てみてください。  
- [発表スライド](https://static.sched.com/hosted_files/kubedayjapan2024/67/Towards_a_World_Without_Dependency_Consideration.pdf?_gl=1*1i6fbd4*_gcl_au*MTAwNjkzNjMwOS4xNzI3NzQxMjQw*FPAU*MTAwNjkzNjMwOS4xNzI3NzQxMjQw)  
- 発表動画
<iframe width="560" height="315" src="https://www.youtube.com/embed/UiDodYNTCMk?si=-MiIWMXT1aP4ud4r" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

<!-- 挨拶文を入れてください -->

#  前提条件
- 今回のデモではKuberentesのバージョン1.30で試しています
- Helmのバージョンはv3.2.0以上が必要です
- IAMのクレデンシャル(アクセスキーとシークレットキー)が必要です
- AWSのサービス(S3のバケットとDynamoDB)を作成するため、これらのリソースを作成できるパーミッションを持つAWSアカウントが必要です
- Crossplaneのv1.14以上をインストール済みであること( Crossplaneの最新バージョンは1.18ですが、デモで使用したバージョンは1.16です。)


# 1. 事前準備
## 1-1. AWSとCrossplaneを接続するためにAWS Providerをインストール

1. DynamoDB用のAWS Providerをインストール
```
cat <<EOF | kubectl apply -f -
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
  name: provider-aws-dynamodb
spec:
  package: xpkg.upbound.io/upbound/provider-aws-dynamodb:v1.10.0
EOF
```
※ここでは、デモで使用したv1.10.0を指定していますが、2024年12月時点での最新バージョンは、v1.17.0です。  


2. インストールされたことを確認します。
```
kubectl get providers

NAME                          INSTALLED   HEALTHY   PACKAGE                                                 AGE
provider-aws-dynamodb         True        True      xpkg.upbound.io/upbound/provider-aws-dynamodb:v1.10.0   125d
provider-aws-s3               True        True      xpkg.upbound.io/upbound/provider-aws-s3:v1.10.0         125d
upbound-provider-family-aws   True        True      xpkg.upbound.io/upbound/provider-family-aws:v1.10.0     125d
```
`provider-aws-dynamodb ` というDynamoDB用のプロバイダーが追加されています。

# 2. CrossplaneのComposite ResourceとしてS3バケットとDynamoDBを作成
## 2-1.  カスタムAPIをインストールするためにComposite Resource Definitions (XRD)を作成

1. Composite Resource Definitionsのyamlファイル(例: compositeresourcedefinition.yaml)を作成します。
```
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: nosqls.database.example.com
spec:
  group: database.example.com
  names:
    kind: NoSQL
    plural: nosqls
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              location:
                type: string
                oneOf:
                  - pattern: '^JP$'
                  - pattern: '^US$'
            required:
              - location
    served: true
    referenceable: true
  claimNames:
    kind: NoSQLClaim
    plural: nosqlclaim
```


```
kubectl create -f compositeresourcedefinition.yaml

compositeresourcedefinition.apiextensions.crossplane.io/nosqls.database.example.com created
```

2. Composite Resource Definitionsが作成されたことを確認します。
```
kubectl get xrds

NAME                          ESTABLISHED   OFFERED   AGE
nosqls.database.example.com   True          True      13s
```

3. カスタムAPIのエンドポイントが作成されたことを確認します。
```
kubectl api-resources | grep nosql

nosqlclaim                                              database.example.com/v1alpha1          true         NoSQLClaim
nosqls                         
```

## 2-2. S3バケットと DynamoDBテーブルを作成
1.   Composition用のyamlファイル(例: composition-labels.yaml)を作成します。
```
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: dynamo-with-bucket
spec:
  resources:
    - name: s3Bucket
      base:
        apiVersion: s3.aws.upbound.io/v1beta1
        kind: Bucket
        metadata:
          name: crossplane-quickstart-bucket
          labels:
            type: bucket
        spec:
          forProvider:
            region: ap-northeast-1
          providerConfigRef:
            name: default
      patches:
        - type: FromCompositeFieldPath
          fromFieldPath: "spec.location"
          toFieldPath: "spec.forProvider.region"
          transforms:
            - type: map
              map:
                JP: "ap-northeast-1"
                US: "us-east-2"
    - name: dynamoDB
      base:
        apiVersion: dynamodb.aws.upbound.io/v1beta1
        kind: Table
        metadata:
          name: crossplane-quickstart-database
          labels:
            type: nosql
        spec:
          forProvider:
            region: "ap-northeast-1"
            writeCapacity: 1
            readCapacity: 1
            attribute:
              - name: S3ID
                type: S
            hashKey: S3ID
      patches:
        - type: FromCompositeFieldPath
          fromFieldPath: "spec.location"
          toFieldPath: "spec.forProvider.region"
          transforms:
            - type: map
              map:
                JP: "ap-northeast-1"
                US: "us-east-2"
  compositeTypeRef:
    apiVersion: database.example.com/v1alpha1
    kind: NoSQL
```

2. S3バケットとDynamoDBテーブルを作成します
```
kubectl create -f composition-labels.yaml
composition.apiextensions.crossplane.io/dynamo-with-bucket created
```

3. 作成されたことを確認します。
 ```
kubectl get compositions

NAME                 XR-KIND   XR-APIVERSION                   AGE
dynamo-with-bucket   NoSQL     database.example.com/v1alpha1   15s
```

## 2-3.NoSQLオブジェクト作成
 1. AWS上にリソースを作成するためのComposite resource用のyamlファイル(例: dynamodb-api.yaml)を作成します。
```
apiVersion: database.example.com/v1alpha1
kind: NoSQL
metadata:
  name: my-nosql-database
spec:
  location: "JP"
```

2. Composite resourceを作成します。
```
kubectl create -f dynamodb-api.yaml
nosql.database.example.com/my-nosql-database created
```

3. Composite resourceが作成されたことを確認します。
```
kubectl get nosqls
NAME                SYNCED   READY   COMPOSITION          AGE
my-nosql-database   True     True    dynamo-with-bucket   2m47s
```

4. Composite resourceが作成されたことで、managed resourceが作成されたことを確認します。
```
kubectl get managed

NAME                                                    SYNCED   READY   EXTERNAL-NAME             AGE
table.dynamodb.aws.upbound.io/my-nosql-database-sgjzd   True     True    my-nosql-database-sgjzd   3m4s

NAME                                               SYNCED   READY   EXTERNAL-NAME             AGE
bucket.s3.aws.upbound.io/my-nosql-database-pm7td   True     True    my-nosql-database-pm7td   3m4s
```

S3 bucketとDynamoDB tableが作成されていることが確認できます。


5. AWS上にも作成されていることを確認します。
```
aws s3 ls |grep nosql
2024-12-03 04:08:15 my-nosql-database-pm7td
```
```
aws dynamodb list-tables
{
    "TableNames": [
        "my-nosql-database-sgjzd"
    ]
}
```

# 3. Usageを使用して、依存関係にあるリソースの削除順を制御
## 3-1. DynamoDBがS3バケットを使用中であるUsageの作成
1. Usageのyamlファイル(例: usage-order-selector.yaml)を作成

```
apiVersion: apiextensions.crossplane.io/v1alpha1
kind: Usage
metadata:
  name: s3-used-by-dynamodb
spec:
  of:
    apiVersion: s3.aws.upbound.io/v1beta1
    kind: Bucket
    resourceSelector:
      matchControllerRef: false
      matchLabels:
        type: bucket
  by:
    apiVersion: dynamodb.aws.upbound.io/v1beta1
    kind: Table
    resourceSelector:
       matchLabels:
          type: nosql
```
`spec.of`で削除を防止する対象を指定します。  
`spec.of.apiVersion`と`spec.of.kind`は、managed resourceで定義した値を記入します。  
`spec.by`には、`spec.of`で指定したリソースを使用しているmanaged resourceを指定します。


2. Usageを作成
```
kubectl create -f usage-order-selector.yaml
usage.apiextensions.crossplane.io/s3-used-by-dynamodb created
```

3. Usageが作成されたことを確認
```
kubectl get usages

NAME                  DETAILS                                                             READY   AGE
s3-used-by-dynamodb   Table/my-nosql-database-sgjzd uses Bucket/my-nosql-database-pm7td   True    11s
```

4. Usageが作成されると、保護対象のリソース(S3バケット)に`crossplane.io/in-use=true`という使用中のラベルが付与されます。  
このラベルが付与されている間はリソースは削除から保護されます。  
in-useラベルが作成したS3バケットに付与されているか確認します。
```
kubectl describe bucket
Name:         my-nosql-database-pm7td
Namespace:
Labels:       crossplane.io/claim-name=
              crossplane.io/claim-namespace=
              crossplane.io/composite=my-nosql-database
              crossplane.io/in-use=true
              type=bucket
```

## 3-3. S3バケットを削除しようとするとUsageが削除を防止するか確認
1. managed resource(bucket)を削除します。
```
kubectl delete buckets my-nosql-database-pm7td

Error from server (This resource is in-use by 1 Usage(s), including the Usage "s3-used-by-dynamodb" by resource Table/my-nosql-database-sgjzd.): admission webhook "nousages.apiextensions.crossplane.io" denied the request: This resource is in-use by 1 Usage(s), including the Usage "s3-used-by-dynamodb" by resource Table/my-nosql-database-sgjzd.
```
無事、DynamoDBが使用中であるため、Admission webhookが削除リクエストを拒否してくれてS3バケットの削除が防止されました。
> admission webhook "nousages.apiextensions.crossplane.io" denied the request: This resource is in-use by 1 Usage(s), including the Usage "s3-used-by-dynamodb" by resource Table/my-nosql-database-sgjzd.



2. DynamoDB tableを削除してみます。
```
kubectl delete tables my-nosql-database-sgjzd
table.dynamodb.aws.upbound.io "my-nosql-database-sgjzd" deleted
```

DynamoDB tableは何からも使用されていないため、削除できました。

3. S3バケットを削除してみます。
```
kubectl delete buckets my-nosql-database-pm7td
bucket.s3.aws.upbound.io "my-nosql-database-pm7td" deleted
```
S3バケットを使用中のテーブルが削除済のため、S3バケットも削除できるようになりました。

# 終わりに

<!-- 〆の挨拶文を入れてください -->
以上、CrossplaneのUsage機能を使用して複数Kubernetesリソースの依存関係を考慮した削除順を制御する機能を紹介しました。  
<!-- 〆の挨拶文を入れてください -->

# 参考リンク

[Usages](https://docs.crossplane.io/latest/concepts/usages/)  
[AWS Quickstart Part 2](https://docs.crossplane.io/v1.16/getting-started/provider-aws-part-2/)

---

<!-- 他社商標に関する記載を適宜修正してください -->

- AWS は、米国その他の諸国における Amazon.com, Inc. またはその関連会社の商標です。
- Kubernetesは、米国およびその他の国におけるThe Linux Foundationの商標または登録商標です。
- Crossplaneは、米国およびその他の国におけるThe Linux Foundationの商標または登録商標です。
- その他、記載されている会社名および商品・製品・サービス名は、各社の商標または登録商標です。