PipeCD Operator を実装してみた
この記事は Qiita: Kubernetes Advent Calendar 2020 その3 及び NTTコミュニケーションズ Advent Calendar 2020 の 19 日目の記事です。
会社の Advent Calendar での参加ですが仕事でやっていることを話さなければいけないというレギュレーションは特に無かったため、Qiita: Kubernetes Advent Calendar 2020 の 11 日目に出した GitOps を実現する CD ツール、PipeCD が良さそうという話 の記事関連で趣味開発の話をします。
今回の話は実装してみた系 & 実装してみてつまずいたところの備忘録的な記事になります。
導入
先日、 PipeCD について GitOps を実現する CD ツール、PipeCD が良さそうという話 という記事を書きました。
PipeCD は現状最初の stable release を目指すフェーズにおり比較的新しいソフトウェアです。そのため自身の勉強と PipeCD の界隈を盛り上げられればという意味を兼ねて、PipeCD を Kubernetes API から操作できるような CRD + Controller を Kubebuilder を用いて実装してみました。
(前の記事で K8s と密結合でないことを PipeCD の利点に上げときながらなんで Operator で Kubernetes API を噛ませるようにしたんやねんという感じですが自学用途ということで、、)
ソースコードは以下になります。 あくまで自学用途なので動作は保証できません。
-
https://github.com/ShotaKitazawa/pipecd-operator
- 記事を書いているときの commit hash は
fcef64f94111b0370f2822c70d0e3c981e62481d
(first commit) です。
- 記事を書いているときの commit hash は
デモ
動作確認時のバージョンはそれぞれ以下になります。
- PipeCD v0.9.0
- Kubernetes v1.19.1 (kind)
CRD, Controller のインストール
以下のコマンドを実行すると CRD, Namespace (pipecd-system
), カスタムコントローラ, カスタムコントローラが Kubernetes の各種リソースにアクセスするための RBAC がそれぞれクラスタに適用されます。
kubectl apply -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/deploy/deploy.yaml
また、以降の作業のために作業用の Namespace を作成します。
kubectl create ns pipecd-demo
Control Plane の構築
まずは PipeCD Control Plane を構築するためのマニフェストを適用します。
kubectl apply -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/secret.yaml
kubectl apply -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_minio.yaml
kubectl apply -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_mongo.yaml
kubectl apply -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_controlplane.yaml
Minio
, Mongo
リソースはそれぞれ Minio , MongoDB の StatefulSet と Service をデプロイするだけのものです。
FileStore, Data Store に Minio, MongoDB 以外を利用する場合を考慮して、これらのリソースは ControlPlane リソースの生成物に含めず別途カスタムリソースして定義しました。
ControlPlane
リソースを適用するとコントローラにより以下が行われます。
- PipeCD Control Plane の各種コンポーネント [1] 用の Deployment, Service リソースをデプロイ
-
ControlPlane.spec.config
以下の宣言をServer
コンポーネント起動時引数として渡される設定ファイルに整形し ConfigMap リソースとして保存 - JWT の署名アルゴリズム HS256 の共有シークレットを Secret リソースとして保存
Pod を確認すると PipeCD Control Plane の各種コンポーネントがデプロイされていることが確認できます。
$ kubectl get pod -n pipecd-demo
NAME READY STATUS RESTARTS AGE
controlplane-sample-cache-6fbf867d56-wvdpv 1/1 Running 0 28s
controlplane-sample-gateway-75655d6fc4-s67df 1/1 Running 0 28s
controlplane-sample-ops-cf5f5649d-6txk2 1/1 Running 0 28s
controlplane-sample-server-89dc5bf8-xsb29 1/1 Running 2 28s
minio-sample-minio-0 1/1 Running 0 31s
mongo-sample-mongo-0 1/1 Running 0 30s
ポートフォワードすることで WebUI へのアクセスも可能です。
$ kubectl port-forward -n pipecd-demo svc/pipecd 8080
Environment の作成
続いて Environment
リソースを適用します。
kubectl apply -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_environment.yaml
Environment
リソースを適用した後 PipeCD の WebUI を確認すると、Environment が作成されていることが確認できます。
Piped の作成・構築
続いて Piped
リソースを適用します。
kubectl apply -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_piped.yaml
Piped
リソースを適用すると、コントローラにより以下が行われます。
- PipeCD の該当 Project, 該当 Environment に紐づく Piped Secret の払い出しリクエスト
- 上記で払い出した Piped Secret Key を Secert リソースとして保存
-
Piped.spec.config
以下の宣言をPiped
コンポーネント起動時引数として渡される設定ファイルに整形し ConfigMap リソースとして保存 - Piped 用の各種リソースをデプロイ
- 上記で払い出した Piped Secret をここで生成する Pod に渡すことで、 Piped Pod が該当の Project, Environment に紐づく Piped として登録される
Pod を確認すると PipeCD の Piped コンポーネントがデプロイされていることが確認できます。
$ kubectl get pod -n pipecd-demo
NAME READY STATUS RESTARTS AGE
controlplane-sample-cache-6fbf867d56-wvdpv 1/1 Running 0 39m
controlplane-sample-gateway-75655d6fc4-s67df 1/1 Running 0 39m
controlplane-sample-ops-cf5f5649d-6txk2 1/1 Running 0 39m
controlplane-sample-server-89dc5bf8-xsb29 1/1 Running 2 39m
minio-sample-minio-0 1/1 Running 0 39m
mongo-sample-mongo-0 1/1 Running 0 39m
piped-sample-68ff77dcb7-9tz6b 1/1 Running 0 5m49s
また WebUI では Piped Secret が払い出され、名前 (test-piped
) の横にバージョン (v0.9.0
) と書かれていることから Pod としてデプロイされた Piped が紐付いていることが確認できます。
TODO
ここまで作成した Environment, Piped を用いて、 WebUI より PipeCD Application を作成することで PipeCD を用いた Continuous Delivery が可能です!
本当は Application もカスタムリソース化したかったのですが時間が足りなくなったので当記事での紹介は断念しました、、誠意実装中です。
お掃除
Kubernetes に適用されているオブジェクトを 順番に 削除します。
順番であることの必要性については 備忘録:オブジェクトの削除処理の順番を制御したい にて後述します。
- Piped の削除
kubectl delete -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_piped.yaml
- Environment の削除
kubectl delete -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_environment.yaml
- ControlPlane の削除
kubectl delete -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_controlplane.yaml
kubectl delete -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_mongo.yaml
kubectl delete -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/pipecd_v1alpha1_minio.yaml
kubectl delete -n pipecd-demo -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/config/samples/secret.yaml
- CRD, Namespace の削除
kubectl delete -f https://raw.githubusercontent.com/ShotaKitazawa/pipecd-operator/master/deploy/deploy.yaml
kubectl delete ns pipecd-demo
備忘録
以降は pipecd-operator を実装する上でつまずいた点や未解決な点を書き連ねます。
PipeCD の一部パッケージが import error になる
ビルドを実行すると https://github.com/pipe-cd/pipe の一部パッケージに未定義エラーが発生します。
$ go build main.go
# github.com/pipe-cd/pipe/pkg/model
../../../../go/pkg/mod/github.com/pipe-cd/pipe@v0.9.0/pkg/model/application.go:24:9: undefined: ApplicationGitPath
../../../../go/pkg/mod/github.com/pipe-cd/pipe@v0.9.0/pkg/model/application.go:34:9: undefined: ApplicationSyncState
../../../../go/pkg/mod/github.com/pipe-cd/pipe@v0.9.0/pkg/model/application_live_state.go:17:9: undefined: ApplicationLiveStateVersion
... (省略)
これが発生する理由は、 https://github.com/pipe-cd/pipe の上記関数が protobuf ファイルから自動生成される関数であるためです。
PipeCD をビルドする場合は Bazel によるビルドプロセスの一環として protobuf から Golang 用のソースコードが自動生成されます。しかしながら今回のように外部リポジトリから https://github.com/pipe-cd/pipe を参照する場合は上記のような未定義エラーが発生してしまいます。
これの対処として、 protoc を手動実行し Golang のソースコードを生成しました。
- github.com/pipe-cd/pipe (v0.9.0) のクローンと protoc の実行
cd $HOME
go get -u github.com/pipe-cd/pipe
go get -u github.com/envoyproxy/protoc-gen-validate
cd $GOPATH/src/github.com/pipe-cd/pipe
git switch -d v0.9.0
for i in $(ls ./pkg/model | grep "proto$"); do protoc -I ./ -I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate --go_out=plugins=grpc:. ./pkg/model/$i; done
protoc -I ./ -I ${GOPATH}/src/github.com/envoyproxy/protoc-gen-validate --go_out=plugins=grpc:. pkg/app/api/service/webservice/service.proto
- 生成した Golang のソースコードを Go Module の管理するディレクトリ以下に移動
sudo mv github.com/pipe-cd/pipe/pkg/model/* $GOPATH/pkg/mod/github.com/pipe-cd/pipe@v0.9.0/pkg/model/
sudo mv github.com/pipe-cd/pipe/pkg/app/api/service/webservice/service.pb.go $GOPATH/pkg/mod/github.com/pipe-cd/pipe@v0.9.0/pkg/app/api/service/webservice/
なおこのビルドエラーに関して、以下の Tweet のスレッドにて開発者の方からコメントを頂いており、将来的に自動生成された Golang のソースコードも commit に含めることを検討してくださっています。
PipeCD に外部サービスが叩くための API がない (PipeCD v0.9.0 以前)
PipeCD v0.9.0 以前において、 PipeCD は外部サービスが叩くための API を提供していません。
pipecd-operator では、gRPC-Web から叩かれる gRPC インタフェース (以下、gRPC WebService インタフェース) を叩くように実装しました。
gRPC WebService インタフェースでは認可に JWT (JSON Web Token) を利用しており、署名アルゴリズムに HS256 を用いています。
pipecd-oprator では、 HS256 の共有シークレットを PipeCD の Server
コンポーネントと pipecd-operator の各種コントローラ間で共有することにより強引に認証認可を突破する方法を取りました。クライアントに当たる pipecd-operator にて JWT を生成している部分のソースコードは このあたり です。
なお、PipeCD v0.9.1 以降にて外部サービスが叩くための API が提供され、 API Key により認証認可が行われるそうです。
今回は実験的に gRPC WebService インタフェースを叩くよう実装しましたが、 PipeCD が Stable になるタイミングで pipecd-operator から PipeCD への接続方法も見直すつもりです。
control-plane-config.yaml, piped-config.yaml のスキーマをそのまま CRD にマッピング出来ない
pipecd-operator では control-plane-config.yaml, piped-config.yaml (以下、実設定ファイル) の内容をそれぞれ ControlPlane.spec.config
, Piped.spec.config
へ記述できるようにしました。
ここで、実設定ファイルには例えば以下の文法があります[2]。この文法は type
の値により config
のスキーマが異なるため、実設定ファイルのスキーマを単純に Kubernetes CRD とすることが出来ません。
apiVersion: pipecd.dev/v1beta1
kind: ControlPlane
spec:
datastore:
type: MONGODB
config:
url: mongodb://pipecd-mongodb:27017/quickstart
database: quickstart
そのため、 pipecd-operator では type
ごとにフィールドを分けるようにしました。
例えば上記の設定を pipecd-operator の ControlPlane
リソースで宣言する場合は以下のようになります。
apiVersion: pipecd.kanatakita.com/v1alpha1
kind: ControlPlane
spec:
config:
datastore:
mongoDBConfig:
url: mongodb://pipecd-mongodb:27017/quickstart
database: quickstart
また上記の ControlPlane.spec.config.datastore
の部分の構造体に 当リンク先 のように MarshalJSON() ([]byte, error)
メソッドを実装することで、ControlPlane.spec.config
を yaml.Marshal するだけで実設定ファイルに変換できるようにしました。
ただ実装してから気付いたこととして、 CRD のスキーマを表す構造体に直接 MarshalJSON() ([]byte, error)
メソッドを実装すると以下のようになってしまうためおそらくアンチパターンであると思っています。
- ユーザがリソースを適用する
- コントローラの Reconciliation Loop が走る。このとき Reconciliation Loop 内で該当オブジェクトの Update を行うと、更新対象のオブジェクトを表す構造体に対し json.Marshal が実行される
- 結果 Kubernetes へ以下ようなオブジェクトの適用が走る。当スキーマは CRD のスキーマ定義と異なるため、オブジェクトの Update はエラーする。
apiVersion: pipecd.kanatakita.com/v1alpha1
kind: ControlPlane
spec:
config:
datastore:
### この部分が invalid
type: MONGODB
config:
url: mongodb://pipecd-mongodb:27017/quickstart
database: quickstart
###
上記問題を回避するために CRD のスキーマを表す構造体に MarshalJSON() ([]byte, error)
を直接生やさずに、別途構造体を用意する必要があると思っています。
オブジェクトの削除処理の順番を制御したい
pipecd-operator で作成するオブジェクトには以下のような依存関係があります。
-
ControlPlane
オブジェクトを適用し PipeCD Control Plane がデプロイされないと、Environment
,Piped
オブジェクトに紐づく外部リソースが作成されない -
Environment
オブジェクトを適用し PipeCD に Environment が作成されないと、Piped
オブジェクトに紐づく外部リソースが作成されない
上記より、依存先オブジェクトが存在する限り依存元オブジェクトも削除されないようにしたいです。
これを Finalizers を用い実現しようとすると以下の図のようになります。
pipecd-operator では現状、各オブジェクトの Finalizers の操作に Patch を用いています。
ここで、上記のケースにて Patch で操作しようとすると以下のような問題があります。
- Merge Patch の場合、更新対象のフィールドが slice なため要素の追加はできるが削除ができない
- Server-side Apply Patch の場合、同時に複数コントローラが更新対象 Object を Get した後に Patch しようとすると Conflict する (
Error from server (Conflict): Apply failed with 1 conflict: ...
)
そのため現状の実装は、コントローラは自身の管理するオブジェクト以外の更新 (上図の例だと controller A から object B の更新) をしないようにしています。
削除処理の順番を制御するために、以下のいずれかの解決策を考えています。
- Server-side Apply Patch を使う。その際、更新対象 Object を Get してから Patch するまで他コントローラが Object を Get 出来ないよう Redis 等でセマフォを用意する。
- Patch ではなく Update を用い
metadata.finalizers
フィールドを書き換える
当問題に関して、そもそもコントローラが自身の管理するオブジェクト以外を更新することがお作法的によろしくないのではないかという懸念があるため、もし削除処理の順番を制御できる他の方法を知っている方が居られたら教えていただきたいです。
感想
PipeCD を Kubernetes API から操作できるよう Kubebuilder で Operator を実装しました。
Kuberenetes Operator はこれまでもいくつか利用したことが有りましたが、自分で実際に実装してみて初めて得られる知見が色々ありました。
また Operator から PipeCD へ繋ぎこむ部分は PipeCD の実装を読み仕様を理解しながら実装したため、実際に動作したときの喜びがありとても楽しかったです。
上記で述べたとおりまだ実装途中な部分 (例. PipeCD Application を操作する Controller) や PipeCD の最新版に追従しきれていない部分 (例. API Key を用いた PipeCD との通信) 、この Advent Calendar の締切駆動開発をしてしまったせいで不十分な部分 (パッケージ構成が雑なことやテストが無いこと等) が色々あるので、これからも継続的に実装を進めたい思います。
Discussion