Files
smartsoltech_site/deploy.sh
2025-11-24 11:31:29 +09:00

475 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 "$@"