pythonyamlansiblecidrvpc

How to find a free VPC CIDR block with Ansible?


I've got an existing set of VPC's in AWS, each with their own CIDR blocks:

vpc1: 20.0.0.0/16 vpc2: 20.10.0.0/16

Now, I'm trying to write a playbook, which pulls in the existing list of VPC's and then finds an available CIDR (e.g. in the above example, the next available one would be 20.20.0.0/16, though I'm not too concerned about keeping it sequential).

I know that doing this works with two lists:

- name: Loop over all possible CIDRs
    debug: msg="Found a free CIDR {{ item }}"
    with_items: all_potential_cidrs
    when: "\"{{ item }}\" not in {{ currently_used_cidrs }}"

However, the way I'm getting the list of existing CIDRs is:

- name: Get list of VPCs and their CIDR blocks
  command: aws ec2 describe-vpcs --output json
  register: cli_output

- name: Register variables
  set_fact:
  current_vpcs: "{{ cli_output.stdout | from_json }}"

The command gives the following data back (in JSON format):

{
    "Vpcs": [
        {
            "VpcId": "vpc-4aad0c23",
            "InstanceTenancy": "default",
            "Tags": [
                {
                    "Value": "vpc1",
                    "Key": "Name"
                }
            ],
            "State": "available",
            "DhcpOptionsId": "dopt-ff6b238f",
            "CidrBlock": "20.0.0.0/16",
            "IsDefault": false
        },
        {
            "VpcId": "vpc-d4101abc",
            "InstanceTenancy": "default",
            "Tags": [
                {
                    "Value": "vpc2",
                    "Key": "Name"
                }
            ],
            "State": "available",
            "DhcpOptionsId": "dopt-eaaab38c",
            "CidrBlock": "20.10.0.0/16",
            "IsDefault": false
        }
    ]
}

Which allows getting all CIDR blocks as follows:

- name: Print filtered VPCs and their subnets
  debug: msg="VPC ID {{ item.VpcId }}, VPC CIDR block {{ item.CidrBlock }}"
  with_items: current_vpcs.Vpcs

However, as "CidrBlock" is a property of a list item, I'm unable to use it in the "when" statement, which requires a List:

when: "{{ item }}" not in {{ list_of_cidrs }}"

How can I take the "CidrBlock" property of each of the "Vpcs" items and turn it into it's own list, in order to pass it to the "when: ... not in ..." statement?


Solution

  • You can turn the list of VPC items into a list of just the CIDR blocks using Jinja's map filter.

    I'm not exactly sure what you're trying to accomplish what this playbook, but here's an example that allows you to convert the list of "Vpcs" items into a list of strings that are the CidrBlock of each vpc item.

    You can see it working in:

    - name: Print just the CIDRS
      debug: msg='{{ current_vpcs.Vpcs|map(attribute="CidrBlock")|list }}'
    

    Assuming you have current_vpcs set, the line above outputs

    TASK: [Print just the CIDRS] **************************************************
    ok: [localhost] => {
        "msg": "[u'20.0.0.0/16', u'20.10.0.0/16']"
     }
    

    Full working example:

    ---
    - name: ok
      hosts: localhost
      gather_facts: no
    
      vars:
        all_available_cidrs:
          - "20.0.0.0/16"
          - "20.10.0.0/16"
          - "20.20.0.0/16"
    
      tasks:
    
        - name: Get list of VPCs and their CIDR blocks
          command: aws ec2 describe-vpcs --output json
          register: cli_output
    
        - name: Register variables
          set_fact:
            current_vpcs: "{{ cli_output.stdout | from_json }}"
    
        - name: Print VPCs and their subnets
          debug: msg="VPC ID {{ item.VpcId }}, VPC CIDR block {{ item.CidrBlock }}"
          with_items: current_vpcs.Vpcs
    
        - name: extract list_of_cidrs into fact
          set_fact: 
            list_of_cidrs: '{{ current_vpcs.Vpcs|map(attribute="CidrBlock")|list|to_json }}'
    
        - name: Print just the CIDRS
          debug: var=list_of_cidrs
    
        - name: Print available unused cidrs
          debug: msg="available unused VPC CIDR block {{ item }}"
          with_items: all_available_cidrs
          when: '"{{ item }}" not in {{ list_of_cidrs }}'
    

    The line that turns current_vpcs.Vpcs into the list ["20.0.0.0/16", "20.10.0.0/16"] is:

    set_fact: 
      list_of_cidrs: '{{ current_vpcs.Vpcs|map(attribute="CidrBlock")|list|to_json }}'
    

    Note: it is necessary to use |list|to_json to get Jinja to template the in statement properly. map returns a generator whose representation can't be used as the argument of a Jinja in statement. There may be a cleaner way to do this, but using |list|to_json is the solution people found in This ticket


    Edit: add variable all_available_cidrs to make the full example closer match the original question