Durante todo el módulo has trabajado con una suposición implícita: que cada nombre corresponde a un archivo y cada archivo tiene un nombre. Esa suposición es falsa, y entender por qué es lo que separa a quien usa Linux de quien lo comprende.

En un sistema de archivos Unix, el nombre y el contenido son cosas separadas. El contenido vive en una estructura llamada inodo; el nombre es simplemente una entrada en un directorio que apunta a ese inodo. Nada impide que dos entradas apunten al mismo sitio, ni que exista un archivo cuyo contenido sea la ruta de otro. De ahí salen los dos tipos de enlaces de esta lección.

No es teoría de sistemas de archivos por curiosidad. Los enlaces están por todas partes en un sistema Linux: /bin es un enlace, tu python3 es un enlace, y el sistema con el que Debian y Ubuntu deciden qué versión de una herramienta se usa está construido sobre enlaces. Y al final de la lección diseñarás el patrón con el que Tramontana Reservas convertirá un despliegue —y una vuelta atrás— en un cambio de enlace instantáneo, sin ventana de servicio caído.

Contenido

  1. Qué es un inodo
  2. Ver el inodo y el contador de enlaces
  3. Enlaces duros con ln
  4. Las dos limitaciones de los enlaces duros
  5. Enlaces simbólicos con ln -s
  6. Absolutos frente a relativos
  7. Enlaces rotos
  8. Qué comandos siguen el enlace y cuáles no
  9. Duro frente a simbólico: la tabla
  10. Enlaces en un sistema Linux real
  11. update-alternatives
  12. El patrón de despliegue de Tramontana

  1. Qué es un inodo

Cuando guardas un archivo, el sistema de archivos crea tres cosas distintas:

  1. Los bloques de datos: el contenido en sí, repartido por el disco.
  2. El inodo: una estructura con todos los metadatos del archivo —permisos, propietario, grupo, tamaño, marcas de tiempo, contador de enlaces y las direcciones de los bloques de datos—. Cada inodo tiene un número único dentro de su sistema de archivos.
  3. La entrada de directorio: un par (nombre, número de inodo) almacenado en el directorio.

Fíjate en lo que no contiene el inodo: el nombre del archivo. El nombre no es una propiedad del archivo; es una propiedad del directorio que lo contiene.

flowchart LR
    subgraph DIR["Directorio /home/operador/datos"]
        E1["'casas.txt' -> 262149"]
        E2["'reservas.csv' -> 262151"]
    end
    subgraph INO["Tabla de inodos"]
        I1["Inodo 262149<br/>permisos, dueño, fechas<br/>enlaces: 1<br/>punteros a bloques"]
        I2["Inodo 262151<br/>permisos, dueño, fechas<br/>enlaces: 1<br/>punteros a bloques"]
    end
    subgraph BLQ["Bloques de datos"]
        B1["mas-figueres;Mas Figueres..."]
        B2["id;fecha;casa;huesped..."]
    end
    E1 --> I1 --> B1
    E2 --> I2 --> B2

Un directorio, visto así, no contiene archivos: contiene una lista de nombres con números de inodo. Es un índice.

Esta arquitectura explica de golpe varias cosas que hasta ahora eran hechos sueltos:

Hecho Explicación por el inodo
mv dentro del mismo disco es instantáneo Solo cambia la entrada de directorio; el inodo y los bloques no se tocan
. tiene el mismo número que su directorio Son dos nombres para el mismo inodo (lo comprobaste en 02-03)
El nombre no está en stat Porque no está en el inodo
Puedes borrar un archivo que otro proceso está leyendo Se quita el nombre, pero el inodo sobrevive mientras alguien lo tenga abierto
df -i puede agotarse con espacio libre Los inodos son un recurso finito, fijado al formatear

Ese penúltimo punto es la explicación definitiva del misterio de la lección 02-03: borras un log de 2 GB, du deja de verlo, pero df sigue diciendo que el espacio está ocupado. El nombre desapareció; el inodo sigue vivo porque el proceso lo tiene abierto, y con él sus bloques. Hasta que el proceso no cierre el archivo —o lo reinicies— no se libera nada.

  1. Ver el inodo y el contador de enlaces

Dos herramientas que ya conoces, ahora con la mirada puesta en los enlaces.

operador@srv-tramontana:~$ ls -li datos/
total 8
262149 -rw-r----- 1 operador operador  418 ago 17 19:40 casas.txt
262151 -rw-r--r-- 1 operador operador 1204 ago 18 07:55 reservas.csv

La primera columna con -i es el número de inodo. La columna que va justo después de los permisos —el 1— es el contador de enlaces: cuántos nombres apuntan a este inodo.

stat lo dice con todas las letras:

operador@srv-tramontana:~$ stat datos/casas.txt
  Fichero: datos/casas.txt
  Tamaño: 418         Bloques: 8          Bloque E/S: 4096   fichero regular
Dispositivo: 8,2	Nodo-i: 262149      Enlaces: 1
Acceso: (0640/-rw-r-----)  Uid: ( 1001/operador)   Gid: ( 1001/operador)

Nodo-i: 262149 y Enlaces: 1. Ese contador es el que gobierna cuándo se libera el espacio de un archivo, como verás en el apartado siguiente.

Y ahora se entiende el comportamiento de los directorios que quedó pendiente en la lección 02-03:

operador@srv-tramontana:~$ ls -ldi /opt/tramontana/app
131074 drwxr-xr-x 4 root root 4096 ago 18 08:30 /opt/tramontana/app

Contador 4, no 1. Los cuatro nombres que apuntan a ese inodo son:

  1. La entrada app dentro de /opt/tramontana.
  2. La entrada . dentro de /opt/tramontana/app.
  3. La entrada .. dentro de /opt/tramontana/app/plantillas.
  4. La entrada .. dentro de otro subdirectorio.

De ahí la fórmula: enlaces de un directorio = 2 + número de subdirectorios. Un directorio sin subdirectorios tiene 2; uno con siete subdirectorios tiene 9.

Un número de inodo solo tiene sentido dentro de su sistema de archivos. Dos archivos en discos distintos pueden tener el mismo número sin relación alguna. Por eso, para comparar, hay que mirar también el dispositivo:

operador@srv-tramontana:~$ stat -c '%d:%i  %n' datos/casas.txt /boot/vmlinuz
2049:262149  datos/casas.txt
2049:131331  /boot/vmlinuz

El par dispositivo:inodo sí identifica un archivo de forma única en toda la máquina.

  1. Enlaces duros con ln

Un enlace duro es, sencillamente, otra entrada de directorio que apunta al mismo inodo. No es una copia ni un acceso directo: es otro nombre igual de legítimo para el mismo archivo.

operador@srv-tramontana:~$ ln datos/casas.txt datos/alojamientos.txt

operador@srv-tramontana:~$ ls -li datos/
total 12
262149 -rw-r----- 2 operador operador  418 ago 17 19:40 alojamientos.txt
262149 -rw-r----- 2 operador operador  418 ago 17 19:40 casas.txt
262151 -rw-r--r-- 1 operador operador 1204 ago 18 07:55 reservas.csv

Tres cosas que confirmar en esa salida:

  1. Mismo inodo (262149) en las dos primeras líneas.
  2. El contador subió a 2 en ambas: hay dos nombres apuntando ahí.
  3. El tamaño se muestra dos veces (418 y 418), pero el archivo ocupa 418 bytes en total, no 836. ls muestra el tamaño del inodo, y el inodo es el mismo.

Ese último punto tiene una consecuencia práctica: du cuenta el espacio una sola vez si encuentra los dos nombres en el mismo recorrido, pero puede contarlo dos veces si están en recorridos distintos. Es una fuente clásica de descuadres al calcular el tamaño de copias de seguridad con enlaces duros.

No hay original ni copia. Los dos nombres son exactamente equivalentes; el sistema no guarda cuál se creó primero. Modificar uno modifica el otro, porque son el mismo archivo:

operador@srv-tramontana:~$ echo "el-moli;El Molí;Ripollès;5" >> datos/alojamientos.txt
operador@srv-tramontana:~$ tail -n 1 datos/casas.txt
el-moli;El Molí;Ripollès;5

Y lo mismo con los metadatos: chmod sobre uno cambia los permisos del otro, porque los permisos están en el inodo.

Qué pasa al borrar

Aquí está el comportamiento que hay que entender bien:

operador@srv-tramontana:~$ rm datos/casas.txt

operador@srv-tramontana:~$ ls -li datos/
262149 -rw-r----- 1 operador operador  446 ago 18 13:02 alojamientos.txt
262151 -rw-r--r-- 1 operador operador 1204 ago 18 07:55 reservas.csv

operador@srv-tramontana:~$ cat datos/alojamientos.txt
mas-figueres;Mas Figueres;Girona;6
...
el-moli;El Molí;Ripollès;5

El contenido sigue intacto. rm no borra archivos: decrementa el contador de enlaces y elimina el nombre. El inodo y sus bloques solo se liberan cuando ocurren dos cosas a la vez:

  • El contador de enlaces llega a 0.
  • Ningún proceso tiene el archivo abierto.

Por eso el comando de bajo nivel se llama unlink(), no delete(). rm es literalmente «desenlazar».

Los tres desenlaces posibles de un rm:

Situación tras decrementar el contador Qué ocurre con los datos
Contador > 0 Siguen accesibles por los demás nombres. Nada se libera
Contador = 0 y nadie lo tiene abierto Se liberan el inodo y los bloques. El archivo desaparece
Contador = 0 pero algún proceso lo tiene abierto Siguen en disco, sin nombre pero vivos: du no los ve, df sí los cuenta

La tercera fila es la del misterio del espacio que no se libera. Y también es un mecanismo que se usa a propósito: un programa que necesita un archivo temporal puede crearlo, abrirlo y borrarlo inmediatamente. El archivo sigue funcionando mientras el programa lo tenga abierto, y desaparece automáticamente cuando el programa termina, incluso si se cierra de forma anormal. No hay que limpiar nada.

  1. Las dos limitaciones de los enlaces duros

No pueden cruzar sistemas de archivos

operador@srv-tramontana:~$ ln datos/alojamientos.txt /boot/prueba.txt
ln: fallo al crear el enlace duro '/boot/prueba.txt' => 'datos/alojamientos.txt':
 Enlace cruzado no válido

El porqué: un enlace duro es una entrada (nombre, número de inodo). El número de inodo solo tiene significado dentro de su sistema de archivos: el inodo 262149 de /dev/sda2 no tiene nada que ver con el inodo 262149 de /dev/sda1. Una entrada en un directorio de /boot que dijera «262149» apuntaría al inodo 262149 de /boot, que es otro archivo completamente distinto.

No es una restricción arbitraria: es que la referencia no significa nada fuera de su ámbito.

Cómo saber si dos rutas están en el mismo sistema de archivos, antes de intentarlo:

operador@srv-tramontana:~$ df --output=source /home/operador /opt/tramontana /boot
S.ficheros
/dev/sda2
/dev/sda2
/dev/sda1

/home y /opt comparten /dev/sda2: se puede enlazar entre ellos. /boot está en /dev/sda1: no.

No se pueden hacer a directorios

operador@srv-tramontana:~$ ln /opt/tramontana/app /tmp/enlace-app
ln: /opt/tramontana/app: no se permite hacer un enlace duro a un directorio

Ni siquiera root puede en Linux. El porqué es más interesante:

El árbol de directorios de Unix es, por diseño, un grafo acíclico: se puede bajar y subir, pero no dar vueltas en círculo. Si pudieras crear un enlace duro a un directorio, podrías construir un bucle:

/a/b/c/  ->  enlace duro a  /a

Consecuencias inmediatas:

  • find, du, tar, rsync y cualquier recorrido recursivo entrarían en un bucle infinito, porque no habría forma de detectar que ya han pasado por ahí sin llevar registro de cada inodo visitado.
  • El contador de enlaces dejaría de servir para saber cuándo liberar el espacio: un ciclo de directorios que se referencian mutuamente tendría contadores mayores que cero aunque nadie pudiera llegar hasta ellos desde la raíz. Sería basura inalcanzable e imposible de recolectar.
  • .. dejaría de tener sentido único: un directorio con dos padres no puede tener un solo ...

Las entradas . y .. son la excepción: son enlaces duros a directorios, creados por el kernel, y por eso los contadores de los directorios valen más de 1. Pero están controlados y su topología es conocida, así que no rompen nada.

Ambas limitaciones desaparecen con el otro tipo de enlace.

  1. Enlaces simbólicos con ln -s

Un enlace simbólico (o symlink, o enlace blando) es un archivo de pleno derecho, con su propio inodo, cuyo contenido es una ruta de texto. Cuando algo lo abre, el kernel lee esa ruta y redirige la operación.

operador@srv-tramontana:~$ ln -s /home/operador/datos/reservas.csv ~/reservas-actual.csv

operador@srv-tramontana:~$ ls -li ~/reservas-actual.csv
262180 lrwxrwxrwx 1 operador operador 36 ago 18 13:20 reservas-actual.csv -> /home/operador/datos/reservas.csv

Todo lo que hay que leer ahí:

Elemento Qué significa
262180 Inodo propio y distinto del destino (262151)
l inicial Tipo enlace simbólico, la l que viste en el Módulo 1
rwxrwxrwx Los permisos del enlace son siempre estos y no significan nada: los que mandan son los del destino
1 Contador de enlaces del propio enlace
36 El tamaño es la longitud de la ruta, 36 caracteres
-> /home/... La ruta que contiene

Ese tamaño confirma qué es un symlink: un archivo cuyo contenido es una cadena de texto.

Al usarlo, todo funciona como si fuera el destino:

operador@srv-tramontana:~$ head -n 2 ~/reservas-actual.csv
id;fecha;casa;huesped;noches;importe
1001;2026-07-03;mas-figueres;Nuria Prat;4;620.00

Y las dos limitaciones del enlace duro no existen:

operador@srv-tramontana:~$ ln -s /boot/vmlinuz ~/kernel-actual
operador@srv-tramontana:~$ ls -l ~/kernel-actual
lrwxrwxrwx 1 operador operador 13 ago 18 13:24 kernel-actual -> /boot/vmlinuz

operador@srv-tramontana:~$ ln -s /opt/tramontana/app ~/app-produccion
operador@srv-tramontana:~$ ls -ld ~/app-produccion
lrwxrwxrwx 1 operador operador 19 ago 18 13:25 app-produccion -> /opt/tramontana/app

Cruza sistemas de archivos y apunta a directorios sin problema, porque no referencia un inodo: referencia una ruta, y una ruta es texto que se resuelve en el momento de usarla.

Un enlace a un directorio se comporta como el directorio:

operador@srv-tramontana:~$ ls ~/app-produccion/
ejecutable  plantillas  version.txt

operador@srv-tramontana:~$ cd ~/app-produccion
operador@srv-tramontana:~/app-produccion$ pwd
/home/operador/app-produccion
operador@srv-tramontana:~/app-produccion$ pwd -P
/opt/tramontana/app

Ahí tienes la explicación de pwd -P que quedó pendiente en la lección 02-03: pwd muestra la ruta lógica, la que usaste para llegar; pwd -P la física, resolviendo los enlaces.

Opciones de ln que conviene conocer:

Opción Qué hace
-s Crea un enlace simbólico (sin ella, duro)
-f Fuerza: reemplaza el enlace si ya existe
-n Trata un enlace a directorio existente como archivo, no entra en él
-r Crea el enlace con una ruta relativa calculada automáticamente
-v Informa de lo que hace
-T Trata el destino siempre como un nombre, nunca como un directorio

La combinación -sfn es la del despliegue y la verás en acción en el apartado 12. Sin -n, reemplazar un enlace que apunta a un directorio crea el nuevo enlace dentro de ese directorio en lugar de sustituirlo, que es un error muy fácil de cometer.

  1. Absolutos frente a relativos

El contenido de un enlace simbólico es una ruta, y esa ruta puede ser de los dos tipos.

operador@srv-tramontana:~$ cd /opt/tramontana

# Absoluto
operador@srv-tramontana:/opt/tramontana$ sudo ln -s /opt/tramontana/releases/3.2.1 app-abs

# Relativo
operador@srv-tramontana:/opt/tramontana$ sudo ln -s releases/3.2.1 app-rel

operador@srv-tramontana:/opt/tramontana$ ls -l app-*
lrwxrwxrwx 1 root root 30 ago 18 13:40 app-abs -> /opt/tramontana/releases/3.2.1
lrwxrwxrwx 1 root root 16 ago 18 13:40 app-rel -> releases/3.2.1

Los dos funcionan igual ahora mismo. Se diferencian en qué pasa cuando algo se mueve.

Un enlace relativo se resuelve desde el directorio donde está el enlace, no desde donde estés tú:

operador@srv-tramontana:~$ ls /opt/tramontana/app-rel/
ejecutable  plantillas  version.txt

Funciona desde tu home porque releases/3.2.1 se resuelve relativo a /opt/tramontana/, que es donde vive el enlace.

Ahora, la prueba que los distingue: copiar todo el árbol a otro sitio.

operador@srv-tramontana:~$ sudo cp -a /opt/tramontana /srv/tramontana/copia-completa

operador@srv-tramontana:~$ ls -l /srv/tramontana/copia-completa/app-*
lrwxrwxrwx 1 root root 30 ago 18 13:40 app-abs -> /opt/tramontana/releases/3.2.1
lrwxrwxrwx 1 root root 16 ago 18 13:40 app-rel -> releases/3.2.1
  • app-abs sigue apuntando a /opt/tramontana/..., es decir, al árbol original. La copia no es autocontenida: depende del original. Si borras /opt/tramontana, la copia queda rota.
  • app-rel apunta a releases/3.2.1 dentro de la copia. El árbol copiado es autónomo y consistente.
Criterio Absoluto Relativo
Sobrevive a mover el enlace No Sí, si se mueve todo el conjunto
Sobrevive a mover el destino No No
Copiar el árbol completo Se rompe la independencia Sigue coherente
Funciona dentro de un chroot o contenedor No (la raíz cambia) Sí
Legibilidad al hacer ls -l Muy clara Hay que calcular
Robusto ante un montaje en otro punto No Sí

Criterio de uso:

  • Relativos dentro de un mismo árbol de aplicación (/opt/tramontana/app → releases/...), porque el conjunto se copia, se respalda y se restaura como una unidad.
  • Absolutos cuando el destino está en otra parte del sistema y no tiene relación estructural con el enlace (por ejemplo, un enlace en tu home hacia /var/log/tramontana).

ln -r calcula el relativo por ti:

operador@srv-tramontana:~$ cd /opt/tramontana
operador@srv-tramontana:/opt/tramontana$ sudo ln -sr /opt/tramontana/releases/3.2.1 app-auto
operador@srv-tramontana:/opt/tramontana$ ls -l app-auto
lrwxrwxrwx 1 root root 16 ago 18 13:47 app-auto -> releases/3.2.1

Le das rutas absolutas, que son fáciles de escribir con Tab sin equivocarse, y él genera el enlace relativo correcto. Es la mejor de las dos opciones.

  1. Enlaces rotos

Como un symlink guarda una ruta y no una referencia al inodo, nada garantiza que el destino exista. Puedes crear un enlace a algo que aún no existe, y el destino puede desaparecer después.

operador@srv-tramontana:~$ ln -s /opt/tramontana/releases/9.9.9 ~/futuro
operador@srv-tramontana:~$ ls -l ~/futuro
lrwxrwxrwx 1 operador operador 33 ago 18 13:52 futuro -> /opt/tramontana/releases/9.9.9

operador@srv-tramontana:~$ cat ~/futuro
cat: /home/operador/futuro: No existe el fichero o el directorio

ls -l muestra el enlace sin quejarse, porque el enlace existe perfectamente: es el destino el que no. Solo al intentar usarlo aparece el error, y el mensaje menciona el nombre del enlace, lo que confunde: parece que no existe ~/futuro cuando ~/futuro está ahí.

Con color activado, ls muestra los enlaces rotos en rojo parpadeante, que es difícil de pasar por alto. Para detectarlos de forma fiable:

# Un enlace concreto
operador@srv-tramontana:~$ ls -lL ~/futuro
ls: no se puede acceder a '/home/operador/futuro': No existe el fichero o el directorio

# Todos los enlaces rotos de un árbol
operador@srv-tramontana:~$ find ~ -xtype l
/home/operador/futuro

# Con más contexto
operador@srv-tramontana:~$ find ~ -xtype l -exec ls -l {} \;
lrwxrwxrwx 1 operador operador 33 ago 18 13:52 /home/operador/futuro -> /opt/tramontana/releases/9.9.9

find -xtype l significa «elementos que, al resolver el enlace, son de tipo enlace», lo que solo ocurre si el enlace no se puede resolver. find se estudia a fondo en la lección 03-03; esta invocación puedes adoptarla ya como receta.

Y readlink es la herramienta específica para inspeccionar enlaces:

operador@srv-tramontana:~$ readlink ~/futuro
/opt/tramontana/releases/9.9.9

operador@srv-tramontana:~$ readlink -f ~/reservas-actual.csv
/home/operador/datos/reservas.csv

operador@srv-tramontana:~$ readlink -e ~/futuro
operador@srv-tramontana:~$ echo $?
1
Opción Qué hace
(ninguna) Muestra el contenido literal del enlace
-f Resuelve toda la cadena de enlaces; no falla si el último no existe
-e Como -f, pero falla si el destino no existe

readlink -e es el verificador: código 0 si el enlace lleva a algo real, distinto de 0 si está roto. Es exactamente lo que usarás en el script de despliegue para comprobar que el enlace app apunta a un release existente antes de arrancar el servicio.

Un enlace roto no siempre es un error. Es una situación normal cuando el destino se creará más tarde, o cuando apunta a un dispositivo extraíble que ahora no está montado. Lo que no debe haber son enlaces rotos que nadie sabe por qué están ahí.

  1. Qué comandos siguen el enlace y cuáles no

Esta es la parte que provoca más errores en la práctica: cada comando decide por su cuenta si actúa sobre el enlace o sobre su destino.

Comando Comportamiento por defecto Cómo cambiarlo
cat, less, editores Siguen el enlace: leen y escriben el destino —
ls Muestra el enlace -L para mostrar el destino
ls -l de un directorio enlazado Lista el contenido del destino -d para ver el enlace
cp Sigue: copia el contenido del destino -P o -d copia el enlace como tal
cp -a No sigue: copia los enlaces como enlaces -L para seguirlos
mv Mueve el enlace, no el destino —
rm Borra el enlace, nunca el destino —
chmod, chown Siguen: cambian el destino chown -h cambia el enlace
du Cuenta el enlace (unos bytes) -L para contar el destino
find No sigue -L para seguir
tar Guarda los enlaces como enlaces -h para guardar el contenido
rsync -a Copia los enlaces como enlaces -L para copiar el contenido

Los tres casos que hay que tener claros:

rm sobre un enlace borra el enlace. Siempre. Es seguro:

operador@srv-tramontana:~$ rm ~/reservas-actual.csv
operador@srv-tramontana:~$ ls datos/reservas.csv
datos/reservas.csv

El destino sigue intacto. Este es el comportamiento que la gente teme y que en realidad nunca falla.

cp sin -a deshace los enlaces:

operador@srv-tramontana:~$ ln -s /opt/tramontana/app/version.txt ~/v.txt

operador@srv-tramontana:~$ cp ~/v.txt /tmp/v-copia.txt
operador@srv-tramontana:~$ ls -l /tmp/v-copia.txt
-rw-rw-r-- 1 operador operador 26 ago 18 14:02 /tmp/v-copia.txt

operador@srv-tramontana:~$ cp -P ~/v.txt /tmp/v-enlace.txt
operador@srv-tramontana:~$ ls -l /tmp/v-enlace.txt
lrwxrwxrwx 1 operador operador 36 ago 18 14:02 /tmp/v-enlace.txt -> /opt/tramontana/app/version.txt

cp normal produjo un archivo real con el contenido; cp -P produjo un enlace. Al copiar un árbol con enlaces, esta diferencia decide si obtienes una copia fiel o una copia inflada donde cada enlace se ha convertido en una copia completa del archivo.

La trampa de la barra final con enlaces a directorios. Esta muerde a todo el mundo:

operador@srv-tramontana:~$ ln -s /opt/tramontana/app ~/app-enlace

# SIN barra: el enlace mismo
operador@srv-tramontana:~$ ls -ld ~/app-enlace
lrwxrwxrwx 1 operador operador 19 ago 18 14:05 /home/operador/app-enlace -> /opt/tramontana/app

# CON barra: el DIRECTORIO al que apunta
operador@srv-tramontana:~$ ls -ld ~/app-enlace/
drwxr-xr-x 4 root root 4096 ago 18 08:30 /home/operador/app-enlace/

La barra final significa «trátalo como directorio», y eso obliga al kernel a resolver el enlace. Y ahora lo peligroso:

# Borra SOLO el enlace. El destino intacto.
operador@srv-tramontana:~$ rm ~/app-enlace

# Con barra final: error, porque rm no borra directorios sin -r
operador@srv-tramontana:~$ rm ~/app-enlace/
rm: no se puede borrar '/home/operador/app-enlace/': Es un directorio

# ESTO SÍ BORRA EL CONTENIDO DEL DESTINO REAL
operador@srv-tramontana:~$ rm -rf ~/app-enlace/

Esa última línea, con -rf y barra final, atraviesa el enlace y borra el contenido de /opt/tramontana/app. La aplicación de producción, eliminada por una barra de más.

Regla de oro: nunca pongas barra final después del nombre de un enlace en un comando destructivo. Y en general, antes de un rm -rf sobre algo que podría ser un enlace, comprueba:

operador@srv-tramontana:~$ ls -ld ~/app-enlace
operador@srv-tramontana:~$ readlink -f ~/app-enlace
/opt/tramontana/app

  1. Duro frente a simbólico: la tabla

Característica Enlace duro Enlace simbólico
Qué es Otra entrada de directorio al mismo inodo Un archivo cuyo contenido es una ruta
Inodo El mismo que el destino Propio y distinto
Se crea con ln origen enlace ln -s origen enlace
En ls -l Indistinguible de un archivo normal Empieza por l, con ->
Tamaño El del archivo La longitud de la ruta
Contador de enlaces del destino Aumenta No cambia
Cruza sistemas de archivos No Sí
Apunta a directorios No Sí
Si se borra el destino Los datos siguen accesibles El enlace queda roto
Si se mueve el destino Sigue funcionando Se rompe (salvo relativos coherentes)
Permisos Los del inodo compartido lrwxrwxrwx, irrelevantes
Detectar el destino No se puede saber cuál era «el original» readlink
Puede apuntar a algo inexistente No Sí
Coste en disco Cero (solo la entrada) Un inodo y unos bytes
Uso típico Copias con rsync --link-dest, deduplicación Todo lo demás

Cuándo elegir cada uno, en la práctica:

Usa enlaces simbólicos por defecto. Son visibles en ls -l, dicen a dónde apuntan, cruzan sistemas de archivos, funcionan con directorios y se pueden reemplazar de forma atómica. El 95 % de los enlaces que crearás y que encontrarás en un sistema Linux son simbólicos.

Usa enlaces duros en dos casos concretos:

  1. Copias de seguridad incrementales con deduplicación. rsync --link-dest crea una copia completa aparente de cada día, pero los archivos que no han cambiado son enlaces duros a los del día anterior: ocupan cero espacio adicional. Es la base de las copias tipo Time Machine y lo verás en la lección 05-08.
  2. Cuando necesitas que el archivo sobreviva al borrado del nombre original, sin que quede un enlace roto.

  1. Enlaces en un sistema Linux real

Los enlaces no son una curiosidad: son estructura del sistema.

La raíz misma:

operador@srv-tramontana:~$ ls -l / | grep '^l'
lrwxrwxrwx   1 root root    7 abr 22  2024 bin -> usr/bin
lrwxrwxrwx   1 root root    7 abr 22  2024 lib -> usr/lib
lrwxrwxrwx   1 root root    9 abr 22  2024 lib64 -> usr/lib64
lrwxrwxrwx   1 root root    8 abr 22  2024 sbin -> usr/sbin

Es la reorganización conocida como /usr-merge: los binarios se consolidaron bajo /usr y se dejaron enlaces en la raíz para que las miles de referencias existentes a /bin/bash siguieran funcionando. Esto es lo que veías con la flecha en tree -L 1 / en la lección 02-03.

Fíjate en que son relativos (usr/bin, no /usr/bin). Deliberadamente: así siguen siendo válidos si el árbol se monta en otro punto, por ejemplo al reparar el sistema desde un disco de rescate montado en /mnt.

El intérprete de Python:

operador@srv-tramontana:~$ ls -l /usr/bin/python3
lrwxrwxrwx 1 root root 10 ago  2 12:04 /usr/bin/python3 -> python3.12

python3 es un enlace a la versión concreta. Actualizar Python a la 3.13 es, en esencia, cambiar ese enlace: todos los scripts que empiezan por #!/usr/bin/python3 pasan a usar la versión nueva sin tocar ni uno.

Cadenas de enlaces:

operador@srv-tramontana:~$ ls -l /usr/bin/vi
lrwxrwxrwx 1 root root 20 ago 18 09:00 /usr/bin/vi -> /etc/alternatives/vi
operador@srv-tramontana:~$ ls -l /etc/alternatives/vi
lrwxrwxrwx 1 root root 17 ago 18 09:00 /etc/alternatives/vi -> /usr/bin/vim.basic
operador@srv-tramontana:~$ readlink -f /usr/bin/vi
/usr/bin/vim.basic

Dos saltos. readlink -f sigue la cadena entera hasta el final. El kernel permite hasta unos 40 niveles de anidamiento antes de dar Demasiados niveles de enlaces simbólicos.

Los servicios de systemd:

operador@srv-tramontana:~$ ls -l /etc/systemd/system/multi-user.target.wants/ | head -n 4
lrwxrwxrwx 1 root root 44 jul  1 10:22 ssh.service -> /lib/systemd/system/ssh.service
lrwxrwxrwx 1 root root 48 jul  1 10:22 cron.service -> /lib/systemd/system/cron.service

Habilitar un servicio en systemd es literalmente crear un enlace simbólico, y deshabilitarlo es borrarlo. Cuando ejecutes systemctl enable en la lección 05-05, lo que ocurrirá por debajo es un ln -s.

Los logs rotados:

operador@srv-tramontana:~$ ls -l /var/log/tramontana/
-rw-r----- 1 root adm  18K ago 18 09:14 acceso.log
-rw-r----- 1 root adm 2,1K ago 17 23:59 acceso.log.1.gz

Aquí no hay enlaces, pero conviene saber que algunos servicios mantienen current.log como enlace al fichero del día.

  1. update-alternatives

Debian y Ubuntu resuelven con enlaces un problema concreto: varios paquetes que proporcionan la misma función. ¿Qué editor abre sudo editor? ¿Qué java se ejecuta si hay tres instaladas? ¿Qué es vi si hay vim, nvi y elvis?

El mecanismo es una doble capa de enlaces simbólicos:

/usr/bin/editor  ->  /etc/alternatives/editor  ->  /bin/nano

La primera capa es fija y está en el PATH. La segunda, en /etc/alternatives/, es la que se cambia. Así el administrador nunca toca /usr/bin, que pertenece a los paquetes.

operador@srv-tramontana:~$ update-alternatives --display editor
editor - modo automático
  el mejor enlace de la versión actual es /bin/nano
  el enlace es actualmente /bin/nano
  el enlace editor es /usr/bin/editor
/bin/nano - prioridad 40
/usr/bin/vim.basic - prioridad 30

Cambiarlo:

operador@srv-tramontana:~$ sudo update-alternatives --config editor
Existen 2 opciones para la alternativa editor (que provee /usr/bin/editor).

  Selección   Ruta                Prioridad  Estado
------------------------------------------------------------
* 0           /bin/nano            40        modo automático
  1           /bin/nano            40        modo manual
  2           /usr/bin/vim.basic   30        modo manual

Pulse <intro> para mantener el valor por omisión[*] o pulse un número de selección: 2
update-alternatives: utilizando /usr/bin/vim.basic para proveer /usr/bin/editor (editor) en modo manual
operador@srv-tramontana:~$ ls -l /etc/alternatives/editor
lrwxrwxrwx 1 root root 18 ago 18 14:20 /etc/alternatives/editor -> /usr/bin/vim.basic

El enlace de la segunda capa ha cambiado. Órdenes principales:

Orden Qué hace
--display <nombre> Muestra el estado y las opciones
--config <nombre> Elegir interactivamente
--list <nombre> Solo las rutas disponibles
--set <nombre> <ruta> Fijar sin interacción (para scripts)
--auto <nombre> Volver al modo automático por prioridad

Por qué importa esto para el EDITOR de tu sesión: cuando ejecutes sudo visudo o crontab -e, el editor que se abre lo decide la variable de entorno EDITOR y, si no está definida, la alternativa editor. Si nunca has configurado nada y te aparece nano, ahora sabes por qué y cómo cambiarlo. La variable EDITOR es de la lección 03-01.

  1. El patrón de despliegue de Tramontana

Aquí es donde todo lo anterior se convierte en una decisión de arquitectura. Es el patrón que Tramontana Reservas va a adoptar y que reaparecerá en la lección 04-07 y en el Módulo 8.

El problema

Hoy, el despliegue de una versión nueva significaría: parar el servicio, borrar o mover /opt/tramontana/app, copiar la versión nueva, arrancar. Problemas:

  • Ventana de indisponibilidad proporcional al tiempo de copia. Copiar 48 MB tarda segundos; en producción, copiar una release grande puede tardar minutos.
  • Estado intermedio inconsistente: durante la copia, el directorio tiene mitad de la versión vieja y mitad de la nueva. Si el servicio arranca en ese momento, se comporta de forma impredecible.
  • Volver atrás es otro despliegue completo, con su propia ventana y sus propios riesgos, ejecutado bajo presión porque algo ha fallado.
  • La versión anterior ya no está, así que volver atrás requiere recuperarla de una copia.

La solución

Separar el contenido (los releases, cada uno en su directorio inmutable) de el puntero (un enlace simbólico que dice cuál está activo).

/opt/tramontana/
├── releases/
│   ├── 3.1.0/          <- versión anterior, intacta
│   ├── 3.2.0/          <- versión anterior, intacta
│   └── 3.2.1/          <- versión nueva
├── app -> releases/3.2.1     <- EL PUNTERO
└── shared/
    └── uploads/        <- datos que sobreviven a los despliegues
flowchart TD
    S["Servicio Tramontana<br/>lee /opt/tramontana/app"] --> L["app<br/>(enlace simbólico)"]
    L -.->|"apunta a"| R321["releases/3.2.1<br/>ACTIVA"]
    R320["releases/3.2.0<br/>en reserva"]
    R310["releases/3.1.0<br/>en reserva"]
    L -.->|"un ln -sfn<br/>y apunta aquí"| R320

El despliegue completo:

# 1. Subir la versión nueva a su propio directorio. El servicio sigue
#    corriendo sobre 3.2.1 y no se entera de nada.
operador@srv-tramontana:~$ sudo mkdir -p /opt/tramontana/releases/3.3.0
operador@srv-tramontana:~$ sudo tar -xzf /srv/tramontana/envios/app-3.3.0.tar.gz \
    -C /opt/tramontana/releases/3.3.0 --strip-components=1

# 2. Verificar la versión nueva ANTES de activarla
operador@srv-tramontana:~$ cat /opt/tramontana/releases/3.3.0/version.txt
Tramontana Reservas 3.3.0
operador@srv-tramontana:~$ ls -l /opt/tramontana/releases/3.3.0/ejecutable
-rwxr-xr-x 1 root root 51203584 ago 18 14:40 ejecutable

# 3. Dejar constancia de qué había antes
operador@srv-tramontana:~$ readlink /opt/tramontana/app
releases/3.2.1

# 4. EL DESPLIEGUE: un solo comando
operador@srv-tramontana:~$ cd /opt/tramontana
operador@srv-tramontana:/opt/tramontana$ sudo ln -sfn releases/3.3.0 app

# 5. Verificar
operador@srv-tramontana:/opt/tramontana$ ls -l app
lrwxrwxrwx 1 root root 16 ago 18 14:41 app -> releases/3.3.0
operador@srv-tramontana:/opt/tramontana$ cat app/version.txt
Tramontana Reservas 3.3.0

# 6. Recargar el servicio
operador@srv-tramontana:/opt/tramontana$ sudo systemctl restart tramontana

La vuelta atrás:

operador@srv-tramontana:/opt/tramontana$ sudo ln -sfn releases/3.2.1 app
operador@srv-tramontana:/opt/tramontana$ sudo systemctl restart tramontana

Un comando. Ese es todo el rollback. Sin copiar nada, sin recuperar de una copia de seguridad, sin ventana de mantenimiento. Y se puede ejecutar a las tres de la mañana por alguien nervioso sin riesgo de equivocarse.

Por qué ln -sfn y no otra cosa

Las tres letras importan:

  • -s simbólico, obviamente.
  • -f fuerza el reemplazo. Sin ella, ln falla porque app ya existe.
  • -n es la crítica. Sin -n, ln ve que app es un enlace a un directorio, entra dentro y crea el enlace ahí:
# SIN -n: el desastre silencioso
operador@srv-tramontana:/opt/tramontana$ sudo ln -sf releases/3.3.0 app
operador@srv-tramontana:/opt/tramontana$ ls -l app/
lrwxrwxrwx 1 root root 15 ago 18 14:45 3.3.0 -> releases/3.3.0
ejecutable  plantillas  version.txt

Ha creado releases/3.2.1/3.3.0 en lugar de cambiar app. El enlace app sigue apuntando a la versión vieja, el servicio no se actualiza y encima has ensuciado el directorio del release anterior, que debía ser inmutable. -n siempre.

Por qué es «atómico»

Reemplazar un enlace simbólico se hace, por debajo, con la llamada al sistema rename(), que el kernel garantiza como atómica: no hay un instante en el que app no exista o apunte a medias. Cualquier proceso que abra /opt/tramontana/app en cualquier momento verá o la versión vieja completa o la nueva completa, nunca un estado intermedio.

Compara con la alternativa ingenua de rm app && ln -s releases/3.3.0 app: entre las dos órdenes hay una ventana, breve pero real, en la que app no existe. Un proceso que intente abrir un archivo justo entonces recibirá un error. Por eso el patrón es ln -sfn, en un solo comando.

Ventajas del patrón, resumidas

Aspecto Despliegue clásico Patrón con enlace
Tiempo de indisponibilidad Lo que tarde la copia El reinicio del servicio
Estado intermedio Existe y es inconsistente No existe
Volver atrás Otro despliegue completo Un comando
Versión anterior Sobrescrita Intacta en disco
Verificar antes de activar No se puede Sí, con calma
Espacio en disco Una copia N copias (hay que rotar)

La contrapartida está en la última fila: hay que eliminar los releases antiguos o /opt crecerá sin límite. La política habitual es conservar los tres o cinco últimos, y el script que lo hace lo escribirás en el Módulo 4.

Otro detalle del esquema: el directorio shared/. Los datos que deben sobrevivir a los despliegues —ficheros subidos por los usuarios, cachés persistentes— no pueden vivir dentro del release, porque el release siguiente no los tendría. Se ponen en shared/ y cada release los enlaza:

operador@srv-tramontana:~$ sudo ln -s ../../shared/uploads /opt/tramontana/releases/3.3.0/uploads

Este patrón, que aquí montas a mano, es exactamente el que implementan las herramientas de despliegue profesionales (Capistrano lo popularizó, y las estructuras de despliegue de muchos PaaS lo replican). Lo automatizarás con un script en la lección 04-07 y lo pondrás en producción en el Módulo 8.

Errores Comunes y Consejos

Creer que un enlace duro es una copia. Modificas uno y cambias los dos, porque son el mismo archivo.

Creer que borrar un enlace duro borra los datos. Solo si era el último nombre.

Hacer ln -sf sin -n sobre un enlace a directorio. Crea el enlace dentro en vez de sustituirlo, silenciosamente.

Poner barra final tras un enlace en un rm -rf. Atraviesa el enlace y borra el destino real. El error más caro de esta lección.

Copiar árboles con enlaces usando cp -r. Convierte los enlaces en copias reales e infla el resultado. Usa cp -a o rsync -a.

Usar enlaces absolutos dentro de un árbol que se va a copiar o mover. La copia acaba dependiendo del original. ln -sr.

Confiar en ls -l para saber si un enlace funciona. Muestra los rotos igual que los buenos. readlink -e o find -xtype l.

Consejo: readlink -f antes de cualquier operación destructiva sobre una ruta que pueda contener enlaces. Te dice a dónde apunta de verdad.

Consejo: pon los enlaces relativos dentro de un mismo árbol y absolutos entre árboles distintos. Es la regla que evita el 90 % de los enlaces rotos.

Consejo: find /ruta -xtype l de vez en cuando. Los enlaces rotos son un síntoma: algo se movió o se borró y nadie actualizó lo que apuntaba a ello.

Consejo: documenta los enlaces estructurales. Un README en /opt/tramontana explicando el patrón de releases evita que el próximo administrador borre releases/ pensando que sobra.

Ejercicios

Ejercicio 1: comprobar el comportamiento de los inodos

En /tmp, sin mirar la teoría:

  1. Crea original.txt con tres líneas. Anota su inodo y su contador de enlaces.
  2. Crea un enlace duro duro.txt y un enlace simbólico blando.txt. Muestra los tres con ls -li y explica cada columna.
  3. Añade una línea a través de duro.txt. ¿Qué ven original.txt y blando.txt?
  4. Borra original.txt. ¿Qué pasa con cada uno de los otros dos? Explica por qué en términos de inodos.
  5. Vuelve a crear un original.txt nuevo con contenido distinto. ¿A qué apunta ahora blando.txt? ¿Y duro.txt?

Ejercicio 2: montar el patrón de despliegue

Monta en srv-tramontana la estructura de releases completa:

  1. Crea /opt/tramontana/releases/3.2.1 con el contenido actual de /opt/tramontana/app.
  2. Sustituye /opt/tramontana/app por un enlace relativo a ese release, sin que el contenido deje de estar accesible en ningún momento verificable.
  3. Simula el despliegue de una versión 3.3.0 y actívala.
  4. Simula que falla y vuelve a 3.2.1.
  5. Escribe una comprobación de una línea que verifique que el enlace apunta a un release que existe realmente.

Ejercicio 3: el enlace de Luis

Luis ha montado en su portátil un directorio de trabajo compartido y te pasa esto para que lo repliques en el servidor:

cd /opt/tramontana
sudo rm app
sudo ln -s /home/luis/proyectos/tramontana/build app

Marta te pregunta si puede aprobarlo. Analiza la propuesta, enumera los problemas y propón la alternativa correcta con su justificación.

Soluciones

Solución 1

operador@srv-tramontana:~$ cd /tmp
operador@srv-tramontana:/tmp$ printf 'linea 1\nlinea 2\nlinea 3\n' > original.txt
operador@srv-tramontana:/tmp$ ls -li original.txt
393221 -rw-rw-r-- 1 operador operador 24 ago 18 15:02 original.txt

Inodo 393221, contador 1: un solo nombre apunta ahí.

operador@srv-tramontana:/tmp$ ln original.txt duro.txt
operador@srv-tramontana:/tmp$ ln -s original.txt blando.txt
operador@srv-tramontana:/tmp$ ls -li original.txt duro.txt blando.txt
393225 lrwxrwxrwx 1 operador operador 12 ago 18 15:03 blando.txt -> original.txt
393221 -rw-rw-r-- 2 operador operador 24 ago 18 15:02 duro.txt
393221 -rw-rw-r-- 2 operador operador 24 ago 18 15:02 original.txt

Lectura columna a columna:

  • duro.txt y original.txt comparten el inodo 393221 y ambos muestran contador 2. Son dos nombres del mismo archivo, sin jerarquía entre ellos.
  • blando.txt tiene inodo propio (393225), tipo l, permisos lrwxrwxrwx (que no significan nada), tamaño 12 —los caracteres de original.txt— y la flecha al destino.
operador@srv-tramontana:/tmp$ echo "linea 4" >> duro.txt
operador@srv-tramontana:/tmp$ cat original.txt
linea 1
linea 2
linea 3
linea 4
operador@srv-tramontana:/tmp$ cat blando.txt
linea 1
linea 2
linea 3
linea 4

Los tres ven lo mismo. duro.txt y original.txt porque son el mismo inodo; blando.txt porque resuelve a original.txt, que sigue existiendo.

operador@srv-tramontana:/tmp$ rm original.txt
operador@srv-tramontana:/tmp$ ls -li duro.txt blando.txt
393225 lrwxrwxrwx 1 operador operador 12 ago 18 15:03 blando.txt -> original.txt
393221 -rw-rw-r-- 1 operador operador 32 ago 18 15:05 duro.txt

operador@srv-tramontana:/tmp$ cat duro.txt
linea 1
linea 2
linea 3
linea 4

operador@srv-tramontana:/tmp$ cat blando.txt
cat: blando.txt: No existe el fichero o el directorio

duro.txt funciona perfectamente y su contador ha bajado de 2 a 1. rm eliminó una entrada de directorio y decrementó el contador; como no llegó a 0, el inodo y sus datos siguen ahí.

blando.txt está roto. Contenía el texto original.txt, y ese nombre ya no existe en el directorio. El enlace no guardaba ninguna referencia al inodo, solo una cadena.

operador@srv-tramontana:/tmp$ echo "contenido completamente distinto" > original.txt
operador@srv-tramontana:/tmp$ ls -li original.txt duro.txt blando.txt
393225 lrwxrwxrwx 1 operador operador 12 ago 18 15:03 blando.txt -> original.txt
393221 -rw-rw-r-- 1 operador operador 32 ago 18 15:05 duro.txt
393230 -rw-rw-r-- 1 operador operador 33 ago 18 15:08 original.txt

operador@srv-tramontana:/tmp$ cat blando.txt
contenido completamente distinto

operador@srv-tramontana:/tmp$ cat duro.txt
linea 1
...

Este último paso es el que fija el concepto:

  • blando.txt se ha «arreglado» solo y ahora apunta al archivo nuevo, que tiene un inodo distinto (393230). El enlace simbólico se resuelve por nombre, en el momento de usarlo, así que sigue a cualquier archivo que ocupe ese nombre. Esa es su virtud y su peligro: puede acabar apuntando a algo que no tiene nada que ver con lo original.
  • duro.txt sigue con el contenido antiguo en el inodo 393221. Nunca dependió del nombre.

Resumen en una frase: el enlace duro está atado al contenido; el simbólico está atado al nombre.

Solución 2

# 1. Crear el release a partir del contenido actual, preservando todo
operador@srv-tramontana:~$ sudo mkdir -p /opt/tramontana/releases
operador@srv-tramontana:~$ sudo rsync -a /opt/tramontana/app/ /opt/tramontana/releases/3.2.1/

operador@srv-tramontana:~$ sudo diff -rq /opt/tramontana/app /opt/tramontana/releases/3.2.1
operador@srv-tramontana:~$ echo $?
0

rsync -a con barra final en el origen copia el contenido, no la carpeta. Y diff -rq sin salida confirma que la copia es idéntica antes de tocar nada.

# 2. Sustituir el directorio por el enlace
operador@srv-tramontana:~$ cd /opt/tramontana

#    Primero renombrar (no borrar): si algo va mal, el original sigue ahí
operador@srv-tramontana:/opt/tramontana$ sudo mv app app.directorio-original

#    Crear el enlace relativo
operador@srv-tramontana:/opt/tramontana$ sudo ln -sfn releases/3.2.1 app

#    Verificar que el contenido sigue accesible por la misma ruta de siempre
operador@srv-tramontana:/opt/tramontana$ ls -l app
lrwxrwxrwx 1 root root 16 ago 18 15:20 app -> releases/3.2.1
operador@srv-tramontana:/opt/tramontana$ cat app/version.txt
Tramontana Reservas 3.2.1
operador@srv-tramontana:/opt/tramontana$ ls app/
ejecutable  plantillas  version.txt

#    Solo cuando todo está verificado, eliminar el directorio antiguo
operador@srv-tramontana:/opt/tramontana$ sudo rm -rI app.directorio-original

El detalle importante es mv en lugar de rm en el paso intermedio. Renombrar es instantáneo y reversible; borrar no. Si el enlace hubiera salido mal, un mv de vuelta lo restauraba todo.

# 3. Desplegar 3.3.0
operador@srv-tramontana:/opt/tramontana$ sudo cp -a releases/3.2.1 releases/3.3.0
operador@srv-tramontana:/opt/tramontana$ echo "Tramontana Reservas 3.3.0" | sudo tee releases/3.3.0/version.txt
Tramontana Reservas 3.3.0

#    Verificar ANTES de activar
operador@srv-tramontana:/opt/tramontana$ cat releases/3.3.0/version.txt
Tramontana Reservas 3.3.0

#    Anotar el estado actual por si hay que volver
operador@srv-tramontana:/opt/tramontana$ readlink app
releases/3.2.1

#    Activar
operador@srv-tramontana:/opt/tramontana$ sudo ln -sfn releases/3.3.0 app
operador@srv-tramontana:/opt/tramontana$ cat app/version.txt
Tramontana Reservas 3.3.0
# 4. Vuelta atrás
operador@srv-tramontana:/opt/tramontana$ sudo ln -sfn releases/3.2.1 app
operador@srv-tramontana:/opt/tramontana$ cat app/version.txt
Tramontana Reservas 3.2.1

Un comando y estás en la versión anterior. Y releases/3.3.0 sigue en disco, así que reintentar el despliegue después de corregir el problema es otro comando.

# 5. Comprobación del enlace
operador@srv-tramontana:/opt/tramontana$ readlink -e app > /dev/null && echo "OK: $(readlink app)" || echo "FALLO: enlace roto"
OK: releases/3.2.1

Desglose: readlink -e resuelve el enlace y falla con código distinto de 0 si el destino no existe. Con && y || de la lección 02-01, la línea informa en ambos casos. Es exactamente la comprobación que irá al principio del script de despliegue del Módulo 4, para no arrancar nunca el servicio con el enlace apuntando a la nada.

Solución 3

Problemas de la propuesta de Luis, de más a menos grave:

1. Producción dependería del directorio personal de un desarrollador. El enlace apunta a /home/luis/proyectos/tramontana/build. Eso significa que la aplicación en producción se ejecuta con el código que Luis tenga en su carpeta de trabajo en ese instante. Cada vez que Luis compile, producción cambia. Sin revisión, sin aprobación de Marta, sin aviso. Es exactamente el escenario que el proceso de despliegue existe para impedir.

2. Ese directorio no existe en srv-tramontana. /home/luis es una ruta del portátil de Luis. En el servidor, el enlace nacería roto:

operador@srv-tramontana:~$ ls -l /opt/tramontana/app
lrwxrwxrwx 1 root root 38 ago 18 15:40 app -> /home/luis/proyectos/tramontana/build
operador@srv-tramontana:~$ readlink -e /opt/tramontana/app
operador@srv-tramontana:~$ echo $?
1

ls -l lo muestra tan tranquilo, pero el servicio no arrancaría. Y el mensaje de error sería No existe el fichero /opt/tramontana/app, cuando app sí existe: es el destino el que no. Diagnosticar eso sin conocer los enlaces cuesta un buen rato.

3. sudo rm app destruye el original antes de tener nada. Si el nuevo enlace sale mal —y va a salir mal—, la aplicación ya no está. No hay vuelta atrás. Debería ser mv, y solo borrar cuando lo nuevo esté verificado.

4. Rompe el FHS. El Módulo 1 justificó /opt/tramontana/app precisamente porque /home/usuario/proyecto es el peor sitio posible: dar de baja a un empleado tumba el servicio. Aquí el enlace reintroduce esa dependencia por la puerta de atrás, con el agravante de que en ls -l /opt/tramontana parece que todo está bien.

5. Problema de permisos y seguridad. El código de producción quedaría bajo el control de un usuario sin privilegios, que puede modificarlo en cualquier momento sin sudo. Cualquiera que comprometa la cuenta de Luis controla la aplicación en producción. La lección 02-07 te dará el vocabulario exacto para explicar por qué esto es grave.

6. No hay versionado ni forma de volver atrás. No se sabe qué versión está desplegada ni cómo recuperar la anterior.

Alternativa correcta, que es el patrón del apartado 12:

# Luis genera un paquete versionado desde su entorno de compilación
luis@portatil-luis:~$ tar -czf tramontana-app-3.3.0.tar.gz -C build .
luis@portatil-luis:~$ sha256sum tramontana-app-3.3.0.tar.gz > tramontana-app-3.3.0.tar.gz.sha256

# El operador lo recibe, verifica su integridad y lo despliega como release
operador@srv-tramontana:~$ sha256sum -c /srv/tramontana/envios/tramontana-app-3.3.0.tar.gz.sha256
tramontana-app-3.3.0.tar.gz: La suma coincide

operador@srv-tramontana:~$ sudo mkdir -p /opt/tramontana/releases/3.3.0
operador@srv-tramontana:~$ sudo tar -xzf /srv/tramontana/envios/tramontana-app-3.3.0.tar.gz \
    -C /opt/tramontana/releases/3.3.0

# Verificar antes de activar
operador@srv-tramontana:~$ cat /opt/tramontana/releases/3.3.0/version.txt
Tramontana Reservas 3.3.0

# Activar con un solo comando atómico
operador@srv-tramontana:~$ cd /opt/tramontana && sudo ln -sfn releases/3.3.0 app
operador@srv-tramontana:/opt/tramontana$ readlink -e app && cat app/version.txt
/opt/tramontana/releases/3.3.0
Tramontana Reservas 3.3.0

Respuesta para Marta:

No conviene aprobar la propuesta tal como está. La idea de fondo —poder cambiar de versión rápidamente— es buena, y de hecho es la que vamos a implementar; el problema es la ejecución concreta.

Tal como está planteada, la aplicación en producción pasaría a ejecutar directamente el directorio de trabajo del portátil de Luis. Eso significa tres cosas: que cualquier compilación suya cambiaría producción al instante y sin aviso, que no sabríamos en ningún momento qué versión está funcionando, y que si Luis se va de la empresa o cambia de portátil, el servicio deja de funcionar. Además, en el servidor esa ruta ni siquiera existe, así que el cambio dejaría la aplicación caída de inmediato.

La alternativa que propongo consigue el mismo objetivo con garantías: cada versión se instala en su propio directorio numerado dentro del servidor, y un puntero indica cuál está activa. Desplegar es cambiar el puntero, y volver a la versión anterior también, en ambos casos con un solo comando y sin copiar nada. La versión anterior permanece intacta en el servidor, así que la vuelta atrás es inmediata y segura. Y en todo momento se puede consultar qué versión está en producción y quién la aprobó.

Se lo comento a Luis: lo que necesitamos de él es el paquete de la versión, no acceso directo desde su equipo.

Conclusión

Has desmontado la suposición con la que empezaste el módulo: un archivo no es un nombre.

  • Un inodo guarda todos los metadatos y las direcciones de los bloques, menos el nombre. Los directorios son índices de pares (nombre, inodo).
  • Esa arquitectura explica por qué mv es instantáneo, por qué . comparte inodo con su directorio, por qué se puede borrar un archivo abierto y por qué df y du a veces no cuadran.
  • Un enlace duro es otro nombre para el mismo inodo. rm desenlaza: el contenido vive mientras el contador no llegue a cero y nadie lo tenga abierto.
  • Los enlaces duros no cruzan sistemas de archivos porque el número de inodo no significa nada fuera del suyo, y no apuntan a directorios porque permitirían ciclos que romperían los recorridos recursivos y la recolección de espacio.
  • Un enlace simbólico es un archivo cuyo contenido es una ruta. Cruza sistemas de archivos, apunta a directorios, puede apuntar a lo que no existe, y se resuelve por nombre en el momento de usarlo.
  • Relativos dentro de un mismo árbol, absolutos entre árboles distintos, y ln -sr los calcula por ti.
  • Los enlaces rotos no los detecta ls: los detectan readlink -e y find -xtype l.
  • Cada comando decide si sigue el enlace o no, y las trampas serias son cp sin -a, que deshace los enlaces, y la barra final en un rm -rf, que atraviesa el enlace y borra el destino real.
  • Los enlaces son estructura del sistema: /bin, /usr/bin/python3, /etc/alternatives y habilitar un servicio de systemd es crear un enlace.
  • Y Tramontana ya tiene su patrón de despliegue: releases/ inmutables y un app -> releases/N que convierte publicar y revertir en un ln -sfn atómico, con -n porque sin él el despliegue falla en silencio.

Queda la última pieza del módulo, y es la que más consecuencias tiene. Has visto que /etc/tramontana/app.conf es -rw-r----- y pertenece a root:tramontana, que el ejecutable es -rwxr-xr-x, y que los permisos de un enlace simbólico no significan nada. Has usado sudo sin preguntarte del todo por qué hacía falta. En la próxima lección, Permisos y Propiedad de Archivos, todo eso deja de ser ruido: leerás la cadena de ls -l carácter a carácter, entenderás por qué x significa cosas distintas en un archivo y en un directorio, traducirás entre notación octal y simbólica en ambos sentidos, calcularás qué hace la umask, reconocerás SUID, SGID y sticky bit cuando los veas, y diseñarás los permisos correctos de cada ruta de Tramontana. Es la lección que separa un servidor que funciona de un servidor que además es seguro.

Curso de Linux: De Principiante a Administrador de Sistemas

Módulo 1: Introducción a Linux

Módulo 2: Comandos Básicos de Linux

Módulo 3: Habilidades Avanzadas en la Línea de Comandos

Módulo 4: Scripting en Shell

Módulo 5: Administración del Sistema

Módulo 6: Redes y Seguridad

Módulo 7: Temas Avanzados

Módulo 8: Proyectos Prácticos

© Copyright 2026. Todos los derechos reservados