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
- Qué es un inodo
- Ver el inodo y el contador de enlaces
- Enlaces duros con
ln - Las dos limitaciones de los enlaces duros
- Enlaces simbólicos con
ln -s - Absolutos frente a relativos
- Enlaces rotos
- Qué comandos siguen el enlace y cuáles no
- Duro frente a simbólico: la tabla
- Enlaces en un sistema Linux real
update-alternatives- El patrón de despliegue de Tramontana
- Qué es un inodo
Cuando guardas un archivo, el sistema de archivos crea tres cosas distintas:
- Los bloques de datos: el contenido en sí, repartido por el disco.
- 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.
- 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.
- 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.csvLa 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/appContador 4, no 1. Los cuatro nombres que apuntan a ese inodo son:
- La entrada
appdentro de/opt/tramontana. - La entrada
.dentro de/opt/tramontana/app. - La entrada
..dentro de/opt/tramontana/app/plantillas. - 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/vmlinuzEl par dispositivo:inodo sí identifica un archivo de forma única en toda la máquina.
- Enlaces duros con
ln
lnUn 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.csvTres cosas que confirmar en esa salida:
- Mismo inodo (262149) en las dos primeras líneas.
- El contador subió a 2 en ambas: hay dos nombres apuntando ahí.
- El tamaño se muestra dos veces (418 y 418), pero el archivo ocupa 418 bytes en total, no 836.
lsmuestra 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;5Y 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;5El 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.
- 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álidoEl 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 directorioNi 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:
Consecuencias inmediatas:
find,du,tar,rsyncy 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.
- Enlaces simbólicos con
ln -s
ln -sUn 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.csvTodo 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.00Y 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/appCruza 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/appAhí 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.
- 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.1Los 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ú:
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.1app-abssigue 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-relapunta areleases/3.2.1dentro 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.1Le 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.
- 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 directoriols -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.9find -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í.
- 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.csvEl 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.txtcp 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
- 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:
- Copias de seguridad incrementales con deduplicación.
rsync --link-destcrea 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. - Cuando necesitas que el archivo sobreviva al borrado del nombre original, sin que quede un enlace roto.
- 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/sbinEs 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.12python3 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.basicDos 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.serviceHabilitar 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.gzAquí no hay enlaces, pero conviene saber que algunos servicios mantienen current.log como enlace al fichero del día.
update-alternatives
update-alternativesDebian 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:
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 30Cambiarlo:
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 manualoperador@srv-tramontana:~$ ls -l /etc/alternatives/editor
lrwxrwxrwx 1 root root 18 ago 18 14:20 /etc/alternatives/editor -> /usr/bin/vim.basicEl 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.
- 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 desplieguesflowchart 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 tramontanaLa vuelta atrás:
operador@srv-tramontana:/opt/tramontana$ sudo ln -sfn releases/3.2.1 app
operador@srv-tramontana:/opt/tramontana$ sudo systemctl restart tramontanaUn 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:
-ssimbólico, obviamente.-ffuerza el reemplazo. Sin ella,lnfalla porqueappya existe.-nes la crítica. Sin-n,lnve queappes 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.txtHa 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:
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:
- Crea
original.txtcon tres líneas. Anota su inodo y su contador de enlaces. - Crea un enlace duro
duro.txty un enlace simbólicoblando.txt. Muestra los tres conls -liy explica cada columna. - Añade una línea a través de
duro.txt. ¿Qué venoriginal.txtyblando.txt? - Borra
original.txt. ¿Qué pasa con cada uno de los otros dos? Explica por qué en términos de inodos. - Vuelve a crear un
original.txtnuevo con contenido distinto. ¿A qué apunta ahorablando.txt? ¿Yduro.txt?
Ejercicio 2: montar el patrón de despliegue
Monta en srv-tramontana la estructura de releases completa:
- Crea
/opt/tramontana/releases/3.2.1con el contenido actual de/opt/tramontana/app. - Sustituye
/opt/tramontana/apppor un enlace relativo a ese release, sin que el contenido deje de estar accesible en ningún momento verificable. - Simula el despliegue de una versión 3.3.0 y actívala.
- Simula que falla y vuelve a 3.2.1.
- 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:
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.txtInodo 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.txtLectura columna a columna:
duro.txtyoriginal.txtcomparten el inodo 393221 y ambos muestran contador 2. Son dos nombres del mismo archivo, sin jerarquía entre ellos.blando.txttiene inodo propio (393225), tipol, permisoslrwxrwxrwx(que no significan nada), tamaño 12 —los caracteres deoriginal.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 4Los 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 directorioduro.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.txtse 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.txtsigue 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 $?
0rsync -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-originalEl 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.1Un 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.1Desglose: 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 $?
1ls -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.0Respuesta 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é
mves instantáneo, por qué.comparte inodo con su directorio, por qué se puede borrar un archivo abierto y por quédfydua veces no cuadran. - Un enlace duro es otro nombre para el mismo inodo.
rmdesenlaza: 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 -srlos calcula por ti. - Los enlaces rotos no los detecta
ls: los detectanreadlink -eyfind -xtype l. - Cada comando decide si sigue el enlace o no, y las trampas serias son
cpsin-a, que deshace los enlaces, y la barra final en unrm -rf, que atraviesa el enlace y borra el destino real. - Los enlaces son estructura del sistema:
/bin,/usr/bin/python3,/etc/alternativesy habilitar un servicio de systemd es crear un enlace. - Y Tramontana ya tiene su patrón de despliegue:
releases/inmutables y unapp -> releases/Nque convierte publicar y revertir en unln -sfnatómico, con-nporque 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
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
