Apuntes DAM
Volver al inicio

Ejercicios de Bash resueltos

Los 51 ejercicios de Bash de la web en una sola hoja: cada uno con su enunciado, los datos que necesitas (código de partida, ejemplos de entrada y salida o la base de datos) y, al final, la solución explicada.

Descargar el PDF

Sistemas Informáticos

1. Saludo con validación

Fácil · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

Lee un nombre por teclado. Si está vacío, muestra «Error: nombre vacío»; si no, muestra «Hola, NOMBRE» y en otra línea «Tu nombre tiene N letras».

Código de partida (bash)
#!/bin/bashread -r nombre # TODO: valida y saluda

Ejemplo: Nombre normal

Entrada

Ana

Salida esperada

Hola, Ana
Tu nombre tiene 3 letras

Ejemplo: Otro nombre

Entrada

Rodrigo

Salida esperada

Hola, Rodrigo
Tu nombre tiene 7 letras

2. Tabla de multiplicar con bucle

Fácil · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

Lee un número. Si no es un entero positivo, muestra «Error: número no válido». Si lo es, muestra su tabla del 1 al 10 con el formato «N x i = resultado».

Código de partida (bash)
#!/bin/bashread -r n # TODO

Ejemplo: Tabla del 7

Entrada

7

Salida esperada

7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70

Ejemplo: Texto

Entrada

hola

Salida esperada

Error: número no válido

3. Encuentra el fallo: el saludo que no se guarda

Fácil · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

El script lee un nombre y debería mostrar «Hola, NOMBRE». En su lugar muestra un error y una línea vacía. Corrígelo.

Código de partida (bash)
#!/bin/bashread -r nombresaludo = "Hola, $nombre"echo "$saludo"

Ejemplo: Ana

Entrada

Ana

Salida esperada

Hola, Ana

Ejemplo: Luis

Entrada

Luis

Salida esperada

Hola, Luis

4. Analizar un fichero de log

Medio · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

Por la entrada llega un log con líneas «FECHA HORA NIVEL IP mensaje». Muestra: • «Errores: N» (líneas con nivel ERROR) • «Avisos: N» (nivel WARN) • «IP con más errores: IP (N)» con la IP que más aparece en las líneas ERROR.

Código de partida (bash)
#!/bin/bashlog=$(cat) # TODO

Ejemplo: Log de ejemplo

Entrada

2025-09-30 10:00:01 INFO 10.0.0.5 Inicio
2025-09-30 10:00:02 ERROR 10.0.0.7 Login fallido
2025-09-30 10:00:05 WARN 10.0.0.5 Disco al 85%
2025-09-30 10:01:00 ERROR 10.0.0.7 Login fallido
2025-09-30 10:02:00 ERROR 10.0.0.9 Timeout

Salida esperada

Errores: 3
Avisos: 1
IP con más errores: 10.0.0.7 (2)

5. Usuarios del sistema desde /etc/passwd

Medio · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

Por la entrada llega el contenido de un /etc/passwd (campos separados por «:» — usuario:x:UID:GID:comentario:home:shell). Muestra, ordenados por UID, los usuarios «reales» (UID ≥ 1000 y distinto de 65534) con el formato «usuario (UID) -> home [shell]», y al final «Total: N».

Código de partida (bash)
#!/bin/bash# La entrada estándar es el contenido de /etc/passwd # TODO

Ejemplo: passwd de ejemplo

Entrada

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
lucia:x:1001:1001:Lucía:/home/lucia:/bin/zsh
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
ana:x:1000:1000:Ana:/home/ana:/bin/bash

Salida esperada

ana (1000) -> /home/ana [/bin/bash]
lucia (1001) -> /home/lucia [/bin/zsh]
Total: 2

6. Crear ficheros y asignar permisos

Medio · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

Por la entrada llega una lista de nombres de fichero (uno por línea). En un directorio temporal (mktemp -d), crea cada fichero y asígnale permisos: 750 si termina en .sh, 600 si termina en .key y 644 en cualquier otro caso. Después muestra, ordenados por nombre, sus permisos en octal y su nombre con stat -c '%a %n'.

Código de partida (bash)
#!/bin/bashdir=$(mktemp -d)cd "$dir" || exit 1 # TODO: crea los ficheros de la entrada y asigna los permisos

Ejemplo: Tres tipos

Entrada

copia.sh
notas.txt
servidor.key

Salida esperada

750 copia.sh
644 notas.txt
600 servidor.key

7. Encuentra el fallo: la alerta que salta siempre

Medio · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

El script lee el porcentaje de uso del disco y debería mostrar «ALERTA: disco al N %» si pasa de 80, u «OK» si no. Con un uso del 9 % también da la alerta. Corrígelo.

Código de partida (bash)
#!/bin/bashread -r usoif [ $uso > 80 ]; then  echo "ALERTA: disco al $uso %"else  echo "OK"fi

Ejemplo: Disco casi lleno

Entrada

95

Salida esperada

ALERTA: disco al 95 %

Ejemplo: Disco con espacio

Entrada

9

Salida esperada

OK

Ejemplo: Justo en el límite

Entrada

80

Salida esperada

OK

8. Encuentra el fallo: el mayor que no lo es

Medio · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

El script lee dos números en la misma línea y muestra el mayor. Con «7 3» funciona, pero con «10 9» dice que el mayor es 9. Corrígelo.

Código de partida (bash)
#!/bin/bashread -r a bif [[ $a > $b ]]; then  echo "El mayor es $a"else  echo "El mayor es $b"fi

Ejemplo: Un dígito

Entrada

7 3

Salida esperada

El mayor es 7

Ejemplo: Dos cifras contra una

Entrada

10 9

Salida esperada

El mayor es 10

Ejemplo: El segundo es mayor

Entrada

4 25

Salida esperada

El mayor es 25

9. Encuentra el fallo: el nombre con espacio

Medio · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

El script da la bienvenida a Ana López y a cualquier otra persona le dice «No te conozco». Cuando escribe ella su nombre completo, también le dice que no la conoce y aparece un error. Corrígelo.

Código de partida (bash)
#!/bin/bashread -r nombreif [ $nombre = "Ana López" ]; then  echo "Bienvenida, Ana"else  echo "No te conozco"fi

Ejemplo: Nombre con espacio

Entrada

Ana López

Salida esperada

Bienvenida, Ana

Ejemplo: Otra persona

Entrada

Luis

Salida esperada

No te conozco

10. Alerta de espacio en disco

Difícil · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

Por la entrada llega la salida de «df -h» (con su cabecera). Muestra «ALERTA: PUNTO_DE_MONTAJE al N%» para cada sistema de ficheros con un uso igual o superior al umbral que se pasa en la primera línea de la entrada, y «Todo correcto» si no hay ninguno.

Código de partida (bash)
#!/bin/bashread -r umbral # TODO

Ejemplo: Umbral 80

Entrada

80
S.ficheros Tamaño Usados Disp Uso% Montado en
/dev/sda1 50G 45G 5G 90% /
/dev/sdb1 200G 100G 100G 50% /datos
/dev/sdc1 20G 16G 4G 80% /var

Salida esperada

ALERTA: / al 90%
ALERTA: /var al 80%

11. ¿Quién intenta entrar por SSH?

Difícil · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

La entrada es un fragmento de /var/log/auth.log. Las líneas de contraseñas fallidas tienen la forma «… sshd[PID]: Failed password for USUARIO from IP port N ssh2» o «… Failed password for invalid user USUARIO from IP port N ssh2». Cuenta los intentos fallidos por IP y muéstralos de más a menos intentos (a igualdad, por IP en orden alfabético) con el formato «IP: N intentos», añadiendo « → BLOQUEAR» si son 5 o más. Termina con «Usuarios inexistentes probados: N» (usuarios distintos que aparecen tras «invalid user»). Las demás líneas se ignoran.

Código de partida (bash)
#!/bin/bash# Lee el log por la entrada estándarlog=$(cat) # TODO: intentos fallidos por IP y usuarios inexistentes probados

Ejemplo: Ataque desde una IP

Entrada

Oct  1 10:00:01 srv sshd[901]: Failed password for root from 203.0.113.9 port 50122 ssh2
Oct  1 10:00:03 srv sshd[901]: Failed password for root from 203.0.113.9 port 50122 ssh2
Oct  1 10:00:05 srv sshd[905]: Failed password for invalid user admin from 203.0.113.9 port 50200 ssh2
Oct  1 10:00:07 srv sshd[905]: Failed password for invalid user test from 203.0.113.9 port 50200 ssh2
Oct  1 10:00:09 srv sshd[905]: Failed password for invalid user admin from 203.0.113.9 port 50201 ssh2
Oct  1 10:02:00 srv sshd[950]: Accepted publickey for ana from 192.168.1.20 port 41000 ssh2
Oct  1 10:05:00 srv sshd[960]: Failed password for ana from 192.168.1.20 port 41100 ssh2

Salida esperada

203.0.113.9: 5 intentos → BLOQUEAR
192.168.1.20: 1 intentos
Usuarios inexistentes probados: 2

Ejemplo: Varias IP empatadas

Entrada

a sshd[1]: Failed password for root from 10.0.0.9 port 1 ssh2
b sshd[1]: Failed password for root from 10.0.0.10 port 1 ssh2
c sshd[1]: Failed password for invalid user pi from 10.0.0.9 port 2 ssh2
d sshd[1]: Failed password for invalid user pi from 10.0.0.10 port 2 ssh2
e sshd[1]: Connection closed by 10.0.0.11 port 3

Salida esperada

10.0.0.10: 2 intentos
10.0.0.9: 2 intentos
Usuarios inexistentes probados: 1

12. Encuentra el fallo: el total que se queda en cero

Difícil · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

El script suma los números que recibe por la entrada, uno por línea, y muestra «Total: N». Siempre muestra «Total: 0». Corrígelo.

Código de partida (bash)
#!/bin/bashtotal=0cat | while read -r n; do  total=$((total + n))doneecho "Total: $total"

Ejemplo: Tres números

Entrada

3
5
2

Salida esperada

Total: 10

Ejemplo: Con un negativo

Entrada

100
-40

Salida esperada

Total: 60

13. Encuentra el fallo: las 08:09 que rompen la cuenta

Difícil · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

El script lee una hora con el formato HH:MM y muestra los minutos que han pasado desde medianoche. Con 10:30 funciona (630), pero con 08:09 da un error. Corrígelo.

Código de partida (bash)
#!/bin/bashread -r horah=${hora%%:*}m=${hora##*:}echo $(( h * 60 + m ))

Ejemplo: Media mañana

Entrada

10:30

Salida esperada

630

Ejemplo: Con ceros delante

Entrada

08:09

Salida esperada

489

Ejemplo: Última hora

Entrada

23:59

Salida esperada

1439

14. Permisos: de letras a octal y al revés

Muy difícil · Administración de Sistemas Operativos · apuntesdam.com/subject/sistemas/topic/administracion-sistemas-operativos

Cada línea es un permiso en notación simbólica de 9 caracteres (rwxr-x---) o en octal de 3 o 4 cifras (750, 4755). Convierte cada uno a la otra notación: «rwxr-x--- = 750» y «0644 = rw-r--r--». Ten en cuenta los bits especiales: setuid (4), setgid (2) y sticky (1), que en la notación simbólica cambian la x del propietario, del grupo o de otros por s o t (o por S o T si ese permiso de ejecución no está). Una entrada simbólica con bits especiales da 4 cifras (rwsr-xr-x = 4755); sin ellos, 3. Una entrada octal se muestra siempre con 4 cifras a la izquierda del «=». Si la línea no es válida, «línea: no válido».

Código de partida (bash)
#!/bin/bash # TODO: convierte cada línea de simbólico a octal o de octal a simbólico

Ejemplo: Permisos habituales

Entrada

rwxr-x---
rw-r--r--
644
750

Salida esperada

rwxr-x--- = 750
rw-r--r-- = 644
0644 = rw-r--r--
0750 = rwxr-x---

Ejemplo: Bits especiales

Entrada

rwsr-xr-x
rwxrwxrwt
rwxrws---
4755
1777
2770

Salida esperada

rwsr-xr-x = 4755
rwxrwxrwt = 1777
rwxrws--- = 2770
4755 = rwsr-xr-x
1777 = rwxrwxrwt
2770 = rwxrws---

15. Contar ficheros por extensión

Medio · Ejercicios de Sistemas Informáticos · apuntesdam.com/subject/sistemas/topic/ejercicios-sistemas

Por la entrada llega una lista de nombres de fichero (uno por línea). Muestra cuántos hay de cada extensión, en minúsculas y ordenadas alfabéticamente, con el formato «ext: N». Los ficheros sin extensión cuentan como «(sin extensión)» y se muestran al final.

Código de partida (bash)
#!/bin/bash# Entrada: un nombre de fichero por línea # TODO

Ejemplo: Varias extensiones

Entrada

informe.PDF
foto.jpg
notas.txt
FOTO2.JPG
Makefile
script.sh
factura.pdf

Salida esperada

jpg: 2
pdf: 2
sh: 1
txt: 1
(sin extensión): 1

16. Validar direcciones IPv4

Difícil · Ejercicios de Sistemas Informáticos · apuntesdam.com/subject/sistemas/topic/ejercicios-sistemas

Por la entrada llega una dirección por línea. Para cada una muestra «IP válida» o «IP no válida». Es válida si tiene cuatro números separados por puntos, cada uno entre 0 y 255 y sin ceros a la izquierda (salvo el propio 0).

Código de partida (bash)
#!/bin/bashwhile read -r ip || [ -n "$ip" ]; do  # TODO: comprueba la IP  echo "IP válida"done

Ejemplo: Casos variados

Entrada

192.168.1.10
256.1.1.1
10.0.0
0.0.0.0
192.168.01.1
hola

Salida esperada

IP válida
IP no válida
IP no válida
IP válida
IP no válida
IP no válida

17. Red y broadcast con aritmética de bits

Difícil · Ejercicios de Sistemas Informáticos · apuntesdam.com/subject/sistemas/topic/ejercicios-sistemas

Por la entrada llega una dirección con prefijo, por ejemplo 192.168.1.130/26. Calcula con bash (sin herramientas externas) y muestra: Red: 192.168.1.128 Broadcast: 192.168.1.191 Hosts: 62

Código de partida (bash)
#!/bin/bashread -r entradaip=${entrada%/*}prefijo=${entrada#*/} # TODO: calcula red, broadcast y hosts

Ejemplo: /26

Entrada

192.168.1.130/26

Salida esperada

Red: 192.168.1.128
Broadcast: 192.168.1.191
Hosts: 62

Ejemplo: /20

Entrada

172.16.37.10/20

Salida esperada

Red: 172.16.32.0
Broadcast: 172.16.47.255
Hosts: 4094

Despliegue de Aplicaciones Web

18. ¿Cuántos nueves tiene tu arquitectura?

Medio · Arquitecturas de Despliegue y la Nube · apuntesdam.com/subject/despliegue/topic/arquitecturas-despliegue

La primera línea es «objetivo X» (la disponibilidad que exige el cliente, en %). Después, una línea por capa de la arquitectura: «capa nombre disponibilidad réplicas». Las réplicas de una capa están en paralelo (la capa solo cae si caen todas: 1 − (1 − a)^n) y las capas están en serie (la disponibilidad total es el producto). Escribe por capa «nombre: n × a % → capa %» (a con 2 decimales, capa con 6), después «Total: …» con 6 decimales, «Caída máxima al mes: X min» (mes de 43.200 minutos, 1 decimal) y «Cumple el objetivo» o «No cumple el objetivo del X %».

Código de partida (bash)
#!/bin/bashexport LC_ALL=C # TODO: calcula con awkcat > /dev/null

Ejemplo: Tienda con réplicas

Entrada

objetivo 99.95
capa balanceador 99.99 1
capa web 99.5 3
capa bd 99.9 2

Salida esperada

balanceador: 1 × 99.99 % → 99.990000 %
web: 3 × 99.50 % → 99.999988 %
bd: 2 × 99.90 % → 99.999900 %
Total: 99.989888 %
Caída máxima al mes: 4.4 min
Cumple el objetivo

Ejemplo: Todo en un servidor

Entrada

objetivo 99.9
capa vps 99.9 1
capa bd 99.5 1

Salida esperada

vps: 1 × 99.90 % → 99.900000 %
bd: 1 × 99.50 % → 99.500000 %
Total: 99.400500 %
Caída máxima al mes: 259.0 min
No cumple el objetivo del 99.90 %

19. Un balanceador ponderado con caídas

Difícil · Arquitecturas de Despliegue y la Nube · apuntesdam.com/subject/despliegue/topic/arquitecturas-despliegue

Nginx reparte las peticiones entre servidores con pesos distintos usando el algoritmo round robin ponderado «suave». Cada servidor tiene un peso w y un contador cw que empieza en 0. En cada petición: a todos los servidores en servicio se les suma su peso a cw; se elige el de mayor cw (a igualdad, el primero de la lista) y a ese se le resta la suma de los pesos en servicio. La entrada tiene líneas «nombre peso», una línea «---» y después eventos: «peticion», «caido nombre» o «vuelve nombre» (al caer o volver, su cw se pone a 0). Escribe «→ nombre» por petición, «nombre fuera de servicio», «nombre de vuelta», «503 sin servidores» si no queda ninguno y, al final, «Reparto: a=N, b=N…» en el orden de la lista.

Código de partida (bash)
#!/bin/bashdeclare -A peso cw activo cuentaorden=()while read -r a b && [ "$a" != "---" ]; do    orden+=("$a"); peso[$a]=$b; cw[$a]=0; activo[$a]=1; cuenta[$a]=0done while read -r evento nombre || [ -n "$evento" ]; do    # TODO    :done reparto=()for s in "${orden[@]}"; do reparto+=("$s=${cuenta[$s]}"); done(IFS=,; echo "Reparto: ${reparto[*]}" | sed 's/,/, /g')

Ejemplo: Pesos 5, 1 y 1

Entrada

a 5
b 1
c 1
---
peticion
peticion
peticion
peticion
peticion
peticion
peticion

Salida esperada

→ a
→ a
→ b
→ a
→ c
→ a
→ a
Reparto: a=5, b=1, c=1

Ejemplo: Caídas y vueltas

Entrada

web1 2
web2 1
---
peticion
peticion
peticion
caido web1
peticion
peticion
caido web2
peticion
vuelve web1
peticion
peticion

Salida esperada

→ web1
→ web2
→ web1
web1 fuera de servicio
→ web2
→ web2
web2 fuera de servicio
503 sin servidores
web1 de vuelta
→ web1
→ web1
Reparto: web1=4, web2=3

20. Auditoría de un servidor recién instalado

Muy difícil · Arquitecturas de Despliegue y la Nube · apuntesdam.com/subject/despliegue/topic/arquitecturas-despliegue

Antes de publicar una aplicación hay que bastionar el servidor. La entrada tiene tres bloques separados por «---»: el fichero sshd_config, la lista de puertos en escucha («protocolo dirección:puerto programa», como la resume ss -tlnp) y una línea «permitidos 22,80,443». Revisa, en este orden: PermitRootLogin yes (ALTO), PasswordAuthentication distinto de no (ALTO; si no aparece, su valor por defecto es yes), MaxAuthTries mayor que 4 (MEDIO; por defecto 6) y X11Forwarding yes (MEDIO). Ojo: en sshd_config las directivas no distinguen mayúsculas, se ignoran las líneas en blanco y los comentarios, y cuenta la PRIMERA aparición de cada directiva. Después, cada puerto escuchando en 0.0.0.0, [::] o * que no esté permitido es un ALTO («puerto N (programa) abierto a internet»), por orden de número y sin repetir (los que escuchan en 127.0.0.1 o ::1 no importan). Termina con «Resultado: N altos, M medios» o «Resultado: sin problemas».

Código de partida (bash)
#!/bin/bash# TODO: analiza los tres bloquescat > /dev/nullecho "Resultado: sin problemas"

Ejemplo: Instalación por defecto

Entrada

# Configuración de ejemplo
Port 22
PermitRootLogin yes
#PasswordAuthentication no
X11Forwarding yes
---
tcp 0.0.0.0:22 sshd
tcp 0.0.0.0:80 nginx
tcp 0.0.0.0:3306 mysqld
tcp [::]:3306 mysqld
tcp 127.0.0.1:6379 redis
---
permitidos 22,80,443

Salida esperada

ALTO: PermitRootLogin yes permite entrar como root con contraseña
ALTO: PasswordAuthentication permite contraseñas (usa solo claves)
MEDIO: MaxAuthTries 6 (recomendado 4 o menos)
MEDIO: X11Forwarding activado sin necesidad
ALTO: puerto 3306 (mysqld) abierto a internet
Resultado: 3 altos, 2 medios

Ejemplo: Bien configurado

Entrada

PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
---
tcp 0.0.0.0:22 sshd
tcp 0.0.0.0:443 nginx
tcp 127.0.0.1:3306 mysqld
tcp [::1]:5432 postgres
---
permitidos 22,80,443

Salida esperada

Resultado: sin problemas

21. Generar un virtual host de Nginx

Fácil · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

La entrada es una línea «dominio raíz [puerto]». Genera el bloque server de Nginx con este formato exacto (4 espacios de sangría): «server {», « listen 80;», « server_name dominio www.dominio;» y, si no hay puerto, « root raíz;» y « index index.html;»; si hay puerto, en su lugar « location / {», « proxy_pass http://127.0.0.1:puerto;», « }». Termina con «}». Si falta el dominio o la raíz, escribe «Uso: dominio raíz [puerto]».

Código de partida (bash)
#!/bin/bashread -r dominio raiz puerto # TODOexit 0

Ejemplo: Sitio estático

Entrada

apuntes.es /var/www/apuntes

Salida esperada

server {
    listen 80;
    server_name apuntes.es www.apuntes.es;
    root /var/www/apuntes;
    index index.html;
}

Ejemplo: Aplicación con proxy

Entrada

tienda.com /srv/tienda 3000

Salida esperada

server {
    listen 80;
    server_name tienda.com www.tienda.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

Ejemplo: Datos incompletos

Entrada

solo-dominio.com

Salida esperada

Uso: dominio raíz [puerto]

22. Estadísticas de un access.log

Medio · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

La entrada es un fichero de registro de Nginx en formato combinado. Muestra «Peticiones: N», «Errores 4xx: N», «Errores 5xx: N» y «IP con más peticiones: IP (N)». Si hay empate, gana la IP menor alfabéticamente.

Código de partida (bash)
#!/bin/bashlog=$(cat) # TODO

Ejemplo: Registro de ejemplo

Entrada

203.0.113.5 - - [30/Sep/2025:10:01:02 +0200] "GET / HTTP/1.1" 200 5120 "-" "Mozilla/5.0"
198.51.100.7 - - [30/Sep/2025:10:01:05 +0200] "GET /login HTTP/1.1" 200 2048 "-" "Mozilla/5.0"
203.0.113.5 - - [30/Sep/2025:10:01:09 +0200] "POST /login HTTP/1.1" 302 0 "-" "Mozilla/5.0"
192.0.2.44 - - [30/Sep/2025:10:02:11 +0200] "GET /wp-admin HTTP/1.1" 404 512 "-" "curl/8.0"
192.0.2.44 - - [30/Sep/2025:10:02:12 +0200] "GET /.env HTTP/1.1" 404 512 "-" "curl/8.0"
203.0.113.5 - - [30/Sep/2025:10:03:40 +0200] "GET /api/pedidos HTTP/1.1" 500 128 "-" "Mozilla/5.0"
198.51.100.7 - - [30/Sep/2025:10:04:01 +0200] "GET /panel HTTP/1.1" 403 256 "-" "Mozilla/5.0"

Salida esperada

Peticiones: 7
Errores 4xx: 3
Errores 5xx: 1
IP con más peticiones: 203.0.113.5 (3)

23. Vigilar la caducidad de los certificados

Medio · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

Certbot renueva solo, hasta que un día no lo hace (un cambio de DNS, un puerto cerrado) y la web muestra un aviso de seguridad a todos los visitantes. La primera línea es «hoy AAAA-MM-DD» y las siguientes, «dominio fecha-de-caducidad». Escribe los dominios ordenados por días restantes (a igualdad, alfabéticamente) con su estado: «CADUCADO hace N días» si ya ha caducado, «CRÍTICO: caduca en N días» si quedan 7 o menos, «AVISO: caduca en N días» si quedan 30 o menos y «ok (N días)» en otro caso. Al final, «Resumen: N críticos, M avisos» (los caducados cuentan como críticos).

Código de partida (bash)
#!/bin/bashread -r _ hoy # TODOcat > /dev/null

Ejemplo: Varios dominios

Entrada

hoy 2026-09-30
apuntes.es 2026-12-01
tienda.com 2026-10-05
blog.tienda.com 2026-10-20
api.tienda.com 2026-09-25

Salida esperada

api.tienda.com: CADUCADO hace 5 días
tienda.com: CRÍTICO: caduca en 5 días
blog.tienda.com: AVISO: caduca en 20 días
apuntes.es: ok (62 días)
Resumen: 2 críticos, 1 avisos

Ejemplo: Empates y límites

Entrada

hoy 2026-02-20
b.es 2026-03-22
a.es 2026-03-22
c.es 2026-02-27
d.es 2026-02-20
e.es 2026-03-23

Salida esperada

d.es: CRÍTICO: caduca en 0 días
c.es: CRÍTICO: caduca en 7 días
a.es: AVISO: caduca en 30 días
b.es: AVISO: caduca en 30 días
e.es: ok (31 días)
Resumen: 2 críticos, 2 avisos

24. Cadenas y bucles de redirecciones

Difícil · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

Tras varias migraciones de una web, las redirecciones 301 se acumulan: /antiguo → /intermedio → /nuevo obliga al navegador (y a Google) a dar varios saltos, y un error puede crear un bucle infinito. La entrada tiene reglas «origen destino» (una por línea), una línea «---» y después URLs a comprobar. Para cada URL sigue las reglas y escribe: «url: 200 sin redirección» si no hay regla, «url: 301 → destino» si hay un solo salto, «url: CADENA de N saltos → final (redirige directamente)» si hay más, o «url: BUCLE a → b → … → a» si vuelve a una URL ya visitada (mostrando el camino hasta la repetición).

Código de partida (bash)
#!/bin/bashdeclare -A reglawhile read -r origen destino && [ "$origen" != "---" ]; do    regla[$origen]=$destinodone while read -r url || [ -n "$url" ]; do    [ -z "$url" ] && continue    # TODO    echo "$url: 200 sin redirección"done

Ejemplo: Migraciones acumuladas

Entrada

/blog /noticias
/noticias /actualidad
/actualidad /revista
/tienda /productos
---
/blog
/tienda
/contacto
/noticias

Salida esperada

/blog: CADENA de 3 saltos → /revista (redirige directamente)
/tienda: 301 → /productos
/contacto: 200 sin redirección
/noticias: CADENA de 2 saltos → /revista (redirige directamente)

Ejemplo: Un bucle

Entrada

/a /b
/b /c
/c /a
/x /x
/inicio /a
---
/inicio
/x
/b

Salida esperada

/inicio: BUCLE /inicio → /a → /b → /c → /a
/x: BUCLE /x → /x
/b: BUCLE /b → /c → /a → /b

25. ¿Qué location atiende la petición?

Muy difícil · Servidores Web: Apache y Nginx · apuntesdam.com/subject/despliegue/topic/servidores-web

Entender cómo elige Nginx el bloque location evita horas de depuración. La entrada tiene bloques location («= /ruta», «^~ /prefijo», «~ expresión», «~* expresión» o solo «/prefijo»), una línea «---» y URIs. Aplica el algoritmo de Nginx: 1) si una location exacta (=) coincide, gana; 2) si no, busca el prefijo (normal o ^~) más largo que coincida; si ese prefijo es ^~, gana sin mirar las expresiones; 3) si no, prueba las expresiones regulares (~ distingue mayúsculas, ~* no) en el orden en que aparecen y gana la primera que coincida; 4) si ninguna coincide, gana el prefijo más largo encontrado en el paso 2. Escribe «uri → location» tal como está escrita (el prefijo normal sin modificador), o «uri → 404 (ninguna)».

Código de partida (bash)
#!/bin/bashmods=(); pats=()while read -r a b && [ "$a" != "---" ]; do    if [ -n "$b" ]; then mods+=("$a"); pats+=("$b"); else mods+=(""); pats+=("$a"); fidone while read -r uri || [ -n "$uri" ]; do    [ -z "$uri" ] && continue    # TODO: aplica el algoritmo de Nginx    echo "$uri → 404 (ninguna)"done

Ejemplo: Configuración típica

Entrada

= /
/
/api/
^~ /static/
~ \.php$
~* \.(jpg|png)$
---
/
/contacto
/api/usuarios
/static/logo.png
/imagenes/FOTO.JPG
/admin/index.php
/api/export.php

Salida esperada

/ → = /
/contacto → /
/api/usuarios → /api/
/static/logo.png → ^~ /static/
/imagenes/FOTO.JPG → ~* \.(jpg|png)$
/admin/index.php → ~ \.php$
/api/export.php → ~ \.php$

Ejemplo: Prefijo más largo y regex

Entrada

/docs/
/docs/v2/
~ ^/docs/v[0-9]+/privado
---
/docs/intro
/docs/v2/guia
/docs/v2/privado/claves
/blog

Salida esperada

/docs/intro → /docs/
/docs/v2/guia → /docs/v2/
/docs/v2/privado/claves → ~ ^/docs/v[0-9]+/privado
/blog → 404 (ninguna)

26. Estado de los servicios

Medio · Servidores de Aplicaciones · apuntesdam.com/subject/despliegue/topic/servidores-aplicaciones

Cada línea es el resultado de comprobar un servicio: «nombre código_http milisegundos». Clasifica cada uno como «OK» (200 y menos de 500 ms), «LENTO» (200 y 500 ms o más) o «CAÍDO» (cualquier otro código), con el formato «nombre: ESTADO». Al final, «Estado general: OK», «DEGRADADO» (si hay alguno lento y ninguno caído) o «CAÍDO» (si hay alguno caído).

Código de partida (bash)
#!/bin/bashwhile read -r nombre codigo ms || [ -n "$nombre" ]; do    # TODO    :done

Ejemplo: Todo bien

Entrada

web 200 120
api 200 340

Salida esperada

web: OK
api: OK
Estado general: OK

Ejemplo: Uno lento

Entrada

web 200 120
api 200 850
cola 200 499

Salida esperada

web: OK
api: LENTO
cola: OK
Estado general: DEGRADADO

Ejemplo: Uno caído

Entrada

web 200 120
api 503 30
pagos 200 900

Salida esperada

web: OK
api: CAÍDO
pagos: LENTO
Estado general: CAÍDO

27. Revisar el server.xml de Tomcat

Medio · Servidores de Aplicaciones · apuntesdam.com/subject/despliegue/topic/servidores-aplicaciones

La entrada es un fichero server.xml (cada elemento en una línea). Muestra, en el orden en que aparecen: «Apagado: puerto P, orden O» (elemento Server), «Conector P PROTOCOLO» con « (TLS)» si tiene SSLEnabled="true" (si no hay protocol, es HTTP/1.1) y «Host nombre → appBase (autoDeploy=valor)» (si no se indica, autoDeploy vale true). Ignora las líneas de comentario (<!-- … -->). Después escribe los avisos, en el orden en que se detectan: «AVISO: puerto de apagado P con la orden por defecto» si el puerto no es -1 y la orden es SHUTDOWN; «PELIGRO: conector AJP P accesible desde fuera (Ghostcat)» si un conector AJP no tiene address local (127.0.0.1, ::1 o localhost); «AVISO: autoDeploy activado en nombre» para cada host que lo tenga. Termina con «Avisos: N».

Código de partida (bash)
#!/bin/bash# TODO: inventario y avisoscat > /dev/nullecho "Avisos: 0"

Ejemplo: Instalación por defecto

Entrada

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
    <!-- <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" /> -->
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
      </Host>
    </Engine>
  </Service>
</Server>

Salida esperada

Apagado: puerto 8005, orden SHUTDOWN
Conector 8080 HTTP/1.1
Conector 8009 AJP/1.3
Host localhost → webapps (autoDeploy=true)
AVISO: puerto de apagado 8005 con la orden por defecto
PELIGRO: conector AJP 8009 accesible desde fuera (Ghostcat)
AVISO: autoDeploy activado en localhost
Avisos: 3

Ejemplo: Bien configurado

Entrada

<Server port="-1" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true" maxThreads="200" />
    <Connector port="8009" protocol="AJP/1.3" address="127.0.0.1" secret="s3cr3t" />
    <Engine name="Catalina" defaultHost="tienda.es">
      <Host name="tienda.es" appBase="/srv/tienda" autoDeploy="false" />
      <Host name="intranet.tienda.es" appBase="/srv/intranet" autoDeploy="false" />
    </Engine>
  </Service>
</Server>

Salida esperada

Apagado: puerto -1, orden SHUTDOWN
Conector 8443 org.apache.coyote.http11.Http11NioProtocol (TLS)
Conector 8009 AJP/1.3
Host tienda.es → /srv/tienda (autoDeploy=false)
Host intranet.tienda.es → /srv/intranet (autoDeploy=false)
Avisos: 0

28. Despliegue atómico con releases y vuelta atrás

Difícil · Servidores de Aplicaciones · apuntesdam.com/subject/despliegue/topic/servidores-aplicaciones

Cada versión se copia en su propia carpeta releases/VERSIÓN y la web apunta a un enlace simbólico current: cambiarlo es instantáneo y volver atrás es apuntarlo a la anterior. Usa carpetas y enlaces reales dentro de /tmp/app. Cada línea es una orden: «desplegar v» (crea releases/v, apunta current a ella y conserva solo las 3 releases más recientes, sin borrar nunca la actual; si v ya existe, «Error: v ya existe»), «rollback» (apunta current a la release desplegada justo antes de la actual; si no hay, «Error: no hay versión anterior») y «estado». Tras cada cambio y con «estado», escribe «current → v (releases: a b c)» con las releases de más antigua a más reciente (o «Sin desplegar»).

Código de partida (bash)
#!/bin/bashbase=/tmp/apprm -rf "$base" && mkdir -p "$base/releases"orden=() while read -r accion version || [ -n "$accion" ]; do    # TODO    echo "Sin desplegar"done

Ejemplo: Varias versiones

Entrada

estado
desplegar v1
desplegar v2
desplegar v3
desplegar v4
rollback
rollback
estado

Salida esperada

Sin desplegar
current → v1 (releases: v1)
current → v2 (releases: v1 v2)
current → v3 (releases: v1 v2 v3)
current → v4 (releases: v2 v3 v4)
current → v3 (releases: v2 v3 v4)
current → v2 (releases: v2 v3 v4)
current → v2 (releases: v2 v3 v4)

Ejemplo: Errores

Entrada

rollback
desplegar a
rollback
desplegar b
desplegar b
rollback
desplegar c
desplegar d
desplegar e

Salida esperada

Error: no hay versión anterior
current → a (releases: a)
Error: no hay versión anterior
current → b (releases: a b)
Error: b ya existe
current → a (releases: a b)
current → c (releases: a b c)
current → d (releases: b c d)
current → e (releases: c d e)

29. Despliegue canary: ¿promocionar o revertir?

Muy difícil · Servidores de Aplicaciones · apuntesdam.com/subject/despliegue/topic/servidores-aplicaciones

En un despliegue canary, la versión nueva recibe primero un 5 % del tráfico, luego un 25 %, un 50 % y por fin el 100 %, y solo avanza si se comporta al menos tan bien como la estable. Cada línea es una evaluación con las métricas de ambas versiones: «peticiones errores p95 | peticiones errores p95» (estable | canario; p95 en milisegundos). En cada evaluación, en este orden: si el canario tiene menos de 50 peticiones, «N: X % → datos insuficientes (P peticiones)» y se sigue igual; si su tasa de errores (en %) supera en más de 0,5 puntos a la estable, «N: X % → REVERTIDA (errores A % frente a B %)» con 2 decimales, y se termina; si su p95 supera en más de un 20 % al estable, «N: X % → REVERTIDA (latencia A ms frente a B ms)» y se termina; si todo va bien, «N: X % → ok, pasa al Y %» o, si ya estaba al 100 %, «N: 100 % → ok, PROMOCIONADA» y se termina. Si se acaban las evaluaciones sin decidir, «Sin decisión: se mantiene el X %».

Código de partida (bash)
#!/bin/bashexport LC_ALL=C# TODO: evalúa cada línea con awkcat > /dev/null

Ejemplo: Todo va bien

Entrada

10000 20 180 | 520 1 190
8000 16 175 | 2600 6 200
6000 12 170 | 6000 15 180
2000 5 160 | 10000 30 185

Salida esperada

1: 5 % → ok, pasa al 25 %
2: 25 % → ok, pasa al 50 %
3: 50 % → ok, pasa al 100 %
4: 100 % → ok, PROMOCIONADA

Ejemplo: Errores y datos insuficientes

Entrada

9000 45 200 | 30 0 150
9000 45 200 | 480 3 210
7000 30 190 | 2400 25 195

Salida esperada

1: 5 % → datos insuficientes (30 peticiones)
2: 5 % → ok, pasa al 25 %
3: 25 % → REVERTIDA (errores 1.04 % frente a 0.43 %)

30. Revisar un Dockerfile

Medio · Contenedores con Docker · apuntesdam.com/subject/despliegue/topic/docker

La entrada es un Dockerfile. Escribe un aviso por línea, en este orden, cuando corresponda: «Imagen base sin versión concreta» (algún FROM sin etiqueta o con :latest), «apt-get install sin limpiar la caché» (un RUN con apt-get install sin rm -rf /var/lib/apt/lists), «Se copia todo el código antes de instalar las dependencias» (un COPY . aparece antes del RUN que ejecuta npm install/npm ci, composer install o pip install) y «El contenedor se ejecuta como root» (no hay ninguna instrucción USER). Si no hay ningún aviso, escribe «Sin avisos».

Código de partida (bash)
#!/bin/bashdockerfile=$(cat)avisos=() # TODO

Ejemplo: Dockerfile mejorable

Entrada

FROM node
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

Salida esperada

Imagen base sin versión concreta
Se copia todo el código antes de instalar las dependencias
El contenedor se ejecuta como root

Ejemplo: Dockerfile correcto

Entrada

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]

Salida esperada

Sin avisos

Ejemplo: apt-get y latest

Entrada

FROM ubuntu:latest
RUN apt-get update && apt-get install -y nginx
USER www-data

Salida esperada

Imagen base sin versión concreta
apt-get install sin limpiar la caché

31. Puertos de un docker compose

Medio · Contenedores con Docker · apuntesdam.com/subject/despliegue/topic/docker

Cada línea describe los puertos de un servicio: «servicio: p1 p2 …», donde cada puerto es «anfitrión:contenedor» (se publica) o solo «contenedor» (queda en la red interna). Para cada puerto, en orden: si es solo interno, «servicio: P solo interno (no se publica)»; si alguno de los números no es un entero entre 1 y 65535, «servicio: P ERROR puerto no válido»; si el puerto del anfitrión ya lo publica otro servicio, «servicio: P ERROR el puerto H ya lo publica otro»; si no, «servicio: H → C», con « (privilegiado)» si el del anfitrión es menor que 1024. Al final, «Publicados: N, errores: M».

Código de partida (bash)
#!/bin/bashdeclare -A usadoerrores=0while IFS= read -r linea || [ -n "$linea" ]; do    [ -z "$linea" ] && continue    servicio=${linea%%:*}    resto=${linea#*:}    for p in $resto; do        # TODO        echo "$servicio: $p"    donedoneecho "Publicados: ${#usado[@]}, errores: $errores"

Ejemplo: Proxy, aplicación y base de datos

Entrada

proxy: 80:80 443:443
app: 3000
db: 3306
adminer: 8080:8080

Salida esperada

proxy: 80 → 80 (privilegiado)
proxy: 443 → 443 (privilegiado)
app: 3000 solo interno (no se publica)
db: 3306 solo interno (no se publica)
adminer: 8080 → 8080
Publicados: 3, errores: 0

Ejemplo: Conflictos y errores

Entrada

web: 8080:80
api: 8080:3000 8081:3000
cola: 70000:5672 abc:15672
metricas: 9090:9090 443:9443

Salida esperada

web: 8080 → 80
api: 8080:3000 ERROR el puerto 8080 ya lo publica web
api: 8081 → 3000
cola: 70000:5672 ERROR puerto no válido
cola: abc:15672 ERROR puerto no válido
metricas: 9090 → 9090
metricas: 443 → 9443 (privilegiado)
Publicados: 4, errores: 3

32. Orden de arranque según depends_on

Difícil · Contenedores con Docker · apuntesdam.com/subject/despliegue/topic/docker

Docker Compose arranca primero los servicios de los que dependen otros. Cada línea es «servicio: dep1 dep2 …» (puede no tener dependencias). Calcula el arranque por rondas: en cada ronda arrancan a la vez, en orden alfabético, todos los servicios cuyas dependencias ya han arrancado («Ronda N: a b c»). Antes, comprueba dos errores: si un servicio depende de uno que no existe, escribe solo «Error: s depende de d, que no existe» (el primero que encuentres, en el orden de las líneas y de sus dependencias); si hay dependencias circulares, escribe solo «Ciclo: a → b → … → a», buscando con una búsqueda en profundidad que visita los servicios y sus dependencias en orden alfabético y muestra el ciclo desde el primer servicio repetido.

Código de partida (bash)
#!/bin/bashdeclare -A deporden=()while IFS=: read -r s d || [ -n "$s" ]; do    [ -z "$s" ] && continue    s=$(echo $s); dep[$s]=$(echo $d); orden+=("$s")done # TODO: errores, ciclos y rondasecho "Ronda 1: ${orden[*]}"

Ejemplo: Tienda completa

Entrada

proxy: app
app: db cache
cache:
db:
worker: db cache
migraciones: db

Salida esperada

Ronda 1: cache db
Ronda 2: app migraciones worker
Ronda 3: proxy

Ejemplo: Un ciclo

Entrada

api: auth
auth: usuarios
usuarios: api
web: api

Salida esperada

Ciclo: api → auth → usuarios → api

Ejemplo: Dependencia inexistente

Entrada

app: db redis
db:

Salida esperada

Error: app depende de redis, que no existe

33. ¿Qué capas se reconstruyen?

Muy difícil · Contenedores con Docker · apuntesdam.com/subject/despliegue/topic/docker

Docker reutiliza cada capa de la construcción anterior si la instrucción es idéntica, si en las instrucciones COPY o ADD no ha cambiado ninguno de los ficheros que copian y si ninguna capa anterior se ha reconstruido (en cuanto una se reconstruye, todas las siguientes también). La entrada tiene el Dockerfile de la construcción anterior, «---», el nuevo, «---» y la lista de ficheros modificados. Una fuente de COPY puede ser «.» (todo), un fichero, una carpeta (acabada o no en /: cubre lo que hay dentro) o un patrón con * (package*.json). El último argumento de COPY es el destino, y un COPY --from=… copia de otra etapa: no depende de los ficheros. Escribe por cada instrucción del nuevo Dockerfile «N instrucción → caché» o «N instrucción → RECONSTRUIR (motivo)», con motivo «instrucción cambiada», «cambió fichero» (el primer fichero modificado de la lista que la afecte) o «capa anterior reconstruida», y al final «Capas reutilizadas: X de N».

Código de partida (bash)
#!/bin/bashmapfile -t todobloque=0; v1=(); v2=(); cambiados=()for l in "${todo[@]}"; do    if [ "$l" = "---" ]; then ((bloque++)); continue; fi    [ -z "$l" ] && continue    case $bloque in 0) v1+=("$l") ;; 1) v2+=("$l") ;; 2) cambiados+=("$l") ;; esacdone # TODO: decide caché o reconstrucción para cada instrucción de v2for i in "${!v2[@]}"; do echo "$((i + 1)) ${v2[i]} → caché"; doneecho "Capas reutilizadas: ${#v2[@]} de ${#v2[@]}"

Ejemplo: Buen orden: solo cambia el código

Entrada

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
---
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
---
src/app.js

Salida esperada

1 FROM node:22-alpine → caché
2 WORKDIR /app → caché
3 COPY package*.json ./ → caché
4 RUN npm ci → caché
5 COPY . . → RECONSTRUIR (cambió src/app.js)
6 RUN npm run build → RECONSTRUIR (capa anterior reconstruida)
Capas reutilizadas: 4 de 6

Ejemplo: Mal orden: se reinstalan las dependencias

Entrada

FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
---
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
---
src/app.js

Salida esperada

1 FROM node:22-alpine → caché
2 WORKDIR /app → caché
3 COPY . . → RECONSTRUIR (cambió src/app.js)
4 RUN npm ci → RECONSTRUIR (capa anterior reconstruida)
5 RUN npm run build → RECONSTRUIR (capa anterior reconstruida)
Capas reutilizadas: 2 de 5

Ejemplo: Cambian las dependencias y una instrucción

Entrada

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
---
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
---
package-lock.json
README.md

Salida esperada

1 FROM node:22-alpine → caché
2 WORKDIR /app → caché
3 COPY package*.json ./ → RECONSTRUIR (cambió package-lock.json)
4 RUN npm ci --omit=dev → RECONSTRUIR (capa anterior reconstruida)
5 COPY . . → RECONSTRUIR (capa anterior reconstruida)
6 RUN npm run build → RECONSTRUIR (capa anterior reconstruida)
Capas reutilizadas: 2 de 6

34. Leer una zona DNS

Medio · FTP, DNS y Servicios de Directorio · apuntesdam.com/subject/despliegue/topic/servicios-red

La entrada es un fichero de zona de BIND con una directiva $ORIGIN. Muestra los registros A, AAAA, CNAME y MX, en su orden, con el nombre completo: «www.tienda.com. CNAME tienda.com.». «@» es el propio origen; un nombre que no termina en punto es relativo y se le añade el origen (también en el destino de un CNAME). En los MX muestra la prioridad y el servidor: «tienda.com. MX 10 correo.tienda.com.». Ignora comentarios (;), directivas y otros tipos. Termina con «Registros: N».

Código de partida (bash)
#!/bin/bash# TODO: procesa la zona con awkcat > /dev/null

Ejemplo: Zona de la tienda

Entrada

$ORIGIN tienda.com.
$TTL 3600
@       IN  SOA ns1.tienda.com. admin.tienda.com. (2025093001 7200 3600 1209600 3600)
@       IN  NS    ns1.tienda.com.
@       IN  A     203.0.113.10
www     IN  CNAME @
api     IN  A     203.0.113.20
api     IN  AAAA  2001:db8::20
@       IN  MX 10 correo.tienda.com.
correo  IN  A     203.0.113.30
; comentario: servidor antiguo
ftp.otro.org. IN A 198.51.100.1

Salida esperada

tienda.com. A 203.0.113.10
www.tienda.com. CNAME tienda.com.
api.tienda.com. A 203.0.113.20
api.tienda.com. AAAA 2001:db8::20
tienda.com. MX 10 correo.tienda.com.
correo.tienda.com. A 203.0.113.30
ftp.otro.org. A 198.51.100.1
Registros: 7

35. Revisar una zona DNS antes de publicarla

Medio · FTP, DNS y Servicios de Directorio · apuntesdam.com/subject/despliegue/topic/servicios-red

Un error en la zona puede dejar sin web o sin correo a toda una empresa. La entrada es un fichero de zona con $ORIGIN (ignora comentarios, líneas vacías y otras directivas; cada registro es «nombre IN tipo datos…»; @ es el origen y los nombres sin punto final son relativos). Comprueba, escribiendo los mensajes en este orden: 1) para cada nombre, por orden de aparición, «ERROR: CNAME en el ápice (nombre)» si el propio dominio tiene un CNAME, o «ERROR: nombre tiene un CNAME y otros registros» si un nombre con CNAME tiene además otros registros; 2) para cada MX, «ERROR: el MX de nombre apunta a destino, que es un CNAME» (el destino de un MX debe tener registros A o AAAA); 3) para cada CNAME, MX o NS cuyo destino no acabe en punto pero termine en el dominio (o sea el dominio), «AVISO: falta el punto final en «destino» (se leería destino.origen)». Termina con «Errores: N, avisos: M».

Código de partida (bash)
#!/bin/bash# TODO: comprueba la zona con awkcat > /dev/nullecho "Errores: 0, avisos: 0"

Ejemplo: Zona con fallos

Entrada

$ORIGIN tienda.es.
$TTL 3600
@ IN SOA ns1.tienda.es. admin.tienda.es. (2026093001 7200 3600 1209600 3600)
@ IN NS ns1.tienda.es.
@ IN A 203.0.113.10
www IN CNAME @
correo IN CNAME www
@ IN MX 10 correo
blog IN CNAME tienda.es
blog IN TXT "verificacion"
ns1 IN A 203.0.113.2

Salida esperada

ERROR: blog.tienda.es. tiene un CNAME y otros registros
ERROR: el MX de tienda.es. apunta a correo.tienda.es., que es un CNAME
AVISO: falta el punto final en «tienda.es» (se leería tienda.es.tienda.es.)
Errores: 2, avisos: 1

Ejemplo: Zona correcta

Entrada

$ORIGIN ejemplo.org.
; zona correcta
@ IN NS ns1.proveedor.net.
@ IN A 192.0.2.1
www IN CNAME @
@ IN MX 10 mail
mail IN A 192.0.2.25

Salida esperada

Errores: 0, avisos: 0

36. Un resolvedor DNS iterativo con caché

Difícil · FTP, DNS y Servicios de Directorio · apuntesdam.com/subject/despliegue/topic/servicios-red

Así trabaja el DNS de tu operadora. La entrada tiene líneas «servidor nombre tipo valor ttl» con lo que sabe cada servidor (el servidor raíz se llama raiz; los demás, por su nombre), una línea «---» y consultas «segundo nombre». Para resolver un nombre: si su registro A está en caché y no ha caducado (caduca en segundo + ttl), responde «(caché)» sin preguntar a nadie. Si no, empieza por el servidor de la delegación (NS) en caché y vigente más específica cuyo dominio contenga el nombre (o por raiz si no hay ninguna) y pregunta: si ese servidor tiene el A del nombre, lo guarda en caché y responde; si tiene un NS de un dominio que contiene el nombre, guarda esa delegación en caché y pasa a preguntar al servidor indicado (si hay varios, el del dominio más largo); si no, el nombre no existe. Escribe «segundo nombre: IP (N consultas)», «… (caché)» o «… no existe (N consultas)» y, al final, «Consultas totales: N».

Código de partida (bash)
#!/bin/bashdeclare -A registro          # "servidor|nombre|tipo" -> "valor ttl"declare -A delegaciones      # servidor -> lista de "dominio|servidor_hijo|ttl"while read -r srv nom tipo val ttl && [ "$srv" != "---" ]; do    if [ "$tipo" = "NS" ]; then delegaciones[$srv]+="$nom|$val|$ttl "; else registro["$srv|$nom|$tipo"]="$val $ttl"; fidone total=0while read -r t nom || [ -n "$t" ]; do    [ -z "$t" ] && continue    # TODO    echo "$t $nom: no existe (0 consultas)"doneecho "Consultas totales: $total"

Ejemplo: Caché de respuestas y delegaciones

Entrada

raiz es. NS a.nic.es. 172800
raiz org. NS a0.org. 172800
a.nic.es. tienda.es. NS ns1.tienda.es. 3600
a.nic.es. academia.es. NS ns.academia.es. 3600
ns1.tienda.es. www.tienda.es. A 203.0.113.10 300
ns1.tienda.es. api.tienda.es. A 203.0.113.20 60
ns.academia.es. academia.es. A 198.51.100.7 600
---
0 www.tienda.es.
10 api.tienda.es.
20 www.tienda.es.
100 api.tienda.es.
400 www.tienda.es.

Salida esperada

0 www.tienda.es.: 203.0.113.10 (3 consultas)
10 api.tienda.es.: 203.0.113.20 (1 consultas)
20 www.tienda.es.: 203.0.113.10 (caché)
100 api.tienda.es.: 203.0.113.20 (1 consultas)
400 www.tienda.es.: 203.0.113.10 (1 consultas)
Consultas totales: 6

Ejemplo: Otros dominios y nombres inexistentes

Entrada

raiz es. NS a.nic.es. 172800
raiz org. NS a0.org. 172800
a.nic.es. tienda.es. NS ns1.tienda.es. 3600
a.nic.es. academia.es. NS ns.academia.es. 3600
ns1.tienda.es. www.tienda.es. A 203.0.113.10 300
ns1.tienda.es. api.tienda.es. A 203.0.113.20 60
ns.academia.es. academia.es. A 198.51.100.7 600
---
500 academia.es.
700 ftp.tienda.es.
800 www.ejemplo.org.
900 academia.es.

Salida esperada

500 academia.es.: 198.51.100.7 (3 consultas)
700 ftp.tienda.es.: no existe (2 consultas)
800 www.ejemplo.org.: no existe (2 consultas)
900 academia.es.: 198.51.100.7 (caché)
Consultas totales: 7

37. Evaluar filtros de búsqueda LDAP

Muy difícil · FTP, DNS y Servicios de Directorio · apuntesdam.com/subject/despliegue/topic/servicios-red

Las aplicaciones buscan en el directorio de la empresa con filtros LDAP en notación prefija: (uid=ana), (&(objectClass=person)(ou=ventas)), (|(ou=ventas)(ou=soporte)), (!(uid=admin)), (mail=*) (el atributo existe) y comodines como (cn=Ana*) o (mail=*@tienda.es). La entrada tiene entradas del directorio («atributo: valor» por línea, separadas por líneas en blanco; un atributo puede repetirse, como objectClass o memberOf), una línea «---» y filtros, uno por línea. Para cada filtro escribe «filtro → uid1, uid2» con los uid de las entradas que lo cumplen, en el orden del directorio, o «filtro → (ninguna)». Los nombres de atributo y las comparaciones de valores no distinguen mayúsculas; un atributo con varios valores cumple la condición si la cumple alguno.

Código de partida (bash)
#!/bin/bash# TODO: lee las entradas y evalúa cada filtro con awkcat > /dev/null

Ejemplo: Filtros básicos

Entrada

dn: uid=ana,ou=personas,dc=tienda,dc=es
objectClass: person
objectClass: inetOrgPerson
uid: ana
cn: Ana García
ou: ventas
mail: ana@tienda.es
memberOf: cn=empleados,ou=grupos,dc=tienda,dc=es

dn: uid=luis,ou=personas,dc=tienda,dc=es
objectClass: person
uid: luis
cn: Luis Pérez
ou: soporte
mail: luis@gmail.com
memberOf: cn=empleados,ou=grupos,dc=tienda,dc=es
memberOf: cn=admins,ou=grupos,dc=tienda,dc=es

dn: uid=admin,dc=tienda,dc=es
objectClass: account
uid: admin

dn: uid=eva,ou=personas,dc=tienda,dc=es
objectClass: person
uid: eva
cn: Eva Ruiz
ou: Ventas
mail: eva@tienda.es
---
(uid=ana)
(ou=ventas)
(&(objectClass=person)(mail=*))
(!(objectClass=person))

Salida esperada

(uid=ana) → ana
(ou=ventas) → ana, eva
(&(objectClass=person)(mail=*)) → ana, luis, eva
(!(objectClass=person)) → admin

Ejemplo: Comodines y combinaciones

Entrada

dn: uid=ana,ou=personas,dc=tienda,dc=es
objectClass: person
objectClass: inetOrgPerson
uid: ana
cn: Ana García
ou: ventas
mail: ana@tienda.es
memberOf: cn=empleados,ou=grupos,dc=tienda,dc=es

dn: uid=luis,ou=personas,dc=tienda,dc=es
objectClass: person
uid: luis
cn: Luis Pérez
ou: soporte
mail: luis@gmail.com
memberOf: cn=empleados,ou=grupos,dc=tienda,dc=es
memberOf: cn=admins,ou=grupos,dc=tienda,dc=es

dn: uid=admin,dc=tienda,dc=es
objectClass: account
uid: admin

dn: uid=eva,ou=personas,dc=tienda,dc=es
objectClass: person
uid: eva
cn: Eva Ruiz
ou: Ventas
mail: eva@tienda.es
---
(mail=*@tienda.es)
(|(ou=soporte)(memberOf=cn=admins,ou=grupos,dc=tienda,dc=es))
(&(objectclass=PERSON)(!(ou=ventas)))
(cn=*ga*)
(telefono=*)

Salida esperada

(mail=*@tienda.es) → ana, eva
(|(ou=soporte)(memberOf=cn=admins,ou=grupos,dc=tienda,dc=es)) → luis
(&(objectclass=PERSON)(!(ou=ventas))) → luis
(cn=*ga*) → ana
(telefono=*) → (ninguna)

38. La siguiente versión semántica

Fácil · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

Cada línea es «versión tipo», donde la versión es MAYOR.MENOR.PARCHE y el tipo es major, minor o patch. Escribe la siguiente versión: major sube el primero y pone los otros a 0; minor sube el segundo y pone a 0 el parche; patch sube el último. Si la versión no tiene el formato correcto o el tipo no es válido, escribe «Versión no válida».

Código de partida (bash)
#!/bin/bashwhile read -r version tipo || [ -n "$version" ]; do    # TODO    :done

Ejemplo: Los tres tipos

Entrada

1.4.2 patch
1.4.2 minor
1.4.2 major

Salida esperada

1.4.3
1.5.0
2.0.0

Ejemplo: Entradas no válidas

Entrada

1.4 patch
v2.0.0 minor
2.0.0 hotfix

Salida esperada

Versión no válida
Versión no válida
Versión no válida

39. Una fusión a tres bandas

Medio · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

Así fusiona Git dos ramas: compara cada línea de la versión común (base) con la nuestra y la suya. La entrada son tres versiones del mismo fichero con el mismo número de líneas, separadas por «---»: base, nuestra y suya. Para cada línea: si nuestra y suya coinciden, se queda esa; si solo cambió una de las dos respecto a la base, gana la que cambió; si cambiaron las dos de forma distinta, hay conflicto. Las líneas en conflicto consecutivas forman un único bloque: «<<<<<<< nuestra», nuestras líneas, «=======», sus líneas, «>>>>>>> suya». Escribe el fichero fusionado y, al final, «Conflictos: N» (número de bloques).

Código de partida (bash)
#!/bin/bashmapfile -t todobase=(); nuestra=(); suya=(); parte=0for l in "${todo[@]}"; do    if [ "$l" = "---" ]; then ((parte++)); continue; fi    case $parte in 0) base+=("$l") ;; 1) nuestra+=("$l") ;; 2) suya+=("$l") ;; esacdone # TODO: fusiona línea a líneaprintf '%s\n' "${nuestra[@]}"echo "Conflictos: 0"

Ejemplo: Cambios compatibles

Entrada

# Tienda
version=1.0
puerto=8080
debug=true
---
# Tienda
version=1.1
puerto=8080
debug=true
---
# Tienda
version=1.0
puerto=8080
debug=false

Salida esperada

# Tienda
version=1.1
puerto=8080
debug=false
Conflictos: 0

Ejemplo: Un conflicto de dos líneas

Entrada

a=1
b=2
c=3
d=4
---
a=1
b=20
c=30
d=4
---
a=1
b=200
c=300
d=40

Salida esperada

a=1
<<<<<<< nuestra
b=20
c=30
=======
b=200
c=300
>>>>>>> suya
d=40
Conflictos: 1

40. Changelog a partir de los commits

Difícil · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

La entrada son mensajes de commit con el formato de Conventional Commits («tipo(ámbito)!: descripción», con ámbito y ! opcionales). Genera un changelog con las secciones «## Cambios incompatibles» (commits con !), «## Novedades» (feat) y «## Correcciones» (fix), en ese orden, cada una con líneas «- descripción» en el orden original y omitiendo las secciones vacías. Los demás tipos (docs, chore, test…) se ignoran salvo que lleven !. Termina con una línea en blanco y «Siguiente versión: major», «minor», «patch» o «ninguna».

Código de partida (bash)
#!/bin/bashrotos=(); novedades=(); correcciones=()while IFS= read -r linea || [ -n "$linea" ]; do    # TODO: clasifica el commit    :done # TODO: escribe el changelog

Ejemplo: Versión con novedades

Entrada

feat(api): añadir login con token
fix: corregir el total del carrito
docs: actualizar el README
feat(web): filtro por categoría
chore: subir dependencias

Salida esperada

## Novedades
- añadir login con token
- filtro por categoría
## Correcciones
- corregir el total del carrito

Siguiente versión: minor

Ejemplo: Cambio incompatible

Entrada

fix(api): validar el email
refactor(api)!: renombrar /users a /usuarios
test: pruebas del carrito

Salida esperada

## Cambios incompatibles
- renombrar /users a /usuarios
## Correcciones
- validar el email

Siguiente versión: major

Ejemplo: Solo mantenimiento

Entrada

docs: arreglar erratas
chore(ci): cachear dependencias

Salida esperada

Siguiente versión: ninguna

41. Las métricas DORA de un equipo

Difícil · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

Las cuatro métricas DORA miden lo bien que un equipo entrega software. La entrada es un registro con líneas «despliegue FECHA HORA commit FECHA HORA resultado» (resultado ok o fallo; la fecha del commit es cuándo se escribió el cambio desplegado) y «restaurado FECHA HORA» (el servicio vuelve a funcionar tras un fallo), en orden cronológico. Calcula: «Despliegues: N en D días (X por semana)», con D los días naturales entre el primer y el último despliegue, ambos incluidos, y X con un decimal; «Tiempo de entrega (mediana): H h M min», la mediana en minutos de (despliegue − commit), y con un número par de valores, la media entera de los dos centrales; «Tasa de fallos: P %» con un decimal; y «Tiempo de recuperación (media): M min» (sin decimales), la media de los minutos entre cada despliegue fallido y el primer «restaurado» posterior, o «—» si no hubo fallos.

Código de partida (bash)
#!/bin/bashexport LC_ALL=C TZ=UTC# TODOcat > /dev/null

Ejemplo: Una semana de trabajo

Entrada

despliegue 2026-09-21 10:00 commit 2026-09-20 16:30 ok
despliegue 2026-09-22 12:00 commit 2026-09-22 09:00 ok
despliegue 2026-09-23 17:00 commit 2026-09-23 15:15 fallo
restaurado 2026-09-23 17:40
despliegue 2026-09-24 11:00 commit 2026-09-23 18:00 ok
despliegue 2026-09-27 09:30 commit 2026-09-25 09:30 ok

Salida esperada

Despliegues: 5 en 7 días (5.0 por semana)
Tiempo de entrega (mediana): 17 h 0 min
Tasa de fallos: 20.0 %
Tiempo de recuperación (media): 40 min

Ejemplo: Sin fallos

Entrada

despliegue 2026-01-10 08:00 commit 2026-01-09 08:00 ok
despliegue 2026-01-10 20:00 commit 2026-01-10 19:00 ok

Salida esperada

Despliegues: 2 en 1 días (14.0 por semana)
Tiempo de entrega (mediana): 12 h 30 min
Tasa de fallos: 0.0 %
Tiempo de recuperación (media): —

42. ¿Qué jobs ejecuta el pipeline?

Muy difícil · Git, Documentación y CI/CD · apuntesdam.com/subject/despliegue/topic/ci-cd

Las plataformas de CI deciden qué trabajos (jobs) lanzar según sus dependencias, la rama y los ficheros cambiados. La entrada tiene una línea por job, en un orden en el que las dependencias aparecen antes: «nombre [needs=a,b] [ramas=main,release/*] [rutas=src/*,Dockerfile]», una línea «---» y el contexto: «rama=…», «cambios=f1,f2…» y «fallan=j1,j2…» (jobs que fallarán si se ejecutan). Para cada job, en orden y comprobando en este orden: si alguna dependencia terminó en FALLO o cancelado → «cancelado (por dep)» (la primera en su lista); si alguna se omitió → «omitido (depende de dep)»; si tiene ramas y ninguna coincide con la rama (con comodines * como los de la shell) → «omitido (rama)»; si tiene rutas y ningún fichero cambiado coincide con ninguna → «omitido (sin cambios)»; si no, se ejecuta: «FALLO» si está en fallan u «ok». Termina con «Resultado: fallido» si algún job falló o «Resultado: éxito».

Código de partida (bash)
#!/bin/bashjobs=(); declare -A ctxenjobs=1while IFS= read -r l || [ -n "$l" ]; do    [ -z "$l" ] && continue    if [ "$l" = "---" ]; then enjobs=0; continue; fi    if (( enjobs )); then jobs+=("$l"); else ctx[${l%%=*}]=${l#*=}; fidone # TODO: decide cada jobfor l in "${jobs[@]}"; do echo "${l%% *}: ok"; doneecho "Resultado: éxito"

Ejemplo: Una rama de funcionalidad

Entrada

lint
pruebas needs=lint
imagen needs=pruebas ramas=main,release/* rutas=src/*,Dockerfile,package.json
docs rutas=docs/*
desplegar_staging needs=imagen ramas=main
desplegar_prod needs=desplegar_staging ramas=release/*
---
rama=feat/filtros
cambios=src/app.js,README.md
fallan=

Salida esperada

lint: ok
pruebas: ok
imagen: omitido (rama)
docs: omitido (sin cambios)
desplegar_staging: omitido (depende de imagen)
desplegar_prod: omitido (depende de desplegar_staging)
Resultado: éxito

Ejemplo: Main con un test roto

Entrada

lint
pruebas needs=lint
imagen needs=pruebas ramas=main,release/* rutas=src/*,Dockerfile,package.json
docs rutas=docs/*
desplegar_staging needs=imagen ramas=main
desplegar_prod needs=desplegar_staging ramas=release/*
---
rama=main
cambios=src/app.js,docs/guia.md
fallan=pruebas

Salida esperada

lint: ok
pruebas: FALLO
imagen: cancelado (por pruebas)
docs: ok
desplegar_staging: cancelado (por imagen)
desplegar_prod: cancelado (por desplegar_staging)
Resultado: fallido

43. Conflictos de puertos en Docker Compose

Medio · Ejercicios de Despliegue de Aplicaciones Web · apuntesdam.com/subject/despliegue/topic/ejercicios-despliegue

Cada línea describe una publicación de puertos de un servicio: «servicio puerto_host:puerto_contenedor». Escribe «CONFLICTO puerto: servicio1, servicio2» por cada puerto del host usado por más de un servicio (en orden numérico de puerto y con los servicios en el orden en que aparecen), y «PRIVILEGIADO servicio: puerto» por cada puerto del host menor que 1024, en el orden de la entrada. Si no hay problemas, escribe «Sin problemas».

Código de partida (bash)
#!/bin/bashdeclare -A usosproblemas=0 # TODO

Ejemplo: Con conflictos

Entrada

web 8080:80
api 3000:3000
adminer 8080:8080
proxy 80:80
proxy 443:443
docs 3000:80

Salida esperada

CONFLICTO 3000: api, docs
CONFLICTO 8080: web, adminer
PRIVILEGIADO proxy: 80
PRIVILEGIADO proxy: 443

Ejemplo: Sin problemas

Entrada

web 8080:80
api 3000:3000

Salida esperada

Sin problemas

44. Informe de un pipeline de CI

Medio · Ejercicios de Despliegue de Aplicaciones Web · apuntesdam.com/subject/despliegue/topic/ejercicios-despliegue

Cada línea es un paso de un pipeline: «paso estado segundos», con estado ok, error o omitido. Escribe «Pasos: N (ok: X, error: Y, omitido: Z)», «Duración total: M min S s» (sumando solo los pasos no omitidos), «Paso más lento: nombre (N s)» entre los ejecutados y, si hay algún error, «Primer error: nombre»; si no, «Resultado: correcto».

Código de partida (bash)
#!/bin/bashwhile read -r paso estado segundos || [ -n "$paso" ]; do    # TODO    :done

Ejemplo: Pipeline con error

Entrada

checkout ok 3
instalar ok 48
lint ok 12
tests error 95
build omitido 0
desplegar omitido 0

Salida esperada

Pasos: 6 (ok: 3, error: 1, omitido: 2)
Duración total: 2 min 38 s
Paso más lento: tests (95 s)
Primer error: tests

Ejemplo: Pipeline correcto

Entrada

checkout ok 2
tests ok 61
build ok 130
desplegar ok 40

Salida esperada

Pasos: 4 (ok: 4, error: 0, omitido: 0)
Duración total: 3 min 53 s
Paso más lento: build (130 s)
Resultado: correcto

45. Rotación de copias de seguridad

Difícil · Ejercicios de Despliegue de Aplicaciones Web · apuntesdam.com/subject/despliegue/topic/ejercicios-despliegue

La primera línea es la fecha de hoy y las siguientes, nombres de copias «backup-AAAA-MM-DD.tar.gz» (desordenados). Aplica esta política: se conservan todas las copias de los últimos 7 días (hoy incluido, es decir, con 0 a 6 días de antigüedad); de las que tienen entre 7 y 30 días, solo las de domingo; y las de más de 30 días se borran. Escribe «CONSERVAR nombre» o «BORRAR nombre» ordenadas de la más reciente a la más antigua, y al final «Conservadas: N · Borradas: M». Los nombres que no siguen el formato se ignoran.

Código de partida (bash)
#!/bin/bashread -r hoy # TODO

Ejemplo: Un mes de copias

Entrada

2025-09-30
backup-2025-09-30.tar.gz
backup-2025-09-24.tar.gz
backup-2025-09-23.tar.gz
backup-2025-09-21.tar.gz
backup-2025-09-20.tar.gz
backup-2025-09-14.tar.gz
backup-2025-08-31.tar.gz
backup-2025-08-24.tar.gz
notas.txt
backup-2025-09-29.tar.gz

Salida esperada

CONSERVAR backup-2025-09-30.tar.gz
CONSERVAR backup-2025-09-29.tar.gz
CONSERVAR backup-2025-09-24.tar.gz
BORRAR backup-2025-09-23.tar.gz
CONSERVAR backup-2025-09-21.tar.gz
BORRAR backup-2025-09-20.tar.gz
CONSERVAR backup-2025-09-14.tar.gz
CONSERVAR backup-2025-08-31.tar.gz
BORRAR backup-2025-08-24.tar.gz
Conservadas: 6 · Borradas: 3

Ejercicios largos

46. Informe de un registro de accesos web

Difícil · 75 minutos · apuntesdam.com/ejercicios/bash/informe-registro-accesos-web

Los servidores web como Apache y Nginx anotan cada petición en un registro de accesos (access.log). Cada línea dice quién pidió qué, cuándo y cómo respondió el servidor, en un formato estándar llamado Common Log Format.

El administrador de una web pequeña quiere un resumen diario sin instalar herramientas de análisis: un script de Bash que lea el registro por la entrada estándar (./informe.sh < access.log) y escriba las cifras importantes.

Un registro real puede tener líneas cortadas (por ejemplo, si el disco se llenó a mitad de una escritura). El script no debe contarlas como peticiones, pero sí decir cuántas había.

Requisitos

  • Cada línea tiene la forma IP - - [fecha zona] "MÉTODO ruta PROTOCOLO" código bytes. Separada por espacios, la fecha ocupa dos campos y la petición tres; el código es el 9.º campo y los bytes el 10.º. Los bytes valen - cuando la respuesta no tiene cuerpo (por ejemplo, una redirección): cuentan como 0.

  • Las líneas vacías se ignoran. Una línea no es válida si no llega al campo de los bytes o si el código no es un número de tres cifras.

  • Si no hay ninguna petición válida, escribe solo No hay peticiones válidas.

  • Escribe Peticiones: N, Visitantes distintos: N (el número de IP distintas), Respuestas: 2xx A · 3xx B · 4xx C · 5xx D (cuántas respuestas hay de cada familia, según la primera cifra del código) y Datos enviados: N KB (la suma de los bytes dividida entre 1024, sin decimales).

  • Bajo IPs con más peticiones:, las 3 IP con más peticiones (o menos, si no hay tantas), de más a menos: el número en 3 caracteres alineado a la derecha, un espacio y la IP. A igual número, por orden de texto de la IP, carácter a carácter (por eso 10.0.0.10 va antes que 10.0.0.2).

  • Página más pedida: ruta (N veces), con «vez» si es 1. Si hay empate, la primera en orden de texto.

  • Errores 404: y debajo, con dos espacios delante, cada ruta distinta que haya respondido 404, en orden de texto. Si no hay ninguna, Errores 404: ninguno en una sola línea.

  • Si ha habido líneas no válidas, termina con Líneas no válidas: N.

Formato de la entrada

  • El registro de accesos por la entrada estándar, una petición por línea en Common Log Format.

Campos de una línea del registro
CampoEjemploSignificado
1192.168.1.10IP del cliente
2 y 3- -identidad y usuario (casi siempre vacíos)
4 y 5[03/Oct/2026:10:15:32 +0200]fecha, hora y zona horaria
6, 7 y 8"GET /index.html HTTP/1.1"método, ruta y protocolo
9200código de respuesta
105120bytes enviados (- si no hay cuerpo)

Ejemplo: Diez peticiones

Entrada

192.168.1.10 - - [03/Oct/2026:10:15:32 +0200] "GET /index.html HTTP/1.1" 200 5120
192.168.1.10 - - [03/Oct/2026:10:15:33 +0200] "GET /css/estilos.css HTTP/1.1" 200 2048
10.0.0.7 - - [03/Oct/2026:10:16:01 +0200] "GET /index.html HTTP/1.1" 200 5120
10.0.0.7 - - [03/Oct/2026:10:16:02 +0200] "GET /favicon.ico HTTP/1.1" 404 512
172.16.0.3 - - [03/Oct/2026:10:17:45 +0200] "POST /login HTTP/1.1" 302 -
172.16.0.3 - - [03/Oct/2026:10:17:46 +0200] "GET /panel HTTP/1.1" 200 8192
192.168.1.10 - - [03/Oct/2026:10:18:10 +0200] "GET /admin HTTP/1.1" 404 512
10.0.0.7 - - [03/Oct/2026:10:19:00 +0200] "GET /index.html HTTP/1.1" 200 5120
192.168.1.10 - - [03/Oct/2026:10:20:15 +0200] "GET /api/datos HTTP/1.1" 500 128
8.8.4.4 - - [03/Oct/2026:10:21:30 +0200] "GET /index.html HTTP/1.1" 200 5120

Salida esperada

Peticiones: 10
Visitantes distintos: 4
Respuestas: 2xx 6 · 3xx 1 · 4xx 2 · 5xx 1
Datos enviados: 31 KB
IPs con más peticiones:
  4 192.168.1.10
  3 10.0.0.7
  2 172.16.0.3
Página más pedida: /index.html (4 veces)
Errores 404:
  /admin
  /favicon.ico

Ejemplo: Empates y líneas rotas

Entrada

10.0.0.2 - - [04/Oct/2026:09:00:00 +0200] "GET /b HTTP/1.1" 200 1024
10.0.0.10 - - [04/Oct/2026:09:00:01 +0200] "GET /a HTTP/1.1" 200 1024
línea rota
10.0.0.2 - - [04/Oct/2026:09:00:02 +0200] "GET /a HTTP/1.1" 301 -
10.0.0.10 - - [04/Oct/2026:09:00:03 +0200] "GET /b HTTP/1.1" 200 2048

10.0.0.3 - - [04/Oct/2026:09:00:04 +0200] "GET /c HTTP/1.1" 200 1024

Salida esperada

Peticiones: 5
Visitantes distintos: 3
Respuestas: 2xx 4 · 3xx 1 · 4xx 0 · 5xx 0
Datos enviados: 5 KB
IPs con más peticiones:
  2 10.0.0.10
  2 10.0.0.2
  1 10.0.0.3
Página más pedida: /a (2 veces)
Errores 404: ninguno
Líneas no válidas: 1
Código de partida (bash)
#!/bin/bash# Orden de texto byte a byte: así sort y [[ < ]] ordenan igual en cualquier máquinaexport LC_ALL=C declare -A por_ip por_ruta rutas_404total=0bytes_total=0invalidas=0c2=0 c3=0 c4=0 c5=0 while read -r ip _ _ _ _ metodo ruta _ codigo bytes _; do  [ -z "$ip" ] && continue  # TODO: valida la línea y actualiza los contadoresdone # TODO: escribe el informe

47. Alta masiva de usuarios a partir de un CSV

Difícil · 70 minutos · apuntesdam.com/ejercicios/bash/alta-masiva-de-usuarios

Al empezar el curso, el administrador del instituto tiene que crear las cuentas de los alumnos nuevos en el servidor Linux. Secretaría le pasa un CSV con nombre, apellidos y grupo, y hacerlo a mano con useradd uno por uno no es una opción.

El login sigue la norma del centro: la inicial del nombre, el primer apellido y la inicial del segundo, en minúsculas y sin tildes ni eñes (Lucía Martín Ruiz → lmartinr). Si ya existe, se le añade un número (lmartinr2, lmartinr3…). Hay que tener en cuenta tanto las cuentas que ya hay en el sistema como las que se van creando en el propio fichero.

Un script que crea usuarios es peligroso de probar, así que primero se ejecuta en modo simulación: en lugar de crear nada, escribe las órdenes que ejecutaría. Así se pueden revisar antes de lanzarlas de verdad.

Requisitos

  • La primera línea es GRUPOS grupo1,grupo2,… (si no, escribe La primera línea debe ser GRUPOS grupo1,grupo2… y termina). La segunda es EXISTEN cuenta1,cuenta2,… con las cuentas que ya hay en el sistema, que puede estar vacía tras la palabra (si no es esa línea, Falta la línea EXISTEN y termina).

  • Cada línea siguiente es nombre;apellido1;apellido2;grupo (el segundo apellido puede estar vacío). Las líneas vacías se ignoran. Si falta el nombre, el primer apellido o el grupo, o hay más campos, escribe # Línea N: faltan o sobran campos; si el grupo no está en la lista, # Línea N: grupo desconocido G (N es el número de línea de la entrada).

  • Login: inicial del nombre + primer apellido + inicial del segundo apellido, sin tildes (á é í ó ú ü), con la ñ como n, en minúsculas y quitando todo lo que no sea una letra de la a a la z. Si coincide con una cuenta existente o ya creada, se prueba añadiendo 2, 3… hasta encontrar uno libre.

  • Por cada alta escribe useradd -m -s /bin/bash -g grupo -c "Nombre Apellido1 Apellido2" login (el comentario con el nombre completo tal como viene, sin segundo apellido si está vacío) y chage -d 0 login (para obligar a cambiar la contraseña en el primer inicio de sesión).

  • Al final, # Altas: N (grupo1 n1, grupo2 n2, …) con todos los grupos en el orden de la línea GRUPOS.

Formato de la entrada

  • Línea 1: GRUPOS lista. Línea 2: EXISTEN lista. Resto: nombre;apellido1;apellido2;grupo.

Ejemplo: Alumnos y profesores nuevos

Entrada

GRUPOS alumnos,profesores,administracion
EXISTEN root,ana,lmartinr
Lucía;Martín;Ruiz;alumnos
Luis;Martín;Rodríguez;alumnos
Íñigo;Núñez;Ibáñez;profesores
María José;O'Donnell;;alumnos
Pedro;Gil;López;conserjes
Sofía;Ruiz

Salida esperada

useradd -m -s /bin/bash -g alumnos -c "Lucía Martín Ruiz" lmartinr2
chage -d 0 lmartinr2
useradd -m -s /bin/bash -g alumnos -c "Luis Martín Rodríguez" lmartinr3
chage -d 0 lmartinr3
useradd -m -s /bin/bash -g profesores -c "Íñigo Núñez Ibáñez" inunezi
chage -d 0 inunezi
useradd -m -s /bin/bash -g alumnos -c "María José O'Donnell" modonnell
chage -d 0 modonnell
# Línea 7: grupo desconocido conserjes
# Línea 8: faltan o sobran campos
# Altas: 4 (alumnos 3, profesores 1, administracion 0)

Ejemplo: Logins repetidos dentro del fichero

Entrada

GRUPOS aula1
EXISTEN
Ana;García;Pérez;aula1
Antonio;García;Pons;aula1
Alba;García;Puig;aula1

Ana;García;Pérez;aula1

Salida esperada

useradd -m -s /bin/bash -g aula1 -c "Ana García Pérez" agarciap
chage -d 0 agarciap
useradd -m -s /bin/bash -g aula1 -c "Antonio García Pons" agarciap2
chage -d 0 agarciap2
useradd -m -s /bin/bash -g aula1 -c "Alba García Puig" agarciap3
chage -d 0 agarciap3
useradd -m -s /bin/bash -g aula1 -c "Ana García Pérez" agarciap4
chage -d 0 agarciap4
# Altas: 4 (aula1 4)
Código de partida (bash)
#!/bin/bash# Genera las órdenes para dar de alta usuarios a partir de un CSV, sin ejecutarlas (modo simulación)export LC_ALL=C # «Lucía Martín Ruiz» → «lmartinr»: sin tildes ni eñes, en minúsculas y solo letrasnormalizar() {  local s=$1  # TODO  echo "$s"} read -r palabra lista_grupos# TODO: comprueba la línea GRUPOS, lee la línea EXISTEN y procesa cada usuario

48. Rotación de copias de seguridad abuelo-padre-hijo

Difícil · 70 minutos · apuntesdam.com/ejercicios/bash/rotacion-de-copias-abuelo-padre-hijo

Un servidor hace cada noche una copia completa en un fichero backup-AAAA-MM-DD.tar.gz. Si no se borra ninguna, el disco se llena; si se guardan solo las últimas, no hay forma de volver a cómo estaba todo hace dos meses. La solución clásica es la rotación abuelo-padre-hijo (GFS): copias diarias (hijos) para la última semana, semanales (padres) para el último mes y mensuales (abuelos) para el último medio año.

En este servidor la política es: conservar las 7 copias más recientes; además, las hechas en domingo de los últimos 28 días; y además, las hechas el día 1 de mes de los últimos 6 meses (contando el actual). Una copia puede cumplir varias reglas a la vez.

El script recibe la fecha de hoy y el listado de ficheros (lo que daría ls) y escribe qué se conserva y por qué, y qué se borra. Así se puede revisar antes de borrar nada.

Requisitos

  • La primera línea es HOY AAAA-MM-DD con una fecha que exista (si no, La primera línea debe ser HOY AAAA-MM-DD y termina). Cada línea siguiente es el nombre de un fichero; las vacías se ignoran.

  • Un nombre que no sea exactamente backup-AAAA-MM-DD.tar.gz con una fecha que exista escribe Ignorado (nombre no válido): nombre; una copia con fecha posterior a hoy, Ignorado (fecha futura): nombre. Los avisos salen en el orden de la entrada. Si no queda ninguna copia válida, No hay copias.

  • Las copias válidas se tratan de la más reciente a la más antigua. Motivos para conservar una copia: diaria si está entre las 7 más recientes; semanal si es de un domingo y han pasado menos de 28 días desde ella hasta hoy; mensual si es del día 1 y es del mes de hoy o de uno de los 5 anteriores.

  • Para cada copia escribe CONSERVAR nombre (motivos) con los motivos separados por coma y espacio en el orden diaria, semanal, mensual, o BORRAR nombre (BORRAR seguido de cuatro espacios, para que los nombres queden alineados).

  • Al final, Se conservan C y se borran B.

Formato de la entrada

  • Línea 1: HOY AAAA-MM-DD. Resto: un nombre de fichero por línea.

Ejemplo: Seis meses de copias

Entrada

HOY 2026-10-04
backup-2026-10-03.tar.gz
backup-2026-10-02.tar.gz
backup-2026-10-01.tar.gz
backup-2026-09-30.tar.gz
backup-2026-09-29.tar.gz
backup-2026-09-28.tar.gz
backup-2026-09-27.tar.gz
backup-2026-09-26.tar.gz
backup-2026-09-25.tar.gz
backup-2026-09-20.tar.gz
backup-2026-09-13.tar.gz
backup-2026-09-06.tar.gz
backup-2026-09-01.tar.gz
backup-2026-08-30.tar.gz
backup-2026-08-01.tar.gz
backup-2026-07-01.tar.gz
backup-2026-06-01.tar.gz
backup-2026-05-01.tar.gz
backup-2026-04-01.tar.gz

Salida esperada

CONSERVAR backup-2026-10-03.tar.gz (diaria)
CONSERVAR backup-2026-10-02.tar.gz (diaria)
CONSERVAR backup-2026-10-01.tar.gz (diaria, mensual)
CONSERVAR backup-2026-09-30.tar.gz (diaria)
CONSERVAR backup-2026-09-29.tar.gz (diaria)
CONSERVAR backup-2026-09-28.tar.gz (diaria)
CONSERVAR backup-2026-09-27.tar.gz (diaria, semanal)
BORRAR    backup-2026-09-26.tar.gz
BORRAR    backup-2026-09-25.tar.gz
CONSERVAR backup-2026-09-20.tar.gz (semanal)
CONSERVAR backup-2026-09-13.tar.gz (semanal)
BORRAR    backup-2026-09-06.tar.gz
CONSERVAR backup-2026-09-01.tar.gz (mensual)
BORRAR    backup-2026-08-30.tar.gz
CONSERVAR backup-2026-08-01.tar.gz (mensual)
CONSERVAR backup-2026-07-01.tar.gz (mensual)
CONSERVAR backup-2026-06-01.tar.gz (mensual)
CONSERVAR backup-2026-05-01.tar.gz (mensual)
BORRAR    backup-2026-04-01.tar.gz
Se conservan 14 y se borran 5

Ejemplo: Nombres no válidos y fechas futuras

Entrada

HOY 2026-03-01
backup-2026-02-29.tar.gz
backup-2026-02-28.tar.gz
copia-2026-02-27.tar.gz
backup-2026-03-02.tar.gz
backup-2026-03-01.tar.gz
backup-2025-11-01.tar.gz
backup-2025-09-01.tar.gz

Salida esperada

Ignorado (nombre no válido): backup-2026-02-29.tar.gz
Ignorado (nombre no válido): copia-2026-02-27.tar.gz
Ignorado (fecha futura): backup-2026-03-02.tar.gz
CONSERVAR backup-2026-03-01.tar.gz (diaria, semanal, mensual)
CONSERVAR backup-2026-02-28.tar.gz (diaria)
CONSERVAR backup-2025-11-01.tar.gz (diaria, mensual)
CONSERVAR backup-2025-09-01.tar.gz (diaria)
Se conservan 4 y se borran 0
Código de partida (bash)
#!/bin/bash# Política abuelo-padre-hijo: 7 diarias, domingos de las últimas 4 semanas y el día 1 de los últimos 6 mesesexport LC_ALL=C read -r palabra hoy# TODO: valida la fecha de hoy, lee los nombres de las copias y decide cuáles se conservan

49. Monitor de espacio en disco con umbrales

Medio · 50 minutos · apuntesdam.com/ejercicios/bash/monitor-de-espacio-en-disco

Un disco lleno tumba bases de datos, impide escribir registros y deja servicios caídos. Por eso los administradores programan con cron un script que revisa el espacio cada pocos minutos y avisa antes de que sea tarde.

La información la da df -P (el formato POSIX, pensado para que lo lean los scripts): una línea por sistema de ficheros con el dispositivo, los bloques de 1 KiB, los usados, los disponibles, el porcentaje y el punto de montaje. Muchas líneas son sistemas de ficheros virtuales (tmpfs, devtmpfs, overlay de Docker, squashfs y /dev/loop de los snaps) que no ocupan disco y no hay que vigilar.

Bash solo sabe hacer cuentas con enteros, así que los GiB con un decimal y los redondeos hay que construirlos a mano: es un buen ejercicio de aritmética entera.

Requisitos

  • La primera línea es UMBRALES aviso crítico con dos enteros, aviso menor que crítico y crítico como mucho 100 (si no, escribe La primera línea debe ser UMBRALES aviso crítico (aviso < crítico <= 100) y termina). La segunda es la cabecera de df, que se ignora; después, las líneas de df -P.

  • Se descartan los dispositivos tmpfs, devtmpfs, overlay, squashfs, udev, none y los que empiezan por /dev/loop. Si no queda ninguno, No hay sistemas de ficheros que vigilar.

  • Estado según el porcentaje de la columna Capacity: URGENTE si es mayor o igual que el umbral crítico, AVISO si es mayor o igual que el de aviso, y OK si no.

  • Una línea por sistema de ficheros, del porcentaje mayor al menor (a igualdad, por punto de montaje): el estado en 8 caracteres a la izquierda, un espacio, el porcentaje en 3 caracteres a la derecha seguido de %, dos espacios, el punto de montaje en 12 caracteres a la izquierda, un espacio y usado U de T (usado y usado + disponible). Si el estado no es OK, se añade · libera N GiB para bajar del A %: los GiB enteros (redondeando hacia arriba) que hay que borrar para que el uso baje hasta el umbral de aviso.

  • Los tamaños se escriben en GiB (1 GiB = 1.048.576 KiB) con un decimal redondeado y coma: 49,7 GiB. Al final, Libre en total: X GiB con la suma de lo disponible en los sistemas vigilados.

Formato de la entrada

  • Línea 1: UMBRALES aviso crítico. Después, la salida completa de df -P (con su cabecera).

Ejemplo: Un servidor con dos discos llenándose

Entrada

UMBRALES 80 90
Filesystem     1024-blocks      Used Available Capacity Mounted on
udev               4006228         0   4006228       0% /dev
tmpfs               804792      1764    803028       1% /run
/dev/sda2         61255492  52113408   5998944      90% /
tmpfs              4023956         0   4023956       0% /dev/shm
/dev/sda1           523248      6220    517028       2% /boot/efi
/dev/sdb1        976284628 801228314 175056314      83% /datos
/dev/loop0           65536     65536         0     100% /snap/core20/2318
overlay           61255492  52113408   5998944      90% /var/lib/docker/overlay2/x/merged

Salida esperada

URGENTE   90%  /            usado 49,7 GiB de 55,4 GiB · libera 6 GiB para bajar del 80 %
AVISO     83%  /datos       usado 764,1 GiB de 931,1 GiB · libera 20 GiB para bajar del 80 %
OK         2%  /boot/efi    usado 0,0 GiB de 0,5 GiB
Libre en total: 173,2 GiB

Ejemplo: Umbrales no válidos

Entrada

UMBRALES 95 90
Filesystem 1024-blocks Used Available Capacity Mounted on

Salida esperada

La primera línea debe ser UMBRALES aviso crítico (aviso < crítico <= 100)
Código de partida (bash)
#!/bin/bash# Lee la salida de «df -P» y avisa de los sistemas de ficheros que se llenanexport LC_ALL=C read -r palabra aviso critico# TODO: valida los umbrales, salta la cabecera y analiza cada línea de df

50. Analizador de la configuración de Nginx

Muy difícil · 90 minutos · apuntesdam.com/ejercicios/bash/analizador-de-configuracion-nginx

En un servidor con varias webs, la configuración de Nginx crece con el tiempo: un server para el dominio, otro para la API, otro que nadie recuerda… y los errores aparecen: un sitio que escucha en el 443 sin certificado (Nginx no arranca), un autoindex on olvidado que enseña los ficheros a cualquiera, o un dominio con HTTPS cuyo puerto 80 sigue sirviendo la web sin cifrar.

nginx -t comprueba la sintaxis, pero no esas buenas prácticas. Vas a escribir un script que lea un fichero de sitios (el que estaría en /etc/nginx/sites-available/), resuma cada bloque server y avise de los problemas.

El formato de Nginx es sencillo de leer línea a línea: directivas terminadas en ; y bloques entre llaves (server { … }, y dentro bloques location … { … }). Para saber dónde termina cada server hay que llevar la cuenta de las llaves abiertas.

Requisitos

  • Se ignoran los comentarios (desde # hasta el final de la línea), los espacios de los extremos y las líneas vacías. Cada línea es server {, otra apertura de bloque (termina en {), un cierre } o una directiva terminada en ;. Las líneas se numeran desde 1 contando todas.

  • Errores, que detienen el script: Error: llave de cierre sin abrir en la línea N, Error: bloque fuera de un server en la línea N, Error: directiva fuera de un server en la línea N y, al final, Error: llaves sin cerrar. Si no hay ningún server, No hay ningún bloque server.

  • De cada server se guardan (también desde dentro de sus location): los puertos de listen (el número tras los dos puntos, si los hay, y ssl si la línea lo lleva; sin repetir; 80 si no hay ningún listen), los nombres de server_name, el primer root, el primer proxy_pass, la URL del último return (lo que va tras el código), si tiene ssl_certificate, si tiene ssl_certificate_key y si tiene autoindex on.

  • Por cada servidor: Servidor N (líneas A-B): puerto P · nombres · acción, con los puertos separados por y (443 ssl y 80), los nombres tal cual o (sin nombre), y como acción redirige a URL, proxy a URL, sirve RUTA o no sirve nada, por ese orden de preferencia.

  • Avisos, primero los de cada servidor en orden (Servidor N: usa ssl pero le falta ssl_certificate y ssl_certificate_key —o solo el que falte—, Servidor N: autoindex activado (muestra la lista de ficheros), Servidor N: no tiene root, return ni proxy_pass) y después, por orden alfabético, dominio: aparece en K servidores del puerto P (para cada dominio y puerto con más de un servidor) y dominio: el puerto 80 no redirige a HTTPS (para cada dominio que se sirve en el 443 sin un servidor en el 80 cuyo return vaya a https://).

  • Escribe Avisos: y cada aviso con - delante, o Sin avisos.

Formato de la entrada

  • Un fichero de configuración de sitios de Nginx.

Ejemplo: Un dominio con API

Entrada

# Sitio principal
server {
    listen 80;
    listen [::]:80;
    server_name ejemplo.com www.ejemplo.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name ejemplo.com www.ejemplo.com;
    ssl_certificate /etc/letsencrypt/live/ejemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem;
    root /var/www/ejemplo;

    location /descargas/ {
        autoindex on;   # se dejó activado tras una prueba
    }
}

server {
    listen 443 ssl;
    server_name api.ejemplo.com;
    ssl_certificate /etc/ssl/api.crt;
    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

Salida esperada

Servidor 1 (líneas 2-7): puerto 80 · ejemplo.com www.ejemplo.com · redirige a https://$host$request_uri
Servidor 2 (líneas 9-20): puerto 443 ssl · ejemplo.com www.ejemplo.com · sirve /var/www/ejemplo
Servidor 3 (líneas 22-29): puerto 443 ssl · api.ejemplo.com · proxy a http://127.0.0.1:3000
Avisos:
  - Servidor 2: autoindex activado (muestra la lista de ficheros)
  - Servidor 3: usa ssl pero le falta ssl_certificate_key
  - api.ejemplo.com: el puerto 80 no redirige a HTTPS

Ejemplo: Servidores repetidos y uno vacío

Entrada

server {
    server_name intranet.local;
}
server {
    listen 8080;
    server_name intranet.local;
    root /srv/intranet;
}
server {
    listen 8080;
    server_name intranet.local;
    return 302 /mantenimiento.html;
}

Salida esperada

Servidor 1 (líneas 1-3): puerto 80 · intranet.local · no sirve nada
Servidor 2 (líneas 4-8): puerto 8080 · intranet.local · sirve /srv/intranet
Servidor 3 (líneas 9-13): puerto 8080 · intranet.local · redirige a /mantenimiento.html
Avisos:
  - Servidor 1: no tiene root, return ni proxy_pass
  - intranet.local: aparece en 2 servidores del puerto 8080
Código de partida (bash)
#!/bin/bash# Revisa un fichero de sitios de Nginx (como /etc/nginx/sites-available/web) y avisa de los fallos típicosexport LC_ALL=C n=0 profundidad=0 numero=0declare -a inicio fin puertos nombres raiz retorno proxy cert clave autoindex while IFS= read -r linea || [ -n "$linea" ]; do  numero=$((numero + 1))  # TODO: quita comentarios y espacios, sigue las llaves y guarda las directivas de cada serverdone # TODO: resumen de cada servidor y avisos

51. Despliegue por releases con enlace simbólico y vuelta atrás

Difícil · 70 minutos · apuntesdam.com/ejercicios/bash/despliegue-por-releases-con-rollback

Copiar los ficheros nuevos encima de los de la web en producción tiene un problema: durante unos segundos conviven ficheros viejos y nuevos, y si algo sale mal no hay forma rápida de volver atrás. Las herramientas de despliegue (Capistrano, Deployer, Envoyer) lo resuelven con releases: cada versión se instala en su propia carpeta (releases/20261004-1000) y el servidor web apunta a un enlace simbólico current.

Desplegar es preparar la carpeta nueva por completo y, solo al final, cambiar el enlace. Ese cambio se hace de forma atómica (se crea un enlace temporal y se renombra encima del antiguo con mv -T), así que ninguna petición ve una web a medias. Volver atrás es cambiar el enlace a la release anterior: un segundo, sin reinstalar nada.

El script recibe las releases que ya hay y una serie de órdenes, y escribe las órdenes de shell que ejecutaría. Las líneas informativas empiezan por #, así la salida se puede revisar y, después, ejecutar tal cual.

Requisitos

  • La primera línea es RELEASES seguida de las carpetas que ya existen, de la más antigua a la más reciente (puede no haber ninguna); la más reciente es la actual. Si no empieza por RELEASES, escribe La primera línea debe ser RELEASES r1 r2… (puede estar vacía) y termina. Las órdenes pueden ir en minúsculas; las líneas vacías se ignoran; una orden desconocida escribe # Orden desconocida: orden.

  • DESPLIEGA AAAAMMDD-HHMM vX.Y.Z: si el formato no es ese, # Uso: DESPLIEGA AAAAMMDD-HHMM vX.Y.Z; si la release ya existe, # La release R ya existe; si es anterior a la más reciente, # La release R es anterior a ÚLTIMA. Si todo va bien, escribe # Desplegando vX.Y.Z en R y las cinco órdenes de la tabla, la release pasa a ser la actual y, si hay más de 3, se escribe rm -rf /var/www/tienda/releases/R para borrar las más antiguas hasta dejar 3.

  • ROLLBACK: si no hay actual o es la más antigua, # No hay una release anterior a la que volver; si no, # Volviendo de ACTUAL a ANTERIOR, la orden del enlace y la recarga de PHP-FPM, y la anterior pasa a ser la actual (se puede volver atrás varias veces seguidas).

  • ESTADO: # Releases (N): y una línea por release de la más antigua a la más reciente, # R y, en la actual, # R ← current; o # Sin releases.

  • Un despliegue después de un rollback despliega la versión nueva como más reciente (la release a la que se había vuelto deja de ser la actual).

Formato de la entrada

  • Línea 1: RELEASES y las releases existentes. Resto: DESPLIEGA fecha vX.Y.Z, ROLLBACK o ESTADO.

Órdenes de un despliegue (R es la release, base /var/www/tienda)
PasoOrden
Descargar la versióngit clone --depth 1 --branch vX.Y.Z https://git.ejemplo.com/tienda.git /var/www/tienda/releases/R
Configuración compartidaln -s /var/www/tienda/shared/.env /var/www/tienda/releases/R/.env
Dependenciascomposer install --no-dev --optimize-autoloader --working-dir=/var/www/tienda/releases/R
Cambio atómicoln -sfn /var/www/tienda/releases/R /var/www/tienda/current.tmp && mv -T /var/www/tienda/current.tmp /var/www/tienda/current
Recargar PHPsystemctl reload php8.3-fpm

Ejemplo: Dos despliegues y una vuelta atrás

Entrada

RELEASES 20260901-1200 20260915-0930
ESTADO
DESPLIEGA 20261001-1800 v1.4.0
DESPLIEGA 20261004-1000 v1.5.0
ROLLBACK
ESTADO

Salida esperada

# Releases (2):
#   20260901-1200
#   20260915-0930  ← current
# Desplegando v1.4.0 en 20261001-1800
git clone --depth 1 --branch v1.4.0 https://git.ejemplo.com/tienda.git /var/www/tienda/releases/20261001-1800
ln -s /var/www/tienda/shared/.env /var/www/tienda/releases/20261001-1800/.env
composer install --no-dev --optimize-autoloader --working-dir=/var/www/tienda/releases/20261001-1800
ln -sfn /var/www/tienda/releases/20261001-1800 /var/www/tienda/current.tmp && mv -T /var/www/tienda/current.tmp /var/www/tienda/current
systemctl reload php8.3-fpm
# Desplegando v1.5.0 en 20261004-1000
git clone --depth 1 --branch v1.5.0 https://git.ejemplo.com/tienda.git /var/www/tienda/releases/20261004-1000
ln -s /var/www/tienda/shared/.env /var/www/tienda/releases/20261004-1000/.env
composer install --no-dev --optimize-autoloader --working-dir=/var/www/tienda/releases/20261004-1000
ln -sfn /var/www/tienda/releases/20261004-1000 /var/www/tienda/current.tmp && mv -T /var/www/tienda/current.tmp /var/www/tienda/current
systemctl reload php8.3-fpm
rm -rf /var/www/tienda/releases/20260901-1200
# Volviendo de 20261004-1000 a 20261001-1800
ln -sfn /var/www/tienda/releases/20261001-1800 /var/www/tienda/current.tmp && mv -T /var/www/tienda/current.tmp /var/www/tienda/current
systemctl reload php8.3-fpm
# Releases (3):
#   20260915-0930
#   20261001-1800  ← current
#   20261004-1000

Ejemplo: Órdenes que no valen

Entrada

RELEASES 20261001-1800
DESPLIEGA 2026-10-04 v1.5
DESPLIEGA 20261001-1800 v1.5.0
DESPLIEGA 20260930-0800 v1.5.0
ROLLBACK
PUBLICA

Salida esperada

# Uso: DESPLIEGA AAAAMMDD-HHMM vX.Y.Z
# La release 20261001-1800 ya existe
# La release 20260930-0800 es anterior a 20261001-1800
# No hay una release anterior a la que volver
# Orden desconocida: PUBLICA
Código de partida (bash)
#!/bin/bash# Despliegue por releases (como Capistrano o Deployer): cada versión en su carpeta y un enlace «current»# que se cambia de golpe. Modo simulación: escribe las órdenes en lugar de ejecutarlas.export LC_ALL=CBASE=/var/www/tiendaMAX_RELEASES=3 releases=()          # carpetas de releases, de la más antigua a la más recienteactual="" read -r palabra resto# TODO: lee las releases existentes y atiende DESPLIEGA, ROLLBACK y ESTADO