SonarQube 是一个代码质量检测平台,可以用来做静态代码扫描、漏洞检查、重复代码检测、测试覆盖率展示和质量门禁。

日常开发里,它最常见的用法是:

开发提交代码
CI 执行 sonar-scanner 或 Maven/Gradle 扫描
扫描结果上传到 SonarQube
SonarQube 根据规则和质量门禁判断是否通过

这篇文章记录在 Kubernetes 集群里部署 SonarQube Community Build。当前集群已经有:

Kubernetes 三主节点
Longhorn 存储
ingress-nginx
MetalLB
cert-manager
CloudNativePG

所以本文采用的部署方式是:

SonarQube Community Build
官方 Helm Chart
外部 PostgreSQL
Longhorn 持久化
ingress-nginx HTTPS 访问

访问域名示例:

https://sonarqube.jihw.top

SonarQube 能做什么

SonarQube 的核心能力包括:

Bugs              可能导致运行错误的问题
Vulnerabilities   安全漏洞
Security Hotspots 需要人工确认的安全风险点
Code Smells       可维护性问题
Duplications      重复代码
Coverage          单元测试覆盖率
Quality Gate      质量门禁

它适合接入 CI/CD,而不是只当成一个偶尔打开的网站。

比较推荐的流程是:

1. 在 SonarQube 创建项目
2. 为项目生成 Token
3. 在 CI 里执行代码扫描
4. 让质量门禁决定是否允许合并或发布

部署前注意事项

SonarQube 不是一个很轻量的组件。它包含 Web、Compute Engine、搜索引擎等内部模块,还依赖 PostgreSQL。

至少准备:

CPU:    2 core 起步
Memory: 4Gi 起步
Disk:   20Gi 起步
DB:     PostgreSQL

如果只是学习和小团队使用,可以先按本文资源配置跑起来。正式团队使用时,建议单独评估 CPU、内存、数据库备份和磁盘容量。

还有一个重要前置条件:SonarQube 的搜索组件需要节点内核参数支持。

在每个可能运行 SonarQube Pod 的节点上设置:

cat <<'EOF' >/etc/sysctl.d/99-sonarqube.conf
vm.max_map_count=524288
fs.file-max=131072
EOF

sysctl --system

确认:

sysctl vm.max_map_count
sysctl fs.file-max

如果这些参数不满足,SonarQube Pod 可能会启动失败或不断重启。

创建命名空间

kubectl create namespace sonarqube

准备 Retain 类型 StorageClass

SonarQube 自身和 PostgreSQL 都是有状态组件,不建议使用 Delete 回收策略。

如果 PVC 被误删,Delete 策略会连底层卷一起删除;Retain 策略会保留 PV 和底层数据,给人工恢复留下机会。

当前集群使用 Longhorn,可以创建一个专门给重要数据使用的 StorageClass:

cat <<'EOF' | kubectl apply -f -
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-retain
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: Immediate
parameters:
  numberOfReplicas: "3"
  staleReplicaTimeout: "30"
  fsType: ext4
EOF

查看:

kubectl get storageclass

如果只是测试环境,也可以继续使用默认的 longhorn。但数据库这类数据卷更推荐 Retain

部署 PostgreSQL

SonarQube 正式使用时应该使用外部 PostgreSQL,不建议依赖临时内置数据库。

当前集群已经使用 CloudNativePG,所以这里用 CloudNativePG 创建一个 PostgreSQL:

cat <<'EOF' | kubectl apply -f -
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: sonarqube-postgresql
  namespace: sonarqube
spec:
  instances: 1

  storage:
    size: 10Gi
    storageClass: longhorn-retain

  bootstrap:
    initdb:
      database: sonarqube
      owner: sonar
EOF

等待 PostgreSQL Ready:

kubectl -n sonarqube get cluster
kubectl -n sonarqube get pods -l cnpg.io/cluster=sonarqube-postgresql -o wide

CloudNativePG 会自动创建应用账号 Secret,通常名字是:

sonarqube-postgresql-app

查看:

kubectl -n sonarqube get secret | grep sonarqube-postgresql

确认数据库连接信息:

kubectl -n sonarqube get secret sonarqube-postgresql-app \
  -o jsonpath='{.data.jdbc-uri}' | base64 -d

SonarQube 连接 PostgreSQL 的地址是:

jdbc:postgresql://sonarqube-postgresql-rw.sonarqube.svc.cluster.local:5432/sonarqube

准备 Helm 仓库

添加 SonarSource 官方 Helm 仓库:

helm repo add sonarqube https://SonarSource.github.io/helm-chart-sonarqube
helm repo update

查看 chart:

helm search repo sonarqube

准备 values.yaml

创建 sonarqube-values.yaml

community:
  enabled: true

monitoringPasscode: "change-this-passcode"

initSysctl:
  enabled: false

service:
  type: ClusterIP

persistence:
  enabled: true
  storageClass: longhorn-retain
  size: 20Gi

jdbcOverwrite:
  enabled: true
  jdbcUrl: "jdbc:postgresql://sonarqube-postgresql-rw.sonarqube.svc.cluster.local:5432/sonarqube"
  jdbcUsername: "sonar"
  jdbcSecretName: "sonarqube-postgresql-app"
  jdbcSecretPasswordKey: "password"

resources:
  requests:
    cpu: 500m
    memory: 2Gi
  limits:
    cpu: "2"
    memory: 4Gi

plugins:
  install:
    - "https://github.com/xuhuisheng/sonar-l10n-zh/releases/download/sonar-l10n-zh-plugin-26.5/sonar-l10n-zh-plugin-26.5.jar"

# 可选:把 SonarQube 固定到一个稳定节点,减少 RWO 卷跨节点挂载。
# nodeSelector:
#   kubernetes.io/hostname: k8s-master2

ingress:
  enabled: true
  ingressClassName: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-alidns-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "100m"
  hosts:
    - name: sonarqube.jihw.top
      path: /
      pathType: Prefix
  tls:
    - secretName: sonarqube-tls
      hosts:
        - sonarqube.jihw.top

几个重点:

community.enabled=true 表示启用免费版 Community Build
initSysctl.enabled=false 表示前面已经在节点上手动配置 sysctl
jdbcOverwrite 指向外部 PostgreSQL
persistence 使用 Longhorn 持久化 SonarQube 数据
plugins.install 安装中文语言包
ingressClassName=nginx 对应当前 ingress-nginx
cert-manager.io/cluster-issuer 让 cert-manager 自动签发证书

cert-manager.io/cluster-issuer 要写成当前集群真实存在的 ClusterIssuer。可以先查看:

kubectl get clusterissuer

当前集群里可用的是:

letsencrypt-alidns-prod
letsencrypt-alidns-staging

monitoringPasscode 不要照抄示例值,实际部署时换成随机字符串。

可以生成一个:

openssl rand -hex 24

安装 SonarQube

执行 Helm 安装:

helm upgrade --install sonarqube sonarqube/sonarqube \
  -n sonarqube \
  -f sonarqube-values.yaml

查看资源:

kubectl -n sonarqube get pods -o wide
kubectl -n sonarqube get pvc
kubectl -n sonarqube get svc
kubectl -n sonarqube get ingress

等待 SonarQube 就绪:

kubectl -n sonarqube rollout status statefulset/sonarqube-sonarqube

如果 StatefulSet 名字不同,先查看:

kubectl -n sonarqube get statefulset

安装中文语言包

SonarQube 默认界面是英文。要变成中文,需要安装中文语言包插件 sonar-l10n-zh

当前部署的 SonarQube 镜像是:

sonarqube:26.5.0.122743-community

所以中文包使用 26.5 版本:

sonar-l10n-zh-plugin-26.5.jar

如果一开始没有在 sonarqube-values.yaml 里写 plugins.install,也可以后面用 Helm 追加:

helm -n sonarqube upgrade sonarqube sonarqube/sonarqube \
  --reuse-values \
  --set 'plugins.install[0]=https://github.com/xuhuisheng/sonar-l10n-zh/releases/download/sonar-l10n-zh-plugin-26.5/sonar-l10n-zh-plugin-26.5.jar'

等待 Pod 重启并就绪:

kubectl -n sonarqube get pods -w
kubectl -n sonarqube logs sonarqube-sonarqube-0 --tail=100

日志里看到下面这类信息,说明 SonarQube 已经启动完成:

SonarQube is operational

然后刷新页面:

https://sonarqube.jihw.top

如果还是英文,可以退出重新登录,或者清理浏览器缓存后再试。

注意:插件版本要和 SonarQube 版本匹配。版本不匹配时,轻则页面语言不生效,重则 SonarQube 启动失败。升级 SonarQube 前,要同步检查中文插件是否有对应版本。

配置域名

Ingress/MetalLB 当前入口地址是:

192.168.3.230

确保 DNS 解析:

sonarqube.jihw.top -> 192.168.3.230

如果只是内网测试,也可以先在本机 hosts 里加:

192.168.3.230 sonarqube.jihw.top

访问:

https://sonarqube.jihw.top

首次登录

默认账号通常是:

username: admin
password: admin

首次登录后会要求修改密码。

建议修改后马上做三件事:

1. 创建一个普通管理员账号
2. 生成项目扫描 Token
3. 配置质量门禁

创建项目

进入 SonarQube 后:

1. Projects
2. Create Project
3. 选择 Manually
4. 输入 Project key 和 Display name
5. 生成 Token
6. 按页面提示选择扫描方式

项目 key 建议用稳定名称:

new-api
jhonlife
backend-service

不要用会频繁变化的分支名或临时目录名。

扫描代码

使用 sonar-scanner

在代码目录下创建 sonar-project.properties

sonar.projectKey=jhonlife
sonar.projectName=jhonlife
sonar.sources=.
sonar.host.url=https://sonarqube.jihw.top

执行扫描:

sonar-scanner \
  -Dsonar.token=你的项目Token

如果本机没有安装 sonar-scanner,可以用容器跑:

docker run --rm \
  -e SONAR_HOST_URL="https://sonarqube.jihw.top" \
  -e SONAR_TOKEN="你的项目Token" \
  -v "$PWD:/usr/src" \
  sonarsource/sonar-scanner-cli

Maven 项目

Maven 项目可以直接执行:

mvn clean verify sonar:sonar \
  -Dsonar.host.url=https://sonarqube.jihw.top \
  -Dsonar.token=你的项目Token

GitHub Actions 示例

name: SonarQube

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: SonarQube Scan
        uses: SonarSource/sonarqube-scan-action@v5
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: https://sonarqube.jihw.top

在 GitHub 仓库里配置 Secret:

SONAR_TOKEN=项目 Token

如果 SonarQube 只在内网访问,GitHub Actions 无法直接连到它。内网环境可以改用自建 Runner,或者在内网 CI 里执行扫描。

常用排查

Pod 一直启动失败

查看日志和事件:

kubectl -n sonarqube get pods
kubectl -n sonarqube describe pod <sonarqube-pod>
kubectl -n sonarqube logs <sonarqube-pod>

重点检查:

vm.max_map_count 是否满足要求
内存是否不足
PostgreSQL 是否 Ready
JDBC 地址、用户名、密码是否正确
PVC 是否 Bound

数据库连不上

检查 PostgreSQL:

kubectl -n sonarqube get cluster
kubectl -n sonarqube get svc | grep postgresql
kubectl -n sonarqube get secret sonarqube-postgresql-app

检查 SonarQube values 里的 JDBC:

jdbcUrl
jdbcUsername
jdbcSecretName
jdbcSecretPasswordKey

Ingress 访问失败

检查:

kubectl -n sonarqube get ingress
kubectl -n sonarqube describe ingress sonarqube-sonarqube
kubectl -n sonarqube get certificate
kubectl -n ingress-nginx get svc ingress-nginx-controller -o wide

如果证书还没签发完成,可以先用:

curl -kI https://sonarqube.jihw.top

这次实际部署时一开始把 ClusterIssuer 写成了不存在的 letsencrypt-prod,证书事件里出现:

Referenced "ClusterIssuer" not found: clusterissuer.cert-manager.io "letsencrypt-prod" not found

修正 Ingress 注解:

kubectl -n sonarqube annotate ingress sonarqube-sonarqube \
  cert-manager.io/cluster-issuer=letsencrypt-alidns-prod \
  --overwrite

为了避免下次 helm upgrade 又改回错误值,还要同步 Helm release:

helm -n sonarqube upgrade sonarqube sonarqube/sonarqube \
  --reuse-values \
  --set-string 'ingress.annotations.cert-manager\.io/cluster-issuer=letsencrypt-alidns-prod'

如果之前已经生成了失败的 CertificateRequest,可以删除 Certificate 让 cert-manager 重新从 Ingress 创建:

kubectl -n sonarqube delete certificate sonarqube-tls --ignore-not-found
kubectl -n sonarqube delete certificaterequest,order,challenge --all --ignore-not-found

重新确认:

kubectl -n sonarqube get certificate,certificaterequest,order,challenge
curl -I https://sonarqube.jihw.top

最终证书状态应该是:

certificate.cert-manager.io/sonarqube-tls   True   sonarqube-tls   letsencrypt-alidns-prod

Longhorn Multi-Attach

这次部署过程中还遇到过一个 Longhorn 卷挂载事件:

Multi-Attach error for volume "pvc-122749df-7e75-49e6-a3e2-3e05cded31d4"
Volume is already exclusively attached to one node and can't be attached to another

这个卷对应的是:

PVC: sonarqube/sonarqube-sonarqube
PV:  pvc-122749df-7e75-49e6-a3e2-3e05cded31d4

原因是 SonarQube 使用的是 ReadWriteOnce 卷。RWO 卷同一时间只能被一个节点以读写方式挂载。

实际事件链路是:

1. sonarqube-sonarqube-0 第一次被调度到 k8s-master1
2. Longhorn 卷挂载到了 k8s-master1
3. 后面 Pod 因配置调整被重建
4. 新 Pod 被调度到 k8s-master2
5. 旧节点上的卷还没完全 detach
6. attachdetach-controller 报 Multi-Attach
7. 等 Longhorn detach 完成后,卷成功 attach 到 k8s-master2

这类短暂 Multi-Attach 在有状态应用重建、节点切换、Helm upgrade 后比较常见。只要旧 Pod 已经退出,Longhorn 能正常 detach,通常等几十秒到几分钟会自动恢复。

这里不是 Kubernetes 不想“先分离旧卷再挂新卷”,而是它不能在不确认旧 Pod 已经完全退出、文件系统已经 unmount、Longhorn engine 已经安全关闭的情况下强行 detach。对 SonarQube、PostgreSQL 这类有状态应用来说,强行 detach 有数据损坏风险。

每次修改 Helm values 后都容易看到这个报错,是因为:

1. Helm upgrade 触发 StatefulSet 更新
2. 旧 Pod 退出需要时间
3. Longhorn detach 需要时间
4. 新 Pod 可能被调度到另一个节点
5. 新节点 attach 卷时,旧节点还没完全释放
6. Kubernetes 为保护 RWO 卷,先报 Multi-Attach

只要后面出现:

SuccessfulAttachVolume

并且 Pod 最终变成 1/1 Running,就说明已经自动恢复。

排查命令:

kubectl -n sonarqube describe pod sonarqube-sonarqube-0
kubectl -n sonarqube get events --sort-by=.lastTimestamp
kubectl -n sonarqube get pvc -o wide
kubectl get pv pvc-122749df-7e75-49e6-a3e2-3e05cded31d4 -o wide
kubectl -n longhorn-system get volumes.longhorn.io pvc-122749df-7e75-49e6-a3e2-3e05cded31d4 -o wide

这次恢复后的状态是:

pod/sonarqube-sonarqube-0        1/1 Running   k8s-master2
pvc/sonarqube-sonarqube          Bound         RWO longhorn-retain
longhorn volume                  attached healthy k8s-master2

处理方式按风险从低到高:

1. 先等 1 到 3 分钟,观察是否自动 SuccessfulAttachVolume
2. 确认旧 Pod 是否已经 Terminating 完成
3. 如果旧 Pod 卡住,可以删除旧 Pod,让 StatefulSet 重新创建
4. 确认 Longhorn 里卷是否仍挂在旧节点
5. 只有确认旧节点没有业务进程使用该卷时,才考虑在 Longhorn UI 里手动 detach

常用恢复命令:

kubectl -n sonarqube get pod -o wide
kubectl -n sonarqube delete pod sonarqube-sonarqube-0
kubectl -n sonarqube describe pod sonarqube-sonarqube-0

不要为了处理 Multi-Attach 直接删除 PVC。PVC 是数据入口,删除 PVC 可能导致 PV 释放,甚至触发底层数据清理。

如果想减少 SonarQube 在节点间来回漂移,可以给 SonarQube 设置 nodeSelectoraffinity,让它固定调度到某个稳定节点。但这会降低节点故障时自动迁移的灵活性。

比如当前 SonarQube 最后稳定运行在 k8s-master2,可以在 sonarqube-values.yaml 里加:

nodeSelector:
  kubernetes.io/hostname: k8s-master2

然后升级:

helm -n sonarqube upgrade sonarqube sonarqube/sonarqube \
  -f sonarqube-values.yaml

如果是安装插件、升级版本、改动较大的配置,可以走更稳的停机升级流程:

kubectl -n sonarqube scale statefulset sonarqube-sonarqube --replicas=0
kubectl -n sonarqube wait --for=delete pod/sonarqube-sonarqube-0 --timeout=180s

kubectl -n longhorn-system get volumes.longhorn.io pvc-122749df-7e75-49e6-a3e2-3e05cded31d4 -o wide

helm -n sonarqube upgrade sonarqube sonarqube/sonarqube \
  -f sonarqube-values.yaml

kubectl -n sonarqube get pods -w

总结一下:

短暂 Multi-Attach 后自动恢复:可以忽略
每次都想减少报错:给 SonarQube 加 nodeSelector 固定节点
大版本升级或插件升级:先 scale 到 0,等卷 detach,再 helm upgrade
不要用 RWX 绕过这个问题,SonarQube 单实例 RWO 更合适
不要删除 PVC

PVC 不要随便删

查看 PVC 和 PV:

kubectl -n sonarqube get pvc
kubectl get pv | grep sonarqube

确认回收策略:

kubectl get pv <pv-name> -o jsonpath='{.spec.persistentVolumeReclaimPolicy}'

数据库和 SonarQube 数据卷建议是:

Retain

如果发现重要 PV 是 Delete,可以改成 Retain

kubectl patch pv <pv-name> \
  -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'

备份建议

SonarQube 需要备份两类数据:

PostgreSQL 数据库
SonarQube 持久化目录

其中最重要的是 PostgreSQL。建议:

1. 用 CloudNativePG 做数据库备份
2. 用 Longhorn Snapshot / Backup 保护卷
3. 升级 SonarQube 前先备份数据库和 PVC
4. 不要把 Retain 当成备份,它只能降低误删风险

卸载

如果只是卸载 SonarQube 应用:

helm -n sonarqube uninstall sonarqube

确认 PVC 是否保留:

kubectl -n sonarqube get pvc
kubectl get pv | grep sonarqube

如果使用的是 Retain,删除 PVC 后底层 PV 仍会保留,需要人工确认后再清理。

不要在没有备份的情况下直接删除数据库 PVC。

参考

  • SonarQube Community Build Kubernetes 部署文档:https://docs.sonarsource.com/sonarqube-community-build/server-installation/on-kubernetes-or-openshift/
  • SonarQube Helm Chart:https://artifacthub.io/packages/helm/sonarqube/sonarqube
  • SonarQube Helm Chart GitHub:https://github.com/SonarSource/helm-chart-sonarqube
  • SonarQube 中文语言包:https://github.com/xuhuisheng/sonar-l10n-zh
  • SonarQube 中文语言包插件页:https://www.sonarplugins.com/l10nzh
  • Kubernetes Ingress 文档:https://kubernetes.io/docs/concepts/services-networking/ingress/
  • Kubernetes PV 文档:https://kubernetes.io/docs/concepts/storage/persistent-volumes/