本章描述了在Kubernetes中设置简单多层应用程序的步骤。我们将演示的示例由两个层次组成:一个简单的Web应用程序和一个数据库。尽管这可能不是最复杂的应用程序,但当学习如何在Kubernetes中管理应用程序时,这是一个很好的起点。
应用程序概览
我们将在示例中使用的应用程序相当简单。它是一个简单的日志服务,具有以下细节:
- 它使用NGINX作为独立的静态文件服务器。
- 它具有一个位于/api路径上的RESTful应用程序编程接口(API),地址为some-host-name.io/api。
- 它在主URL some-host-name.io 上拥有一个文件服务器。
- 它使用Let's Encrypt服务来管理SSL(安全套接层)。
图1-1展示了这个应用程序的图示。如果您一开始不理解所有组件,不用担心;它们将在本章中详细解释。我们将逐步演示如何构建这个应用程序,首先使用YAML配置文件,然后使用Helm图表。
管理配置文件
在深入讨论如何在Kubernetes中构建这个应用程序之前,值得讨论一下我们如何管理配置本身。在Kubernetes中,一切都以声明方式表示。这意味着您将应用程序在集群中的期望状态写成文件(通常是YAML或JSON文件),这些声明的期望状态定义了应用程序的所有组件。这种声明式方法远比命令式方法更可取,命令式方法中集群的状态是一系列对集群的更改的总和。如果集群以命令方式配置,那么很难理解和复制集群达到该状态的方式,从而使理解或解决应用程序的问题变得困难。
在声明应用程序的状态时,人们通常更喜欢YAML而不是JSON,尽管Kubernetes支持它们两者。这是因为YAML比JSON更为简洁,更容易人工编辑。然而,值得注意的是,YAML对缩进很敏感;在Kubernetes配置中,错误通常可以追溯到YAML中的不正确缩进。如果事情没有按预期运行,检查缩进是开始故障排除的好方法。大多数编辑器都支持JSON和YAML的语法高亮显示。在处理这些文件时,安装此类工具以更容易找到配置中的作者和文件错误是一个好主意。对于Kubernetes文件,Visual Studio Code也有一个出色的扩展,支持更丰富的错误检查。
由于包含在这些YAML文件中的声明状态作为应用程序的真实来源,因此正确管理这种状态对于应用程序的成功至关重要。在修改应用程序的期望状态时,您希望能够管理更改,验证其正确性,审计谁进行了更改,以及在失败时可能回滚更改。幸运的是,在软件工程的背景下,我们已经开发了管理声明状态的更改以及审计和回滚所需工具。具体而言,关于版本控制和代码审查的最佳实践直接适用于管理应用程序的声明状态的任务。
如今,大多数人将他们的Kubernetes配置存储在Git中。尽管版本控制系统的具体细节并不重要,但Kubernetes生态系统中的许多工具都希望文件位于Git存储库中。对于代码审查,有更多的异构性;尽管GitHub显然很受欢迎,但其他人使用本地代码审查工具或服务。无论您如何为应用程序配置实施代码审查,都应该像对待源代码控制一样认真对待它,专注于它。
在组织应用程序的文件系统布局时,值得使用文件系统提供的目录组织来组织您的组件。通常,一个单独的目录用于包含一个应用服务。什么构成应用服务的定义可能因团队而异,但通常是由8-12人的团队开发的服务。在该目录中,使用子目录来管理应用程序的子组件。
对于我们的应用程序,我们将文件布局如下:
journal/
frontend/
redis/
fileserver/
在每个目录中,都有定义服务所需的具体YAML文件。正如您稍后将看到的,当我们开始将应用程序部署到多个不同的地区或集群时,这个文件布局会变得更加复杂。
使用部署(Deployments)创建一个复制的服务
为了描述我们的应用程序,我们将从前端开始,然后向下工作。日志的前端应用程序是一个使用TypeScript实现的Node.js应用程序。完整的应用程序太大,无法包含在本书中,因此我们将其托管在我们的GitHub上。将来的示例代码也可以在那里找到,所以值得收藏。该应用程序在端口8080上公开了一个HTTP服务,用于处理/api/*路径的请求,并使用Redis后端来添加、删除或返回当前的日志条目。如果您计划在本地计算机上使用接下来的YAML示例,您需要使用Dockerfile构建这个应用程序成为一个容器镜像,并将其推送到您自己的镜像存储库。然后,在代码中,您将使用您的容器镜像名称,而不是我们的示例文件名。
镜像管理的最佳实践
尽管一般来说,构建和维护容器镜像超出了本书的范围,但识别一些构建和命名镜像的一般最佳实践是值得的。总的来说,镜像构建过程可能容易受到"供应链攻击"的威胁。在这种攻击中,恶意用户将代码或二进制文件注入到某个来自受信任源的依赖项中,然后将其构建到您的应用程序中。由于存在这种攻击的风险,构建镜像时基于仅来自知名和受信任的镜像提供商非常重要。或者,您可以从头开始构建所有的镜像。对于一些语言(例如Go),从头开始构建很容易,因为它们可以构建静态二进制文件,但对于像Python、JavaScript或Ruby这样的解释语言来说,从头开始构建要复杂得多。
与镜像相关的其他最佳实践涉及到命名。尽管镜像仓库中的容器镜像版本在理论上是可变的,但您应该将版本标签视为不可变的。特别是,使用语义版本和构建镜像的提交的SHA哈希的组合是一种良好的命名实践(例如v1.0.1-bfeda01f)。如果不指定镜像版本,默认情况下使用latest。尽管在开发中这可能很方便,但对于生产使用来说是一个不好的想法,因为latest显然在每次构建新镜像时都在变化。
创建一个复制的应用程序
我们的前端应用程序是无状态的;它完全依赖Redis后端来维护其状态。因此,我们可以任意复制它而不会影响流量。尽管我们的应用程序不太可能维持大规模的使用,但至少运行两个副本仍然是一个好主意,这样您就可以处理意外崩溃或无需停机地发布新版本的应用程序。
在Kubernetes中,ReplicaSet资源是直接管理复制容器化应用程序的特定版本的资源。由于随着您修改代码,所有应用程序的版本随时间而变化,因此不建议直接使用ReplicaSet。相反,您可以使用Deployment资源。Deployment结合了ReplicaSet的复制功能、版本控制以及进行分阶段的发布能力。通过使用Deployment,您可以使用Kubernetes内置的工具从一个应用程序版本切换到下一个。
我们的应用程序的Kubernetes Deployment资源如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
# All pods in the Deployment will have this label
app: frontend
name: frontend
namespace: default
spec:
# We should always have at least two replicas for reliability
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: my-repo/journal-server:v1-abcde
imagePullPolicy: IfNotPresent
name: frontend
# TODO: Figure out what the actual resource needs are
resources:
request:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
在这个Deployment中有几个需要注意的事项。首先是我们使用标签来标识Deployment以及Deployment创建的ReplicaSets和pods。我们为所有这些资源添加了app: frontend标签,以便我们可以在单个请求中查看特定层的所有资源。随着我们添加其他资源,我们将遵循相同的做法。
此外,我们在YAML的多个位置添加了注释。尽管这些注释不会出现在存储在服务器上的Kubernetes资源中,就像代码中的注释一样,它们有助于引导首次查看此配置的人。
您还应该注意,在Deployment中为容器指定了资源请求的请求和限制,并将请求设置为与限制相等。运行应用程序时,请求是保证在运行它的主机上的资源保留。限制是容器将被允许的最大资源使用量。当您刚开始使用时,将请求设置为与限制相等将导致应用程序的行为最可预测。这种可预测性是以资源利用率为代价的。因为将请求设置为与限制相等可以防止您的应用程序过度调度或消耗过多的空闲资源,除非您非常小心地调整请求和限制,否则您将无法实现最大的资源利用。随着您对Kubernetes资源模型的理解更加深入,您可能会考虑单独修改应用程序的请求和限制,但通常大多数用户认为可预测性带来的稳定性比降低利用率更值得。
通常情况下,正如我们的注释所建议的那样,很难知道这些资源限制的正确值。首先高估估计值,然后使用监控来调整到正确的值是一个相当不错的方法。然而,如果您要启动一个新的服务,请记住,第一次遇到大规模流量时,您的资源需求可能会显著增加。此外,有些语言,特别是垃圾回收语言,愿意消耗所有可用的内存,这可能会使确定内存的正确最小值变得困难。在这种情况下,可能需要进行某种形式的二分搜索,但请记住在测试环境中执行此操作,以免影响生产环境!
现在我们已经定义了Deployment资源,我们将其检入到版本控制中,并将其部署到Kubernetes:
git add frontend/deployment.yaml
git commit -m "Added deployment" frontend/deployment.yaml
kubectl apply -f frontend/deployment.yaml
确保集群的内容与源代码控制的内容完全匹配也是一种最佳实践。确保这一点的最佳模式是采用GitOps方法,并仅从源代码控制的特定分支使用持续集成/持续交付(CI/CD)自动化部署到生产环境。通过这种方式,您可以确保源代码控制和生产环境匹配。尽管对于一个简单的应用程序来说,完整的CI/CD流水线可能看似过于复杂,但自动化本身独立于其提供的可靠性通常值得花费的时间来设置它。而将CI/CD集成到现有的、以命令方式部署的应用程序中是极其困难的。
在后续章节中,我们将回到这个应用程序描述的YAML,以检查其他元素,如ConfigMap和secret卷,以及Pod的服务质量。
设置外部入口以处理HTTP流量
我们的应用程序容器现在已部署,但目前还无法让任何人访问该应用程序。默认情况下,集群资源仅在集群内部可用。为了将我们的应用程序暴露给外部世界,我们需要创建一个服务和负载均衡器,以提供外部IP地址并将流量引导到我们的容器。为了实现外部暴露,我们将使用两个Kubernetes资源。第一个是负载均衡传输控制协议(TCP)或用户数据报协议(UDP)流量的服务。在我们的情况下,我们使用TCP协议。第二个是Ingress资源,它提供基于HTTP路径和主机的HTTP(S)负载均衡请求的智能路由。对于这样一个简单的应用程序,您可能会想知道为什么选择使用更复杂的Ingress,但正如您将在后续章节中看到的,即使是这个简单的应用程序也将为来自两个不同服务的HTTP请求提供服务。此外,具有Ingress边缘资源可以为将来扩展我们的服务提供灵活性。
在定义Ingress资源之前,需要有一个Kubernetes服务,以便Ingress指向它。我们将使用标签来将服务定向到之前创建的Pod。定义服务要比部署简单得多,如下所示:
apiVersion: v1
kind: Service
metadata:
labels:
app: frontend
name: frontend
namespace: default
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 8080
selector:
app: frontend
type: ClusterIP
在定义了服务之后,您可以定义一个Ingress资源。与服务资源不同,Ingress需要在集群中运行Ingress控制器容器。您可以选择多种不同的实现方式,可以由您的云提供商提供,也可以使用开源服务器来实现。如果选择安装开源Ingress提供者,建议使用Helm软件包管理器来安装和维护它。nginx或haproxy Ingress提供者是常见的选择:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend-ingress
spec:
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 8080
创建了Ingress资源后,我们的应用程序已准备好为全球范围内的Web浏览器提供流量。接下来,我们将看看如何设置您的应用程序,以便进行轻松的配置和定制。
使用ConfigMap配置应用程序
每个应用程序都需要一定程度的配置。这可以是每页显示的日志条目数量,特定背景的颜色,特殊的节日显示,或许多其他类型的配置。通常,将这种配置信息与应用程序本身分开是一个最佳实践。
这种分离有几个原因。首先,您可能希望根据设置的不同配置相同的应用程序二进制文件。在欧洲,您可能希望点亮复活节特别节目,而在中国,您可能希望展示春节特别节目。除了这种环境专业化之外,还有分离的灵活性原因。通常,一个二进制发布包含多个不同的新功能;如果通过代码启用这些功能,修改活动功能的唯一方法是构建并发布一个新的二进制文件,这可能是一个昂贵且缓慢的过程。
使用配置来激活一组功能意味着您可以根据用户需求或应用程序代码故障快速(甚至动态地)激活和停用功能。功能可以逐个基础上进行部署和回滚。这种灵活性确保了大多数功能都在不断取得进展,即使需要回滚一些功能以解决性能或正确性问题。
在Kubernetes中,这种类型的配置由一个名为ConfigMap的资源表示。ConfigMap包含多个键/值对,代表配置信息或文件。此配置信息可以通过文件或环境变量的方式提供给Pod中容器。假设您想配置在线日志应用程序,以显示每页可配置数量的日志条目。为实现这一目标,您可以定义一个如下的ConfigMap:
kubectl create configmap frontend-config --from-literal=journalEntries=10
为了配置您的应用程序,您可以将配置信息暴露为应用程序本身的环境变量。要做到这一点,您可以在之前定义的部署中的容器资源中添加以下内容:
...
# The containers array in the PodTemplate inside the Deployment
containers:
- name: frontend
...
env:
- name: JOURNAL_ENTRIES
valueFrom:
configMapKeyRef:
name: frontend-config
key: journalEntries
...
尽管这演示了如何使用ConfigMap配置应用程序,但在实际的部署环境中,您至少每周都要定期进行对配置的更改。也许诱人的方法是通过简单地更改ConfigMap本身来进行这些更改,但出于以下原因,这并不是一个最佳实践:首先,更改配置实际上不会触发现有Pod的更新。配置仅在Pod重新启动时应用。因此,部署不是基于健康的,可以是临时或随机的。另一个原因是,ConfigMap的唯一版本控制是在您的版本控制中,要进行回滚可能非常困难。
更好的方法是在ConfigMap的名称中加入版本号。而不是将其命名为frontend-config,将其命名为frontend-config-v1。当您要进行更改时,而不是就地更新ConfigMap,您创建一个新的v2 ConfigMap,然后更新Deployment资源以使用该配置。这样做时,将自动触发Deployment的升级,使用适当的健康检查和更改之间的暂停。此外,如果您需要回滚,v1配置位于集群中,回滚就像再次更新Deployment一样简单。
使用Secrets进行身份验证管理
到目前为止,我们还没有真正讨论我们的前端连接的Redis服务。但在任何真实的应用程序中,我们需要保护服务之间的连接。部分原因是为了确保用户和其数据的安全,另外,防止像将开发前端与生产数据库连接这样的错误是至关重要的。
Redis数据库使用简单的密码进行身份验证。也许您认为将这个密码存储在应用程序的源代码中,或者在镜像中的文件中会很方便,但出于各种原因,这两者都不是好主意。首先,您已经将您的秘密(密码)泄露到一个您不一定考虑访问控制的环境中。如果您将密码放入源代码控制中,您就将源代码的访问权限与所有秘密的访问权限联系在一起。这不是最佳做法,因为您可能有更广泛的用户可以访问您的源代码,而这些用户可能不应该真正访问您的Redis实例。同样,有权访问您的容器镜像的人不一定应该访问您的生产数据库。
除了对访问控制的担忧,避免将秘密与源代码和/或镜像绑定的另一个原因是参数化。您希望能够在各种环境中使用相同的源代码和镜像(例如开发、金丝雀和生产)。如果秘密与源代码或镜像紧密绑定,那么您需要为每个环境创建不同的镜像(或不同的代码)。
在前一节中介绍了ConfigMaps之后,您可能立刻想到密码可以存储为配置,然后作为应用程序特定配置填充到应用程序中。您绝对正确,认为将配置与应用程序分开与将秘密与应用程序分开是一样的。但事实是,秘密本身是一个重要的概念。您可能希望以不同的方式处理秘密的访问控制、处理和更新,而不是处理配置。更重要的是,您希望开发人员在访问秘密时有不同的思考方式,而在访问配置时有不同的思考方式。出于这些原因,Kubernetes具有内置的Secret资源来管理秘密数据。
您可以创建Redis数据库的密码秘密如下:
kubectl create secret generic redis-passwd --from-literal=passwd=${RANDOM}
显然,您可能希望使用随机数字以外的内容作为密码。此外,您可能希望使用秘密/密钥管理服务,无论是通过云提供商,如Microsoft Azure Key Vault,还是通过开源项目,如HashiCorp的Vault。当您使用密钥管理服务时,它们通常与Kubernetes的Secret密切集成。
在将Redis密码存储为Kubernetes中的秘密后,您需要在将应用程序部署到Kubernetes时将该秘密绑定到正在运行的应用程序。为此,您可以使用Kubernetes Volume。Volume实际上是一个文件或目录,可以挂载到正在运行的容器中的用户指定位置。对于秘密,Volume将作为tmpfs RAM支持的文件系统创建,然后挂载到容器中。这可以确保即使物理上攻破了机器(在云中可能性很小,但在数据中心可能性存在),攻击者更难获得秘密。
要向部署中添加秘密Volume,您需要在部署的YAML文件中指定两个新条目。第一个是为Pod添加Volume的Pod的volume条目:
...
volumes:
- name: passwd-volume
secret:
secretName: redis-passwd
容器存储接口(CSI)驱动程序使您能够使用位于Kubernetes集群之外的密钥管理系统(KMS)。这通常是大型或受监管组织内部合规性和安全性的要求。如果您使用其中一个CSI驱动程序,您的Volume会如下所示:
...
volumes:
- name: passwd-volume
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "azure-sync"
...
无论您使用哪种方法,在Pod中定义了Volume后,您需要将它挂载到特定的容器中。您可以通过容器描述中的volumeMounts字段来执行此操作:
...
volumeMounts: - name: passwd-volume
readOnly: true
mountPath: "/etc/redis-passwd"
...
这将将秘密Volume挂载到redis-passwd目录,以便客户端代码可以访问它。将所有这些内容组合在一起,您将得到完整的部署,如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: frontend
name: frontend
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: my-repo/journal-server:v1-abcde
imagePullPolicy: IfNotPresent
name: frontend
volumeMounts:
- name: passwd-volume
readOnly: true
mountPath: "/etc/redis-passwd"
resources:
requests:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
volumes:
- name: passwd-volume
secret:
secretName: redis-passwd
到目前为止,我们已经配置了客户端应用程序,以便具有可用于对Redis服务进行身份验证的秘密。配置Redis以使用此密码类似;我们将其挂载到Redis Pod并从文件中加载密码。
部署一个简单的有状态数据库
尽管从概念上讲,部署有状态应用程序与部署前端等客户端类似,但状态会带来更多的复杂性。首先,在Kubernetes中,Pod可以因多种原因而重新调度,例如节点健康、升级或重新平衡。当发生这种情况时,Pod可能会移动到不同的机器上。如果与Redis实例相关的数据位于特定的机器上或容器本身内,当容器迁移或重新启动时,数据将丢失。为防止这种情况发生,在Kubernetes中运行有状态工作负载时,使用远程持久卷来管理与应用程序相关的状态非常重要。
在Kubernetes中,有各种各样的持久卷实现,但它们都具有共同的特征。与前面描述的秘密卷一样,它们与一个Pod相关联,并挂载到容器的特定位置。与秘密不同,持久卷通常是通过某种网络协议(基于文件的,如网络文件系统(NFS)或服务器消息块(SMB),或基于块的(iSCSI,基于云的磁盘等)远程存储。通常,对于数据库等应用程序,基于块的磁盘更可取,因为它们提供更好的性能,但如果性能不太重要,基于文件的磁盘有时提供更大的灵活性。
要部署我们的Redis服务,我们使用StatefulSet资源。StatefulSet是在初始的Kubernetes版本之后添加的,作为ReplicaSet资源的补充,它提供了稍微更强的保证,比如一致的名称(没有随机哈希!)和规定的伸缩顺序。当部署单实例时,这可能不太重要,但当您想要部署复制的有状态实例时,这些属性非常方便。
要获得用于我们的Redis的持久卷,我们使用PersistentVolumeClaim。您可以将声明视为“资源请求”。我们的Redis在抽象上声明它需要50 GB的存储空间,Kubernetes集群会确定如何提供适当的持久卷。这样做的原因有两个。首先,这样我们可以编写一个在不同云和本地环境之间具有可移植性的StatefulSet,其中磁盘的详细信息可能不同。另一个原因是,尽管许多持久卷类型只能挂载到单个Pod,但我们可以使用Volume claims编写一个模板,该模板可以进行复制,而每个Pod仍然分配到自己的特定持久卷。
以下示例显示了带有持久卷的Redis StatefulSet:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: "redis"
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:5-alpine
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
这部署了Redis服务的单个实例,但假设您想要复制Redis集群,以便进行读操作的扩展和容错性,以应对故障。要实现这一点,您显然需要将副本数增加到三,但您还需要确保两个新的副本连接到Redis的写主。我们将在下一节中看到如何建立这种连接。
当为Redis StatefulSet创建无头Service时,它会创建一个DNS条目redis-0.redis;这是第一个副本的IP地址。您可以使用这个来创建一个可以在所有容器中启动的简单脚本:
#!/bin/sh
PASSWORD=$(cat /etc/redis-passwd/passwd)
if [[ "${HOSTNAME}" == "redis-0" ]]; then
redis-server --requirepass ${PASSWORD}
else
redis-server --slaveof redis-0.redis 6379 --masterauth ${PASSWORD}
--requirepass ${PASSWORD}
fi
您可以将此脚本创建为一个ConfigMap:
kubectl create configmap redis-config --from-file=./launch.sh
然后,您将此ConfigMap添加到StatefulSet,并将其用作容器的命令。让我们还添加本章前面创建的用于身份验证的密码。
完整的三个副本的Redis如下所示:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: "redis"
replicas: 3
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:5-alpine
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: data
mountPath: /data
- name: script
mountPath: /script/launch.sh
subPath: launch.sh
- name: passwd-volume
mountPath: /etc/redis-passwd
command:
- sh
- -c
- /script/launch.sh
volumes:
- name: script
configMap:
name: redis-config
defaultMode: 0777
- name: passwd-volume
secret:
secretName: redis-passwd
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
现在,您的Redis已经配置为容忍故障。如果由于任何原因三个Redis副本中的一个失败,您的应用程序可以继续运行,直到第三个副本被恢复。
通过使用服务创建TCP负载均衡器
既然我们已经部署了有状态的Redis服务,现在我们需要将其提供给我们的前端。为此,我们创建了两个不同的Kubernetes服务。第一个是用于从Redis读取数据的服务。因为Redis正在将数据复制到StatefulSet的所有三个成员,所以我们不关心我们的请求去哪里进行读取。因此,我们使用一个基本的服务来进行读取:
apiVersion: v1
kind: Service
metadata:
labels:
app: redis
name: redis
namespace: default
spec:
ports:
- port: 6379
protocol: TCP
targetPort: 6379
selector:
app: redis
sessionAffinity: None
type: ClusterIP
要启用写入操作,您需要将目标指向Redis主服务器(副本#0)。为此,创建一个无头服务。无头服务没有集群IP地址;相反,它为StatefulSet中的每个Pod编程了一个DNS条目。这意味着我们可以通过redis-0.redis DNS名称访问我们的主服务器:
apiVersion: v1
kind: Service
metadata:
labels:
app: redis-write
name: redis-write
spec:
clusterIP: None
ports:
- port: 6379
selector:
app: redis
因此,当我们要连接到Redis进行写入操作或事务性读/写操作时,我们可以构建一个单独的写入客户端,连接到redis-0.redis-write服务器。
使用Ingress将流量路由到静态文件服务器
我们应用程序中的最后一个组件是静态文件服务器。静态文件服务器负责提供HTML、CSS、JavaScript和图像文件。将静态文件服务与前面描述的API服务前端分开,既更高效又更专注。我们可以使用高性能的现成静态文件服务器(如NGINX)来提供文件,同时允许我们的开发团队专注于实现API所需的代码。
幸运的是,Ingress资源使这种微型微服务架构非常容易。就像前端一样,我们可以使用Deployment资源来描述一个复制的NGINX服务器。让我们将静态图像构建到NGINX容器中,并将它们部署到每个副本中。Deployment资源如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: fileserver
name: fileserver
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: fileserver
template:
metadata:
labels:
app: fileserver
spec:
containers:
# This image is intended as an example, replace it with your own
# static files image.
- image: my-repo/static-files:v1-abcde
imagePullPolicy: Always
name: fileserver
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
resources:
requests:
cpu: "1.0"
memory: "1G"
limits:
cpu: "1.0"
memory: "1G"
dnsPolicy: ClusterFirst
restartPolicy: Always
现在有一个复制的静态Web服务器正在运行,您将创建一个Service资源,用作负载均衡器:
apiVersion: v1
kind: Service
metadata:
labels:
app: fileserver
name: fileserver
namespace: default
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: fileserver
sessionAffinity: None
type: ClusterIP
现在您有了用于静态文件服务器的Service,扩展Ingress资源以包含新的路径。重要的是要注意,您必须在/api路径之后放置/路径,否则它将包含/api并将API请求定向到静态文件服务器。新的Ingress如下所示:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: frontend-ingress
spec:
rules:
- http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: fileserver
port:
number: 8080
# NOTE: this should come after /api or else it will hijack requests
- path: /
pathType: Prefix
backend:
service:
name: fileserver
port:
number: 80
现在您已经为文件服务器设置了Ingress资源,除了之前设置的API的Ingress,应用程序的用户界面已经准备好使用。大多数现代应用程序将静态文件(通常是HTML和JavaScript)与使用服务器端编程语言(如Java、.NET或Go)实现的动态API服务器结合在一起。
通过使用Helm对您的应用程序进行参数化
到目前为止,我们讨论的重点是将服务的单个实例部署到单个集群。然而,在现实中,几乎每个服务和服务团队都需要部署到多个环境(即使它们共享一个集群)。即使您是一个单独的开发者,只是在一个应用程序上工作,您可能希望至少有一个开发版本和一个生产版本的应用程序,以便进行迭代和开发,而不会影响生产用户。考虑到集成测试和CI/CD,即使只有一个服务和少数开发者,您可能也想要部署到至少三个不同的环境,如果考虑处理数据中心级别的故障,可能还要更多。让我们探讨一些部署选项。
许多团队的初始故障模式是简单地将文件从一个集群复制到另一个集群。与其拥有一个单独的frontend/目录,不如有一个frontend-production/和frontend-development/目录对。虽然这是一个可行的选项,但也是危险的,因为您现在需要确保这些文件保持彼此同步。如果它们本来应该完全相同,那可能很容易,但由于您将开发新功能,预计开发和生产之间会出现一些差异。关键是,这种差异是有意的,而且容易管理的。
另一个实现这一目标的选项是使用分支和版本控制,其中生产和开发分支从中央存储库分开,分支之间的差异清晰可见。对于某些团队来说,这可能是一个可行的选项,但当您想要同时将软件部署到不同的环境时(例如,一个CI/CD系统,将软件部署到多个不同的云区域),在分支之间切换的机制会带来挑战。
因此,大多数人最终都会采用模板系统。模板系统将模板与参数相结合,形成应用程序配置的集中骨干,参数用于将模板专门化为特定的环境配置。通过这种方式,您可以拥有一个通常共享的配置,只需要进行有意义(且容易理解)的自定义。Kubernetes有各种模板系统,但迄今为止最受欢迎的是Helm。
在Helm中,应用程序打包成一个称为chart(在容器和Kubernetes的世界中充斥着航海笑话)的文件集合。
一个chart以一个chart.yaml文件开始,该文件定义了chart本身的元数据:
apiVersion: v1
appVersion: "1.0"
description: A Helm chart for our frontend journal server.
name: frontend
version: 0.1.0
这个文件放在chart目录的根目录下(例如,frontend/)。在这个目录中,有一个templates目录,用于存放模板文件。一个模板基本上是先前示例中的一个YAML文件,其中文件中的一些值被参数引用替代。例如,假设您想要对前端的副本数量进行参数化。以前,部署文件如下:
...
spec:
replicas: 2
...
在模板文件(frontend-deployment.tmpl)中,它看起来如下:
...
spec:
replicas: {{ .replicaCount }}
...
这意味着在部署图表时,您将使用适当的参数替换副本的值。这些参数本身在values.yaml文件中定义。每个应用程序应该部署的环境都会有一个values文件。这个简单图表的values文件如下所示:
replicaCount: 2
将所有这些组合起来,您可以使用helm工具部署此图表,如下所示:
helm install path/to/chart --values path/to/environment/values.yaml
这样可以将您的应用程序参数化并部署到Kubernetes。随着时间的推移,这些参数化将扩展以适应您应用程序的各种环境。
部署服务的最佳实践
Kubernetes是一个强大的系统,看起来可能复杂。但如果遵循以下最佳实践,设置基本的应用程序以确保成功将会变得简单:
- 大多数服务应该部署为Deployment资源。Deployment创建相同的副本以提高冗余性和扩展性。
- 可以使用Service来公开部署。Service实际上是一个负载均衡器。Service可以在集群内(默认设置)或外部公开。如果要公开HTTP应用程序,可以使用Ingress控制器添加请求路由和SSL等功能。
- 最终,您将希望参数化您的应用程序,以使其配置更容易在不同环境中重复使用。打包工具如Helm是实现这种参数化的最佳选择。
总结
本章构建的应用程序虽然简单,但包含了构建更大、更复杂的应用程序所需的几乎所有概念。理解这些组件如何配合使用以及如何使用基本的Kubernetes组件对于成功地使用Kubernetes至关重要。
通过版本控制、代码审查和不断交付您的服务来奠定正确的基础,确保无论构建什么,都是坚实的。在随后的章节中,当我们讨论更高级的主题时,请牢记这些基础信息。