Iterate over multiple outputs in Terraform template file - for-loop

I am trying to create a Ansible inventory file with Terraform in the following format
10.10.10.10 #test-vm
output.tf:
output "vm_name" {
value = toset([
for vm_names in azurerm_linux_virtual_machine.vm : vm_names.name
])
}
output "vm_ips" {
value = toset([
for vm_ips in azurerm_linux_virtual_machine.vm : vm_ips.private_ip_address ])
}
Terraform template file:
%{ for vm in vm_ips}:
%{for vm in vm_names ~}:
${mc} ${mc_name}
%{ endfor ~}
%{ endfor ~}
The above produces
10.1.0.14 #vm1
10.1.0.14 #vm2
10.1.0.7 #vm1
10.1.0.7 #vm2
Instead of
10.1.0.14 #vm1
10.1.0.7 #vm2
Any suggestion how to iterate over two outputs correctly?

You should create a list of map or tuple instead, so virtual machines names and IPs are linked.
output "vms" {
value = [for vm in azurerm_linux_virtual_machine.vm : {
name = vm.name,
ip = vm.private_ip_address
}]
}
Then in your template:
%{ for vm in vms ~}
${vm.ip} #${vm.name}
%{ endfor ~}
This would renders in the expected
10.1.0.14 #vm1
10.1.0.7 #vm2

Related

How to pass ip-address from terraform to ansible [duplicate]

I am trying to create Ansible inventory file using local_file function in Terraform (I am open for suggestions to do it in a different way)
module "vm" config:
resource "azurerm_linux_virtual_machine" "vm" {
for_each = { for edit in local.vm : edit.name => edit }
name = each.value.name
resource_group_name = var.vm_rg
location = var.vm_location
size = each.value.size
admin_username = var.vm_username
admin_password = var.vm_password
disable_password_authentication = false
network_interface_ids = [azurerm_network_interface.edit_seat_nic[each.key].id]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
output "vm_ips" {
value = toset([
for vm_ips in azurerm_linux_virtual_machine.vm : vm_ips.private_ip_address
])
}
When I run terraform plan with the above configuration I get:
Changes to Outputs:
+ test = [
+ "10.1.0.4",
]
Now, in my main TF I have the configuration for local_file as follows:
resource "local_file" "ansible_inventory" {
filename = "./ansible_inventory/ansible_inventory.ini"
content = <<EOF
[vm]
${module.vm.vm_ips}
EOF
}
This returns the error below:
Error: Invalid template interpolation value
on main.tf line 92, in resource "local_file" "ansible_inventory":
90: content = <<EOF
91: [vm]
92: ${module.vm.vm_ips}
93: EOF
module.vm.vm_ips is set of string with 1 element
Cannot include the given value in a string template: string required.
Any suggestion how to inject the list of IPs from the output into the local file while also being able to format the rest of the text in the file?
If you want the Ansible inventory to be statically sourced from a file in INI format, then you basically need to render a template in Terraform to produce the desired output.
module/templates/inventory.tmpl:
[vm]
%{ for ip in ips ~}
${ip}
%{ endfor ~}
alternative suggestion from #mdaniel:
[vm]
${join("\n", ips)}
module/config.tf:
resource "local_file" "ansible_inventory" {
content = templatefile("${path.module}/templates/inventory.tmpl",
{ ips = module.vm.vm_ips }
)
filename = "${path.module}/ansible_inventory/ansible_inventory.ini"
file_permission = "0644"
}
A couple of additional notes though:
You can modify your output to be the entire map of objects of exported attributes like:
output "vms" {
value = azurerm_linux_virtual_machine.vm
}
and then you can access more information about the instances to populate in your inventory. Your templatefile argument would still be the module output, but the for expression(s) in the template would look considerably different depending upon what you want to add.
You can also utilize the YAML or JSON inventory formats for Ansible static inventory. With those, you can then leverage the yamldecode or jsondecode Terraform functions to make the HCL2 data structure transformation much easier. The template file would become a good bit cleaner in that situation for more complex inventories.

Override group_vars by external source

I have a pilote project keeping many common variables in group_vars.
group_vars/
group1.yml
group2.yml
group3.yml
For different implementations (usually per client), I'd like to maintain reserved file which overrides the content of group_vars, where the content of that file could have following format, i.e. client1.yml :
group1:
var11_to_override: "foo"
var12_to_override: "bar"
group2:
var21_to_override: "foo"
var22_to_override: "bar"
Is there a simple possibility to say to Ansible that file client1.yml overrides group_vars content?
The module include_vars could be certainly the first step together with set_facts within a loop, but it requires probably complicated jinja2 filter expressions ...
Have I to write a new module or filter updating hostvars?
Finally resolved by custom filter updating a dict by another:
filter_plugins/vars_update.py
import copy
import collections
class FilterModule(object):
def update_hostvars(self, _origin, overlay):
origin = copy.deepcopy(_origin)
for k, v in overlay.items():
if isinstance(v, collections.Mapping):
origin[k] = self.update_hostvars(origin.get(k, {}), v)
else:
origin[k] = v
return origin
def filters(self):
return {"update_hostvars": self.update_hostvars}
.. and using this filter when updating all variables:
- name: Include client file
include_vars:
file: "{{ client_file_path }}"
name: client_overlay
- name: Update group_vars by template client
set_fact:
"{{ item.key }}": "{{ hostvars[inventory_hostname][item.key] | update_hostvars(item.value) }}"
with_dict: "{{ client_overlay }}"
Using the examples given in this thread i made my own solution:
The "external source" feeds in an inventory item using --extra-vars "#". The file content itself is uploaded as base64 encoded content and then decoded/written to fs.
The external file has a list of overrides per role/group like so:
role_overrides: [{
"groups": [
"my-group"
],
"overrides": {
"foo": "value",
"bar": "value",
}
},
but then jsonified obviously...
The filter module
#!/usr/bin/env python
class FilterModule(object):
def filters(self):
return {
"filter_hostvars_overrides": self.filter_hostvars_overrides,
}
def filter_hostvars_overrides(self, role_overrides, group_names):
"""
filter the overrides for the ones to apply for this host
[
{
"groups": [
"my-group"
],
"overrides": {
"foo: 42,
}
},
:param group_names: List of groups this host is member of
:param role_overrides: document with all overrides; to be filtered using groups_names
:return: items to be set
"""
overrides = {}
for idx, per_group_overrides in enumerate(role_overrides):
groups = per_group_overrides.get("groups", [])
if set(groups).intersection(set(group_names)):
overrides.update(per_group_overrides.get("overrides", {}))
return overrides
The play code:
- name: Apply group overrides
set_fact:
"{{ item.key }}": "{{ item.value }}"
with_dict: "{{ role_overrides | filter_hostvars_overrides(group_names) }}"

ANSIBLE: How to append variable from inventory groups

I have an ansible inventory with groups as follows:
+hosts/
+all/
+group_vars/
- linux.yml
- webserver.yml
- dbserver.yml
...
And I have a playbook that sets monitoring for hosts; and the kind of monitoring is done by plugins. So in each group y set a list monitoring_plugins that contain the plugin to be able t monitor each service.
Inside each group yml I try to "append" to the list:
monitoring_plugins: {{ monitoring_plugins|default([]) + [ 'whatever_plugin_correspond_to_group' ] }}
But it doesn't work as expected, being expected that if a host belongs to more than one group, it should have the plugins corresponding to those groups.
Is there a way to acommplish this?
Thanks in advance.
What you're describing should work as expected from within a task, but you cannot have executable code in a vars or group_vars yaml or json file -- those are static
So you will need to set a distinct name at the low level and then roll them up at the top level:
group_vars/
dbserver.yml # sets monitoring_plugins_dbserver: ["a", "b"]
linux.yml # sets monitoring_plugins_linux: ["c", "d"]
and then in your tasks, something like this (but be forewarned I haven't tested this specific example):
- set_fact:
monitoring_plugins: >-
{% set results = [] %}
{% for g in groups.keys() %}
{% set _ = results.extend(vars['monitoring_plugins_'+g]|d([])) %}
{% endif %}
{{ results }}

how to get var value which with a var in name using jinja2

I am using ansible to template a jinja2 file.
IP:{{ ansible_eth0.ipv4.address }}
IP:{{ ansible_docker0.ipv4.address }}
IP:{{ ansible_{{ ka_interface }}.ipv4.address }}
there is a var named ka_interface for network adapter.
but you will get error in 3rd var
(IP:{{ ansible_{{ ka_interface }}.ipv4.address }} )
It seems that var in jinja2 template can be nested.
It's not possible to construct a dynamic variable with Jinja2 syntax.
However, you can access any play-bound variables via the builit-in vars hash object:
{{ vars['ansible_' + ka_interface]['ipv4']['address] }}
Edit: Fixed hash syntax
follow Chris Lam 's advice,
It works
- name: test
shell: echo {{ vars['ansible_' + ka_interface]['ipv4']['address'] }}
tags: test

How does Ansible module return fact

I write Ansible module my_module that need to set some facts.
I define in module the below code
....
response = {
"hello": "world",
"ansible_facts" : {
"my_data": "xjfdks"
}
}
module.exit_json(changed=False, meta=response)
Now in playbook after execution my_module I want access to new facts, but it's not define
- my_module
- debug: msg="My new fact {{ my_data }}"
What is the correct way to do it?
You should set ansible_facts directly in module's output, not inside meta.
To return all response's keys from your example:
module.exit_json(changed=False, **response)
Or only for ansible_facts:
module.exit_json(changed=False, ansible_facts=response['ansible_facts'])

Resources