technologie

Cron v kontejnerizované webové aplikaci

Jak správně spouštět cronem příkazy v moderní webové aplikaci, která je napsaná v PHP, používá Symfony konzoli (nebo jiný command line entrypoint) a běží v Docker kontejnerech?

Popis problému

V moderních webových aplikacích postavených na PHP je většinou entrypoint skript určený ke spouštění konzolových příkazů (něco jako console.php). Ten je pak může zpracovávat sám nebo k tomu využít například Symfony Console. Příkazy se následně spouštějí buď ručně, nebo pomocí nastaveného cronu, který se o ně postará automaticky v daných intervalech. Jeho nastavení je na běžném serveru přímočaré – administrátor spustí příkaz crontab -e a napíše, co a jak často se má spouštět. Například pro spouštění příkazu každou minutu napíše něco jako:

* * * * *  php /path/to/console.php command [arguments...]

Co když ale běží webová aplikace v kontejnerech? Samozřejmě můžeme přidat crontab do PHP kontejneru, ale to není doporučený postup (best practise). Tím je totiž mít pro každý proces vlastní kontejner.

Jak vypadá běžná kontejnerizovaná webová aplikace?

Taková aplikace bude mít dva kontejnery: jeden s webovým serverem (například nginx, apache nebo libovolný jiný) a druhý s PHP-FPM. Běžně bude asi obsahovat i kontejner pro databázi nebo jiné perzistentní úložiště, který ale pro náš problém není podstatný.

Pokud přijde na server HTTP požadavek, bude nasměrován do kontejneru s webovým serverem. Ten odbaví požadavky na statický obsah. Pokud dostane požadavek na PHP skript, předá ho pomocí FastCGI do kontejneru s PHP-FPM.

Spouštění příkazů cronem

Pro spouštění příkazů v naplánovaných intervalech přidáme další kontejner. Máme tedy kontejner, který ví kdy co spustit (cron), a kontejner, který “to” spustit umí (PHP). Jediné, co zbývá, je oba kontejnery propojit.

Tady ale narážíme na největší problém. Jak můžeme pomocí jednoho kontejneru, který ví, kdy má jaký příkaz spustit, spustit příkaz v kontejneru druhém? Nepřemýšlejme nad tím, jak do kontejneru s PHP přidat třeba SSH démona. Tím bychom přidali další proces a rovnou bychom tam mohli nainstalovat přímo cron. Naštěstí nám stačí využít to, co už máme.

Máme PHP-FPM, které umí zpracovávat požadavky přes FastCGI protokol. K tomu máme připravený entrypoint pro požadavek, který přijde skrz webový server (typicky index.php). Tento entrypoint zjišťuje, co má dělat z HTTP požadavku (informace o něm jsou v polích $_SERVER, $_GET, $_POST a dalších). Pak máme entrypoint pro konzolové příkazy (typicky console.php), který zjišťuje, co má dělat pomocí argumentů předaných při spuštění příkazu (v poli $argv). Takže stačí zjistit, jak přes FastCGI protokol předat místo informací o HTTP požadavku tyto argumenty a máme vyhráno.

Bohužel tady narazíme, jelikož to prostě nejde. FastCGI je navržen pro rozhraní mezi aplikací a webovým serverem. Bylo by ale chybou tento směr vzdát kvůli takovému „detailu“. Proto si na pomoc vezmeme techniku, kterou ať už vědomě nebo ne, většina z nás ve webových aplikacích používá, a tou je serializace. Stačí argumenty na straně cronu serializovat a na druhé straně přidat PHP skript, který je dokáže deserializovat. Ten už můžeme bez problémů volat přes FastCGI. Hurá, máme hotovo. Pokud si stále nejste jistí, jak to teda všechno poskládat, nezoufejte. Koukněte do přiloženého Github repozitáře, kde najdete funkční příklad. Přece jenom kód vydá za tisíc slov.

jankonas/docker-cron-php - GitHub

Bezpečnost na prvním místě

Důležité je přemýšlet i nad bezpečností každého řešení. Konzolové příkazy mají většinou benevolentnější konfiguraci – mohou běžet déle než zpracování HTTP požadavku a v některých případech i konzumovat více paměti. Proto jsou v přiloženém příkladu odděleny dva PHP-FPM pooly a časový limit je zrušen pouze v tom, který se stará o spouštění konzolových příkazů. Na ten by bylo dobré omezit připojení pouze z kontejneru s cronem. Snadno tak lze učinit například nastavením direktivy listen.allowed_clients, nicméně tím, že tato direktiva přijímá pouze IP adresy a ne hostname, způsob jejího nastavení záleží na způsobu, jak docker kontejnery spouštíme na daném prostředí, a proto není součástí příkladu.

Dalším způsobem, jak zlepšit bezpečnost, může být přidání sdíleného tajemství mezi cron kontejnerem a skriptem, který zpracovává FastCGI požadavky na spuštění příkazu.

Máte otázky nebo připomínky?

Pokud máte nápady, jak popisované řešení vylepšit, nebo pokud si myslíte, že není ideální, otevřete issue v linkovaném repozitáři nebo mi napište na e-mail jan.konas@apploud.cz a rád to s vámi proberu.

Další články