New Event! Join us January 28th in Durham, NC for Cocktails and Cloud Governance Register Now ❯

CloudQuery

AWS
GCP
Security
Tutorials

How to Inventory All Public IP Addresses Across AWS and GCP using CloudQuery

That public IP address you assigned to a test EC2 instance last Tuesday? It's probably already getting hammered by SSH brute-force attempts. Public IPs get indexed by Shodan within minutes of exposure. Bots start scanning them immediately. Your security team needs to know every single one exists.
The problem isn't tracking one IP address. The problem is tracking all of them when they're scattered across EC2 instances, RDS databases, load balancers, NAT Gateways, ECS tasks, API Gateways, and then you remember you also have GCP resources with Compute Engine instances, Cloud SQL databases, and Load Balancers.
No single command exists to list them all. Each cloud provider has different services. Each service has different API calls. You'd need to write custom scripts, maintain them as APIs change, and somehow combine results from two different cloud platforms into one view.
We'll show you how to inventory all your public IP addresses across AWS and GCP using CloudQuery. One configuration file. One sync command. One SQL query to see everything.

The Public IP Visibility Problem #

Public IP addresses exist in over 10 different AWS services. EC2 instances have them. RDS databases might have them (yours shouldn't, but let's check). Elastic Load Balancers definitely have them. ECS tasks can get them. NAT Gateways and Internet Gateways route traffic through public IPs. ElastiCache clusters might be exposed. Redshift clusters could be public. API Gateway custom domains use them.
GCP adds its own complexity. Compute Engine instances use external IPs. Cloud SQL instances might be publicly accessible. Cloud Load Balancing uses forwarding rules with external IPs. Cloud Run services can have public endpoints. Google Kubernetes Engine nodes might have external IPs.
Each service requires different API calls to extract IP information. AWS EC2 uses DescribeInstances to list instances with public IPs. DescribeAddresses shows Elastic IPs. DescribeNetworkInterfaces reveals network interface details. RDS requires DescribeDBInstances. Load balancers need DescribeLoadBalancers.
GCP uses completely different APIs. compute.instances.list shows Compute Engine instances. compute.addresses.list lists reserved external IP addresses. compute.forwardingRules.list shows load balancer IPs.
Security teams need this inventory for attack surface mapping. Every public IP represents potential exposure. OWASP Cloud Security guidelines recommend maintaining accurate asset inventories. CIS AWS Foundations Benchmark requires knowing which resources face the internet.
Cost teams need it too. Since February 2024, AWS charges $0.005 per hour for every public IPv4 address—attached or not. That's $3.65 per month per IP. An organization with 100 public IPs pays $365 monthly just for IP addresses. Unattached Elastic IPs deserve scrutiny—some are intentionally reserved for scaling or allowlisting, but many are simply forgotten.
Compliance frameworks demand it. PCI DSS requires documenting all system components that store, process, or transmit cardholder data. SOC 2 auditors ask for network diagrams showing internet-facing systems. You need the actual list of public IPs to answer these questions accurately.

Comparing Your Options for Multi-Cloud IP Inventory #

ApproachSetup TimeMulti-Cloud SupportQuery FlexibilityHistorical TrackingCost
AWS CLI + GCP gcloudHours (write custom scripts)Separate scripts for each cloudLimited to bash/jqManual storage requiredFree (your time)
AWS Config + GCP Asset InventoryDays (configure per account)Platform-specific, no unified viewPredefined queries only90 days (AWS Config)$0.003/item (AWS)
CSPM toolsDays to weeks (vendor onboarding)Yes (3+ clouds)Predefined queriesVendor-dependent$5k-$50k+/year
CloudQueryMinutes (one YAML file)70+ integrationsFull SQL in PostgreSQLUnlimited (append mode)Usage-based (pricing)

How CloudQuery Solves Multi-Cloud IP Inventory #

CloudQuery calls all the relevant AWS and GCP APIs automatically. It syncs the data to PostgreSQL (or any destination you choose). You write one SQL query to find every public IP address across both cloud platforms.
The CloudQuery AWS source plugin calls AWS APIs like EC2's DescribeInstances, DescribeNetworkInterfaces, and DescribeAddresses. It handles pagination automatically when you have thousands of resources. It respects AWS rate limits. It extracts public IP information from EC2 instances, Elastic IPs, network interfaces, RDS databases, and load balancers.
The CloudQuery GCP source plugin calls GCP APIs like compute.instances.list, compute.addresses.list, and compute.forwardingRules.list. It authenticates using service account keys. It handles GCP's quota system. It extracts external IP addresses from Compute Engine instances, reserved addresses, and load balancing rules.
Both plugins sync data to your destination database. PostgreSQL gives you full SQL query capabilities. The data arrives in normalized tables. AWS EC2 instances land in aws_ec2_instances. GCP Compute instances go to gcp_compute_instances. Elastic IPs populate aws_ec2_eips.
You query this data using standard SQL. Join tables. Filter by tags. Group by region or project. Export to CSV. Build dashboards. The data sits in your database, under your control.

Implementation: Tracking Public IPs Across AWS and GCP #

You'll need Docker installed and cloud credentials configured. AWS credentials go in ~/.aws/credentials or environment variables. GCP requires a service account key JSON file.
Create a CloudQuery configuration file that defines your AWS and GCP sources plus a PostgreSQL destination:
kind: source
spec:
  name: aws
  path: cloudquery/aws
  version: 'v32.58.1' # Check hub.cloudquery.io for latest
  tables:
    - aws_ec2_instances
    - aws_ec2_eips
    - aws_ec2_security_groups
    - aws_rds_instances
    - aws_elbv1_load_balancers
    - aws_elbv2_load_balancers
  skip_dependent_tables: false # Include child tables like security group rules
  destinations: ['postgresql']
  spec:
    regions: ['us-east-1', 'us-west-2'] # Add your regions

---
kind: source
spec:
  name: gcp
  path: cloudquery/gcp
  version: 'v19.13.4' # Check hub.cloudquery.io for latest
  tables:
    - gcp_compute_instances
    - gcp_compute_addresses
    - gcp_compute_forwarding_rules
  destinations: ['postgresql']
  spec:
    project_ids: ['your-gcp-project-id']

---
kind: destination
spec:
  name: postgresql
  path: cloudquery/postgresql
  version: 'v8.13.3' # Check hub.cloudquery.io for latest
  send_sync_summary: true
  spec:
    connection_string: 'postgresql://user:password@localhost:5432/cloudquery'
Run the sync command to extract data from AWS and GCP:
cloudquery sync config.yml
First sync takes 5-10 minutes for environments with 1000+ resources. Subsequent syncs run faster because CloudQuery tracks what changed. You'll see progress output showing which tables sync and how many rows insert.
Query for all public IP addresses across both clouds:
-- AWS EC2 instances with public IPs
SELECT
  account_id,
  region,
  instance_id,
  public_ip_address,
  'EC2 Instance' as resource_type,
  tags
FROM aws_ec2_instances
WHERE public_ip_address IS NOT NULL

UNION ALL

-- AWS Elastic IPs
SELECT
  account_id,
  region,
  allocation_id as instance_id,
  public_ip AS public_ip_address,
  'Elastic IP' as resource_type,
  tags
FROM aws_ec2_eips

UNION ALL

-- AWS RDS instances with public endpoints
SELECT
  account_id,
  region,
  db_instance_identifier as instance_id,
  endpoint_address as public_ip_address,
  'RDS Database' as resource_type,
  tags
FROM aws_rds_instances
WHERE publicly_accessible = true

UNION ALL

-- AWS Classic Load Balancers (ELBv1)
SELECT
  account_id,
  region,
  name as instance_id,
  dns_name as public_ip_address,
  'Classic Load Balancer' as resource_type,
  tags
FROM aws_elbv1_load_balancers
WHERE scheme = 'internet-facing'

UNION ALL

-- AWS Application/Network Load Balancers (ELBv2)
SELECT
  account_id,
  region,
  name as instance_id,
  dns_name as public_ip_address,
  CASE
    WHEN type = 'application' THEN 'Application Load Balancer'
    WHEN type = 'network' THEN 'Network Load Balancer'
    ELSE 'Gateway Load Balancer'
  END as resource_type,
  tags
FROM aws_elbv2_load_balancers
WHERE scheme = 'internet-facing'

UNION ALL

-- GCP Load Balancers (Forwarding Rules)
SELECT
  project_id as account_id,
  region,
  name as instance_id,
  ip_address as public_ip_address,
  'GCP Load Balancer' as resource_type,
  labels as tags
FROM gcp_compute_forwarding_rules
WHERE load_balancing_scheme = 'EXTERNAL'

UNION ALL

-- GCP Compute instances with external IPs
SELECT
  project_id as account_id,
  zone as region,
  name as instance_id,
  (network_interfaces->0->'accessConfigs'->0->>'natIP') as public_ip_address,
  'GCP Compute' as resource_type,
  labels as tags
FROM gcp_compute_instances
WHERE (network_interfaces->0->'accessConfigs'->0->>'natIP') IS NOT NULL;
This query returns every resource with a public IP address. The resource_type column tells you what kind of resource owns each IP. Tags help you identify which team or environment owns it.
Expected output:
 account_id   |   region   |      instance_id       |    public_ip_address     |      resource_type       |           tags
--------------+------------+------------------------+--------------------------+--------------------------+---------------------------
 123456789012 | us-east-1  | i-0abc123def456789     | 54.123.45.67             | EC2 Instance             | {"Team": "Platform", ...}
 123456789012 | us-east-1  | eipalloc-0123abcd      | 52.45.67.89              | Elastic IP               | {"Environment": "prod"}
 123456789012 | us-west-2  | mydb-prod              | mydb.abc.rds.amazonaws..| RDS Database             | {"Team": "Data"}
 123456789012 | us-east-1  | prod-api-lb            | prod-api.us-east-1.elb...| Application Load Balancer| {"Team": "Platform"}
 123456789012 | us-west-2  | legacy-lb              | legacy.us-west-2.elb...  | Classic Load Balancer    | {"Environment": "prod"}
 my-gcp-proj  | us-central1| frontend-lb            | 35.190.45.67             | GCP Load Balancer        | {"team": "frontend"}
 my-gcp-proj  | us-central1| gcp-instance-1         | 35.123.45.67             | GCP Compute              | {"team": "backend"}

Finding Security Risks and Cost Waste #

Security teams use this inventory to map their attack surface. This query often reveals RDS databases with public endpoints that shouldn't be internet-accessible. Teams frequently discover resources they didn't know were exposed.
Here's how to find RDS instances that probably shouldn't be public:
SELECT
  db_instance_identifier,
  endpoint_address,
  engine,
  region,
  tags
FROM aws_rds_instances
WHERE publicly_accessible = true
ORDER BY region, db_instance_identifier;
Every row returned represents a database accepting connections from the internet. Check if each one actually needs public access. Most production databases sit behind application servers in private subnets.
Cost optimization teams use this to find potential savings. While all public IPs now cost $0.005 per hour, unattached Elastic IPs are worth reviewing. Some may be intentionally reserved for scaling events or allowlisting, but many are forgotten allocations that can be safely released.
Find unattached Elastic IPs costing you money:
SELECT
  account_id,
  region,
  allocation_id,
  public_ip,
  tags,
  ROUND(0.005 * 730, 2) as monthly_cost_usd  -- 730 hours average per month
FROM aws_ec2_eips
WHERE association_id IS NULL;
Expected output showing waste:
 account_id   |   region   |    allocation_id    |  public_ip   | monthly_cost_usd
--------------+------------+---------------------+--------------+------------------
 123456789012 | us-east-1  | eipalloc-abc123     | 52.1.2.3     |             3.65
 123456789012 | us-east-1  | eipalloc-def456     | 52.4.5.6     |             3.65
 123456789012 | us-west-2  | eipalloc-ghi789     | 54.7.8.9     |             3.65
Each unattached Elastic IP costs $3.65 monthly. Organizations commonly find dozens of forgotten test IPs allocated months ago and never released. Even a handful of these add up to hundreds of dollars annually in unnecessary spend.
Find which security groups allow SSH from anywhere:
SELECT
  i.instance_id,
  i.public_ip_address,
  i.region,
  sg.group_id,
  sg.group_name,
  sgr.from_port,
  sgr.to_port
FROM aws_ec2_instances i
JOIN aws_ec2_instance_security_groups isg
  ON i.arn = isg.instance_arn
JOIN aws_ec2_security_groups sg
  ON isg.group_id = sg.group_id
JOIN aws_ec2_security_group_ip_permissions sgr
  ON sg.arn = sgr.security_group_arn
WHERE i.public_ip_address IS NOT NULL
  AND sgr.from_port <= 22
  AND sgr.to_port >= 22
  AND sgr.ip_protocol = 'tcp'
  AND EXISTS (
    SELECT 1
    FROM jsonb_array_elements(sgr.ip_ranges) as ip_range
    WHERE ip_range->>'cidr_ip' = '0.0.0.0/0'
  );
This query joins EC2 instances with their security groups and examines ingress rules. Any instance with a public IP that allows port 22 from 0.0.0.0/0 shows up in results. You'll probably want to restrict those to your corporate VPN or specific IP ranges.

Organizing Public IPs by Team and Environment #

Most teams tag their cloud resources with ownership metadata. Tags like Team, Environment, Project, or CostCenter help attribute resources to the right group.
Group public IP counts by team using tags:
WITH all_public_ips AS (
  SELECT
    account_id,
    region,
    instance_id,
    public_ip_address,
    'EC2 Instance' as resource_type,
    tags
  FROM aws_ec2_instances
  WHERE public_ip_address IS NOT NULL

  UNION ALL

  SELECT
    account_id,
    region,
    allocation_id,
    public_ip AS public_ip_address,
    'Elastic IP' as resource_type,
    tags
  FROM aws_ec2_eips

  UNION ALL

  SELECT
    account_id,
    region,
    db_instance_identifier,
    endpoint_address,
    'RDS Database' as resource_type,
    tags
  FROM aws_rds_instances
  WHERE publicly_accessible = true

  UNION ALL

  SELECT
    account_id,
    region,
    name,
    dns_name,
    'Classic Load Balancer' as resource_type,
    tags
  FROM aws_elbv1_load_balancers
  WHERE scheme = 'internet-facing'

  UNION ALL

  SELECT
    account_id,
    region,
    name,
    dns_name,
    CASE
      WHEN type = 'application' THEN 'Application Load Balancer'
      WHEN type = 'network' THEN 'Network Load Balancer'
      ELSE 'Gateway Load Balancer'
    END as resource_type,
    tags
  FROM aws_elbv2_load_balancers
  WHERE scheme = 'internet-facing'

  UNION ALL

  SELECT
    project_id,
    region,
    name,
    ip_address,
    'GCP Load Balancer' as resource_type,
    labels as tags
  FROM gcp_compute_forwarding_rules
  WHERE load_balancing_scheme = 'EXTERNAL'

  UNION ALL

  SELECT
    project_id,
    zone,
    name,
    (network_interfaces->0->'accessConfigs'->0->>'natIP'),
    'GCP Compute' as resource_type,
    labels as tags
  FROM gcp_compute_instances
  WHERE (network_interfaces->0->'accessConfigs'->0->>'natIP') IS NOT NULL
)
SELECT
  tags->>'Team' as team,
  COUNT(*) as public_ip_count,
  array_agg(DISTINCT region) as regions,
  array_agg(DISTINCT resource_type) as resource_types
FROM all_public_ips
WHERE tags->>'Team' IS NOT NULL
GROUP BY tags->>'Team'
ORDER BY public_ip_count DESC;
Expected output:
    team     | public_ip_count |        regions         |                    resource_types
-------------+-----------------+------------------------+-------------------------------------------------------------
 Platform    |              50 | {us-east-1,us-west-2}  | {EC2 Instance,Elastic IP,Application Load Balancer}
 Data        |              12 | {us-east-1,us-central1}| {RDS Database,GCP Compute,GCP Load Balancer}
 Mobile      |               8 | {us-east-1}            | {EC2 Instance,Classic Load Balancer}
This shows you which teams have the most internet-facing resources. The platform team might have 50 public IPs for infrastructure services. The data science team might have 12 for notebook servers. The mobile team might have 8 for API endpoints.
Track environment exposure separately:
SELECT
  tags->>'Environment' as environment,
  COUNT(*) as public_ip_count,
  COUNT(CASE WHEN resource_type LIKE '%RDS%' THEN 1 END) as public_databases
FROM (
  -- Same union query as above
) all_public_ips
WHERE tags->>'Environment' IS NOT NULL
GROUP BY tags->>'Environment'
ORDER BY public_ip_count DESC;
Production environments should have far fewer public IPs than development or staging. Most production resources sit behind load balancers. If you see 100 public IPs in production but 20 in development, investigate why production has so much direct internet exposure.

Compliance Reporting with Public IP Inventory #

PCI DSS Section 1.1.4 requires maintaining accurate network diagrams. PCI Security Standards Council publishes the full requirements. Network diagrams must show all connections between the cardholder data environment and other networks, including internet connections.
Generate a compliance report showing all internet-facing systems:
WITH all_public_ips AS (
  -- Same union query from earlier
)
SELECT
  resource_type,
  account_id,
  region,
  instance_id,
  public_ip_address,
  CASE
    WHEN tags->>'PCI_Scope' = 'true' THEN 'In Scope'
    WHEN tags->>'PCI_Scope' = 'false' THEN 'Out of Scope'
    ELSE 'Not Tagged'
  END as pci_scope,
  tags->>'Owner' as owner,
  tags->>'Environment' as environment
FROM all_public_ips
ORDER BY pci_scope, resource_type, region;
This report shows auditors exactly which systems can be reached from the internet. The pci_scope tag identifies which resources handle payment data. Resources in scope need additional security controls.
SOC 2 Type II audits require documenting your security monitoring. Auditors want to know how you track new internet-facing resources. Set up scheduled CloudQuery syncs (every 6 hours or daily) and track when public IPs first appear using CloudQuery's sync timestamp:
SELECT
  instance_id,
  public_ip_address,
  MIN(_cq_sync_time) as first_seen,
  MAX(_cq_sync_time) as last_seen,
  COUNT(*) as times_observed
FROM aws_ec2_instances
WHERE public_ip_address IS NOT NULL
GROUP BY instance_id, public_ip_address
HAVING MIN(_cq_sync_time) > NOW() - INTERVAL '7 days'
ORDER BY first_seen DESC;
This shows public IPs that appeared in the last week. New IPs from the most recent sync appear at the top.

Setting Up Automated Tracking #

Schedule CloudQuery syncs using cron or your task scheduler:
# Run every 6 hours
0 */6 * * * /usr/local/bin/cloudquery sync /path/to/config.yml
Store results with timestamps to track changes over time. Modify your destination configuration to append data instead of replacing it:
kind: destination
spec:
  name: postgresql
  path: cloudquery/postgresql
  version: 'v8.13.3'
  spec:
    connection_string: 'postgresql://user:password@localhost:5432/cloudquery'
    write_mode: append # Don't replace data, append it
Each sync adds rows with a _cq_sync_time timestamp column. Query historical data to track trends.
Build dashboards using Grafana, Metabase, or any PostgreSQL-compatible visualization tool. Track public IP counts over time. Graph new IPs per day. Show distribution across teams and environments.

Key Takeaways: Multi-Cloud Public IP Inventory #

Implementation & Results:
  • CloudQuery syncs all AWS and GCP public IPs to PostgreSQL in minutes with a single configuration file
  • One SQL query shows every public-facing resource across both clouds instead of dozens of separate API calls
  • Security teams find exposed databases and overly permissive security groups that manual reviews miss
  • Cost teams identify unused Elastic IPs wasting $30-70 monthly per organization
Technical Architecture:
  • AWS plugin calls DescribeInstances, DescribeAddresses, DescribeNetworkInterfaces and related APIs automatically
  • GCP plugin uses compute.instances.list, compute.addresses.list, compute.forwardingRules.list for comprehensive coverage
  • Handles pagination, rate limiting, and exponential backoff without custom code
  • Normalized PostgreSQL tables enable joins across services, clouds, and resource types
Security & Compliance:
  • PCI DSS and SOC 2 compliance requires knowing all internet-facing systems - this provides the actual inventory
  • Track when new public IPs appear using timestamped incremental syncs
  • Cross-reference security groups and firewall rules to find high-risk exposure
  • Historical tracking shows attack surface changes over weeks and months
Looking to implement public IP tracking for your multi-cloud environment? Contact our team to discuss your specific security and compliance requirements, or explore our security solutions to see how CloudQuery fits into your broader cloud governance strategy.

Frequently Asked Questions #

What cloud providers does CloudQuery support for public IP inventory? #

CloudQuery supports AWS, GCP, and Azure for public IP tracking. The AWS plugin extracts IPs from EC2, RDS, Load Balancers, and other services. The GCP plugin covers Compute Engine, Cloud SQL, and Load Balancing. The Azure plugin handles Virtual Machines, Load Balancers, and Application Gateways. Over 70 total integrations are available for different cloud platforms and SaaS services.

How often should I sync my cloud infrastructure data? #

Sync frequency depends on how quickly your infrastructure changes. Development environments with frequent deployments benefit from syncs every 2-4 hours. Production environments with stable infrastructure work fine with daily syncs. Security teams tracking attack surface might sync every hour. Cost optimization teams tracking Elastic IP waste typically sync weekly. CloudQuery handles incremental syncs efficiently, so frequent syncs don't resync everything each time.

Can CloudQuery track public IP changes over time? #

Yes, using append mode instead of replace mode. Configure your destination to append data with timestamps rather than replacing it. Each sync adds rows with a _cq_sync_time column showing when CloudQuery captured the data. Query historical data to see when resources appeared, changed, or disappeared. Track trends like "public IPs per team over the last 6 months" or "new public databases each week."

Does CloudQuery support Azure for public IP inventory? #

CloudQuery's Azure plugin extracts public IP information from Azure Virtual Machines, Load Balancers, Application Gateways, and other services. Query Azure data using the same SQL approach shown for AWS and GCP. Combine all three clouds in a single query using UNION ALL to see your complete multi-cloud public IP inventory.

Can I export the public IP inventory to a CSV or JSON file? #

Yes. The simplest approach is to use CloudQuery's File destination plugin to sync directly to CSV or JSON files instead of PostgreSQL. Alternatively, if you're using PostgreSQL, export query results using COPY or \copy commands for CSV, or row_to_json() functions for JSON. Most SQL clients also have built-in export features.

What are the main differences between AWS and GCP public IP management? #

Since February 2024, AWS charges $0.005/hour for all public IPv4 addresses—attached or not. AWS separates ephemeral public IPs (change on instance restart) from Elastic IPs (static). GCP uses external IPs that can be either ephemeral or static (reserved). GCP static IPs cost $0.010/hour when unused but only $0.004/hour when in use. AWS automatically assigns public IPs unless you disable this. GCP requires explicitly requesting external IPs. Both clouds support bringing your own IP addresses (BYOIP) for specific use cases.

How does CloudQuery handle API rate limits when syncing thousands of resources? #

CloudQuery implements exponential backoff and automatic retries when hitting rate limits. AWS API throttling gets handled transparently - CloudQuery pauses briefly and retries. GCP quota limits work the same way. Large accounts with 10,000+ resources see longer initial sync times (15-20 minutes) but subsequent syncs complete much faster because CloudQuery only fetches changed resources. You don't need to tune rate limiting parameters; the plugins handle this automatically.

Related posts

Turn cloud chaos into clarity

Find out how CloudQuery can help you get clarity from a chaotic cloud environment with a personalized conversation and demo.


© 2025 CloudQuery, Inc. All rights reserved.