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 设置 nodeSelector 或 affinity,让它固定调度到某个稳定节点。但这会降低节点故障时自动迁移的灵活性。
比如当前 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/