
从零实践在 GitHub Actions 中模拟 Kubernetes 微服务项目的 CI/CD 流水线前言最近在学习 Kubernetes 和 CI/CD 流水线遇到了一个很好的开源项目——kubernetes-microservices。这个项目包含三个微服务Backend、Web Frontend、Auth Frontend并配置了完整的 Helm Chart 和 GitHub Actions CI/CD 工作流。然而当我尝试 Fork 这个项目并运行其 CI/CD 流水线时遇到了各种问题从 YAML 语法错误到 Helm 模板问题再到 dry-run 模式下的集群连接问题。经过一番折腾终于成功跑通了整个流程。本文将完整记录我的实践过程、遇到的问题以及对应的解决方案希望能帮助到同样在学习和实践 GitHub Actions Kubernetes 的同学。一、项目背景1.1 项目结构textkubernetes-microservices/ ├── camp-backend/ # Python Flask 后端服务 │ ├── Dockerfile │ └── requirements.txt ├── camp-web-frontend/ # Web 前端服务 │ └── Dockerfile ├── camp-auth-frontend/ # 认证前端服务 │ └── Dockerfile ├── helm/ │ └── camp/ # Helm Chart │ ├── Chart.yaml │ ├── values.yaml │ └── templates/ │ ├── deployment.yaml │ ├── service.yaml │ └── ingress.yaml ├── clusters/ │ └── cluster-config.yaml # 集群配置 ├── environments/ │ ├── values-dev.yaml │ ├── values-staging.yaml │ └── values-prod.yaml └── .github/workflows/ ├── build-and-deploy.yml # 主要 CI/CD 流水线 └── repository-status.yml # 仓库状态检查1.2 CI/CD 工作流设计build-and-deploy.yml工作流包含三个作业作业作用触发条件build-and-lint代码结构检查、Helm 语法验证始终运行push/PR/workflow_dispatchbuild-and-push构建 Docker 镜像并推送到 GHCR仅workflow_dispatchbuildtruedeploy部署到 Kubernetes 集群仅workflow_dispatchdeploytrue二、第一次尝试Fork 项目并触发工作流2.1 操作步骤Fork 项目进入原项目elishatheodore/kubernetes-microservices点击 Fork复制到自己的 GitHub 账号zxcspider。启用 Actions在 Fork 仓库的 Actions 页面点击 Enable workflows。手动触发工作流选择Build and Deploy CAMP Platform点击Run workflow填写参数Build and push images:falseTarget cluster:local-devTarget environment:devDeploy after build:trueDry run deployment:true2.2 遇到的问题❌ 问题 1Invalid workflow file ——secrets不能在if条件中使用错误信息text(Line: 123, Col: 9): Unrecognized named-value: secrets原因分析在build-and-push作业的if条件中直接使用了secrets.GHCR_TOKEN ! 。GitHub Actions 出于安全考虑不允许在if条件中直接引用secrets上下文以防止敏感信息通过日志暴露。原始代码yamlbuild-and-push: if: github.event_name workflow_dispatch github.event.inputs.build true secrets.GHCR_TOKEN ! # ❌ 这行导致了错误解决方案简化if条件移除对secrets的检查。因为当buildfalse时该作业本来就不会执行即使buildtrue但 secret 未配置后续的 Docker 登录步骤也会自然失败。修改后yamlbuild-and-push: if: github.event_name workflow_dispatch github.event.inputs.build true❌ 问题 2Helm 模板语法错误 ——unexpected .. in operand错误信息text[ERROR] templates/: parse error at (camp/templates/ingress.yaml:44): unexpected .. in operand原因分析helm/camp/templates/ingress.yaml第 44 行存在非法的模板语法如错误的占位符或表达式导致helm lint失败。解决方案用标准 Helm Ingress 模板替换原有内容。标准模板兼容 Kubernetes 1.14 和 1.19 版本且语法正确。修改文件helm/camp/templates/ingress.yamlyaml{{- if .Values.ingress.enabled -}} {{- $fullName : include camp.fullname . -}} {{- $svcPort : .Values.service.port -}} {{- if and .Values.ingress.className (not (semverCompare 1.18-0 .Capabilities.KubeVersion.GitVersion)) }} {{- if not (hasKey .Values.ingress.annotations kubernetes.io/ingress.class) }} {{- $_ : set .Values.ingress.annotations kubernetes.io/ingress.class .Values.ingress.className}} {{- end }} {{- end }} {{- if semverCompare 1.19-0 .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1 {{- else if semverCompare 1.14-0 .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: {{- include camp.labels . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if and .Values.ingress.className (semverCompare 1.18-0 .Capabilities.KubeVersion.GitVersion) }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} {{- if .pathType }} pathType: {{ .pathType }} {{- end }} backend: {{- if .service }} service: name: {{ .service.name }} port: number: {{ .service.port }} {{- else }} service: name: {{ $fullName }} port: number: {{ $svcPort }} {{- end }} {{- end }} {{- end }} {{- end }}❌ 问题 3kubectlcontext 错误 ——no context exists with the name: minikube错误信息texterror: no context exists with the name: minikube原因分析在deploy作业中Configure kubectl context步骤尝试切换到minikubecontext但 GitHub Actions Runner 环境中并没有 minikube 集群。实际上我们选择clusterlocal-dev且dry_runtrue时根本不需要连接任何真实的 Kubernetes 集群。解决方案为这些依赖集群连接的步骤添加if条件仅当dry_run ! true且目标集群是 AKS 时才执行。修改前yaml- name: Configure kubectl context run: | CLUSTER_CONTEXT$(yq eval .clusters.${{ github.event.inputs.cluster }}.context clusters/cluster-config.yaml) kubectl config use-context $CLUSTER_CONTEXT修改后yaml- name: Configure kubectl context if: github.event.inputs.dry_run ! true startsWith(github.event.inputs.cluster, aks) run: | CLUSTER_CONTEXT$(yq eval .clusters.${{ github.event.inputs.cluster }}.context clusters/cluster-config.yaml) kubectl config use-context $CLUSTER_CONTEXT同样为Verify cluster connectivity步骤添加了相同的条件。❌ 问题 4helm upgrade --dry-run仍然需要集群连接错误信息textError: Kubernetes cluster unreachable: Get http://localhost:8080/version: dial tcp [::1]:8080: connect: connection refused原因分析即使使用--dry-run参数helm upgrade命令仍然会尝试连接 Kubernetes API Server 来验证集群状态。在 GitHub Actions Runner 环境中没有配置集群因此连接失败。解决方案在dry_runtrue模式下改用helm template命令直接渲染模板不连接任何集群。修改后的Deploy with Helm步骤yaml- name: Deploy with Helm run: | # 设置参数 ENVIRONMENT${{ github.event.inputs.environment }} IMAGE_TAG${{ github.event.inputs.image_tag }} NAMESPACEcamp-${ENVIRONMENT} if [[ ${{ github.event.inputs.dry_run }} true ]]; then # Dry-run 模式使用 helm template 渲染 YAML echo Dry-run mode: rendering Helm templates only helm template camp-${ENVIRONMENT} helm/camp \ --namespace $NAMESPACE \ --values helm/camp/values.yaml \ --values environments/values-${ENVIRONMENT}.yaml \ --set image.tag$IMAGE_TAG \ --debug else # 实际部署模式 kubectl create namespace $NAMESPACE --dry-runclient -o yaml | kubectl apply -f - helm upgrade --install camp-${ENVIRONMENT} helm/camp \ --namespace $NAMESPACE \ --values helm/camp/values.yaml \ --values environments/values-${ENVIRONMENT}.yaml \ --set image.tag$IMAGE_TAG \ --wait \ --timeout 10m fi三、Git 推送问题及解决3.1 Git 代理配置错误在尝试将本地修改推送到 GitHub 时遇到了代理配置问题错误信息textfatal: unable to access ...: Unsupported proxy syntax in proxy-server:port原因分析之前设置了无效的 Git 全局代理占位符proxy-server:port。解决方案bash# 查看当前代理配置 git config --global --get http.proxy git config --global --get https.proxy # 清除无效代理 git config --global --unset http.proxy git config --global --unset https.proxy3.2 远程仓库推送冲突错误信息text! [rejected] main - main (fetch first) error: failed to push some refs to ...原因分析远程仓库已有初始提交如 README.md本地没有这些提交导致历史不一致。解决方案使用强制推送bashgit push -f origin main⚠️注意强制推送会覆盖远程仓库的现有提交。在自己 Fork 的仓库中使用是安全的但在团队协作中请谨慎使用。四、最终成功的运行结果在完成所有修复并推送代码后手动触发工作流终于看到Success状态4.1 运行摘要作业耗时状态Build and Lint5s✅ 成功Build and Push0s⏭️ 跳过buildfalseDeploy to Cluster19s✅ 成功4.2 关键验证点✅helm lint通过无语法错误✅helm template成功渲染所有 Kubernetes 资源✅ 无需连接真实集群即可预览部署配置✅ 工作流总耗时仅 29 秒五、完整工作流 YAML最终版本以下是最终修复后完整的build-and-deploy.yml文件供参考yamlname: Build and Deploy CAMP Platform on: push: branches: [ main, develop ] tags: [ v* ] pull_request: branches: [ main ] workflow_dispatch: inputs: build: description: Build and push images required: false type: boolean default: false cluster: description: Target cluster required: true type: choice options: - local-dev - aks-dev - aks-staging - aks-prod default: local-dev environment: description: Target environment required: true type: choice options: - dev - staging - prod default: dev image_tag: description: Docker image tag required: false type: string default: latest deploy: description: Deploy after build required: false type: boolean default: false dry_run: description: Dry run deployment required: false type: boolean default: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true REGISTRY: ghcr.io IMAGE_NAME: kubernetes-microservices jobs: build-and-lint: name: Build and Lint runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkoutv4 - name: Validate project structure run: | echo Checking project structure... test -f camp-backend/requirements.txt || { echo requirements.txt not found; exit 1; } test -f camp-backend/run.py || { echo run.py not found; exit 1; } test -f helm/camp/Chart.yaml || { echo Helm Chart.yaml not found; exit 1; } test -f README.md || { echo README.md not found; exit 1; } echo ✅ Project structure validation passed - name: Set up Helm uses: azure/setup-helmv3 with: version: v3.12.0 - name: Validate Helm chart run: | helm lint helm/camp - name: Test chart rendering run: | helm template camp-release helm/camp --dry-run - name: Validate Dockerfiles run: | echo Checking Dockerfile existence... test -f camp-backend/Dockerfile || { echo Backend Dockerfile not found; exit 1; } test -f camp-web-frontend/Dockerfile || { echo Web frontend Dockerfile not found; exit 1; } test -f camp-auth-frontend/Dockerfile || { echo Auth frontend Dockerfile not found; exit 1; } echo ✅ All Dockerfiles found build-and-push: name: Build and Push runs-on: ubuntu-latest needs: build-and-lint # 关键修复移除 secrets 引用 if: github.event_name workflow_dispatch github.event.inputs.build true permissions: contents: read packages: write outputs: image-tag: ${{ steps.meta.outputs.tags }} image-digest: ${{ steps.build.outputs.digest }} steps: - name: Checkout code uses: actions/checkoutv4 - name: Set up Docker Buildx uses: docker/setup-buildx-actionv3 - name: Log in to Container Registry uses: docker/login-actionv3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GHCR_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-actionv5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | typeref,eventbranch typeref,eventpr typesemver,pattern{{version}} typesemver,pattern{{major}}.{{minor}} typesemver,pattern{{major}} typesha,prefix{{branch}}- typeraw,valuelatest,enable{{is_default_branch}} - name: Build and push backend image uses: docker/build-push-actionv6 with: context: ./camp-backend push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/camp-backend:${{ steps.meta.outputs.version }} labels: ${{ steps.meta.outputs.labels }} cache-from: typegha cache-to: typegha,modemax platforms: linux/amd64,linux/arm64 - name: Build and push web frontend image uses: docker/build-push-actionv6 with: context: ./camp-web-frontend push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/camp-web:${{ steps.meta.outputs.version }} labels: ${{ steps.meta.outputs.labels }} cache-from: typegha cache-to: typegha,modemax platforms: linux/amd64,linux/arm64 - name: Build and push auth frontend image uses: docker/build-push-actionv6 with: context: ./camp-auth-frontend push: true tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/camp-auth:${{ steps.meta.outputs.version }} labels: ${{ steps.meta.outputs.labels }} cache-from: typegha cache-to: typegha,modemax platforms: linux/amd64,linux/arm64 deploy: name: Deploy to Cluster runs-on: ubuntu-latest needs: build-and-lint if: github.event_name workflow_dispatch github.event.inputs.deploy true steps: - name: Checkout code uses: actions/checkoutv4 - name: Set up tools run: | curl https://get.helm.sh/helm-v3.12.0-linux-amd64.tar.gz | tar xz sudo mv linux-amd64/helm /usr/local/bin/ curl -LO https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 sudo chmod x /usr/local/bin/yq - name: Configure Azure CLI (for AKS) if: startsWith(github.event.inputs.cluster, aks) uses: azure/loginv1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Get AKS credentials if: startsWith(github.event.inputs.cluster, aks) run: | CLUSTER_INFO$(yq eval .clusters.${{ github.event.inputs.cluster }} clusters/cluster-config.yaml) RESOURCE_GROUP$(echo $CLUSTER_INFO | yq eval .resourceGroup -) CLUSTER_NAME$(echo $CLUSTER_INFO | yq eval .clusterName -) az aks get-credentials --resource-group $RESOURCE_GROUP --name $CLUSTER_NAME --overwrite-existing # 关键修复仅在非 dry-run 且为 AKS 时执行 - name: Configure kubectl context if: github.event.inputs.dry_run ! true startsWith(github.event.inputs.cluster, aks) run: | CLUSTER_CONTEXT$(yq eval .clusters.${{ github.event.inputs.cluster }}.context clusters/cluster-config.yaml) kubectl config use-context $CLUSTER_CONTEXT - name: Verify cluster connectivity if: github.event.inputs.dry_run ! true startsWith(github.event.inputs.cluster, aks) run: | kubectl cluster-info kubectl get nodes - name: Deploy with Helm run: | ENVIRONMENT${{ github.event.inputs.environment }} IMAGE_TAG${{ github.event.inputs.image_tag }} NAMESPACEcamp-${ENVIRONMENT} if [[ $IMAGE_TAG latest ]]; then if [[ ${{ needs.build-and-push.outputs.image-tag }} ! ]]; then IMAGE_TAG${{ needs.build-and-push.outputs.image-tag }} fi fi # 核心修复dry-run 时使用 helm template if [[ ${{ github.event.inputs.dry_run }} true ]]; then echo Dry-run mode: rendering Helm templates only helm template camp-${ENVIRONMENT} helm/camp \ --namespace $NAMESPACE \ --values helm/camp/values.yaml \ --values environments/values-${ENVIRONMENT}.yaml \ --set image.tag$IMAGE_TAG \ --debug else kubectl create namespace $NAMESPACE --dry-runclient -o yaml | kubectl apply -f - helm upgrade --install camp-${ENVIRONMENT} helm/camp \ --namespace $NAMESPACE \ --values helm/camp/values.yaml \ --values environments/values-${ENVIRONMENT}.yaml \ --set image.tag$IMAGE_TAG \ --wait \ --timeout 10m fi - name: Verify deployment if: github.event.inputs.dry_run ! true run: | NAMESPACEcamp-${{ github.event.inputs.environment }} kubectl wait --forconditionavailable --timeout300s deployment/camp-${{ github.event.inputs.environment }}-backend -n $NAMESPACE kubectl wait --forconditionavailable --timeout300s deployment/camp-${{ github.event.inputs.environment }}-web -n $NAMESPACE kubectl wait --forconditionavailable --timeout300s deployment/camp-${{ github.event.inputs.environment }}-auth -n $NAMESPACE kubectl get pods -n $NAMESPACE kubectl get services -n $NAMESPACE kubectl get ingress -n $NAMESPACE - name: Run smoke tests if: github.event.inputs.dry_run ! true run: | NAMESPACEcamp-${{ github.event.inputs.environment }} INGRESS_URL$(kubectl get ingress -n $NAMESPACE -o jsonpath{.items[0].status.loadBalancer.ingress[0].ip} 2/dev/null || echo ) if [[ -n $INGRESS_URL ]]; then curl -f http://$INGRESS_URL/api/test || exit 1 curl -f http://$INGRESS_URL/api/health || exit 1 curl -f http://$INGRESS_URL/ || exit 1 curl -f http://$INGRESS_URL/auth || exit 1 echo ✅ Smoke tests passed else echo ⚠️ No ingress URL found, skipping smoke tests fi六、关键知识点总结6.1 GitHub Actions 核心要点知识点说明if条件限制不能直接使用secrets上下文应使用env中转或简化条件workflow_dispatch手动触发工作流支持自定义输入参数needs依赖控制作业执行顺序被依赖的作业失败会影响后续作业permissions控制 GITHUB_TOKEN 的权限如packages: write用于推送镜像6.2 Helm 使用技巧场景推荐命令验证 Chart 语法helm lint chart预览渲染结果无需集群helm template release chart模拟安装需要集群连接helm install --dry-run --debug预览模板变量值helm template --debug查看COMPUTED VALUES6.3 Dry-run 最佳实践在 CI 中实现 dry-run 的正确姿势代码验证使用helm lint检查语法模板渲染预览使用helm template输出 YAML不需要集群避免使用helm upgrade --dry-run需要集群连接6.4 Git 代理配置bash# 设置代理 git config --global http.proxy http://proxy:port git config --global https.proxy https://proxy:port # 清除代理 git config --global --unset http.proxy git config --global --unset https.proxy6.5 问题排查流程遇到 CI 失败时按以下顺序排查查看 Actions 日志定位具体失败的作业和步骤检查 YAML 语法GitHub 会标注Invalid workflow file本地模拟测试在本地执行helm lint和helm template检查条件逻辑确认if条件是否正确验证环境变量确认env和secrets是否传递正确七、收获与总结通过这次实践我不仅成功跑通了一个完整的 Kubernetes 微服务项目的 CI/CD 流水线更重要的是学会了调试 GitHub Actions 工作流从 YAML 语法到逻辑条件每个环节都有了更深入的理解。掌握了 Helm 在 CI 中的最佳实践dry-run 不等于helm upgrade --dry-run使用helm template更轻量、更安全。理解了敏感信息处理的限制GitHub 对secrets的保护机制以及在if条件中不能直接使用的原因。积累了 Git 协作经验代理配置、强制推送等实用技巧。希望这篇文章能帮助到正在学习 GitHub Actions Kubernetes 的你。如果有什么问题欢迎在评论区交流讨论相关链接GitHub Actions 文档Helm 官方文档项目仓库地址