Ansible templates en handlers gebruiken: een voorbeeld
Leer hoe u met Jinja2 templates een nginx configuratie rendert en handlers inzet voor idempotente playbooks. Ontdek hoe u wijzigingen efficiënt doorvoert zonder onnodige restarts.
Wat Ansible-templates en -handlers toevoegen aan uw eerste playbook
Ansible-templates en -handlers zijn de twee onderdelen die van een statisch playbook een nuttig playbook maken. Een template genereert een configuratiebestand op basis van uw variabelen, waardoor één bestand volstaat voor elke host. Een handler wordt alleen uitgevoerd wanneer een taak daadwerkelijk iets heeft gewijzigd; hierdoor wordt de service alleen herladen bij een echte configuratiewijziging en blijft deze de rest van de tijd ongemoeid.
Deze handleiding gaat verder waar uw eerste Ansible-playbook op een VPS ophoudt. U heeft al een play die een pakket installeert en een service start. Alles hieronder wordt uitgevoerd op één machine, omdat de play zich richt op localhost via een lokale verbinding. U heeft geen tweede server nodig om dit te volgen. Dezelfde play werkt op echte inventory-hosts zonder wijzigingen aan de taken, en de laatste sectie behandelt wat er wel verandert.
De werkmap instellen
sudo apt update
sudo apt install -y ansible nginx
ansible --version
mkdir -p ~/ansible-templates/templates
cd ~/ansible-templatesnginx wordt hier enkel gebruikt omdat het een echte service is met een configuratiebestand en een commando voor herladen; dit is alles wat het voorbeeld nodig heeft. ansible --version toont de versie van ansible-core en de Python-interpreter die zal worden gebruikt. Noteer beide. Het onderstaande playbook gebruikt volledig gekwalificeerde modulenamen zoals ansible.builtin.template, waarvoor Ansible 2.10 of nieuwer vereist is; elk huidig distributiepakket voldoet hier ruimschoots aan.
Maak inventory.ini aan:
[local]
localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"ansible_connection=local instrueert Ansible om elke taak als een lokaal proces uit te voeren in plaats van een SSH-sessie naar zichzelf te openen. De tweede instelling is geen decoratie. Wanneer u localhost in een inventory-bestand schrijft, wordt het een gewone host en verliest het de interpreter die Ansible standaard aan de impliciete localhost toewijst. Hierdoor valt het terug op interpreter discovery en kan het een andere Python-versie kiezen dan degene die het play uitvoert. ansible_playbook_python is de interpreter die op dit moment ansible-playbook uitvoert, wat ervoor zorgt dat beide synchroon blijven.
Maak ansible.cfg aan:
[defaults]
inventory = inventory.iniZonder dat bestand moet u -i inventory.ini meegeven bij elk commando. Zonder inventory geeft Ansible [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' weer, en een play met hosts: all komt dan met niets overeen. Nog een punt over ansible.cfg: Ansible negeert het bestand wanneer het in een map staat die voor iedereen schrijfbaar is; bewaar het project daarom in uw home-map. Een inventory-bestand bevat meer dan alleen een lijst met hosts, en dit is de kleinst mogelijke variant die aan de vereisten voldoet.
template versus copy: wanneer gebruikt u welke?
ansible.builtin.copy verplaatst een bestand zoals het is. ansible.builtin.template voert het bestand eerst uit via Jinja2 en verplaatst het resultaat. De module-broncode beschrijft template als "een virtuele module die volledig is geïmplementeerd als een action plugin en draait op de controller". Dit heeft een consequentie die u moet onthouden: het renderen gebeurt op de machine waar u ansible-playbook heeft getypt. De doelhost ziet nooit uw variabelen en hoeft Jinja2 niet geïnstalleerd te hebben.
Gebruik copy wanneer het bestand op elke host identiek is. Gebruik template zodra één waarde per host verschilt, of wanneer u een {% for %}-loop of een {% if %}-blok nodig heeft. copy heeft wel een content:-parameter en variabelen daarbinnen worden vervangen zoals bij elk ander taakargument, maar er zijn daar geen loops of conditionals mogelijk. Alles met een structuur hoort daarom in een template thuis. Beide modules accepteren dezelfde bestandsopties, omdat ze beide gebruikmaken van dezelfde documentatiefragmenten. Daarom gedragen owner, group, mode, backup en validate zich in beide gevallen op dezelfde manier.
Het template schrijven: één variabele, één lus
Sla dit op als templates/app.conf.j2:
# {{ ansible_managed }}
upstream {{ app_name }}_backend {
{% for backend in app_backends %}
server {{ backend.host }}:{{ backend.port }} weight={{ backend.weight }};
{% endfor %}
}
server {
listen {{ app_listen_port }};
server_name {{ app_server_name }};
location / {
proxy_pass http://{{ app_name }}_backend;
proxy_set_header Host $host;
}
}Twee soorten Jinja2-tags voeren hier het werk uit. {{ ... }} is een expressie en drukt de waarde ervan af. {% ... %} is een statement en drukt zelf niets af. app_backends is een lijst met dictionaries, dus backend.host leest één key uit elk item, en de lus schrijft één server-regel per item, ongeacht hoeveel u er definieert.
Eén detail over de witruimte, omdat dit mensen verrast die Jinja2 uit andere contexten kennen. Ansible stelt trim_blocks standaard in op yes, wat Jinja2 zelf niet doet. Hierdoor wordt de nieuwe regel direct na een {% ... %}-tag verwijderd en laat de lus geen lege regel achter. Ansible laat lstrip_blocks op no staan, dus alle spaties die u vóór een {%-tag plaatst, blijven behouden en verschijnen in het gegenereerde bestand. Als uw output ongewenste inspringing bevat, stelt u lstrip_blocks: true in op de template-taak.
{{ ansible_managed }} wordt standaard gerenderd als de letterlijke tekst Ansible managed. Laat dit zo. Mensen herdefiniëren ansible_managed vaak in ansible.cfg om een datum toe te voegen, en zodra ze dat doen, verschilt het gegenereerde bestand bij elke uitvoering, rapporteert de taak bij elke uitvoering een wijziging en herlaadt de service bij elke uitvoering. Die ene instelling vernietigt de eigenschap waar de rest van deze handleiding over gaat. De .j2-extensie is een conventie en Ansible controleert deze niet.
Het playbook
Sla dit op als site.yml:
- name: Render an nginx site from a template
hosts: local
become: true
vars:
app_name: learn
app_listen_port: 8080
app_server_name: learn.example.com
app_backends:
- host: 127.0.0.1
port: 9001
weight: 3
- host: 127.0.0.1
port: 9002
weight: 1
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
cache_valid_time: 3600
- name: Render the site configuration
ansible.builtin.template:
src: templates/app.conf.j2
dest: "/etc/nginx/conf.d/{{ app_name }}.conf"
owner: root
group: root
mode: '0644'
backup: true
notify: nginx config changed
- name: Make sure nginx is enabled and running
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Test the nginx configuration
ansible.builtin.command:
cmd: /usr/sbin/nginx -t
changed_when: false
listen: nginx config changed
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
listen: nginx config changedmode: '0644' staat bewust tussen aanhalingstekens. De documentatie voor bestandsopties stelt dat octale getallen tussen aanhalingstekens moeten staan "zodat Ansible een string ontvangt en zelf de conversie van string naar getal kan uitvoeren". Zonder aanhalingstekens leest de YAML-parser 0644 als een gewoon getal, waardoor u mogelijk andere rechten krijgt dan u had beoogd.
notify: nginx config changed benoemt een onderwerp, geen handler. Beide handlers bevatten listen: nginx config changed, dus één notify bereikt ze allebei. Voeg later een derde handler toe met dezelfde listen-regel en de template-taak hoeft niet te worden aangepast. cache_valid_time: 3600 voorkomt dat een tweede run binnen hetzelfde uur opnieuw verbinding maakt met de pakket-mirrors.
Voer het eenmaal uit en lees wat er werd afgedrukt
ansible-playbook site.ymlAls uw sudo om een wachtwoord vraagt, voeg dan -K toe en Ansible zal hierom vragen.
Lees eerst de regels per taak en daarna de PLAY RECAP onderaan. Elke taak drukt changed: af wanneer Ansible een actie moest uitvoeren, of ok: wanneer de host al in de gewenste staat verkeerde; het overzicht telt deze tellers per host op. Nadat elke taak in de play is voltooid, en geen moment eerder, krijgt u RUNNING HANDLER [Test the nginx configuration] gevolgd door RUNNING HANDLER [Reload nginx].
Controleer nu de machine zelf in plaats van op de uitvoer te vertrouwen:
sudo cat /etc/nginx/conf.d/learn.conf
sudo /usr/sbin/nginx -t
curl -sI http://127.0.0.1:8080/nginx -t drukt nginx: configuration file /etc/nginx/nginx.conf test is successful af wanneer de samengestelde configuratie correct wordt geparseerd. De curl geeft een statusregel van nginx terug, en 502 Bad Gateway is hier het juiste antwoord, omdat het serverblok actief is en er niets luistert op poort 9001 of 9002. sudo tail /var/log/nginx/error.log vermeldt de reden in duidelijke bewoordingen: connect() failed (111: Connection refused) while connecting to upstream.
Voer het een tweede keer uit om de idempotentie te bewijzen
ansible-playbook site.ymlDit is de uitvoering die ertoe doet, dus vergelijk de output regel voor regel met de eerste. De template-taak zou nu ok: moeten weergeven waar deze changed: afdrukte, en geen van beide handlers zou ergens in de output moeten verschijnen.
Het mechanisme is eenvoudig en het is de moeite waard om te kennen, omdat u hierop debugt. template rendert het bestand op de controller en vergelijkt de checksum van het resultaat met de checksum van het bestand dat zich al op dest bevindt. Overeenkomende inhoud, eigenaarschap en rechten betekenen dat er niets hoeft te gebeuren, dus rapporteert de taak ok, waardoor notify nooit wordt geactiveerd en de handler dus nooit draait. Handlers worden geactiveerd bij changed en bij niets anders.
Bewijs ook de andere richting. Wijzig weight: 3 naar weight: 1 in vars, voer de play opnieuw uit, en de template-taak rapporteert changed, beide handlers draaien, en sudo cat /etc/nginx/conf.d/learn.conf toont de nieuwe waarde.
Als een tweede identieke uitvoering nog steeds een wijziging rapporteert, is de rendering niet stabiel. Zoek eerst naar iets dat tijdsafhankelijk is in de output, omdat dat de meest voorkomende oorzaak is en een aangepaste ansible_managed meestal de boosdoener is. Controleer daarna of mode en owner op de taak overeenkomen met wat er daadwerkelijk op de schijf staat, omdat een mismatch daar een wijziging is, zelfs als de bytes identiek zijn.
Bekijk een wijziging voordat u deze doorvoert
ansible-playbook site.yml --check --diff--check voert de play uit zonder de host te wijzigen. --diff toont wat elke taak zou hebben aangepast; voor template is dit een regel-voor-regel verschil tussen de render en het bestand op de schijf. Samen beantwoorden ze de vraag "wat zou deze run doen" zonder deze daadwerkelijk uit te voeren. Check mode heeft zijn eigen beperkingen, vooral bij taken waarvan het resultaat afhangt van een eerdere taak die in check mode niet daadwerkelijk is uitgevoerd.
Waarom handlers wachten tot het einde van de play
De documentatie over handlers is hierover duidelijk: "Standaard worden handlers uitgevoerd nadat alle taken in een specifieke play zijn voltooid. Aangeroepen handlers worden automatisch uitgevoerd na elk van de volgende secties, in deze volgorde: pre_tasks, roles/tasks en post_tasks."
De reden hiervoor is batchverwerking. Een play die vier configuratiebestanden voor één service genereert, moet die service één keer herstarten aan het einde, wanneer alle vier de bestanden op hun plek staan. Herstarten na elk bestand zou leiden tot vier herstarts, waarbij drie van die herstarts een onvolledige configuratie zouden laden. Dezelfde pagina stelt de garantie expliciet: "Het meerdere keren aanroepen van dezelfde handler resulteert erin dat de handler slechts één keer wordt uitgevoerd, ongeacht hoeveel taken deze aanroepen."
De volgorde staat eveneens vast: "Handlers worden uitgevoerd in de volgorde waarin ze zijn gedefinieerd in de sectie handlers, niet in de volgorde waarin ze worden vermeld in de notify-instructie." Daarom staat Test the nginx configuration boven Reload nginx in de playbook. De test wordt als eerste uitgevoerd omdat deze als eerste is geschreven, en niets in de notify-regel heeft daar invloed op.
Handlers vroegtijdig uitvoeren en uitvoeren na een fout
Soms moet een latere taak in dezelfde play beschikken over een service die al op de nieuwe configuratie draait. Forceer de uitvoering van de aangemelde handlers op dat punt met de meta-module. Volgens de documentatie zorgt dit ervoor dat "Ansible alle handler-taken uitvoert die tot dat moment zijn aangemeld".
- name: Run the notified handlers now instead of at the end of the play
ansible.builtin.meta: flush_handlers
- name: Wait for the new listener to accept connections
ansible.builtin.wait_for:
host: 127.0.0.1
port: 8080
timeout: 10Laat die meta-regel weg en de wait_for-taak wordt uitgevoerd terwijl nginx nog steeds de oude configuratie serveert. Bij een eerste run luistert er nog niets op poort 8080, waardoor de taak de volledige tien seconden wacht en vervolgens faalt.
Het tweede geval is een fout. "Als een taak een handler aanmeldt, maar een andere taak later in de play faalt, wordt de handler standaard niet uitgevoerd op die host. Dit kan de host in een onverwachte toestand achterlaten." Een play die een configuratie rendert en vervolgens struikelt over een niet-gerelateerde taak, laat daarom het nieuwe bestand op de schijf staan terwijl de oude configuratie nog in de actieve service is geladen. Overschrijf dit gedrag met --force-handlers op de opdrachtregel, of met force_handlers: true in de play. Dezelfde schakelaar bestaat als force_handlers = True onder [defaults] in ansible.cfg, en als de omgevingsvariabele ANSIBLE_FORCE_HANDLERS. De standaardwaarde is False.
Handler-namen conflicteren en de verliezer blijft stil
De documentatie stelt de volgende regel: "Elke handler moet een globaal unieke naam hebben. Als er meerdere handlers met dezelfde naam worden gedefinieerd, kan alleen de laatste die in de play wordt geladen, worden aangeroepen en uitgevoerd." Handlers die binnen een role zijn gedefinieerd, zijn bovendien niet beperkt tot die role. Ze worden ingevoegd in één globale handler-lijst voor de gehele play. Hierdoor resulteren twee roles die elk Restart nginx definiëren in een naam die naar slechts één van beide verwijst. De laadvolgorde bepaalt welke dit is, niet de role van waaruit u de melding verstuurt.
Test deze regel voordat u erop vertrouwt. Sla dit op als handlers-dup.yml:
- name: Two handlers, one name
hosts: local
gather_facts: false
tasks:
- name: Notify the duplicated name
ansible.builtin.command:
cmd: /bin/true
changed_when: true
notify: Duplicated handler
handlers:
- name: Duplicated handler
ansible.builtin.file:
path: /tmp/dup-first
state: touch
mode: '0644'
- name: Duplicated handler
ansible.builtin.file:
path: /tmp/dup-second
state: touch
mode: '0644'rm -f /tmp/dup-first /tmp/dup-second
ansible-playbook handlers-dup.yml
ls -l /tmp/dup-first /tmp/dup-secondDe play slaagt, RUNNING HANDLER [Duplicated handler] verschijnt één keer en ls print een regel voor /tmp/dup-first en ls: cannot access '/tmp/dup-second': No such file or directory voor de andere. De handler die wordt uitgevoerd is degene die als eerste is geschreven, niet de laatste die is geladen. Dit is het tegenovergestelde van wat de zin voorspelt.
Het verschil is belangrijk om te begrijpen, omdat de gedocumenteerde regel betrekking heeft op handler-blokken in plaats van op regels in een bestand. Handlers die uit verschillende bronnen komen, zoals de ene role na de andere, zijn afzonderlijke blokken; een later blok overschrijft inderdaad een eerder blok. Een eenvoudige handlers:-lijst in een play is echter één enkel blok. Het zoeken binnen een blok verloopt van boven naar beneden en stopt bij de eerste naam die overeenkomt. Binnen één bestand reageert dus de eerste definitie en is de tweede onbereikbaar, terwijl tussen roles het overschrijven verloopt zoals de documentatie beschrijft. In beide gevallen kunt u nooit beide bereiken en op geen van beide methoden kunt u bouwen.
Er zijn twee nette oplossingen. Geef elke handler-naam een voorvoegsel dat specifiek is voor de role, of gebruik de gekwalificeerde vorm role_name : handler_name. De documentatie geeft dit aan als de manier "om te garanderen dat een handler uit een role wordt aangeroepen in plaats van een handler van buiten de role met dezelfde naam". De spaties rond de dubbele punt maken deel uit van die syntaxis. Dit wordt een acuut probleem zodra u roles gaat gebruiken die u niet zelf heeft geschreven.
Nog een regel van dezelfde pagina: "Vermijd het plaatsen van variabelen in de naam van de handler. Omdat handler-namen vroegtijdig worden getemplated, beschikt Ansible mogelijk nog niet over een waarde voor een handler-naam zoals deze." Een handler genaamd Restart {{ service_name }} laat de gehele play falen wanneer die variabele op het moment van templating nog niet is gedefinieerd. Door handler-namen als vaste strings te behouden en ze te groeperen met listen, vermijdt u dit probleem.
validate: weigeren om een defect render-bestand te installeren
validate voert een commando uit op het gerenderde bestand voordat Ansible het op de juiste plek plaatst. De documentatie stelt: "Het validatiecommando dat moet worden uitgevoerd voordat het bijgewerkte bestand naar de definitieve bestemming wordt gekopieerd. Er wordt een tijdelijk bestandspad gebruikt voor de validatie, dat wordt doorgegeven via %s, wat aanwezig moet zijn zoals in de onderstaande voorbeelden. Bovendien wordt het commando veilig doorgegeven, waardoor shell-functies zoals expansie en pipes niet werken."
Twee regels vloeien direct voort uit die tekst. De %s is verplicht, en een validate-string zonder deze parameter laat de taak falen met validate must contain %s. Daarnaast is er geen shell beschikbaar, dus pipes, redirectie, globbing en && werken niet. Eén commando, één bestandsargument.
De officiële modulevoorbeelden zijn de twee gevallen waarin dit perfect werkt:
- name: Copy a new sudoers file into place, after passing validation with visudo
ansible.builtin.template:
src: /mine/sudoers
dest: /etc/sudoers
validate: /usr/sbin/visudo -cf %s
- name: Update sshd configuration safely, avoid locking yourself out
ansible.builtin.template:
src: etc/ssh/sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0600'
validate: /usr/sbin/sshd -t -f %s
backup: yesBeide werken omdat elke checker één bestand accepteert en dit op eigen merites beoordeelt. visudo -cf leest een sudoers-bestand. sshd -t -f leest een volledige sshd_config.
Waarom validate het nginx-bestand in deze handleiding niet kan controleren
Voeg validate: /usr/sbin/nginx -t -c %s toe aan de bovenstaande template-taak en de taak faalt. Het bericht benoemt de oorzaak:
nginx: [emerg] "upstream" directive is not allowed here in <ansible temporary path>:2nginx -t -c verwacht een volledige configuratie die op het hoogste niveau begint met events en http blokken. Het bestand dat deze play rendert is een fragment, dat in het http blok wordt getrokken door include /etc/nginx/conf.d/*.conf; binnen /etc/nginx/nginx.conf. Op zichzelf staand, buiten die context, is upstream inderdaad een directive op de verkeerde plek, dus nginx wijst een bestand af dat volledig correct is op de plek waar het daadwerkelijk thuishoort. De checker kreeg een fragment aangeleverd en werd gevraagd dit als een volledige configuratie te behandelen.
Het werkbare antwoord is het antwoord dat al in de playbook staat. Installeer het fragment en controleer vervolgens de samengestelde configuratie in een handler die boven de reload-handler is gedefinieerd. Omdat handlers worden uitgevoerd in de volgorde waarin ze zijn gedefinieerd, ziet nginx -t de echte /etc/nginx/nginx.conf inclusief uw fragment, en een fout daar laat de play falen voordat systemctl reload ooit wordt aangeroepen. Wees duidelijk over de consequentie: het defecte bestand staat op de schijf wanneer die controle faalt, en nginx blijft de laatste configuratie serveren die het heeft geladen totdat iemand het herstart.
Dat is waar backup: true zijn waarde bewijst. Het schrijft een kopie van het vorige bestand naast het origineel voordat het wordt overschreven, genaamd basename.PID.YYYY-MM-DD@HH:MM:SS~, waardoor de map uiteindelijk items bevat zoals learn.conf.4127.2026-08-20@11:42:09~. Voer sudo ls -l /etc/nginx/conf.d/ uit na een wijziging en u zult er een vinden.
Dat naamgevingsdetail is belangrijker dan het lijkt. De back-up is onschadelijk in /etc/nginx/conf.d/ omdat de hoofdconfiguratie alleen conf.d/*.conf bevat en de back-upnaam eindigt op een tilde. Het is niet onschadelijk in een map die wordt opgenomen met een kale *, en op Debian en Ubuntu bevat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* op precies die manier. Template naar sites-enabled met backup: true en nginx laadt de back-up als een tweede live server-blok, en daarom schrijft deze play naar conf.d in plaats daarvan.
Dezelfde play uitvoeren op echte inventory-hosts
Wijzig hosts: local in de groepsnaam die u gebruikt; verder hoeft er niets in de play te worden aangepast. De template wordt eenmaal per host gegenereerd, dus app_listen_port en app_backends kunnen afkomstig zijn uit group_vars en host_vars, terwijl het templatebestand zelf ongewijzigd blijft. Dat is het voordeel van het plaatsen van waarden in variabelen in plaats van in het bestand zelf.
Twee zaken veranderen wel. become: true vereist nu een sudo-wachtwoord op elke target, tenzij u daar wachtwoordloze sudo hebt geconfigureerd; voeg daarom -K toe. En elk geheim in die template, zoals een databasewachtwoord of een API-token, mag niet in leesbare vars: in een bestand staan dat u commit. Versleutel deze waarden met Ansible Vault en verwijs er op dezelfde manier naar als u nu doet, aangezien het voor de template niet uitmaakt waar een variabele vandaan komt.
Wanneer de play groeit voorbij één service, hebben vars:, templates/ en handlers: al een standaardlocatie die voor hen klaarstaat. Het verplaatsen daarvan is precies het doel van de splitsing tussen een playbook en een role.
FAQ
Why did my Ansible handler not run?
Almost always because the task that notifies it reported ok rather than changed. Handlers fire on change and on nothing else, so a template task whose render matches the file already on disk never notifies anything. After that, check four things. The string in notify must match the handler name or a listen topic exactly, including case and spacing. A later task that failed on that host suppresses notified handlers unless you pass --force-handlers. A handler defined in a different play is not visible from this one. And a notifying task skipped by a when condition never notifies at all.
Why does my playbook report changed on every run?
The rendered text is not stable between runs. The most common cause is a timestamp in the output, and a customised ansible_managed string that includes a date does exactly that. The next thing to check is mode and owner on the task: if they do not match the file already on disk, Ansible corrects them and reports a change even though the content is identical. Run ansible-playbook site.yml --check --diff to see which of the two it is, because --diff shows you the difference the task intends to make.
What is the difference between template and copy in Ansible?
ansible.builtin.copy sends a file unchanged. ansible.builtin.template renders it through Jinja2 on the controller first and then sends the result, so variables and loops are resolved before the file ever reaches the target host. Use copy for a file that is byte identical everywhere. Use template for anything that varies by host. They share the same file options, so mode, owner, backup and validate work the same way in both.
How do I make a handler run in the middle of a play?
Add ansible.builtin.meta: flush_handlers as a task at the point you want them to run. It triggers every handler notified so far, then the play carries on normally. Use it when a later task in the same play depends on the service already running the new configuration, for example a wait_for on a port that only exists after the reload. It is the supported way to run a handler before the end of the play.
Can I use validate with an nginx config fragment?
Not with nginx -t -c %s. That command expects a complete configuration starting with the top level events and http blocks, so it rejects a conf.d fragment with a message like "upstream" directive is not allowed here. The fragment is valid inside the http block and invalid on its own. Install the file, then run nginx -t against the assembled configuration in a handler defined above the reload handler. Handlers run in the order they are defined, so a bad configuration fails the play before the reload is attempted. Set backup: true on the template task so the previous file is still there to put back.