现在您已经拥有了一个持续运行、副本化的应用程序,您可以将其暴露在网络上。
Kubernetes 假设 Pod 可以与集群中的其他 Pod 通信,无论它们位于哪个主机上。Kubernetes 为每个 Pod 分配了其集群私有的 IP 地址,因此您无需显式地在 Pod 之间创建链接或将容器端口映射到主机端口。这意味着 Pod 内的所有容器都可以通过 localhost 访问彼此的端口,并且集群中的所有 Pod 都可以通过网络互通,而无需 NAT。本文档的其余部分将详细说明如何在这样的网络模型上运行可靠的服务。
本教程使用一个简单的 nginx Web 服务器来演示这个概念。
我们在之前的示例中做过这一点,但让我们再次操作,并专注于网络角度。创建一个 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 网络模型的内容。
所以我们在一个扁平的、全集群范围的地址空间中运行了 nginx Pod。理论上,您可以直接与这些 Pod 通信,但如果节点宕机了会发生什么?Pod 会随之死亡,ReplicaSet(在 Deployment 中)会创建新的 Pod,并分配不同的 IP。这就是 Service 要解决的问题。
Kubernetes Service 是一种抽象,它定义了一组运行在集群中的逻辑 Pod,它们都提供相同的功能。创建 Service 后,它会被分配一个唯一的 IP 地址(也称为 clusterIP)。该地址与 Service 的生命周期绑定,在 Service 存续期间不会改变。可以配置 Pod 与该 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 端口,并将其暴露在抽象的服务端口上(targetPort 是容器接受流量的端口,port 是抽象的服务端口,其他 Pod 可以使用该端口来访问 Service)。查看 Service API 对象以查看服务定义中支持的字段列表。检查您的 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 通过 EndpointSlices 暴露。Service 的选择器将被持续评估,结果将被 POST 到连接到该 Service 的 EndpointSlice 中(使用 标签)。当 Pod 死亡时,它会自动从包含它的 EndpointSlices 中移除。符合 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 IP 是完全虚拟的,它不会出现在物理网络线上。如果您对它是如何工作的感兴趣,可以阅读更多关于 service proxy 的内容。
Kubernetes 支持两种主要的查找 Service 的模式——环境变量和 DNS。前者可以直接使用,而后者需要 CoreDNS 集群插件。
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 就会瘫痪。我们可以通过杀死这 2 个 Pod 并等待 Deployment 重新创建它们来解决这个问题。这次 Service 在副本之前就存在了。这将为您提供调度层面的 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
Kubernetes 提供了一个 DNS 集群插件服务,自动为其他 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,并且 DNS 服务器已为该 IP 分配了一个名称。这里我们使用 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 服务器。在将 Service 暴露给互联网之前,您需要确保通信通道是安全的。为此,您需要:
您可以从 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 -noenc -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 清单值得注意的点:
/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 与 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 暴露到外部 IP 地址。Kubernetes 支持两种方式来实现这一点:NodePorts 和 LoadBalancers。上一节中创建的 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
...