第 2 章 开发工作流

Kubernetes 专为可靠运行的软件而构建。它使用面向应用程序的 API、自我修复属性和有用的工具(如Deployment)的推出简化了应用程序的部署和管理,实现软件零停机时间。尽管所有这些工具都很有用,但它们对于为 Kubernetes 开发应用程序来说并没有多大作用。此外,尽管许多群集旨在运行生产应用程序,因此开发人员工作流很少访问,但启用开发工作流以 Kubernetes 为目标也至关重要,这通常意味着具有群集或至少具有群集群集的一部分,用于开发。建立这样的集群,以方便Kubernetes 的应用程序开发,是确保 Kubernetes 成功的关键部分。显然,如果没有为群集构建代码,则群集本身不会实现太多工作。

目标

在描述构建开发群集的最佳做法之前,有必要说明我们针对此类群集的目标。显然,最终目标是使开发人员能够快速轻松地在 Kubernetes 上构建应用程序,但这在实践中的真正含义是什么,以及这如何反映在开发群集的实际功能中?

识别开发人员与群集交互的阶段非常有用。

第一阶段是入职。这是新开发人员加入团队时。此阶段包括向用户提供群集登录,以及让用户面向其首次部署。此阶段的目标是在最短的时间内将开发人员的脚弄湿。您应该为此过程设置关键绩效指标 (KPI) 目标。一个合理的目标是,用户可以在不到半小时的中从0基础到当前应用程序运行。每当有人成为团队的新人时,都要测试你是如何实现这个目标的。

第二阶段开发。这是开发人员的日常活动。此阶段的目标是确保快速迭代和调试。开发人员需要快速反复地将代码推送到群集。他们还需要能够轻松地测试他们的代码,并在代码无法正常运行时对其进行调试。此阶段的 KPI 测量起来更具挑战性,但您可以通过测量获取拉取请求 (PR) 的时间或更改群集中的启动和运行时间,或者通过调查用户感知的工作效率来估计它,或者同时评估两者。您还可以在团队的整体生产力中衡量这一点。

第三阶段是测试。此阶段与开发交错,用于在提交和合并之前验证代码。这一阶段的目标是双重的。首先,开发人员应该能够在提交 PR 之前为其环境运行所有测试。其次,在代码合并到存储库之前,所有测试都应自动运行。除了这些目标之外,您还应为测试的运行时间长度设置 KPI。随着项目变得越来越复杂,越来越多的测试自然会需要更长的时间。当这种情况发生时,在提交PR之前,确定一组较小的冒烟测试可能变得有价值。 片状测试是偶尔(或偶尔)失败的测试。在任何合理活跃的项目中,每千次运行超过一次的失败率会导致开发人员摩擦。您需要确保群集环境不会导致片状测试。有时由于代码中的问题而出现片状测试,但也可能由于开发环境的干扰(例如,资源耗尽和邻居噪音)而发生。您应该通过测量测试的模糊性并迅速采取行动来修复它,以确保开发环境没有此类问题。

构建开发集群

当人们开始考虑在 Kubernetes 上开发时,首先的选择之一是构建单个大型开发群集,还是每个开发人员拥有一个群集。请注意,此选项仅在动态群集创建容易的环境中(如公共云)有意义。在物理环境中,一个大型群集可能是唯一的选择。

如果你有一个选择,你应该考虑每个选项的优缺点。如果选择每个用户都有一个开发群集,则此方法的显著缺点是,它将更昂贵,效率更低,并且您将有大量不同的开发群集来管理。额外费用来自这样一个事实,即每个集群可能严重未达到充分利用。此外,由于开发人员创建了不同的群集,跟踪和垃圾回收不再使用的资源变得更加困难。按用户群集的方法的优点很简单:每个开发人员都可以自助管理自己的群集,并且从隔离中,不同的开发人员很难彼此踩脚趾。

另一方面,单个开发群集的效率会大大提高;在共享群集上,您可以维持相同数量的开发人员,价格为三分之一(或更少)。此外,安装共享群集服务(例如监视和日志记录)也更加容易,这使得生成开发人员友好的群集变得更加容易。共享开发群集的缺点是用户管理和开发人员之间的潜在干扰过程。由于向 Kubernetes 群集添加新用户和名称空间的过程当前尚未简化,因此您需要激活一个进程以加入新开发人员。尽管 Kubernetes 资源管理和基于角色的访问控制 (RBAC) 可以减少两个开发人员发生冲突的可能性,但用户始终有可能通过消耗太多资源来使其他应用程序和开发人员无法安排开发群集。此外,您仍然需要确保开发人员不会泄漏并忘记他们创建的资源。不过,这比开发人员各自创建自己的群集的方法要容易得多。

尽管这两种方法都是可行的,但通常还是建议为所有开发人员提供一个大型群集。尽管开发人员之间的干扰存在挑战,但可以管理它们,最终,将成本效率和轻松地向群集添加组织范围功能的能力超过了干扰的风险。但是,您需要在开发人员入职、资源管理和垃圾回收过程中进行投资。我们的建议是尝试单个大型群集作为第一个选项。随着组织的发展(或者如果已经很大),您可以考虑为每个团队或组(10 到 20 人)设置一个群集,而不是为数百个用户设置一个巨型群集。这可以使计费和管理更加容易。

为多个开发人员设置共享群集

设置大型群集时,主要目标是确保多个用户可以同时使用群集,而无需踩到彼此的脚趾。分离不同开发人员的明显方法是使用 Kubernetes 名称空间。名称空间可以用作服务部署的范围,以便一个用户的前端服务不会干扰其他用户的前端服务。名称空间也是 RBAC 的范围,可确保一个开发人员不会意外删除另一个开发人员的工作。因此,在共享群集中,使用名称空间作为开发人员的工作区是有意义的。以下各节将介绍载入用户以及创建和保护名称空间的过程。

载入用户

在将用户分配到名称空间之前,必须将该用户板载到 Kubernetes 群集本身。为此,有两个选项。您可以使用基于证书的身份验证为用户创建新证书,并为他们提供一个 kubconfig 文件,供他们登录,也可以将群集配置为使用外部标识系统(例如,Microsoft Azure 活动目录或 AWS 标识和访问管理 [IAM])进行群集访问。

通常,使用外部标识系统是最佳做法,因为它不要求您维护两个不同的标识源,但在某些情况下这是不可能的,您需要使用证书。幸运的是,您可以使用 Kubernetes 证书 API 来创建和管理此类证书。下面是将新用户添加到现有群集的过程。

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

 1package main
 2
 3import (
 4	"crypto/rand"
 5	"crypto/rsa"
 6	"crypto/x509"
 7	"crypto/x509/pkix"
 8	"encoding/asn1"
 9	"encoding/pem"
10	"os"
11)
12
13func main() {
14	name := os.Args[1]
15	user := os.Args[2]
16
17	key, err := rsa.GenerateKey(rand.Reader, 1024)
18	if err != nil {
19		panic(err)
20	}
21	keyDer := x509.MarshalPKCS1PrivateKey(key)
22	keyBlock := pem.Block{
23		Type:  "RSA PRIVATE KEY",
24		Bytes: keyDer,
25	}
26	keyFile, err := os.Create(name + "-key.pem")
27	if err != nil {
28		panic(err)
29	}
30	pem.Encode(keyFile, &keyBlock)
31	keyFile.Close()
32
33	commonName := user
34	// You may want to update these too
35	emailAddress := "someone@myco.com"
36
37	org := "My Co, Inc."
38	orgUnit := "Widget Farmers"
39	city := "Seattle"
40	state := "WA"
41	country := "US"
42
43	subject := pkix.Name{
44		CommonName:         commonName,
45		Country:            []string{country},
46		Locality:           []string{city},
47		Organization:       []string{org},
48		OrganizationalUnit: []string{orgUnit},
49		Province:           []string{state},
50	}
51
52	asn1, err := asn1.Marshal(subject.ToRDNSequence())
53	if err != nil {
54		panic(err)
55	}
56	csr := x509.CertificateRequest{
57		RawSubject:         asn1,
58		EmailAddresses:     []string{emailAddress},
59		SignatureAlgorithm: x509.SHA256WithRSA,
60	}
61
62	bytes, err := x509.CreateCertificateRequest(rand.Reader, &csr, key)
63	if err != nil {
64		panic(err)
65	}
66	csrFile, err := os.Create(name + ".csr")
67	if err != nil {
68		panic(err)
69	}
70
71	pem.Encode(csrFile, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: bytes})
72	csrFile.Close()
73}

您可以运行它,如下所示:

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

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

 1#!/bin/bash
 2
 3csr_name="my-client-csr"
 4name="${1:-my-user}"
 5
 6csr="${2}"
 7
 8
 9cat <<EOF | kubectl create -f -
10apiVersion: certificates.k8s.io/v1beta1
11kind: CertificateSigningRequest
12metadata:
13  name: ${csr_name}
14spec:
15  groups:
16  - system:authenticated
17  request: $(cat ${csr} | base64 | tr -d '\n')
18  usages:
19  - digital signature
20  - key encipherment
21  - client auth
22EOF
23
24echo
25echo "Approving signing request."
26kubectl certificate approve ${csr_name}
27
28echo
29echo "Downloading certificate."
30kubectl get csr ${csr_name} -o jsonpath='{.status.certificate}' \
31	| base64 --decode > $(basename ${csr} .csr).crt
32
33echo
34echo "Cleaning up"
35kubectl delete csr ${csr_name}
36
37echo
38echo "Add the following to the 'users' list in your kubeconfig file:"
39echo "- name: ${name}"
40echo "  user:"
41echo "    client-certificate: ${PWD}/$(basename ${csr} .csr).crt"
42echo "    client-key: ${PWD}/$(basename ${csr} .csr)-key.pem"
43echo
44echo "Next you may want to add a role-binding for this user."

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

创建和保护名称空间

预配名称空间的第一步实际上只是创建它。您可以使用kubectl create namespace my-namespace来执行此操作。

但事实是,在创建名称空间时,您希望将一组元数据附加到该名称空间,例如,构建部署到名称空间中的组件的团队的联系信息。通常,这是以注释的形式;您可以使用某些模板(如Jinja或其他)生成 YAML 文件,也可以创建名称空间并进行分文。执行此操作的简单脚本如下所示:

1ns='my-namespace'
2kubectl create namespace ${ns}
3kubectl annotate namespace ${ns} annotation_key=annotation_value

创建名称空间时,您希望通过确保可以将对名称空间的访问权限授予特定用户来保护它。为此,可以在该名称空间的上下文中将角色绑定到用户。为此,可以通过在名称空间本身中创建RoleBinding对象来执行此操作。RoleBinding示例如下所示:

 1apiVersion: rbac.authorization.k8s.io/v1
 2kind: RoleBinding
 3metadata:
 4  name: example
 5  namespace: my-namespace
 6roleRef:
 7  apiGroup: rbac.authorization.k8s.io
 8  kind: ClusterRole
 9  name: edit
10subjects:
11- apiGroup: rbac.authorization.k8s.io
12  kind: User
13  name: myuser

要创建它,只需运行kubectl create -f role-binding.yaml 。请注意,只要更新绑定中名称以指向正确的名称空间,就可以根据需要尽可能重用此绑定。如果确保用户没有任何其他角色绑定,则可以确保此名称空间是用户有权访问的群集的唯一部分。合理的做法是查看权限允许访问整个群集;这样,开发人员就可以看到其他人在做什么,以防干扰他们的工作。但是,在授予此类读取访问权限时要小心,因为它将包括对群集中机密资源的访问。通常,在开发群集中,这是可以的,因为每个人都在同一个组织,并且机密仅用于开发;但是,如果这是一个问题,则可以创建一个更细粒度的角色,从而消除读取机密的能力。

如果要限制特定名称空间消耗的资源量,可以使用 ResourceQuota 资源对任何特定名称空间消耗的资源总数设置限制。例如,以下配额将名称空间限制为名称空间中 Pod 的requestslimits的 10 个内核和 100 GB 内存:

 1apiVersion: v1
 2kind: ResourceQuota
 3metadata:
 4  name: limit-compute
 5  namespace: my-namespace
 6spec:
 7  hard:
 8    requests.cpu: "10"
 9    requests.memory: 100Gi
10    limits.cpu: "10"
11    limits.memory: 100Gi

管理名称空间

现在,您已经了解如何为载入新用户,以及如何创建要用作工作区的名称空间,现在的问题仍然是如何将开发人员分配到名称空间。与很多事情一样,没有一个单一的完美答案;相反,有两种方法。第一种是为每个用户提供自己的名称空间,作为载入过程的一部分。这非常有用,因为在用户加入后,他们始终有一个专用工作区,他们可以在其中开发和管理其应用程序。但是,使开发人员的名称空间过于持久会鼓励开发人员在完成名称空间后将内容保留在名称空间中,并且垃圾收集和记帐单个资源更为复杂。另一种方法是临时创建和分配具有有限制生存时间 (TTL) 的名称空间。这可确保开发人员认为群集中的资源是暂时性的,并且很容易在删除整个名称空间时在其 TTL 过期时构建自动化。

在此模型中,当开发人员想要开始新项目时,他们使用工具为项目分配新的名称空间。创建名称空间时,它具有与用于管理和记帐的名称空间关联的元数据选择。显然,此元数据包括名称空间的 TTL,但它也包括分配给它的开发人员、应分配给名称空间的资源(例如 CPU 和内存)以及名称空间的团队和用途。此元数据可确保既可以跟踪资源使用情况,也可以在正确的时间删除名称空间。

开发按需分配名称空间的工具似乎是一项挑战,但简单的工具开发相对简单。例如,可以使用创建名称空间的简单脚本实现新名称空间的分配,并提示相关元数据附加到名称空间。

如果要与 Kubernetes 进行更多集成,可以使用自定义资源定义 (CRD) 使用户能够使用kubectl该工具动态创建和分配新的名称空间。如果你有时间和倾向,这绝对是一个很好的做法,因为它使名称空间管理声明性,也支持使用Kubernetes RBAC。

使用工具启用名称空间分配后,还需要在工具的 TTL 过期时添加工具以重新获得名称空间。同样,您可以使用一个简单的脚本来实现此目的,该脚本检查名称空间并删除具有过期 TTL 的名称空间。

您可以将此脚本构建到容器中,并使用 ScheduledJob 以每小时一次的间隔运行它。结合在一起,这些工具可以确保开发人员可以轻松地根据需要为其项目分配独立资源,但这些资源也将在适当的时间间隔内获得,以确保您没有浪费资源和旧资源不要妨碍新的发展。

群集级服务

除了分配和管理命名空间的工具外,还有有用的群集级服务,最好在开发群集中启用这些服务。第一种是日志聚合到中央日志记录即服务 (LaaS) 系统。开发人员了解其应用程序操作的最简单方法之一是向 STDOUT 编写一些东西。虽然您可以通过 kubectl logs 访问这些日志,但该日志的长度有限,并且不是特别可搜索的。相反,如果您自动将这些日志运送到 LaaS 系统(如云服务或 Elasticsearch 群集),开发人员可以轻松地在日志中搜索相关信息,以及跨其服务中的多个容器聚合日志记录信息。

启用开发人员工作流

现在,我们成功地进行了共享群集设置,并且可以将新的应用程序开发人员安装到群集本身,我们需要真正让他们开发他们的应用程序。请记住,我们要测量的关键 KPI 之一是从载入到在群集中运行的初始应用程序的时间。很显然,通过刚刚描述的载入脚本,我们可以快速对用户进行群集身份验证并分配名称空间,但如何开始使用应用程序呢?遗憾的是,即使有一些技术有助于此过程,它通常需要比自动化更多的约定来启动和运行初始应用程序。在以下各节中,我们将介绍实现这一目标的一种方法;它绝不是唯一的方法或唯一的解决办法。您可以选择按相同方式应用该方法,也可以从想法中得到启发,以找到自己的解决方案。

初始设置

部署应用程序的主要挑战之一是安装所有依赖项。在许多情况下,特别是在现代微服务体系结构中,甚至要开始在其中一个微服务上开发,都需要部署多个依赖项(数据库或其他微服务)。尽管应用程序本身的部署相对简单,但识别和部署所有依赖项以构建完整应用程序的任务通常是一个令人沮丧的尝试和错误案例,这些尝试和错误与不完整或过时的结合指示。

要解决此问题,引入描述和安装依赖项的约定通常很有价值。这可以被视为类似 npm install 之类的类似 npm 的东西,它安装所有必需的 JavaScript 依赖项。最终,可能会有一个类似于为基于 Kubernetes 的应用程序提供此服务的工具,但在此之前,最佳做法是依靠团队中的约定。

约定的一个选项是在所有项目存储库的根目录中创建 setup.sh 脚本。此脚本的职责是创建特定名称空间中的所有依赖项,以确保正确创建应用程序的所有依赖项。例如,设置脚本可能如下所示:

1kubectl create my-service/database-stateful-set-yaml
2kubectl create my-service/middle-tier.yaml
3kubectl create my-service/configs.yaml

然后,您可以通过npm包.json中添加以下内容来集成此脚本:

1{
2    ...
3    "scripts": {
4        "setup": "./setup.sh",
5        ...
6    }
7}

使用此设置,新的开发人员只需运行 npm run setup,群集依赖项将安装。显然,此特定集成特定于 Node.js/npm。在其他编程语言中,与特定于语言的工具集成将更有意义。例如,在 Java 中,您可以改为与 Maven pom.xml文件集成。

启动主动开发

设置了具有所需依赖项的开发人员工作区后,下一个任务是使他们能够快速地在其应用程序上进行迭代。第一个先决条件是能够构建和推送容器镜像。假设您已经设置好了;如果没有,你可以阅读如何做到这一点在许多其他在线资源和书籍。

构建并推送容器镜像后,任务是将其部署到群集。与传统部署不同,在开发人员迭代的情况下,维护可用性实际上不是一个问题。因此,部署新代码的最简单方法是简单地删除与上一个部署关联的部署对象,然后创建一个指向新构建镜像的Deployment。也可以就地更新现有 Deployment,但这将触发Deployment 资源中的 rollout 逻辑。尽管可以将Deployment 配置为快速回滚代码,但这样做会带来开发环境和生产环境之间的差异,这些差异可能引入危险或不稳定的差异。例如,假设您不小心将部署的开发配置推送到生产中;您会突然意外地将新版本部署到生产中,而无需在推出的各个阶段进行适当的测试和延迟。由于存在此风险,并且存在替代方法,最佳做法是删除并重新创建Deployment。

与安装依赖项一样,制作用于执行此部署的脚本也是一个很好的做法。deploy.sh脚本的示例可能如下所示:

1kubectl delete -f ./my-service/deployment.yaml
2perl -pi -e 's/${old_version}/${new_version}/' ./my-service/deployment.yaml
3kubectl create -f ./my-service/deployment.yaml

和以前一样,您可以将其与现有的编程语言工具集成,以便(例如)开发人员只需运行npm run deploy即可将其新代码部署到群集中。

启用测试和调试

用户成功部署其应用程序的开发版本后,需要对其进行测试,如果存在问题,则需要调试应用程序的任何问题。在 Kubernetes 中开发时,这也是一道障碍,因为她们并不总是很清楚如何与群集交互。kubectl命令行是一个名副其实的瑞士军刀,从kubectl logskubectl execkubectl port-forward,但学习如何使用所有不同的选项并熟悉工具需要大量的经验。此外,由于该工具在终端中运行,它通常需要多个窗口的组合来同时检查应用程序的源代码和正在运行的应用程序本身。

为了简化测试和调试体验,Kubernetes 工具越来越多地集成到开发环境中,例如,用于 Kubernetes 的 Visual Studio Code 的开源扩展。该扩展易于从 VS 代码市场免费安装。安装后,它会自动发现kubeconfig文件中已有的任何群集,并提供树视图导航窗格,让您一目了然地查看群集的内容。

除了能够一目了然地查看群集状态外,该集成还允许开发人员以直观、可发现的方式使用可通过kubectl的工具。在树视图中,如果右键单击 Kubernetes pod,可以立即使用端口转发将连接到该pod的网络连接直接带到本地计算机。同样,您可以访问 Pod 的日志,甚至可以在正在运行的容器中获取终端。

将这些命令与原型用户界面期望(例如,右键单击显示上下文菜单)集成,以及将这些体验与应用程序本身的代码集成,使开发人员能够使用最少的 Kubernetes 在开发群集中快速提高工作效率的经验。

当然,这个VS Code 扩展并不是 Kubernetes 和开发环境之间唯一的集成;有几个其他,你可以安装,这取决于你选择的编程环境和风格(vi,emacs,等)。

设置开发环境最佳实践

在 Kubernetes 上建立成功的工作流程是生产力和幸福的关键。遵循这些最佳实践有助于确保开发人员快速启动和运行:

  • 考虑开发人员在三个阶段的经验:入职、开发和测试。请确保您构建的开发环境支持所有这三个阶段。
  • 构建开发群集时,可以在一个大型群集和每个开发人员的群集之间进行选择。每种方法都有优点和缺点,但通常单个大型群集是更好的方法。
  • 当您将用户添加到群集时,请使用他们自己的身份并访问他们自己的名称空间来添加。使用资源限制来限制他们可以使用的群集数量。
  • 管理名称空间时,考虑如何获取旧的、未使用的资源。开发人员在删除未使用的内容时会有不良的卫生习惯。使用自动化来清理它们。
  • 您可以考虑为所有用户设置群集级服务,如日志和监视。有时,群集级依赖项(如数据库)也可用于使用 Helm chart等模板代表所有用户进行设置。

总结

我们到达了这样一个境界:创建 Kubernetes 群集(尤其是在云中)是一个相对简单的实践,但是让开发人员能够有效地使用此类群集就不那么明显和容易了。在考虑使开发人员能够在 Kubernetes 上成功构建应用程序时,考虑有关载入、迭代、测试和调试应用程序的关键目标非常重要。同样,熟悉一些特定于用户载入、名称空间预配和群集服务(如基本日志聚合)的基本工具也是值得的。将开发群集和代码存储库视为标准化和应用最佳实践的机会,将确保开发人员满意且高效,成功构建代码以部署到生产 Kubernetes 群集。