Integración con systemd

Aprovecha systemd para crear servicios robustos y escalables

NIVEL EXPERTO

¿Qué es systemd?

systemd es un sistema de inicialización y gestor de servicios para sistemas Linux modernos. Reemplaza al tradicional System V init y proporciona un marco robusto para gestionar procesos, servicios y recursos del sistema.

Ventajas de systemd
  • Paralelización: Inicia servicios en paralelo para un arranque más rápido
  • Supervisión: Reinicia automáticamente servicios que fallan
  • Logging: Integrado con journald para logging centralizado
  • Dependencias: Gestión inteligente de dependencias entre servicios
  • Activación por socket: Servicios que se activan bajo demanda

Creando un Servicio Básico

1. Estructura de un archivo .service

# /etc/systemd/system/mi-aplicacion.service
[Unit]
Description=Mi Aplicación Web
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/mi-aplicacion
ExecStart=/opt/mi-aplicacion/start.sh
ExecStop=/opt/mi-aplicacion/stop.sh
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

2. Script de inicio completo

#!/bin/bash
# /opt/mi-aplicacion/start.sh

set -euo pipefail

# Configuración
APP_NAME="mi-aplicacion"
APP_DIR="/opt/${APP_NAME}"
LOG_DIR="/var/log/${APP_NAME}"
PID_FILE="/run/${APP_NAME}.pid"
CONFIG_FILE="${APP_DIR}/config/app.conf"

# Logging
exec 1> >(logger -s -t "${APP_NAME}" -p user.info)
exec 2> >(logger -s -t "${APP_NAME}" -p user.error)

log_info() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"
}

log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2
}

# Función de limpieza
cleanup() {
    log_info "Deteniendo aplicación..."
    if [[ -f "$PID_FILE" ]]; then
        local pid=$(cat "$PID_FILE")
        if kill -0 "$pid" 2>/dev/null; then
            kill -TERM "$pid"
            wait "$pid" 2>/dev/null || true
        fi
        rm -f "$PID_FILE"
    fi
    exit 0
}

# Manejo de señales
trap cleanup SIGTERM SIGINT

# Verificaciones previas
if [[ ! -f "$CONFIG_FILE" ]]; then
    log_error "Archivo de configuración no encontrado: $CONFIG_FILE"
    exit 1
fi

if [[ ! -d "$LOG_DIR" ]]; then
    mkdir -p "$LOG_DIR"
    chown www-data:www-data "$LOG_DIR"
fi

# Cargar configuración
source "$CONFIG_FILE"

# Verificar dependencias
command -v node >/dev/null 2>&1 || {
    log_error "Node.js no está instalado"
    exit 1
}

# Verificar puerto disponible
if netstat -tuln | grep -q ":${APP_PORT:-3000} "; then
    log_error "Puerto ${APP_PORT:-3000} ya está en uso"
    exit 1
fi

log_info "Iniciando $APP_NAME en el puerto ${APP_PORT:-3000}"

# Iniciar aplicación
cd "$APP_DIR"
exec node app.js &

# Guardar PID
echo $! > "$PID_FILE"

# Esperar indefinidamente
wait
Consejos para Servicios Robustos
  • Usa set -euo pipefail para manejo estricto de errores
  • Implementa logging estructurado con journald
  • Maneja señales SIGTERM y SIGINT correctamente
  • Verifica dependencias antes del inicio
  • Usa archivos PID para control de procesos

Gestión Avanzada de Servicios

1. Template de servicio con variables

#!/bin/bash
# create-service.sh - Generador de servicios systemd

SERVICE_TEMPLATE="[Unit]
Description={{DESCRIPTION}}
After=network.target{{AFTER_TARGETS}}
Requires={{REQUIRES}}
{{ADDITIONAL_UNIT}}

[Service]
Type={{SERVICE_TYPE}}
User={{USER}}
Group={{GROUP}}
WorkingDirectory={{WORKDIR}}
ExecStart={{EXEC_START}}
ExecStop={{EXEC_STOP}}
ExecReload={{EXEC_RELOAD}}
Restart={{RESTART_POLICY}}
RestartSec={{RESTART_SEC}}
StandardOutput=journal
StandardError=journal
Environment={{ENVIRONMENT}}
{{ADDITIONAL_SERVICE}}

[Install]
WantedBy={{WANTED_BY}}"

create_service() {
    local service_name="$1"
    local config_file="$2"
    
    # Cargar configuración
    source "$config_file"
    
    # Reemplazar variables
    local service_content="$SERVICE_TEMPLATE"
    service_content=${service_content//\{\{DESCRIPTION\}\}/${DESCRIPTION:-"Servicio personalizado"}}
    service_content=${service_content//\{\{AFTER_TARGETS\}\}/${AFTER_TARGETS}}
    service_content=${service_content//\{\{REQUIRES\}\}/${REQUIRES}}
    service_content=${service_content//\{\{SERVICE_TYPE\}\}/${SERVICE_TYPE:-"simple"}}
    service_content=${service_content//\{\{USER\}\}/${USER:-"nobody"}}
    service_content=${service_content//\{\{GROUP\}\}/${GROUP:-"nobody"}}
    service_content=${service_content//\{\{WORKDIR\}\}/${WORKDIR:-"/tmp"}}
    service_content=${service_content//\{\{EXEC_START\}\}/${EXEC_START}}
    service_content=${service_content//\{\{EXEC_STOP\}\}/${EXEC_STOP}}
    service_content=${service_content//\{\{EXEC_RELOAD\}\}/${EXEC_RELOAD}}
    service_content=${service_content//\{\{RESTART_POLICY\}\}/${RESTART_POLICY:-"always"}}
    service_content=${service_content//\{\{RESTART_SEC\}\}/${RESTART_SEC:-"10"}}
    service_content=${service_content//\{\{ENVIRONMENT\}\}/${ENVIRONMENT}}
    service_content=${service_content//\{\{WANTED_BY\}\}/${WANTED_BY:-"multi-user.target"}}
    service_content=${service_content//\{\{ADDITIONAL_UNIT\}\}/${ADDITIONAL_UNIT}}
    service_content=${service_content//\{\{ADDITIONAL_SERVICE\}\}/${ADDITIONAL_SERVICE}}
    
    # Escribir archivo de servicio
    echo "$service_content" > "/etc/systemd/system/${service_name}.service"
    
    # Recargar systemd y habilitar servicio
    systemctl daemon-reload
    systemctl enable "$service_name"
    
    echo "Servicio $service_name creado y habilitado"
}

# Ejemplo de uso
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    if [[ $# -ne 2 ]]; then
        echo "Uso: $0  "
        exit 1
    fi
    
    create_service "$1" "$2"
fi

2. Archivo de configuración de ejemplo

# webapp.conf
DESCRIPTION="Aplicación Web Node.js"
AFTER_TARGETS=" mysql.service redis.service"
REQUIRES="network.target"
SERVICE_TYPE="simple"
USER="webapp"
GROUP="webapp"
WORKDIR="/opt/webapp"
EXEC_START="/opt/webapp/bin/start.sh"
EXEC_STOP="/opt/webapp/bin/stop.sh"
EXEC_RELOAD="/bin/kill -USR1 \$MAINPID"
RESTART_POLICY="always"
RESTART_SEC="5"
ENVIRONMENT="NODE_ENV=production PORT=3000"
WANTED_BY="multi-user.target"
ADDITIONAL_SERVICE="LimitNOFILE=65536
TimeoutStartSec=30
KillMode=mixed"

Timer Units - Alternativa a Cron

1. Servicio de backup

# /etc/systemd/system/backup-db.service
[Unit]
Description=Backup de Base de Datos
Wants=network.target

[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup-database.sh
StandardOutput=journal
StandardError=journal

2. Timer para el backup

# /etc/systemd/system/backup-db.timer
[Unit]
Description=Ejecutar backup de BD cada 6 horas
Requires=backup-db.service

[Timer]
OnBootSec=30min
OnUnitActiveSec=6h
Persistent=true

[Install]
WantedBy=timers.target

3. Script de backup completo

#!/bin/bash
# /usr/local/bin/backup-database.sh

set -euo pipefail

# Configuración
BACKUP_DIR="/backup/mysql"
MYSQL_USER="backup_user"
MYSQL_PASS="backup_password"
RETENTION_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="/var/log/backup/mysql-backup.log"

# Logging
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
    logger -t "mysql-backup" "$*"
}

# Función de limpieza
cleanup() {
    local exit_code=$?
    if [[ $exit_code -ne 0 ]]; then
        log "ERROR: Backup falló con código $exit_code"
        # Enviar notificación de error
        /usr/local/bin/send-alert.sh "Backup MySQL falló" "El backup de MySQL falló con código $exit_code"
    fi
    exit $exit_code
}

trap cleanup EXIT

# Crear directorio de backup si no existe
mkdir -p "$BACKUP_DIR"
mkdir -p "$(dirname "$LOG_FILE")"

log "Iniciando backup de MySQL"

# Obtener lista de bases de datos
databases=$(mysql -u"$MYSQL_USER" -p"$MYSQL_PASS" -e "SHOW DATABASES;" | tail -n +2 | grep -v information_schema | grep -v performance_schema | grep -v mysql | grep -v sys)

# Backup de cada base de datos
for db in $databases; do
    log "Haciendo backup de $db"
    
    backup_file="${BACKUP_DIR}/${db}_${DATE}.sql.gz"
    
    if mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASS" \
        --single-transaction \
        --routines \
        --triggers \
        --events \
        --flush-logs \
        --master-data=2 \
        "$db" | gzip > "$backup_file"; then
        
        log "Backup de $db completado: $backup_file"
        
        # Verificar integridad del archivo
        if ! gzip -t "$backup_file"; then
            log "ERROR: Archivo de backup corrupto: $backup_file"
            rm -f "$backup_file"
            exit 1
        fi
        
        # Calcular checksum
        checksum=$(sha256sum "$backup_file" | cut -d' ' -f1)
        echo "$checksum  $backup_file" > "${backup_file}.sha256"
        log "Checksum: $checksum"
        
    else
        log "ERROR: Falló el backup de $db"
        exit 1
    fi
done

# Limpieza de backups antiguos
log "Limpiando backups antiguos (> ${RETENTION_DAYS} días)"
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "*.sha256" -mtime +$RETENTION_DAYS -delete

# Estadísticas del backup
backup_count=$(find "$BACKUP_DIR" -name "*_${DATE}.sql.gz" | wc -l)
total_size=$(find "$BACKUP_DIR" -name "*_${DATE}.sql.gz" -exec du -ch {} + | tail -1 | cut -f1)

log "Backup completado: $backup_count bases de datos, tamaño total: $total_size"

# Enviar notificación de éxito
/usr/local/bin/send-alert.sh "Backup MySQL exitoso" "Backup completado: $backup_count DBs, $total_size"

exit 0
Comandos Útiles para Timers
# Habilitar y iniciar timer
sudo systemctl enable backup-db.timer
sudo systemctl start backup-db.timer

# Ver estado de timers
systemctl list-timers

# Ver logs del servicio
journalctl -u backup-db.service -f

# Ejecutar manualmente
sudo systemctl start backup-db.service

Monitoring y Logging con systemd

1. Script de monitoreo de servicios

#!/bin/bash
# /usr/local/bin/service-monitor.sh

set -euo pipefail

SERVICES_TO_MONITOR=(
    "nginx"
    "mysql" 
    "redis"
    "webapp"
    "backup-db.timer"
)

WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
ALERT_FILE="/var/run/service-alerts"

send_alert() {
    local service="$1"
    local status="$2"
    local message="$3"
    
    # Evitar spam de alertas
    local alert_key="${service}_${status}"
    if grep -q "$alert_key" "$ALERT_FILE" 2>/dev/null; then
        return 0
    fi
    
    echo "$alert_key" >> "$ALERT_FILE"
    
    # Enviar a Slack
    curl -X POST -H 'Content-type: application/json' \
        --data "{\"text\":\"🚨 Service Alert: $service is $status - $message\"}" \
        "$WEBHOOK_URL" || true
    
    # Enviar email
    echo "$message" | mail -s "Service Alert: $service $status" [email protected] || true
    
    # Log local
    logger -t "service-monitor" "$service is $status: $message"
}

clear_alert() {
    local service="$1"
    local status="$2"
    local alert_key="${service}_${status}"
    
    if [[ -f "$ALERT_FILE" ]]; then
        grep -v "$alert_key" "$ALERT_FILE" > "${ALERT_FILE}.tmp" || true
        mv "${ALERT_FILE}.tmp" "$ALERT_FILE"
    fi
}

check_service() {
    local service="$1"
    
    if systemctl is-active --quiet "$service"; then
        # Servicio está activo
        clear_alert "$service" "failed"
        clear_alert "$service" "inactive"
        
        # Verificar si ha tenido reiniciocios recientes
        local restarts=$(systemctl show "$service" --property=NRestarts --value)
        if [[ "$restarts" -gt 0 ]]; then
            local restart_time=$(systemctl show "$service" --property=ExecMainStartTimestamp --value)
            if [[ -n "$restart_time" ]]; then
                local restart_epoch=$(date -d "$restart_time" +%s)
                local now_epoch=$(date +%s)
                local minutes_ago=$(( (now_epoch - restart_epoch) / 60 ))
                
                if [[ $minutes_ago -lt 5 ]]; then
                    send_alert "$service" "restarted" "Service restarted $minutes_ago minutes ago (total restarts: $restarts)"
                fi
            fi
        fi
        
    elif systemctl is-failed --quiet "$service"; then
        send_alert "$service" "failed" "Service has failed. Check with: journalctl -u $service"
        
    else
        send_alert "$service" "inactive" "Service is not running"
    fi
}

# Crear archivo de alertas si no existe
touch "$ALERT_FILE"

# Verificar cada servicio
for service in "${SERVICES_TO_MONITOR[@]}"; do
    echo "Verificando $service..."
    check_service "$service"
done

# Limpiar alertas antiguas (más de 24 horas)
find "$ALERT_FILE" -mtime +1 -delete 2>/dev/null || true

echo "Monitoreo completado"

2. Dashboard de servicios en tiempo real

#!/bin/bash
# /usr/local/bin/service-dashboard.sh

create_html_dashboard() {
    local html_file="/var/www/html/services-dashboard.html"
    
    cat > "$html_file" << 'EOF'




    Services Dashboard
    
    


    

Services Status Dashboard

Last updated: $(date)
EOF # Generar estado de servicios for service in nginx mysql redis webapp; do local status="inactive" local class="inactive" local memory="" local cpu="" local uptime="" if systemctl is-active --quiet "$service"; then status="active" class="active" # Obtener métricas memory=$(systemctl show "$service" --property=MemoryCurrent --value 2>/dev/null | numfmt --to=iec 2>/dev/null || echo "N/A") cpu=$(systemctl show "$service" --property=CPUUsageNSec --value 2>/dev/null || echo "N/A") uptime=$(systemctl show "$service" --property=ActiveEnterTimestamp --value 2>/dev/null || echo "N/A") elif systemctl is-failed --quiet "$service"; then status="failed" class="failed" fi cat >> "$html_file" << EOF

$service

Status: $status
Memory: $memory
Uptime: $uptime
EOF done echo "" >> "$html_file" } # Crear dashboard create_html_dashboard # Generar métricas JSON para APIs generate_metrics_json() { local json_file="/var/www/html/services-metrics.json" echo "{" > "$json_file" echo " \"timestamp\": \"$(date -Iseconds)\"," >> "$json_file" echo " \"services\": {" >> "$json_file" local first=true for service in nginx mysql redis webapp; do [[ "$first" == true ]] && first=false || echo "," >> "$json_file" local status="inactive" local memory=0 local restarts=0 if systemctl is-active --quiet "$service"; then status="active" memory=$(systemctl show "$service" --property=MemoryCurrent --value 2>/dev/null || echo "0") restarts=$(systemctl show "$service" --property=NRestarts --value 2>/dev/null || echo "0") elif systemctl is-failed --quiet "$service"; then status="failed" fi cat >> "$json_file" << EOF "$service": { "status": "$status", "memory_bytes": $memory, "restarts": $restarts }EOF done echo "" >> "$json_file" echo " }" >> "$json_file" echo "}" >> "$json_file" } generate_metrics_json echo "Dashboard actualizado en /var/www/html/services-dashboard.html"

Ejercicios Prácticos

Ejercicio 1: Servicio de API REST

Crea un servicio systemd para una API REST que:

  • Se ejecute como usuario no privilegiado
  • Tenga reinicio automático con backoff exponencial
  • Registre logs en journald
  • Dependa de la base de datos
Ejercicio 2: Timer de Limpieza

Implementa un timer que:

  • Limpie logs antiguos cada día a las 2 AM
  • Elimine archivos temporales
  • Comprima logs antes de archivar
  • Envíe reporte por email
Ejercicio 3: Sistema de Monitoreo

Desarrolla un sistema que:

  • Monitoree múltiples servicios
  • Genere alertas por Slack/email
  • Cree dashboard web en tiempo real
  • Registre métricas históricas