Introducción a systemd

Convierte tus aplicaciones en servicios administrados y monitorizados sin necesidad de aplicaciones externas.

Introducción

systemd es una poderosa (aunque controvertida) suite utilizada para administrar y configurar sistemas Unix. Uno de los usos más extendidos de systemd es el de gestionar el sistema de inicio (init system), para administrar fácilmente procesos y servicios. Aunque systemd es capaz de administrar muchas más cosas, en este artículo veremos como utilizar systemd para crear, arrancar, parar, reiniciar y, en general, mantener sanos y monitorizados nuestros servicios propios.

Vamos a utilizar de ejemplo una aplicación creada en Node.js. Si bien podríamos utilizar uno de los muchos gestores de procesos que se instalan a través de npm, como por ejemplo pm2, forever o supervisor, el problema es que estas herramientas están unidas a Node.js y su futuro dependen del mantenimiento que reciban estas librerías. En el caso de systemd, es una suite muy sólida que viene de serie en muchas distribuciones Linux y saber cómo funciona nos brinda nuevas oportunidades.

Otro problema es que tras reiniciar la máquina necesitaríamos un servicio o tarea que ejecute de nuevo nuestro gestor de procesos basado en Node.js. Así pues, vamos a prescindir de este tipo de herramientas, aunque podríamos combinarlas y obtener lo mejor de ambos mundos.

Primero veremos como crear unidades, después como administrarlas, y por último como monitorizarlas.

systemd

Systemd trabaja con 2 conceptos: units y targets.

Units

Las unidades son componentes (como por ejemplo servicios) que deberían funcionar como piezas independientes de software. En este caso, una unidad sería por ejemplo MySQL, Redis o Node.js.

Salvo que tu distribución Linux sea muy especial, guardarás tus unidades en la ruta /etc/systemd/system con un nombre tipo app.service. Para servicios de usuario también es posible almacenarlos en /home/$USER/.config/systemd/user.

Una unidad se compone de secciones, como por ejemplo [Unit], [Service] o [Install]. Estas secciones contienen directivas y aquí veremos unas cuantas necesarias para nuestro cometido. Si quieres ver todas las secciones disponibles o sus directivas en detalle, consulta la documentación oficial.

En el caso de [Unit], encontraremos metadatos que definen la unidad, como por ejemplo Description para añadir una descripción de texto, o Requires, Wants, Before, After, BindsTo o Conflicts que sirven para relacionar nuestra unidad con otras unidades.

Por ejemplo si nuestra unidad Node.js indica Requires=redis.service, se iniciará la unidad de Redis antes de que arranque la de Node.js.

La sección [Install] se encarga de interactuar con los targets, algo que veremos más adelante.

Por último, en [Service] indicaremos el funcionamiento de nuestra unidad. Unas cuantas directivas útiles son:

  • Directivas de control de flujo: ExecStart, ExecStartPre, ExecStartPost, ExecReload, ExecStop y ExecStopPost.
  • Directivas de control de fallos: RestartSec (tiempo a esperar para reiniciar la unidad tras un fallo), Restart (directiva para definir nuestra política de reinicio cuyo valor puede ser always, on-success, on-failure, on-abnormal, on-abort o on-watchdog) o TimeoutSec (directiva con la que especificamos cuanto tiempo esperar para que la unidad sea considerada como fallida).
  • Otras directivas: A modo de ejemplo, Environment se encargaría de pasar variables de entorno a nuestra aplicación (se pueden utilizar tantas como queramos), mientras que User y Group se encargarían de asignar permisos de ejecución. PIDFile también es útil cuando queramos asociar un fichero pid a nuestro servicio.

Puedes consultar la documentación sobre estas secciones así como sus directivas en Unit/Install y Service.

Vamos al lío. Nuestro servicio basado en Node.js lo definiremos en un fichero llamado app.service con el siguiente contenido:

app.service
[Unit]
Description=Node HTTP service

[Service]
Environment="MY_PORT=3000"
ExecStart=/usr/bin/node /srv/http/app/index.js
Restart=on-failure

[Install]
WantedBy=multi-user.target

Sencillo, ¿verdad?

Podríamos utilizar más directivas como RestartSec pero en principio vamos a dejar esas directivas con sus valores por defecto.

En cuanto las directivas de control de flujo (como ExecStart), hay una opción que nos ayudará muy a menudo. Cambiando ExecStart=/usr/bin/node /srv/http/app/index.js por ExecStart=-/usr/bin/node /srv/http/app/index.js (es decir, añadiendo un guión antes del comando) evitaremos que la unidad sea considerada como fallida en caso de que el comando devuelva un resultado considerado como erróneo, evitando así el reinicio, el reporte en los registros, etc. Esto es especialmente útil cuando queramos ejecutar comandos que sean opcionales y no nos importe si la salida es válida o errónea, puesto que el error será silenciado e ignorado (silenciado).

Si te preguntas por qué no hemos añadido After=network.target, es porque el target multi-user ya depende de la conexión de red. Más adelante veremos qué son los targets y como hacer para habilitar nuestro servicio en uno (en este caso multi-user) para que se encargue de arrancar nuestro proceso automáticamente tras iniciar/reiniciar el sistema.

Templates

A pesar de que no hemos utilizado plantillas para nuestra unidad si merece la pena hacer una mención a dicha funcionalidad. Como adivinarás, su uso es perfecto para la clusterización de componentes.

Para generar una plantilla llamaremos al fichero app@.service (añadir un símbolo arroba). Esto será un placeholder para que nuestras instancias adquieran el nombre de app@1.service, app@2.service, etc.

Después utilizaríamos el argumento generado a través de la variable %i. Podríamos generar una variable de entorno tipo Environment=LISTEN_PORT=300%i, y así en nuestra aplicación recibiríamos dicha variable para así ejecutar varias instancias de nuestra aplicación corriendo bajo diferentes puertos (3001, 3002, etc).

Targets

Los targets sirven para agrupar unidades. Pueden compararse a runlevels en otros sistemas de inicio aunque a diferencia de estos, una unidad puede pertenecer a varios targets al mismo tiempo e incluso un target puede agrupar otros targets. Veamos algunos ejemplos para entenderlo mejor.

Un caso sencillo podría ser que tuvieramos un servicio que arranca con el sistema y necesita de interfaz gráfica para funcionar. En ese caso, esta unidad formaría parte del target llamado graphical.target.

Otro ejemplo sería un servicio que se encargase de reproducir música de alguna radio online y se iniciase al arrancar el sistema. Tiene sentido que esta unidad dependa de sound.target y network.target, ¿verdad?

En el caso de targets agrupando targets, el caso más claro es multi-user, un target que al ejecutarse indica a nuestro sistema que ya está preparado para aceptar inicios de sesión de usuarios del sistema. Este target depende directa e indirectamente de otros, como por ejemplo systemd-networkd.target, swap.target o getty.target, por lo que si tenemos disponible el target multi-user, será porque los otros se han iniciado correctamente.

Cuando instalemos una unidad en un target lo que en realidad se crea es un enlace simbólico (symlink). Estos enlaces se encuentran en la ruta /etc/systemd/system/*.target.wants/ (por ejemplo /etc/systemd/system/multi-user.target.wants/ en el caso de multi-user.target). En el caso de utilizar la ruta de usuario sería /home/$USER/.config/systemd/user/*.target.wants/.

systemctl

Ahora mismo os estaréis preguntando como hacer para arrancar una unidad, instalarla, etc. Bienvenidos a systemctl.

systemctl es un comando para administrar y controlar el funcionamiento de systemd. En este apartado veremos unos cuantos usos generales pero útiles.

Ser o no ser

systemctl corre bajo el usuario root así que necesita permisos de administrador, a no ser que utilicemos el parámetro --user, algo útil para correr servicios bajo nuestro usuario.

Arrancar y parar

Para arrancar una unidad:

systemctl --user start app.service

Para parar una unidad:

systemctl --user stop app.service

Reiniciar y recargar

Para reiniciar una unidad:

systemctl --user restart app.service

Si nuestra aplicación es capaz de recargar su configuración sin reiniciar podremos utilizar:

systemctl --user reload app.service

Y si no estamos seguros de ello:

systemctl --user reload-or-restart app.service

Habilitar y deshabilitar

Para que nuestra unidad arranque al iniciar el sistema (o más bien, cuando se inicie el target asociado a nuestra unidad), debemos habilitar la unidad mediante el siguiente comando:

systemctl --user enable app.service
¡No funciona!

Habilitar una unidad no hace que arranque en ese preciso momento. Para eso debemos utilizar systemctl start app.service o simplemente systemctl --user enable --now app.service.

Para deshabilitarla:

systemctl --user disable app.service

Recordemos que esto lo que hace es crear o borrar un enlace simbólico.

Estado

Para comprobar el estado de nuestra aplicación, utilizaremos:

systemctl --user status app.service
● app.service - Node HTTP service
   Loaded: loaded (/home/$USER/.config/systemd/user/app.service; disabled; vendor preset: disabled)
   Active: active (running) since jue 2016-10-06 18:58:00 CEST; 5s ago
 Main PID: 30453 (node)
    Tasks: 6 (limit: 4915)
   Memory: 8.4M
      CPU: 77ms
   CGroup: /system.slice/app.service
           └─30453 /usr/bin/node /srv/http/app/index.js

oct 06 18:58:00 earth systemd[1]: Started Node HTTP service.
oct 06 18:58:00 earth node[30453]: Server running at http://127.0.0.1:3000/

También podemos comprobar el estado de nuestra unidad de una manera más directa mediante varios comandos:

systemctl --user is-active app.service
systemctl --user is-enabled app.service
systemctl --user is-failed app.service

Enmascarar y desenmascarar

Si necesitamos enmascarar nuestra unidad para que no arranque de ninguna manera (ni automática ni manualmente), podemos utilizar:

systemctl --user mask app.service

Esto hará que nuestra unidad apunte a /dev/null y no se inicie.

Y para desenmascarar:

systemctl --user unmask app.service

Ver, editar y borrar unidades

Estas operaciones pueden realizarse por separado o mediante systemctl. Para ver el contenido de una unidad, systemctl nos provee del siguiente comando:

systemctl --user cat app.service

Y si queremos acceder a una información de más bajo nivel lo hacemos mediante:

systemctl --user show app.service

Para editar una unidad contamos con edit, aunque no funciona tal como esperarías. Cuando editamos una unidad en realidad estamos creando un drop-in. Se crea una carpeta asociada a cada unidad llamada /etc/systemd/system/app.service.d/ (o /home/$USER/.config/systemd/user/app.service.d/) y dentro se crea un fichero que se encarga de proveer cambios a la unidad. En este fichero podemos reemplazar directivas a nuestro gusto, añadir nuevas, o devolver directivas a su estado inicial.

systemctl --user edit app.service

Si queremos editar la unidad sin utilizar drop-ins, lo hacemos de la siguiente manera:

systemctl --user edit --full app.service

Podemos borrar tanto drop-ins como la unidad entera mediante el comando rm.

Si hemos realizado modificaciones sin utilizar systemctl edit o hemos borrado algo mediante rm, informaremos a systemctl de nuestros cambios ejecutando este comando:

systemctl --user daemon-reload

Instancias

Si utilizamos plantillas podemos realizar operaciones a múltiples instancias de la siguiente manera:

systemctl --user start app@1.service
systemctl --user start app@2.service

También podemos realizar operaciones en varias unidades a la vez mediante la siguiente sintaxis (esto depende de nuestro intérprete de comandos):

systemctl --user start app@{1,2,3,4,5}.service
systemctl --user start app@{1..5}.service

Targets

Por supuesto systemctl nos provee de comandos para realizar operaciones sobre targets, como por ejemplo cambiar el target por defecto del sistema, poner la máquina en un target específico, etc. En nuestro caso, solo vamos a ver los comandos para consultar la lista de targets que tenemos disponibles en nuestra máquina.

Versión resumida:

systemctl --user list-unit-files --type=target

Versión detallada:

systemctl --user list-units --type=target

Mediante --type podemos filtrar unidades de otros tipos.

Dependencias

Para consultar la lista de dependencias tanto de nuestras unidades como de los targets utilizaremos:

systemctl --user list-dependencies app.service
systemctl --user list-dependencies multi-user.target

journalctl

Ya sabemos como crear, iniciar, parar y en general, administrar nuestras aplicaciones mediante systemd, pero esta completa suite aún tiene algo poderoso que ofrecernos: journalctl.

journalctl es un comando para visualizar registros (logs) de nuestras unidades (o del sistema en general). Vamos a ver unos cuantos comandos útiles para facilitarnos el día a día.

Si simplemente ejecutamos journalctl veremos registros de todo nuestro sistema desde el principio de los tiempos. El primer argumento que nos vendrá bien es --utc, que como habrás podido adivinar nos mostrará los registros con fecha y hora acorde a UTC.

Root o no root

Recuerda que journalctl también corre bajo el usuario root. Si vas a monitorizar servicios del sistema utiliza sudo, o el parámetro --user si solo vas a consultar registros de un servicio que corre bajo tu usuario.

Filtrar por inicio de máquina

journalctl es capaz de segmentar nuestros registros por inicios del sistema. Para ver los registros desde el último reinicio de la máquina utilizaremos:

journalctl -b

Si queremos ver los registros del penúltimo reinicio, utilizaremos:

journalctl -b -1

Luego iría -2, -3... y así sucesivamente. Si queremos consultar cuantos reinicios han habido así como su posición, identificador y rango de fechas, utilizaremos:

journalctl --list-boots
-2 ae4450adc26e47c69f943bf54c1ec488 dom 2016-08-28 08:43:36 CEST—sáb 2016-09-10 23:29:04 CEST
-1 16e0c81dff134320920dc07822ddc4b3 sáb 2016-09-10 23:29:38 CEST—mié 2016-09-21 18:54:58 CEST
 0 31c4459b64c1449184700bd6cccb09aa mié 2016-09-21 19:17:47 CEST—jue 2016-10-06 19:55:08 CEST

Filtrar por fecha

Para filtrar por fecha utilizaremos los argumentos since y until, especificando el valor en formato YYYY-MM-DD HH:MM:SS. Ejemplo:

journalctl --since "2016-10-01" --until "2016-10-07 01:00"
Omitir datos

Podemos omitir fragmentos como los segundos o la hora entera y estos adoptarán el valor 00.

También reconoce otros formatos relativos como yesterday, today o now:

journalctl --since yesterday --until "2 hours ago"

Filtrar por unidad

Si solo queremos ver los registros de una unidad en concreto utilizaremos el argumento u (de unit):

journalctl -u app.service

Otros argumentos

Merece la pena destacar otros argumentos como el formato de salida (output) que acepta valores como json o json-pretty entre otros. Ejemplo:

journalctl -o json
journalctl -o json-pretty

El argumento n nos mostrará los últimos N registros, siendo por defecto 10 si no le indicamos un número.

journalctl -n
journalctl -n 20
journalctl -n 100

Y por último pero no menos importante, el argumento f seguirá las actualizaciones del registro en tiempo real, de igual manera que haríamos con tail -f:

journalctl -f

Conclusión

Hemos visto como funciona systemd a grandes rasgos: como crear unidades, como administrarlas mediante systemctl, y como journalctl se convierte en nuestro poderoso aliado para monitorizar y depurar el estado de nuestros procesos y servicios.

Por supuesto el potencial de toda esta suite daría como para escribir una biblia. Aquí hemos visto, de manera rápida, el funcionamiento básico para poder empezar a utilizar systemd para nuestros servicios, evitando utilizar herramientas específicas que solo nos servirían para casos específicos.

Puedes apoyarme para que pueda dedicar aún más tiempo a escribir artículos y tener recursos para crear nuevos proyectos. ¡Gracias!