K8s集群部署Traefik Ingress Controller实战:从NodePort到Python服务验证
2026/9/11 4:11:50 网站建设 项目流程

先交代一下背景。我之前在本地虚拟机里折腾K8s集群,应用Pod跑起来了,Service也建了,结果在浏览器里怎么都访问不到服务。折腾了一圈才发现问题出在入口流量这一层——NodePort暴露端口只是临时方案,真正要在集群里把HTTP服务稳定地暴露出去,还是得靠Ingress Controller。这篇文章就围绕这个话题,讲讲我在K8s集群里部署Traefik,并用一个Python HTTP服务做端到端验证的完整过程。适合刚把K8s集群搭起来、正准备接入真实业务流量的朋友参考,整个流程从零开始,不需要你之前接触过Traefik。

1. 为什么我把入口流量交给Traefik而不是暴露一堆NodePort

很多初学者(包括曾经的我)在集群里跑起第一个服务时,第一反应就是给Service配一个NodePort,浏览器里用http://节点IP:端口访问。这种方式做实验没问题,一旦服务数量多起来,各种问题就暴露了:端口管理混乱、高并发下NodePort性能衰减、缺失七层路由能力(比如按域名分流、按路径转发)。我选Traefik的核心理由就在这——它是K8s生态里最成熟的七层入口网关方案之一。

1.1 NodePort、LoadBalancer和Ingress到底有什么区别

先理清概念。NodePort是K8s里最朴素的暴露方式,它在每个节点上开一个端口(默认范围30000-32767),流量从节点的这个端口转给后端Service。它的优点就是简单,缺点是端口资源有限,而且业务层根本不知道来的是什么协议、什么域名。

LoadBalancer一般用在云环境,云厂商会给你创建一个负载均衡器,外部流量先进LB再进节点。但是在本地虚拟机、裸机环境或者自建机房,没有云厂商提供的LB组件,Service的EXTERNAL-IP就会一直处于<pending>状态。

Ingress则是K8s的七层流量入口抽象,它不像NodePort那样需要为每个服务分配端口。Ingress Controller本身是一个运行在集群里的Pod,它只有一个入口(通常是80/443端口),根据Ingress规则把请求按域名、按路径转发到对应的Service。这样你只需要维护一个外部入口,后面接多少个服务都行。Traefik就是这类Ingress Controller的实现之一。

1.2 Traefik和其他Ingress Controller选型对比

我自己实际比较过几种主流的Ingress Controller,这里直接说结论:

方案配置方式学习曲线适合场景
TraefikIngressRoute CRD、Helm Values中小集群、动态服务多的场景,配置直观
Nginx IngressIngress YAML、Annotations大流量、需要精细Nginx调优的场景
云厂商自带LB控制台或IaC工具纯云环境、不折腾集群的时候

Traefik有个很独特的优势:它自己就是一个反向代理服务,后端服务注册后自动发现,几乎不需要重启。它的IngressRoute自定义资源定义(CRD)写起来比传统Ingress的Annotation清晰很多。而且自带的Dashboard把后端服务状态、路由规则、请求统计可视化,排查问题的时候直接看面板就能定位。这些特性在调试阶段帮了大忙。

2. 实验环境搭建:用最简单的方式跑起一个能用的K8s集群

开始部署Traefik之前,得先有一个状态正常的K8s集群。这里我不展开讲生产级集群(比如二进制部署或者kubeadm高可用)怎么搭,那些内容篇幅太长,只说实验环境怎么快速跑起来。

2.1 本地实验环境的选型

如果你的目标只是学习和验证,我建议不要一上来就搞三节点甚至五节点的集群,一台2核4G的Linux虚拟机就够了。本地实验环境我推荐两种方案:

  • k3s:Rancher出品的轻量K8s发行版,安装包小,内存占用低,对硬件要求低,默认自带Traefik作为Ingress Controller。很多人在本地用k3s替代完整K8s做开发测试,它和标准K8s的API兼容性做得很不错。
  • minikube:官方提供的单机K8s环境,功能完整,适合跑Kubernetes原本的样子,但资源占用比k3s高一些。

我这次用的是k3s。因为k3s自带的Service LoadBalancer实现在单节点下很好用,后面验证Traefik的LoadBalancer类型Service时不用额外折腾。

2.2 集群安装与基础验证

k3s的安装非常简单,一条命令就搞定:

curl -sfL https://get.k3s.io | sh -

装完之后,检查集群状态:

sudo k3s kubectl get nodes

如果你是把k3s作为服务直接安装,那么kubectl配置会自动生成在/etc/rancher/k3s/k3s.yaml。有些版本为了方便,我把kubectl命令做了软链或者配置KUBECONFIG环境变量。之后所有kubectl操作都指向这个k3s集群。

集群起来之后先做一个基线验证——在default命名空间跑一个临时的nginx,确认Pod能正常调度、Service能正常解析:

kubectl run test-nginx --image=nginx kubectl expose pod test-nginx --port=80 kubectl get svc test-nginx

这时候能看到CLUSTER-IP,说明集群内部网络正常。这一步虽然简单,但能帮你把"集群本身有问题"和"后面部署的组件有问题"这两个变量隔离。

注意:k3s自身默认已经装了一个Traefik,如果不需要它自带的,可以在安装时通过--disable traefik参数禁用。为了实验完全可控,我自己更习惯在安装时禁用自带的Traefik,然后手动用Helm装一个,下面所有步骤都是在这个前提下进行的。

3. Helm一条命令装好Traefik:参数背后的含义要搞清楚

部署Traefik的方式有好几种,可以直接用kubectl apply -f,也可以用Helm Chart。我的建议是:除非你只是临时看看效果,否则一律用Helm管理。原因很简单——Traefik的配置项实在太多,纯手工维护YAML不现实,Helm让你用一条命令就能升级、回滚、传参。

3.1 为什么要用Helm Chart方式部署

Helm是K8s的包管理工具,打个比方:如果kubectl是"直接编辑文本文件",Helm就是"用模板批量生成文本文件"。Traefik官方维护的Helm Chart把Deployment、Service、RBAC、CRD安装等全部封装好了,你只需要关心几个核心参数,它还自动帮你装好IngressRoute要用到的CRD。

如果你还没装Helm,先装一下:

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh

装好之后添加Traefik的Chart仓库:

helm repo add traefik https://traefik.github.io/charts helm repo update

3.2 核心values参数逐项说明

安装之前,我做了一个traefik-values.yaml文件,把关键的配置参数拆开说明一下:

service: type: LoadBalancer spec: loadBalancerIP: "" # 本地环境为空,让k3s自动分配 ingressRoute: dashboard: enabled: true # 启用Dashboard路由 healthCheck: enabled: true # 开启健康检查路径 dashboard: enabled: true domain: traefik.local.example # Dashboard的访问域名 logs: access: enabled: true # 开启访问日志,排查问题必备

这里几个参数我展开说一下:

  • service.type: LoadBalancer:在k3s环境下,它会自动创建Service LoadBalancer(k3s内置了对应的负载均衡器实现),外部流量能直接到达Traefik Pod。在标准K8s集群里如果没有LB组件,这里可以临时改成NodePort,后面我们验证阶段用NodePort访问也没问题。
  • ingressRoute.dashboard.enabled:Traefik的Dashboard是它自己的一个Web管理界面,可以通过IngressRoute规则暴露出来。调试的时候开着它,可以实时看到路由规则和后端健康状态。
  • logs.access.enabled:这一项强烈建议从一开始就打开。Traefik的访问日志会记录每个请求的Host、路径、状态码、转发时间。后面排查"为什么转发失败"的时候,这个日志就是第一手证据。

执行安装命令:

helm install traefik traefik/traefik -n traefik --create-namespace -f traefik-values.yaml

3.3 验证Traefik是否正常启动

安装完成后,检查Pod和Service状态:

kubectl get pods -n traefik kubectl get svc -n traefik

正常情况下,Pod的STATUS是Running,Service的外部IP(如果是k3s环境)会变成192.168.x.x或类似地址。如果Service的EXTERNAL-IP一直是<pending>,大概率是你的集群没有LoadBalancer实现。这时候把traefik-values.yaml里的service.type改成NodePort重新upgrade,也能继续后面的验证。

验证Traefik的Web入口是否正常,可以先看Pod日志:

kubectl logs -n traefik deploy/traefik --tail=20

看到类似Started v3.x的日志,说明Traefik进程已经起来了。这时候访问你的节点IP的80端口,如果返回404(或者403、503,取决于有没有匹配的路由),说明Traefik已经在监听流量了——因为没有任何路由规则时,它不会直接把请求吞掉,而是返回一个错误页。这个现象反而是好现象,说明网关本身在工作,只是路由还是空的。

4. 写一个能用Docker跑起来的Python HTTP服务

Traefik装好了,相当于给K8s集群开了一个"门口",现在得有一个实际业务服务来验证这个门口好不好使。我用Python写了一个极简的HTTP服务,目的就一个:方便验证从浏览器到Ingress再到后端Pod的整条链路是否打通。

4.1 一组不到20行的Flask服务代码

我是用Flask写的,因为它的路由逻辑直观,依赖也轻,在容器里跑很省心。app.py内容如下:

from flask import Flask, request, jsonify import os import socket app = Flask(__name__) @app.route("/") def index(): return jsonify({ "message": "Hello from Python HTTP Service", "hostname": socket.gethostname(), "path": request.path, "client_ip": request.remote_addr }) @app.route("/healthz") def healthz(): status = {"status": "ok"} return jsonify(status) if __name__ == "__main__": port = int(os.environ.get("PORT", "8000")) app.run(host="0.0.0.0", port=port)

/healthz是给K8s探活用的,后面配Deployment时会用到。/接口返回了容器的主机名和请求信息,验证负载均衡效果的时候,你多访问几次就能看到不同的hostname,说明流量被分发到了不同的Pod副本。

4.2 镜像构建的注意事项

Dockerfile同样很简单:

FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . ENV PORT=8000 EXPOSE 8000 CMD ["python", "app.py"]

requirements.txt内容:

flask==3.0.0

构建镜像:

docker build -t flask-hello:v1 .

这里要特别提醒一个在K8s环境里踩过的坑:如果你是在单节点上跑k3s(尤其是用containerd作为运行时),构建完镜像之后需要确认k3s能"看到"这个镜像。k3s默认使用containerd而不是docker,所以docker images里看到的镜像,k3s那边不一定能拉取到。最简单的办法:要么把镜像推到一个私有仓库(比如registry),要么把镜像导出/导入到k3s的运行时里,要么干脆用我下面的方式——在Deployment里指定imagePullPolicy: IfNotPresent,并且确保镜像名和k3s节点本地镜像一致。

因为是单节点实验环境,如果容器运行时和k3s的运行时是同一套,一般docker build后K3s能直接使用。如果不一致,使用k3s ctr images import或者docker save | k3s ctr images import把镜像导入进去:

docker save flask-hello:v1 | sudo k3s ctr images import -

4.3 Deployment与Service的定义要点

我写了一个python-http.yaml,包含Deployment和Service两部分:

apiVersion: apps/v1 kind: Deployment metadata: name: python-http-app labels: app: python-http-app spec: replicas: 2 selector: matchLabels: app: python-http-app template: metadata: labels: app: python-http-app spec: containers: - name: python-http-app image: flask-hello:v1 imagePullPolicy: IfNotPresent ports: - containerPort: 8000 env: - name: PORT value: "8000" readinessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 3 periodSeconds: 5 livenessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 10 periodSeconds: 10 --- apiVersion: v1 kind: Service metadata: name: python-http-app-svc labels: app: python-http-app spec: type: ClusterIP selector: app: python-http-app ports: - name: http port: 80 targetPort: 8000

readinessProbelivenessProbe一定要配置。特别是readinessProbe——Traefik自动发现后端时会检查Pod的就绪状态,如果后端Pod一直处于未就绪状态,流量就会504。这种小问题在裸奔的演示中很容易被忽略,生产上却是第一道保命符。

Service端口我用了port: 80, targetPort: 8000,意思是Service内部暴露在80,转发到Pod的8000。IngressRoute后面匹配Service时用的是这个80端口。

应用并确认状态:

kubectl apply -f python-http.yaml kubectl get pods -l app=python-http-app kubectl get svc python-http-app-svc

两个Pod都进入Running并且READY 2/2之后,整个后端服务就绪。

5. 把服务挂到Traefik上:IngressRoute的完整写法

Traefik支持传统K8s的Ingress资源,也支持它自己的IngressRoute CRD。我强烈建议用IngressRoute,因为它的表达能力更强,语义也更清晰。一个路由规则包含三个核心部分:入口点(entryPoint)、匹配规则(match)、转发目标(forwardTo)。

5.1 IngressRoute的字段拆解

下面是我用来暴露Python服务的IngressRoute定义:

apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: python-http-app-route namespace: default spec: entryPoints: - web routes: - kind: Rule match: Host(`hello.local.test`) && PathPrefix(`/`) services: - name: python-http-app-svc port: 80

拆开解释:

  • entryPoints: [web]:Traefik默认有两个入口点,web对应80端口,websecure对应443端口。我们需要HTTP访问,所以绑到web
  • match: Host(\hello.local.test`):当请求的Host头是hello.local.test时命中这个路由。如果只有一个服务、不做多域名分流,你也可以直接匹配PathPrefix(`/`)`。但多域名场景下,Host匹配是必备的。
  • services.name / port:流转发的目标Service名称和Service端口(不是Pod端口,注意区分)。这里指向python-http-app-svc:80

应用它:

kubectl apply -f ingressroute.yaml

应用完成后,可以查看Traefik自动发现到的后端状态。在Dashboard页面(下一节讲怎么访问)的HTTP路由部分,应该能看到新注册的hello.local.test路由,目标指向default-python-http-app-svc-80这样的内部名称。

5.2 域名与流量的对应关系

在本地实验环境,通常没有真实域名,也不方便改DNS。有两个办法:

  1. 改本机/etc/hosts,把节点IP映射到hello.local.test
  2. 使用curl --resolve参数,临时指定域名解析

我推荐第二种,验证完不用留痕迹:

curl --resolve hello.local.test:80:192.168.1.100 http://hello.local.test/

其中192.168.1.100是运行K3s的节点IP。这样不需要动系统文件,很方便。

6. 端到端验证:从Pod一路追踪到浏览器请求

部署完成后最兴奋的一步就是把整条链路验证通。这部分我分了三层验证:先验证集群内部Service,再通过Traefik做外部访问,最后用Dashboard和日志确认转发细节。

6.1 第一层验证:集群内部访问Service

在进入Traefik之前,先排除后端问题。进入一个临时Pod,直接请求Service:

kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- curl http://python-http-app-svc:80/

如果配置没问题,返回内容应该是带hostname字段的JSON。这一步通过,说明Deployment里的Pod、Service的标签选择器、端口映射都是对的。如果这里就卡住了,根因多半是标签选择器写错,Service选不到Pod。

6.2 第二层验证:通过Traefik外部访问

Service内部通了,接下来验证Traefik入口。先获取Traefik的访问地址:

kubectl get svc -n traefik

如果是LoadBalancer类型,EXTERNAL-IP就是入口IP;如果是NodePort类型,则是端口。执行:

curl --resolve hello.local.test:80:EXTERNAL_IP http://hello.local.test/

看到返回的JSON里message字段是Hello from Python HTTP Service,链路就通了。为了确认负载均衡是否正常工作,多执行几次,观察返回的hostname字段变化:

for i in {1..5}; do curl --resolve hello.local.test:80:EXTERNAL_IP http://hello.local.test/ | grep hostname; done

如果两个Pod的hostname交替出现,说明Traefik在多个后端副本之间做了轮询负载均衡。

6.3 结合Dashboard确认路由与后端健康状态

Traefik的Dashboard是通过IngressRoute暴露的,我前面安装时开启了dashboard。配置一个访问Dashboard的路由:

apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: traefik-dashboard namespace: traefik spec: entryPoints: - web routes: - kind: Rule match: Host(`traefik.local.example`) services: - name: api@internal kind: TraefikService

然后访问http://traefik.local.example(同样用--resolve参数解析到节点IP)。Dashboard首页的"HTTP Routers"列表里可以看到所有注册的路由,包括hello.local.testtraefik.local.example。点击路由详情,还能看到Service的健康状态、请求次数和响应状态码分布。如果哪个后端的健康检查失败,面板上会直接标红。

6.4 从访问日志验证转发细节

Dashboard显示的是聚合后的数据,想看单条请求的流转细节,还得翻Traefik的访问日志:

kubectl logs -n traefik deploy/traefik --tail=20

再次请求一次:

curl --resolve hello.local.test:80:EXTERNAL_IP http://hello.local.test/healthz

日志里会出现类似下面的一行:

10.42.0.1 - - [date] "GET /healthz HTTP/1.1" 200 12 "-" "curl/x.x.x" "python-http-app-svc-xxx" "192.168.1.100:8000"

关键信息是最后两段:python-http-app-svc-xxx是实际被选中的后端Pod名,末尾的192.168.1.100:8000是Pod的IP和端口。这一行日志完整印证了整个转发链路——请求先进Traefik,再被转发到Service匹配到的具体Pod。

7. 部署过程中我踩过的坑和排查思路

最后这部分是全文最有价值的章节。我在这个实验里故意留了一些"坑",也遇到了一些原本没预料到的问题。整个排查过程记录下来,希望能给你省下几个小时的折腾时间。

7.1 问题一:访问IngressRoute返回404,但Dashboard里路由是存在的

这个坑非常典型。现象是:在Dashboard里能看到hello.local.test这个路由,状态也正常,但用curl访问时返回404。

排查过程:

首先确认命中的规则。Traefik匹配路由时要求Host头和Path都匹配,我只配了Host(\hello.local.test`),请求时也确实带了Host头。那问题出在哪?

逐步排查:

  1. 查看Traefik访问日志,发现请求进来后,Traefik匹配到的路由是traefik-dashboard那条,而不是我新加的python-http-app-route——因为traefik.local.examplehello.local.test都挂在同一个web入口点上,而curl默认带的Host是hello.local.test,理论上应该命中路由B。接着我看了一下IngressRoute的命名空间,果然是在default命名空间,Traefik也能跨命名空间发现。问题只剩一个可能——请求的目标IP不对。

  2. curl请求时解析的EXTERNAL_IP192.168.1.100,但实际上k3s的Service LoadBalancer把流量导到了另一个节点的IP上,也就是我访问的IP和Traefik Service的ExternalIP不一致。补上--resolve参数指向正确的ExternalIP后,请求立即返回200。

这个问题的根因不算复杂,但过程很值得记住:Dashboard里路由存在,只代表Traefik注册了这个路由,不代表你访问的IP/域名组合一定命中了它。排查时先确认"访问入口对没对上",再谈"路由匹配对不对"。

7.2 问题二:Pod显示Running但IngressRoute后端标红

有几次我改了Deployment的镜像版本,Pod重建后一直显示Running,但Dashboard里后端的健康状态是红色。去查发现readinessProbe一直没过——原来我改镜像时改了应用监听端口为8080,但Probe还指向8000。这种问题在真实项目中特别容易发生,因为应用是"活的"(livenessProbe没过会重启),但对外表现为不可服务(readinessProbe没过则不会有流量进来)。

排查和修复方式很简单:

kubectl describe pod -l app=python-http-app | grep -A10 Readiness

看到Readiness probe failed的具体错误,调整端口或者Probe路径,重新apply Deployment即可。这再次印证了UI里看到的健康状态其实就是K8s探活/就绪探针的结果,IngressRoute的后端状态完全取决于这个。

7.3 问题三:NodePort模式下,外部IP拿不到

如果你用的是标准K8s集群(没有LoadBalancer实现),把Traefik的Service改成NodePort后,EXTERNAL-IP自然永远拿不到,这是正常的。访问方式不是http://外部IP:80,而是http://节点IP:NodePort端口

kubectl get svc -n traefik

PORT(S)列显示80:31563/TCP,那就用http://192.168.1.100:31563访问。注意此时浏览器的Host头是节点IP,和hello.local.test不匹配。所以curl时仍然需要加上--resolve hello.local.test:31563:192.168.1.100。NodePort模式下端口是随机分配的,每次重建可能变,建议在Values里手动指定一个固定NodePort端口,比如:

service: type: NodePort spec: ports: - port: 80 nodePort: 31800

这样调试时就不用每次去看分配的端口了。

7.4 问题四:Python版本不一致导致的Local镜像拉取失败

如果你在构建镜像时用的本机Python版本和容器里声明的不一致,pip install可能失败。我解决这类问题最稳妥的方式是锁定python:3.11-slim基础镜像,本机开发环境只负责写代码,构建和运行完全以Docker镜像为准。K8s环境里最怕"在我电脑上明明好的"这种问题,容器化第一次部署就严格用干净基础镜像构建,后面能少踩很多坑。

另外,如果你改了代码却看不到效果,大概率是镜像构建后没有正确导入k3s运行时。这个问题我在4.2节就说了,单节点本地环境建议直接在节点上执行docker save | k3s ctr images import -,或者使用imagePullPolicy: IfNotPresent配合正确导入,确保用的就是新构建的镜像。

结尾:这套组合跑通之后,下一步怎么扩展

整个环境的链路现在是完整的:外部HTTP请求 → Traefik(LoadBalancer/NodePort入口)→ IngressRoute路由匹配 → Service发现Pod → Python Flask返回JSON数据。这套组合跑通之后,后面在这个集群里加新服务就是流水线操作——写Deployment + Service,写一条IngressRoute,Dashboard里刷新一下就能看到路由自动注册,不需要重启Traefik,也不需要改任何全局配置。这也是我在本地实验环境一直偏用Traefik的原因,它的动态发现特性对频繁更换调试服务来说太友好了。

最后分享两个小技巧。第一,Traefik的访问日志在排查问题时有不可替代的价值,凡是遇到"我配置了为什么还是不通",先翻日志定位请求到底匹配到了哪个路由、转发到了哪台Pod。第二,k3s自带的Traefik如果和你手动安装的有冲突,强烈建议直接从源头禁掉,否则排查问题时会出现"我以为用的是这个,实际用的是另一个"的混乱局面。现在这套流程已经成了我本地开发环境的标配,希望这篇记录也能让你少走几步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询