Inspired by this tweet from @karloncode I thought it might be useful to put together a blog entry on the benefits of using GraphQL over simple rest API calls. This has come about because of my own experience using GraphQL and Nautobot in the day job.

Karl asked two questions:

  1. My lazy brain tells me to stay simple/RESTy, but before I get started I’m curious if anyone has opinions on if/how I should think about GraphQL?
  2. am wondering [what] GraphQL .. buys me that I can’t just roll myself over a simple REST interface.

My answer to the first question is yes, and the answer to the second question is “more efficent client apps and reduced API calls”. In this blog I’m going to rely on my own experiences of using API and GraphQL queries with Nautobot. They point here is not about Nautobot specifically, or even about Network Automation more generally, but a more general coding point about performance and efficency.

For the purpose of this example, I have used the Nautobot LAB container with the mock data loaded. Nautobot has a number of internal models to manage the data stored inside itself. At a very simple level, for the purpose of this blog entry we are interested in four models:

  1. Sites (/dcim/sites)
  2. Devices (/dcim/devices)
  3. Interfaces (/dcim/interfaces)
  4. IP Address (/ipam/ip-addresses)

Each of these are distinct models inside Nautobot. There is a 1 to many relationship between Sites and Devices, and another between Devices and Interfaces, and finally another one between Interfaces and IP Address.

Nautobot Mock Singapore

Now, suppose I want a list of all the interfaces in a site which have an IP address configured on them.

Using the REST API I can send the following query based on the above screenshot: https://HOSTNAME/api/dcim/interfaces/?site=sin which gives the following response

Postman Response

We can see there are 348 interfaces in the site. Looking closely at the first entry (Ethernet1/1 on sin-edge-1):

       {
            "id": "999b3017-fef2-46a6-9c06-02e51e291ab7",
            "url": "http://127.0.0.1:8000/api/dcim/interfaces/999b3017-fef2-46a6-9c06-02e51e291ab7/",
            "device": {
                "id": "166ace48-a3f8-4dba-bc4b-3eca0e9d6f4a",
                "url": "http://127.0.0.1:8000/api/dcim/devices/166ace48-a3f8-4dba-bc4b-3eca0e9d6f4a/",
                "name": "sin-edge-01",
                "display": "sin-edge-01"
            },
            "name": "Ethernet1/1",
            "label": "",
            "type": {
                "value": "100gbase-x-qsfp28",
                "label": "QSFP28 (100GE)"
            },
            "enabled": true,
            "lag": null,
            "mtu": null,
            "mac_address": null,
            "mgmt_only": false,
            "description": "",
            "mode": null,
            "untagged_vlan": null,
            "tagged_vlans": [],
            "cable": {
                "id": "f26ecb30-298a-471c-8069-1dc1e8e6123c",
                "url": "http://127.0.0.1:8000/api/dcim/cables/f26ecb30-298a-471c-8069-1dc1e8e6123c/",
                "label": "",
                "display": "#f26ecb30-298a-471c-8069-1dc1e8e6123c"
            },
            "cable_peer": {
                "id": "5888b775-7519-4253-89e4-e4c0b1fef995",
                "url": "http://127.0.0.1:8000/api/dcim/interfaces/5888b775-7519-4253-89e4-e4c0b1fef995/",
                "device": {
                    "id": "11a12d9d-c346-4fca-842e-4d354ecb5201",
                    "url": "http://127.0.0.1:8000/api/dcim/devices/11a12d9d-c346-4fca-842e-4d354ecb5201/",
                    "name": "sin-edge-02",
                    "display": "sin-edge-02"
                },
                "name": "Ethernet1/1",
                "cable": "f26ecb30-298a-471c-8069-1dc1e8e6123c",
                "display": "Ethernet1/1"
            },
            "cable_peer_type": "dcim.interface",
            "connected_endpoint": {
                "id": "5888b775-7519-4253-89e4-e4c0b1fef995",
                "url": "http://127.0.0.1:8000/api/dcim/interfaces/5888b775-7519-4253-89e4-e4c0b1fef995/",
                "device": {
                    "id": "11a12d9d-c346-4fca-842e-4d354ecb5201",
                    "url": "http://127.0.0.1:8000/api/dcim/devices/11a12d9d-c346-4fca-842e-4d354ecb5201/",
                    "name": "sin-edge-02",
                    "display": "sin-edge-02"
                },
                "name": "Ethernet1/1",
                "cable": "f26ecb30-298a-471c-8069-1dc1e8e6123c",
                "display": "Ethernet1/1"
            },
            "connected_endpoint_type": "dcim.interface",
            "connected_endpoint_reachable": true,
            "tags": [],
            "count_ipaddresses": 1,
            "custom_fields": {
                "role": "peer"
            },
            "display": "Ethernet1/1"
        },

We can see that there is one IP address assigned to this interface (based on the count_ipaddresses field). What we can’t see is what that IP address is. This would neccessitate a follow up query:

GET http://HOSTNAME/api/ipam/ip-addresses/?assigned_object_id=999b3017-fef2-46a6-9c06-02e51e291ab7

which gives the following response:

{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": "36b12783-4ccc-40bf-a562-114abb0201f3",
            "url": "http://127.0.0.1:8000/api/ipam/ip-addresses/36b12783-4ccc-40bf-a562-114abb0201f3/",
            "family": {
                "value": 4,
                "label": "IPv4"
            },
            "address": "10.17.192.0/32",
            "vrf": null,
            "tenant": null,
            "status": null,
            "role": null,
            "assigned_object_type": "dcim.interface",
            "assigned_object_id": "999b3017-fef2-46a6-9c06-02e51e291ab7",
            "assigned_object": {
                "id": "999b3017-fef2-46a6-9c06-02e51e291ab7",
                "url": "http://127.0.0.1:8000/api/dcim/interfaces/999b3017-fef2-46a6-9c06-02e51e291ab7/",
                "device": {
                    "id": "166ace48-a3f8-4dba-bc4b-3eca0e9d6f4a",
                    "url": "http://127.0.0.1:8000/api/dcim/devices/166ace48-a3f8-4dba-bc4b-3eca0e9d6f4a/",
                    "name": "sin-edge-01",
                    "display": "sin-edge-01"
                },
                "name": "Ethernet1/1",
                "cable": "f26ecb30-298a-471c-8069-1dc1e8e6123c",
                "display": "Ethernet1/1"
            },
            "nat_inside": null,
            "nat_outside": null,
            "dns_name": "",
            "description": "",
            "tags": [],
            "custom_fields": {},
            "created": "2021-04-16",
            "last_updated": "2021-04-16T13:43:02.906269Z",
            "display": "10.17.192.0/32"
        }
    ]
}

Now this raises a couple of issues:

  1. All I want is a structure that has site names, devices, interfaces and ips. There’s a lot of information here I don’t need.
  2. The way this works is going to be a loop - potentially over 348 entities in this case - where I may have to make an API call.

The first issue is less significant - though if you have a lot of interfaces in question it may take some time to transfer the data (the test sample is about 307KB of data - in a DC with a few switches with 1800 ports that could be a significant amount to be transferring).

The second issue is probably the bigger source of delay though - each API call accross the network is relatively expensive because of the setup.

In any case, the first thing I would need to do is to immediately process the JSON structure and build the structure I need.

The alternative though is GraphQL:

{
  interfaces(site: "sin") {
    name
    device {
      name
    }
    ip_addresses {
      address
    }
  }
}

Sending this via a POST to https://HOSTNAME/api/graphql can be done with this python code:

import requests
import json

url = "http://127.0.0.1:8000/api/graphql/"

payload="{\"query\":\"{\\n  interfaces(site: \\\"sin\\\") {\\n    name\\n    device {\\n      name\\n    }\\n    ip_addresses {\\n      address\\n    }\\n  }\\n}\",\"variables\":{}}"
headers = {
  'Authorization': 'Token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
  'Content-Type': 'application/json',
  'Cookie': 'REDACTED'
}

response = requests.request("POST", url, headers=headers, data=payload)

print(response.text)

And this is (part of) the returned data.

{
    "data": {
        "interfaces": [
            {
                "name": "Ethernet1/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.0/32"
                    }
                ]
            },
            {
                "name": "Ethernet2/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.2/32"
                    }
                ]
            },
            {
                "name": "Ethernet3/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.4/32"
                    }
                ]
            },
            {
                "name": "Ethernet4/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.8/32"
                    }
                ]
            },
            {
                "name": "Ethernet5/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.12/32"
                    }
                ]
            },
            {
                "name": "Ethernet6/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.16/32"
                    }
                ]
            },
            {
                "name": "Ethernet7/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.20/32"
                    }
                ]
            },
            {
                "name": "Ethernet8/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.24/32"
                    }
                ]
            },
            {
                "name": "Ethernet9/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.28/32"
                    }
                ]
            },
            {
                "name": "Ethernet10/1",
                "device": {
                    "name": "sin-edge-01"
                },
                "ip_addresses": [
                    {
                        "address": "10.17.192.32/32"
                    }
                ]
            },
          ]
            # Rest deleted for brevity...
}

Two things to note here:

  1. In this particular case, the data transferred accross the network is now only 27KB.
  2. There’s only one API call to get all the data.

Using the general principle that it’s less expensive to do processing on the local device than to transfer data accross the network, it seems clear to me that using the GraphQL is a good way to speed up code.

A couple of minor caveats though:

  1. Instead of doing the processing of the relationship between the models on client side you’ve now transferred that load onto the server side.
  2. in discussion with some of the folks at NtC last year they did point out that complex GraphQL queries could take “a long time” to respond - I don’t think the example given here counts as complex though.