<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/feed/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Mohammad Abu Mattar | Case Studies</title><description>The latest Case Studies from Mohammad Abu Mattar.</description><link>https://mkabumattar.com/</link><item><title>Migrating a Monolith to Kubernetes Without a Big-Bang Cutover</title><link>https://mkabumattar.com/case-studies/post/monolith-to-kubernetes-strangler-migration/</link><guid isPermaLink="true">https://mkabumattar.com/case-studies/post/monolith-to-kubernetes-strangler-migration/</guid><description>Using the strangler-fig pattern to move a large monolith onto EKS service by service, with a routing facade, gradual traffic shifting, and a rollback at every step.</description><pubDate>Mon, 20 Jul 2026 11:54:26 GMT</pubDate><content:encoded>&lt;p&gt;Almost every failed &amp;quot;let&amp;#39;s move off the monolith&amp;quot; project shares one detail: the plan was a big-bang cutover. Rewrite in parallel, pick a weekend, flip the switch, and pray. This is the opposite of that. A large application moved onto EKS one service at a time using the strangler-fig pattern, with a routing facade in front, traffic shifting gradually per route, and a working rollback at every single step. No freeze, no weekend gamble, and no moment where the whole thing was in the air.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;The application reached Kubernetes with no big-bang moment. Every route moved gradually behind a facade with a working rollback, so no single step was ever high stakes and delivery never froze.&lt;/p&gt;
&lt;p&gt;The gains were structural rather than a one-time event. Each extracted service got independent deploys and its own scaling, so teams stopped blocking each other and the hot paths no longer forced the whole application to scale with them.&lt;/p&gt;
&lt;div class=&quot;not-prose my-8 flex flex-wrap gap-x-10 gap-y-6&quot;&gt;
  &lt;StatCounter client:load value={0} label=&quot;big-bang cutover events&quot; /&gt;
  &lt;StatCounter client:load value={0} label=&quot;feature freezes&quot; /&gt;
  &lt;StatCounter
    client:load
    value={100}
    suffix=&quot;%&quot;
    label=&quot;of traffic shifted route by route&quot;
  /&gt;
&lt;/div&gt;&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;The monolith itself was not the enemy. It ran fine, the team knew it, and it paid the bills. The problem was that it had become the bottleneck for everything else. Deploys were all-or-nothing, so one risky change held up every other team&amp;#39;s work. Scaling meant scaling the entire application even when only one part was hot. And onboarding a new engineer meant handing them the whole thing at once.&lt;/p&gt;
&lt;p&gt;The tempting fix, a full rewrite with a cutover, is where teams get hurt. You freeze features to build the replacement, the replacement drifts from the original as the original keeps changing, and the cutover becomes a single high-stakes event with no safe rollback. If anything goes wrong at 2am on migration night, the only option is a panicked revert of everything.&lt;/p&gt;
&lt;p&gt;The goal was to get the benefits of independent services without ever betting the business on one cutover. That means the old and new systems have to run side by side, in production, for as long as it takes.&lt;/p&gt;
&lt;h2&gt;Constraints&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No big-bang cutover.&lt;/strong&gt; At no point could correctness depend on a single switch-flip.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No feature freeze.&lt;/strong&gt; The monolith kept shipping features throughout the migration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A rollback at every step.&lt;/strong&gt; Each increment had to be revertible in minutes, not hours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No shared-database free-for-all.&lt;/strong&gt; Extracted services own their data; the goal was decoupling, not a distributed monolith on one schema.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prove parity before deleting anything.&lt;/strong&gt; Old code stayed until the new path was verified against it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;Before the migration, the shape was familiar: an Application Load Balancer in front of a monolith running across an Auto Scaling group, all talking to one shared relational database.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0003-monolith-to-kubernetes-strangler-migration/before-topology.drawio&quot;
  title=&quot;Before: monolith on EC2&quot;
  caption=&quot;An ALB in front of the monolith on an Auto Scaling group, backed by a single shared RDS database.&quot;
  height={480}
/&gt;&lt;/p&gt;
&lt;p&gt;The target keeps the monolith running, containerized, inside an EKS cluster, and puts a routing facade in front of everything. The facade is the heart of the pattern. It looks at each request and decides whether that path has been migrated to a new service or still belongs to the monolith. Extracted services get their own data stores; the monolith keeps its shared database until its remaining parts are small.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0003-monolith-to-kubernetes-strangler-migration/after-topology.drawio&quot;
  title=&quot;After: strangler facade on EKS&quot;
  caption=&quot;The facade routes migrated paths (users, billing) to extracted services with their own databases, while everything else still goes to the containerized monolith.&quot;
  height={560}
/&gt;&lt;/p&gt;
&lt;p&gt;The name comes from the strangler fig, a plant that grows around a tree and gradually replaces it. The new system grows around the monolith, taking over one responsibility at a time, until the original is either gone or small enough to leave alone. Nothing about it requires a dramatic finish.&lt;/p&gt;
&lt;h2&gt;The routing facade&lt;/h2&gt;
&lt;p&gt;The facade is where the safety comes from. Every request enters through it, and a route table decides the destination. A path that has been migrated goes to the new service; everything else defaults to the monolith. Crucially, migration of a single route is itself gradual: you shift a small percentage of that route&amp;#39;s traffic to the new service, watch it, and widen only when it holds. If the new service misbehaves, the facade falls straight back to the monolith, which is still running and still correct.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0003-monolith-to-kubernetes-strangler-migration/strangler-routing.drawio&quot;
  title=&quot;Strangler routing&quot;
  caption=&quot;The facade sends migrated paths to the extracted service and everything else to the monolith, shifting each route gradually with instant fallback.&quot;
  height={440}
/&gt;&lt;/p&gt;
&lt;p&gt;In practice the facade can be an ingress with weighted routing, an API gateway, or a service mesh. The mechanism matters less than the property: per-path routing plus per-path traffic weight plus instant fallback.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# /users is being migrated: 10% to the new service, 90% still to the monolith.
http:
  - match:
      - uri:
          prefix: /users
    route:
      - destination: {host: users-service}
        weight: 10
      - destination: {host: monolith}
        weight: 90
  - route: # default: everything else stays on the monolith
      - destination: {host: monolith}
        weight: 100
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;The migration ran as a loop, not a project plan with an end date. Each pass picked one seam, extracted it, shifted traffic, verified, and cleaned up.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0003-monolith-to-kubernetes-strangler-migration/extraction-sequence.drawio&quot;
  title=&quot;Extraction sequence&quot;
  caption=&quot;Pick a loosely-coupled seam with a clear data owner, build it with its own store, route to it gradually, verify parity, remove it from the monolith, and repeat until the monolith is small enough.&quot;
  height={480}
/&gt;&lt;/p&gt;
&lt;Steps&gt;&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Containerize the monolith first.&lt;/strong&gt; Before extracting anything, get the monolith itself running in EKS behind the facade. Now old and new live in the same place, and the facade is the only thing in front.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pick a loosely-coupled seam.&lt;/strong&gt; Choose a capability with a clear boundary and a data set it mostly owns, for example users or billing. Avoid the tangled core on the first pass; early wins build trust.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build the service with its own data.&lt;/strong&gt; Give the extracted service its own database rather than pointing it at the monolith&amp;#39;s schema. Backfill and keep it in sync during the transition, but the target is independent ownership.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Route to it gradually.&lt;/strong&gt; Add the path to the facade and shift a small slice of traffic, then widen. Watch latency and error rates during each step, and keep the monolith path warm as a fallback.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verify parity, then delete.&lt;/strong&gt; Once the new service matches the monolith&amp;#39;s behavior under real traffic, remove that code from the monolith. Deleting the old path is what makes the win permanent.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repeat, and know when to stop.&lt;/strong&gt; Move to the next seam. Stop when what remains is small and stable enough that extracting it would cost more than it returns.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/Steps&gt;&lt;Notice type=&quot;warning&quot;&gt;
  The most dangerous shortcut is pointing a new service at the monolith&apos;s
  database so you can &quot;extract later.&quot; That gives you two services coupled
  through one schema, which is a distributed monolith: all of the network
  overhead, none of the independence. Give the service its own data, even if
  that means a sync period during the transition.
&lt;/Notice&gt;&lt;p&gt;Data is the genuinely hard part, and it is worth being honest about that. Moving stateless request handling is straightforward; moving the data it owns without downtime is not. The workable approach is to give the new service its own store, backfill it, keep it in sync while both paths run, and cut the monolith&amp;#39;s write path over only once the new service is authoritative and verified. Where strict consistency is required during the overlap, treat the monolith as the source of truth until the very last step.&lt;/p&gt;
&lt;Notice type=&quot;tip&quot;&gt;
  Measure the migration by how much of the monolith is gone, not by how many
  services exist. A useful signal is the share of production traffic served by
  extracted services and the amount of code deleted from the monolith. Services
  created without code removed from the original is motion without progress.
&lt;/Notice&gt;&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;The application moved onto EKS without a single cutover event and without a feature freeze. Because each route shifted gradually with a live fallback, no migration step was a high-stakes moment; the riskiest change only ever touched a small slice of one path at a time. Independent deploys arrived for each extracted service, so teams stopped blocking each other, and the hot paths could scale on their own instead of forcing the whole application to scale with them.&lt;/p&gt;
&lt;p&gt;The migration also did not finish in the storybook sense, and that was the right outcome. A stable, low-change remainder of the monolith stayed in place, containerized and behind the facade, because extracting it would have cost more than it returned. Treat &amp;quot;the monolith is gone&amp;quot; as a possible ending, not the goal.&lt;/p&gt;
&lt;h2&gt;Lessons&lt;/h2&gt;
&lt;p&gt;The facade is the whole safety story. Because every request always had a valid destination and an instant fallback, no step was irreversible. That single property is what let the team move quickly instead of cautiously.&lt;/p&gt;
&lt;p&gt;Extract the easy seams first. The instinct to start with the messy core is a trap. Early, low-risk extractions build the tooling and the team&amp;#39;s confidence, so the hard ones later are routine instead of terrifying.&lt;/p&gt;
&lt;p&gt;Data ownership is the real migration. The service boundary is easy; the data boundary is the work. Any plan that hand-waves the database is a plan to build a distributed monolith.&lt;/p&gt;
&lt;p&gt;Give yourself permission to stop. The goal was never zero monolith. It was independent, deployable, scalable services for the parts that needed it, and a small stable remainder for the parts that did not.&lt;/p&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;Accordion client:load title=&quot;What exactly is the strangler-fig pattern?&quot;&gt;&lt;p&gt;It is an incremental migration approach where a new system grows around an old one and takes over its responsibilities one at a time, until the old system is replaced or reduced to a small remainder. A routing facade sits in front and directs each request to either the new component or the old one, so both run in production together and you never need a single cutover.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;Why not just rewrite and cut over on a weekend?&quot;&gt;&lt;p&gt;Because a cutover is a single high-stakes event with no safe rollback. You freeze features to build the replacement, it drifts from the original as the original keeps changing, and if anything breaks on migration night your only option is reverting everything at once. Strangler-fig keeps the old system live the whole time, so every step is small and reversible.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;What makes a good first service to extract?&quot;&gt;&lt;p&gt;Low coupling and a clear data owner. Pick a capability with a clean boundary that mostly owns its own data, like users or billing, so you are not untangling shared state on your first attempt. Early, low-risk wins build the tooling and the confidence you will need for the harder seams later.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;How do you handle the shared database?&quot;&gt;&lt;p&gt;Give each extracted service its own store rather than pointing it at the monolith&amp;#39;s schema. Backfill it and keep it in sync while both paths run, then cut the monolith&amp;#39;s write path over only once the new service is authoritative and verified. Sharing one database across services is a distributed monolith and defeats the point of the migration.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;How do you know when the migration is done?&quot;&gt;&lt;p&gt;When the remaining monolith is small and stable enough that extracting more would cost more than it returns. Track the share of production traffic served by extracted services and the amount of code deleted from the monolith. Done does not have to mean zero monolith; a low-change remainder behind the facade is a perfectly good ending.&lt;/p&gt;
&lt;/Accordion&gt;&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/bliki/StranglerFigApplication.html&quot;&gt;Martin Fowler: StranglerFigApplication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html&quot;&gt;Amazon EKS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-decomposing-monoliths/strangler-fig.html&quot;&gt;AWS Prescriptive Guidance: strangler fig pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/ingress/&quot;&gt;Kubernetes Ingress&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://microservices.io/patterns/data/database-per-service.html&quot;&gt;Database decomposition patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>DevOps</category><category>Cloud Native</category><category>Architecture</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/case-studies/0003-monolith-to-kubernetes-strangler-migration/hero.png" length="0" type="image/jpeg"/></item><item><title>Cutting a SaaS AWS Bill 41% Without Slowing Delivery</title><link>https://mkabumattar.com/case-studies/post/aws-cost-optimization-saas-case-study/</link><guid isPermaLink="true">https://mkabumattar.com/case-studies/post/aws-cost-optimization-saas-case-study/</guid><description>A FinOps case study on a SaaS running on EKS with full GitOps and progressive delivery: how tagging, right-sizing node groups, and Savings Plans matched to the roadmap cut the AWS bill without freezing feature work.</description><pubDate>Sun, 19 Jul 2026 12:29:14 GMT</pubDate><content:encoded>&lt;p&gt;A growing SaaS ran on EKS with a full GitOps pipeline, and it was over its AWS budget nearly every month. The reflex from leadership was the usual one: freeze features until the bill comes down. That would have worked, and it would have been the wrong call. Freezing delivery to save money trades a problem you can measure for one you can&amp;#39;t. This is how the bill came down by roughly 41% over two quarters while the team kept shipping through a 25% canary rollout on every release, and why almost none of the win came from turning things off in a panic.&lt;/p&gt;
&lt;Notice type=&quot;note&quot;&gt;
  The percentages here are representative of what this pattern achieves, not a
  single audited client figure. The AWS and Kubernetes mechanics (tagging, Cost
  Categories, Budgets, Anomaly Detection, Savings Plans, EKS node groups,
  Karpenter, Argo CD, Argo Rollouts) are exactly as described. Your real savings
  depend on how much waste you start with and how much of your compute is
  commitment-eligible.
&lt;/Notice&gt;&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;The bill came down without a feature freeze. Over two quarters the monthly AWS spend dropped by roughly 41% while the team kept shipping through the same 25% canary it uses for any feature.&lt;/p&gt;
&lt;p&gt;None of it came from a panic switch-off. Every lever was reversible and canary-guarded, so the savings held without trading away reliability or delivery speed, and cost turned into a normal signal that shows up in pull requests instead of a quarterly fire drill.&lt;/p&gt;
&lt;div class=&quot;not-prose my-8 flex flex-wrap gap-x-10 gap-y-6&quot;&gt;
  &lt;StatCounter
    client:load
    value={41}
    suffix=&quot;%&quot;
    label=&quot;lower monthly AWS bill&quot;
  /&gt;
  &lt;StatCounter client:load value={2} label=&quot;quarters, zero delivery pauses&quot; /&gt;
  &lt;StatCounter
    client:load
    value={25}
    suffix=&quot;%&quot;
    label=&quot;canary on every cost change&quot;
  /&gt;
&lt;/div&gt;&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;The bill was growing faster than revenue, which is the signal that matters. Nobody could say where the money went, because nothing was labeled. A single line item for &amp;quot;EC2&amp;quot; across a dozen teams and six node groups tells you nothing you can act on. When finance asked engineering to explain a spike, the honest answer was &amp;quot;we&amp;#39;re not sure,&amp;quot; and that answer is what turns a cost conversation into a feature freeze.&lt;/p&gt;
&lt;p&gt;There was also a dashboard, and everyone pointed at it as proof they were &amp;quot;doing FinOps.&amp;quot; A dashboard shows you the number. It does not change anyone&amp;#39;s behavior, and it definitely does not tell an engineer that the node group they oversized last sprint is the reason the graph bent upward. Visibility without attribution is just a prettier version of not knowing.&lt;/p&gt;
&lt;p&gt;The last piece was fear. Every proposed saving came with &amp;quot;will this break production?&amp;quot; and without data nobody could answer, so nothing happened. The goal was to make cost a normal, reversible engineering decision instead of a quarterly emergency, and to do it without touching the delivery pipeline the team depended on.&lt;/p&gt;
&lt;h2&gt;Constraints&lt;/h2&gt;
&lt;p&gt;A few limits shaped the whole approach.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No feature freeze.&lt;/strong&gt; Delivery velocity was the business. Any optimization that slowed shipping was off the table.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No reliability regressions.&lt;/strong&gt; Saving money by removing redundancy or headroom was not a real saving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep the delivery model.&lt;/strong&gt; Full GitOps, dev to staging to production promotion, and 25% canary rollouts all had to stay exactly as they were.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data stays isolated.&lt;/strong&gt; The data tier runs in subnets with no internet route, and that boundary was non-negotiable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respect confidentiality.&lt;/strong&gt; Real dollar figures stay private, so success is reported as percentages and unit economics.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The SaaS runs entirely on a single EKS cluster per environment, across three availability zones. Each VPC has three tiers of subnets: three public subnets for ingress and NAT, three private subnets for the EKS worker nodes, and three isolated subnets with no internet route for the data tier. Everything the product needs runs in the cluster.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0002-aws-cost-optimization-saas-case-study/aws-architecture.drawio&quot;
  title=&quot;EKS platform architecture&quot;
  caption=&quot;Three AZs, three subnet tiers (public, private, isolated). EKS node groups run the app and platform; the data tier sits in isolated subnets with no internet route.&quot;
  height={700}
/&gt;&lt;/p&gt;
&lt;p&gt;Compute is split into purpose-built managed node groups rather than one big pool, which is what makes both scheduling and cost control tractable. There are separate node groups for the frontend, the backend, data and ETL work, observability, the GitOps controllers, and the internal dashboards, plus a Spot-backed group for batch and preview workloads. Karpenter handles just-in-time node provisioning on top, so capacity follows demand instead of sitting idle.&lt;/p&gt;
&lt;p&gt;The whole platform toolchain lives in the cluster, scheduled onto those node groups: Keycloak for single sign-on across the dashboards, Argo CD, and Grafana; SonarQube and Trivy in the delivery path; Argo CD and Argo Rollouts for GitOps and progressive delivery; and Prometheus with Grafana for observability.&lt;/p&gt;
&lt;Notice type=&quot;warning&quot;&gt;
  The data tier (RDS with a multi-AZ standby, plus ElastiCache) lives in the
  isolated subnets. Those subnets have no NAT and no internet gateway route, so
  the databases cannot reach the internet and the internet cannot reach them.
  Nodes talk to them only over private VPC routes.
&lt;/Notice&gt;&lt;p&gt;Cost work fails when it lives only in a finance spreadsheet, so the first move was to make spend attributable. A small, enforced tag taxonomy flows into AWS Cost Categories, which maps raw line items to teams and products, and from there into Budgets, Cost Anomaly Detection, and per-team showback.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0002-aws-cost-optimization-saas-case-study/cost-attribution-flow.drawio&quot;
  title=&quot;Cost attribution and guardrail flow&quot;
  caption=&quot;Tagged resources feed Cost Categories, which drives Budgets, Anomaly Detection, and per-team showback.&quot;
  height={440}
/&gt;&lt;/p&gt;
&lt;p&gt;The taxonomy was deliberately small. Five mandatory tags, not twenty, because a taxonomy nobody follows is worse than none. &lt;code&gt;Environment&lt;/code&gt;, &lt;code&gt;CostCenter&lt;/code&gt;, &lt;code&gt;Application&lt;/code&gt;, and &lt;code&gt;Owner&lt;/code&gt; covered almost every question we needed to answer, and &lt;code&gt;ManagedBy&lt;/code&gt; flagged anything created by hand instead of through code. On EKS those tags also propagate to node groups and volumes, so cluster compute is attributable per team, not lumped under one anonymous bill.&lt;/p&gt;
&lt;p&gt;Enforcement matters more than intent, so the tags were governed centrally. AWS Organizations Tag Policies defined the allowed keys and values, Service Control Policies blocked non-compliant resources, and consolidated billing plus the Cost and Usage Report gave one clean view across every account.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0002-aws-cost-optimization-saas-case-study/guardrail-architecture.drawio&quot;
  title=&quot;Governance and reporting topology&quot;
  caption=&quot;Organizations enforces tag policies and SCPs across member accounts; billing data lands in one place for dashboards and alerts.&quot;
  height={480}
/&gt;&lt;/p&gt;
&lt;Notice type=&quot;tip&quot;&gt;
  Chargeback is tempting, but it needs near-perfect tagging and it starts turf
  wars early. We started with showback: show each team its own spend, let
  central finance keep paying the bill, and move to chargeback only once the
  tags were trustworthy. Awareness drove most of the savings before any money
  changed hands internally.
&lt;/Notice&gt;&lt;h2&gt;Delivery: GitOps and progressive rollout&lt;/h2&gt;
&lt;p&gt;None of the cost work was allowed to disturb delivery, so it helps to see what delivery looks like. Every change runs through the same GitOps pipeline. CI builds and tests, SonarQube enforces a quality gate, and Trivy scans the image and the IaC. Only a clean build pushes to ECR and bumps the image digest in the GitOps manifests repository. Argo CD notices the change and syncs it to the cluster.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0002-aws-cost-optimization-saas-case-study/gitops-pipeline.drawio&quot;
  title=&quot;GitOps delivery pipeline&quot;
  caption=&quot;Build, SonarQube gate, Trivy scan, push to ECR, bump the manifest digest, then Argo CD syncs and Argo Rollouts runs the canary with automated analysis and rollback.&quot;
  height={520}
/&gt;&lt;/p&gt;
&lt;p&gt;Nothing goes fully live at once. Argo Rollouts takes over at the cluster and shifts traffic in steps, starting at 25%, then pausing to check analysis metrics before it widens. If the metrics stay healthy it promotes; if they degrade it rolls back on its own, with no human in the loop. That single behavior is what let the cost changes ship safely, because a right-sized deployment that misbehaved would be caught at 25% of traffic, not 100%.&lt;/p&gt;
&lt;p&gt;Environments follow the same path every time. A feature or preview environment spins up per pull request on the Spot-backed node group, merges auto-deploy to dev, the same image digest promotes to staging for integration tests, and only then does it reach production behind the canary.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0002-aws-cost-optimization-saas-case-study/environments-promotion.drawio&quot;
  title=&quot;Environments and promotion&quot;
  caption=&quot;Feature envs to dev to staging to production, promoting the same image digest, with a 25% canary before production goes wide.&quot;
  height={420}
/&gt;&lt;/p&gt;
&lt;p&gt;The canary itself is a few lines of Argo Rollouts config, and it is the same for a feature change or a cost change.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: frontend
spec:
  strategy:
    canary:
      steps:
        - setWeight: 25
        - pause: {duration: 10m}
        - setWeight: 50
        - pause: {duration: 10m}
        - setWeight: 100
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;The baseline was code. Rather than tag resources by hand, every provider inherited a default set of tags, so new infrastructure was attributable from the moment it existed.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;provider &amp;quot;aws&amp;quot; {
  region = &amp;quot;us-east-1&amp;quot;

  default_tags {
    tags = {
      Environment = &amp;quot;Prod&amp;quot;
      CostCenter  = &amp;quot;1001&amp;quot;
      ManagedBy   = &amp;quot;Terraform&amp;quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then came the guardrails, automated so nobody had to remember to check a dashboard. A monthly budget with a forecast alert catches planned overspend before the month ends, and Cost Anomaly Detection catches the surprise 3am spike.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;aws budgets create-budget \
  --account-id 111122223333 \
  --budget &amp;#39;{
    &amp;quot;BudgetName&amp;quot;: &amp;quot;MonthlyCost&amp;quot;,
    &amp;quot;BudgetLimit&amp;quot;: { &amp;quot;Amount&amp;quot;: &amp;quot;50000&amp;quot;, &amp;quot;Unit&amp;quot;: &amp;quot;USD&amp;quot; },
    &amp;quot;TimeUnit&amp;quot;: &amp;quot;MONTHLY&amp;quot;,
    &amp;quot;BudgetType&amp;quot;: &amp;quot;COST&amp;quot;
  }&amp;#39;

aws ce create-anomaly-monitor \
  --anomaly-monitor &amp;#39;{
    &amp;quot;MonitorName&amp;quot;: &amp;quot;CoreServices&amp;quot;,
    &amp;quot;MonitorType&amp;quot;: &amp;quot;DIMENSIONAL&amp;quot;,
    &amp;quot;MonitorDimension&amp;quot;: &amp;quot;SERVICE&amp;quot;
  }&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With attribution and guardrails in place, the actual optimization ran as normal GitOps changes, each behind the canary, each with a one-commit rollback.&lt;/p&gt;
&lt;Steps&gt;&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Find the waste.&lt;/strong&gt; Use Cost Explorer grouped by the new tags, plus Kubernetes right-sizing signals from the metrics stack, to rank the most over-provisioned node groups and workloads.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Right-size node groups in reversible steps.&lt;/strong&gt; Drop one instance size or one replica at a time, ship it through the 25% canary, and watch the SLOs. The old manifest is one revert away.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Let Karpenter consolidate.&lt;/strong&gt; Enable consolidation so underused nodes are drained and replaced with fewer, better-packed ones, and move interruptible and preview work to the Spot node group.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Put dev and staging to sleep.&lt;/strong&gt; Scale non-production node groups to zero overnight and on weekends. Nothing runs when nobody is working.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Commit last, not first.&lt;/strong&gt; Only after cluster usage was stable did we buy Compute Savings Plans, so we committed to real baseline usage rather than to waste.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/Steps&gt;&lt;p&gt;The commitment step is where teams most often lose money, by chasing the deepest discount for a workload they are about to change. The rule we used was simple: match the commitment to the roadmap, not to the current instance.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0002-aws-cost-optimization-saas-case-study/savings-plan-decision.drawio&quot;
  title=&quot;Choosing the right commitment&quot;
  caption=&quot;Interruptible goes to Spot; anything you plan to modernize takes a flexible Compute Savings Plan; only a truly stable workload earns the deepest locked-in discount.&quot;
  height={620}
/&gt;&lt;/p&gt;
&lt;p&gt;The team was midway through moving several backend services to Graviton for better price-performance. A three-year EC2 Instance Savings Plan on the old family would have looked cheaper on paper and then stranded the moment those services migrated. A Compute Savings Plan gave up a few points of discount but stayed flexible across families, regions, Fargate, and Lambda, which matters even more on EKS where node groups change shape often. That small premium was cheap insurance.&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;Over two quarters, the monthly bill came down by roughly 41%, and delivery never paused. Every cost change went out through the same 25% canary as any feature. The breakdown, as representative shares of the total reduction, looked like this.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;left&quot;&gt;Lever&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Share of the saving&lt;/th&gt;
&lt;th align=&quot;left&quot;&gt;Nature of the change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Right-sizing node groups + Karpenter consolidation&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Largest&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Reversible, canary-guarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Scheduling dev and staging to sleep&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Large&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Fully reversible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Compute Savings Plans matched to the roadmap&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Meaningful&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;1-year, flexible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Spot for batch, preview, and CI&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Meaningful&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Interruption-tolerant only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;left&quot;&gt;Storage cleanup and lifecycle policies&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;Smaller&lt;/td&gt;
&lt;td align=&quot;left&quot;&gt;One-time plus ongoing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The more durable result was cultural. Cost stopped being a quarterly fire drill. Teams could see their own node-group spend, cost showed up in pull requests as a normal signal, and the &amp;quot;will this break?&amp;quot; fear faded because every change had a canary and a rollback. Treat the percentage as illustrative and the mechanics as the real deliverable.&lt;/p&gt;
&lt;h2&gt;Lessons&lt;/h2&gt;
&lt;p&gt;Attribution is the whole game. Nothing else worked until spend had an owner, because you cannot optimize a shared cluster you cannot see per team. The five-tag taxonomy, enforced in code and propagated to node groups, paid for itself before a single node was resized.&lt;/p&gt;
&lt;p&gt;Progressive delivery is what makes cost work safe. On a normal deploy model, right-sizing production feels risky enough that teams avoid it. With a 25% canary and automated rollback, a bad resize is a non-event, so the team actually did the work instead of flinching.&lt;/p&gt;
&lt;p&gt;Commit to usage, not to hope. The most expensive mistake in cloud cost work is a long, rigid commitment bought early to chase a headline discount. Buy commitments after usage is stable, prefer flexibility while the architecture is still moving, and treat the discount rate as secondary to not stranding the plan.&lt;/p&gt;
&lt;p&gt;If I did it again, I would wire a pull-request cost estimate in on day one. Putting the number in front of the engineer at the moment they change a manifest moved behavior more than any dashboard did.&lt;/p&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;Accordion client:load title=&quot;Why not just freeze features until the bill comes down?&quot;&gt;&lt;p&gt;A freeze trades a measurable problem for an unmeasurable one. You save some money and lose delivery velocity, customer momentum, and team morale, none of which show up cleanly on the bill. Almost all of the saving here came from waste and mismatched commitments, not from doing less, so the freeze would have hurt the business while barely touching the real cost drivers.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;How do you right-size EKS node groups without causing incidents?&quot;&gt;&lt;p&gt;Treat it like any other change. Drop one size or one replica at a time, ship it through the same 25% Argo Rollouts canary as a feature, and watch the SLOs during the pause windows. Let Karpenter consolidate underused nodes rather than doing it by hand. Because every step is a GitOps commit, the rollback is a one-line revert, so a bad resize is caught at 25% of traffic and reverted, not discovered in a postmortem.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;Why start the canary at 25% instead of a smaller slice?&quot;&gt;&lt;p&gt;Twenty-five percent is a deliberate balance. It is a big enough slice that real traffic patterns and enough metric volume show up quickly, so the analysis step can make an honest call, but small enough that a bad release only touches a quarter of users before it rolls back. Smaller first steps are reasonable for very high-risk changes, but 25% gave this team fast, trustworthy signal without much blast radius.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;How does the isolated data tier stay reachable if it has no internet?&quot;&gt;&lt;p&gt;The isolated subnets have no NAT and no internet gateway route, so the databases cannot reach the internet and vice versa. The application nodes in the private subnets reach RDS and ElastiCache over private VPC routes only. Anything the data tier genuinely needs from an AWS service goes through VPC endpoints, which keep that traffic on the AWS network rather than the public internet.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;Does the GitOps and canary setup make cost work harder?&quot;&gt;&lt;p&gt;It makes it safer, which in practice makes it happen. Every cost change is a normal pull request that flows dev to staging to production behind the canary, with SonarQube and Trivy gates on the way. There is no separate risky &amp;quot;cost project,&amp;quot; just ordinary changes with the same guardrails as everything else, so teams approve them quickly.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;What is the single highest-impact first step?&quot;&gt;&lt;p&gt;Enforced tagging. Until spend is attributable per team, product, environment, and node group, every other optimization is guesswork. A small mandatory taxonomy, applied through Terraform default tags and AWS Organizations tag policies, turns the bill from one opaque number into a map you can act on.&lt;/p&gt;
&lt;/Accordion&gt;&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html&quot;&gt;Amazon EKS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://karpenter.sh/&quot;&gt;Karpenter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/&quot;&gt;Argo CD&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://argo-rollouts.readthedocs.io/&quot;&gt;Argo Rollouts (progressive delivery)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@anandctx/argocd-image-rollouts-a9d91943195d&quot;&gt;Argo CD image rollouts walkthrough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.keycloak.org/documentation&quot;&gt;Keycloak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sonarsource.com/sonarqube-server/latest/&quot;&gt;SonarQube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://trivy.dev/&quot;&gt;Trivy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/cost-management/latest/userguide/manage-cost-categories.html&quot;&gt;AWS Cost Categories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html&quot;&gt;AWS Budgets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/cost-management/latest/userguide/manage-ad.html&quot;&gt;AWS Cost Anomaly Detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/savingsplans/latest/userguide/&quot;&gt;AWS Savings Plans&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags&quot;&gt;Terraform AWS provider default_tags&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Cloud Computing</category><category>DevOps</category><category>Cloud Native</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/case-studies/0002-aws-cost-optimization-saas-case-study/hero.png" length="0" type="image/jpeg"/></item><item><title>QuenchWorks: Building a 0-CVE Container Image and Helm Chart Catalog</title><link>https://mkabumattar.com/case-studies/post/quenchworks-zero-cve-catalog/</link><guid isPermaLink="true">https://mkabumattar.com/case-studies/post/quenchworks-zero-cve-catalog/</guid><description>How a from-scratch catalog replaced Bitnami with 150+ container images and 120+ Helm charts built from source on Wolfi, gated to zero fixable CVEs, signed, and pinned by digest.</description><pubDate>Wed, 15 Jul 2026 09:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When Bitnami moved its long-trusted catalog behind a paid tier, thousands of teams woke up to a supply-chain problem they didn&amp;#39;t choose. The free images they had pinned in production would stop getting updates, and the migration clock started that morning. QuenchWorks is my answer to that: a from-scratch, security-first catalog of container images and Helm charts, built entirely from source, hardened under a strict zero-CVE build gate, signed, and free. This is how it&amp;#39;s put together and why each decision earns its place.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;QuenchWorks is real and in production use, not a proof of concept. It replaced Bitnami for common workloads with a catalog that is built from source, provable, and free.&lt;/p&gt;
&lt;p&gt;Everything below is verifiable: pull any image and check its signature, SBOM, and provenance yourself. The build gate stays green because the base is small enough that there is almost nothing to be vulnerable in, so the numbers hold instead of drifting the week after launch.&lt;/p&gt;
&lt;div class=&quot;not-prose my-8 flex flex-wrap gap-x-10 gap-y-6&quot;&gt;
  &lt;StatCounter
    client:load
    value={150}
    suffix=&quot;+&quot;
    label=&quot;images built 0-CVE from source&quot;
  /&gt;
  &lt;StatCounter
    client:load
    value={120}
    suffix=&quot;+&quot;
    label=&quot;production Helm charts&quot;
  /&gt;
  &lt;StatCounter
    client:load
    value={100}
    suffix=&quot;%&quot;
    label=&quot;cosign-signed, SBOM + provenance&quot;
  /&gt;
&lt;/div&gt;&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;The Bitnami catalog was popular for good reasons. It was broad, it was versioned, and it was maintained well enough that most teams never thought about it. Its weaknesses only became obvious once access changed: you didn&amp;#39;t control the build, you couldn&amp;#39;t prove what was inside a given image, and continued free access was never actually guaranteed.&lt;/p&gt;
&lt;p&gt;That last point is the one that bites. When an upstream catalog changes its terms, every &lt;code&gt;image:&lt;/code&gt; line you pinned becomes a liability at once. You either pay, fork, or scramble. And even before that day comes, an opaque image is its own quiet risk. If you can&amp;#39;t see how a layer was produced, you can&amp;#39;t reason about what a scanner finds inside it, and you can&amp;#39;t answer a security review with anything better than &amp;quot;we trust the vendor.&amp;quot;&lt;/p&gt;
&lt;p&gt;Most hardened-image alternatives fix one slice of this and charge for the rest. I wanted the whole thing: a catalog broad enough to actually replace Bitnami for common workloads, provable rather than &amp;quot;trust us,&amp;quot; and free with no pull limits and no lock-in. If it couldn&amp;#39;t be all three, it wasn&amp;#39;t worth building.&lt;/p&gt;
&lt;h2&gt;Constraints&lt;/h2&gt;
&lt;p&gt;A handful of hard limits shaped every later decision.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Zero fixable CVEs, enforced by the build.&lt;/strong&gt; Not a nightly report someone reads later. A gate that fails the build so a vulnerable image never ships in the first place.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built from source.&lt;/strong&gt; No repackaging of someone else&amp;#39;s opaque binary layers. If it&amp;#39;s in the image, we produced it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provable.&lt;/strong&gt; Every image needs a bill of materials and build provenance that a consumer can verify without trusting me.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free to run and maintain.&lt;/strong&gt; The whole system builds on free CI, so cost can never be the reason it slips behind a paywall later.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-arch.&lt;/strong&gt; amd64 and arm64, because production is both now, not one or the other.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those constraints pull against each other. Zero fixable CVEs across 150+ images sounds impossible if you picture a fat base image. Building everything from source sounds slow. The architecture is what makes them coexist.&lt;/p&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;The catalog is a pipeline, not a pile of Dockerfiles. Each image is declared as an &lt;code&gt;apko&lt;/code&gt; plus &lt;code&gt;melange&lt;/code&gt; spec, built from source on Wolfi, scanned against a zero-fixable-CVE gate, signed, and only then published pinned by digest. Charts sit on a shared library chart and reference those images by digest.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0001-quenchworks-zero-cve-catalog/architecture.drawio&quot;
  title=&quot;QuenchWorks build pipeline&quot;
  caption=&quot;From declarative spec to signed, digest-pinned image and chart, with a nightly rescan self-heal loop.&quot;
  height={640}
/&gt;&lt;/p&gt;
&lt;p&gt;The single decision that makes the zero-CVE gate realistic is the base. QuenchWorks builds on &lt;strong&gt;Wolfi&lt;/strong&gt;, a glibc Linux undistro designed for containers. Most images start with no shell, no package manager, and a tiny set of packages. There&amp;#39;s simply very little in the image that can be vulnerable, so keeping the gate green is a fight you can actually win instead of an endless race against a bloated base.&lt;/p&gt;
&lt;p&gt;The image itself is assembled declaratively. &lt;code&gt;melange&lt;/code&gt; builds signed APK packages from source, and &lt;code&gt;apko&lt;/code&gt; composes those packages plus the Wolfi base into an OCI image with no Dockerfile involved. Because the whole thing is declared, the contents are known, reproducible, and easy to record as a bill of materials.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0001-quenchworks-zero-cve-catalog/image-composition.drawio&quot;
  title=&quot;Image composition with melange and apko&quot;
  caption=&quot;melange builds APKs from source; apko assembles them with the Wolfi base into a nonroot, multi-arch image and an SBOM.&quot;
  height={420}
/&gt;&lt;/p&gt;
&lt;Notice type=&quot;tip&quot;&gt;
  The zero-CVE gate and the minimal base are the same decision viewed twice. You
  don&apos;t reach zero fixable CVEs by patching harder. You reach it by shipping so
  little that there&apos;s almost nothing to patch.
&lt;/Notice&gt;&lt;p&gt;A catalog is never done, though, because CVEs are disclosed against packages long after an image ships. So the pipeline runs in reverse on a schedule. A nightly Trivy rescan checks every published image, and when a fix lands upstream the affected image rebuilds, re-enters the gate, gets re-signed, and republishes under a new digest. The catalog trends toward zero drift without anyone babysitting it.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0001-quenchworks-zero-cve-catalog/self-heal-loop.drawio&quot;
  title=&quot;Nightly rescan and self-heal loop&quot;
  caption=&quot;Published images are rescanned nightly; a new fixable CVE triggers an automatic rebuild, re-gate, and re-sign.&quot;
  height={420}
/&gt;&lt;/p&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;Each image is a pair of specs. &lt;code&gt;melange&lt;/code&gt; describes how to build the package from source, and &lt;code&gt;apko&lt;/code&gt; describes how to assemble the final image. Here&amp;#39;s the shape of both, trimmed for clarity.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;package:
  name: my-app
  version: 1.2.3
environment:
  contents:
    packages:
      - build-base
pipeline:
  - uses: fetch
    with:
      uri: https://example.com/my-app-${{package.version}}.tar.gz
      expected-sha256: &amp;#39;...&amp;#39;
  - uses: autoconf/configure
  - uses: autoconf/make
  - uses: autoconf/make-install
&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;contents:
  repositories:
    - https://packages.wolfi.dev/os
  packages:
    - my-app
    - ca-certificates-bundle
accounts:
  users:
    - username: nonroot
      uid: 65532
  run-as: 65532
archs:
  - x86_64
  - aarch64
entrypoint:
  command: /usr/bin/my-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The gate is one Trivy call, and it&amp;#39;s deliberately strict about what counts. It only fails on CVEs that have a fix available, because a vulnerability with no upstream patch isn&amp;#39;t something a rebuild can clear. Everything fixable has to be at zero before the image is allowed out.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Fail the build if any FIXABLE HIGH/CRITICAL vulnerability is present
trivy image --ignore-unfixed --severity HIGH,CRITICAL \
  --exit-code 1 ghcr.io/quenchworks/my-app:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once an image passes, it gets signed and attested before it&amp;#39;s pushed for real. Signing is keyless with Cosign, so there&amp;#39;s no long-lived private key to leak, and the SBOM and SLSA provenance ride along as attestations.&lt;/p&gt;
&lt;Steps&gt;&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Build from source.&lt;/strong&gt; &lt;code&gt;melange&lt;/code&gt; produces signed APKs; &lt;code&gt;apko&lt;/code&gt; assembles the image with the Wolfi base, nonroot user, and read-only root filesystem defaults.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gate on zero fixable CVEs.&lt;/strong&gt; Trivy runs in comprehensive mode. One fixable HIGH or CRITICAL fails the pipeline, so a vulnerable image never reaches the registry.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sign and attest.&lt;/strong&gt; Cosign signs the image keyless, then attaches an SPDX SBOM and a SLSA build-provenance attestation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Publish pinned by digest.&lt;/strong&gt; The image is pushed, and the Helm charts reference it by &lt;code&gt;sha256:&lt;/code&gt; digest, never by a movable tag.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rescan nightly.&lt;/strong&gt; A scheduled Trivy run watches for new fixes and triggers the self-heal rebuild loop.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/Steps&gt;&lt;p&gt;On the delivery side, charts are the second half of the story. Every chart builds on a shared &lt;code&gt;quench-common&lt;/code&gt; library chart, so common concerns like security context, probes, and labels live in one place instead of being copy-pasted 120 times. Each chart pins its image by digest.&lt;/p&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0001-quenchworks-zero-cve-catalog/chart-topology.drawio&quot;
  title=&quot;Chart topology&quot;
  caption=&quot;App charts depend on the quench-common library chart and reference images by digest, which Helm then installs into the cluster.&quot;
  height={480}
/&gt;&lt;/p&gt;
&lt;p&gt;Pinning by digest instead of tag is what makes the catalog trustworthy in practice. A tag can be moved; a digest can&amp;#39;t. When a consumer pins a QuenchWorks chart, they get exactly the bytes that passed the gate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;image:
  repository: ghcr.io/quenchworks/postgresql
  # Pinned by digest, not tag. This is the exact image that passed the gate.
  digest: &amp;#39;sha256:abc123...&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The proof only matters if consumers can check it, so verification is a first-class step, not an afterthought. Anyone can verify an image&amp;#39;s signature and attestations before it runs, and an admission policy can enforce that in the cluster so unsigned or unverifiable images never schedule.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cosign verify \
  --certificate-identity-regexp &amp;#39;^https://github.com/quenchworks/&amp;#39; \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/quenchworks/postgresql@sha256:abc123...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;DrawIO
  client:load
  src=&quot;/assets/case-studies/0001-quenchworks-zero-cve-catalog/verification-flow.drawio&quot;
  title=&quot;Consumer verification workflow&quot;
  caption=&quot;A consumer pulls by digest, verifies the keyless signature and the SBOM/SLSA attestations, and only then does admission allow the deploy.&quot;
  height={640}
/&gt;&lt;/p&gt;
&lt;h2&gt;Results&lt;/h2&gt;
&lt;p&gt;The catalog is real and in use, not a proof of concept.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;150+ container images&lt;/strong&gt; built from source on Wolfi, each gated to zero fixable CVEs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;120+ production Helm charts&lt;/strong&gt; on the shared &lt;code&gt;quench-common&lt;/code&gt; library, every one pinned to its image by digest.&lt;/li&gt;
&lt;li&gt;Every image &lt;strong&gt;cosign-signed&lt;/strong&gt; with an SPDX SBOM and SLSA provenance, published under an &lt;strong&gt;ArtifactHub verified-publisher&lt;/strong&gt; organization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-arch&lt;/strong&gt; (amd64 and arm64), nonroot, and read-only root filesystem by default.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;nightly rescan and self-heal rebuild loop&lt;/strong&gt; that keeps the catalog current as upstream ships fixes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free and independent.&lt;/strong&gt; No subscription, no registry pull limits, no lock-in.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The counts above are current catalog figures. Per-image build times and scan times vary by package, so I&amp;#39;d treat any single number there as indicative rather than a benchmark.&lt;/p&gt;
&lt;h2&gt;Lessons&lt;/h2&gt;
&lt;p&gt;The biggest lesson is that the base image choice decides everything downstream. Trying to reach zero CVEs on a fat base is a treadmill; starting from Wolfi&amp;#39;s minimal surface turns the gate into something you can keep green for months. If I&amp;#39;d started anywhere else, the self-heal loop would be firing constantly and the whole thing would feel like bailing water.&lt;/p&gt;
&lt;p&gt;The second lesson is that provenance costs far less than it&amp;#39;s worth. Signing and generating SBOMs added very little build time, but they change the catalog&amp;#39;s whole posture. It stops being &amp;quot;trust me&amp;quot; and becomes &amp;quot;verify it yourself,&amp;quot; which is the entire point of replacing an opaque upstream. If I were doing it again, I&amp;#39;d wire verification into the consumer docs even earlier, because an unverified signed image is only half the value.&lt;/p&gt;
&lt;p&gt;The one thing I&amp;#39;d watch more carefully next time is chart sprawl. The &lt;code&gt;quench-common&lt;/code&gt; library chart paid for itself immediately, but library conventions need to be locked down early. Once a few charts drift from the shared patterns, every future change gets more expensive.&lt;/p&gt;
&lt;h2&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;Accordion client:load title=&quot;How is &apos;zero CVE&apos; actually possible across 150+ images?&quot;&gt;&lt;p&gt;It&amp;#39;s zero &lt;em&gt;fixable&lt;/em&gt; CVEs, and it&amp;#39;s mostly a consequence of the base. Wolfi images ship with almost nothing beyond what the app needs, so there&amp;#39;s very little surface for a vulnerability to live in. The Trivy gate then fails any build with a fixable HIGH or CRITICAL, so a vulnerable image can&amp;#39;t ship. Vulnerabilities with no upstream fix are tracked but don&amp;#39;t block, because a rebuild can&amp;#39;t clear them.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;Why pin charts to images by digest instead of a tag?&quot;&gt;&lt;p&gt;A tag is a movable pointer; a digest is the content itself. If you pin &lt;code&gt;:latest&lt;/code&gt; or even &lt;code&gt;:1.2.3&lt;/code&gt;, the bytes behind that tag can change. Pinning &lt;code&gt;sha256:...&lt;/code&gt; guarantees you get exactly the image that passed the gate and was signed. It&amp;#39;s the difference between &amp;quot;probably the right image&amp;quot; and &amp;quot;provably the right image.&amp;quot;&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;How do I verify an image before running it?&quot;&gt;&lt;p&gt;Use &lt;code&gt;cosign verify&lt;/code&gt; with the QuenchWorks certificate identity and OIDC issuer, as shown above. That checks the keyless signature against the transparency log. You can also verify the SBOM and SLSA provenance attestations, and enforce all of it in-cluster with an admission policy so nothing unsigned ever schedules.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;What happens when a new CVE is disclosed after an image ships?&quot;&gt;&lt;p&gt;The nightly Trivy rescan catches it. If the CVE is fixable, the affected image rebuilds from source, goes back through the gate, gets re-signed, and republishes under a new digest. You pick up the fix by moving your pin to the new digest. Nobody has to notice the CVE manually for the loop to run.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;Is QuenchWorks really free, and what&apos;s the catch?&quot;&gt;&lt;p&gt;It&amp;#39;s free, with no subscription and no registry pull limits. The catch, if you call it one, is that you verify and pin things yourself rather than outsourcing trust to a vendor relationship. That&amp;#39;s a feature for most teams: you get provenance you can audit instead of a support contract you have to believe.&lt;/p&gt;
&lt;/Accordion&gt;&lt;Accordion client:load title=&quot;Can I use the charts without adopting the whole catalog?&quot;&gt;&lt;p&gt;Yes. The charts and images are independent. You can pull a single hardened image by digest, or install one chart, without buying into everything. The &lt;code&gt;quench-common&lt;/code&gt; library chart is an implementation detail of the charts, not something you have to adopt in your own repos.&lt;/p&gt;
&lt;/Accordion&gt;&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://quench-works.com/&quot;&gt;QuenchWorks catalog and website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/quenchworks&quot;&gt;QuenchWorks on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/wolfi-dev&quot;&gt;Wolfi undistro&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/chainguard-dev/apko&quot;&gt;apko&lt;/a&gt; and &lt;a href=&quot;https://github.com/chainguard-dev/melange&quot;&gt;melange&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://trivy.dev/&quot;&gt;Trivy vulnerability scanner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.sigstore.dev/&quot;&gt;Sigstore Cosign&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spdx.dev/&quot;&gt;SPDX&lt;/a&gt; and &lt;a href=&quot;https://slsa.dev/&quot;&gt;SLSA provenance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://artifacthub.io/&quot;&gt;ArtifactHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>Security</category><category>DevOps</category><category>Cloud Native</category><author>Mohammad Abu Mattar</author><enclosure url="https://mkabumattar.com/assets/case-studies/0001-quenchworks-zero-cve-catalog/hero.png" length="0" type="image/jpeg"/></item></channel></rss>