prod
This commit is contained in:
474
deploy.sh
Executable file
474
deploy.sh
Executable 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 "$@"
|
||||
Reference in New Issue
Block a user