はじめに
Kubebuilderでは、リソースを独自定義する事ができます。
GCSの論理名を定義すれば、それをリソースとして宣言し、勝手に作成したり、削除したりしてくれないかな?という事を検証すること、またKubebuilderを学ぶために作ってみる事を目的としています。
GKEのリソースとして、 論理的なexample
というGCSのリソースを定義すれば、勝手にその物理的なバケット(example-xxxxxx
)も作成したりしてくれる・・・これがあると、k8sのnamaepsace毎、プロジェクト毎に同一のアプリケーションを構築するのがとても楽になるなと思った次第です。開発環境、ITb環境、ステージング環境・・・これが全て宣言的なコンポーンネントの管理が行えるようになり、CDがとてもしやすくなります。
なぜ、論理名と物理名を分けているか?と言えば、バケットは全GCPプロジェクトでユニークなバケット名の必要があり、「どの環境もこの名前で作成する」というのができないからです。ユニークにするのは、誰かが勝手に制御して欲しいわけです。
後、GCPでは Service Brokerがあります。通常の人はこちらを使った方が幸せになれます。
最初に学んだこと
これを、読むだけ。
https://book.kubebuilder.io/
出来上がったもの
初めてgolangで色々ソースを書きましたが本当に勉強になりました。。。
https://github.com/h-r-k-matsumoto/gcs-crd
https://cloud.docker.com/u/hirokimatsumoto/repository/docker/hirokimatsumoto/gcs-crd
説明
リポジトリの初期化
book.kubebuilder.io/quick-start.html にある通り、下記コマンドを実行しました。
{my.domain}
は、自身の管理化にあるドメインを指定した方が良いのでしょう。
$ kubebuilder init --domain {my.domain}
↓ #実際に私が入力したコマンド
$ kubebuilder init --domain matsumo.dev
次にControllerを作成するための雛形を作成します。
$ kubebuilder create api --group {group} --version {version} --kind {kind}
↓ #実際に私が入力したコマンド
$ kubebuilder create api --group storage --version v1 --kind gcs
ここらへんの詳細は、
KubebuilderでKubernetesのカスタムコントローラを作ってみる
をご確認した方が良いです。
ソースの修正
Kubebuilderで作る上では、ほとんどのケースでは、CRD (Custom Resource Definitions)の仕様となる xxxx_types.go
と、そのリソースのイベントを受信し、制御する xxxx_controller.go
を修正するだけで事足ります。
今回私が入力した情報だと、 gcs_types.goとgcs_controller.goが対象となります。
gcs_types.go
GCSのバケットを扱うために、下記情報が必要となります。
- 論理的なバケット名
- 物理的なバケット名
- バケットのプロジェクトID
人が宣言する情報は、Spec
として仕様を記載します。論理的なバケット名をBucketName
、プロジェクトIDをProjectID
として定義しました。
type GcsSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
ProjectID string `json:"projectID"`
BucketName string `json:"bucketName"`
}
人が原則管理しない、Controller側で管理すべき情報を、 Status
側に記載しています。
こちらにのみ、物理名を表す BucketFullName
を追加で定義しています。
type GcsStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
ProjectID string `json:"projectID"`
BucketName string `json:"bucketName"`
BucketFullName string `json:"bucketFullName"`
}
gcs_controller.go
雛形だと、余計なdeploymentsとの連携が入っているのでまずそれを全て削除します。
また、それに付随し、アノテーションも色々書かれているので削除します。
アノテーションは、
// +kubebuilder:rbac:groups=storage.matsumo.dev,resources=gcs,verbs=get;list;watch;create;update;patch;delete
のように書かれているものです。主にRBAC周りが多いです。
そして実際のビジネスロジックを記載していくわけですが、変更するのは、Reconcileメソッドが中心となります。
下記の部分で、まずは、gcsオブジェクトの情報を取得しに行きます。取れなかったエラーです。
// Fetch the Gcs instance
instance := &storagev1alpha1.Gcs{}
err = r.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
// Object not found, return. Created objects are automatically garbage collected.
// For additional cleanup logic use finalizers.
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
次に、リソースが削除されており、且つ後述するバケットを削除するためのFinalizerが存在しており、バケットが問題なく削除された場合は、Finalizerをクリアして更新します。
リソースが削除されている場合は、このif文のブロックで処理は終了します。
myFinalizerName := "bucket.finalizer.gcs.storage.matsumo.dev"
if !instance.ObjectMeta.DeletionTimestamp.IsZero() {
log.Info("delete object.")
// The object is being deleted
if containsString(instance.ObjectMeta.Finalizers, myFinalizerName) {
// our finalizer is present, so lets handle our external dependency
if err := r.deleteExternalDependency(instance); err != nil {
return reconcile.Result{}, err
}
// remove our finalizer from the list and update it.
instance.ObjectMeta.Finalizers = removeString(instance.ObjectMeta.Finalizers, myFinalizerName)
if err := r.Update(context.Background(), instance); err != nil {
return reconcile.Result{}, err
}
}
// if deleted, logic finished.
return reconcile.Result{}, nil
}
リソースが、まだ削除されていないのであれば、削除された時にバケットを削除するためのFinalizerを登録します。
また、実際にバケットが作成されていない場合は、バケットの物理的名称を生成してセットします。
// update finalizer,and bucket name.
modified := false
if !containsString(instance.ObjectMeta.Finalizers, myFinalizerName) {
instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, myFinalizerName)
modified = true
}
if instance.Spec.BucketName != instance.Status.BucketName {
instance.Status.BucketName = instance.Spec.BucketName
instance.Status.BucketFullName = GenerateBacketFullName(instance.Spec.BucketName)
modified = true
}
manifest情報が更新されていたら、実際に更新を行います。
if modified {
if err := r.Update(context.Background(), instance); err != nil {
return reconcile.Result{}, err
}
}
最後に、GCSのバケットが作成されていなかったら、作成を行います。
実際のGCSの操作は、gcs_operater.goファイルとして独自に作成しています。
client, err := NewGcsClient(context.Background())
if err != nil {
return reconcile.Result{}, err
}
//bucket operation.
exists := IfExistsBucket(context.Background(), client, instance.Spec.ProjectID, instance.Status.BucketFullName)
if !exists {
log.Info(fmt.Sprintf("create bucket[ %s ]", instance.Status.BucketFullName))
err := CreateBucket(context.Background(), client, instance.Spec.ProjectID, instance.Status.BucketFullName)
if err != nil {
return reconcile.Result{}, err
}
}
動作確認
README.mdのように動かせば動くと思います。
ハマったところ
make deployでエラー
この問題はもうすぐ対応されるでしょう。とりあえずディレクトリの配置を変えれば動きます。
$ make deploy
go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go all
CRD manifests generated under '/Users/hiroki-matsumoto/Dev/go/src/github.com/h-r-k-matsumoto/gcs-crd/config/crds'
RBAC manifests generated under '/Users/hiroki-matsumoto/Dev/go/src/github.com/h-r-k-matsumoto/gcs-crd/config/rbac'
kubectl apply -f config/crds
customresourcedefinition.apiextensions.k8s.io/gcs.storage.matsumo.dev created
kustomize build config/default | kubectl apply -f -
Error: rawResources failed to read Resources: Load from path ../rbac/rbac_role.yaml failed: security; file '../rbac/rbac_role.yaml' is not in or below '/Users/hiroki-matsumoto/Dev/go/src/github.com/h-r-k-matsumoto/gcs-crd/config/default'
error: no objects passed to apply
make: *** [deploy] Error 1
$
GKEで動かすとエラーになる。
x509: failed to load system roots and no roots provided
とエラーになります。
{"level":"error","ts":1557927559.5866375,"logger":"kubebuilder.controller","msg":"Reconciler error","controller":"gcs-controller","request":"default/gcs-sample","error":"Post https://www.googleapis.com/storage/v1/b?alt=json&prettyPrint=false&project=xxxxxx: x509: failed to load system roots and no roots provided","stacktrace":"github.com/h-r-k-matsumoto/gcs-crd/vendor/github.com/go-logr/zapr.(*zapLogger).Error\n\t/go/src/github.com/h-r-k-matsumoto/gcs-crd/vendor/github.com/go-logr/zapr/zapr.go:128\ngithub.com/h-r-k-matsumoto/gcs-crd/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).processNextWorkItem\n\t/go/src/github.com/h-r-k-matsumoto/gcs-crd/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go:217\ngithub.com/h-r-k-matsumoto/gcs-crd/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Start.func1\n\t/go/src/github.com/h-r-k-matsumoto/gcs-crd/vendor/sigs.k8s.io/controller-runtime/pkg/internal/controller/controller.go:158\ngithub.com/h-r-k-matsumoto/gcs-crd/vendor/k8s.io/apimachinery/pkg/util/wait.JitterUntil.func1\n\t/go/src/github.com/h-r-k-matsumoto/gcs-crd/vendor/k8s.io/apimachinery/pkg/util/wait/wait.go:133\ngithub.com/h-r-k-matsumoto/gcs-crd/vendor/k8s.io/apimachinery/pkg/util/wait.JitterUntil\n\t/go/src/github.com/h-r-k-matsumoto/gcs-crd/vendor/k8s.io/apimachinery/pkg/util/wait/wait.go:134\ngithub.com/h-r-k-matsumoto/gcs-crd/vendor/k8s.io/apimachinery/pkg/util/wait.Until\n\t/go/src/github.com/h-r-k-matsumoto/gcs-crd/vendor/k8s.io/apimachinery/pkg/util/wait/wait.go:88"}
動いているノードから証明書情報を持って来ましょう。manager.yamlにマウント情報を追加しました。
volumeMounts:
<<省略>>
- mountPath: /etc/ssl/certs
name: ssl-certs
<<省略>>
volumes:
<<省略>>
- name: ssl-certs
hostPath:
path: /etc/ssl/certs
独自のサービスアカウントでbucket操作したい
ServiceAccountをGOOGLE_APPLICATION_CREDENTIALSの環境変数を指定して使うようにmanager.yamlを変更しました。
/opt/google/certs/secret.json
から読み込むようにしました。
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /opt/google/certs/secret.json
次にvolumeMounts、volumesを指定します。secretgcs-service-account
を指定しました。
volumeMounts:
- mountPath: /opt/google/certs
name: google-apps-credential
volumes:
- name: google-apps-credential
secret:
secretName: gcs-service-account
最後に、secretgcs-service-account
を定義します
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: gcs-service-account
data:
secret.json: <<change your service account credential file>>
その他 (TODO )
テストに関して
頑張ります。まだあまり理解してません・・・。
アプリケーション側から、どう扱うか?
やはりアプリケーション側からは、実際のBucketの名前でアクセスしなくてはなりません。
- Serviceのようにproxyを挟む
- podのinitで環境変数、設定を埋め込む
- 設定は・・・アプリケーション側のmanifestで定義して手動なりなんなりで書き換える。
という形での実現になりそうです。1、もしくは2.じゃないと管理がめんどくさいですね。
もっと簡単にできればいいんですけど・・・。
まだ想定外の処理がいまいち
-
削除処理にてすでにバケットが存在しなかったら無限ループするなぁ・・・とか思ってます。 そのうち直そうと思います。修正完了 commit messageが・・・。 -
作成処理もいまいち。バケット名がconflictした場合、対応完了Status
の情報をクリアしてループさせるか何かしないとこれも無限ループするのかな?と思ってます。(試して直せよ・・・という話ですけど )