在上篇文章中,简单介绍了 K8s 的原理。单单通过文字,不足以帮助我们学习 K8s。所以本文通过实际的例子,由浅到深带大家在本地动手走一遍 K8s 的操作流程,旨在加深对 K8s 的理解。

我们将从最基础的 container 容器的定义出发,动手去跑 pod、deployment、service、ingress、namespace、configmap、secret、job 等操作的教程。下面的内容很干,跟着教程走一遍,相信你会大幅增加你对 K8s 的了解。在学习的过程中,不懂的及时问 GPT 或者查官方文档,这会事半功倍💪!

准备工作

在开始动手练习之前,请确保在本地配置好必要的环境。按照以下步骤进行设置:

1. 安装 Docker Desktop

首先,点击这个 链接 下载 Docker Desktop。安装完成后,可以通过运行以下命令来验证是否成功安装:

docker run hello-world

2. 搭建本地 K8s 集群

建议使用 Minikube 搭建本地 Kubernetes 集群,当然你也可以选择 Kind、Docker 自带的 K8s 等工具。对于 macOS 用户,可以通过以下命令快速安装 Minikube:

brew install minikube

对于 Linux 和 Windows 用户,请参考 官方教程 进行安装。

3. 启动本地 K8s 集群

安装完成后,你可以通过以下命令启动 Minikube:

minikube start --vm-driver docker --container-runtime=docker

以下是 Minikube 的一些常用命令:

  • minikube stop:停止 VM 和 K8s 集群,但不会删除任何数据。
  • minikube delete:删除所有 Minikube 启动后的数据。
  • minikube ip:查看集群和 Docker Engine 运行的 IP 地址。
  • minikube pause:暂停当前的资源和 K8s 集群。
  • minikube status:查看当前集群状态。

4. 安装 kubectl

kubectl 是用于操作 Kubernetes 集群的命令行工具。你可以通过以下命令在 macOS 上快速安装 kubectl,其他操作系统的用户请参考 官方文档 进行安装。

5. 注册 Docker Hub 账号并登录

为了便于发布 Docker 镜像供他人下载使用,并支持 Minikube 后续下载镜像,你需要在 Docker Hub 注册一个账号。注册完成后,通过以下命令登录你的 Docker Hub 账号:

docker login

完成以上步骤后,你的本地环境就配置好了,接下来可以开始实践 Kubernetes 的各种操作了!

动手练习

使用 Docker 构建镜像

创建并发布 Docker 镜像

在本节中,我们将动手创建一个简单的 Java 应用镜像,并将其发布到 DockerHub 仓库,以便后续在 Minikube 中下载和使用。

首先,新建一个名为 HelloKubernetes.java 的文件,并将以下代码复制到文件中:

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class HelloKubernetes {
    public static void main(String[] args) throws IOException {
        // 创建一个 HttpServer 实例,监听端口 3000
        HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);
        
        // 定义处理函数,当访问根路径时返回响应
        server.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange exchange) throws IOException {
                String response = "[v1] Hello, Kubernetes!";
                exchange.sendResponseHeaders(200, response.getBytes().length);
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        });

        // 启动服务器
        server.setExecutor(null); // 使用默认的执行器
        server.start();
    }
}

这段 Java 代码的功能非常简单:启动一个 HTTP 服务器,监听 3000 端口,并在访问根路径 / 时返回字符串 [v1] Hello, Kubernetes!

在传统环境下,运行这段代码需要先安装 JDK,然后编译并运行。而通过容器技术,你只需准备好这段代码和对应的 Dockerfile,即使你对 Java 不熟悉,也能顺利运行这段代码。

容器是一种基于 Linux 技术(如 Namespace、Cgroups、chroot 等)的沙盒环境。如果你想深入了解,可以参考这个 视频

准备 Dockerfile

接下来,我们为上述 Java 代码准备对应的 Dockerfile 文件。这个 Dockerfile 使用了多阶段构建的方式。第一个阶段是使用轻量级的 openjdk:8-jdk-alpine 镜像来编译 Java 代码。第二个阶段则使用 distroless 镜像来提高安全性和减小镜像大小。即使你不理解 Dockerfile 的内容,也不会影响后续的操作。

# 使用 OpenJDK 8 的 Alpine 作为构建阶段的基础镜像
FROM openjdk:8-jdk-alpine AS builder

# 创建工作目录 /src
WORKDIR /src

# 将当前目录下的所有文件复制到容器的 /src 目录中
COPY . .

# 编译 Java 源代码
RUN javac HelloKubernetes.java

# 使用 distroless 作为运行阶段的基础镜像,以提高安全性和减小镜像大小
FROM gcr.io/distroless/java-debian10

# 设置工作目录为根目录
WORKDIR /

# 从构建阶段复制所有编译好的字节码文件到运行阶段镜像中
COPY --from=builder /src/ /app/

# 设置类路径,并运行应用程序
ENTRYPOINT ["java", "-cp", "/app", "HelloKubernetes"]

# 暴露应用程序监听的端口
EXPOSE 3000

DockerfileHelloKubernetes.java 文件放在同一目录下,然后通过以下命令构建镜像。请注意,将其中的 tangrl177 替换为你自己的 DockerHub 账号名,以便后续将镜像推送到你的 DockerHub 仓库中。

docker build . -t tangrl177/hellok8s:v1

你可以使用 docker images 命令查看镜像是否构建成功。然后,通过以下命令启动容器,其中 -p 指定将容器的 3000 端口映射到主机,-d 表示在后台运行容器。

docker run -p 3000:3000 --name hellok8s -d tangrl177/hellok8s:v1

如果你使用的是 Mac 的 ARM 系统,请使用以下命令指定 amd64 平台来运行容器,以避免架构不匹配的问题:

docker run --platform linux/amd64 -p 3000:3000 --name hellok8s -d tangrl177/hellok8s:v1

容器启动成功后,你可以通过浏览器或 curl 命令访问 http://127.0.0.1:3000,验证是否成功返回字符串 [v1] Hello, Kubernetes!

最后,使用 docker push 命令将镜像上传到 DockerHub,这样你和其他人都可以方便地下载和使用该镜像。再次提醒,将 tangrl177 替换为你自己的 DockerHub 账号名:

docker push tangrl177/hellok8s:v1

通过以上步骤,你已经成功创建并发布了一个 Docker 镜像,为后续在 Kubernetes 中使用做好了准备。

使用 Pod 部署应用

Pod 和 Container 的区别

Pod 是 Kubernetes 中创建和管理的最小可部署单元,它与 Container 有着关键的区别。Pod 可以运行多个 Container,而 Container 的本质是进程,Pod 则是用于管理这些进程及其资源的单位。在某些场景下,例如服务之间需要进行文件交换(如日志收集),或者需要进行本地网络通信(通过 localhost 或 Socket 文件进行本地通信),Pod 的这种多容器管理方式非常有用。

Pod 相关练习 1

通过以下练习,我们将进一步理解 Pod 的概念。

首先,我们创建一个名为 nginx.yaml 的文件,编写用于运行 nginx 容器的 Pod 定义文件。文件内容如下所示,其中 kind 表示我们要创建的资源类型为 Podmetadata.name 用于指定 Pod 的名称,这个名称在集群中需要唯一。spec.containers 用于定义运行的容器名称及其使用的镜像。默认情况下,镜像来源于 DockerHub。

# nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
    - name: nginx-container
      image: nginx

接下来,运行以下命令来创建 nginx Pod:

# 创建 nginx Pod
kubectl apply -f nginx.yaml

使用 kubectl get pods 命令查看 Pod 是否成功启动:

# 查看 pod 是否正常启动
kubectl get pods

然后,使用以下命令将 nginx 容器的默认 80 端口映射到本机的 4000 端口,打开浏览器或使用 curl 命令访问 http://127.0.0.1:4000,验证是否成功访问到 nginx 的默认页面:

# 将 nginx 默认的 80 端口映射到本机的 4000 端口
kubectl port-forward nginx-pod 4000:80

你还可以使用 kubectl exec -it 命令进入 Pod 内部的容器 Shell,并修改 nginx 的首页内容。通过以下命令进行操作:

# 进入 Pod 容器的 Shell
kubectl exec -it nginx-pod /bin/bash
# 进入 Pod 容器的 Shell 后执行,修改 `nginx` 的首页内容
echo "hello kubernetes by nginx!" > /usr/share/nginx/html/index.html
# 退出 Pod 容器的 Shell 并重新映射端口
exit
kubectl port-forward nginx-pod 4000:80

然后再次访问 http://127.0.0.1:4000,确保 nginx 成功启动并返回自定义的字符串 hello kubernetes by nginx!

Pod 相关练习 2

接下来,我们使用之前在 Container 小节中构建的 hellok8s:v1 镜像,参考 nginx Pod 的资源定义文件,编写 hellok8s:v1 Pod 的资源定义文件。通过 port-forward 将 Pod 的端口转发到本地 3000 端口,最终你将会看到 [v1] Hello, Kubernetes! 的输出。

以下是 hellok8s:v1 Pod 的资源定义文件和相应的命令。请注意,tangrl177 应该替换为你自己的 DockerHub 账号名:

# hellok8s.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hellok8s
spec:
  containers:
    - name: hellok8s-container
      image: tangrl177/hellok8s:v1

运行以下命令来启动和访问 hellok8s

kubectl apply -f hellok8s.yaml
kubectl get pods
kubectl port-forward hellok8s 3000:3000
解决 Pod 启动失败

如果 Pod 的状态显示为 ErrImagePullImagePullBackOff,这可能是由于网络问题导致无法从 DockerHub 拉取镜像。此时,你可以运行以下命令来解决问题:

eval $(minikube docker-env)

然后重新构建镜像,而无需将其推送到 DockerHub。这样,构建的镜像可以直接被 Minikube 获取,避免了 DockerHub 拉取镜像的延迟或失败。

上述解决方法来源于官方文档

Pod 相关命令

以下是一些常用的 kubectl 命令:

# 查看 pod 日志
kubectl logs --follow nginx
# 进入 pod 内部容器的 Shell 或执行容器命令                              
kubectl exec nginx -- ls
# 删除 pod 资源
kubectl delete pod nginx
# 删除 pod 的配置文件
kubectl delete -f nginx.yaml

你可以参考 官方文档 获取 kubectl 支持的所有命令。

使用 Deployment 管理应用部署

在实际的生产环境中,我们通常不会直接管理 Pod,而是使用 Kubernetes 的 Deployment 资源来帮助管理 Pod。Deployment 可以执行自动扩容、自动升级等操作。例如,如果你已经部署了 10 个 hellok8s:v1 的 Pod,需要扩容到 20 个,或者需要将它们升级为 hellok8s:v2 版本,那么 Deployment 将是最佳选择。

自动扩容

在进行以下练习之前,建议通过 kubectl get pods 查看并删除上一节创建的 Pod。如果 Pod 是通过 Deployment 创建的,则需要使用 kubectl delete deployments xxx 删除相应的 Deployment 资源。

首先,创建一个 deployment.yaml 文件,用来管理 hellok8s Pod。其中,kind 表示资源类型为 Deploymentmetadata.name 定义了 Deployment 的名称,这个名称必须唯一

spec 中,replicas 指定 Pod 的副本数量,selector 定义了 Deployment 如何关联 Pod。在这个例子中,Deployment 会管理所有 labels=hellok8s 的 Pod。

template 用来定义 Pod 资源的结构。你会发现它与前面 Hellok8s Pod 资源的定义非常相似。唯一的区别是,我们在 template 中添加了 metadata.labels 来与 selector.matchLabels 对应,标明 Pod 是由这个 Deployment 管理的。Deployment 会为我们自动生成 Pod 的唯一 name,所以在 template 中无需手动定义 metadata.name

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hellok8s
  template:
    metadata:
      labels:
        app: hellok8s
    spec:
      containers:
        - image: guangzhengli/hellok8s:v1
          name: hellok8s-container

使用以下命令来创建 Deployment 资源。你可以通过 getdelete pod 命令来体验 Deployment 的功能。请注意,每次创建的 Pod 名称都会变化,因此某些命令需要替换为你当前 Pod 的名称。

# 创建 deployment
kubectl apply -f deployment.yaml
# 查询 deployment
kubectl get deployments
# 查询 pod 的信息
kubectl get pods             
# 删除某个 pod
kubectl delete pod hellok8s-deployment-xxx
# 获取新的 pod,你会发现删除一个 pod 后,deployment 会自动生成一个新的 pod
kubectl get pods                                       

你会发现,当你手动删除一个 Pod 资源后,Deployment 会自动创建一个新的 Pod。这与我们之前手动创建 Pod 资源的方式有很大的区别🤯!这意味着在生产环境中管理成千上万个 Pod 时,我们不需要逐个管理,只需维护好 deployment.yaml 文件的资源定义即可。

接下来,通过自动扩容来进一步理解 Deployment 的优势。当我们需要将 hellok8s:v1 的副本数量扩容到 3 个时,只需将 replicas 的值设置为 3,然后重新执行 kubectl apply -f deployment.yaml。如下所示:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hellok8s
  template:
    metadata:
      labels:
        app: hellok8s
    spec:
      containers:
        - image: guangzhengli/hellok8s:v1
          name: hellok8s-container

你可以在执行 kubectl apply 之前,新建一个命令行窗口并执行 kubectl get pods --watch,实时观察 Pod 启动和删除的过程。减少副本数的方法也很简单,只需将 replicas 的值减少即可。

升级版本

接下来,我们将所有 v1 版本的 Pod 升级到 v2 版本。首先,需要构建 hellok8s:v2 的镜像,唯一的区别是将响应字符串替换为 [v2] Hello, Kubernetes!

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class HelloKubernetes {
    public static void main(String[] args) throws IOException {
        // 创建一个 HttpServer 实例,监听端口 3000
        HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);
        
        // 定义处理函数,当访问根路径时返回响应
        server.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange exchange) throws IOException {
                String response = "[v2] Hello, Kubernetes!";
                exchange.sendResponseHeaders(200, response.getBytes().length);
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        });

        // 启动服务器
        server.setExecutor(null); // 使用默认的执行器
        server.start();
    }
}

然后,按照之前的步骤构建镜像并将其推送到 DockerHub 仓库中。记得将 tangrl177 替换为你自己的 DockerHub 账号名。

# 构建镜像
docker build . -t tangrl177/hellok8s:v2
# 将镜像推送到 DockerHub 仓库
docker push tangrl177/hellok8s:v2

接着,更新 deployment.yaml 文件,以使用 v2 版本的镜像。

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hellok8s
  template:
    metadata:
      labels:
        app: hellok8s
    spec:
      containers:
        - image: tangrl177/hellok8s:v2
          name: hellok8s-container

最后,执行以下命令更新配置,并在浏览器中打开 http://localhost:3000,查看输出是否已更新为 v2

# 更新配置
kubectl apply -f deployment.yaml
# 查询 pod 详情
kubectl get pods 
# 映射某个 pod 的端口,注意更换 pod 名称
kubectl port-forward hellok8s-deployment-xxx 3000:3000
滚动更新

在生产环境中,如果你需要管理多个 hellok8s:v1 副本并将其升级到 v2 版本,直接替换所有 Pod 的方式可能会带来问题。因为这会导致服务在升级过程中不可用——旧版本的 Pod 会被删除,而新版本的 Pod 尚未完全就绪。

这时,你可以使用滚动更新(Rolling Update)来逐步替换旧版本的 Pod,而不影响服务的可用性。在 Deployment 资源定义中,spec.strategy.type 有两种选择:

  • RollingUpdate: 逐步增加新版本的 Pod,同时逐步减少旧版本的 Pod。
  • Recreate: 在增加新版本的 Pod 之前,先删除所有旧版本的 Pod。

通常情况下,我们会选择滚动更新(RollingUpdate)。滚动更新可以通过 maxSurgemaxUnavailable 字段来控制升级 Pod 的速率,具体细节可以参考官方文档

  • maxSurge: 最大峰值,用来指定可以创建的超出期望 Pod 个数的 Pod 数量。
  • maxUnavailable: 最大不可用数量,用来指定更新过程中不可用的 Pod 的上限。

你可以使用以下命令回滚 Deployment。通过端口映射测试,验证回滚是否符合预期。

# 回滚 deployment
kubectl rollout undo deployment hellok8s-deployment
# 查询 pod 详情
kubectl get pods                                    
# 查询某个 pod 的信息,注意替换 pod 名称
kubectl describe pod hellok8s-deployment-xxx
# 输出应包含 Image: tangrl177/hellok8s:v1

除了上述命令,还可以使用 history 查看历史版本,并通过 --to-revision=2 回滚到指定版本。

kubectl rollout history deployment hellok8s-deployment
kubectl rollout undo deployment/hellok8s-deployment --to-revision=2

接下来,在 deployment.yaml 文件中设置 strategy=rollingUpdatemaxSurge=1maxUnavailable=1,并将 replicas 设置为 3。这意味着最大可能会创建 4 个 hellok8s Pod(replicas + maxSurge),最少会有 2 个 `hell

ok8s Pod 存活(replicas - maxUnavailable`)。

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s-deployment
spec:
  strategy:
     rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  replicas: 3
  selector:
    matchLabels:
      app: hellok8s
  template:
    metadata:
      labels:
        app: hellok8s
    spec:
      containers:
      - image: tangrl177/hellok8s:v2
        name: hellok8s-container
滚动更新示意图(图片来自Kubernetes练习手册)
存活探针

存活探针(Liveness Probe)用于检测容器是否需要重启。例如,探针可以探测到应用的死锁情况(应用程序在运行,但无法继续执行后续步骤)。重启此类容器有助于提高应用的可用性,尽管其中可能存在缺陷。

在生产环境中,某些 Bug 可能导致应用死锁或线程耗尽,最终使应用无法继续提供服务。如果没有手段自动监控和处理这种情况,问题可能会持续很长时间而不被发现。Kubelet 使用存活探针来确定何时重启容器。

接下来,我们编写一个 /health 接口来说明存活探针的工作原理。这个接口会在服务器启动后的 15 秒内正常返回 200 状态码,但在 15 秒后将一直返回 500 状态码。

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.time.Instant;

public class HelloKubernetes {
    public static void main(String[] args) throws IOException {
        // 记录服务器启动时间
        Instant started = Instant.now();

        // 创建一个 HttpServer 实例,监听端口 3000
        HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);

        // 定义处理函数,当访问根路径时返回响应
        server.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange exchange) throws IOException {
                String response = "[v2] Hello, Kubernetes!";
                exchange.sendResponseHeaders(200, response.getBytes().length);
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        });

        // 定义处理函数,当访问 /health 时返回健康检查结果
        server.createContext("/health", new HttpHandler() {
            @Override
            public void handle(HttpExchange exchange) throws IOException {
                Duration duration = Duration.between(started, Instant.now());
                String response;
                if (duration.getSeconds() > 15) {
                    response = "error: " + duration.getSeconds();
                    exchange.sendResponseHeaders(500, response.getBytes().length);
                } else {
                    response = "ok";
                    exchange.sendResponseHeaders(200, response.getBytes().length);
                }
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        });

        // 启动服务器
        server.setExecutor(null); // 使用默认的执行器
        server.start();
    }
}

Dockerfile 内容保持不变。构建镜像时,将 Tag 设置为 liveness,并推送到远程仓库。如果镜像拉取失败,请参考 Pod 小节中的 “解决 Pod 启动失败” 部分。

# 构建镜像
docker build . -t tangrl177/hellok8s:liveness
# 将镜像推送到 DockerHub
docker push tangrl177/hellok8s:liveness

最后,编写 Deployment 的定义文件。在这里,我们使用 HTTP GET 请求方式的存活探针,探测刚才定义的 /health 接口。periodSeconds 字段指定 Kubelet 每隔 3 秒执行一次存活探测。initialDelaySeconds 字段告诉 Kubelet 在执行第一次探测前应该等待 3 秒。如果 /health 路径返回成功代码,则 Kubelet 认为容器是健康的。如果返回失败代码,Kubelet 会重启容器。

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s-deployment
spec:
  strategy:
     rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  replicas: 3
  selector:
    matchLabels:
      app: hellok8s
  template:
    metadata:
      labels:
        app: hellok8s
    spec:
      containers:
        - image: tangrl177/hellok8s:liveness
          name: hellok8s-container
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 3
            periodSeconds: 3

使用 getdescribe 命令可以发现 Pod 一直处于重启状态。

# 回滚 deployment
kubectl rollout undo deployment hellok8s-deployment
# 查询 pod 详情
kubectl get pods                                    
# 查询某个 pod 的信息,注意替换 pod 名称
kubectl describe pod hellok8s-deployment-xxx
就绪探针

就绪探针(Readiness Probe)用于检测容器何时准备好接收请求流量。只有当一个 Pod 内的所有容器都就绪时,Pod 才会被视为就绪。这种信号的一个用途是控制哪些 Pod 作为 Service 的后端。如果 Pod 尚未就绪,它将从 Service 的负载均衡器中剔除。

在生产环境中,升级服务版本是日常需求。如果发布的版本存在问题,就不应该让它升级成功。Kubelet 使用就绪探针来检测容器是否准备好接受请求流量。如果 Pod 升级后不能就绪,则不应让流量进入该 Pod,并且配合 rollingUpdate 功能,防止不稳定版本的 Pod 升级。

在以下示例中,我们将 /health 接口设置为始终返回 500 状态码,模拟一个有问题的版本。

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class HelloKubernetes {
    public static void main(String[] args) throws IOException {
        // 记录服务器启动时间
        Instant started = Instant.now();

        // 创建一个 HttpServer 实例,监听端口 3000
        HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);

        // 定义处理函数,当访问根路径时返回响应
        server.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange exchange) throws IOException {
                String response = "[v2] Hello, Kubernetes!";
                exchange.sendResponseHeaders(200, response.getBytes().length);
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        });

        // 定义处理函数,当访问 /health 时返回健康检查结果
        server.createContext("/health", new HttpHandler() {
            @Override
            public void handle(HttpExchange exchange) throws IOException {
                String response = "error";
                exchange.sendResponseHeaders(500, response.getBytes().length);
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        });

        // 启动服务器
        server.setExecutor(null); // 使用默认的执行器
        server.start();
    }
}

Dockerfile 内容保持不变。构建镜像时,将 Tag 设置为 bad,并推送到远程仓库。如果镜像拉取失败,请参考 Pod 小节中的 “解决 Pod 启动失败” 部分。

# 构建镜像
docker build . -t tangrl177/hellok8s:bad
# 将镜像推送到 DockerHub
docker push tangrl177/hellok8s:bad

接着编写 Deployment 资源文件。Probe 提供了许多配置字段,可以用来精确控制就绪探针的行为:

  • initialDelaySeconds: 容器启动后等待多少秒后才启动存活和就绪探针,默认值为 0 秒,最小值为 0。
  • periodSeconds: 探测的时间间隔(单位为秒),默认值为 10 秒,最小值为 1。
  • timeoutSeconds: 探测超时时的等待时间,默认值为 1 秒,最小值为 1。
  • successThreshold: 探测失败后,视为成功的最小连续成功数,默认值为 1。对于存活和启动探针,这个值必须为 1,最小

值为 1。

  • failureThreshold: 探测失败时 Kubernetes 的重试次数。对于存活探针,放弃探测意味着重新启动容器;对于就绪探针,放弃探测意味着 Pod 会被标记为未就绪。默认值为 3,最小值为 1。
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s-deployment
spec:
  strategy:
     rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  replicas: 3
  selector:
    matchLabels:
      app: hellok8s
  template:
    metadata:
      labels:
        app: hellok8s
    spec:
      containers:
        - image: tangrl177/hellok8s:bad
          name: hellok8s-container
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 1
            successThreshold: 5

使用 get 命令可以发现两个 Pod 一直处于未就绪状态。使用 describe 命令可以看到失败原因是 Readiness probe failed: HTTP probe failed with status code: 500。由于设置了 maxUnavailable=1,确保剩余两个 v2 版本的 hellok8s Pod 仍能继续提供服务。

# 回滚 deployment
kubectl rollout undo deployment hellok8s-deployment
# 查询 pod 详情
kubectl get pods                                    
# 查询某个 pod 的信息,注意替换 pod 名称
kubectl describe pod hellok8s-deployment-xxx

使用 Service 管理应用访问和负载均衡

经过前面的练习,你可能会有以下疑问:

  • 当 Pod 未就绪(Ready)时,Kubernetes 为什么不会将流量重定向到该 Pod?这是如何实现的?
  • 之前通过 port-forward 将 Pod 的端口暴露到本地,既需要准确填写 Pod 的名称,还要面对 Deployment 重新创建新 Pod 时,Pod 名称和 IP 地址会随之变化的问题。如何保证访问地址的稳定性?
  • 如果使用 Deployment 部署了多个 Pod 副本,如何实现负载均衡?

为了解决这些问题,Kubernetes 提供了一种名为 Service 的资源。Service 为 Pod 提供了一个稳定的访问端点(Endpoint)。它位于 Pod 的前面,负责接收请求并将它们传递给后面的所有 Pod。一旦服务中的 Pod 集合发生变化,Endpoints 也会随之更新,确保请求总是能够重定向到最新的 Pod。

ClusterIP

Service 默认使用 ClusterIP 类型。在进一步练习之前,我们将之前的 hellok8s:v2 升级为 v3 版本,并添加返回当前服务所在主机名的功能。

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.net.InetAddress;

public class HelloKubernetes {

    private static String hostname;

    public static void main(String[] args) throws IOException {
        // 获取主机名
        try {
            hostname = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            e.printStackTrace();
            hostname = "unknown";
        }

        // 创建一个 HttpServer 实例,监听端口 3000
        HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);

        // 定义处理函数,当访问根路径时返回响应
        server.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange exchange) throws IOException {
                String response = String.format("[v3] Hello, Kubernetes!, From host: %s", hostname);
                exchange.sendResponseHeaders(200, response.getBytes().length);
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        });

        // 启动服务器
        server.setExecutor(null); // 使用默认的执行器
        server.start();
    }
}

保持 Dockerfile 不变,在构建镜像时将 Tag 设置为 v3,并推送到远程仓库。如果遇到镜像拉取问题,请参考 Pod 小节中的“解决 Pod 启动失败”部分。

# 构建镜像
docker build . -t tangrl177/hellok8s:v3
# push 镜像到 DockerHub
docker push tangrl177/hellok8s:v3

修改 Deployment,将 hellok8s 升级为 v3 版本。执行 kubectl apply -f deployment.yaml 更新 Deployment。

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hellok8s
  template:
    metadata:
      labels:
        app: hellok8s
    spec:
      containers:
        - image: tangrl177/hellok8s:v3
          name: hellok8s-container

接着,定义 Service 资源,使用 ClusterIP 类型。ClusterIP 通过集群内部 IP 暴露服务,当我们只需要让集群中运行的其他应用程序访问 Pod 时,可以使用这种类型的 Service。首先,创建一个 service-hellok8s-clusterip.yaml 文件。

# service-hellok8s-clusterip.yaml
apiVersion: v1
kind: Service
metadata:
  name: service-hellok8s-clusterip
spec:
  type: ClusterIP
  selector:
    app: hellok8s
  ports:
  - port: 3000
    targetPort: 3000

通过以下命令查看 Endpoints 信息。Selector 选中的 Pod 称为 Service 的 Endpoints,它维护着 Pod 的 IP 地址。只要服务中的 Pod 集合发生变化,Endpoints 就会被更新。使用 kubectl get pod -o wide 可以看到 3 个 Pod 的 IP 地址与 Endpoints 中的信息保持一致。你可以尝试增加或减少 Deployment 中 Pod 的副本数,观察 Endpoints 是否随之变化。

# 更新配置
kubectl apply -f service-hellok8s-clusterip.yaml
# 查看 Endpoints
kubectl get endpoints
# 查看 Pod 更多信息,可以看到 3 个 Pod 的 IP 地址与 Endpoints 保持一致
kubectl get pod -o wide
# 查看 Service 信息
kubectl get service

接着,我们可以通过集群内的其他应用程序访问 service-hellok8s-clusterip 的 IP 地址,从而访问 hellok8s:v3 服务。

你可以通过执行 kubectl apply -f deployment-nginx.yaml 在集群内创建一个 nginx 来访问 hellok8s 服务。然后通过执行 kubectl exec -it nginx-pod /bin/bash 进入 nginx 容器,并使用 curl 命令访问 service-hellok8s-clusterip

# deployment-nginx.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: nginx
spec:
  containers:
    - name: nginx-container
      image: nginx

在容器内多次执行 curl,你会发现返回的 hellok8s:v3 hostname 不同,这表明 Service 可以接收请求并将它们传递给后面的所有 Pod,同时自动实现负载均衡。调用过程如下图所示:

Service ClusterIP 负载均衡示意图(图片来自Kubernetes练习手册)

Kubernetes ServiceTypes 允许指定 Service 类型,默认是 ClusterIP。其他可用的 Type 包括:

  • ClusterIP:通过集群的内部 IP 暴露服务,仅能在集群内部访问。这是默认的 ServiceType
  • NodePort:通过每个节点的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到自动创建的 ClusterIP 服务。通过请求 <节点 IP>:<节点端口>,你可以从集群外部访问 NodePort 服务。
  • LoadBalancer:使用云提供商的负载均衡器向外部暴露服务。外部负载均衡器将流量路由到自动创建的 NodePortClusterIP 服务。
  • ExternalName:通过返回 CNAME 和对应值,将服务映射到 externalName 字段的内容(例如 foo.bar.example.com)。无需创建任何类型的代理。
NodePort

我们知道 Kubernetes 集群并不是单机运行,它管理着多台节点(Node)。NodePort 类型的 Service 通过每个节点上的 IP 和静态端口(NodePort)暴露服务。如下图所示,如果集群中有两台 Node 运行着 hellok8s:v3,我们可以创建一个 NodePort 类型的 Service,将 hellok8s:v33000 端口映射到 Node 机器的 30000 端口(端口范围为 30000-32767)。这样我们就可以通过访问 http://node1-ip:30000http://node2-ip:30000 来访问服务。

NodePort 服务架构图(图片来自Kubernetes练习手册)

minikube 为例,你可以通过 minikube ip 命令获取 k8s 集群节点的 IP 地址。下面的教程以我的本机 IP 192.168.49.2 为例,请替换成你自己的 IP 地址。

minikube ip
# 192.168.49.2

接着,我们创建一个 NodePort 类型的 Service 来接管 Pod 流量。通过 minikube 节点上的 IP 192.168.49.2 暴露服务。NodePort 服务会路由到自动创建的 ClusterIP 服务。通过请求 <节点IP>:<节点端口>——192.168.49.2:6000,你可以从集群外部访问 NodePort 服务,最终将请求重定向到 hellok8s:v33000 端口。

# service-hellok8s-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
  name: service-hellok8s-nodeport
spec:
  type: NodePort
  selector:
    app: hellok8s
  ports:
  - port: 3000
    nodePort: 6000

创建 service-hellok8s-nodeport Service 后,使用 curl 命令或浏览器访问 http://192.168.49.2:6000 可以得到结果。

kubectl apply -f service-hellok8s-nodeport.yaml
kubectl get service
kubectl get pods
curl http://192.168.49.2:6000
curl http://192.168.49.2:6000
curl http://192.168.49.2:6000

如果你使用的是 Docker Desktop(minikube start --driver=docker),可能无法通过 minikube ip 获取的 IP 地址访问服务,因为 Docker 的网络限制导致无法直接连接 Docker 容器。这意味着 NodePort 类型的 Service 和 Ingress 组件都无法通过 minikube ip 提供的 IP 地址访问。此时,可以通过 minikube service service-hellok8s-nodeport --url 来公开服务,然后使用 curl 或浏览器访问。

minikube service service-hellok8s-nodeport --url
# 输出 http://127.0.0.1:54724
# 因为你在 Windows 上使用 Docker driver,终端需要保持打开状态。
curl http://127.0.0.1:54724
# [v3] Hello, Kubernetes!, From host: hellok8s-deployment-559cfdd58c-zp2pc
curl http://127.0.0.1:54724
# [v3] Hello, Kubernetes!, From host: hellok8s-deployment-559cfdd58c-2j2x2
LoadBalancer

LoadBalancer 类型的 Service 使用云提供商的负载均衡器向外部暴露服务。外部负载均衡器将流量路由到自动创建的 NodePortClusterIP 服务。如果你在 AWSEKS 集群上创建一个 LoadBalancer 类型的 Service,Kubernetes 会自动创建一个 ELB(Elastic Load Balancer),并从配置的 IP 池中分配一个独立的 IP 地址供外部访问。

由于我们使用的是 minikube,可以通过 minikube tunnel 来模拟创建 LoadBalancer 的 EXTERNAL_IP。具体教程可以参考官网文档。不过,minikube 模拟的 LoadBalancer 与实际云提供商提供的 LoadBalancer 有本质区别。如果你有条件,可以在 AWSEKS 集群上创建一个 ELB 进行测试。

下图显示了 LoadBalancer 的 Service 架构。

LoadBalancer 服务架构图(图片来自Kubernetes练习手册)

使用 Ingress 管理外部流量路由

Ingress 资源用于公开从集群外部到集群内部 Service 的 HTTP 和 HTTPS 路由。流量的路由由 Ingress 资源上定义的规则控制。通过 Ingress,可以为 Service 提供外部可访问的 URL、负载均衡流量、SSL/TLS 支持以及基于名称的虚拟主机。不过,需要注意的是,你必须拥有一个 Ingress 控制器 才能使 Ingress 生效。仅创建 Ingress 资源本身不会产生任何效果。Ingress 控制器通常负责通过负载均衡器实现 Ingress,例如 minikube 默认使用 nginx-ingress,此外 minikube 还支持 Kong-Ingress

简单来说,Ingress 可以被视为服务的网关(Gateway),它是集群中所有流量的入口,并根据配置的路由规则,将流量重定向到后端的服务。

minikube 中,可以通过以下命令启用 Ingress 控制器功能,默认使用 nginx-ingress

minikube addons enable ingress

接下来删除之前创建的所有 PodDeploymentService 资源。

kubectl delete deployment,service --all

然后,按照之前的教程,重新创建 hellok8s:v3nginxDeploymentService 资源,Service 的类型设为 ClusterIP

hellok8s:v3 的端口映射为 3000:3000nginx 的端口映射为 4000:80,这些端口映射将在后续编写 Ingress 路由规则时使用。

# hellok8s.yaml
apiVersion: v1
kind: Service
metadata:
  name: service-hellok8s-clusterip
spec:
  type: ClusterIP
  selector:
    app: hellok8s
  ports:
  - port: 3000
    targetPort: 3000

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hellok8s-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hellok8s
  template:
    metadata:
      labels:
        app: hellok8s
    spec:
      containers:
        - image: guangzhengli/hellok8s:v3
          name: hellok8s-container
# nginx.yaml
apiVersion: v1
kind: Service
metadata:
  name: service-nginx-clusterip
spec:
  type: ClusterIP
  selector:
    app: nginx
  ports:
  - port: 4000
    targetPort: 80

---

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx-container
kubectl apply -f hellok8s.yaml 
kubectl apply -f nginx.yaml 
kubectl get pods   
kubectl get service

现在,集群中有 3 个 hellok8s:v3 的 Pod 和 2 个 nginx 的 Pod。hellok8s:v3 的端口映射为 3000:3000nginx 的端口映射为 4000:80。在此基础上,我们接下来将编写 Ingress 资源定义,其中 nginx.ingress.kubernetes.io/ssl-redirect: "false" 表示关闭 HTTPS 连接,仅使用 HTTP 连接。

我们将匹配前缀为 /hello 的路由规则,重定向到 hellok8s:v3 服务;匹配前缀为 / 的根路径,将流量重定向到 nginx 服务。

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-ingress
  annotations:
    # 该注解用于暂时关闭 nginx 的 https 重定向
    nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
  rules:
    - http:
        paths:
          - path: /hello
            pathType: Prefix
            backend:
              service:
                name: service-hellok8s-clusterip
                port:
                  number: 3000
          - path: /
            pathType: Prefix
            backend:
              service:
                name: service-nginx-clusterip
                port:
                  number: 4000
kubectl apply -f ingress.yaml
kubectl get ingress          
curl http://192.168.49.2/hello
curl http://192.168.49.2/

Service 类似,如果你使用的是 Docker Desktop(minikube start --driver=docker),可能无法通过 minikube ip 获取的 IP 地址访问服务。你可以先通过 minikube service list 查看服务列表,然后使用 minikube service ingress-nginx-controller -n ingress-nginx --url 来公开服务,再通过 curl 或浏览器访问。

minikube service list
minikube service ingress-nginx-controller -n ingress-nginx --url
# 输出如下
# http://127.0.0.1:55201	http
# http://127.0.0.1:55202	https
curl http://127.0.0.1:55201/hello
curl http://127.0.0.1:55201/

在上面的教程中,我们将所有流量发送到 Ingress 中,如下图所示:

Ingress 流量示意图(图片来自Kubernetes练习手册)

使用 Namespace 隔离不同环境的资源

在实际的开发过程中,我们经常需要为不同的环境设置独立的资源配置,例如 dev 环境供开发使用,test 环境供 QA 使用。这样可以保证各个环境的资源互相隔离,互不干扰。那么,Kubernetes 是否可以帮助我们在不同环境(如 devtestuatprod)中区分资源,使得它们独立存在呢?答案是肯定的,Kubernetes 提供了一种名为 Namespace 的机制来帮助实现资源隔离。

在 Kubernetes 中,命名空间(Namespace) 是一种将同一集群中的资源划分为相互隔离的组的机制。同一个命名空间内的资源名称必须唯一,但跨命名空间时则没有这个限制。命名空间主要作用于带有命名空间属性的对象,例如 Deployment、Service 等。

在之前的教程中,我们默认使用的是 default 命名空间。接下来,我们将展示如何创建新的命名空间,以及如何在这些命名空间下管理资源。

首先,以下是一个定义了两个不同命名空间(devtest)的 namespace.yaml 文件:

# namespaces.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: dev
  
---

apiVersion: v1
kind: Namespace
metadata:
  name: test

你可以通过以下命令创建这两个新的命名空间:

kubectl apply -f namespaces.yaml  
kubectl get namespaces 

创建完成后,我们就有了 devtest 两个独立的命名空间。那么如何在这些命名空间下创建资源并获取资源呢?只需要在命令后面加上 -n <namespace> 参数即可。例如,我们可以在 dev 命名空间下创建 hellok8s:v3 的 Deployment 资源:

kubectl apply -f deployment.yaml -n dev
kubectl get pods -n dev

通过这种方式,Kubernetes 的 Namespace 能够有效地帮助我们在不同环境中隔离资源,使得 devtestuatprod 等环境的资源能够独立管理,互不影响。

使用 ConfigMap 管理环境配置

在前面的内容中我们提到过,在不同的环境(如 devtestuatprod)中区分资源,可以使各环境的资源相互独立,互不干扰。然而,这也带来了一些新的挑战。例如,不同环境的数据库地址通常是不一样的,如果在代码中写死同一个数据库地址,就会导致问题。

为了解决这个问题,Kubernetes 提供了 ConfigMap,将配置信息与应用程序代码分离。ConfigMap 用于保存非机密性的键值对数据,方便在不同环境中灵活配置。例如,不同环境的数据库地址可以通过 ConfigMap 来管理。需要注意的是,ConfigMap 设计上不是用来保存大量数据的,它保存的数据不能超过 1 MiB。如果需要保存更大的数据量,可以考虑使用存储卷(Volume)。

下面我们通过一个例子来演示如何使用 ConfigMap 假设不同环境的数据库地址不同。我们将修改之前的代码,让它从环境变量中获取 DB_URL 并返回该值。

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.net.InetAddress;

public class HelloKubernetes {

    private static String hostname;
    private static String dbURL;

    public static void main(String[] args) throws IOException {
        // 获取主机名
        try {
            hostname = InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            e.printStackTrace();
            hostname = "unknown";
        }

        // 获取环境变量中的数据库连接 URL
        dbURL = System.getenv("DB_URL");
        if (dbURL == null) {
            dbURL = "not set";
        }

        // 创建一个 HttpServer 实例,监听端口 3000
        HttpServer server = HttpServer.create(new InetSocketAddress(3000), 0);

        // 定义处理函数,当访问根路径时返回响应
        server.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange exchange) throws IOException {
                String response = String.format("[v4] Hello, Kubernetes! From host: %s, Get Database Connect URL: %s", hostname, dbURL);
                exchange.sendResponseHeaders(200, response.getBytes().length);
                OutputStream os = exchange.getResponseBody();
                os.write(response.getBytes());
                os.close();
            }
        });

        // 启动服务器
        server.setExecutor(null); // 使用默认的执行器
        server.start();
    }
}

在构建镜像时,保持 Dockerfile 不变,将 tag 设置为 v4,并推送到远程仓库。如果遇到镜像拉取问题,请参考 Pod 小节中的“解决 Pod 启动失败”部分。同时,删除之前所有的资源。

# 构建镜像
docker build . -t tangrl177/hellok8s:v4
#  push 到远程仓库
docker push tangrl177/hellok8s:v4
# 删除之前所有资源
kubectl delete pod,deployment,service,ingress --all

接下来,我们在不同的 namespace 中创建 ConfigMap 来存放 DB_URL

首先,创建 hellok8s-config-dev.yaml 文件:

# hellok8s-config-dev.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: hellok8s-config
data:
  DB_URL: "http://DB_ADDRESS_DEV"

然后,创建 hellok8s-config-test.yaml 文件:

# hellok8s-config-test.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: hellok8s-config
data:
  DB_URL: "http://DB_ADDRESS_TEST"

分别在上一节创建的 devtest 两个 namespace 中创建相同名称的 ConfigMap,名称都叫 hellok8s-config,但存放的键值对中的值不同。

# 在 dev namespace 中创建 ConfigMap
kubectl apply -f hellok8s-config-dev.yaml -n dev
# 在 test namespace 中创建 ConfigMap
kubectl apply -f hellok8s-config-test.yaml -n test 
# 获取所有命名空间的 ConfigMap 信息
kubectl get configmap --all-namespaces

接下来,使用 Pod 部署 hellok8s:v4,并通过 ConfigMap 配置环境变量。env.name 表示将 ConfigMap 中的值写入环境变量,使代码能够从环境变量中获取 DB_URLvalueFrom 表示从哪里读取配置,这里使用 configMapKeyRef 表示从名为 hellok8s-config 的 ConfigMap 中读取 DB_URL 的值。

# hellok8s.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hellok8s-pod
spec:
  containers:
    - name: hellok8s-container
      image: tangrl177/hellok8s:v4
      env:
        - name: DB_URL
          valueFrom:
            configMapKeyRef:
              name: hellok8s-config
              key: DB_URL

最后,在 devtest 两个 namespace 中分别创建 hellok8s:v4 Pod。然后,通过 port-forward 的方式访问不同 namespace 的服务,你会发现返回的 Get Database Connect URL 对应不同的环境值,例如 http://DB_ADDRESS_DEVhttp://DB_ADDRESS_TEST

kubectl apply -f hellok8s.yaml -n dev             
kubectl apply -f hellok8s.yaml -n test
kubectl port-forward hellok8s-pod 3000:3000 -n dev
curl http://localhost:3000
kubectl port-forward hellok8s-pod 3000:3000 -n test
curl http://localhost:3000

通过这种方式,Kubernetes 的 ConfigMap 可以帮助我们轻松管理不同环境下的配置,确保在不同环境中部署时,能够正确使用相应的数据库地址或其他配置信息。

使用 Secret 管理敏感信息

之前我们提到,通常会选择使用 ConfigMap 来挂载配置信息,但当我们的配置信息需要加密时,ConfigMap 就无法满足这个要求。例如,当需要挂载数据库密码时,ConfigMap 只能以明文方式进行挂载,这就带来了安全隐患。

为了解决这一问题,Kubernetes 提供了 Secret 来存储加密信息。虽然 Secret 在资源文件中仅通过 Base64 方式进行了简单编码,但在实际生产过程中,可以结合 CI/CD pipeline 或专业的密钥管理服务(如 AWS KMS)来进一步保护敏感数据,从而大大降低安全风险。

Secret 是一种包含少量敏感信息(如密码、令牌或密钥)的对象。由于 Secret 的创建可以独立于使用它们的 Pod,因此在创建、查看和编辑 Pod 的过程中,暴露 Secret(及其数据)的风险较小。Kubernetes 和集群中的应用程序还可以采取额外的预防措施,例如避免将机密数据写入非易失性存储。

然而,默认情况下,Kubernetes 的 Secret 是未加密地存储在 API 服务器的底层数据存储(etcd)中的。这意味着任何拥有 API 访问权限的人都可以检索或修改 Secret,任何有权访问 etcd 的人也可以如此。此外,任何有权限在命名空间中创建 Pod 的人也能够间接获取该命名空间中的 Secret 内容。因此,为了安全地使用 Secret,请至少执行以下步骤:

  1. 为 Secret 启用静态加密
  2. 启用或配置 RBAC 规则以限制读取和写入 Secret 数据的权限(包括间接方式)。注意,具有创建 Pod 权限的人也隐式获得了获取 Secret 内容的权限。
  3. 根据需要,可以使用 RBAC 等机制限制哪些主体能够创建新 Secret 或替换现有 Secret。

Secret 的资源定义与 ConfigMap 结构基本一致,区别在于 kindSecret,且 Value 需要使用 Base64 编码。不过,Secret 也提供了 stringData 字段,可以直接存储未编码的值,系统会自动处理 Base64 编码。

以下是如何使用 Base64 进行编码和解码的命令示例:

echo "password" | base64
# cGFzc3dvcmQK
echo "cGFzc3dvcmQK" | base64 -d
# password

将 Base64 编码后的值填入对应的 key-value 对中,如下所示:

# hellok8s-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: hellok8s-secret
data:
  DB_PASSWORD: "cGFzc3dvcmQK"

然后,在 Pod 的定义中,使用 Secret 来设置环境变量:

# hellok8s.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hellok8s-pod
spec:
  containers:
    - name: hellok8s-container
      image: tangrl177/hellok8s:v5
      env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: hellok8s-secret
              key: DB_PASSWORD

在代码中,可以通过读取 DB_PASSWORD 环境变量来获取并返回对应的字符串。Secret 的使用方法与 ConfigMap 基本一致,因此在此不再赘述。

按照以下步骤操作即可:

# 构建镜像并推送到远程仓库
docker build . -t tangrl177/hellok8s:v5
docker push tangrl177/hellok8s:v5
# 删除之前所有资源
kubectl delete pod,deployment,service,ingress --all
# 创建 Secret 和 Pod
kubectl apply -f hellok8s-secret.yaml
kubectl apply -f hellok8s.yaml
# 通过 port-forward 访问服务
kubectl port-forward hellok8s-pod 3000:3000
curl http://localhost:3000

通过这种方式,Kubernetes Secret 可以帮助我们安全地管理敏感信息,例如数据库密码,从而避免将这些信息以明文形式暴露在配置文件中。

使用 Job 和 CronJob 处理一次性任务

在实际的开发过程中,有一类任务并不需要长期运行,它们只需要执行一次即可完成,例如一些计算任务。这类任务不适合用 Deployment 或 Pod 来管理,而是应该使用 Job 来处理。

Job 会创建一个或多个 Pod,并且在指定数量的 Pod 成功终止之前会继续重试 Pod 的执行。Job 会跟踪成功完成的 Pod 数量,当这个数量达到指定的阈值时,Job 就会标记为完成。删除 Job 会清除它创建的所有 Pod,而挂起 Job 会删除其所有活跃的 Pod,直到 Job 恢复执行。

一种常见的使用场景是,你可以创建一个 Job 对象来确保 Pod 可靠地执行任务直到完成。如果第一个 Pod 失败或被删除(例如因为节点硬件故障或重启),Job 对象会启动一个新的 Pod 来继续执行任务。

以下是一个 Job 的资源定义示例:

# hello-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: hello-job
spec:
  parallelism: 3
  completions: 5
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: echo
          image: busybox
          command:
            - "/bin/sh"
          args:
            - "-c"
            - "for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done"

在这个示例中,parallelism 指定了最大并发执行的 Pod 数量为 3,completions 指定了总共需要成功执行的 Pod 数量为 5。也就是说,会同时启动 3 个 Pod 来执行任务,一旦某个 Pod 执行完成,会创建新的 Pod 继续执行,直到成功执行的 Pod 数量达到 5 为止。restartPolicy: OnFailure 表示如果 Pod 执行失败,会在同一节点重新启动该 Pod。

通过以下命令可以创建 Job,并使用 kubectl get pods -w 来观察 Job 创建 Pod 的过程和结果。最后,可以通过 kubectl logs 命令查看 Pod 的日志输出。

kubectl delete pod,deployment,service,ingress --all
kubectl apply -f hello-job.yaml
kubectl get jobs
kubectl get pods               
kubectl logs -f hello-job--xxx

Job 完成时,不会再创建新的 Pod,但已有的 Pod 通常也不会被自动删除。保留这些 Pod 可以帮助你查看日志输出,以便检查是否有错误、警告或其他诊断性信息。如果你需要删除 Job,可以使用 kubectl delete -f hello-job.yaml,这样会连同它创建的所有 Pod 一并删除。

CronJob

CronJob 是另一种 Kubernetes 资源,适用于定时任务,它能够基于 Cron 表达式调度周期性 Jobs

CronJob 适合执行周期性任务,例如备份或报告生成。这些任务应该配置为定期重复执行(例如每天、每周或每月一次)。每次任务执行完毕后,Pod 会被删除,新的 Pod 会在下一个周期创建并执行任务。

Cron 时间表的语法如下所示:

# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 月的某天 (1 - 31)
# │ │ │ ┌───────────── 月份 (1 - 12)
# │ │ │ │ ┌───────────── 周的某天 (0 - 6)(周日到周六;在某些系统上,7 也是星期日)
# │ │ │ │ │                          或者是 sun,mon,tue,wed,thu,fri,sat
# │ │ │ │ │
# │ │ │ │ │
# * * * * *

除了需要指定 Cron 表达式外,CronJob 的使用方式与 Job 基本一致。

# hello-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello-cronjob
spec:
  schedule: "* * * * *" # 每分钟执行一次
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: echo
              image: busybox
              command:
                - "/bin/sh"
              args:
                - "-c"
                - "for i in 9 8 7 6 5 4 3 2 1 ; do echo $i ; done"

使用 CronJob 的命令与 Job 基本相同,以下命令可以用于创建和管理 CronJob:

kubectl delete job,pod,deployment,service,ingress --all
kubectl apply -f hello-cronjob.yaml
kubectl get cronjob                
kubectl get pods   

通过这些方法,Kubernetes 的 Job 和 CronJob 可以帮助你轻松管理一次性任务和定时任务,使得任务的执行更加自动化和可靠。

总结

本文带大家通过动手去操作 K8s 中常用的资源,相信已经揭开了 K8s 神秘的面纱,大大加深了你对 K8s 的理解。如果你不是运维的话,上面介绍的内容,在日常工作中往往就够用了。K8s 还有更多的资源和概念,如果想更深入学习可以去看看官方文档。

参考

Kubernetes 练习手册

minikube

Kubernetes 指南

Kubernetes 官方文档

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐