K8s中的Service服务
为何需要 Service
Kubernetes 中 Pod 是随时可以消亡的(节点故障、容器内应用程序错误等原因)。如果使用 Deployment 运行您的应用程序,Deployment 将会在 Pod 消亡后再创建一个新的 Pod 以维持所需要的副本数。每一个 Pod 有自己的 IP 地址,然而,对于 Deployment 而言,对应 Pod 集合是动态变化的。
这个现象导致了如下问题:
如果某些 Pod(假设是 'backends')为另外一些 Pod(假设是 'frontends')提供接口,在 'backends' 中的 Pod 集合不断变化(IP 地址也跟着变化)的情况下,'frontends' 中的 Pod 如何才能知道应该将请求发送到哪个 IP 地址?
Service 存在的意义,就是为了解决这个问题。
Kubernetes 通过引入 Service 的概念,将前端与后端解耦。
从 Kuboard 工作负载编辑器的视角来看,Service 与其他重要的 Kubernetes 对象之间的关系如下图所示:
创建service
如下:创建一个满足以下条件的service
1)名字为 my-service
2)目标端口为 TCP 9376
3)选取所有包含标签 app=MyApp 的 Pod
vi my-service.yaml apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: MyApp ports: - protocol: TCP port: 80 targetPort: 9376
补充:
1)Kubernetes 将为该 Service 分配一个 IP 地址(ClusterIP 或 集群内 IP),供 Service Proxy 使用。
2)Kubernetes 将不断扫描符合该 selector 的 Pod,并将最新的结果更新到与 Service 同名 my-service 的 Endpoint 对象中。
虚拟 IP 和服务代理
Kubernetes 集群中的每个节点都运行了一个 kube-proxy,负责为 Service(ExternalName 类型的除外)提供虚拟 IP 访问。
Kubernetes 支持三种 proxy mode(代理模式)
iptables proxy mode 模式
在 iptables proxy mode 下:
1)kube-proxy 监听 kubernetes master 以获得添加和移除 Service / Endpoint 的事件
2)kube-proxy 在其所在的节点(每个节点都有 kube-proxy)上为每一个 Service 安装 iptable 规则
3)iptables 将发送到 Service 的 ClusterIP / Port 的请求重定向到 Service 的后端 Pod 上
a、对于 Service 中的每一个 Endpoint,kube-proxy 安装一个 iptable 规则
b、默认情况下,kube-proxy 随机选择一个 Service 的后端 Pod
IPVS 代理模式
需要着重了解下ipvs模式,在 IPVS proxy mode 下:
1)kube-proxy 监听 kubernetes master 以获得添加和移除 Service / Endpoint 的事件
2)kube-proxy 根据监听到的事件,调用 netlink 接口,创建 IPVS 规则;并且将 Service/Endpoint 的变化同步到 IPVS 规则中
3)当访问一个 Service 时,IPVS 将请求重定向到后端 Pod
IPVS 模式的优点:
IPVS proxy mode 基于 netfilter 的 hook 功能,与 iptables 代理模式相似,但是 IPVS 代理模式使用 hash table 作为底层的数据结构,并在 kernel space 运作。这就意味着
1)IPVS 代理模式可以比 iptables 代理模式有更低的网络延迟,在同步代理规则时,也有更高的效率
2)与 user space 代理模式 / iptables 代理模式相比,IPVS 模式可以支持更大的网络流量
多端口的Service
Kubernetes 中,您可以在一个 Service 对象中定义多个端口,此时,您必须为每个端口定义一个名字。如下:
apiVersion: v1 kind: Service metadata: name: my-service spec: selector: app: MyApp ports: - name: http protocol: TCP port: 80 targetPort: 9376 - name: https protocol: TCP port: 443 targetPort: 9377
服务发现
Kubernetes 支持两种主要的服务发现模式:
1)环境变量
如果要在 Pod 中使用基于环境变量的服务发现方式,必须先创建 Service,再创建调用 Service 的 Pod。否则,Pod 中不会有该 Service 对应的环境变量。
如果使用基于 DNS 的服务发现,您无需担心这个创建顺序的问题。
2)DNS
首先确保集群中已安装了 DNS 服务,例如:Core DNS
CoreDNS 监听 Kubernetes API 上创建和删除 Service 的事件,并为每一个 Service 创建一条 DNS 记录。集群中所有的 Pod 都可以使用 DNS Name 解析到 Service 的 IP 地址。
发布Service
Kubernetes 中可以通过不同方式发布 Service,通过 ServiceType 字段指定,该字段的默认值是 ClusterIP,可选值有:
1)ClusterIP:默认值。通过集群内部的一个 IP 地址暴露 Service,只在集群内部可以访问
2)NodePort:通过每一个节点上的的静态端口(NodePort)暴露 Service,同时自动创建 ClusterIP 类型的访问方式
a、在集群内部通过 $(ClusterIP): $(Port) 访问
b、在集群外部通过 $(NodeIP): $(NodePort) 访问
3)LoadBalancer:通过云服务供应商(AWS、Azure、GCE 等)的负载均衡器在集群外部暴露 Service,同时自动创建 NodePort 和 ClusterIP 类型的访问方式
a、在集群内部通过 $(ClusterIP): $(Port) 访问
b、在集群外部通过 $(NodeIP): $(NodePort) 访问
c、在集群外部通过 $(LoadBalancerIP): $(Port) 访问
4)ExternalName:将 Service 映射到 externalName 指定的地址(例如:foo.bar.example.com),返回值是一个 CNAME 记录。不使用任何代理机制。
补充:如使用 ExternalName 类型的 Service,CoreDNS 版本不能低于 1.7
Service/Pod的DNS
Kubernetes 集群中运行了一组 DNS Pod,配置了对应的 Service,并由 kubelete 将 DNS Service 的 IP 地址配置到节点上的容器中以便解析 DNS names。
集群中的每一个 Service(包括 DNS 服务本身)都将被分配一个 DNS name。
Service
1)A记录
2)SRV 记录
Pods
1)Pod 的 hostname / subdomain
2)Pod 的 DNS Policy
小实验-Service连接应用程序
在集群中部署pod
1、通过yaml文件创建pod
#创建文件 run-my-nginx.yaml
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
2执行以下命令,部署 Pod 并检查运行情况:
kubectl apply -f ./run-my-nginx.yaml kubectl get pods -l run=my-nginx -o wide
查看pod的ip
4、测试访问验证
在集群中的任意节点上,测试访问,可以获得 nginx 的响应。此时:
1)容器并没有使用节点上的 80 端口
2)没有使用 NAT 规则对容器端口进行映射
这意味着,您可以
1)在同一节点上使用 80 端口运行多个 nginx Pod
2)在集群的任意节点/Pod 上使用 nginx Pod 的 clusterIP 访问 nginx 的 80 端口
同 Docker 一样,Kubernets 中,仍然可以将 Pod 的端口映射到宿主节点的网络地址上(使用 nodePort),但是使用 Kubernetes 的网络模型时,这类需求已经大大减少了。
创建 Service
Service详情:
1)定义了集群中一组 Pod 的逻辑集合,该集合中的 Pod 提供了相同的功能
2)被创建后,获得一个唯一的 IP 地址(ClusterIP)。直到该 Service 被删除,此地址不会发生改变
3)Pod 可以直接连接 Service IP 地址上的端口,且发送到该 IP 地址的网络请求被自动负载均衡分发到 Service 所选取的 Pod 集合中
执行命令:
kubectl expose deployment/my-nginx |
可以为上面的两个 nginx Pod 创建 Service,执行结果如下:
以上命令等价于定义通过yaml文件创建service,如下:
#创建nginx-svc.yaml vi nginx-svc.yaml apiVersion: v1 kind: Service metadata: name: my-nginx labels: run: my-nginx spec: ports: - port: 80 targetPort: 80 protocol: TCP selector: run: my-nginx #执行yaml kubectl apply -f nginx-svc.yaml |
查看service ip信息
kubectl get svc my-nginx |
结果如下:
Service 的后端 Pod 实际上通过 Endpoints 来暴露。Kubernetes 会持续检查 Service 的 label selector spec.selector,并将符合条件的 Pod 更新到与 Service 同名(my-nginx)的 Endpoints 对象。如果 Pod 终止了,该 Pod 将被自动从 Endpoints 中移除,新建的 Pod 将自动被添加到该 Endpoint。
从下图可以看到该service的endpoints地址和上面pod的ip地址相同。
访问 Service
Kubernetes 支持两种方式发现服务:
1)环境变量
2)DNS
环境变量
针对每一个有效的 Service,kubelet 在创建 Pod 时,向 Pod 添加一组环境变量,但是要注意pod和service顺序问题。
执行命令:
kubectl exec my-nginx-75897978cd-llgr7 -- printenv | grep SERVICE |
输出结果如下:
此时命令删除pod,命令如下:
kubectl delete pods -l run=my-nginx |
然后再执行命令查看重建的pod,命令如下:
kubectl get pods -l run=my-nginx -o wide |
再次查看信息,命令如下:
kubectl exec my-nginx-75897978cd-f4hkg -- printenv | grep SERVICE |
输出结果如下:
从下图可以看到Service 相关的环境变量已经被正确设置。
DNS
Kubernetes 提供了一个 DNS cluster addon,可自动为 Service 分配 DNS name。
可以通过如下命令确认集群中是否有安装对应组件,命令如下:
kubectl get services kube-dns --namespace=kube-system |
输出结果如下:
证明该组件已经安装,基于该组件可以从集群中任何 Pod 中按 Service 的名称访问该 Service。
此时再创建一个pod,命令如下:
kubectl run curl --image=radial/busyboxplus:curl -i --tty |
输出结果如下:
此时在该pod中测试解析server名称,可以正常解析。输出结果如下:
测试访问service对应服务,可以正常访问。输出结果如下:
保护 Service 的安全
上面测试都是在集群内部访问的,如果将service公布到互联网,必须保证是安全的,所以必须做如下措施:
1)准备 https 证书(购买,或者自签名)
2)将该 nginx 服务配置好,并使用该 https 证书
3)配置 Secret,以使得其他 Pod 可以使用该证书
可以如下步骤配置nginx自签证书
1)创建密钥对,命令如下:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/nginx.key -out /tmp/nginx.crt -subj "/CN=my-nginx/O=my-nginx" |
输出结果如下:
2)将密钥对转换为 base64 编码,命令如下:
cat /tmp/nginx.crt | base64 cat /tmp/nginx.key | base64 |
3)创建一个如下格式的 nginxsecrets.yaml 文件,使用前面命令输出的 base64 编码替换其中的内容(base64编码内容不能换行)(请使用前面两行命令生成的结果替换 nginx.crt 和 nginx.key 的内容,)
apiVersion: "v1" kind: "Secret" metadata: name: "nginxsecret" namespace: "default" data: nginx.crt: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURIekNDQWdlZ0F3SUJBZ0lKQU9lQlVRWGN4eEllTUEwR0NTcUdTSWIzRFFFQkN3VUFNQ1l4RVRBUEJnTlYKQkFNTUNHMTVMVzVuYVc1NE1SRXdEd1lEVlFRS0RBaHRlUzF1WjJsdWVEQWVGdzB5TURFeU1UVXhORFU1TVRaYQpGdzB5TVRFeU1UVXhORFU1TVRaYU1DWXhFVEFQQmdOVkJBTU1DRzE1TFc1bmFXNTRNUkV3RHdZRFZRUUtEQWh0CmVTMXVaMmx1ZURDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUdZNlU5T1V4dkEKTVhXWWd0VmJPKzYwaEtjVW16RlZBSFRYWEluRnZLQXBXT3hwd3A4S3AxRTRoLzE2bUFpV2k1ZUhDSDI5YmtyNwo3S1dnVVo0MUFjT1F2N2c2c1ZUaStZZXd3blVONjJtOGtNVk44ZURubnZFVm5DR3RwL1ZldjVJV3NCdU44Y3JMCjJxU1V6bGgyNWtiMlZIeXkwL1MrU3ZUU2YxMWpsdFBEanMzSTlxMUdwbUsxMkxvREtLS3FYSlVlUTNiYmU1Q3cKWDFPN0UreWxPU0kvd0g0TjFsTmZ0Wm5GbHZ5amtJL2J3VVRtYkRQc3hGOVRXTDUwZUtRQ1Z3amd3Unpkdnl6TwpWRkhIdWN1a1pXYk01dXNxNFNmdHhMck13U1dmNXhMcVdMT25LL2ovektVdHdhcCtlSFBqZGlkY25vWGdLTmNYCmw5ak1aelNpY3BjQ0F3RUFBYU5RTUU0d0hRWURWUjBPQkJZRUZNUSt4aUtBNkpOZ1p2dE03VWl0c1VBcmtRdCsKTUI4R0ExVWRJd1FZTUJhQUZNUSt4aUtBNkpOZ1p2dE03VWl0c1VBcmtRdCtNQXdHQTFVZEV3UUZNQU1CQWY4dwpEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBS0kweW15YzdCQjVOVFlJYzl0aGlQWjgwV3NIeUpqNTl5cFEraHMxCkR5VFgreE9JYUU4dEpHWlM4VkVFdFJ1QkVkRW9WczAxWWN3UkVaR2tlQVFxTWkwTk9QMXVRTGNabWVFOEIyVkgKZmZ1SSsrWHFRN2d2bzdkblZMSmx3eGRpVWRWUEhrSjA5MU1HUWQyU200aGdoWTJLUmlpMk1LU2ZGYnQ3cVJuZwoyN2g2L0hMUFRvY3E3cGZ0V0ZrNkJ0Y1d1cGlVQngyRFYra2JSMmRXN1RYZzUxVUtwN1J0aUprSk5wZlVnMDhBClNoaVV1YTZEbVN2TVdsU3hFZXF4dElKSzVWYTcyakFXTnE2bDg4QTBMb3FRWm4wUVB3MUFFWkRLR0pqazBCeDgKZGFXSm5yMkJJVHFRQ3R1NnJQeHhRTCtROSs2WVBncHp3YWd1RUp0NmtNU3dMZzg9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K" nginx.key: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRREJtT2xQVGxNYndERjEKbUlMVld6dnV0SVNuRkpzeFZRQjAxMXlKeGJ5Z0tWanNhY0tmQ3FkUk9JZjllcGdJbG91WGh3aDl2VzVLKyt5bApvRkdlTlFIRGtMKzRPckZVNHZtSHNNSjFEZXRwdkpERlRmSGc1NTd4Rlp3aHJhZjFYcitTRnJBYmpmSEt5OXFrCmxNNVlkdVpHOWxSOHN0UDB2a3IwMG45ZFk1YlR3NDdOeVBhdFJxWml0ZGk2QXlpaXFseVZIa04yMjN1UXNGOVQKdXhQc3BUa2lQOEIrRGRaVFg3V1p4WmI4bzVDUDI4RkU1bXd6N01SZlUxaStkSGlrQWxjSTRNRWMzYjhzemxSUgp4N25McEdWbXpPYnJLdUVuN2NTNnpNRWxuK2NTNmxpenB5djQvOHlsTGNHcWZuaHo0M1luWEo2RjRDalhGNWZZCnpHYzBvbktYQWdNQkFBRUNnZ0VBV0tabW43UWtCbDhaeHRtUmY1V0plaU1iWTA3aVNkSzhwTVJCVWExRVZLcmEKVjFlT2tUbFZxTEdVUkJReExpMlJ1azlSS2hMbVFGdmYzMi9zTDkvaDhPV0ZoUjBMT3UyOGljLzlHNURHTGVMMQpBQVJUTUVPZjJwR2tyeDlQKytIcEkwSlVYaW0vZ0xlY1pTVG00RWVCNXhqQlUyQ29BamhwSm5hRmNBUThlcmNUCjk1ZFlMTVJzWVMrbkFiMVdRNjdMUXRSa1k1UVRjdkhrNGhLM3J1N0pGVUNJN2c1N2VkQnJidTZNaGdtOVhDaVAKVmF5Zi9RZTV5VVpKdWg1TlF5ekxzdkJPcS9TZ3c1MU94MW5ienpFb3d1azhmWmpObWtFZGpHZ3pFUnllMlZSZAo5SkNVZEJFblc0M25JNkJEdExqcUw0cnRJdGI2QWMwT0Q4akx2MHlmQVFLQmdRRGdGcUkrQ3h4TXJBLzNkMk1BClBpekJHYXc2K2JiRnZ0NDlhNkpieWxySW42M1paVHJ2d1pGYmFrcm5WVDdWVWJjVU4yam5zdmh2YjBPbGlsbWMKZXgrMWdmN2xMOHZQdEZXelFiK1pXYU9ISmJBZk9TYzh5a1Ryc2J4NFZSOU9Wd2Q4UVFnNElNVklDSmNpNlFucApwMjFkVm40S0s1ZWR4M2VyNXZlQUNjazgxd0tCZ1FEZEtyR3ZWWkxXMHpEU3FVZFNQQU5Td1NSN0NWaTRhOVg5ClhMN3lURDFlNHI3MWowUWpiRzBsTHJxTXozTk9memJySXJicDRnMEhkSTVuUXZ0YWlPdkpCbnMwM05uZDdnbXQKb1lXNFF4UFN4WTZNT3hJenI2WUVjVTd2MUl4bk5OSXkyb2ptcXdVMUZadXl5ZzhpejdLV1lYeGpSUzJRQk9hKwpNbThzSUlZQVFRS0JnRlhLOUpTeVprUklmOWhyd2VCKzdWMkV1YmJla2daRlhBckI0YWdvNGZiN2czQyszQUNjCjZFektkaUQ1TnhRdXM5d3VscUJXbWR6NENUc2dxOHhJSzB5dmwyb2hrWE5WQkphYnJvSkVtbUlNb05CamJrMU0KMTNReFdRbnM5UTVtTFh4NTNXNjN4VEFkOGRjd3gxWElmd3VFS0w4MkQxY2QzZ2hYWmh5RUlxS0RBb0dCQUt5UgpZNU1yY2lldElhczk2aWlBS3hlbkhJL2oreFhyRGsxaTBKcStZaVJuU0JqU2NKZ3pRZmFCUDQ0ZlVCa3ZxZXBPCmErcVNOeGhhR2NMNHdLY2pydFpyK0RhSEhIZk9CRDgyaU4vOWRybys2N3IvWEhSMzJWSWVGem1LM3dLb2RGcTUKcVpoU1dQM1NubW9pdnl5cVl1NXpvbHJTMzNwQVdNcVBENitlc1BBQkFvR0FHSGpMQzZTWVJiM09BeHlyc2F5cgpubTVLZWd4Qm8xdXpreXVpdllwVHNjSUIyaTFxcnYyem5RWDNUUCtWYUdQaTJjUkNUM21MeGFXd3ZFY2VJelFyCk5Mc0hSMGNDbUdBVzFHOVJRanIvMVVwYllUMVhyRTc5YmdOejIwN2htNWVKN253L2V4SHlFM0tBV1BzOVY2RWMKbnR2ZTVPRzAza0YxWllPV1A3T1p4ZFk9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K" |
4)使用该文件创建 Secrets
# 创建 Secrets kubectl apply -f nginxsecrets.yaml # 查看 Secrets kubectl get secrets
输出结果如下:
5)修改 nginx 部署,使 nginx 使用 Secrets 中的 https 证书,修改 Service,使其暴露 80 端口和 443端口。nginx-secure-app.yaml 文件如下所示:
vi nginx-secure-app.yaml apiVersion: v1 kind: Service metadata: name: my-nginx labels: run: my-nginx spec: type: NodePort ports: - port: 80 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 containers: - name: nginxhttps image: bprashanth/nginxhttps:1.0 ports: - containerPort: 443 - containerPort: 80 volumeMounts: - mountPath: /etc/nginx/ssl name: secret-volume
deployment以及service,命令如下:
kubectl delete deployments,svc my-nginx kubectl create -f ./nginx-secure-app.yaml
然后执行创建命令,如下:
访问验证,测试可以从任何节点访问该 nginx server
补充:curl -k
1)在 curl 命令中指定 —k 参数,是因为我们在生成 https 证书时,并不知道 Pod 的 IP 地址,因此,在执行 curl 命令时必须忽略 CName 不匹配的错误。
2)通过创建 Service,我们将 https 证书的 CName 和 Service 的实际 DNS Name 联系起来,因此,我们可以尝试在另一个 Pod 中使用 https 证书的公钥访问 nginx Service。此时,curl 指令不在需要 -k 参数
创建 curlpod.yaml 文件,内容如下:、
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
curlpod 的部
kubectl apply -f ./curlpod.yaml kubectl get pods -l app=curlpod
输出结果如下:
测试访问:
kubectl exec curl-deployment-f8c5c685b-lw68q -- curl https://my-nginx --cacert /etc/nginx/ssl/nginx.crt |
输出结果如下:
暴露 Service
在您的应用程序中,可能有一部分功能需要通过 Service 发布到一个外部的 IP 地址上。Kubernetes 支持如下两种方式:
1)NodePort
2)LoadBalancer 该方式需要云环境支持
在上面实验中我们创建的service就是NodePort类型,所以如果节点有公网地址,则 nginx HTTPS 部署已经可以接受来自于互联网的请求了。