在Grafana实验室,我们使用Tanka将工作负载部署到我们的Kubernetes集群。随着我们组织的发展,我们问自己。我们应该如何以一致的方式大规模地管理工作负载的配置?
一点背景
一开始,工程师们都是手动调用Tanka,从他们的本地机器上应用配置。因为Tanka是为了将Tanka环境与单个Kubernetes集群紧密结合,并设置一个默认的命名空间,他们经常会发现本地的Kube上下文与他们想要应用的集群不匹配。
从那时起,我们的工程团队迅速成长,Kubernetes集群的数量增加,Tanka环境的数量也爆炸性增长。这给工程师们带来了沉重的负担,他们经常要向有多少个集群的Tanka环境提出申请,弄清漂移的情况,因为人们会犯错,而且经常忘记一个或另一个环境。为了解决这个问题,我们自然地实施了持续部署。这解决了我们在现有集群和环境中的许多问题。
随着Grafana实验室的不断发展,我们的工程师的平台也需要增长。这意味着更多的Kubernetes集群,甚至更多的Tanka环境。你已经看到这里的问题了吗?这是一个永无止境的故事。管理所有这些Tanka环境成为一个真正的麻烦。不同集群的应用环境开始相互偏离,因为一些集群需要的配置与其他集群略有不同。随着新集群的不断增加或迁移,工程师们需要手动启动一个新的环境,手动将其与新的API服务器耦合,并创建命名空间,重新考虑集群的特定例外。
使Tanka环境保持一致
上述问题都会对业务产生潜在的影响。例如,集群之间的漂移使得事件中的调试更加困难,缺失的命名空间会阻碍CD进程,而启动则会增加工程成本。
让我们把它归结为三个问题:
- 不同集群间同一应用的配置漂移。
- 在新集群中引导新的Tanka环境。
- 引导新集群(例如,创建命名空间)。
我们在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 与原始文件的匹配。