Ansible jinja2 template from JSON format provided as extra-vars

ansibleansible-playbookjinjajinja2

I have this jinja2 template:

# {{ ansible_managed }}

{% for vhost in nginx_vhosts %}
{%- if vhost.name == item.name -%}

# redirect www to non-www
server {
    listen {{ nginx_port }};
    listen [::]:{{ nginx_port }};
    port_in_redirect off;

    server_name www.{{ vhost.name }};
    return 301 http://{{ vhost.name }}$request_uri;
}
{%- endif -%}
{%- endfor -%}

An ansible role with an yaml file vhosts.yml containing definitions like this:

nginx_vhosts:
      - name: "test1.com"
        repo: "git1"
        branch: master
        state: present
      - name: "test2.com"
        repo: "git2"
        branch: master
        state: present
...
      - name: "test101.com"
        repo: "git101"
        branch: master
        state: present

A task inside playbook.yml:

- name: "Generate nginx vhost configuration file"
  template:
    src: templates/nginx-vhost-template.j2
    dest: "{{ nginx_vhosts_dir }}/{{ item.name }}.conf"
    owner: "{{ nginx_user }}"
    group: "{{ nginx_group }}"
    mode: 0640
  with_items:
    - "{{ nginx_vhosts }}"
  when:
    - item.state == 'present'
  notify:
    - nginx-restart

I ran a taks like:

ansible-playbook -l web1 playbook.yml --tags=nginx-vhost-config

which is working fine, it will create from template a nginx vhost configuration file on the remote server as domain1.com.conf and so on for all the found definitions.

Assuming that in the vhosts.yml file I have test1.com up to test100.com, I'll add let's say test101.com and I want to run the tasks strictly for that test101.com and not for all previous hosts. So I tried something like this:

ansible-playbook -l web1 playbook.yml --tags=nginx-vhost-config -e "{ 'nginx_vhosts': { 'name': 'test101.com', 'state': 'present', 'repo': 'git101', 'branch': 'master' }}"

The issue with this is that it results in an error when trying to replace values from the jinja2 template.

An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ansible.errors.AnsibleUndefinedVariable: 'ansible.parsing.yaml.objects.AnsibleUnicode object' has no attribute 'name'

I've also tried using loop instead of with_items but no luck.

I understand that when using the extra-vars, the provided content is in JSON format but I was not able to find a different way to pass the content from vhosts.yml as extra vars for one single entry. Is there any way to make this functional?

Is there a better approach maybe?

Best Answer

You are passing in an object/dictionary but your code is expecting a list. You need to either wrap it in a list when you pass it in, or account for the different possible structures when you consume it.

You should first reduce the number of places that reference nginx_vhosts by using the current loop item directly in your template:

# {{ ansible_managed }}

# redirect www to non-www
server {
    listen {{ nginx_port }};
    listen [::]:{{ nginx_port }};
    port_in_redirect off;

    server_name www.{{ item.name }};
    return 301 http://{{ item.name }}$request_uri;
}

You can then modify the structure you pass in slightly:

"{ 'nginx_vhosts': [{ 'name': 'test101.com', 'state': 'present', 'repo': 'git101', 'branch': 'master' }]}"

Or modify your loop slightly:

- name: "Generate nginx vhost configuration file"
  template:
    src: templates/nginx-vhost-template.j2
    dest: "{{ nginx_vhosts_dir }}/{{ item.name }}.conf"
    owner: "{{ nginx_user }}"
    group: "{{ nginx_group }}"
    mode: "0640"
  loop: "{{ [ nginx_vhosts ] | flatten }}"
  when:
    - item.state == 'present'
  notify:
    - nginx-restart