AWS
Tutorials
AWS Quick Wins
In this tutorial, you will find AWS resources that are likely safe to delete. This can be a significant cost savings: one CloudQuery customer saves approximately $30k/month in costs by using queries like this.
Get started #
Install CloudQuery, point it at a Postgres you control (local Docker/RDS), and grant read-only EC2 permissions (
ec2:DescribeVolumes
, ec2:DescribeAddresses
, ec2:DescribeSecurityGroups
, ec2:DescribeNetworkInterfaces
). Save the config below as aws-safe-deletes.yaml
, export POSTGRES_URL
and your AWS creds/region in your shell, then run cq sync
. Paste the SQL into Postgres to get a screenshot-ready savings list.CloudQuery Config and Sync #
This config will sync AWS metadata for EC2 and related resources to Postgres.
kind: source
spec:
name: aws
path: cloudquery/source/aws
version: 'vX.Y.Z' # pin a recent version
# Keep it tight: only the tables we query
tables:
- aws_ec2_ebs_volumes
- aws_ec2_eips
- aws_ec2_security_groups
- aws_ec2_network_interfaces
destinations: [postgres]
---
kind: destination
spec:
name: postgres
path: cloudquery/destination/postgresql
version: 'vA.B.C' # whatever your postgres version is
spec:
connection_string: ${POSTGRES_URL} # e.g. postgres://user:pass@host:5432/db
After you create your config file, all you need to do is run a sync. Make sure to set your environment variables!
cq sync aws-safe-deletes.yaml
Data structure #
aws_ec2_ebs_volumes
Rows: ~hundreds (depends on account)
Columns:
- volume_id (utf8) e.g. "vol-xxxxxxx"
- state (utf8) e.g. "in-use", "available"
- size (int64) e.g. 100 (GB)
- volume_type (utf8) e.g. "gp3"
- attachments (json) e.g. '[{"InstanceId":"i-0abc123..."}]'
- region (utf8) e.g. "us-east-1"
- tags (json) e.g. '{"Name":"db-data","Owner":"TeamA"}'
aws_ec2_eips
Rows: usually small (<50)
Columns:
- allocation_id (utf8) e.g. "eipalloc-0123456789abcdef"
- public_ip (utf8) e.g. "1.11.22.333"
- association_id (utf8) e.g. NULL if unattached
- instance_id (utf8) e.g. "i-0abc123def456"
- region (utf8) e.g. "us-east-1"
- tags (json) e.g. '{"Env":"prod"}'
aws_ec2_security_groups
Rows: varies widely (dozens → hundreds)
Columns:
- group_id (utf8) e.g. "sg-0abc123def456"
- group_name (utf8) e.g. "default", "web-sg"
- description (utf8) e.g. "Web tier security group"
- vpc_id (utf8) e.g. "vpc-0abcd1234ef567"
- ip_permissions (json) inbound rules (array of objects)
- ip_permissions_egress (json) outbound rules
- region (utf8) e.g. "us-east-1"
- tags (json) e.g. '{"Owner":"sec-team"}'
Useful queries #
Unattached EBS volumes #
-- Value: Volumes with no attachments (state='available' or empty attachments array) cost $/GB-mo without use.
-- Heuristic: gp3 ~ $0.08/GB-mo; adjust if you prefer.
SELECT
v.volume_id,
v.region,
v.volume_type,
v.state,
v.size::int AS size_gb,
(v.size * 0.08)::numeric(12,2) AS est_monthly_cost_usd,
v.tags
FROM aws_ec2_ebs_volumes v
WHERE v.state = 'available'
OR (
v.attachments IS NOT NULL
AND jsonb_typeof(v.attachments) = 'array'
AND jsonb_array_length(v.attachments) = 0
)
ORDER BY est_monthly_cost_usd DESC NULLS LAST, v.size DESC;
Unassociated Elastic IPs #
-- Value: EIPs with no association_id incur hourly charges (~$3.65/mo at ~$0.005/hr).
SELECT
e.public_ip,
e.allocation_id,
e.region,
e.association_id,
CASE WHEN e.association_id IS NULL THEN 3.65 ELSE 0 END::numeric(10,2) AS est_monthly_cost_usd,
e.tags
FROM aws_ec2_eips e
WHERE e.association_id IS NULL
ORDER BY est_monthly_cost_usd DESC, e.public_ip;
Unused security groups #
-- Value: Security groups not referenced by any ENI (excluding the literal 'default') are cleanup candidates.
-- Note: aws_ec2_network_interfaces.groups is JSON (array of objects). We extract group_id robustly, handling key variants.
WITH attached AS (
SELECT DISTINCT
COALESCE( g.elem->>'group_id',
g.elem->>'GroupId',
g.elem->>'groupId' ) AS group_id
FROM aws_ec2_network_interfaces eni
CROSS JOIN LATERAL jsonb_array_elements(enit.groups) AS g(elem)
WHERE eni.groups IS NOT NULL
AND jsonb_typeof(eni.groups) = 'array'
)
SELECT
sg.group_id,
sg.group_name,
sg.vpc_id,
sg.description,
sg.region,
sg.tags
FROM aws_ec2_security_groups sg
LEFT JOIN attached a ON a.group_id = sg.group_id
WHERE a.group_id IS NULL
AND sg.group_name <> 'default'
ORDER BY sg.vpc_id, sg.group_name;
Wrap-up #
CloudQuery is lightning-fast data movement that runs on your infrastructure. This is perfect for working with sensitive data (like your key AWS resources!). Get started free here to realize these cost savings. We’ve got a 14 day free trial, with no credit card required!