<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://lukemainframe.top/feed.xml" rel="self" type="application/atom+xml" /><link href="https://lukemainframe.top/" rel="alternate" type="text/html" /><updated>2026-10-12T17:33:05+00:00</updated><id>https://lukemainframe.top/feed.xml</id><title type="html">LukeMainFrame</title><subtitle>Knowledge Is Power</subtitle><entry><title type="html">Inside a Graphics Card: Architecture, Tensor Cores, and Defective Chips</title><link href="https://lukemainframe.top/2026/10/06/graphics-card-architecture/" rel="alternate" type="text/html" title="Inside a Graphics Card: Architecture, Tensor Cores, and Defective Chips" /><published>2026-10-06T00:00:00+00:00</published><updated>2026-10-06T00:00:00+00:00</updated><id>https://lukemainframe.top/2026/10/06/graphics-card-architecture</id><content type="html" xml:base="https://lukemainframe.top/2026/10/06/graphics-card-architecture/"><![CDATA[<p>How many calculations does your graphics card perform every second while running a modern game? Running Super Mario 64 back in 1996 required around 100 million calculations per second. Minecraft in 2011 needed something like 100 billion. A game like Cyberpunk 2077 needs a GPU capable of tens of trillions, and the current top consumer card, the <strong>RTX 5090</strong>, does about <strong>105 trillion</strong> per second.</p>

<p>To put that number in perspective: imagine every single person on Earth doing one long multiplication per second. To match an RTX 5090 you would need about <strong>12,800 Earths</strong> full of people, all working together without a break.</p>

<p>Everybody now knows that GPUs are also the engine behind the AI boom, but few people can explain <em>why</em> a graphics card is so much better than a CPU at this kind of work, what a “tensor core” actually is, or why an RTX 5090 is technically a “defective” chip. In this article we take a look at all of it: the design philosophy behind a GPU, what’s physically inside a graphics card, how work is actually executed on thousands of cores, what tensor cores do, and how NVIDIA (and basically every chip maker) turns imperfect silicon into a full product lineup. We’ll use NVIDIA’s current <strong>Blackwell</strong> generation (RTX 50 series) as the example throughout.</p>

<p>A lot of the material here is inspired by Branch Education’s excellent video <a href="https://www.youtube.com/watch?v=h9Z4oGN89MU">How do Graphics Cards Work? Exploring GPU Architecture</a>, which explores a torn-down RTX 3090 with great 3D animations. The architecture has evolved since then, but the principles are exactly the same, and I strongly recommend it.</p>

<h2 id="part-1-built-for-throughput">Part 1: Built for Throughput</h2>

<p>Inside an RTX 5090, the GPU has over <strong>21,000 cores</strong>. A high-end desktop CPU on the same motherboard might have <strong>16 or 24</strong>. So the GPU is 1,000 times more powerful? Not really: the two are built for completely different jobs.</p>

<p>A useful analogy is to think of a CPU as a <strong>math professor</strong> and a GPU as a <strong>stadium full of students</strong>. The professor can solve any problem, even a long one full of steps and decisions, and solves it quickly. Each student can only do simple sums, and slowly. But hand out 10,000 simple sums, and the stadium finishes long before the professor is done with the first hundred. Likewise a GPU is good at a narrow set of simple operations (mostly arithmetic) applied to <strong>mountains of data</strong>, and it can’t run an operating system or talk directly to input devices or networks. In technical terms, a CPU is optimised for <strong>latency</strong> (finish <em>one</em> stream of instructions as fast as possible), while a GPU is optimised for <strong>throughput</strong> (finish <em>millions</em> of independent small tasks per second, even if each single task is slow).</p>

<p>That difference in goal leads to a completely different use of the silicon area:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>              CPU                                       GPU
+-------------------------------+        +-------------------------------+
| +---------+  +---------+      |        | ##### ##### ##### ##### ##### |
| | Core    |  | Core    |      |        | ##### ##### ##### ##### ##### |
| | (big)   |  | (big)   |      |        | ##### ##### ##### ##### ##### |
| +---------+  +---------+      |        | ##### ##### ##### ##### ##### |
| +---------+  +---------+      |        | ##### ##### ##### ##### ##### |
| | Core    |  | Core    |      |        | ##### ##### ##### ##### ##### |
| +---------+  +---------+      |        | ##### ##### ##### ##### ##### |
|                               |        |-------------------------------|
|      Large L2 / L3 cache      |        |          L2 cache             |
+-------------------------------+        +-------------------------------+
  few powerful cores,                       thousands of simple ALUs,
  lots of control logic + cache             little control logic per ALU
</code></pre></div></div>

<p>Most of a CPU core’s area doesn’t do arithmetic at all: it goes into branch predictors, out-of-order execution and big caches, all there to keep a single thread from ever waiting. A GPU makes the opposite bet. It assumes the work is <strong>massively parallel</strong>: the same operation applied to millions of pixels, vertices or matrix elements. In that world, smart prediction is a waste of area; it’s far better to spend those transistors on <em>more arithmetic units</em>. Let’s open one up and see.</p>

<h2 id="part-2-inside-the-graphics-card">Part 2: Inside the Graphics Card</h2>

<h3 id="the-board">The board</h3>

<p>Taking apart an RTX 5090, this is roughly what we find:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-----------------------------------------------------------------------+
|  Heatsink + heat pipes / vapor chamber + fans                         |
|  (most of the weight of the card)                                     |
+-----------------------------------------------------------------------+
| PCB                                                                   |
|  [Display]   +------+ +------+          +---------+    [12V-2x6 power]|
|  [ ports ]   | VRAM | | VRAM |  ...     |   GPU   |    [  connector  ]|
|              +------+ +------+          |  GB202  |                   |
|              (16 GDDR7 chips total)     +---------+                   |
|              ..... VRM: dozens of inductors, capacitors, MOSFETs ..... |
+------------------------------||||||||||||||||-------------------------+
                               PCIe 5.0 x16 -&gt; motherboard
</code></pre></div></div>

<ul>
  <li><strong>Display outputs</strong> (HDMI 2.1b, DisplayPort 2.1b) on one side.</li>
  <li>The <strong>12 V power connector</strong> (the 16-pin 12V-2x6) on the other side.</li>
  <li><strong>PCIe 5.0 x16 pins</strong> that plug into the motherboard, which carry data to and from the CPU and system memory.</li>
  <li>Most of the small components on the PCB form the <strong>Voltage Regulator Module (VRM)</strong>. It takes the incoming 12 V and converts it to around 1 V for the GPU. An RTX 5090 is rated at <strong>575 W</strong>, and at such a low voltage that means <strong>hundreds of amps</strong> flowing into the chip.</li>
  <li>All that power becomes heat, so most of the card’s weight is the <strong>cooler</strong>: heat pipes or a vapor chamber carry heat from the GPU and memory chips to the radiator fins, where the fans blow it away. (NVIDIA’s own 5090 Founders Edition even splits the electronics over separate boards so that air can flow straight through the card, and uses liquid metal between the GPU and the cooler.)</li>
  <li><strong>32 GB of graphics memory</strong> (GDDR7) spread across 16 chips around the GPU. We’ll get back to these.</li>
  <li>And in the center, the brain: the <strong>GPU</strong> itself.</li>
</ul>

<h3 id="the-gpu-chip-anatomy-of-gb202">The GPU chip: anatomy of GB202</h3>

<p>The die inside the RTX 5090 is called <strong>GB202</strong> (Blackwell architecture). It’s a chip of about <strong>750 mm²</strong> made of <strong>92 billion transistors</strong>, and the vast majority of its area is taken up by processing cores, organised hierarchically:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GB202 (full die)
 +-- 12 GPCs  (Graphics Processing Clusters)
       +-- 8 TPCs each  (Texture Processing Clusters)
             +-- 2 SMs each  (Streaming Multiprocessors)
                   +-- 1 RT core (ray tracing)
                   +-- 4 processing blocks, each with:
                         +-- 1 warp scheduler
                         +-- 32 CUDA cores
                         +-- 1 Tensor core

12 × 8 × 2   = 192 SMs
192 × 4 × 32 = 24,576 CUDA cores
192 × 4      = 768 Tensor cores
192 × 1      = 192 RT cores
</code></pre></div></div>

<p>(As we’ll see in the last part, the RTX 5090 doesn’t have all of these enabled.)</p>

<p>These three types of cores do all of the computation, each with a different job:</p>

<ul>
  <li><strong>CUDA cores</strong> (also called shading cores) are simple calculators with an “add” button, a “multiply” button and a few others. They do most of the work when running games.</li>
  <li><strong>Tensor cores</strong> (5th generation on Blackwell) are matrix multiplication and addition calculators, used for geometric transformations and above all for neural networks and AI.</li>
  <li><strong>Ray tracing cores</strong> (4th generation) are the largest but fewest, and they accelerate the ray tracing algorithms (finding which triangle a ray of light hits).</li>
</ul>

<p>The <strong>SM</strong> is the real basic building block. Besides the cores, each SM has a big register file (256 KB), 128 KB of L1 cache / shared memory, and a few <strong>Special Function Units (SFUs)</strong>.</p>

<h3 id="inside-a-cuda-core">Inside a CUDA core</h3>

<p>Note that the word <em>core</em> is pure marketing: a CUDA core is a single arithmetic lane, not a core in the CPU sense. It has no instruction fetch, no branch predictor, nothing. A better comparison is that one SM is roughly a “core”, and the CUDA cores are its very wide vector unit.</p>

<p>What is left is a very simple calculator: it does basic math (add, multiply, compare and a few bit operations) on 32-bit numbers, either floating-point (FP32) or integer (INT32). On Blackwell, all 128 cores of an SM can do either type, while on the previous generations only half of them could handle integers.</p>

<p>Of these operations, one matters more than the others: the <strong>FMA (Fused Multiply-Add)</strong>, a multiplication and an addition done in a single step:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FMA (Fused Multiply-Add):   result = A × B + C
</code></pre></div></div>

<p>It’s not the only thing a core does, but it’s the operation that almost all 3D graphics and neural network math boils down to, so it’s the one that defines the GPU’s performance numbers.</p>

<p>Each CUDA core completes one FMA per clock cycle, which counts as two operations (a multiply and an add). So for an RTX 5090:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>21,760 cores × 2.41 GHz × 2 operations = ~105 trillion operations per second (or 105 TFLOPS)
</code></pre></div></div>

<p>There’s our 105 trillion. (For comparison: RTX 3090 ~36 trillion, RTX 4090 ~83 trillion.)</p>

<p>What about more complicated operations like division, square roots or trigonometric functions? These are handled by the <strong>Special Function Units</strong>, which are far fewer, only a handful per SM against 128 CUDA cores. GPUs are built for the assumption that the common case is multiply and add.</p>

<h3 id="the-rest-of-the-chip">The rest of the chip</h3>

<p>Around the cores we find:</p>

<ul>
  <li><strong>16 memory controllers</strong> around the edge, each 32 bits wide, which talk to the GDDR7 chips (16 × 32 = <strong>512-bit</strong> bus).</li>
  <li>The <strong>PCIe 5.0</strong> interface to the CPU. (Older big chips like the RTX 3090’s GA102 also had NVLink to connect two cards together; consumer cards dropped it starting with the 40 series.)</li>
  <li>A big <strong>L2 cache</strong> shared by the whole chip: 128 MB on the full GB202, 96 MB enabled on the RTX 5090. For comparison, the RTX 3090 had only 6 MB.</li>
  <li>The <strong>GigaThread Engine</strong>, which manages all the GPCs and SMs and distributes work among them.</li>
  <li>Fixed-function blocks for video encoding/decoding (NVENC/NVDEC) and the display engine.</li>
</ul>

<h3 id="graphics-memory-feeding-the-beast">Graphics memory: feeding the beast</h3>

<p>Even 96 MB of L2 can hold only a small fraction of a game scene. Everything else lives in the graphics memory (VRAM), and different chunks of the scene are continuously transferred between VRAM and the GPU. In fact, when you’re staring at a loading screen, most of that time is spent moving the 3D models and textures of the level from your SSD into the VRAM.</p>

<p>Because the cores are performing tens of trillions of calculations per second, GPUs are <strong>data hungry</strong> machines that need to be continuously fed. Back to our stadium: the students are only as fast as the people handing out the worksheets, so the memory system is built to hand out data on many lanes at the same time: the 16 memory chips on a 5090 together transfer <strong>512 bits at a time</strong> (the <em>bus width</em>), each pin running at 28 Gbit/s:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>512 bits × 28 Gbit/s / 8 = 1,792 GB/s  (~1.8 TB/s)
</code></pre></div></div>

<p>Compare that to the DRAM sticks supporting a CPU: each channel is only <strong>64 bits</strong> wide, and even a dual-channel DDR5 desktop gets something around 80-100 GB/s. For many workloads, especially AI, memory bandwidth (not compute) is the real bottleneck.</p>

<h3 id="more-than-ones-and-zeros">More than ones and zeros</h3>

<p>You may think computers only use binary 1s and 0s on their wires. To squeeze more data out of each wire, modern graphics memory uses <strong>multiple voltage levels</strong>:</p>

<ul>
  <li><strong>GDDR6X</strong> (RTX 30 and 40 series) used <strong>PAM-4</strong>: 4 voltage levels, so each symbol carries <strong>2 bits</strong>.</li>
  <li><strong>GDDR7</strong> (RTX 50 series) switched to <strong>PAM-3</strong>: 3 voltage levels (-1, 0, +1), i.e. <em>ternary</em> digits. Bits are packed into trits with encoding schemes such as <strong>3 bits → 2 trits</strong> (3² = 9 combinations, enough for 2³ = 8) and <strong>11 bits → 7 trits</strong> (3⁷ = 2187, enough for 2¹¹ = 2048). Combined, they send 276 bits using only 176 ternary symbols, about 1.5 bits per symbol.</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NRZ   (binary):   2 levels -&gt; 1 bit per symbol
PAM-3 (GDDR7):    3 levels -&gt; ~1.5 bits per symbol
PAM-4 (GDDR6X):   4 levels -&gt; 2 bits per symbol
</code></pre></div></div>

<p>Why go from 4 levels <em>down</em> to 3? Because with fewer levels the voltage steps are bigger and easier to tell apart: better signal-to-noise ratio, simpler encoders, and better power efficiency, which allows much higher clock rates and in the end more bandwidth. That’s how the 5090 gets ~78% more bandwidth than the 4090.</p>

<h3 id="hbm-memory-for-ai-chips">HBM: memory for AI chips</h3>

<p>Datacenter AI accelerators use a different kind of memory: <strong>HBM (High Bandwidth Memory)</strong>. Instead of separate chips on the PCB, HBM is a <strong>stack</strong> of DRAM dies connected vertically by <strong>TSVs (Through-Silicon Vias)</strong>, tiny vertical wires going through the silicon, forming a “cube” of memory placed right next to the GPU on the same package. A single HBM3E stack holds 24 to 36 GB, and a datacenter Blackwell chip like the B200 is surrounded by 8 of them, for <strong>192 GB</strong> and about 8 TB/s of bandwidth.</p>

<h2 id="part-3-how-the-work-actually-runs">Part 3: How the Work Actually Runs</h2>

<p>Now that we know the hardware, how do we keep 20,000 cores busy at once?</p>

<h3 id="embarrassingly-parallel-problems">Embarrassingly parallel problems</h3>

<p>Video game rendering, scientific simulations and neural networks are what computer scientists call <strong>embarrassingly parallel</strong> problems: the work splits naturally into millions of small tasks that don’t depend on each other. GPUs run them with <strong>SIMD</strong> (<strong>S</strong>ingle <strong>I</strong>nstruction, <strong>M</strong>ultiple <strong>D</strong>ata): one instruction, repeated across thousands of different numbers.</p>

<p>Take a cowboy hat on a table in a 3D game. The hat is made of about <strong>14,000 vertices</strong>, each with X, Y and Z coordinates relative to the center of the hat. To place it in the game world, every vertex needs the same operation: “add the hat’s position in the world”.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>world_x = model_x + hat_x
world_y = model_y + hat_y      &lt;- same instruction, 14,000 vertices,
world_z = model_z + hat_z         one vertex per core
</code></pre></div></div>

<p>No vertex depends on any other, so they can all be computed at the same time. A full scene from the video has <strong>8.3 million vertices</strong>, about <strong>25 million additions</strong>, and that’s only one of the first steps of rendering a frame.</p>

<h3 id="threads-warps-blocks-and-grids">Threads, warps, blocks and grids</h3>

<p>Each small task, like moving one vertex, is called a <strong>thread</strong>. The GPU doesn’t manage threads one by one: it groups them in teams of <strong>32 called warps</strong>, and all 32 threads in a warp execute the same instruction at the same time, each on its own piece of data. Warps are then grouped into <strong>blocks</strong>, each assigned to one SM, and all the blocks of a job form a <strong>grid</strong> that covers the whole GPU:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Thread  (one small task)     -&gt;  1 CUDA core
Warp    (32 threads)         -&gt;  1 processing block
Block   (many warps)         -&gt;  1 SM
Grid    (many blocks)        -&gt;  the whole GPU
</code></pre></div></div>

<p>The downside of moving in teams of 32 is <strong>branches</strong>. If half the threads in a warp need to do one thing and the other half something else (an <code class="language-plaintext highlighter-rouge">if</code>/<code class="language-plaintext highlighter-rouge">else</code>), the warp has to run both paths one after the other, with half the threads waiting each time. This is called <strong>warp divergence</strong>, and it’s why GPUs are fast on uniform work and slow on code full of decisions.</p>

<h3 id="hiding-latency-instead-of-avoiding-it">Hiding latency instead of avoiding it</h3>

<p>Reading data from memory is slow: it takes hundreds of cycles. A CPU fights this with big caches and clever prediction. A GPU takes a simpler approach: it keeps lots of warps ready at the same time, and whenever one is waiting for data, it immediately switches to another one that’s ready to go. Think of a chef with many dishes on the stove: while one pot is waiting to boil, they work on the next one instead of standing still.</p>

<p>Each SM can juggle up to 48 warps (1,536 threads) this way, so a 5090 can have over <strong>260,000 threads</strong> in flight at once. That’s why the GPU never needs to be smart about any single thread: there’s always another one ready.</p>

<h2 id="part-4-tensor-cores">Part 4: Tensor Cores</h2>

<p>Even with thousands of FP32 ALUs, NVIDIA realised around 2016 that deep learning was spending almost all of its time doing one single operation: <strong>matrix multiplication</strong>. Every fully connected layer, every convolution (after some rearranging), and every attention block in a transformer is, at its heart, a big <code class="language-plaintext highlighter-rouge">A × B</code> matrix multiply. Neural networks and generative AI need trillions to quadrillions of these operations, on very large matrices.</p>

<p>Doing that with regular CUDA cores means issuing one FMA instruction per element pair. That works, but a lot of energy goes into fetching instructions, reading registers and moving data around, not into the actual math.</p>

<p>So with the <strong>Volta</strong> architecture (V100, 2017), NVIDIA introduced the <strong>Tensor Core</strong>: a dedicated unit that takes three matrices, multiplies the first two, adds the third, and outputs the result:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>D = A × B + C

  A (4x4)        B (4x4)        C (4x4)        D (4x4)
[ a a a a ]    [ b . . . ]    [ c . . . ]    [ d . . . ]
[ . . . . ]  x [ b . . . ]  + [ . . . . ]  = [ . . . . ]
[ . . . . ]    [ b . . . ]    [ . . . . ]    [ . . . . ]
[ . . . . ]    [ b . . . ]    [ . . . . ]    [ . . . . ]

d = a·b + a·b + a·b + a·b + c   (row of A × column of B, plus C)
</code></pre></div></div>

<p>Each value in the output is the sum of the products of a <strong>row</strong> of the first matrix with a <strong>column</strong> of the second, plus the corresponding value of the third. The key point: since all the values of the three input matrices are available at the same time, the tensor core computes <strong>all</strong> the output values concurrently, in one go. The first Volta tensor core performed 64 FMAs per clock (a 4×4×4 multiply); a regular CUDA core does <strong>one</strong>. Each generation since has made them bigger and faster. Larger matrices are simply broken into tiles and fed to the tensor cores tile by tile.</p>

<h3 id="why-lower-precision-is-the-key">Why lower precision is the key</h3>

<p>The second trick of tensor cores is <strong>reduced precision</strong>. First, a quick word on the names. <strong>FP</strong> stands for <strong>floating point</strong>, the standard way computers store numbers with decimals (a kind of scientific notation in binary), and the number after it is the total bits: FP32 uses 32 bits, FP16 uses 16, and so on. Each number is split into three parts:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FP32:  [sign 1 bit] [exponent 8 bits] [mantissa 23 bits]

sign      -&gt; positive or negative
exponent  -&gt; how big or small the number can be (the range)
mantissa  -&gt; how many significant digits it keeps (the precision)
</code></pre></div></div>

<p>Fewer bits means less range, less precision, or both. You’ll also meet <strong>BF16</strong> (<em>brain floating point</em>, invented at Google Brain) and <strong>TF32</strong> (<em>TensorFloat-32</em>, NVIDIA’s own format): two formats that split their bits between exponent and mantissa differently, as we’ll see below.</p>

<p>Neural networks turn out to be remarkably tolerant to noise: you don’t need 32-bit floats to store a weight that will be nudged slightly in a random direction thousands of times anyway. Halving the number of bits means:</p>

<ul>
  <li>half the memory and bandwidth per value,</li>
  <li>roughly a quarter of the silicon for a multiplier (multiplier area grows with the square of the mantissa width),</li>
  <li>so many more operations per clock in the same area and power.</li>
</ul>

<p>Each generation of tensor cores has added support for lower-precision formats:</p>

<table>
  <thead>
    <tr>
      <th>Architecture</th>
      <th>Year</th>
      <th>Example GPU</th>
      <th>Formats added</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Volta</td>
      <td>2017</td>
      <td>V100</td>
      <td>FP16 (with FP32 accumulate)</td>
    </tr>
    <tr>
      <td>Turing</td>
      <td>2018</td>
      <td>RTX 20xx</td>
      <td>INT8, INT4 (for inference)</td>
    </tr>
    <tr>
      <td>Ampere</td>
      <td>2020</td>
      <td>A100, RTX 30xx</td>
      <td>TF32, BF16, 2:4 structured sparsity</td>
    </tr>
    <tr>
      <td>Ada / Hopper</td>
      <td>2022</td>
      <td>RTX 40xx, H100</td>
      <td>FP8 (Transformer Engine on Hopper)</td>
    </tr>
    <tr>
      <td>Blackwell</td>
      <td>2024-25</td>
      <td>B200, RTX 50xx</td>
      <td>FP4, FP6</td>
    </tr>
  </tbody>
</table>

<p>The new formats are <strong>added</strong>, not swapped in: a Blackwell tensor core still runs FP16, BF16, TF32, INT8 and FP8, and it simply runs FP4 too. (With few exceptions: INT4, for example, was dropped from datacenter chips starting with Hopper, since FP8 and FP4 do the job better.) There are no separate “FP16 cores” and “FP4 cores” either: the same tensor core handles all of them, and it gets faster as the numbers get smaller. Roughly, each time the precision is halved, the throughput doubles:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Same Blackwell tensor core, relative throughput:

TF32  (19 bits)   x0.5
FP16  (16 bits)   x1
FP8   (8 bits)    x2
FP4   (4 bits)    x4
</code></pre></div></div>

<p>The inputs can be tiny, but the results are usually <strong>accumulated</strong> in higher precision (FP16 or FP32), so the rounding errors don’t pile up over thousands of additions. The programmer, or more often the framework like PyTorch, picks the format for each job: higher precision for training, where accuracy matters, and the lowest precision the model can tolerate for running it, where speed matters.</p>

<p>A couple of these deserve a word:</p>

<ul>
  <li><strong>TF32</strong> has the range of FP32 (8-bit exponent) but only a 10-bit mantissa. Its trick is that the tensor core accepts normal FP32 numbers and rounds them to TF32 internally, so code written for FP32 can use the tensor cores without being rewritten. Frameworks like PyTorch can turn this on with a single setting, for a big speedup at a precision loss most training doesn’t notice.</li>
  <li><strong>BF16</strong> keeps the 8-bit exponent of FP32 and only 7 mantissa bits. Same range as FP32, less precision, which is exactly the trade-off training wants (overflow is a much bigger problem than a little rounding).</li>
  <li><strong>2:4 structured sparsity</strong>: if in every group of 4 weights at least 2 are zero, the tensor core can skip them and double its throughput.</li>
  <li><strong>FP4</strong> (new on Blackwell) is just 4 bits per number: only 16 possible values! It works for running (not training) many models, with per-block scale factors to keep the accuracy acceptable, and it doubles throughput again compared to FP8.</li>
</ul>

<p>On an RTX 5090 the plain FP32 CUDA cores deliver (as we calculated earlier) about <strong>105 TFLOPS</strong>, that is 105 trillion floating-point operations per second. NVIDIA markets the same card at <strong>3,352 “AI TOPS (Trillions of Operations Per Second)”</strong>, which is the tensor core throughput in FP4 with sparsity: about 30 times more. Even with the marketing tricks removed (no sparsity, higher precision), the tensor cores are many times faster than the CUDA cores. That’s why every serious deep learning framework goes out of its way to express work as large matrix multiplies in low precision: it’s the only path through the chip that runs at full speed.</p>

<p>Tensor cores are not idle silicon for gamers either: <strong>DLSS</strong> (NVIDIA’s AI upscaling) runs a neural network on the tensor cores to reconstruct a high-resolution frame from a lower-resolution one, and with DLSS 4 on the 50 series, <strong>Multi Frame Generation</strong> uses them to generate up to three extra frames for every frame actually rendered.</p>

<p>Tensor cores are an example of a general rule: the more specialised the hardware, the faster it is at its one job. Bitcoin mining shows the extreme case. GPUs took over mining in its early years, because hashing billions of candidate blocks is perfectly parallel, but they were soon replaced by <strong>ASICs</strong>, chips that can compute <em>only</em> the SHA-256 hash and nothing else, thousands of times faster than a graphics card. CPU → GPU → tensor core → ASIC is a scale from flexible to specialised.</p>

<h2 id="part-5-binning-or-how-to-sell-broken-chips">Part 5: Binning, or How to Sell Broken Chips</h2>

<p>Now to the most “business” part of chip design. Here’s a surprising fact: the RTX 5090 does <strong>not</strong> use a complete GB202 chip. Out of 192 SMs, only <strong>170</strong> are enabled (about 88%), along with 96 MB of the 128 MB of L2 cache. And the same GB202 die is also sold in the <strong>RTX PRO 6000 Blackwell</strong>, a professional card with <strong>188</strong> SMs and 96 GB of memory, for several times the price. Why?</p>

<h3 id="defects-and-yield">Defects and yield</h3>

<p>During manufacturing, patterning errors, dust particles and other small imperfections create <strong>defective areas</strong> on the chip. They land randomly across the wafer, so the bigger the chip, the more likely it is to catch at least one. With a typical defect rate for a mature process, the fraction of perfect dies (the <strong>yield</strong>) looks roughly like this:</p>

<table>
  <thead>
    <tr>
      <th>Die</th>
      <th>Area</th>
      <th>Perfect dies</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Small die (GB206, RTX 5060)</td>
      <td>1.81 cm²</td>
      <td>~83%</td>
    </tr>
    <tr>
      <td>Medium die (GB203, RTX 5080)</td>
      <td>3.78 cm²</td>
      <td>~69%</td>
    </tr>
    <tr>
      <td>Big die (GB202, RTX 5090)</td>
      <td>7.50 cm²</td>
      <td>~47%</td>
    </tr>
  </tbody>
</table>

<p>So more than <strong>half</strong> of all GB202 dies would have at least one defect somewhere. And on a 300 mm wafer only around 70 of them fit. If you had to throw away every die with a single defect, high-end GPUs would cost a fortune (well, even more than they already do).</p>

<h3 id="the-trick-redundancy-by-design">The trick: redundancy by design</h3>

<p>The solution is the <strong>highly repetitive design</strong> we saw earlier. A GPU is made of dozens of identical SMs, so a small defect in one core only damages that particular SM and doesn’t affect the rest of the chip. Instead of throwing the chip away, engineers find the defective region and <strong>permanently isolate and deactivate</strong> the circuitry around it, by blowing tiny on-chip <strong>eFuses</strong> during testing, so the configuration can’t be changed afterwards. The same applies to L2 cache slices and memory controllers.</p>

<p>The chips are then tested and categorised, or <strong>binned</strong>, according to how many working units they have. Because the defect can land anywhere, two RTX 5090s don’t necessarily have the <em>same</em> SMs disabled, they just have the same <em>number</em> of working ones.</p>

<h3 id="one-die-many-products">One die, many products</h3>

<p>NVIDIA designs a handful of dies of different sizes and then cuts each one into several products, depending on how many units survived (and on market demand). Here’s the RTX 50 desktop lineup:</p>

<table>
  <thead>
    <tr>
      <th>Die</th>
      <th>Full die SMs</th>
      <th>Product</th>
      <th>Active SMs</th>
      <th>CUDA cores</th>
      <th>Memory</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GB202</td>
      <td>192</td>
      <td>RTX PRO 6000 (pro)</td>
      <td>188</td>
      <td>24,064</td>
      <td>96 GB GDDR7, 512-bit</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td>RTX 5090</td>
      <td>170</td>
      <td>21,760</td>
      <td>32 GB GDDR7, 512-bit</td>
    </tr>
    <tr>
      <td>GB203</td>
      <td>84</td>
      <td>RTX 5080</td>
      <td>84</td>
      <td>10,752</td>
      <td>16 GB GDDR7, 256-bit</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td>RTX 5070 Ti</td>
      <td>70</td>
      <td>8,960</td>
      <td>16 GB GDDR7, 256-bit</td>
    </tr>
    <tr>
      <td>GB205</td>
      <td>50</td>
      <td>RTX 5070</td>
      <td>48</td>
      <td>6,144</td>
      <td>12 GB GDDR7, 192-bit</td>
    </tr>
    <tr>
      <td>GB206</td>
      <td>36</td>
      <td>RTX 5060 Ti</td>
      <td>36</td>
      <td>4,608</td>
      <td>8/16 GB GDDR7, 128-bit</td>
    </tr>
    <tr>
      <td> </td>
      <td> </td>
      <td>RTX 5060</td>
      <td>30</td>
      <td>3,840</td>
      <td>8 GB GDDR7, 128-bit</td>
    </tr>
    <tr>
      <td>GB207</td>
      <td>20</td>
      <td>RTX 5050</td>
      <td>20</td>
      <td>2,560</td>
      <td>8 GB GDDR6, 128-bit</td>
    </tr>
  </tbody>
</table>

<p>A few interesting things stand out:</p>

<ul>
  <li>The <strong>RTX 5080 is a flawless GB203</strong>, all 84 SMs enabled, while GB203 dies with a few defects become 5070 Ti. Same chip, same memory bus, roughly a 20% difference in cores and a big difference in price.</li>
  <li>There is a <strong>huge gap</strong> between the 5090 (170 SMs) and the 5080 (84 SMs): GB202 is literally about twice the size of GB203.</li>
  <li>A common misconception is that a 5060 is just a “broken 5090”. It isn’t: they are completely different dies, and GB206 is less than a quarter of the size of GB202. Designing several dies is necessary because cutting a 750 mm² chip down to 30 SMs would mean selling a very expensive piece of silicon for 300 euros. But <strong>within</strong> each die family, binning is exactly what happens.</li>
</ul>

<h3 id="speed-binning">Speed binning</h3>

<p>Defects are not the only thing that varies. Even among fully working dies, some can reach higher clocks at lower voltage than others, due to tiny variations in the manufacturing process. Testing sorts them by this too:</p>

<ul>
  <li>the best dies can go into higher-clocked or more power-efficient products (top SKUs, laptop chips where every watt counts),</li>
  <li>the others are sold at lower clocks.</li>
</ul>

<p>This is also why the “silicon lottery” exists among overclockers: two cards of the same model can overclock very differently.</p>

<h3 id="not-only-nvidia">Not only NVIDIA</h3>

<p>Binning is universal in the semiconductor industry:</p>

<ul>
  <li><strong>AMD Ryzen</strong> CPUs are built from 8-core chiplets; a 6-core Ryzen 5 is very often an 8-core chiplet with two cores disabled.</li>
  <li><strong>Intel “F”</strong> CPUs (like the i5-14400F) are chips where the integrated GPU is disabled, often because it was defective.</li>
  <li>The <strong>PlayStation 5</strong> and <strong>Xbox Series X</strong> ship with some GPU compute units disabled on purpose, so that chips with a defect can still be used and console production is not limited by yield.</li>
</ul>

<h3 id="the-uncomfortable-part">The uncomfortable part</h3>

<p>As the process matures, defect density drops and more dies come out perfect. But the market still wants cheaper SKUs, so manufacturers sometimes take a perfectly good die and <strong>disable working units</strong> to sell it as a lower model. From a pure engineering point of view it sounds wasteful, but economically it’s the same chip, with the same manufacturing cost, sold at different prices to different customers. The price of a GPU has little to do with how much it costs to make <em>that specific chip</em>, and a lot to do with how many working SMs it has and who wants to buy it.</p>

<h2 id="conclusion">Conclusion</h2>

<p>To recap:</p>

<ul>
  <li>A <strong>GPU</strong> is the stadium, not the professor: it is built for throughput, with thousands of simple FMA calculators, fed by a very wide memory bus, hiding latency by switching between thousands of threads.</li>
  <li>The chip is a hierarchy of identical blocks: <strong>GPCs → TPCs → SMs → processing blocks → CUDA cores</strong>, and the SM is the real building block.</li>
  <li>Work is organised in <strong>threads, warps of 32, thread blocks and grids</strong>, and it shines on <strong>embarrassingly parallel</strong> problems like graphics and AI.</li>
  <li><strong>Tensor cores</strong> compute small matrix multiply-accumulate operations all at once, at low precision, and they are the reason GPUs dominate AI.</li>
  <li><strong>Binning</strong> turns imperfect silicon into a product lineup: the same die, with more or fewer SMs fused off, becomes a 5070 Ti or a 5080, a 5090 or an RTX PRO 6000.</li>
</ul>

<p>So next time you see “21,760 CUDA cores” on a spec sheet, you’ll know it means 170 SMs that survived testing out of 192, each juggling dozens of warps of 32 threads, with 4 little matrix engines doing most of the heavy lifting for AI.</p>

<p>As always, I hope you found this article helpful! :)</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="hardware" /><category term="technology" /><summary type="html"><![CDATA[How many calculations does your graphics card perform every second while running a modern game? Running Super Mario 64 back in 1996 required around 100 million calculations per second. Minecraft in 2011 needed something like 100 billion. A game like Cyberpunk 2077 needs a GPU capable of tens of trillions, and the current top consumer card, the RTX 5090, does about 105 trillion per second.]]></summary></entry><entry><title type="html">Kubernetes Probes in Production: Configuration, Failure Modes, and Common Mistakes</title><link href="https://lukemainframe.top/2026/09/22/kubernetes-probes-production/" rel="alternate" type="text/html" title="Kubernetes Probes in Production: Configuration, Failure Modes, and Common Mistakes" /><published>2026-09-22T00:00:00+00:00</published><updated>2026-09-22T00:00:00+00:00</updated><id>https://lukemainframe.top/2026/09/22/k8s-probes-2</id><content type="html" xml:base="https://lukemainframe.top/2026/09/22/kubernetes-probes-production/"><![CDATA[<p>In <a href="/2026/09/08/kubernetes-probes/">the previous article</a> we covered what the three probes are for: liveness, readiness, and startup. But  knowing which probe to use solve only part of the problem. What’s left is to understand all those parameters you encounter when you try to configure probes properly: which probe <em>mechanism</em> to use, timing parameters (and their dangerous defaults), plus the handful of configurations that often cause outages. Lets give a deeper look in this article.</p>

<h2 id="part-1-probe-types">Part 1: Probe Types</h2>

<p>Kubernetes probes support four different protocols for monitoring pod health, and while <code class="language-plaintext highlighter-rouge">httpGet</code> is the most common, it isn’t always the ideal choice for every scenario. Let us review all available options to see how they work.</p>

<ul>
  <li>
    <h3 id="httpget">httpGet</h3>
  </li>
</ul>

<p>Kubernetes issues an HTTP GET against the container. Any <code class="language-plaintext highlighter-rouge">2xx</code> or <code class="language-plaintext highlighter-rouge">3xx</code> status is a success; everything else fails. For an HTTP application this is almost always the correct choice, because if the HTTP server has problems, the probe notices right away.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">readinessProbe</span><span class="pi">:</span>
  <span class="na">httpGet</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/health/ready</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>

<p>Note that the probe is sent by the kubelet from the node, not from inside the container, so it bypasses anything you configured for in-cluster traffic. Avoid the tempting anti-pattern of wrapping curl in an exec probe:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># don't do this</span>
<span class="na">readinessProbe</span><span class="pi">:</span>
  <span class="na">exec</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">curl"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">-f"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">localhost:8080/health"</span><span class="pi">]</span>  
</code></pre></div></div>

<p>That forks a process and loads a binary on every check to do something the kubelet does natively, for free.</p>

<ul>
  <li>
    <h3 id="tcpsocket">tcpSocket</h3>
  </li>
</ul>

<p>The kubelet just opens a TCP connection to the port. If the handshake completes, the probe passes.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">livenessProbe</span><span class="pi">:</span>
  <span class="na">tcpSocket</span><span class="pi">:</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
  <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">3</span>
</code></pre></div></div>

<p>This is a weak signal, and deliberately so. A listening socket says nothing about whether the application behind it works: the accept queue can be full, the worker pool exhausted, the event loop deadlocked, every request failing and yet the handshake still succeeds. Use it for things that genuinely have no health endpoint, such as a database or a plain TCP service, and don’t mistake it for a health check.</p>

<ul>
  <li>
    <h3 id="exec">exec</h3>
  </li>
</ul>

<p>The kubelet runs a command inside the container; exit code <code class="language-plaintext highlighter-rouge">0</code> passes, anything else fails.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">readinessProbe</span><span class="pi">:</span>
  <span class="na">exec</span><span class="pi">:</span>
    <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">/app/bin/check-ready"</span><span class="pi">]</span>
</code></pre></div></div>

<p>Useful when the application ships a purpose-built health command, or for things with no network surface at all (batch workers, sidecars). The production caveat is cost: this spawns a process inside the container on every single check. At <code class="language-plaintext highlighter-rouge">periodSeconds: 5</code> across 100 Pods that is 72,000 process creations an hour, so the command must be genuinely small and not a shell script that shells out six more times.</p>

<ul>
  <li>
    <h3 id="grpc">grpc</h3>
  </li>
</ul>

<p>For gRPC services, Kubernetes can speak the standard gRPC health checking protocol directly:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">livenessProbe</span><span class="pi">:</span>
  <span class="na">grpc</span><span class="pi">:</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
  <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">10</span>
</code></pre></div></div>

<p>This replaces the old pattern of shipping a <code class="language-plaintext highlighter-rouge">grpc_health_probe</code> binary in the image and calling it with an exec probe.</p>

<table>
  <thead>
    <tr>
      <th>Mechanism</th>
      <th>Use it for</th>
      <th>Watch out for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">httpGet</code></td>
      <td>HTTP/REST services with health endpoints</td>
      <td>Nothing; this is the default</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">grpc</code></td>
      <td>gRPC services implementing the health protocol</td>
      <td>Service must implement the standard</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">tcpSocket</code></td>
      <td>Services with no health endpoint at all</td>
      <td>An open port proves almost nothing</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">exec</code></td>
      <td>Purpose-built health commands, non-network workloads</td>
      <td>Process spawn cost on every check</td>
    </tr>
  </tbody>
</table>

<h2 id="part-2-timing-parameters">Part 2: Timing Parameters</h2>

<p>This is where most production problems actually come from, and the defaults deserve a look before anything else:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Default</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">initialDelaySeconds</code></td>
      <td>0</td>
      <td>Probing starts immediately</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">periodSeconds</code></td>
      <td>10</td>
      <td>How often to check</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">timeoutSeconds</code></td>
      <td><strong>1</strong></td>
      <td>The one that bites people</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">failureThreshold</code></td>
      <td>3</td>
      <td>Consecutive failures before acting</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">successThreshold</code></td>
      <td>1</td>
      <td>Must be 1 for liveness and startup</td>
    </tr>
  </tbody>
</table>

<p>That <code class="language-plaintext highlighter-rouge">timeoutSeconds: 1</code> default is the single most common cause of mysterious restarts. If your health endpoint occasionally takes 1.2 seconds under GC pressure or a cold connection pool, the default silently fails the probe and if it’s a liveness probe, it restarts a container that was working fine.</p>

<h3 id="initialdelayseconds">initialDelaySeconds</h3>

<p>How long the kubelet waits after container start before probing at all.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">30</span>
</code></pre></div></div>

<p>Try to avoid it since it is a fixed, unconditional waiting time: set it to 30 and every Pod in every rollout, forever, waits 30 seconds even when it was ready in two. Worse, setting it large to accommodate a slow start (<code class="language-plaintext highlighter-rouge">initialDelaySeconds: 300</code> is a real thing people write) tells Kubernetes to ignore a genuinely dead container for five minutes.</p>

<p>A startup probe does the same job adaptively, it keeps checking, so a fast start is detected immediately and a slow one still gets its full budget. If you have a startup probe, you almost never need <code class="language-plaintext highlighter-rouge">initialDelaySeconds</code>.</p>

<h3 id="periodseconds">periodSeconds</h3>

<p>How often the check runs. Readiness generally wants a shorter period than liveness, because reacting quickly to “stop sending me traffic” is cheap and reversible, while “restart this container” is not cheap nor reversible:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Readiness: every 5 seconds
Liveness:  every 10 seconds
</code></pre></div></div>

<h3 id="timeoutseconds">timeoutSeconds</h3>

<p>How long the check may take before it counts as a failure. Set it above the realistic p99 of the endpoint. Setting it very high has the opposite problem: <code class="language-plaintext highlighter-rouge">timeoutSeconds: 60</code> on a liveness probe means a hung container can sit there for minutes before anyone notices.</p>

<p>If your endpoint takes an entire minute to respond to a health check, it requires an upstream fix; a proper probe should reply in a fraction of a second and if cannot complete in well under a second is simply trying to do too much.</p>

<h3 id="failurethreshold">failureThreshold</h3>

<p>How many <em>consecutive</em> failures are required before Kubernetes acts.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Probe 1 -&gt; FAIL
Probe 2 -&gt; FAIL
Probe 3 -&gt; FAIL
             |
             v
       Threshold reached -&gt; act
</code></pre></div></div>

<p>The counter resets on any success, which is the whole point: it distinguishes a persistent fault from the transient packet loss, GC pause, or CPU contention that every real cluster produces continuously.</p>

<h2 id="-part-3-readiness-during-rollouts-and-termination">## Part 3: Readiness During Rollouts and Termination</h2>

<p>Readiness is not only a failure-handling mechanism it runs constantly, and it governs every rolling update.</p>

<p>A Pod failing readiness is simply removed from the Service’s endpoints while continuing to run:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Service
   |
   +---- Pod 1 [Ready]    &lt;- receives traffic
   |
   +---- Pod 2 [Not Ready] &lt;- running, no traffic
   |
   +---- Pod 3 [Ready]    &lt;- receives traffic
</code></pre></div></div>

<p><strong>Not Ready is not Not Alive</strong>, and during a deploy that distinction is doing real work: the Deployment waits for each new Pod to pass readiness before it proceeds to the next, so an honest readiness probe is what prevents a rollout from shifting traffic onto instances that aren’t warm yet. A readiness probe that returns <code class="language-plaintext highlighter-rouge">200</code> the instant the process starts makes a rolling update look successful while it drops requests.</p>

<p>Termination is where this gets complicated for critical systems <strong><em>where zero-downtime deployments matter</em></strong>, and it’s worth being precise because the common advice is wrong. When a Pod is deleted, two things happen <strong>in parallel</strong>:</p>
<ol>
  <li>Traffic routing stops: The cluster starts removing the pod’s IP address from all load balancers, ingress controllers, and proxies (EndpointSlices).</li>
  <li>Shutdown begins: The pod receives a  <code class="language-plaintext highlighter-rouge">SIGTERM</code> signal telling the application to shut down, and any <code class="language-plaintext highlighter-rouge">preStop</code> hook starts running.</li>
</ol>

<p>Updating every router and proxy across the cluster takes a few seconds because it happens asynchronously.
During that brief window, incoming traffic keeps arriving at the pod, even though the application has already been told to shut down. If the app stops immediately, those incoming requests fail with errors.</p>

<p>So there is a window where the application has already received <code class="language-plaintext highlighter-rouge">SIGTERM</code> but traffic is still arriving. Failing your readiness probe on shutdown does not close that window since deletion already triggered the service removal; the race is in the propagation. What actually closes it is a short <code class="language-plaintext highlighter-rouge">preStop</code> delay that keeps the Pod serving while the removal spreads:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">lifecycle</span><span class="pi">:</span>
  <span class="na">preStop</span><span class="pi">:</span>
    <span class="na">exec</span><span class="pi">:</span>
      <span class="na">command</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">sleep"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">5"</span><span class="pi">]</span>

<span class="na">terminationGracePeriodSeconds</span><span class="pi">:</span> <span class="m">30</span>
</code></pre></div></div>

<p>The grace period is the total budget for <code class="language-plaintext highlighter-rouge">preStop</code> plus draining; overrun it and the container is <code class="language-plaintext highlighter-rouge">SIGKILL</code>ed mid-request. Probes are one part of lifecycle management, not all of it.</p>

<h2 id="part-4-configurations-that-cause-outages">Part 4: Configurations That Cause Outages</h2>

<h3 id="probes-that-are-too-aggressive">Probes that are too aggressive</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">timeoutSeconds</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">1</span>
</code></pre></div></div>

<p>This says: one slow response, and act. Production clusters produce transient slowness constantly GC pauses, CPU contention from a noisy neighbour, a node under memory pressure, ordinary packet loss and none of it means the application is broken. Aggressive thresholds on a <em>liveness</em> probe convert that background noise into restarts, which is how a mild latency incident becomes a crash loop.</p>

<h3 id="timeouts-tuned-too-low">Timeouts tuned too low</h3>

<p>Covered above, but it’s the most common single misconfiguration: leaving <code class="language-plaintext highlighter-rouge">timeoutSeconds</code> at its default of 1 while the health endpoint’s p99 is well above that. The probe fails under load, which is precisely when you can least afford to lose replicas.</p>

<h3 id="a-startup-budget-smaller-than-the-real-startup-time">A startup budget smaller than the real startup time</h3>

<p><code class="language-plaintext highlighter-rouge">periodSeconds × failureThreshold</code> for the startup probe is a hard deadline. If it’s shorter than a cold start with migrations and an empty cache, the Pod is killed, restarted, and killed again presenting as <code class="language-plaintext highlighter-rouge">CrashLoopBackOff</code> with no error in the application logs.</p>

<h3 id="the-ones-from-part-one-still-true-here">The ones from part one, still true here</h3>

<p>Three mistakes carry over from <a href="/2026/09/08/kubernetes-probes/">part one</a> and are worth naming because they’re just as common at scale: checking external dependencies in <strong>liveness</strong> (restarting a Pod doesn’t fix a database), pointing all three probes at a <strong>single <code class="language-plaintext highlighter-rouge">/health</code> endpoint</strong> so they can’t express different answers, and making <strong>readiness too strict</strong>, so a degraded-but-useful Pod gets pulled from the Service over a dependency it could tolerate.</p>

<h2 id="part-5-quick-checklist">Part 5: Quick Checklist</h2>

<p><strong>Mechanism</strong> <code class="language-plaintext highlighter-rouge">httpGet</code> for HTTP, <code class="language-plaintext highlighter-rouge">grpc</code> for gRPC, <code class="language-plaintext highlighter-rouge">tcpSocket</code> only when there’s nothing better, <code class="language-plaintext highlighter-rouge">exec</code> only when it’s genuinely cheap.</p>

<p><strong>Timing</strong></p>

<ul>
  <li>Is <code class="language-plaintext highlighter-rouge">timeoutSeconds</code> above the endpoint’s p99? (The default of 1 is rarely right.)</li>
  <li>Does <code class="language-plaintext highlighter-rouge">periodSeconds × failureThreshold</code> give acceptable detection time?</li>
  <li>Is the threshold tolerant of a transient network issue or a brief packet loss?</li>
  <li>Is the startup budget larger than the slowest realistic cold start?</li>
</ul>

<p><strong>Semantics</strong></p>

<ul>
  <li>Does liveness check only things a restart could fix?</li>
  <li>Does readiness answer “can I still serve traffic even if i am degradated state”?</li>
  <li>Are the three probes on separate endpoints?</li>
</ul>

<p><strong>Lifecycle</strong></p>

<ul>
  <li>Does readiness stay honest during a rolling update?</li>
  <li>Is there a <code class="language-plaintext highlighter-rouge">preStop</code> delay to cover endpoint propagation?</li>
  <li>Is <code class="language-plaintext highlighter-rouge">terminationGracePeriodSeconds</code> longer than <code class="language-plaintext highlighter-rouge">preStop</code> plus drain time?</li>
</ul>

<p><strong>Global</strong></p>

<ul>
  <li>If every replica fails this check simultaneously, what happens?</li>
</ul>

<p>Kubernetes already provides all the tools we need to keep our applications healthy through probes; our only job is to configure them correctly.</p>

<p>As always, I hope you found both of these articles helpful! :).</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="secops" /><category term="technology" /><category term="linux" /><summary type="html"><![CDATA[In the previous article we covered what the three probes are for: liveness, readiness, and startup. But knowing which probe to use solve only part of the problem. What’s left is to understand all those parameters you encounter when you try to configure probes properly: which probe mechanism to use, timing parameters (and their dangerous defaults), plus the handful of configurations that often cause outages. Lets give a deeper look in this article.]]></summary></entry><entry><title type="html">Kubernetes Probes: How to Use Liveness, Readiness, and Startup Probes Correctly</title><link href="https://lukemainframe.top/2026/09/08/kubernetes-probes/" rel="alternate" type="text/html" title="Kubernetes Probes: How to Use Liveness, Readiness, and Startup Probes Correctly" /><published>2026-09-08T00:00:00+00:00</published><updated>2026-09-08T00:00:00+00:00</updated><id>https://lukemainframe.top/2026/09/08/k8s-probes</id><content type="html" xml:base="https://lukemainframe.top/2026/09/08/kubernetes-probes/"><![CDATA[<p>Kubernetes probes look like three simple fields, but treating them all as a generic “restart the pod if anything feels slightly off” panic button is a classic trap. They actually have very different jobs, and using them interchangeably is a quick way to turn a minor hiccup into an endless loop of unnecessary restarts.
Let’s learn to use them properly!</p>

<p>The three probes answer three different questions, and each one triggers a different action:</p>

<table>
  <thead>
    <tr>
      <th>Probe</th>
      <th>Question it answers</th>
      <th>What a failure does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Liveness</td>
      <td>Is this container stuck in a way a restart could fix?</td>
      <td>Restart the container</td>
    </tr>
    <tr>
      <td>Readiness</td>
      <td>Can this Pod serve traffic right now?</td>
      <td>Remove the Pod from the Service</td>
    </tr>
    <tr>
      <td>Startup</td>
      <td>Has the application finished initializing?</td>
      <td>Keep waiting, hold off the others</td>
    </tr>
  </tbody>
</table>

<p>That table is the whole article, really. The rest is about why mixing the three up is expensive: a badly designed liveness probe can turn a two-second network blip into a full outage.</p>

<h2 id="where-probes-are-defined">Where Probes Are Defined</h2>

<p>Probes are not part of the Service. They are defined on the container, inside the Pod template — so in practice in a Deployment, StatefulSet, or DaemonSet:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">my-api</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">api</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">my-api:1.0</span>

          <span class="na">livenessProbe</span><span class="pi">:</span>
            <span class="na">httpGet</span><span class="pi">:</span>
              <span class="na">path</span><span class="pi">:</span> <span class="s">/health/live</span>
              <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>

          <span class="na">readinessProbe</span><span class="pi">:</span>
            <span class="na">httpGet</span><span class="pi">:</span>
              <span class="na">path</span><span class="pi">:</span> <span class="s">/health/ready</span>
              <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>

<p>The Service never mentions probes at all. It simply routes traffic to whichever Pods are currently <em>Ready</em>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                    Kubernetes Service
                           |
                           v
                Ready Pods receive traffic
                           |
              +------------+------------+
              |            |            |
           Pod #1       Pod #2       Pod #3
              |            |            |
          readiness    readiness    readiness
            probe        probe        probe
</code></pre></div></div>

<p>When a readiness probe fails, Kubernetes drops that Pod from the Service’s set of ready endpoints. Traffic stops, but the container keeps running. That separation between <em>stop the traffic</em> and <em>restart the container</em> is the entire reason there is more than one probe.</p>

<h2 id="the-three-probes">The Three Probes</h2>

<h3 id="startup--have-i-finished-starting">Startup — “have I finished starting?”</h3>

<p>Some applications simply take a long time to come up: loading configuration, warming caches, running migrations, loading large models. If liveness starts checking immediately, Kubernetes may kill the container before it ever had a chance to startup!</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">startupProbe</span><span class="pi">:</span>
  <span class="na">httpGet</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/health/startup</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
  <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">5</span>
  <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">30</span>
</code></pre></div></div>

<p>While a startup probe is defined and still failing, the liveness and readiness probes are disabled entirely. Here the application gets up to 150 seconds (5 × 30) to initialize, and can finish much sooner if it is fast. Once the startup probe succeeds it is never run again, and the other two take over. This is far better than guessing a large <code class="language-plaintext highlighter-rouge">initialDelaySeconds</code>, which forces you to pay the worst case every single time.</p>

<h3 id="liveness--should-kubernetes-restart-me">Liveness — “should Kubernetes restart me?”</h3>

<p>A liveness probe is for a container that is alive at the process level but no longer doing useful work: a deadlock, an exhausted thread pool, a wedged event loop. The process still exists, so without a probe Kubernetes would consider it perfectly healthy forever.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">livenessProbe</span><span class="pi">:</span>
  <span class="na">httpGet</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/health/live</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
  <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">10</span>
  <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">3</span>
</code></pre></div></div>

<p>With these values the container is restarted only after roughly thirty seconds of <em>consistent</em> failure. That word matters: liveness should describe the application’s own ability to function, not the health of everything it happens to talk to.</p>

<h3 id="readiness--should-i-receive-traffic">Readiness — “should I receive traffic?”</h3>

<p>Readiness answers a narrower question: this Pod is running, but should it be serving requests at this moment?</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">readinessProbe</span><span class="pi">:</span>
  <span class="na">httpGet</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/health/ready</span>
    <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
  <span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">5</span>
  <span class="na">failureThreshold</span><span class="pi">:</span> <span class="m">2</span>
</code></pre></div></div>

<p>Readiness failure is cheap and fully reversible. The Pod stays alive, drops out of the Service endpoints, and returns as soon as the probe succeeds again — no restart, no lost in-memory state, no startup cost paid. This is why readiness, not liveness, is the right place for “I am temporarily unable to do my job”.</p>

<h2 id="the-database-trap">The Database Trap</h2>

<p>This is the mistake worth internalizing, because it is both extremely common and actively harmful.</p>

<p>Imagine an API with three replicas, all talking to the same database, and a <code class="language-plaintext highlighter-rouge">/health</code> endpoint that checks database connectivity wired into the <strong>liveness</strong> probe. The database has a brief network problem, a few seconds of network issue, the kind of thing that happens routinely.</p>

<p>All three replicas share that dependency, so they all fail at the same time:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DB hiccup
   |
   +------&gt; Pod 1 liveness fails
   |
   +------&gt; Pod 2 liveness fails
   |
   +------&gt; Pod 3 liveness fails
                 |
                 v
          Entire fleet restarts
                 |
                 v
       Cold caches, reconnect storm
                 |
                 v
     More pressure on the recovering DB
</code></pre></div></div>

<p>By the time the database is back, every Pod is in the middle of a cold start that can take few minutes depending from the application, 
and they all hit the recovering database at once. 
The miss-configured probes has taken a transient dependency failure and amplified it into a real outage.</p>

<p>The rule that prevents this is a single question:</p>

<blockquote>
  <p>Liveness asks whether restarting <strong>this container</strong> could plausibly fix the problem.</p>
</blockquote>

<p>Restarting an API Pod does not repair a database. So database connectivity has no business being in a liveness probe. It belongs in readiness, where the consequence matches the problem:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Database unavailable
        |
        +----&gt; Readiness fails
        |          |
        |          v
        |      Pod removed from Service
        |      (still running)
        |
        +----&gt; Liveness still healthy
                   |
                   v
               No restart
</code></pre></div></div>

<p>The Pods stay up, stop advertising themselves as able to serve, and become Ready again on their 
own the moment the database recovers. Same failure, but back online in seconds with no restarts!</p>

<h2 id="how-much-should-readiness-check">How Much Should Readiness Check?</h2>

<p>The opposite mistake also exists. Readiness is not a dumping ground for every dependency either.</p>

<p>If your API genuinely cannot answer anything useful without the database, then checking it in readiness is correct. But suppose the application can still serve cached data, <code class="language-plaintext highlighter-rouge">/metrics</code>, and a handful of read paths when Redis is down. Making readiness fail on Redis pulls every Pod out of the Service — and since all replicas share that Redis, you have just taken the whole application offline over a dependency it could have tolerated.</p>

<p>The question to ask is:</p>

<blockquote>
  <p>Can this Pod still serve the traffic the Service is actually sending it?</p>
</blockquote>

<p>If the answer is yes, it should stay Ready, even in a degraded state.</p>

<h2 id="designing-health-endpoints">Designing Health Endpoints</h2>

<p>The cleanest approach is one endpoint per question, rather than a single <code class="language-plaintext highlighter-rouge">/health</code> doing everything:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">/health/live</code></strong> — lightweight, and limited to the application itself. It should generally avoid touching databases, queues, or remote APIs unless there is a very specific reason.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">/health/ready</code></strong> — may check the dependencies that are genuinely required to serve requests, returning <code class="language-plaintext highlighter-rouge">503</code> when they are missing.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">/health/startup</code></strong> — reports only whether initialization has completed.</li>
</ul>

<p>Whatever they check, these endpoints must be cheap. With 100 Pods and a five-second period, you are generating tens of thousands of health checks per hour, so they need to be fast, deterministic, side-effect free, and safe to call repeatedly. A readiness check that fans out to six backends is not a health check, it is a distributed tracing span you run twelve times a minute per Pod — and when one of those backends gets slow, the probe gets slow, Pods drop out of the Service, and the remaining Pods absorb more traffic. The probe itself becomes the outage.</p>

<h2 id="choosing-the-right-probe">Choosing the Right Probe</h2>

<p>When you are unsure where a particular check belongs, don’t ask whether it indicates a problem. Ask what you want Kubernetes to <em>do</em> about it:</p>

<ul>
  <li>“Restart the container, because restarting might fix it” → <strong>liveness</strong></li>
  <li>“Stop sending traffic here until this resolves” → <strong>readiness</strong></li>
  <li>“Give it more time to finish starting” → <strong>startup</strong></li>
</ul>

<p>This works because probes are not monitoring endpoints. Monitoring tells a human that something is wrong; a probe tells Kubernetes what action to take, and Kubernetes takes it immediately, on every replica, without asking. If you want to <em>observe</em> something, export a metric and alert on it. Put it in a probe only when you want the corresponding action.</p>

<h2 id="what-to-remember">What to remember</h2>

<p>Choosing the right probe is extremely important, because that gives Kubernetes the right 
signal for the action you actually want. A temporary database outage should never be a reason to restart every API Pod.</p>

<p>So to conclude, liveness is a big hammer: it just restarts pods, so reserve it for problems a restart can genuinely fix. Readiness is the gentle, reversible one, and it is where most dependency conditions belong. 
Startup exists so your container has enough time to start, before the other two take over.</p>

<p>In <a href="/2026/09/22/kubernetes-probes-production/">the next article</a> we will look at probe types, the timing parameters, and how to tune all of this for real production workloads.</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="secops" /><category term="technology" /><category term="linux" /><summary type="html"><![CDATA[Kubernetes probes look like three simple fields, but treating them all as a generic “restart the pod if anything feels slightly off” panic button is a classic trap. They actually have very different jobs, and using them interchangeably is a quick way to turn a minor hiccup into an endless loop of unnecessary restarts. Let’s learn to use them properly!]]></summary></entry><entry><title type="html">Inside the Xbox 360 RGH hack</title><link href="https://lukemainframe.top/2026/05/22/xbox-360-rgh/" rel="alternate" type="text/html" title="Inside the Xbox 360 RGH hack" /><published>2026-05-22T00:00:00+00:00</published><updated>2026-05-22T00:00:00+00:00</updated><id>https://lukemainframe.top/2026/05/22/xbox369-mod</id><content type="html" xml:base="https://lukemainframe.top/2026/05/22/xbox-360-rgh/"><![CDATA[<p>In my opinion, the Reset Glitch Hack (RGH) on the Xbox 360 stands as one of the most elegant, and great achievements 
in the history of console reverse engineering. In this article I will explain in details each phase of the exploit and what is happening under the hood.
Notice this is NOT a step-by-step practical tutorial (there are <a href="https://xbox360hub.com/guides/rgh-3-guide/">already plenty online</a>). 
Instead, this article is an explanation of those steps, so if you are curious to fully understand how was possible to mod the xbox360, this article is for you!</p>

<p>Before we start, let’s get the standard legal disclaimer out of the way:</p>
<ul>
  <li>This article is strictly for educational purposes. Do not use this knowledge to hurt poor consoles.</li>
  <li>I have only ever observed this exploit from a safe, theoretical distance and I have <em>never ever ever</em> actually performed it myself (I may recall a friend of a friend of a friend that did it)</li>
  <li>I hold a deep, unwavering reverence for the flawless, totally-not-sloppy security architectures of massive mega-corporations like Microsoft.</li>
  <li>«PLACEHOLDER FOR OTHER BS»</li>
</ul>

<p>I hope that this suffices. Now lets start with an overview.</p>

<h2 id="some-history">Some History</h2>

<p>Launched in 2005, the Xbox 360 was built like a digital fortress, with an innovative secure-boot chain and 
hypervisor architecture designed to make homebrew impossible. 
For the first few years, the only available hacks were software exploits that Microsoft quickly patched through system updates.</p>

<p>Then in 2011, French researcher <code class="language-plaintext highlighter-rouge">GliGli</code> discovered an hardware 
vulnerability (called <code class="language-plaintext highlighter-rouge">Reset Glitch Hack</code>) baked into the CPU. This new vulnerability was purely hardware related, <strong>and completely unpatchable by Microsoft.</strong>
For the next years, people started to exploit this vulnerability by using external modchips.</p>

<p>After several iterations like <code class="language-plaintext highlighter-rouge">RGH1.2</code> and <code class="language-plaintext highlighter-rouge">S-RGH</code>, in late 2021, user <a href="https://github.com/15432">15432</a> presented the <code class="language-plaintext highlighter-rouge">RGH3</code>, 
a new exploit that skipped external chips entirely by hijacking the console’s internal standby processor.
Because the <code class="language-plaintext highlighter-rouge">RGH3</code> is the most complete hack, we will focus on that.</p>

<h2 id="general-overview-the-rgh-workflow">General Overview: The RGH Workflow</h2>

<p>The Xbox 360 stores its operating system, bootloaders, and system-specific data (such as the console’s unique Keyvault) inside a physical flash memory chip on the motherboard, 
known as the <code class="language-plaintext highlighter-rouge">NAND</code>. The content of this memory is encrypted with <em>encryption keys</em> that are baked into the CPU and <strong>are unique for each console</strong>. This means
that you cannot just clone nand data to a new console, because it would not be able to decrypt the nand content.</p>

<p>So for the RGH, the first thing to do is to read and dump the encrypted nand content. This is done by soldering <code class="language-plaintext highlighter-rouge">an external SPI</code> reader
(or <a href="https://github.com/X360Tools/PicoFlasher/releases/">rpi-pico</a>) to the motherboard to extract a exact image of this storage. 
It is extremely important that you make several backups of this file since, 
if something goes wrong, you can always restore the console with this (console-tailored) image.</p>

<p>Once you have the NAND dump extracted you can feed it into the <code class="language-plaintext highlighter-rouge">J-Runner</code> software. This software replaces the console’s factory second-stage bootloaders with <strong>XeLL</strong> (the Xenon Linux Loader),
a bare-metal, open-source bootloader. At the same time, if selecting <strong>RGH3</strong> tickbox, the software alters the firmware 
of the console’s <strong>SMC (System Management Controller)</strong> (a separate stand-by chip processor) to inject custom glitch-timing loops (more details in the section below).
This modified image is then written back to the physical flash chip.</p>

<p>When the console is powered on, the modified SMC code activates. It synchronizes with the motherboard’s clock (The POST Wire) and drops 
the voltage to the CPU (The RST/PLL Wire) at a precise microsecond to bypass security verification, allowing unsigned <code class="language-plaintext highlighter-rouge">XeLL</code> binary to run. 
This binary is basically another OS similar to when you boot your PC from a usb stick.</p>

<p>Once Booted, the <strong>only thing that XeLL does</strong> is to sends direct instructions to the CPU to read its internal <strong>eFuses</strong>, 
displaying the unencrypted 128-bit <strong>CPU Key</strong> onto the screen.</p>

<p>With the CPU Key successfully recovered, they are entered to the J-Runner program.
The software uses this key to fully decrypt the original NAND backup, extracting the system’s unique serial number and pairing data. 
It then takes a clean, official Microsoft Dashboard update, strips out its core security restrictions, re-encrypts the package using the console’s specific CPU Key, 
and generates a final custom image called a <strong>XeBuild Image</strong>. This new image can be written back to the console. 
The console is now cured, and you may proceed to install custom Dashboard such as Aurora.</p>

<p>Here is a quick overview of those steps:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+------------------+     +--------------------+     +---------------------+
| 1. Dump NAND     | --&gt; | 2. Patch &amp; Write   | --&gt; | 3. Execute Glitch &amp; |
|    (Raw Backup)  |     |    XeLL image      |     |    Boot XeLL        |
+------------------+     +--------------------+     +---------------------+
                                                               |
+------------------+     +--------------------+                v
| 6. Flash Custom  | &lt;-- | 5. Build XeBuild   | &lt;-- | 4. Read CPU Key     |
|    (Dashboard)   |     |    Custom Image    |     |    From eFuses      |
+------------------+     +--------------------+     +---------------------+
</code></pre></div></div>

<hr />

<h2 id="the-mechanics-of-the-glitch">The Mechanics of the Glitch</h2>

<p>To appreciate why RGH is so remarkable, one must look at how the physical exploit forces the CPU to run unauthorized software.</p>

<h3 id="voltage-fault-injection-attacks">Voltage Fault Injection Attacks</h3>
<p>The Xbox 360 utilizes a secure boot chain overseen by a hardware-level Boot ROM embedded in the CPU. 
When the console starts up, it runs a memory comparison loop (<code class="language-plaintext highlighter-rouge">memcmp</code>) to verify that the digital signature of the next bootloader matches Microsoft’s private RSA keys. 
If the signature doesn’t match, the CPU panics and halts immediately.</p>

<p>RGH breaks this mechanism via <strong>voltage fault injection</strong>:</p>

<ol>
  <li><strong>Clock Slowdown:</strong> To widen the window of opportunity, the modded SMC code or an external modchip forces the motherboard’s clock generator to slow down
the CPU’s operating frequency by a factor of 128.</li>
  <li><strong>The Timing Loop:</strong> The SMC counts individual clock cycles from a known hardware trigger (such as traffic on the <code class="language-plaintext highlighter-rouge">POST</code> or <code class="language-plaintext highlighter-rouge">I2C</code> buses) to 
track the exact nanosecond the signature check loop begins.</li>
  <li><strong>The Glitch Pulse:</strong> At the precise microsecond the CPU processes the branch instruction determining if the signature check 
passed or failed, the SMC drops the voltage on the CPU’s reset or PLL lines.</li>
  <li><strong>The Gate Flip:</strong> This intentional electrical instability starves the processor’s logic gates of power for a fraction of a clock cycle. 
Instead of evaluating the mismatched signature and executing a jump to the “shutdown” subroutine, a logic gate miscalculates and 
slips directly into the “success” pathway. The CPU is tricked into believing the unsigned code is completely authentic.</li>
</ol>

<p>Because subtle factors like ambient temperature, wire routing, and silicon variances can shift the exact cycle the CPU processes this data, 
the glitch can occasionally miss. If it misses, the CPU panics, the SMC detects the failure, instantly triggers a hard reset of the system, 
and tries the exact sequence again on a brand-new boot loop.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Normal Boot Check:   [Verify Signature] ---&gt; (Invalid) ---&gt; [System Panic/Halt]
Glitched Boot Check: [Verify Signature] --(Voltage Drop)--&gt; [Logic Error: Pass] ---&gt; [Boot Code]
</code></pre></div></div>

<h3 id="external-modchips-vs-rgh3-internal-microcode-exploitation">External Modchips vs. RGH3 (Internal Microcode Exploitation)</h3>
<p>Historically, older RGH methods (RGH 1, 2, and S-RGH) relied on an external CPLD microchip (like the <code class="language-plaintext highlighter-rouge">CoolRunner</code>, <code class="language-plaintext highlighter-rouge">Matrix</code> or <code class="language-plaintext highlighter-rouge">Ace</code>) 
to perform a precise timing attack. This chip was listening to the motherboard’s signals and injecting the glitch, but because the timing had to be absolutely perfect, 
the chip often missed its tiny window of opportunity. Whenever a glitch failed, the CPU would panic and halt, 
forcing the chip to completely reboot the console to trigger a brand-new boot cycle and try again. 
This constant cycle of crashing and restarting meant users often had to wait minutes for the console to successfully boot.
If you ever had the chance to observe one of those chip modded consoles, you would see a flashing LED on the modchip blinking at every single failed attempt.</p>

<p><strong>RGH3</strong> eliminated external silicon entirely. Instead of an external chip listening for signals,
developers patched the firmware of the built-in <strong>System Management Controller (SMC)</strong> inside the NAND. Because the SMC lives natively on the motherboard architecture, 
it already has flawless synchronization with the master clock line. Only two basic connection wires are needed:</p>

<ul>
  <li>
    <p>The POST Wire: Connects the CPU’s boot status line (<code class="language-plaintext highlighter-rouge">POST1</code>) to the SMC’s monitoring point (<code class="language-plaintext highlighter-rouge">SMC_POST1</code>). This lets the SMC listen in and know exactly what stage of the boot sequence the CPU is in.</p>
  </li>
  <li>
    <p>The RST/PLL Wire: Connects a CPU control line (<code class="language-plaintext highlighter-rouge">PLL_BYPASS</code> on Slim models) directly to the SMC’s pulse point (<code class="language-plaintext highlighter-rouge">SMC_PLL</code>). 
This is the line SMC uses to drop the voltage and trigger the glitch loop.</p>
  </li>
</ul>

<h2 id="the-patched-hypervisor">The Patched Hypervisor</h2>
<p>As we saw earlier, <code class="language-plaintext highlighter-rouge">J-Runner</code> uses the unique CPU key to decrypt, patch, and rebuild a 
completely custom version of the NAND image. Without this permanently modified system kernel, 
the console would immediately reject anything that lacks Microsoft’s official digital signature; you wouldn’t even be able to boot custom 
dashboards like Aurora, because the operating system would still be strictly blocking unauthorized file execution.</p>

<p>To fix this we need <code class="language-plaintext highlighter-rouge">J-Runner</code> to patch the nand. The primary target of the XeBuild image is the <strong>Hypervisor (HV)</strong>, the highest privilege level of the Xbox 360 software environment.
The Hypervisor acts as a strict ring-0 security layer that polices system memory. <code class="language-plaintext highlighter-rouge">XeBuild</code> patches the Hypervisor’s assembly 
code inside the system RAM to disable three fundamental defense mechanisms:</p>

<h3 id="1-rsa-signature-elimination">1. RSA Signature Elimination</h3>
<p>The original Hypervisor checks the digital signature of every executable file (<code class="language-plaintext highlighter-rouge">.xex</code>) before letting it run. XeBuild modify the signature verification function to return “always pass”. This modification allows the console to load homebrew dashboards, game backups, and emulators flawlessly.</p>

<h3 id="2-overriding-the-wx-memory-policy">2. Overriding the W^X Memory Policy</h3>
<p>To block modern buffer-overflow exploits, the official Hypervisor strictly enforces <strong>W^X (Write Xor Execute)</strong> memory isolation. 
A memory segment can be modified (written to), or it can execute instructions, but it can never do both simultaneously. XeBuild modifies the Hypervisor’s page table allocation rules, 
allowing specific memory spaces to be writable and executable at the same time. 
This modification is what enables real-time game modding engines, custom plugins, and trainers to run concurrently in the background.</p>

<h3 id="3-removal-of-hardware-and-regional-barriers">3. Removal of Hardware and Regional Barriers</h3>
<p>XeBuild patches the subroutines handling area identifiers (NTSC, PAL, NTSC-J) converting the console into region-free device.</p>

<h2 id="conclusions">Conclusions</h2>

<p>I hope you enjoyed this deep technical article and found it useful for diving into the brilliant engineering behind the Xbox 360 mod.</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="hack" /><category term="hardware" /><category term="linux" /><summary type="html"><![CDATA[In my opinion, the Reset Glitch Hack (RGH) on the Xbox 360 stands as one of the most elegant, and great achievements in the history of console reverse engineering. In this article I will explain in details each phase of the exploit and what is happening under the hood. Notice this is NOT a step-by-step practical tutorial (there are already plenty online). Instead, this article is an explanation of those steps, so if you are curious to fully understand how was possible to mod the xbox360, this article is for you!]]></summary></entry><entry><title type="html">Kubernetes Deep Dive: From Kubelet to CNI</title><link href="https://lukemainframe.top/2026/05/05/kubernetes-deep-dive/" rel="alternate" type="text/html" title="Kubernetes Deep Dive: From Kubelet to CNI" /><published>2026-05-05T00:00:00+00:00</published><updated>2026-05-05T00:00:00+00:00</updated><id>https://lukemainframe.top/2026/05/05/kubernetes-deep-dive</id><content type="html" xml:base="https://lukemainframe.top/2026/05/05/kubernetes-deep-dive/"><![CDATA[<p>When people first get into Kubernetes, it’s easy to think of it as one big system that “just runs containers.” But under the hood, it’s more like a collection of small, specialized components constantly talking to each other.
To understand networking (and especially CNI, iptables, and eBPF), you need to zoom out first and see the bigger picture; who’s responsible for what. In this article we will give a look at the basic kubernetes blocks and some of the CNI implementation details.</p>

<p>At a high level, Kubernetes is split into two parts: the <strong>control plane</strong> that decides what should happen and the <strong>nodes</strong> that make it actually happen.</p>

<p>The control plane includes these components:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Kube-apiserver</code> that is the central entry point. Everything goes through this. Every component talks to it.</li>
  <li><code class="language-plaintext highlighter-rouge">Kube-scheduler</code> it decides <em>where</em> a pod should run.</li>
  <li><code class="language-plaintext highlighter-rouge">Kube-controller-manager</code> constantly tries to make reality match the desired state (replicas, nodes, jobs, etc.).</li>
  <li><code class="language-plaintext highlighter-rouge">etcd</code> is the database storing the entire cluster state.</li>
</ul>

<p>On the node side, things are more focused to make “things” run. There is <code class="language-plaintext highlighter-rouge">container runtime</code> (like containerd) that actually launches containers, 
<code class="language-plaintext highlighter-rouge">Kubelet</code> that makes sure containers actually run, the <code class="language-plaintext highlighter-rouge">CNI plugin</code> that wires up networking and <code class="language-plaintext highlighter-rouge">kube-proxy</code> that route traffic from Kubernetes Services to the pods.</p>

<p>In this article we will look more in detail the role of Kubelet and CNI plugins.</p>

<h2 id="kubelet--the-primary-node-agent">Kubelet — The primary node agent</h2>

<p>The Kubelet is often described as a <code class="language-plaintext highlighter-rouge">node agent</code>, but that is a bit minimalistic respect to what it really does. It’s the core component that continuously reconciles what the control plane <em>wants</em> with what’s actually running on the machine. Notice that It doesn’t make decisions like the scheduler and it doesn’t store state like etcd. Instead, it’s constantly asking:
 “What pods am I supposed to run, and are they actually running?”. If the state is not the same, it will reconcile them.
To achieve that, Kubelet operates as a loop:</p>

<ul>
  <li>It watches the API server for pod specs assigned to its node.</li>
  <li>It compares that desired state with what’s running locally.</li>
  <li>If something is missing or broken, it fixes it.</li>
</ul>

<p>For example actions that are normally performed by kubelet are:</p>
<ul>
  <li>pulling images</li>
  <li>starting/stopping containers</li>
  <li>restarting crashed containers</li>
  <li>reporting status back to the control plane</li>
</ul>

<p>However, is important to notice that kubelet does not get involved in any networking tasks. 
When a pod is about to start, Kubelet prepares everything, from filesystem to volumes to runtime configuration, but when it comes to networking, it hands off responsibility to CNI essentially saying: “Here’s a new container. Give it an IP and make sure it can talk to the cluster.”</p>

<p>This design is an architectural choice of Kubernetes. <strong>In fact, Kubernetes doesn’t include a native networking solution by default</strong>; instead, it delegates that entire responsibility to external CNI compliant plugins.</p>

<h2 id="cni--the-network-layer">CNI — the network layer</h2>

<p>CNI stands for Container Network Interface. It’s one of those things that sounds like a service, but it’s really just a <strong>contract</strong>.
Indeed, strictly speaking is just a set of specifications that defines:</p>
<ul>
  <li>how a runtime asks for networking</li>
  <li>what inputs/outputs look like</li>
  <li>how plugins should behave</li>
</ul>

<p>Everything else is implementation detail. kubernetes only state that  <em>“Every pod gets its own IP, and all pods need to be able to talk to each other directly (no NAT in between).”</em>
This sounds simple, but it creates a big challenge:</p>

<ul>
  <li>How do you make pod IPs routable across nodes?</li>
  <li>How do you scale that to thousands of pods?</li>
</ul>

<p>That is where different CNI plugins take different approaches that will see in the next paragraph.
So, when Kubelet asks the CNI plugin to “set up” a pod, the plugin executes a series of low-level Linux networking operations. This is where things get very real and very close to the OS.
To wire up a pod, the plugin will:</p>

<ul>
  <li>create a new network namespace for the pod (isolated networking stack)</li>
  <li>create a <strong>veth pair</strong> (two virtual interfaces connected like a cable)</li>
  <li>move one end into the pod namespace</li>
  <li>attach the other end to the host network (bridge, overlay, or routing layer)</li>
  <li>assign an IP address</li>
  <li>configure routes so packets know where to go</li>
</ul>

<p>Once pods have IPs, how does traffic actually get routed the right pod behind a Service? This is where <code class="language-plaintext highlighter-rouge">kube-proxy</code> enters the picture.
<code class="language-plaintext highlighter-rouge">kube-proxy</code> runs on every node and watches the API server for Service and Endpoint changes. Its job is to make sure that when you send traffic to a Service IP, it gets forwarded to one of the correct backend pods.
In the traditional setup, <code class="language-plaintext highlighter-rouge">kube-proxy</code> implements this using iptables.
It translates high-level Kubernetes concepts like:</p>
<ul>
  <li>“Service X points to Pods A, B, and C”
into low-level packet handling rules inside the Linux kernel.</li>
</ul>

<h3 id="a-deeper-look-at-cni-implementations--cilium-case">A deeper look at CNI implementations -Cilium case</h3>
<p>As we saw before, kubernetes only give specifications for CNI leave a lot of freedom to the actual implementation. 
Some plugins (like Flannel) use overlays which encapsulate traffic between nodes.
Others (like Calico) rely more on routing and BGP. Newer ones like Cilium move the roting logic into the kernel using eBPF.</p>

<p>But regardless of implementation, they all sit behind the same CNI interface. <strong>To Kubelet, they look identical.</strong></p>

<p>Ok, CNI plugins are responsible for wiring up networking, but how do they actually control traffic?</p>

<p>This is where the difference between traditional approaches (iptables) and modern ones (eBPF) really matters.
For a long time, Kubernetes networking leaned heavily on iptables.
iptables works by defining chains of rules that every packet is checked against. When a packet enters the system, it’s evaluated step by step until a matching rule is found. That rule might say:</p>

<ul>
  <li>forward this packet</li>
  <li>rewrite its destination (NAT)</li>
  <li>drop it</li>
  <li>send it somewhere else</li>
</ul>

<p>In Kubernetes, this mechanism gets used for several things at once:</p>

<ul>
  <li>implementing Services (ClusterIP, NodePort)</li>
  <li>load balancing between pods</li>
  <li>handling outbound traffic (SNAT)</li>
  <li>enforcing some network policies</li>
</ul>

<p>Over time, as the number of services and pods grows, the number of rules grows with it. And because iptables evaluates rules sequentially, performance becomes increasingly tied to how long those chains are and this is where eBPF changes the game.
Instead of building long rule chains, eBPF allows you to attach small, purpose-built programs directly into the Linux kernel. These programs can intercept packets at various points and make decisions immediately, often using efficient data structures like maps and hash tables.</p>

<p>So instead of saying: “Check rule 1, then rule 2, then rule 3…”</p>

<p>with eBPF you’re saying:</p>

<p>“Run this program that already knows how to handle this packet.”</p>

<p>This reduces overhead and makes behavior far more performant at scale. Cilium is a CNI plugin that fully embrace this model.
Rather than relying on iptables for service routing and policy enforcement, Cilium installs eBPF programs into the kernel that handle:</p>

<ul>
  <li>service load balancing</li>
  <li>network policies (including L7 in some cases)</li>
  <li>packet forwarding decisions</li>
</ul>

<p>In additions Cilium can replace kube-proxy entirely. Instead of using iptables to implement Services routing, it handles them directly via eBPF.
From the outside, your Kubernetes cluster still behaves the same with Pods getting IPs, Services and Traffic flows working fine, but internally, the mechanics are very different.
With eBPF (via Cilium), the system becomes more efficient with packets handled by logic in the kernel that can evolve without rewriting massive rule sets.</p>

<p>This leads to higher performance at scale, faster updates (no need to rebuild long rule chains) and deeper observability, since eBPF can inspect traffic more intelligently.</p>

<h2 id="key-takeaways">Key Takeaways</h2>

<p>At this point, a short resume to make things clearer:</p>

<ul>
  <li>The control plane (API server, scheduler, controllers) decides what should exist.</li>
  <li>Kubelet ensures pods actually run on a node.</li>
  <li>The CNI layer gives those pods a network identity.</li>
  <li>The underlying implementation (iptables or eBPF via Cilium) determines how traffic is handled.</li>
</ul>

<p>If you zoom out far enough, Kubernetes networking is less about “one technology” and more about <strong>layers of delegation</strong>:</p>

<ul>
  <li>Kubernetes delegates networking to CNI</li>
  <li>CNI plugins delegate packet handling to the kernel</li>
  <li>The kernel executes that logic via iptables or eBPF</li>
</ul>

<p>I hope this article made Kubernetes appear now much less mysterious.</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="secops" /><category term="technology" /><category term="linux" /><summary type="html"><![CDATA[When people first get into Kubernetes, it’s easy to think of it as one big system that “just runs containers.” But under the hood, it’s more like a collection of small, specialized components constantly talking to each other. To understand networking (and especially CNI, iptables, and eBPF), you need to zoom out first and see the bigger picture; who’s responsible for what. In this article we will give a look at the basic kubernetes blocks and some of the CNI implementation details.]]></summary></entry><entry><title type="html">Kobo Glo HD Hack: Freeing The E-Reader and Expanding Its Memory</title><link href="https://lukemainframe.top/2025/11/19/kobo-glohd-hack/" rel="alternate" type="text/html" title="Kobo Glo HD Hack: Freeing The E-Reader and Expanding Its Memory" /><published>2025-11-19T00:00:00+00:00</published><updated>2025-11-19T00:00:00+00:00</updated><id>https://lukemainframe.top/2025/11/19/kobo-glohd-hack</id><content type="html" xml:base="https://lukemainframe.top/2025/11/19/kobo-glohd-hack/"><![CDATA[<p>The Kobo Glo HD is a fantastic e-reader, but what truly makes it special is the level of freedom and hackability it offers. 
This freedom is precisely why I chose it over competitors like the Kindle. In this post I will describe how to disable the intrusive cloud login and perform a major 
memory upgrade. Despite performed on the GlovoHD, What I write here I probably true for any Kobo device.</p>

<h2 id="skipping-the-cloud-and-gaining-independence">Skipping the Cloud and Gaining Independence</h2>

<p>One of the best initial hacks is to force the Kobo into local-only account mode, which allows you to completely bypass 
the online registration and cloud login features. This is a must for those who prefer to sideload all their books and maintain privacy.</p>

<p>To enable local-only mode, you need to edit the Kobo’s configuration file. I did this on a freshly reset device, before you even turn it on the first time.</p>

<ol>
  <li>Access the Config File: Connect your Kobo to your PC and navigate to the <code class="language-plaintext highlighter-rouge">.kobo/Kobo</code> folder.</li>
  <li>Locate Kobo <code class="language-plaintext highlighter-rouge">Kobo eReader.conf</code>: Open this file using a plain text editor (like Notepad++ or VS Code).</li>
  <li>
    <p>Under the <code class="language-plaintext highlighter-rouge">[ApplicationPreferences]</code> section, add the following line:</p>

    <pre><code class="language-TOML"> SideloadedMode=true
</code></pre>
  </li>
  <li>Save and Eject: Save the file, safely eject the Kobo, and reboot.</li>
</ol>

<p>Now, your Kobo will prompt you for local details instead of forcing a cloud registration, giving you complete sideloading freedom.
I have also tested this for the Clara HD version.</p>

<h2 id="the-hardware-fix-repairing-the-micro-usb-port">The Hardware Fix: Repairing the Micro-USB Port</h2>

<p>After few years of usage even a nice device like this can break up. For me the weak spot was the micro-USB connector. 
It eventually broke, preventing both charging and PC connectivity. 
So, I had to fix it. I considered upgrading the connector to USB-C but Since I already had similar Surface-Mount Technology (SMT) micro-USB connectors 
in my home lab, and I rarely need to charge the ebook reader, I decided to stick with the original port type instead of upgrading it.</p>

<h3 id="the-soldering-challenge-medium-difficulty">The Soldering Challenge (Medium Difficulty)</h3>

<p>The repair required careful soldering, a task of medium difficulty due to the small size of the contacts:</p>
<ul>
  <li>Open the Case: the case is not glued but only clicked. so you can open it with help from a flat screwdriver</li>
  <li>Old Connector Removal: I first carefully desoldered the broken port and cleaned the remaining tin residue from the pads on the PCB.</li>
  <li>Alignment and Stabilization: The key was alignment. I placed the new connector, first soldering the two external anchor wings (the large structural tabs). 
This stabilized the connector perfectly. This worked fine because the new SMT micro usb port was basically identical to the old one!</li>
  <li>Data Pins: Finally, I soldered the four data/power pins to the board. Since the connector was already aligned and stable, this part was straightforward.</li>
</ul>

<p>The repair was successful on the first attempt! The Kobo was charging and connecting to the PC again.</p>

<h2 id="the-upgrade-4gb-to-32gb-memory-expansion">The Upgrade: 4GB to 32GB Memory Expansion</h2>

<p>While the Kobo was open for the USB repair, I couldn’t resist to see what was inside.
I immediately noticed an internal MicroSD card. Driven by curiosity, I unplugged the card and connected it to my PC. 
It turns out, this card is the device’s main storage, housing everything: the OS partition, a Recovery partition, and the Data partition for user books. 
My Kobo Glo HD came with a small 4GB card…clearly worth an upgrade.</p>

<h3 id="the-expansion-process-from-4gb-to-32gb">The Expansion Process (From 4GB to 32GB)</h3>

<p>The goal was to move everything to a new 32GB card and expand the user storage.</p>
<ol>
  <li>Full Image Backup: Using the dd command, I created a byte-for-byte clone of the original 4GB card. This is essential to transfer the OS and partitions exactly as they were.</li>
</ol>

<pre><code class="language-Bash">
    # Backup the original 4GB image to a file
    sudo dd if=/dev/mmcblk0 of=~/kobo_reader.img bs=4M status=progress
    # Restore the image onto the new 32GB card (inverse command)
    sudo dd if=~/kobo_reader.img of=/dev/mmcblk0 bs=4M status=progress
</code></pre>

<ol>
  <li>
    <p>Resizing the Partition: After the image was restored to the 32GB card, I used <code class="language-plaintext highlighter-rouge">disk</code> to modify the partition table. 
The original image still defined the Data partition (partition 3) size as 4GB, so I extended the Data partition (<code class="language-plaintext highlighter-rouge">/dev/mmcblk0p3</code>) to utilize 
all the newly available space on the 32GB card. This step adjusted the boundary in the partition table.</p>
  </li>
  <li>
    <p>The FAT32 Filesystem expansion: The Data partition was formatted as FAT32. Typically, after resizing the partition, you expand the filesystem to fill the new space. 
However, When I attempted to use <code class="language-plaintext highlighter-rouge">fatresize</code>  (e.g., <code class="language-plaintext highlighter-rouge">sudo fatresize -s max /dev/mmcblk0p3</code>), the command failed to recognize the filesystem correctly 
and was unable to complete the resize operation.</p>
  </li>
</ol>

<p>The Workaround was basically: Backup, Delete, Recreate. Since direct filesystem resizing failed, I opted for a clean, reliable recreation:</p>
<ol>
  <li>I backed up all the original data files from the existing FAT32 filesystem.</li>
  <li>I re-formatted the data partition as FAT32, ensuring it had the required volume label:</li>
</ol>

<pre><code class="language-Bash">  sudo mkfs.vfat -F 32 -n "KOBOeReader" /dev/mmcblk0p3
</code></pre>

<ol>
  <li>Finally, I restored the original data files to the new, massive partition.</li>
</ol>

<p>After inserting the 32GB card and reassembling the Kobo, it booted perfectly! The e-reader now fully recognizes and utilizes the 32GB of expanded memory, 
a massive increase that is even reflected in the device’s settings menu.</p>

<h1 id="final-thoughts">Final Thoughts</h1>

<p>With a disabled cloud login, a fully repaired and reliable charging port, and a massive 32GB of internal storage, this Kobo Glo HD is now better than new. 
This project proves that the Kobo is not just an e-reader but a versatile piece of hardware that truly rewards the technically inclined people with easy to fix hardware and nice freedom… Good job Kobo!</p>

<p>As always I hope you enjoyed this article.</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="hack" /><category term="hardware" /><category term="linux" /><summary type="html"><![CDATA[The Kobo Glo HD is a fantastic e-reader, but what truly makes it special is the level of freedom and hackability it offers. This freedom is precisely why I chose it over competitors like the Kindle. In this post I will describe how to disable the intrusive cloud login and perform a major memory upgrade. Despite performed on the GlovoHD, What I write here I probably true for any Kobo device.]]></summary></entry><entry><title type="html">Using Terraform On-Premises: Extending GitOps Beyond the Cloud</title><link href="https://lukemainframe.top/2025/10/17/terraform-on-premises/" rel="alternate" type="text/html" title="Using Terraform On-Premises: Extending GitOps Beyond the Cloud" /><published>2025-10-17T00:00:00+00:00</published><updated>2025-10-17T00:00:00+00:00</updated><id>https://lukemainframe.top/2025/10/17/terraform-on-premises</id><content type="html" xml:base="https://lukemainframe.top/2025/10/17/terraform-on-premises/"><![CDATA[<p>When people think about Terraform, they usually associate it with cloud automation — AWS, Azure, GCP, and similar environments. 
However, Terraform’s true power lies not in being cloud-specific, but in its ability to describe and manage infrastructure declaratively, regardless of where it runs. Over the past months, I’ve been successfully applying Terraform principles to an on-premises environment, and the results have been both powerful and elegant.</p>

<h2 id="what-terraform-is">What Terraform Is</h2>

<p>Terraform, created by HashiCorp, is an open-source tool for Infrastructure as Code (IaC). It allows you to define your 
entire infrastructure, from virtual machines to DNS records, in human-readable configuration files. 
Terraform then automates the provisioning and lifecycle management of these resources.
With Terraform, you no longer need to click through dashboards or manually configure infrastructure: everything is defined in code. 
Creating a virtual machine, for example, is as simple as writing a 10-line block that declares its CPU, memory, and network. 
Want to change the CPU count? Just edit a single number. Want to remove it? Delete the block. 
It’s like magic, your entire infrastructure becomes reproducible, versioned, and self-documenting, with no need to 
remember how a particular network or database was configured.
The key advantage is idempotency: Terraform knows what already exists and only applies the necessary changes to reach the desired state. 
It also provides a plan phase, where changes can be reviewed before being executed, ensuring predictability and control.
For example, suppose you have a Terraform configuration that defines a Kubernetes namespace called monitoring with a 
Helm chart deploying Prometheus. If you later modify the Helm chart version or add a new configuration value, you can run:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terraform plan
</code></pre></div></div>
<p>Terraform will show a detailed execution plan, such as:</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># helm_release.prometheus will be updated in-place</span>
<span class="err">~</span> <span class="k">resource</span> <span class="s2">"helm_release"</span> <span class="s2">"prometheus"</span> <span class="p">{</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"25.5.0"</span> <span class="err">-&gt;</span> <span class="s2">"25.6.1"</span>
      <span class="nx">values</span>  <span class="p">=</span> <span class="p">[</span>
          <span class="p">...</span>
          <span class="err">+</span> <span class="s2">"alertmanager.enabled=true"</span>
      <span class="p">]</span>
<span class="p">}</span>
<span class="nx">Plan</span><span class="err">:</span> <span class="mi">0</span> <span class="nx">to</span> <span class="nx">add</span><span class="err">,</span> <span class="mi">1</span> <span class="nx">to</span> <span class="nx">change</span><span class="err">,</span> <span class="mi">0</span> <span class="nx">to</span> <span class="nx">destroy</span><span class="err">.</span>
</code></pre></div></div>
<p>This preview step lets you clearly see what Terraform intends to change, (in this case, upgrading Prometheus and enabling 
Alertmanager), before you apply the modifications. In a GitOps setup, this becomes even more powerful. 
The <code class="language-plaintext highlighter-rouge">terraform plan</code> command can be automatically triggered whenever a <code class="language-plaintext highlighter-rouge">Pull Request</code> is opened. The plan output is 
posted as a comment or status check in the PR, allowing the team to review the proposed infrastructure changes just like application code.
Once the PR is approved and merged, a pipeline can then safely execute <code class="language-plaintext highlighter-rouge">terraform apply</code>, applying only the reviewed and approved changes. 
This ensures that every infrastructure modification is traceable, peer-reviewed, and auditable; aligned with gitops principles.</p>

<h3 id="terraform-beyond-the-cloud">Terraform Beyond the Cloud</h3>
<p>While Terraform is commonly used for cloud deployments, its provider ecosystem allows it to manage almost anything with 
an API, including on-premises infrastructure. This means you can use it to orchestrate:</p>
<ul>
  <li>Kubernetes clusters running locally</li>
  <li>Bare-metal servers</li>
  <li>VMware or OpenStack virtual environments</li>
  <li>Network and storage configurations</li>
  <li>Helm charts and Kubernetes resources</li>
</ul>

<p>In my case, the underlying infrastructure, Kubernetes clusters, load balancers, and base networking, is bootstrapped using Ansible. 
Ansible is great for low-level provisioning and configuration, especially when Terraform can’t directly manage the underlying hardware,
making it ideal for setting up Kubernetes clusters or configuring operating systems.</p>

<p>Once that foundation is in place, Terraform takes over. Using its Kubernetes and Helm providers, Terraform manages the higher-level 
deployments: namespaces, ingress controllers, monitoring stacks, applications, and policies. This combination allows both flexibility and 
consistency: Ansible handles the initial provisioning, while Terraform governs the continuous state of the platform and applications.</p>

<h2 id="alignment-with-gitops-principles">Alignment with GitOps Principles</h2>

<p>This hybrid approach fits perfectly within the GitOps philosophy.
<em>GitOps</em> is about using Git as the single source of truth for your infrastructure and application configurations. 
All changes go through version-controlled pull requests, and automated pipelines apply those changes to the environment. 
The principles are simple but powerful:</p>

<ul>
  <li>Declarative infrastructure — define your desired state in code.</li>
  <li>Versioned and auditable — all changes are tracked in Git.</li>
  <li>Automated reconciliation — systems automatically converge toward the declared state.</li>
  <li>Observability and feedback — visibility into what’s running versus what’s declared.</li>
</ul>

<p>By using Terraform configurations stored in Git, combined with CI/CD or GitOps tools (like Argo CD or Flux), you can bring 
these same principles to your <strong>on-premises environments</strong>. Every infrastructure change becomes a pull request, 
every deployment is reproducible, and rollbacks are as simple as reverting a commit.</p>

<h2 id="benefits">Benefits</h2>
<p>Since adopting this approach, I’ve seen many benefits:</p>
<ul>
  <li>Consistency across environments: development, staging, production, etc are defined the same way.</li>
  <li>Reproducibility: rebuilding an environment from scratch is straightforward. This is extremely important for disaster recovery!</li>
  <li>Transparency and collaboration: infrastructure changes are reviewed just like application code.</li>
  <li>Reduced drift: Terraform’s state tracking ensures the environment matches the desired configuration.</li>
  <li>Seamless integration with existing automation tools like Ansible, Jenkins, or Argo CD.</li>
</ul>

<h1 id="final-thoughts">Final Thoughts</h1>

<p>Terraform is not limited to the cloud. It’s a universal IaC tool, and when combined with Ansible and GitOps practices, 
it enables a modern, automated, and auditable on-premises infrastructure workflow.</p>

<p>This approach has been a success in my experience, bridging the gap between traditional datacenter automation 
and modern DevOps methodologies. It proves that the power of declarative infrastructure and GitOps 
principles is not bound to any specific platform: <strong>it’s a mindset that can transform how we manage infrastructure anywhere</strong>.</p>

<p>As always I hope this article was usefull.</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="secops" /><category term="terraform" /><category term="technology" /><summary type="html"><![CDATA[When people think about Terraform, they usually associate it with cloud automation — AWS, Azure, GCP, and similar environments. However, Terraform’s true power lies not in being cloud-specific, but in its ability to describe and manage infrastructure declaratively, regardless of where it runs. Over the past months, I’ve been successfully applying Terraform principles to an on-premises environment, and the results have been both powerful and elegant.]]></summary></entry><entry><title type="html">How Bitnami Betrayed the Open Source Community</title><link href="https://lukemainframe.top/2025/09/30/the-great-bitnami-betrayal/" rel="alternate" type="text/html" title="How Bitnami Betrayed the Open Source Community" /><published>2025-09-30T00:00:00+00:00</published><updated>2025-09-30T00:00:00+00:00</updated><id>https://lukemainframe.top/2025/09/30/Bitnami-betrayal</id><content type="html" xml:base="https://lukemainframe.top/2025/09/30/the-great-bitnami-betrayal/"><![CDATA[<p>For years, Bitnami was the quiet backbone of Kubernetes deployments. 
Their Helm charts and container images were a trusted, stable foundation for developers, startups, and even enterprise teams running production systems. 
If you wanted something that “just worked”, secure, well-maintained, and up-to-date, Bitnami was where you went.
Over the years, they delivered high-quality, production ready software that countless people came to depend on.</p>

<p>But now, under the ownership of <code class="language-plaintext highlighter-rouge">Broadcom</code>, all that trust has been taken down. Instead of continuing their open source legacy, they’ve decided to cash in at the expense of the very community that made Bitnami what it was.</p>

<p>Broadcom has launched a new <code class="language-plaintext highlighter-rouge">“Secure Images”</code> subscription product and, in the process, effectively pulled the rug out from under thousands of developers and companies worldwide.</p>

<p>This wasn’t a simple “end of life” notice. That was an act that ensured maximum pain and minimal time for users to react.</p>

<h2 id="the-deprecation-that-destroys">The Deprecation That Destroys</h2>

<p>Effective August 28th, 2025, Bitnami did not just stop updates to their free tier; they actively purged all versioned, stable image tags from the main <code class="language-plaintext highlighter-rouge">docker.io/bitnami</code> repository and moved to a new repository: <code class="language-plaintext highlighter-rouge">docker.io/bitnamilegacy</code>. In doing so, many tags were renamed or altered, breaking automated deployments and making migration intentionally more painful.</p>

<p>For teams that followed best practices by pinning versions to avoid surprises,  this change turned stable systems into time bombs.
Overnight, critical services that had been running fine for years were suddenly failing  in the attempts to pull a versioned image from the old path (e.g., <code class="language-plaintext highlighter-rouge">docker.io/bitnami/postgresql:13.7.0</code>), getting <code class="language-plaintext highlighter-rouge">ImagePullBackOff</code> or <code class="language-plaintext highlighter-rouge">ErrImagePull</code> errors.</p>

<p>And if you wanted an “easy fix”? Well, Broadcom has a solution: pay for their new subscription service, that is reportedly priced in the <strong><em>tens of thousands of dollars annually</em></strong>. The message is basically “pay us or break”.</p>

<h2 id="quick-fix">Quick fix</h2>
<p>To stop the immediate, bleeding in your systems, you are forced to use the legacy images in your Helm chart configurations. For example in case of <code class="language-plaintext highlighter-rouge">docker.io/bitnami/postgresql:13.7.0</code>  you can use <code class="language-plaintext highlighter-rouge">docker.io/bitnamilegacy/postgresql:13.7.0</code>. Just make sure the tag is present in the new repo. The change is simple in syntax but do not rely on for long-term since those images are static and will have long term security implications.</p>

<h2 id="the-path-forward-migrate-and-never-look-back">The Path Forward: Migrate and Never Look Back</h2>

<p>Bitnami and Broadcom have made their intentions clear: they are no longer interested in the community that built them up. They are only interested in extracting maximum profit from enterprises caught in the migration trap.</p>

<p>The real fix is simple: migrate away completely.</p>

<ul>
  <li>Audit all your Bitnami Dependencies:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-A</span> <span class="nt">-o</span> json | jq <span class="nt">-r</span> <span class="s1">'..|.image? // empty'</span> | <span class="nb">sort</span> <span class="nt">-u</span> | <span class="nb">grep</span> <span class="nt">-i</span> bitnami 
</code></pre></div>    </div>
  </li>
  <li>Embrace Community Alternatives: Look to official upstream projects, the Kubernetes Community Helm Charts, or trusted vendors not owned by broadcom that value the open-source spirit.</li>
</ul>

<h1 id="final-takeaway">Final Takeaway</h1>
<p>This is a textbook example of vendor lock-in, where the vendor waits until you are fully reliant, then flips the switch to a massive paywall.
The Bitnami deprecation is a business maneuver that provides a clear warning: do not build your production on a foundation controlled by a company that has proven it will break your systems for profit.</p>

<p>The real takeaway? Keep your SecOps and DevOps teams sharp (and well payed :P). Their skill is the only thing that will fix this mess without forcing you to surrender to the Bitnami pay-or-break blackmail.</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="secops" /><category term="docker" /><category term="technology" /><summary type="html"><![CDATA[For years, Bitnami was the quiet backbone of Kubernetes deployments. Their Helm charts and container images were a trusted, stable foundation for developers, startups, and even enterprise teams running production systems. If you wanted something that “just worked”, secure, well-maintained, and up-to-date, Bitnami was where you went. Over the years, they delivered high-quality, production ready software that countless people came to depend on.]]></summary></entry><entry><title type="html">EKS Terraform 21.x IRSA Issue: How to Fix Karpenter on Fargate</title><link href="https://lukemainframe.top/2025/05/30/karpenter-fargate-irsa-fix/" rel="alternate" type="text/html" title="EKS Terraform 21.x IRSA Issue: How to Fix Karpenter on Fargate" /><published>2025-08-06T00:00:00+00:00</published><updated>2025-08-06T00:00:00+00:00</updated><id>https://lukemainframe.top/2025/05/30/karpenter-on-fargate-irsa-fix</id><content type="html" xml:base="https://lukemainframe.top/2025/05/30/karpenter-fargate-irsa-fix/"><![CDATA[<p>In a previous <a href="/2025/02/26/karpenter-on-aws/">article</a>, I explained why running Karpenter on AWS Fargate was highly recommended. 
The key motivation is that Karpenter itself should not run on the managed nodes it provisions. So a simple solution would be to run it on a small dedicated fargate instance.</p>

<p>Since that article, AWS has also introduced a new “EKS Cluster Auto-mode”, which provisions control plane and data plane resources more seamlessly. However, this comes with increased cost and reduced control. For many teams, especially where cost control is important, sticking with traditional EKS + Fargate still makes more sense.</p>

<p>However, the Terraform EKS module introduced breaking changes in version <a href="https://github.com/terraform-aws-modules/terraform-aws-eks/releases/tag/v21.0.0">21.0.0</a>, notably removing native IRSA (IAM Roles for Service Accounts) support. 
This has significant implications, since by removing IRSA:</p>

<ul>
  <li>Any workloads relying on it to assume IAM roles (e.g., Karpenter) will break.</li>
  <li>Fargate workloads, which must use IRSA for AWS permissions will break.</li>
</ul>

<p>While EKS now supports Pod Identity as an alternative, Fargate cannot leverage that since is not <a href="https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html">supported</a>. The core reason is that EKS Pod Identity relies on a dedicated agent (add-on) that runs as a DaemonSet on each worker node to provide credentials. Since AWS Fargate does not support DaemonSets, this agent cannot be deployed, making Pod Identity incompatible with Fargate. For pods on Fargate, you must continue to use IAM Roles for Service Accounts (IRSA).</p>

<p>It is particularly surprising that the EKS Terraform module developers, when deprecating native IRSA support in favor of Pod Identity, did not provide a documented solution for common use cases like Karpenter on Fargate, which cannot use the new approach..</p>

<p>So if you’re using Karpenter on Fargate, and you’re now stuck… you need to manually restore IRSA support. but before i show you how, lets clarify the difference between IRSA and Pod Identity.</p>

<h1 id="irsa-vs-pod-identity">IRSA vs Pod Identity</h1>

<h2 id="what-is-irsa">What is IRSA?</h2>

<p>IAM Roles for Service Accounts (IRSA) is the original and widely adopted method of securely assigning AWS IAM permissions to Kubernetes pods.
With IRSA, the flow works like this:</p>
<ul>
  <li>You create an IAM Role with a trust policy that allows the EKS OIDC provider to assume it.</li>
  <li>The trust policy ensures only a specific service account in a specific namespace can use that role.</li>
  <li>The Kubernetes service account is annotated with the role ARN.</li>
  <li>When the pod starts, EKS injects a web identity token into the pod, which AWS STS uses to validate and issue temporary credentials.</li>
</ul>

<p>Key advantages:</p>
<ul>
  <li>Secure, least-privilege model.</li>
  <li>Easy to audit and manage.</li>
  <li>Works perfectly with EKS and Fargate.</li>
</ul>

<h2 id="what-is-pod-identity">What is Pod Identity?</h2>

<p>EKS Pod Identity is a newer approach introduced by AWS in 2024, offering another way to associate IAM roles with pods.</p>

<p>Instead of relying on OIDC providers and complex trust policies, you create an association that directly links an IAM role to a Kubernetes service account within a specific namespace using the EKS API. This new approach relies on a daemonset running on worker nodes, and as stated above, is not supported by fargate.</p>

<h1 id="manually-restoring-irsa-for-karpenter-on-fargate">Manually Restoring IRSA for Karpenter on Fargate</h1>

<p>If you’re using the EKS Terraform module version 21.x or higher, the built-in IRSA configuration for Karpenter on Fargate is no longer available. 
To restore this functionality, you must manually define the necessary IAM role trust policy and pass it to the Karpenter module.
This approach effectively recreates the IRSA trust relationship that was previously managed by the module, allowing your Karpenter pods on Fargate to assume their required IAM role.
if you do not restore the policy, karpenter will fail to start with this error message</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> failed to retrieve credentials, operation error STS: AssumeRoleWithWebIdentity, https response error StatusCode: 403, RequestID: 3d312d21-22d2-4d80-c222-227ae8c27cc1, api error AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity"
</code></pre></div></div>

<p>To restore the policy, here is the snippet of terraform code that implements these changes. It has been adapted from a GitHub <a href="https://github.com/terraform-aws-modules/terraform-aws-eks/issues/3394">discussion</a></p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"karpenter_controller_assume_role_policy"</span> <span class="p">{</span>
    <span class="nx">statement</span> <span class="p">{</span>
        <span class="nx">effect</span> <span class="p">=</span> <span class="s2">"Allow"</span>
    
        <span class="nx">principals</span> <span class="p">{</span>
          <span class="nx">type</span>        <span class="p">=</span> <span class="s2">"Federated"</span>
          <span class="nx">identifiers</span> <span class="p">=</span> <span class="p">[</span><span class="k">module</span><span class="p">.</span><span class="nx">YOUR_EKS_CLUSTER</span><span class="p">.</span><span class="nx">oidc_provider_arn</span><span class="p">]</span>
        <span class="p">}</span>
    
        <span class="nx">condition</span> <span class="p">{</span>
          <span class="nx">test</span>     <span class="p">=</span> <span class="s2">"StringEquals"</span>
          <span class="k">variable</span> <span class="p">=</span> <span class="s2">"</span><span class="k">${module</span><span class="p">.</span><span class="nx">YOUR_EKS_CLUSTER</span><span class="p">.</span><span class="nx">oidc_provider</span><span class="k">}</span><span class="s2">:sub"</span>
          <span class="nx">values</span>   <span class="p">=</span> <span class="p">[</span><span class="s2">"system:serviceaccount:karpenter:karpenter"</span><span class="p">]</span>
        <span class="p">}</span>
    
        <span class="nx">condition</span> <span class="p">{</span> <span class="c1"># Required for OIDC trust</span>
          <span class="nx">test</span>     <span class="p">=</span> <span class="s2">"StringEquals"</span>
          <span class="k">variable</span> <span class="p">=</span> <span class="s2">"</span><span class="k">${module</span><span class="p">.</span><span class="nx">YOUR_EKS_CLUSTER</span><span class="p">.</span><span class="nx">oidc_provider</span><span class="k">}</span><span class="s2">:aud"</span>
          <span class="nx">values</span>   <span class="p">=</span> <span class="p">[</span><span class="s2">"sts.amazonaws.com"</span><span class="p">]</span>
        <span class="p">}</span>
    
        <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"sts:AssumeRoleWithWebIdentity"</span><span class="p">]</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>and then link this policy to karpenter:</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="s2">"karpenter"</span> <span class="p">{</span>
  <span class="p">...</span>
  <span class="nx">iam_role_source_assume_policy_documents</span> <span class="p">=</span> <span class="p">[</span>
    <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">karpenter_controller_assume_role_policy</span><span class="p">.</span><span class="nx">json</span><span class="p">,</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I’ve tested these changes and can confirm that Karpenter is successfully able to assume the role and provision new nodes.</p>

<p>As always, I hope you found this article helpful.</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="secops" /><category term="aws" /><category term="technology" /><summary type="html"><![CDATA[In a previous article, I explained why running Karpenter on AWS Fargate was highly recommended. The key motivation is that Karpenter itself should not run on the managed nodes it provisions. So a simple solution would be to run it on a small dedicated fargate instance.]]></summary></entry><entry><title type="html">Docker Compose: Automated Updates with Watchtower</title><link href="https://lukemainframe.top/2025/05/30/automated-updates-with-watchtower/" rel="alternate" type="text/html" title="Docker Compose: Automated Updates with Watchtower" /><published>2025-05-30T00:00:00+00:00</published><updated>2025-05-30T00:00:00+00:00</updated><id>https://lukemainframe.top/2025/05/30/automatic-update-watchtower</id><content type="html" xml:base="https://lukemainframe.top/2025/05/30/automated-updates-with-watchtower/"><![CDATA[<p>In the world where everything is running in containers, staying up-to-date with the latest image versions is mandatory for security and performance improvements. 
While Docker Compose simplifies the orchestration of multi-container applications, manually monitoring and updating the images defined in 
your <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> file can become a time-consuming task. This is where Watchtower steps in as your automated guardian, 
ensuring your Docker Compose services are always running on the newest images.</p>

<h1 id="what-is-watchtower">What is Watchtower?</h1>

<p>Watchtower is a lightweight and powerful open-source Docker container that automates the process of updating your running Docker containers. 
It basically monitors your running containers and, when it detects a new version of their base image on Docker Hub (or other registry),
it gracefully stops the old container, pulls the updated image, and restarts a new container using the same configuration 
parameters (environment variables, volumes, network settings, etc.) defined in your Docker Compose file.</p>

<p>Integrating Watchtower into your Docker Compose workflow offers many advantages:</p>
<ul>
  <li>Automation: The most significant benefit is the automatic image updates. Once configured, Watchtower handles the entire update cycle without requiring manual intervention.</li>
  <li>Security: Regularly updating container images is an important security practice. Watchtower ensures you’re always running the latest versions, which often include critical security patches.</li>
  <li>Staying Up-to-Date with Features: New image versions often bring performance improvements and new functionalities.</li>
</ul>

<p>Before we start to dig deeper into Watchtower usage, I want to clarify few things first. 
While <code class="language-plaintext highlighter-rouge">Kubernetes</code> and tools like <code class="language-plaintext highlighter-rouge">Minikube</code> offer powerful orchestration for complex deployments, 
for a small home environment, Docker Compose remains significantly more convenient and easier to manage. Its simplicity in defining and running multi-container 
applications with a single docker-compose.yml file is ideal for personal projects and home servers. Watchtower perfectly complements this setup by automating image updates,
a task often manually handled in such environments.</p>

<p>It’s also important to note that Watchtower does not change the tag specified in your Docker Compose file; rather, it monitors 
the registry for updates to the image associated with the specified tag. 
This means that using <code class="language-plaintext highlighter-rouge">:latest</code> will trigger an update whenever a new version is pushed to the latest tag. 
However, if you specify a fixed tag like <code class="language-plaintext highlighter-rouge">:1.2.1</code>, Watchtower will only update if a new image is pushed to that exact tag 
and <strong>will not</strong> automatically pull versions tagged differently, such as <code class="language-plaintext highlighter-rouge">:1.2.2</code>.</p>

<p>Also, be aware that <strong>Watchtower automatic upgrade come with the risk of unexpected breaking of applications</strong> (eg latest tag moved across major version updates).
Because these potential issues it should not be used anywhere else except your home/hobby projects, as specified in their <a href="https://github.com/containrrr/watchtower">official repo</a>:</p>

<blockquote>
  <p>Watchtower is intended to be used in homelabs, media centers, local dev environments, and similar. We do not recommend using Watchtower in a commercial or production environment. If that is you, you should be looking into using Kubernetes. If that feels like too big a step for you, please look into solutions like MicroK8s and k3s that take away a lot of the toil of running a Kubernetes cluster.</p>
</blockquote>

<h1 id="how-to-integrate-watchtower-with-docker-compose">How to Integrate Watchtower with Docker Compose</h1>

<p>Imagine a typical Docker Compose setup where you have an application and a database running as services. 
In this common case, integrating Watchtower is very easy. You essentially add Watchtower as another service to your <code class="language-plaintext highlighter-rouge">docker-compose.yaml</code> file.</p>

<p>Here’s a general example of how to configure Watchtower in your <code class="language-plaintext highlighter-rouge">docker-compose.yml</code>:</p>

<pre><code class="language-YML">version: '3.8'

services:
  your_app:
    image: your/app:latest
    # ... other configurations for your application ...
    restart: unless-stopped
    
  your_db:
    image: your/db:10.9
    # ... other configurations for your DB ...
    restart: unless-stopped

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      WATCHTOWER_CLEANUP: "true"
      WATCHTOWER_SCHEDULE: "0 0 4 * * *"
    command: your_app
</code></pre>
<p>Let’s break down the watchtower configuration.</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">image: containrrr/watchtower</code>: This specifies the official Watchtower image from Docker Hub.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">restart: unless-stopped</code>: This ensures that if the Watchtower container restarts for any reason, Docker will automatically bring it back up.</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">volumes: - /var/run/docker.sock:/var/run/docker.sock</code>: This is crucial. It mounts the Docker socket inside the Watchtower container, 
allowing it to communicate with the Docker daemon and manage other containers.</p>
  </li>
  <li><code class="language-plaintext highlighter-rouge">environment</code>: This section configures Watchtower using environment variables:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">WATCHTOWER_CLEANUP: "true"</code>: This tells Watchtower to automatically remove the old containers after a successful update, keeping your system tidy.</li>
      <li><code class="language-plaintext highlighter-rouge">WATCHTOWER_SCHEDULE: "0 0 4 * * *"</code>: This configures Watchtower to run on a cron schedule. The example 0 0 4 * * * specifies that Watchtower will check for updates every day at 4:00 AM.</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">command: your_app</code>: This instructs Watchtower to specifically monitor and update the container named <code class="language-plaintext highlighter-rouge">your_app</code> in the docker-compose.yml file. 
You can list multiple service names here, separated by spaces, if you want Watchtower to manage more than one service (e.g. <code class="language-plaintext highlighter-rouge">command: your_app your_db</code>)
if you omit this command, watchtower will manage all the container used in the docker compose file. 
Alternatively, you can also add labels to your service and then configure watchtower to only manage container that have that label (leading to the same results).</li>
</ul>

<p>One final note. if you tell Watchtower to monitor your database service (e.g., your_db) and you specified a minor version tag like <code class="language-plaintext highlighter-rouge">:10.9</code>,
this will ensure it receives patch updates(eg a new image pushed with <code class="language-plaintext highlighter-rouge">:10.9</code> tag) without the risk of automatic major version upgrades that could break the database.</p>

<h1 id="conclusion">Conclusion</h1>

<p>Watchtower presents a powerful and convenient solution for automating container image updates within a home Docker Compose environment, 
simplifying its maintenance. Its ability to automatically fetch and deploy newer versions ensures you’re running with the latest security patches. 
However, leveraging this automation without caution can lead to problems. For critical services like databases, 
there is a serius risk of causing data corruption or service instability. 
In these cases I recommend to carefully select the tags manged by Watchtower and limiting the latest tag for less critical internet exposed services.</p>]]></content><author><name>Lord_evron</name></author><category term="Technical" /><category term="secops" /><category term="docker" /><category term="technology" /><summary type="html"><![CDATA[In the world where everything is running in containers, staying up-to-date with the latest image versions is mandatory for security and performance improvements. While Docker Compose simplifies the orchestration of multi-container applications, manually monitoring and updating the images defined in your docker-compose.yml file can become a time-consuming task. This is where Watchtower steps in as your automated guardian, ensuring your Docker Compose services are always running on the newest images.]]></summary></entry></feed>