Qué es systemd y cómo puede ayudarnos a gestionar aplicaciones en forma de servicios

Con systemd podrás convertir tus aplicaciones en servicios administrados y monitorizados sin necesidad de aplicaciones externas.

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 procesos y servicios de nuestro sistema fácilmente. 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.

systemd nos ofrece multitud de herramientas para administrar nuestro sistema
systemd nos ofrece multitud de herramientas para administrar nuestro sistema

En principio este artículo iba a ser una pequeña guía de como utilizar systemd para gestionar nuestra aplicación Node.js, pero finalmente se ha convertido en un artículo un poco más extenso que en vez de ofrecer un trozo de código para crear nuestro servicio y un par de comandos para iniciarlo y pararlo, veremos como desenvolvernos en esta suite mediante comandos que utilizaremos en el día a día para administrar de manera completa nuestros servicios.

En el caso de Node.js, 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 bien 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.

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:

[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.

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, por lo que podríamos generar una variable de entorno tipo Environment=LISTEN_PORT=300%i y así, en nuestra aplicación Node.js, recibiríamos dicha variable y podríamos ejecutar varias instancias de nuestra aplicación corriendo bajo diferentes puertos (3001, 3002, etc).

Targets

Los targets sirven para agrupar unidades básicamente. 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 para que arranque automáticamente cuando el sistema cargue dicho target, lo que en realidad se crea es un enlace simbólico (symlink). Estos enlaces se encuentran en la ruta /etc/systemd/system/multi-user.target.wants/ (en el caso de multi-user.target por ejemplo).

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.

systemctl necesita permisos de administrador del sistema por lo que si no eres root, utiliza el comando sudo.

Podemos omitir .service ya que systemd sabe que probablemente nos referimos a un servicio, aunque en el artículo seguiré utilizándolo por claridad.

Arrancar y parar

Para arrancar una unidad:

systemctl start app.service

Para parar una unidad:

systemctl stop app.service

Reiniciar y recargar

Para reiniciar una unidad:

systemctl restart app.service

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

systemctl reload app.service

Y si no estamos seguros de ello:

systemctl 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 enable app.service

Habilitar una unidad no hace que arranque en ese preciso momento. Para eso debemos utilizar systemctl start app.service.

Para deshabilitarla:

systemctl 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 status app.service
● app.service - Node HTTP service
   Loaded: loaded (/etc/systemd/system/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 is-active app.service
systemctl is-enabled app.service
systemctl 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 mask app.service

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

Y para desenmascarar:

systemctl 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 cat app.service

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

systemctl 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 llamada /etc/systemd/system/app.service.d/ (la ruta cambia en cada unidad) 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 edit app.service

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

systemctl 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 daemon-reload

Instancias

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

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

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

systemctl start app@{1,2,3,4,5}.service
systemctl 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 list-unit-files --type=target

Versión detallada:

systemctl list-units --type=target

Exacto, 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 list-dependencies app.service
systemctl 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 argumentos útiles para facilitarnos el día a día.

Por supuesto, si simplemente introducimos 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.

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"

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

También reconoce otros formatos y palabras 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 y la mezcla de sus argumentos convierten a este comando en un 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 un libro bastante extenso, pero aquí hemos visto de manera rápida el funcionamiento básico para poder empezar a utilizar systemd para nuestros servicios y así evitar utilizar herramientas específicas que solo nos servirían para casos específicos.

¿Te ha sido de utilidad este artículo? ¡Puedes dejar un comentario aquí abajo!

Compartir en

Facebook Twitter Google+ LinkedIn