This commit is contained in:
2025-11-24 11:31:29 +09:00
parent ce7119e9e9
commit 1da6180658
30 changed files with 4352 additions and 272 deletions

474
deploy.sh Executable file
View File

@@ -0,0 +1,474 @@
#!/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 "$@"