第9章:K8s 核心概念(下)- 服务发现与配置
在上一章,我们的 my-app 应用已经通过 Deployment 在 K8s 中稳定运行了。但它就像一座“孤岛”:
- 对外:集群外部的用户无法访问它。
- 对内:如果集群内有其他服务(比如一个前端应用)想要调用它,也很困难。
为什么困难?因为 Pod 的 IP 地址是动态的、不固定的。每当 Deployment 进行更新或 Pod 发生故障被重建时,新 Pod 的 IP 都会改变。我们不能依赖这个不稳定的 IP 地址。
K8s 用一个绝妙的资源解决了这个问题:Service。
9.1 Service:让你的应用被“看见”
Service 是 K8s 网络世界的核心。你可以把它想象成一个应用的 “固定门牌号” 或 “内部总机号码”。
- 核心职责:为一个或多个功能相同的 Pod,提供一个稳定的、统一的访问入口。
- 工作原理:
- 你创建一个 Service,并为它定义一个选择器 (Selector),这个选择器会“圈出”所有带有特定标签 (Label) 的 Pod。
- Service 会获得一个虚拟的、固定的 IP 地址(称为 ClusterIP)和一个稳定的 DNS 名称。
- 当有流量访问这个 Service 的地址时,Service 会自动地将流量负载均衡到它背后所代理的、健康的 Pod 上。
这个过程对客户端是完全透明的。客户端(无论是集群内的其他 Pod 还是外部用户)只需要知道 Service 的固定地址,而完全不需要关心后端 Pod 的数量、IP 地址或健康状况。
Service 的几种类型
根据你想如何暴露应用,Service 提供了几种不同的 type:
-
ClusterIP
- 这是默认类型。
- Service 只会获得一个集群内部的 IP 地址。
- 用途:仅用于集群内部服务之间的通信。比如,一个 Web 后端服务需要被前端服务调用,但不需要暴露给外部用户。
-
NodePort
- 在 ClusterIP 的基础上,K8s 会在每一个 Node 节点上,都开放一个相同的、固定的端口(范围通常是 30000-32767)。
- 用途:用于将服务对外暴露。你可以通过访问
http://<任何一个Node节点的IP>:<NodePort>来访问你的服务。这在开发和测试环境中非常方便。
-
LoadBalancer
- 这是 NodePort 的增强版。
- 用途:在公有云环境(如 AWS, GCP, Azure)下,当你创建一个 LoadBalancer 类型的 Service 时,云服务商会自动为你创建一个外部负载均衡器,并将其绑定到这个 Service。这是在生产环境中暴露服务的标准方式。
9.2 ConfigMap & Secret:将配置与代码解耦
一个健壮的应用,应该将配置与代码分离。我们不应该把数据库地址、API 密钥等信息硬编码在 Docker 镜像里。
K8s 提供了两种专门的资源来管理配置信息:
-
ConfigMap
- 用途:用于存储非敏感的、键值对形式的配置数据。比如,应用的日志级别、功能开关、主题名称等。
-
Secret
- 用途:专门用于存储敏感信息,如数据库密码、API Token、TLS 证书等。
- 特点:Secret 中的数据是以 Base64 编码的形式存储的(注意:编码不是加密!),但 K8s 会以更安全的方式来管理和分发它。
创建好 ConfigMap 或 Secret 后,我们可以通过两种主要方式将它们“注入”到 Pod 中:
- 作为环境变量:将键值对直接注入为容器的环境变量。
- 作为数据卷挂载:将键值对挂载成文件,让容器内的应用去读取。
9.3 [实战] 为我们的应用创建 Service,并从外部访问
是时候为我们的 my-app-deployment 打开通往世界的大门了。
第一步:创建 Service YAML 文件
在 my-app 目录下,创建一个 service.yaml 文件:
# API 版本
apiVersion: v1
# 资源类型为 Service
kind: Service
# 元数据
metadata:
name: my-app-service
# 规格
spec:
# Service 类型,我们用 NodePort 来从外部访问
type: NodePort
# 选择器,告诉 Service 要代理哪些 Pod
# 这里的标签必须和 Deployment 中 Pod 模板的标签完全一致
selector:
app: my-app
# 端口定义
ports:
- protocol: TCP
# Service 自身监听的端口
port: 80
# 流量要转发到 Pod 的哪个端口
targetPort: 8080
# 在 Node 节点上暴露的端口
# 如果不指定,K8s 会自动分配一个
# nodePort: 30007
第二步:应用配置
kubectl apply -f service.yaml
验证 Pod 状态
在访问之前,确保你的 Pod 真的在运行:
# 查看 Pod 状态
kubectl get pods
# 查看 Pod 详细信息
kubectl describe pods
# 检查 Service 是否匹配到了 Pod
kubectl get endpoints my-app-service
如果 kubectl get endpoints my-app-service 显示为空,说明 Service 没有找到匹配的 Pod,请检查 Deployment 的标签是否与 Service 的选择器一致。
第三步:查看 Service 并访问
# 查看 Service
kubectl get service my-app-service
# 或者简写
kubectl get svc
你会看到类似这样的输出,记下 PORT(S) 那一列中 80 后面映射的 NodePort 端口号(比如 31006)。
| NAME | TYPE | CLUSTER-IP | EXTERNAL-IP | PORT(S) | AGE |
|---|---|---|---|---|---|
| my-app-service | NodePort | 10.107.33.38 | 80:31006/TCP | 8s |
重要:访问 Minikube 中的服务
由于 Minikube 运行在虚拟机中,直接通过 NodePort 访问可能会遇到网络问题(我在 macOS + Docker Desktop 环境就遇到了)。我们有几种方式来访问服务:
方法 1:使用 minikube service 命令(推荐)
# 自动打开浏览器并处理端口转发
minikube service my-app-service
运行结果:
mac@192 docker-k8s-little-book-source-code % minikube service my-app-service
|-----------|----------------|-------------|---------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|----------------|-------------|---------------------------|
| default | my-app-service | 80 | http://192.168.49.2:31006 |
|-----------|----------------|-------------|---------------------------|
🏃 为服务 my-app-service 启动隧道。
|-----------|----------------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|----------------|-------------|------------------------|
| default | my-app-service | | http://127.0.0.1:53952 |
|-----------|----------------|-------------|------------------------|
🎉 正通过默认浏览器打开服务 default/my-app-service...
❗ 因为你正在使用 darwin 上的 Docker 驱动程序,所以需要打开终端才能运行它。
访问http://127.0.0.1:53952(注意:这个每次运行的时候端口号会不一样),就会看到我们的示例出来了
方法 2:使用端口转发(最可靠)
# 将 Service 端口转发到本地 8080 端口
kubectl port-forward service/my-app-service 8080:80
# 然后在浏览器访问 http://localhost:8080
这里因为指定的8080端口,就不会每次不一样了
方法 3:直接访问(某些环境可行)
获取 Minikube 节点的 IP 地址:
minikube ip
假设得到的 IP 是 192.168.49.2,NodePort 是 31006。现在,打开浏览器,访问 http://192.168.49.2:31006。
📋 适合方法 3 minikube ip 方案的情况:
- Linux 系统 + 裸机 Minikube (不是 Docker Desktop)
- Windows 系统 + Hyper-V 驱动
- macOS + VirtualBox/Parallels 驱动 (不是 Docker Desktop)
- 云环境 中的 Minikube ❌ 不适合的情况:
- macOS + Docker Desktop
- Windows + Docker Desktop
成功了! 你已经通过 NodePort 访问到了 K8s 集群内部的 Pod。
9.4 [实战] 使用 ConfigMap 注入环境变量
我们来修改一下应用,让它返回的问候语可以通过环境变量来配置。
第一步:创建 ConfigMap
创建一个 configmap.yaml 文件:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
data:
# 定义我们的键值对配置
GREETING_MESSAGE: "Hello from ConfigMap!"
应用它:kubectl apply -f configmap.yaml
第二步:修改 Deployment
修改 deployment.yaml(示例代码中我们创建一个 deployment.1.3.yaml),让它从 ConfigMap 中读取数据并注入为环境变量。
# ...
spec:
template:
# ...
spec:
containers:
- name: my-app-container
image: my-app:1.3
ports:
- containerPort: 8080
# 新增 env 部分
env:
- name: GREETING # 在容器内定义的环境变量名
valueFrom:
configMapKeyRef:
# 引用哪个 ConfigMap
name: my-app-config
# 引用那个 Key
key: GREETING_MESSAGE
第三步:修改应用代码并部署
修改 app.js 来读取这个环境变量:
(示例代码中我们创建一个 app.1.3.js)
// ...
const GREETING = process.env.GREETING || 'Hello, default world!';
app.get('/', (req, res) => {
res.send(GREETING);
});
// ...
pnpm build:minikube:1.3构建新镜像 (my-app:1.3),并重新 kubectl apply -f deployment.yaml。
eval $(minikube docker-env) && docker build -f Dockerfile.1.3 -t my-app:1.3 ./
kubectl apply -f deployment.1.3.yaml
当滚动更新完成后,再次访问你的 NodePort 地址,你会发现页面返回的信息已经变成了 "Hello from ConfigMap!"。我们成功地将配置从代码中解耦了!
# 第9章:K8s 核心概念(下)- 服务发现与配置
在上一章,我们的 my-app 应用已经通过 Deployment 在 K8s 中稳定运行了。但它就像一座“孤岛”:
- 对外:集群外部的用户无法访问它。
- 对内:如果集群内有其他服务(比如一个前端应用)想要调用它,也很困难。
为什么困难?因为 Pod 的 IP 地址是动态的、不固定的。每当 Deployment 进行更新或 Pod 发生故障被重建时,新 Pod 的 IP 都会改变。我们不能依赖这个不稳定的 IP 地址。
K8s 用一个绝妙的资源解决了这个问题:Service。
9.1 Service:让你的应用被“看见”
Service 是 K8s 网络世界的核心。你可以把它想象成一个应用的 “固定门牌号” 或 “内部总机号码”。
- 核心职责:为一个或多个功能相同的 Pod,提供一个稳定的、统一的访问入口。
- 工作原理:
- 你创建一个 Service,并为它定义一个选择器 (Selector),这个选择器会“圈出”所有带有特定标签 (Label) 的 Pod。
- Service 会获得一个虚拟的、固定的 IP 地址(称为 ClusterIP)和一个稳定的 DNS 名称。
- 当有流量访问这个 Service 的地址时,Service 会自动地将流量负载均衡到它背后所代理的、健康的 Pod 上。
这个过程对客户端是完全透明的。客户端(无论是集群内的其他 Pod 还是外部用户)只需要知道 Service 的固定地址,而完全不需要关心后端 Pod 的数量、IP 地址或健康状况。
Service 的几种类型
根据你想如何暴露应用,Service 提供了几种不同的 type:
-
ClusterIP
- 这是默认类型。
- Service 只会获得一个集群内部的 IP 地址。
- 用途:仅用于集群内部服务之间的通信。比如,一个 Web 后端服务需要被前端服务调用,但不需要暴露给外部用户。
-
NodePort
- 在 ClusterIP 的基础上,K8s 会在每一个 Node 节点上,都开放一个相同的、固定的端口(范围通常是 30000-32767)。
- 用途:用于将服务对外暴露。你可以通过访问
http://<任何一个Node节点的IP>:<NodePort>来访问你的服务。这在开发和测试环境中非常方便。
-
LoadBalancer
- 这是 NodePort 的增强版。
- 用途:在公有云环境(如 AWS, GCP, Azure)下,当你创建一个 LoadBalancer 类型的 Service 时,云服务商会自动为你创建一个外部负载均衡器,并将其绑定到这个 Service。这是在生产环境中暴露服务的标准方式。
9.2 ConfigMap & Secret:将配置与代码解耦
一个健壮的应用,应该将配置与代码分离。我们不应该把数据库地址、API 密钥等信息硬编码在 Docker 镜像里。
K8s 提供了两种专门的资源来管理配置信息:
-
ConfigMap
- 用途:用于存储非敏感的、键值对形式的配置数据。比如,应用的日志级别、功能开关、主题名称等。
-
Secret
- 用途:专门用于存储敏感信息,如数据库密码、API Token、TLS 证书等。
- 特点:Secret 中的数据是以 Base64 编码的形式存储的(注意:编码不是加密!),但 K8s 会以更安全的方式来管理和分发它。
创建好 ConfigMap 或 Secret 后,我们可以通过两种主要方式将它们“注入”到 Pod 中:
- 作为环境变量:将键值对直接注入为容器的环境变量。
- 作为数据卷挂载:将键值对挂载成文件,让容器内的应用去读取。
9.3 [实战] 为我们的应用创建 Service,并从外部访问
是时候为我们的 my-app-deployment 打开通往世界的大门了。
第一步:创建 Service YAML 文件
在 my-app 目录下,创建一个 service.yaml 文件:
# API 版本
apiVersion: v1
# 资源类型为 Service
kind: Service
# 元数据
metadata:
name: my-app-service
# 规格
spec:
# Service 类型,我们用 NodePort 来从外部访问
type: NodePort
# 选择器,告诉 Service 要代理哪些 Pod
# 这里的标签必须和 Deployment 中 Pod 模板的标签完全一致
selector:
app: my-app
# 端口定义
ports:
- protocol: TCP
# Service 自身监听的端口
port: 80
# 流量要转发到 Pod 的哪个端口
targetPort: 8080
# 在 Node 节点上暴露的端口
# 如果不指定,K8s 会自动分配一个
# nodePort: 30007
第二步:应用配置
kubectl apply -f service.yaml
验证 Pod 状态
在访问之前,确保你的 Pod 真的在运行:
# 查看 Pod 状态
kubectl get pods
# 查看 Pod 详细信息
kubectl describe pods
# 检查 Service 是否匹配到了 Pod
kubectl get endpoints my-app-service
如果 kubectl get endpoints my-app-service 显示为空,说明 Service 没有找到匹配的 Pod,请检查 Deployment 的标签是否与 Service 的选择器一致。
第三步:查看 Service 并访问
# 查看 Service
kubectl get service my-app-service
# 或者简写
kubectl get svc
你会看到类似这样的输出,记下 PORT(S) 那一列中 80 后面映射的 NodePort 端口号(比如 31006)。
| NAME | TYPE | CLUSTER-IP | EXTERNAL-IP | PORT(S) | AGE |
|---|---|---|---|---|---|
| my-app-service | NodePort | 10.107.33.38 | 80:31006/TCP | 8s |
重要:访问 Minikube 中的服务
由于 Minikube 运行在虚拟机中,直接通过 NodePort 访问可能会遇到网络问题(我在 macOS + Docker Desktop 环境就遇到了)。我们有几种方式来访问服务:
方法 1:使用 minikube service 命令(推荐)
# 自动打开浏览器并处理端口转发
minikube service my-app-service
运行结果:
mac@192 docker-k8s-little-book-source-code % minikube service my-app-service
|-----------|----------------|-------------|---------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|----------------|-------------|---------------------------|
| default | my-app-service | 80 | http://192.168.49.2:31006 |
|-----------|----------------|-------------|---------------------------|
🏃 为服务 my-app-service 启动隧道。
|-----------|----------------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|----------------|-------------|------------------------|
| default | my-app-service | | http://127.0.0.1:53952 |
|-----------|----------------|-------------|------------------------|
🎉 正通过默认浏览器打开服务 default/my-app-service...
❗ 因为你正在使用 darwin 上的 Docker 驱动程序,所以需要打开终端才能运行它。
访问http://127.0.0.1:53952(注意:这个每次运行的时候端口号会不一样),就会看到我们的示例出来了
方法 2:使用端口转发(最可靠)
# 将 Service 端口转发到本地 8080 端口
kubectl port-forward service/my-app-service 8080:80
# 然后在浏览器访问 http://localhost:8080
这里因为指定的8080端口,就不会每次不一样了
方法 3:直接访问(某些环境可行)
获取 Minikube 节点的 IP 地址:
minikube ip
假设得到的 IP 是 192.168.49.2,NodePort 是 31006。现在,打开浏览器,访问 http://192.168.49.2:31006。
📋 适合方法 3 minikube ip 方案的情况:
- Linux 系统 + 裸机 Minikube (不是 Docker Desktop)
- Windows 系统 + Hyper-V 驱动
- macOS + VirtualBox/Parallels 驱动 (不是 Docker Desktop)
- 云环境 中的 Minikube
❌ 不适合的情况:
- macOS + Docker Desktop
- Windows + Docker Desktop
成功了! 你已经通过 NodePort 访问到了 K8s 集群内部的 Pod。
9.4 [实战] 使用 ConfigMap 注入环境变量
我们来修改一下应用,让它返回的问候语可以通过环境变量来配置。
第一步:创建 ConfigMap
创建一个 configmap.yaml 文件:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
data:
# 定义我们的键值对配置
GREETING_MESSAGE: "Hello from ConfigMap!"
应用它:kubectl apply -f configmap.yaml
第二步:修改 Deployment
修改 deployment.yaml(示例代码中我们创建一个 deployment.1.3.yaml),让它从 ConfigMap 中读取数据并注入为环境变量。
# ...
spec:
template:
# ...
spec:
containers:
- name: my-app-container
image: my-app:1.3
ports:
- containerPort: 8080
# 新增 env 部分
env:
- name: GREETING # 在容器内定义的环境变量名
valueFrom:
configMapKeyRef:
# 引用哪个 ConfigMap
name: my-app-config
# 引用那个 Key
key: GREETING_MESSAGE
第三步:修改应用代码并部署
修改 app.js 来读取这个环境变量:
(示例代码中我们创建一个 app.1.3.js)
// ...
const GREETING = process.env.GREETING || 'Hello, default world!';
app.get('/', (req, res) => {
res.send(GREETING);
});
// ...
pnpm build:minikube:1.3构建新镜像 (my-app:1.3),并重新 kubectl apply -f deployment.yaml。
eval $(minikube docker-env) && docker build -f Dockerfile.1.3 -t my-app:1.3 ./
kubectl apply -f deployment.1.3.yaml
当滚动更新完成后,再次访问你的 NodePort 地址,你会发现页面返回的信息已经变成了 "Hello from ConfigMap!"。我们成功地将配置从代码中解耦了!
9.5 本章小结
你现在已经掌握了 K8s 中服务暴露和配置管理的关键知识。
-
本章回顾:
- 我们理解了 Service 是 Pod 的稳定访问入口,并能实现负载均衡。
- 我们学习了 Service 的几种类型,并通过 NodePort 成功地从集群外部访问了我们的应用。
- 我们学会了使用 ConfigMap 和 Secret 来将配置与代码解耦。
- 我们通过实战,成功地将 ConfigMap 中的数据作为环境变量注入到了 Pod 中。
-
避坑指南:
- 在 macOS 上使用 Docker Desktop 时,Minikube 的 NodePort 服务 不会直接暴露到宿主机网络,这种情况下 NodePort 服务无法直接通过 minikube ip:nodePort 访问
- 需要通过 minikube service 命令创建本地隧道来访问
我们的应用现在既能被访问,配置也变得灵活。但还有一个环节没有闭合:如果我们的应用是有状态的,比如一个数据库,它的数据该如何持久化呢?下一章,我们将深入 K8s 的存储世界。
9.5 本章小结
你现在已经掌握了 K8s 中服务暴露和配置管理的关键知识。
-
本章回顾:
- 我们理解了 Service 是 Pod 的稳定访问入口,并能实现负载均衡。
- 我们学习了 Service 的几种类型,并通过 NodePort 成功地从集群外部访问了我们的应用。
- 我们学会了使用 ConfigMap 和 Secret 来将配置与代码解耦。
- 我们通过实战,成功地将 ConfigMap 中的数据作为环境变量注入到了 Pod 中。
-
避坑指南:
- 在 macOS 上使用 Docker Desktop 时,Minikube 的 NodePort 服务 不会直接暴露到宿主机网络,这种情况下 NodePort 服务无法直接通过 minikube ip:nodePort 访问
- 需要通过 minikube service 命令创建本地隧道来访问
我们的应用现在既能被访问,配置也变得灵活。但还有一个环节没有闭合:如果我们的应用是有状态的,比如一个数据库,它的数据该如何持久化呢?下一章,我们将深入 K8s 的存储世界。