#!/bin/bash ############################################################################### # SmartSolTech Automated Deployment Script # Автоматический скрипт развертывания с проверками и rollback ############################################################################### set -e # Остановка при ошибке # Цвета для вывода RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Конфигурация PROJECT_DIR="/var/www/smartsoltech.kr" COMPOSE_FILE="docker-compose.yml" BACKUP_DIR="/var/backups/smartsoltech" MAX_BACKUPS=5 HEALTHCHECK_TIMEOUT=60 DOMAIN="smartsoltech.kr" # Логирование log_info() { echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" } # Проверка прав root check_permissions() { if [[ $EUID -ne 0 ]] && ! groups | grep -q docker; then log_error "Запустите скрипт с правами root или от пользователя в группе docker" exit 1 fi } # Проверка необходимых команд check_dependencies() { log_info "Проверка зависимостей..." local deps=("docker" "git" "curl") for cmd in "${deps[@]}"; do if ! command -v "$cmd" &> /dev/null; then log_error "Команда '$cmd' не найдена. Установите перед продолжением." exit 1 fi done if ! docker compose version &> /dev/null; then log_error "Docker Compose не установлен" exit 1 fi log_success "Все зависимости установлены" } # Проверка .env файла check_env_file() { log_info "Проверка .env файла..." if [[ ! -f "$PROJECT_DIR/.env" ]]; then log_error ".env файл не найден в $PROJECT_DIR" exit 1 fi # Проверка критичных переменных source "$PROJECT_DIR/.env" local required_vars=("SECRET_KEY" "POSTGRES_DB" "POSTGRES_USER" "POSTGRES_PASSWORD") for var in "${required_vars[@]}"; do if [[ -z "${!var}" ]]; then log_error "Переменная $var не установлена в .env" exit 1 fi done # Проверка DEBUG режима if [[ "$DEBUG" == "True" ]]; then log_warning "DEBUG=True обнаружен! В продакшене должно быть DEBUG=False" read -p "Продолжить? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi fi # Проверка SECRET_KEY if [[ ${#SECRET_KEY} -lt 50 ]]; then log_warning "SECRET_KEY слишком короткий (меньше 50 символов)" fi log_success ".env файл проверен" } # Создание резервной копии БД backup_database() { log_info "Создание резервной копии базы данных..." mkdir -p "$BACKUP_DIR" local backup_file="$BACKUP_DIR/db_backup_$(date +%Y%m%d_%H%M%S).sql" if docker ps --format '{{.Names}}' | grep -q "^postgres_db$"; then docker exec postgres_db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > "$backup_file" if [[ -f "$backup_file" ]] && [[ -s "$backup_file" ]]; then log_success "Резервная копия создана: $backup_file" # Сжатие бэкапа gzip "$backup_file" log_info "Бэкап сжат: ${backup_file}.gz" # Удаление старых бэкапов cleanup_old_backups else log_error "Не удалось создать резервную копию" exit 1 fi else log_warning "Контейнер postgres_db не запущен, пропускаем бэкап" fi } # Очистка старых бэкапов cleanup_old_backups() { local backup_count=$(ls -1 "$BACKUP_DIR"/db_backup_*.sql.gz 2>/dev/null | wc -l) if [[ $backup_count -gt $MAX_BACKUPS ]]; then log_info "Удаление старых бэкапов (оставляем последние $MAX_BACKUPS)..." ls -1t "$BACKUP_DIR"/db_backup_*.sql.gz | tail -n +$((MAX_BACKUPS + 1)) | xargs rm -f log_success "Старые бэкапы удалены" fi } # Получение последних изменений из Git pull_latest_code() { log_info "Получение последних изменений из Git..." cd "$PROJECT_DIR" # Проверка наличия изменений git fetch origin local local_commit=$(git rev-parse HEAD) local remote_commit=$(git rev-parse origin/master) if [[ "$local_commit" == "$remote_commit" ]]; then log_info "Код уже актуален (коммит: ${local_commit:0:8})" return 0 fi log_info "Обнаружены новые коммиты, обновление..." # Сохранение текущего коммита для возможного rollback echo "$local_commit" > "$BACKUP_DIR/last_commit.txt" git pull origin master log_success "Код обновлен до коммита: ${remote_commit:0:8}" } # Остановка контейнеров stop_containers() { log_info "Остановка контейнеров..." cd "$PROJECT_DIR" if docker compose ps | grep -q "Up"; then docker compose down log_success "Контейнеры остановлены" else log_info "Контейнеры уже остановлены" fi } # Сборка образов build_images() { log_info "Сборка Docker образов..." cd "$PROJECT_DIR" docker compose build --no-cache log_success "Образы собраны" } # Запуск контейнеров start_containers() { log_info "Запуск контейнеров..." cd "$PROJECT_DIR" docker compose up -d log_success "Контейнеры запущены" } # Применение миграций run_migrations() { log_info "Применение миграций базы данных..." # Ожидание запуска БД sleep 10 if docker exec django_app python manage.py migrate --check &>/dev/null; then docker exec django_app python manage.py migrate --noinput log_success "Миграции применены" else log_error "Ошибка при проверке миграций" return 1 fi } # Сборка статических файлов collect_static() { log_info "Сборка статических файлов..." docker exec django_app python manage.py collectstatic --noinput log_success "Статические файлы собраны" } # Проверка здоровья приложения healthcheck() { log_info "Проверка работоспособности приложения..." local timeout=$HEALTHCHECK_TIMEOUT local elapsed=0 local interval=5 while [[ $elapsed -lt $timeout ]]; do # Проверка контейнеров if ! docker compose ps | grep -q "Up"; then log_error "Один или несколько контейнеров не запущены" docker compose ps return 1 fi # Проверка HTTP ответа local http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/ || echo "000") if [[ "$http_code" == "200" ]]; then log_success "Приложение отвечает (HTTP $http_code)" return 0 fi log_info "Ожидание запуска приложения... ($elapsed/$timeout сек, HTTP: $http_code)" sleep $interval elapsed=$((elapsed + interval)) done log_error "Приложение не отвечает после $timeout секунд" return 1 } # Проверка всех эндпоинтов test_endpoints() { log_info "Тестирование критичных эндпоинтов..." local endpoints=( "/" "/services/" "/about/" "/contact/" "/blog/" "/news/" "/portfolio/" "/career/" "/admin/" ) local failed=0 for endpoint in "${endpoints[@]}"; do local http_code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8000${endpoint}" || echo "000") if [[ "$http_code" == "200" ]] || [[ "$http_code" == "302" ]]; then log_success "✓ ${endpoint} - HTTP $http_code" else log_error "✗ ${endpoint} - HTTP $http_code" ((failed++)) fi done if [[ $failed -gt 0 ]]; then log_error "$failed эндпоинтов не прошли проверку" return 1 fi log_success "Все эндпоинты работают корректно" return 0 } # Проверка логов на ошибки check_logs() { log_info "Проверка логов на критичные ошибки..." local error_patterns=("ERROR" "CRITICAL" "Exception" "Traceback") local errors_found=0 for pattern in "${error_patterns[@]}"; do if docker logs django_app --tail 100 2>&1 | grep -qi "$pattern"; then ((errors_found++)) fi done if [[ $errors_found -gt 0 ]]; then log_warning "Обнаружены ошибки в логах ($errors_found типов)" log_info "Последние 20 строк логов:" docker logs django_app --tail 20 return 1 fi log_success "Критичных ошибок в логах не обнаружено" return 0 } # Откат изменений rollback() { log_error "Выполнение отката изменений..." cd "$PROJECT_DIR" # Откат кода if [[ -f "$BACKUP_DIR/last_commit.txt" ]]; then local last_commit=$(cat "$BACKUP_DIR/last_commit.txt") log_info "Откат кода к коммиту: ${last_commit:0:8}" git reset --hard "$last_commit" fi # Восстановление БД local latest_backup=$(ls -1t "$BACKUP_DIR"/db_backup_*.sql.gz 2>/dev/null | head -n 1) if [[ -n "$latest_backup" ]]; then log_info "Восстановление БД из: $latest_backup" gunzip -c "$latest_backup" | docker exec -i postgres_db psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" log_success "База данных восстановлена" fi # Перезапуск контейнеров docker compose down docker compose up -d log_warning "Откат завершен. Проверьте работоспособность." } # Отправка уведомления (опционально) send_notification() { local status=$1 local message=$2 # TODO: Добавить интеграцию с Telegram/Email/Slack log_info "Уведомление: [$status] $message" } # Показ статуса show_status() { log_info "Статус контейнеров:" docker compose ps echo "" log_info "Использование ресурсов:" docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" echo "" log_info "Последние 10 строк логов Django:" docker logs django_app --tail 10 } # Главная функция main() { local start_time=$(date +%s) echo "═══════════════════════════════════════════════════════════" echo " 🚀 SmartSolTech Automated Deployment" echo " 📅 $(date '+%Y-%m-%d %H:%M:%S')" echo "═══════════════════════════════════════════════════════════" echo "" # Проверки перед деплоем check_permissions check_dependencies if [[ ! -d "$PROJECT_DIR" ]]; then log_error "Директория проекта не найдена: $PROJECT_DIR" exit 1 fi check_env_file # Создание директории для бэкапов mkdir -p "$BACKUP_DIR" # Резервное копирование backup_database # Обновление кода pull_latest_code # Остановка старых контейнеров stop_containers # Сборка и запуск if ! build_images; then log_error "Ошибка при сборке образов" rollback exit 1 fi start_containers # Применение изменений if ! run_migrations; then log_error "Ошибка при применении миграций" rollback exit 1 fi collect_static # Проверка здоровья if ! healthcheck; then log_error "Проверка работоспособности не пройдена" rollback exit 1 fi # Тестирование эндпоинтов if ! test_endpoints; then log_warning "Некоторые эндпоинты не прошли проверку" read -p "Продолжить без отката? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then rollback exit 1 fi fi # Проверка логов check_logs || log_warning "Обнаружены предупреждения в логах, но деплой продолжен" # Показ статуса echo "" show_status # Подсчет времени local end_time=$(date +%s) local duration=$((end_time - start_time)) echo "" echo "═══════════════════════════════════════════════════════════" log_success "🎉 Деплой успешно завершен за ${duration} секунд!" echo "═══════════════════════════════════════════════════════════" send_notification "SUCCESS" "Деплой завершен успешно за ${duration}s" } # Обработка ошибок trap 'log_error "Скрипт прерван"; exit 1' INT TERM # Запуск main "$@"