Día 91 - Ansible Lineinfile Module (gestión de una línea + insertbefore/regexp + touch no trunca)
Problema / Desafío
El equipo Nautilus quiere un httpd simple en todos los app servers, con una página de muestra desplegada con Ansible. La tarea:
- Crear
/home/thor/ansible/playbook.yml(el inventario ya existe) - Instalar
httpden todos los app servers y dejar el servicio arrancado y corriendo - Crear
/var/www/html/index.htmlcon el contenido: - Usando el módulo
lineinfile, añadir esta línea al inicio del archivo: - El archivo debe ser propiedad de
apache:apacheen todos los app servers - Los permisos deben ser
0744
Restricción de validación: se corre con ansible-playbook -i inventory playbook.yml — sin argumentos extra. El become debe vivir dentro del playbook.
Es el contrapunto del Día 88: si
blockinfilegestiona un bloque de líneas,lineinfilegestiona una sola línea. Y este lab esconde una lección sobre por quéstate: touchno borra el contenido previo (ver el cuadro más abajo).
Conceptos clave
El módulo lineinfile — asegurar que una línea esté (o no) en un archivo
- name: add welcome line at the top
ansible.builtin.lineinfile:
path: /var/www/html/index.html
line: 'Welcome to xFusionCorp Industries!'
insertbefore: BOF
| Parámetro | Función |
|---|---|
path: |
Archivo a editar |
line: |
La línea exacta que debe existir |
regexp: |
Patrón para buscar una línea existente y reemplazarla (el uso más potente) |
insertbefore: |
Dónde insertar si la línea es nueva: BOF o un regex |
insertafter: |
Dónde insertar: EOF (default) o un regex |
state: |
present (default, asegura la línea) / absent (la borra) |
create: |
yes → crear el archivo si no existe |
backrefs: |
Usar grupos capturados del regexp en el line |
Idempotencia: lineinfile matchea la línea completa
Esta es la clave para entender el output del lab. lineinfile con state: present:
- Busca si la
line(o elregexp) ya existe en el archivo. - Si existe → no hace nada (
ok, nochanged). No la mueve ni la duplica, aunque haya uninsertbefore/insertafter. - Si no existe → la inserta en la posición indicada.
El punto sutil:
insertbefore/insertaftersolo aplican cuando la línea se está agregando. Si la línea ya está en el archivo, su posición actual se respeta.
insertbefore / insertafter — dónde cae la línea nueva
| Valor | Posición |
|---|---|
insertbefore: BOF |
Beginning Of File — al inicio |
insertafter: EOF |
End Of File — al final (default) |
insertbefore: '^ServerName' |
Justo antes de la primera línea que matchea el regex |
insertafter: '^\[main\]' |
Justo después de la línea que matchea |
Gotcha de orden (LIFO en BOF): si dos tasks insertan con insertbefore: BOF, la que corre última queda arriba, porque cada inserción empuja a la anterior hacia abajo. En este lab, "add welcome" corre después de "add base content", por eso Welcome... termina en el tope — justo lo que pide el requisito.
★ Por qué sobrevive This is KodeKloud Ansible Lab ! — state: touch NO trunca
El curl del lab muestra tres líneas, pero el playbook solo añade dos:
Welcome to xFusionCorp Industries! ← añadida por lineinfile (insertbefore BOF)
This is KodeKloud Ansible Lab ! ← ¿de dónde salió esto?
This is a Nautilus sample file, created using Ansible! ← preexistente
La línea de KodeKloud no la añade nadie en el playbook. El lab pre-siembra el index.html, y el task de creación usa state: touch:
- name: create file
ansible.builtin.file:
path: /var/www/html/index.html
state: touch # ← solo actualiza el mtime; NO borra el contenido
owner: apache
group: apache
mode: '0744'
state: touch sobre un archivo que ya existe solo cambia el timestamp — no lo vacía. Por eso el contenido pre-existente sobrevive. La reconstrucción del archivo inicial:
# Archivo pre-sembrado por el lab:
This is KodeKloud Ansible Lab !
This is a Nautilus sample file, created using Ansible!
Luego:
add base content(This is a Nautilus..., insertbefore BOF) → la línea ya existe → no-op idempotente, queda donde estaba (abajo).add welcome(Welcome..., insertbefore BOF) → línea nueva → se inserta arriba de todo.
Resultado final = el output del curl. Si se quisiera un archivo limpio (sin la línea de KodeKloud), touch es la herramienta equivocada — habría que usar copy/content, o borrar la línea con lineinfile state=absent, o template.
El verdadero superpoder de lineinfile: regexp (editar config files)
El caso de uso real de lineinfile no es "añadir una línea de bienvenida" — es buscar y reemplazar una directiva en un archivo de configuración:
- name: forzar PermitRootLogin no en sshd_config
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin' # ← matchea la línea exista o esté comentada
line: 'PermitRootLogin no' # ← la reemplaza por esta
Aquí regexp localiza la línea (esté como PermitRootLogin yes, comentada, o ausente) y la deja exactamente como line. Sin regexp, cada cambio de valor crearía una línea nueva en vez de reemplazar la vieja — regexp es lo que hace a lineinfile idempotente sobre directivas que cambian de valor.
lineinfile vs blockinfile vs copy/template — cuándo cada uno
| Módulo | Para |
|---|---|
lineinfile |
Una línea: añadir o reemplazar una directiva (regexp) |
blockinfile |
Un bloque de varias líneas marcado (Día 88) |
copy |
Archivo completo desde el control node |
template |
Archivo completo con variables Jinja2 |
Regla: para 1 directiva → lineinfile; para N líneas que van juntas → blockinfile; para el archivo entero → copy/template.
El playbook completo — anatomía
- hosts: all
become: true
tasks:
- name: install httpd # Día 87
ansible.builtin.yum:
name: httpd
state: present
- name: manage service # Día 89
ansible.builtin.service:
name: httpd
state: started
enabled: yes
- name: create file # Día 85 (touch + owner/group/mode)
ansible.builtin.file:
path: /var/www/html/index.html
owner: apache
group: apache
state: touch
mode: '0744'
- name: add base content # protagonista de hoy
ansible.builtin.lineinfile:
path: /var/www/html/index.html
line: 'This is a Nautilus sample file, created using Ansible!'
insertbefore: BOF
- name: add welcome line at the top
ansible.builtin.lineinfile:
path: /var/www/html/index.html
line: 'Welcome to xFusionCorp Industries!'
insertbefore: BOF # corre último → queda arriba
Los permisos 0744 (rwxr--r--): owner puede leer/escribir/ejecutar, grupo y otros solo leer. El bit de ejecución del owner es irrelevante para un HTML, pero el lab lo pide literal → comillas '0744' para que YAML no lo lea como octal.
Pasos
- Login al jump host como
thor;cd /home/thor/ansible - Validar conectividad:
ansible -i inventory all -m ping - Crear
/home/thor/ansible/playbook.yml:yum→service→file(touch + perms) →lineinfilex2 - Correr
ansible-playbook -i inventory playbook.yml - Validar con
curl http://stappNNy revisando owner/perms
Comandos / Código
Playbook (solución utilizada)
# /home/thor/ansible/playbook.yml
- hosts: all
become: true
tasks:
- name: install httpd
ansible.builtin.yum:
name: httpd
state: present
- name: manage service
ansible.builtin.service:
name: httpd
state: started
enabled: yes
- name: create file
file:
path: /var/www/html/index.html
owner: apache
group: apache
state: touch
mode: '0744'
- name: add base content
lineinfile:
path: /var/www/html/index.html
line: 'This is a Nautilus sample file, created using Ansible!'
insertbefore: BOF
- name: add welcome line at the top
lineinfile:
path: /var/www/html/index.html
line: 'Welcome to xFusionCorp Industries!'
insertbefore: BOF
Verificación
Output real del lab:
Welcome to xFusionCorp Industries!
This is KodeKloud Ansible Lab !
This is a Nautilus sample file, created using Ansible!
Welcome...arriba (lo que pide el lab). La línea de KodeKloud es contenido pre-sembrado questate: touchno borró. La línea Nautilus quedó abajo porque ya existía ylineinfileno la movió.
# Owner y permisos en los 3 hosts
ansible -i inventory all -m shell -a "ls -l /var/www/html/index.html" --become
# esperado: -rwxr--r--. 1 apache apache ... index.html
Variantes (referencia)
Crear un archivo limpio (sin contenido pre-existente)
Si se quisiera que el archivo tenga solo las líneas del lab, touch no sirve. Opciones:
# Opción A: copy con content (reemplaza todo el archivo)
- ansible.builtin.copy:
dest: /var/www/html/index.html
content: |
Welcome to xFusionCorp Industries!
This is a Nautilus sample file, created using Ansible!
owner: apache
group: apache
mode: '0744'
# Opción B: lineinfile con create (sin tocar mtime de un archivo que ya existe)
- ansible.builtin.lineinfile:
path: /var/www/html/index.html
line: 'This is a Nautilus sample file, created using Ansible!'
create: yes
owner: apache
group: apache
mode: '0744'
Reemplazar una directiva con regexp
- ansible.builtin.lineinfile:
path: /etc/httpd/conf/httpd.conf
regexp: '^Listen '
line: 'Listen 8080'
Borrar una línea
- ansible.builtin.lineinfile:
path: /var/www/html/index.html
line: 'This is KodeKloud Ansible Lab !'
state: absent # quita esa línea exacta
Insertar después de un patrón
- ansible.builtin.lineinfile:
path: /etc/hosts
insertafter: '^127\.0\.0\.1'
line: '127.0.0.1 myapp.local'
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
| Aparecen líneas que no añadí (ej. "KodeKloud Lab") | state: touch no trunca; el archivo tenía contenido pre-existente |
Usar copy/content o lineinfile state=absent para limpiar lo no deseado |
La línea no se mueve al top pese a insertbefore: BOF |
La línea ya existía → lineinfile es idempotente y no la reubica |
Normal; insertbefore solo aplica al agregar una línea nueva |
Welcome no quedó arriba |
Orden de tasks: la última inserción en BOF gana el top | Poner el task de la línea que va arriba al final (o usar un solo task) |
| Cada corrida crea una línea duplicada con valor distinto | Se omitió regexp al editar una directiva variable |
Usar regexp: para matchear y reemplazar la línea existente |
Destination does not exist en el task lineinfile |
El archivo no existe y falta create: yes |
Asegurar el task file/copy antes, o create: yes en lineinfile |
El task file sale changed siempre |
state: touch no es idempotente (toca mtime cada run) |
Normal; usar state: present (sin touch) si no se quiere modificar el mtime |
Funciona con --become pero no sin args |
La validación corre sin --become |
Poner become: true dentro del play |
Conexión con días anteriores
- Día 88 (
blockinfile): el par conceptual — bloque vs línea única.blockinfileusa marcadores para reidentificar su bloque;lineinfilematchea la línea completa (o unregexp). - Día 85 (
file+touch+mode): el task de creación reusa exactamente eso (touch + owner/group/mode con comillas). Hoy se ve el lado oscuro detouch: no trunca. - Días 87-89 (
yum+service): los dos primeros tasks son el stack httpd ya conocido; hoy se le añade la gestión fina del contenido línea por línea. - Idempotencia (Días 84, 87):
lineinfilees idempotente sobre la línea/regexp;state: touchno lo es (mtime). Conviven en el mismo playbook — útil para distinguir qué task saldráchangedsiempre.