Issue about defining variable within the task does not work

Hello guys

I created the following structure:

amis:
us-east-1:
amazonlinux:
owner_id: "137112412989"
x86_64: "ami-01bc990364452ab3e"
arm64: "ami-0900a8f768a21540a"
user: "ec2-user"
ubuntu:
owner_id: "099720109477"
x86_64: "ami-0fc5d935ebf8bc3bc" # Ubuntu 22
arm64: "ami-016485166ec7fa705" #
user: "ubuntu"

and I have a json file that I fill in to raise an ec2 instance:

{
"aws_region":"us-east-1",
"architecture":"arm64",
"os_version":"ubuntu",
"instance_name":"test6",
"domain":"example.corp",
"subnet":"SUBNET-PUB-A",
"instance_type":"t4g.micro",
"security_groups": [ "sg-xxxxxx", "sg-yyyyyyyy"],
"aws_role":"default-role",
"root_volume_size":20,
"ebs_swap_size": 4,
"keyname": "ssh-key",
"ebs_type": "gp3",
"backup": "no",

"boxenv":"DEV"
}

And I read this json and create the variables with the tasks:

- name: Read json configuration
shell: cat config-ec2-launch.json
register: result

- name: save the Json data to a Variable as a Fact
set_fact:
jsondata: "{{ result.stdout | from_json }}"

- name: Variable | Get aws_region variable
set_fact:
aws_region: "{{ jsondata | json_query(jmesquery) }}"
vars:
jmesquery: 'aws_region'

- name: Variable | Get architecture variable
set_fact:
architecture: "{{ jsondata | json_query(jmesquery) }}"
vars:
jmesquery: 'architecture'

- name: Variable | Get os_version variable
set_fact:
os_version: "{{ jsondata | json_query(jmesquery) }}"
vars:
jmesquery: 'os_version'

and created the tasks right away:


- name: AWS Ec2 Instance | Create the EC2 instance
ec2_instance:
state: started # started state=running + waits for EC2 status checks to report OK if wait=true
image_id: "{{ newest_ami }}"
name: "{{ instance_name }}"
detailed_monitoring: false
metadata_options:
http_tokens: required
instance_type: "{{ instance_type }}"
region: "{{ aws_region }}"
vpc_subnet_id: "{{ [subnet_facts.subnets.0.id](http://subnet_facts.subnets.0.id) }}"
instance_initiated_shutdown_behavior: stop
instance_role: "{{ aws_roles[aws_role] }}"
volumes:
- device_name: "{{ name_device_root_ami.images.0.root_device_name }}"
ebs:
volume_type: gp3
volume_size: "{{ root_volume_size }}"
delete_on_termination: true
security_groups: "{{ security_groups }}"
tags:
Name: "{{ instance_name }}"
Domain: "{{ domain }}"
Backup: "{{ backup }}"
BOXENV: "{{ boxenv }}"
key_name: "{{ aws_key_names[keyname] }}"
wait: yes
register: ec2

- name: AWS Ec2 ebs | Add volume to swap
amazon.aws.ec2_vol:
instance: "{{ ec2.instances[0].instance_id }}"
volume_size: "{{ ebs_swap_size }}"
volume_type: "{{ ebs_type }}"
delete_on_termination: True
device_name: "/dev/sdf"
region: '{{ aws_region }}'
tags:
Name: "{{ instance_name }}:swap"
BOXENV: "{{ boxenv }}"

- name: Swap | List all devices on {{ instance_name }}
vars:
ansible_ssh_user: "{{ amis[ aws_region ][ os_version ].user }}"
ansible.builtin.shell: echo "{{ aws_region }} {{ ec2.instances.0.network_interfaces.0.private_ip_address }}"
register: devices_host
delegate_to: "{{ ec2.instances.0.network_interfaces.0.private_ip_address }}"
args:
executable: /bin/bash

But when I run it, it displays the error below

FAILED! => {"msg": "'aws_region' is undefined"}

If I comment the line ansible_ssh_user: "{{ amis[ aws_region ][ os_version ].user }}" it displays the contents of the aws_region variable. Any idea where I’m going wrong?

Thanks

It’s even stranger than that (until you realize what’s going on).
Rather than commenting out the “ansible_ssh_user:” line, just misspell the variable, like “ansible_ssh_userx”. It’ll work then, too — but with the wrong user of course.

What you’re running into is a combination of things: set_fact, delegate_to, and the bit you didn’t show, which is your playbook’s “hosts:” line and/or your command’s “–limit” specification. Also the “special variable” function of the “ansible_ssh_user” variable itself. Maybe throw in lazy variable template evaluation for good measure.

The error message is correct but incomplete, in that “aws_region” is undefined. What the message leaves out is “… in the context of the delegation host ‘ec2.instances.0.network_interfaces.0.private_ip_address’”.

You set the task variable “ansible_ssh_user” to a template. By lazy template evaluation, that template isn’t evaluated until the variable is used, which is at the point of connection. So it’s evaluated in the context of a host which has no set_fact-derived variables defined: ‘ec2.instances.0.network_interfaces.0.private_ip_address’. Only your play hosts will have set_fact-derived variables.

Assuming your hosts line is something like “hosts: localhost”, then you can work around it by changing your variable definition to
ansible_ssh_user: “{{ amis[**hostvars[‘localhost’].**aws_region][**hostvars[‘localhost’].**os_version].user }}”

I’m reasonably sure (but could be wrong) that the “amis” dict is defined, because it isn’t a host variable. That’s different from set_fact-derived variables which are both host-specific and template resolved at their creation.

Let us know if this resolves your issue.

Ricardo-
I would also recommend changing how you are sending the json to the playbook. I have done this a lot in AWS and found it is much cleaner to either A) use a variable group file, or B) pass it via a parameter file. Both methods help avoid some of what Todd is referring to.

Thanks Todd,

It worked. So because “delegate_to” the localhost host variables are not available I need to pass the variables with hostvars[ host ], right?

Thank you I will correct the entire playbook

Thanks for the tip Evan :slight_smile: