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