《Kubernetes Best Practices》第二章:开发者工作流程

4,174 阅读9分钟

Kubernetes旨在可靠地运行软件。它通过具有面向应用程序的API、自愈特性以及诸如零停机软件发布的部署工具等有用工具来简化应用程序的部署和管理。尽管所有这些工具都很有用,但它们并不能太多地简化为Kubernetes开发应用程序。这就是开发者工作流程的作用。尽管许多集群旨在运行生产应用程序,因此很少被开发者工作流程访问,但启用开发工作流程以针对Kubernetes进行开发至关重要,通常这意味着具有一个专为开发而设的集群或至少集群的一部分。建立这样一个集群以便轻松为Kubernetes应用程序进行开发对于确保在Kubernetes上取得成功至关重要。如果没有为您的集群构建代码,那么集群本身并没有完成太多工作。

目标

在描述构建开发集群的最佳实践之前,值得说明一下我们为这种集群设定的目标。显然,终极目标是使开发人员能够在Kubernetes上快速且轻松地构建应用程序,但这在实践中是什么意思,它如何反映在开发集群的实际特性中呢?

为了回答这个问题,让我们首先确定与集群的开发者互动的各个阶段。

第一个阶段是入职。这是新开发人员加入团队的时候。这个阶段包括为用户提供集群登录以及使他们熟悉第一个部署。这个阶段的目标是在尽量短的时间内让开发人员入门。你应该为这个过程设定一个关键绩效指标(KPI)的目标。一个合理的目标可能是,用户能够在不到半个小时的时间内从零开始运行当前应用的HEAD版本。每当有新成员加入团队时,都要测试你在实现这一目标方面的表现。

第二个阶段是开发。这是开发者的日常活动。这个阶段的目标是确保快速的迭代和调试。开发人员需要快速、反复地将代码推送到集群。他们还需要能够轻松测试代码并在其运行不正常时进行调试。这个阶段的KPI更难以测量,但你可以通过测量将拉取请求(PR)或更改设置并运行在集群中的时间来进行估算,或通过用户的感知生产率调查,或两者兼而有之。你还可以通过团队的整体生产率来衡量这个阶段。

第三个阶段是测试。这个阶段与开发交织在一起,用于验证提交和合并之前的代码。这个阶段的目标是双重的。首先,开发人员应该能够在提交PR之前在其环境中运行所有测试。其次,在代码合并到代码库之前,所有测试都应该自动运行。除了这些目标,你还应该为测试所需的时间设置一个KPI。随着项目变得更加复杂,更多的测试自然会花费更长的时间。随着这种情况的发生,有可能更有价值地确定一个较小的一组冒烟测试,开发人员可以在提交PR之前用于初始验证。你还应该对测试的不稳定性设置非常严格的KPI。不稳定的测试是偶尔(或不那么偶尔)失败的测试。在任何相当活跃的项目中,每一千次运行中超过一个失败的不稳定测试都会导致开发人员的不满。你需要确保你的集群环境不会导致不稳定的测试。虽然有时不稳定的测试是由于代码中的问题而引起的,但它们也可能是由于开发环境的干扰(例如,资源不足和嘈杂的邻居)引起的。你应该通过测量测试的不稳定性并迅速采取措施来解决这个问题,以确保你的开发环境没有这些问题。

构建开发集群

当人们开始思考在Kubernetes上进行开发时,首先要考虑的选择之一是是建立一个大型的开发集群还是为每个开发者建立一个集群。请注意,此选择只在动态创建集群容易的环境中才有意义,比如公共云。在物理环境中,可能只有一个大型集群是唯一的选择。

如果您有选择,应该考虑每种选项的优缺点。如果选择为每个用户建立一个开发集群,这种方法的显著缺点是它将更加昂贵和效率较低,而且您将不得不管理大量不同的开发集群。额外的成本来自于每个集群可能会被大量闲置。此外,由于开发者创建不同的集群,跟踪和清理不再使用的资源会更加困难。采用每用户一个集群的方法的优势在于简单性:每个开发者可以自助管理自己的集群,并且在隔离的情况下,不同的开发者很难互相干扰。

另一方面,单一的开发集群将更加高效,您可以在共享的集群上维护相同数量的开发者,成本可以减少三分之一甚至更少。此外,您可以更容易地安装共享集群服务,例如监控和日志记录,这将显著提高集群的开发者友好性。共享开发集群的缺点是用户管理的过程和开发者之间的潜在干扰。由于将新用户和命名空间添加到Kubernetes集群的过程目前并不是很流畅,您将需要激活一个过程来接纳新的开发者。尽管Kubernetes资源管理和基于角色的访问控制(RBAC)可以降低两个开发者发生冲突的概率,但仍然有可能用户通过消耗过多资源而使开发集群变砖,以至于其他应用程序和开发者无法进行调度。此外,您仍然需要确保开发者不会泄漏和忘记他们创建的资源。然而,与每个开发者创建自己的集群的方法相比,这要容易一些。

尽管两种方法都可行,但通常,我们建议为所有开发者建立一个单一的大型集群。尽管在开发者之间存在干扰的挑战,但它们可以被管理,最终成本效益和轻松向集群添加全组织范围的功能的能力会胜过干扰的风险。但您需要投资于开发者入职、资源管理和垃圾回收的流程。我们的建议是首选尝试单一的大型集群。随着组织的发展(或者如果组织已经很大),您可以考虑为每个团队或小组(10至20人)建立一个集群,而不是为数百名用户建立一个巨大的集群。这可以使计费和管理都更加容易。使用多个集群可以使确保一致性更加复杂,但类似fleet管理工具可以更容易地管理一组集群。

为多个开发者设置共享集群

在设置大型集群时,主要目标是确保多个用户可以同时使用集群而互不干扰。区分不同的开发者的明显方法是使用Kubernetes命名空间。命名空间可以作为服务部署的作用域,以确保一个用户的前端服务不会干扰另一个用户的前端服务。命名空间还可以用作RBAC的作用域,确保一个开发者不会意外删除另一个开发者的工作。因此,在共享集群中,将命名空间用作开发者的工作空间是有意义的。用户入职、创建和保护命名空间的过程将在以下部分中进行描述。

用户入职

在将用户分配给命名空间之前,您需要将该用户引入 Kubernetes 集群本身。为了实现这一点,有两种选择。您可以使用基于证书的身份验证为用户创建新证书,并提供一个 kubeconfig 文件,供用户用于登录,或者您可以配置集群以使用外部身份验证系统(例如 Microsoft Entra ID 或 AWS Identity and Access Management [IAM])进行集群访问。

一般来说,使用外部身份验证系统是最佳做法,因为它不需要维护两个不同的身份来源。此外,大多数外部系统使用短寿命令牌而不是长寿命证书,因此意外泄漏令牌会对安全性产生有限时间的影响。如果可能的话,您应该限制开发人员通过外部身份验证提供程序证明他们的身份。

不过,有些情况下这可能不可行,您需要使用证书。幸运的是,您可以使用 Kubernetes 证书 API 来创建和管理这种证书。以下是向现有集群添加新用户的流程。

首先,您需要生成一个证书签名请求以生成新证书。以下是一个执行此操作的简单 Go 程序:

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/pem"
	"os"
)

func main() {
	name := os.Args[1]
	user := os.Args[2]

	key, err := rsa.GenerateKey(rand.Reader, 1024)
	if err != nil {
		panic(err)
	}
	keyDer := x509.MarshalPKCS1PrivateKey(key)
	keyBlock := pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: keyDer,
	}
	keyFile, err := os.Create(name + "-key.pem")
	if err != nil {
		panic(err)
	}
	pem.Encode(keyFile, &keyBlock)
	keyFile.Close()

	commonName := user
	// You may want to update these too
	emailAddress := "someone@myco.com"

	org := "My Co, Inc."
	orgUnit := "Widget Farmers"
	city := "Seattle"
	state := "WA"
	country := "US"

	subject := pkix.Name{
		CommonName:         commonName,
		Country:            []string{country},
		Locality:           []string{city},
		Organization:       []string{org},
		OrganizationalUnit: []string{orgUnit},
		Province:           []string{state},
	}

	asn1, err := asn1.Marshal(subject.ToRDNSequence())
	if err != nil {
		panic(err)
	}
	csr := x509.CertificateRequest{
		RawSubject:         asn1,
		EmailAddresses:     []string{emailAddress},
		SignatureAlgorithm: x509.SHA256WithRSA,
	}

	bytes, err := x509.CreateCertificateRequest(rand.Reader, &csr, key)
	if err != nil {
		panic(err)
	}
	csrFile, err := os.Create(name + ".csr")
	if err != nil {
		panic(err)
	}

	pem.Encode(csrFile, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes:
      bytes})
	csrFile.Close()
}

您可以按以下方式运行它:

 go run csr-gen.go client <user-name>;

这将创建名为client-key.pem和client.csr的文件。然后,您可以运行以下脚本来创建和下载新的证书:

#!/bin/bash

csr_name="my-client-csr"
name="${1:-my-user}"

csr="${2}"


cat <<EOF | kubectl create -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: ${csr_name}
spec:
  groups:
  - system:authenticated
  request: $(cat ${csr} | base64 | tr -d '\n')
  usages:
  - key encipherment
  - client auth
EOF

echo
echo "Approving signing request."
kubectl certificate approve ${csr_name}

echo
echo "Downloading certificate."
kubectl get csr ${csr_name} -o jsonpath='{.status.certificate}' \
	| base64 --decode > $(basename ${csr} .csr).crt

echo
echo "Cleaning up"
kubectl delete csr ${csr_name}

echo
echo "Add the following to the 'users' list in your kubeconfig file:"
echo "- name: ${name}"
echo "  user:"
echo "    client-certificate: ${PWD}/$(basename ${csr} .csr).crt"
echo "    client-key: ${PWD}/$(basename ${csr} .csr)-key.pem"
echo
echo "Next you may want to add a role-binding for this user."

该脚本会打印出最终信息,您可以将其添加到kubeconfig文件中以启用该用户。当然,该用户没有访问权限,因此您需要应用Kubernetes RBAC为用户授予对命名空间的权限。

创建和保护命名空间