从零搭建一个单节点 K8S 可观测实验室(八):安装 Jenkins,把 CI 接进实验室 在上一篇中我们从一次 Pod 创建过程出发把 Kubernetes 背后的主要组件和工作流程梳理了一遍。到目前为止这套单节点 Kubernetes 实验室已经有了KubernetesPrometheus GrafanaLoki Fluent BitTempo OpenTelemetry Collector也就是说Metrics、Logs、Traces 三条可观测链路已经基本齐了。但还有一个地方一直比较“原始”部署应用还是手工完成的。比如修改代码之后我们可能需要自己执行docker build ... kubectl apply ... kubectl get pods如果每次代码修改都这么操作显然不太像真正的软件开发流程。所以从这一篇开始我们再给实验室补上一块CI —— Continuous Integration持续集成。这一篇先安装 Jenkins让代码从Git ↓ Jenkins ↓ Docker Build ↓ Kubernetes ↓ Pod自动跑起来。至于 GitHub Actions我们留到下一篇单独讲。一、Jenkins 到底干什么如果只看名字“持续集成”可能有点抽象。其实我们现在手工做的事情无非就是修改代码 ↓ 拉取代码 ↓ 编译 / 测试 ↓ 构建 Docker 镜像 ↓ 部署 Kubernetes ↓ 检查运行结果Jenkins 做的事情就是把这些步骤写成一条自动流水线。以后不再需要我们逐条执行docker build ... kubectl apply ... kubectl rollout status ...而是告诉 JenkinsStage 1Checkout Stage 2Build Stage 3Deploy Stage 4Verify点击一次 BuildJenkins 自己往下跑。进一步配置 Git Webhook 后甚至可以做到git push ↓ Jenkins 自动开始构建 ↓ 自动部署 Kubernetes这就是最基本的 CI/CD 雏形。二、Jenkins 装在哪里Jenkins 自己当然也可以部署到 Kubernetes 里面。甚至还可以让 Jenkins 动态创建 Kubernetes Pod 作为构建 Agent。这在真正的大型 CI 平台中很常见。但这次我不准备这么折腾。原因很简单我们的目标是学习 CI不是先研究怎么把 Jenkins 云原生化。而且这套单节点实验室里已经运行了 Prometheus、Grafana、Loki、Tempo 等不少组件。所以这次采用最简单的方案Ubuntu K8S Node │ ├── Docker ├── Kubernetes ├── Jenkins │ └── K8S Pods ├── Prometheus ├── Grafana ├── Loki ├── Tempo └── Demo App直接把 Jenkins 安装在 Kubernetes 节点所在的 Ubuntu 系统上。这样 Jenkins 天然就能使用本机的Docker kubectl整个实验会简单很多。三、安装 JenkinsJenkins 本身是 Java 程序因此先安装 Java。当前 Jenkins 官方 Debian/Ubuntu 安装文档推荐 OpenJDK 21并建议先安装 Java再安装 Jenkins。否则 Jenkins 服务可能因为找不到可用 Java 而启动失败。执行sudo apt update sudo apt install -y fontconfig openjdk-21-jre确认 Javajava -version类似openjdk version 21... OpenJDK Runtime Environment ... OpenJDK 64-Bit Server VM ...接下来添加 Jenkins LTS 软件源。下载 Jenkins 当前的软件源签名sudo wget -O /etc/apt/keyrings/jenkins-keyring.asc \ https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key添加 Jenkins LTS Repositoryecho deb [signed-by/etc/apt/keyrings/jenkins-keyring.asc] \ https://pkg.jenkins.io/debian-stable binary/ \ | sudo tee /etc/apt/sources.list.d/jenkins.list /dev/null更新软件源sudo apt update安装 Jenkinssudo apt install -y jenkins以上也是 Jenkins 官方目前给出的 Debian/Ubuntu LTS 安装方式。四、启动 Jenkins启动 Jenkinssudo systemctl start jenkins设置开机启动sudo systemctl enable jenkins查看状态sudo systemctl status jenkins正常情况下应该看到Active: active (running)Jenkins 默认监听8080确认一下ss -lntp | grep 8080然后在宿主机浏览器访问http://K8S节点IP:8080例如http://192.168.x.x:8080应该可以看到 Jenkins 的Unlock Jenkins页面。五、初始化 Jenkins第一次启动 Jenkins 时需要输入初始化密码。执行sudo cat /var/lib/jenkins/secrets/initialAdminPassword复制密码到浏览器。接下来 Jenkins 会询问Install suggested plugins Select plugins to install这里我们直接选择Install suggested plugins先把常用插件装上。如果有一些Failed插件可以试试Retry安装完成后创建管理员账户。最后进入Jenkins Dashboard到这里 Jenkins 本身就已经安装完成了。六、先搞清楚一个很重要的问题Jenkins 是谁在执行命令我们平时登录 Ubuntu 后执行docker ps kubectl get pods使用的是当前 Linux 用户。但 Jenkins 并不是使用我们的用户执行 Pipeline。安装 Jenkins 时系统会创建一个jenkins用户。可以查看id jenkins以后 Jenkinsfile 中写sh docker build ...实际上差不多等于sudo -u jenkins docker build ...所以接下来必须解决两个权限问题Jenkins │ ├── 能不能使用 Docker │ └── 能不能使用 kubectl否则 Jenkins 虽然装好了却什么都干不了。七、让 Jenkins 可以使用 Docker先验证sudo -u jenkins docker ps大概率会遇到permission denied while trying to connect to the Docker daemon socket原因是 Docker Socket/var/run/docker.sock通常只允许root docker group访问。所以把 Jenkins 用户加入docker组sudo usermod -aG docker jenkins然后重启 Jenkinssudo systemctl restart jenkins再次验证sudo -u jenkins docker ps如果能够正常看到容器列表就说明 Docker 权限已经搞定。一个安全提醒把 Jenkins 加进docker用户组其实意味着 Jenkins 获得了非常高的宿主机权限。在个人实验室里这样做很方便。但生产环境就不能这么随意了。生产环境通常会使用独立 Jenkins AgentKubernetes AgentBuildKitKaniko独立构建节点来隔离构建环境。这里我们的目的只是把 CI 流程先跑通。八、让 Jenkins 可以使用 kubectlDocker 能用了下一步是 Kubernetes。先执行sudo -u jenkins kubectl get nodes这时候通常会失败。不是kubectl不存在而是 Jenkins 用户没有~/.kube/config我们目前登录用户能够执行kubectl get nodes是因为已经有~/.kube/config所以实验环境里最简单的办法就是先复制一份给 Jenkins。创建目录sudo mkdir -p /var/lib/jenkins/.kube复制配置sudo cp ~/.kube/config \ /var/lib/jenkins/.kube/config修改文件所有者sudo chown -R jenkins:jenkins \ /var/lib/jenkins/.kube限制权限sudo chmod 600 \ /var/lib/jenkins/.kube/config验证sudo -u jenkins \ env KUBECONFIG/var/lib/jenkins/.kube/config \ kubectl get nodes正常情况下应该看到我们的单节点NAME STATUS ROLES k8s-node Ready control-plane再测试sudo -u jenkins \ env KUBECONFIG/var/lib/jenkins/.kube/config \ kubectl get pods -A如果 Pod 也能正常显示Jenkins → Kubernetes 的通路就已经打通了。同样有一个安全问题这里为了实验简单直接复制了现有 kubeconfig。这意味着 Jenkins 拥有的 Kubernetes 权限可能非常高。生产环境不能这样干。更合理的方式应该是ServiceAccount ↓ Role ↓ RoleBinding ↓ 只允许 Jenkins 操作指定 Namespace也就是 Kubernetes 常说的最小权限原则。这一篇先把流水线跑通后续需要时再单独优化 RBAC。九、准备一个最简单的 Demo现在Jenkins → Docker OK Jenkins → Kubernetes OK接下来终于可以跑 Pipeline 了。为了避免把文章搞复杂这里不重新写一套大型应用。准备一个最简单的 Nginx Demo 即可。目录结构jenkins-k8s-demo/ ├── Dockerfile ├── index.html ├── deployment.yaml ├── service.yaml └── Jenkinsfile创建mkdir -p ~/Codes/jenkins-k8s-demo cd ~/Codes/jenkins-k8s-demo十、准备页面创建cat index.html EOF !DOCTYPE html html head meta charsetUTF-8 titleJenkins K8S Demo/title /head body h1Hello Jenkins Kubernetes!/h1 pThis application was deployed by Jenkins./p /body /html EOF以后只要修改这段页面再触发 Jenkins就能非常直观地确认新版本有没有部署成功。十一、准备 Dockerfile创建cat Dockerfile EOF FROM nginx:1.27-alpine COPY index.html /usr/share/nginx/html/index.html EOF加入 docker 组sudo usermod -aG docker $USER退出 SSH然后重新 SSH 登录exit先手工测试一次cd ~/Codes/jenkins-k8s-demo docker build \ -t jenkins-k8s-demo:test .查看docker images jenkins-k8s-demo如果镜像正常生成REPOSITORY TAG jenkins-k8s-demo test说明 Dockerfile 没问题。十二、准备 Kubernetes Deployment创建cat deployment.yaml EOF apiVersion: apps/v1 kind: Deployment metadata: name: jenkins-k8s-demo namespace: production spec: replicas: 1 selector: matchLabels: app: jenkins-k8s-demo template: metadata: labels: app: jenkins-k8s-demo spec: containers: - name: nginx image: jenkins-k8s-demo:latest imagePullPolicy: Never ports: - containerPort: 80 EOF这里有一个很重要的配置imagePullPolicy: Never为什么因为这套实验室的 Kubernetes 使用 Docker cri-dockerd。而 Jenkins 又运行在同一个节点。所以 Jenkins 执行docker build产生的镜像就是 Kubernetes 节点本地能够直接使用的镜像。流程变成Jenkins ↓ docker build ↓ 节点本地 Docker Image ↓ cri-dockerd ↓ Kubernetes Pod因此这一篇暂时不需要 Docker Hub、GHCR 或 Harbor。这个办法非常适合我们的单节点实验室。但生产环境显然不能这么干。如果 Kubernetes 有多个 Worker Node镜像只存在当前节点其他节点根本看不到。真正的流程应该是Jenkins ↓ Build Image ↓ Push Registry ↓ Kubernetes ↓ Pull Image这一点后面再升级。十三、准备 Service创建cat service.yaml EOF apiVersion: v1 kind: Service metadata: name: jenkins-k8s-demo namespace: production spec: selector: app: jenkins-k8s-demo ports: - port: 80 targetPort: 80 type: ClusterIP EOF到这里 Kubernetes 部分也非常简单Deployment ↓ Pod ↑ Service十四、第一次写 Jenkinsfile终于到这篇的核心了。Jenkins 官方推荐把 Pipeline 定义写进源码仓库中的Jenkinsfile这就是所谓Pipeline as Code。这样流水线本身也可以和代码一样进行版本管理、Code Review 和历史追踪。Jenkins 同时支持 Declarative 和 Scripted 两种 Pipeline 语法本文使用更容易阅读的 Declarative Pipeline。创建pipeline { agent any environment { IMAGE_NAME jenkins-k8s-demo IMAGE_TAG build-${BUILD_NUMBER} KUBECONFIG /var/lib/jenkins/.kube/config } stages { stage(Build Image) { steps { sh docker build \ -t ${IMAGE_NAME}:${IMAGE_TAG} \ -t ${IMAGE_NAME}:latest \ . } } stage(Deploy) { steps { sh kubectl apply -f deployment.yaml kubectl apply -f service.yaml kubectl set image \ deployment/jenkins-k8s-demo \ nginx${IMAGE_NAME}:${IMAGE_TAG} \ -n production } } stage(Verify) { steps { sh kubectl rollout status \ deployment/jenkins-k8s-demo \ -n production \ --timeout120s kubectl get pods \ -n production \ -l appjenkins-k8s-demo } } } }如果之前没有productionNamespace先创建kubectl create namespace production十五、这条 Pipeline 实际在干什么不要被 Jenkinsfile 的 Groovy 格式吓到。它其实就是把我们以前手工执行的命令分成几个阶段。第一步Build Image执行docker build产生jenkins-k8s-demo:build-1下一次jenkins-k8s-demo:build-2再下一次jenkins-k8s-demo:build-3这里BUILD_NUMBER是 Jenkins 自动生成的构建编号。同时每次生成jenkins-k8s-demo:latestlatest用于满足 Kubernetes 初始 Deployment 中的镜像引用。需要注意这种方式适合单节点学习环境不适合作为生产方案。生产环境通常应该通过 Registry 保存带版本号的镜像并让 Kubernetes 从 Registry 拉取镜像。第二步Deploy先kubectl apply确保 Deployment 和 Service 存在。然后kubectl set image把 Deployment 更新成这一轮刚刚构建出的镜像。第三步Verify这里我觉得非常重要。不是kubectl apply没报错就宣布部署成功。而是继续kubectl rollout status等 Kubernetes 真正完成 Deployment 更新。如果新 Pod 起不来ImagePullBackOff CrashLoopBackOff Readiness Probe FailedPipeline 就应该失败。这才更像真正的 CI/CD。十六、把项目放进 Git初始化git init加入文件git add .提交git commit -m Add Jenkins K8S demo然后 Push 到自己的 Git Repository。这里 GitHub、GitLab 或其他普通 Git Server 都可以。Jenkins 只需要能够git clone即可。十七、在 Jenkins 创建 Pipeline打开Jenkins Dashboard点击New Item输入jenkins-k8s-demo选择Pipeline点击OK然后找到PipelineDefinition 选择Pipeline script from SCMSCMGit填入 Git Repository URL。例如https://github.com/mosesyyoung/jenkins-k8s-demoBranch 根据自己的仓库填写例如*/masterScript PathJenkinsfile保存。十九(可选) 给 Jenkins 配代理如果需要代理做 Git 操作比如 Clone需要给 Jenkins 配置代理编辑 Jenkins servicesudo systemctl edit jenkins在### Anything between here and the comment below will become the contents of the drop-in file和### Edits below this comment will be discarded之间添加[Service] Environmenthttp_proxyhttp://proxy_address:proxy_port Environmenthttps_proxyhttp://proxy_address:proxy_port EnvironmentHTTP_PROXYhttp://proxy_address:proxy_port EnvironmentHTTPS_PROXYhttp://proxy_address:proxy_port EnvironmentNO_PROXYlocalhost,127.0.0.1,10.0.2.15,192.168.0.0/16 Environmentno_proxylocalhost,127.0.0.1,10.0.2.15,192.168.0.0/16注意这里的代理地址必须是Jenkins 所在 Ubuntu 能访问的地址。然后sudo systemctl daemon-reload sudo systemctl restart jenkins验证 Jenkins 环境systemctl show jenkins | grep -i proxy应该看到http_proxy... https_proxy...十九、第一次 Build点击Build NowJenkins 开始执行 Pipeline。进入Console Output可以看到整个过程。首先拉取源码。然后Build Image接着Deploy最后Verify如果一切正常最终应该看到类似deployment jenkins-k8s-demo successfully rolled outPipelineSUCCESS二十、看看 Kubernetes 发生了什么查看 Podkubectl get pods -n production应该出现jenkins-k8s-demo-xxxxxxxxxx-xxxxx查看 Deploymentkubectl get deployment \ jenkins-k8s-demo \ -n production查看当前镜像kubectl get deployment \ jenkins-k8s-demo \ -n production \ -o jsonpath{.spec.template.spec.containers[0].image}应该类似jenkins-k8s-demo:build-1这意味着Git 中的代码 ↓ Jenkins ↓ Docker Image ↓ Deployment ↓ Pod整条链路已经打通。二十一、访问一下 Jenkins 部署的应用由于现在是 ClusterIP Service我们还是使用之前实验中很熟悉的方法kubectl port-forward \ service/jenkins-k8s-demo \ 8081:80 \ -n production保持运行。然后另开一个 SSH 窗口执行curl http://127.0.0.1:8081应该返回!DOCTYPE html html head meta charsetUTF-8 titleJenkins K8S Demo/title /head body h1Hello Jenkins Kubernetes!/h1 pThis application was deployed by Jenkins./p /body /html二十二、修改代码再来一次现在修改 index.htmlh1Hello Jenkins Kubernetes V2!/h1提交git add . git commit -m Update demo to v2 git push目前我们还没有配置 Webhook。所以先回到 JenkinsBuild Now再次运行。这一次 Jenkins 会生成jenkins-k8s-demo:build-2然后kubectl set image触发 Deployment Rolling Update。查看kubectl get deployment \ jenkins-k8s-demo \ -n production \ -o jsonpath{.spec.template.spec.containers[0].image}应该变成jenkins-k8s-demo:build-2保持运行kubectl port-forward \ service/jenkins-k8s-demo \ 8081:80 \ -n production另一个 SSH 窗口执行curl http://127.0.0.1:8081应该返回!DOCTYPE html html head meta charsetUTF-8 titleJenkins K8S Demo/title /head body h1Hello Jenkins Kubernetes V2!/h1 pThis application was deployed by Jenkins./p /body /html这就是一次非常简单但完整的 CI/CD 部署。二十三、可观测系统现在开始发挥作用了到这里其实还只是CI但别忘了我们做的是Kubernetes 可观测实验室。Jenkins 部署出的 Pod 和以前手工创建的 Pod 没有区别。所以之前搭建的系统仍然可以继续观察它。Prometheus 可以看到CPU Memory Pod Status RestartLoki 可以看到 Nginx 日志。例如在 Grafana Explore 中查询{namespaceproduction}然后筛选jenkins-k8s-demo每次访问页面产生的 Nginx Access Log 都会被 Fluent Bit 收集进入 Loki。如果以后把这个 Nginx Demo 换成前面使用过的 OpenTelemetry Demo AppJenkins ↓ Kubernetes ↓ Application ├── Metrics → Prometheus ├── Logs → Loki └── Traces → Tempo ↓ GrafanaCI 和可观测系统就真正连起来了。二十四、为什么这一篇还没做 Git Push 自动触发你可能已经发现现在还是git push ↓ 手工点击 Build Now严格来说还不够自动化。真正应该是git push ↓ Git Webhook ↓ Jenkins ↓ 自动 Build ↓ 自动 Deploy但这里有一个现实问题我们的 Jenkins 跑在局域网 Ubuntu VMGitHub 在 Internet 上。GitHub Webhook 默认并不能直接访问192.168.x.x:8080所以如果现在继续讲 Webhook就又会引入公网 IPNAT反向代理TunnelGitHub WebhookJenkins GitHub Plugin文章会迅速跑题。因此这一篇先做到Jenkins Pipeline 可以完整自动执行 Build → Deploy → Verify。至于git push → 自动触发可以后面再根据实验环境决定是否加入。下一篇我们还会尝试另一种思路GitHub Actions Self-hosted Runner。这套方式对于家里的局域网 Kubernetes 实验室反而很有意思。二十五、当前 CI 架构现在我们的实验室已经变成Git Repository │ ▼ Jenkins │ ┌────────┴────────┐ │ │ Docker Build kubectl │ │ └────────┬────────┘ ▼ Kubernetes │ Deployment │ Pod │ ┌─────────────┼─────────────┐ │ │ │ ▼ ▼ ▼ Prometheus Loki Tempo │ │ │ └─────────────┴─────────────┘ │ ▼ Grafana相比之前最大的变化是以前人 ↓ docker build ↓ kubectl ↓ Kubernetes现在变成人 ↓ Git ↓ Jenkins ↓ Docker kubectl ↓ Kubernetes自动化终于进入了这套实验室。二十六、目前这套 Jenkins 方案有哪些“不正规”的地方这一篇追求的是先跑通。所以有几个地方显然不能直接照搬到生产环境。1. Jenkins 直接拥有 Docker 权限我们把jenkins加入了docker组。这相当于给 Jenkins 很高的宿主机权限。2. Jenkins 使用了现有 kubeconfig这让 Jenkins 对 Kubernetes 的权限太大。正规的方案应该使用ServiceAccount RBAC限制它只能操作必要的 Namespace 和资源。3. 没有 Registry现在镜像jenkins-k8s-demo:build-2只存在这个节点本机。因为这是单节点实验室所以完全没问题。但多节点 Kubernetes 必须有Docker Hub GHCR Harbor 其他 Registry4. Jenkins Controller 自己承担构建任务真正的 Jenkins 通常会把构建任务交给Agent甚至Kubernetes Dynamic AgentController 主要负责管理任务而不是自己天天docker build。这些都是后面可以继续升级的地方。但对于学习 Jenkins 来说现在这套结构反而最容易看懂。二十七、清理 Demo如果这只是临时实验可以删除kubectl delete deployment \ jenkins-k8s-demo \ -n production删除 Servicekubectl delete service \ jenkins-k8s-demo \ -n production查看镜像docker images jenkins-k8s-demo按需要删除docker rmi \ jenkins-k8s-demo:build-1 \ jenkins-k8s-demo:build-2Jenkins 本身暂时不要删除。下一篇还可以继续拿它与 GitHub Actions 做比较。二十八、小结这一篇我们没有增加新的 Kubernetes 核心组件。但整个实验室发生了一个很重要的变化。之前部署应用是写代码 ↓ 手工 docker build ↓ 手工 kubectl ↓ Kubernetes现在变成Git ↓ Jenkins ↓ Build ↓ Deploy ↓ Verify ↓ KubernetesJenkinsfile 又把整条流水线变成了代码Pipeline as Code而 Kubernetes 后面的Prometheus Loki Tempo Grafana并没有发生变化。这也让之前搭建的那些组件开始真正串起来代码 ↓ CI ↓ Container Image ↓ Kubernetes ↓ Application ↓ Metrics / Logs / Traces ↓ Grafana到这里这套单节点实验室已经不只是一个“能看监控的 Kubernetes”。它开始越来越接近真正的软件研发流程。下一篇我们换一种现在非常流行的 CI 方式GitHub Actions Self-hosted Runner。看看不自己维护 Jenkins Controller能不能同样把 GitHub 上的代码送进我们这台局域网里的 Kubernetes。参考资料Jenkins 官方文档Installing Jenkins