Saltar a contenido

exec: Ejecutar programas externos en nginx-module-lua sin crear un shell o bloquear

Instalación

Si no has configurado la suscripción al repositorio RPM, regístrate. Luego puedes proceder con los siguientes pasos.

CentOS/RHEL 7 o Amazon Linux 2

yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-exec

CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-exec

Para usar esta biblioteca Lua con NGINX, asegúrate de que nginx-module-lua esté instalado.

Este documento describe lua-resty-exec v3.0.3 lanzado el 22 de agosto de 2017.


Un pequeño módulo Lua para ejecutar procesos. Está destinado principalmente a ser utilizado con OpenResty, pero también funcionará en aplicaciones Lua regulares. Cuando se usa con OpenResty, es completamente no bloqueante (de lo contrario, recurre a usar LuaSocket y sí bloquea).

Es similar a (y está inspirado en) lua-resty-shell, la principal diferencia es que este módulo utiliza sockexec, que no crea un shell; en su lugar, proporcionas un array de cadenas de argumentos, lo que significa que no necesitas preocuparte por las reglas de escape/citación/análisis del shell.

Además, a partir de la versión 2.0.0, puedes usar resty.exec.socket para acceder a una interfaz de nivel inferior que permite la comunicación bidireccional con programas. ¡Puedes leer y escribir en aplicaciones en ejecución!

Esto requiere que tu servidor web tenga una instancia activa de sockexec en ejecución.

Changelog

  • 3.0.0
  • nuevo campo devuelto: unknown - si esto sucede, ¡por favor envíame un error!
  • 2.0.0
  • Nuevo módulo resty.exec.socket para usar una conexión dúplex
  • resty.exec ya no utiliza el argumento bufsize
  • resty.exec ahora acepta un argumento timeout, especificado en milisegundos, por defecto 60s
  • Esta es una revisión importante, ¡por favor prueba a fondo antes de actualizar!
  • No hay changelog antes de 2.0.0

Uso de resty.exec

local exec = require'resty.exec'
local prog = exec.new('/tmp/exec.sock')

Crea un nuevo objeto prog, utilizando /tmp/exec.sock para su conexión a sockexec.

A partir de ahí, puedes usar prog de un par de maneras diferentes:

modo ez

local res, err = prog('uname')

-- res = { stdout = "Linux\n", stderr = nil, exitcode = 0, termsig = nil }
-- err = nil

ngx.print(res.stdout)

Esto ejecutará uname, sin datos en stdin.

Devuelve una tabla de códigos de salida/error, con err establecido en cualquier error encontrado.

Configurar argv de antemano

prog.argv = { 'uname', '-a' }
local res, err = prog()

-- res = { stdout = "Linux localhost 3.10.18 #1 SMP Tue Aug 2 21:08:34 PDT 2016 x86_64 GNU/Linux\n", stderr = nil, exitcode = 0, termsig = nil }
-- err = nil

ngx.print(res.stdout)

Configurar stdin de antemano

prog.stdin = '¡esto es genial!'
local res, err = prog('cat')

-- res = { stdout = "¡esto es genial!", stderr = nil, exitcode = 0, termsig = nil }
-- err = nil

ngx.print(res.stdout)

Llamar con argv explícito, datos de stdin, callbacks de stdout/stderr

local res, err = prog( {
    argv = 'cat',
    stdin = '¡divertido!',
    stdout = function(data) print(data) end,
    stderr = function(data) print("error:", data) end
} )

-- res = { stdout = nil, stderr = nil, exitcode = 0, termsig = nil }
-- err = nil
-- '¡divertido!' se imprime

Nota: aquí argv es una cadena, lo cual está bien si tu programa no necesita argumentos.

Configurar callbacks de stdout/stderr

Si estableces prog.stdout o prog.stderr en una función, se llamará para cada fragmento de datos de stdout/stderr recibido.

Ten en cuenta que no hay garantías de que stdout/stderr sea una cadena completa, ¡ni nada particularmente sensato al respecto!

prog.stdout = function(data)
    ngx.print(data)
    ngx.flush(true)
end

local res, err = prog('some-program')

Tratar los timeouts como no errores

Por defecto, sockexec trata un timeout como un error. Puedes desactivar esto estableciendo la clave timeout_fatal del objeto en false. Ejemplos:

-- establecer timeout_fatal = false en los objetos prog
prog.timeout_fatal = false

-- o, establecerlo en el momento de la llamada:
local res, err = prog({argv = {'cat'}, timeout_fatal = false})

¡Pero realmente quiero un shell!

¡No hay problema! Solo puedes hacer algo como:

local res, err = prog('bash','-c','echo $PATH')

O si quieres ejecutar un script completo:

prog.stdin = script_data
local res, err = prog('bash')

-- esto es aproximadamente equivalente a ejecutar `bash < script` en la CLI

Desacoplando procesos

Generalmente, recomiendo no desacoplar procesos; creo que es mucho mejor usar algún tipo de cola de mensajes y/o sistema de supervisión, para que puedas monitorear procesos, tomar acciones en caso de fallos, etc.

Dicho esto, si quieres iniciar algún proceso, podrías usar start-stop-daemon, es decir:

local res, err = prog('start-stop-daemon','--pidfile','/dev/null','--background','--exec','/usr/bin/sleep', '--start','--','10')

esto iniciará sleep 10 como un proceso en segundo plano desacoplado.

Si no quieres lidiar con start-stop-daemon, tengo una pequeña utilidad para iniciar un programa en segundo plano llamada idgaf, es decir:

local res, err = prog('idgaf','sleep','10')

Esto logrará básicamente lo mismo que hace start-stop-daemon sin requerir un millón de flags.

Uso de resty.exec.socket

local exec_socket = require'resty.exec.socket'

-- puedes especificar el timeout en milisegundos, opcional
local client = exec_socket:new({ timeout = 60000 })

-- cada nueva instancia de programa requiere una nueva
-- llamada a connect
local ok, err = client:connect('/tmp/exec.sock')

-- enviar argumentos del programa, solo acepta una tabla de
-- argumentos
client:send_args({'cat'})

-- enviar datos para stdin
client:send('hola ahí')

-- recibir datos
local data, typ, err = client:receive()

-- `typ` puede ser uno de:
--    `stdout`   - datos de stdout del programa
--    `stderr`   - datos de stderr del programa
--    `exitcode` - el código de salida del programa
--    `termsig`  - si se terminó a través de una señal, qué señal se utilizó

-- si `err` está establecido, data y typ serán nil
-- valores comunes de `err` son `closed` y `timeout`
print(string.format('Recibidos %s datos: %s',typ,data))
-- imprimirá 'Recibidos stdout datos: hola ahí'

client:send('hey este proceso de cat todavía está corriendo')
data, typ, err = client:receive()
print(string.format('Recibidos %s datos: %s',typ,data))
-- imprimirá 'Recibidos stdout datos: hey este proceso de cat todavía está corriendo'

client:send_close() -- cierra stdin
data, typ, err = client:receive()
print(string.format('Recibidos %s datos: %s',typ,data))
-- imprimirá 'Recibidos exitcode datos: 0'

data, typ, err = client:receive()
print(err) -- imprimirá 'closed'

Métodos del objeto client:

  • ok, err = client:connect(path)

Conecta a través de un socket unix a la ruta dada. Si esto se está ejecutando en nginx, la cadena unix: se añadirá automáticamente.

  • bytes, err = client:send_args(args)

Envía una tabla de argumentos a sockexec y comienza el programa.

  • bytes, err = client:send_data(data)

Envía data a la entrada estándar del programa.

  • bytes, err = client:send(data)

Solo un acceso directo a client:send_data(data).

  • bytes, err = client:send_close()

Cierra la entrada estándar del programa. También puedes enviar una cadena vacía, como client:send_data('').

  • data, typ, err = client:receive()

Recibe datos del proceso en ejecución. typ indica el tipo de datos, que puede ser stdout, stderr, termsig, exitcode.

err es típicamente closed o timeout.

  • client:close()

Cierra forzosamente la conexión del cliente.

  • client:getfd()

Un método getfd, útil si quieres monitorear la conexión de socket subyacente en un bucle de selección.

Algunos ejemplos de configuraciones de nginx

Suponiendo que estás ejecutando sockexec en /tmp/exec.sock

$ sockexec /tmp/exec.sock

Luego en tu configuración de nginx:

location /uname-1 {
    content_by_lua_block {
        local prog = require'resty.exec'.new('/tmp/exec.sock')
        local data,err = prog('uname')
        if(err) then
            ngx.say(err)
        else
            ngx.say(data.stdout)
        end
    }
}
location /uname-2 {
    content_by_lua_block {
        local prog = require'resty.exec'.new('/tmp/exec.sock')
        prog.argv = { 'uname', '-a' }
        local data,err = prog()
        if(err) then
            ngx.say(err)
        else
            ngx.say(data.stdout)
        end
    }
}
location /cat-1 {
    content_by_lua_block {
        local prog = require'resty.exec'.new('/tmp/exec.sock')
        prog.stdin = '¡esto es genial!'
        local data,err = prog('cat')
        if(err) then
            ngx.say(err)
        else
            ngx.say(data.stdout)
        end
    }
}
location /cat-2 {
    content_by_lua_block {
        local prog = require'resty.exec'.new('/tmp/exec.sock')
        local data,err = prog({argv = 'cat', stdin = 'increíble'})
        if(err) then
            ngx.say(err)
        else
            ngx.say(data.stdout)
        end
    }
}
location /slow-print {
    content_by_lua_block {
        local prog = require'resty.exec'.new('/tmp/exec.sock')
        prog.stdout = function(v)
            ngx.print(v)
            ngx.flush(true)
        end
        prog('/usr/local/bin/slow-print')
    }
    # busca en `/misc` de este repositorio para `slow-print`
}
location /shell {
    content_by_lua_block {
        local prog = require'resty.exec'.new('/tmp/exec.sock')
        local data, err = prog('bash','-c','echo $PATH')
        if(err) then
            ngx.say(err)
        else
            ngx.say(data.stdout)
        end
    }
}

GitHub

Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-exec.