<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sanket Patharkar</title>
    <description>The latest articles on DEV Community by Sanket Patharkar (@sanket_patharkar).</description>
    <link>https://dev.to/sanket_patharkar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3309580%2F8126bdf2-5b94-4c3b-a1ad-eb870c3f3e79.jpeg</url>
      <title>DEV Community: Sanket Patharkar</title>
      <link>https://dev.to/sanket_patharkar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanket_patharkar"/>
    <language>en</language>
    <item>
      <title>Real-World AWS Cost Optimization Strategies — A Technical Deep Dive</title>
      <dc:creator>Sanket Patharkar</dc:creator>
      <pubDate>Fri, 25 Sep 2026 18:04:33 +0000</pubDate>
      <link>https://dev.to/sanket_patharkar/real-world-aws-cost-optimization-strategies-a-technical-deep-dive-489l</link>
      <guid>https://dev.to/sanket_patharkar/real-world-aws-cost-optimization-strategies-a-technical-deep-dive-489l</guid>
      <description>&lt;p&gt;In a cloud-first world, keeping costs under control without losing performance, scalability or reliability matters for any business that wants to last. AWS gives you a lot of tools for measuring, managing and optimizing spend. The hard part is wiring them into an &lt;strong&gt;engineering system&lt;/strong&gt; instead of doing a clean-up once a quarter.&lt;/p&gt;

&lt;p&gt;I'm a Senior Cloud Operations Engineer with 6+ years across AWS, DevOps, CI/CD and enterprise cloud architecture, much of it in the automotive systems domain. The approaches below mix &lt;strong&gt;automation, governance and real-time monitoring&lt;/strong&gt;, and each comes with the CLI commands, JSON/YAML configs and gotchas you need to use it in production.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Cloud Architects, DevOps/SRE engineers and FinOps practitioners who want measurable, automated, production-ready cost control, not just a checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About the prices:&lt;/strong&gt; Dollar amounts below are &lt;strong&gt;illustrative us-east-1 list prices at the time of writing&lt;/strong&gt;. They vary by Region and change over time. Discount figures such as “up to 72%” or “up to 90%” are what &lt;strong&gt;AWS advertises as a maximum&lt;/strong&gt;, not a saving you should expect on every workload. Check the current AWS pricing pages before you build a business case on any of them.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  TL;DR — The Eight Levers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Key AWS services&lt;/th&gt;
&lt;th&gt;Where the savings come from&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Measure before you optimize&lt;/td&gt;
&lt;td&gt;Data Exports (CUR 2.0), Athena, Cost Explorer, Budgets, Cost Anomaly Detection&lt;/td&gt;
&lt;td&gt;Knowing &lt;em&gt;who&lt;/em&gt; spends &lt;em&gt;what&lt;/em&gt;, and finding spikes within hours instead of at month-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Right-size compute&lt;/td&gt;
&lt;td&gt;Compute Optimizer, CloudWatch Agent, Auto Scaling, Graviton&lt;/td&gt;
&lt;td&gt;Removing idle headroom; ~20% lower price per instance on Graviton&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Commitments + Spot&lt;/td&gt;
&lt;td&gt;Savings Plans, Reserved Instances, EC2 Fleet, Karpenter&lt;/td&gt;
&lt;td&gt;AWS advertises up to 72% (Savings Plans) and up to 90% (Spot) off On-Demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Storage tiering&lt;/td&gt;
&lt;td&gt;S3 Lifecycle, Intelligent-Tiering, Storage Lens, EBS gp3, EFS IA&lt;/td&gt;
&lt;td&gt;About 20% on gp2→gp3 in many Regions; AWS advertises up to ~95% for cold archive tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Network cost engineering&lt;/td&gt;
&lt;td&gt;VPC Gateway/Interface Endpoints, CloudFront, AZ-aware routing&lt;/td&gt;
&lt;td&gt;Avoiding NAT processing and cross-AZ charges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Non-prod shutdown&lt;/td&gt;
&lt;td&gt;EventBridge Scheduler, Lambda, Instance Scheduler on AWS&lt;/td&gt;
&lt;td&gt;60–70% of non-prod compute hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Database optimization&lt;/td&gt;
&lt;td&gt;Aurora Serverless v2, Aurora I/O-Optimized, DynamoDB modes, RDS on Graviton&lt;/td&gt;
&lt;td&gt;Paying for actual load instead of peak provisioning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Continuous governance&lt;/td&gt;
&lt;td&gt;Organizations SCPs, Tag Policies, AWS Config, Budgets Actions&lt;/td&gt;
&lt;td&gt;Stopping expensive mistakes before they reach the bill&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Big Picture — A Closed FinOps Loop
&lt;/h2&gt;

&lt;p&gt;Cost optimization isn't a project. It's a &lt;strong&gt;control loop&lt;/strong&gt;: measure, detect, act, govern, then measure again. Every strategy in this article plugs into one of those four stages.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: AWS cost optimization reference architecture — Measure, Detect &amp;amp; Alert, Act/Automate, and Govern.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure:&lt;/strong&gt; Billing and usage data (CUR 2.0) lands in S3, is queried with Athena and visualized in QuickSight/Cost Explorer. Tags turn line items into team-level accountability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detect &amp;amp; Alert:&lt;/strong&gt; Anomaly Detection, Budgets, Compute Optimizer and Cost Optimization Hub produce signals. SNS sends them to Slack/Teams/email.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act / Automate:&lt;/strong&gt; Schedulers, Lambda remediations, SSM runbooks and Karpenter make the change, ideally without a human opening the console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Govern:&lt;/strong&gt; SCPs, Tag Policies and Config rules stop the waste from coming back.&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Production note:&lt;/strong&gt; The examples below are reference implementations. Validate pricing, service availability, quotas, IAM permissions, and regional behavior against your AWS environment before applying them to production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Measure Before You Optimize
&lt;/h2&gt;

&lt;p&gt;You can't optimize what you can't see. Treat cost like any other telemetry signal: &lt;strong&gt;granular, tagged and close to real time&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 Set up CUR 2.0 (Data Exports) + Athena
&lt;/h3&gt;

&lt;p&gt;Cost Explorer is great for exploring, but a &lt;strong&gt;Cost and Usage Report (CUR 2.0)&lt;/strong&gt; export, delivered through &lt;strong&gt;Billing and Cost Management → Data Exports&lt;/strong&gt;, is the practical dataset for line-item analysis. What it contains depends on the export you configure. Hourly granularity and resource IDs are settings you turn on. They are not a promise that every charge has a resource identifier: some fees, credits, and tax lines do not. Treat the export as the working record for the charges it is configured to include, and confirm the columns before you build chargeback on it.&lt;/p&gt;

&lt;p&gt;Recommended export settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Export type:&lt;/strong&gt; Standard data export → CUR 2.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include resource IDs:&lt;/strong&gt; ✅ (you can't chase waste without them)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time granularity:&lt;/strong&gt; Hourly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format:&lt;/strong&gt; Parquet, overwrite, delivered to a dedicated S3 bucket&lt;/li&gt;
&lt;li&gt;Use the Athena/Glue integration (or a Glue crawler) to create the table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Query 1: top spend by service and environment&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Partition/column names depend on how your Glue table was created&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;line_item_product_code&lt;/span&gt;                    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;resource_tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_environment'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_item_unblended_cost&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cost_usd&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cur2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;billing_period&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-08'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cost_usd&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Query 2: untagged spend (your accountability gap)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;line_item_product_code&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_item_unblended_cost&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;untagged_cost&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cur2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;billing_period&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-08'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;line_item_line_item_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Usage'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource_tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_owner'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;resource_tags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user_owner'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Query 3: the silent killers, NAT processing and cross-AZ transfer&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;line_item_usage_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;line_item_resource_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_item_usage_amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;gb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_item_unblended_cost&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cost_usd&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cur2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;billing_period&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2026-08'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line_item_usage_type&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%NatGateway-Bytes%'&lt;/span&gt;
       &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;line_item_usage_type&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%DataTransfer-Regional-Bytes%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cost_usd&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Insight: unblended vs amortized.&lt;/strong&gt; If you've bought Savings Plans or RIs, &lt;em&gt;unblended&lt;/em&gt; cost puts all the upfront or recurring fees on one line and makes teams look "free". For chargeback, use &lt;strong&gt;amortized&lt;/strong&gt; cost (&lt;code&gt;savings_plan_savings_plan_effective_cost&lt;/code&gt; and &lt;code&gt;reservation_effective_cost&lt;/code&gt; in CUR), so every team carries its fair share of the commitment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.2 Activate cost allocation tags
&lt;/h3&gt;

&lt;p&gt;Tags only show up in billing data after you &lt;strong&gt;activate&lt;/strong&gt; them in the payer account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ce update-cost-allocation-tags-status &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cost-allocation-tags-status&lt;/span&gt; &lt;span class="nv"&gt;TagKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Environment,Status&lt;span class="o"&gt;=&lt;/span&gt;Active &lt;span class="nv"&gt;TagKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Project,Status&lt;span class="o"&gt;=&lt;/span&gt;Active &lt;span class="nv"&gt;TagKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Owner,Status&lt;span class="o"&gt;=&lt;/span&gt;Active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Required baseline: &lt;code&gt;Environment&lt;/code&gt;, &lt;code&gt;Project&lt;/code&gt;, &lt;code&gt;Owner&lt;/code&gt; (plus &lt;code&gt;CostCenter&lt;/code&gt; if finance charges back). Section 8 covers how to enforce them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 Cost Explorer API: query programmatically, but know it costs money
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ce get-cost-and-usage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--time-period&lt;/span&gt; &lt;span class="nv"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01,End&lt;span class="o"&gt;=&lt;/span&gt;2026-09-01 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--granularity&lt;/span&gt; DAILY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metrics&lt;/span&gt; UnblendedCost &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-by&lt;/span&gt; &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;TAG,Key&lt;span class="o"&gt;=&lt;/span&gt;Environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Gotcha:&lt;/strong&gt; AWS currently charges &lt;strong&gt;$0.01 per paginated Cost Explorer API request&lt;/strong&gt; against the primary billing view. A custom billing view is charged at $0.01 per source in that view. The Cost Explorer console is free. A dashboard polling the API every minute runs up a real bill. Hourly granularity is a separate opt-in charge and only covers the last 14 days. Confirm the current rate on the &lt;a href="https://aws.amazon.com/aws-cost-management/aws-cost-explorer/pricing/" rel="noopener noreferrer"&gt;Cost Explorer pricing page&lt;/a&gt; before you automate it. For heavy analysis, query CUR in Athena instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1.4 Budgets with forecast alerts
&lt;/h3&gt;

&lt;p&gt;Alert on &lt;strong&gt;forecasted&lt;/strong&gt; spend, not just actual. By the time actual spend crosses 100%, the money is already gone.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;budget.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"BudgetName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platform-monthly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"BudgetLimit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"TimeUnit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MONTHLY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"BudgetType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CostFilters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"TagKeyValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"user:Project$platform"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;notifications.json&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"NotificationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FORECASTED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ComparisonOperator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREATER_THAN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ThresholdType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PERCENTAGE"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Subscribers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"SubscriptionType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SNS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sns:us-east-1:111122223333:finops-alerts"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"NotificationType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACTUAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ComparisonOperator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GREATER_THAN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ThresholdType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PERCENTAGE"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Subscribers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"SubscriptionType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SNS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sns:us-east-1:111122223333:finops-alerts"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws budgets create-budget &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account-id&lt;/span&gt; 111122223333 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--budget&lt;/span&gt; file://budget.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--notifications-with-subscribers&lt;/span&gt; file://notifications.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send the SNS topic to Slack or Teams through &lt;strong&gt;Amazon Q Developer in chat applications&lt;/strong&gt; (formerly AWS Chatbot).&lt;/p&gt;

&lt;h3&gt;
  
  
  1.5 Cost Anomaly Detection
&lt;/h3&gt;

&lt;p&gt;A budget catches &lt;em&gt;how much&lt;/em&gt;. Anomaly Detection catches &lt;em&gt;something changed&lt;/em&gt;, like a runaway Lambda loop or a debug log level left on in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ce create-anomaly-monitor &lt;span class="nt"&gt;--anomaly-monitor&lt;/span&gt; &lt;span class="s1"&gt;'{
  "MonitorName": "per-service",
  "MonitorType": "DIMENSIONAL",
  "MonitorDimension": "SERVICE"
}'&lt;/span&gt;

aws ce create-anomaly-subscription &lt;span class="nt"&gt;--anomaly-subscription&lt;/span&gt; &lt;span class="s1"&gt;'{
  "SubscriptionName": "anomalies-over-100-usd",
  "MonitorArnList": ["arn:aws:ce::111122223333:anomalymonitor/abcd-1234"],
  "Subscribers": [{ "Type": "SNS", "Address": "arn:aws:sns:us-east-1:111122223333:finops-alerts" }],
  "Frequency": "IMMEDIATE",
  "ThresholdExpression": {
    "Dimensions": { "Key": "ANOMALY_TOTAL_IMPACT_ABSOLUTE", "Values": ["100"], "MatchOptions": ["GREATER_THAN_OR_EQUAL"] }
  }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Insight: track unit cost, not just total cost.&lt;/strong&gt; A rising bill can be healthy growth. Divide spend by a business metric (cost per 1,000 API requests, per tenant, per CI build) and put it on the same dashboard as your latency SLOs. When unit cost goes up, that's a regression.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Right-Sizing Compute Resources
&lt;/h2&gt;

&lt;p&gt;Right-sizing is more than CPU. You also have to look at &lt;strong&gt;memory, network bandwidth, EBS throughput and CPU credit behaviour&lt;/strong&gt; against real performance baselines.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1 Feed Compute Optimizer the metric it can't see: memory
&lt;/h3&gt;

&lt;p&gt;By default, EC2 doesn't publish memory metrics. Without them, Compute Optimizer can't safely recommend moving to a smaller or memory-optimized family. Install the CloudWatch Agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"append_dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"InstanceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${aws:InstanceId}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metrics_collected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"measurement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cpu_usage_active"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"totalcpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"measurement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mem_used_percent"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"disk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"measurement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"used_percent"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compute Optimizer picks up &lt;code&gt;mem_used_percent&lt;/code&gt; from the &lt;code&gt;CWAgent&lt;/code&gt; namespace automatically. Then pull the over-provisioned list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws compute-optimizer get-ec2-instance-recommendations &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Finding,values&lt;span class="o"&gt;=&lt;/span&gt;Overprovisioned &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'instanceRecommendations[].{id:instanceArn,current:currentInstanceType,best:recommendationOptions[0].instanceType,risk:recommendationOptions[0].performanceRisk}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Insight: size on p95/p99, not averages.&lt;/strong&gt; A 12% average CPU can hide a 90% p99 spike during batch windows. Per-core metrics (&lt;code&gt;resources: ["*"]&lt;/code&gt;) also show single-threaded apps pinning one core while the others sit idle. Those need a &lt;em&gt;faster&lt;/em&gt; core, not more cores. Turn on &lt;strong&gt;enhanced infrastructure metrics&lt;/strong&gt; in Compute Optimizer for a 3-month lookback on seasonal workloads.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Gotcha: burstable (T-family) instances.&lt;/strong&gt; &lt;code&gt;t3&lt;/code&gt;/&lt;code&gt;t4g&lt;/code&gt; default to &lt;em&gt;unlimited&lt;/em&gt; mode. A T instance that burns through its CPU credits all day gets billed for surplus credits and can cost more than an &lt;code&gt;m&lt;/code&gt;-family instance. Watch &lt;code&gt;CPUSurplusCreditsCharged&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2.2 Graviton migration checklist
&lt;/h3&gt;

&lt;p&gt;Graviton (arm64) instances usually cost ~20% less than comparable x86 instances, and AWS quotes up to 40% better price-performance.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check compatibility:&lt;/strong&gt; Interpreted and JVM languages (Java, Python, Node.js, Go, .NET 6+) usually just work. Check native dependencies and agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build multi-arch images:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-t&lt;/span&gt; 111122223333.dkr.ecr.us-east-1.amazonaws.com/api:1.4.0 &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Canary:&lt;/strong&gt; Put a Graviton ASG or node pool behind the same ALB target group and shift traffic gradually while watching latency and error rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go beyond EC2:&lt;/strong&gt; Lambda on &lt;code&gt;arm64&lt;/code&gt; is ~20% cheaper per GB-second. RDS/Aurora (&lt;code&gt;db.r7g&lt;/code&gt;, &lt;code&gt;db.m7g&lt;/code&gt;), ElastiCache and OpenSearch all offer Graviton classes. These are often the easiest wins because there's no code to change.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.3 Scale on the right signal
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws autoscaling put-scaling-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auto-scaling-group-name&lt;/span&gt; web-asg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-name&lt;/span&gt; cpu-target-50 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-type&lt;/span&gt; TargetTrackingScaling &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-tracking-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
    "PredefinedMetricSpecification": { "PredefinedMetricType": "ASGAverageCPUUtilization" },
    "TargetValue": 50.0
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For web tiers, &lt;code&gt;ALBRequestCountPerTarget&lt;/code&gt; tracks real demand more closely than CPU.&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;predictive scaling&lt;/strong&gt; for workloads with a daily pattern so capacity arrives &lt;em&gt;before&lt;/em&gt; the morning ramp instead of sitting there all night.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;attribute-based instance selection&lt;/strong&gt; (&lt;code&gt;InstanceRequirements&lt;/code&gt;) so the ASG isn't tied to one instance type. That matters again in section 3.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Savings Plans &amp;amp; Spot Fleet Engineering
&lt;/h2&gt;

&lt;p&gt;The biggest single lever on compute is &lt;strong&gt;how you buy it&lt;/strong&gt;. The rule is simple: layer your capacity.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: Compute purchasing strategy — Savings Plans for the baseline, Spot for variable demand, On-Demand for short peaks.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Savings Plans: commit to the floor, not the average
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Max discount&lt;/th&gt;
&lt;th&gt;Flexibility&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compute Savings Plans&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;up to 66%&lt;/td&gt;
&lt;td&gt;Any family, size, Region, OS; also covers &lt;strong&gt;Fargate &amp;amp; Lambda&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Most organizations; containers and serverless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EC2 Instance Savings Plans&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;up to 72%&lt;/td&gt;
&lt;td&gt;Locked to a family in a Region (size/OS flexible)&lt;/td&gt;
&lt;td&gt;Very stable, known fleets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reserved Instances (RDS, ElastiCache, OpenSearch, Redshift)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;td&gt;Per service&lt;/td&gt;
&lt;td&gt;Databases and data stores&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;How to size the commitment:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pull the recommendation from AWS as a starting point:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   aws ce get-savings-plans-purchase-recommendation &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--savings-plans-type&lt;/span&gt; COMPUTE_SP &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--term-in-years&lt;/span&gt; ONE_YEAR &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--payment-option&lt;/span&gt; NO_UPFRONT &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--lookback-period-in-days&lt;/span&gt; THIRTY_DAYS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Commit to roughly &lt;strong&gt;90–95% of your lowest sustained hourly usage&lt;/strong&gt;, &lt;em&gt;after&lt;/em&gt; right-sizing. Committing first and right-sizing afterwards leaves you paying for a commitment you no longer use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ladder&lt;/strong&gt; your purchases, for example a small tranche every quarter. Commitments then expire on a rolling basis, and you're never locked into one big bet.&lt;/li&gt;
&lt;li&gt;Track &lt;strong&gt;utilization&lt;/strong&gt; (are you using what you bought?) and &lt;strong&gt;coverage&lt;/strong&gt; (how much of your eligible spend is covered?). These are Cost Explorer APIs, &lt;code&gt;ce:GetSavingsPlansUtilization&lt;/code&gt; and &lt;code&gt;ce:GetSavingsPlansCoverage&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   aws ce get-savings-plans-utilization &lt;span class="nt"&gt;--time-period&lt;/span&gt; &lt;span class="nv"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01,End&lt;span class="o"&gt;=&lt;/span&gt;2026-09-01
   aws ce get-savings-plans-coverage   &lt;span class="nt"&gt;--time-period&lt;/span&gt; &lt;span class="nv"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-08-01,End&lt;span class="o"&gt;=&lt;/span&gt;2026-09-01 &lt;span class="nt"&gt;--granularity&lt;/span&gt; MONTHLY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;Targets I use:&lt;/strong&gt; utilization &lt;strong&gt;&amp;gt; 95%&lt;/strong&gt;, coverage of eligible compute &lt;strong&gt;70–85%&lt;/strong&gt;. Coverage near 100% usually means you over-committed and have no room left for Spot.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.2 Spot: engineering for interruption
&lt;/h3&gt;

&lt;p&gt;AWS advertises Spot discounts of up to 90% versus On-Demand. In exchange, the instance can be reclaimed with a &lt;strong&gt;2-minute notice&lt;/strong&gt;. Three things make it production-safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a) Diversify and use &lt;code&gt;price-capacity-optimized&lt;/code&gt;.&lt;/strong&gt; It's the recommended allocation strategy. It picks the deepest Spot pools and weighs price, so you get fewer interruptions than with &lt;code&gt;lowest-price&lt;/code&gt; and better prices than with pure &lt;code&gt;capacity-optimized&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mixed-instances.json&lt;/code&gt; (ASG):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"LaunchTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LaunchTemplateSpecification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"LaunchTemplateName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web-lt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$Latest"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Overrides"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"InstanceRequirements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"VCpuCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"MemoryMiB"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Min"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"CpuManufacturers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"intel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amd"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"InstanceGenerations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"InstancesDistribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"OnDemandBaseCapacity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"OnDemandPercentageAboveBaseCapacity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SpotAllocationStrategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"price-capacity-optimized"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws autoscaling create-auto-scaling-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auto-scaling-group-name&lt;/span&gt; web-asg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--mixed-instances-policy&lt;/span&gt; file://mixed-instances.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-size&lt;/span&gt; 2 &lt;span class="nt"&gt;--max-size&lt;/span&gt; 40 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-zone-identifier&lt;/span&gt; &lt;span class="s2"&gt;"subnet-aaa,subnet-bbb,subnet-ccc"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capacity-rebalance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--capacity-rebalance&lt;/code&gt; launches a replacement as soon as AWS sends a &lt;em&gt;rebalance recommendation&lt;/em&gt;, which often comes before the 2-minute notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;b) Karpenter for EKS.&lt;/strong&gt; Karpenter provisions nodes directly from pod requirements, prefers Spot when you allow it, and &lt;strong&gt;consolidates&lt;/strong&gt; under-utilized nodes all the time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.sh/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePool&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;general&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requirements&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.sh/capacity-type&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spot"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on-demand"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/arch&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arm64"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amd64"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;       &lt;span class="c1"&gt;# multi-arch images required&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.k8s.aws/instance-category&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;m"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.k8s.aws/instance-generation&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gt&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;nodeClassRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;karpenter.k8s.aws&lt;/span&gt;
        &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EC2NodeClass&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
      &lt;span class="na"&gt;expireAfter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;720h&lt;/span&gt;
  &lt;span class="na"&gt;disruption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;consolidationPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;WhenEmptyOrUnderutilized&lt;/span&gt;
    &lt;span class="na"&gt;consolidateAfter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point Karpenter at an SQS &lt;strong&gt;interruption queue&lt;/strong&gt; (the &lt;code&gt;settings.interruptionQueue&lt;/code&gt; Helm value) so it drains nodes gracefully on Spot interruption and rebalance events. If you run self-managed node groups without Karpenter, use the &lt;strong&gt;AWS Node Termination Handler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;c) Graceful shutdown on plain EC2.&lt;/strong&gt; Poll instance metadata (IMDSv2) and drain in time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# /usr/local/bin/spot-watch.sh — run as a systemd service&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"http://169.254.169.254/latest/api/token"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token-ttl-seconds: 300"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;CODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    http://169.254.169.254/latest/meta-data/spot/instance-action&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 200 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;logger &lt;span class="s2"&gt;"Spot interruption notice received - draining"&lt;/span&gt;
    systemctl stop myapp        &lt;span class="c"&gt;# finish in-flight work, deregister, flush buffers&lt;/span&gt;
    &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;5
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Insight: what should &lt;em&gt;not&lt;/em&gt; run on Spot.&lt;/strong&gt; Single-instance stateful services, long jobs without checkpoints, and anything with a license tied to a host. Everything else, including CI runners, batch/ETL, stateless APIs behind a load balancer and EKS workers with PodDisruptionBudgets, is a candidate.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Storage Cost Optimization
&lt;/h2&gt;

&lt;p&gt;Storage costs grow quietly because nothing ever gets deleted.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 S3: pick the class by access pattern
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage class&lt;/th&gt;
&lt;th&gt;~$/GB-month&lt;/th&gt;
&lt;th&gt;Min. duration&lt;/th&gt;
&lt;th&gt;Retrieval&lt;/th&gt;
&lt;th&gt;Use for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S3 Standard&lt;/td&gt;
&lt;td&gt;0.023&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;instant&lt;/td&gt;
&lt;td&gt;Hot data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Intelligent-Tiering&lt;/td&gt;
&lt;td&gt;0.023 → auto-tiered&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;instant (frequent/IA/archive-instant tiers)&lt;/td&gt;
&lt;td&gt;Unknown or changing access patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Standard-IA&lt;/td&gt;
&lt;td&gt;0.0125&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;td&gt;instant, per-GB fee&lt;/td&gt;
&lt;td&gt;Monthly-access data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Glacier Instant Retrieval&lt;/td&gt;
&lt;td&gt;0.004&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;milliseconds&lt;/td&gt;
&lt;td&gt;Quarterly-access data, backups you might restore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Glacier Flexible Retrieval&lt;/td&gt;
&lt;td&gt;0.0036&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;td&gt;minutes–hours&lt;/td&gt;
&lt;td&gt;Archives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 Glacier Deep Archive&lt;/td&gt;
&lt;td&gt;0.00099&lt;/td&gt;
&lt;td&gt;180 days&lt;/td&gt;
&lt;td&gt;~12–48 hours&lt;/td&gt;
&lt;td&gt;Compliance retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A production-grade lifecycle policy.&lt;/strong&gt; Note that &lt;code&gt;Filter&lt;/code&gt; replaces the legacy top-level &lt;code&gt;Prefix&lt;/code&gt;, and there's a separate "hygiene" rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs-tiering"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Transitions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STANDARD_IA"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLACIER_IR"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"StorageClass"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DEEP_ARCHIVE"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Expiration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;730&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bucket-hygiene"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"AbortIncompleteMultipartUpload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"DaysAfterInitiation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"NoncurrentVersionExpiration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"NoncurrentDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"NewerNoncurrentVersions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-lifecycle-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-app-logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lifecycle-configuration&lt;/span&gt; file://lifecycle.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Gotchas that cost real money:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incomplete multipart uploads&lt;/strong&gt; are invisible in the console but billed. The &lt;code&gt;AbortIncompleteMultipartUpload&lt;/code&gt; rule fixes that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versioned buckets&lt;/strong&gt; keep every overwrite forever unless you expire &lt;em&gt;noncurrent&lt;/em&gt; versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small objects:&lt;/strong&gt; by default, lifecycle doesn't transition objects under 128 KB. IA classes bill a 128 KB minimum, and Intelligent-Tiering doesn't auto-tier objects under 128 KB. Millions of tiny files belong in aggregated/compacted formats instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transition requests cost money&lt;/strong&gt; (per 1,000 objects). Moving 100M tiny objects to Glacier can cost more than it saves.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Use &lt;strong&gt;S3 Storage Lens&lt;/strong&gt; (the free metrics are enough to start) to find buckets with no recent activity, large noncurrent-version bytes or incomplete multipart uploads. Use &lt;strong&gt;S3 Inventory&lt;/strong&gt; for object-level audits.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 EBS: gp2 → gp3 is often about a 20% storage saving
&lt;/h3&gt;

&lt;p&gt;In many Regions, gp3 is about 20% cheaper per GB than gp2 and includes a &lt;strong&gt;3,000 IOPS / 125 MiB/s baseline, independent of size&lt;/strong&gt;. Confirm the current per-GB price before you migrate a large fleet. One catch: gp2 volumes above 1 TiB get &lt;em&gt;more&lt;/em&gt; than 3,000 baseline IOPS (3 IOPS/GiB), so set IOPS explicitly or large volumes will lose performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Migrate all gp2 volumes to gp3, preserving the gp2 baseline IOPS (3 IOPS/GiB, capped at 16,000)&lt;/span&gt;
aws ec2 describe-volumes &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;volume-type,Values&lt;span class="o"&gt;=&lt;/span&gt;gp2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Volumes[].[VolumeId,Size]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text |
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; VOL SIZE&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;IOPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; SIZE &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; IOPS &amp;lt; 3000 &lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;IOPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; IOPS &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 16000 &lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;IOPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16000
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Migrating &lt;/span&gt;&lt;span class="nv"&gt;$VOL&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SIZE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;GiB) -&amp;gt; gp3 @ &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;IOPS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; IOPS"&lt;/span&gt;
  aws ec2 modify-volume &lt;span class="nt"&gt;--volume-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$VOL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--volume-type&lt;/span&gt; gp3 &lt;span class="nt"&gt;--iops&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IOPS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--throughput&lt;/span&gt; 250
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration is online, with no detach and no downtime. The same volume can't be modified again for 6 hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hunt for orphans:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Unattached EBS volumes&lt;/span&gt;
aws ec2 describe-volumes &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;status,Values&lt;span class="o"&gt;=&lt;/span&gt;available &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Volumes[].{ID:VolumeId,GB:Size,Type:VolumeType,Created:CreateTime}'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Unassociated Elastic IPs (each public IPv4 address is billed hourly)&lt;/span&gt;
aws ec2 describe-addresses &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Addresses[?AssociationId==null].[PublicIp,AllocationId]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Move old snapshots you must keep to the &lt;strong&gt;EBS Snapshots Archive tier&lt;/strong&gt; (about 75% cheaper, 90-day minimum, 24–72h restore):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  aws ec2 modify-snapshot-tier &lt;span class="nt"&gt;--snapshot-id&lt;/span&gt; snap-0abc123 &lt;span class="nt"&gt;--storage-tier&lt;/span&gt; archive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Automate snapshot retention with &lt;strong&gt;Amazon Data Lifecycle Manager&lt;/strong&gt; or &lt;strong&gt;AWS Backup&lt;/strong&gt; instead of hand-written cron jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.3 EFS and CloudWatch Logs, the ones people forget
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EFS:&lt;/strong&gt; Turn on lifecycle management to move cold files to &lt;strong&gt;Infrequent Access&lt;/strong&gt; and &lt;strong&gt;Archive&lt;/strong&gt;, with &lt;em&gt;transition back to Standard on access&lt;/em&gt; for files that heat up again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Logs:&lt;/strong&gt; New log groups keep data &lt;strong&gt;forever&lt;/strong&gt; by default, and ingestion (~$0.50/GB) is often a top-10 line item. Set retention on everything:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="k"&gt;for &lt;/span&gt;LG &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;aws logs describe-log-groups &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'logGroups[?!retentionInDays].logGroupName'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;aws logs put-retention-policy &lt;span class="nt"&gt;--log-group-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--retention-in-days&lt;/span&gt; 30
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For high-volume, rarely queried logs, use the &lt;strong&gt;Infrequent Access log class&lt;/strong&gt; (about half the ingestion price). Also drop DEBUG logging in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Network Traffic Cost Engineering
&lt;/h2&gt;

&lt;p&gt;Data transfer is the &lt;strong&gt;silent killer&lt;/strong&gt; of AWS budgets. It's spread across dozens of usage types, so it rarely looks like one big problem.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure: Network cost engineering — before-and-after paths for S3/DynamoDB, AWS APIs, service-to-service traffic, and content delivery.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Gateway VPC Endpoints: free, and frequently missing
&lt;/h3&gt;

&lt;p&gt;Traffic from private subnets to S3 or DynamoDB through a NAT Gateway pays NAT data-processing charges. AWS has listed this at about &lt;strong&gt;$0.045/GB&lt;/strong&gt; in us-east-1. Confirm the rate for your Region. A &lt;strong&gt;Gateway Endpoint&lt;/strong&gt; costs nothing and takes one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 create-vpc-endpoint &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; vpc-0abc123 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-endpoint-type&lt;/span&gt; Gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; com.amazonaws.us-east-1.s3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--route-table-ids&lt;/span&gt; rtb-0priv1a rtb-0priv1b rtb-0priv1c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Worked example (illustrative, using $0.045/GB):&lt;/strong&gt; A data pipeline moves 20 TB/month from EC2 to S3 through NAT.&lt;br&gt;
&lt;code&gt;20,480 GB × $0.045 = $921.60/month&lt;/code&gt;. With a Gateway Endpoint, the NAT processing charge on that path becomes &lt;strong&gt;$0&lt;/strong&gt;. The dollar result changes if your Region’s rate is different.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.2 Interface Endpoints (PrivateLink): do the break-even math
&lt;/h3&gt;

&lt;p&gt;For ECR, STS, CloudWatch Logs, SQS and similar services, an Interface Endpoint has often been listed at about $0.01/hr per AZ plus about $0.01/GB. Compared with NAT data processing at about $0.045/GB, the per-GB difference is about $0.035. Recalculate with your Region’s prices.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3 AZs × $0.01 × 730 h ≈ &lt;strong&gt;$21.90/month fixed&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Break-even ≈ &lt;code&gt;21.90 / 0.035&lt;/code&gt; ≈ &lt;strong&gt;~625 GB/month&lt;/strong&gt; through NAT to that service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EKS clusters pulling large container images from ECR through NAT usually pass that break-even quickly. Put ECR endpoints (&lt;code&gt;ecr.api&lt;/code&gt;, &lt;code&gt;ecr.dkr&lt;/code&gt;) in place, plus the S3 gateway endpoint, because ECR image layers are stored in S3.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.3 Cross-AZ traffic: stay multi-AZ, route locally
&lt;/h3&gt;

&lt;p&gt;Cross-AZ transfer has commonly been listed at about &lt;strong&gt;$0.01/GB in each direction&lt;/strong&gt;. Chatty microservices, Kafka replication and read replicas in another AZ add up fast. The fix is &lt;strong&gt;not&lt;/strong&gt; to give up multi-AZ. Keep the redundancy and keep the &lt;em&gt;traffic&lt;/em&gt; local:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes:&lt;/strong&gt; topology-aware routing (&lt;code&gt;service.kubernetes.io/topology-mode: Auto&lt;/code&gt;) or &lt;code&gt;trafficDistribution: PreferClose&lt;/code&gt; on Services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kafka/MSK:&lt;/strong&gt; configure &lt;code&gt;client.rack&lt;/code&gt; so consumers fetch from the closest replica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databases:&lt;/strong&gt; send read traffic to a replica in the same AZ as the caller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ALB:&lt;/strong&gt; check whether cross-zone load balancing is actually needed for each target group.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  5.4 CloudFront in front of everything public
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Data transfer from AWS origins &lt;strong&gt;to&lt;/strong&gt; CloudFront is free, and CloudFront's egress rates are generally lower than EC2 internet egress at volume.&lt;/li&gt;
&lt;li&gt;Every cache hit is a request your origin fleet doesn't serve, so the ALB/EC2 tier can be smaller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin Shield&lt;/strong&gt; adds a regional caching layer that collapses duplicate origin fetches from many edge locations.&lt;/li&gt;
&lt;li&gt;Track &lt;strong&gt;cache hit ratio&lt;/strong&gt; as a cost KPI. Tune &lt;code&gt;Cache-Control&lt;/code&gt; headers and cache keys (drop unnecessary query strings and cookies).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  5.5 Public IPv4 addresses are no longer free
&lt;/h3&gt;

&lt;p&gt;AWS currently bills in-use and idle public IPv4 addresses. The published rate has been about &lt;strong&gt;$0.005 per hour&lt;/strong&gt; (about $3.65 per month). Confirm it before you forecast a large cleanup. Hundreds of instances with public IPs "just in case" add up. Use private subnets and load balancers, and look at IPv6 where you can. &lt;strong&gt;VPC IP Address Manager (IPAM)&lt;/strong&gt; shows public IP usage across accounts.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. Automating Non-Prod Shutdown
&lt;/h2&gt;

&lt;p&gt;A dev environment that runs only during office hours (12h × 5 days = 60 of 168 weekly hours) cuts that compute bill by &lt;strong&gt;~64%&lt;/strong&gt;. It's the highest-ROI automation in this article.&lt;/p&gt;
&lt;h3&gt;
  
  
  6.1 No-code option: EventBridge Scheduler universal targets
&lt;/h3&gt;

&lt;p&gt;EventBridge Scheduler can call almost any AWS API directly, so you don't need a Lambda:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws scheduler create-schedule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; stop-dev-ec2-evening &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--schedule-expression&lt;/span&gt; &lt;span class="s2"&gt;"cron(0 20 ? * MON-FRI *)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--schedule-expression-timezone&lt;/span&gt; &lt;span class="s2"&gt;"Asia/Kolkata"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--flexible-time-window&lt;/span&gt; &lt;span class="nv"&gt;Mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Arn": "arn:aws:scheduler:::aws-sdk:ec2:stopInstances",
    "RoleArn": "arn:aws:iam::111122223333:role/scheduler-ec2-startstop",
    "Input": "{\"InstanceIds\":[\"i-0abc123\",\"i-0def456\"]}"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a matching &lt;code&gt;startInstances&lt;/code&gt; schedule for the morning.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 Tag-driven option: one Lambda for EC2 + RDS
&lt;/h3&gt;

&lt;p&gt;Hard-coded instance IDs don't scale. Drive the schedule from tags (&lt;code&gt;Schedule=OfficeHours&lt;/code&gt;) instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;ec2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ec2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# EventBridge passes {"action": "stop"|"start"}
&lt;/span&gt;    &lt;span class="n"&gt;current_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stopped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# EC2: find tagged instances in the right state
&lt;/span&gt;    &lt;span class="n"&gt;paginator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_paginator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;describe_instances&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;InstanceId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;paginator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag:Schedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OfficeHours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instance-state-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;current_state&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reservations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Instances&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_instances&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_instances&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;InstanceIds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# RDS: stop/start tagged DB instances (Aurora clusters need stop_db_cluster/start_db_cluster)
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_db_instances&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBInstances&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TagList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Schedule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OfficeHours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBInstanceStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;available&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop_db_instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DBInstanceIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBInstanceIdentifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBInstanceStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stopped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_db_instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DBInstanceIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DBInstanceIdentifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ec2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For many accounts with complex calendars, deploy the &lt;strong&gt;Instance Scheduler on AWS&lt;/strong&gt; solution. It works off the &lt;code&gt;Schedule&lt;/code&gt; tag and keeps schedule definitions in DynamoDB.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Gotchas:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto Scaling groups replace stopped instances.&lt;/strong&gt; For ASGs, scale to zero instead: &lt;code&gt;aws autoscaling update-auto-scaling-group --auto-scaling-group-name dev-asg --min-size 0 --desired-capacity 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS restarts a stopped instance after 7 days.&lt;/strong&gt; Your schedule has to stop it again. It's not a set-and-forget "off" switch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EKS:&lt;/strong&gt; scale node groups or Karpenter &lt;code&gt;limits&lt;/code&gt; down, or scale deployments to zero after hours.&lt;/li&gt;
&lt;li&gt;A stopped EC2 instance still pays for its &lt;strong&gt;EBS volumes and Elastic IPs&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  7. Database Optimization
&lt;/h2&gt;

&lt;p&gt;Databases are usually the second-largest line item, and the hardest to change later. Pick the right model early.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1 Aurora
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Aurora Serverless v2&lt;/strong&gt; scales capacity (ACUs) in fine-grained steps, within seconds. It fits spiky or unpredictable workloads and dev/test databases, and it can now scale down to &lt;strong&gt;0 ACUs with auto-pause&lt;/strong&gt;, so idle non-prod databases cost almost nothing for compute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora I/O-Optimized:&lt;/strong&gt; if I/O charges are more than &lt;strong&gt;~25% of your Aurora bill&lt;/strong&gt;, switching the cluster storage configuration removes per-I/O charges in exchange for higher instance and storage prices. You can switch back once every 30 days.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Graviton classes&lt;/strong&gt; (&lt;code&gt;db.r7g&lt;/code&gt;, &lt;code&gt;db.r8g&lt;/code&gt; where available) for provisioned instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7.2 RDS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Right-size with &lt;strong&gt;Performance Insights / CloudWatch Database Insights&lt;/strong&gt; (load by wait event, top SQL) instead of CPU alone. A slow query is cheaper to fix than a bigger instance.&lt;/li&gt;
&lt;li&gt;Move RDS storage to &lt;strong&gt;gp3&lt;/strong&gt; and set IOPS independently of size.&lt;/li&gt;
&lt;li&gt;Tune &lt;code&gt;max_connections&lt;/code&gt; and use &lt;strong&gt;RDS Proxy&lt;/strong&gt; when many Lambda functions or short-lived connections would otherwise force a larger instance.&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Extended Support surcharges:&lt;/strong&gt; engine versions past end of standard support (e.g., older MySQL/PostgreSQL majors) are automatically enrolled in &lt;strong&gt;RDS Extended Support&lt;/strong&gt;, billed per vCPU-hour. The rate goes up in later years. Upgrading is a cost optimization.&lt;/li&gt;
&lt;li&gt;Buy &lt;strong&gt;Reserved Instances&lt;/strong&gt; for steady production databases once they're right-sized.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7.3 DynamoDB
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On-Demand vs Provisioned:&lt;/strong&gt; On-Demand fits unpredictable or spiky traffic and got much cheaper after AWS's late-2024 price cut. Provisioned capacity with auto scaling (plus reserved capacity) is still cheaper for steady, high-throughput tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard-IA table class:&lt;/strong&gt; about 60% cheaper storage and more expensive reads/writes. Use it for tables where &lt;strong&gt;storage dominates cost&lt;/strong&gt; (history, audit, old orders).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL&lt;/strong&gt; deletes expired items at no cost, which beats a scan-and-delete job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAX&lt;/strong&gt; reduces read cost and latency for read-heavy hot keys, but it's a cluster you pay for by the hour. Measure before you add it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7.4 Caching
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;ElastiCache for Valkey&lt;/strong&gt; is priced lower than the Redis OSS engine (AWS quotes ~20% lower for nodes and ~33% lower for Serverless), and it's a drop-in replacement for most Redis workloads.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Continuous Cost Governance
&lt;/h2&gt;

&lt;p&gt;Optimization without governance decays. Six months later, the waste is back. Make cost a &lt;strong&gt;DevOps responsibility&lt;/strong&gt; backed by guardrails.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.1 Service Control Policies: block expensive mistakes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyExpensiveInstanceTypes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2:RunInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ec2:*:*:instance/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ec2:InstanceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"p4d.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p5.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p5e.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u-*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"x2*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.metal*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.24xlarge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.32xlarge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.48xlarge"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnNotLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::*:role/PlatformAdmin"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyLaunchWithoutCostCenter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2:RunInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ec2:*:*:instance/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Null"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"aws:RequestTag/CostCenter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach it to non-prod and sandbox OUs first. The &lt;code&gt;PlatformAdmin&lt;/code&gt; exception gives a justified GPU or large-instance request a controlled path.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.2 Tag Policies + AWS Config
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tag Policies&lt;/strong&gt; (AWS Organizations) standardize tag &lt;em&gt;keys and allowed values&lt;/em&gt; (&lt;code&gt;Environment&lt;/code&gt; ∈ &lt;code&gt;prod|staging|dev&lt;/code&gt;), so &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;Env&lt;/code&gt; and &lt;code&gt;ENV&lt;/code&gt; don't fragment your reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Config &lt;code&gt;required-tags&lt;/code&gt;&lt;/strong&gt; flags non-compliant resources:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configservice put-config-rule &lt;span class="nt"&gt;--config-rule&lt;/span&gt; &lt;span class="s1"&gt;'{
  "ConfigRuleName": "required-tags",
  "Source": { "Owner": "AWS", "SourceIdentifier": "REQUIRED_TAGS" },
  "InputParameters": "{\"tag1Key\":\"Environment\",\"tag2Key\":\"Project\",\"tag3Key\":\"Owner\"}",
  "Scope": { "ComplianceResourceTypes": ["AWS::EC2::Instance","AWS::EC2::Volume","AWS::RDS::DBInstance","AWS::S3::Bucket"] }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Insight: Config has its own cost.&lt;/strong&gt; Recording every configuration change of high-churn resources can be expensive. Use &lt;strong&gt;periodic recording&lt;/strong&gt; for noisy resource types and record only what you actually govern.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  8.3 Budgets Actions: guardrails that act
&lt;/h3&gt;

&lt;p&gt;AWS Budgets can &lt;strong&gt;apply an IAM policy or SCP, or stop specific EC2/RDS instances&lt;/strong&gt;, when a threshold is crossed, either automatically or after approval. A sandbox account that hits 120% of its budget can be locked down without anyone being paged at 2 a.m.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.4 The monthly FinOps review
&lt;/h3&gt;

&lt;p&gt;Run it from CUR + Athena (or QuickSight/CUDOS dashboards) and review these KPIs every month:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KPI&lt;/th&gt;
&lt;th&gt;Target (starting point)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;% of spend with valid owner tags&lt;/td&gt;
&lt;td&gt;&amp;gt; 95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Savings Plans / RI utilization&lt;/td&gt;
&lt;td&gt;&amp;gt; 95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commitment coverage of eligible compute&lt;/td&gt;
&lt;td&gt;70–85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spot share of stateless compute&lt;/td&gt;
&lt;td&gt;growing quarter over quarter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle / orphaned resource spend&lt;/td&gt;
&lt;td&gt;&amp;lt; 2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit cost (e.g., $ per 1k requests)&lt;/td&gt;
&lt;td&gt;flat or decreasing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every item gets an &lt;strong&gt;owner, a date and a ticket&lt;/strong&gt;. A review without owners is just a meeting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: Hidden Cost Killers Checklist
&lt;/h2&gt;

&lt;p&gt;These show up in almost every account I review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] NAT Gateway processing for S3/DynamoDB/ECR traffic (fix: endpoints)&lt;/li&gt;
&lt;li&gt;[ ] CloudWatch Logs with no retention, and DEBUG logs in production&lt;/li&gt;
&lt;li&gt;[ ] Idle load balancers and NAT Gateways in unused AZs&lt;/li&gt;
&lt;li&gt;[ ] Unattached EBS volumes, old snapshots and unused AMIs&lt;/li&gt;
&lt;li&gt;[ ] Public IPv4 addresses on instances that don't need them&lt;/li&gt;
&lt;li&gt;[ ] gp2 volumes that were never migrated to gp3&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;EKS clusters on Kubernetes versions in extended support&lt;/strong&gt; (AWS has published about $0.60/hr instead of about $0.10/hr per cluster, roughly $438 vs $73/month; confirm the current extended-support rate)&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;RDS engines in Extended Support&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] T-family instances in unlimited mode charging surplus credits&lt;/li&gt;
&lt;li&gt;[ ] S3 incomplete multipart uploads and endless noncurrent versions&lt;/li&gt;
&lt;li&gt;[ ] Cross-Region replication or backups nobody asked for&lt;/li&gt;
&lt;li&gt;[ ] Dev/test environments running 24×7&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  AWS Services Reference: What Each One Does for Your Bill
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;How it helps cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Exports (CUR 2.0)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Line-item billing data delivered to S3&lt;/td&gt;
&lt;td&gt;Working dataset for chargeback, anomaly hunting and unit cost, for the columns the export includes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon Athena&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serverless SQL over S3&lt;/td&gt;
&lt;td&gt;Query CUR and VPC Flow Logs without running a database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon QuickSight (CUDOS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BI dashboards&lt;/td&gt;
&lt;td&gt;Self-service cost visibility per team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Cost Explorer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Interactive cost analysis&lt;/td&gt;
&lt;td&gt;Trends, forecasts, SP/RI coverage and utilization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost Categories&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rules that group costs&lt;/td&gt;
&lt;td&gt;Map accounts and tags to business units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Budgets / Budgets Actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Thresholds + automated responses&lt;/td&gt;
&lt;td&gt;Early warning; automatic lockdown of runaway accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost Anomaly Detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ML-based spend monitoring&lt;/td&gt;
&lt;td&gt;Catches spikes within hours, not at month-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost Optimization Hub&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consolidated recommendations&lt;/td&gt;
&lt;td&gt;One deduplicated, prioritized list of savings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Compute Optimizer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ML right-sizing&lt;/td&gt;
&lt;td&gt;EC2, ASG, EBS, Lambda, ECS on Fargate, RDS recommendations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Trusted Advisor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Best-practice checks&lt;/td&gt;
&lt;td&gt;Idle resources, low-utilization instances, unassociated EIPs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Savings Plans / Reserved Instances&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Commitment discounts&lt;/td&gt;
&lt;td&gt;AWS advertises up to 72% off steady compute; RIs for databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EC2 Spot / EC2 Fleet / ASG mixed instances&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spare capacity at a discount&lt;/td&gt;
&lt;td&gt;AWS advertises up to 90% off fault-tolerant compute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Karpenter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes node autoscaler&lt;/td&gt;
&lt;td&gt;Spot-first provisioning, bin-packing, consolidation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Graviton&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ARM-based instances&lt;/td&gt;
&lt;td&gt;Lower price and better price-performance across EC2, Lambda, RDS, ElastiCache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EC2 Auto Scaling (target tracking, predictive)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Elastic capacity&lt;/td&gt;
&lt;td&gt;Pay for demand instead of peak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3 Lifecycle / Intelligent-Tiering / Storage Lens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tiering and visibility&lt;/td&gt;
&lt;td&gt;Move cold data to cheaper classes automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon EBS gp3 / Snapshots Archive / Data Lifecycle Manager&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Block storage controls&lt;/td&gt;
&lt;td&gt;Often about 20% cheaper volumes, cheaper snapshot retention, automated cleanup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon EFS lifecycle (IA/Archive)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File storage tiering&lt;/td&gt;
&lt;td&gt;Cold files at a fraction of Standard price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VPC Gateway Endpoints&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Private route to S3/DynamoDB&lt;/td&gt;
&lt;td&gt;Removes NAT processing charges at no cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS PrivateLink (Interface Endpoints)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Private access to AWS services&lt;/td&gt;
&lt;td&gt;Cheaper than NAT above the break-even volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon CloudFront + Origin Shield&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CDN&lt;/td&gt;
&lt;td&gt;Lower egress, fewer origin requests, smaller origin fleet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon EventBridge Scheduler&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serverless scheduler&lt;/td&gt;
&lt;td&gt;Start/stop non-prod directly through API calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Lambda / SSM Automation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serverless compute / runbooks&lt;/td&gt;
&lt;td&gt;Automated remediation and right-sizing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instance Scheduler on AWS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tag-based scheduling solution&lt;/td&gt;
&lt;td&gt;Org-wide office-hours schedules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aurora Serverless v2 / I/O-Optimized&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Elastic / I/O-flat database pricing&lt;/td&gt;
&lt;td&gt;Pay for real load; predictable cost for I/O-heavy workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DynamoDB On-Demand / Standard-IA / TTL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Capacity modes and table classes&lt;/td&gt;
&lt;td&gt;Match pricing to traffic and storage profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ElastiCache for Valkey&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed in-memory cache&lt;/td&gt;
&lt;td&gt;Lower price than the Redis OSS engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Organizations (SCPs, Tag Policies)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multi-account guardrails&lt;/td&gt;
&lt;td&gt;Prevent costly launches; consistent tagging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS Config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resource compliance&lt;/td&gt;
&lt;td&gt;Enforce tags and flag non-compliant resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Amazon Q Developer in chat apps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ChatOps (formerly AWS Chatbot)&lt;/td&gt;
&lt;td&gt;Cost alerts where engineers already work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  A 30-60-90 Day Rollout Plan
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Days 0–30: visibility + quick wins&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable CUR 2.0 → Athena, activate cost allocation tags, create Budgets and Anomaly Detection&lt;/li&gt;
&lt;li&gt;Gateway Endpoints for S3/DynamoDB in every VPC&lt;/li&gt;
&lt;li&gt;gp2 → gp3, delete orphaned volumes and EIPs, set CloudWatch Logs retention&lt;/li&gt;
&lt;li&gt;S3 lifecycle "hygiene" rule on every bucket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Days 31–60: structural savings&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Right-size from Compute Optimizer (with memory metrics)&lt;/li&gt;
&lt;li&gt;Non-prod schedules through EventBridge Scheduler / Instance Scheduler&lt;/li&gt;
&lt;li&gt;Spot for CI and batch; Karpenter with Spot on EKS&lt;/li&gt;
&lt;li&gt;First Savings Plans tranche, sized to the &lt;em&gt;post&lt;/em&gt;-right-sizing baseline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Days 61–90: make it stick&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SCPs, Tag Policies, Config &lt;code&gt;required-tags&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Graviton canaries for stateless services and managed databases&lt;/li&gt;
&lt;li&gt;Unit-cost dashboards and the monthly FinOps review, with owners&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Real-world AWS cost optimization isn't one trick. It's an &lt;strong&gt;engineering discipline&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure&lt;/strong&gt; with granular, tagged, near-real-time data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right-size&lt;/strong&gt; on real percentiles, then &lt;strong&gt;commit&lt;/strong&gt; to the floor and put the rest on &lt;strong&gt;Spot&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier&lt;/strong&gt; storage and &lt;strong&gt;engineer&lt;/strong&gt; network paths deliberately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate&lt;/strong&gt; the boring parts, like schedules, clean-ups and remediation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Govern&lt;/strong&gt; with guardrails so the waste doesn't come back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Combine measurement, automation and governance, and you &lt;strong&gt;reduce waste, maximize savings and enforce accountability&lt;/strong&gt; without giving up performance or reliability.&lt;/p&gt;

&lt;p&gt;If this helped, drop a comment with the biggest hidden cost you've found in your AWS account. I'd love to compare notes. 👇&lt;/p&gt;




&lt;h3&gt;
  
  
  🧑‍💼 About the Author
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sanket Satish Patharkar&lt;/strong&gt; is a Senior Cloud Operations Engineer with 6+ years of experience in AWS, DevOps, CI/CD and enterprise-grade cloud architecture, particularly in the automotive systems domain.&lt;/p&gt;

&lt;p&gt;He specializes in building highly available, cost-efficient and automated infrastructure. He has designed cloud architectures that meet high availability (HA) and scalability requirements and keep applications running during component failures or traffic spikes. Sanket combines practical engineering, automation and governance to help organizations cut costs while keeping their cloud environments resilient and efficient.&lt;/p&gt;

&lt;p&gt;For any issue or support: &lt;a href="https://www.linkedin.com/in/sanket-patharkar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>finops</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Dockerized Monitoring Stack</title>
      <dc:creator>Sanket Patharkar</dc:creator>
      <pubDate>Fri, 25 Sep 2026 16:52:07 +0000</pubDate>
      <link>https://dev.to/sanket_patharkar/dockerized-monitoring-stack-ib8</link>
      <guid>https://dev.to/sanket_patharkar/dockerized-monitoring-stack-ib8</guid>
      <description>&lt;p&gt;&lt;strong&gt;By Sanket Satish Patharkar – Senior Cloud Operation Engineer&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Cloud Infrastructure &amp;amp; DevOps Specialist | 3× AWS Certified&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;If you've ever SSH'd into a box at 2 AM wondering &lt;em&gt;"is it the database, the cache, or the load balancer?"&lt;/em&gt;, you already know why a monitoring stack matters.&lt;/p&gt;

&lt;p&gt;In this post I'll walk through how we &lt;strong&gt;dockerized our whole observability stack&lt;/strong&gt; — Prometheus, Grafana, and a set of exporters for MongoDB, Redis, NGINX and TCP port checks — into &lt;strong&gt;one &lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/strong&gt; that can be deployed to multiple environments (UAT and Prod) with a single command.&lt;/p&gt;

&lt;p&gt;You'll get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🧱 A block diagram of the architecture&lt;/li&gt;
&lt;li&gt;🐳 The Docker Compose setup, explained piece by piece&lt;/li&gt;
&lt;li&gt;⚙️ Config for Prometheus, exporters and Grafana provisioning&lt;/li&gt;
&lt;li&gt;💻 Every command you need to run, terminal-style&lt;/li&gt;
&lt;li&gt;🚀 A Jenkins pipeline for deploy + automatic rollback&lt;/li&gt;
&lt;li&gt;🩹 Lessons learned the hard way&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;All hostnames, IPs, and credentials below are placeholders. Replace them with your own.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  1. The Architecture
&lt;/h2&gt;

&lt;p&gt;Here's the big picture. Everything inside the dashed box runs as containers on &lt;strong&gt;one monitoring host&lt;/strong&gt; (an EC2 instance in our case). The exporters reach out to the real infrastructure, Prometheus scrapes the exporters, and Grafana sits on top.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmvjppvbr7g0sldw48r0o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmvjppvbr7g0sldw48r0o.png" alt="Dockerized Monitoring Stack architecture" width="800" height="549"&gt;&lt;/a&gt;&lt;br&gt;
And this is how a change gets from Git to the server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zqhqr4jt4ugi8t8q7x0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zqhqr4jt4ugi8t8q7x0.png" alt="Deployment flow from Git to the monitoring host" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  1.1 How the data actually flows (internals)
&lt;/h3&gt;

&lt;p&gt;Understanding the internals makes every later config decision obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pull model.&lt;/strong&gt; Prometheus &lt;em&gt;pulls&lt;/em&gt;. Every &lt;code&gt;scrape_interval&lt;/code&gt; it sends an HTTP &lt;code&gt;GET /metrics&lt;/code&gt; to each target, parses the text exposition format, and appends samples to its TSDB. Exporters are stateless translators: on each scrape they query the backend (MongoDB &lt;code&gt;serverStatus&lt;/code&gt;, Redis &lt;code&gt;INFO&lt;/code&gt;, NGINX &lt;code&gt;stub_status&lt;/code&gt;, a TCP handshake) and render the result as metrics. The only push path is &lt;strong&gt;Pushgateway&lt;/strong&gt;, for short-lived jobs that finish before a scrape could happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A scrape, step by step:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjs7q40mqvga3yzhacqfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjs7q40mqvga3yzhacqfi.png" alt="Prometheus scrape path from service discovery to retention" width="519" height="2161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;relabel_configs&lt;/code&gt;&lt;/strong&gt; run &lt;em&gt;before&lt;/em&gt; the scrape, on target metadata (&lt;code&gt;__address__&lt;/code&gt;, &lt;code&gt;__meta_ec2_*&lt;/code&gt;). This is where the Blackbox trick happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;metric_relabel_configs&lt;/code&gt;&lt;/strong&gt; run &lt;em&gt;after&lt;/em&gt; the scrape, on every sample. Use them to drop high-cardinality series you don't need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Head block + WAL&lt;/strong&gt;: the last ~2 hours live in memory, protected by a write-ahead log on the &lt;code&gt;prometheus_data&lt;/code&gt; volume. That's why a named volume matters: without it, a container restart loses the WAL and the head block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;up&lt;/code&gt; metric&lt;/strong&gt;: for every target Prometheus synthesises &lt;code&gt;up{job,instance}&lt;/code&gt; = 1/0, plus &lt;code&gt;scrape_duration_seconds&lt;/code&gt; and &lt;code&gt;scrape_samples_scraped&lt;/code&gt;. These are your first debugging tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Two kinds of "down":&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;up{job="redis-exporter"} == 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prometheus can't reach the &lt;strong&gt;exporter&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redis_up == 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The exporter is fine, but it can't reach &lt;strong&gt;Redis&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;probe_success == 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blackbox couldn't open a TCP connection to the &lt;strong&gt;target port&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Alert on all three separately. They point to different fixes.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. What's in the Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prom/prometheus:v3.14.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9090&lt;/td&gt;
&lt;td&gt;Scrapes &amp;amp; stores metrics (30 days)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grafana&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;grafana/grafana:13.2.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;Dashboards + alerting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mongodb-exporter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;percona/mongodb_exporter:0.53.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9216&lt;/td&gt;
&lt;td&gt;MongoDB server/replica-set metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mongodb-query-exporter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ghcr.io/raffis/mongodb-query-exporter:v5.1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9412&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Business metrics&lt;/strong&gt; from Mongo aggregations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redis-exporter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;oliver006/redis_exporter:v1.90.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9121&lt;/td&gt;
&lt;td&gt;Redis memory, clients, keys, latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nginx-api-exporter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nginx/nginx-prometheus-exporter:1.5.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9113&lt;/td&gt;
&lt;td&gt;NGINX &lt;code&gt;stub_status&lt;/code&gt; connections/requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blackbox-exporter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prom/blackbox-exporter:v0.28.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9115&lt;/td&gt;
&lt;td&gt;TCP "is the port up?" checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pushgateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prom/pushgateway:v1.11.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9091&lt;/td&gt;
&lt;td&gt;Receives metrics from short-lived jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;generate-hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python:3-alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;Helper: bcrypt hash for Prometheus auth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pin your image versions.&lt;/strong&gt; &lt;code&gt;latest&lt;/code&gt; will eventually break your dashboards on a random Tuesday.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  3. Project Layout
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monitoring-stack/
├── docker-compose.yml
├── blackbox/
│   └── blackbox.yml
├── env/
│   ├── uat/            ├── .env  └── env.properties
│   └── prod/        ├── .env  └── env.properties
├── prometheus/
│   ├── web.yml                     # basic-auth users (bcrypt)
│   ├── uat/prometheus-tmpl.yml     # templates with ${VARS}
│   └── prod/prometheus-tmpl.yml
├── query-exporter/
│   └── &amp;lt;env&amp;gt;/config.yaml           # Mongo aggregation → metric
├── grafana/
│   ├── conf/grafana.ini
│   ├── provisioning/dashboards/&amp;lt;env&amp;gt;/dashboards.yml
│   └── dashboards/&amp;lt;env&amp;gt;/**.json    # folder structure = Grafana folders
├── scripts/
│   └── prometheus-healthcheck.sh
├── ssl-certs/
│   └── ca.pem                      # CA for Mongo TLS
└── Jenkinsfile (deploy.groovy)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The key idea: &lt;strong&gt;one compose file, many environments.&lt;/strong&gt; Environment differences live in &lt;code&gt;env/&amp;lt;env&amp;gt;/.env&lt;/code&gt;, &lt;code&gt;prometheus/&amp;lt;env&amp;gt;/&lt;/code&gt;, &lt;code&gt;query-exporter/&amp;lt;env&amp;gt;/&lt;/code&gt; and &lt;code&gt;grafana/dashboards/&amp;lt;env&amp;gt;/&lt;/code&gt; — never in the compose file itself.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Prerequisites on the Monitoring Host
&lt;/h2&gt;

&lt;p&gt;You need Docker Engine and the Compose v2 plugin (&lt;code&gt;docker compose&lt;/code&gt;, not the old &lt;code&gt;docker-compose&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon Linux 2023:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ec2-user@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker git gettext
ec2-user@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; docker
ec2-user@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; newgrp docker

&lt;span class="c"&gt;# Compose v2 plugin&lt;/span&gt;
ec2-user@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /usr/local/lib/docker/cli-plugins
ec2-user@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-SL&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/local/lib/docker/cli-plugins/docker-compose
ec2-user@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x /usr/local/lib/docker/cli-plugins/docker-compose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Ubuntu:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ubuntu@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh
ubuntu@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gettext-base git
ubuntu@monitoring-host:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker &lt;span class="nv"&gt;$USER&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; newgrp docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="go"&gt;Docker version 27.3.1, build ce12230
&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose version
&lt;span class="go"&gt;Docker Compose version v2.29.7
&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;envsubst &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;
&lt;span class="go"&gt;envsubst (GNU gettext-runtime) 0.21
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;gettext&lt;/code&gt; gives us &lt;code&gt;envsubst&lt;/code&gt;, which we use to render the Prometheus config from a template.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Network / security group checklist:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inbound&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;Grafana UI (ideally behind a reverse proxy / ALB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inbound&lt;/td&gt;
&lt;td&gt;9090&lt;/td&gt;
&lt;td&gt;Prometheus UI (restrict to VPN/office)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;27017 / 6379 / 80 / 8082 …&lt;/td&gt;
&lt;td&gt;Exporters → targets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;Pull images, Slack/email webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If Prometheus uses EC2 service discovery (below), attach an &lt;strong&gt;IAM instance role&lt;/strong&gt; with &lt;code&gt;ec2:DescribeInstances&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Dockerizing the Stack — the Compose File, Explained
&lt;/h2&gt;

&lt;p&gt;Let's go through the interesting parts of &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1 Profiles = environments
&lt;/h3&gt;

&lt;p&gt;Each service declares which environments it belongs to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;blackbox-exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prom/blackbox-exporter:v0.28.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blackbox-exporter&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;uat&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--config.file=/etc/blackbox_exporter/config.yml&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9115:9115"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A service with &lt;strong&gt;no matching profile is simply not started&lt;/strong&gt;. That lets UAT run extra QA exporters while prod runs a leaner set — from the same file.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Same container name, different config per environment
&lt;/h3&gt;

&lt;p&gt;Our production MongoDB is big. Percona's &lt;code&gt;--collect-all&lt;/code&gt; includes &lt;code&gt;collstats&lt;/code&gt;, which on a large DB &lt;strong&gt;starved the connection pool&lt;/strong&gt; so the important &lt;code&gt;diagnosticdata&lt;/code&gt; collector never ran. The fix: two service definitions, same &lt;code&gt;container_name&lt;/code&gt;, mutually exclusive profiles.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# UAT: smaller DB tolerates collect-all&lt;/span&gt;
  &lt;span class="na"&gt;mongodb-exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;percona/mongodb_exporter:0.53.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongodb-exporter&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;uat&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--collect-all&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MONGODB_URI=mongodb://${MONGO_DB_USERNAME}:${MONGO_DB_PASSWORD_ENCODED}@${MONGO_DB_HOST}:${MONGO_DB_PORT}/?directConnection=true&amp;amp;authSource=${MONGO_DB_NAME}&amp;amp;tls=true&amp;amp;tlsCAFile=/ssl-certs/ca.pem&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./ssl-certs:/ssl-certs:ro&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9216:9216"&lt;/span&gt;
    &lt;span class="na"&gt;extra_hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${MONGO_DB_HOST_DNS}"&lt;/span&gt;

  &lt;span class="c1"&gt;# Prod: only the collectors the dashboards need&lt;/span&gt;
  &lt;span class="na"&gt;mongodb-exporter-prod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;percona/mongodb_exporter:0.53.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongodb-exporter&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--collector.diagnosticdata&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--collector.replicasetstatus&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--collector.dbstats&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--collector.topmetrics&lt;/span&gt;
    &lt;span class="c1"&gt;# ...same environment / volumes / ports as above&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth copying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;extra_hosts&lt;/code&gt;&lt;/strong&gt; injects &lt;code&gt;hostname:ip&lt;/code&gt; into the container's &lt;code&gt;/etc/hosts&lt;/code&gt;. Mongo TLS certs are issued for a hostname, so we connect by name even when there's no private DNS. The value comes from &lt;code&gt;.env&lt;/code&gt;, e.g. &lt;code&gt;MONGO_DB_HOST_DNS=mongo-primary.internal:x.x.x.x&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;MONGO_DB_PASSWORD_ENCODED&lt;/code&gt;&lt;/strong&gt; — URL-encode the password before putting it in a URI. A single &lt;code&gt;@&lt;/code&gt; or &lt;code&gt;/&lt;/code&gt; in a password will ruin your afternoon.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5.3 Business metrics straight from MongoDB
&lt;/h3&gt;

&lt;p&gt;Infrastructure metrics tell you the DB is healthy. They don't tell you &lt;em&gt;"success rate for API calls dropped from 99% to 80%"&lt;/em&gt;. For that we use &lt;strong&gt;mongodb-query-exporter&lt;/strong&gt;, which turns an aggregation pipeline into a Prometheus gauge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;mongodb-query-exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/raffis/mongodb-query-exporter:v5.1.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongodb-query-exporter&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;uat&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MDBEXPORTER_MONGODB_URI=mongodb://${MONGO_DB_USERNAME}:${MONGO_DB_PASSWORD}@${MONGO_DB_HOST}:${MONGO_DB_PORT}/?authSource=${MONGO_DB_NAME}&amp;amp;tls=true&amp;amp;tlsCAFile=/ssl-certs/ca.pem&amp;amp;readPreference=secondaryPreferred&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MDBEXPORTER_CONFIG=/config/config.yaml&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./query-exporter/${env}/config.yaml:/config/config.yaml:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./ssl-certs:/ssl-certs:ro&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9412:9412"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;readPreference=secondaryPreferred&lt;/code&gt; — &lt;strong&gt;never run reporting aggregations on your primary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;query-exporter/&amp;lt;env&amp;gt;/config.yaml&lt;/code&gt; (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.0&lt;/span&gt;
&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:9412&lt;/span&gt;
&lt;span class="na"&gt;metricsPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/metrics&lt;/span&gt;

&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;queryTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;120s"&lt;/span&gt;
  &lt;span class="na"&gt;maxConnection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;aggregations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appdb&lt;/span&gt;
    &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;transactions&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5m"&lt;/span&gt;          &lt;span class="c1"&gt;# run the pipeline at most every 5 minutes&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull&lt;/span&gt;
    &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_total_calls&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gauge&lt;/span&gt;
        &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;calls&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;per&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hour)"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tenantId&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app_success_calls&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gauge&lt;/span&gt;
        &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Successful&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;calls&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;per&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hour)"&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;success&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tenantId&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;[&lt;/span&gt;
        &lt;span class="s"&gt;{ "$match": { "createdAt": { "$gte": { "$date": { "$numberLong": "-3600000" } } } } },&lt;/span&gt;
        &lt;span class="s"&gt;{ "$group": {&lt;/span&gt;
            &lt;span class="s"&gt;"_id": "$tenantId",&lt;/span&gt;
            &lt;span class="s"&gt;"total":   { "$sum": 1 },&lt;/span&gt;
            &lt;span class="s"&gt;"success": { "$sum": { "$cond": [{ "$eq": ["$status", "SUCCESS"] }, 1, 0] } }&lt;/span&gt;
        &lt;span class="s"&gt;} },&lt;/span&gt;
        &lt;span class="s"&gt;{ "$project": { "_id": 0, "tenantId": "$_id", "total": 1, "success": 1 } }&lt;/span&gt;
      &lt;span class="s"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;app_success_calls / app_total_calls&lt;/code&gt; is a success-rate panel and an alert rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Redis &amp;amp; NGINX exporters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;redis-exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oliver006/redis_exporter:v1.90.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-exporter&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;uat&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--redis.addr=redis://redis.internal:${REDIS_PORT}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--redis.password=${REDIS_DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9121:9121"&lt;/span&gt;
    &lt;span class="na"&gt;extra_hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${REDIS_HOST_DNS}"&lt;/span&gt;

  &lt;span class="na"&gt;nginx-api-exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx/nginx-prometheus-exporter:1.5.3&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-api-exporter&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;uat&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--nginx.scrape-uri=http://nginx1:8082/stub_status&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--nginx.scrape-uri=http://nginx2:8082/stub_status&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9113:9113"&lt;/span&gt;
    &lt;span class="na"&gt;extra_hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${NGINX_HOST1}"&lt;/span&gt;   &lt;span class="c1"&gt;# nginx1:x.x.x.x&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${NGINX_HOST2}"&lt;/span&gt;   &lt;span class="c1"&gt;# nginx2:x.x.x.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the NGINX side you need a &lt;code&gt;stub_status&lt;/code&gt; location — keep it on an internal-only port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;8082&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="s"&gt;.0.0/8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/stub_status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kn"&gt;stub_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.5 Prometheus — host networking, auth, healthcheck
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prom/prometheus:v3.14.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;uat&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;PROMETHEUS_PASSWORD=${PROMETHEUS_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./prometheus/web.yml:/etc/prometheus/web.yml:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./scripts/prometheus-healthcheck.sh:/usr/local/bin/prometheus-healthcheck:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus_data:/prometheus&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--config.file=/etc/prometheus/prometheus.yml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.config.file=/etc/prometheus/web.yml'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--storage.tsdb.retention.time=30d'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.enable-lifecycle'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.listen-address=0.0.0.0:9090'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--web.external-url=${PROMETHEUS_WEB_EXTERNAL_URL}'&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bin/sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/prometheus-healthcheck"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongodb-query-exporter&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;blackbox-exporter&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis-exporter&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nginx-api-exporter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why these choices?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;network_mode: host&lt;/code&gt;&lt;/strong&gt; — every exporter publishes its port on the host, so Prometheus can scrape them all as &lt;code&gt;localhost:&amp;lt;port&amp;gt;&lt;/code&gt;. Simple, and EC2 service discovery hits real private IPs without Docker NAT in the way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--web.enable-lifecycle&lt;/code&gt;&lt;/strong&gt; — lets you hot-reload config with &lt;code&gt;POST /-/reload&lt;/code&gt; instead of restarting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named volume &lt;code&gt;prometheus_data&lt;/code&gt;&lt;/strong&gt; — metrics survive &lt;code&gt;docker compose down&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;web.yml&lt;/code&gt;&lt;/strong&gt; — Prometheus' built-in basic auth, so the UI and API aren't wide open.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5.6 Grafana — plugins, provisioning, healthcheck
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana/grafana:13.2.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;uat&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SERVER_ROOT_URL=${GF_SERVER_ROOT_URL}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SERVER_HTTP_PORT=3000&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SMTP_ENABLED=${GF_SMTP_ENABLED}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SMTP_HOST=${GF_SMTP_HOST}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SMTP_USER=${GF_SMTP_USER}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SMTP_PASSWORD=${GF_SMTP_PASSWORD}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_INSTALL_PLUGINS=redis-datasource&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;grafana_data:/var/lib/grafana&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./grafana/conf/grafana.ini:/etc/grafana/grafana.ini:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./grafana/provisioning/dashboards/${env}:/etc/grafana/provisioning/dashboards:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./grafana/dashboards/${env}:/etc/grafana/dashboards:ro&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wget&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--no-verbose&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--tries=1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--spider&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/api/health&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prometheus_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dashboards are &lt;strong&gt;file-provisioned&lt;/strong&gt; — &lt;code&gt;grafana/provisioning/dashboards/&amp;lt;env&amp;gt;/dashboards.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Default'&lt;/span&gt;
    &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;file&lt;/span&gt;
    &lt;span class="na"&gt;disableDeletion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;updateIntervalSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;allowUiUpdates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/grafana/dashboards&lt;/span&gt;
      &lt;span class="na"&gt;foldersFromFilesStructure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# sub-folders become Grafana folders&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop a JSON file into &lt;code&gt;grafana/dashboards/&amp;lt;env&amp;gt;/redis/cluster-health.json&lt;/code&gt; and it shows up in a &lt;strong&gt;redis&lt;/strong&gt; folder within 10 seconds. Dashboards-as-code, no clicking.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.7 A "tools" profile for one-off helpers
&lt;/h3&gt;

&lt;p&gt;Need a bcrypt hash for &lt;code&gt;prometheus/web.yml&lt;/code&gt;? Don't install Python on the server — run it in a throwaway container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;generate-hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3-alpine&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;sh -c "pip install --quiet bcrypt &amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="s"&gt;python3 -c 'import bcrypt, sys;&lt;/span&gt;
      &lt;span class="s"&gt;print(bcrypt.hashpw(sys.argv[1].encode(), bcrypt.gensalt(rounds=10)).decode())'&lt;/span&gt;
      &lt;span class="s"&gt;${PROMETHEUS_PASSWORD}"&lt;/span&gt;
    &lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because its only profile is &lt;code&gt;tools&lt;/code&gt;, it never starts with the normal stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Configuration Files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6.1 The &lt;code&gt;.env&lt;/code&gt; file
&lt;/h3&gt;

&lt;p&gt;Non-secret, per-environment values are committed in &lt;code&gt;env/&amp;lt;env&amp;gt;/.env&lt;/code&gt;. Secrets are &lt;strong&gt;not&lt;/strong&gt; — they're injected at deploy time (see section 8).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# env/uat/.env  (example – placeholders only)&lt;/span&gt;
&lt;span class="nb"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;uat
&lt;span class="nv"&gt;COMPOSE_PROFILES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;uat

&lt;span class="nv"&gt;MONGO_DB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mongo-primary.internal
&lt;span class="nv"&gt;MONGO_DB_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;27017
&lt;span class="nv"&gt;MONGO_DB_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin
&lt;span class="nv"&gt;MONGO_DB_HOST_DNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mongo-primary.internal:x.x.x.x

&lt;span class="nv"&gt;REDIS_HOST_DNS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis.internal:x.x.x.x
&lt;span class="nv"&gt;REDIS_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6379

&lt;span class="nv"&gt;NGINX_HOST1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx1:x.x.x.x
&lt;span class="nv"&gt;NGINX_HOST2&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx2:x.x.x.x

&lt;span class="nv"&gt;GF_SERVER_ROOT_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://grafana.example.com
&lt;span class="nv"&gt;PROMETHEUS_WEB_EXTERNAL_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://prometheus.example.com
&lt;span class="nv"&gt;PROMETHEUS_AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-west-2

&lt;span class="c"&gt;# Injected at deploy time – NOT committed:&lt;/span&gt;
&lt;span class="c"&gt;# MONGO_DB_USERNAME, MONGO_DB_PASSWORD, MONGO_DB_PASSWORD_ENCODED,&lt;/span&gt;
&lt;span class="c"&gt;# REDIS_DB_PASSWORD, PROMETHEUS_PASSWORD, GF_SECURITY_ADMIN_PASSWORD, GF_SMTP_PASSWORD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Setting &lt;code&gt;COMPOSE_PROFILES&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; means plain &lt;code&gt;docker compose up -d&lt;/code&gt; automatically picks the right profile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  6.2 Prometheus template
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;prometheus/&amp;lt;env&amp;gt;/prometheus-tmpl.yml&lt;/code&gt; contains &lt;code&gt;${VARS}&lt;/code&gt; that get rendered with &lt;code&gt;envsubst&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
  &lt;span class="na"&gt;evaluation_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;

&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prometheus'&lt;/span&gt;
    &lt;span class="na"&gt;basic_auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${PROMETHEUS_PASSWORD}"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost:9090'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grafana'&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost:3000'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mongodb-exporter'&lt;/span&gt;
    &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost:9216'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mongodb-query-exporter'&lt;/span&gt;
    &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;
    &lt;span class="na"&gt;scrape_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;55s&lt;/span&gt;        &lt;span class="c1"&gt;# aggregations can be slow&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost:9412'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;redis-exporter'&lt;/span&gt;
    &lt;span class="na"&gt;scrape_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost:9121'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nginx-api-exporter'&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost:9113'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# --- Blackbox TCP port checks via EC2 service discovery ---&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app-port-check'&lt;/span&gt;
    &lt;span class="na"&gt;metrics_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/probe&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;tcp_connect&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;ec2_sd_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PROMETHEUS_AWS_REGION}&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
        &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tag:cluster&lt;/span&gt;
            &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;APP-CLUSTER&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;relabel_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;__meta_ec2_private_ip&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${1}:8080'&lt;/span&gt;
        &lt;span class="na"&gt;target_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;__param_target&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;__meta_ec2_tag_Name&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;target_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;instance&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target_label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;__address__&lt;/span&gt;
        &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localhost:9115&lt;/span&gt;   &lt;span class="c1"&gt;# send the probe to blackbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blackbox relabel dance, in plain English:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;EC2 SD finds every instance tagged &lt;code&gt;cluster=APP-CLUSTER&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Its private IP becomes the &lt;code&gt;target&lt;/code&gt; query param.&lt;/li&gt;
&lt;li&gt;The actual scrape goes to &lt;code&gt;localhost:9115/probe?module=tcp_connect&amp;amp;target=x.x.x.x:8080&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You get &lt;code&gt;probe_success{instance="app-1"} = 1&lt;/code&gt; (up) or &lt;code&gt;0&lt;/code&gt; (down).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;New instances in the cluster are monitored &lt;strong&gt;automatically&lt;/strong&gt; — no config change.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.3 Blackbox module
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# blackbox/blackbox.yml&lt;/span&gt;
&lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tcp_connect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;prober&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tcp&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.4 Prometheus basic auth
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus/web.yml&lt;/span&gt;
&lt;span class="na"&gt;basic_auth_users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$2b$10$REPLACE_WITH_BCRYPT_HASH"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;rounds=10&lt;/code&gt; keeps bcrypt verification around ~50–80 ms. Prometheus checks the hash on &lt;strong&gt;every&lt;/strong&gt; authenticated request (including Grafana's queries), so don't raise it to 14+ unless you enjoy slow dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.5 Least-privilege credentials for exporters
&lt;/h3&gt;

&lt;p&gt;Exporters should never connect as an admin. Create dedicated read-only identities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MongoDB&lt;/strong&gt; — the &lt;code&gt;clusterMonitor&lt;/code&gt; role covers &lt;code&gt;serverStatus&lt;/code&gt;, &lt;code&gt;replSetGetStatus&lt;/code&gt; and &lt;code&gt;dbStats&lt;/code&gt;. The query exporter additionally needs &lt;code&gt;read&lt;/code&gt; on the databases it aggregates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// mongosh, connected to the primary as an admin&lt;/span&gt;
&lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="nx"&gt;admin&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prom_exporter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;passwordPrompt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clusterMonitor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// oplog window metrics&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appdb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;    &lt;span class="c1"&gt;// query-exporter aggregations&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Redis 6+ ACL&lt;/strong&gt; — allow only the commands &lt;code&gt;redis_exporter&lt;/code&gt; uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli ACL SETUSER prom_exporter on &lt;span class="s1"&gt;'&amp;gt;STRONG_PASSWORD'&lt;/span&gt; &lt;span class="s1"&gt;'~*'&lt;/span&gt; &lt;span class="s1"&gt;'&amp;amp;*'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  -@all +@connection +memory +info +latency +slowlog +config|get +cluster|info &lt;span class="se"&gt;\&lt;/span&gt;
  +client +scan +type +strlen +xinfo +xlen +pfcount +zcard +scard +llen +hlen +get +eval &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-hello&lt;/span&gt; &lt;span class="nt"&gt;-echo&lt;/span&gt; &lt;span class="nt"&gt;-quit&lt;/span&gt; &lt;span class="nt"&gt;-auth&lt;/span&gt; &lt;span class="nt"&gt;-reset&lt;/span&gt; &lt;span class="nt"&gt;-readonly&lt;/span&gt; &lt;span class="nt"&gt;-readwrite&lt;/span&gt; &lt;span class="nt"&gt;-asking&lt;/span&gt; &lt;span class="nt"&gt;-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AWS IAM&lt;/strong&gt; — the instance role only needs to &lt;em&gt;describe&lt;/em&gt; instances for EC2 service discovery and read its own secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeAvailabilityZones"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:secretsmanager:*:&amp;lt;ACCOUNT_ID&amp;gt;:secret:monitoring/*"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Running the Stack — Step by Step
&lt;/h2&gt;

&lt;p&gt;Here's the full manual flow, exactly as you'd type it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Clone the repo
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; /opt
&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/patharkar123/monitoring-stack.git monitoring
&lt;span class="go"&gt;Cloning into 'monitoring'...
remote: Enumerating objects: 1432, done.
Receiving objects: 100% (1432/1432), 2.10 MiB | 8.40 MiB/s, done.
&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Build the runtime &lt;code&gt;.env&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Start from the committed env file, then append secrets (from your secret manager, vault, or — for a lab — by hand):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ENV_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;uat
&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cp env&lt;/span&gt;/&lt;span class="nv"&gt;$ENV_NAME&lt;/span&gt;/.env .env
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;aws secretsmanager get-secret-value &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;    --secret-id monitoring/$&lt;/span&gt;ENV_NAME &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    --query SecretString --output text \
&lt;/span&gt;&lt;span class="gp"&gt;  | jq -r 'to_entries[] | "\(.key)=\"\(.value)\""' &amp;gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 .env
&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; .env
&lt;span class="go"&gt;58
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3 — Generate the Prometheus password hash
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; tools run &lt;span class="nt"&gt;--rm&lt;/span&gt; generate-hash
&lt;span class="go"&gt;[+] Pulling 1/1
 ✔ generate-hash Pulled                                                     3.2s
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;2b&lt;span class="nv"&gt;$10$Qm8x0l3d0Vb7gJ1tq5o1KeY8W2r3oNQ3m2xF1c9bZ0t4pF7yXh6uS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste that hash into &lt;code&gt;prometheus/web.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Render &lt;code&gt;prometheus.yml&lt;/code&gt; from the template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .env&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; +a&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;    envsubst &amp;lt; prometheus/$&lt;/span&gt;ENV_NAME/prometheus-tmpl.yml &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; prometheus/prometheus.yml&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; promtool &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;    -v $&lt;/span&gt;PWD/prometheus:/p prom/prometheus:v3.14.0 check config /p/prometheus.yml
&lt;span class="go"&gt;Checking /p/prometheus.yml
 SUCCESS: /p/prometheus.yml is valid prometheus config file syntax
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;✅ Always run &lt;code&gt;promtool check config&lt;/code&gt; before starting. A YAML typo otherwise shows up as a crash-looping container.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 5 — Validate the compose file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$ENV_NAME&lt;/span&gt; config &lt;span class="nt"&gt;--services&lt;/span&gt;
&lt;span class="go"&gt;mongodb-exporter
mongodb-query-exporter
blackbox-exporter
redis-exporter
nginx-api-exporter
pushgateway
prometheus
grafana
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows exactly which services the profile will start — a great sanity check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6 — Start everything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; monitoring &lt;span class="nt"&gt;--profile&lt;/span&gt; &lt;span class="nv"&gt;$ENV_NAME&lt;/span&gt; up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;span class="go"&gt;[+] Running 11/11
 ✔ Network monitoring_default            Created                            0.1s
 ✔ Volume "monitoring_prometheus_data"   Created                            0.0s
 ✔ Volume "monitoring_grafana_data"      Created                            0.0s
 ✔ Container blackbox-exporter           Started                            0.8s
 ✔ Container redis-exporter              Started                            0.8s
 ✔ Container mongodb-exporter            Started                            0.9s
 ✔ Container mongodb-query-exporter      Started                            0.9s
 ✔ Container nginx-api-exporter          Started                            0.8s
 ✔ Container pushgateway                 Started                            0.7s
 ✔ Container prometheus                  Started                            1.2s
 ✔ Container grafana                     Started                            1.6s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7 — Check container health
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; monitoring ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s2"&gt;"table {{.Name}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.Status}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.Ports}}"&lt;/span&gt;
&lt;span class="go"&gt;NAME                     STATUS                    PORTS
&lt;/span&gt;&lt;span class="gp"&gt;blackbox-exporter        Up 2 minutes              0.0.0.0:9115-&amp;gt;&lt;/span&gt;9115/tcp
&lt;span class="go"&gt;grafana                  Up 2 minutes (healthy)
&lt;/span&gt;&lt;span class="gp"&gt;mongodb-exporter         Up 2 minutes              0.0.0.0:9216-&amp;gt;&lt;/span&gt;9216/tcp
&lt;span class="gp"&gt;mongodb-query-exporter   Up 2 minutes              0.0.0.0:9412-&amp;gt;&lt;/span&gt;9412/tcp
&lt;span class="gp"&gt;nginx-api-exporter       Up 2 minutes              0.0.0.0:9113-&amp;gt;&lt;/span&gt;9113/tcp
&lt;span class="go"&gt;prometheus               Up 2 minutes (healthy)
&lt;/span&gt;&lt;span class="gp"&gt;pushgateway              Up 2 minutes              0.0.0.0:9091-&amp;gt;&lt;/span&gt;9091/tcp
&lt;span class="gp"&gt;redis-exporter           Up 2 minutes              0.0.0.0:9121-&amp;gt;&lt;/span&gt;9121/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Prometheus and Grafana show no ports because they use host networking.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8 — Verify each exporter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:9216/metrics | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m1&lt;/span&gt; &lt;span class="s1"&gt;'^mongodb_up'&lt;/span&gt;
&lt;span class="go"&gt;mongodb_up{cluster_role="mongod"} 1

&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:9121/metrics | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^redis_up'&lt;/span&gt;
&lt;span class="go"&gt;redis_up 1

&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:9113/metrics | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^nginx_up'&lt;/span&gt;
&lt;span class="go"&gt;nginx_up 1

&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"localhost:9115/probe?module=tcp_connect&amp;amp;target=x.x.x.x:27017"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^probe_success'&lt;/span&gt;
&lt;span class="go"&gt;probe_success 1

&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:9412/metrics | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m2&lt;/span&gt; &lt;span class="s1"&gt;'^app_'&lt;/span&gt;
&lt;span class="go"&gt;app_total_calls{tenantId="tenant-a"} 1824
app_success_calls{tenantId="tenant-a"} 1797
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 9 — Verify Prometheus targets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; admin:&lt;span class="nv"&gt;$PROMETHEUS_PASSWORD&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    localhost:9090/api/v1/targets \
  | jq -r '.data.activeTargets[] | "\(.health)\t\(.labels.job)"' | sort | uniq -c
      1 up  grafana
      4 up  app-port-check
      1 up  mongodb-exporter
      1 up  mongodb-query-exporter
      1 up  nginx-api-exporter
      1 up  prometheus
      1 up  redis-exporter
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything showing &lt;code&gt;down&lt;/code&gt;? Check &lt;code&gt;.lastError&lt;/code&gt; on that target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; admin:&lt;span class="nv"&gt;$PROMETHEUS_PASSWORD&lt;/span&gt; localhost:9090/api/v1/targets &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;  | jq -r '.data.activeTargets[] | select(.health!="up") | "\(.labels.job): \(.lastError)"'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 10 — Open Grafana
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:/opt/monitoring$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:3000/api/health
&lt;span class="go"&gt;{
  "database": "ok",
  "version": "13.2.0"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browse to &lt;code&gt;http://&amp;lt;host&amp;gt;:3000&lt;/code&gt;, log in with &lt;code&gt;GF_SECURITY_ADMIN_USER&lt;/code&gt; / &lt;code&gt;GF_SECURITY_ADMIN_PASSWORD&lt;/code&gt;, add Prometheus (&lt;code&gt;http://localhost:9090&lt;/code&gt;, basic auth) as a data source, and your provisioned dashboards are already waiting in their folders.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. PromQL, Recording Rules &amp;amp; Alerting
&lt;/h2&gt;

&lt;p&gt;Collecting metrics is half the job. These are the queries that actually power the dashboards and alerts.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.1 Queries worth knowing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Business success rate per tenant (%), from mongodb-query-exporter
100 * app_success_calls / clamp_min(app_total_calls, 1)

# MongoDB replication lag per secondary (seconds; optimeDate is in ms)
(scalar(max(mongodb_rs_members_optimeDate{member_state="PRIMARY"}))
  - mongodb_rs_members_optimeDate{member_state="SECONDARY"}) / 1000

# MongoDB connection saturation (%)
100 * mongodb_ss_connections{conn_type="current"}
    / (mongodb_ss_connections{conn_type="current"} + mongodb_ss_connections{conn_type="available"})

# Redis memory usage vs maxmemory (%)
100 * redis_memory_used_bytes / clamp_min(redis_memory_max_bytes, 1)

# Redis cache hit ratio (5m)
rate(redis_keyspace_hits_total[5m])
  / (rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m]))

# NGINX requests per second across the cluster
sum(rate(nginx_http_requests_total[1m]))

# Which ports are down right now?
probe_success == 0

# Exporter itself unreachable
up == 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;code&gt;clamp_min(x, 1)&lt;/code&gt; avoids divide-by-zero &lt;code&gt;NaN&lt;/code&gt; when a tenant or instance has no traffic. &lt;code&gt;NaN&lt;/code&gt; never triggers alerts, which silently hides real outages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  8.2 Recording rules
&lt;/h3&gt;

&lt;p&gt;Heavy queries evaluated on every dashboard refresh waste CPU. Pre-compute them with &lt;strong&gt;recording rules&lt;/strong&gt;, then point dashboards at the recorded series.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# prometheus/rules/recording.yml&lt;/span&gt;
&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app-sli&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1m&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tenant:success_ratio:pct&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100 * app_success_calls / clamp_min(app_total_calls, 1)&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cluster:nginx_requests:rate1m&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum(rate(nginx_http_requests_total[1m]))&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;instance:redis_memory_used:pct&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100 * redis_memory_used_bytes / clamp_min(redis_memory_max_bytes, 1)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load them by adding &lt;code&gt;rule_files: ['/etc/prometheus/rules/*.yml']&lt;/code&gt; to the template and mounting &lt;code&gt;./prometheus/rules:/etc/prometheus/rules:ro&lt;/code&gt;. The naming convention &lt;code&gt;level:metric:operation&lt;/code&gt; tells readers at a glance what was aggregated.&lt;/p&gt;

&lt;h3&gt;
  
  
  8.3 Alert rules
&lt;/h3&gt;

&lt;p&gt;We manage alerts in Grafana (provisioned from JSON via its API), but the logic is identical in native Prometheus rule syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;availability&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServicePortDown&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;probe_success == &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2m&lt;/span&gt;                     &lt;span class="c1"&gt;# ignore single failed probes&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;critical&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Port&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;check&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.instance&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TCP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;connect&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.probe_target&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;minutes."&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MongoReplicationLagHigh&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;(scalar(max(mongodb_rs_members_optimeDate{member_state="PRIMARY"}))&lt;/span&gt;
            &lt;span class="s"&gt;- mongodb_rs_members_optimeDate{member_state="SECONDARY"}) / 1000 &amp;gt; 30&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;warning&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TenantSuccessRateDrop&lt;/span&gt;
        &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tenant:success_ratio:pct &amp;lt; 90 and app_total_calls &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;
        &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;critical&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Success&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$labels.tenantId&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$value&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;printf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;%.1f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}%"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Design notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;for:&lt;/code&gt;&lt;/strong&gt; is your noise filter. Port checks: 1–2 minutes. Capacity alerts: 10+ minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;and app_total_calls &amp;gt; 50&lt;/code&gt;&lt;/strong&gt; prevents alerting on a 1-of-2 failure at 3 AM when traffic is near zero.&lt;/li&gt;
&lt;li&gt;Route &lt;code&gt;severity=critical&lt;/code&gt; to on-call chat/pager and &lt;code&gt;warning&lt;/code&gt; to email. Everyone ignores a channel that pages for everything.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  9. Capacity Planning &amp;amp; Hardening
&lt;/h2&gt;

&lt;h3&gt;
  
  
  9.1 Sizing Prometheus storage
&lt;/h3&gt;

&lt;p&gt;Prometheus stores roughly &lt;strong&gt;1–2 bytes per sample&lt;/strong&gt; after compression. Estimate disk with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;disk_bytes ≈ retention_seconds × ingested_samples_per_second × bytes_per_sample
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked example for this stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Active series&lt;/td&gt;
&lt;td&gt;~60,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average scrape interval&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Samples/second&lt;/td&gt;
&lt;td&gt;60,000 / 30 = &lt;strong&gt;2,000&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retention&lt;/td&gt;
&lt;td&gt;30 d = 2,592,000 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bytes/sample&lt;/td&gt;
&lt;td&gt;~1.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,592,000 × 2,000 × 1.5 ≈ &lt;strong&gt;7.8 GB&lt;/strong&gt; (+ WAL &amp;amp; compaction headroom → provision &lt;strong&gt;20 GB&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check your real numbers instead of guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; admin:&lt;span class="nv"&gt;$PROMETHEUS_PASSWORD&lt;/span&gt; localhost:9090/api/v1/status/tsdb &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    | jq '.data.headStats, (.data.seriesCountByMetricName[:5])'
{
  "numSeries": 58312,
  "chunkCount": 121480,
  "minTime": 1790370000000,
  "maxTime": 1790377200000
}
[
  { "name": "mongodb_ss_wt_cache_bytes", "value": 4210 },
  { "name": "mongodb_collstats_storageStats_indexSizes", "value": 3877 },
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;]

&lt;/span&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; admin:&lt;span class="nv"&gt;$PROMETHEUS_PASSWORD&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    'localhost:9090/api/v1/query?query=rate(prometheus_tsdb_head_samples_appended_total[5m])' \
    | jq -r '.data.result[0].value[1]'
1987.4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;seriesCountByMetricName&lt;/code&gt; list is your &lt;strong&gt;cardinality hit list&lt;/strong&gt;. If one metric dominates, drop it with &lt;code&gt;metric_relabel_configs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mongodb-exporter'&lt;/span&gt;
    &lt;span class="na"&gt;metric_relabel_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source_labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;__name__&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;regex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mongodb_collstats_storageStats_indexSizes'&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Memory rule of thumb:&lt;/strong&gt; ~3–4 KB of RAM per active series in the head block. 60k series ≈ 250 MB, so a &lt;strong&gt;t3.medium (4 GB)&lt;/strong&gt; runs this whole stack comfortably.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.2 Container resource limits &amp;amp; log rotation
&lt;/h3&gt;

&lt;p&gt;Without limits, one misbehaving exporter can starve Prometheus. Without log rotation, Docker's &lt;code&gt;json-file&lt;/code&gt; logs will eventually fill the disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;x-defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;defaults&lt;/span&gt;
  &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
  &lt;span class="na"&gt;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json-file&lt;/span&gt;
    &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;max-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10m"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;max-file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redis-exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaults&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oliver006/redis_exporter:v1.90.0&lt;/span&gt;
    &lt;span class="na"&gt;mem_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128m&lt;/span&gt;
    &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25&lt;/span&gt;

  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*defaults&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prom/prometheus:v3.14.0&lt;/span&gt;
    &lt;span class="na"&gt;mem_limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2g&lt;/span&gt;
    &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;x-&lt;/code&gt; extension field + YAML anchor (&lt;code&gt;&amp;amp;defaults&lt;/code&gt; / &lt;code&gt;&amp;lt;&amp;lt;: *defaults&lt;/code&gt;) keeps these settings in one place.&lt;/p&gt;

&lt;h3&gt;
  
  
  9.3 Network hardening
&lt;/h3&gt;

&lt;p&gt;Prometheus and Grafana use host networking, so they reach exporters via &lt;code&gt;localhost&lt;/code&gt;. That means &lt;strong&gt;exporter ports don't need to be exposed on the network interface at all&lt;/strong&gt;. Bind them to loopback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:9216:9216"&lt;/span&gt;   &lt;span class="c1"&gt;# instead of "9216:9216"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify what's actually listening externally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;ec2-user@monitoring-host:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR==1 || /:(3000|9090|91[0-9][0-9]|92[0-9][0-9]|94[0-9][0-9])/'&lt;/span&gt;
&lt;span class="go"&gt;State   Recv-Q  Send-Q  Local Address:Port   Peer Address:Port  Process
LISTEN  0       4096        127.0.0.1:9216        0.0.0.0:*      users:(("docker-proxy",pid=2211))
LISTEN  0       4096        127.0.0.1:9121        0.0.0.0:*      users:(("docker-proxy",pid=2240))
LISTEN  0       4096                *:9090              *:*      users:(("prometheus",pid=2302))
LISTEN  0       4096                *:3000              *:*      users:(("grafana",pid=2355))
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then put Grafana and Prometheus behind a TLS-terminating load balancer or reverse proxy, and restrict the security group so ports 3000/9090 accept traffic &lt;strong&gt;only&lt;/strong&gt; from that proxy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Docker publishes ports by writing its own iptables rules, which &lt;strong&gt;bypass &lt;code&gt;ufw&lt;/code&gt;/&lt;code&gt;firewalld&lt;/code&gt;&lt;/strong&gt;. Loopback binding or security groups are the reliable controls, not the host firewall.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  10. Automating It — Jenkins Deploy with Rollback
&lt;/h2&gt;

&lt;p&gt;Doing the above by hand is fine once. For every change we use a Jenkins pipeline with one parameter: &lt;strong&gt;which environment&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="s2"&gt;"${whereTo}-jenkins-agent"&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s1"&gt;'whereTo'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;choices:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'uat'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'prod'&lt;/span&gt;&lt;span class="o"&gt;],&lt;/span&gt; &lt;span class="nl"&gt;description:&lt;/span&gt; &lt;span class="s1"&gt;'Which env?'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s1"&gt;'branch'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="nl"&gt;defaultValue:&lt;/span&gt; &lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;booleanParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s1"&gt;'skipDeploy'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;defaultValue:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;stages&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'checkout'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'monitoring'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;git&lt;/span&gt; &lt;span class="nl"&gt;url:&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REPO_URL&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;branch:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;credentialsId:&lt;/span&gt; &lt;span class="s1"&gt;'git-ssh'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'init'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'monitoring'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;envVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"env/${whereTo}/.env"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readProperties&lt;/span&gt; &lt;span class="nl"&gt;file:&lt;/span&gt; &lt;span class="s2"&gt;"env/${whereTo}/env.properties"&lt;/span&gt;   &lt;span class="c1"&gt;// hostIps=x.x.x.x,...&lt;/span&gt;
        &lt;span class="n"&gt;hostIps&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;props&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hostIps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// secrets → appended to .env (never committed)&lt;/span&gt;
        &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readJSON&lt;/span&gt; &lt;span class="nl"&gt;text:&lt;/span&gt; &lt;span class="n"&gt;getSecret&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"monitoring/${whereTo}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;envVars&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;"\n${k}=\"${v}\""&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;writeFile&lt;/span&gt; &lt;span class="nl"&gt;file:&lt;/span&gt; &lt;span class="s1"&gt;'.env'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;text:&lt;/span&gt; &lt;span class="n"&gt;envVars&lt;/span&gt;

        &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s2"&gt;"""(set +x; set -a; source .env; set +a;
               envsubst &amp;lt; prometheus/${whereTo}/prometheus-tmpl.yml &amp;gt; prometheus/prometheus.yml)"""&lt;/span&gt;
      &lt;span class="o"&gt;}}}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'deploy'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;expression&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;skipDeploy&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;hostIps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
          &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;doRelease&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
          &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"❌ Release failed on ${ip}: ${e} — rolling back"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;doRelease&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;}}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;doRelease&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rollback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;def&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/opt/monitoring'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/opt/monitoring-bk'&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rollback&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sudo rm -rf ${live} &amp;amp;&amp;amp; sudo mv ${bk} ${live}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sudo bash -c 'mkdir -p ${live} &amp;amp;&amp;amp; rm -rf ${bk} /tmp/monitoring &amp;amp;&amp;amp; mv ${live} ${bk}'"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'monitoring'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/tmp'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;// push the rendered workspace&lt;/span&gt;
    &lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sudo mv /tmp/monitoring ${live}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"sudo bash -c 'cd ${live} &amp;amp;&amp;amp; source .env &amp;amp;&amp;amp; docker compose -p monitoring down || true &amp;amp;&amp;amp; docker compose up -d'"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pattern is simple but effective:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   live dir ──mv──► backup dir          (keep last good release)
   new files ──scp──► /tmp ──mv──► live dir
   docker compose down &amp;amp;&amp;amp; up -d
        │
        ├─ success ─► done ✅
        └─ failure ─► rm live, mv backup → live, compose up ─► previous version running ↩️
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the &lt;strong&gt;rendered&lt;/strong&gt; &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;prometheus.yml&lt;/code&gt; travel with the release, the rollback restores both code &lt;em&gt;and&lt;/em&gt; config.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Day-2 Operations Cheat Sheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Tail logs &lt;span class="k"&gt;for &lt;/span&gt;one service
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; monitoring logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 prometheus
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Hot-reload Prometheus after editing prometheus.yml &lt;span class="o"&gt;(&lt;/span&gt;no restart&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-u&lt;/span&gt; admin:&lt;span class="nv"&gt;$PROMETHEUS_PASSWORD&lt;/span&gt; localhost:9090/-/reload
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Restart a single exporter
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; monitoring restart mongodb-query-exporter
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Upgrade an image: bump the tag &lt;span class="k"&gt;in &lt;/span&gt;docker-compose.yml, &lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; monitoring pull grafana
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; monitoring up &lt;span class="nt"&gt;-d&lt;/span&gt; grafana
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;See resource usage
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker stats &lt;span class="nt"&gt;--no-stream&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s2"&gt;"table {{.Name}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.CPUPerc}}&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;{{.MemUsage}}"&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;How big is the TSDB?
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker system &lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;prometheus_data
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Push a metric from a cron/batch job
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"backup_last_success_timestamp &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    | curl --data-binary @- localhost:9091/metrics/job/nightly_backup

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Stop everything &lt;span class="o"&gt;(&lt;/span&gt;volumes – and your data – are kept&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; monitoring down
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;⚠️ Stop AND delete all metrics &amp;amp; Grafana data
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;-p&lt;/span&gt; monitoring down &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Clean up old images after upgrades
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker image prune &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  12. Troubleshooting Guide
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;How to confirm&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;up{job="mongodb-query-exporter"} == 0&lt;/code&gt; intermittently&lt;/td&gt;
&lt;td&gt;Aggregation slower than &lt;code&gt;scrape_timeout&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scrape_duration_seconds&lt;/code&gt; ≈ timeout&lt;/td&gt;
&lt;td&gt;Add indexes, raise &lt;code&gt;cache&lt;/code&gt;, keep &lt;code&gt;scrape_timeout&lt;/code&gt; &amp;lt; &lt;code&gt;scrape_interval&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mongodb_up 0&lt;/code&gt;, exporter &lt;code&gt;up 1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;TLS hostname mismatch / wrong &lt;code&gt;extra_hosts&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker logs mongodb-exporter&lt;/code&gt; → &lt;code&gt;x509: certificate is valid for …&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Connect by the cert's hostname; fix &lt;code&gt;*_HOST_DNS&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mongodb_ss_*&lt;/code&gt; series missing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;collstats&lt;/code&gt; starving the connection pool&lt;/td&gt;
&lt;td&gt;Only &lt;code&gt;mongodb_collstats_*&lt;/code&gt; present&lt;/td&gt;
&lt;td&gt;Use explicit &lt;code&gt;--collector.*&lt;/code&gt; flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compose: &lt;code&gt;service "x" depends on undefined service&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;depends_on&lt;/code&gt; a service outside the active profile&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose --profile &amp;lt;env&amp;gt; config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only depend on services present in every profile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus restarts in a loop&lt;/td&gt;
&lt;td&gt;Invalid rendered &lt;code&gt;prometheus.yml&lt;/code&gt; (empty &lt;code&gt;${VAR}&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;promtool check config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ensure all template variables exist in &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana "401" on Prometheus datasource&lt;/td&gt;
&lt;td&gt;Hash in &lt;code&gt;web.yml&lt;/code&gt; ≠ password in datasource&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl -u admin:&amp;lt;pw&amp;gt; localhost:9090/-/healthy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Regenerate hash, restart Prometheus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk full on host&lt;/td&gt;
&lt;td&gt;Unrotated container logs or TSDB growth&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker system df -v&lt;/code&gt;, &lt;code&gt;du -sh /var/lib/docker/containers/*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Log rotation (9.2), drop high-cardinality metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;probe_success 0&lt;/code&gt; but service works from your laptop&lt;/td&gt;
&lt;td&gt;Security group blocks the monitoring host&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nc -vz &amp;lt;ip&amp;gt; &amp;lt;port&amp;gt;&lt;/code&gt; from the host&lt;/td&gt;
&lt;td&gt;Allow the monitoring host's SG as source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  13. Lessons Learned (the Hard Way)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;depends_on&lt;/code&gt; + profiles can break the whole project.&lt;/strong&gt;&lt;br&gt;
If Prometheus &lt;code&gt;depends_on: mongodb-exporter-prod&lt;/code&gt; but you run the &lt;code&gt;uat&lt;/code&gt; profile, Compose treats that service as undefined and &lt;strong&gt;refuses to load the project&lt;/strong&gt;. Only depend on services that exist in &lt;em&gt;every&lt;/em&gt; profile the dependent runs in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;--collect-all&lt;/code&gt; is not free.&lt;/strong&gt;&lt;br&gt;
On large MongoDB clusters, &lt;code&gt;collstats&lt;/code&gt; monopolised the exporter's connection and critical metrics (&lt;code&gt;mongodb_ss_*&lt;/code&gt;, replica-set state) went missing. Enable only the collectors your dashboards use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Point business queries at secondaries and cache them.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;readPreference=secondaryPreferred&lt;/code&gt; + &lt;code&gt;cache: "5m"&lt;/code&gt; + a generous &lt;code&gt;scrape_timeout&lt;/code&gt; kept heavy aggregations away from production traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Use &lt;code&gt;extra_hosts&lt;/code&gt; for TLS hostnames.&lt;/strong&gt;&lt;br&gt;
TLS verification needs the hostname on the certificate. Injecting &lt;code&gt;host:ip&lt;/code&gt; via &lt;code&gt;extra_hosts&lt;/code&gt; avoids hacking &lt;code&gt;/etc/hosts&lt;/code&gt; on the server itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Keep secrets out of Git, render at deploy time.&lt;/strong&gt;&lt;br&gt;
Committed &lt;code&gt;.env&lt;/code&gt; = non-secret config. Secrets are fetched from a secret manager and appended in the pipeline workspace. The Prometheus password is &lt;code&gt;envsubst&lt;/code&gt;-ed into &lt;code&gt;prometheus.yml&lt;/code&gt; only on the deploy host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Validate before you restart.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;docker compose config&lt;/code&gt;, &lt;code&gt;promtool check config&lt;/code&gt;, and a &lt;code&gt;--dry-run&lt;/code&gt;/&lt;code&gt;skipDeploy&lt;/code&gt; flag in the pipeline catch 90% of mistakes before they take monitoring down — which is the one system you really need up when everything else is on fire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Know who owns what in Grafana.&lt;/strong&gt;&lt;br&gt;
If you mix file provisioning, API provisioning and Grafana's Git Sync, make each one own a clearly separate set of resources (e.g. dashboards via files/Git, alerts &amp;amp; datasources via API). Overlap leads to "who deleted my datasource?" mysteries.&lt;/p&gt;


&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;With one &lt;code&gt;docker-compose.yml&lt;/code&gt;, a handful of config files and a small pipeline, we got:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Infra metrics for MongoDB, Redis, NGINX&lt;/li&gt;
&lt;li&gt;✅ Business KPIs straight from MongoDB aggregations&lt;/li&gt;
&lt;li&gt;✅ Automatic port checks for every instance in a cluster via EC2 discovery&lt;/li&gt;
&lt;li&gt;✅ Dashboards-as-code with folder structure&lt;/li&gt;
&lt;li&gt;✅ Repeatable, multi-environment deploys with automatic rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole stack comes up in under a minute on a fresh host:&lt;/p&gt;

&lt;p&gt;The full stack is in &lt;a href="https://github.com/patharkar123/monitoring-stack" rel="noopener noreferrer"&gt;patharkar123/monitoring-stack&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/patharkar123/monitoring-stack.git monitoring
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;monitoring
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cp env&lt;/span&gt;/uat/.env .env   &lt;span class="c"&gt;# + secrets&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;envsubst &amp;lt; prometheus/uat/prometheus-tmpl.yml &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; prometheus/prometheus.yml
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;docker compose &lt;span class="nt"&gt;--profile&lt;/span&gt; uat up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you found this useful, drop a ❤️ or a comment — and tell me what &lt;em&gt;you&lt;/em&gt; monitor that I didn't cover here. For any issue or support, reach me on &lt;a href="https://www.linkedin.com/in/sanket-patharkar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. Happy monitoring! 📈&lt;/p&gt;




&lt;h2&gt;
  
  
  🔜 Up Next
&lt;/h2&gt;

&lt;p&gt;In the next article I'll cover &lt;strong&gt;alerting as code&lt;/strong&gt;: provisioning Grafana alert rules, contact points and datasources through the Grafana HTTP API from Jenkins, with idempotent diffing and dry-run support.&lt;/p&gt;




&lt;h2&gt;
  
  
  👤 About the Author
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sanket Satish Patharkar&lt;/strong&gt; is a Senior Cloud Operation Engineer with over 6 years of experience in AWS, DevOps, CI/CD, and cloud architecture for enterprise-grade systems. 3× AWS Certified, with a focus on cloud infrastructure, observability, automation, and GenAI.&lt;/p&gt;

&lt;p&gt;📍 Pune, Maharashtra, India · 🎓 B.E. in Electronics &amp;amp; Telecommunication, Savitribai Phule Pune University&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/patharkar123/monitoring-stack" rel="noopener noreferrer"&gt;github.com/patharkar123/monitoring-stack&lt;/a&gt; · For any issue or support: &lt;a href="https://www.linkedin.com/in/sanket-patharkar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>infrastructure</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Designing a Highly Available and Scalable Architecture on AWS</title>
      <dc:creator>Sanket Patharkar</dc:creator>
      <pubDate>Thu, 03 Jul 2025 08:41:04 +0000</pubDate>
      <link>https://dev.to/sanket_patharkar/designing-a-highly-available-and-scalable-architecture-on-aws-1iap</link>
      <guid>https://dev.to/sanket_patharkar/designing-a-highly-available-and-scalable-architecture-on-aws-1iap</guid>
      <description>&lt;p&gt;By Sanket Satish Patharkar, Senior Cloud Operation Engineer&lt;/p&gt;

&lt;p&gt;This document is a practical design for an application that must stay available when an instance, an Availability Zone, or a deployment fails, and that must scale with demand instead of with a fixed fleet. It is written so each section can be copied as-is.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Scope and design targets
&lt;/h2&gt;

&lt;p&gt;High availability and disaster recovery are different problems. Multi-AZ keeps a single Region serving traffic through an instance or Availability Zone failure. Multi-Region is for the loss of a Region, and it is justified only after recovery objectives are explicit.&lt;/p&gt;

&lt;p&gt;Set these before choosing services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Availability target. 99.99% allows about 52 minutes of downtime per year. That target requires at least two Availability Zones, health-checked load balancing, and a data store that fails over without a manual restore.&lt;/li&gt;
&lt;li&gt;Recovery point objective (RPO). How much data loss is acceptable. RDS Multi-AZ and Aurora use synchronous storage inside the Region, so the RPO for an Availability Zone failure is near zero. A cross-Region replica is asynchronous, so the RPO is the replication lag.&lt;/li&gt;
&lt;li&gt;Recovery time objective (RTO). How long the service may be down. An Application Load Balancer removes a failed task in the health-check interval. An RDS Multi-AZ failover is typically one to two minutes. Aurora failover is usually under a minute. A Regional failover is measured in minutes only if the second Region is already warm.&lt;/li&gt;
&lt;li&gt;Scale unit. What grows: requests per second, messages per second, or stored bytes. The Auto Scaling signal has to match that unit.
## 2. Reference architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One Virtual Private Cloud, three Availability Zones, three subnet tiers.&lt;/p&gt;

&lt;p&gt;Public subnets hold only the load balancer nodes and the NAT gateways. Application subnets are private and hold Amazon EC2, Amazon ECS, or Amazon EKS. Data subnets are private and hold Amazon RDS or Amazon Aurora, and Amazon ElastiCache. There is no path from the internet to the data tier.&lt;/p&gt;

&lt;p&gt;Request path for a synchronous API:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Amazon Route 53 resolves the name. A latency or failover record selects the Region. The record TTL for a failover name is 60 seconds or less.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Amazon CloudFront terminates viewer TLS with an AWS Certificate Manager certificate, caches what is safe to cache, and forwards dynamic requests to the Application Load Balancer. AWS WAF is attached here or on the load balancer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Application Load Balancer spans all three Availability Zones, checks a /health endpoint, and sends traffic only to healthy targets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The application is stateless. Session and hot keys live in ElastiCache or Amazon DynamoDB. The instance disk is disposable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Writes go to the RDS or Aurora writer. Reads that tolerate replication lag go to a reader endpoint. Connection spikes go through Amazon RDS Proxy so a scale-out does not exhaust database connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Work that does not need an immediate response is published to Amazon SQS or Amazon EventBridge. The API returns after the enqueue.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Outbound path from a private subnet: a NAT gateway in the same Availability Zone, or a VPC endpoint when the destination is an AWS API. Amazon S3 and DynamoDB use gateway endpoints. AWS Secrets Manager, Amazon KMS, Amazon CloudWatch Logs, Amazon ECR, and AWS Systems Manager use interface endpoints so a NAT failure does not block pulls, secrets, or log shipping.&lt;/p&gt;

&lt;p&gt;Figure 1. Multi-AZ reference architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Edge, DNS, and load balancing
&lt;/h2&gt;

&lt;p&gt;Route 53 health checks the load balancer or a synthetic endpoint, not a single instance. Failover routing moves DNS to the standby Region. Latency routing is for active-active. Do not create the standby DNS record until the standby can actually serve.&lt;/p&gt;

&lt;p&gt;Use an Application Load Balancer for HTTP and HTTPS. Use a Network Load Balancer only when you need a static IP, TCP or UDP, or millions of connections per second. Enable cross-zone load balancing on the Application Load Balancer. Set deregistration delay to match the longest request you must finish, commonly 30 to 60 seconds, so a scale-in does not cut in-flight work.&lt;/p&gt;

&lt;p&gt;The target health check uses the application health path, an interval of 10 seconds, and a threshold of 2. A process that is up but cannot reach its database must fail that check.&lt;/p&gt;

&lt;p&gt;Amazon CloudFront sits in front of the load balancer for TLS at the edge and to absorb bursts of static content. Amazon API Gateway is the front door when the client is a device or a partner calling a REST or WebSocket API, with throttling per API key. AWS Global Accelerator is the alternative when clients need static anycast IPs and fast Regional failover without waiting on DNS TTL.&lt;/p&gt;

&lt;p&gt;AWS WAF on CloudFront or the Application Load Balancer blocks common web exploits. AWS Shield Standard is automatic. Shield Advanced is for workloads that are attacked and need cost protection and the response team, not for every design.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Compute and scaling
&lt;/h2&gt;

&lt;p&gt;Amazon EC2 Auto Scaling uses a launch template, not a launch configuration. The template enforces IMDSv2, encrypts EBS with a customer managed KMS key, and attaches an instance profile. No long-lived access keys are placed on the instance.&lt;/p&gt;

&lt;p&gt;Minimum capacity is at least two, spread across Availability Zones, and that minimum runs On-Demand. Spot capacity is extra scale above the minimum. If the minimum the availability target depends on is Spot, a capacity withdrawal is an outage.&lt;/p&gt;

&lt;p&gt;Scaling policy is target tracking on the signal that matches the workload: ALBRequestCountPerTarget for a request-driven API, or CPU only when CPU is the real bottleneck. Scale-in is slower than scale-out. Health check type is ELB, so a target the load balancer has marked unhealthy is replaced.&lt;/p&gt;

&lt;p&gt;A rolling deployment uses a minimum healthy percentage so both Availability Zones are never replaced in the same batch. For Amazon ECS on AWS Fargate, the service spans the private subnets, the deployment minimum healthy percent stays at 100 with a maximum of 200, and tasks are replaced only after the new task is healthy. Amazon EKS uses the same rule: a PodDisruptionBudget and topology spread so one zone cannot hold every replica.&lt;/p&gt;

&lt;p&gt;AWS Lambda is for event-driven work with uneven load. Set reserved concurrency on functions that call the database so a spike cannot exhaust connections. Lambda destinations or an SQS dead-letter queue capture failed asynchronous invocations.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Data
&lt;/h2&gt;

&lt;p&gt;Amazon RDS Multi-AZ keeps a synchronous standby in another Availability Zone and fails over the writer endpoint. Use it when the engine must be MySQL, PostgreSQL, SQL Server, or Oracle and failover in one to two minutes is acceptable. Amazon Aurora uses a shared storage volume across three Availability Zones and fails over faster. Use Aurora when the workload is MySQL- or PostgreSQL-compatible and the RTO is tighter. A read replica, and the Aurora reader endpoint, scale reads. They are not the failover mechanism for the writer.&lt;/p&gt;

&lt;p&gt;Turn on automated backups with a retention that matches the RPO, and point-in-time recovery. Take snapshots before a schema change. Amazon RDS Proxy pools connections and makes a Multi-AZ failover less visible to the application. Store the master password in AWS Secrets Manager and rotate it. Do not put it in the task definition or user data.&lt;/p&gt;

&lt;p&gt;Amazon DynamoDB is the choice when access is by key and the workload is spiky. It is Multi-AZ by default. Point-in-time recovery and on-demand or provisioned capacity with auto scaling belong in the table design. DynamoDB Accelerator (DAX) is a cache in front of DynamoDB, not a general application cache. Global Tables replicate across Regions asynchronously. That RPO is the replication lag, and writes must be idempotent because the same key can be written in two Regions.&lt;/p&gt;

&lt;p&gt;Amazon ElastiCache for Redis uses a replication group with Multi-AZ and automatic failover. The application uses the primary endpoint for writes and the reader endpoint for cache reads. Memcached has no replication. Use it only for data that can be rebuilt. Do not store the only copy of a session in Memcached.&lt;/p&gt;

&lt;p&gt;Amazon S3 holds objects, logs, and backups. Block public access at the account, enable versioning on buckets that hold data you cannot recreate, and encrypt with KMS. S3 Cross-Region Replication copies objects for a Regional loss. It is not a substitute for database backups. AWS Backup is the control plane for RDS, Aurora, EBS, and DynamoDB backup policies, copy to a second Region, and a vault lock when retention must be immutable.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Messaging and events
&lt;/h2&gt;

&lt;p&gt;Amazon SQS decouples the API from slow work. Set the visibility timeout longer than the handler's worst case. Set a redrive policy to a dead-letter queue after a small max receive count, and alarm when that queue is non-empty. The consumer must be idempotent, because a timeout delivers the message again.&lt;/p&gt;

&lt;p&gt;Use SQS standard queues for throughput. Use FIFO only when order per group and exactly-once processing matter, and accept the lower throughput. Amazon SNS fan-out is for one event and many independent consumers. Amazon EventBridge is for routing events between applications by rule, including a scheduled rule that replaces a single cron host.&lt;/p&gt;

&lt;p&gt;Amazon Kinesis Data Streams is the ingest path for ordered, high-rate telemetry. Size shards for the write rate, and use enhanced fan-out when many consumers read the same stream. Amazon Data Firehose delivers the stream to S3 without operating consumers. A Lambda consumer on the stream needs a parallelization factor and a failure destination, or one bad record blocks the shard.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Security that the design depends on
&lt;/h2&gt;

&lt;p&gt;Identity. Humans use IAM Identity Center. Workloads use IAM roles. Resource policies restrict who can assume those roles. There are no IAM users with access keys on servers.&lt;/p&gt;

&lt;p&gt;Network. Security groups are stateful and reference other security groups, not wide CIDR ranges. The database security group allows the application security group on the database port only. Network ACLs stay close to the default unless a compliance rule requires them. A single NAT gateway is rejected in review, because its Availability Zone becomes a dependency for every private subnet.&lt;/p&gt;

&lt;p&gt;Encryption and secrets. Customer managed KMS keys encrypt RDS, EBS, S3, and ElastiCache. Secrets Manager holds database and API credentials. ACM holds the public certificates for CloudFront and the load balancer. Private keys are not checked into Git.&lt;/p&gt;

&lt;p&gt;Detection. Amazon GuardDuty on the account, AWS CloudTrail with log file validation writing to a dedicated S3 bucket, and AWS Config for the rules that drift breaks availability: a security group opened to the world, or a load balancer left in one subnet.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Observability
&lt;/h2&gt;

&lt;p&gt;Amazon CloudWatch metrics, logs, and alarms are the minimum. Each alarm needs an owner and an action. The first alarms are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UnHealthyHostCount and HTTPCode_Target_5XX_Count on the load balancer.&lt;/li&gt;
&lt;li&gt;CPU, database connections, free storage, and replica lag on RDS or Aurora. Alarm on failover events, not only on saturation.&lt;/li&gt;
&lt;li&gt;ApproximateAgeOfOldestMessage on each SQS queue, and depth of each dead-letter queue.&lt;/li&gt;
&lt;li&gt;ElastiCache evictions and replication lag.&lt;/li&gt;
&lt;li&gt;NAT gateway ErrorPortAllocation and packets dropped, per Availability Zone.&lt;/li&gt;
&lt;li&gt;AWS X-Ray or OpenTelemetry traces a request from the load balancer through the application to the database. CloudTrail is the audit log of API calls, not an application log. A subscription filter ships application logs to the tooling you already operate, such as Prometheus and Grafana or an ELK stack, but the CloudWatch alarms above still exist so a third-party outage does not hide a production failure.
## 9. Delivery&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infrastructure is Terraform or AWS CloudFormation, reviewed like application code. Application delivery is GitHub Actions or AWS CodePipeline, with CodeBuild or the GitHub runner producing an immutable image in Amazon ECR. CodeDeploy or the ECS deployment controller rolls that image. A pipeline does not SSH to an instance and edit it.&lt;/p&gt;

&lt;p&gt;Systems Manager Session Manager is the break-glass shell. Patch Manager or a rebuilt image handles OS patches. An instance is replaced, not patched in place, when the launch template is the source of truth.&lt;/p&gt;

&lt;p&gt;A change that cannot roll back is not shipped on a Friday. Database migrations expand first and contract later, so the previous application version still runs during the rollout.&lt;/p&gt;

&lt;p&gt;Figure 2. Failure boundaries: instance, Availability Zone, and Region.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. What happens when something fails
&lt;/h2&gt;

&lt;p&gt;Instance or task failure. The load balancer stops sending traffic after two failed health checks. Auto Scaling or the ECS service replaces the task. In-flight requests finish during deregistration delay. No session is lost, because no session lived on the task.&lt;/p&gt;

&lt;p&gt;Bad deployment. The new tasks fail the health check and the rollout stops. The previous tasks keep the minimum healthy percentage. Both Availability Zones are never drained by the same batch.&lt;/p&gt;

&lt;p&gt;Availability Zone failure. The load balancer, Auto Scaling, RDS Multi-AZ or Aurora, and ElastiCache failover continue in the remaining zones. NAT gateways in the surviving zones still have their own path out. Capacity planning assumes one zone is gone: the remaining zones must hold the steady-state load, not only the peak with all three zones up.&lt;/p&gt;

&lt;p&gt;Dependency failure. A full database connection pool, a stuck queue consumer, or a poisoned message. RDS Proxy, a dead-letter queue, and idempotent consumers are what keep that from becoming an outage. Reserved concurrency on Lambda protects the database from the function.&lt;/p&gt;

&lt;p&gt;Region failure. Route 53 failover sends traffic to the second Region only if that Region is running a pilot light or a warm standby: data replicated, image present, and a scale-up path tested. A DNS record with an empty Region behind it does not meet the RTO. Aurora Global Database or DynamoDB Global Tables set the data RPO. S3 Cross-Region Replication covers objects. AWS Elastic Disaster Recovery is the option when EC2 servers must be replicated at the block level rather than rebuilt from a launch template.&lt;/p&gt;

&lt;p&gt;The test. On a schedule, stop one instance, fail over RDS, and shift the load balancer off one Availability Zone. Record the observed RTO. A design that has not been failed is still a diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Worked example: automotive telematics
&lt;/h2&gt;

&lt;p&gt;A telematics pipeline ingested vehicle signals at a rate that a single instance could not absorb and could not be allowed to drop.&lt;/p&gt;

&lt;p&gt;Vehicles publish to Amazon API Gateway or directly to Amazon Kinesis Data Streams. Kinesis retains the stream so a slow consumer does not lose data. AWS Lambda, with reserved concurrency, validates and writes the hot record to DynamoDB and hands the bulk path to Amazon Data Firehose, which lands objects in S3. ElastiCache holds the latest position per vehicle for the read API. The read API is an Application Load Balancer in front of a stateless service across three Availability Zones, with Aurora or DynamoDB behind it depending on the query shape. Amazon QuickSight reads the curated S3 data for fleet dashboards. GitHub Actions builds the image and CodePipeline deploys it.&lt;/p&gt;

&lt;p&gt;That system was operated at 99.99% availability and scaled with shard count and function concurrency instead of a fixed fleet, with about 60% less operational overhead than the host-based pipeline it replaced. The properties that made the number real were a stateless read path, a retained ingest stream, Multi-AZ data stores, and a deployment that could not take every zone down together.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Review checklist
&lt;/h2&gt;

&lt;p&gt;Use this list before calling an environment production-ready.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three Availability Zones. Private application and data subnets. One NAT gateway per zone. Gateway endpoints for S3 and DynamoDB. Interface endpoints for Secrets Manager, KMS, CloudWatch Logs, ECR, and Systems Manager.&lt;/li&gt;
&lt;li&gt;Route 53 health check and an intentional TTL. CloudFront or API Gateway in front. ACM certificate. WAF attached. Application Load Balancer health check on a path that checks dependencies.&lt;/li&gt;
&lt;li&gt;Launch template with IMDSv2, encrypted EBS, and an instance role. Auto Scaling minimum on On-Demand across zones. Target-tracking policy. Rolling deploy with a minimum healthy percentage.&lt;/li&gt;
&lt;li&gt;RDS Multi-AZ or Aurora, backups and point-in-time recovery, RDS Proxy, credentials in Secrets Manager. ElastiCache replication group with Multi-AZ if the cache holds sessions. DynamoDB point-in-time recovery where DynamoDB is the system of record.&lt;/li&gt;
&lt;li&gt;SQS redrive to a dead-letter queue. Idempotent consumers. Kinesis retention long enough to replay.&lt;/li&gt;
&lt;li&gt;CloudWatch alarms on unhealthy hosts, 5xx, replica lag, queue age, and dead-letter depth. CloudTrail enabled. A tested instance failure, database failover, and single-zone evacuation, with the measured RTO written down.
## Closing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A highly available design is a small set of decisions applied consistently: no single Availability Zone on the critical path, no state on a node you intend to throw away, a data store that fails over inside its RTO, and a deployment and an alarm that you have already watched work. The service list is long. The design is those four rules.&lt;/p&gt;

&lt;p&gt;Author: Sanket Satish Patharkar&lt;/p&gt;

&lt;p&gt;Senior Cloud Operation Engineer. AWS, DevOps, and CI/CD for production systems.&lt;/p&gt;

&lt;p&gt;For any issue or support: &lt;a href="https://www.linkedin.com/in/sanket-patharkar/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>design</category>
      <category>cloudnative</category>
    </item>
  </channel>
</rss>
