LoginSignup
5
4

More than 3 years have passed since last update.

Kubernetes Custom Resource for GCS on GKE

Last updated at Posted at 2019-05-15

はじめに

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} は、自身の管理化にあるドメインを指定した方が良いのでしょう。

init
$ kubebuilder init --domain {my.domain}#実際に私が入力したコマンド
$ kubebuilder init --domain matsumo.dev

次にControllerを作成するための雛形を作成します。

create
$ 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.gogcs_controller.goが対象となります。

gcs_types.go

GCSのバケットを扱うために、下記情報が必要となります。

  • 論理的なバケット名
  • 物理的なバケット名
  • バケットのプロジェクトID

人が宣言する情報は、Spec として仕様を記載します。論理的なバケット名をBucketName、プロジェクトIDをProjectIDとして定義しました。

gcs_types.go
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を追加で定義しています。

gcs_types.go
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との連携が入っているのでまずそれを全て削除します。
また、それに付随し、アノテーションも色々書かれているので削除します。
アノテーションは、

gcs_controller.go
// +kubebuilder:rbac:groups=storage.matsumo.dev,resources=gcs,verbs=get;list;watch;create;update;patch;delete

のように書かれているものです。主にRBAC周りが多いです。

そして実際のビジネスロジックを記載していくわけですが、変更するのは、Reconcileメソッドが中心となります。

下記の部分で、まずは、gcsオブジェクトの情報を取得しに行きます。取れなかったエラーです。

gcs_controller.go
    // 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文のブロックで処理は終了します。

gcs_controller.go
    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を登録します。
また、実際にバケットが作成されていない場合は、バケットの物理的名称を生成してセットします。

gcs_controller.go
    // 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情報が更新されていたら、実際に更新を行います。

gcs_controller.go
    if modified {
        if err := r.Update(context.Background(), instance); err != nil {
            return reconcile.Result{}, err
        }
    }

最後に、GCSのバケットが作成されていなかったら、作成を行います。
実際のGCSの操作は、gcs_operater.goファイルとして独自に作成しています。

gcs_controller.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にマウント情報を追加しました。

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から読み込むようにしました。

manager.yaml
        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の名前でアクセスしなくてはなりません。

  1. Serviceのようにproxyを挟む
  2. podのinitで環境変数、設定を埋め込む
  3. 設定は・・・アプリケーション側のmanifestで定義して手動なりなんなりで書き換える。

という形での実現になりそうです。1、もしくは2.じゃないと管理がめんどくさいですね。
もっと簡単にできればいいんですけど・・・。

まだ想定外の処理がいまいち

  • 削除処理にてすでにバケットが存在しなかったら無限ループするなぁ・・・とか思ってます。 そのうち直そうと思います。 修正完了 commit messageが・・・
  • 作成処理もいまいち。バケット名がconflictした場合、 Statusの情報をクリアしてループさせるか何かしないとこれも無限ループするのかな?と思ってます。(試して直せよ・・・という話ですけど ) 対応完了

参考

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4