ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kubernetes域名访问实践:从Service到Ingress的完整指南

Kubernetes域名访问实践:从Service到Ingress的完整指南 做过线上服务的人多半都经历过这么一件事服务已经跑在Kubernetes里了Deployment也正常Pod也Ready可别人要访问它总不能每次都用IP加端口。尤其是对外提供服务大家习惯的是输入一个域名就直接打开页面。这篇文章就围绕一个很实际的场景展开在k8s集群中通过域名访问Deployment管理的Pod。我会把背后涉及的原理、几种实现方案、完整实操步骤和踩坑记录都梳理出来适合刚接触k8s的运维和开发也适合集群已经跑起来但对外访问还没打通的团队。先说一个很容易混淆的点k8s里的“域名访问”其实分两条路。一条是集群内部的Service DNS也就是Pod之间通过服务名互相访问另一条是集群外部用真实域名访问集群内的Pod。这两条路的流量路径完全不一样很多人一开始被绕进去就是因为没分清这套逻辑。下面我会从原理讲到实操把这条路完整走一遍。1. 先搞清楚为什么“域名访问Pod”这件事值得单独聊1.1 Pod的IP天生不稳定域名才是长期可依赖的入口在Kubernetes里Pod是最小的调度单位但它有个让人头疼的特性——IP不稳定。Deployment管理的Pod只要发生滚动更新、节点故障、资源紧张导致驱逐、手动扩容缩容Pod都会被重建而重建之后的IP几乎必然变化。假设你辛辛苦苦配好一个负载均衡把流量转发到某个Pod IP上结果第二天Pod重启了IP换了所有转发全部失效。这就是为什么Deployment这个控制器很重要它保证了Pod始终维持在期望的副本数而且每个Pod都是“临时工”随时可以被替换。面对这种流动性极强的环境我们不能拿IP当长期入口必须找一个稳定的抽象层。这个稳定入口就是Service。Service通过标签选择器selector动态关联一组Pod对外提供固定的虚拟IP和端口无论背后的Pod怎么换Service入口都不变。很多刚接触k8s的人会把k8s和Docker混为一谈。其实Docker解决的是“在一台机器上怎么跑容器”而k8s解决的是“在多台机器上怎么统一调度和管理这些容器”。在这个场景里Docker负责单机容器运行k8s负责跨节点调度、服务发现和流量接入两者是不同层面的东西。所以当你说“通过域名访问Deployment的Pod”时本质上是在问如何在k8s这个分布式平台上为动态变化的Pod提供一个稳定的域名入口。1.2 域名访问实际拆成两件事集群内DNS解析和集群外流量接入k8s集群内部天生带一套DNS服务通常由CoreDNS实现。这套DNS负责解析Service域名格式一般是服务名.命名空间.svc.cluster.local。比如你在default命名空间下建了一个名为my-service的Service那么集群内任何一个Pod都可以通过my-service.default.svc.cluster.local访问到它也可以直接用简写my-service访问同命名空间下的服务。这套内部DNS解决了“服务之间的互相调用”但它管不到集群外部。外部用户输入app.example.com要访问你的Pod这个域名需要在公网DNS或公司内部DNS里能解析到集群入口流量先打到集群入口再经过路由规则转发到Service最后Service分发到Pod。这个链路里内部域名解析和外部域名解析用的是两套不同的DNS系统两套配置各自独立经常有人内部DNS调通了外部还是访问不了问题就出在外面那一段。1.3 一句话说清本篇文章要解决的问题我用一个流程图般的思路把目标场景描述清楚大家先有个整体概念用户浏览器输入 app.example.com ↓ 公网DNS解析到集群入口云负载均衡/节点IP ↓ Ingress Controller 收到请求根据域名和路径匹配规则 ↓ 转发到对应名称空间的 Service ↓ Service 转发到后端 Pod容器实例最终达到的效果是用户只记一个域名剩下的路由、负载均衡、Pod自动伸缩都不用关心。这篇文章完整讲解的就是这个链路的搭建、配置和排障过程。2. 方案选型到底用哪种方式把域名接到Pod上2.1 ClusterIP、NodePort、LoadBalancer 的定位与局限Kubernetes里的Service有几种类型各有各的用途。先说ClusterIP这是默认类型也是内部服务发现的主力。它只给Service分配一个集群内部可达的虚拟IP外部流量进不来只能在集群内部通过DNS或ClusterIP访问。如果你只想让A服务调用B服务那ClusterIP完全够用这也是集群内最推荐的方式因为它不暴露端口安全性高。然后是NodePort。这种类型的Service会在每个节点的IP上开放一个指定端口默认范围是30000-32767外部可以通过任意节点IP:NodePort访问到Service。NodePort的好处是不依赖云厂商裸机集群也能直接使用但缺点也很明显用户需要记住一堆随机端口非常不雅观而且节点IP一旦变化端口入口也会跟着变端口还容易冲突。项目多起来之后端口和IP的对应关系就是一场灾难。最后是LoadBalancer。这是云环境下的方案Service创建后云厂商会自动分配一个负载均衡器并绑定一个公网IP或域名。外部流量经过云负载均衡器分发到各个节点上的NodePort。LoadBalancer体验比NodePort好但意味着每个Service都要买一个负载均衡器费用不低而且同域名下的多个路径也做不到统一管理。如果你只有几套服务LoadBalancer还能接受一旦服务数量上来了成本和管理复杂度都会飙升。2.2 Ingress 才是域名访问的标准答案如果说Service解决了Pod的不稳定IP问题那Ingress解决的就是“如何用一个统一入口管理所有外部流量”。Ingress不是一种Service类型而是一个独立的API对象它的职责是定义“域名路径”到Service的路由规则。真正处理流量转发的是Ingress Controller比如NGINX Ingress Controller、Traefik、HAProxy Ingress等它们会读取Ingress规则将外部HTTP/HTTPS请求转发到对应的Service。有了Ingress之后多个服务可以共享同一个入口IP。比如app.example.com这个域名下/api路径转发到A服务的Service/web路径转发到B服务的Service而另一个域名admin.example.com则整体转发到C服务的Service。这种模式最大的优势就是收敛所有外部流量都进一个入口证书、路由、分流策略都集中管理不用每加一个服务就申请一个新负载均衡器。而且Ingress支持TLS证书卸载。你在Ingress上配置好证书外部请求通过HTTPS到达Ingress后Ingress内部再用HTTP转发给后端Service。这样证书只需管理一份不用每个Pod都塞证书大幅降低了证书维护成本。2.3 方案对比一张表看懂四种方式我把四种对外访问方式的核心差异整理成一张表方便大家做技术选型时直接参考访问方式流量入口是否支持域名成本适用场景ClusterIP集群内部虚拟IP仅集群内DNS无服务间内部调用NodePort所有节点的高位端口需要自己绑定节点IP端口低临时测试、小规模服务LoadBalancer云厂商负载均衡器云厂商分配公网IP/域名高云环境下的独立服务Ingress统一入口IP/域名支持多域名路径路由中生产环境对外服务的标准方案结论很明显如果你要在生产环境里通过域名稳定访问Deployment管理的Pod优先考虑Ingress。它把域名、证书、路由规则都集中到了入口层运维成本最低扩展性最好。3. 完整实操从Deployment到域名访问的落地过程3.1 前置条件集群、域名、Ingress Controller在开始之前需要确认三个前提条件。第一你有一个可用的k8s集群不管是裸机二进制部署、kubeadm搭建还是云厂商托管集群都行本文的操作不区分集群类型命令都是一样的。第二你有一个可以配置解析的域名生产环境建议使用正规注册的域名如果只是本地测试可以用nip.io这类通配域名服务它会自动把127.0.0.1.nip.io解析到127.0.0.1省去购买域名的步骤。第三集群里已经安装了Ingress Controller。这里以NGINX Ingress Controller为例安装方式很简单官方提供了部署yaml。这里需要注意一个热词里很多人踩过的坑国内网络环境下从GitHub拉取镜像可能比较慢需要提前把镜像换成可访问的镜像源地址。安装核心命令如下kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/cloud/deploy.yaml如果你的集群环境无法直接访问GitHub可以把yaml下载到本地修改其中的镜像地址部分再apply。安装完成后确认Controller是否正常运行kubectl get pods -n ingress-nginx正常情况下你会看到名为ingress-nginx-controller-xxxx的Pod处于Running状态。这里要特别提醒这个Controller Pod默认通过hostNetwork: true运行也就是说它会直接占用节点上的80和443端口。如果你的节点上已经有其他服务占用了这两个端口Controller会启动失败这是最常见的一个前置问题后面排障章节会细说。3.2 创建Deployment和Service集群和入口就绪后接下来就是搭建真正的业务服务。我这里用一个简单的nginx示例来模拟业务Pod实际项目中替换成你自己的镜像即可。先创建DeploymentapiVersion: apps/v1 kind: Deployment metadata: name: demo-web namespace: default spec: replicas: 2 selector: matchLabels: app: demo-web template: metadata: labels: app: demo-web spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80这个Deployment会创建两个nginx Pod负责跑实际业务。注意Pod的标签是app: demo-web这个标签非常关键因为后面Service要靠它找到这些Pod。执行创建命令kubectl apply -f deployment.yaml确认Pod创建成功并处于Running状态kubectl get pods -l appdemo-web接下来创建Service。为了让外部流量能够到达这组Pod需要定义一个Service并将它的selector指向app: demo-web。这里我用ClusterIP类型因为外部的流量会先走IngressIngress再转发到Service所以Service本身不需要暴露到集群外apiVersion: v1 kind: Service metadata: name: demo-web-svc namespace: default spec: type: ClusterIP selector: app: demo-web ports: - port: 80 targetPort: 80这里的port是Service对外监听的端口targetPort是Pod容器实际监听的端口示例中两个都是80。执行创建并验证kubectl apply -f service.yaml kubectl get svc demo-web-svc你会看到Service被分配了一个ClusterIP地址。此时在集群内的任意Pod里都可以通过demo-web-svc.default.svc.cluster.local或简写demo-web-svc访问到这个Service。这里的逻辑可以理解为Service是一个固定的门牌号Pod是不断更换的新住户快递员流量只需要认识门牌号就行不用关心住户是谁。3.3 创建Ingress规则把域名指向ServiceService已经就绪现在到了最关键的一步——创建Ingress规则。Ingress要完成两件事告诉Ingress Controller“当域名是什么的时候请把流量转给哪个Service”。我以app.example.com为例apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-web-ingress namespace: default spec: ingressClassName: nginx rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: name: demo-web-svc port: number: 80这里面有几个关键点ingressClassName: nginx告诉Ingress Controller使用哪个Ingress Class对应你安装的NGINX Ingress Controllerhost指定匹配的域名pathType: Prefix表示前缀匹配/会匹配该域名下的所有路径。执行创建kubectl apply -f ingress.yaml创建之后一定要检查Ingress配置是否正确kubectl get ingress kubectl describe ingress demo-web-ingress在describe的输出里你会看到类似下面的信息Rules: Host Path Backends ---- ---- -------- app.example.com / demo-web-svc:80 (10.244.0.5:80, 10.244.0.6:80)括号里的IP列表就是后端的Pod地址如果这里没有显示Pod地址说明Service的selector没有匹配到任何Pod流量就会回到404或者503。这一步是排查问题的核心入口后面还会展开。3.4 域名解析与本地验证Ingress规则创建完成后Ingress Controller已经知道该把哪个域名的流量转到哪个Service但外部用户要访问app.example.com还需要把这个域名解析到Ingress Controller所在的节点IP或者解析到Ingress Controller前面挂的负载均衡器IP。首先找到Ingress Controller的对外访问地址。在裸机或自建集群中Controller通常直接监听节点IP的80和443端口所以域名应该解析到Controller所在节点的IP。在云环境里你通常会给Controller Service配置LoadBalancer类型此时域名要解析到云负载均衡器分配的公网IP或域名。查看方式kubectl get svc -n ingress-nginx kubectl get pods -n ingress-nginx -o wide拿到IP之后去你的DNS管理后台配置一条A记录将app.example.com指向这个IP。配置完成后可以用ping或nslookup确认解析是否生效。在正式切换线上流量之前我建议先在本地做一次Host文件级别的验证避免DNS还没生效时误判配置有误。修改本地/etc/hosts文件添加一行123.123.123.123 app.example.com然后执行curl http://app.example.com如果你看到nginx的默认欢迎页面说明整条链路已经打通域名 → DNS解析 → Ingress Controller → Service → Pod。此时再把测试用的Host记录删掉让DNS正式接管即可。3.5 补充HTTPS证书配置生产环境光有HTTP是不够的浏览器会直接拦截不安全的访问。给Ingress配置HTTPS有两种常见做法一是手动创建TLS Secret二是在集群里装cert-manager自动签发证书。这里推荐第二种因为证书到期自动续期不用人肉记着什么时候去更新。先安装cert-manager安装方式官方文档写得很清楚这里直接给部署命令kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml然后创建一个ClusterIssuer告诉cert-manager去哪申请证书以Lets Encrypt为例apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: youexample.com privateKeySecretRef: name: letsencrypt-prod-key solvers: - http01: ingress: class: nginx接着修改Ingress配置把证书签发注解和tls段落加进去apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demo-web-ingress namespace: default annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: nginx tls: - hosts: - app.example.com secretName: demo-web-tls rules: - host: app.example.com http: paths: - path: / pathType: Prefix backend: service: name: demo-web-svc port: number: 80重新apply之后等待一两分钟用下面的命令查看证书是否申请成功kubectl get certificate kubectl get secret demo-web-tls一切正常的话curl https://app.example.com就能直接访问且证书可信。这个过程里cert-manager会通过HTTP-01方式校验域名归属它会在Ingress里临时插入一条验证路径所以前提是你的域名已经正确解析到Ingress Controller了否则证书申请会一直报错。4. 实战中常见的问题与排查技巧4.1 页面一直404/503先查这条链路在实际操作中域名通了但页面打不开是最常见的问题而且404和503还不一样。404通常说明Ingress Controller收到了请求但找不到对应的后端Service503通常说明Controller找到了Service但转发不到Pod可能是Pod还没就绪或者Service没有匹配到任何Pod。拿到404先执行kubectl describe ingress demo-web-ingress看Backends那一行有没有Pod地址。如果没有Pod地址问题基本出在Service的selector写错了仔细检查Service里的spec.selector是否和Deployment的Pod模板标签一致。这里特别容易踩的坑是Deployment里标签写的是run: demo但Service里selector写的是app: demo完全对不上后端自然为空。拿到503进容器里验证一下Service是否能转发到Podkubectl get endpoints demo-web-svc如果Endpoints里有Pod IP说明Service到Pod这一段没问题如果没有检查Pod是否处于Running状态是否因为资源不足一直Pending或者容器启动失败了。记住一条原则Ingress只是路由真正的流量断点往往在Service的selector或者Pod本身所以排查时要沿着“Ingress → Service → Endpoints → Pod”这条链路一层层往下看哪里断了修哪里。4.2 集群内DNS解析异常的排查内部DNS也是容易出问题的地方。有时候外部域名访问都通了但Pod之间互相调用时却解析不了Service名比如A Pod里执行curl http://demo-web-svc报错“could not resolve host”。这种情况基本可以锁死是CoreDNS的问题。先看CoreDNS Pod是否正常运行kubectl get pods -n kube-system -l k8s-appkube-dns如果Pod正常进入一个业务Pod里测试DNS解析kubectl run -it --rm test-pod --imagebusybox -- sh nslookup demo-web-svc.default.svc.cluster.local如果解析不了看看Pod里的/etc/resolv.conf配置正常情况下它应该指向kube-dns.kube-system.svc.cluster.local。还有一个很隐蔽的问题如果集群的clusterDomain不是默认的cluster.local那么Service的完整域名也会跟着变需要检查集群初始化时的配置。二进制搭建的集群尤其常见这种定制化配置查完就清楚了。4.3 证书和Ingress配置的常见坑证书这块踩的坑也不少。最典型的问题是cert-manager一直不生成证书kubectl describe certificate里报错“Failed to verify domain”。这种情况先确认域名解析是否已经生效因为HTTP-01验证必须让CA服务器能访问到你的域名如果你还在用本地hosts测试CA服务器根本访问不到自然验证失败。还有一个坑是Ingress的secretName和已有Secret重名。如果集群里已经存在一个同名的Secretcert-manager可能不会覆盖它导致证书一直是旧的。遇到这种情况把旧的Secret删掉再重新申请即可。另外如果你在一个Ingress里配置了多个域名需要确保每个域名都写进tls的hosts列表否则某个域名会一直报证书不匹配。4.4 上线后性能与稳定性调整建议链路通了之后我会建议做几件锦上添花的事。第一给Ingress Controller的Service配置外部负载均衡时不要只挂一个节点最好在云厂商负载均衡层做健康检查后面挂多个Controller副本避免单点故障。裸机集群可以通过kubectl scale deployment ingress-nginx-controller -n ingress-nginx --replicas3扩容。第二静态资源类请求可以在Ingress层直接开启gzip压缩减少带宽消耗。NGINX Ingress Controller默认支持可以通过ConfigMap配置gzip-enabled: true。第三如果你有大量读请求可以考虑在Service后面加HPA自动扩容Deployment配合HPA能实现在入口流量增加时自动拉起更多Pod这对保持访问体验很有帮助。曾经遇到一个场景就是热词里提到的“2c4g的Pod能支撑多少并发”这个没有标准答案不同业务的资源消耗差异极大但我建议先压测再上生产别靠猜。5. 踩坑几次之后的个人体会文章写到最后分享一下我自己的实际操作体会。早期我图省事服务直接用NodePort暴露端口从30001排到32000后来服务一多端口和IP的对应关系乱得像密码本同事问我要某个服务的访问地址我得翻半天表格。切到Ingress之后所有服务都收敛到一个入口IP用户只记域名就行不管是新增服务还是调整路径改一份Ingress配置就能搞定运维体验提升了好几个档次。还有一个很深的感触是在k8s里排查问题不要凭感觉跳步骤。很多人在浏览器打开域名发现404第一反应是去改Ingress规则改来改去还是404最后发现是Service的selector少写了一个标签。我的习惯是严格按照“DNS解析 → Ingress → Service Endpoints → Pod运行状态”的顺序逐层验证每一步都有对应的命令、有明确的输出看到哪个环节不对再动手修这样效率最高。最后再分享一个小技巧每次改完Deployment、Service或Ingress的yaml先用kubectl describe看一眼Events比什么都管用往往几秒钟就能定位问题。希望这篇文章能帮你在k8s域名访问这条路上少走一些弯路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进