Skip to content

Commit ec909a3

Browse files
committed
added AWS Cost Protection Strategy engineering note
1 parent 3a72b81 commit ec909a3

6 files changed

Lines changed: 467 additions & 64 deletions

File tree

114 KB
Loading
49.6 KB
Loading
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<!--
6+
ENGINEERING NOTE TEMPLATE
7+
Duplicate this file for new Engineering Notes articles.
8+
-->
9+
10+
<meta charset="UTF-8">
11+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
12+
13+
<title>AWS Cost Protection Strategy | Cliffable</title>
14+
15+
<meta name="description"
16+
content="How I built a layered AWS cost protection strategy using budgets, billing audits, IAM guardrails, region restrictions and high-risk service controls.">
17+
18+
<meta name="robots" content="noindex, nofollow">
19+
20+
<link rel="canonical" href="https://cliffable.com/engineering-notes/aws-cost-protection-strategy.html">
21+
22+
<!-- Google Fonts -->
23+
<link rel="preconnect" href="https://fonts.googleapis.com">
24+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
25+
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600&display=swap" rel="stylesheet">
26+
27+
<link rel="stylesheet" href="../styles.css">
28+
29+
<!-- Favicons -->
30+
<link rel="icon" type="image/x-icon" href="../favicon/favicon.ico">
31+
<link rel="icon" type="image/png" sizes="32x32" href="../favicon/favicon-32x32.png">
32+
<link rel="icon" type="image/png" sizes="16x16" href="../favicon/favicon-16x16.png">
33+
<link rel="apple-touch-icon" sizes="180x180" href="../favicon/apple-touch-icon.png">
34+
35+
<!-- Google Analytics -->
36+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-L41X6NGBMK"></script>
37+
38+
<script>
39+
window.dataLayer = window.dataLayer || [];
40+
41+
function gtag() {
42+
dataLayer.push(arguments);
43+
}
44+
45+
gtag('js', new Date());
46+
47+
gtag('config', 'G-L41X6NGBMK');
48+
</script>
49+
50+
</head>
51+
52+
<body>
53+
54+
<header>
55+
<div class="container header-inner">
56+
57+
<div class="logo">
58+
<a href="/">
59+
<img src="/assets/logos/cliffable-logo-white-v2.svg" alt="Cliffable logo" height="60">
60+
</a>
61+
</div>
62+
63+
<nav class="main-nav">
64+
<a href="/projects/">Projects</a>
65+
<a href="/engineering-notes/">Engineering Notes</a>
66+
<a href="/about.html">About</a>
67+
<a href="/contact.html">Contact</a>
68+
</nav>
69+
70+
</div>
71+
</header>
72+
73+
<main>
74+
75+
<!-- HERO -->
76+
77+
<section class="hero engineering-note-article-hero">
78+
<div class="container engineering-note-container">
79+
80+
<p class="engineering-note-article-meta">
81+
AWS • FinOps • IAM
82+
</p>
83+
84+
<p class="engineering-note-date">
85+
Published 24 June 2026
86+
</p>
87+
88+
<h1>AWS Cost Protection Strategy</h1>
89+
90+
<p class="engineering-note-intro">
91+
After finding several small but unexpected AWS charges, I built a layered cost protection strategy
92+
across my accounts using budget alerts, monthly audits, region restrictions and IAM guardrails.
93+
</p>
94+
95+
</div>
96+
</section>
97+
98+
<!-- FEATURED IMAGE -->
99+
100+
<section class="engineering-note-featured-image">
101+
<div class="container engineering-note-container">
102+
103+
<a href="../assets/images/engineering-notes/aws-cost-protection-strategy/aws-cost-protection-strategy-budget-thresholds.png"
104+
class="lightbox-trigger">
105+
106+
<img src="../assets/images/engineering-notes/aws-cost-protection-strategy/aws-cost-protection-strategy-budget-thresholds.png"
107+
alt="AWS budget preview showing multiple cost alert thresholds">
108+
109+
</a>
110+
111+
<p class="engineering-note-image-caption">
112+
Multiple budget thresholds were configured to provide escalating alerts as AWS costs increase.
113+
</p>
114+
115+
</div>
116+
</section>
117+
118+
<!-- ARTICLE CONTENT -->
119+
120+
<section class="engineering-note-content-section">
121+
<div class="container engineering-note-container">
122+
123+
<h2>Why I reviewed my AWS cost controls</h2>
124+
125+
<p>
126+
I started with a simple billing audit across my AWS accounts. The aim was to check whether anything
127+
had been left running after labs, experiments or infrastructure changes.
128+
</p>
129+
130+
<p>
131+
The audit found several small but unexpected charges, including AWS Config recording and an old RDS
132+
snapshot. None of the charges were serious on their own, but they
133+
highlighted a wider issue: small AWS costs can persist quietly if they are not reviewed regularly.
134+
</p>
135+
136+
<p>
137+
The audit also exposed the limits of budget alerts. They are useful for detecting unexpected spend,
138+
but they only react after costs have started to accumulate.
139+
</p>
140+
141+
<p>
142+
One of the more uncomfortable discoveries was realising that AWS does not provide a simple
143+
hard spending cap for an account. Budgets and alerts can warn about costs, but they do not
144+
stop resources being created. In theory, a sufficiently expensive service, configuration
145+
mistake or compromised account could generate costs running into tens or even hundreds of
146+
thousands of pounds before a bill became due.
147+
</p>
148+
149+
<p>
150+
That realisation changed the direction of the project. Instead of focusing solely on cost
151+
monitoring, I started looking for ways to prevent high-cost actions from happening in the
152+
first place.
153+
</p>
154+
155+
<h2>Detection controls</h2>
156+
157+
<p>
158+
The first layer of the strategy was detection. I configured monthly budgets in each account, with
159+
normal alerts for expected usage and additional emergency thresholds for unusual spend.
160+
</p>
161+
162+
<p>
163+
Each account now has alerts for:
164+
</p>
165+
166+
<ul>
167+
<li>85% of the monthly budget</li>
168+
<li>100% of the monthly budget</li>
169+
<li>forecasted spend exceeding the budget</li>
170+
<li>emergency thresholds at $50, $100, $200 and $300</li>
171+
</ul>
172+
173+
<p>
174+
The lower budget alerts are useful for normal cost awareness. The higher thresholds are deliberately
175+
different. They are not there for small overspend; they are there to flag that something has gone
176+
badly wrong and needs immediate attention. I call these my 'WTF alerts'.
177+
</p>
178+
179+
<h2>Removing noisy alerts</h2>
180+
181+
<p>
182+
I also reviewed AWS Cost Anomaly Detection. In practice, it was adding more noise than value for
183+
these small accounts. If I received an anomaly email, I would usually have received a budget alert
184+
at the same time, and I would follow the same investigation process either way.
185+
</p>
186+
187+
<p>
188+
I decided to remove the anomaly alert subscriptions and rely instead on budget alerts plus a
189+
scheduled monthly billing audit. This keeps the alerting model simple and reduces the chance of
190+
ignoring important emails because too many overlapping systems are notifying me.
191+
</p>
192+
193+
<h2>Preventive controls</h2>
194+
195+
<p>
196+
Having established the monitoring layer, the next step was to add preventive controls using
197+
IAM policies attached to a RestrictedUsers group in each account.
198+
</p>
199+
200+
<p>
201+
The guardrails restrict several high-risk areas:
202+
</p>
203+
204+
<ul>
205+
<li>regions outside the ones I intentionally use</li>
206+
<li>large EC2 instance types and expensive accelerator families</li>
207+
<li>large RDS instance classes</li>
208+
<li>services such as SageMaker, Bedrock, DataZone and EMR</li>
209+
<li>additional analytics services such as Redshift and OpenSearch in non-training accounts</li>
210+
</ul>
211+
212+
<p>
213+
The region restrictions are especially useful in a training account. If I accidentally follow a lab
214+
in the wrong region, or if credentials are ever used somewhere unexpected, AWS now blocks actions
215+
outside the approved regions.
216+
</p>
217+
218+
<!-- OPTIONAL INLINE IMAGE -->
219+
220+
<div class="engineering-note-inline-image">
221+
222+
<a href="../assets/images/engineering-notes/aws-cost-protection-strategy/aws-cost-protection-strategy-instance-launch-blocked.png"
223+
class="lightbox-trigger">
224+
225+
<img src="../assets/images/engineering-notes/aws-cost-protection-strategy/aws-cost-protection-strategy-instance-launch-blocked.png"
226+
alt="AWS EC2 launch failure caused by an explicit deny policy">
227+
228+
</a>
229+
230+
<p class="engineering-note-image-caption">
231+
IAM guardrails prevented the launch of a high-cost EC2 instance, demonstrating the difference
232+
between detecting unexpected spend and preventing it.
233+
</p>
234+
235+
</div>
236+
237+
<h2>Testing the guardrails</h2>
238+
239+
<p>
240+
After creating the policies, I tested them directly rather than assuming they worked.
241+
</p>
242+
243+
<p>
244+
I confirmed that approved regions still worked, blocked regions returned explicit deny errors, and
245+
attempts to launch a large EC2 instance were stopped by the guardrail policy. I also found that
246+
blocking SageMaker required including Amazon DataZone, because the newer SageMaker Unified Studio
247+
setup uses DataZone behind the scenes.
248+
</p>
249+
250+
<h2>What this does not solve</h2>
251+
252+
<p>
253+
These controls are mainly designed to stop mistakes and reduce the chance of unexpected costs.
254+
I am the sole administrator for these accounts, but the guardrails
255+
are not a complete defence against a fully compromised administrator account.
256+
</p>
257+
258+
<p>
259+
A compromised administrator account could still remove IAM policies or take a user out of the
260+
RestrictedUsers
261+
group.
262+
Strong MFA, protected root accounts, no unnecessary access keys and careful credential management
263+
remain essential.
264+
</p>
265+
266+
<p>
267+
For stronger account-level controls, I considered AWS Organizations and Service Control
268+
Policies, but I decided against this as I need separate billing for tax-deductible and non-tax
269+
deductible accounts.
270+
</p>
271+
272+
<h2>Conclusion</h2>
273+
274+
<p>
275+
The biggest lesson from this work was that AWS cost management is not just about watching the bill.
276+
Budget alerts are important, but they are detection controls. IAM guardrails add a preventive layer
277+
by stopping selected high-risk actions before they can create charges.
278+
</p>
279+
280+
<p>
281+
The final setup is intentionally simple: monthly audits, budget alerts, emergency thresholds,
282+
approved-region restrictions and deny policies for services and instance types I do not currently
283+
need. It is not a hard spending cap, but it makes expensive mistakes much harder to make
284+
accidentally.
285+
</p>
286+
287+
</div>
288+
</section>
289+
290+
</main>
291+
292+
<!-- FOOTER -->
293+
294+
<footer>
295+
<div class="container footer-content">
296+
297+
<div class="footer-contact">
298+
299+
<p class="footer-heading">Contact</p>
300+
301+
<div class="footer-links">
302+
303+
<a href="mailto:mailcliffsmith@gmail.com">
304+
Email
305+
</a>
306+
307+
<a href="https://www.linkedin.com/in/cliff-smith-london/" target="_blank" rel="noopener noreferrer">
308+
LinkedIn
309+
</a>
310+
311+
<a href="https://github.com/cliffable" target="_blank" rel="noopener noreferrer">
312+
GitHub
313+
</a>
314+
315+
</div>
316+
</div>
317+
318+
<div class="footer-meta">
319+
<p>© 2026 Cliff Smith</p>
320+
</div>
321+
322+
</div>
323+
</footer>
324+
325+
<!-- LIGHTBOX -->
326+
327+
<div id="lightbox" class="lightbox">
328+
<img id="lightbox-img" src="" alt="">
329+
</div>
330+
331+
<script>
332+
333+
const lightboxTriggers = document.querySelectorAll('.lightbox-trigger');
334+
335+
const lightbox = document.getElementById('lightbox');
336+
337+
const lightboxImg = document.getElementById('lightbox-img');
338+
339+
lightboxTriggers.forEach(trigger => {
340+
341+
trigger.addEventListener('click', event => {
342+
343+
event.preventDefault();
344+
345+
lightbox.style.display = 'flex';
346+
347+
lightboxImg.src = trigger.href;
348+
349+
});
350+
351+
});
352+
353+
lightbox.addEventListener('click', () => {
354+
355+
lightbox.style.display = 'none';
356+
357+
lightboxImg.src = '';
358+
359+
});
360+
361+
</script>
362+
363+
</body>
364+
365+
</html>

0 commit comments

Comments
 (0)