2023年8月,我负责的一个电商项目需要紧急修复线上bug。手动SSH登录服务器,拉代码,重启服务,一套流程下来15分钟。结果因为手抖输错了命令,把生产环境配置文件覆盖了,导致线上宕机20分钟。老板在群里@我,那感觉,懂的都懂。
事后复盘:团队5个人,每次部署都要手动操作,平均耗时12分钟/次,出错率高达30%。必须上自动化部署。
| 方案 | 配置复杂度 | 维护成本 | 回滚能力 | 学习曲线 |
|---|---|---|---|---|
| Jenkins | 高(需单独部署) | 高(插件兼容性) | 强 | 陡 |
| GitLab CI/CD | 低(内置) | 低(YAML配置) | 中(需自定义) | 平缓 |
| 手动脚本 | 低 | 高(无版本控制) | 弱 | 无 |
我们最终选择GitLab CI/CD,因为:
- 代码仓库和CI/CD一体化,无需额外搭建
- .gitlab-ci.yml文件版本化管理,可追溯
- 支持Docker Runner,环境隔离好
- 社区活跃,遇到问题容易搜到解决方案
创建部署用户,配置SSH免密登录:
# 在目标服务器上执行
sudo useradd -m -s /bin/bash deployer
sudo passwd deployer # 设置强密码
sudo usermod -aG www-data deployer # 加入web用户组
# 切换到deployer用户
su - deployer
mkdir -p ~/.ssh
chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# 生成部署密钥对(如果还没有)
ssh-keygen -t ed25519 -C "gitlab-ci@deploy" -f ~/.ssh/deploy_key -N ""
# 将公钥加入authorized_keys
cat ~/.ssh/deploy_key.pub >> ~/.ssh/authorized_keys
# 配置sudo权限(允许deployer重启服务)
echo "deployer ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx, /usr/bin/systemctl restart php8.1-fpm" | sudo tee /etc/sudoers.d/deployer
# 在GitLab服务器或独立Runner机器上
docker run -d --name gitlab-runner --restart always \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest
# 注册Runner
docker exec -it gitlab-runner gitlab-runner register \
--url https://gitlab.example.com \
--registration-token YOUR_TOKEN \
--executor docker \
--docker-image alpine:latest \
--description "Docker Runner for Deploy" \
--tag-list "deploy,production" \
--run-untagged="false" \
--locked="false"
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
DEPLOY_SERVER: "192.168.1.100"
DEPLOY_USER: "deployer"
DEPLOY_PATH: "/var/www/project"
PHP_VERSION: "8.1"
cache:
paths:
- vendor/
- node_modules/
before_script:
- apt-get update -qq && apt-get install -y -qq ssh rsync
# 阶段1:代码检查
test:
stage: test
image: php:${PHP_VERSION}-cli
script:
- php -v
- composer install --no-interaction --prefer-dist
- php vendor/bin/phpunit --coverage-text --colors=never
only:
- main
- develop
tags:
- deploy
# 阶段2:构建
build:
stage: build
image: node:18-alpine
script:
- npm ci
- npm run build
- tar -czf build.tar.gz dist/
artifacts:
paths:
- build.tar.gz
expire_in: 1 week
only:
- main
tags:
- deploy
# 阶段3:部署到生产
deploy-production:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache openssh-client rsync
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan -H ${DEPLOY_SERVER} >> ~/.ssh/known_hosts
- rsync -avz --delete --exclude='.env' --exclude='storage/' ./ ${DEPLOY_USER}@${DEPLOY_SERVER}:${DEPLOY_PATH}/
- ssh ${DEPLOY_USER}@${DEPLOY_SERVER} "cd ${DEPLOY_PATH} && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan config:cache && php artisan route:cache && sudo systemctl restart php8.1-fpm && sudo systemctl restart nginx"
environment:
name: production
url: https://example.com
only:
- main
when: manual # 手动触发,防止误操作
tags:
- deploy
# 阶段4:回滚
rollback:
stage: deploy
image: alpine:latest
script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
- ssh ${DEPLOY_USER}@${DEPLOY_SERVER} "cd ${DEPLOY_PATH} && git stash && git checkout $CI_COMMIT_BEFORE_SHA && composer install --no-dev --optimize-autoloader && php artisan migrate:rollback && php artisan config:cache && sudo systemctl restart php8.1-fpm"
environment:
name: production
only:
- main
when: manual
tags:
- deploy
# 在GitLab项目设置 -> CI/CD -> Variables 中添加
# 变量名: SSH_PRIVATE_KEY
# 值: 服务器上deployer用户的私钥内容
# 类型: File
# 保护: 勾选(仅保护分支可用)
# 获取私钥内容
cat ~/.ssh/deploy_key
#!/bin/bash
# deploy.sh - 放在项目根目录
set -e # 任何命令失败立即退出
DEPLOY_SERVER="$1"
DEPLOY_USER="$2"
DEPLOY_PATH="$3"
BRANCH="$4"
echo "=== 开始部署到 ${DEPLOY_SERVER}:${DEPLOY_PATH} ==="
# 1. 同步代码
rsync -avz --delete \
--exclude='.env' \
--exclude='storage/' \
--exclude='.git/' \
--exclude='node_modules/' \
--exclude='tests/' \
./ ${DEPLOY_USER}@${DEPLOY_SERVER}:${DEPLOY_PATH}/
# 2. 远程执行命令
ssh ${DEPLOY_USER}@${DEPLOY_SERVER} bash -c "'
cd ${DEPLOY_PATH}
# 安装依赖
composer install --no-dev --optimize-autoloader --no-interaction
# 数据库迁移
php artisan migrate --force
# 清理缓存
php artisan optimize:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
# 设置权限
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
# 重启服务
sudo systemctl restart php8.1-fpm
sudo systemctl restart nginx
echo \"部署完成,时间:\$(date)\"
'"
echo "=== 部署成功 ==="
#!/bin/bash
# health_check.sh - 部署后自动检查
URL="https://example.com/health"
EXPECTED_CODE=200
TIMEOUT=30
echo "正在检查服务健康状态..."
for i in $(seq 1 $TIMEOUT); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" $URL)
if [ "$HTTP_CODE" -eq "$EXPECTED_CODE" ]; then
echo "服务正常,HTTP状态码: $HTTP_CODE"
exit 0
fi
echo "等待服务启动... ($i/$TIMEOUT)"
sleep 2
done
echo "服务启动失败,HTTP状态码: $HTTP_CODE"
exit 1
# .env.production - 生产环境配置
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=production_db
DB_USERNAME=deployer
DB_PASSWORD=strong_password_here
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
SESSION_DRIVER=redis
CACHE_DRIVER=redis
| 部署方式 | 平均耗时 | 最长耗时 | 最短耗时 | 样本数 |
|---|---|---|---|---|
| 手动部署 | 12分30秒 | 18分20秒 | 8分15秒 | 50次 |
| GitLab CI/CD | 2分45秒 | 3分50秒 | 1分55秒 | 200次 |
| 优化后CI/CD | 1分30秒 | 2分10秒 | 1分05秒 | 300次 |
优化点:
- 使用rsync增量同步,只传输变更文件
- 缓存vendor和node_modules目录
- 并行执行测试和构建阶段
| 指标 | 手动部署 | CI/CD部署 |
|---|---|---|
| 部署失败率 | 30% | 2.5% |
| 人为错误导致故障 | 8次/月 | 0次/月 |
| 回滚次数 | 5次/月 | 1次/月 |
| 平均恢复时间(MTTR) | 25分钟 | 3分钟 |
GitLab Runner(Docker executor)资源占用:
- CPU:平均5%,峰值15%
- 内存:256MB(基础镜像)
- 磁盘:每次构建约500MB临时空间,自动清理
现象:CI流水线执行到ssh连接时报错"Permission denied (publickey)"
原因:GitLab CI/CD变量中私钥格式问题,换行符被转义
解决:在变量值前后加引号,使用tr -d '\r'去除回车符
# 正确的私钥处理方式
echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
现象:rsync同步时提示"Permission denied"
原因:deployer用户对目标目录没有写权限
解决:确保deployer用户属于www-data组,且目录权限为775
# 在服务器上执行
sudo chown -R deployer:www-data /var/www/project
sudo chmod -R 775 /var/www/project
现象:composer install时提示"Allowed memory size exhausted"
原因:默认PHP内存限制128MB,composer需要更多
解决:在CI脚本中临时增加内存限制
# 在.gitlab-ci.yml中
composer install --no-dev --optimize-autoloader --no-interaction \
-d memory_limit=-1
现象:多个流水线同时执行时,数据库迁移报错
原因:并发迁移导致表结构冲突
解决:使用数据库锁或串行化部署
# 在.gitlab-ci.yml中添加资源组
deploy-production:
resource_group: production
script:
- # 部署命令
现象:CI日志中打印了数据库密码
原因:脚本中echo了敏感变量
解决:使用GitLab的Masked变量,并在脚本中避免echo
# 错误做法
echo "DB_PASSWORD=$DB_PASSWORD"
# 正确做法
# 直接使用变量,不打印
#!/bin/bash
# blue_green_deploy.sh
# 蓝绿部署实现
CURRENT=$(ssh deployer@server "readlink /var/www/current")
if [ "$CURRENT" == "/var/www/blue" ]; then
NEW="green"
OLD="blue"
else
NEW="blue"
OLD="green"
fi
echo "部署到 $NEW 环境"
rsync -avz ./ deployer@server:/var/www/$NEW/
ssh deployer@server "cd /var/www/$NEW && composer install && php artisan migrate"
echo "切换流量到 $NEW"
ssh deployer@server "ln -sfn /var/www/$NEW /var/www/current && sudo systemctl reload nginx"
echo "旧环境 $OLD 保留,用于回滚"
# .gitlab-ci.yml 增加自动回滚
deploy-production:
after_script:
- |
# 部署后自动健康检查
sleep 10
if ! curl -f https://example.com/health; then
echo "健康检查失败,自动回滚"
curl -X POST -F "token=$CI_JOB_TOKEN" \
-F "ref=$CI_COMMIT_REF_NAME" \
"https://gitlab.example.com/api/v4/projects/$CI_PROJECT_ID/trigger/pipeline" \
-F "variables[ROLLBACK]=true"
fi
这套GitLab CI/CD方案已经在我们团队运行了6个月,部署了超过500次,零人为故障。核心收益:
- 部署时间从12分钟降到1分30秒
- 错误率从30%降到2.5%
- 回滚时间从25分钟降到3分钟
如果你还在手动部署,强烈建议花一天时间搭建这套流水线。踩过的坑我都写在上面了,照着做基本不会出问题。
专注技术分享与实战