Ambienti di esecuzione (RTE)
Cos’è un RTE, come esegue un playbook, e cosa può toccare
Per gli amministratori: capisci l’RTE prima di eseguirne uno, dimensionarlo o spostarlo in un’altra rete.
| Pagina | Cosa tratta |
|---|---|
| Immagini | Le tre immagini RTE, e come costruire la tua con le collection usate dai tuoi playbook |
| Modalità | In uscita, in entrata o database: chi apre la connessione, lo spool, i file del job |
| Eseguire un RTE | Le impostazioni di ogni modalità, e un esempio docker run di ciascuna |
| Più RTE | Capacità, repliche dietro un solo indirizzo, e cosa succede quando un RTE si ferma o muore |
| Un altro datacenter | Un runbook: aggiungere un RTE in un secondo datacenter, da un host vuoto al suo primo job |
Cos’è un RTE
AnsibleForms 7 non esegue nessun playbook nel container dell’app. Il job di un modulo ansible viene affidato a un runner di tipo rte, un ambiente di esecuzione: il server di AnsibleForms avviato con AF_ROLE=rte, accanto a python e ansible, senza interfaccia web. Parla con l’app tramite un unico WebSocket e non contiene credenziali del database (RTE_MODE outbound o inbound, vedi Modalità): l’app gli invia il job, l’RTE esegue ansible-playbook, e l’app salva l’output e lo stato finale che le rimanda, dove il browser li legge.
L’app risolve le credenziali del job e le consegna all’RTE sigillate solo per quell’RTE e quel job. L’RTE non legge mai la tabella delle credenziali né un archivio di segreti, e un playbook non eredita le variabili d’ambiente di AnsibleForms (ENCRYPTION_SECRET, DB_*, RTE_TOKEN e le altre): passa un valore del genere come extravar o come credenziale. L’RTE dalla 7.0 alla 7.2, che leggeva e scriveva da solo nel database, gira ancora nella 7.x come modalità database deprecata (vedi Modalità database).
Come gira l’RTE
L’RTE è un container che esegui tu, accanto all’app o in un’altra rete: AnsibleForms non ne crea mai uno. Qui si vede dove gira, cosa condivide con l’app, e cosa succede al suo interno dal momento in cui si avvia un modulo fino a quando il processo del playbook non c’è più:
Il diagramma mostra un socket per RTE, qualunque sia il lato che lo ha aperto: l’app contatta un RTE in entrata, un RTE in uscita contatta l’app (vedi Modalità). Una volta aperto, gli stessi messaggi viaggiano in entrambe le direzioni, e solo l’app legge e scrive nel database.
Chi lo avvia. Un RTE è un container di lunga durata, avviato da ciò che esegue i tuoi altri container. Attende i job e continua a girare tra un job e l’altro:
| Come esegui AnsibleForms | Chi avvia l’RTE |
|---|---|
docker run o podman run | Tu, con un comando come quello di Eseguire un RTE, su qualsiasi host che raggiunge l’app, o che l’app raggiunge |
| Docker Compose | Il servizio rte di docker-compose.yml, accanto ad app e mysqldb. Il suo config seed lo dichiara come runner predefinito rte, in entrata, su http://rte:8000 |
| Chart Helm | Kubernetes, come secondo container chiamato rte nel pod dell’app (containers.rte). L’app lo contatta su 127.0.0.1, in entrata, e il seed del chart lo dichiara come runner predefinito |
| L’installer | Docker Compose o il chart Helm come sopra, o un servizio systemd senza container; un Runner proprio, che chiede di unirsi e attende l’approvazione di un admin |
Il motore dei container è tuo: Docker o Podman sull’host, o il runtime dei container del tuo cluster Kubernetes, installato e mantenuto aggiornato da te. AnsibleForms non parla mai con esso. Né l’app né l’RTE chiamano Docker o Podman, ne montano il socket o avviano un container: l’app parla solo con l’RTE tramite il suo socket.
Cosa c’è dentro. Un solo container, con ciò che serve per eseguire un playbook e nient’altro:
- Il server : lo stesso server Node.js dell’app, avviato con
AF_ROLE=rte(impostato nell’immagine) sottotini - Cosa è escluso : l’interfaccia web, l’accesso, il pianificatore, il worker e ogni modulo del database, che questo ruolo non carica
- Su cosa è in ascolto : la sua
PORT(8000):/livee/readyper le probe e/metricsper Prometheus, e in entrata il socket che l’app contatta,/rte/v1/socket. Un RTE in uscita non accetta connessioni dall’app - Python e ansible : portati dal livello dell’immagine che scegli (vedi Le immagini RTE)
- L’utente : il server e ogni playbook girano come
node(uid 1000), mai come root
Cosa condivide con l’app. Il suo token, e con RTE_FILES=local i file dell’app. Con RTE_FILES=transfer, il predefinito, i playbook e gli inventari arrivano con ogni job, e l’RTE non ha bisogno di un disco condiviso:
| Condiviso con l’app | Cosa ne fa l’RTE |
|---|---|
RTE_TOKEN | Lo presenta quando contatta l’app (in uscita), o lo verifica quando l’app lo contatta (in entrata). L’app conserva lo stesso valore nella credenziale RTE del runner, e i segreti del job vengono sigillati con esso |
Il volume persistente, /app/dist/persistent, solo con RTE_FILES=local | La cartella dei playbook (ANSIBLE_PATH), o il repository dei playbook che il worker ha clonato sotto REPO_PATH, gli inventari (INVENTORY_PATH e i repository degli inventari) e i caricamenti. L’RTE scrive i file delle extravars del job nella cartella del playbook, quindi montala in lettura e scrittura |
La cartella ssh, /home/node/.ssh | La chiave privata e il known_hosts usati dalle connessioni ssh del playbook: monta quella dell’app, gestita nelle sue pagine delle impostazioni, o dai all’RTE una propria |
L’RTE non clona nulla: un repository gli arriva nei file di un job, o sul volume che condivide con l’app. Collection, ruoli e ansible.cfg arrivano dall’immagine, dalle cartelle /etc/ansible/collections, /etc/ansible/roles e /etc/ansible/ansible.cfg quando le monti lì (il progetto Docker Compose lo fa), o da un ansible.cfg accanto ai playbook.
La vita di un job. I numeri qui sotto sono quelli del diagramma, per un modulo di playbook avviato dal browser o dall’API:
- Avvio : l’app verifica il modulo e i suoi valori, costruisce le extravars e salva il job con stato
running. Un modulo con un’approvazione si ferma qui finché un approvatore non lo accetta, e prima di allora non raggiunge nessun RTE - Scelta del runner : l’app prende l’RTE che il modulo indica con
runner: <name>, altrimenti il runner predefinito di tipo RTE, e ne salva il nome sul job. Risolve le credenziali del job dal database o da un archivio di segreti, il suo accesso ansible e la sua password vault, e le sigilla con AES-256-GCM con una chiave derivata dal token di quel runner e dall’id del job - Consegna : il nodo dell’app che detiene il socket dell’RTE gli invia il job: le extravars, le opzioni del modulo, i segreti sigillati e gli hash dei file del job. Qualsiasi nodo dell’app può avviare il job; quello che detiene il socket lo invia entro due secondi. Un RTE pieno, o non connesso, fa attendere il job, fino a
RTE_QUEUE_MINUTES - Presa in carico : l’RTE risponde
claimed, orefusedquando è pieno, si sta fermando, non riesce ad aprire i segreti, o il job arriva con un altro contratto rispetto al suo. ConRTE_FILES=transferprende i playbook e gli inventari dalla sua cache in base al loro hash, chiede all’app un hash che non ha, e li estrae in una cartella propria del job - Partenza : nella cartella del playbook (più il
playbookSubPathdel modulo, che deve esistere) scrive due file:extravars_<id>.jsoncon i valori del modulo e le credenziali, e uno nascosto con l’accesso ansible. Poi avviaansible-playbookcome proprio processo figlio, senza shell, in un gruppo di processi proprio, con-e @quei file, un-iper inventario, e i tag, il limit, il check, il diff e la verbosità del modulo. La password vault entra da stdin, mai in un file o sulla riga di comando, e il processo gira comenodecon un ambiente filtrato (vedi sotto) - Esecuzione : il playbook si connette agli host gestiti dal container RTE, via ssh con la chiave e il
known_hostsdi/home/node/.ssh, consshpassper un accesso con password, o tramite le API che chiamano i suoi moduli. È l’RTE, non l’app, che deve raggiungerli - Output e fine : l’RTE raggruppa stdout e stderr ogni mezzo secondo o 64 KB, con le credenziali mascherate, e numera ogni gruppo. Scrive ogni elemento nello spool del job sul suo disco prima di inviarlo: l’output, gli eventi, il log del job di
.joblogs/job_log_<id>.log(ogni 2 secondi), l’inizio e la fine con lo stato finale (success,failedoaborted). L’app salva ogni elemento una sola volta e ne conferma la ricezione, e l’RTE elimina lo spool una volta salvata la fine. OltrePROCESS_MAX_BUFFERbyte su un flusso (50 MB per impostazione predefinita) non invia altro, e il playbook continua. L’app invia poi le notifiche del modulo - Seguire : il browser non parla mai con l’RTE: la pagina del modulo rilegge il job tramite l’app ogni 2 secondi, e la pagina Job lo segue in tempo reale man mano che l’app salva ogni elemento
Un socket caduto. Il playbook continua. L’RTE ricontatta l’app (in uscita), o un nodo dell’app lo ricontatta (in entrata), e reinvia ciò di cui l’app non aveva confermato la ricezione: l’app salta ciò che ha già salvato, quindi nessuna riga va persa o viene salvata due volte. Questo vale finché il socket torna entro due minuti: l’app scrive l’heartbeat dell’RTE solo finché il suo socket è aperto, e una volta che quell’heartbeat ha due minuti il worker termina i job in esecuzione dell’RTE come abbandonati, anche se i loro playbook stanno ancora girando. Tieni lo spool (RTE_SPOOL_PATH) su un volume, così un RTE riavviato invia ancora ciò che aveva conservato. Un playbook che era in esecuzione quando il suo RTE si è riavviato non può continuare: l’app termina il suo job come abbandonato quando l’RTE torna senza di esso.
Interruzione. Un’interruzione imposta il flag di interruzione del job, e il nodo dell’app che detiene il socket dell’RTE invia cancel entro due secondi. L’RTE invia SIGTERM al gruppo di processi del playbook, così ansible-playbook e ogni fork che ha avviato si fermano insieme, e il job termina aborted. Un cancel che arriva prima del suo job viene conservato per un minuto, e il job parte allora già interrotto.
Pulizia. Appena il processo termina, qualunque sia l’esito, l’RTE rimuove entrambi i file delle extravars (un modulo con keepExtravars conserva quello visibile, mai quello nascosto), il file degli artifact in /tmp, il file di log del job e la cartella dei file propria del job. La fine viene salvata una sola volta: un secondo scrittore, come l’app che fa fallire un job che l’RTE ha già terminato, non cambia nulla.
Un processo per job, non un container per job. AWX e AAP avviano un nuovo pod o container da un’immagine di execution environment per ogni job. AnsibleForms non avvia nulla: il container RTE è già in esecuzione, e ogni job è un processo figlio al suo interno. Un job parte senza scaricare un’immagine né pianificare un pod, e AnsibleForms non ha bisogno di diritti sul tuo motore dei container o sul tuo cluster. In cambio, i playbook che girano su un RTE ne condividono CPU, memoria, /tmp, cartella home e python: dai al container risorse sufficienti per RTE_MAX_JOBS playbook con i loro fork. Per cambiare ciò che un playbook può usare, cambia l’immagine, non il job.
Il confine di sicurezza
L’RTE esegue codice scritto dagli autori dei moduli, quindi è costruito per contenere il meno possibile:
- Solo il suo token : l’RTE non contiene credenziali del database né
ENCRYPTION_SECRET. Non accetta chiamate se non il socket dell’app, con il suo token, e non ha utenti, accesso né pagine web. Tramite il socket può solo eseguire i job che l’app gli invia, riferire su di essi e chiederne i file: non può creare un job, leggere un altro job o distribuire credenziali - Segreti sigillati : per eseguire un job, l’RTE non legge mai la tabella delle credenziali né un archivio di segreti. Riceve i segreti dei job che esegue, sigillati solo per il suo token e quell’id di job, così un pacchetto destinato a un altro RTE o a un altro job non si apre, e un byte modificato viene rifiutato
- Una connessione verificata : in uscita, l’RTE verifica il certificato dell’app. In entrata su HTTPS, l’app fissa il certificato proprio dell’RTE quando si unisce, e lo contatta solo quando presenta quello (vedi Modalità). Un RTE in entrata su HTTP semplice va su una rete privata, come quella del progetto Docker Compose
- Nessun ambiente ereditato :
ansible-playbookriceve l’ambiente dell’RTE senza nessuna delle variabili che AnsibleForms legge per sé (ENCRYPTION_SECRET,DB_PASSWORD,RTE_TOKEN,VAULT_TOKEN,ACCESS_TOKEN_SECRET, leMYSQL_*e le altre), quindilookup('env', ...)non può raggiungerle. Ciò che imposti per i playbook (PATH, proxy,ANSIBLE_*) passa così com’è - Nessun template dagli utenti : un valore di un modulo che contiene
{{,{%o{#arriva al playbook come stringa unsafe che ansible non elabora mai come template, a meno che il modulo non impostiallowJinjaInExtravars, e nessuna shell esegue mai la riga di comando - Non root : l’RTE e i suoi playbook girano come
node(uid 1000), quindi un playbook può fare ciò che può quell’utente nel container, come leggere i suoi file e la chiave ssh, e niente di più
Tratta il suo container, la sua immagine e il suo ambiente con la stessa cura di quelli dell’app.
Modalità database (deprecata)
L’RTE dalla 7.0 alla 7.2, senza RTE_MODE, gira per tutta la 7.x e verrà rimosso nella 8.0.0. Differisce dai passaggi qui sopra in quattro modi:
- Contiene il database : le sue impostazioni
DB_*, eENCRYPTION_SECRETquando si registra da solo (RTE_REGISTER) - La consegna è una chiamata HTTP : l’app chiama
POST <address>/rte/v1/jobscon il token e i segreti sigillati, e l’RTE prende in carico il job scrivendo il suo nome su di esso nel database (passaggi 3 e 4) - Scrive l’output da solo : nel database, a gruppi, senza spool (passaggio 7). Un’interruzione lo raggiunge tramite il flag di interruzione del job, che legge ogni 2 secondi, e una chiamata
/cancel - Legge i file dal volume dell’app : ha bisogno dello stesso disco dell’app, come con
RTE_FILES=local
La sua pagina del runner non ha nessuna delle impostazioni RTE: legge le sue variabili. Per passarlo a una modalità socket, vedi Modalità.