GKEのConfig Syncについて内部と挙動を調査してみた

AvatarPosted by

SREの徳田です。
今回は先日リリースされたConfig Syncについて、内部のオブジェクトや挙動などについて調査してみました。

はじめに

Config Syncという機能がリリースされましたね。
Config Syncはざっくり言うとKubernetesクラスタにポリシー系統の設定をGitリポジトリと同期してくれるツールです。

要はポリシー用GitOpsツールというところでしょうか。一味違うのは、

  • 抽象名前空間ディレクトリという考えがあり、設定を継承する仕組みがある
  • ClusterSelectorやNamespaceSelectorという特定の条件によって設定を適用させる仕組みがある

などがあります。

詳しくはこの辺りを見てください。

Config Syncの概要
Config Syncのコンセプト
Config Syncのインストール

これらに目を通すなり実際に動かしてみると雰囲気はわかるかと思います。

注意:
  この記事はTutorialなどをやり、Config Syncについての一定の知識・理解がある人向けです。
  また、この記事での内容・挙動はDocumentationされている内容では ない ため、変更される可能性があります。

出てくる疑問

自分が上の文献を見つつ色々やってみたところ、いくつか疑問が出てきました。

  • どのリソースがGitリポジトリからの同期対象なのか
  • どのリソースが継承される対象なのか
  • 内部でどのようなリソースが作成・管理されているのか

これらについていくつか検証・調査してみることにしました。

Config Syncインストール直後

まず、Config Syncをインストールした際にManifestを適用していますが、何を適用しているか見てみましょう。

$ grep '^kind' config-sync-operator.yaml
kind: CustomResourceDefinition
kind: ClusterRole
kind: ClusterRoleBinding
kind: ServiceAccount
kind: Deployment
kind: Namespace

これを見てざっとわかるのは

  • ConfigManagementというCRD
  • CRDで定義したリソースを処理するDeployment
  • CRDのリソースに対して操作するためのClusterRole/ClusterRoleBinding/ServiceAccount
  • Config Syncのコンポーネントが動作するNamespace

というところでしょうか。
実のところはConfig Syncを導入するためのOperatorです。
これ以上気になる方はインストール時のManifestを眺めてみてください。

ConfigManagementリソースを適用した後

さて、Tutorialに沿ってGitのリポジトリをForkしてConfigManagementリソースを作成すると、インストール時に作成したNamespaceに色々作成されます。

$ kubectl get all -n config-management-system
NAME                                READY   STATUS    RESTARTS   AGE
pod/git-importer-5c9dd78497-2t676   2/2     Running   0          103s
pod/monitor-c46b96d69-f6c7d         1/1     Running   0          103s
pod/syncer-78fdcbbfbc-zzw8c         1/1     Running   0          103s

NAME                   TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
service/git-importer   ClusterIP   10.0.36.112           8675/TCP   103s
service/monitor        ClusterIP   10.0.35.179           8675/TCP   103s
service/syncer         ClusterIP   10.0.40.180           8675/TCP   102s

NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/git-importer   1/1     1            1           103s
deployment.apps/monitor        1/1     1            1           103s
deployment.apps/syncer         1/1     1            1           103s

NAME                                      DESIRED   CURRENT   READY   AGE
replicaset.apps/git-importer-5c9dd78497   1         1         1       103s
replicaset.apps/monitor-c46b96d69         1         1         1       103s
replicaset.apps/syncer-78fdcbbfbc         1         1         1       103s

パッと見て、

  • git-importer: Gitリポジトリを取り込む
  • monitor: リソースの変更をモニタする
  • syncer: Configなどの同期をする

という感じでしょうか。

さて、ここでCRDを見てみます。

$ kubectl get crd
NAME                                         CREATED AT
backendconfigs.cloud.google.com              2020-03-18T11:47:54Z
clusterconfigs.configmanagement.gke.io       2020-03-18T12:21:51Z
clusters.clusterregistry.k8s.io              2020-03-18T12:21:51Z
clusterselectors.configmanagement.gke.io     2020-03-18T12:21:51Z
configmanagements.configmanagement.gke.io    2020-03-18T12:09:40Z
fulfillmentcenters.foo-corp.com              2020-03-18T12:22:13Z
hierarchyconfigs.configmanagement.gke.io     2020-03-18T12:21:51Z
managedcertificates.networking.gke.io        2020-03-18T11:47:21Z
namespaceconfigs.configmanagement.gke.io     2020-03-18T12:21:51Z
namespaceselectors.configmanagement.gke.io   2020-03-18T12:21:52Z
repos.configmanagement.gke.io                2020-03-18T12:21:52Z
scalingpolicies.scalingpolicy.kope.io        2020-03-18T11:47:22Z
storagestates.migration.k8s.io               2020-03-18T11:47:20Z
storageversionmigrations.migration.k8s.io    2020-03-18T11:47:19Z
syncs.configmanagement.gke.io                2020-03-18T12:21:52Z
updateinfos.nodemanagement.gke.io            2020-03-18T11:47:21Z

結構色々と追加されてみます。ConfigManagementリソースを作成した後に作成されたCRDだけに絞ってみると以下の8個のCRDになります。
(実際は9個なのですが、そのうち1つは同期対象のリポジトリに定義されているCRDになります)

  • clusterconfigs.configmanagement.gke.io
  • clusters.clusterregistry.k8s.io
  • clusterselectors.configmanagement.gke.io
  • hierarchyconfigs.configmanagement.gke.io
  • namespaceconfigs.configmanagement.gke.io
  • namespaceselectors.configmanagement.gke.io
  • repos.configmanagement.gke.io
  • syncs.configmanagement.gke.io
$ kubectl get crd -o jsonpath='{.items[*].spec.names.kind}' \
    clusterconfigs.configmanagement.gke.io \
    clusters.clusterregistry.k8s.io \
    clusterselectors.configmanagement.gke.io \
    hierarchyconfigs.configmanagement.gke.io \
    namespaceconfigs.configmanagement.gke.io \
    namespaceselectors.configmanagement.gke.io \
    repos.configmanagement.gke.io \
    syncs.configmanagement.gke.io
ClusterConfig Cluster ClusterSelector HierarchyConfig NamespaceConfig NamespaceSelector Repo Sync

ということで以下のオブジェクトが作成されたようです。

  • ClusterConfig
  • Cluster
  • ClusterSelector
  • HierarchyConfig
  • NamespaceConfig
  • NamespaceSelector
  • Repo
  • Sync

この内、

  • Cluster
  • ClusterSelector
  • NamespaceSelector

の3つについてはドキュメントに説明がありますし、それ以上のことがないので調査から外します。
よって

  • ClusterConfig
  • HierarchyConfig
  • NamespaceConfig
  • Repo
  • Sync

についてどういうものなのか見ていきましょう!

Repo

実はこのRepoオブジェクトはチュートリアルで使用したRepoのsystemフォルダに入っているリソースです。
一応クラスタに保存されているRepoを取得してみましょう。

$ kubectl get repo
NAME   AGE
repo   37m

GitリポジトリのConfigの通り、一つだけリソースがあるようです。中を見てみます。

$ kubectl get repo/repo -o yaml
apiVersion: configmanagement.gke.io/v1
kind: Repo
metadata:
  creationTimestamp: "2020-03-18T12:22:08Z"
  generation: 1
  name: repo
  resourceVersion: "18097"
  selfLink: /apis/configmanagement.gke.io/v1/repos/repo
  uid: 263d20fa-64a2-4b8f-9279-69c4f47970dc
spec:
  version: 1.0.0
status:
  import:
    lastUpdate: "2020-03-18T12:22:19Z"
    token: 0f8738755c64402c09a8f5e1936bd6f7c96ef536
  source:
    token: 0f8738755c64402c09a8f5e1936bd6f7c96ef536
  sync:
    lastUpdate: "2020-03-18T13:00:03Z"
    latestToken: 0f8738755c64402c09a8f5e1936bd6f7c96ef536

完全に内部の状態を管理するファイルですねw
spec には追跡するべきブランチの情報、 status には各コンポーネントの情報が入っています。

ClusterConfig

clusterconfigを取得してみましょう。

$ kubectl get clusterconfig
NAME                                   AGE
config-management-cluster-config       28m
config-management-crd-cluster-config   28m

どうやらCRDとそれ以外のconfigがあるみたいですね。
yamlで出してみましょう。
(長いですがご容赦ください🙇‍♂️)

$ kubectl get clusterconfig
apiVersion: v1
items:
- apiVersion: configmanagement.gke.io/v1
  kind: ClusterConfig
  metadata:
    creationTimestamp: "2020-03-18T12:22:13Z"
    generation: 1
    name: config-management-cluster-config
    resourceVersion: "9252"
    selfLink: /apis/configmanagement.gke.io/v1/clusterconfigs/config-management-cluster-config
    uid: df96ee68-eff8-4c98-bfc7-ed6236562544
  spec:
    importTime: "2020-03-18T12:22:13Z"
    resources:
    - group: rbac.authorization.k8s.io
      kind: ClusterRole
      versions:
      - objects:
        - apiVersion: rbac.authorization.k8s.io/v1
          kind: ClusterRole
          metadata:
            annotations:
              configmanagement.gke.io/cluster-name: my-cluster
              configmanagement.gke.io/source-path: cluster/namespace-reader-clusterrole.yaml
            creationTimestamp: null
            name: namespace-reader
          rules:
          - apiGroups:
            - ""
            resources:
            - namespaces
            verbs:
            - get
            - watch
            - list
        - apiVersion: rbac.authorization.k8s.io/v1
          kind: ClusterRole
          metadata:
            annotations:
              configmanagement.gke.io/cluster-name: my-cluster
              configmanagement.gke.io/source-path: cluster/pod-creator-clusterrole.yaml
            creationTimestamp: null
            name: pod-creator
          rules:
          - apiGroups:
            - ""
            resources:
            - pods
            verbs:
            - '*'
        version: v1
    - group: rbac.authorization.k8s.io
      kind: ClusterRoleBinding
      versions:
      - objects:
        - apiVersion: rbac.authorization.k8s.io/v1
          kind: ClusterRoleBinding
          metadata:
            annotations:
              configmanagement.gke.io/cluster-name: my-cluster
              configmanagement.gke.io/source-path: cluster/namespace-reader-clusterrolebinding.yaml
            creationTimestamp: null
            name: namespace-readers
          roleRef:
            apiGroup: rbac.authorization.k8s.io
            kind: ClusterRole
            name: namespace-reader
          subjects:
          - apiGroup: rbac.authorization.k8s.io
            kind: User
            name: cheryl@foo-corp.com
        version: v1
    - group: policy
      kind: PodSecurityPolicy
      versions:
      - objects:
        - apiVersion: policy/v1beta1
          kind: PodSecurityPolicy
          metadata:
            annotations:
              configmanagement.gke.io/cluster-name: my-cluster
              configmanagement.gke.io/source-path: cluster/pod-security-policy.yaml
            creationTimestamp: null
            name: psp
          spec:
            fsGroup:
              rule: RunAsAny
            runAsUser:
              rule: RunAsAny
            seLinux:
              rule: RunAsAny
            supplementalGroups:
              rule: RunAsAny
            volumes:
            - '*'
        version: v1beta1
    token: 0f8738755c64402c09a8f5e1936bd6f7c96ef536
  status:
    syncState: synced
    syncTime: "2020-03-18T12:22:18Z"
    token: 0f8738755c64402c09a8f5e1936bd6f7c96ef536
- apiVersion: configmanagement.gke.io/v1
  kind: ClusterConfig
  metadata:
    creationTimestamp: "2020-03-18T12:22:13Z"
    generation: 1
    name: config-management-crd-cluster-config
    resourceVersion: "9205"
    selfLink: /apis/configmanagement.gke.io/v1/clusterconfigs/config-management-crd-cluster-config
    uid: f88f3f07-158f-42b1-8711-6f24caf02d39
  spec:
    importTime: "2020-03-18T12:22:13Z"
    resources:
    - group: apiextensions.k8s.io
      kind: CustomResourceDefinition
      versions:
      - objects:
        - apiVersion: apiextensions.k8s.io/v1beta1
          kind: CustomResourceDefinition
          metadata:
            annotations:
              configmanagement.gke.io/cluster-name: my-cluster
              configmanagement.gke.io/source-path: cluster/fulfillmentcenter-crd.yaml
            creationTimestamp: null
            name: fulfillmentcenters.foo-corp.com
          spec:
            group: foo-corp.com
            names:
              kind: FulfillmentCenter
              plural: fulfillmentcenters
              singular: fulfillmentcenter
            scope: Namespaced
            validation:
              openAPIV3Schema:
                properties:
                  spec:
                    properties:
                      address:
                        type: string
                    required:
                    - address
                    type: object
            versions:
            - name: v1
              served: true
              storage: false
            - name: v2
              served: true
              storage: true
          status:
            acceptedNames:
              kind: ""
              plural: ""
            conditions: null
            storedVersions: null
        version: v1beta1
    token: 0f8738755c64402c09a8f5e1936bd6f7c96ef536
  status:
    syncState: synced
    syncTime: "2020-03-18T12:22:13Z"
    token: 0f8738755c64402c09a8f5e1936bd6f7c96ef536
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

このリソースを見たところ、

  • Cluster-wideなオブジェクト毎にリソースをまとめている
  • おそらくこれはこのClusterに適用されるべきConfig群
  • CRDだけ別のManifestとして管理されている

という感じですね。
これらのリソースが実際にClusterに適用されており、その際CRDから適用が行われているのでしょう。
気が利いてますね!!

このリソースはconfig-management-system内部で利用されるリソースであって、我々が編集する余地はなさそうですね。

NamespaceConfig

ClusterConfigというオブジェクトがあったところみると、これはそのNamespace版、というところでしょうか。
見てみましょう!

$ kubectl get namespaceconfig
NAME               AGE
audit              40m
shipping-dev       40m
shipping-prod      40m
shipping-staging   40m

GitリポジトリにConfigとしておいてあるNamespaceたちが表示されていますね。
代表して色々SelectorなどでConfigが適用されていた shipping-prod のリソースを見てみましょう。
(長いですがご容赦ください🙇‍♂️)

$ kubectl get namespaceconfig shipping-prod -o yaml
apiVersion: configmanagement.gke.io/v1
kind: NamespaceConfig
metadata:
  annotations:
    audit: "true"
    configmanagement.gke.io/cluster-name: my-cluster
    configmanagement.gke.io/source-path: namespaces/online/shipping-app-backend/shipping-prod/namespace.yaml
  creationTimestamp: "2020-03-18T12:22:13Z"
  generation: 1
  labels:
    env: prod
  name: shipping-prod
  resourceVersion: "9272"
  selfLink: /apis/configmanagement.gke.io/v1/namespaceconfigs/shipping-prod
  uid: 252a5203-02e6-41ca-9fba-0ae934e16491
spec:
  deleteSyncedTime: null
  importTime: "2020-03-18T12:22:13Z"
  resources:
  - group: foo-corp.com
    kind: FulfillmentCenter
    versions:
    - objects:
      - apiVersion: foo-corp.com/v1
        kind: FulfillmentCenter
        metadata:
          annotations:
            configmanagement.gke.io/cluster-name: my-cluster
            configmanagement.gke.io/source-path: namespaces/online/shipping-app-backend/shipping-prod/fulfillmentcenter.yaml
          name: production
          namespace: shipping-prod
        spec:
          address: 100 Industry St.
      version: v1
  - group: rbac.authorization.k8s.io
    kind: RoleBinding
    versions:
    - objects:
      - apiVersion: rbac.authorization.k8s.io/v1
        kind: RoleBinding
        metadata:
          annotations:
            configmanagement.gke.io/cluster-name: my-cluster
            configmanagement.gke.io/namespace-selector: sre-supported
            configmanagement.gke.io/source-path: namespaces/sre-rolebinding.yaml
          creationTimestamp: null
          name: sre-admin
          namespace: shipping-prod
        roleRef:
          apiGroup: rbac.authorization.k8s.io
          kind: ClusterRole
          name: admin
        subjects:
        - apiGroup: rbac.authorization.k8s.io
          kind: Group
          name: sre@foo-corp.com
      - apiVersion: rbac.authorization.k8s.io/v1
        kind: RoleBinding
        metadata:
          annotations:
            configmanagement.gke.io/cluster-name: my-cluster
            configmanagement.gke.io/source-path: namespaces/viewers-rolebinding.yaml
          creationTimestamp: null
          name: viewers
          namespace: shipping-prod
        roleRef:
          apiGroup: rbac.authorization.k8s.io
          kind: ClusterRole
          name: view
        subjects:
        - apiGroup: rbac.authorization.k8s.io
          kind: Group
          name: system:serviceaccounts:audit
      - apiVersion: rbac.authorization.k8s.io/v1
        kind: RoleBinding
        metadata:
          annotations:
            configmanagement.gke.io/cluster-name: my-cluster
            configmanagement.gke.io/source-path: namespaces/online/shipping-app-backend/pod-creator-rolebinding.yaml
          creationTimestamp: null
          name: pod-creators
          namespace: shipping-prod
        roleRef:
          apiGroup: rbac.authorization.k8s.io
          kind: ClusterRole
          name: pod-creator
        subjects:
        - apiGroup: rbac.authorization.k8s.io
          kind: User
          name: bob@foo-corp.com
      version: v1
  - kind: ResourceQuota
    versions:
    - objects:
      - apiVersion: v1
        kind: ResourceQuota
        metadata:
          annotations:
            configmanagement.gke.io/cluster-name: my-cluster
            configmanagement.gke.io/source-path: namespaces/online/shipping-app-backend/quota.yaml
          creationTimestamp: null
          name: backend-quota
          namespace: shipping-prod
        spec:
          hard:
            cpu: "1"
            memory: 1Gi
            pods: "3"
        status: {}
      version: v1
  token: 0f8738755c64402c09a8f5e1936bd6f7c96ef536
status:
  syncState: synced
  syncTime: "2020-03-18T12:22:18Z"
  token: 0f8738755c64402c09a8f5e1936bd6f7c96ef536

ClusterConfigとデータの持ち方はほぼ同じで、 shipping-prod のNamespaceに適用されていたリソース群がありますね!
また、NamespaceSelectorを使った適用をしていた sre-admin というRoleBindingがあることも確認できます。

このことからNamespaceConfigはNamespaceに適用するリソース群が保持されているリソースということがわかりました。

ClusterConfig同様、内部で利用されるリソースみたいですね!

HierarchyConfig

こちらのオブジェクト、実は1つも作成されていないし、CRDを見てもスキーマが定義されていないため、クラスタのリソースを見ただけでは全くなにかわかりません。

しかし、こちらのドキュメントに簡単にですが例が解説されています。

Manifestを引用します。

# system/hierarchy-config.yaml
kind: HierarchyConfig
apiVersion: configmanagement.gke.io/v1
metadata:
  name: rbac
spec:
  resources:
  # Configure Role to only be allowed in leaf namespaces.
  - group: rbac.authorization.k8s.io
    kinds: [ "RoleBinding" ]
    hierarchyMode: none

どうやらHierarchyConfigは同期対象のリソースについての継承をするかしないかを設定できるようです。
hierarchyModenone を指定することで継承させないように設定できるようですね。
この hierarchyMode に何が設定できるのだろうか?と思いましたが、リファレンスなども特にないため、 hoge と入れて適用させてみたところ、 git-importer の importer コンテナでエラーが出ておりました。

[1] KNV1042: HierarchyMode "hoge" is not a valid value for the APIResource "RoleBinding.rbac.authorization.k8s.io". Allowed values are [none,inherit].

とのことで、 hierarchyMode には none / inherit が設定できるようですね。

まとめると、HierarchyConfigオブジェクトでは指定のオブジェクトの継承について設定することができ、Gitリポジトリの system/ ディレクトリに配置して設定する、ということですね。

Sync

最後にSyncです。
とりあえず取得してみましょう。

$ kubectl get sync
NAME                                            AGE
clusterrole.rbac.authorization.k8s.io           90m
clusterrolebinding.rbac.authorization.k8s.io    90m
customresourcedefinition.apiextensions.k8s.io   90m
fulfillmentcenter.foo-corp.com                  90m
podsecuritypolicy.policy                        90m
resourcequota                                   90m
role.rbac.authorization.k8s.io                  90m
rolebinding.rbac.authorization.k8s.io           90m

どうやらポリシー系統のオブジェクトとCRDに関する名前のリソースがあります。
一つピックアップして見てみましょう。

$ kubectl get sync resourcequota -o yaml
apiVersion: configmanagement.gke.io/v1
kind: Sync
metadata:
  creationTimestamp: "2020-03-18T12:22:13Z"
  finalizers:
  - syncer.configmanagement.gke.io
  generation: 1
  name: resourcequota
  resourceVersion: "9227"
  selfLink: /apis/configmanagement.gke.io/v1/syncs/resourcequota
  uid: 37706879-d091-4b6e-b40c-2987c5f47ab9
spec:
  group: ""
  kind: ResourceQuota
status:
  status: syncing

spec を見る限り、APIオブジェクトを指定しているみたいです。また、 status に同期しているかの状態が保存されています。

これから推測するに、オブジェクトの同期対象をこのリソースによって設定しているようですね。

検証してみる

長々リソースなどを見てきて、いじって面白そうなものは以下の2つでしょう。

  • HierarchyConfig
  • Sync

この2つがうまく設定できれば任意のリソースを継承させたりしつつクラスタに同期させることができます。

それでは、

Deploymentを抽象名前空間に配置し、継承されつつ各Namespaceに同期されるか

これを確認してみましょう。
とりあえずHierarchyConfigだけ作ってみてCommitしてみましょう。

以下のManifestを system/deployment.yaml に保存します。

apiVersion: configmanagement.gke.io/v1
kind: HierarchyConfig
metadata:
  name: deployment
spec:
  resources:
  - group: apps
    kinds: [ "Deployment" ]
    hierarchyMode: inherit

そしてnginxを動かすDeploymentを namespaces/online/shipping-app-backend/nginx.yaml として保存します。
対象のNamespaceにはResourceQuotaが作成されるのでCPUなどのPodのリソースについての指定をします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        resources:
          limits:
            cpu: 100m
            memory: 8Mi

それではCommitしてにPushしてみます。
こちらのリポジトリで確認できます。

するとどうでしょう・・・

$ kubectl get deploy --all-namespaces | grep nginx
shipping-dev               nginx                                      1/1     1            1           6m16s
shipping-prod              nginx                                      1/1     1            1           6m17s
shipping-staging           nginx                                      1/1     1            1           6m16s

各NamespaceにDeploymentが継承されてる・・・!
ちゃんと同期されて動いています!!

ここでSyncはどうなっているでしょうか?確認してみましょう!

$ kubectl get sync
NAME                                            AGE
clusterrole.rbac.authorization.k8s.io           114m
clusterrolebinding.rbac.authorization.k8s.io    114m
customresourcedefinition.apiextensions.k8s.io   114m
deployment.apps                                 7m43s
fulfillmentcenter.foo-corp.com                  114m
podsecuritypolicy.policy                        114m
resourcequota                                   114m
role.rbac.authorization.k8s.io                  114m
rolebinding.rbac.authorization.k8s.io           114m

deployment.apps が追加されています!
どうやらHierarchyConfigの設定をもとに内部でSyncリソースを作成してくれているみたいです。

これによって任意のリソースを継承の設定をさせつつ同期させることができますね!!

最後に

Config Syncで使用されているオブジェクトについて確認し、そこから簡単な検証を行いました。
Config SyncにおいてHierarchyConfigが全てな感じがありますね!

ただ、何でもかんでもConfig Syncで同期させるのはあまり良くなさそうだと思っており、ポリシー系のオブジェクトのみにとどめておくべきだなと感じました。
通常のアプリケーションなどで利用するオブジェクトは継承の仕組みがあまり必要ないため、と個人的に思っています。それ以外の観点では基本Config Syncのコンセプトで解説されていますのでそちらをどうぞw
しかし、ポリシー系の拡張されたリソースなどはあるので、それらをConfig Syncのシステムで管理するのはとても良さそうだと思っています!

さておき、通常のアプリケーションはどう管理すればいいんだ!となるところですが、これまた最近Application Managerというアドオンがリリースされたようで、そちらで管理するのが良さそうです。
こちらのアドオンも近いうち試してみたいと思っています!

それでは!!🙋‍♂️