exec: Exécuter des programmes externes dans nginx-module-lua sans lancer un shell ou bloquer
Installation
Si vous n'avez pas configuré d'abonnement au dépôt RPM, inscrivez-vous. Ensuite, vous pouvez procéder avec les étapes suivantes.
CentOS/RHEL 7 ou 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
Pour utiliser cette bibliothèque Lua avec NGINX, assurez-vous que nginx-module-lua est installé.
Ce document décrit lua-resty-exec v3.0.3 publié le 22 août 2017.
Un petit module Lua pour exécuter des processus. Il est principalement destiné à être utilisé avec OpenResty, mais fonctionnera également dans des applications Lua classiques. Lorsqu'il est utilisé avec OpenResty, il est complètement non-bloquant (sinon, il revient à utiliser LuaSocket et bloque).
Il est similaire à (et inspiré par) lua-resty-shell, la principale différence étant que ce module utilise sockexec, qui ne lance pas de shell - à la place, vous fournissez un tableau de chaînes d'arguments, ce qui signifie que vous n'avez pas à vous soucier des règles d'échappement/de citation/de parsing du shell.
De plus, à partir de la version 2.0.0, vous pouvez utiliser resty.exec.socket pour accéder à une interface de bas niveau qui permet une communication bidirectionnelle avec les programmes. Vous pouvez lire et écrire dans des applications en cours d'exécution !
Cela nécessite que votre serveur web ait une instance active de sockexec en cours d'exécution.
Changelog
3.0.0- nouveau champ retourné :
unknown- si cela se produit, veuillez m'envoyer un bug ! 2.0.0- Nouveau module
resty.exec.socketpour utiliser une connexion duplex resty.execn'utilise plus l'argumentbufsizeresty.execaccepte désormais un argumenttimeout, spécifié en millisecondes, par défaut à 60s- Il s'agit d'une révision majeure, veuillez tester soigneusement avant de mettre à niveau !
- Pas de changelog avant
2.0.0
Utilisation de resty.exec
local exec = require'resty.exec'
local prog = exec.new('/tmp/exec.sock')
Crée un nouvel objet prog, utilisant /tmp/exec.sock pour sa connexion à sockexec.
À partir de là, vous pouvez utiliser prog de plusieurs manières différentes :
mode ez
local res, err = prog('uname')
-- res = { stdout = "Linux\n", stderr = nil, exitcode = 0, termsig = nil }
-- err = nil
ngx.print(res.stdout)
Cela exécutera uname, sans données sur stdin.
Retourne une table de codes de sortie/d'erreur, avec err défini sur toute erreur rencontrée.
Configurer argv à l'avance
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)
Configurer stdin à l'avance
prog.stdin = 'c\'est génial !'
local res, err = prog('cat')
-- res = { stdout = "c'est génial !", stderr = nil, exitcode = 0, termsig = nil }
-- err = nil
ngx.print(res.stdout)
Appeler avec argv explicite, données stdin, callbacks stdout/stderr
local res, err = prog( {
argv = 'cat',
stdin = 'fun !',
stdout = function(data) print(data) end,
stderr = function(data) print("erreur :", data) end
} )
-- res = { stdout = nil, stderr = nil, exitcode = 0, termsig = nil }
-- err = nil
-- 'fun !' est imprimé
Remarque : ici argv est une chaîne, ce qui est acceptable si votre programme n'a pas besoin d'arguments.
Configurer des callbacks stdout/stderr
Si vous définissez prog.stdout ou prog.stderr sur une fonction, elle sera appelée pour chaque morceau de données stdout/stderr reçu.
Veuillez noter qu'il n'y a aucune garantie que stdout/stderr soit une chaîne complète, ou quoi que ce soit de particulièrement sensé à ce sujet !
prog.stdout = function(data)
ngx.print(data)
ngx.flush(true)
end
local res, err = prog('some-program')
Traiter les timeouts comme des non-erreurs
Par défaut, sockexec traite un timeout comme une erreur. Vous pouvez désactiver cela en définissant la clé timeout_fatal de l'objet sur false. Exemples :
-- définir timeout_fatal = false sur les objets prog
prog.timeout_fatal = false
-- ou, le définir au moment de l'appel :
local res, err = prog({argv = {'cat'}, timeout_fatal = false})
Mais je veux vraiment un shell !
Pas de problème ! Vous pouvez simplement faire quelque chose comme :
local res, err = prog('bash','-c','echo $PATH')
Ou si vous voulez exécuter un script entier :
prog.stdin = script_data
local res, err = prog('bash')
-- cela équivaut à exécuter `bash < script` dans le CLI
Daemoniser des processus
Je recommande généralement de ne pas daemoniser des processus - je pense qu'il est bien mieux d'utiliser une sorte de file d'attente de messages et/ou un système de supervision, afin que vous puissiez surveiller les processus, prendre des mesures en cas d'échec, etc.
Cela dit, si vous souhaitez lancer un processus, vous pouvez utiliser start-stop-daemon, c'est-à-dire :
local res, err = prog('start-stop-daemon','--pidfile','/dev/null','--background','--exec','/usr/bin/sleep', '--start','--','10')
Cela lancera sleep 10 en tant que processus détaché en arrière-plan.
Si vous ne voulez pas traiter avec start-stop-daemon, j'ai un petit utilitaire pour lancer un programme en arrière-plan appelé idgaf, c'est-à-dire :
local res, err = prog('idgaf','sleep','10')
Cela accomplira essentiellement la même chose que start-stop-daemon sans nécessiter un milliard de flags.
Utilisation de resty.exec.socket
local exec_socket = require'resty.exec.socket'
-- vous pouvez spécifier un timeout en millisecondes, optionnel
local client = exec_socket:new({ timeout = 60000 })
-- chaque nouvelle instance de programme nécessite un nouvel
-- appel à connect
local ok, err = client:connect('/tmp/exec.sock')
-- envoyer des arguments de programme, n'accepte qu'un tableau de
-- arguments
client:send_args({'cat'})
-- envoyer des données pour stdin
client:send('bonjour là-bas')
-- recevoir des données
local data, typ, err = client:receive()
-- `typ` peut être l'un des :
-- `stdout` - données de stdout du programme
-- `stderr` - données de stderr du programme
-- `exitcode` - le code de sortie du programme
-- `termsig` - si terminé via un signal, quel signal a été utilisé
-- si `err` est défini, data et typ seront nil
-- les valeurs `err` courantes sont `closed` et `timeout`
print(string.format('Données reçues %s : %s',typ,data))
-- imprimera 'Données reçues stdout : bonjour là-bas'
client:send('hey ce processus cat est toujours en cours d\'exécution')
data, typ, err = client:receive()
print(string.format('Données reçues %s : %s',typ,data))
-- imprimera 'Données reçues stdout : hey ce processus cat est toujours en cours d\'exécution'
client:send_close() -- ferme stdin
data, typ, err = client:receive()
print(string.format('Données reçues %s : %s',typ,data))
-- imprimera 'Données reçues exitcode : 0'
data, typ, err = client:receive()
print(err) -- imprimera 'closed'
Méthodes de l'objet client :
ok, err = client:connect(path)
Se connecte via un socket unix au chemin donné. Si cela s'exécute dans nginx, la chaîne unix: sera automatiquement préfixée.
bytes, err = client:send_args(args)
Envoie un tableau d'arguments à sockexec et démarre le programme.
bytes, err = client:send_data(data)
Envoie data à l'entrée standard du programme.
bytes, err = client:send(data)
Juste un raccourci vers client:send_data(data)
bytes, err = client:send_close()
Ferme l'entrée standard du programme. Vous pouvez également envoyer une chaîne vide, comme client:send_data('')
data, typ, err = client:receive()
Reçoit des données du processus en cours d'exécution. typ indique le type de données, qui peut être stdout, stderr, termsig, exitcode
err est généralement soit closed soit timeout
client:close()
Ferme de force la connexion client
client:getfd()
Une méthode getfd, utile si vous souhaitez surveiller la connexion socket sous-jacente dans une boucle select
Quelques exemples de configurations nginx
En supposant que vous exécutez sockexec à /tmp/exec.sock
$ sockexec /tmp/exec.sock
Ensuite, dans votre configuration 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 = 'c\'est génial !'
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 = 'génial'})
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')
}
# regardez dans `/misc` de ce dépôt pour `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
Vous pouvez trouver des conseils de configuration supplémentaires et de la documentation pour ce module dans le dépôt GitHub pour nginx-module-exec.