广告:Codex Token 低价中转站稳定接口 · 快速接入 · 开发者备用通道
Engineering article

高手进阶 | 重排序:自动化实现

我用kubernetes做自动化部署,关键点是你得把所有依赖打包成镜像,避免容器之间互相依赖,这样才有可重复性。更狠的是,你得在jenkins里配置docker build命令,用 --build-arg 指定环境变量,这样ci才能自动拉取代码,编译,构建镜像,然后推送到harbor。别问我怎么知道的,我之前用jenkins + docker + k8s做全

高手进阶 | 重排序:自动化实现
配图来源于网络和AI生成,仅供参考。
我用kubernetes做自动化部署,关键点是你得把所有依赖打包成镜像,避免容器之间互相依赖,这样才有可重复性。更狠的是,你得在jenkins里配置docker build命令,用 --build-arg 指定环境变量,这样ci才能自动拉取代码,编译,构建镜像,然后推送到harbor。别问我怎么知道的,我之前用jenkins + docker + k8s做全链路自动化,结果镜像版本混乱,最后搞了dockerfile的版本控制,才解决这个问题。再说说k8s的deploy配置,你得在spec里加 imagePullPolicy: IfNotPresent,否则每次pull镜像都从远程拉,浪费时间,还容易出错。还有,别用docker-compose,docker-compose的network配置在k8s里不适用,得用link或者service的dns来解决服务发现的问题。最后,别忘了在k8s的ingress里配置TLS证书,不然你用https会被拦截,导致前端请求失败。

▌ 技术引导

实际使用中,我遇到过很多关于自动化的坑,最头疼的是还是依赖管理。比如,如果某个服务依赖数据库,那你得在ci里先启动数据库容器,再运行应用的build,否则build会因为找不到依赖而失败。这时候,docker-compose的depends_on虽然能提示启动顺序,但实际执行时服务可能还没准备好,所以得用健康检查机制。具体操作就是,用curl或者wget检查数据库的端口是否开放,再执行build,这在jenkins的shell脚本里能写出来。还有一种情况,就是镜像版本不对,每次ci build会拉取最新镜像,但实际部署的版本可能还是旧的,这就得在dockerfile里加构建时间戳,然后在ci里用docker build --build-arg=build_time=now这样的方式来确保每次构建的版本是唯一的。更高级的玩法是用git commit hash来作为镜像标签,这样每次提交都能生成一个独立的镜像,方便回滚和追踪。另外,如果用k8s做自动化部署,那得确保每个deploy的imagePullPolicy是IfNotPresent,否则每次都会从远程拉取,这样对网络和速度要求太高了。

▌ 技术参考

kubernetes的自动化部署主要依赖于控制器,比如deployment和statefulset,它们能确保应用处于预期状态。在使用这些控制器时,必须确保每个组件的镜像版本是明确的,这样即使多次触发ci,也不会出现版本混乱的问题。通常做法是在ci中构建镜像时,使用docker build --build-arg=build_time=now这样的命令,并将build_time作为镜像标签的一部分。这能有效避免因为镜像版本不一致导致的问题。同时,确保所有服务的容器都使用相同的标签,这样在k8s中就能统一管理镜像版本。

我用jenkins做ci,每次拉取代码后,先执行docker build命令,然后推送到harbor。在jenkins的配置里,必须设置DOCKER_HOST环境变量,指向本地docker守护进程的地址。如果使用docker-in-docker,这一步是必须的。此外,jenkins的docker插件能自动处理镜像的拉取和推送,但如果出现权限问题,得手动添加harbor的账号密码到docker配置文件里。在jenkins的docker build step中,可以添加--build-arg=ENV=production这样的参数,这样dockerfile就能根据不同的环境变量来决定是否开启调试模式或者使用正式数据库连接。

在kubernetes的部署配置中,必须显式定义imagePullPolicy,建议使用IfNotPresent,这样可以避免每次都从远程拉取镜像,提升部署效率。同时,确保每个pod的容器使用相同的imagePullPolicy配置,这样整个集群的镜像行为才能保持一致。在yaml文件中,可以配置为:imagePullPolicy: IfNotPresent,这样就能在本地镜像存在时直接使用,否则从远程拉取。这在测试和生产环境中都适用,但需要注意,在生产环境中如果镜像更新频繁,最好还是用Always或者Never。

在实际操作中,我遇到过一个典型的问题,就是ci构建的镜像版本和部署的版本不一致。这种情况往往是因为ci没有正确处理镜像标签。例如,如果在dockerfile里没有使用构建时间或者git commit hash作为标签的一部分,那么每次构建的镜像可能都会被标记为latest,这样就很难确定到底部署的是哪个版本。解决方法是,在ci的docker build命令里显式指定镜像标签,如docker build -t myrepo/myapp:1.2.3 -f Dockerfile.prod .,这样就能确保每次构建的版本是唯一的。同时,在k8s的部署文件中,必须确保引用的镜像标签和ci构建的标签一致。

另一个值得注意的场景是,在ci构建过程中,如果某个依赖的版本更新导致镜像构建失败,这时候得确保ci的步骤能自动回滚到之前的版本。做法是在ci里维护一个镜像版本列表,一旦构建失败,就从列表中选择一个稳定的版本进行部署。这需要ci有回滚机制,比如在jenkins里配置一个回滚任务,或者使用git的commit hash来指定版本。在k8s里,这样做的方式是修改deploy的image字段,然后触发滚动更新。如果配置得当,整个过程几乎不用人工干预。

在k8s的部署中,使用ingress来暴露服务是一个常见做法,但必须确保ingress的配置文件正确引用了服务的端口和协议。例如,在ingress的yaml中,需要配置service: myapp-service和port: 80,这样外部的请求才能正确路由到内部服务。如果配置错误,比如端口写成了443,而实际服务运行在80端口,那就会导致404错误。这时候,检查ingress的配置和service的定义是否一致是关键。此外,记得在ingress里配置TLS证书,否则使用https会导致请求失败。

如果使用helm来做k8s的部署,那必须确保values.yaml里的参数能正确传递到部署文件中。例如,在values.yaml里定义image.tag,然后在deployment的yaml里用{{ .Values.image.tag }}来引用,这样就能统一管理镜像版本。同时,helm的release名称也要和ci的构建流程绑定,这样每次构建都能生成一个唯一的release名,方便管理。在实际操作中,我发现有些团队在helm配置里没有明确指定image.tag,导致每次发布都用latest,这样实际部署的版本就无法追踪,容易出错。

在ci构建过程中,使用docker-in-docker是一个常见做法,但需要注意资源分配问题。比如,在jenkins的docker agent配置里,得确保有足够的内存和cpu来运行构建任务,否则会出现build失败或者非常慢的情况。如果发现docker-in-docker的环境变量没有正确设置,就得检查DOCKER_HOST、DOCKER_TLS_CERTDIR等变量是否包含在agent的环境里。此外,在docker build命令里,如果使用了multi-stage build,那必须确保每个阶段的镜像都是可重用的,否则会浪费时间和存储空间。

如果在k8s中部署的应用需要持久化存储,那必须在deployment的spec里配置volumeMounts和volumes,同时在pvc中指定存储类和访问模式。例如,在pvc中设置accessModes: ReadWriteOnce,这样只能有一个pod能访问存储,避免数据混乱。在实际操作中,我看到很多团队没有正确配置pvc,导致应用无法写入数据,或者数据被多个实例同时修改。这时候,必须在ci的部署步骤中显式指定pvc的命名和挂载路径,确保一致性。

在部署过程中,网络配置也是一个容易出错的地方。比如,如果在k8s里使用link来解决服务发现的问题,那必须确保每个服务的链接名称和容器名称一致,否则会无法访问。比如,在docker-compose里,服务A通过link连接到服务B,那么服务B的名称必须和容器名称一致,否则服务A在运行时会找不到服务B。这时候,用k8s的service名称来替代link是一个更好的做法,因为service名称和容器名称是自动对齐的,不需要手动配置。

在ci构建和部署过程中,必须确保镜像的缓存策略正确。例如,在docker build时,如果使用了--no-cache,那每次都会重新构建镜像,这样会浪费时间。但有时候,为了确保镜像是最新的,必须关闭缓存。这时候得在ci的docker build命令里显式添加--no-cache参数。同时,在ci的配置中,如果使用了git commit hash来作为镜像标签,那每次提交都会生成一个唯一的标签,这样就能避免因为缓存导致的版本混乱。

在k8s的部署中,如果应用需要访问外部服务,那必须在service的yaml里配置externalIPs或者type为LoadBalancer。例如,如果应用需要访问某个数据库,那得确保数据库的ip和端口在k8s的service里正确配置。否则,应用在启动时会找不到数据库,导致启动失败。这时候,必须在ci的部署步骤里显式指定service的externalIPs或者使用LoadBalancer类型,这样应用才能正确连接。

在ci的部署过程中,如果遇到镜像拉取失败的问题,那必须检查harbor的镜像是否能被访问。比如,在k8s的pod里运行的时候,如果无法拉取镜像,那可能是因为harbor的地址配置错误,或者没有正确的认证信息。这时候,可以在k8s的serviceAccount里添加harbor的pull secret,确保pod能正确访问镜像仓库。在实际操作中,我发现很多团队没有配置pull secret,导致部署失败。

在部署过程中,如果应用需要处理大量数据,那必须考虑使用statefulset来确保数据持久化。比如,某些应用需要保存会话状态,这时候statefulset就比deployment更合适。在statefulset的配置里,必须指定volumeClaimTemplates,确保每个pod都有自己的存储卷。同时,在ci的部署步骤里,如果使用了helm,必须确保values.yaml里的storage部分正确配置,这样才能确保statefulset的正确生成。

在ci的部署中,如果遇到镜像构建失败的问题,那必须检查dockerfile里的各个步骤是否正确。例如,在编译阶段,如果使用了make或者npm install,必须确保这些命令的执行环境是正确的。如果dockerfile里没有安装必要的依赖,那镜像构建会失败。这时候,可以在dockerfile里添加RUN apt-get update && apt-get install -y curl wget git这样的命令,确保构建环境的完整性。

在k8s的滚动更新中,如果用的是deployment,那必须确保maxUnavailable和maxSurge参数配置得当。例如,如果设置maxUnavailable=0,那每次更新都会重新启动所有pod,这样会影响服务的可用性。但有时候为了快速更新,必须允许短暂不可用,这时候可以设置maxUnavailable=1,这样每次更新只有一个pod不可用,其他pod可以继续响应请求。在实际操作中,我发现一些团队没有配置这些参数,导致更新失败或者服务不可用。

在ci的部署流程中,必须确保所有步骤都是幂等的,这样即使多次执行也不会导致不可预知的问题。比如,在部署时,如果使用kubectl apply,那必须确保yaml文件里的资源定义是正确的,否则可能会因为资源冲突导致失败。这时候,可以使用kubectl replace或者kubectl patch来更新资源,而不是apply,这样就能避免因为apply导致的资源状态混乱。在实际操作中,我发现很多团队直接使用apply,结果出现资源版本冲突,最终导致部署失败。

在自动化部署中,日志收集也是一个关键点。比如,如果应用在运行时出现错误,那必须确保日志能被正确收集和查看。这时候,可以在k8s的pod里配置livenessProbe和readinessProbe,这样就能及时发现应用的问题。同时,在ci的部署步骤里,必须确保这些探测配置正确,否则应用会一直运行在错误状态下。在实际操作中,我发现很多团队没有配置这些探测,导致问题无法及时发现。