一、在 Ubuntu 上部署 SonarQube
1. 基础环境
- 登录 EC2(Ubuntu):
ssh ubuntu@${SONAR_IP}
- 设置时区为 UTC(如果还没设):
sudo timedatectl set-timezone UTC
timedatectl
- 安装 Docker(Ubuntu):
sudo apt update
sudo apt install -y docker.io
sudo systemctl enable --now docker
docker --version
docker compose version # Ubuntu 22.04 上 docker.io 自带新 compose
如果
docker compose没有,安装docker-compose-plugin或使用官方 Docker Engine 文档安装新版 Docker。
2. 准备 SonarQube 目录结构
sudo mkdir -p /opt/sonar-stack/postgres
sudo mkdir -p /opt/sonar-stack/sonarqube
sudo chown -R ubuntu:ubuntu /opt/sonar-stack
cd /opt/sonar-stack
3. 配置环境变量文件 .env
注意:密码请自己改成强密码。
cat > .env <<'EOF'
# Postgres
POSTGRES_USER=sonar
POSTGRES_PASSWORD=Strong_Pwd
POSTGRES_DB=sonar
# Sonar DB 连接
SONAR_JDBC_URL=jdbc:postgresql://postgres:5432/sonar
SONAR_JDBC_USERNAME=sonar
SONAR_JDBC_PASSWORD=Strong_Passw0rd
EOF
4. 编写 docker-compose.yml
cat > docker-compose.yml <<'EOF'
version: "3.8"
services:
postgres:
image: postgres:15
container_name: sonar-postgres
restart: always
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- ./postgres/data:/var/lib/postgresql/data
sonarqube:
image: sonarqube:lts-community
container_name: sonarqube
depends_on:
- postgres
restart: always
environment:
SONAR_JDBC_URL: ${SONAR_JDBC_URL}
SONAR_JDBC_USERNAME: ${SONAR_JDBC_USERNAME}
SONAR_JDBC_PASSWORD: ${SONAR_JDBC_PASSWORD}
ports:
- "9000:9000"
volumes:
- ./sonarqube/data:/opt/sonarqube/data
- ./sonarqube/extensions:/opt/sonarqube/extensions
- ./sonarqube/logs:/opt/sonarqube/logs
EOF
5. 修复 Elasticsearch 目录权限
Sonar 容器内用户为 UID=1000,GID=0:
sudo chown -R 1000:0 sonarqube/data sonarqube/logs sonarqube/extensions
6. 启动 SonarQube
cd /opt/sonar-stack
docker compose up -d
docker compose logs -f sonarqube
看到类似 SonarQube is up 日志后,访问:
http://${SONAR_IP}:9000(测试)- 通过 Nginx/ALB 反向代理为
https://${SONAR_HOST}(对外)
7. Sonar 基础配置与安全
- 第一次登录
https://${SONAR_HOST}:- 默认账号:
admin / admin - 强制修改密码。
- 默认账号:
- 配置基础参数:
Administration → Configuration → General → Server base URL
设置为:https://${SONAR_HOST}
- 建立 CI 用 Token(后面 GitLab 会用):
- 右上角头像 → My Account → Security → Generate Tokens
- 取一个名字(如
gitlab-ci-batch),记下生成的 Token:SONAR_TOKEN
二、准备 GitLab 侧访问和 GitLab Runner
1. 在 GitLab 创建 PAT(访问项目 & Group)
在 GitLab:
- 用管理员或有足够权限的账号登录;
- 用户头像 →
Edit profile→Access Tokens(或Personal Access Tokens); - 创建 Token:
- 名称:
sonar-batch-scan - 过期时间:按公司策略
- Scopes 勾选:至少
api/read_api/read_repository
- 名称:
- 生成后记住 Token:
GITLAB_TOKEN
记录:
GITLAB_URL:例如http://***:80GITLAB_API_URL=${GITLAB_URL}/api/v4
2. 找到要扫描的 Group ID
浏览器访问(需带上 Token,可以先在浏览器里试 GET):
${GITLAB_URL}/api/v4/groups?per_page=100
或用命令行:
curl --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
"${GITLAB_API_URL}/groups?per_page=100" | jq '.[] | {id, full_path}'
记录你要扫描的 Group id,例如:1,3,5,10,11,12。
后面用环境变量 TARGET_GROUP_IDS 来配置。
3. 部署 GitLab Runner(Docker executor)
在一台装了 Docker 的机器(可与 Sonar 同机):
sudo mkdir -p /opt/gitlab-runner
cd /opt/gitlab-runner
创建 docker-compose.yml:
cat > docker-compose.yml <<'EOF'
version: "3.8"
services:
gitlab-runner:
image: gitlab/gitlab-runner:alpine
restart: always
volumes:
- ./config:/etc/gitlab-runner
- /var/run/docker.sock:/var/run/docker.sock
EOF
启动:
docker compose up -d
注册 Runner:
docker compose exec gitlab-runner gitlab-runner register
交互过程建议:
- GitLab URL:
${GITLAB_URL} - Registration token:在 GitLab 后台(Admin Area → Runners 或项目 → Settings → CI/CD → Runners)里复制
- 描述:
sonar-batch-runner - Tags:
sonar - Executor:
docker - 默认镜像:
node:20-bullseye或alpine:3(后面会覆盖)
在 GitLab 中确认:
- 项目 Settings → CI/CD → Runners 里看到这个 Runner;
- 状态为
active; - Tags 包含
sonar; - “Can run untagged jobs” 可以关掉。
三、创建“扫库工程”项目
1. 在 GitLab 上建项目
- 新建项目:如
framework/gitlab-sonar-batch - 初始化一个 README,推到主分支(如
master)
本地 clone:
git clone ${GITLAB_URL}/framework/gitlab-sonar-batch.git
cd gitlab-sonar-batch
2. 创建 scan_group.sh(完整脚本)
这个脚本是整个“批量扫描 GitLab → Sonar”的核心逻辑。
cat > scan_group.sh <<'EOF'
#!/usr/bin/env bash
# 不用 set -e,避免某一个项目失败导致整个 Job 直接挂掉
set -u
echo "=== scan_group.sh: start ==="
# ========= 必需环境变量检查 =========
: "${GITLAB_API_URL:?GITLAB_API_URL is not set. e.g. http://gitlab.example.com/api/v4}"
: "${GITLAB_TOKEN:?GITLAB_TOKEN is not set}"
: "${SONAR_HOST_URL:?SONAR_HOST_URL is not set}"
: "${SONAR_TOKEN:?SONAR_TOKEN is not set}"
: "${TARGET_GROUP_IDS:?TARGET_GROUP_IDS is not set. e.g. 1,3,5}"
echo "GITLAB_API_URL=${GITLAB_API_URL}"
echo "TARGET_GROUP_IDS=${TARGET_GROUP_IDS}"
echo "SONAR_HOST_URL=${SONAR_HOST_URL}"
# ========= 从 GITLAB_API_URL 提取 HTTP_BASE,用于拼 clone 地址 =========
HTTP_BASE="${GITLAB_API_URL%/api/v4}"
# 在 // 后插入 oauth2:TOKEN@,生成带 token 的 clone 基地址
CLONE_BASE_WITH_TOKEN="${HTTP_BASE/\/\//\/\/oauth2:${GITLAB_TOKEN}@}"
echo "HTTP_BASE=${HTTP_BASE}"
echo "CLONE_BASE_WITH_TOKEN=${CLONE_BASE_WITH_TOKEN%%${GITLAB_TOKEN}@*}***"
# ========= 工作目录 =========
WORKDIR="${CI_PROJECT_DIR:-$(pwd)}/work"
echo "WORKDIR=${WORKDIR}"
mkdir -p "${WORKDIR}"
cd "${WORKDIR}"
# ========= 解析 group id 列表 =========
echo "RAW TARGET_GROUP_IDS=${TARGET_GROUP_IDS}"
for RAW_ID in $(echo "${TARGET_GROUP_IDS}" | tr ',' ' '); do
echo "Processing raw group id: '${RAW_ID}'"
GROUP_ID="$(echo "${RAW_ID}" | tr -d '[:space:]')"
echo "Parsed group id: '${GROUP_ID}'"
if [[ -z "${GROUP_ID}" ]]; then
echo " -> empty id, skip"
continue
fi
if ! [[ "${GROUP_ID}" =~ ^[0-9]+$ ]]; then
echo " -> invalid numeric id, skip"
continue
fi
echo "=== Fetching projects for group ${GROUP_ID} ==="
page=1
while :; do
echo "-----> Requesting page ${page} for group ${GROUP_ID}"
PROJECTS_JSON=$(curl --silent --show-error --fail \
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
"${GITLAB_API_URL}/groups/${GROUP_ID}/projects?include_subgroups=true&per_page=100&page=${page}") || {
echo "⚠️ 获取 group ${GROUP_ID} 第 ${page} 页项目列表失败,跳过该 group。"
break
}
if [[ "$(echo "${PROJECTS_JSON}" | jq 'length')" -eq 0 ]]; then
echo " No more projects on page ${page} for group ${GROUP_ID}"
break
fi
echo "${PROJECTS_JSON}" \
| jq -c '.[] | select(.archived == false) | {id, path_with_namespace, default_branch}' \
| while read -r proj; do
ID=$(echo "${proj}" | jq -r '.id')
PATH_WITH_NS=$(echo "${proj}" | jq -r '.path_with_namespace')
DEFAULT_BRANCH=$(echo "${proj}" | jq -r '.default_branch // "main"')
echo "------> Scanning project ${PATH_WITH_NS} (id=${ID}, branch=${DEFAULT_BRANCH})"
rm -rf "${ID}"
mkdir -p "${ID}"
cd "${ID}"
REPO_URL="${HTTP_BASE}/${PATH_WITH_NS}.git"
CLONE_URL="${CLONE_BASE_WITH_TOKEN}/${PATH_WITH_NS}.git"
echo " git clone ${REPO_URL} (branch=${DEFAULT_BRANCH})"
if ! git clone --depth 1 --branch "${DEFAULT_BRANCH}" "${CLONE_URL}" repo; then
echo " ⚠️ clone ${PATH_WITH_NS} 失败,跳过该项目"
cd "${WORKDIR}"
continue
fi
cd repo
# ========= 清理容易出问题的二进制文档 =========
echo " Cleanup: remove Office/CHM/PDF docs that may break paths"
find . -type f \
\( -iname '*.chm' \
-o -iname '*.xls' -o -iname '*.xlsx' \
-o -iname '*.doc' -o -iname '*.docx' \
-o -iname '*.ppt' -o -iname '*.pptx' \
-o -iname '*.pdf' \
\) -delete 2>/dev/null || true
# ========= Java 项目:提供空 binaries 目录,避免缺少 sonar.java.binaries 报错 =========
SONAR_JAVA_OPTS=""
if find . -type f -name '*.java' -print -quit | grep -q .; then
echo " Java sources detected, using empty binaries dir for sonar.java.binaries"
mkdir -p .sonar-empty-binaries
SONAR_JAVA_OPTS="-Dsonar.java.binaries=.sonar-empty-binaries"
fi
PROJECT_KEY=$(echo "${PATH_WITH_NS}" | tr '/' '-')
PROJECT_NAME="${PATH_WITH_NS}"
echo " ▶ SonarQube scan: ${PROJECT_NAME} (key=${PROJECT_KEY})"
set +e
sonar-scanner \
-Dsonar.projectKey="${PROJECT_KEY}" \
-Dsonar.projectName="${PROJECT_NAME}" \
-Dsonar.sources=. \
-Dsonar.host.url="${SONAR_HOST_URL}" \
-Dsonar.token="${SONAR_TOKEN}" \
${SONAR_JAVA_OPTS}
SCAN_EXIT_CODE=$?
set -e || true 2>/dev/null || true
if [[ ${SCAN_EXIT_CODE} -ne 0 ]]; then
echo " ⚠️ Sonar 扫描 ${PATH_WITH_NS} 失败(exit=${SCAN_EXIT_CODE}),继续下一个项目"
else
echo " ✅ Sonar 扫描 ${PATH_WITH_NS} 成功"
fi
cd "${WORKDIR}"
done
page=$((page + 1))
done
done
echo "=== scan_group.sh: finished ==="
exit 0
EOF
chmod +x scan_group.sh
3. 编写 .gitlab-ci.yml
这里采用 现成的 node:20-bullseye 镜像 + 在 before_script 中安装 Java 和 sonar-scanner 的方案,比自建镜像简单。
cat > .gitlab-ci.yml <<'EOF'
stages:
- sonar
sonar_group_scan:
stage: sonar
image:
name: node:20-bullseye
entrypoint: [""]
tags:
- sonar
before_script:
- echo "=== Install Java + tools ==="
- apt-get update
- apt-get install -y --no-install-recommends \
openjdk-17-jre-headless curl unzip git jq
- java -version
- node -v
# 安装 sonar-scanner CLI(版本可以按需调整)
- export SONAR_SCANNER_VERSION=5.0.1.3006
- echo "=== Download sonar-scanner ${SONAR_SCANNER_VERSION} ==="
- curl -sSLo /tmp/sonar-scanner.zip \
"https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-linux.zip"
- unzip /tmp/sonar-scanner.zip -d /opt
- mv /opt/sonar-scanner-* /opt/sonar-scanner
- rm /tmp/sonar-scanner.zip
- export PATH="/opt/sonar-scanner/bin:${PATH}"
- sonar-scanner -v || echo "Sonar scanner installed"
script:
- echo "=== Start sonar batch scan job ==="
- chmod +x ./scan_group.sh
- ./scan_group.sh
artifacts:
when: never
rules:
# 定时任务使用
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: always
# 手动 Run pipeline 触发
- if: '$CI_PIPELINE_SOURCE == "web"'
when: manual
EOF
4. 提交到 GitLab
git add scan_group.sh .gitlab-ci.yml
git commit -m "Add Sonar batch scan pipeline"
git push
四、在“扫库工程”里配置 CI/CD 变量
在 GitLab Web:
- 打开 项目 → Settings → CI/CD → Variables;
- 添加如下变量(类型 Text,Mask 勾选与否看需要):
GITLAB_API_URL
例如:http://ip:port/api/v4GITLAB_TOKEN
刚才创建的 PATSONAR_HOST_URL
例如:https://sonar.company.comSONAR_TOKEN
在 SonarQube 生成的 TokenTARGET_GROUP_IDS
例如:1,3,5,10,11,12(要扫描的 Group ID,逗号分隔)
保存。
五、手工测试一次扫描
- 建议先只扫一个小 Group,比如
TARGET_GROUP_IDS=5; - 在项目 CI 页面点击 Run pipeline:
- 选主分支;
- 不用传变量(直接用刚配置好的值);
- 打开
sonar_group_scan这个 Job,看日志:- 前面会打印:
GITLAB_API_URL=...TARGET_GROUP_IDS=5
- 中间有:
Scanning project xxx/yyy;▶ SonarQube scan: ...;
- 成功时会出现:
ANALYSIS SUCCESSFULEXECUTION SUCCESS
- 前面会打印:
- 打开
https://${SONAR_HOST}→Projects:- 能看到刚刚扫描的项目列表,说明链路打通。
六、配置定时扫描(Schedule)
在“扫库工程”项目:
- 点击
CI/CD → Schedules → New schedule; - 例如设置:
- Description:
Nightly Sonar batch scan - Interval:每天
- 时间:
03:00(UTC,对应北京 11:00,可按需求调整)
- Description:
- 在 Schedule 下方的 Variables 中:
- 可以覆盖
TARGET_GROUP_IDS(比如夜间扫更多 Group) - 如果将来有第二个 GitLab,也可在 Schedule 里覆盖
GITLAB_API_URL与GITLAB_TOKEN
- 可以覆盖
保存后,Schedule 到点会自动跑 sonar_group_scan。
七、常见问题速查(结合你这次踩过的坑)
-
Job 一直 Pending,提示 no runners / tag 不匹配
- 检查 Runner:
- 状态 active
- Tag 包含
sonar
- Job 中
tags: ['sonar'],与 Runner 一致。
- 检查 Runner:
-
Sonar 容器 ES 权限错误(path.data / logs 无法访问)
-
到宿主机上执行:
cd /opt/sonar-stack sudo chown -R 1000:0 sonarqube/data sonarqube/logs sonarqube/extensions docker compose restart sonarqube
-
-
扫描报:InvalidPathException(文件名乱码,如 .chm / .xls)
- 已在
scan_group.sh中通过find . -type f ... -delete删除常见 Office/CHM/PDF 文档; - 如将来遇到其它特殊扩展名,根据报错再添加到列表即可。
- 已在
-
Java 项目报:Your project contains .java files, please provide compiled classes…
- 已通过空目录
.sonar-empty-binaries并设置sonar.java.binaries解决; - 对集中扫库来说,这样足够;真正要严格质量门槛的项目可以在项目自有 CI 中“先编译后 Sonar”。
- 已通过空目录
-
Job 后面 Uploading artifacts 报 413 Request Entity Too Large
- 这是上传
work/目录太大导致的; - 当前
.gitlab-ci.yml已禁用 artifacts(artifacts: when: never),因此不会再出现。
- 这是上传
本文来自博客园,作者:茄子_2008,转载请注明原文链接:https://www.cnblogs.com/xd502djj/p/19444923
浙公网安备 33010602011771号