使用服务连接应用程序
Kubernetes 连接容器的模型
现在,您已经拥有了一个持续运行且具有副本的应用,接下来可以将其暴露在网络上。
Kubernetes 假定 Pod 之间可以相互通信,无论它们落在哪个主机上。Kubernetes 为每个 Pod 分配了其自己的集群内部 IP 地址,因此您无需明确地在 Pod 之间创建链接或将容器端口映射到主机端口。这意味着 Pod 中的所有容器都可以通过 localhost 访问彼此的端口,并且集群中的所有 Pod 都可以相互通信而无需 NAT。本文档的其余部分将详细说明如何在这样的网络模型上运行可靠的服务。
本教程使用一个简单的 nginx Web 服务器来演示此概念。
将 Pod 暴露给集群
我们已经在前面的示例中完成了此操作,但让我们再次执行此操作,并重点关注网络方面。创建一个 nginx Pod,并注意它具有容器端口规范。
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
run: my-nginx
replicas: 2
template:
metadata:
labels:
run: my-nginx
spec:
containers:
- name: my-nginx
image: nginx
ports:
- containerPort: 80
这使得它可以从集群中的任何节点访问。检查 Pod 运行的节点。
kubectl apply -f ./run-my-nginx.yaml
kubectl get pods -l run=my-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE
my-nginx-3800858182-jr4a2 1/1 Running 0 13s 10.244.3.4 kubernetes-minion-905m
my-nginx-3800858182-kna2y 1/1 Running 0 13s 10.244.2.5 kubernetes-minion-ljyd
检查您的 Pod IP。
kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
POD_IP
[map[ip:10.244.3.4]]
[map[ip:10.244.2.5]]
您应该能够通过 SSH 连接到集群中的任何节点,并使用 `curl` 等工具对这两个 IP 进行查询。请注意,容器**没有**使用节点上的 80 端口,也没有任何特殊的 NAT 规则将流量路由到 Pod。这意味着您可以在同一节点上运行多个 nginx Pod,它们都使用相同的 `containerPort`,并且可以使用分配给 Pod 的 IP 地址从集群中的任何其他 Pod 或节点访问它们。如果您想安排主机节点上的特定端口转发到后端 Pod,您可以这样做,但网络模型意味着您不需要这样做。
如果您好奇,可以阅读更多关于 Kubernetes 网络模型 的信息。
创建 Service
现在我们有了在集群范围内扁平地址空间中运行 nginx 的 Pod。理论上,您可以直接与这些 Pod 通信,但是当节点宕机时会发生什么?Pod 也会随之宕机,Deployment 中的 ReplicaSet 将创建新的 Pod,并分配不同的 IP。这就是 Service 所解决的问题。
Kubernetes Service 是一种抽象,它定义了集群中某个位置运行的逻辑 Pod 集合,这些 Pod 都提供相同的功能。创建 Service 后,每个 Service 都被分配一个唯一的 IP 地址(也称为 ClusterIP)。此地址与 Service 的生命周期绑定,在 Service 存活期间不会改变。Pod 可以配置为与 Service 通信,并且知道与 Service 的通信将自动负载均衡到属于该 Service 的某个 Pod。
您可以使用 kubectl expose
为您的 2 个 nginx 副本创建 Service。
kubectl expose deployment/my-nginx
service/my-nginx exposed
这等同于以下 YAML 文件中的 kubectl apply -f
命令:
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
run: my-nginx
spec:
ports:
- port: 80
protocol: TCP
selector:
run: my-nginx
此规范将创建一个 Service,它会针对任何带有 run: my-nginx
标签的 Pod 的 TCP 端口 80,并通过一个抽象的 Service 端口(targetPort
:容器接受流量的端口,port
:抽象的 Service 端口,可以是任何其他 Pod 用于访问 Service 的端口)将其暴露。查看 Service API 对象以查看 Service 定义中支持的字段列表。检查您的 Service。
kubectl get svc my-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-nginx ClusterIP 10.0.162.149 <none> 80/TCP 21s
如前所述,Service 由一组 Pod 提供支持。这些 Pod 通过 EndpointSlice 暴露。Service 的选择器将持续评估,结果将 POST 到通过标签连接到 Service 的 EndpointSlice。当 Pod 死亡时,它会自动从包含其作为端点的 EndpointSlice 中移除。与 Service 选择器匹配的新 Pod 将自动添加到该 Service 的 EndpointSlice 中。检查端点,并注意 IP 与第一步中创建的 Pod 相同。
kubectl describe svc my-nginx
Name: my-nginx
Namespace: default
Labels: run=my-nginx
Annotations: <none>
Selector: run=my-nginx
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.0.162.149
IPs: 10.0.162.149
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.244.2.5:80,10.244.3.4:80
Session Affinity: None
Events: <none>
kubectl get endpointslices -l kubernetes.io/service-name=my-nginx
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
my-nginx-7vzhx IPv4 80 10.244.2.5,10.244.3.4 21s
现在,您应该能够从集群中的任何节点,通过 `<CLUSTER-IP>:<PORT>` curl 访问 nginx Service。请注意,Service IP 完全是虚拟的,它从不触及物理网络。如果您对它是如何工作的感到好奇,可以阅读更多关于 服务代理 的信息。
访问 Service
Kubernetes 支持两种主要的服务发现模式:环境变量和 DNS。前者开箱即用,而后者需要 CoreDNS 集群插件。
注意
如果不需要服务环境变量(因为可能与预期的程序变量冲突,变量过多无法处理,仅使用 DNS 等),您可以通过将 Pod 规范中的enableServiceLinks
标志设置为 false
来禁用此模式。环境变量
当 Pod 在节点上运行时,kubelet 会为每个活跃的 Service 添加一组环境变量。这带来了一个排序问题。为了理解原因,检查您正在运行的 nginx Pod 的环境(您的 Pod 名称将不同)。
kubectl exec my-nginx-3800858182-jr4a2 -- printenv | grep SERVICE
KUBERNETES_SERVICE_HOST=10.0.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
请注意,没有提及您的 Service。这是因为您在 Service 之前创建了副本。这样做 的另一个缺点是调度器可能会将两个 Pod 放在同一台机器上,如果它宕机,将导致您的整个 Service 停止。我们可以通过杀死这两个 Pod 并等待 Deployment 重新创建它们来正确处理。这次 Service 存在于副本**之前**。这将为您提供 Pod 的调度器级别服务传播(前提是所有节点都具有相同的容量),以及正确的环境变量。
kubectl scale deployment my-nginx --replicas=0; kubectl scale deployment my-nginx --replicas=2;
kubectl get pods -l run=my-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE
my-nginx-3800858182-e9ihh 1/1 Running 0 5s 10.244.2.7 kubernetes-minion-ljyd
my-nginx-3800858182-j4rm4 1/1 Running 0 5s 10.244.3.8 kubernetes-minion-905m
您可能会注意到 Pod 的名称不同,因为它们被杀死并重新创建了。
kubectl exec my-nginx-3800858182-e9ihh -- printenv | grep SERVICE
KUBERNETES_SERVICE_PORT=443
MY_NGINX_SERVICE_HOST=10.0.162.149
KUBERNETES_SERVICE_HOST=10.0.0.1
MY_NGINX_SERVICE_PORT=80
KUBERNETES_SERVICE_PORT_HTTPS=443
DNS
Kubernetes 提供了一个 DNS 集群插件 Service,该 Service 会自动为其他 Service 分配 DNS 名称。您可以检查它是否在您的集群中运行。
kubectl get services kube-dns --namespace=kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.0.0.10 <none> 53/UDP,53/TCP 8m
本节的其余部分将假定您有一个具有长期 IP(my-nginx)的 Service,以及一个已为该 IP 分配名称的 DNS 服务器。这里我们使用 CoreDNS 集群插件(应用程序名称为 kube-dns
),因此您可以使用标准方法(例如 gethostbyname()
)从集群中的任何 Pod 与 Service 通信。如果 CoreDNS 未运行,您可以参考 CoreDNS README 或 安装 CoreDNS 来启用它。让我们运行另一个 curl 应用程序来测试一下。
kubectl run curl --image=radial/busyboxplus:curl -i --tty --rm
Waiting for pod default/curl-131556218-9fnch to be running, status is Pending, pod ready: false
Hit enter for command prompt
然后,按回车键并运行 nslookup my-nginx
。
[ root@curl-131556218-9fnch:/ ]$ nslookup my-nginx
Server: 10.0.0.10
Address 1: 10.0.0.10
Name: my-nginx
Address 1: 10.0.162.149
保护服务
到目前为止,我们只从集群内部访问了 nginx 服务器。在将服务暴露到互联网之前,您需要确保通信通道是安全的。为此,您需要:
- 用于 HTTPS 的自签名证书(除非您已有身份证书)
- 配置为使用证书的 Nginx 服务器
- 一个Secret,使证书可供 Pod 访问
您可以从nginx https 示例中获取所有这些。这需要安装 Go 和 make 工具。如果您不想安装这些工具,请稍后按照手动步骤操作。简而言之:
make keys KEY=/tmp/nginx.key CERT=/tmp/nginx.crt
kubectl create secret tls nginxsecret --key /tmp/nginx.key --cert /tmp/nginx.crt
secret/nginxsecret created
kubectl get secrets
NAME TYPE DATA AGE
nginxsecret kubernetes.io/tls 2 1m
还有 ConfigMap
kubectl create configmap nginxconfigmap --from-file=default.conf
您可以在Kubernetes 示例项目仓库中找到 default.conf
的示例。
configmap/nginxconfigmap created
kubectl get configmaps
NAME DATA AGE
nginxconfigmap 1 114s
您可以使用以下命令查看 nginxconfigmap
ConfigMap 的详细信息。
kubectl describe configmap nginxconfigmap
输出类似于:
Name: nginxconfigmap
Namespace: default
Labels: <none>
Annotations: <none>
Data
====
default.conf:
----
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
listen 443 ssl;
root /usr/share/nginx/html;
index index.html;
server_name localhost;
ssl_certificate /etc/nginx/ssl/tls.crt;
ssl_certificate_key /etc/nginx/ssl/tls.key;
location / {
try_files $uri $uri/ =404;
}
}
BinaryData
====
Events: <none>
以下是如果运行 make 时遇到问题(例如在 Windows 上)时要遵循的手动步骤:
# Create a public private key pair
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /d/tmp/nginx.key -out /d/tmp/nginx.crt -subj "/CN=my-nginx/O=my-nginx"
# Convert the keys to base64 encoding
cat /d/tmp/nginx.crt | base64
cat /d/tmp/nginx.key | base64
使用前述命令的输出创建如下的 YAML 文件。Base64 编码的值应全部位于一行中。
apiVersion: "v1"
kind: "Secret"
metadata:
name: "nginxsecret"
namespace: "default"
type: kubernetes.io/tls
data:
# NOTE: Replace the following values with your own base64-encoded certificate and key.
tls.crt: "REPLACE_WITH_BASE64_CERT"
tls.key: "REPLACE_WITH_BASE64_KEY"
现在使用该文件创建 Secret。
kubectl apply -f nginxsecrets.yaml
kubectl get secrets
NAME TYPE DATA AGE
nginxsecret kubernetes.io/tls 2 1m
现在修改您的 nginx 副本,以使用 Secret 中的证书启动一个 https 服务器,并使用 Service 暴露两个端口(80 和 443)。
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
run: my-nginx
spec:
type: NodePort
ports:
- port: 8080
targetPort: 80
protocol: TCP
name: http
- port: 443
protocol: TCP
name: https
selector:
run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
run: my-nginx
replicas: 1
template:
metadata:
labels:
run: my-nginx
spec:
volumes:
- name: secret-volume
secret:
secretName: nginxsecret
- name: configmap-volume
configMap:
name: nginxconfigmap
containers:
- name: nginxhttps
image: bprashanth/nginxhttps:1.0
ports:
- containerPort: 443
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/ssl
name: secret-volume
- mountPath: /etc/nginx/conf.d
name: configmap-volume
关于 nginx-secure-app 清单的值得注意的几点:
- 它在同一个文件中包含了 Deployment 和 Service 规范。
- nginx 服务器在端口 80 上提供 HTTP 流量,在端口 443 上提供 HTTPS 流量,并且 nginx Service 暴露了这两个端口。
- 每个容器都可以通过挂载到
/etc/nginx/ssl
的卷访问密钥。这在 nginx 服务器启动**之前**就已设置。
kubectl delete deployments,svc my-nginx; kubectl create -f ./nginx-secure-app.yaml
此时,您可以从任何节点访问 nginx 服务器。
kubectl get pods -l run=my-nginx -o custom-columns=POD_IP:.status.podIPs
POD_IP
[map[ip:10.244.3.5]]
node $ curl -k https://10.244.3.5
...
<h1>Welcome to nginx!</h1>
请注意,我们在上一步中是如何给 curl 提供了 -k
参数的,这是因为在证书生成时我们对运行 nginx 的 Pod 一无所知,所以我们必须告诉 curl 忽略 CName 不匹配。通过创建 Service,我们将证书中使用的 CName 与 Service 查找期间 Pod 实际使用的 DNS 名称关联起来。让我们从 Pod 中测试一下(为简单起见,重复使用了相同的 Secret,Pod 只需要 nginx.crt 即可访问 Service)。
apiVersion: apps/v1
kind: Deployment
metadata:
name: curl-deployment
spec:
selector:
matchLabels:
app: curlpod
replicas: 1
template:
metadata:
labels:
app: curlpod
spec:
volumes:
- name: secret-volume
secret:
secretName: nginxsecret
containers:
- name: curlpod
command:
- sh
- -c
- while true; do sleep 1; done
image: radial/busyboxplus:curl
volumeMounts:
- mountPath: /etc/nginx/ssl
name: secret-volume
kubectl apply -f ./curlpod.yaml
kubectl get pods -l app=curlpod
NAME READY STATUS RESTARTS AGE
curl-deployment-1515033274-1410r 1/1 Running 0 1m
kubectl exec curl-deployment-1515033274-1410r -- curl https://my-nginx --cacert /etc/nginx/ssl/tls.crt
...
<title>Welcome to nginx!</title>
...
暴露 Service
对于您的应用程序的某些部分,您可能希望将 Service 暴露到外部 IP 地址。Kubernetes 支持两种方式:NodePort 和 LoadBalancer。上一节中创建的 Service 已经使用了 NodePort
,因此如果您的节点具有公共 IP,您的 nginx HTTPS 副本已准备好为互联网上的流量提供服务。
kubectl get svc my-nginx -o yaml | grep nodePort -C 5
uid: 07191fb3-f61a-11e5-8ae5-42010af00002
spec:
clusterIP: 10.0.162.149
ports:
- name: http
nodePort: 31704
port: 8080
protocol: TCP
targetPort: 80
- name: https
nodePort: 32453
port: 443
protocol: TCP
targetPort: 443
selector:
run: my-nginx
kubectl get nodes -o yaml | grep ExternalIP -C 1
- address: 104.197.41.11
type: ExternalIP
allocatable:
--
- address: 23.251.152.56
type: ExternalIP
allocatable:
...
$ curl https://<EXTERNAL-IP>:<NODE-PORT> -k
...
<h1>Welcome to nginx!</h1>
现在,让我们重新创建 Service 以使用云负载均衡器。将 my-nginx
Service 的 Type
从 NodePort
更改为 LoadBalancer
。
kubectl edit svc my-nginx
kubectl get svc my-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-nginx LoadBalancer 10.0.162.149 xx.xxx.xxx.xxx 8080:30163/TCP 21s
curl https://<EXTERNAL-IP> -k
...
<title>Welcome to nginx!</title>
EXTERNAL-IP
列中的 IP 地址是公共互联网上可用的地址。CLUSTER-IP
仅在您的集群/私有云网络内部可用。
请注意,在 AWS 上,类型为 LoadBalancer
会创建一个 ELB,它使用一个(很长的)主机名,而不是 IP。实际上,它太长了,无法完全显示在标准 kubectl get svc
输出中,因此您需要执行 kubectl describe service my-nginx
才能看到它。您会看到类似以下内容:
kubectl describe service my-nginx
...
LoadBalancer Ingress: a320587ffd19711e5a37606cf4a74574-1142138393.us-east-1.elb.amazonaws.com
...
下一步
- 了解更多关于 使用 Service 访问集群中的应用程序 的信息。
- 了解更多关于 使用 Service 连接前端到后端 的信息。
- 了解更多关于 创建外部负载均衡器 的信息。