Implementa sistemas de despliegue automatizado y CI/CD con Bash
La automatización de despliegues es fundamental en DevOps moderno. Bash proporciona la flexibilidad y potencia necesaria para crear pipelines de despliegue robustos que pueden manejar desde simples aplicaciones web hasta complejos sistemas distribuidos.
#!/bin/bash
# Script de despliegue básico para aplicaciones web
# Versión: 1.0
set -euo pipefail
# Configuración
APP_NAME="MiAplicacion"
APP_DIR="/var/www/miapp"
BACKUP_DIR="/var/backups/miapp"
REPO_URL="https://github.com/usuario/miapp.git"
BRANCH="${DEPLOY_BRANCH:-main}"
SERVICE_NAME="miapp"
# Colores
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'
# Variables de estado
DEPLOYMENT_ID="deploy_$(date +%Y%m%d_%H%M%S)"
DEPLOYMENT_LOG="/var/log/deployments/$DEPLOYMENT_ID.log"
ROLLBACK_AVAILABLE=false
# Crear directorio de logs
mkdir -p "$(dirname "$DEPLOYMENT_LOG")"
# Función de logging
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
INFO)
echo -e "${BLUE}[INFO]${NC} $message" | tee -a "$DEPLOYMENT_LOG"
;;
SUCCESS)
echo -e "${GREEN}[SUCCESS]${NC} $message" | tee -a "$DEPLOYMENT_LOG"
;;
WARN)
echo -e "${YELLOW}[WARN]${NC} $message" | tee -a "$DEPLOYMENT_LOG"
;;
ERROR)
echo -e "${RED}[ERROR]${NC} $message" | tee -a "$DEPLOYMENT_LOG"
;;
esac
echo "[$timestamp] [$level] $message" >> "$DEPLOYMENT_LOG"
}
# Banner del despliegue
show_deployment_banner() {
clear
echo -e "${BLUE}"
cat << "EOF"
╔══════════════════════════════════════════════════════════════╗
║ SISTEMA DE DESPLIEGUE ║
║ v1.0 ║
╚══════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}"
log INFO "Iniciando despliegue de $APP_NAME"
log INFO "ID de despliegue: $DEPLOYMENT_ID"
log INFO "Rama: $BRANCH"
}
# Verificar prerrequisitos
check_prerequisites() {
log INFO "Verificando prerrequisitos..."
# Verificar conectividad a repositorio
if ! git ls-remote "$REPO_URL" &>/dev/null; then
log ERROR "No se puede acceder al repositorio: $REPO_URL"
return 1
fi
# Verificar espacio en disco (al menos 1GB libre)
local available_space=$(df /var | tail -1 | awk '{print $4}')
if [ "$available_space" -lt 1048576 ]; then
log ERROR "Espacio insuficiente en disco"
return 1
fi
# Verificar servicios necesarios
local required_services=("nginx" "php-fpm")
for service in "${required_services[@]}"; do
if ! systemctl is-active "$service" &>/dev/null; then
log WARN "Servicio no activo: $service"
fi
done
log SUCCESS "Prerrequisitos verificados"
}
# Crear backup de la versión actual
create_backup() {
log INFO "Creando backup de la versión actual..."
if [ ! -d "$APP_DIR" ]; then
log WARN "Directorio de aplicación no existe, omitiendo backup"
return 0
fi
local backup_path="$BACKUP_DIR/$DEPLOYMENT_ID"
mkdir -p "$backup_path"
# Crear backup completo
if tar -czf "$backup_path/app_backup.tar.gz" -C "$APP_DIR" . 2>/dev/null; then
log SUCCESS "Backup creado: $backup_path/app_backup.tar.gz"
ROLLBACK_AVAILABLE=true
echo "$backup_path" > "/tmp/last_backup_$APP_NAME"
else
log ERROR "Error creando backup"
return 1
fi
# Backup de base de datos si existe
if command -v mysqldump &>/dev/null; then
local db_name="${APP_NAME,,}"
if mysql -e "USE $db_name;" &>/dev/null; then
mysqldump "$db_name" > "$backup_path/database_backup.sql"
log SUCCESS "Backup de base de datos creado"
fi
fi
}
# Descargar nueva versión
download_new_version() {
log INFO "Descargando nueva versión desde $REPO_URL (rama: $BRANCH)..."
local temp_dir=$(mktemp -d)
# Clonar repositorio
if git clone -b "$BRANCH" --single-branch "$REPO_URL" "$temp_dir" &>/dev/null; then
log SUCCESS "Código descargado exitosamente"
else
log ERROR "Error descargando el código"
rm -rf "$temp_dir"
return 1
fi
# Obtener información de la versión
cd "$temp_dir"
local commit_hash=$(git rev-parse HEAD)
local commit_message=$(git log -1 --pretty=format:"%s")
local commit_author=$(git log -1 --pretty=format:"%an")
local commit_date=$(git log -1 --pretty=format:"%ci")
log INFO "Commit: $commit_hash"
log INFO "Mensaje: $commit_message"
log INFO "Autor: $commit_author"
log INFO "Fecha: $commit_date"
# Guardar información de versión
cat > "/tmp/deploy_info_$APP_NAME" << EOF
DEPLOYMENT_ID=$DEPLOYMENT_ID
COMMIT_HASH=$commit_hash
COMMIT_MESSAGE=$commit_message
COMMIT_AUTHOR=$commit_author
COMMIT_DATE=$commit_date
DEPLOY_TIME=$(date)
EOF
# Mover código al directorio temporal para despliegue
mv "$temp_dir" "/tmp/deploy_$DEPLOYMENT_ID"
log SUCCESS "Código preparado para despliegue"
}
# Ejecutar pruebas
run_tests() {
log INFO "Ejecutando pruebas..."
cd "/tmp/deploy_$DEPLOYMENT_ID"
# Verificar si hay archivo de pruebas
if [ -f "package.json" ]; then
if command -v npm &>/dev/null; then
log INFO "Ejecutando pruebas npm..."
if npm test &>/dev/null; then
log SUCCESS "Pruebas npm pasaron"
else
log ERROR "Pruebas npm fallaron"
return 1
fi
fi
fi
# Verificar si hay composer.json para PHP
if [ -f "composer.json" ]; then
if command -v composer &>/dev/null; then
log INFO "Instalando dependencias PHP..."
composer install --no-dev --optimize-autoloader &>/dev/null
log SUCCESS "Dependencias PHP instaladas"
fi
fi
# Verificar sintaxis básica para archivos PHP
if command -v php &>/dev/null; then
local php_errors=0
while IFS= read -r -d '' file; do
if ! php -l "$file" &>/dev/null; then
log ERROR "Error de sintaxis en: $file"
((php_errors++))
fi
done < <(find . -name "*.php" -print0)
if [ $php_errors -gt 0 ]; then
log ERROR "Se encontraron $php_errors archivos PHP con errores de sintaxis"
return 1
fi
log SUCCESS "Sintaxis PHP verificada"
fi
}
# Desplegar nueva versión
deploy_new_version() {
log INFO "Desplegando nueva versión..."
local source_dir="/tmp/deploy_$DEPLOYMENT_ID"
# Crear directorio de aplicación si no existe
mkdir -p "$APP_DIR"
# Detener servicios temporalmente si es necesario
if systemctl is-active "$SERVICE_NAME" &>/dev/null; then
log INFO "Deteniendo servicio $SERVICE_NAME"
systemctl stop "$SERVICE_NAME"
fi
# Copiar archivos nuevos
if rsync -av --exclude='.git' "$source_dir/" "$APP_DIR/"; then
log SUCCESS "Archivos copiados exitosamente"
else
log ERROR "Error copiando archivos"
return 1
fi
# Establecer permisos
chown -R www-data:www-data "$APP_DIR"
find "$APP_DIR" -type f -exec chmod 644 {} \;
find "$APP_DIR" -type d -exec chmod 755 {} \;
# Hacer ejecutables los scripts necesarios
if [ -d "$APP_DIR/bin" ]; then
chmod +x "$APP_DIR/bin/"*
fi
log SUCCESS "Permisos establecidos"
# Reiniciar servicios
if systemctl is-enabled "$SERVICE_NAME" &>/dev/null; then
log INFO "Iniciando servicio $SERVICE_NAME"
systemctl start "$SERVICE_NAME"
fi
# Recargar nginx
if systemctl is-active nginx &>/dev/null; then
systemctl reload nginx
log SUCCESS "Nginx recargado"
fi
}
# Verificar despliegue
verify_deployment() {
log INFO "Verificando despliegue..."
# Verificar que los servicios estén activos
local services_to_check=("$SERVICE_NAME" "nginx" "php-fpm")
for service in "${services_to_check[@]}"; do
if systemctl is-enabled "$service" &>/dev/null; then
if systemctl is-active "$service" &>/dev/null; then
log SUCCESS "Servicio $service está activo"
else
log ERROR "Servicio $service no está activo"
return 1
fi
fi
done
# Health check HTTP
local health_url="http://localhost/health"
if command -v curl &>/dev/null; then
log INFO "Ejecutando health check..."
if curl -f -s "$health_url" &>/dev/null; then
log SUCCESS "Health check HTTP exitoso"
else
log WARN "Health check HTTP falló"
fi
fi
# Verificar archivos críticos
local critical_files=("$APP_DIR/index.php" "$APP_DIR/config/app.php")
for file in "${critical_files[@]}"; do
if [ -f "$file" ]; then
log SUCCESS "Archivo crítico presente: $file"
else
log WARN "Archivo crítico faltante: $file"
fi
done
log SUCCESS "Verificación completada"
}
# Rollback en caso de error
rollback() {
log ERROR "Iniciando rollback..."
if [ "$ROLLBACK_AVAILABLE" = false ]; then
log ERROR "No hay backup disponible para rollback"
return 1
fi
local backup_path
if [ -f "/tmp/last_backup_$APP_NAME" ]; then
backup_path=$(cat "/tmp/last_backup_$APP_NAME")
else
log ERROR "No se encontró información de backup"
return 1
fi
# Detener servicios
if systemctl is-active "$SERVICE_NAME" &>/dev/null; then
systemctl stop "$SERVICE_NAME"
fi
# Restaurar desde backup
if [ -f "$backup_path/app_backup.tar.gz" ]; then
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR"
tar -xzf "$backup_path/app_backup.tar.gz" -C "$APP_DIR"
chown -R www-data:www-data "$APP_DIR"
log SUCCESS "Aplicación restaurada desde backup"
fi
# Restaurar base de datos si existe
if [ -f "$backup_path/database_backup.sql" ] && command -v mysql &>/dev/null; then
local db_name="${APP_NAME,,}"
mysql "$db_name" < "$backup_path/database_backup.sql"
log SUCCESS "Base de datos restaurada"
fi
# Reiniciar servicios
if systemctl is-enabled "$SERVICE_NAME" &>/dev/null; then
systemctl start "$SERVICE_NAME"
fi
if systemctl is-active nginx &>/dev/null; then
systemctl reload nginx
fi
log SUCCESS "Rollback completado"
}
# Limpiar archivos temporales
cleanup() {
log INFO "Limpiando archivos temporales..."
rm -rf "/tmp/deploy_$DEPLOYMENT_ID" 2>/dev/null || true
rm -f "/tmp/deploy_info_$APP_NAME" 2>/dev/null || true
# Mantener solo los últimos 10 backups
if [ -d "$BACKUP_DIR" ]; then
find "$BACKUP_DIR" -maxdepth 1 -type d -name "deploy_*" | \
sort -r | tail -n +11 | xargs -r rm -rf
fi
log SUCCESS "Limpieza completada"
}
# Enviar notificación
send_notification() {
local status="$1"
local message="$2"
log INFO "Enviando notificación..."
# Notificación por email (si está configurado)
if command -v mail &>/dev/null && [ -n "${DEPLOY_EMAIL:-}" ]; then
echo "$message" | mail -s "$APP_NAME: Despliegue $status" "$DEPLOY_EMAIL"
fi
# Notificación por Slack (si está configurado)
if command -v curl &>/dev/null && [ -n "${SLACK_WEBHOOK:-}" ]; then
local color="good"
[ "$status" != "exitoso" ] && color="danger"
local payload=$(cat << EOF
{
"attachments": [
{
"color": "$color",
"title": "$APP_NAME - Despliegue $status",
"text": "$message",
"ts": $(date +%s)
}
]
}
EOF
)
curl -X POST -H 'Content-type: application/json' \
--data "$payload" "$SLACK_WEBHOOK" &>/dev/null
fi
}
# Función principal
main() {
show_deployment_banner
# Trap para manejar errores y ejecutar rollback automático
trap 'handle_error' ERR
# Ejecutar pipeline de despliegue
check_prerequisites
create_backup
download_new_version
run_tests
deploy_new_version
verify_deployment
# Si llegamos aquí, el despliegue fue exitoso
cleanup
local success_message="Despliegue de $APP_NAME completado exitosamente (ID: $DEPLOYMENT_ID)"
log SUCCESS "$success_message"
send_notification "exitoso" "$success_message"
echo ""
echo -e "${GREEN}🎉 ¡Despliegue completado con éxito!${NC}"
echo "ID: $DEPLOYMENT_ID"
echo "Log: $DEPLOYMENT_LOG"
}
# Manejador de errores
handle_error() {
local exit_code=$?
log ERROR "Error detectado en el despliegue (código: $exit_code)"
local error_message="Despliegue de $APP_NAME falló (ID: $DEPLOYMENT_ID). Ejecutando rollback automático."
send_notification "fallido" "$error_message"
# Ejecutar rollback automático
rollback || log ERROR "Rollback también falló"
cleanup
exit $exit_code
}
# Verificar privilegios
if [ "$EUID" -ne 0 ]; then
log ERROR "Este script debe ejecutarse con privilegios de root"
exit 1
fi
# Ejecutar función principal
main "$@"
sudo ./deploy_basic.sh
DEPLOY_BRANCH=development sudo ./deploy_basic.sh
Mantiene dos entornos idénticos (azul y verde) alternando entre ellos para despliegues sin downtime.
Actualiza instancias de forma gradual, manteniendo siempre alguna versión disponible.
Despliega la nueva versión a un pequeño porcentaje de usuarios antes del rollout completo.
#!/bin/bash
# Script de despliegue Blue-Green
# Permite despliegues sin downtime alternando entre dos entornos
set -euo pipefail
# Configuración
APP_NAME="webapp"
BLUE_DIR="/var/www/webapp-blue"
GREEN_DIR="/var/www/webapp-green"
CURRENT_LINK="/var/www/webapp-current"
NGINX_CONFIG="/etc/nginx/sites-available/webapp"
REPO_URL="https://github.com/user/webapp.git"
BRANCH="${DEPLOY_BRANCH:-main}"
# Colores
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# Función de logging
log() {
local level="$1"
shift
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $*"
}
# Determinar entorno activo actual
get_current_environment() {
if [ -L "$CURRENT_LINK" ]; then
local target=$(readlink "$CURRENT_LINK")
if [[ "$target" == *"blue"* ]]; then
echo "blue"
elif [[ "$target" == *"green"* ]]; then
echo "green"
else
echo "unknown"
fi
else
echo "none"
fi
}
# Obtener directorio del entorno inactivo
get_inactive_environment() {
local current=$(get_current_environment)
case "$current" in
"blue") echo "green" ;;
"green") echo "blue" ;;
*) echo "blue" ;; # Por defecto usar blue si no hay entorno activo
esac
}
# Obtener directorio del entorno
get_environment_dir() {
local env="$1"
case "$env" in
"blue") echo "$BLUE_DIR" ;;
"green") echo "$GREEN_DIR" ;;
*) echo "" ;;
esac
}
# Preparar entorno inactivo
prepare_inactive_environment() {
local inactive_env=$(get_inactive_environment)
local inactive_dir=$(get_environment_dir "$inactive_env")
log INFO "Preparando entorno inactivo: $inactive_env"
log INFO "Directorio: $inactive_dir"
# Limpiar directorio si existe
if [ -d "$inactive_dir" ]; then
rm -rf "$inactive_dir"
fi
# Crear directorio
mkdir -p "$inactive_dir"
# Clonar código
log INFO "Clonando código desde $REPO_URL (rama: $BRANCH)"
if git clone -b "$BRANCH" --single-branch "$REPO_URL" "$inactive_dir"; then
log INFO "✓ Código clonado exitosamente"
else
log ERROR "✗ Error clonando código"
return 1
fi
# Cambiar al directorio y configurar
cd "$inactive_dir"
# Instalar dependencias si es necesario
if [ -f "package.json" ] && command -v npm &>/dev/null; then
log INFO "Instalando dependencias npm..."
npm install --production &>/dev/null
log INFO "✓ Dependencias npm instaladas"
fi
if [ -f "composer.json" ] && command -v composer &>/dev/null; then
log INFO "Instalando dependencias PHP..."
composer install --no-dev --optimize-autoloader &>/dev/null
log INFO "✓ Dependencias PHP instaladas"
fi
# Configurar permisos
chown -R www-data:www-data "$inactive_dir"
find "$inactive_dir" -type f -exec chmod 644 {} \;
find "$inactive_dir" -type d -exec chmod 755 {} \;
# Ejecutar scripts de build si existen
if [ -f "build.sh" ]; then
log INFO "Ejecutando script de build..."
bash build.sh
log INFO "✓ Build completado"
fi
log INFO "✓ Entorno $inactive_env preparado"
echo "$inactive_env"
}
# Ejecutar pruebas en el entorno
run_environment_tests() {
local env="$1"
local env_dir=$(get_environment_dir "$env")
log INFO "Ejecutando pruebas en entorno $env"
cd "$env_dir"
# Ejecutar pruebas unitarias
if [ -f "package.json" ] && command -v npm &>/dev/null; then
if grep -q '"test"' package.json; then
log INFO "Ejecutando pruebas npm..."
if npm test &>/dev/null; then
log INFO "✓ Pruebas npm pasaron"
else
log ERROR "✗ Pruebas npm fallaron"
return 1
fi
fi
fi
# Verificar sintaxis PHP
if command -v php &>/dev/null; then
local php_errors=0
while IFS= read -r -d '' file; do
if ! php -l "$file" &>/dev/null; then
log ERROR "Error de sintaxis en: $file"
((php_errors++))
fi
done < <(find . -name "*.php" -print0 2>/dev/null)
if [ $php_errors -gt 0 ]; then
log ERROR "✗ Errores de sintaxis PHP encontrados"
return 1
fi
log INFO "✓ Sintaxis PHP verificada"
fi
log INFO "✓ Pruebas completadas exitosamente"
}
# Health check en el entorno
health_check() {
local env="$1"
local port="${2:-8080}"
log INFO "Ejecutando health check en entorno $env (puerto $port)"
# Configurar servidor temporal para pruebas
local env_dir=$(get_environment_dir "$env")
local pid_file="/tmp/test_server_${env}.pid"
# Iniciar servidor PHP temporal para pruebas
if [ -f "$env_dir/index.php" ]; then
cd "$env_dir"
php -S "localhost:$port" &>"$env_dir/test_server.log" &
local server_pid=$!
echo "$server_pid" > "$pid_file"
# Esperar que el servidor inicie
sleep 3
# Ejecutar health check
local health_url="http://localhost:$port/health"
local fallback_url="http://localhost:$port/"
if command -v curl &>/dev/null; then
if curl -f -s "$health_url" &>/dev/null || curl -f -s "$fallback_url" &>/dev/null; then
log INFO "✓ Health check exitoso"
# Detener servidor temporal
kill "$server_pid" 2>/dev/null || true
rm -f "$pid_file"
return 0
else
log ERROR "✗ Health check falló"
# Detener servidor temporal
kill "$server_pid" 2>/dev/null || true
rm -f "$pid_file"
return 1
fi
else
log WARN "curl no disponible, omitiendo health check HTTP"
kill "$server_pid" 2>/dev/null || true
rm -f "$pid_file"
fi
fi
return 0
}
# Cambiar entorno activo
switch_environment() {
local new_env="$1"
local new_dir=$(get_environment_dir "$new_env")
local current_env=$(get_current_environment)
log INFO "Cambiando de entorno $current_env a $new_env"
# Crear backup del enlace actual
if [ -L "$CURRENT_LINK" ]; then
cp -L "$CURRENT_LINK" "${CURRENT_LINK}.backup" 2>/dev/null || true
fi
# Actualizar enlace simbólico
rm -f "$CURRENT_LINK"
ln -sf "$new_dir" "$CURRENT_LINK"
# Verificar que el enlace se creó correctamente
if [ -L "$CURRENT_LINK" ] && [ -d "$CURRENT_LINK" ]; then
log INFO "✓ Enlace actualizado: $CURRENT_LINK -> $new_dir"
else
log ERROR "✗ Error actualizando enlace simbólico"
return 1
fi
# Recargar nginx
if systemctl is-active nginx &>/dev/null; then
log INFO "Recargando nginx..."
if systemctl reload nginx; then
log INFO "✓ Nginx recargado"
else
log ERROR "✗ Error recargando nginx"
return 1
fi
fi
log INFO "✓ Cambio de entorno completado"
}
# Verificar despliegue final
verify_deployment() {
local env="$1"
log INFO "Verificando despliegue final..."
# Verificar enlace simbólico
if [ ! -L "$CURRENT_LINK" ]; then
log ERROR "✗ Enlace simbólico no existe"
return 1
fi
local current_target=$(readlink "$CURRENT_LINK")
local expected_dir=$(get_environment_dir "$env")
if [ "$current_target" != "$expected_dir" ]; then
log ERROR "✗ Enlace simbólico apunta al directorio incorrecto"
return 1
fi
# Verificar servicios
if systemctl is-active nginx &>/dev/null; then
log INFO "✓ Nginx está activo"
else
log ERROR "✗ Nginx no está activo"
return 1
fi
# Health check final
if command -v curl &>/dev/null; then
local app_url="http://localhost"
if curl -f -s "$app_url" &>/dev/null; then
log INFO "✓ Aplicación responde correctamente"
else
log WARN "⚠ Aplicación no responde en $app_url"
fi
fi
log INFO "✓ Verificación completada"
}
# Rollback a entorno anterior
rollback() {
local current_env=$(get_current_environment)
case "$current_env" in
"blue") switch_environment "green" ;;
"green") switch_environment "blue" ;;
*)
log ERROR "No se puede determinar el entorno para rollback"
return 1
;;
esac
log INFO "✓ Rollback completado"
}
# Mostrar estado actual
show_status() {
local current_env=$(get_current_environment)
echo ""
echo -e "${BLUE}=== ESTADO BLUE-GREEN DEPLOYMENT ===${NC}"
echo "Entorno activo actual: $current_env"
if [ -L "$CURRENT_LINK" ]; then
local target=$(readlink "$CURRENT_LINK")
echo "Enlace actual: $CURRENT_LINK -> $target"
else
echo "Sin enlace activo"
fi
# Mostrar estado de directorios
echo ""
echo "Estado de entornos:"
for env in "blue" "green"; do
local dir=$(get_environment_dir "$env")
local status="No desplegado"
local color="$RED"
if [ -d "$dir" ]; then
status="Disponible"
color="$GREEN"
if [ "$env" = "$current_env" ]; then
status="Activo"
color="$BLUE"
fi
fi
echo -e " $env: ${color}$status${NC} ($dir)"
done
echo ""
}
# Función principal
main() {
case "${1:-deploy}" in
deploy)
echo -e "${BLUE}=== INICIANDO BLUE-GREEN DEPLOYMENT ===${NC}"
show_status
# Preparar entorno inactivo
local inactive_env
if ! inactive_env=$(prepare_inactive_environment); then
log ERROR "Error preparando entorno inactivo"
exit 1
fi
# Ejecutar pruebas
if ! run_environment_tests "$inactive_env"; then
log ERROR "Pruebas fallaron, cancelando despliegue"
exit 1
fi
# Health check
if ! health_check "$inactive_env" 8081; then
log ERROR "Health check falló, cancelando despliegue"
exit 1
fi
# Cambiar al nuevo entorno
if ! switch_environment "$inactive_env"; then
log ERROR "Error cambiando entorno"
exit 1
fi
# Verificación final
if ! verify_deployment "$inactive_env"; then
log ERROR "Verificación falló, ejecutando rollback"
rollback
exit 1
fi
echo -e "${GREEN}✓ Despliegue Blue-Green completado exitosamente${NC}"
show_status
;;
rollback)
echo -e "${YELLOW}=== EJECUTANDO ROLLBACK ===${NC}"
rollback
show_status
;;
status)
show_status
;;
*)
echo "Uso: $0 [deploy|rollback|status]"
echo ""
echo "Comandos:"
echo " deploy - Ejecutar despliegue Blue-Green"
echo " rollback - Cambiar al entorno anterior"
echo " status - Mostrar estado actual"
exit 1
;;
esac
}
# Verificar privilegios
if [ "$EUID" -ne 0 ]; then
echo "Este script debe ejecutarse con privilegios de root"
exit 1
fi
# Ejecutar función principal
main "$@"
#!/bin/bash
# Pipeline CI/CD completo con Bash
# Integración con Git hooks, testing automático y despliegue
set -euo pipefail
# Configuración del pipeline
PIPELINE_ID="pipeline_$(date +%Y%m%d_%H%M%S)"
PROJECT_NAME="${PROJECT_NAME:-myapp}"
REPO_URL="${REPO_URL}"
BRANCH="${BRANCH:-main}"
BUILD_DIR="/tmp/builds/$PIPELINE_ID"
ARTIFACTS_DIR="/var/lib/ci-artifacts"
DEPLOY_ENV="${DEPLOY_ENV:-staging}"
# Configuración de entornos
declare -A ENVIRONMENTS=(
["staging"]="staging.myapp.com"
["production"]="myapp.com"
["testing"]="test.myapp.com"
)
# Configuración de notificaciones
SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"
EMAIL_RECIPIENTS="${EMAIL_RECIPIENTS:-}"
# Colores y estilos
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m'
# Variables de estado del pipeline
PIPELINE_START_TIME=$(date +%s)
PIPELINE_STATUS="RUNNING"
FAILED_STAGE=""
STAGES_COMPLETED=()
# Crear directorios necesarios
mkdir -p "$BUILD_DIR" "$ARTIFACTS_DIR"
# Función de logging avanzado
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
HEADER)
echo -e "\n${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║$(printf "%-62s" " $message")║${NC}"
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}\n"
;;
STAGE)
echo -e "\n${PURPLE}► $message${NC}"
;;
INFO)
echo -e "${CYAN} ℹ $message${NC}"
;;
SUCCESS)
echo -e "${GREEN} ✓ $message${NC}"
;;
WARN)
echo -e "${YELLOW} ⚠ $message${NC}"
;;
ERROR)
echo -e "${RED} ✗ $message${NC}"
;;
esac
# Log a archivo también
echo "[$timestamp] [$level] $message" >> "$ARTIFACTS_DIR/$PIPELINE_ID.log"
}
# Mostrar progreso del pipeline
show_progress() {
local completed="$1"
local total="$2"
local stage_name="$3"
local percent=$((completed * 100 / total))
local filled=$((percent * 40 / 100))
printf "\n${WHITE}Pipeline Progress: [${NC}"
for ((i=1; i<=40; i++)); do
if [ $i -le $filled ]; then
printf "${GREEN}█${NC}"
else
printf "░"
fi
done
printf "${WHITE}] %d%% - %s${NC}\n\n" "$percent" "$stage_name"
}
# Enviar notificaciones
send_notification() {
local status="$1"
local message="$2"
local detailed_message="$3"
# Calcular duración del pipeline
local end_time=$(date +%s)
local duration=$((end_time - PIPELINE_START_TIME))
local duration_formatted=$(printf "%02d:%02d" $((duration/60)) $((duration%60)))
local full_message="$message
Proyecto: $PROJECT_NAME
Pipeline ID: $PIPELINE_ID
Duración: $duration_formatted
Entorno: $DEPLOY_ENV"
# Notificación por Slack
if [ -n "$SLACK_WEBHOOK" ] && command -v curl &>/dev/null; then
local color="good"
local icon=":white_check_mark:"
case "$status" in
"FAILED")
color="danger"
icon=":x:"
;;
"WARNING")
color="warning"
icon=":warning:"
;;
esac
local payload=$(cat << EOF
{
"attachments": [
{
"color": "$color",
"title": "$icon $PROJECT_NAME - Pipeline $status",
"text": "$full_message",
"fields": [
{
"title": "Branch",
"value": "$BRANCH",
"short": true
},
{
"title": "Environment",
"value": "$DEPLOY_ENV",
"short": true
}
],
"ts": $(date +%s)
}
]
}
EOF
)
curl -X POST -H 'Content-type: application/json' \
--data "$payload" "$SLACK_WEBHOOK" &>/dev/null || true
fi
# Notificación por email
if [ -n "$EMAIL_RECIPIENTS" ] && command -v mail &>/dev/null; then
{
echo "$full_message"
echo ""
echo "Detalles:"
echo "$detailed_message"
} | mail -s "$PROJECT_NAME: Pipeline $status" "$EMAIL_RECIPIENTS"
fi
}
# Stage 1: Checkout del código
stage_checkout() {
log STAGE "Stage 1/7: Checkout del código"
show_progress 1 7 "Checkout"
cd "$BUILD_DIR"
log INFO "Clonando repositorio: $REPO_URL"
log INFO "Rama: $BRANCH"
if git clone -b "$BRANCH" --single-branch "$REPO_URL" source; then
log SUCCESS "Código descargado exitosamente"
cd source
# Obtener información del commit
local commit_hash=$(git rev-parse HEAD)
local commit_message=$(git log -1 --pretty=format:"%s")
local commit_author=$(git log -1 --pretty=format:"%an")
log INFO "Commit: $commit_hash"
log INFO "Autor: $commit_author"
log INFO "Mensaje: $commit_message"
# Guardar información del pipeline
cat > "$BUILD_DIR/pipeline_info.json" << EOF
{
"pipeline_id": "$PIPELINE_ID",
"project_name": "$PROJECT_NAME",
"branch": "$BRANCH",
"commit_hash": "$commit_hash",
"commit_message": "$commit_message",
"commit_author": "$commit_author",
"timestamp": "$(date -Iseconds)",
"environment": "$DEPLOY_ENV"
}
EOF
STAGES_COMPLETED+=("checkout")
return 0
else
log ERROR "Error descargando el código"
return 1
fi
}
# Stage 2: Análisis de código estático
stage_static_analysis() {
log STAGE "Stage 2/7: Análisis de código estático"
show_progress 2 7 "Análisis estático"
cd "$BUILD_DIR/source"
local analysis_errors=0
# Análisis de sintaxis PHP
if find . -name "*.php" | head -1 | grep -q php; then
log INFO "Analizando sintaxis PHP..."
while IFS= read -r -d '' file; do
if ! php -l "$file" &>/dev/null; then
log ERROR "Error de sintaxis en: $file"
((analysis_errors++))
fi
done < <(find . -name "*.php" -print0)
if [ $analysis_errors -eq 0 ]; then
log SUCCESS "Sintaxis PHP correcta"
fi
fi
# Análisis con shellcheck para scripts Bash
if command -v shellcheck &>/dev/null && find . -name "*.sh" | head -1 | grep -q sh; then
log INFO "Analizando scripts Bash con shellcheck..."
if find . -name "*.sh" -exec shellcheck {} \; &>/dev/null; then
log SUCCESS "Scripts Bash verificados"
else
log WARN "Advertencias encontradas en scripts Bash"
fi
fi
# Análisis de JavaScript/TypeScript con ESLint si está disponible
if [ -f "package.json" ] && command -v npm &>/dev/null; then
if npm list eslint &>/dev/null; then
log INFO "Ejecutando ESLint..."
if npm run lint &>/dev/null; then
log SUCCESS "Linting JavaScript/TypeScript completado"
else
log WARN "Advertencias de linting encontradas"
fi
fi
fi
if [ $analysis_errors -gt 0 ]; then
log ERROR "Análisis estático falló con $analysis_errors errores"
return 1
fi
log SUCCESS "Análisis estático completado"
STAGES_COMPLETED+=("static_analysis")
}
# Stage 3: Instalación de dependencias
stage_dependencies() {
log STAGE "Stage 3/7: Instalación de dependencias"
show_progress 3 7 "Dependencias"
cd "$BUILD_DIR/source"
# Dependencias npm/yarn
if [ -f "package.json" ]; then
if command -v yarn &>/dev/null && [ -f "yarn.lock" ]; then
log INFO "Instalando dependencias con Yarn..."
yarn install --frozen-lockfile &>/dev/null
log SUCCESS "Dependencias Yarn instaladas"
elif command -v npm &>/dev/null; then
log INFO "Instalando dependencias con npm..."
npm ci &>/dev/null
log SUCCESS "Dependencias npm instaladas"
fi
fi
# Dependencias Composer para PHP
if [ -f "composer.json" ] && command -v composer &>/dev/null; then
log INFO "Instalando dependencias PHP con Composer..."
composer install --no-dev --optimize-autoloader --no-interaction &>/dev/null
log SUCCESS "Dependencias PHP instaladas"
fi
# Dependencias Python
if [ -f "requirements.txt" ] && command -v pip &>/dev/null; then
log INFO "Instalando dependencias Python..."
pip install -r requirements.txt &>/dev/null
log SUCCESS "Dependencias Python instaladas"
fi
STAGES_COMPLETED+=("dependencies")
}
# Stage 4: Build de la aplicación
stage_build() {
log STAGE "Stage 4/7: Build de la aplicación"
show_progress 4 7 "Build"
cd "$BUILD_DIR/source"
# Build con npm/yarn
if [ -f "package.json" ]; then
local build_script=""
if grep -q '"build"' package.json; then
build_script="build"
elif grep -q '"compile"' package.json; then
build_script="compile"
fi
if [ -n "$build_script" ]; then
log INFO "Ejecutando build script: $build_script"
if command -v yarn &>/dev/null && [ -f "yarn.lock" ]; then
yarn run "$build_script" &>/dev/null
else
npm run "$build_script" &>/dev/null
fi
log SUCCESS "Build completado"
fi
fi
# Build personalizado
if [ -f "build.sh" ]; then
log INFO "Ejecutando script de build personalizado..."
bash build.sh &>/dev/null
log SUCCESS "Build personalizado completado"
fi
# Crear archivo de versión
local version="${BRANCH}-$(git rev-parse --short HEAD)-$(date +%Y%m%d%H%M)"
echo "$version" > VERSION
log INFO "Versión: $version"
STAGES_COMPLETED+=("build")
}
# Stage 5: Testing
stage_testing() {
log STAGE "Stage 5/7: Ejecución de pruebas"
show_progress 5 7 "Testing"
cd "$BUILD_DIR/source"
local test_failures=0
# Pruebas unitarias con npm/yarn
if [ -f "package.json" ] && grep -q '"test"' package.json; then
log INFO "Ejecutando pruebas unitarias JavaScript..."
if command -v yarn &>/dev/null && [ -f "yarn.lock" ]; then
if yarn test &>/dev/null; then
log SUCCESS "Pruebas JavaScript pasaron"
else
log ERROR "Pruebas JavaScript fallaron"
((test_failures++))
fi
else
if npm test &>/dev/null; then
log SUCCESS "Pruebas JavaScript pasaron"
else
log ERROR "Pruebas JavaScript fallaron"
((test_failures++))
fi
fi
fi
# Pruebas PHP con PHPUnit
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
if command -v phpunit &>/dev/null || [ -f "vendor/bin/phpunit" ]; then
log INFO "Ejecutando pruebas PHPUnit..."
local phpunit_cmd="phpunit"
[ -f "vendor/bin/phpunit" ] && phpunit_cmd="vendor/bin/phpunit"
if $phpunit_cmd &>/dev/null; then
log SUCCESS "Pruebas PHP pasaron"
else
log ERROR "Pruebas PHP fallaron"
((test_failures++))
fi
fi
fi
# Pruebas Python
if [ -f "pytest.ini" ] || [ -f "setup.cfg" ] || find . -name "test_*.py" | head -1 | grep -q test_; then
if command -v pytest &>/dev/null; then
log INFO "Ejecutando pruebas pytest..."
if pytest &>/dev/null; then
log SUCCESS "Pruebas Python pasaron"
else
log ERROR "Pruebas Python fallaron"
((test_failures++))
fi
fi
fi
# Pruebas de integración personalizadas
if [ -f "test.sh" ]; then
log INFO "Ejecutando pruebas personalizadas..."
if bash test.sh &>/dev/null; then
log SUCCESS "Pruebas personalizadas pasaron"
else
log ERROR "Pruebas personalizadas fallaron"
((test_failures++))
fi
fi
if [ $test_failures -gt 0 ]; then
log ERROR "Testing falló con $test_failures conjuntos de pruebas"
return 1
fi
log SUCCESS "Todas las pruebas pasaron exitosamente"
STAGES_COMPLETED+=("testing")
}
# Stage 6: Creación de artefactos
stage_artifacts() {
log STAGE "Stage 6/7: Creación de artefactos"
show_progress 6 7 "Artefactos"
cd "$BUILD_DIR/source"
local artifact_name="${PROJECT_NAME}-$(cat VERSION).tar.gz"
local artifact_path="$ARTIFACTS_DIR/$artifact_name"
log INFO "Creando artefacto: $artifact_name"
# Excluir archivos innecesarios del artefacto
local exclude_patterns=(
".git"
"node_modules"
".npm"
"tests"
"test"
"*.log"
".env*"
"coverage"
)
local exclude_args=""
for pattern in "${exclude_patterns[@]}"; do
exclude_args+=" --exclude=$pattern"
done
# Crear artefacto comprimido
if tar czf "$artifact_path" $exclude_args -C "$BUILD_DIR/source" .; then
log SUCCESS "Artefacto creado: $artifact_path"
# Calcular checksum
local checksum=$(sha256sum "$artifact_path" | cut -d' ' -f1)
echo "$checksum" > "$artifact_path.sha256"
log INFO "Checksum SHA256: $checksum"
# Copiar información del pipeline
cp "$BUILD_DIR/pipeline_info.json" "$ARTIFACTS_DIR/${PIPELINE_ID}_info.json"
# Crear metadatos del artefacto
cat > "$ARTIFACTS_DIR/${artifact_name%.tar.gz}_metadata.json" << EOF
{
"artifact_name": "$artifact_name",
"project_name": "$PROJECT_NAME",
"version": "$(cat VERSION)",
"checksum_sha256": "$checksum",
"size_bytes": $(stat -f%z "$artifact_path" 2>/dev/null || stat -c%s "$artifact_path"),
"created_at": "$(date -Iseconds)",
"pipeline_id": "$PIPELINE_ID"
}
EOF
STAGES_COMPLETED+=("artifacts")
return 0
else
log ERROR "Error creando artefacto"
return 1
fi
}
# Stage 7: Despliegue
stage_deployment() {
log STAGE "Stage 7/7: Despliegue a $DEPLOY_ENV"
show_progress 7 7 "Despliegue"
local target_host="${ENVIRONMENTS[$DEPLOY_ENV]}"
local artifact_name="${PROJECT_NAME}-$(cat $BUILD_DIR/source/VERSION).tar.gz"
local artifact_path="$ARTIFACTS_DIR/$artifact_name"
if [ -z "$target_host" ]; then
log ERROR "Entorno de despliegue no configurado: $DEPLOY_ENV"
return 1
fi
log INFO "Desplegando a: $target_host"
log INFO "Artefacto: $artifact_name"
# En un entorno real, aquí se haría el despliegue real
# Por ejemplo: scp, rsync, kubectl, docker, etc.
# Simular despliegue exitoso
sleep 2
# Verificar despliegue (health check)
log INFO "Ejecutando verificación post-despliegue..."
# Aquí iría un health check real
# if curl -f "https://$target_host/health" &>/dev/null; then
# log SUCCESS "Health check exitoso"
# else
# log ERROR "Health check falló"
# return 1
# fi
log SUCCESS "Despliegue completado exitosamente"
log SUCCESS "Aplicación disponible en: https://$target_host"
STAGES_COMPLETED+=("deployment")
}
# Manejar fallos del pipeline
handle_pipeline_failure() {
local failed_stage="$1"
PIPELINE_STATUS="FAILED"
FAILED_STAGE="$failed_stage"
log ERROR "Pipeline falló en stage: $failed_stage"
# Generar reporte de fallo
generate_failure_report
# Enviar notificación de fallo
send_notification "FAILED" \
"Pipeline falló en stage: $failed_stage" \
"Revisar logs en: $ARTIFACTS_DIR/$PIPELINE_ID.log"
cleanup_pipeline
}
# Generar reporte de fallo
generate_failure_report() {
local report_file="$ARTIFACTS_DIR/${PIPELINE_ID}_failure_report.json"
cat > "$report_file" << EOF
{
"pipeline_id": "$PIPELINE_ID",
"project_name": "$PROJECT_NAME",
"status": "$PIPELINE_STATUS",
"failed_stage": "$FAILED_STAGE",
"stages_completed": [$(IFS=,; echo "${STAGES_COMPLETED[*]/#/\"}" | sed 's/"/"/g' | sed 's/,/","/g')"],
"duration_seconds": $(($(date +%s) - PIPELINE_START_TIME)),
"timestamp": "$(date -Iseconds)",
"log_file": "$ARTIFACTS_DIR/$PIPELINE_ID.log"
}
EOF
log INFO "Reporte de fallo generado: $report_file"
}
# Limpiar recursos del pipeline
cleanup_pipeline() {
log INFO "Limpiando recursos del pipeline..."
# Eliminar directorio de build
rm -rf "$BUILD_DIR" 2>/dev/null || true
# Mantener solo los últimos 10 logs y artefactos
find "$ARTIFACTS_DIR" -name "pipeline_*.log" -type f | sort -r | tail -n +11 | xargs -r rm -f
find "$ARTIFACTS_DIR" -name "*_failure_report.json" -type f | sort -r | tail -n +11 | xargs -r rm -f
log SUCCESS "Limpieza completada"
}
# Función principal del pipeline
run_pipeline() {
log HEADER "PIPELINE CI/CD - $PROJECT_NAME"
log INFO "Pipeline ID: $PIPELINE_ID"
log INFO "Entorno objetivo: $DEPLOY_ENV"
# Trap para manejar errores
trap 'handle_pipeline_failure "${BASH_COMMAND%% *}"' ERR
# Ejecutar stages del pipeline
stage_checkout
stage_static_analysis
stage_dependencies
stage_build
stage_testing
stage_artifacts
stage_deployment
# Pipeline completado exitosamente
PIPELINE_STATUS="SUCCESS"
local duration=$(($(date +%s) - PIPELINE_START_TIME))
local duration_formatted=$(printf "%02d:%02d" $((duration/60)) $((duration%60)))
log SUCCESS "Pipeline completado exitosamente en $duration_formatted"
# Enviar notificación de éxito
send_notification "SUCCESS" \
"Pipeline completado exitosamente" \
"Duración: $duration_formatted\nTodos los stages completados correctamente"
cleanup_pipeline
}
# Función para mostrar ayuda
show_help() {
cat << EOF
Pipeline CI/CD v1.0
USO:
$0 [OPCIONES]
OPCIONES:
--project NAME Nombre del proyecto
--repo URL URL del repositorio Git
--branch BRANCH Rama a desplegar (default: main)
--env ENV Entorno de despliegue (staging/production/testing)
--slack-webhook URL URL del webhook de Slack para notificaciones
--email RECIPIENTS Lista de emails separados por coma
--help Mostrar esta ayuda
VARIABLES DE ENTORNO:
PROJECT_NAME Nombre del proyecto
REPO_URL URL del repositorio
BRANCH Rama (default: main)
DEPLOY_ENV Entorno (default: staging)
SLACK_WEBHOOK Webhook de Slack
EMAIL_RECIPIENTS Emails para notificaciones
EJEMPLOS:
$0 --project myapp --repo https://github.com/user/myapp.git --env staging
PROJECT_NAME=myapp REPO_URL=https://github.com/user/myapp.git $0
EOF
}
# Procesar argumentos de línea de comandos
while [ $# -gt 0 ]; do
case $1 in
--project)
PROJECT_NAME="$2"
shift 2
;;
--repo)
REPO_URL="$2"
shift 2
;;
--branch)
BRANCH="$2"
shift 2
;;
--env)
DEPLOY_ENV="$2"
shift 2
;;
--slack-webhook)
SLACK_WEBHOOK="$2"
shift 2
;;
--email)
EMAIL_RECIPIENTS="$2"
shift 2
;;
--help|-h)
show_help
exit 0
;;
*)
log ERROR "Argumento desconocido: $1"
show_help
exit 1
;;
esac
done
# Validar parámetros requeridos
if [ -z "${REPO_URL:-}" ]; then
log ERROR "URL del repositorio es requerida"
show_help
exit 1
fi
if [ -z "${ENVIRONMENTS[$DEPLOY_ENV]:-}" ]; then
log ERROR "Entorno de despliegue inválido: $DEPLOY_ENV"
log INFO "Entornos disponibles: ${!ENVIRONMENTS[*]}"
exit 1
fi
# Ejecutar pipeline
run_pipeline
Crea un pipeline que maneje múltiples entornos:
Desarrolla un sistema para desplegar microservicios: