使用Tanka进行大规模一致的配置管理的最佳实践

334 阅读5分钟

在Grafana实验室,我们使用Tanka将工作负载部署到我们的Kubernetes集群。随着我们组织的发展,我们问自己。我们应该如何以一致的方式大规模地管理工作负载的配置?

一点背景

一开始,工程师们都是手动调用Tanka,从他们的本地机器上应用配置。因为Tanka是为了将Tanka环境与单个Kubernetes集群紧密结合,并设置一个默认的命名空间,他们经常会发现本地的Kube上下文与他们想要应用的集群不匹配。

从那时起,我们的工程团队迅速成长,Kubernetes集群的数量增加,Tanka环境的数量也爆炸性增长。这给工程师们带来了沉重的负担,他们经常要向有多少个集群的Tanka环境提出申请,弄清漂移的情况,因为人们会犯错,而且经常忘记一个或另一个环境。为了解决这个问题,我们自然地实施了持续部署。这解决了我们在现有集群和环境中的许多问题。

随着Grafana实验室的不断发展,我们的工程师的平台也需要增长。这意味着更多的Kubernetes集群,甚至更多的Tanka环境。你已经看到这里的问题了吗?这是一个永无止境的故事。管理所有这些Tanka环境成为一个真正的麻烦。不同集群的应用环境开始相互偏离,因为一些集群需要的配置与其他集群略有不同。随着新集群的不断增加或迁移,工程师们需要手动启动一个新的环境,手动将其与新的API服务器耦合,并创建命名空间,重新考虑集群的特定例外。

使Tanka环境保持一致

上述问题都会对业务产生潜在的影响。例如,集群之间的漂移使得事件中的调试更加困难,缺失的命名空间会阻碍CD进程,而启动则会增加工程成本。

让我们把它归结为三个问题:

  1. 不同集群间同一应用的配置漂移。
  2. 在新集群中引导新的Tanka环境。
  3. 引导新集群(例如,创建命名空间)。

我们在Jsonnet中以Tanka内联环境为基础解决了这些问题。内联环境允许我们动态地创建具有Jsonnet所提供的所有功能和结构的环境。一个内联环境只不过是spec.json ,其中有一个data: 字段,包含Kubernetes资源。有了这个 tanka-util库,我们可以快速配置一个新的Tanka环境:

// environments/grafana/main.jsonnet
local grafana = import 'github.com/grafana/jsonnet-libs/grafana/grafana.libsonnet';
local tanka = import 'github.com/grafana/jsonnet-libs/tanka-util/main.libsonnet';

{
  local this = self,

  data:: {
    grafana:
      grafana
      + grafana.withAnonymous(),
  },

  env:
    tanka.environment.new(
      name='grafana',
      namespace='grafana',
      apiserver='https://127.0.1.1:6443',
    )
    + tanka.environment.withLabels({ cluster: cluster.name })
    + tanka.environment.withData(this.data),
}

为了在一个新的集群中加入这个,我们可以用相同的数据块做一个env: ,让我们已经有了更多的一致性。然而,这仍然是手工劳动。让我们更进一步,也用Jsonnet来描述我们的集群:

// lib/meta/meta.libsonnet
{
  clusters: {
    'dev-01': {
      name: 'dev-01',
      status: 'dev',
      apiserver: 'https://127.0.1.1:6443',
    },
    'prod-01': {
      name: 'prod-01',
      status: 'prod',
      apiserver: 'https://127.0.2.1:6443',
    },
  },
}

这将使我们能够在开发和生产集群中创建一致的Grafana部署,只需几行额外的代码:

 // environments/grafana/main.jsonnet
 local grafana = import 'github.com/grafana/jsonnet-libs/grafana/grafana.libsonnet';
 local tanka = import 'github.com/grafana/jsonnet-libs/tanka-util/main.libsonnet';
+local meta = import 'meta/meta.libsonnet';

 {
   local this = self,

   data:: {
     grafana:
       grafana
       + grafana.withAnonymous(),
   },

-   env:
+   env(cluster)::
     tanka.environment.new(
       name='grafana',
       namespace='grafana',
-      apiserver='https://127.0.1.1:6443',
+      apiserver=cluster.apiserver,
     )
     + tanka.environment.withLabels({ cluster: cluster.name })
     + tanka.environment.withData(this.data),

+  envs: {
+    [name]: this.env(meta.clusters[name])
+    for name in std.objectFields(meta.clusters)
+  },
 }

这可以防止问题1,集群之间的配置漂移。

让我们把Grafana添加到另一个集群:

 // lib/meta/meta.libsonnet
 {
   clusters: {
     'dev-01': {
       name: 'dev-01',
       status: 'dev',
       apiserver: 'https://127.0.1.1:6443',
     },
+    'dev-02': {
+      name: 'dev-02',
+      status: 'dev',
+      apiserver: 'https://127.0.1.2:6443',
+    },
     'prod-01': {
       name: 'prod-01',
       status: 'prod',
       apiserver: 'https://127.0.2.1:6443',
     },
   },
 }

就是这样!不需要再对Grafana应用配置或Tanka环境进行修改。这就缓解了问题2,即引导新的Tanka环境:

注意:在Grafana实验室,我们用Terraform创建我们的Kubernetes集群。因此,lib/meta 中的集群列表是由Terraform生成的,这就更加减轻了人工负担。

我听到你的声音了。"但是,但是......我有一个集群与其他集群不同;那个'漂移'是故意的!"针对集群的覆盖可以简单地通过扩展envs 对象来完成:

 // environments/grafana/main.jsonnet
 local grafana = import 'github.com/grafana/jsonnet-libs/grafana/grafana.libsonnet';
 local tanka = import 'github.com/grafana/jsonnet-libs/tanka-util/main.libsonnet';
 local meta = import 'meta/meta.libsonnet';

 {
   local this = self,

   data:: {
     grafana:
       grafana
       + grafana.withAnonymous(),
   },

   env(cluster)::
     tanka.environment.new(
       name='grafana',
       namespace='grafana',
       apiserver=cluster.apiserver,
     )
     + tanka.environment.withLabels({ cluster: cluster.name })
     + tanka.environment.withData(this.data),

   envs: {
     [name]: this.env(meta.clusters[name])
     for name in std.objectFields(meta.clusters)
+  } + {
+    'prod-01'+: {
+      data+: { grafana+: grafana.withTheme('dark') },
+    },
   },
 }

命名空间的空间

传统上,命名空间要么是手动创建,要么是在Tanka环境中创建。如果一个命名空间是在Tanka环境中创建的,那么这个Tanka环境就有可能不是唯一拥有这个命名空间资源的环境。移除该命名空间可能会破坏该命名空间中的所有资源。

幸运的是,我们有所有的Tanka环境可以通过tk env list ,我们可以简单地根据Tanka环境规范来生成命名空间清单。让我们生成一个JSON数据文件,这样我们就可以在lib/meta 中使用它:

tk env list --json environments/ | jq . > lib/meta/raw/environments.json

看看lib/meta/raw/environments.json (修剪过的),并注意spec.namespace 的值:

[  {    "apiVersion": "tanka.dev/v1alpha1",    "kind": "Environment",    "metadata": {      "name": "grafana",      "namespace": "environments/grafana/main.jsonnet",      "labels": { "cluster": "dev-01" }    },    "spec": {      "apiServer": "https://127.0.1.1:6443",      "namespace": "grafana"    }  },  {    "apiVersion": "tanka.dev/v1alpha1",    "kind": "Environment",    "metadata": {      "name": "grafana",      "namespace": "environments/grafana/main.jsonnet",      "labels": { "cluster": "dev-02" }    },    "spec": {      "apiServer": "https://127.0.1.2:6443",      "namespace": "grafana"    }  },  {    "apiVersion": "tanka.dev/v1alpha1",    "kind": "Environment",    "metadata": {      "name": "grafana",      "namespace": "environments/grafana/main.jsonnet",      "labels": { "cluster": "prod-01" }    },    "spec": {      "apiServer": "https://127.0.2.1:6443",      "namespace": "grafana"    }  }]

从那里,我们可以生成一个命名空间/集群对的列表:

// lib/meta/meta.libsonnet
local envs = import './raw/environments.json';
{
  clusters: {/* ... */ },

  namespaces:
    std.foldr(
      function(env, k) k + (
        {
          [env.spec.namespace]+: {
            clusters+: [env.metadata.labels.cluster],
          },
        }
      ),
      envs,
      {}
    ),
}

而最终我们可以在Tanka的内联环境中生成命名空间清单,类似于Grafana环境:

//environments/cluster-resources/main.jsonnet
local k = import 'github.com/grafana/jsonnet-libs/ksonnet-util/kausal.libsonnet';
local tanka = import 'github.com/grafana/jsonnet-libs/tanka-util/main.libsonnet';
local meta = import 'meta/meta.libsonnet';

{
  local this = self,

  data(cluster):: {
    namespaces: {
      [ns]: k.core.v1.namespace.new(ns)
      for ns in std.objectFields(meta.namespaces)
      if std.length(std.find(cluster.name, meta.namespaces[ns].clusters)) > 0
    },
  },

  env(cluster)::
    tanka.environment.new(
      name='cluster-resources',
      namespace='namespace',
      apiserver=cluster.apiserver,
    )
    + tanka.environment.withLabels({ cluster: cluster.name })
    + tanka.environment.withData(this.data(cluster)),

  envs: {
    [name]: this.env(meta.clusters[name])
    for name in std.objectFields(meta.clusters)
  },
}

现在,如果我们用一个新的命名空间创建一个Tanka环境,我们更新lib/meta/raw/environments.json ,一个新的命名空间将被创建。只要该集群中存在具有grafana 命名空间的Tanka环境,命名空间的清单就会在这里。最后,CI检查验证了tk env list --json 与原始文件的匹配。