Kubernetes实战:从零构建可扩展应用

在当今这个以云原生为主导的时代,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.jsonserver.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.jsonpackage-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应用部署,我们主要需要两种资源:DeploymentService

一个关键步骤: 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/v1kind: 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)。几分钟内,您会看到:

  1. TARGETS列的CPU使用率开始攀升,超过50%。
  2. 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

我们添加了livenessProbereadinessProbeinitialDelaySeconds参数非常重要,它给了应用足够的启动时间,避免了容器刚一启动就被探针判定为失败。

使用 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

您会看到非常有趣的变化:

  1. 那个被我们操作的Pod的状态会短暂变为ErrorCrashLoopBackOff
  2. 它的RESTARTS计数会从0增加到1。
  3. 很快,它又会变回Running状态。

这是因为,当我们杀死进程后,容器内的应用不再响应HTTP请求。存活探针在几次尝试失败后,会通知Kubelet该容器不健康。Kubelet随即杀死了这个容器,并根据Deployment的默认策略,重新创建了一个新的、健康的容器来替代它。整个过程完全自动化,无需人工干预,从而保证了服务的高可用性。这就是Kubernetes自我修复能力的魅力所在。

整合CI/CD流水线:自动化部署

到目前为止,我们所有的操作——构建镜像、应用YAML配置——都是手动在命令行中完成的。在个人学习和实验阶段,这完全没有问题。但在一个真实的团队协作和生产环境中,这种手动流程是低效且极易出错的。这正是CI/CD (持续集成/持续部署) 发挥作用的地方。CI/CD是DevOps文化的核心实践,其目标是自动化软件的构建、测试和发布流程,实现快速、可靠的交付。

我们将探索如何利用当今最流行的CI/CD工具之一——GitHub Actions,来为我们的Kubernetes应用构建一个全自动的部署流水线。我们的目标是:每当我们将新的代码推送到GitHub仓库的main分支时,GitHub Actions会自动执行以下任务:

  1. 构建新的Docker镜像。
  2. 将镜像推送到一个容器镜像仓库(如Docker Hub或GitHub Container Registry)。
  3. 更新Kubernetes集群中的Deployment,使其使用我们新构建的镜像,从而触发滚动更新。

使用GitHub Actions自动化CI/CD

GitHub Actions允许我们直接在GitHub仓库中定义工作流(Workflows)。这些工作流由事件触发(如pushpull_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集群,我们可以通过以下方式获取:
    # 这个命令会输出你的kubeconfig文件内容,将其完整复制
    cat ~/.kube/config
            
    重要: kind生成的kubeconfig中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 builddocker 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流水线。您不再只是听说过DockerKubernetesDevOps这些时髦的词汇,而是通过一个完整的Kubernetes实践例子,将它们串联成了一个有机的整体,真正理解了它们是如何协同工作的。

我们回顾一下整个流程:

  1. 环境准备: 使用 `kind` 快速搭建了本地Kubernetes开发环境。
  2. 容器化: 编写了优化的 `Dockerfile`,应用了多阶段构建等最佳实践,将应用打包成高效的Docker镜像。
  3. 核心部署: 学习并编写了 `Deployment` 和 `Service` 的YAML文件,将应用部署到集群并使其可被访问,理解了标签和选择器的核心作用。
  4. 弹性与健壮性: 配置了 `HPA` 来实现应用的自动水平伸缩,并添加了 `Liveness/Readiness Probes` 来赋予应用自我修复的能力。
  5. 自动化: 利用GitHub Actions创建了一个实用的CI/CD工作流,实现了从代码提交到自动部署的完整闭环。

这趟旅程为您打下了坚实的Kubernetes基础,但它仅仅是一个开始。云原生的世界广阔而深邃,还有许多激动人心的领域等待您去探索。为了帮助您规划接下来的学习路径,我为您整理了一份“面向初学者的DevOps路线图”。

面向初学者的DevOps路线图

这份路线图旨在为您提供一个结构化的学习方向,帮助您从入门到精通,逐步成长为一名全面的DevOps工程师。

  1. 第一阶段:容器化基础 (您已完成)
    • 核心技能: 深入掌握 Docker,包括 `Dockerfile` 的高级优化、Docker Compose 用于本地多服务编排、理解容器网络和存储卷。
    • 目标: 能够将任何类型的应用(前端、后端、数据库)高效地容器化。
  2. 第二阶段:容器编排 (您已入门)
    • 核心技能: 深入学习 Kubernetes。除了我们今天接触的 Deployment 和 Service,还需要掌握 `StatefulSet` (用于有状态应用如数据库)、`DaemonSet` (用于在每个节点运行一个Pod)、`ConfigMap` 和 `Secret` (用于配置管理)、`PersistentVolume` 和 `PersistentVolumeClaim` (用于持久化存储)。
    • 目标: 能够独立设计和部署复杂的、包含有状态服务的应用到Kubernetes。
  3. 第三阶段:基础设施即代码 (IaC)
    • 核心技能: 学习使用 Terraform 或 Pulumi 来通过代码管理和部署云基础设施(如VPC、虚拟机、Kubernetes集群本身)。学习使用 Ansible 或 Chef/Puppet 进行配置管理。
    • 目标: 实现从零开始全自动地创建一套完整的、可复用的云环境。
  4. 第四阶段:CI/CD 精通 (您已实践)
    • 核心技能: 深入探索您选择的CI/CD工具。学习更复杂的流水线技术,如蓝绿部署、金丝雀发布、制品库管理(Artifactory/Nexus)。
    • 目标: 能够为任何项目设计和实现安全、高效、可靠的发布策略。
  5. 第五阶段:监控、日志与可观测性
    • 核心技能: 掌握云原生监控的事实标准 Prometheus 和 Grafana。学习使用 ELK Stack (Elasticsearch, Logstash, Kibana) 或 EFK Stack (加上 Fluentd) 进行集中式日志管理。了解分布式追踪(Jaeger/Zipkin)的概念。
    • 目标: 能够为您的系统建立全面的可观测性,做到问题的主动发现、快速定位和解决。
  6. 第六阶段:安全 (DevSecOps)
    • 核心技能: 将安全左移到开发流程的早期。学习容器镜像扫描(Trivy/Clair)、依赖项安全检查、Kubernetes安全策略(PodSecurityPolicy/OPA Gatekeeper)、密钥管理(Vault)。
    • 目标: 构建不仅高效,而且从一开始就具备安全基因的系统。

云原生和DevOps的旅程是持续学习和不断实践的过程。希望本文能成为您这段旅程中一块坚实的垫脚石。现在,最好的学习方式就是动手去尝试。修改我们的示例应用,为它增加一个数据库,尝试使用 `StatefulSet` 和 `PVC` 来部署它;或者探索更高级的部署策略。世界在您手中,祝您探索愉快!

Post a Comment