Render and Validate Configuration Templates

AnsibleBeginner
Practice Now

Introduction

Template automation keeps one source while producing configuration from each host's inputs. You will render files from variables and facts, transform a header with a built-in Jinja filter, choose a conditional branch, and load validated nginx configuration through a handler. Actual HTTP requests and a rejected invalid candidate will show the difference between source text, managed files and live service behavior.

Render Host Variables and Facts into Managed Content

In this step, you will create one page template and render distinct real files from the same source on both targets.

The prepared nodes have nginx installed as a prerequisite. Your work is to render and apply configuration, rather than install the package again. Templates are rendered on the controller using each target's variable context and then transferred to managed destinations.

Enter the controller project and create a templates directory. mkdir -p creates the directory if needed without failing when it already exists:

cd /home/labex/project
mkdir -p templates

Define typed host inputs. The environment and listening port differ, and the unquoted maintenance_mode values are YAML Booleans:

cat > host_vars/web1.yml <<'YAML'
environment_label: development
service_port: 8081
maintenance_mode: true
YAML
cat > host_vars/web2.yml <<'YAML'
environment_label: production
service_port: 8082
maintenance_mode: false
YAML

Jinja expressions inside double braces insert values into text. The .j2 suffix is a helpful convention for template sources, rather than a requirement on the managed destination filename. Create one source that uses the current inventory name, environment variable and discovered distribution fact:

cat > templates/page.txt.j2 <<'JINJA'
host={{ inventory_hostname }}
environment={{ environment_label }}
distribution={{ ansible_facts.distribution }}
JINJA

Use the template module for rendering. Copy would normally transfer source bytes without interpreting these Jinja expressions. Gather facts before consuming the distribution value, and declare explicit managed ownership and permissions:

cat > templates.yml <<'YAML'
- name: Render host-specific website content
  hosts: all
  gather_facts: true
  become: true
  tasks:
    - name: Prepare the rendered site directory
      ansible.builtin.file:
        path: /var/www/template-site
        state: directory
        owner: root
        group: root
        mode: "0755"

    - name: Render each host's website content
      ansible.builtin.template:
        src: templates/page.txt.j2
        dest: /var/www/template-site/index.html
        owner: root
        group: root
        mode: "0644"
YAML

Render both managed pages:

ansible-playbook templates.yml --syntax-check
ansible-playbook templates.yml

Read the actual target files:

ansible all -m ansible.builtin.command -a 'cat /var/www/template-site/index.html'

Each page should identify its own host, the matching development/production environment and distribution=Ubuntu. The controller still has one reusable source, while the two managed outputs differ.

Add a Built-In Filter and a Conditional Configuration Branch

In this step, you will render a valid nginx configuration that changes HTTP behavior according to each host's inputs.

A Jinja filter transforms a value using the pipe symbol. upper is a built-in filter that converts text to uppercase; it is not a custom plugin. Jinja control statements use percent-brace delimiters. The if/else/endif branch below selects one maintenance header based on the Boolean input.

Create a complete nginx configuration template. Port, environment header and maintenance header all come from per-host data. The root and index settings serve the page you already rendered:

cat > templates/nginx.conf.j2 <<'JINJA'
events {}
http {
    server_tokens off;
    server {
        listen {{ service_port }};
        root /var/www/template-site;
        index index.html;
        add_header X-Environment "{{ environment_label | upper }}" always;
{% if maintenance_mode %}
        add_header X-Maintenance "enabled" always;
{% else %}
        add_header X-Maintenance "disabled" always;
{% endif %}
    }
}
JINJA

Append configuration and service tasks plus the named reload handler to the existing playbook. The template task's validate command tests a temporary rendered candidate (%s) before replacing the managed configuration. A changed valid configuration notifies the handler; the ordinary service task keeps nginx running.

cat >> templates.yml <<'YAML'

    - name: Render and validate the nginx candidate before replacement
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: "0644"
        validate: /usr/sbin/nginx -t -c %s
      notify: Reload nginx

    - name: Keep the rendered site service running
      ansible.builtin.service:
        name: nginx
        state: started

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
YAML

Apply the complete playbook:

ansible-playbook templates.yml

The configuration task should change both hosts and trigger the reload handler. Inspect web1 on port 8081 and web2 on port 8082. curl -i includes response headers together with the body:

ansible web1 -m ansible.builtin.command -a 'curl --fail --silent --show-error -i http://127.0.0.1:8081/'
ansible web2 -m ansible.builtin.command -a 'curl --fail --silent --show-error -i http://127.0.0.1:8082/'

Web1 should return X-Environment: DEVELOPMENT and X-Maintenance: enabled; web2 should return X-Environment: PRODUCTION and X-Maintenance: disabled. Both bodies retain the correct per-host page. Header dates vary; compare the data-driven values and successful status.

Change Inputs and Reject an Invalid Candidate

In this step, you will update persistent input without editing the templates, observe the actual live result, and prove that validation protects working configuration.

Change web1's saved environment to staging and its maintenance mode to false, keeping its existing port:

cat > host_vars/web1.yml <<'YAML'
environment_label: staging
service_port: 8081
maintenance_mode: false
YAML

Apply the same source templates:

ansible-playbook templates.yml

Web1's page and configuration should change, notifying its reload handler; web2 should remain unchanged. Inspect web1 again:

ansible web1 -m ansible.builtin.command -a 'curl --fail --silent --show-error -i http://127.0.0.1:8081/'

Its body should show environment=staging, with X-Environment: STAGING and X-Maintenance: disabled. The templates stayed reusable as the input changed.

Now test candidate rejection. cp saves the valid controller source so it can be restored. Replace only the nginx template with deliberately invalid text:

cp templates/nginx.conf.j2 templates/nginx.conf.j2.good
printf 'invalid nginx configuration;\n' > templates/nginx.conf.j2

The next playbook run is expected to fail candidate validation. The shell's || runs the message only after a nonzero exit, allowing this intentional failure exercise to continue:

ansible-playbook templates.yml || printf 'Expected candidate validation rejection\n'

Read the task failure: nginx should reject the temporary candidate. The managed working configuration must remain in place, so both live services should still respond correctly:

ansible web1 -m ansible.builtin.command -a 'curl --fail --silent --show-error -i http://127.0.0.1:8081/'
ansible web2 -m ansible.builtin.command -a 'curl --fail --silent --show-error -i http://127.0.0.1:8082/'

mv restores the saved valid source to its original filename. Run the complete playbook once more:

mv templates/nginx.conf.j2.good templates/nginx.conf.j2
ansible-playbook templates.yml

Both hosts should report changed=0, failed=0 and unreachable=0, with no reload handler scheduled. Validation protected the live configuration without making a copy of an invalid candidate the desired end state.

Summary

You rendered host variables and facts with the template module, used the built-in upper filter and a Boolean Jinja branch, and applied validated configuration through a handler. Changed input produced different live output without rewriting the sources. An invalid candidate was rejected before replacement, and restored valid templates converged without extra changes.