CI/CD: от "фтп в продакшн" до zero-downtime deploy за 4 минуты
Это история эволюции. Начнём с того момента, когда деплой выглядел так: "Ваня, выгрузи файлики на сервер через FileZilla". И закончим тем, что у нас сейчас — полностью автоматический pipeline с тестами, security checks, zero-downtime deploy и автоматическим rollback.
Эра FTP (тёмные времена)
Просто знайте: когда я пришёл в эту компанию, деплой выглядел следующим образом:
Разработчик локально делал изменения
Открывал FileZilla
Перетаскивал папку
app/на серверМолился
Это было в 2019 году. Не в 2009, а в 2019. Такое ещё встречается.
Шаг 1: Git + простой CI
# .gitlab-ci.yml v1.0 — наивная версия
stages:
- test
- deploy
test:
stage: test
script:
- composer install
- php vendor/bin/phpunit
deploy:
stage: deploy
script:
- ssh deploy@production "cd /var/www/app && git pull && composer install"
only:
- main
git pull на продакшне — это тоже не очень хорошая идея, но это следующий шаг.
Шаг 2: Деплой через rsync + atomic switch
#!/bin/bash
set -euo pipefail
DEPLOY_PATH="/var/www"
RELEASES_PATH="$DEPLOY_PATH/releases"
CURRENT_LINK="$DEPLOY_PATH/current"
RELEASE_ID=$(date +%Y%m%d_%H%M%S)
RELEASE_PATH="$RELEASES_PATH/$RELEASE_ID"
mkdir -p "$RELEASE_PATH"
rsync -az --exclude='.git' --exclude='vendor' --exclude='writable' \
./ "$RELEASE_PATH/"
ln -sf "$SHARED_PATH/writable" "$RELEASE_PATH/writable"
ln -sf "$SHARED_PATH/.env" "$RELEASE_PATH/.env"
cd "$RELEASE_PATH"
composer install --no-dev --optimize-autoloader --no-interaction
php spark migrate --no-interaction
# Атомарное переключение симлинка — zero downtime!
ln -sfn "$RELEASE_PATH" "$CURRENT_LINK"
nginx -s reload
ls -dt "$RELEASES_PATH"/* | tail -n +6 | xargs rm -rf
echo "✅
Deploy $RELEASE_ID complete"
Rollback:
ls -dt /var/www/releases/* | head -2 | tail -1 | xargs -I{} ln -sfn {} /var/www/current
nginx -s reload
Шаг 3: Docker + Kubernetes
Dockerfile для PHP 8.3 + CI4:
FROM composer:2.7 AS vendor-builder
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative
FROM php:8.3-fpm-alpine AS production
RUN apk add --no-cache libzip-dev icu-dev \
&& docker-php-ext-install pdo_mysql zip intl opcache \
&& pecl install redis igbinary \
&& docker-php-ext-enable redis igbinary
COPY docker/php/php.ini /usr/local/etc/php/php.ini
COPY docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf
WORKDIR /var/www/html
COPY --from=vendor-builder /app/vendor ./vendor
COPY --from=vendor-builder /app/app ./app
COPY --from=vendor-builder /app/system ./system
COPY --from=vendor-builder /app/public ./public
COPY --from=vendor-builder /app/spark ./spark
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD php-fpm -t || exit 1
EXPOSE 9000
USER www-data
CMD ["php-fpm"]
GitLab CI/CD — финальная версия:
stages:
- validate
- test
- security
- build
- deploy-staging
- integration-test
- deploy-production
- verify
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
lint:
stage: validate
script:
- find app/ -name "*.php" -exec php -l {} \;
- composer validate --strict
unit-tests:
stage: test
services:
- mysql:8.0
- redis:7.0-alpine
script:
- php spark migrate --env=testing
- vendor/bin/phpunit --coverage-min=70
sast:
stage: security
script:
- vendor/bin/psalm --no-cache
- composer audit --no-dev
build-image:
stage: build
script:
- docker build --target production -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
deploy-production:
stage: deploy-production
when: manual # Требует ручного подтверждения!
script:
- kubectl set image deployment/php-app php-fpm=$DOCKER_IMAGE -n production
- kubectl rollout status deployment/php-app -n production --timeout=10m
post-deploy-verify:
stage: verify
script:
- sleep 60
- php spark synthetic:run --suite=smoke --env=production
- |
ERROR_RATE=$(curl -s "http://prometheus/api/v1/query?query=rate(http_errors_total[5m])" | jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
echo "Error rate too high! Auto-rollback!"
kubectl rollout undo deployment/php-app -n production
exit 1
fi
Итого: что получили
Метрика | FTP era | Git + rsync | Docker + k8s |
|---|---|---|---|
Время деплоя | 5-40 мин | 3-8 мин | 2-4 мин |
Downtime при деплое | 30s-5min | 0 | 0 |
Rollback время | 10-30 мин | 2 мин | 45 секунд |
Деплоев в день | 1-2 | 5-10 | 20-30 |
Инцидентов из-за деплоя | 2-3/мес | 0-1/мес | ~0 |
Больше деплоев в день — это не потому что мы хаотичнее. Маленькие частые деплои безопаснее больших редких. Каждый PR идёт в прод в тот же день. Это называется continuous delivery. И это меняет жизнь. 🚀
Recommended Comments