TECHSTEP

ITインフラ関連の記事を公開してます。

Custom Resourceに対するValidationをGitHub Actions + k3sで試してみる

はじめに

今回はCustom Resourceに対してのValidationを何とかできないかと考え、GitHub Actions + k3sを組み合わせてみた結果を残しておきます。

経緯

Kubernetesマニフェストファイルに対するValidation

Kubernetesにリソースを作成する場合、マニフェストファイルに各リソースを定義し、kubectlコマンド等でデプロイを行ったり、Argo CDFluxを利用してGitOpsによる自動デプロイを行うことが多いかと思います。

Kubernetesは各リソースをYAML/JSON形式のマニフェストファイルで定義するため、ファイルの書き方(構文)に問題がある場合は、リソースの作成に失敗します。そのため、作成したマニフェストファイルをデプロイする前に、何かしらの方法でファイルの構文をチェックする必要があります。

またプロジェクトによっては、特定のセキュリティポリシーへの準拠やラベルの付与などをルールとして定めており、それに従っているかをチェックする必要もあります。

これらを全て人の目でチェックするのは、大規模環境はもちろん、中・小規模の環境でも相当な労力となってしまいます。そのため、マニフェストファイルに対するValidationを行う各種ツールが開発されています。

  • kubeval: Kubernetes YAML/JSONファイルのValidationを行う。Kubernetes OpenAPIを利用して生成されるスキーマを利用するため、Kubernetesの複数のバージョンをまたいで利用できる。

  • kube-score: Kubernetesマニフェストファイルに対する静的コード解析を行うツール。信頼性・セキュリティを中心とした項目に対しての解析を行う。

  • conftest: 構造化された設定ファイルに対してのテストを利用者が定義し、ポリシーを満たすかをテストするツール。Kubernetesマニフェストファイルだけでなく、TerraformやTekton Pipelineなどの定義ファイルに対しても利用できる。

これらのツールを利用することで、クラスターへのデプロイを行う前に、マニフェストファイルの構文やポリシーのチェックを行うことができます。

Custom Resourceに対するValidation

一方で、KubernetesはCustom Resource (CR)を利用することでKubernetes APIを拡張し、独自のリソースをKubernetes上に展開することができます。Kubernetesはアプリケーションやプラットフォームを実現するための基盤であり、運用の自動化・効率化やユーザーへ提供する機能を実現するため、どのような形でKubernetesを利用するかに関わらず、Custom Resourceを利用するケースはかなり多いと思われます。

Custom Resourceを利用する場合もマニフェストファイル上に定義をするため、こちらも構文やポリシーをチェックする必要が出てきます。

※参考:

ここで、Custom Resourceに対するValidationをするうえで、それに対応したツールというのが現状あまり見当たらない、という課題が出てきました。正確にはツールが全くない、というわけではなく、例えば前述のconftestを利用することで、各OSSのCustom Resourceに対するポリシーを定義すれば、テストすることは可能です。しかし、これにはいくつかの課題があると考えました。

1. プロダクトごとに異なるポリシーを用意しなければならない: あるプロダクトのために用意したポリシーを、別のプロダクトに使いまわすことは難しい。

2. プロダクトのアップデートに合わせてメンテナンスが必要になる: プロダクトのバージョンアップにより、マニフェストファイルのスキーマが変更された場合、利用するプロダクトのバージョンに追従するだけでなく、テストのほうも修正をする必要がある。

このため、すべてのテストをconftestのようなツールで行う場合、ポリシーの実装とメンテナンスのコストがかなり高くなるのではないか、と考えました。

そこで、Custom Resourceに対するValidationを、より簡単に、より汎用的に実現できないかと考え、表題のようにGitHub Actions + k3sでのテストを試してみました。

Custom Resourceを利用するにはCustom Resource Definitionを事前に用意する必要があります。Custom Resource DefinitionにはOpenAPI schemaを利用したValidation機能が備わっており、Custom Resourceデプロイ時に問題があればエラーを返します。今回はこの機能を利用し、k3sへテスト対象のリソースを実際にデプロイすることで、Validationを行いました。

※下記画像はこちらの記事中の画像を一部編集したもの

f:id:FY0323:20201221140127j:plain

※参考:

GitHub Actions + k3sの実行方法

今回試したのは以下のような構成です。なお、マニフェストファイルはGitHubリポジトリ上で管理することを前提としております。

f:id:FY0323:20201221143607j:plain

まずGitHubリポジトリマニフェストファイルをPushすることでGitHub Actionsが起動します。GitHub Actionsでは最初にk3sをワークフロー用インスタンス上にインストールし、そこへ対象のOSSをインストールします。最後に、テスト対象のマニフェストファイルをkubectlコマンドでデプロイし、エラーが発生しないかを確認します。

今回は検証するOSSとして、Argo CDRookを選択しました。2つのOSSを利用する理由としては、複数のOSSのCustom Resourceを対象にすることで、この方法が共通利用できるかを確認したかったためです。

リポジトリ中のフォルダ構成は以下のようにしています。

/testrepo
|
|-- .github
|    |
|    `-- workflows
|        |
|        |-- argocd-validate.yaml
|        `-- rook-validate.yaml
|
|-- app
|    |
|    `-- sample-app-argocd.yaml
|
`-- storage
     |
     `-- sample-block-storage.yaml

各Workflowの定義ファイルは以下の通りです。今回は各OSS毎に個別にWorkflowファイルを用意し、GitHub Actionsで並列に実行するようにしました。

OSSのインストール中、必要に応じてsleepコマンドを行っていますが、それぞれの処理毎に必要な待機時間については測定していないため、適宜変更する必要があります。

また、k3sの利用に際し、こちらのActionを利用しました。

argocd-validate.yaml

name: ArgoCD

on:
 push:
   branches: [ main ]
 workflow_dispatch:

jobs:
 argocd-validation:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v2
     - name: k3s-install
       uses: debianmaster/actions-k3s@master
       id: k3s
       with:
         version: 'v1.17.4-k3s1'
     - name: k3s-install-check
       run: |
         kubectl get nodes
     - name: argocd-install
       run: |
         kubectl create namespace argocd
         sleep 3
         kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
     - name: argocd-app-deploy-test
       run: |
         kubectl apply -f ./app/

rook-validate.yaml

name: Rook
on:
 push:
   branches: [ main ]
 workflow_dispatch:
jobs:
 rook-validation:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v2
     - name: k3s-install
       uses: debianmaster/actions-k3s@master
       id: k3s
       with:
         version: 'v1.17.4-k3s1'
     - name: k3s-install-check
       run: |
         kubectl get nodes
     - name: rook-install
       run: |
         git clone https://github.com/rook/rook.git
         sleep 5
         kubectl create -f rook/cluster/examples/kubernetes/ceph/crds.yaml
         sleep 5
         kubectl create -f rook/cluster/examples/kubernetes/ceph/common.yaml
         sleep 5
         kubectl create -f rook/cluster/examples/kubernetes/ceph/operator.yaml
         sleep 60
         kubectl create -f rook/cluster/examples/kubernetes/ceph/cluster-test.yaml
     - name: rook-storage-deploy-test
       run: |
         kubectl apply -f ./storage/

今回テスト対象としたのは、以下の2ファイルです。

sample-app-argocd-yaml

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
 name: sampleapp
 namespace: argocd
spec:
 project: default
 source:
   repoURL: https://github.com/argoproj/argocd-example-apps.git
   targetRevision: HEAD
   path: guestbook
 destination:
   server: https://kubernetes.default.svc
   namespace: argocd
 syncPolicy:
   automated:
     prune: false
     selfHeal: false

sample-block-storage.yaml

apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
 name: replicapool
 namespace: rook-ceph
spec:
 failureDomain: host
 replicated:
   size: 1
   requireSafeReplicaSize: false
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
    clusterID: rook-ceph
    pool: replicapool
    imageFormat: "2"
    imageFeature: layering
    csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
    csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
    csi.storage.k8s.io/controller-expand-secret-name: rook-csi-rbd-provisioner
    csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph     
    csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
    csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
    csi.storage.k8s.io/fstype: ext4
allowVolumeExpansion: true
reclaimPolicy: Delete

上記ファイルを用意できたら、対象のGitHubリポジトリへPushし、GitHub Actionsを起動します。

Validation成功時

まずは定義ファイルに問題がない場合です。デプロイに成功した場合は、以下の画像のように、エラーも発生せず正常終了します(一部ワークフロー中のコマンドやファイル名が異なりますが、結果には影響ありません)。

f:id:FY0323:20201221152810j:plain

f:id:FY0323:20201221144044j:plain

Validation失敗時

次に定義ファイルに問題がある場合です。今回は以下のように、必須項目の部分を一部修正し、エラーを発生させるようにしました。

sample-app-argocd-yaml

(中略)

spec:
 project: default
 source:
   repoURL: https://github.com/argoproj/argocd-example-apps.git
   targetRevision: HEAD
   path: guestbook
 destinations: # “spec.destination”は必須項目のためエラーとなる
   server: https://kubernetes.default.svc

(中略)

sample-storage-block.yaml

apiVersion: ceph.rook.io/v1
kind: CephBLockPool # スペルミス
metadata:

(中略)

これらをリポジトリへPushした結果は以下の通りです。それぞれエラーが発生し、Jobが失敗している様子が確認できます。

f:id:FY0323:20201221144223j:plain

f:id:FY0323:20201221144412j:plain

ここまでで、ひとまず簡単なValidationを行えることは確認できました。

追加検証

ここから、個人的に気になっていたポイントについて、もう少し試してみました。

同時に複数ファイルをテストするとどうなるか

Custom Resourceを1つテストする分には特に問題なく動作しましたが、複数のリソースを同時にデプロイするとどうなるかを見てみました。

今回はArgo CDで利用するApplicationリソースを複数用意し、それらを同時にデプロイしました。

# 100個のコピーを作成し、"metadata.name"を変更
$ for i in `seq 100`; do cp -p app/sample-app-argocd.yaml app/sample-app-argocd-$i.yaml; done
$ ll app/ | grep sample-app | wc -l
101
$ for i in `seq 100`; do sed -i -e "s/name: sampleapp/name: sampleapp-$i/g" app/sample-app-argocd-$i.yaml; done

上記操作後、一部Applicationリソースのみエラーを起こすよう修正し、リポジトリへPushしました。その結果、以下の画像の通り、エラーを含むリソースが表示され、Jobの結果もエラーとなって終了しました。

f:id:FY0323:20201221144633j:plain

余分な項目が追加されている場合は検出できるか

前項では、設定が必須の項目や指定された値以外を設定した場合を見ましたが、余分なデータ、例えば本来設定する必要のないデータが含まれる場合はどうなるか見てみました。

ここでは、以下のようなデータを追加して、リポジトリへPushしてみます。

(中略)

spec:
 project: default
 source:
   repoURL: https://github.com/argoproj/argocd-example-apps.git
   targetRevision: HEAD
   path: guestbook
   errorData: true  # 追加
 destination:
   server: https://kubernetes.default.svc
   namespace: argocd
   errorData: true  # 追加

(中略)
(中略)

spec:
 failureDomain: host
 replicated:
   size: 1
   requireSafeReplicaSize: false
   errorData: true  # 追加

(中略)

すると、エラーは発生せず、リソースの作成が正常に完了してしまいました。

f:id:FY0323:20201221144800j:plain

f:id:FY0323:20201221144850j:plain

これはkubevalを利用する場合とも共通した問題となりそうですが、意味のない余分なデータの検出は難しいと思われます。

気になること

現時点で気になることは色々とあるのですが、ポジティブな点とネガティブな点について書いておきます。

ポジティブな点

今回GitHub Actions中にk3sへCustom Resourceをデプロイすることで、Custom Resourceを定義するマニフェストファイル中の構文エラーを検出することができました。また、2つのOSSのCustom Resourceに対してチェックを行い、どちらも構文の間違いを検出できたため、Custom Resourceを利用するプロダクトに対しては、ある程度汎用的に使えるのではないか、と思っています。

また、今回は各OSSをk3sへインストールする際に、最新版をデプロイするように指定をしております。これにより、バージョン更新によるマニフェストスキーマ変更に気づけるのではないか、とも考えています。OSSはバージョン更新の際、マニフェストファイルのスキーマが変更される場合があり、古いバージョンでのマニフェストの書き方と一致しなくなる場合があります。CIパイプライン等に今回の仕組みを導入しておくことで、旧バージョン仕様のCustom Resourceのデプロイが失敗することを契機として、スキーマ変更に気づけるかもしれません。

ネガティブな点

GitHub Actions + k3sで簡単にValidationができそうだと思った一方、いくつかの課題もありそうです。

まずはシンプルに実行結果が見にくいと感じました。テストの成功・失敗はGitHub Actions上のステータスからわかるので良いですが、失敗した場合の理由や内容が少し見にくいと感じました。

また、もっと大きな問題として、一度に全てのエラー箇所を表示してくれない、というものがあります。例えばkindなどの指定が誤っている場合、エラー内容としては以下の画像のように、該当するリソースが見つからない、と表記されます。

f:id:FY0323:20201221145048j:plain

しかし、ここでテストを行ったマニフェストファイルを見てみます。マニフェストファイルを見てみると、kindの他にも複数のエラーが含まれており、これらはこの時点では検出されません。

apiVersion: argoproj.io/v1alpha1
kind: Applications  # エラー
metadata:
 name: sampleapp
 namespace: argocd
spec:
 project: default
 source:
   repoURL: https://github.com/argoproj/argocd-example-apps.git
   targetRevision: HEAD
   path: guestbook
 destinations:  # エラー
   server: https://kubernetes.default.svc
   namespace: argocd
 syncPolicy:
   automate:  # エラー
     prune: false
     selfHeal: false

kindの箇所を修正して再びPushすると、今度はspec.destinationが見つからない、というエラーメッセージが表示されますが、その下にあるspec.syncPolicy.automateのエラーは表示されません。

f:id:FY0323:20201221145211j:plain

このように、一度に全てのエラーが表示されないため、何度も同じような修正作業を行う必要が出てしまいます。

最後に、これもkubevalでも同様の問題がありそうですが、テスト対象のファイルを含むフォルダの構成は検討したほうがよさそうだとも感じました。テスト対象のマニフェストファイルが複数のフォルダに分散して配置されている場合、GitHub Actionsのワークフロー定義ファイル中で、それらを個別に指定する必要があります。これはテストを用意するうえで手間となりますし、ワークフローの定義ファイルをコピーして別のマニフェストファイルに対するテストを用意するときも面倒です。

そのため、可能であれば、テスト対象のファイルは、まとめて一つのフォルダに格納したほうがよさそうです。

参考ドキュメント