Escape or differentiate between jinja template variables in Ansible and <service>. - ansible

History:
We've been using Ansible to deploy our services and config files over the last few months and we've been making use of the Ansible variables.
The variables get placed in our (config_name).yml.j2 files which allows us to easily make changes without having to hard code all of the config.
For example in Ansibles group_vars we may have:
metric_port_var: "9100"
And the (config_name).yml.j2 will contain a line somewhat:
EXPOSE_METRIC_PORT={{ metric_port_var}}
When the config is deployed the config on the box is now:
EXPOSE_METRIC_PORT=9100
Problem:
Now we're deploying configuration for AlertManager/Prometheus.
The issue that has arisen is that AlertManager is also using jinja template variables in it's own configuration file that we are trying to deploy. These other jinja templates will come from other config files on the box.
This means our (config_name).yml.j2 will in theory contain a mix of curly brace variables where some might belong to Ansible and others will belong to this other file.
We can no longer use Ansible's "Template" module to deploy our config due to an error being thrown when a variable isn't found in group_vars, as instead the particular variable should come from AlertManager.
We need a way of either mixing the jinja templates or escaping some curly braces but not others. For now we're back to hard coding our config and letting AlertManager use all the variables.

This works fine for me:
template.j2:
foo {{ ansible_var }}
bar {{ '{{' }} other_var {{ '}}' }}
zzz {%raw%}{{ another_var }}{%endraw%}
output:
foo val
bar {{ other_var }}
zzz {{ another_var }}

AlertManager is also using jinja template variables in it's own configuration file
The alertmanager uses Go templates.
See the escaping section of the Jinja docs:
key={{ '{{' }}the_var{{ '}}' }}
Will be rendered as:
key={{the_var}}

Brian-Brazil is correct. (As to be expected, google his name)
We use this technique a lot here and have .j2.j2 files in our Ansible templates that translate to .j2 files in dockers who are parsed on container start.
Here is an example more specific to your use-case.
The alertmanager indeed uses Go templates but this might look confusing on first sight when mixed in a jinja template, I agree.
Say you have a file called alertmanager.yml.j2 of which the following lines are an extract.
receivers:
- name: '{{ name_of_receiver_group }}'
opsgenie_configs:
- api_key: 123-123-123-123-123
send_resolved: false
{% raw %}
# protecting the go templates inside the raw section.
details: { details: "{{ .CommonAnnotations.SortedPairs.Values | join \" \" }}" }
{% endraw %}
You'll have an Ansible task that looks similar like this.
- name: copy helper scripts
template: src={{ item }}.j2 dest={{ container_dir }}/{{ item }} mode=0755
with_items:
- alertmanager.yml

Related

Jinja2 if variable is defined set variable from Ansible defaults which references variable

In an Ansible role I am testing inside an Jinja2 template if a variable is defined. If it is defined, a variable from Ansible's defaults/main.yml should be set which references this variable.
This works fine as long as the variable which gets tested in the Jinja2 template is defined. If it is not, Ansible complains about an undefined variable.
Example role files:
$ cat defaults/main.yml
---
database_name: "prefix-{{ database }}"
$ cat tasks/main.yml
---
- name: Deploy config
template:
src: config.j2
dest: /opt/config
mode: 0644
$ cat templates/config.j2
# this works
{% if database is defined %}
Database={{ database }}
{% endif %}
# this is broken
{% if database is defined %}
Database={{ database_name }}
{% endif %}
Using the variable directly (see "this works" above) after testing if it exists works just fine.
How can I make the use of the variable in defaults/main.yml work?
Long story short: in defaults/main.yml
database_name: "prefix-{{ database | default('db_does_not_exist') }}"
You can read ansible and jinja2 code to understand clearly why, but basically, Jinja2 tries to read your var at interpretation time, even inside your if condition which turns out to be false, and finds an other var that it tries to interpret again which is undefined.
The above default value will never be used in your case but will prevent your template from firing this error (or any other task using it)

Can I render ansible templates to a variable vs. to disk?

Is writing a file the only way to render an ansible template? In the past, with python and jinja2, I've rendered jinja templates to python vars directly and was hoping I can do the same with ansible.
What I'm trying to do is take the content of my template and pipe it to another command without writing the template to a file on disk and cat'ing the file. Doable?
You have several options for creating a variable from a template. For small templates, you can just use an inline template with set_fact:
- name: render a template to a variable
set_fact:
myvar: |-
This is a template.
This host is {{ inventory_hostname }}.
For longer templates, you can use the template lookup:
- name: render a template to a variable
set_fact:
myvar: "{{ lookup('template', 'template_name.j2') }}"
And of course this isn't limited to the set_fact module. You can do this anywhere you can put a string value in Ansible.

In Ansible, how can I use different template substitutions based on a variable value? [duplicate]

This question already has an answer here:
How can I use a condition to set a variable value in Ansible?
(1 answer)
Closed 5 years ago.
I have a template of a system.d service script that I get filled using Ansible playbook.
The template includes experssion
[Service]
Environment="JAVA_OPTS=-Djava.net.preferIPv4Stack=true -Denvironmentname={{environment_name | default('NOT_SET') }}"
where environment_name above is a variable present in Ansible while running the playbook. Playbook has this instruction:
- name: Copy systemd service script
template: src=systemd.service dest="{{systemd_dir}}/{{systemd_service_name}}.service"
I want to add memory-related parameters to the JAVA_OPTS such as:
if environment_name=DEV, then add to JAVA_OPTS '-Xmx=2000Mb -Xms=1000Mb', if environment_name=PROD, then add '-Xmx=20000Mb -Xms=10000Mb'*, etc (I have several environments).
How I can encode such substitutions in template or in calling script?
You can use the if Jinja template directive. Maybe something like:
{% if environment_name == 'DEV' %}
{% set extra_java_opts = "-Xmx=2000Mb -Xms=1000Mb" %}
{% elif environment_name == 'PROD' %}
{% set extra_java_opts = "-Xmx=20000Mb -Xms=10000Mb" %}
{% endif %}
Environment="JAVA_OPTS=-Djava.net.preferIPv4Stack=true -Denvironmentname={{environment_name | default('NOT_SET') }} {{ extra_java_opts }}"
This isn't the only way of tackling this problem (for example, you could put an if block inline in the Environment= statement, but I think that gets messy).
Read through the control structures section of the Jinja documentation for more information about if/then, and read about assignments for information about the set directive.
Having said that, I would probably put this logic in my ansible playbook, rather than embedding it in the template:
- set_fact:
extra_java_opts: "-Xmx=2000Mb -Xms=1000Mb"
when: environment == 'DEV'
- set_fact:
extra_java_opts: "-Xmx=20000Mb -Xms=10000Mb"
when: environment == 'PROD'
This makes the templates much simpler, and keeps all your logic in one place rather than splitting it between the playbooks and the templates.

How are Ansible group variable files parsed?

In particular, I have a group_vars/all.yml file, with essentially the following contents:
my_foo: asdf
my_bar: '{{ my_foo }}'
If I later, while templating a file in a playbook, such as:
- name: template a file
template:
src: something.j2
dest: '....'
And in that something.j2 file, I use {{ my_bar }}, I somehow get asdf. How?
My prior understanding was that files such as group_vars/all.yml where essentially parsed as parse_yaml(parse_and_evaluate_jinja2(contents_of_the_file)). But if this were true, the line my_bar: '{{ my_foo }}' would not correctly evaluate: either we'd get an error, since my_foo is undefined to Jinja, or some default text, like "undefined", or ''. It's like the YAML was being parsed at the same time as the the Jinja2, which seems unlikely. Is that really what Ansible does? Or am I missing something else entirely?
(I couldn't find any exact documentation on how variable files are parsed in Ansibles docs, so if they exist, links are appreciated.)
My prior understanding was that files such as group_vars/all.yml where essentially parsed as parse_yaml(parse_and_evaluate_jinja2(contents_of_the_file))
Wrong. Ansible does all evaluation in runtime, for example:
You tell Ansible:
- debug:
msg: "{{ my_bar }}"
So when Ansible is going to pass msg parameter to debug module, it will do:
What is the parameter?
The parameter is {{ my_bar }}. Oh, it's a template and there's variable my_bar.
What is the my_bar's value?
The value is {{ my_foo }}. Oh, it's a template again, and there's variable my_foo.
What is the my_foo's value?
The value is asdf. Good! Nothing more to template.
So, my_bar's value is now asdf.
And parameter value is not asdf.
Let's print asdf.
By writing:
my_var: "{{ my_other_var }}"
you don't assign variables as in general programming, but write a simple template to calculate my_var's value in any given time (runtime), and not in time of parsing.

Include external yaml file in ansible task

I am creating an ecs_taskdefinition in ansible, but I would like the task-defintion in a sperate file. Can I somehow do something like this:
ecs_taskdefintion:
containers: {{ load_external_yaml containers.yaml }}
volumes: {{ load_external_yaml_volumes.yaml }}
So I want to load the yaml data from external files.
You may try to combine file lookup and from_yaml filter like this:
{{ lookup('file','containers.yaml') | from_yaml }}
Remember that lookups are local, so containers.yaml should be on ansible control host.
Since your file is YAML, you may use include_vars
from https://docs.ansible.com/ansible/latest/collections/ansible/builtin/include_vars_module.html :
- name: Setup vars
tags: ["always"]
include_vars:
file: "./vars/tintin.yaml"
name: tintin
Use tintin as a normal var everywhere!

Resources