Publi

Crea rápidamente servicios con Systemd para iniciar demonios o ejecutar scripts


Hay quien lo odia, hay personas a quienes les gusta y también a quien no le importa. Pero es un sistema que ha entrado en las vidas de muchos de nosotros. Tanto usuarios como sysadmins. Ya que este post va a ser un post pequeño, quiero mostrar un ligero ejemplo paso a paso de la creación de un servicio utilizando systemd. El servicio será sencillo y se limitará a ejecutar un script durante el arranque y el apagado de nuestro sistema, nuestro servidor o nuestro cacharro (si hablamos de IoT).

¿Para qué podemos utilizarlo?

Systemd tiene muchísimas opciones, y esta configuración va a ser muy básica. En principio ejecutar un script de este tipo al arranque puede servir para actualizar automáticamente programas de nuestro servidor, o nuestra Raspberry. Por ejemplo yo lo suelo utilizar para:

  • Poner algún demonio en el arranque. Estos demonios pueden estar hechos en C, en Python, en Java, en Bash o en el lenguaje que queramos. Serán aplicaciones que permanecerán abiertas todo el tiempo para monitorizar o para proporcionar algún servicio.
  • Cargar reglas de iptables, con algún script de ayuda.
  • Actualizar el código de nuestra web desde un repositorio. Vamos, hacer un git pull antes de empezar a servir la web.
  • Enviar una señal para saludar a otros equipos de la red. Logearnos en algún sitio. O enviar nuestra IP externa de un cliente a nuestro servidor para poder ofrecerle mantenimiento.
  • Cargar módulos o realizar configuraciones personalizadas para el sistema. Pueden ser posibles workarounds o chapucillas, carga de módulos, etc.
  • ¿Sugerencias? Dejad un comentario con vuestros servicios personalizados.

Ejecutar scripts al inicio

Imaginemos un script sencillo como este (lo colocamos en /usr/local/bin/mystartup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash

cd /var/www/mi_proyecto
git pull origin master
STATUS=$?
if (( $STATUS!=0 )); then
   logger "Error al actualizar web"
   exit
fi

npm update
STATUS=$?
if (( $STATUS!=0 )); then
   logger "Error al actualizar npm"
   exit
fi

composer update
STATUS=$?
if (( $STATUS!=0 )); then
   logger "Error al actualizar composer"
   exit
fi

curl -X POST https://axon/machines/webserver -d status=up

Con este script actualizaremos el código de nuestra web (que lo tenemos en un repositorio git), luego actualizaremos los paquetes npm y composer de la misma. Además, tengo un demonio en otro equipo que controla todas las máquinas que tengo conectadas, avisa de posibles errores, da de alta y baja servidores en balanceadores de carga, etc. Es sólo un ejemplo.

A mí también me gusta hacer un script muy sencillo que se ejecuta cuando se apaga o reinicia el ordenador (/usr/local/bin/myshutdown):

1
2
3
4
#!/bin/bash

logger "Shutting down"
curl -X POST https://axon/machines/webserver -d status=down

Con esto, le digo al servidor que controla el estado de mis máquinas que ésta va a ser apagada.

Luego crearemos un archivo en /etc/systemd/system/mystartup.service:

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=My startup scripts
After=networking.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/mystartup
ExecStop=/usr/local/bin/myshutdown
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Con este archivo:

  • Establecemos una descripción (Description=) para el servicio.
  • Le decimos que debe ejecutarse después (After=) de la inicialización de la red (networking.target)
  • Es de tipo (Type=) oneshot. Se ejecutan los archivos y se espera a su finalización.
  • ExecStart y ExecEnd especifican los archivos que se ejecutan cuando el servicio se inicia y se detiene respectivamente.
  • RemainAfterExit indica que tras la ejecución del script el servicio se marca como activo.
  • WantedBy nos indica el objetivo que cumple el servicio. Normalmente multi-user.target se ejecuta siempre.

Activar e iniciar

Si no queremos reiniciar el ordenador (cosa que no nos gusta mucho a los usuarios de GNU/Linux, podemos utilizar (podemos utilizar sudo si no nos deja ejecutar los comandos con nuestro usuario actual):

systemctl daemon-reload

Con esto, habremos recargado la configuración del demonio y systemd tiene que haber reconocido nuestro nuevo servicio. Ahora podemos hacer:
systemctl start mystartup.service

O (en modo compatibilidad):
sudo service mystartup start

Una vez iniciado nuestro servicio y viendo que funciona bien podemos hacer lo siguiente:

systemctl enable mystartup.service

Para iniciarlo siempre que iniciemos nuestro sistema. Y, si queremos consultar información sobre el estado del servicio, debemos ejecutar:
systemctl status mystartup.service
● mystartup.service -- My startup scripts
Loaded: loaded (/etc/systemd/system/mystartup.service; disabled; vendor preset: enabled)
Active: active (exited) since dom 2017-11-19 17:58:25 CET; 29min ago
Process: 22085 ExecStart=/usr/local/bin/mystartup (code=exited, status=0/SUCCESS)
Main PID: 22085 (code=exited, status=0/SUCCESS)
nov 19 17:58:25 gaspy-RedDragoN systemd[1]: Starting My startup scripts…
nov 19 17:58:25 gaspy-RedDragoN systemd[1]: Started My startup scripts.

Tipos de servicio

Systemd no está limitado a esto, en los tipos (Type=) de servicio podemos utilizar, además de oneshot:

  • simple: ejecuta el proceso en segundo plano y continúa la inicialización de servicios.
  • forking: éste es para los demonios tradicionales UNIX, esos que se ejecutan en modo demonio. Que utilizan fork() para devolver el control al usuario dejando una versión en segundo plano del programa ejecutándose. Si usamos este modo, es aconsejable utilizar la opción PIDFile= especificando un archivo que almacene la PID del proceso del demonio, ya que systemd no tendrá acceso a ella de otra forma.
  • idle: ejecuta todo cuando no esté ejecutando nada más. Será útil si queremos ver la salida por consola de nuestro demonio o script sin que se mezcle con otros programas que se estén ejecutando.
  • notify: espera a que el servicio envíe una notificación vía sd_notify() es una función de C que encontramos en systemd/sd-daemon.h. Aunque haremos nuestro demonio muy dependiente de systemd.
  • dbus: el servicio figurará como iniciado cuando tenga un identificador dbus propio.

Reiniciar automáticamente el servicio

Al contrario que cuando ejecutamos un script, como en el ejemplo. Un script que tiene un comienzo y su ejecución tiene también un final. Nuestro servicio sea un demonio, es decir, un programa que deba ejecutarse en segundo plano y mantenerse ejecutando todo el tiempo. Por ejemplo, servidores web, websockets, cgi, bases de datos, APIs, servidores de archivos, y cosas así. En ese caso, necesitamos proporcionar un servicio constante en el tiempo. Y durante el tiempo de servicio pueden ocurrir desastres que hagan que nuestro demonio deje de ejecutarse. Estos pueden ser fallos de software, otro proceso lo ha cerrado (SIGINT, SIGTERM, SIGQUIT, SIGKILL, etc), Out Of Memory Kill, etc. En ese tipo de casos podemos estar dejando de proporcionar un servicio esperando que nuestro servidor sea atendido por un administrador que reinicie el servicio.

Es cierto que como administrador nos interesaría saber que ese servicio se ha reiniciado (y de hecho podemos consultar el log del demonio o incluso enviarnos un e-mail cuando eso pase), pero la solución inmediata del problema es reiniciar el servicio. Seguro que habrá veces que reiniciar el servicio no es suficiente, pero por experiencia son las menos. Muchas veces, incluso puede ocurrir una corrupción de memoria en un programa, o un memory leak que haga que nuestro demonio consuma cada vez más memoria y nada más reiniciar vuelve a funcionar. No estoy diciendo que dejemos así nuestros programas, pero a veces nos vemos obligados a hacer chapucillas para asegurar la continuidad del servicio. Otras veces, puede que el programa llegue a un punto que no hemos controlado en su desarrollo o simplemente se ha consumido la memoria del servidor y el kernel ha cerrado el programa. Son casos en los que no podemos permitir una parada, porque estas cosas siempre pasan cuando el sysadmin duerme…

Para eso tenemos la orden Restart= en la que indicamos en qué caso se reiniciará el servicio automáticamente por systemd. Sus posibles valores son:

  • no: No reinicia el servicio bajo ningún concepto.
  • on-success: Reinicia el servicio cuando éste haya terminado con un código de salida de éxito. Normalmente, un programa termina bien cuando su código de salida es 0. Aunque aquí se incluyen algunas señales como SIGHUP o SIGTERM, e incluso podemos utilizar SuccessExitStatus= para especificar qué consideramos una salida exitosa del programa.
  • on-abort: Reinicia el servicio cuando se envíe una señal y no se haya capturado. Por ejemplo, muchos programas no tienen nada programado cuando reciben un SIGUSR1 y por tanto terminan su ejecución de mala manera.
  • on-watchdog: systemd implementa un watchdog. Es decir, un sistema que detendrá nuestro servicio si éste no da señales de vida en un tiempo determinado. El mecanismo que tenemos para dar señales de vida es la función sd_notify() dentro de nuestro demonio. Y el tiempo del que disponemos entre llamada y llamada lo podemos especificar con WatchdogSec=. Si Restart tiene el valor on-watchdog, el servicio no se parará, sino que se reiniciará.
  • on-abnormal: El servicio se reiniciará si se cumple on-abort, on-watchdog o si se sobrepasa el tiempo de espera de arranque o apagado. En systemd podemos especificar el tiempo máximo que el servicio debe tardar en iniciarse (TimeoutStartSec=) o en pararse (TimeoutStopSec=). Los dos juntos se pueden especificar con (TimeoutSec=).
  • on-failure: El servicio se reiniciará si se cumple on-abnormal o el código de salida del programa no es exitoso.
  • always: Reiniciaremos el servicio si se cumple cualquier caso de los anteriores.

Casi siempre podemos utilizar:

Restart=always

Aunque con todas estas opciones podemos afinar un poco la configuración.

También es importante indicar que RestartSec= nos permite especificar el tiempo de espera antes de reiniciar. Además, puede darse el caso de un servicio que se reinicie todo el rato, por lo que podemos configurar StartLimitBurst= con el número de veces que se reiniciará el servicio y StartLimitIntervalSec= con el tiempo máximo en el que podemos reiniciar ese número de veces. Por tanto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=My startup scripts
After=networking.target

[Service]
Type=simple
ExecStart=/usr/local/bin/mydaemon
Restart=always
RestartSec=10
StartLimitBurst=5
StartLimitInterval=600

[Install]
WantedBy=multi-user.target

En este caso, si el servicio, durante 10 minutos (600 segundos), sólo se podrá reiniciar 5 veces (esperando 10 segundos antes de reiniciarse cada vez). Y si el servicio en esos 10 minutos no termina de arrancar, systemd desistirá y no lo intentará más hasta dentro de 10 minutos.

Por poder, podemos poner un script que cada diez minutos se reinicie dos veces y se pare. Como no es un demonio y va a finalizar siempre estaremos siempre reiniciándolo. Aunque para eso podemos utilizar por ejemplo cron, o hacer que nuestro script tenga esperas (sleep) con el fin de no recargar constantemente el programa, ya que todo esto tiene un coste para el sistema operativo.

Tareas previas y posteriores

Podemos, además, ejecutar programas antes y después de iniciar servicio con ExecStartPre=, ExecStartPost= que podemos poner todas las veces que queramos para ejecutar todos los programas o scripts que sean necesarios antes de iniciar el servicio y una vez éste esté iniciado.

También disponemos de Use ExecStopPost= para ejecutar comandos una vez se haya detenido el servicio.

Más información

Lo mejor para completar la información es ver el manual. Aquí vemos dos páginas que nos ayudarán:

Foto principal: Denys Nevozhai

También podría interesarte...

There are 3 comments left Ir a comentario

  1. Pingback: Crea rápidamente servicios con Systemd para iniciar demonios o ejecutar scripts | PlanetaLibre /

  2. 5duros /
    Usando Google Chrome Google Chrome 62.0.3202.94 en Windows Windows 7

    Buenas tardes,en mi tesis estoy utilizando la creación de servicios, para tal he creado un servicio que ejecute un comando, en concreto es este ikec -r “configuración” -u usuario -p contraseña -a , el cual establece una conexión VPN, utilizando el software “shrew software vpn”, pero en el log al iniciar el sistema, me dice que falla a la hora de cargar el archivo de configuración. Pero cuando ya ha cargado todo el SO y desde la interfaz gráfica ejecuto un shell y reinicio este servicio este empieza a funcionar correctamente.
    Por lo que mi pregunta es, como puedo iniciar este servicio despues de X tiempo (como si del cron se tratase, pero sin utilizar el cron) o que se ejecute cuando ya se ha cargado todo el SO?
    El archivo de mi servicio contiene esto:

    [Unit]
    Description= Custom automatic vpn configuration

    [Service]
    Type=simple
    ExecStart=/usr/local/bin/custom_vpn_start.sh
    Restart=always

    [Install]
    WantedBy=default.target

    1. Gaspar Fernández / Post Author
      Usando Mozilla Firefox Mozilla Firefox 57.0 en Ubuntu Linux Ubuntu Linux

      Podrías incluir algo dentro de [Service]

      1
      2
      [Service]
      ExecStartPre=/bin/sleep 30

      Con esto te aseguras de ejecutar sleep antes de cargar el servicio. Aunque no es lo más bonito, también podrías ver si necesitas que un servicio se cargue antes que el tuyo, y dentro de
      [Unit] colocar la configuración:

      1
      2
      [Unit]
       After=servicio

Leave a Reply