Aller au contenu

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.socket pour utiliser une connexion duplex
  • resty.exec n'utilise plus l'argument bufsize
  • resty.exec accepte désormais un argument timeout, 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.