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.socketpara usar una conexión dúplex resty.execya no utiliza el argumentobufsizeresty.execahora acepta un argumentotimeout, 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.