Ansible's handling of string representation of complex objects

I’ve run into a weird behaviour regarding how Ansible handles converting strings to complex objects.

I have the following string representation of a list of dicts that I’m returning from a Cloudformation stack output:

[{“id”: “subnet-f3i4ven6lxvxr3eu”,“az”: “us-east-1a”},{“id”: “subnet-0wovp0yu5sr8pnwv”,“az”: “us-east-1b”}]

I’ve been using this in a playbook, passing it to the “loop” parameter in the following task:

  • name: provision instances
    ec2:
    key_name: keypair1
    instance_type: t2.micro
    image: “{{ ami_id }}”
    wait: no
    exact_count: 1
    count_tag:
    Name: “instance{{ loop_count + 1 }}”
    Type: type1
    AZ: “{{ item.az }}”
    group_id: “{{ cf_results.stack_outputs.SecurityGroupDetails|from_json|map(attribute=‘id’)|list }}”
    termination_protection: yes
    vpc_subnet_id: “{{ item.id }}”
    volumes:
  • device_name: /dev/sda1
    volume_type: gp2
    volume_size: 50
    delete_on_termination: true
    instance_tags:
    Name: “instance{{ loop_count + 1 }}”
    Type: type1
    Environment: prod
    VPC: vpc1
    Subnet: “{{ item.name }}”
    AZ: “{{ item.az }}”
    monitoring: yes
    region: us-east-1
    loop: “{{ cf_results.stack_outputs.SubnetDetails }}”
    loop_control:
    index_var: loop_count
    register: instances

The above task works fine. However, I’ve been trying to use it to create another, single EC2 instance and I’m using filters to just select the first subnet from the list, as follows:

  • name: provision vpn instance
    ec2:
    key_name: keypair1
    instance_type: t2.micro
    image: “{{ ami_id }}”
    wait: no
    exact_count: 1
    count_tag:
    Name: extra_instance
    Type: type2
    AZ: “{{ cf_results.stack_outputs.SubnetDetails[0].az }}”
    group_id: “{{ cf_results.stack_outputs.SecurityGroupDetails|from_json|map(attribute=‘id’)|list }}”
    termination_protection: yes
    vpc_subnet_id: “{{ cf_results.stack_outputs.SubnetDetails[0].id }}”
    assign_public_ip: yes
    instance_tags:
    Name: extra_instance
    Type: type2
    Environment: prod
    VPC: vpc1
    Subnet: “{{ cf_results.stack_outputs.SubnetDetails[0].id }}”
    AZ: “{{ cf_results.stack_outputs.SubnetDetails[0].az }}”
    monitoring: yes
    region: “{{ static_config.region }}”

register: extra_instance

When the above task is executed it fails with the following error: “The task includes an option with an undefined variable. The error was: ‘str object’ has no attribute ‘az’”.

I added a ‘debug’ task to just output the first item from the list, it worked but as you can see it just outputs the first character in the string:

TASK [debug] *****************************************************************************************
ok: [localhost] => {
“cf_results.stack_outputs.SubnetDetails[0]”: “[”

}

Is there a reason why the loop parameter successfully converts the string to a list but the filter doesn’t?

Thanks,
Guy

Due to some historical integrations between ansible and jinja2, we try to convert things that look like python data structures, to actual python data structures. This is because historically, jinja2 could only return string representations of python data structures, not actual python data structures.

Because you have to template out the value to pass to the loop, it get’s magically converted to a python DS before it hits the loop.

This happens in: https://github.com/ansible/ansible/blob/e32d60bbcd44ccc7761268c6d6f00db036db962b/lib/ansible/template/init.py#L577-L590

It only happens if the result of the template action produces something that starts with { or [

So in your second case, you are trying to access an element of a string.

Looking in your first task I see you are using the following:

group_id: “{{ cf_results.stack_outputs.SecurityGroupDetails|from_json|map(attribute=‘id’)|list }}”

You would need to do something like that, where you use from_json in your other uses.