Convert an array of strings to a dictionary with JQ?

Viewed 1402

I have trying to convert the AWS public IP ranges into a format that can be used with the Terraform external data provider so I can create a security group rule based off the AWS public CIDRs. The provider requires a single JSON object with this format:

{"string": "string"}

Here is a snippet of the public ranges JSON document:

{
  "syncToken": "1589917992",
  "createDate": "2020-05-19-19-53-12",
  "prefixes": [
    {
      "ip_prefix": "35.180.0.0/16",
      "region": "eu-west-3",
      "service": "AMAZON",
      "network_border_group": "eu-west-3"
    },
    {
      "ip_prefix": "52.94.76.0/22",
      "region": "us-west-2",
      "service": "AMAZON",
      "network_border_group": "us-west-2"
    },
    // ...
]

I can successfully extract the ranges I care about with this, [.prefixes[] | select(.region == "us-west-2") | .ip_prefix] | sort | unique, and it gives me this:

[
  "100.20.0.0/14",
  "108.166.224.0/21",
  "108.166.240.0/21",
  "13.248.112.0/24",
  ...
]

I can't figure out how to convert this to an arbitrarily-keyed object with jq. In order to properly use the array object, I need to convert it to a dictionary, something like {"arbitrary-key": "100.20.0.0/14"}, so that I can use it in Terraform like this:

data "external" "amazon-ranges" {
  program = [
    "cat",
    "${path.cwd}/aws-ranges.json"
    ]
}

resource "aws_default_security_group" "allow-mysql" {
  vpc_id = aws_vpc.main.id

  ingress {
    description = "MySQL"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = [
      values(data.external.amazon-ranges.result)
    ]
  }
}

What is the most effective way to extract the the AWS public IP ranges document into a single object with arbitrary keys?

2 Answers

The following script uses the .ip_prefix as the key, thus perhaps avoiding the need for the sort|unique. It yields:

{
  "35.180.0.0/16": "35.180.0.0/16",
  "52.94.76.0/22": "52.94.76.0/22"
}

Script

#!/bin/bash

function data {
    cat <<EOF
{
  "syncToken": "1589917992",
  "createDate": "2020-05-19-19-53-12",
  "prefixes": [
    {
      "ip_prefix": "35.180.0.0/16",
      "region": "eu-west-3",
      "service": "AMAZON",
      "network_border_group": "eu-west-3"
    },
    {
      "ip_prefix": "52.94.76.0/22",
      "region": "us-west-2",
      "service": "AMAZON",
      "network_border_group": "us-west-2"
    }
    ]
}
EOF
}

data | jq '
  .prefixes 
  | map(select(.region | test("west"))
        | {(.ip_prefix): .ip_prefix} )
  | add '

There's a better option to get at the AWS IP ranges data in Terraform, which is to use the aws_ip_ranges data source, instead of trying to mangle things with the external data source and jq.

The example in the above linked documentation shows a similar, but also slightly more complex, thing to what you're trying to do here:

data "aws_ip_ranges" "european_ec2" {
  regions  = ["eu-west-1", "eu-central-1"]
  services = ["ec2"]
}

resource "aws_security_group" "from_europe" {
  name = "from_europe"

  ingress {
    from_port        = "443"
    to_port          = "443"
    protocol         = "tcp"
    cidr_blocks      = data.aws_ip_ranges.european_ec2.cidr_blocks
    ipv6_cidr_blocks = data.aws_ip_ranges.european_ec2.ipv6_cidr_blocks
  }

  tags = {
    CreateDate = data.aws_ip_ranges.european_ec2.create_date
    SyncToken  = data.aws_ip_ranges.european_ec2.sync_token
  }
}

To do your exact thing you would do something like this:

data "aws_ip_ranges" "us_west_2_amazon" {
  regions  = ["us_west_2"]
  services = ["amazon"]
}

resource "aws_default_security_group" "allow-mysql" {
  vpc_id = aws_vpc.main.id

  ingress {
    description = "MySQL"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = data.aws_ip_ranges.us_west_2_amazon.cidr_blocks
  }
}

However, there are 2 things that are bad here.

The first, and most important, is that you're allowing access to your database from every IP address that AWS has in US-West-2 across all services. That means that anyone in the world is able to spin up an EC2 instance or Lambda function in US-West-2 and then have network access to your database. This is a very bad idea.

The second is that if that returns more than 60 CIDR blocks you are going to end up with more than 60 rules in your security group. AWS security groups have a limit of 60 security group rules per IP address type (IPv4 vs IPv6) and per ingress/egress:

You can have 60 inbound and 60 outbound rules per security group (making a total of 120 rules). This quota is enforced separately for IPv4 rules and IPv6 rules; for example, a security group can have 60 inbound rules for IPv4 traffic and 60 inbound rules for IPv6 traffic. A rule that references a security group or prefix list ID counts as one rule for IPv4 and one rule for IPv6.

From https://docs.aws.amazon.com/vpc/latest/userguide/amazon-vpc-limits.html#vpc-limits-security-groups

This is technically a soft cap and you can ask AWS to raise this limit in exchange for reducing the amount of security groups that can be applied to a network interface to keep the maximum amount of security group rules at or below 1000 per network interface. It's probably not something you want to mess around with though.

Related