在当今这个以云原生为主导的时代,Kubernetes (K8s) 已经从一个“值得关注”的技术,演变成了构建现代化、可扩展、高弹性系统的基石。然而,对于许多开发者来说,Kubernetes的学习曲线似乎相当陡峭。官方文档详尽但理论性强,网络上的教程又常常零散,缺乏一个从头到尾、连贯且贴近真实生产环境的实践案例。这正是本文旨在解决的问题。作为一名在云原生领域摸爬滚打了多年的全栈开发者,我深知理论与实践之间的鸿沟。因此,我将摒弃空洞的概念堆砌,带领您完成一个完整的 Kubernetes 实践例子,从一个简单的Web应用开始,一步步将其容器化,部署到Kubernetes集群,并最终实现CI/CD自动化,构建起一套真正实用的DevOps流水线。
本文的目标读者是那些对Docker有基本了解,但对Kubernetes感到迷茫,渴望通过一个实际项目来掌握其核心用法的开发者。我们将不仅仅是“运行”一个应用,更重要的是理解其背后的“为什么”和“怎么做”。您将学到如何搭建本地开发环境、如何编写高效的Dockerfile、如何定义Kubernetes的核心资源(Deployment、Service)、如何实现应用的自动伸缩与健康检查,以及如何利用GitHub Actions将这一切自动化。这不仅仅是一个教程,更像是一份浓缩的实战笔记,希望能为您在Kubernetes的探索之路上点亮一盏灯。
准备工作:搭建本地Kubernetes环境
在我们开始部署应用之前,首先需要一个功能完备的Kubernetes集群。幸运的是,我们无需一开始就购买昂贵的云服务。社区提供了众多优秀的工具,可以在我们自己的开发机器上模拟出一个单节点或多节点的Kubernetes环境。这对于学习、开发和测试来说是至关重要的,它能提供一个快速、低成本的反馈循环。选择合适的本地集群工具可以极大地提升我们的开发效率。
目前市面上主流的本地Kubernetes环境工具有Minikube、kind (Kubernetes in Docker) 和 Docker Desktop内置的Kubernetes。它们各有千秋,适用于不同的场景。让我们通过一个表格来清晰地比较它们的特点:
| 工具名称 | 底层实现 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Minikube | 虚拟机 (VirtualBox, Hyper-V) 或 Docker 容器 | 功能最完善,支持多种驱动,社区成熟,文档丰富。 | 资源占用相对较高,启动速度稍慢。 | 需要模拟完整单节点集群功能,对附加组件(Addons)有较多需求的开发者。 |
| kind (Kubernetes in Docker) | Docker 容器 | 轻量、快速,完全基于容器,启动和销毁极快,非常适合CI/CD环境。 | 网络配置相对复杂一些,对Docker环境有强依赖。 | 追求极致启动速度、需要频繁创建和销毁集群的场景,如自动化测试和本地快速迭代。 |
| Docker Desktop | 集成在Docker Desktop应用中 (背后使用虚拟机) | 安装和启用最简单,与Docker无缝集成,一键开启。 | 定制化能力弱,资源占用较高,可能会遇到一些平台特有的Bug。 | Windows和Mac用户,希望最简单快捷地体验Kubernetes的初学者。 |
对于本次实践,我强烈推荐使用 kind。它的核心理念是“Kubernetes in Docker”,即每个Kubernetes节点都表现为一个Docker容器。这种设计的最大好处是轻量和快速。我们可以在几十秒内启动一个全新的、纯净的Kubernetes集群,这对于需要反复试验的开发过程来说,体验极佳。此外,由于它完全运行在Docker之上,也使得环境更加纯粹和可预测。
使用 kind 安装本地集群
首先,确保您的机器上已经安装了 Docker。然后,根据您的操作系统,按照 kind 官方文档 安装 kind CLI。
安装完成后,创建一个集群就只需要一条简单的命令:
# 创建一个名为 'dev-cluster' 的集群
kind create cluster --name dev-cluster
这条命令会拉取所需的节点镜像,并创建作为Kubernetes节点的Docker容器。稍等片刻,当命令执行完毕,您的本地Kubernetes集群就准备就绪了。您可以通 kubectl 来验证集群状态。
安装并配置 kubectl
kubectl 是与Kubernetes集群交互的命令行工具,是每个K8s开发者都必须掌握的核心工具。如果您还没有安装,可以参考 Kubernetes官方文档进行安装。
kind在创建集群时,会自动将新集群的配置信息写入到您的 ~/.kube/config 文件中,并将其设置为当前上下文。您可以通过以下命令来检查集群是否正常连接:
# 查看集群信息,确认客户端和服务器版本
kubectl cluster-info
# 查看当前集群中的所有节点 (对于kind,您应该能看到一个名为 'dev-cluster-control-plane' 的节点)
kubectl get nodes
如果以上命令都能成功返回信息,那么恭喜您,您的本地Kubernetes实验环境已经搭建完毕!现在,我们可以开始真正的应用部署之旅了。
第一步:容器化我们的示例应用
Kubernetes本身并不直接运行源代码,它管理的是容器(Container)。因此,在将应用部署到K8s之前,我们必须先将其打包成一个标准的容器镜像。这个过程就是“容器化”。我们将使用目前最主流的容器技术——Docker来完成这项工作。
为了使例子简单明了,我们创建一个非常基础的 Node.js Web 应用。这个应用只有一个功能:监听8080端口,当收到HTTP请求时,返回一条带有主机名的欢迎信息。这有助于我们在后续的负载均衡和伸缩实验中,清晰地看到请求被路由到了哪个Pod。
示例 Node.js 应用
首先,创建一个名为 k8s-app 的项目目录。在该目录下,创建两个文件:package.json 和 server.js。
package.json:
{
"name": "k8s-practical-example",
"version": "1.0.0",
"description": "A simple Node.js app for Kubernetes practical example",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
server.js:
const express = require('express');
const os = require('os');
const app = express();
const PORT = 8080;
app.get('/', (req, res) => {
const hostname = os.hostname();
res.send(`Hello from Kubernetes! \nRunning on host: ${hostname}\n`);
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
在项目目录下运行 npm install 来安装依赖。然后运行 npm start,您应该可以在浏览器中访问 http://localhost:8080 看到欢迎信息。
编写 Dockerfile
接下来,我们在项目根目录下创建 Dockerfile 文件,它定义了如何构建我们的应用镜像。
# 使用一个官方的 Node.js 18 的精简版作为基础镜像
FROM node:18-alpine
# 设置工作目录
WORKDIR /usr/src/app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装生产环境依赖
RUN npm install --only=production
# 复制应用源代码
COPY . .
# 暴露应用监听的端口
EXPOSE 8080
# 定义容器启动时执行的命令
CMD [ "node", "server.js" ]
开发者提示: 为什么选择
node:18-alpine?Alpine Linux是一个极度轻量级的Linux发行版,基于它构建的镜像体积非常小。在生产环境中,更小的镜像意味着更快的拉取速度、更少的存储占用和更小的攻击面。这是一个简单而有效的优化技巧。
如何优化Docker镜像
上面的 Dockerfile 虽然能工作,但在真实的DevOps流程中,我们总是追求极致的效率和安全。这就引出了一个重要的话题:如何优化Docker镜像。一个臃肿、构建缓慢的镜像会严重拖慢整个CI/CD流水线。以下是几个关键的优化策略,我们将通过改进我们的Dockerfile来实践它们。
1. 使用多阶段构建 (Multi-stage Builds)
多阶段构建是优化镜像大小的“杀手锏”。我们的应用在构建时可能需要开发依赖(如devDependencies)、编译器、测试工具等,但最终运行应用时,这些东西都是不需要的。多阶段构建允许我们使用一个临时的“构建”阶段来安装所有依赖并编译代码,然后只将最终的产物复制到一个干净、轻量的“生产”阶段镜像中。
对于我们的Node.js应用,虽然没有编译步骤,但我们可以利用这个特性来确保只有生产依赖和源代码被打包进去,避免任何开发工具的残留。
2. 利用构建缓存
Docker在构建镜像时会逐层缓存。如果某一层的内容没有变化,Docker会直接使用缓存,从而大大加快构建速度。观察我们最初的Dockerfile,COPY . . 这一步会复制所有文件。如果我们只修改了一个文档文件,npm install 这一层也会因为缓存失效而重新运行,这非常低效。
正确的做法是:先复制package.json和package-lock.json,然后运行npm install,最后再复制应用代码。这样,只要依赖没有变化,npm install这一步就会一直使用缓存。
3. 使用 .dockerignore 文件
类似于 .gitignore,.dockerignore 文件可以告诉Docker在构建镜像时忽略哪些文件或目录。这可以防止将一些本地开发环境的临时文件、日志、或者node_modules目录(我们希望在容器内重新安装)打包进镜像,既减小了体积,也避免了潜在的冲突。
在项目根目录创建 .dockerignore 文件:
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
优化后的 Dockerfile
结合以上策略,我们可以将 Dockerfile 升级为以下版本:
# ---- 构建阶段 (Builder Stage) ----
FROM node:18-alpine AS builder
WORKDIR /usr/src/app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装所有依赖,包括开发依赖(如果需要构建或测试)
# 这里我们仍然只安装生产依赖,但这是一个放置构建步骤的好地方
RUN npm install --only=production
# ---- 生产阶段 (Production Stage) ----
FROM node:18-alpine
WORKDIR /usr/src/app
# 从构建阶段复制 node_modules
COPY --from=builder /usr/src/app/node_modules ./node_modules
# 复制应用源代码
COPY . .
# 暴露端口
EXPOSE 8080
# 启动命令
CMD [ "node", "server.js" ]
这个版本虽然看起来更复杂,但它遵循了最佳实践,构建出的镜像是纯净且高效的。对于编译型语言(如Go、Java、Rust),多阶段构建的效果会更加惊人,镜像体积可以减小90%以上。
构建并测试镜像
现在,我们使用优化后的Dockerfile来构建镜像。
# 构建镜像,-t 参数用于给镜像打上标签 'name:tag'
# 注意最后的 '.',表示使用当前目录的 Dockerfile
docker build -t k8s-app:1.0 .
# 运行容器进行测试
# -p 8080:8080 将主机的8080端口映射到容器的8080端口
# --rm 容器退出后自动删除
docker run --rm -p 8080:8080 k8s-app:1.0
再次访问 http://localhost:8080,如果应用正常工作,说明我们的容器镜像已经成功构建!下一步,就是将这个镜像部署到我们的Kubernetes集群中。
Kubernetes核心概念实战:部署应用
应用已经成功容器化,现在是时候让 Kubernetes 登场了。在K8s中,我们不再通过手动的 docker run 命令来启动应用,而是通过声明式API来定义我们应用的“期望状态”。我们告诉Kubernetes:“我想要运行3个我们刚才构建的k8s-app:1.0镜像的副本,并让它们可以通过网络访问”,然后Kubernetes的控制平面就会努力工作,使得集群的“实际状态”与我们的“期望状态”保持一致。
这种声明式的方法是Kubernetes强大功能的核心。我们通过YAML文件来描述这些期望状态。对于一个基础的Web应用部署,我们主要需要两种资源:Deployment 和 Service。
一个关键步骤: kind集群运行在Docker容器网络中,它无法直接访问您本地机器的Docker守护进程中的镜像。我们需要先将本地构建的镜像加载到kind集群内部。
kind load docker-image k8s-app:1.0 --name dev-cluster这条命令会将
k8s-app:1.0镜像推送到我们的kind集群节点中,这样K8s在创建Pod时才能找到它。
Deployment:定义应用的期望状态
Deployment 是Kubernetes中用于管理无状态应用的最常用资源。它负责以下几件事:
- 定义使用哪个容器镜像。
- 指定需要运行多少个应用的副本(Pod)。
- 管理应用的滚动更新、回滚等生命周期操作。
让我们来创建一个 deployment.yaml 文件。
apiVersion: apps/v1
kind: Deployment
metadata:
# Deployment 的名称,在命名空间内必须唯一
name: k8s-app-deployment
labels:
app: k8s-app
spec:
# 定义期望的副本数量
replicas: 2
selector:
# 这个 selector 必须匹配下面 template 中的 labels
# 它告诉 Deployment 要管理哪些 Pod
matchLabels:
app: k8s-app
template:
# Pod 模板,定义了如何创建 Pod
metadata:
# Pod 的标签,非常重要,用于 Service 的选择
labels:
app: k8s-app
spec:
containers:
- name: k8s-app-container
# 使用我们加载到 kind 集群的镜像
image: k8s-app:1.0
# !!!重要:这确保了K8s总是在本地查找镜像,而不是去远程仓库拉取
imagePullPolicy: IfNotPresent
ports:
# 声明容器暴露的端口
- containerPort: 8080
让我们逐段解析这个YAML文件:
apiVersion: apps/v1和kind: Deployment:定义了这是一个Deployment资源。metadata:包含了资源的元数据,如名称和标签。标签(labels)是K8s中组织和选择资源的核心机制。spec.replicas: 2:声明我们希望运行2个应用的副本。spec.selector.matchLabels:这是Deployment和它管理的Pod之间的桥梁。Deployment会持续监控集群中带有app: k8s-app标签的Pod,并确保其数量始终为2。spec.template:这部分是Pod的定义模板。每当Deployment需要创建一个新的Pod时,都会使用这个模板。template.metadata.labels:为Pod打上标签。注意,这里的标签必须与spec.selector.matchLabels匹配。template.spec.containers:定义了Pod中运行的容器列表。这里我们只有一个容器。image: k8s-app:1.0:指定了容器镜像。imagePullPolicy: IfNotPresent:这是一个在本地开发时非常重要的设置。默认情况下,K8s会尝试从远程镜像仓库拉取镜像。设置为IfNotPresent后,如果本地已经存在该镜像,则直接使用,否则才去拉取。因为我们的镜像是手动加载到kind节点中的,所以必须设置此项。containerPort: 8080:声明了我们的应用在容器内部监听的端口。
现在,使用 kubectl 应用这个配置文件:
kubectl apply -f deployment.yaml
应用成功后,我们可以检查Deployment和Pod的状态:
# 查看 Deployment 状态,可以看到 DESIRED, CURRENT, UP-TO-DATE, AVAILABLE 副本数
kubectl get deployment k8s-app-deployment
# 查看 Pod 列表,可以看到两个由该 Deployment 创建的 Pod 正在运行
# -l 参数用于通过标签过滤
kubectl get pods -l app=k8s-app
# 我们可以查看其中一个 Pod 的日志
# kubectl logs [POD_NAME]
kubectl logs k8s-app-deployment-xxxxxxxx-yyyyy
此时,我们的应用已经在K8s集群中运行起来了!但是,我们还无法从外部访问它。这些Pod只在集群的内部网络中拥有IP地址。要让外界能够访问,我们需要创建下一个核心资源:Service。
Service:向外部暴露我们的应用
Service 在Kubernetes中扮演着“服务发现”和“负载均衡”的角色。它为一组功能相同的Pod提供一个单一、稳定的入口点。即使后面的Pod因为扩缩容、更新或故障而发生IP地址变化,Service的地址是保持不变的。Service通过标签选择器(Label Selector)来找到它应该代理的Pod。
Kubernetes提供了几种不同类型的Service,以适应不同的暴露需求:
| Service 类型 | 描述 | 典型用例 |
|---|---|---|
| ClusterIP | (默认类型) 仅在集群内部暴露服务。每个Service会获得一个虚拟的内部IP,只能从集群内的其他Pod访问。 | 后端服务之间的通信,如Web应用访问数据库服务。 |
| NodePort | 在每个节点的同一个静态端口上暴露服务。可以通过 <NodeIP>:<NodePort> 从集群外部访问服务。 |
开发和测试环境中快速暴露服务进行调试。 |
| LoadBalancer | 使用云服务提供商(如AWS, GCP, Azure)的负载均衡器来向公网暴露服务。云平台会自动创建和配置一个外部负载均衡器。 | 在生产环境中向公网用户提供服务的标准方式。 |
| ExternalName | 将服务映射到外部的一个DNS名称,通过返回CNAME记录实现。用于在集群内部访问外部服务。 | 集群内部服务需要通过一个固定别名访问集群外部的某个服务。 |
对于我们的本地kind集群,使用 NodePort 是最方便的暴露方式。让我们创建 service.yaml 文件。
apiVersion: v1
kind: Service
metadata:
name: k8s-app-service
spec:
# 定义 Service 类型为 NodePort
type: NodePort
selector:
# 这个 selector 必须匹配我们之前 Deployment 中 Pod 的标签
# Service 将会把流量转发到所有带有 'app: k8s-app' 标签的 Pod
app: k8s-app
ports:
# 定义端口映射关系
- protocol: TCP
# Service 自身在集群内部监听的端口
port: 80
# 流量最终要转发到 Pod 的哪个端口
targetPort: 8080
# 在节点上暴露的端口,如果不指定,K8s会随机分配一个 (通常在 30000-32767 之间)
# 为了方便记忆,我们手动指定一个
nodePort: 30001
解析一下这个文件:
type: NodePort:明确指定Service类型。selector.app: k8s-app:这是Service与Pod关联的关键。Service会持续扫描并把所有标签为app: k8s-app的Pod作为其后端端点(Endpoints)。ports:定义端口的映射规则。port: 80:Service在集群内部的ClusterIP上监听的端口。targetPort: 8080:Pod内容器实际监听的端口,必须与我们Dockerfile中EXPOSE以及应用本身监听的端口一致。nodePort: 30001:在集群的每个物理/虚拟节点上开放的端口。我们的外部请求将通过这个端口进入。
应用这个Service配置:
kubectl apply -f service.yaml
查看Service状态:
kubectl get service k8s-app-service
# 或者简写
kubectl get svc k8s-app-service
现在,我们可以通过访问我们本地机器的 localhost 和指定的 nodePort 来访问应用了。因为kind节点(一个Docker容器)的端口被映射到了我们的主机上。
在浏览器中打开 http://localhost:30001。您应该能看到应用的欢迎信息!尝试多次刷新页面,您可能会发现Running on host: ... 后面的主机名(即Pod名称)在变化。这是因为Service在默认情况下会对后端的两个Pod进行轮询负载均衡。这完美地展示了Kubernetes服务发现和负载均衡的强大能力。
实现应用的水平扩展与自我修复
我们已经成功部署了应用,并实现了基本的负载均衡。但现代应用面临的挑战是流量的波动性。当用户访问量激增时,我们需要能够自动增加应用实例来处理负载;当流量回落时,又需要减少实例以节约资源。同时,我们也需要确保系统在面对单个实例故障时能够自动恢复,保持服务的可用性。这正是Kubernetes最吸引人的两个特性:自动伸缩和自我修复。接下来,我们将通过实践来探索这两个强大的功能。
水平Pod自动缩放 (Horizontal Pod Autoscaler - HPA)
HPA是Kubernetes中实现自动伸缩的核心资源。它能够监控目标资源(通常是Deployment)的Pod的度量指标(最常见的是CPU或内存使用率),并根据预设的阈值自动调整Pod的副本数量。
要使用HPA,我们的集群中必须有一个能够提供度量数据的组件,即Metrics Server。Metrics Server会从每个节点上的Kubelet收集资源使用情况,并通过Kubernetes聚合API暴露出来,供HPA等组件消费。
1. 安装 Metrics Server
对于kind集群,安装Metrics Server非常简单。我们只需要应用其官方的部署YAML文件即可。
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
安装后,需要稍等一两分钟让Metrics Server启动并开始收集数据。您可以通过以下命令检查它是否正常工作:
# 查看Pod资源使用情况
kubectl top pods
# 查看Node资源使用情况
kubectl top nodes
如果这些命令能够返回CPU和内存的使用数据,说明Metrics Server已经准备就绪。
2. 创建 HPA 资源
现在,我们来创建一个HPA,让它监控我们的 k8s-app-deployment。我们的目标是:当所有Pod的平均CPU使用率超过50%时,就增加Pod的数量,最多增加到5个;当负载下降时,再自动缩减回去,最少保留2个。
创建 hpa.yaml 文件:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: k8s-app-hpa
spec:
# HPA要监控和伸缩的目标资源
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: k8s-app-deployment
# 最小和最大副本数范围
minReplicas: 2
maxReplicas: 5
# 定义伸缩所依据的度量指标
metrics:
- type: Resource
resource:
name: cpu
# 目标是所有Pod的平均CPU使用率达到50%
target:
type: Utilization
averageUtilization: 50
应用这个配置:
kubectl apply -f hpa.yaml
我们可以通过以下命令来观察HPA的状态:
# -w 参数表示持续观察 (watch)
kubectl get hpa k8s-app-hpa -w
刚开始,您会看到 TARGETS 列显示 <unknown>/50%,这是因为HPA需要一点时间从Metrics Server获取到数据。很快,它就会显示一个具体的数值,比如 1%/50%。
3. 制造负载并观察自动伸缩
为了触发自动伸缩,我们需要给应用增加负载。我们可以通过在一个终端中运行一个循环的curl命令来模拟大量请求:
# 在一个新的终端窗口中执行
while true; do curl http://localhost:30001; done
为了让CPU负载更明显,我们可以稍微修改一下我们的server.js,加入一些计算密集型任务。但为了简单起见,我们将通过启动一个临时的负载生成Pod来施加压力。
# 运行一个临时的 busybox 容器,在其中不断向我们的 service 发送请求
kubectl run -it --rm load-generator --image=busybox /bin/sh
# 在进入 load-generator 的 shell 后,执行以下命令
# 注意:k8s-app-service 是我们 service 的名称,它会被K8s的内部DNS解析
while true; do wget -q -O- http://k8s-app-service; done
现在,回到观察HPA的终端窗口 (kubectl get hpa -w)。几分钟内,您会看到:
TARGETS列的CPU使用率开始攀升,超过50%。REPLICAS列的数字会从2变为3,然后可能是4,甚至5。
同时,您可以打开另一个终端,观察Pod的数量变化:
kubectl get pods -l app=k8s-app -w
您会看到新的Pod被创建并进入Running状态。当您停止负载生成器(关闭那个终端或按Ctrl+C),几分钟后,HPA会检测到负载下降,并自动将副本数缩减回minReplicas(即2个)。这个过程完美地展示了Kubernetes应对流量变化的弹性能力。
自我修复能力:探针 (Probes)
除了应对负载变化,一个健壮的系统还需要能够处理个体故障。比如,如果我们的应用因为一个未捕获的异常而崩溃,或者陷入死锁不再响应请求,该怎么办?Kubernetes的探针(Probes)机制就是为了解决这个问题。
Kubelet使用探针来检查容器的健康状况。主要有三种类型的探针:
- Liveness Probe (存活探针): 检查容器是否还在运行。如果存活探针失败,Kubelet会杀死该容器,并根据其重启策略(Restart Policy)来决定是否重启它。这用于处理应用死锁等问题。
- Readiness Probe (就绪探针): 检查容器是否准备好接收流量。如果就绪探针失败,容器不会被杀死,但它会从Service的端点列表中被移除,不再接收新的请求。这对于那些需要较长启动时间的应用非常有用,可以防止在应用完全就绪前将流量打进来。
- Startup Probe (启动探针): 检查容器内的应用是否已经启动。在启动探针成功之前,其他的探针(存活和就绪)都不会被执行。这为启动缓慢的应用提供了保护,避免它们因为启动时间过长而被存活探针误杀。
让我们为我们的应用添加存活探针和就绪探针。我们将在deployment.yaml文件中进行修改。我们将使用HTTP GET探针,Kubelet会定期向我们应用的/路径发送HTTP请求,如果返回200-399范围内的状态码,则认为探针成功。
更新后的 deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-app-deployment
labels:
app: k8s-app
spec:
replicas: 2
selector:
matchLabels:
app: k8s-app
template:
metadata:
labels:
app: k8s-app
spec:
containers:
- name: k8s-app-container
image: k8s-app:1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
# --- 新增的探针配置 ---
livenessProbe:
httpGet:
# 探测的路径
path: /
# 探测的端口
port: 8080
# 容器启动后首次探测前的延迟时间(秒)
initialDelaySeconds: 5
# 探测的频率(秒)
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
我们添加了livenessProbe和readinessProbe。initialDelaySeconds参数非常重要,它给了应用足够的启动时间,避免了容器刚一启动就被探针判定为失败。
使用 kubectl apply -f deployment.yaml 来更新我们的Deployment。Kubernetes会以滚动更新的方式,用带有新配置的Pod替换掉旧的Pod。
模拟故障并观察自我修复
现在来模拟一次应用故障。我们可以exec进入其中一个Pod,并手动杀死Node.js进程。
# 1. 获取一个Pod的名称
POD_NAME=$(kubectl get pods -l app=k8s-app -o jsonpath='{.items[0].metadata.name}')
# 2. 进入该Pod的shell
kubectl exec -it $POD_NAME -- /bin/sh
# 3. 在Pod的shell中,找到并杀死node进程
# / # ps
# PID USER TIME COMMAND
# 1 node 0:00 node server.js
# ...
# / # kill 1
杀死进程后,立即退出Pod的shell,并快速观察Pod的状态:
kubectl get pods -l app=k8s-app -w
您会看到非常有趣的变化:
- 那个被我们操作的Pod的状态会短暂变为
Error或CrashLoopBackOff。 - 它的
RESTARTS计数会从0增加到1。 - 很快,它又会变回
Running状态。
这是因为,当我们杀死进程后,容器内的应用不再响应HTTP请求。存活探针在几次尝试失败后,会通知Kubelet该容器不健康。Kubelet随即杀死了这个容器,并根据Deployment的默认策略,重新创建了一个新的、健康的容器来替代它。整个过程完全自动化,无需人工干预,从而保证了服务的高可用性。这就是Kubernetes自我修复能力的魅力所在。
整合CI/CD流水线:自动化部署
到目前为止,我们所有的操作——构建镜像、应用YAML配置——都是手动在命令行中完成的。在个人学习和实验阶段,这完全没有问题。但在一个真实的团队协作和生产环境中,这种手动流程是低效且极易出错的。这正是CI/CD (持续集成/持续部署) 发挥作用的地方。CI/CD是DevOps文化的核心实践,其目标是自动化软件的构建、测试和发布流程,实现快速、可靠的交付。
我们将探索如何利用当今最流行的CI/CD工具之一——GitHub Actions,来为我们的Kubernetes应用构建一个全自动的部署流水线。我们的目标是:每当我们将新的代码推送到GitHub仓库的main分支时,GitHub Actions会自动执行以下任务:
- 构建新的Docker镜像。
- 将镜像推送到一个容器镜像仓库(如Docker Hub或GitHub Container Registry)。
- 更新Kubernetes集群中的Deployment,使其使用我们新构建的镜像,从而触发滚动更新。
使用GitHub Actions自动化CI/CD
GitHub Actions允许我们直接在GitHub仓库中定义工作流(Workflows)。这些工作流由事件触发(如push、pull_request等),并在一系列预定义的作业(Jobs)和步骤(Steps)中执行我们的自动化任务。
1. 准备工作
- GitHub仓库: 将我们本地的
k8s-app项目初始化为Git仓库,并推送到一个新的GitHub仓库。 - 容器镜像仓库: 我们需要一个地方来存储构建好的Docker镜像。GitHub Container Registry (GHCR) 是一个绝佳的选择,因为它与GitHub Actions无缝集成。你也可以使用Docker Hub。
- Kubernetes集群访问凭证: GitHub Actions的运行环境(Runner)需要一种安全的方式来访问我们的Kubernetes集群。由于我们使用的是本地kind集群,这个过程会比较特殊。在真实的云环境中,通常会创建一个专用的ServiceAccount,并将其凭证作为Secret存储在GitHub中。为了演示,我们将整个
kubeconfig文件的内容作为Secret。安全警告: 在生产环境中,绝不应该暴露完整的
kubeconfig文件。应遵循最小权限原则,创建权限受限的ServiceAccount。
2. 配置GitHub Secrets
在你的GitHub仓库页面,进入 Settings > Secrets and variables > Actions,创建以下两个Repository secrets:
DOCKERHUB_USERNAME: 你的Docker Hub用户名(或者其他镜像仓库的用户名)。DOCKERHUB_TOKEN: 你的Docker Hub访问令牌(在Docker Hub的账户设置中生成)。KUBE_CONFIG_DATA: 这是访问集群的凭证。对于kind集群,我们可以通过以下方式获取:
重要: kind生成的# 这个命令会输出你的kubeconfig文件内容,将其完整复制 cat ~/.kube/configkubeconfig中server地址可能是https://127.0.0.1:xxxxx。GitHub Actions的Runner无法访问你本机的这个地址。你需要将其修改为你的电脑在局域网中的IP地址,或者使用像ngrok这样的工具创建一个公网隧道来暴露你的K8s API Server。为了演示的简单性,我们假设有一个公网可访问的K8s集群。
3. 编写 GitHub Actions Workflow 文件
在你的项目根目录下,创建 .github/workflows/ci-cd.yml 文件。这是定义我们工作流的地方。
name: Deploy to Kubernetes
# 触发工作流的事件:当有代码推送到 main 分支时
on:
push:
branches:
- main
jobs:
build-and-deploy:
# 指定运行环境
runs-on: ubuntu-latest
steps:
# 步骤1: 检出代码
- name: Checkout code
uses: actions/checkout@v3
# 步骤2: 登录到Docker Hub
# 这里使用了我们之前设置的Secrets
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# 步骤3: 设置Docker Buildx
# Buildx是Docker的下一代构建工具,支持更多高级功能
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# 步骤4: 构建并推送Docker镜像
# 我们使用GitHub的SHA作为镜像的唯一标签,这是最佳实践
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/k8s-app:${{ github.sha }}
# 步骤5: 安装和配置kubectl
# 我们将KUBE_CONFIG_DATA这个secret的内容写入一个临时文件
- name: Setup kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG_DATA }}" > ~/.kube/config
chmod 600 ~/.kube/config
# 步骤6: 部署到Kubernetes
# 使用 `kubectl set image` 命令来更新Deployment的镜像
# 这比修改YAML文件并重新apply更直接
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/k8s-app-deployment k8s-app-container=${{ secrets.DOCKERHUB_USERNAME }}/k8s-app:${{ github.sha }} --record=true
让我们详细解读这个工作流:
on: push: branches: [ main ]: 定义了触发器。jobs.build-and-deploy.steps: 定义了一系列按顺序执行的步骤。uses: actions/checkout@v3: 这是一个官方的Action,用于将仓库代码下载到Runner环境中。docker/login-action: 同样是一个社区提供的Action,用于安全地登录到容器仓库。docker/build-push-action: 核心步骤,它会执行docker build和docker push。我们使用了${{ github.sha }}这个GitHub Actions的内置变量作为镜像标签。这是一个非常好的实践,因为它确保了每个commit都有一个唯一对应的、可追溯的镜像版本。Setup kubectl: 这个步骤展示了如何使用我们存储在Secret中的kubeconfig数据来配置kubectl。Deploy to Kubernetes: 这是部署的关键。我们没有使用kubectl apply -f deployment.yaml,因为我们不想每次都修改本地的YAML文件。而是使用了kubectl set image命令,它能够直接告诉Kubernetes:“请更新名为k8s-app-deployment的Deployment中,名为k8s-app-container的容器,使其使用新的镜像标签”。--record=true参数会将这次变更记录在资源的注解中,方便后续回滚。
现在,将你的代码(包括这个新的workflow文件)提交并推送到GitHub的main分支。然后进入你GitHub仓库的“Actions”标签页,你会看到一个新的工作流正在运行。你可以点进去查看每一步的实时日志。如果一切配置正确,几分钟后,工作流会成功完成。此时,你可以通过kubectl describe deployment k8s-app-deployment来确认镜像已经被更新为最新的commit SHA标签,Kubernetes已经自动完成了应用的滚动更新。你已经成功建立了一个完整的自动化CI/CD流水线!
Jenkins与Kubernetes集成 (可选方案)
虽然GitHub Actions非常流行且易于上手,但在许多企业环境中,Jenkins 仍然是CI/CD领域的“老大哥”。Jenkins拥有极其强大的灵活性和庞大的插件生态系统。将Jenkins与Kubernetes集成也是一种非常常见的模式。
与GitHub Actions不同,Jenkins本身是一个需要独立部署和维护的服务。最现代化的集成方式是使用Jenkins Kubernetes Plugin。这个插件允许Jenkins Master动态地在Kubernetes集群中创建Jenkins Agent Pod来执行构建任务。这样做有几个巨大的好处:
- 资源高效: Agent只在需要时被创建,任务完成后自动销毁,不会一直占用资源。
- 环境隔离: 每个构建任务都在一个干净、独立的Pod中运行,避免了环境污染。
- 可伸缩性: 当构建任务增多时,可以自动创建更多的Agent Pod来应对。
一个典型的使用Jenkins与Kubernetes集成的CI/CD流程,通常通过一个名为 Jenkinsfile 的文件来定义,这个文件与源代码放在一起。
一个简化的 Jenkinsfile 示例可能如下:
pipeline {
agent {
// 动态地在Kubernetes中分配一个Agent Pod
kubernetes {
// 定义Pod模板
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: docker
image: docker:20.10.17
command:
- cat
tty: true
- name: kubectl
image: bitnami/kubectl:latest
command:
- cat
tty: true
"""
}
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build Image') {
steps {
container('docker') {
// Jenkinsfile通常会使用 build number 作为标签
sh 'docker build -t your-repo/k8s-app:${BUILD_NUMBER} .'
}
}
}
stage('Push Image') {
steps {
container('docker') {
// 使用withCredentials来安全地注入凭证
withCredentials([usernamePassword(credentialsId: 'DOCKER_HUB_CREDENTIALS', usernameVariable: 'USER', passwordVariable: 'PASS')]) {
sh 'echo $PASS | docker login -u $USER --password-stdin'
sh 'docker push your-repo/k8s-app:${BUILD_NUMBER}'
}
}
}
}
stage('Deploy to Kubernetes') {
steps {
container('kubectl') {
// 同样,Kubeconfig可以通过Jenkins的凭证管理来注入
withKubeconfig([credentialsId: 'KUBE_CONFIG_CREDENTIALS']) {
sh 'kubectl set image deployment/k8s-app-deployment k8s-app-container=your-repo/k8s-app:${BUILD_NUMBER}'
}
}
}
}
}
}
通过这个对比,我们可以看到,虽然最终目标相同,但GitHub Actions和Jenkins的实现方式和理念有所不同。GitHub Actions更倾向于使用预制的、可组合的“Actions”来构建流程,而Jenkins则提供了更底层的控制能力和脚本化的灵活性。选择哪个工具,取决于团队的技术栈、基础设施和具体需求。
总结与展望:你的DevOps下一步
恭喜您!如果您一路跟随到这里,您已经完成了一次意义非凡的旅程。我们从一个简单的本地Node.js应用出发,亲手实践了容器化、Kubernetes部署、服务暴露、自动伸缩、自我修复,并最终通过GitHub Actions构建了一条自动化CI/CD流水线。您不再只是听说过Docker、Kubernetes、DevOps这些时髦的词汇,而是通过一个完整的Kubernetes实践例子,将它们串联成了一个有机的整体,真正理解了它们是如何协同工作的。
我们回顾一下整个流程:
- 环境准备: 使用 `kind` 快速搭建了本地Kubernetes开发环境。
- 容器化: 编写了优化的 `Dockerfile`,应用了多阶段构建等最佳实践,将应用打包成高效的Docker镜像。
- 核心部署: 学习并编写了 `Deployment` 和 `Service` 的YAML文件,将应用部署到集群并使其可被访问,理解了标签和选择器的核心作用。
- 弹性与健壮性: 配置了 `HPA` 来实现应用的自动水平伸缩,并添加了 `Liveness/Readiness Probes` 来赋予应用自我修复的能力。
- 自动化: 利用GitHub Actions创建了一个实用的CI/CD工作流,实现了从代码提交到自动部署的完整闭环。
这趟旅程为您打下了坚实的Kubernetes基础,但它仅仅是一个开始。云原生的世界广阔而深邃,还有许多激动人心的领域等待您去探索。为了帮助您规划接下来的学习路径,我为您整理了一份“面向初学者的DevOps路线图”。
面向初学者的DevOps路线图
这份路线图旨在为您提供一个结构化的学习方向,帮助您从入门到精通,逐步成长为一名全面的DevOps工程师。
- 第一阶段:容器化基础 (您已完成)
- 核心技能: 深入掌握 Docker,包括 `Dockerfile` 的高级优化、Docker Compose 用于本地多服务编排、理解容器网络和存储卷。
- 目标: 能够将任何类型的应用(前端、后端、数据库)高效地容器化。
- 第二阶段:容器编排 (您已入门)
- 核心技能: 深入学习 Kubernetes。除了我们今天接触的 Deployment 和 Service,还需要掌握 `StatefulSet` (用于有状态应用如数据库)、`DaemonSet` (用于在每个节点运行一个Pod)、`ConfigMap` 和 `Secret` (用于配置管理)、`PersistentVolume` 和 `PersistentVolumeClaim` (用于持久化存储)。
- 目标: 能够独立设计和部署复杂的、包含有状态服务的应用到Kubernetes。
- 第三阶段:基础设施即代码 (IaC)
- 核心技能: 学习使用 Terraform 或 Pulumi 来通过代码管理和部署云基础设施(如VPC、虚拟机、Kubernetes集群本身)。学习使用 Ansible 或 Chef/Puppet 进行配置管理。
- 目标: 实现从零开始全自动地创建一套完整的、可复用的云环境。
- 第四阶段:CI/CD 精通 (您已实践)
- 核心技能: 深入探索您选择的CI/CD工具。学习更复杂的流水线技术,如蓝绿部署、金丝雀发布、制品库管理(Artifactory/Nexus)。
- 目标: 能够为任何项目设计和实现安全、高效、可靠的发布策略。
- 第五阶段:监控、日志与可观测性
- 核心技能: 掌握云原生监控的事实标准 Prometheus 和 Grafana。学习使用 ELK Stack (Elasticsearch, Logstash, Kibana) 或 EFK Stack (加上 Fluentd) 进行集中式日志管理。了解分布式追踪(Jaeger/Zipkin)的概念。
- 目标: 能够为您的系统建立全面的可观测性,做到问题的主动发现、快速定位和解决。
- 第六阶段:安全 (DevSecOps)
- 核心技能: 将安全左移到开发流程的早期。学习容器镜像扫描(Trivy/Clair)、依赖项安全检查、Kubernetes安全策略(PodSecurityPolicy/OPA Gatekeeper)、密钥管理(Vault)。
- 目标: 构建不仅高效,而且从一开始就具备安全基因的系统。
云原生和DevOps的旅程是持续学习和不断实践的过程。希望本文能成为您这段旅程中一块坚实的垫脚石。现在,最好的学习方式就是动手去尝试。修改我们的示例应用,为它增加一个数据库,尝试使用 `StatefulSet` 和 `PVC` 来部署它;或者探索更高级的部署策略。世界在您手中,祝您探索愉快!
Post a Comment