To stay within budget you need to proactively monitor your AWS usage. That becomes a complex and tedious task when you run many AWS Organizations. At Turbot, for example, we use a number of AWS Organizations to develop features in Turbot Cloud. Steampipe helps us consolidate the billing data across them. In this post we'll show how queries enabled by Steampipe's AWS plugin can help you do the same for your AWS organizations.
Get started
Here's what you need for this exercise:
Steampipe installed
Access to the Management Account of the Organization(s)
The AWS Plugin for Steampipe installed and connected
Step 1: Find AWS cost for specified record types
Let's start with a single organization. We'll use the aws_cost_by_record_type_monthly table to find AWS costs for the current month. Since we only care about actual usage, we'll ignore Record Types such as Refund, Credit, DiscountedUsage, etc..
-- query q1selectlinked_account_id,period_start,period_end,sum(blended_cost_amount)fromorg_1.aws_cost_by_record_type_monthlywhererecord_type in ('Usage', 'Recurring', 'Support', 'Tax')and period_start >= date_trunc('month', now())group bylinked_account_id,period_start,period_end
+-------------------+---------------------------+---------------------------+---------------+| linked_account_id | period_start | period_end | sum |+-------------------+---------------------------+---------------------------+---------------+| 111111110001 | 2022-08-01T05:30:00+05:30 | 2022-08-24T05:30:00+05:30 | 0.5140557876 || 111111110000 | 2022-08-01T05:30:00+05:30 | 2022-08-24T05:30:00+05:30 | 10.4237148188 || 111111110002 | 2022-08-01T05:30:00+05:30 | 2022-08-24T05:30:00+05:30 | 14.5075173753 || 111111110003 | 2022-08-01T05:30:00+05:30 | 2022-08-24T05:30:00+05:30 | 2.3648034494 || 111111110004 | 2022-08-01T05:30:00+05:30 | 2022-08-24T05:30:00+05:30 | 0.0000000572 || 111111110005 | 2022-08-01T05:30:00+05:30 | 2022-08-24T05:30:00+05:30 | 0.000025 |+-------------------+---------------------------+---------------------------+---------------+
Step 2: Prettify accounts and organization(s) with user-friendly names
Format the sum of
blended_cost_amount
as currency typeJoin with the table aws_organizations_account which has more details about the linked account
Add a user-friendly name for the Organization by sourcing the Management account id.
In this case we querying for the current month, so can omit period_start
and period_end
.
-- query q2selectcasewhen a.account_id = '111111110000' then 'Infinity Gems'endas organization,a.id as member_id,a.name as account_name,a.status,sum(m.blended_cost_amount::numeric::money) as current_monthfromorg_1.aws_organizations_account ajoinorg_1.aws_cost_by_record_type_monthly mona.id = m.linked_account_idwherem.record_type in ('Usage', 'Recurring', 'Support', 'Tax')and m.period_start >= date_trunc('month', now())group bya.account_id,a.id,a.name,a.status
+---------------+--------------+--------------+--------+---------------+| organization | member_id | account_name | status | current_month |+---------------+--------------+--------------+--------+---------------+| Infinity Gems | 111111110001 | Reality | ACTIVE | $0.52 || Infinity Gems | 111111110000 | Soul | ACTIVE | $10.53 || Infinity Gems | 111111110002 | Power | ACTIVE | $14.68 || Infinity Gems | 111111110003 | Mind | ACTIVE | $2.40 || Infinity Gems | 111111110004 | Space | ACTIVE | $0.00 || Infinity Gems | 111111110005 | Time | ACTIVE | $0.00 |+---------------+--------------+--------------+--------+---------------+
STEP 3: Tag accounts with budget and owner details
Typically we have a budget for each account. It's a good practice to regularly check if the account is underutilized or overutilized. To enable that, let's add tags to each account.
owner
: The person to notify of anomalies. The value can be an email, Slack, or any other unique ID. We'll use email.
budget
: The number to compare against current spending.
Here is an example of the account tags on the Organizations page.
owner: warlock@mcu.xyzbudget: 100
-- query q3selectcasewhen a.account_id = '111111110000' then 'Infinity Gems'end as organization,a.id as member_id,a.name as account_name,a.tags ->> 'owner' as owner,(a.tags ->> 'budget')::money as budget,sum(m.blended_cost_amount::numeric::money) as current_monthfromorg_1.aws_organizations_account as a, -- implicit cross join againorg_1.aws_cost_by_record_type_monthly as mwherem.record_type in ('Usage', 'Recurring', 'Support', 'Tax')and a.status = 'ACTIVE'and a.id = m.linked_account_idand m.period_start >= date_trunc('month', now())group bya.account_id,a.id,a.name,a.status,a.tags
+---------------+--------------+---------------+--------------------------+--------+---------------+| organization | member_id | account_name | owner | budget | current_month |+---------------+--------------+---------------+--------------------------+--------+---------------+| Infinity Gems | 111111110001 | Reality | thanos@mcu.xyz | $25.00 | $0.52 || Infinity Gems | 111111110000 | Soul | warlock@mcu.xyz | $25.00 | $10.53 || Infinity Gems | 111111110002 | Power | draxthedestroyer@mcu.xyz | $25.00 | $14.68 || Infinity Gems | 111111110003 | Mind | moondragon@mcu.xyz | $25.00 | $2.40 || Infinity Gems | 111111110004 | Space | pipthetroll@mcu.xyz | $1.00 | $0.00 || Infinity Gems | 111111110005 | Time | gamora@mcu.xyz | $1.00 | $0.00 |+---------------+--------------+---------------+--------------------------+--------+---------------+
Alternatively, if you have the account owner and budget details in a .csv file then can join it with the AWS tables using Steampipe's CSV plugin. Follow the example Joining CSV and API tables for more help.
Step 4: Budget Status
We run query 3 on the 7th, 14th, 21st, and 28th of the month. For us, the budget
tag denotes a monthly cap, so we distribute that total across four weeks and check relative to the current week. So for example the status is "over the budget" if an account has consumed more than 50% of its allocated budget by the 14th of the month, otherwise it is "under the budget."
-- query q4with monthly_billing_data as(selectdate_part('day', CURRENT_DATE) as today,casewhen a.account_id = '111111110000' then 'Infinity Gems'end as organization,a.id as member_id,a.name as account_name,a.tags ->> 'owner' as owner,(a.tags ->> 'budget')::money as budget,sum(m.blended_cost_amount::numeric::money) as current_monthfromorg_1.aws_organizations_account as a, -- implicit cross joinorg_1.aws_cost_by_record_type_monthly as mwherem.record_type in ('Usage', 'Recurring', 'Support', 'Tax')and a.status = 'ACTIVE'and a.id = m.linked_account_idand m.period_start >= date_trunc('month', now())group bya.account_id,a.id,a.name,a.status,a.tags)selectorganization,member_id,account_name,owner,budget,current_month,casewhen budget is null then 'Unknown'when today = 7 and current_month < (budget*0.25) then 'Under'when today = 7 and current_month >= (budget*0.25) then 'Over'when today = 14 and current_month < (budget*0.5) then 'Under'when today = 14 and current_month >= (budget*0.5) then 'Over'when today = 21 and current_month < (budget*0.75) then 'Under'when today = 21 and current_month >= (budget*0.75) then 'Over'when today = 28 and current_month < (budget) then 'Under'when today = 28 and current_month >= (budget) then 'Over'end as budget_statefrommonthly_billing_dataorder bycurrent_month desc
+---------------+--------------+--------------+--------------------------+--------+---------------+--------------+| organization | member_id | account_name | owner | budget | current_month | budget_state |+---------------+--------------+--------------+--------------------------+--------+---------------+--------------+| Infinity Gems | 111111110002 | Power | draxthedestroyer@mcu.xyz | $25.00 | $14.68 | Under || Infinity Gems | 111111110000 | Soul | warlock@mcu.xyz | $25.00 | $10.53 | Under || Infinity Gems | 111111110003 | Mind | moondragon@mcu.xyz | $25.00 | $2.40 | Under || Infinity Gems | 111111110001 | Reality | thanos@mcu.xyz | $25.00 | $0.52 | Under || Infinity Gems | 111111110004 | Space | pipthetroll@mcu.xyz | $1.00 | $0.00 | Under || Infinity Gems | 111111110005 | Time | gamora@mcu.xyz | $1.00 | $0.00 | Under |+---------------+--------------+--------------+--------------------------+--------+---------------+--------------+
Step 5: Aggregator Connection for multi-organization
Now we're ready to apply this technique to multiple Organizations. To do that, create an aggregator connection in your aws.spc
file, like so:
connection "org_1" {plugin = "aws"profile = "infinity-gems-mgmt-account"regions = ["us-east-1"]}connection "org_2" {plugin = "aws"profile = "mcandarins-rings-mgmt-account"regions = ["us-east-1"]}connection "org_all" {plugin = "aws"type = "aggregator"connections = ["org_1", "org_2"]}
Then change the connection in the query from org_1
to org_all
and adjust the user-friendly names. In this examle, account_id
refers to the Management Account Id.
casewhen a.account_id = '111111110000' then 'Infinity Gems'when a.account_id = '222222220000' then 'Mandarin Rings'end as organization
You should now have a working query that returns AWS usage for multiple Organizations!
Compare against last month
It may be helpful to capture the previous month's usage as well.
--query q5with monthly_billing_data as (selectcasewhen a.account_id = '111111110000' then 'Infinity Gems'when a.account_id = '222222220000' then 'Mandarin Rings'end as organization,a.id as member_id,a.name as account_name,a.tags ->> 'owner' as owner,(a.tags ->> 'budget')::money as budget,sum(m.blended_cost_amount::numeric::money) as current_month,m.period_startfromorg_all.aws_organizations_account as a,org_all.aws_cost_by_record_type_monthly as mwherem.record_type in ('Usage', 'Recurring', 'Support', 'Tax')and a.status = 'ACTIVE'and a.id = m.linked_account_idand m.period_start >= date_trunc('month', now() - interval '1 months')group bya.account_id,a.id,a.name,a.tags,m.period_start), monthly_aggreggrate_billing as (selectorganization,member_id,account_name,owner,budget,string_agg(current_month::text ,'|' order by period_start) as billingfrommonthly_billing_datagroup byorganization,account_name,member_id,owner,budget), current_and_previous_month_billing as (selectorganization,member_id,account_name,owner,budget,billing,date_part('day', CURRENT_DATE) as today,-- If the AWS Account is created this month, the previous month billing should be $0.00casewhen (LENGTH(billing::text) - LENGTH(REPLACE(billing::text, '|', '')) + 1) = 2 then split_part(billing::text, '|', 1)when (LENGTH(billing::text) - LENGTH(REPLACE(billing::text, '|', '')) + 1) = 1 then '$0.00' end as previous_month,casewhen (LENGTH(billing::text) - LENGTH(REPLACE(billing::text, '|', '')) + 1) = 2 then split_part(billing::text, '|', 2)when (LENGTH(billing::text) - LENGTH(REPLACE(billing::text, '|', '')) + 1) = 1 then split_part(billing::text, '|', 1) end as current_monthfrom monthly_aggreggrate_billing)select organization, member_id, account_name, owner, budget, previous_month::money, current_month::money,casewhen budget is null then 'Unknown'when today = 7 and current_month::money < (budget*0.25) then 'Under'when today = 7 and current_month::money >= (budget*0.25) then 'Over'when today = 14 and current_month::money < (budget*0.5) then 'Under'when today = 14 and current_month::money >= (budget*0.5) then 'Over'when today = 21 and current_month::money < (budget*0.75) then 'Under'when today = 21 and current_month::money >= (budget*0.75) then 'Over'when today = 28 and current_month::money < (budget) then 'Under'when today = 28 and current_month::money >= (budget) then 'Over'end as budget_statefromcurrent_and_previous_month_billingorder bycurrent_month desc
+------------------+--------------+---------------+----------------------------+---------+----------------+---------------+--------------+| organization | member_id | account_name | owner | budget | previous_month | current_month | budget_state |+------------------+--------------+---------------+----------------------------+---------+----------------+---------------+--------------+| Mandarin Rings | 222222220001 | Incandescence | abigailburns@mcu.xyz | $100.00 | $439.16 | $323.49 | Over || Mandarin Rings | 222222220000 | Spin | aleceiffel@mcu.xyz | $50.00 | $89.66 | $69.71 | Over || Mandarin Rings | 222222220002 | Lightning | lightningconductor@mcu.xyz | $25.00 | $22.95 | $21.56 | Under || Infinity Gems | 111111110002 | Power | draxthedestroyer@mcu.xyz | $25.00 | $19.69 | $14.68 | Under || Infinity Gems | 111111110000 | Soul | warlock@mcu.xyz | $25.00 | $11.48 | $10.53 | Under || Mandarin Rings | 222222220003 | Zero | endotherm@mcu.xyz | $25.00 | $6.62 | $5.04 | Under || Infinity Gems | 111111110003 | Mind | moondragon@mcu.xyz | $25.00 | $2.96 | $2.40 | Under || Mandarin Rings | 222222220004 | Influence | colinsixty@mcu.xyz | $25.00 | $1.15 | $0.86 | Under || Infinity Gems | 111111110001 | Reality | thanos@mcu.xyz | $25.00 | $0.61 | $0.52 | Under || Infinity Gems | 111111110005 | Time | gamora@mcu.xyz | $1.00 | $0.00 | $0.00 | Under || Infinity Gems | 111111110004 | Space | pipthetroll@mcu.xyz | $1.00 | $0.00 | $0.00 | Under |+------------------+--------------+---------------+----------------------------+---------+----------------+---------------+--------------+
Conclusion
The techniques we've shown here work well for us, but everyone's situation is unique and you may find a solution that works better for you. If so, please let us know: we love to learn from our community!