ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python应用容器化实战:从Docker到Kubernetes的部署指南

Python应用容器化实战:从Docker到Kubernetes的部署指南 写Python写了好几年最让我头疼的从来不是语言本身而是在我电脑上能跑这句话。本地开发环境好不容易跑起来的服务交给别人一部署就崩要么Python版本对不上要么系统库缺失要么MySQL连不上每次都要把环境问题重新踩一遍。后来我把Python应用容器化用Docker打包成镜像、用Docker Compose编排本地依赖服务、再放到Kubernetes集群里统一调度才发现这套组合拳真正解决了Python项目从开发到上线的最后一公里。这篇文章不打算讲太多虚的就从Python开发者的实际痛点出发把Docker和Kubernetes这条链路拆开揉碎讲清楚每一步为什么这么做、实际操作会遇到哪些坑、怎么排查。无论你是刚入门Python、还在折腾Python安装环境的初学者还是已经能用Flask/FastAPI写接口、想把服务正经部署上线的开发者这篇都会有你能直接拿去用的东西。1. 先把Python应用装进容器Dockerfile的正确打开方式容器化的第一步不是去学Kubernetes而是把Python应用先装进一个标准化的容器里。一个能跑起来的镜像干净的依赖环境加上可复现的启动命令后面所有编排和调度都建立在这块地基上。这里最常见的三个坑我至少帮人排查过几十回基础镜像选错、依赖反复安装、容器里用root乱跑。1.1 基础镜像选型全量、slim还是alpine很多Python开发者的第一个Dockerfile上来就是FROM python:3.12这是最省事但也是最糙的选择。python官方镜像分好几个变体选型差别直接决定镜像体积和编译各种Python包时的体验选错了一个pandas或psycopg2能把你编译到怀疑人生。镜像变体基础系统体积参考libc适合场景python:3.12Debian完整版340MBglibc兼容性最好但体积大python:3.12-slimDebian精简版120MB~150MBglibcWeb服务首选python:3.12-alpineAlpine Linux50MB~80MBmusl极致瘦身但有ABI风险我自己的选择标准很简单默认用slim碰都不碰alpine除非这个服务就是一个纯标准库脚本、一个第三方依赖都没有才考虑alpine。原因在于Python生态里有大量带C扩展的包像numpy、pandas、psycopg2、lxml、bcrypt这类。它们大多数是编译好的wheel包而wheel是区分平台和libc的。Alpine用的是musl libc很多wheel包没有musl版本pip会当场给你编译慢不说还经常缺编译工具链。glibc系的slim镜像里几乎常见的包都有预编译wheelpip install就是解压复制秒装完。用一句话给新手解释全量镜像像精装修房子啥都有但搬起来费劲slim像简装房基础功能齐全又不浪费空间alpine像毛坯房看着小装修编译依赖全靠你自己。日常基于slim去加你需要的东西是最平衡的做法。1.2 依赖层的缓存顺序一行代码引发的重装地狱刚开始写Dockerfile很多人喜欢图省事把依赖安装和代码复制写在一起FROM python:3.12-slim WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD [python, main.py]这个写法在功能上没问题但会让每次构建都遭受重装地狱——因为Docker的镜像是一层一层叠加的COPY的改动会使得该层以及之后的所有缓存层全部失效。这段代码里COPY把所有代码都放进了一层然后RUN pip install相当于在代码层之上。于是只要你的Python代码改了一个字符整个层缓存失效Docker就要重新执行pip install把依赖全部装一遍。正确的姿势是充分利用层缓存把不常变动的依赖和经常变动的代码分开FROM python:3.12-slim ENV PIP_NO_CACHE_DIR1 \ PIP_DISABLE_PIP_VERSION_CHECK1 WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD [python, main.py]这样拆分之后只要requirements.txt没变pip install这层缓存就一直有效构建一次之后后续改代码再构建只需要重新COPY代码那一层构建时间从几分钟直接降到十几秒。想再进一步提速的话Docker BuildKit还支持为pip的下载缓存单独开一个持久化的cache挂载点加上这个参数之后即使requirements.txt变动了之前下过的wheel包也还在# syntaxdocker/dockerfile:1 RUN --mounttypecache,target/root/.cache/pip \ pip install -r requirements.txt我个人在实际项目里的体会是先COPY依赖文件再安装、后COPY源代码这个顺序对开发体验的提升非常明显。团队里每次改完接口都要重新构建镜像快慢直接影响大家的效率和心情。1.3 镜像里别用root跑非root用户和PID 1问题镜像能跑起来和应该这样跑是两回事。默认情况下容器内就是root用户这就带来一个实际风险如果应用被攻破攻击者拿到的就是容器内的最高权限配合不恰当的挂载卷比如把宿主机目录挂进去了容器逃逸的风险会成倍上升。安全加固的常规做法是在Dockerfile里创建专用用户再用USER切换让Python进程以非root身份运行FROM python:3.12-slim RUN groupadd -r appuser useradd -r -g appuser -u 1000 appuser WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . USER appuser EXPOSE 8000 CMD [python, main.py]这里有个新手容易踩的坑切换成非root用户之后如果代码里有依赖运行期写文件的逻辑比如写日志、生成缓存而工作目录的所有者不是这个用户就会报PermissionError。我一般会在Dockerfile里先建好目录并归属给appuserRUN mkdir -p /app/logs chown -R appuser:appuser /app还有一个特别容易被忽略的细节CMD的写法。Dockerfile里的CMD有两种形式——exec形式和shell形式。exec形式是CMD [python, main.py]shell形式是CMD python main.py。exec形式会直接让python进程成为容器内的1号进程PID 1这样才能正确接收来自Docker和Kubernetes的SIGTERM信号实现优雅退出。shell形式则会在中间多夹一层shell信号传递会被干扰前面提到的容器和本地行为不一致往往就是这么来的。2. 本地开发绕不开的组合拳Docker Desktop与Compose实战镜像写好了接下来就要在本地把应用跑起来。作为Python开发者你的服务大概率不是孤零零的——要连MySQL要接Redis。如果MySQL和Redis也直接装在本地电脑上团队里每个人环境都不一样典型的在我电脑上能跑就又回来了。用Docker Compose把这些依赖服务和应用一起编排起来是本地开发最顺手的方案。2.1 Docker Desktop启动失败的常见原因与排查思路Windows上装Docker Desktop我遇到最多的问题就是启动时报错Docker Desktop failed to start because virtualisation support wasnt detected。很多人看到这个报错就慌了其实排查起来按顺序来就行进任务管理器切换到性能标签看CPU一栏右下角虚拟化是否显示已启用。如果显示已禁用去BIOS里把Intel VT-x或AMD-V打开。有些品牌的机器BIOS里虚拟化选项藏得比较深搜索虚拟化或Virtualization关键词慢慢找。如果虚拟化已经启用还报错多半是WSL2的问题。在PowerShell里跑wsl --status看看状态如果是WSL1执行wsl --update升级到WSL2。Docker Desktop的Settings里把Use the WSL 2 based engine勾选上而不是用旧的Hyper-V后端。新版Docker Desktop默认就是WSL2后端但如果你从旧版本升级上来这个设置可能会被保留成Hyper-V。这个报错里还有一个很隐蔽的坑Windows的内核隔离功能基于虚拟化的安全VBS会和Docker Desktop产生冲突。如果你按前面步骤排查完还是启动失败去Windows安全中心-设备安全性-内核隔离把内存完整性关掉再试试。关掉VBS之后Docker Desktop大概率就能正常启动了。Docker Desktop好不容易装好我建议顺手在设置里把资源上限调整一下。不少PC默认给WSL2的内存上限只有2GB如果你本地要同时跑Python服务、MySQL、Redis内存很容易不够。一般给WSL2分4GB~6GB比较舒服在Settings的Resources里调整。2.2 Compose编排Python应用、MySQL、Redis一次拉起本地开发玩Docker Desktop最常用的其实不是docker run而是docker compose。写一个docker-compose.yml把应用、MySQL、Redis全部编排起来一条命令全部启动。services: app: build: . ports: - 8000:8000 environment: MYSQL_HOST: mysql MYSQL_PORT: 3306 REDIS_HOST: redis REDIS_PORT: 6379 depends_on: mysql: condition: service_healthy redis: condition: service_healthy volumes: - .:/app mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: example MYSQL_DATABASE: appdb ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 redis: image: redis:7-alpine ports: - 6379:6379 healthcheck: test: [CMD, redis-cli, ping] interval: 5s timeout: 3s retries: 10 volumes: mysql_data:这里有一个值得认真理解的地方为什么Python代码里连接MySQL和Redis时host要写mysql和redis因为在同一个Compose网络里服务名就是其他服务访问你的域名Docker内置DNS会自动做解析。你在代码里配MYSQL_HOSTmysql就行不同机器上这个配置完全通用不用关心MySQL跑在哪台机器上。depends_on加上condition: service_healthy的写法也很关键。如果只写depends_on: [mysql, redis]Compose只保证MySQL容器启动了不保证MySQL已经准备好接受连接。Python应用启动早于MySQL就绪时连库直接报错。加了healthcheck之后Compose会等MySQL的mysqladmin ping检查通过再启动你的应用。数据库容器要不要把端口映射到宿主机我的建议是本地开发为了方便调试映射一下可以接受但如果是生产环境接的数据库不通过Compose管理那就算了。此外数据库数据一定要用命名卷挂载否则容器删除之后数据全丢这是本地开发最容易出的惨案。2.3 容器里跑Python和本地的几个差异点哪怕你的Dockerfile和Compose没任何问题容器里跑Python应用和本地直跑依然会有若干让你困惑的行为差异时区差异基础镜像默认是UTCPython的datetime.now()拿到的和本地差8个小时查日志时对不上时间。在Dockerfile里加一句ENV TZAsia/Shanghai或者在Compose里的environment里设置TZ。代码挂载与权限Compose里通过volumes把当前目录挂到容器/app宿主机上文件属主不是容器里那个appuser的话Python进程可能会写不了文件。如果只是本地开发不想纠结权限可以暂时不用USER指令生产镜像则必然用非root用户配合宿主机目录权限单独处理。pycache权限问题挂载代码进容器后Python运行时会在目录里生成__pycache__而容器内appuser对宿主机目录没有写权限时Python就会悄悄把字节码缓存写到其他位置或者提示缓存错误。这也是本地能跑、容器里报错的经典来源之一。开发阶段我在Compose里用root跑不纠结这个生产环境则把缓存开关关掉或确保目录权限正确。遇到容器里行为和本地不一致的问题第一反应不是瞎改代码而是进入容器内部去看docker exec -it container_name bash进去之后挨个看工作目录、用户身份、环境变量、Python版本、pip list差异通常一眼就能看出来。查完再退出去改配置和Dockerfile。3. 从Docker到Kubernetes先换掉这套心智模型Docker和Compose用顺手之后你会遇到下一个问题这些容器是在单台机器上跑的。假如宿主机挂了、内存不够了、需要在几台机器上同时部署同一套服务Compose就完全不够用了。Kubernetes解决的是集群层面的容器编排问题但它不是Docker的上级也不是多机版Compose它引入了全新的抽象模型需要你把思维切过来。3.1 为什么我们需要Kubernetes它和Docker的边界在哪在单机场景下Docker像一家只经营一家门店的公司所有店员容器都在这一家店里工作店门关了宿主机宕机所有店员都没法营业。数据要扩容就只能给这家门店拼命加货架加大机器配置这就是所谓的垂直扩容有上限钱花了也不一定扛得住。Kubernetes则是一个总部它能同时管理几十上百家门店节点哪家门店倒了总部把店员Pod调度到还在营业的门店里去顾客突然增多总部自动多派店员过去水平扩容店员挂了总部自动补一个一模一样的店员进去。所以Docker和Kubernetes不是替代关系。Docker负责构建镜像、本地运行容器Kubernetes负责的是把这些容器放到一个资源池里自动调度、自动恢复、自动伸缩。可以理解为Docker解决了怎么打包和运行Kubernetes解决了怎么管理和保证运行时的稳定性。3.2 从Compose的思维切换Pod、Deployment、Service、Ingress刚开始学Kubernetes很多人用Compose的概念去套结果一头雾水。这里有一组最基础的概念对应关系可以帮助快速建立框架Docker Compose概念Kubernetes概念作用service服务PodKubernetes最小的调度和运行单元服务副本Deployment定义Pod模板、期望副本数、滚动更新策略服务名/DNSService为Pod集群提供稳定的访问入口和负载均衡端口映射Ingress将集群外流量按域名/路径路由到Serviceenvironment环境变量ConfigMap / Secret配置和敏感信息管理数据卷PersistentVolume / PVC有状态数据的持久化存储健康检查Probe探针就绪、存活、启动检测这里最重要的一个思维切换是Pod才是Kubernetes里的最小调度单元而不是容器。一个Pod里面可以放一个容器也可以放多个容器。为什么要搞出Pod这么一层因为有些场景下我们需要把两个容器绑在同一个宿主机上运行共享网络、共享存储比如只有一个sidecar负责收集日志、导流或者一个容器起代理服务、一个容器跑业务进程。Pod把它们作为一个整体调度要么一起被调度到某个节点要么一起被销毁。理解了Pod之后另一个容易糊涂的地方是Deployment和Service的分工。Deployment管的是Pod的生死——期望几个副本、怎么滚动更新、节点挂了怎么办。而Service管的则是一组Pod的入口——Pod经常被销毁重建IP是飘忽不定的Service给这一组Pod分配一个稳定的虚拟IP和DNS名字外部请求先到Service再由它转发到后端的Pod上。Service相当于店铺门口的总服务台顾客不用管店员是谁找服务台就行。至于Ingress你把它想象成是商场总入口处的门卫根据访问的域名和URL路径把不同的顾客引导到不同的楼层和柜台。比如api.example.com/api转到A服务的Serviceapi.example.com/admin转到B服务的Service。3.3 本地搭一个最小的Kubernetes环境minikube、kind还是k3s学习Kubernetes除非你有现成的托管集群可用否则第一步是本地搞一个轻量环境。常见的三个选择minikube、kind、k3s。工具底层方式资源占用适合场景minikube单节点虚拟机/或容器驱动较高最接近真实K8s功能完整kindDocker容器里跑K8s节点中等轻量CI友好启动快k3s二进制containerd较低资源紧张或边缘场景如果说你已经装了Docker Desktop那么新版的Docker Desktop本来就可以一键开启Kubernetes在Settings的Kubernetes页勾选Enable Kubernetes就能得到一个单节点集群。这种方式最省事对新手足够友好。唯一的问题是它出来得比minikube早集群版本升级跟着Docker Desktop走自己没法灵活选版本。如果你不想让Docker Desktop这个全家桶承载太多功能用kind是最快的方案——因为它本质上是在Docker容器里再跑一套K8s节点不依赖虚拟化技术几个命令行就能起一个集群kind create cluster --name dev跑完用kubectl get nodes就能看到节点状态。我个人用kind比较多因为它在CI里也能很稳定地跑适合做本地验证。如果想要更接近生产环境的体验用minikube它能模拟出存储、负载均衡器等更多组件。4. 把FastAPI服务部署到Kubernetes一份能直接抄的清单概念过一遍之后最重要的还是把服务真正部署到集群里看它跑起来。下面我用一个FastAPI服务为例把整个部署清单逐行拆给你看。这个服务不需要多复杂就是能响应/healthz健康检查、能连上MySQL和Redis就行。需要说明的是以下镜像地址和配置都是示例实际操作时替换成你自己的镜像仓库和配置即可。4.1 镜像前置工作打包、打标签、推送到镜像仓库Kubernetes节点从镜像仓库拉镜像默认是去Docker Hub或你指定的私有镜像仓库。本地构建好的镜像要打上仓库地址的标签再推送docker build -t registry.example.com/fastapi-demo:1.0.0 . docker push registry.example.com/fastapi-demo:1.0.0这里有个容易忽视的细节Kubernetes的镜像名需要是全限定名就是必须包含仓库地址。如果你只写fastapi-demo:1.0.0kubelet默认会去Docker Hub找这个镜像因为Docker Hub上没有肯定拉不到。这和本地Docker跑镜像的规则有点差异本地你用短名字跑得欢一到K8s就拉取失败ImagePullBackOff多半就是这个原因。4.2 Deployment清单逐行拆解Deployment是Kubernetes里管无状态应用副本的核心资源。把下面这个YAML保存为deployment.yaml这就是一个相对完整的FastAPI服务部署配置apiVersion: apps/v1 kind: Deployment metadata: name: fastapi-app namespace: default spec: replicas: 3 selector: matchLabels: app: fastapi-app template: metadata: labels: app: fastapi-app spec: containers: - name: app image: registry.example.com/fastapi-demo:1.0.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8000 protocol: TCP env: - name: MYSQL_HOST value: mysql-service - name: MYSQL_PORT value: 3306 - name: REDIS_HOST value: redis-service resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 5 periodSeconds: 5 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 15 periodSeconds: 20 failureThreshold: 3一行一行拆开看replicas: 3期望保持3个Pod副本。Kubernetes里有个控制器Deployment controller会一直盯着一旦某个Pod挂了就立刻重新调度一个新Pod出来。selector.matchLabelsDeployment通过这个label匹配自己管辖的Pod。注意这个selector一旦创建就不能改因为控制器靠它来识别归属。imagePullPolicy: IfNotPresent如果节点上还没有这个镜像就去拉取。镜像tag是latest时Kubernetes会强制每次都拉取所以生产环境应该尽量避免使用latest标签而是用带版本号的tag方便回滚。env环境变量的注入方式。这里用value直接写更规范的应该用ConfigMap和Secret来管理下一章详细说。resources.requests和limitsrequests是调度器为Pod预留的最小资源limits是运行时允许占据的上限。对Python服务来说一开始requests设100m CPU和128Mi内存是比较保守的起步值生产环境要根据压测结果调整。如果只设limits不设requests调度器不知道你实际需要多少资源部署时容易把Pod堆积到同一台机器而某台机器资源还很闲。readinessProbe和livenessProbe两类探针。readiness决定Pod能不能接收流量fail的话就从Service的endpoints里摘除但容器不会重启liveness决定容器是否还活着连续失败会重启容器。FastAPI里对应的/healthz路由需要尽快加上它是Kubernetes判断你服务状态的唯一依据。4.3 Service与Ingress让外部流量找到你的服务Deployment创建好之后Pod是有IP的但Pod会漂移你不能把Pod IP直接给用户用。所以需要一个Service给这一组Pod提供一个稳定的访问入口apiVersion: v1 kind: Service metadata: name: fastapi-app spec: selector: app: fastapi-app ports: - port: 80 targetPort: 8000 type: ClusterIPService的类型主要有三种适用场景完全不同Service类型访问方式适用场景ClusterIP集群内部虚拟IP服务间内部调用默认类型NodePort节点IP 固定端口临时调试、小规模对外暴露LoadBalancer云厂商负载均衡器云上生产环境直接对外ClusterIP类型的Service在集群内部访问没问题但集群外的浏览器访问不到它。如果你的应用要对外提供API生产上一般用LoadBalancer或Ingress。Ingress的好处是能把多个服务按域名/路径统一成单一入口apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress spec: rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: fastapi-app port: number: 80Ingress本身不管流量转发它只是一个规则声明真正干活的是Ingress Controller比如nginx-ingress。你创建了Ingress资源但集群里没有Ingress Controller这个规则不会生效。很多新手在minikube里配好Ingress之后访问不了就是因为在装minikube时没有同时启用ingress组件。要检查本地集群有没有Ingress Controller可以看kubectl get pods -n kube-system或者直接minikube addons enable ingress启用它。5. 生产环境最关键的细节探针、滚动更新与优雅退出部署上去能跑只是开始真正考验的是版本更新时服务不中断、某个Pod出错时自动恢复、整个发布过程对用户无感知。这三个问题对应Kubernetes里三个最实用的机制探针、滚动更新、优雅退出。5.1 三种探针配错了比不配更麻烦前面提到readiness和liveness其实Kubernetes还有第三种探针startupProbe。三种探针分工明确startupProbe针对启动特别慢的应用。比如FastAPI服务启动时要加载大型机器学习模型可能要花30秒到1分钟默认的探针周期根本等不到它起来。给一个足够长的startupProbe窗口Kubernetes就会耐心等而不会因为liveness检测太早失败就把容器反复杀掉。livenessProbe探活判断容器是不是卡死了。连续失败就重启容器。适合检测死循环、死锁这类导致进程活着但无法正常工作的情况。readinessProbe判断是否就绪是否要把流量打给它。连续失败就把它从Service里摘除但不重启容器。这里容易踩的坑是有人图省事把三个探针配成同一个检查路径同一个参数。假如你的应用因为某种原因进入了能响应healthz但业务数据链路已断的状态比如能起HTTP服务但连不上MySQL了。如果healthz里没检查数据库连接readiness仍然返回200Service就继续把流量打给这个状态异常的Pod用户在接口层看到的就是服务器开了但报错。所以生产环境的healthz端点我一般会让它同时做依赖检查查MySQL连接、查Redis连接依赖挂了就返回500。这样readiness才能真实反映服务的可用状态。5.2 滚动更新maxSurge和maxUnavailable怎么配Deployment更新镜像版本时默认采用滚动更新策略分批创建新Pod、销毁旧Pod保证整个过程服务在线。控制这个节奏的是strategy.rollingUpdate下的两个参数spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0maxUnavailable更新过程中允许有多少个旧Pod不可用。设成0意味着无论如何都保证有足够的Pod在线适合用户流量不能断的严肃场景。maxSurge允许超出期望副本数多少个Pod。设成1意味着可以多启动1个新Pod等新Pod就绪后再杀掉旧Pod实现先起新的、再下旧的。对Python服务这种启动速度不算快的进程这两个值按1/0来配会是比较稳妥的做法更新过程中Pod总数会短暂超过期望值但不会有服务空的瞬间。如果实例数很少比如就2个副本保守一点可以把maxUnavailable设成0maxSurge设成1空间换时间。还有一个和滚动更新强相关的点是优雅退出。Kubernetes在缩容或滚动更新时会给Pod发送SIGTERM信号然后在默认的30秒terminationGracePeriodSeconds宽限期之后发SIGKILL强杀进程。如果你的Python应用启动慢、当前还有正在处理的请求SIGTERM之后要是没在30秒内退出旧的Pod会被直接杀掉正在处理中的请求就会中断。Python服务里最容易遇到这种情况的是异步任务或者长耗时请求。假如K8s一滚动更新线上就报连接被重置大概率就是优雅退出没做好。要让FastAPI服务优雅退出最简单的方案是使用Gunicorn作为进程管理器而不是直接用uvicorn裸跑。Gunicorn作为容器主进程时会收到SIGTERM它会停止接收新连接并给正在处理的worker一段时间完成当前请求再优雅退出。在Dockerfile里我通常这样写CMD [gunicorn, app.main:app, -w, 4, -b, 0.0.0.0:8000, --timeout, 60]如果业务代码里需要更精细的控制可以在FastAPI的lifespan事件里处理或者注册signal handler。其实我的经验是容器里Python应用尽量别自己做信号处理把进程管理交给Gunicorn这种成熟的进程管理器省心很多。滚动更新还有一个反直觉的细节更新完成后旧Pod的Terminating状态和Pod的终止前钩子preStop hook有关。如果代码里没有特殊的收尾逻辑比如通知注册中心下线、等待几秒再退出很多问题都是可以靠加一个preStop sleep来缓解的给流量从Service里摘除留出时间lifecycle: preStop: exec: command: [sh, -c, sleep 5]5.3 配置管理ConfigMap和Secret别把配置写死在镜像里最后一个是很多Python项目从单机搬到K8s后会踩的坑配置散落在环境变量和代码文件里。在镜像里写死配置意味着每次改配置都得重新构建镜像太痛苦。正确的做法是把配置从镜像中剥离用Kubernetes的ConfigMap和Secret来管理。ConfigMap用于非敏感配置比如URL、端口、日志级别、功能开关。Secret用于敏感信息比如数据库密码、API密钥、Redis密码。apiVersion: v1 kind: ConfigMap metadata: name: app-config data: MYSQL_HOST: mysql-service MYSQL_PORT: 3306 LOG_LEVEL: info --- apiVersion: v1 kind: Secret metadata: name: app-secret type: Opaque stringData: MYSQL_PASSWORD: SuperSecretPassword然后在Deployment里通过envFrom一次性注入spec: containers: - name: app envFrom: - configMapRef: name: app-config - secretRef: name: app-secret有两个点必须提醒。第一Secret并不是加密的它只是base64编码任何一个能在集群里看Secret的人都能轻松解码看到明文。真正的机密场景需要配合外部密钥管理系统来用而不是指望Secret本身保密。第二修改ConfigMap之后已经运行中的Pod并不会自动拿到新配置需要重启Pod或触发一次滚动更新。有些团队会用第三方工具比如Reloader监听ConfigMap变化自动触发滚动更新也可以靠自己手动kubectl rollout restart deployment/fastapi-app。写到最后再分享一个我自己练手时的习惯。每次我想验证一套K8s配置是否正确都会先在本地用kind起一个临时集群把Deployment、Service、ConfigMap全部apply进去再kubectl port-forward svc/fastapi-app 8000:80把服务映射到本地直接访问。这套流程既不污染开发环境又能快速复现问题。容器化这条路刚开始看着工具链很长但等你把Python应用、Docker和Kubernetes三者真正串起来之后部署这件事就会变得异常清晰和平稳。
返回列表