Kubernetes 部署 GitLab Runner 及 Java CI/CD 实践指南

本文档包含如何在 Kubernetes 集群中部署 GitLab Runner,并配置基于 Git 管理部署清单的 Java 项目 CI/CD 流水线。

1. 在 Kubernetes 中部署 GitLab Runner

⚠️ 重要前置:配置 Kubernetes 全局域名解析

由于我们使用内部 IP 搭建了 GitLab 和 Registry(例如 172.30.187.7),如果在 Kubernetes 中不配置全局解析,会导致两个致命问题:

  1. Kubelet 无法解析 registry.aioil.top,导致拉取基础镜像时报 ErrImagePull: no such host
  2. Runner 无法解析 gitlab.aioil.top,导致无法拉取代码。

解决步骤:

  1. 修改 CoreDNS 的 ConfigMap 添加全局解析:
    kubectl edit configmap coredns -n kube-system
    
  2. Corefileready 下方插入 hosts 配置:
            ready
            # 新增这块配置
            hosts {
                172.30.187.7 gitlab.aioil.top
                172.30.187.7 registry.aioil.top
                fallthrough
            }
            kubernetes cluster.local in-addr.arpa ip6.arpa {
    
  3. 重启 CoreDNS 生效:
    kubectl rollout restart deployment coredns -n kube-system
    
  4. 双保险(极其重要):为了防止某些容器运行时直接读取宿主机网络,请在所有 Kubernetes 节点(Master 和 Worker)的宿主机上执行:
    echo "172.30.187.7 gitlab.aioil.top registry.aioil.top" | sudo tee -a /etc/hosts
    

推荐使用官方 Helm Chart 部署 GitLab Runner。

1.1 前置准备

  1. 确保已安装 Helm。
  2. 在 GitLab 中获取全局 Runner 注册令牌 (Registration Token):访问 http://gitlab.aioil.top/admin/runners(Admin area -> Runners),点击页面右上角 “New instance runner” 旁边的三个点 按钮,在弹出的菜单中点击复制图标获取 “Registration token”。

1.2 添加 Helm 仓库并配置

helm repo add gitlab https://charts.gitlab.io/
helm repo update

创建 values.yaml 配置文件:

# GitLab 服务器的 URL
cat << 'EOF' > values.yaml
# GitLab 服务器的 URL
gitlabUrl: "https://gitlab.aioil.top/"

# 解决 Helm Chart 注册阶段报错 x509: certificate is valid for higress-gateway
# 将这三项全部设置为 true/空 才能彻底跳过注册时的验证
certsSecretName: ""
cert: ""
tlsVerify: false

# 刚才获取的 Runner Token
runnerRegistrationToken: "X8cmMT-a5nEdFsREEJcA"

# 开启 RBAC 权限(允许 Runner 在 K8s 中创建 Pod 来执行 Job)
rbac:
  create: true
  clusterWideAccess: true

# Runner 的并发数限制
concurrent: 10

runners:
  # 默认的镜像,当 .gitlab-ci.yml 中没有指定 image 时使用
  image: ubuntu:22.04
  # 分配给这个 Runner 的标签,必须与注册时填写的匹配
  tags: "k8s-runner"
  
  # K8s 执行器配置
  config: |
    concurrent = 10
    [[runners]]
      environment = ["GIT_SSL_NO_VERIFY=true"]
      tls-skip-verify = true
      request_concurrency = 4
      [runners.kubernetes]
        namespace = "{{.Release.Namespace}}"
        image = "ubuntu:22.04"
        privileged = false
EOF

注意:现代 K8s CI/CD 推荐使用 Kaniko 构建镜像,因此不需要开启 privileged = true

1.3 执行部署

helm upgrade --install gitlab-runner gitlab/gitlab-runner \
  --namespace gitlab-runner --create-namespace \
  -f values.yaml

部署完成后,可以通过以下几种方式验证服务是否正常运行:

1. 检查 Kubernetes 中的 Pod 状态

kubectl get pods -n gitlab-runner

正常情况下,你应该能看到类似 gitlab-runner-xxxx-xxxx 的 Pod 处于 Running 状态。

2. 查看 Runner 注册日志

kubectl logs -f -l app=gitlab-runner -n gitlab-runner

在日志中,寻找包含 Registering runner... succeeded 的输出,这说明 Runner 已成功连接到你的 GitLab 实例。

3. 在 GitLab 界面验证
访问您的 GitLab 管理后台:http://gitlab.aioil.top/admin/runners(Admin area -> Runners)。
如果一切正常,你应该能在页面上看到一个**带绿色圆点(在线状态)**的新 Runner,并且带有 k8s-runner 的标签。


2. Java CI/CD 流水线配置

通过 .gitlab-ci.yml 实现构建、测试、打包(Kaniko)以及使用 Git 中管理的 Kubernetes YAML 文档进行部署。

2.1 目录结构要求

项目代码库中除了 Java 代码,还应包含 Dockerfile 和用于部署的 k8s-manifests 目录。

my-java-app/
├── src/
├── pom.xml
├── Dockerfile
├── k8s-manifests/           <-- Git 管理的部署测试文档
│   ├── deployment.yaml
│   └── service.yaml
└── .gitlab-ci.yml

2.2 编写 .gitlab-ci.yml

workflow:
  rules:
    # 如果是 Tag 提交,使用 Tag 作为镜像标签
    - if: $CI_COMMIT_TAG
      variables:
        IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
    # 否则,默认使用 SHA 作为镜像标签
    - if: $CI_COMMIT_BRANCH
      variables:
        IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA

stages:
  - build
  - test
  - package
  - deploy

variables:
  MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
  # Git 管理的部署文档所在目录
  K8S_DEPLOY_DIR: "k8s-manifests"

# 在每个 Job 执行前运行,用于打印变量方便 Debug
before_script:
  - echo "====== Print Variables for Debugging ======"
  - echo "MAVEN_OPTS=$MAVEN_OPTS"
  - echo "IMAGE_TAG=$IMAGE_TAG"
  - echo "K8S_DEPLOY_DIR=$K8S_DEPLOY_DIR"
  - echo "CI_REGISTRY_IMAGE=$CI_REGISTRY_IMAGE"
  - echo "CI_COMMIT_SHORT_SHA=$CI_COMMIT_SHORT_SHA"
  - echo "CI_REGISTRY=$CI_REGISTRY"
  - echo "CI_REGISTRY_USER=$CI_REGISTRY_USER"
  - echo "CI_PROJECT_DIR=$CI_PROJECT_DIR"
  - echo "==========================================="

# 缓存 Maven 依赖加速构建
cache:
  paths:
    - .m2/repository/
    - target/

# 1. 编译与打包
build:
  stage: build
  # 替换为您私有仓库中的 Maven 基础镜像
  image: registry.aioil.top/prod/infra/base-images/maven:3.8.6-openjdk-11
  script:
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - target/*.jar
    expire_in: 1 hour
  tags:
    - k8s-runner

# 2. 测试
test:
  stage: test
  image: registry.aioil.top/prod/infra/base-images/maven:3.8.6-openjdk-11
  script:
    - mvn test
  tags:
    - k8s-runner

# 3. 构建 Docker 镜像 (使用 Kaniko,无需 Docker in Docker)
package:
  stage: package
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  # 因为 Kaniko 镜像里没有 maven 等常规工具,所以需要覆盖全局的 before_script
  before_script:
    - echo "Skip global before_script in Kaniko job to avoid 'command not found' errors."
  script:
    # 配置 Registry 认证
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
    # 构建并推送镜像 (由于线上 Registry 配置为 HTTP 协议,需要增加 insecure 参数)
    - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $IMAGE_TAG --insecure --insecure-pull
  tags:
    - k8s-runner

# 4. 部署到 K8s
deploy:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    # 将 Git 部署文档中的镜像占位符替换为刚刚构建的镜像 TAG
    - sed -i "s|APP_IMAGE_PLACEHOLDER|$IMAGE_TAG|g" $K8S_DEPLOY_DIR/deployment.yaml
    # 打印修改后的 deployment.yaml,确认镜像已被正确替换
    - "cat $K8S_DEPLOY_DIR/deployment.yaml | grep image:"
    # 应用 Git 管理的清单文件进行部署
    - kubectl apply -f $K8S_DEPLOY_DIR/
    # 强制 K8s 重启该 Deployment 以拉取新镜像并应用最新状态;如果是首次部署报错则忽略
    - "kubectl rollout restart deployment/java-app --namespace=default || true"
    # 等待部署完成,确保 Pod 正常运行
    - kubectl rollout status deployment/java-app --namespace=default --timeout=300s
  environment:
    name: production
  tags:
    - k8s-runner
  only:
    # 触发条件:只有当 Git 标签 (Tag) 满足 vX.Y.Z 格式时才执行部署
    - /^v\d+\.\d+\.\d+$/

注意:在 deploy 阶段,Runner 使用的 ServiceAccount 需要有操作对应 Namespace 资源的权限(在安装 Runner 的 values.yaml 中配置 RBAC)。

2.3 Git 管理的 Kubernetes 部署文档示例

k8s-manifests/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: java-app
  template:
    metadata:
      labels:
        app: java-app
    spec:
      containers:
      - name: java-app
        # 这里的 APP_IMAGE_PLACEHOLDER 会被 CI 脚本中的 sed 命令替换
        image: APP_IMAGE_PLACEHOLDER
        ports:
        - containerPort: 8080
        # 容器内健康检查测试配置
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
      imagePullSecrets:
        - name: gitlab-registry-secret

k8s-manifests/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: java-app-svc
spec:
  selector:
    app: java-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

2.4 配置 Kubernetes 拉取私有镜像仓库的凭证

在 Kubernetes 部署应用时,集群需要从 GitLab Container Registry 拉取我们构建好的私有镜像。最官方、最稳定、最安全的做法是使用 Deploy Tokens (部署令牌) 创建 Kubernetes Secret,而不是使用个人访问令牌或 CI 环境变量(CI_JOB_TOKEN 的有效期仅限于流水线执行期间,会导致 Pod 重启时拉取镜像失败)。

第一步:在 GitLab 中生成访问凭证 (Token)

根据您的使用场景,您可以选择生成项目级、群组级或全局级别的 Token:

方案 A:生成项目级 Deploy Token(推荐用于单个项目)

  1. 进入您的项目页面
  2. 左侧菜单导航至 Settings (设置) -> Repository (仓库)
  3. 展开 Deploy tokens (部署令牌) 区域。
  4. 点击 Add token (添加令牌)
    • Name (名称):填入 k8s-registry-token
    • Scopes (权限范围):勾选 read_registry(如果 CI 还需要推送镜像,需一并勾选 write_registry
  5. 点击 Create deploy token,复制屏幕顶部生成的 UsernameToken

方案 B:生成群组级 Deploy Token(推荐用于微服务架构)
如果您有多个微服务项目放在同一个 GitLab Group(群组)下,可以在 Group 级别生成:

  1. 进入您的 Group (群组) 页面
  2. 导航至 Settings (设置) -> Repository (仓库) -> Deploy tokens
  3. 创建步骤同上。使用这个 Token,K8s 可以拉取该群组下所有项目的镜像。

方案 C:生成全局管理员 Token(仅限私有环境,通杀所有项目)
如果您希望 Kubernetes 集群只需配置一个 Secret 就能拉取 GitLab 里的任意镜像,可以创建一个全局的个人访问令牌:

  1. 使用管理员账号(如 root)登录 GitLab。
  2. 点击右上角头像 -> Edit profile (编辑个人资料) -> 左侧菜单选择 Access Tokens (访问令牌)
  3. 点击 Add new token
    • Token name: k8s-global-registry-token
    • Expiration date: 留空(不过期)或设置一个较长的日期
    • Scopes: 勾选 read_registry(仅用于 K8s 拉取镜像)
  4. 点击 Create personal access token
  5. 注意:这种情况下,您的 Username 就是您登录的用户名(例如 root),Password 就是生成的这个 Token。
第二步:在 K8s 中创建 Registry Secret

概念辨析:为什么需要创建 Secret(2.4 章节)还要配置 DOCKER_AUTH_CONFIG(2.5 章节)?

  • 2.4 章节的 Secret:是给业务应用使用的。当 CI/CD 流水线跑完,您的代码已经被打包成镜像。此时 K8s 需要把这个业务镜像拉下来运行,它使用的是 deployment.yaml 中配置的 imagePullSecrets
  • 2.5 章节的 DOCKER_AUTH_CONFIG:是给 GitLab Runner 的 CI 任务容器使用的。当流水线刚开始运行时,Runner 需要拉取一个“基础环境镜像”(比如带有 Maven、Node.js 的环境)来执行编译脚本,此时 K8s 还没有开始部署业务,它拉取基础镜像的凭证来自于 DOCKER_AUTH_CONFIG 变量注入。

结论:这两个配置必须同时存在。前者负责最终业务上线时的镜像拉取,后者负责 CI 过程构建环境的镜像拉取,它们作用于 CI/CD 生命周期的不同阶段。

在 Kubernetes 集群控制节点(Master 节点)上执行以下命令,将上一步复制的凭证填入:

kubectl create secret docker-registry gitlab-registry-secret \
  --docker-server=registry.aioil.top \
  --docker-username=root \
  --docker-password=glpat-3_8PwL74JJ8YLxW9ydVn \
  --namespace=default

(注意:请确保 --namespace 与您的业务应用部署的 Namespace 保持一致。)

总结优势
  1. 安全性高:Deploy Token 只与当前项目绑定,且仅具备读镜像权限,不会泄露个人账号权限。
  2. 稳定性强:Token 永久有效,无论未来 K8s 何时重启 Pod 或扩容,拉取镜像都不会报错。
  3. CI 解耦:CI 流水线脚本无需处理权限生成逻辑,保持干净整洁。

2.5 解决 GitLab Runner 无法拉取私有基础镜像的问题

当我们在 .gitlab-ci.ymlimage 字段指定使用我们自己私有仓库(例如 registry.aioil.top/...)的基础镜像时,可能会遇到拉取失败(insufficient_scope: authorization failed)的问题。

原因分析:虽然 CI 脚本有 $CI_JOB_TOKEN 的权限,但负责底层创建 Pod 的 K8s Kubelet 默认是不携带这些认证信息的,导致被 Registry 拒绝。

解决办法:通过配置 DOCKER_AUTH_CONFIG 变量,GitLab Runner 在向 K8s 发起创建 Pod 的请求时,会自动将拉取凭证注入给 Kubelet。

操作步骤:

1. 生成认证字符串
在任意终端执行以下命令(建议使用前面生成的 Deploy Token 用户名和密码):

# 将 Username 和 Password 替换为您实际的 Deploy Token 信息
echo -n "root:glpat-3_8PwL74JJ8YLxW9ydVn" | base64 -w 0

假设输出的结果是 cm9vdDpnbHBhdC0zXzhQd0w3NEpKOFlMeFc5eWRWbg==

2. 组装 JSON 字符串
将上面的 Base64 结果填入以下 JSON 模板的 auth 字段中,并替换您的 Registry 地址:

{
    "auths": {
        "registry.aioil.top": {
            "auth": "cm9vdDpnbHBhdC0zXzhQd0w3NEpKOFlMeFc5eWRWbg=="
        }
    }
}

3. 在 GitLab 中配置变量 (支持项目/群组/全局)

您可以根据实际需求,将这个变量配置在不同的级别,作用范围越广,需要重复配置的次数越少。

选项 A:配置为全局级(Instance-level) - 推荐私有化环境
如果配置在此处,GitLab 实例中的所有项目在运行 CI 时都会自动携带这个拉取凭证。

  1. 以管理员(root)身份登录 GitLab。
  2. 访问管理后台:Admin area -> Settings -> CI/CD
  3. 展开 Variables,点击 Add variable
  4. 填写要求同下。

选项 B:配置为群组级(Group-level)

  1. 进入对应的 Group (群组) 页面
  2. 左侧菜单导航至 Settings (设置) -> CI/CD
  3. 展开 Variables,点击 Add variable
  4. 填写要求同下。

选项 C:配置为项目级(Project-level)

  1. 进入具体的 项目页面
  2. 导航至 Settings (设置) -> CI/CD -> Variables

变量具体填写内容:

  • Key (键): 必须严格填写 DOCKER_AUTH_CONFIG
  • Value (值): 把上面组装好的整段 JSON 粘贴进去。
  • Type (类型): 保持 Variable 不变。
  • 取消勾选 Protect variable(确保非保护分支也能使用该凭证拉取基础镜像)。
  • 点击 Add variable 保存。

配置完成后,重新运行流水线,Runner 就能成功拉取私有仓库中的基础镜像并启动 CI 容器了。


2.6 CI/CD 部署到 K8s 的权限配置(两种方案)

deploy 阶段执行 kubectl apply 时,需要有操作 K8s 集群的权限。以下提供两种常见的配置方案,您可以根据实际需求任选其一:

方案一:为 Runner 配置 RBAC 权限(推荐,同集群部署)

如果您的 GitLab Runner 与目标业务应用部署在同一个 Kubernetes 集群,最安全且原生的方式是通过 RBAC 赋予执行 Job 的 Pod 权限。

1. 权限范围解析与安全建议
  • ClusterRole (gitlab-runner-deploy-clusterrole): 定义了集群级别的权限规则,允许对所有命名空间下的 Deployment、Service、Pod 和 ReplicaSet 等核心资源执行增删改查操作。
  • ClusterRoleBinding (gitlab-runner-deploy-clusterrolebinding): 将上述定义好的全局权限角色绑定给 gitlab-runner 命名空间下的 default ServiceAccount。因为 Runner 动态创建的执行 Job 的 Pod 默认会使用这个 ServiceAccount。

⚠️ 安全评估
当前示例使用的是 ClusterRoleClusterRoleBinding,这意味着 GitLab Runner 拥有操作整个 K8s 集群所有命名空间的能力。

  • 优点:配置极其简单,适合公司内部完全互信的私有环境,一个配置打通所有项目的部署。
  • 风险:如果 GitLab 被恶意人员访问,他们可以通过编写恶意的 .gitlab-ci.yml 去删除或篡改集群中其他不相关项目(甚至是 kube-system)的资源。
  • 加固建议(可选):如果是多团队共享的生产集群,建议将 ClusterRole 改为 Role,将 ClusterRoleBinding 改为 RoleBinding,并逐个指定允许操作的业务命名空间(Namespace),实现严格的权限隔离。
2. 生成文件并应用配置

为了方便后期排查和维护,建议将配置输出到文件中,然后再应用。在服务器终端执行以下 Shell 命令:

cat << 'EOF' > gitlab-runner-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1 
kind: ClusterRole 
metadata: 
  name: gitlab-runner-deploy-clusterrole 
rules: 
- apiGroups: ["", "apps", "extensions"] 
  resources: ["deployments", "services", "pods", "replicasets"] 
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 
--- 
apiVersion: rbac.authorization.k8s.io/v1 
kind: ClusterRoleBinding 
metadata: 
  name: gitlab-runner-deploy-clusterrolebinding 
subjects: 
- kind: ServiceAccount 
  name: default 
  namespace: gitlab-runner 
roleRef: 
  kind: ClusterRole 
  name: gitlab-runner-deploy-clusterrole 
  apiGroup: rbac.authorization.k8s.io
EOF

# 应用配置
kubectl apply -f gitlab-runner-rbac.yaml
方案二:通过 CI/CD 变量注入 kubeconfig(适合跨集群部署)

如果您不需要/不想在集群内配置 RBAC,或者您需要将应用部署到其他远程 Kubernetes 集群,可以将具有目标集群操作权限的 kubeconfig 文件内容作为 GitLab CI 变量注入到流水线中。

1. 在 GitLab 中配置变量 (支持项目/群组/全局)

您可以根据管理范围,将 KUBECONFIG_CONTENT 变量配置在不同级别:

  • 全局级(Instance-level):访问 Admin area -> Settings -> CI/CD -> Variables

    ⚠️ 安全警告:如果配置在全局,意味着 GitLab 上的所有项目都能获取您的 K8s 集群操作权限。除非这是您个人的私有环境且所有项目绝对互信,否则极度不推荐!

  • 群组级(Group-level):在群组的 Settings -> CI/CD -> Variables 配置,适合将业务部署到同一个集群的微服务群组。
  • 项目级(Project-level):在单个项目的 Settings -> CI/CD -> Variables 配置,权限隔离最好。

配置具体参数

  1. 复制有权限的集群 ~/.kube/config 文件内容。
  2. 添加一个变量:
    • Key: KUBECONFIG_CONTENT
    • Value: 粘贴您的 kubeconfig 文件内容
    • Type: Variable(纯文本)
    • 取消勾选 “Protect variable”(除非只在受保护的分支上运行)
2. 修改 .gitlab-ci.yml 部署脚本

deploy 阶段的 script 中,在执行 kubectl 之前生成 .kube/config 文件:

deploy:
  stage: deploy
  image: bitnami/kubectl:latest
  script:
    # 1. 注入 kubeconfig (使用纯文本变量)
    - mkdir -p ~/.kube
    - echo "$KUBECONFIG_CONTENT" > ~/.kube/config
    # 2. 替换镜像标签并部署
    - sed -i "s|APP_IMAGE_PLACEHOLDER|$IMAGE_TAG|g" $K8S_DEPLOY_DIR/deployment.yaml
    - kubectl apply -f $K8S_DEPLOY_DIR/

更多推荐