<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Infrastructure | Law Zava</title><link>https://lawzava.com/topics/infrastructure/</link><description>Compute, networking, capacity, and the physical constraints that outrank the roadmap.</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Mon, 12 Oct 2026 00:07:28 +0000</lastBuildDate><atom:link href="https://lawzava.com/topics/infrastructure/index.xml" rel="self" type="application/rss+xml"/><item><title>AI Build vs. Buy in 2026: An Operational Cost Question</title><link>https://lawzava.com/blog/2026-04-30-ai-build-vs-buy/</link><pubDate>Thu, 30 Apr 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-04-30-ai-build-vs-buy/</guid><description>AI build vs. buy in 2026 turns on operational cost: who owns telemetry, fallbacks, and data residency. When to buy, when to build, and the hybrid default.</description><content:encoded><![CDATA[<p>In 2026, build vs. buy is not a taste question. It is an operational cost question. Are you prepared to own the telemetry, the fallback paths, and the failure modes that come with the stack? Buying gives you speed and leaves the analytics with someone else. Building gives you control and hands you the overhead.</p>
<h2 id="the-myth-of-the-headline-price">The Myth of the Headline Price</h2>
<p>Most teams compare API pricing to GPU rental and stop there. That is the wrong first-order model.</p>
<p>Token price is the easiest number to quote and the least useful number to trust. The real bill shows up in the work around the model:</p>
<ul>
<li><strong>Telemetry &amp; Evals:</strong> If you self-host, you must build the pipeline that captures, scores, and reviews output. Vendor APIs may bundle some of this, but then they also own the metadata.</li>
<li><strong>Graceful Degradation:</strong> When the provider throttles you at peak, do you have local fallback? Hybrid systems buy resilience, but they also add systems-engineering work.</li>
<li><strong> <a href="/blog/2026-04-06-sovereign-systems-privacy-non-optional/"
   
   >Data Sovereignty</a>
:</strong> Sometimes the reason to build is simple: the data cannot legally leave your VPC. Once that is true, the token price stops mattering.</li>
</ul>
<h2 id="when-to-buy-the-commodity-highway">When to Buy (The Commodity Highway)</h2>
<p>Buy when the AI capability is a feature, not the product.</p>
<p>If you are building an internal documentation chatbot, a support-ticket summarizer, or a semantic search overlay, buy the API. Do not spend engineering throughput standing up vLLM instances and chasing KV-cache optimizations for a problem that is not your moat.</p>
<p>The catch is lock-in at the integration layer. If your code imports vendor-specific classes directly, you will feel the squeeze when prices change or a model line is deprecated.  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >Keep the provider behind an internal interface</a>
.</p>
<h2 id="when-to-build-the-crucible-of-control">When to Build (The Crucible of Control)</h2>
<p>Build when AI sits inside unit economics or inside a hard trust boundary.</p>
<p>You must build if:</p>
<ol>
<li>Your margins depend on it. Billions of tokens a day can make  <a href="/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/"
   
   >the API tax</a>
 the difference between a healthy product and a broken one.</li>
<li>You operate under zero-trust or residency constraints. In parts of healthcare, finance, and defense, the data cannot touch a multi-tenant cloud edge.</li>
<li>You need hardware-level optimization. Sub-150ms tail latency usually means quantization, attention fusion, and serious control over the runtime.</li>
</ol>
<p>That is the part teams underestimate. You are no longer building a prompt pipeline. You are operating a distributed, heavily constrained state machine. That takes engineers who understand memory bandwidth, not just prompting.</p>
<h2 id="the-hybrid-default">The Hybrid Default</h2>
<p>The mature pattern in 2026 is a barbell.</p>
<p>Buy frontier models for complex reasoning, planning, and high-context zero-shot tasks. Build or host  <a href="/blog/2024-08-05-small-models-big-impact/"
   
   >quantized, heavily tuned 8B models</a>
 for the large volume of routing, formatting, and classification work that sits underneath the product.</p>
<p>The CTO&rsquo;s job is to make the handoff between buy and build a config change instead of a rewrite. Picking a camp is the easy part.</p>
]]></content:encoded></item><item><title>Why AI Agents Fail on Legacy Network Infrastructure</title><link>https://lawzava.com/blog/2026-03-23-agenticops-networking-bottleneck/</link><pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-03-23-agenticops-networking-bottleneck/</guid><description>Many AI agent failures trace to the network: stale DNS, flat trust, retry storms, shallow queues. Zero-trust identity and per-step reliability fix them.</description><content:encoded><![CDATA[<p> <a href="/blog/2026-01-19-ai-agent-reliability/"
   
   >Agent reliability</a>
 is mostly infrastructure reliability. Most AI agent failures I see are infrastructure failures wearing a model mask. Legacy networking assumptions, flat trust boundaries, and missing circuit breakers create brittle agent behavior that looks like &ldquo;the AI is unreliable&rdquo; when the real problem is &ldquo;the network can&rsquo;t support autonomous execution patterns.&rdquo; Fix the infrastructure and the agents get markedly more reliable.</p>
<h2 id="the-execution-path-nobody-drew-on-a-whiteboard">The Execution Path Nobody Drew on a Whiteboard</h2>
<p>Agent tasks fan out across DNS resolution, TLS handshakes, token exchanges, service mesh routing, and backend queries. The multi-hop latency problem is well-understood (I covered the general case in  <a href="/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/"
   
   >the cloud-heavy architecture post</a>
), but the networking-specific failure modes deserve their own treatment: stale DNS caches that route agents to decommissioned endpoints, TLS handshakes that repeat across 40 tool calls when connections aren&rsquo;t reused, service mesh sidecars that add milliseconds per hop invisibly (more at the tail), and queue depth limits that silently drop requests during agent-scale bursts. All of them surface as agent unreliability.</p>
<h2 id="the-hidden-cost-of-20th-century-network-assumptions">The Hidden Cost of 20th-Century Network Assumptions</h2>
<p>Most enterprise networks were designed around two assumptions: traffic flows north-south through a perimeter, and anything inside the perimeter is trusted. AI agents violate both assumptions simultaneously.</p>
<p>Agent traffic is east-west by default. A single task might call an internal knowledge base, a code execution sandbox, an external search API, and a database, all in a single reasoning loop. The traffic pattern looks like a mesh, not a pipeline. Networks designed for request-response patterns between a frontend and a backend choke on this.</p>
<p>The trusted-network assumption is worse. When an agent has a service account with broad permissions, every tool call inherits those permissions. If the agent can read from a document store, it can read from all of it. If it can write to a database, the blast radius of a prompt injection extends to every table the service account can touch. This isn&rsquo;t a theoretical risk. It&rsquo;s the default configuration in most deployments I&rsquo;ve seen.</p>
<p>Latency compounds differently for agents than for traditional services. A human user tolerates 200ms of added latency on a page load. An agent making 40 tool calls in a single task turns 200ms of unnecessary overhead per call into 8 seconds of total delay. At scale, this means the difference between an agent that completes tasks in seconds and one that takes minutes. Users notice. They lose trust. They stop using the feature.</p>
<h2 id="zero-trust-identity-for-autonomous-systems">Zero-Trust Identity for Autonomous Systems</h2>
<p>You don&rsquo;t need a network redesign. You need an identity redesign at the network layer.</p>
<p>Every agent tool call should carry a scoped identity that specifies what the agent can reach, for how long, and on behalf of which user or task. This is standard zero-trust thinking applied to agent traffic patterns. (For the broader tool permission and output validation side of this, see  <a href="/blog/2026-02-23-ai-security-evolution/"
   
   >my earlier post on AI security</a>
.)</p>
<p>In practice, the networking-specific concerns are:</p>
<p><strong>Per-task credentials with network scope.</strong> Instead of a long-lived service account, mint a short-lived token for each agent task. The token carries the minimum permissions needed for that specific workflow, and critically, it limits which network endpoints the agent can reach. When the task ends, the token expires. If the agent is compromised mid-task, the blast radius is one task&rsquo;s worth of permissions and one task&rsquo;s set of reachable services.</p>
<p><strong>Per-call authentication overhead.</strong> Every tool call crossing a network boundary needs auth, and that auth has a cost. TLS mutual authentication, token validation, and policy lookup all add latency. The design tradeoff is between granular identity (every call authenticated independently) and performance (connection pooling, session tokens, cached auth decisions). Get this wrong and your zero-trust layer becomes the latency bottleneck it was meant to protect against.</p>
<p><strong>Network segmentation per agent class.</strong> Not all agents need the same network access. An agent that summarizes documents has no business reaching your billing API. Segment your network so each agent class can only route to the services it needs. This is basic network segmentation, but most teams skip it because their agents all share one service account with broad network access.</p>
<h2 id="reliability-engineering-for-agent-workflows">Reliability Engineering for Agent Workflows</h2>
<p>Traditional reliability patterns need adjustment for agentic workloads. The standard toolkit, retries, timeouts, circuit breakers, still applies, but the parameters and placement change.</p>
<p><strong>Timeouts need to be per-step, not per-request.</strong> An agent task might legitimately run for 30 seconds across 20 tool calls. A global timeout of 30 seconds will kill valid workflows. A per-step timeout of 3 seconds will catch hung dependencies without killing the task.</p>
<p><strong>Retry logic needs backpressure awareness.</strong> An agent that retries a failed tool call immediately, while 50 other agent instances are doing the same thing, creates a retry storm that takes down the dependency. Exponential backoff with jitter is the minimum. Better: a circuit breaker that trips after a threshold and fails fast for all agent instances, with a clear error message the model can reason about.</p>
<p><strong>Queue depth matters more than you think.</strong> Agent workloads are bursty. A user action that triggers 10 agent tasks, each making 15 tool calls, puts 150 requests into your service mesh in seconds. If the target service has a queue depth of 50, you&rsquo;re dropping requests before the agent even knows there&rsquo;s a problem. Size your queues for agent-scale fan-out, not human-scale request rates.</p>
<p><strong>Graceful degradation over hard failure.</strong> When a tool call fails, the agent should get a structured error it can reason about, not a 500 or a timeout. &ldquo;Knowledge base unavailable, try alternative approach&rdquo; is actionable. A raw HTTP error is not. Design your tool contracts to return machine-readable failure modes.</p>
<h2 id="observability-for-agent-decision-traces">Observability for Agent Decision Traces</h2>
<p>Standard APM tools show you request latency and error rates. For agent workflows, you need something more: a trace that follows the agent&rsquo;s reasoning across tool calls, captures the decision points, and shows why the agent chose one path over another.</p>
<p>This means correlating model inputs, outputs, and tool calls into a single trace. Each agent task gets a trace ID. Each tool call within that task gets a span. The spans include the tool arguments, the response, the latency, and the policy decision. When you look at a slow or failed agent task, you can see exactly which step took too long, which dependency failed, and whether the agent&rsquo;s retry behavior made things better or worse.</p>
<p>The teams doing this well treat agent traces like they treat database query plans. They review them regularly, look for patterns, and optimize the hot paths. A tool call that takes 500ms and gets called 20 times per task is a bigger problem than a tool call that takes 2 seconds but only gets called once.</p>
<h2 id="migration-path">Migration Path</h2>
<p>You don&rsquo;t need to rebuild your infrastructure to start.</p>
<ol>
<li><strong>Instrument first.</strong> Add trace IDs to agent tool calls. Log latency, errors, and retry counts per step.</li>
<li><strong>Add identity boundaries.</strong> Replace long-lived service accounts with per-task tokens, starting with agents that have write access.</li>
<li><strong>Circuit-break external calls.</strong> Add circuit breakers and per-step timeouts for every external dependency. Size queues for agent-scale fan-out.</li>
<li><strong>Migrate to mesh.</strong> Deploy a  <a href="/blog/2022-04-04-service-mesh-decision-guide/"
   
   >service mesh</a>
 or policy layer for tool call routing. Start in audit mode, then shift to enforcement.</li>
</ol>
<p>Each step is small and reversible. Together they compound into a fundamentally more reliable agent platform.</p>
<h2 id="checklist-risk-reduction-in-90-days">Checklist: Risk Reduction in 90 Days</h2>
<ul>
<li><input disabled="" type="checkbox"> Map every tool an agent can call, its permissions, and its failure modes</li>
<li><input disabled="" type="checkbox"> Add per-task trace IDs to all agent tool calls</li>
<li><input disabled="" type="checkbox"> Replace at least one long-lived service account with scoped, short-lived tokens</li>
<li><input disabled="" type="checkbox"> Set per-step timeouts on all agent tool calls</li>
<li><input disabled="" type="checkbox"> Add circuit breakers for external API dependencies</li>
<li><input disabled="" type="checkbox"> Deploy a policy layer in audit mode for tool call authorization</li>
<li><input disabled="" type="checkbox"> Review agent decision traces weekly for latency outliers and retry storms</li>
<li><input disabled="" type="checkbox"> Load test agent workflows at 10x expected concurrency</li>
<li><input disabled="" type="checkbox"> Document failure modes and give agents structured error responses</li>
<li><input disabled="" type="checkbox"> Establish an error budget for agent reliability separate from service reliability</li>
</ul>
<p>Teams that keep tuning prompts to work around infrastructure problems will keep wondering why their agents are &ldquo;flaky.&rdquo; Network and identity design is core agent product work. Budget for it accordingly.</p>
]]></content:encoded></item><item><title>Local-First AI Agents: Why Cloud-Heavy Architecture Fails</title><link>https://lawzava.com/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/</link><pubDate>Mon, 09 Mar 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-03-09-the-end-of-fat-cloud-agentic-economy/</guid><description>Agents that chain dozens of cloud hops pay in latency variance, cost, and failure surface. How to move routine inference local without a rewrite.</description><content:encoded><![CDATA[<p>Most teams building agentic systems default to cloud-heavy architectures because that&rsquo;s what they know. The result is unpredictable latency, runaway costs on bursty workloads, and a privacy posture that depends entirely on someone else&rsquo;s infrastructure. Local-first, hardware-aware design fixes the economics and gives you failure modes you can reason about. Compute placement is an architecture decision. Don&rsquo;t leave it for a later optimization pass.</p>
<h2 id="the-cloud-heavy-anti-pattern">The Cloud-Heavy Anti-Pattern</h2>
<p>The standard agentic stack looks like this: application code in one cloud region calls a model API in another, pulls context from a vector database in a third, and writes results back through a gateway that adds its own hop. Every step crosses a network boundary. Every boundary adds latency variance, failure surface, and cost.</p>
<p>For a single inference call, the overhead is tolerable. For an agent that chains ten to fifty calls per task, with tool use, retrieval, and self-correction loops, the overhead compounds. A p50 latency of 200ms per hop becomes 2-10 seconds of pure network time on a moderately complex agent run. At p99, you&rsquo;re looking at timeouts and retries that double or triple your effective cost.</p>
<p>The measurable symptoms are consistent across teams:</p>
<ul>
<li><strong>Latency variance dominates execution time.</strong> The model itself is fast. The network between your orchestrator and the model, plus the hops to retrieval and tool services, is where time disappears.</li>
<li><strong>Cost scales with hops, not intelligence.</strong> You pay for every round trip: egress, ingress, token overhead from context reassembly, and retry loops when any hop fails.</li>
<li><strong>Failure modes are combinatorial.</strong> When five services must all be healthy for one agent task to complete, your effective availability is the product of their individual availabilities. Five nines times five is not five nines.</li>
</ul>
<p>None of this argues against cloud. It argues against cloud-only, cloud-default architecture for workloads that don&rsquo;t need it.</p>
<h2 id="consolidating-runtime-layers">Consolidating Runtime Layers</h2>
<p>The fix is straightforward: move compute closer to the data and the user. Consolidate runtime layers so agent orchestration, context retrieval, and lightweight inference happen in the same process or at least on the same machine.</p>
<p>This is not a new idea. Databases figured this out decades ago. You don&rsquo;t run your query planner in a different availability zone from your storage engine. Agentic systems are hitting the same lesson: when the workload is latency-sensitive and involves tight feedback loops, co-location wins.</p>
<p>In practice, consolidation means running a  <a href="/blog/2025-08-18-local-ai-development/"
   
   >local inference server</a>
 for small models (classification, routing, extraction), keeping your retrieval index on the same node as your orchestrator, and reserving cloud API calls for frontier-model tasks that actually need them. The local layer handles the high-frequency, low-complexity work. The cloud layer handles the hard problems.</p>
<p>The cost difference is significant. Handling routine calls locally with a quantized model on commodity GPU hardware removes most of the per-request metering from a typical agent run. The frontier API cost doesn&rsquo;t disappear, but it shrinks because you&rsquo;re only sending it the work that justifies the price.</p>
<h2 id="cloud-only-vs-hybrid-cost-envelopes">Cloud-Only vs. Hybrid Cost Envelopes</h2>
<p>The math depends on workload shape, but the pattern is consistent.</p>
<p>Cloud-only architectures have  <a href="/blog/2026-02-09-ai-cost-trends/"
   
   >variable cost</a>
 that scales linearly with usage and offers no marginal improvement at volume. You pay the same per-token rate whether you run one task or a million. Egress fees, retry overhead, and context window waste compound on top.</p>
<p>Hybrid local-first architectures have a higher fixed cost (hardware, setup, maintenance) but dramatically lower marginal cost. Once the local inference server is running, the incremental cost of a routing decision or an extraction call is effectively zero. You&rsquo;re paying for electricity and depreciation, not per-request metering.</p>
<p>The crossover point arrives faster than most teams expect. For workloads above a few thousand agent tasks per day, local-first is cheaper within months, not years. Below that threshold, cloud-only is simpler and the cost premium is manageable.</p>
<p>The latency picture is even more decisive. Small classification and routing models on a mid-range local GPU answer in milliseconds, with no network variance on top. For agent loops that make dozens of calls per task, local inference can cut total wall-clock time by 60-80%.</p>
<h2 id="where-systems-languages-matter">Where Systems Languages Matter</h2>
<p>Agent runtimes written in Python work fine for prototyping and low-throughput production. But as you move inference and orchestration onto local hardware, you start caring about memory predictability, startup time, and per-request overhead in ways that an interpreted runtime like Python doesn&rsquo;t handle well.</p>
<p> <a href="/blog/2021-02-22-rust-for-cloud-services/"
   
   >Rust</a>
 is showing up in this layer for practical reasons. It gives you memory safety without garbage collection pauses, which matters when you&rsquo;re serving inference requests with tight latency budgets.</p>
<p>You don&rsquo;t need to rewrite your application in a systems language. The candidates are the runtime layer: the inference server, the orchestration loop, the retrieval engine. These are the hot paths where predictable performance translates directly into cost savings and reliability. The application logic on top can stay in whatever language your team knows.</p>
<p>The practical signal: if your agent runtime&rsquo;s p99 latency is dominated by GC pauses or memory allocation overhead rather than actual inference time, a systems-language runtime will help. If inference time dominates, the language doesn&rsquo;t matter.</p>
<h2 id="adoption-without-full-rewrites">Adoption Without Full Rewrites</h2>
<p>Teams with existing cloud-heavy architectures don&rsquo;t need to rip and replace. The migration is incremental and each step produces measurable improvement.</p>
<p><strong>Step 1: Instrument and classify.</strong> Before moving anything, measure what your agent stack actually does. Break down time and cost by call type: routing decisions, context retrieval, small-model inference, frontier-model inference. Most teams discover that the bulk of calls are routine work that doesn&rsquo;t need a frontier model or a cloud round trip.</p>
<p><strong>Step 2: Add a local inference tier.</strong> Deploy a quantized model locally for the routine calls you identified. Route classification, extraction, and simple generation through it. Keep the cloud API as the escalation path. This is a routing change, not a rewrite.</p>
<p><strong>Step 3: Co-locate retrieval.</strong> Move your vector index or retrieval layer onto the same infrastructure as your orchestrator. This eliminates the retrieval round trip, which is often the single largest latency contributor after model inference.</p>
<p><strong>Step 4: Evaluate and tighten.</strong> With local tiers in place, measure again. Adjust routing thresholds. Identify the next tier of work that can move local. Each iteration reduces cloud dependency and improves predictability.</p>
<p>The entire migration can happen alongside normal feature work. No flag days, no cutover weekends.</p>
<h2 id="governance-and-data-residency">Governance and Data Residency</h2>
<p>Local-first architecture has a governance benefit that&rsquo;s easy to overlook: your data stays on your infrastructure. For teams operating under GDPR, HIPAA, or sector-specific data residency requirements, this simplifies compliance significantly.</p>
<p>When agent tasks process user data through a cloud API, that data traverses networks you don&rsquo;t control and resides, however briefly, on infrastructure you don&rsquo;t own. The compliance burden of documenting, auditing, and risk-managing that data flow is real and growing. Local inference eliminates the flow entirely for tasks that don&rsquo;t require cloud escalation.</p>
<p>This doesn&rsquo;t mean you avoid cloud APIs altogether. It means you have architectural control over which data leaves your perimeter and which doesn&rsquo;t. That&rsquo;s a better conversation to have with your compliance team than &ldquo;everything goes to a third-party API.&rdquo;</p>
<h2 id="decision-rubric">Decision Rubric</h2>
<p>When deciding how to place compute for agentic workloads, ask these questions:</p>
<ol>
<li><strong>Volume</strong>: Are you running more than a few thousand agent tasks per day? If yes, the economics of local inference likely favor hybrid.</li>
<li><strong>Latency sensitivity</strong>: Do your agent loops involve more than ten chained calls? If yes, network overhead is probably your bottleneck.</li>
<li><strong>Data sensitivity</strong>: Does your agent process PII, health data, or regulated information? If yes, local-first reduces compliance surface.</li>
<li><strong>Team capability</strong>: Do you have infrastructure engineers who can operate local GPU servers? If no, start with managed options or cloud-based inference with a clear migration path.</li>
<li><strong>Workload predictability</strong>: Are your traffic patterns bursty or steady? Bursty workloads benefit most from local capacity that handles baseline load with cloud burst for peaks.</li>
</ol>
<h2 id="common-traps">Common Traps</h2>
<ul>
<li><strong>Over-investing in local hardware before measuring workload shape.</strong> Instrument first. Buy hardware based on data, not intuition.</li>
<li><strong>Treating local and cloud as either/or.</strong> The right answer is almost always hybrid. The question is where to draw the line.</li>
<li><strong>Ignoring operational cost of self-hosted infrastructure.</strong> Local inference is cheaper per request but requires someone to keep it running. Factor in ops time.</li>
<li><strong>Optimizing for p50 when p99 is what breaks your SLA.</strong> Agentic workloads are chains. One slow hop at p99 delays the entire task.</li>
</ul>
<p>Hardware placement is a first-order architecture decision. Make it early, measure it continuously, and adjust as your workload evolves.</p>
]]></content:encoded></item><item><title>AI Data Pipelines for RAG: ETL With a Retrieval Layer</title><link>https://lawzava.com/blog/2025-05-26-ai-data-pipelines/</link><pubDate>Mon, 26 May 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-05-26-ai-data-pipelines/</guid><description>AI data pipelines are ETL with a retrieval layer bolted on. The discipline is the same as always: detect change, chunk intelligently, keep indexes fresh.</description><content:encoded><![CDATA[<p>Stop overcomplicating AI pipelines. They&rsquo;re ETL plus retrieval ops. Diff your inputs, chunk by structure (not token count), upsert with stable IDs, and treat reindexing as a deliberate, versioned event. Skip the diffing step and retrieval drifts into garbage. I&rsquo;ve seen it happen three times this year alone.</p>
<p>I&rsquo;ve been building  <a href="/blog/2017-04-24-building-data-pipelines-that-dont-break/"
   
   >data pipelines</a>
 since before anyone called them &ldquo;data pipelines.&rdquo; At the fintech startup we were ingesting financial data from hundreds of sources, normalizing it, and serving it for real-time retrieval. That was 2017. The core problems haven&rsquo;t changed.</p>
<p>What has changed is that your pipeline now has a second consumer: a  <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >retrieval system</a>
 feeding an LLM. If you treat that consumer as an afterthought, your AI product will deliver confidently wrong answers. Ruthless focus on the basics separates pipelines that work from pipelines that demo well.</p>
<h2 id="the-shape-of-an-ai-pipeline">The Shape of an AI Pipeline</h2>
<p>Every AI pipeline I&rsquo;ve seen in production boils down to six stages. The skeleton:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">pipeline</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">stages</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">extract</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Pull from sources, normalize formats</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># PDF, HTML, API responses -&gt; clean markdown or structured text</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">diff</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Hash-based change detection</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># This is the stage most teams skip. Don&#39;t.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">chunk</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Split by document structure first, token count second</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Preserve section boundaries and headings</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">embed</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Generate vectors using a pinned model version</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Log the model version. You will need it later.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">index</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Upsert with stable IDs and rich metadata</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># source_id + chunk_position = deterministic ID</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">verify</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Check for missing chunks, stale entries, orphans</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># Alert on drift from expected source freshness</span>
</span></span></code></pre></div><p>Nothing exotic. The magic is in the discipline of each stage, not in clever architecture.</p>
<h2 id="the-diff-step-is-everything">The Diff Step Is Everything</h2>
<p>Most teams skip change detection and reprocess everything on every run. At small scale, this is fine. At production scale, it&rsquo;s expensive, noisy, and makes debugging a nightmare.</p>
<p>A simple content-hash approach works well:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">hasChanged</span>(<span style="color:#a6e22e">sourceID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">content</span> []<span style="color:#66d9ef">byte</span>, <span style="color:#a6e22e">store</span> <span style="color:#a6e22e">HashStore</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">newHash</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sha256</span>.<span style="color:#a6e22e">Sum256</span>(<span style="color:#a6e22e">content</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">existing</span>, <span style="color:#a6e22e">found</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">sourceID</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">found</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">sourceID</span>, <span style="color:#a6e22e">newHash</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">existing</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">newHash</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">sourceID</span>, <span style="color:#a6e22e">newHash</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>When I built the ingestion pipeline at the fintech startup, adding a diff layer cut downstream processing costs by roughly 60%. Most sources don&rsquo;t change on most runs. Detecting that early saves everything downstream.</p>
<p>The diff step also gives you auditability. You can answer &ldquo;what changed and when&rdquo; instead of shrugging at a  <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >vector store</a>
 that silently drifted.</p>
<h2 id="chunking-structure-before-size">Chunking: Structure Before Size</h2>
<p>This is where most  <a href="/blog/2023-04-17-rag-architecture-patterns/"
   
   >RAG pipelines</a>
 go wrong. Teams reach for a token-count splitter because it&rsquo;s the default in every tutorial, then wonder why retrieval returns fragments of ideas instead of coherent answers.</p>
<p>Split by document structure first. Headings, sections, code blocks, and list items are natural semantic boundaries. Only fall back to token-count splitting when a single section exceeds your context window.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">chunk_by_structure</span>(doc: Document) <span style="color:#f92672">-&gt;</span> list[Chunk]:
</span></span><span style="display:flex;"><span>    chunks <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> section <span style="color:#f92672">in</span> doc<span style="color:#f92672">.</span>sections:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> section<span style="color:#f92672">.</span>token_count <span style="color:#f92672">&lt;=</span> MAX_CHUNK_TOKENS:
</span></span><span style="display:flex;"><span>            chunks<span style="color:#f92672">.</span>append(Chunk(
</span></span><span style="display:flex;"><span>                content<span style="color:#f92672">=</span>section<span style="color:#f92672">.</span>text,
</span></span><span style="display:flex;"><span>                metadata<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;source_id&#34;</span>: doc<span style="color:#f92672">.</span>id,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;section_heading&#34;</span>: section<span style="color:#f92672">.</span>heading,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;position&#34;</span>: section<span style="color:#f92672">.</span>index,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;doc_version&#34;</span>: doc<span style="color:#f92672">.</span>version,
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>                <span style="color:#75715e"># Deterministic ID: no duplicates on re-ingestion</span>
</span></span><span style="display:flex;"><span>                id<span style="color:#f92672">=</span><span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>doc<span style="color:#f92672">.</span>id<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">{</span>section<span style="color:#f92672">.</span>index<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>            ))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># Fall back to sliding window only for oversized sections</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> sub <span style="color:#f92672">in</span> sliding_window(section, MAX_CHUNK_TOKENS, overlap<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span>):
</span></span><span style="display:flex;"><span>                chunks<span style="color:#f92672">.</span>append(Chunk(
</span></span><span style="display:flex;"><span>                    content<span style="color:#f92672">=</span>sub<span style="color:#f92672">.</span>text,
</span></span><span style="display:flex;"><span>                    metadata<span style="color:#f92672">=</span>{<span style="color:#f92672">**</span>section<span style="color:#f92672">.</span>metadata, <span style="color:#e6db74">&#34;sub_position&#34;</span>: sub<span style="color:#f92672">.</span>index},
</span></span><span style="display:flex;"><span>                    id<span style="color:#f92672">=</span><span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">{</span>doc<span style="color:#f92672">.</span>id<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">{</span>section<span style="color:#f92672">.</span>index<span style="color:#e6db74">}</span><span style="color:#e6db74">:</span><span style="color:#e6db74">{</span>sub<span style="color:#f92672">.</span>index<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                ))
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> chunks
</span></span></code></pre></div><p>Two things matter here. First, the <code>id</code> is deterministic, derived from source and position, not random. This means re-ingesting the same content produces upserts, not duplicates. Second, metadata travels with every chunk. When retrieval returns a chunk, you know exactly where it came from, which version, and which section.</p>
<p>I can&rsquo;t overstate how many production RAG systems I&rsquo;ve reviewed where chunks had no stable ID. Every reindex created duplicates. Users got the same passage three times in their context window, and the model hallucinated a consensus that didn&rsquo;t exist.</p>
<h2 id="freshness-is-an-operational-problem">Freshness Is an Operational Problem</h2>
<p>Your pipeline isn&rsquo;t done when it runs once. Sources change, APIs update, and documents get deleted. If your index doesn&rsquo;t reflect reality, your AI lies with confidence.</p>
<p>Three rules I enforce on every pipeline:</p>
<ol>
<li>
<p><strong>Reindex on embedding model changes.</strong> If you swap or upgrade your  <a href="/blog/2023-07-10-embedding-models-deep-dive/"
   
   >embedding model</a>
, every existing vector is stale. This is a full reindex event. No exceptions. Pin your model version and log it.</p>
</li>
<li>
<p><strong>Purge on source deletion.</strong> If a document disappears from the source, its chunks must disappear from the index. Orphaned chunks are a retrieval poison pill.</p>
</li>
<li>
<p><strong>Alert on freshness drift.</strong> Every source has an expected update cadence. If your financial data feed hasn&rsquo;t updated in 6 hours, something is wrong. Don&rsquo;t wait for a user to notice.</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">freshness_policy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">sources</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">product_docs</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">expected_interval</span>: <span style="color:#ae81ff">24h</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">alert_after</span>: <span style="color:#ae81ff">36h</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api_changelog</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">expected_interval</span>: <span style="color:#ae81ff">7d</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">alert_after</span>: <span style="color:#ae81ff">10d</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">support_kb</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">expected_interval</span>: <span style="color:#ae81ff">48h</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">alert_after</span>: <span style="color:#ae81ff">72h</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">on_embedding_change</span>: <span style="color:#ae81ff">full_reindex</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">on_source_delete</span>: <span style="color:#ae81ff">purge_chunks</span>
</span></span></code></pre></div><h2 id="the-mistakes-i-keep-seeing">The Mistakes I Keep Seeing</h2>
<p>Across the pipelines I&rsquo;ve built and reviewed, in telecom and elsewhere, the failure pattern is remarkably consistent:</p>
<p><strong>No stable IDs.</strong> Updates create duplicates. Retrieval returns the same content multiple times. The model treats repetition as emphasis and doubles down on whatever it found.</p>
<p><strong>Token-count-only chunking.</strong> A paragraph about authentication gets split mid-sentence. The first half lands in one chunk, the second half in another. Retrieval finds the first half. The model confidently gives half an answer.</p>
<p><strong>Ad-hoc reindexing.</strong> Someone runs a reindex on a Friday afternoon. Nobody knows what changed. Retrieval quality shifts. The team argues about whether it got better or worse. No one can prove either way because there&rsquo;s no baseline.</p>
<p><strong>Missing permission metadata.</strong> The chunks are indexed without access control data. A user with restricted access asks a question and gets an answer sourced from documents they shouldn&rsquo;t see. This is a compliance incident waiting to happen.</p>
]]></content:encoded></item><item><title>AI Infrastructure at Scale Is Just Infrastructure</title><link>https://lawzava.com/blog/2024-12-09-ai-infrastructure-scale/</link><pubDate>Mon, 09 Dec 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-12-09-ai-infrastructure-scale/</guid><description>AI infrastructure at scale is just infrastructure. Gateways, caching, workload separation, budgets, and circuit breakers solve the same old problems.</description><content:encoded><![CDATA[<p>I&rsquo;m tired of seeing AI infrastructure treated as if it needs a whole new discipline. A year ago I complained that  <a href="/blog/2023-12-18-ai-infrastructure-scale/"
   
   >AI features were re-learning every scaling problem</a>
 distributed systems teams had already solved. That hasn&rsquo;t changed.</p>
<p>It doesn&rsquo;t. It&rsquo;s the same infrastructure engineering we&rsquo;ve been doing for decades, applied to a workload that happens to involve model inference. The latency problems are the same. The cost problems are the same. The reliability problems are the same. And the solutions are the same.</p>
<p>And yet every week I review a team&rsquo;s architecture and find they&rsquo;ve reinvented service meshes, badly, because they assumed AI needed something different.</p>
<h2 id="the-demo-to-production-gap-is-infrastructure">The Demo-to-Production Gap Is Infrastructure</h2>
<p>Here&rsquo;s what happens: a team builds a demo. It works great at one request per minute. Then real traffic arrives and everything falls apart. Latency spikes. Costs explode. The system goes down when the provider rate-limits them.</p>
<p>None of these are AI problems. They&rsquo;re infrastructure problems that we solved years ago in every other context. The teams that scale AI successfully are the ones that apply those solutions without reinventing them.</p>
<h2 id="put-a-gateway-in-front-please">Put a Gateway in Front. Please.</h2>
<p>I&rsquo;m genuinely baffled by how many production AI systems I see where every service calls the model provider directly. No centralized routing. No rate limiting. No budget enforcement. No observability.</p>
<p>This is like building a web application in 2024 without a load balancer. Nobody would do that. But somehow AI gets a pass.</p>
<p>A gateway (call it a broker, proxy, or control plane, whatever you want) does the boring work:</p>
<ul>
<li>Routes requests to the right model based on task type</li>
<li>Enforces rate limits and budgets per user, per feature, per environment</li>
<li>Caches deterministic responses</li>
<li>Provides a single point for observability and tracing</li>
<li>Handles provider failover when one API goes down</li>
</ul>
<p>You can build a basic version in a day: a YAML config and a reverse proxy. It doesn&rsquo;t need to be fancy. It needs to exist.</p>
<h2 id="separate-your-workloads">Separate Your Workloads</h2>
<p>Interactive requests and batch processing shouldn&rsquo;t share the same execution path. I keep saying this, and teams keep ignoring it until interactive latency tanks because a batch job saturated the rate limit.</p>
<p>Interactive work gets tight latency budgets and priority access. Batch work gets queued and retried patiently. The split is trivial to implement and painful to retrofit after the fact.</p>
<h2 id="cache-every-deterministic-request">Cache Every Deterministic Request</h2>
<p>If you&rsquo;re sending the same prompt with the same inputs to the same model and not caching the response, you&rsquo;re burning money. Literally.</p>
<p>Exact-match caching for deterministic requests is table stakes. Similarity-based caching for near-duplicate requests is a bonus. Even a simple  <a href="/blog/2024-03-25-prompt-caching-strategies/"
   
   >TTL-based cache with invalidation on prompt updates</a>
 can cut costs significantly.</p>
<p>One team was spending $40k/month on model inference. After adding exact-match caching for their classification pipeline, it dropped to $15k. Same outputs. Same quality. Less waste.</p>
<h2 id="cost-controls-arent-optional">Cost Controls Aren&rsquo;t Optional</h2>
<p>&ldquo;We&rsquo;ll optimize costs later&rdquo; is the AI equivalent of &ldquo;we&rsquo;ll add tests later.&rdquo; You won&rsquo;t. And when the bill arrives, it becomes an emergency.</p>
<p>Budget enforcement belongs in the gateway. Hard caps with clear error messages. Soft limits that degrade to cheaper models or slower paths. Per-user and per-feature attribution so you know where the money goes.</p>
<p>I&rsquo;ve seen teams discover that a single feature was responsible for 70% of their AI spend because nobody was tracking attribution. The feature wasn&rsquo;t even high-value. It was just chatty.</p>
<h2 id="reliability-isnt-heroics">Reliability Isn&rsquo;t Heroics</h2>
<p>Retry with backoff. Circuit breakers. Graceful degradation. Provider failover.</p>
<p>This is baseline production engineering. If your AI system doesn&rsquo;t have them, it isn&rsquo;t production-ready. It&rsquo;s a demo with a billing account.</p>
<p>Graceful degradation is a product decision, not an ops feature. If the full response is unavailable, a simpler response or a cached response or even a &ldquo;try again in a moment&rdquo; is better than an error page. Design for this upfront. Don&rsquo;t bolt it on during an incident.</p>
<p>The gateway, the cache, budget enforcement, workload separation, circuit breakers: none of it is novel. All of it is necessary. Your SRE team already knows how to do this. Let them.</p>
]]></content:encoded></item><item><title>Scaling AI Features: GPUs, Rate Limits, and Backpressure</title><link>https://lawzava.com/blog/2023-12-18-ai-infrastructure-scale/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-12-18-ai-infrastructure-scale/</guid><description>GPUs are scarce, rate limits are hard ceilings, and AI demos collapse under real traffic. The distributed systems patterns that keep AI features up.</description><content:encoded><![CDATA[<p>I&rsquo;m going to be blunt: the state of AI infrastructure heading into 2024 is embarrassing.</p>
<p>We have models that can write poetry, generate code, and analyze images. We don&rsquo;t have enough GPUs to run them reliably. We don&rsquo;t have pricing that makes sense at scale. And we definitely don&rsquo;t have the operational maturity to treat these systems like the production dependencies they have become.</p>
<p>I&rsquo;ve spent December watching AI features I helped build run into every scaling problem distributed systems teams have been solving for twenty years. Rate limits. Cascading failures. Cost explosions. Latency spikes. The problems aren&rsquo;t new. The industry is just re-learning them with a fresh coat of hype.</p>
<h2 id="the-gpu-situation-is-absurd">The GPU Situation Is Absurd</h2>
<p>You can&rsquo;t get H100s. You can&rsquo;t reliably get inference capacity from any major provider unless you sign a months-long commitment or an enterprise contract that costs more than most startups raise in a seed round. The entire industry is building products on top of infrastructure that&rsquo;s supply-constrained, and nobody wants to talk about what happens when demand doubles next year.</p>
<p>I tried to reserve inference capacity for a production workload last month. The response from one provider was &ldquo;we can put you on a waitlist.&rdquo; A waitlist. For compute. In 2023. It&rsquo;s a supply chain problem wearing a technology costume.</p>
<h2 id="rate-limits-are-a-production-constraint">Rate Limits Are a Production Constraint</h2>
<p>Every AI API has rate limits. At low volume, you don&rsquo;t notice them. At production scale, they become the hardest ceiling in your architecture.</p>
<p>I hit OpenAI&rsquo;s  <a href="/blog/2022-06-27-rate-limiting-strategies/"
   
   >rate limit</a>
 during a load test and watched requests queue up until the entire feature became unusable. Not degraded. Unusable. The fix wasn&rsquo;t clever engineering. It was a priority queue, backpressure, and load shedding. Distributed systems 101. The fact that most AI teams are learning this for the first time worries me.</p>
<h2 id="your-demo-wont-survive-real-traffic">Your Demo Won&rsquo;t Survive Real Traffic</h2>
<p>When your AI feature goes from 100 requests per day to 10,000:</p>
<p>Latency goes from &ldquo;acceptable&rdquo; to &ldquo;users are closing the tab.&rdquo; Costs go from &ldquo;rounding error&rdquo; to &ldquo;someone just Slacked asking why the API bill tripled.&rdquo; A provider outage that used to affect a handful of test users now takes down a production feature that the sales team just promised to a client.</p>
<p>I&rsquo;ve seen all three of these happen at the same company. In the same month.</p>
<h2 id="the-patterns-that-hold-up">The Patterns That Hold Up</h2>
<p><strong>Queues and backpressure.</strong> Treat your AI traffic as a managed stream, not an open pipe. Priority queues for critical requests. Backpressure when the system is saturated. Load shedding for low-priority work. This isn&rsquo;t optional once you have real users.</p>
<p><strong>Circuit breakers.</strong> Your model provider will have bad hours. Mine had a bad day last week. Circuit breakers stop a provider outage from cascading through your entire system. They&rsquo;re boring. They&rsquo;re essential. I&rsquo;ve been building systems with circuit breakers for years, including at a large telecom company I worked with. The pattern hasn&rsquo;t changed. The dependency has.</p>
<p><strong>Graceful degradation.</strong> When GPT-4 is down, what happens? If the answer is &ldquo;the feature breaks,&rdquo; you don&rsquo;t have a production system. You have a demo with users. Fall back to cached responses. Fall back to a  <a href="/blog/2023-01-23-llm-integration-patterns/"
   
   >smaller, faster model</a>
. Fall back to a static message that says &ldquo;this feature is temporarily unavailable.&rdquo; Anything is better than a spinning loader.</p>
<p><strong> <a href="/blog/2023-07-24-ai-cost-optimization/"
   
   >Cost controls</a>
 that are actually enforced.</strong> Per-tenant budgets. Per-feature budgets. Daily caps. If you don&rsquo;t enforce them, you&rsquo;ll get a surprise invoice that triggers an emergency meeting. I&rsquo;ve seen a single prompt change (adding two paragraphs of context) increase monthly costs by 35%. Token pricing is deceptively simple until you multiply it by production volume.</p>
<p><strong>Caching.</strong> Exact-match caching is trivial to implement and saves real money. Same question, same context, same answer: serve it from cache. Semantic caching is fancier and worth exploring, but start with the easy wins.</p>
<h2 id="this-is-distributed-systems-work">This Is Distributed Systems Work</h2>
<p>None of this is novel. Queues, circuit breakers, graceful degradation, cost controls, caching: these are patterns from every distributed systems textbook ever written. The only thing that&rsquo;s new is the dependency type.</p>
<p>What frustrates me is that the AI community is treating infrastructure as a solved problem while building on top of infrastructure that&rsquo;s anything but solved. The models are impressive. The plumbing is held together with optimism and rate limit retries.</p>
<p>Build your AI features like you would build any production system that depends on an unreliable, expensive, supply-constrained external service. Because that&rsquo;s exactly what it is.</p>
]]></content:encoded></item><item><title>Vector Databases Explained: What They Are, When You Need One</title><link>https://lawzava.com/blog/2023-04-03-vector-databases-explained/</link><pubDate>Mon, 03 Apr 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-04-03-vector-databases-explained/</guid><description>What vector databases store, how similarity search and ANN indexes work, and when pgvector is enough versus a dedicated vector database.</description><content:encoded><![CDATA[<p>Everyone is talking about vector databases right now, and about half of what I hear is wrong. A vector database stores numeric representations of data and finds the closest matches. That&rsquo;s it. The real decisions are about indexing strategy, hybrid search, and whether you need a dedicated system or can extend what you already have. Most teams overthink the database choice and underthink the data pipeline.</p>
<h2 id="what-is-actually-stored">What Is Actually Stored</h2>
<p>An embedding model turns text, images, or structured data into vectors: arrays of floating-point numbers that encode meaning. &ldquo;Cat&rdquo; and &ldquo;kitten&rdquo; end up close together in vector space. &ldquo;Cat&rdquo; and &ldquo;quarterly earnings report&rdquo; don&rsquo;t.</p>
<p>A vector database stores these vectors and lets you find the closest ones to a query. That&rsquo;s the core primitive: similarity search. Traditional databases give you exact matches, range queries, and transactions. Vector databases give you &ldquo;find me the things most similar to this.&rdquo;</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Document</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ID</span>        <span style="color:#66d9ef">string</span>            <span style="color:#e6db74">`json:&#34;id&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Content</span>   <span style="color:#66d9ef">string</span>            <span style="color:#e6db74">`json:&#34;content&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Embedding</span> []<span style="color:#66d9ef">float64</span>         <span style="color:#e6db74">`json:&#34;embedding&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Metadata</span>  <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;metadata&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">SearchResult</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Document</span> <span style="color:#a6e22e">Document</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Score</span>    <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That&rsquo;s the data model. A document, its vector, and some metadata. Everything else is indexing and  <a href="/blog/2023-01-23-llm-integration-patterns/"
   
   >retrieval strategy</a>
.</p>
<h2 id="similarity-metrics">Similarity Metrics</h2>
<p>&ldquo;Closest&rdquo; needs a definition. The two common choices:</p>
<p><strong>Cosine similarity</strong> measures the angle between vectors, ignoring magnitude. Good when you care about direction (meaning) and not length. Most text embedding models are trained with this in mind.</p>
<p><strong>Dot product</strong> considers both direction and magnitude. Useful when the embedding model encodes importance in vector length.</p>
<p>Pick the metric that matches your embedding model&rsquo;s training objective. The wrong choice can look fine on a small test set and produce garbage results at scale. I&rsquo;ve seen this firsthand: a team using dot product with a model trained for cosine, wondering why their search quality was inconsistent.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">cosineSimilarity</span>(<span style="color:#a6e22e">a</span>, <span style="color:#a6e22e">b</span> []<span style="color:#66d9ef">float64</span>) <span style="color:#66d9ef">float64</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dot</span>, <span style="color:#a6e22e">normA</span>, <span style="color:#a6e22e">normB</span> <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">a</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">dot</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">a</span>[<span style="color:#a6e22e">i</span>] <span style="color:#f92672">*</span> <span style="color:#a6e22e">b</span>[<span style="color:#a6e22e">i</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">normA</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">a</span>[<span style="color:#a6e22e">i</span>] <span style="color:#f92672">*</span> <span style="color:#a6e22e">a</span>[<span style="color:#a6e22e">i</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">normB</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">b</span>[<span style="color:#a6e22e">i</span>] <span style="color:#f92672">*</span> <span style="color:#a6e22e">b</span>[<span style="color:#a6e22e">i</span>]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">normA</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">normB</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">dot</span> <span style="color:#f92672">/</span> (<span style="color:#a6e22e">math</span>.<span style="color:#a6e22e">Sqrt</span>(<span style="color:#a6e22e">normA</span>) <span style="color:#f92672">*</span> <span style="color:#a6e22e">math</span>.<span style="color:#a6e22e">Sqrt</span>(<span style="color:#a6e22e">normB</span>))
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="the-deployment-decision">The Deployment Decision</h2>
<p>There&rsquo;s no universal answer. It depends on your constraints:</p>
<p><strong>Extend your existing database.</strong> PostgreSQL with pgvector, for example (it currently offers exact search and IVFFlat indexes). Good when vectors are one part of a larger transactional system. You get joins, transactions, and familiar operations alongside similarity search. The tradeoff is that you&rsquo;re bolting a new capability onto infrastructure not designed for it. Fine for moderate scale. Starts to hurt when similarity search becomes the dominant workload.</p>
<p><strong>Use a dedicated vector database.</strong> Pinecone, Weaviate, Qdrant, Milvus. Good when similarity search is central and you need low latency at scale. These systems are built for this workload. The tradeoff is another piece of infrastructure to operate, another data pipeline to maintain.</p>
<p><strong>Go managed.</strong> Pinecone or similar. Good when operational overhead is a bigger risk than vendor lock-in. You pay more per query but you don&rsquo;t page at 3am because the index rebuild failed.</p>
<p>For most teams I talk to, the answer is:  <a href="/blog/2021-07-12-serverless-databases/"
   
   >start with pgvector</a>
. It&rsquo;s good enough for most workloads, you already know how to operate Postgres, and you avoid adding a new system to your stack until you actually need to. Move to a dedicated solution when latency or scale forces your hand.</p>
<h2 id="indexing-exact-vs-approximate">Indexing: Exact vs. Approximate</h2>
<p>Exact search compares the query against every vector. Accurate, simple, and slow as your data grows. At 10,000 vectors it&rsquo;s fine. At 10 million it isn&rsquo;t.</p>
<p>Approximate nearest neighbor (ANN) indexes trade a small amount of accuracy for much better performance. HNSW is the most common algorithm. It builds a navigable graph that lets you skip most comparisons while still finding results that are close to the true nearest neighbors.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">IndexConfig</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Type</span>       <span style="color:#66d9ef">string</span> <span style="color:#75715e">// &#34;flat&#34;, &#34;hnsw&#34;, &#34;ivf&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Dimensions</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Metric</span>     <span style="color:#66d9ef">string</span> <span style="color:#75715e">// &#34;cosine&#34;, &#34;dot&#34;, &#34;l2&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// HNSW-specific</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">EfConstruction</span> <span style="color:#66d9ef">int</span> <span style="color:#75715e">// build-time quality (higher = slower build, better recall)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">EfSearch</span>       <span style="color:#66d9ef">int</span> <span style="color:#75715e">// query-time quality (higher = slower query, better recall)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">M</span>              <span style="color:#66d9ef">int</span> <span style="color:#75715e">// connections per node (higher = more memory, better recall)</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The parameters matter. <code>EfConstruction</code> and <code>M</code> control the build-time quality-speed tradeoff. <code>EfSearch</code> controls the same tradeoff at query time. Treat these as part of your performance budget. Tune them with your actual data and actual queries, not defaults from a blog post.</p>
<h2 id="chunking-strategy">Chunking Strategy</h2>
<p>Most text is too large to embed as a single vector. You need to chunk it into pieces that are small enough to be meaningful but large enough to carry context.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ChunkConfig</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxTokens</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Overlap</span>   <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Separator</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">chunkDocument</span>(<span style="color:#a6e22e">content</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">cfg</span> <span style="color:#a6e22e">ChunkConfig</span>) []<span style="color:#a6e22e">Chunk</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sentences</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">splitSentences</span>(<span style="color:#a6e22e">content</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">chunks</span> []<span style="color:#a6e22e">Chunk</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">current</span> []<span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">currentLen</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">sentences</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">sLen</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tokenCount</span>(<span style="color:#a6e22e">s</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">currentLen</span><span style="color:#f92672">+</span><span style="color:#a6e22e">sLen</span> &gt; <span style="color:#a6e22e">cfg</span>.<span style="color:#a6e22e">MaxTokens</span> <span style="color:#f92672">&amp;&amp;</span> len(<span style="color:#a6e22e">current</span>) &gt; <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">chunks</span> = append(<span style="color:#a6e22e">chunks</span>, <span style="color:#a6e22e">Chunk</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Text</span>:  <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">current</span>, <span style="color:#e6db74">&#34; &#34;</span>),
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Index</span>: len(<span style="color:#a6e22e">chunks</span>),
</span></span><span style="display:flex;"><span>            })
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Keep overlap</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">overlapStart</span> <span style="color:#f92672">:=</span> len(<span style="color:#a6e22e">current</span>) <span style="color:#f92672">-</span> <span style="color:#a6e22e">cfg</span>.<span style="color:#a6e22e">Overlap</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">overlapStart</span> &lt; <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">overlapStart</span> = <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">current</span> = <span style="color:#a6e22e">current</span>[<span style="color:#a6e22e">overlapStart</span>:]
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">currentLen</span> = <span style="color:#a6e22e">tokenCountSlice</span>(<span style="color:#a6e22e">current</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">current</span> = append(<span style="color:#a6e22e">current</span>, <span style="color:#a6e22e">s</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">currentLen</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">sLen</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> len(<span style="color:#a6e22e">current</span>) &gt; <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">chunks</span> = append(<span style="color:#a6e22e">chunks</span>, <span style="color:#a6e22e">Chunk</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Text</span>:  <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">current</span>, <span style="color:#e6db74">&#34; &#34;</span>),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Index</span>: len(<span style="color:#a6e22e">chunks</span>),
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">chunks</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Key decisions: chunk size (I default to 256-512 tokens), overlap (10-20% prevents losing context at boundaries), and whether to chunk by sentences, paragraphs, or fixed token counts. Sentence-based chunking preserves meaning better. Fixed-token chunking is simpler.</p>
<p>Store metadata with every chunk: source document ID, position index, creation timestamp, access control labels. You&rsquo;ll need all of these for filtering, ordering, and debugging.</p>
<h2 id="hybrid-search">Hybrid Search</h2>
<p>Vector search is great at semantic matching. It&rsquo;s terrible at exact matches. If a user searches for &ldquo;RFC 7231&rdquo; you want the exact document, not the three most semantically similar documents about HTTP specifications.</p>
<p>Combine vector search with keyword search. The simplest approach: run both, merge results, and let a weighted score decide the final ranking. More sophisticated approaches use reciprocal rank fusion or learn the weights from user behavior.</p>
<p>Don&rsquo;t overthink the blending logic at the start. A simple weighted combination that you can adjust is better than a clever algorithm you can&rsquo;t debug.</p>
<h2 id="reindexing">Reindexing</h2>
<p>Embeddings go stale. Your data changes. Your embedding model gets updated. You need a reindexing pipeline that&rsquo;s repeatable and safe.</p>
<p>Build it as a batch job that can run alongside the live index. Write to a new index, validate it against your eval set, then swap. Blue-green deployments for vectors. Nothing fancy, but it has to work reliably because a corrupted index is a corrupted search experience.</p>
<h2 id="the-eval-loop">The Eval Loop</h2>
<p>Before you ship, build a small eval set: 50-100 representative queries with known good results. Measure recall and latency. Then test again at production data volume. A vector search that works on 1,000 documents and breaks on 100,000 is a demo, not a product.</p>
<p>Track these numbers over time. Embedding model updates, data changes, and index configuration tweaks all affect quality. If you aren&rsquo;t measuring, you&rsquo;re guessing.</p>
]]></content:encoded></item><item><title>Cloud Cost Optimization Starts With Visibility</title><link>https://lawzava.com/blog/2022-12-19-infrastructure-cost-optimization/</link><pubDate>Mon, 19 Dec 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-12-19-infrastructure-cost-optimization/</guid><description>Most cloud cost problems are visibility problems. Fix tagging, kill idle resources, right-size the rest, and make cost a regular engineering conversation.</description><content:encoded><![CDATA[<p>Every client I&rsquo;ve talked to in the last three months has the same question: how do we cut our cloud bill? The economy tightened, budgets got scrutiny, and suddenly that AWS invoice everyone ignored is a board-level concern.</p>
<p>Most cloud cost problems are visibility problems. You&rsquo;re paying for things you don&rsquo;t know about. I made the longer version of this argument  <a href="/blog/2022-10-03-cloud-cost-management/"
   
   >in October</a>
; this is the short, practical one.</p>
<h2 id="step-one-know-what-youre-paying-for">Step One: Know What You&rsquo;re Paying For</h2>
<p>Before optimizing anything, fix your tagging. I know. Nobody wants to hear that. It isn&rsquo;t glamorous. But if you can&rsquo;t break down cost by service and environment, you&rsquo;re guessing.</p>
<p>In a recent engagement, we found that 30% of one organization&rsquo;s compute spend was on non-production environments running 24/7. Development and staging instances that nobody used after 6pm, burning money overnight and on weekends. The fix (scheduling those environments to shut down outside business hours) took a week to implement and cut the monthly bill meaningfully.</p>
<p>You can&rsquo;t fix what you can&rsquo;t see.</p>
<h2 id="the-easy-wins">The Easy Wins</h2>
<p>These require no architectural changes, minimal risk, and work across every cloud provider:</p>
<p><strong>Kill orphaned resources.</strong> Unattached EBS volumes, unused Elastic IPs, old load balancers pointing at nothing, snapshots from services that were decommissioned months ago. Every AWS account I&rsquo;ve audited has thousands of dollars in monthly orphan costs.</p>
<p><strong>Right-size underutilized compute.</strong> Pull two weeks of CPU and memory metrics. If your instances consistently sit at 10-15% utilization, drop a size. This is obvious but most teams never look at the data.</p>
<p><strong>Schedule non-production environments.</strong> Dev and staging don&rsquo;t need to run at 3am. Shut them down evenings and weekends. Automate the start/stop.</p>
<p><strong>Apply storage lifecycle policies.</strong> Move cold data to cheaper tiers. Set expiration policies on logs. CloudWatch log groups default to never expiring. If nobody configured retention, you&rsquo;re paying to store every log line forever.</p>
<h2 id="commitments-be-careful">Commitments: Be Careful</h2>
<p>Reserved instances and savings plans can save real money on stable workloads. The trap is overcommitting. I&rsquo;ve seen teams lock in three-year reservations right before a product pivot made half their infrastructure obsolete.</p>
<p>Commit only to what you&rsquo;re confident will be steady for the commitment period. In late 2022, flexibility is worth more than a marginal discount.</p>
<h2 id="the-recurring-habit">The Recurring Habit</h2>
<p>Cost optimization is a habit, not a project. Monthly review of trends and anomalies, clear ownership per service, and guardrails that catch new waste before it accumulates.</p>
<p>Make cost visible to the teams creating it. When engineers see the bill for their services, they make different decisions, because they have information they didn&rsquo;t have before.</p>
<p>The goal is discipline, not austerity. Spending money on infrastructure is fine. Spending money on infrastructure you don&rsquo;t need is waste. And in late 2022, waste is the first thing that gets cut, usually clumsily, by someone who doesn&rsquo;t understand what the infrastructure does. Better to cut it yourself, surgically, while you still have the context.</p>
]]></content:encoded></item><item><title>Platform Engineering: DevOps Grew Up</title><link>https://lawzava.com/blog/2022-11-07-platform-engineering-rise/</link><pubDate>Mon, 07 Nov 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-11-07-platform-engineering-rise/</guid><description>Platform engineering is what happens when you realize &amp;amp;lsquo;you build it, you run it&amp;amp;rsquo; does not scale past a handful of teams.</description><content:encoded><![CDATA[<p>DevOps told us every team should own the full delivery lifecycle. Build it, run it, deploy it, monitor it. Great principle. Terrible at scale.</p>
<p>By now, the pattern is familiar. You start with a few teams, each running their own pipelines and infrastructure. It works. Then you grow to twenty teams. Each one has reinvented service templates, built slightly different CI pipelines, and solved the same security problems in incompatible ways. Suddenly you have twenty bespoke deployment processes, none of which are documented well enough for anyone outside the team to understand.</p>
<p>That isn&rsquo;t a failure of DevOps principles. It&rsquo;s a scale problem, and platform engineering is the pragmatic fix. In 2020 I called  <a href="/blog/2020-10-19-platform-engineering-practices/"
   
   >platform engineering DevOps with a rebrand</a>
. At twenty teams, the rebrand earns its keep.</p>
<h2 id="what-platform-engineering-actually-is">What Platform Engineering Actually Is</h2>
<p>Strip away the conference talks and vendor pitches, and platform engineering is simple: a team that builds the paved road so product teams don&rsquo;t have to pave their own.</p>
<p>The platform team builds opinionated defaults for the boring stuff: service scaffolding, deployment pipelines, observability, security baselines. Product teams use those defaults and focus on product work. If a team needs to deviate, they can, but they start from a working foundation instead of a blank page.</p>
<p>At a large consumer platform, the infrastructure team had elements of this approach. The teams that used the supported path shipped faster and had fewer incidents. The teams that went custom spent meaningful time on plumbing that had nothing to do with their actual domain.</p>
<h2 id="the-golden-path-is-a-contract">The Golden Path Is a Contract</h2>
<p>I like the &ldquo;golden path&rdquo; framing because it makes the deal explicit. Follow this path, and the platform team supports you end to end. Deviate, and you own the consequences.</p>
<p>The point is sustainability. A platform team can&rsquo;t support infinite variation. Opinionated defaults reduce the support surface and let the platform team actually invest in quality instead of fighting fires across twenty different setups.</p>
<p>A good golden path covers the boring lifecycle: create a service, deploy it, observe it, roll it back. Make each step predictable and fast. If &ldquo;create a new service&rdquo; takes a developer two days of config and ticket filing, your golden path isn&rsquo;t golden. It&rsquo;s bureaucracy with a nice name.</p>
<h2 id="treat-it-like-a-product-or-dont-bother">Treat It Like a Product (Or Don&rsquo;t Bother)</h2>
<p>This is where most platform efforts fail. They build internal tooling with an infrastructure team&rsquo;s mindset: here is the tool, use it, file a ticket if it breaks.</p>
<p>That doesn&rsquo;t work. An internal platform needs product thinking. Who are the users? What workflows are painful? Where is the friction? What does adoption look like?</p>
<p>If developers avoid your platform, you have a product problem, and more training won&rsquo;t fix it. Either the platform doesn&rsquo;t solve a real pain point, or it creates more friction than it removes.</p>
<p>Adoption is the only metric that matters early on. Everything else (consistency, reliability, cost savings) follows from teams actually choosing to use the thing.</p>
<h2 id="what-goes-into-a-platform">What Goes Into a Platform</h2>
<p>Keep it focused. An internal platform typically needs three capabilities:</p>
<p><strong>A developer entry point.</strong> Somewhere that surfaces service ownership, docs, and supported workflows. This can be a  <a href="/blog/2021-03-08-internal-developer-portals/"
   
   >portal</a>
, a CLI, or even a well-maintained repo with templates. The format matters less than the discoverability.</p>
<p><strong>Self-service provisioning.</strong> Environments, pipelines, databases, queues: the resources teams need to ship. If provisioning requires a ticket and a three-day wait, you haven&rsquo;t solved the problem.</p>
<p><strong>Shared guardrails.</strong> Observability, security controls, cost defaults. Things that every team needs but nobody wants to build from scratch. Bake them into the golden path so compliance is the default, not an afterthought.</p>
<h2 id="build-vs-buy">Build vs. Buy</h2>
<p>Build what encodes your internal standards and unique workflows. Buy what&rsquo;s commoditized. Most organizations don&rsquo;t need a custom CI system or a bespoke container registry. They do need custom service templates, deployment policies, and integration glue that ties everything together in a way that makes sense for their stack.</p>
<p>The real cost is the ongoing maintenance and integration work, more than the license fee. Every tool you adopt becomes a dependency you have to keep running.</p>
<h2 id="the-team-matters">The Team Matters</h2>
<p>A platform team needs infrastructure depth, software engineering skill, and (this gets overlooked) the ability to write decent documentation. A platform nobody can figure out is a platform nobody uses.</p>
<p>Avoid the ticket-queue trap. If the platform team&rsquo;s primary interface is a Jira board where developers file requests and wait, you&rsquo;ve built an ops team with a fancier name. The whole point is self-service.</p>
<p>Platform engineering is DevOps growing up, acknowledging that &ldquo;everyone does everything&rdquo; doesn&rsquo;t scale, and building the shared foundations that let teams move fast without reinventing the wheel every sprint.</p>
]]></content:encoded></item><item><title>Cloud Cost Management: You Do Not Need a FinOps Team</title><link>https://lawzava.com/blog/2022-10-03-cloud-cost-management/</link><pubDate>Mon, 03 Oct 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-10-03-cloud-cost-management/</guid><description>Most cloud cost work is deleting unused resources, tagging, right-sizing, and committing to the baseline. Engineering teams should own it.</description><content:encoded><![CDATA[<p>The FinOps Foundation would like you to believe that cloud cost management is a specialized discipline requiring dedicated teams, certified practitioners, and a maturity model. I would like you to believe that it&rsquo;s mostly about deleting things nobody uses and tagging the rest.</p>
<p>I&rsquo;ve managed cloud spend at startups and at enterprises, and I still treat  <a href="/blog/2019-12-02-finops-cloud-cost-management/"
   
   >the bill as a design document</a>
. The problems are always the same. The solutions are always boring. The industry has wrapped basic hygiene in jargon and created a cottage industry of dashboards, consultants, and conferences around it.</p>
<h2 id="the-problem-isnt-complicated">The Problem Isn&rsquo;t Complicated</h2>
<p>Cloud spend drifts for three reasons:</p>
<p><strong>Nobody owns it.</strong> Engineering provisions resources. Finance sees the bill. Neither side can attribute cost to a service, team, or decision. When nobody owns a cost center, the cost center grows. Always.</p>
<p><strong>Defaults are generous.</strong> Console presets and copy-pasted templates push you toward larger instances, higher throughput, and more redundancy than you need. Good for reliability. Bad for your invoice. An <code>m5.xlarge</code> picked &ldquo;to be safe&rdquo; for a service that uses 200m CPU is waste.</p>
<p><strong>Data transfer is invisible.</strong> Most teams focus on compute and storage. Data transfer costs creep in silently. Cross-AZ traffic, cross-region replication, egress to the internet. At one company, data transfer was 30% of the monthly bill and nobody on the engineering team knew it.</p>
<h2 id="six-boring-fixes">Six Boring Fixes</h2>
<h3 id="delete-things">Delete Things</h3>
<p>Start here. Always start here.</p>
<p>Unused load balancers. Orphaned EBS volumes. Dev environments that have been running for six months with nobody logged in. Staging environments sized like production. Snapshots from 2020.</p>
<p>At every company I&rsquo;ve consulted for, the first pass of deleting unused resources has cut the bill by 15-25%. No analysis required. No tooling required. Just look at what&rsquo;s running and ask &ldquo;does anyone use this?&rdquo;</p>
<h3 id="tag-everything">Tag Everything</h3>
<p>Enforce tags at creation time. Team, service, environment, cost center. If a resource doesn&rsquo;t have tags, it&rsquo;s impossible to attribute costs. If it&rsquo;s impossible to attribute costs, nobody owns them.</p>
<p>This isn&rsquo;t a technology problem. It&rsquo;s a policy problem. Terraform can enforce tags. AWS Service Control Policies can enforce tags. The blocker is usually organizational will, not tooling.</p>
<h3 id="right-size-with-data">Right-Size With Data</h3>
<p>Look at two weeks of CPU and memory utilization (the same approach works for  <a href="/blog/2021-10-18-cost-effective-kubernetes/"
   
   >Kubernetes requests</a>
). If a service averages 10% CPU utilization, it&rsquo;s oversized. Downsize it. If it averages 80%, leave it alone or optimize the code.</p>
<p>Don&rsquo;t right-size based on a single metric snapshot. Don&rsquo;t right-size based on a vendor tool&rsquo;s recommendation without verifying it. I&rsquo;ve seen recommendations that would have put a production database below its working set size.</p>
<h3 id="commit-to-the-floor-not-the-ceiling">Commit to the Floor, Not the Ceiling</h3>
<p>Reserved instances and savings plans save money on stable baseline usage. The key word is &ldquo;stable.&rdquo; Commit to the minimum you&rsquo;re confident you&rsquo;ll use for the next year. Keep the variable portion on demand.</p>
<p>Over-commitment is a hidden risk. Product priorities shift. Services get decommissioned. A three-year RI on infrastructure you no longer need is a sunk cost.</p>
<h3 id="spot-instances-for-batch-work">Spot Instances for Batch Work</h3>
<p>Batch jobs, CI runners, data pipelines: anything that can tolerate interruption. AWS advertises Spot discounts of up to 90% off on-demand prices. The catch is fault tolerance. If your batch job can&rsquo;t resume after an interruption, spot isn&rsquo;t for you.</p>
<h3 id="watch-data-transfer">Watch Data Transfer</h3>
<p>Put services close to their data. Cache aggressively. Avoid cross-region traffic when same-region will do. Set up a VPC endpoint instead of routing through the public internet. These are small changes with disproportionate cost impact.</p>
<h2 id="why-finops-annoys-me">Why FinOps Annoys Me</h2>
<p>The FinOps movement takes straightforward engineering practices (tagging, right-sizing, deleting waste, monitoring spend) and wraps them in a maturity model with phases like &ldquo;Inform, Optimize, Operate.&rdquo; It creates the illusion that cloud cost management requires a specialized function.</p>
<p>It doesn&rsquo;t. It requires engineering teams that own their costs and a culture that treats waste as a bug. That&rsquo;s it.</p>
<p>A dedicated FinOps team is a sign that your engineering organization has externalized a responsibility it should own. It&rsquo;s the same antipattern as a separate &ldquo;quality&rdquo; team that owns testing: it lets everyone else stop caring.</p>
<p>No maturity model required. No certifications. No dedicated team. Just engineers who look at what they run and care about what it costs.</p>
]]></content:encoded></item><item><title>Kubernetes Requests and Limits: Lessons From an Outage</title><link>https://lawzava.com/blog/2022-09-05-kubernetes-resource-management/</link><pubDate>Mon, 05 Sep 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-09-05-kubernetes-resource-management/</guid><description>CPU is compressible and memory is not. How to set Kubernetes requests and limits from real usage, when to skip CPU limits, and which guardrails to add.</description><content:encoded><![CDATA[<p>Last year, a team I was working with shipped a service to production with no resource requests and no limits. Default namespace, no LimitRange, no guardrails. The service had a slow memory leak, about 50 MB per hour. Harmless-looking. For the first two days it ran fine.</p>
<p>On day three, during a traffic spike, the container hit 4 GB. Kubernetes evicted it. The replacement pod landed on the same node, inherited the same traffic, and the cycle repeated. The node started thrashing. Other pods on that node, including a critical payment service, got evicted too. What started as one leaky container turned into a 45-minute partial outage.</p>
<p>The fix was three lines of YAML. The investigation took most of a day.</p>
<h2 id="the-one-thing-you-need-to-understand">The One Thing You Need to Understand</h2>
<p>CPU is compressible. When a container exceeds its CPU limit, Kubernetes throttles it. The process slows down but keeps running.</p>
<p>Memory isn&rsquo;t compressible. When a container exceeds its memory limit, the kernel kills it. OOMKilled. The process restarts from scratch.</p>
<p>That asymmetry drives almost every resource management decision. CPU limits trade throughput for predictability. Memory limits are a hard safety net. Treat them differently.</p>
<h2 id="requests-vs-limits">Requests vs Limits</h2>
<p><strong>Requests</strong> tell the scheduler how much capacity to reserve. They&rsquo;re a guarantee. The scheduler won&rsquo;t place your pod on a node unless that capacity is available.</p>
<p><strong>Limits</strong> tell the runtime when to intervene. CPU limits trigger throttling. Memory limits trigger OOMKill.</p>
<p>A cluster can overcommit limits: the sum of all limits can exceed the node capacity. It can&rsquo;t overcommit requests. This is why requests matter more for scheduling stability.</p>
<h2 id="setting-them">Setting Them</h2>
<p>Start with measurements, not guesses. If your monitoring shows a Go service using 150m CPU on average and 200 MB of memory, set requests near those values:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;200m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;512Mi&#34;</span>
</span></span></code></pre></div><p>Notice: no CPU limit. For latency-sensitive services, CPU limits can cause throttling spikes even when the node has spare capacity. The CFS throttling behavior in Linux is well-documented and surprising: a service using 180m of CPU can still get throttled with a 200m limit because of how the quota is applied per scheduling period.</p>
<p>I default to: set CPU requests, skip CPU limits, always set memory limits. That is a change from  <a href="/blog/2020-06-01-kubernetes-resource-management/"
   
   >what I recommended in 2020</a>
. If you need CPU limits for noisy-neighbor isolation, set them generously: 2x to 3x the request.</p>
<h2 id="measuring-and-iterating">Measuring and Iterating</h2>
<p>Don&rsquo;t set values once and forget them. Traffic patterns change. Code changes. Dependencies change.</p>
<p>Look at these metrics over at least two weeks:</p>
<ul>
<li>Container CPU usage (the actual usage, not the request)</li>
<li>Memory working set (not RSS; working set is what Kubernetes uses for eviction decisions)</li>
<li>Throttling events (container_cpu_cfs_throttled_seconds_total)</li>
</ul>
<p>Set requests around the P50 usage. Set memory limits around the P99 plus a buffer. If you&rsquo;re seeing throttling events on a latency-sensitive service, either raise the CPU limit or remove it.</p>
<p>Tools like VPA (Vertical Pod Autoscaler) can generate recommendations. Use them as input, not as autopilot. I&rsquo;ve seen VPA recommendations that would have cut memory limits below the application&rsquo;s startup footprint. Trust but verify.</p>
<h2 id="qos-classes">QoS Classes</h2>
<p>Kubernetes assigns a quality-of-service class based on how you configure requests and limits:</p>
<ul>
<li><strong>Guaranteed</strong>: requests equal limits for both CPU and memory. Last to be evicted.</li>
<li><strong>Burstable</strong>: requests set but not equal to limits. Most common. Middle priority for eviction.</li>
<li><strong>BestEffort</strong>: no requests, no limits. First to be evicted. Never appropriate for production.</li>
</ul>
<p>The team from my opening story ran BestEffort pods on shared nodes. When the node ran out of memory, the BestEffort pods died first, as designed. But the leaky pod kept landing back on the same node, and the pressure spread to the other pods running there.</p>
<h2 id="guardrails">Guardrails</h2>
<p>Two cluster-level mechanisms prevent the &ldquo;no resources specified&rdquo; scenario:</p>
<p><strong>LimitRange</strong> sets default requests and limits for containers that don&rsquo;t specify their own. Set this on every namespace. The defaults don&rsquo;t need to be perfect. They need to prevent BestEffort pods from accidentally running in production.</p>
<p><strong>ResourceQuota</strong> caps the total resource usage in a namespace. This prevents one team from consuming the cluster. Set quotas per team namespace and review them quarterly.</p>
<p>At a large consumer platform, we enforced both. LimitRange provided sane defaults, and ResourceQuota kept growth predictable. Teams that needed more resources had to justify it, which had the side effect of surfacing inefficient services early.</p>
<h2 id="the-three-failure-modes">The Three Failure Modes</h2>
<p><strong>OOMKilled.</strong> Memory limit is too low, or the application has a leak. Check the limit against actual usage. If usage grows linearly over time, it&rsquo;s a leak: fix the code, not the limit.</p>
<p><strong>CPU throttling.</strong> Shows up as latency spikes with available CPU on the node. The container is being throttled by CFS even though the node isn&rsquo;t overloaded. Raise or remove the CPU limit.</p>
<p><strong>Pending pods.</strong> Requests are too large for available capacity, or scheduling constraints (node selectors, taints, affinity rules) are too restrictive. Either reduce requests, add capacity, or relax the constraints.</p>
<p>All three are observable. All three are fixable. The usual problem is that nobody looks at the metrics until an incident forces it.</p>
<h2 id="the-minimum">The Minimum</h2>
<ol>
<li>Set memory limits on every container. No exceptions.</li>
<li>Set CPU requests based on measured usage.</li>
<li>Apply LimitRange defaults on every namespace.</li>
<li>Apply ResourceQuota on team namespaces.</li>
<li>Monitor throttling, OOMKill events, and pending pods.</li>
<li>Review resource settings quarterly or after significant traffic changes.</li>
</ol>
<p>Resource management isn&rsquo;t glamorous work. It&rsquo;s the kind of thing that only gets attention after an outage. But three lines of YAML and fifteen minutes of looking at Grafana dashboards can prevent the kind of cascading failure that ruins your weekend.</p>
]]></content:encoded></item><item><title>Platform Engineering Maturity: Why Teams Stall at Level 2</title><link>https://lawzava.com/blog/2021-11-01-platform-engineering-maturity/</link><pubDate>Mon, 01 Nov 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-11-01-platform-engineering-maturity/</guid><description>Most platform teams build tools nobody asked for while developers wait in ticket queues. Lessons from platform teams at large companies.</description><content:encoded><![CDATA[<p>Platform engineering maturity has five real levels: ad hoc, standardized, self-service, product-minded, and optimized. Most enterprise platform teams I&rsquo;ve seen are stuck between levels 1 and 2. The fix isn&rsquo;t more tools. It&rsquo;s treating the platform as a product with actual users, feedback loops, and a definition of &ldquo;done&rdquo; that includes developer happiness. Stop building tools nobody asked for. Start removing ticket queues.</p>
<p>I&rsquo;ve looked closely at platform teams in a number of large organizations. The pattern is depressingly consistent. There&rsquo;s a &ldquo;platform team&rdquo; somewhere in the org chart. They have built some Terraform modules. Maybe a Jenkins pipeline or two. Possibly a  <a href="/blog/2021-03-08-internal-developer-portals/"
   
   >Backstage instance</a>
 that three people use.</p>
<p>Meanwhile, developers still open Jira tickets to get a database provisioned. Lead time from &ldquo;I need an environment&rdquo; to &ldquo;I have an environment&rdquo; is measured in days. Sometimes weeks.</p>
<p>The platform team is busy. They are just busy building the wrong thing.</p>
<h2 id="the-five-levels">The Five Levels</h2>
<p>I use a maturity model when I assess platform teams. Not because I love frameworks, but because it gives me and the team a shared language for &ldquo;here&rsquo;s where you are&rdquo; and &ldquo;here&rsquo;s where the payoff is.&rdquo;</p>
<p><strong>Level 1: Ad hoc.</strong> Everything is manual. Knowledge lives in people&rsquo;s heads. Getting a new environment means finding the one person who knows how. Deployments happen on Tuesdays because that&rsquo;s when the ops team has capacity. This is where most startups are and where some surprisingly large companies still live.</p>
<p><strong>Level 2: Standardized.</strong> Infrastructure as code exists. CI pipelines exist. But the platform team gates everything. Developers can build, but they can&rsquo;t deploy or provision without filing a request. The automation is real, but the bottleneck just moved.</p>
<p><strong>Level 3: Self-service.</strong> Developers can provision environments, deploy services, and roll back on their own, within guardrails. This is where the platform starts paying for itself. Wait time drops. Consistency improves. The platform team stops being a ticket queue and starts being an enablement function.</p>
<p><strong>Level 4: Product-minded.</strong> The platform has a roadmap based on user feedback. There are golden paths for common service types. An internal CLI or portal handles the 80% case. Progressive delivery is standard. The platform team runs NPS surveys and actually reads them.</p>
<p><strong>Level 5: Optimized.</strong> Data drives changes to platform defaults. Automated remediation handles known failure modes. Cost, reliability, and security metrics improve continuously. This is rare. I&rsquo;ve seen maybe two organizations here.</p>
<h2 id="where-most-enterprises-actually-are">Where Most Enterprises Actually Are</h2>
<p>Based on what I&rsquo;ve seen in large, telecom-scale organizations, most enterprises are at Level 2. Some pockets are at Level 3. Almost nobody is at Level 4.</p>
<p>The gap between Level 2 and Level 3 is the most expensive gap in enterprise software delivery. It&rsquo;s the difference between &ldquo;we have automation&rdquo; and &ldquo;developers can actually use it without waiting.&rdquo; Every day a developer spends waiting for provisioning is a day they aren&rsquo;t shipping features. Multiply that by hundreds of developers and the cost is staggering.</p>
<h2 id="why-platform-teams-get-stuck">Why Platform Teams Get Stuck</h2>
<p>Three patterns I see repeatedly:</p>
<p><strong>Building tools instead of workflows.</strong> A team builds a slick Terraform module for spinning up EKS clusters. Great. But developers still need to file a ticket to get the module run for them. The tool exists. The self-service doesn&rsquo;t. The developer experience didn&rsquo;t actually change.</p>
<p><strong>Centralizing instead of enabling.</strong> The platform team takes on more ownership instead of giving ownership away safely. They become the bottleneck for every infrastructure change. They are drowning in operational work and have no time to build the self-service layer that would free them.</p>
<p><strong>Ignoring their users.</strong> Platform teams that never talk to developers build platforms that developers avoid. I&rsquo;ve walked into organizations where three competing shadow platforms exist because the official one didn&rsquo;t solve actual problems.</p>
<h2 id="how-to-move-up">How to Move Up</h2>
<h3 id="level-1-to-2-document-and-automate-the-basics">Level 1 to 2: Document and automate the basics</h3>
<p>Write down how to get from code to production. Adopt infrastructure as code for core services. Get CI pipelines running. Define standard environments. This isn&rsquo;t glamorous work, but it&rsquo;s the foundation.</p>
<h3 id="level-2-to-3-kill-the-ticket-queue">Level 2 to 3: Kill the ticket queue</h3>
<p>This is the most impactful transition. Give developers templates and modules they can run themselves. Automate deployments with guardrails instead of gates that require human approval for every change. Bake observability in by default so developers don&rsquo;t have to think about it. Clarify who owns what.</p>
<p>The goal is simple: a developer should be able to go from &ldquo;I want to deploy&rdquo; to &ldquo;it&rsquo;s deployed&rdquo; in minutes, not days.</p>
<h3 id="level-3-to-4-treat-it-like-a-product">Level 3 to 4: Treat it like a product</h3>
<p>Build an  <a href="/blog/2019-03-11-building-internal-developer-platforms/"
   
   >internal CLI or portal</a>
 around the most common workflows. Publish golden paths and reduce unnecessary choices. Measure developer satisfaction. Run feedback sessions. Build a roadmap based on what developers actually need, not what the platform team thinks is interesting.</p>
<h3 id="level-4-to-5-let-data-drive">Level 4 to 5: Let data drive</h3>
<p>Automate remediations for known incident types. Tune platform defaults based on real usage patterns. Track cost and performance per service. This is continuous improvement territory; it never ends.</p>
<h2 id="the-assessment">The Assessment</h2>
<p>When I assess a platform, I score six dimensions independently:</p>
<ol>
<li>Infrastructure provisioning and environment consistency</li>
<li>Delivery workflows and release safety</li>
<li>Observability and operational feedback</li>
<li>Security controls and policy enforcement</li>
<li>Developer experience and self-service</li>
<li>Platform team operating model and ownership</li>
</ol>
<p>Each one gets a 1-5. I don&rsquo;t average them. I look at the lowest two scores because that&rsquo;s where the biggest drag on delivery lives. A platform with great CI/CD but terrible provisioning still has developers waiting.</p>
<h2 id="metrics-that-show-its-working">Metrics That Show It&rsquo;s Working</h2>
<p>Forget vanity metrics. These tell you if your platform is working:</p>
<ul>
<li><strong>Lead time from commit to production.</strong> If this is longer than an hour for most services, something is broken.</li>
<li><strong>Time to provision a new environment.</strong> If this takes more than a day, your self-service isn&rsquo;t actually self-service.</li>
<li><strong>Deployment frequency.</strong> Teams that trust their platform deploy more often.</li>
<li><strong>Developer satisfaction.</strong> Ask them. Regularly. Act on what they say.</li>
</ul>
<p>The best platform I&rsquo;ve seen wasn&rsquo;t the one with the most tools. It was the one where a new developer could deploy their first change to production on day one. Everything else followed from that principle.</p>
<h2 id="its-an-organizational-problem">It&rsquo;s an Organizational Problem</h2>
<p>Most platform immaturity is organizational. The platform team doesn&rsquo;t have a mandate to remove ticket queues. Leadership doesn&rsquo;t treat developer experience as a first-class investment. The team is understaffed for the scope they&rsquo;re expected to cover.</p>
<p>If you&rsquo;re a platform engineer reading this and nodding, show this to your VP of Engineering. The conversation needs to happen at the level where headcount and priorities are decided. No amount of Terraform modules fixes a staffing problem.</p>
]]></content:encoded></item><item><title>Kubernetes Cost Optimization: Right-Size Requests First</title><link>https://lawzava.com/blog/2021-10-18-cost-effective-kubernetes/</link><pubDate>Mon, 18 Oct 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-10-18-cost-effective-kubernetes/</guid><description>Most clusters I review run at 20-40% utilization. How I help teams cut Kubernetes bills: honest requests, better bin packing, spot nodes, and ownership.</description><content:encoded><![CDATA[<p>Most Kubernetes clusters I review run at 20-40% actual utilization. The rest is wasted money. Right-size your resource requests using real data, stop guessing at CPU limits, use spot instances for stateless workloads, and review your spend monthly. I&rsquo;ve helped teams cut 30-50% off their K8s bills without a single reliability regression.</p>
<p>Last month I looked at a company spending $47k/month on their Kubernetes clusters. Actual utilization across the board? 23%. They were paying for ghost capacity that nobody asked for and nobody noticed.</p>
<p>This isn&rsquo;t unusual. I see it at almost every large company I look at. Kubernetes makes scaling trivially easy, which makes over-provisioning trivially easy too. Some developer copy-pasted resource requests from a Stack Overflow answer two years ago. Nobody questioned it. The cluster autoscaler dutifully spun up nodes to satisfy those fictional requests. The bill grew.</p>
<h2 id="why-your-bill-keeps-growing">Why Your Bill Keeps Growing</h2>
<p>Kubernetes schedules based on resource <em>requests</em>, not actual usage. Your cloud bill pays for the nodes backing those requests. If every pod requests 2 CPU cores but uses 0.3, you&rsquo;re paying for 2 cores of node capacity per pod. Multiply that across hundreds of pods. Yeah.</p>
<p>The usual culprits:</p>
<ul>
<li>Copy-pasted resource blocks that become tribal defaults</li>
<li>Memory requests inflated to &ldquo;never get OOM-killed&rdquo; levels, without anyone measuring actual usage</li>
<li>CPU limits set defensively and never revisited</li>
<li>Zero cost visibility per team or per service</li>
</ul>
<h2 id="measure-first-seriously">Measure First. Seriously.</h2>
<p>Before you touch a single resource request, get visibility into what you&rsquo;re actually using.</p>
<p>Build a dashboard (Grafana, Kubecost, whatever) that shows:</p>
<ul>
<li>CPU requested vs CPU used at p95</li>
<li>Memory requested vs peak working set</li>
<li>Utilization broken down by namespace</li>
<li>Nodes that can&rsquo;t scale down because of inflated requests</li>
</ul>
<p>The first time an engineering lead sees their team requesting 64 cores and using 11, the conversation changes fast.</p>
<h2 id="the-cost-comparison">The Cost Comparison</h2>
<p>Here is what I typically see before and after right-sizing:</p>
<table>
  <thead>
      <tr>
          <th>Category</th>
          <th>Before</th>
          <th>After</th>
          <th>Savings</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CPU requests vs actual</td>
          <td>4x over-provisioned</td>
          <td>1.3x buffer</td>
          <td>~60% node reduction</td>
      </tr>
      <tr>
          <td>Memory requests</td>
          <td>2-3x peak usage</td>
          <td>Peak + 20% buffer</td>
          <td>~40% node reduction</td>
      </tr>
      <tr>
          <td>Node types</td>
          <td>Single large instance type</td>
          <td>Mixed instance pools</td>
          <td>15-25% better bin packing</td>
      </tr>
      <tr>
          <td>Spot usage</td>
          <td>0%</td>
          <td>40-60% of stateless workloads</td>
          <td>60-70% on those nodes</td>
      </tr>
      <tr>
          <td>Dev/staging environments</td>
          <td>Same specs as prod</td>
          <td>Right-sized, spot-heavy</td>
          <td>50-70% reduction</td>
      </tr>
      <tr>
          <td><strong>Typical monthly bill</strong></td>
          <td><strong>$45-50k</strong></td>
          <td><strong>$20-25k</strong></td>
          <td><strong>45-55%</strong></td>
      </tr>
  </tbody>
</table>
<p>These are real numbers from a mid-sized company running about 200 microservices. Your mileage will vary, but the pattern holds.</p>
<h2 id="right-size-resource-requests">Right-Size Resource Requests</h2>
<p> <a href="/blog/2020-06-01-kubernetes-resource-management/"
   
   >Stop guessing</a>
. Use p95 CPU as your request baseline. Use peak memory working set plus a 15-20% buffer for memory requests. Set memory limits to protect against runaway processes. Drop CPU limits entirely for most services. They cause throttling that hurts latency more than it helps anything.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#ae81ff">250m     </span> <span style="color:#75715e"># based on p95 actual usage</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">1Gi   </span> <span style="color:#75715e"># based on peak working set + buffer</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#ae81ff">2Gi   </span> <span style="color:#75715e"># hard cap for runaway protection</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># no CPU limit -- let it burst</span>
</span></span></code></pre></div><p>Run VPA in recommendation mode first. It watches actual usage and suggests request values. Don&rsquo;t let it auto-apply yet. Review the recommendations manually, make sure they make sense, then update your deployments. Build trust before you automate.</p>
<h2 id="fix-the-node-layer">Fix the Node Layer</h2>
<p>Right-sized pods on wrong-sized nodes still waste money. If you have a bunch of pods requesting 500m CPU and 512Mi memory, shoving them onto m5.4xlarge instances is terrible bin packing.</p>
<p>Mix your instance types. Use smaller instances for smaller workloads. Create separate node pools for workloads with different profiles (CPU-heavy vs memory-heavy vs general). Enable Cluster Autoscaler and let it remove nodes that are actually idle.</p>
<p>Autoscaling only works when requests are honest. Inflated requests mean the autoscaler sees &ldquo;full&rdquo; nodes everywhere and keeps adding capacity you don&rsquo;t need.</p>
<h2 id="spot-instances-free-money-almost">Spot Instances: Free Money (Almost)</h2>
<p>Spot instances are typically 60-70% cheaper than on-demand. The catch: they can be reclaimed with two minutes notice.</p>
<p>Good candidates for spot:</p>
<ul>
<li>Stateless services with 3+ replicas</li>
<li>Batch jobs and queue processors</li>
<li>Dev and staging environments (all of it)</li>
<li>Anything that handles graceful shutdown</li>
</ul>
<p>Bad candidates: single-replica stateful services, databases, anything where losing a node means losing data.</p>
<p>Use taints and tolerations to pin critical workloads to on-demand nodes. Spread spot across multiple instance types and AZs so reclamation doesn&rsquo;t take out your entire fleet at once.</p>
<h2 id="guardrails-that-stick">Guardrails That Stick</h2>
<p>Optimization without guardrails is a one-time win. Things drift back within months.</p>
<p>Set up:</p>
<ul>
<li><strong>ResourceQuota</strong> per namespace so no team can accidentally claim half the cluster</li>
<li><strong>LimitRange</strong> to enforce minimum and maximum resource requests</li>
<li><strong>Labels</strong> for team, environment, and service. You need these for cost allocation</li>
<li><strong>Monthly review cadence</strong>: look at the top 10 over-provisioned workloads, adjust, repeat</li>
</ul>
<p>That last one matters most. Cost optimization isn&rsquo;t a project. It&rsquo;s a habit. A 30-minute monthly review catches drift before it becomes a $10k/month problem.</p>
<h2 id="cost-is-an-ownership-problem">Cost Is an Ownership Problem</h2>
<p>Most Kubernetes cost problems aren&rsquo;t technical problems. They&rsquo;re ownership problems.  <a href="/blog/2019-12-02-finops-cloud-cost-management/"
   
   >Nobody owns the bill</a>
. Nobody sees the bill broken down by team. Nobody gets asked &ldquo;why are you requesting 8 cores for a service that peaks at 0.5?&rdquo;</p>
<p>Fix the visibility and the accountability first. The technical optimization follows naturally.</p>
<p>I&rsquo;ve watched teams cut their bills in half within two months just by making resource usage visible to the teams that own the workloads. No fancy tooling. No replatforming. Just data, ownership, and a monthly conversation.</p>
]]></content:encoded></item><item><title>Zero Trust Architecture: Identity First, Then Segmentation</title><link>https://lawzava.com/blog/2021-08-23-zero-trust-architecture/</link><pubDate>Mon, 23 Aug 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-08-23-zero-trust-architecture/</guid><description>Zero trust from cyber-defense exercises and a large telecom: identity first, default-deny segmentation, device posture, and common mistakes.</description><content:encoded><![CDATA[<p>Zero trust means &ldquo;stop trusting the network.&rdquo; Every request gets authenticated and authorized regardless of where it comes from. Implementation order: identity first (MFA + service identity), then segmentation (default deny between services), then continuous verification (device posture, context signals). Most companies fail by buying a product instead of changing their architecture.</p>
<p>I&rsquo;ve seen zero trust from two very different angles. Taking part in cyber-defense exercises put me in environments where &ldquo;trust nothing&rdquo; wasn&rsquo;t a buzzword. It was the baseline assumption. Everything was compartmentalized. Access was explicit, scoped, and audited. You didn&rsquo;t get access to a system because you were on the right network. You got access because you had the right role, the right need-to-know, and the right authentication.</p>
<p>Then there was a large telecom company I worked with. It had a corporate VPN and the assumption that anything inside the VPN was trusted. Thousands of employees, contractors, and partners, all on the same flat network. When I asked about lateral movement controls, the answer was basically &ldquo;we have firewalls at the perimeter.&rdquo; The perimeter. With half the workforce remote.</p>
<p>That gap between defense-grade security thinking and typical enterprise reality is where zero trust lives.</p>
<h2 id="what-zero-trust-actually-means">What zero trust actually means</h2>
<p>Strip away the vendor marketing and zero trust is four ideas:</p>
<p><strong>Verify explicitly.</strong> Every request is authenticated and authorized. Not just at the front door, but at every service boundary. A valid network connection isn&rsquo;t a valid credential.</p>
<p><strong>Least privilege.</strong> Access is scoped to what you need, when you need it. Privileged access is temporary, auditable, and rare. No permanent admin tokens sitting in CI pipelines.</p>
<p><strong>Assume breach.</strong> Design your architecture so that a compromised service or endpoint has limited blast radius. If an attacker gets into one service, they shouldn&rsquo;t be able to reach everything.</p>
<p><strong>No implicit trust.</strong> Internal traffic isn&rsquo;t automatically trusted. The network is a transport layer, not a security boundary.</p>
<p>NIST SP 800-207 formalized this in 2020. It&rsquo;s worth reading. The core idea: access decisions are policy-driven and tied to identity, device posture, and context rather than IP addresses.</p>
<h2 id="identity-is-the-control-plane">Identity is the control plane</h2>
<p>This is where implementation starts. Everything else depends on knowing who or what is making a request.</p>
<p><strong>For humans:</strong> MFA everywhere. Non-negotiable. Ideally FIDO2/WebAuthn for phishing resistance. At minimum, app-based TOTP. SMS is better than nothing but barely. Centralize on one SSO provider so policy changes propagate everywhere.</p>
<p>At one telecom company, we found 14 different authentication systems. Some services used LDAP directly. Some had their own user databases. Two services still accepted basic auth over internal HTTP. Consolidating to a single identity provider took months but it was the foundation for everything else.</p>
<p><strong>For services:</strong> Every service gets an identity. Short-lived credentials. Mutual TLS between services where possible. OAuth 2.0 / OIDC for API-to-API access. No long-lived static tokens.</p>
<p>The pattern I&rsquo;ve been recommending:</p>
<pre tabindex="0"><code>Client -&gt; Identity Provider -&gt; Short-lived JWT -&gt; Service A
Service A -&gt; mTLS with SPIFFE identity -&gt; Service B
Service B -&gt; IAM role (scoped) -&gt; Database
</code></pre><p>Each hop has its own authentication. Each credential is scoped and temporary. If Service A is compromised, the attacker gets Service A&rsquo;s permissions, not Service B&rsquo;s database credentials.</p>
<h2 id="segmentation-default-deny">Segmentation: default deny</h2>
<p>The network architecture piece that makes zero trust tangible. Every service-to-service connection is explicitly allowed. Everything else is denied.</p>
<p>In Kubernetes, this starts with  <a href="/blog/2019-04-22-kubernetes-security-hardening/"
   
   >NetworkPolicies</a>
:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">NetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">orders-service</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">orders</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyTypes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">Ingress</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">Egress</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">from</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">app</span>: <span style="color:#ae81ff">api-gateway</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">port</span>: <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">egress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">to</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">app</span>: <span style="color:#ae81ff">postgres</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">port</span>: <span style="color:#ae81ff">5432</span>
</span></span></code></pre></div><p>The orders service can only receive traffic from the API gateway and can only talk to its own database. Nothing else. If an attacker compromises the orders service, they can&rsquo;t pivot to the user service, the payment service, or anything else on the network.</p>
<p>This is basic stuff but I&rsquo;ve seen it absent at companies with hundreds of engineers. The excuse is always &ldquo;we&rsquo;ll add network policies later.&rdquo; Later never comes.</p>
<p>If you&rsquo;re running a service mesh (Istio, Linkerd), you get mTLS and authorization policies on top of network segmentation. The mesh handles the identity and encryption at the transport layer. Your services don&rsquo;t need to implement TLS themselves.</p>
<h2 id="device-posture">Device posture</h2>
<p>With remote work as the default now, you can&rsquo;t assume devices are on a managed network. Device trust needs to be part of the access decision.</p>
<p>Signals that matter:</p>
<ul>
<li>Is this a managed/enrolled device?</li>
<li>Is the OS patched?</li>
<li>Is disk encryption enabled?</li>
<li>Is EDR running?</li>
</ul>
<p>An unmanaged device connecting from an unusual location at 3am should get constrained access at best. Not blocked entirely (that breaks usability), but restricted to lower-sensitivity resources until additional verification happens.</p>
<p>At that same company, we implemented tiered access. Managed devices with current patches got full access. Unmanaged devices got read-only access to a subset of tools. Unknown devices got nothing except the enrollment portal.</p>
<h2 id="the-implementation-path">The implementation path</h2>
<p>This is how I recommend rolling it out. Four phases, roughly in order:</p>
<p><strong>Phase 1: Inventory and visibility.</strong> You can&rsquo;t secure what you don&rsquo;t know about. Map your identities (human and service), your assets, and your data flows. Turn on logging for authentication and authorization decisions. This alone will surface problems you didn&rsquo;t know you had.</p>
<p><strong>Phase 2: Identity.</strong> Enforce MFA for all human access. Standardize on SSO. Establish service identities with short-lived credentials. Kill long-lived static tokens.</p>
<p><strong>Phase 3: Segmentation.</strong> Implement default deny between services. Start with your most sensitive systems (payments, PII, auth) and expand outward. Network policies in Kubernetes, security groups in AWS, or service mesh authorization policies.</p>
<p><strong>Phase 4: Continuous verification.</strong> Add device posture to access decisions. Implement risk-based re-authentication. Monitor for anomalous behavior. This phase never really ends; it&rsquo;s ongoing tuning.</p>
<h2 id="what-companies-get-wrong">What companies get wrong</h2>
<p><strong>Buying a product.</strong> Zero trust is an architecture, not a SKU. I&rsquo;ve seen companies spend millions on a &ldquo;zero trust platform&rdquo; and still have flat networks, long-lived credentials, and no service-to-service authentication. The product doesn&rsquo;t help if the architecture doesn&rsquo;t change.</p>
<p><strong>Ignoring legacy.</strong> The oldest, crustiest systems are often the most sensitive. Leaving them outside the zero trust model because they &ldquo;can&rsquo;t support it&rdquo; means your biggest risk is unprotected.</p>
<p><strong>Breaking developer workflows.</strong> If the security controls are so painful that developers route around them (hardcoding credentials, disabling MFA on service accounts, punching holes in network policies), you&rsquo;ve made things worse. Security that people circumvent is security theater.</p>
<p><strong>VPN as a crutch.</strong> A  <a href="/blog/2020-11-02-zero-trust-vs-vpn/"
   
   >VPN</a>
 puts you on the network. That&rsquo;s it. It&rsquo;s not authentication. It&rsquo;s not authorization. It&rsquo;s not segmentation. I still see organizations where VPN access grants implicit trust to hundreds of internal services. That&rsquo;s a perimeter model with extra steps.</p>
<h2 id="where-to-start">Where to start</h2>
<p>Zero trust is the right direction. The perimeter model is dead and pretending otherwise is dangerous. But implementation is hard, slow, and requires sustained investment. It&rsquo;s a multi-year journey for most organizations, not a quarterly initiative.</p>
<p>Start with identity. Get that right. Then segment. Then add continuous verification. Don&rsquo;t try to do everything at once. And don&rsquo;t let a vendor tell you their product is the answer. The answer is architecture change, and that&rsquo;s harder than writing a purchase order.</p>
]]></content:encoded></item><item><title>Postgres Reliability: Restores, Failover, Safe Migrations</title><link>https://lawzava.com/blog/2021-08-09-database-reliability-engineering/</link><pubDate>Mon, 09 Aug 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-08-09-database-reliability-engineering/</guid><description>Practical database reliability from running Postgres in production: configs, safe migration patterns, and the operational habits that prevent outages.</description><content:encoded><![CDATA[<p>Define your RPO/RTO before you pick a replication strategy. Test your restores monthly; most backup failures are actually restore failures. Use  <a href="/blog/2016-08-15-database-migrations-without-downtime/"
   
   >safe migration patterns</a>
 (nullable columns, concurrent indexes, lock timeouts). Monitor replication lag, slow queries, and disk growth. Practice failovers before you need them. Below are the Postgres configs and SQL patterns I use.</p>
<p>I&rsquo;ve been responsible for databases at the fintech startup (financial data platform, Postgres), at my infrastructure startup, and at several enterprises. The lesson that took me the longest to internalize: database reliability is mostly a habits problem.</p>
<p>You can run the best hardware, the fanciest replication setup, the most expensive managed service. If you don&rsquo;t practice restores, don&rsquo;t test migrations against production data volumes, and don&rsquo;t have runbooks for failover, you&rsquo;re going to have a bad day. The technology just determines how bad.</p>
<h2 id="start-with-numbers-not-architecture">Start with numbers, not architecture</h2>
<p>Before touching any config, agree on three numbers with your team and your business stakeholders:</p>
<p><strong>RPO (Recovery Point Objective):</strong> How much data can you lose? For the fintech startup&rsquo;s financial data, the answer was &ldquo;essentially zero&rdquo;: we couldn&rsquo;t afford to lose transactions. For an analytics pipeline, losing 5 minutes of data might be fine.</p>
<p><strong>RTO (Recovery Time Objective):</strong> How long can the database be down? A consumer app might tolerate 30 seconds. A payment system might need sub-second failover.</p>
<p><strong> <a href="/blog/2019-05-20-effective-slos/"
   
   >Latency SLOs</a>
:</strong> What&rsquo;s the p99 query latency you&rsquo;re committing to? This drives capacity planning, connection pooling, and index strategy.</p>
<p>These numbers dictate everything downstream. Synchronous replication for near-zero RPO. Automated failover for aggressive RTO. Proper connection pooling and query optimization for latency SLOs. Don&rsquo;t skip this conversation.</p>
<h2 id="replication-pick-your-tradeoff">Replication: pick your tradeoff</h2>
<p>At the fintech startup we ran  <a href="/blog/2020-01-20-database-replication-patterns/"
   
   >synchronous replication</a>
 for our primary financial data. The write latency penalty was real: every commit waited for at least one standby to acknowledge. But for a financial data platform, losing committed transactions wasn&rsquo;t an option.</p>
<p>For our analytics and reporting databases, we used asynchronous replication. Faster writes, slight risk of data loss on primary failure. Acceptable for data that could be re-derived.</p>
<p>In Postgres, the config difference is small but the operational impact is significant:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e"># Synchronous replication - zero data loss, higher write latency</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">synchronous_commit</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">on</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">synchronous_standby_names</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;FIRST 1 (standby1, standby2)&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Asynchronous replication - lower latency, possible data loss on failover</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">synchronous_standby_names</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;   # no synchronous standbys; commits don&#39;t wait for replicas</span>
</span></span></code></pre></div><p>The mistake I keep seeing: running async replication without understanding the RPO implication. If your primary dies and the standby is 5 seconds behind, you just lost 5 seconds of data. If that includes a payment confirmation, you have a problem that no incident postmortem can fix.</p>
<h2 id="failover-automate-with-guardrails">Failover: automate with guardrails</h2>
<p>Automated failover sounds great until it promotes a standby that&rsquo;s behind the primary and you get split-brain. Or it triggers during a network partition and now you have two primaries accepting writes.</p>
<p>At a large telecom company I worked with, I helped set up Patroni for Postgres HA. The key decisions:</p>
<ul>
<li>Health checks from multiple locations (not just one monitoring server)</li>
<li>Minimum replication lag threshold before promotion is allowed</li>
<li>Fencing of the old primary before the new one accepts writes</li>
<li>Manual fallback documented and practiced</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># patroni.yml: failover constraints live in the DCS settings</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">bootstrap</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dcs</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ttl</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">retry_timeout</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">maximum_lag_on_failover</span>: <span style="color:#ae81ff">1048576</span>  <span style="color:#75715e"># ~1MB of WAL</span>
</span></span></code></pre></div><p>The <code>maximum_lag_on_failover</code> setting is critical. It prevents promotion of a standby that&rsquo;s too far behind. The value above is Patroni&rsquo;s default (1MB of WAL); decide deliberately whether that much potential loss fits your RPO. I&rsquo;ve seen teams never look at it and then wonder why they lost data during failover.</p>
<p>Test your failover. Quarterly at minimum. The first time you practice it, something will go wrong. Better during a planned exercise than during an actual outage at 3am.</p>
<h2 id="backups-the-restore-is-the-backup">Backups: the restore is the backup</h2>
<p>Your backup strategy is only as good as your last tested restore.</p>
<p>At the fintech startup, we ran:</p>
<ul>
<li>Daily base backups via <code>pg_basebackup</code></li>
<li>Continuous WAL archiving to S3 for point-in-time recovery</li>
<li>Weekly automated restore tests into a staging environment</li>
<li>Monthly manual restore drills where an engineer actually performed the full recovery</li>
</ul>
<p>The weekly automated test caught a corrupted backup exactly once. Once was enough. Without that test, we would have discovered the corruption during an actual emergency.</p>
<p>A solid Postgres backup setup:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e"># postgresql.conf - enable WAL archiving</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">archive_mode</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">on</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">archive_command</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;aws s3 cp %p s3://backup-bucket/wal/%f&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wal_level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">replica</span>
</span></span></code></pre></div><p>Point-in-time recovery is the real value here. Full backups let you restore to the last backup. WAL archiving lets you restore to any point in time. When someone runs a bad <code>DELETE</code> at 2:47pm, you can recover to 2:46pm. That capability has saved me more than once.</p>
<h2 id="safe-migrations-the-number-one-reliability-risk">Safe migrations: the number one reliability risk</h2>
<p>Schema changes cause more database incidents than hardware failures. I&rsquo;ve seen it at every company. Someone runs an <code>ALTER TABLE</code> that locks a table for 20 minutes during peak traffic. The connection pool fills up. Upstream services start timing out. Cascade failure.</p>
<p>The patterns that keep you safe:</p>
<p><strong>Add columns as nullable.</strong> Never add a <code>NOT NULL</code> column without a default to an existing table in a single step. Add it nullable, backfill, then add the constraint.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Step 1: Add nullable column
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> orders <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> status_v2 text;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Step 2: Backfill in batches
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> orders <span style="color:#66d9ef">SET</span> status_v2 <span style="color:#f92672">=</span> status
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> id <span style="color:#66d9ef">BETWEEN</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">AND</span> <span style="color:#ae81ff">10000</span> <span style="color:#66d9ef">AND</span> status_v2 <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>;
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- repeat for remaining batches
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Step 3: Add constraint after data is clean
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> orders <span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">COLUMN</span> status_v2 <span style="color:#66d9ef">SET</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>;
</span></span></code></pre></div><p>On a large table, <code>SET NOT NULL</code> still scans the whole table under an exclusive lock. On Postgres 12+, add a <code>CHECK (status_v2 IS NOT NULL) NOT VALID</code> constraint first, run <code>VALIDATE CONSTRAINT</code>, and <code>SET NOT NULL</code> can then skip the scan.</p>
<p><strong>Build indexes concurrently.</strong> A regular <code>CREATE INDEX</code> locks the table for writes. On a large table, that&rsquo;s an outage.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- This blocks writes:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_orders_status <span style="color:#66d9ef">ON</span> orders(status);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- This doesn&#39;t:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_status <span style="color:#66d9ef">ON</span> orders(status);
</span></span></code></pre></div><p><strong>Set lock and statement timeouts.</strong> Prevent runaway migrations from holding locks indefinitely.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Before running migration
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">SET</span> lock_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;5s&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SET</span> statement_timeout <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;30s&#39;</span>;
</span></span></code></pre></div><p>If a migration can&rsquo;t acquire its lock within 5 seconds, it fails instead of queuing behind long-running queries and creating a pileup.</p>
<p><strong>Test against production volumes.</strong> A migration that runs in 2 seconds on your dev database with 1,000 rows might take 45 minutes on production with 50 million rows. I learned this lesson at the fintech startup when a &ldquo;quick&rdquo; index build blocked writes for 12 minutes because we hadn&rsquo;t tested against actual data volume.</p>
<h2 id="monitoring-watch-the-right-things">Monitoring: watch the right things</h2>
<p>Most database monitoring setups track too many things and miss the ones that matter. What I alert on:</p>
<p><strong>Alert immediately:</strong></p>
<ul>
<li>Replication lag above threshold (for us, 10 seconds)</li>
<li>Disk usage above 80%</li>
<li>Connection pool saturation above 90%</li>
<li>Failed backup or missed WAL archive</li>
</ul>
<p><strong>Alert on sustained trends:</strong></p>
<ul>
<li>Slow query count increasing over 15 minutes</li>
<li>Lock wait time increasing over 5 minutes</li>
<li>Transaction wraparound approaching (Postgres-specific, but it will bite you)</li>
</ul>
<p><strong>Track in dashboards, don&rsquo;t alert:</strong></p>
<ul>
<li>Query latency distribution (p50, p95, p99)</li>
<li>Table and index sizes over time</li>
<li>Autovacuum activity</li>
<li>Cache hit ratios</li>
</ul>
<p>A Postgres-specific query I run weekly to find trouble:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Tables approaching transaction wraparound
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">SELECT</span> relname, age(relfrozenxid) <span style="color:#66d9ef">as</span> xid_age,
</span></span><span style="display:flex;"><span>       pg_size_pretty(pg_total_relation_size(oid)) <span style="color:#66d9ef">as</span> total_size
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_class
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> relkind <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;r&#39;</span> <span style="color:#66d9ef">AND</span> age(relfrozenxid) <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">200000000</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span> age(relfrozenxid) <span style="color:#66d9ef">DESC</span>;
</span></span></code></pre></div><p>Transaction wraparound is the kind of problem that gives you little warning anyone notices and then takes the database down hard. Monitor it.</p>
<h2 id="maintenance-isnt-optional">Maintenance isn&rsquo;t optional</h2>
<p>Postgres needs vacuuming. It needs statistics updates. It needs index maintenance. These aren&rsquo;t nice-to-haves.</p>
<p>Autovacuum handles most of the work, but the defaults are conservative. For busy tables:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Per-table override for high-write tables
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> events <span style="color:#66d9ef">SET</span> (
</span></span><span style="display:flex;"><span>    autovacuum_vacuum_scale_factor <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">01</span>,
</span></span><span style="display:flex;"><span>    autovacuum_analyze_scale_factor <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">005</span>
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>The default <code>autovacuum_vacuum_scale_factor</code> of 0.2 means vacuum runs after 20% of the table has changed. On a 100-million-row table, that&rsquo;s 20 million dead tuples before vacuum kicks in. Dropping it to 0.01 keeps things cleaner.</p>
<p>Index bloat is the other silent killer. Indexes grow as data is updated and deleted. Periodically check <code>pg_stat_user_indexes</code> for indexes with low usage and high size. Rebuild bloated indexes during maintenance windows using <code>REINDEX CONCURRENTLY</code> (Postgres 12+).</p>
<h2 id="the-operational-habits-that-matter">The operational habits that matter</h2>
<p>Technology is the easy part. Habits are what prevent outages:</p>
<ul>
<li><strong>Monthly restore drills.</strong> Someone on the team restores a backup from scratch. Time it. Document the steps. Every time.</li>
<li><strong>Quarterly failover tests.</strong> Actually promote a standby. Verify data integrity. Fail back. Fix whatever surprised you.</li>
<li><strong>Runbooks for every alert.</strong> If an alert fires and the on-call engineer has to figure out what to do from scratch, the runbook is missing.</li>
<li><strong>Post-incident follow-through.</strong> Writing the postmortem is easy. Actually fixing the root cause is what most teams skip.</li>
</ul>
<p>Database reliability isn&rsquo;t something you build once. It&rsquo;s something you practice until the team can handle a 3am failover without panicking. The configs and queries above are tools. The discipline to use them consistently is the actual hard part.</p>
]]></content:encoded></item><item><title>WebAssembly Beyond the Browser: A 2021 Progress Report</title><link>https://lawzava.com/blog/2021-07-26-webassembly-beyond-browser/</link><pubDate>Mon, 26 Jul 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-07-26-webassembly-beyond-browser/</guid><description>Server-side Wasm in mid-2021: edge platforms and Envoy plugins are real, WASI still lacks networking, and containers aren&amp;amp;rsquo;t going anywhere.</description><content:encoded><![CDATA[<p>Wasm outside the browser has moved from &ldquo;interesting idea&rdquo; to &ldquo;early production use&rdquo; in about a year. Edge platforms like Cloudflare Workers and Fastly Compute@Edge are the clearest wins. Plugin systems (Envoy filters, extensible platforms) are real. WASI is still immature: no networking, limited filesystem. Don&rsquo;t use Wasm to replace containers. Use it where sandboxing, fast startup, and portability actually matter.</p>
<p>I wrote about WebAssembly in  <a href="/blog/2020-03-02-webassembly-beyond-browser/"
   
   >March 2020</a>
 when the server-side story was mostly theoretical. Sixteen months later, here is the progress report. Some things moved faster than I expected. Some haven&rsquo;t moved at all.</p>
<h2 id="whats-actually-changed">What&rsquo;s actually changed</h2>
<p>The biggest shift is that Wasm is showing up in production, not just conference demos. Not everywhere. Not for everything. But in specific niches where the tradeoffs make sense.</p>
<p><strong> <a href="/blog/2019-11-18-edge-computing-architecture/"
   
   >Edge computing</a>
 is the clearest win.</strong> Cloudflare Workers has supported Wasm since 2018, and Fastly&rsquo;s Compute@Edge runs Wasm on Lucet, its ahead-of-time compiler, which Fastly is now merging into Wasmtime. The value proposition is straightforward: Wasm modules start in microseconds, use minimal memory, and run in a sandbox. For edge compute, where you need code running close to users, starting fast, and tearing down quickly, that beats spinning up a container.</p>
<p>I&rsquo;ve been using Compute@Edge for a few side projects. The developer experience is decent. You write Rust or AssemblyScript, compile to Wasm, deploy. Cold starts are fast. The limitation is the restricted API surface: you can handle HTTP request/response flows, but anything much beyond that gets painful.</p>
<p><strong>Plugin systems are the second real use case.</strong> Envoy merged Wasm filter support upstream in 1.17 this January. It&rsquo;s still marked experimental, but teams can already ship custom proxy logic (rate limiting, header manipulation, auth checks) without rebuilding the Envoy binary. At one company, we evaluated this for custom traffic-routing rules that varied per tenant. The alternative was maintaining a fork of Envoy. Wasm filters were dramatically simpler to deploy and update.</p>
<p>The pattern works for any platform that needs user-extensible logic in a safe sandbox. Think database UDFs, SaaS platform plugins, policy engines. The host controls resources and data access. The plugin runs in a box with no ambient authority. Language choice is flexible because Wasm doesn&rsquo;t care what you compiled from.</p>
<h2 id="wasi-still-early-still-important">WASI: still early, still important</h2>
<p>WASI preview1 is the standard everyone targets. It gives you filesystem access (capability-based, not ambient), clocks, random number generation, and environment variables. That&rsquo;s enough for CLI tools, data transformation, and controlled plugin workloads.</p>
<p>What&rsquo;s still missing: networking. No sockets, no HTTP client, no DNS. This is the single biggest limitation for server-side Wasm. If your workload needs outbound calls, you&rsquo;re either working around it through host-provided functions or you&rsquo;re waiting. The WASI networking proposal exists, but it&rsquo;s still in early design.</p>
<p>For Go developers specifically, the story is mixed. TinyGo compiles to Wasm and targets WASI, but you lose chunks of the standard library. The main Go compiler has experimental Wasm support, but it&rsquo;s browser-focused and the binaries are large. I&rsquo;ve had reasonable results with TinyGo for small, focused modules. I wouldn&rsquo;t build a full service with it.</p>
<h2 id="runtimes-are-getting-solid">Runtimes are getting solid</h2>
<p>Wasmtime (from the Bytecode Alliance) and Wasmer are both maturing. Wasmtime in particular is becoming the runtime others build on. The embedding story is good: you can load a Wasm module into a Go, Rust, or Python host, expose a narrow API, and let the module run in a sandbox.</p>
<p>I&rsquo;ve been embedding Wasmtime in a Go service for a side project where users can define custom validation rules. The module loads in milliseconds, executes deterministically, and can&rsquo;t touch anything the host doesn&rsquo;t explicitly allow. Compared to running user code in a Docker container or subprocess, the operational story is dramatically simpler.</p>
<p>The debugging experience, though, is still rough. Print debugging works (you can wire up stdout through WASI). Proper step-through debugging with source maps is technically possible but clunky. Profiling is even worse. This matters less for small, plugin-style modules, but it would be a problem for anything substantial.</p>
<h2 id="where-it-doesnt-make-sense">Where it doesn&rsquo;t make sense</h2>
<p>I need to be clear about this because the hype cycle is doing its thing.</p>
<p>Wasm isn&rsquo;t replacing containers. It&rsquo;s not a general-purpose server runtime. If your workload needs full OS access, mature networking, established tooling, and rich library ecosystems, containers are still the answer.  <a href="/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/"
   
   >Docker is boring</a>
 and it works.</p>
<p>Wasm also struggles with anything that needs high-performance I/O or tight integration with native libraries. The cross-boundary calling overhead is real. Data copying between the host and the Wasm module can become the bottleneck. For compute-heavy, data-light workloads this is fine. For data-heavy workloads, measure before committing.</p>
<h2 id="where-id-use-it-today">Where I&rsquo;d use it today</h2>
<p>Three scenarios where Wasm outside the browser makes sense right now:</p>
<ol>
<li>
<p><strong>Edge compute</strong> where startup time and memory footprint matter. Cloudflare Workers and Fastly Compute@Edge are the obvious platforms.</p>
</li>
<li>
<p><strong>Plugin systems</strong> where you need safe, sandboxed execution of user-provided or tenant-specific logic. Envoy filters are the most mature example.</p>
</li>
<li>
<p><strong>CLI tools and data transformers</strong> where portability across platforms matters and the workload fits within WASI&rsquo;s current API surface.</p>
</li>
</ol>
<p>For everything else, I&rsquo;d wait. Wasm is promising, but the tooling, the system interface, and the cross-language interface work all need another year or two of work before server-side Wasm becomes a general-purpose option.</p>
<h2 id="the-trajectory">The trajectory</h2>
<p>The direction is right. The Bytecode Alliance is doing real work. WASI is evolving (slowly). Edge platforms are proving the model. The interface types and module linking proposals could eventually solve the cross-language composition problem in a way that nothing else does.</p>
<p>But right now, in July 2021, Wasm beyond the browser is a sharp tool for specific problems. Not a platform. Use it where the constraints fit. Wait where they don&rsquo;t.</p>
]]></content:encoded></item><item><title>Data Engineering Patterns: Batch vs. CDC vs. Streaming</title><link>https://lawzava.com/blog/2021-05-17-data-engineering-patterns/</link><pubDate>Mon, 17 May 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-05-17-data-engineering-patterns/</guid><description>Batch vs. CDC vs. streaming ingestion, compared from building financial data pipelines at a fintech startup, and how to pick by real latency needs.</description><content:encoded><![CDATA[<p>Most teams default to batch ingestion and that&rsquo;s usually correct. CDC gives you near-real-time without the operational overhead of full streaming. True streaming is powerful but expensive to run and easy to get wrong. Pick based on your actual latency requirement, not what sounds impressive.</p>
<p>At the fintech startup we  <a href="/blog/2017-04-24-building-data-pipelines-that-dont-break/"
   
   >processed financial data</a>
 and market data from hundreds of sources. Some of it needed to be available in seconds: breaking news that moves stock prices. Some of it needed to be correct and complete for daily analytics: aggregated sentiment scores, content performance metrics. And some of it sat in between.</p>
<p>We ended up running all three ingestion patterns because different problems demanded different tradeoffs, not because we planned it that way. After living with all three in production, I&rsquo;ve got strong opinions about when each one belongs.</p>
<h2 id="the-comparison">The comparison</h2>
<table>
  <thead>
      <tr>
          <th></th>
          <th><strong>Batch</strong></th>
          <th><strong>CDC</strong></th>
          <th><strong>Streaming</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Latency</strong></td>
          <td>Hours</td>
          <td>Minutes</td>
          <td>Seconds</td>
      </tr>
      <tr>
          <td><strong>Complexity</strong></td>
          <td>Low</td>
          <td>Medium</td>
          <td>High</td>
      </tr>
      <tr>
          <td><strong>Cost</strong></td>
          <td>Low</td>
          <td>Medium</td>
          <td>High</td>
      </tr>
      <tr>
          <td><strong>Schema evolution</strong></td>
          <td>Easy (reload)</td>
          <td>Moderate</td>
          <td>Hard</td>
      </tr>
      <tr>
          <td><strong>Backfill</strong></td>
          <td>Trivial</td>
          <td>Moderate</td>
          <td>Painful</td>
      </tr>
      <tr>
          <td><strong>Ordering guarantees</strong></td>
          <td>N/A (snapshot)</td>
          <td>Per-table</td>
          <td>Requires design</td>
      </tr>
      <tr>
          <td><strong>Failure recovery</strong></td>
          <td>Rerun the job</td>
          <td>Replay from WAL</td>
          <td>Depends on offset management</td>
      </tr>
      <tr>
          <td><strong>Good for</strong></td>
          <td>Reporting, analytics, finance</td>
          <td>Replication, audit trails, near-RT analytics</td>
          <td>Event-driven products, real-time features</td>
      </tr>
      <tr>
          <td><strong>Operational burden</strong></td>
          <td>Minimal</td>
          <td>Moderate (connector management)</td>
          <td>Significant (cluster, schemas, monitoring)</td>
      </tr>
  </tbody>
</table>
<p>That table is the result of learning things the hard way.</p>
<h2 id="batch-the-boring-default-that-works">Batch: the boring default that works</h2>
<p>At the fintech startup, our analytics pipeline ran on batch. Every night, we pulled the day&rsquo;s content, computed sentiment aggregates, calculated engagement metrics, and loaded the results into our warehouse for the analytics team.</p>
<p>Batch ingestion is a scheduled extract. Query the source, pull the data, load it somewhere. The tools are mature: Airflow for orchestration, simple scripts or managed connectors for extraction, a cloud warehouse for storage.</p>
<p>Why batch works so well:</p>
<ul>
<li><strong>Idempotency is natural.</strong> If a job fails, rerun it. You get the same result. There&rsquo;s no state to recover, no offsets to manage.</li>
<li><strong>Backfills are trivial.</strong> Need to reprocess last month? Change the date parameter and run the job again.</li>
<li><strong>Schema changes are manageable.</strong> Source adds a column? Your next batch picks it up. Source renames a column? You fix the mapping and reprocess.</li>
</ul>
<p>The latency tradeoff is real. Batch means your data is hours old at best. For dashboards and reports, that&rsquo;s fine. For anything user-facing and real-time, it&rsquo;s not.</p>
<p>My rule: default to batch unless someone can articulate a specific business reason for lower latency. &ldquo;It would be nice to have real-time data&rdquo; isn&rsquo;t a business reason. &ldquo;Users leave the product if news is more than 30 seconds old&rdquo; is.</p>
<h2 id="cdc-the-middle-ground-most-people-skip">CDC: the middle ground most people skip</h2>
<p>Change Data Capture reads the database&rsquo;s write-ahead log (WAL) and streams row-level changes to a target system. Debezium is the standard open-source tool. Most managed data platforms offer CDC connectors now.</p>
<p>At the fintech startup, we used CDC to replicate our content database to the analytics warehouse. Instead of running nightly full extracts that hammered the production database, we streamed changes continuously. The warehouse stayed within minutes of production, and the load on the source database dropped dramatically.</p>
<p>CDC hits a sweet spot:</p>
<ul>
<li><strong>Near-real-time without the streaming overhead.</strong> You get minute-level freshness without running a Kafka cluster or designing event schemas.</li>
<li><strong>Auditable by default.</strong> Every change is captured. You get a complete history of what changed and when.</li>
<li><strong>Lower source load.</strong> Reading the WAL is far cheaper than running full table scans.</li>
</ul>
<p>The downsides are real but manageable. CDC connectors need monitoring. They can fall behind, hit WAL retention limits, or break on DDL changes. Schema evolution requires more care than batch because the connector is continuously running. And initial snapshots for large tables can be slow.</p>
<p>For most teams that want better-than-daily freshness, CDC is the right answer. It&rsquo;s the pattern I recommend most often because it solves the latency problem without introducing streaming complexity.</p>
<h2 id="streaming-powerful-and-expensive">Streaming: powerful and expensive</h2>
<p>True event streaming ( <a href="/blog/2019-09-09-message-queues-patterns/"
   
   >Kafka</a>
, Kinesis, Pulsar) is for when your product needs to react to events in real time. At the fintech startup, our breaking news pipeline was streaming. Content arrived from news APIs, got classified and enriched, and appeared in user feeds within seconds.</p>
<p>Streaming is the right tool when:</p>
<ul>
<li>User experience depends on seconds-level latency.</li>
<li>Events need to trigger immediate downstream actions.</li>
<li>You need to join multiple event streams in real time.</li>
</ul>
<p>Streaming is the wrong tool when people want it because it sounds impressive on an architecture diagram.</p>
<p>What streaming costs in practice:</p>
<ul>
<li><strong>Cluster management.</strong> Kafka isn&rsquo;t a fire-and-forget service. Broker configuration, partition management, replication, consumer group coordination. Even managed Kafka (Confluent Cloud, MSK) requires operational attention.</li>
<li><strong>Schema discipline.</strong> When producers and consumers are decoupled and running continuously, a schema change can break downstream consumers silently. You need a schema registry. You need compatibility policies. You need someone who owns them.</li>
<li><strong>Ordering and exactly-once semantics.</strong> These sound like solved problems until you hit partition rebalancing, consumer crashes, or cross-partition joins. The guarantees are achievable but require careful design.</li>
<li><strong>Backfills are painful.</strong> Need to reprocess a week of events? You need to replay from stored offsets, hope retention was sufficient, and deal with the downstream impact of reprocessing alongside live traffic.</li>
</ul>
<p>At the fintech startup we had two engineers who understood the streaming pipeline deeply. When they were unavailable, incidents on the streaming side took significantly longer to resolve than batch or CDC issues. That knowledge concentration is a real operational risk.</p>
<h2 id="the-transformation-layer">The transformation layer</h2>
<p>Regardless of how data gets in, it needs to be transformed for consumption. ELT (load raw, transform in the warehouse) has become the standard pattern, and dbt is the tool that made it practical.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- models/content_daily_metrics.sql
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">select</span>
</span></span><span style="display:flex;"><span>  content_id,
</span></span><span style="display:flex;"><span>  source_name,
</span></span><span style="display:flex;"><span>  published_date,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">count</span>(<span style="color:#f92672">*</span>) <span style="color:#66d9ef">as</span> view_count,
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">avg</span>(sentiment_score) <span style="color:#66d9ef">as</span> avg_sentiment
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">from</span> <span style="color:#960050;background-color:#1e0010">{{</span> <span style="color:#66d9ef">ref</span>(<span style="color:#e6db74">&#39;stg_content_events&#39;</span>) <span style="color:#960050;background-color:#1e0010">}}</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">group</span> <span style="color:#66d9ef">by</span> <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>, <span style="color:#ae81ff">3</span>
</span></span></code></pre></div><p>Version-controlled SQL. Testable. Documented. This is one of the few areas where the tooling has clearly gotten better in the last two years. At the fintech startup we adopted dbt for our analytics models and the improvement in confidence and velocity was immediate.</p>
<h2 id="what-i-tell-teams-now">What I tell teams now</h2>
<p>Start with batch. Seriously. Get your data warehouse set up, your orchestration running, your dbt models tested. Build the operational muscle for data quality: freshness checks, row count monitoring, schema validation.</p>
<p>When batch latency becomes a proven problem for a specific use case, add CDC for that source. Don&rsquo;t migrate everything. Just the tables where minutes matter.</p>
<p>Reserve streaming for the use cases where seconds matter and the business impact justifies the operational cost. Have at least two people who understand the streaming infrastructure deeply. Monitor consumer lag obsessively.</p>
<p>The most common mistake I see: teams adopt streaming first because it&rsquo;s exciting, then spend months building operational maturity that batch would have given them for free. The boring choice is usually the right choice. Optimize for reliability and then add speed where the business actually needs it.</p>
]]></content:encoded></item><item><title>Multi-Cloud Is Mostly a Marketing Strategy</title><link>https://lawzava.com/blog/2021-04-05-multi-cloud-strategy/</link><pubDate>Mon, 05 Apr 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-04-05-multi-cloud-strategy/</guid><description>Multi-cloud sounds great in vendor pitches. In practice, it doubles your operational burden for benefits most teams will never need.</description><content:encoded><![CDATA[<p>Multi-cloud is a consulting engagement, not an architecture pattern.</p>
<p>I&rsquo;ve seen it up close. Some of the large organizations I&rsquo;ve worked with run workloads across two or three cloud providers. Every single one wishes it didn&rsquo;t. The ones that are happy with multi-cloud are happy because a specific regulatory or customer requirement forced their hand, not because they chose it for technical elegance.</p>
<p>The rest got there by accident. Different teams picked different clouds. An acquisition brought in a second platform. Someone in leadership read a Gartner report. Now they have two sets of IAM policies, two networking models, two monitoring stacks, and a platform team that&rsquo;s permanently underwater.</p>
<h2 id="the-pitch-vs-the-reality">The pitch vs. the reality</h2>
<p>The multi-cloud pitch goes like this: avoid vendor lock-in, leverage best-of-breed services from each provider, improve resilience through provider redundancy.</p>
<p><strong>&ldquo;Avoid lock-in.&rdquo;</strong> You&rsquo;re not avoiding lock-in. You&rsquo;re doubling it. Now you&rsquo;re locked into two vendors instead of one, and you&rsquo;re also locked into whatever abstraction layer you built to make them look the same. That abstraction layer is its own maintenance burden. It lags behind both providers&rsquo; features. And when it breaks, nobody outside your platform team knows how to fix it.</p>
<p><strong>&ldquo;Best-of-breed.&rdquo;</strong> This sounds rational until you try to integrate BigQuery with an application running on AWS. Cross-cloud data movement is slow, expensive, and architecturally painful. Data has gravity. Once it lives somewhere, everything that touches it wants to live there too. Picking services from multiple providers means fighting data gravity constantly.</p>
<p><strong>&ldquo;Provider redundancy.&rdquo;</strong> This is the one that really gets me. How many organizations have actually tested a full provider failover? I&rsquo;ve asked this question dozens of times. The answer is almost always no. They have multi-cloud on a slide deck. They don&rsquo;t have a tested failover runbook. If AWS goes down, their &ldquo;redundant&rdquo; GCP deployment hasn&rsquo;t been exercised under real load and will probably fall over for entirely different reasons.</p>
<h2 id="when-it-actually-makes-sense">When it actually makes sense</h2>
<p>I&rsquo;m not saying multi-cloud is never justified. There are real reasons.</p>
<p>Regulatory or data residency requirements can force specific workloads into specific regions that only one provider serves well. Mergers and acquisitions bring in existing infrastructure that you can&rsquo;t migrate overnight. Some customers contractually require their data to run on a specific platform.</p>
<p>Those are constraints. You respond to constraints. That&rsquo;s different from choosing multi-cloud because it feels like good architecture.</p>
<h2 id="what-i-actually-recommend">What I actually recommend</h2>
<p>Default to one cloud. Pick the one your team knows best. Go deep on it. Use its managed services. Learn its IAM model properly. Build in  <a href="/blog/2019-06-17-multi-region-architecture/"
   
   >multiple regions</a>
 for resilience. That covers most failure scenarios with a fraction of the operational complexity.</p>
<p>Design for portability at clear boundaries. Containers and Kubernetes give you a portable compute layer, and that&rsquo;s real. But Kubernetes doesn&rsquo;t abstract away networking, storage, IAM, DNS, load balancing, or any of the managed data services. Treat Kubernetes as a helpful layer, not a magic portal between clouds.</p>
<p>Keep data in one place unless you have a specific, measured reason to replicate it. Cross-cloud data synchronization is a beast. Latency, consistency, cost, and operational complexity all increase dramatically. I&rsquo;ve seen teams spend more engineering hours managing cross-cloud data replication than they spent building the features that use the data.</p>
<p>Have an exit plan even if you&rsquo;re single-cloud. Know how to export your data. Understand which services are proprietary and which have open equivalents. Negotiate contracts with exit timelines. This is smart vendor management. It doesn&rsquo;t require running production workloads on two platforms.</p>
<h2 id="who-benefits-from-multi-cloud">Who benefits from multi-cloud</h2>
<p>Multi-cloud is mostly sold by people who benefit from the complexity it creates. Cloud vendors sell multi-cloud management tools. Consulting firms sell multi-cloud strategy projects. Platform vendors sell abstraction layers.</p>
<p>The teams who actually run the infrastructure? They&rsquo;re drowning in operational overhead and wondering why everything takes twice as long.</p>
<p>If someone proposes multi-cloud at your organization, ask one question: what specific problem does a second cloud provider solve that multi-region on our current provider doesn&rsquo;t? If the answer is vague, you have your answer.</p>
]]></content:encoded></item><item><title>2020 in Review: The Year That Broke the Playbook</title><link>https://lawzava.com/blog/2020-12-28-year-in-review-2020/</link><pubDate>Mon, 28 Dec 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-12-28-year-in-review-2020/</guid><description>My company survived its first real crisis, I led teams at two larger platforms, and the industry learned what remote work looks like. A strange year.</description><content:encoded><![CDATA[<p>I&rsquo;m writing this on December 28th and I can&rsquo;t tell you what month it is without checking. That&rsquo;s 2020 in one sentence.</p>
<p>A year ago I was heads-down building a cloud infrastructure company, convinced that the biggest challenge ahead was product-market fit. Then March happened and the biggest challenge became making sure the company didn&rsquo;t die. Different skill set entirely.</p>
<h2 id="the-company-made-it">The Company Made It</h2>
<p>I&rsquo;ll start with the thing I&rsquo;m most proud of. The company survived 2020. But I want to be straight about what that actually looked like, because the LinkedIn version of startup survival stories is always cleaner than reality.</p>
<p>March and April were ugly. Two potential customers froze their budgets overnight. A partnership I&rsquo;d been working on for months went silent. I spent a few weeks unsure if the company would make it to summer. The thing that saved it was that the product was a cloud infrastructure tool during the exact moment when every company on earth suddenly needed better cloud infrastructure. Lucky timing dressed up as strategy.</p>
<p>By Q3 I was shipping features faster than I was comfortable with. Some of that code is going to haunt me in 2021 and I know it. But the company is alive, and if you&rsquo;d told me that in April I would have hugged you.</p>
<h2 id="leading-teams-at-larger-platforms">Leading Teams at Larger Platforms</h2>
<p>The other big part of this year happened alongside the company. I spent 2020 as a tech lead at two larger platforms. Both taught me things a small startup can&rsquo;t.</p>
<p>The biggest lesson: large organizations fail at decision-making far more often than at technology. I watched a team of perfectly competent engineers sit paralyzed for three weeks because nobody could get sign-off on a Kubernetes cluster configuration. The technology was the easy part. Getting six stakeholders to agree on a risk tolerance was the hard part.</p>
<p>I also learned that my founder experience translated directly. The problems startups solve with speed and scrappiness are the same problems enterprises solve with process and budget. Same problems, different constraints. Understanding both sides made me better at each.</p>
<h2 id="remote-work-what-the-industry-got-wrong">Remote Work: What the Industry Got Wrong</h2>
<p>I need to rant about this because the discourse around remote work in 2020 was insufferable.</p>
<p>Half the industry spent the year writing thought pieces about how remote work is the future and offices are dead. The other half spent it writing thought pieces about how remote work is destroying culture and we need to get back to the office immediately. Both camps were wrong in the same way: they treated remote work as a binary instead of a design problem.</p>
<p>Remote work is infrastructure: a set of systems and processes that either work or don&rsquo;t. I&rsquo;ve been running distributed teams since my days at a mobility startup. It works when you design for it. It fails when you take your office habits and paste them onto Zoom.</p>
<p>The companies that struggled in 2020 had never invested in written communication,  <a href="/blog/2020-04-13-async-communication-practices/"
   
   >async decision-making</a>
, or documentation. They replaced hallway conversations with eight hours of video calls and then blamed the format.</p>
<p>My company had been remote since day one, so the lockdown changed little about how I worked there. Not because remote is magic. Because I already documented decisions and wrote things down instead of keeping them in my head.</p>
<p>Office or remote won&rsquo;t decide which companies thrive in 2021. The ones that design their communication systems will beat the ones that default to whatever they did before.</p>
<h2 id="what-i-got-wrong">What I Got Wrong</h2>
<p>I try to own my mistakes in these posts, so here goes.</p>
<p>I underestimated how much the pandemic would affect the teams I worked with. I spent Q2 optimizing for output when I should have been optimizing for sustainability. We shipped a lot, but a couple of people burned out, and I should have noticed sooner.</p>
<p>I also overestimated how quickly large organizations would modernize. Compressed timelines don&rsquo;t mean compressed politics. Some organizations moved their infrastructure to the cloud in weeks but took months to update their security policies to match. The gap between technical capability and organizational readiness is wider than I thought.</p>
<h2 id="the-solarwinds-wake-up-call">The SolarWinds Wake-Up Call</h2>
<p>I can&rsquo;t write a 2020 review without mentioning  <a href="/blog/2020-12-14-solarwinds-attack-analysis/"
   
   >SolarWinds</a>
. In December we learned that a major software supply chain had been compromised for months. Government agencies, Fortune 500 companies, the works.</p>
<p>This is the kind of thing I&rsquo;ve been warning about for years. Your security is only as good as your weakest vendor. Most organizations treat vendor risk as a compliance checkbox. SolarWinds proved it&rsquo;s an operational reality. I expect this to dominate security conversations well into 2021.</p>
<h2 id="what-im-carrying-into-2021">What I&rsquo;m Carrying Into 2021</h2>
<p>The company has momentum. The work keeps growing. I have a clearer picture of how enterprises and startups can learn from each other. Specifically:</p>
<p>I want to build better. The code I shipped under pressure in 2020 needs attention.  <a href="/blog/2016-02-22-the-true-cost-of-technical-debt/"
   
   >Technical debt is a loan</a>
 and the interest is compounding.</p>
<p>I want to write more. 2020 made everything feel too heavy to write about casually. But I think writing forces clarity and I need more of that.</p>
<p>2020 was the strangest year of my career. It broke every plan I had and replaced them with better ones. I wouldn&rsquo;t want to do it again. But I&rsquo;m grateful for what it taught me, and I&rsquo;m heading into 2021 with more conviction than I&rsquo;ve had in years.</p>
<p>Onwards.</p>
]]></content:encoded></item><item><title>Apple M1 and ARM Servers: What Changes for Infrastructure</title><link>https://lawzava.com/blog/2020-11-16-apple-silicon-server-implications/</link><pubDate>Mon, 16 Nov 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-11-16-apple-silicon-server-implications/</guid><description>The M1 is impressive hardware. The &amp;amp;lsquo;ARM everywhere in the data center&amp;amp;rsquo; takes are not. What it changes for infrastructure: multi-arch builds, now.</description><content:encoded><![CDATA[<p>Everyone lost their minds over the M1 benchmarks last week. The chip deserves it. But the hot takes about ARM taking over the data center overnight are wildly premature.</p>
<p>I run a cloud infrastructure startup. I think about server architectures constantly. And my reaction to the M1 is: great laptop chip, almost irrelevant to my server decisions today.</p>
<h2 id="the-hype-vs-the-reality">The hype vs. the reality</h2>
<p>Apple designed the M1 for consumer devices. Tight integration between CPU, GPU, unified memory, the whole SoC approach. That&rsquo;s what makes it fast. It&rsquo;s also what makes it completely unlike anything you&rsquo;d rack in a data center.</p>
<p>Server ARM is a different animal. Graviton2 and Ampere Altra exist already. They&rsquo;re real. They&rsquo;re decent. But they&rsquo;re not the M1, and the M1 isn&rsquo;t them. People conflating Apple&rsquo;s consumer chip with the state of ARM servers are making a category error.</p>
<h2 id="the-real-impact-is-on-developer-laptops">The real impact is on developer laptops</h2>
<p>The M1&rsquo;s biggest server impact is on developer laptops.</p>
<p>Every Mac developer is about to be running <code>arm64</code> natively. That means your CI pipeline, your Docker images, your build tooling: all of it needs to handle multi-arch or you&rsquo;re going to have a bad time. This is already annoying. It&rsquo;s about to get much more annoying.</p>
<p>At my infrastructure startup I started shipping multi-arch images a few months ago. Not because I love ARM servers. Because I saw this coming and didn&rsquo;t want the Go binaries breaking the moment developer laptops switched to ARM.</p>
<p>Cross-compile your stuff (Go makes this  <a href="/blog/2019-11-04-building-cli-tools/"
   
   >nearly free</a>
). Publish <code>arm64</code> and <code>amd64</code> images. Test on both. This isn&rsquo;t optional anymore.</p>
<h2 id="arm-servers-be-skeptical-not-dismissive">ARM servers: be skeptical, not dismissive</h2>
<p>I&rsquo;ve benchmarked Graviton2 instances for several of our workloads. Results are mixed. Some services run 15-20% cheaper at comparable performance, which matters if you  <a href="/blog/2019-12-02-finops-cloud-cost-management/"
   
   >read your cloud bill</a>
. Others are worse. The &ldquo;ARM is always more efficient&rdquo; line is marketing. Reality is workload-specific.</p>
<p>Native extensions are still a pain. Some vendor SDKs don&rsquo;t have ARM builds. The ecosystem is getting there but it&rsquo;s not there.</p>
<p>My advice: pick one stateless, non-critical service. Run it on ARM for a month. Measure everything. Then decide if it&rsquo;s worth the migration overhead for your specific stack. Don&rsquo;t migrate because a blog post told you ARM is the future.</p>
<h2 id="my-prediction">My prediction</h2>
<p>Five years from now, ARM will have meaningful server market share. Not because of Apple. Because of economics: AWS is building its own chips, and when your cloud provider is also your chip vendor, the pricing incentives get very real.</p>
<p>But that&rsquo;s a five-year story. Not a next-quarter story. And definitely not a &ldquo;rewrite your deployment pipeline this weekend&rdquo; story.</p>
<p>Fix your multi-arch builds. Ignore the hype. Check back in 2022.</p>
]]></content:encoded></item><item><title>Zero Trust vs VPN: How to Migrate Off the Perimeter</title><link>https://lawzava.com/blog/2020-11-02-zero-trust-vs-vpn/</link><pubDate>Mon, 02 Nov 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-11-02-zero-trust-vs-vpn/</guid><description>VPNs trust the network; zero trust checks every request. Lessons from cyber-defense exercises and my infrastructure startup, plus a six-step migration path off the VPN.</description><content:encoded><![CDATA[<p>VPNs give you a tunnel into a flat network and hope for the best. Zero trust checks identity, device, and context on every single request. COVID just accelerated what was already obvious: the perimeter is dead.</p>
<p>March 2020. Every company on earth scrambles to get employees working from home. The answer for most?  <a href="/blog/2020-05-04-vpn-scaling-challenges/"
   
   >Crank up the VPN capacity</a>
. Buy more licenses. Pray.</p>
<p>I watched this unfold from the other side. At my infrastructure startup I was building infrastructure that assumed zero trust from day one, and before that, at the fintech startup, we had moved most services behind identity-aware proxies. Meanwhile, companies that had spent a decade building fat VPN pipes were routing all their cloud traffic through a single gateway in an office nobody was sitting in.</p>
<p>The absurdity was staggering.</p>
<h2 id="the-vpn-mental-model-is-from-1996">The VPN mental model is from 1996</h2>
<p>Here&rsquo;s how a VPN works in practice:</p>
<pre tabindex="0"><code>Remote laptop -&gt; encrypted tunnel -&gt; office network -&gt; everything
</code></pre><p>That arrow into &ldquo;everything&rdquo; is the problem. Once you&rsquo;re through the gate, the network treats you like you&rsquo;re sitting at a desk in the building. Same access. Same trust. Same flat network where a compromised laptop can reach the database server, the admin panel, the file shares, all of it.</p>
<p>VPNs do get a few things right. Encryption in transit is solid. The connect-then-work model is simple for users. And for weird legacy protocols that can&rsquo;t be proxied, sometimes a network tunnel is the only option.</p>
<p>But the failure modes are brutal:</p>
<ul>
<li><strong>Lateral movement is trivial.</strong> One stolen credential and the attacker is &ldquo;inside.&rdquo; Game over on a flat network.</li>
<li><strong>Performance is terrible for cloud workloads.</strong> You&rsquo;re backhauling traffic to an office gateway just to reach AWS. In 2020. While nobody is in the office.</li>
<li><strong>Visibility stops at the perimeter.</strong> You know someone connected. You don&rsquo;t know what they&rsquo;re doing once they&rsquo;re in.</li>
<li><strong>Scaling is painful.</strong> VPN concentrators have hard capacity limits. Ask anyone who hit them in March.</li>
</ul>
<p>In the cyber-defense exercises I took part in, the model was already shifting. You can&rsquo;t protect a network perimeter when the network spans many organizations, temporary sites, and several trust levels. The answer there was granular access control tied to identity and role, not network location.</p>
<h2 id="zero-trust-is-an-architecture-decision">Zero trust is an architecture decision</h2>
<p>I need to say this because vendors have completely destroyed the term. You can&rsquo;t buy zero trust, and a firewall with a new label isn&rsquo;t it. It&rsquo;s an  <a href="/blog/2018-02-19-zero-trust-security-architecture/"
   
   >architecture decision</a>
: <strong>no request is trusted by default, regardless of where it comes from.</strong></p>
<p>Every access decision considers:</p>
<ul>
<li><strong>Who</strong> is making the request (identity, authentication strength, MFA)</li>
<li><strong>What device</strong> they&rsquo;re on (managed? encrypted? patched?)</li>
<li><strong>What context</strong> surrounds the request (location, time, risk signals)</li>
<li><strong>What resource</strong> they&rsquo;re trying to reach (and whether they actually need it)</li>
</ul>
<p>In practice, this looks like per-application access policies instead of network-wide trust:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">app</span>: <span style="color:#ae81ff">internal-wiki</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">employee-access</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">conditions</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">identity</span>: <span style="color:#ae81ff">authenticated</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">mfa</span>: <span style="color:#ae81ff">required</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">device</span>: <span style="color:#ae81ff">managed</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">action</span>: <span style="color:#ae81ff">allow</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">default</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">action</span>: <span style="color:#ae81ff">deny</span>
</span></span></code></pre></div><p>Simple. Explicit. Every app gets its own policy. No more &ldquo;you&rsquo;re on the VPN so you can reach everything.&rdquo;</p>
<h2 id="what-changes-day-to-day">What changes day to day</h2>
<p><strong>Blast radius.</strong> VPN compromise gives attackers the network. Zero trust compromise gives them one application session, maybe, if they also have the right device posture and MFA token.</p>
<p><strong>Performance.</strong> VPN routes everything through a chokepoint. Zero trust lets users connect directly to the resource. Your team in Madrid doesn&rsquo;t need to hairpin through a London office to reach a Frankfurt data center.</p>
<p><strong>Auditability.</strong> With a VPN you get connection logs. With zero trust you get per-request decisions with full context. When something goes wrong, and it will, you actually know what happened.</p>
<p><strong>User experience.</strong> This one surprises people. Zero trust done right is invisible. No &ldquo;connect to VPN&rdquo; step. No split-tunnel headaches. You open the app, the policy engine checks everything behind the scenes, and you&rsquo;re in. Or you&rsquo;re not.</p>
<h2 id="when-to-keep-the-vpn">When to keep the VPN</h2>
<p>I&rsquo;m not dogmatic about this. VPNs still earn their place in specific scenarios:</p>
<ul>
<li>Legacy applications that require L3/L4 network access and can&rsquo;t be proxied</li>
<li>Air-gapped or highly isolated environments where direct internet connectivity isn&rsquo;t allowed</li>
<li>Temporary access for third-party contractors working with ancient systems</li>
<li>Specific protocols that don&rsquo;t speak HTTP and can&rsquo;t be wrapped</li>
</ul>
<p>The key distinction: VPN as a tactical tool for specific legacy needs, not as your security architecture.</p>
<h2 id="a-migration-path-that-works">A migration path that works</h2>
<p>At my infrastructure startup, I built with zero trust from scratch. Most companies don&rsquo;t have that luxury. Here&rsquo;s the migration path I&rsquo;ve seen work:</p>
<p><strong>Step 1: Get identity right.</strong> Centralize authentication. Enforce MFA everywhere. Not &ldquo;for admins.&rdquo; Everywhere. This is the foundation and nothing else works without it.</p>
<p><strong>Step 2: Know your devices.</strong> Define what a &ldquo;trusted device&rdquo; means. Managed. Encrypted. Patched within some reasonable window. Start enrolling endpoints.</p>
<p><strong>Step 3: Move web apps first.</strong> SaaS tools and internal web applications are the easiest to put behind an identity-aware proxy. BeyondCorp style. This covers 70-80% of what people actually use daily.</p>
<p><strong>Step 4: Shrink the VPN scope.</strong> Every app you move to per-app access is one less reason for the VPN to exist. Segment your internal network. Tighten VPN access to only the systems that need it.</p>
<p><strong>Step 5: Instrument everything.</strong> Log every access decision. Not just &ldquo;allowed/denied&rdquo; but the full context. You&rsquo;ll need this to tune policies and prove the model works.</p>
<p><strong>Step 6: Iterate.</strong> Review access logs weekly. Find overly broad policies and tighten them. Find unnecessary friction and reduce it. This isn&rsquo;t a project with an end date. It&rsquo;s how you operate now.</p>
<h2 id="the-traps-ive-seen-teams-fall-into">The traps I&rsquo;ve seen teams fall into</h2>
<p><strong>Buying a &ldquo;zero trust product&rdquo; and calling it done.</strong> You didn&rsquo;t adopt zero trust. You bought a proxy. The architecture change is what matters.</p>
<p><strong>Exception creep.</strong> Someone important complains about access friction, you create a broad exception, and suddenly you&rsquo;ve rebuilt the perimeter you were trying to eliminate. Every exception should have an expiration date and an owner.</p>
<p><strong>Ignoring device health.</strong> MFA plus a compromised laptop is still a compromised session. Device posture isn&rsquo;t optional.</p>
<p><strong>No communication plan.</strong> People will lose access to things. If you don&rsquo;t explain why and provide a clear path to regain access, you&rsquo;ll face a revolt and the project will get rolled back.</p>
<h2 id="where-this-is-going">Where this is going</h2>
<p>The perimeter model worked when your employees, your servers, and your data all lived in the same building. That world is gone and it&rsquo;s not coming back.</p>
<p>Zero trust isn&rsquo;t new. Google published the BeyondCorp papers years ago. The military has operated on need-to-know principles forever. What&rsquo;s new is that 2020 made it impossible to ignore. Every company just ran a forced experiment in remote access at scale, and the VPN-first companies suffered the most.</p>
<p>Start the migration now. Identity and MFA this month. Per-app access for your top ten applications this quarter. Shrink the VPN to legacy-only by end of year.</p>
<p>The network perimeter is dead. Stop defending a border that doesn&rsquo;t exist.</p>
]]></content:encoded></item><item><title>Platform Engineering vs DevOps: Mostly a Rebrand</title><link>https://lawzava.com/blog/2020-10-19-platform-engineering-practices/</link><pubDate>Mon, 19 Oct 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-10-19-platform-engineering-practices/</guid><description>The industry loves renaming things. Platform teams and internal developer platforms are DevOps done properly, and most companies still won&amp;amp;rsquo;t do it right.</description><content:encoded><![CDATA[<p>Platform engineering isn&rsquo;t a new discipline. It&rsquo;s what DevOps was supposed to be before we let the term get diluted into &ldquo;developer who also writes Terraform.&rdquo;</p>
<p>I&rsquo;ve been working with companies on their infrastructure stories for a while now. I keep seeing the same pattern. Someone reads a blog post about how Spotify or Netflix built an internal developer platform. They get excited. They rebrand their ops team. Nothing actually changes.</p>
<h2 id="the-naming-problem">The naming problem</h2>
<p>Our industry has a compulsive need to mint new labels. We did it with &ldquo;DevOps&rdquo; when we meant &ldquo;ops engineers who can code.&rdquo; We did it with &ldquo;SRE&rdquo; when we meant &ldquo;on-call but with better job titles.&rdquo; Now we&rsquo;re starting to do it with &ldquo;platform teams&rdquo; and &ldquo;internal developer platforms&rdquo; when we mean &ldquo;build internal tools so developers stop filing tickets.&rdquo;</p>
<p>The underlying idea is fine. Good, even. Build self-service paths. Automate the repetitive stuff. Treat your internal tooling like a product. None of this is new. It&rsquo;s what good infrastructure teams have done for years, and what  <a href="/blog/2017-12-28-building-platform-teams/"
   
   >I tried to build at the fintech startup</a>
. We just keep repackaging it.</p>
<h2 id="what-a-platform-has-to-do">What a platform has to do</h2>
<p>Strip away the branding and you get a short list. Your developers should be able to spin up a service, deploy it, and observe it without waiting on another team. That&rsquo;s it. That&rsquo;s the whole thing.</p>
<p>The hard part was never the concept. It&rsquo;s the execution. I&rsquo;ve seen companies build  <a href="/blog/2019-03-11-building-internal-developer-platforms/"
   
   >elaborate internal platforms that nobody uses</a>
 because the team built what they thought was cool instead of what developers actually needed. Classic product mistake, applied to infrastructure.</p>
<p>The teams that get it right have three habits:</p>
<p><strong>They start embarrassingly small.</strong> One good deploy pipeline beats a half-finished portal with a service catalog. Ship the boring stuff first.</p>
<p><strong>They talk to their users.</strong> Your users are the other engineers. If you&rsquo;re not sitting with them, watching them struggle with your tooling, you&rsquo;re guessing. And you&rsquo;re probably guessing wrong.</p>
<p><strong>They don&rsquo;t mandate adoption.</strong> If your platform is better, people will use it. If you have to force adoption, that&rsquo;s feedback. Listen to it.</p>
<h2 id="the-cynical-take">The cynical take</h2>
<p>Part of me expects the &ldquo;platform&rdquo; rebrand to become a way to hire for the same roles at higher salaries. New title, new comp band. I&rsquo;d get it. Supply and demand.</p>
<p>But the other part of me worries that every time we rename a discipline, we lose institutional knowledge. Junior engineers will think platform teams were invented last year. They miss the decades of operations wisdom that came before. They reinvent mistakes that were solved in 2012.</p>
<h2 id="do-the-boring-work">Do the boring work</h2>
<p>If you&rsquo;re building an internal platform right now, my advice is simple. Ignore the conference talks. Ignore the vendor pitches about developer portals. Go sit with a developer on your team. Watch them deploy something. Count the manual steps. Automate the worst one. Repeat.</p>
<p>That&rsquo;s platform engineering. It&rsquo;s also DevOps. It&rsquo;s also just&hellip; good engineering.</p>
<p>Call it whatever you want. Just do the work.</p>
]]></content:encoded></item><item><title>Kubernetes Operators in Go: Lessons From Six in Production</title><link>https://lawzava.com/blog/2020-08-03-kubernetes-operators-deep-dive/</link><pubDate>Mon, 03 Aug 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-08-03-kubernetes-operators-deep-dive/</guid><description>Lessons from building production operators at a cloud infrastructure startup: the reconciliation loop, controller-runtime patterns, and the mistakes that cost us sleep.</description><content:encoded><![CDATA[<p>Building Kubernetes operators is straightforward. Building ones that don&rsquo;t page you at 3am isn&rsquo;t.</p>
<p>My startup manages cloud infrastructure. VMs, networks, storage volumes: all of it provisioned and lifecycle-managed through  <a href="/blog/2018-04-02-kubernetes-operators-extending-the-platform/"
   
   >Kubernetes operators</a>
. Not because it&rsquo;s trendy. Because our customers expect <code>kubectl apply</code> and then things just work.</p>
<p>I&rsquo;ve written six operators over the past year and a half. Some are rock solid. Two of them cost me weekends. The difference was never the complexity of the domain. It was always the same three things: how I structured the reconciliation loop, how I designed the CRD, and whether I handled deletion properly.</p>
<p>This is the post I wish I&rsquo;d had when I started.</p>
<h2 id="what-an-operator-actually-is">What an operator actually is</h2>
<p>Strip away the marketing and an operator is just a control loop. You define a custom resource (your API), you write a controller that watches it, and every time something changes, your controller tries to make reality match the spec.</p>
<p>That&rsquo;s it. The entire pattern:</p>
<pre tabindex="0"><code>User writes CR spec -&gt; Controller sees change -&gt; Controller reconciles -&gt; Updates status -&gt; Waits
</code></pre><p>The &ldquo;waits&rdquo; part matters. Your controller isn&rsquo;t a script that runs once. It runs forever. It gets called when things change, when things break, when Kubernetes restarts it, when someone fat-fingers a <code>kubectl edit</code>. If your reconciler can&rsquo;t handle being called twice in a row with the same input and produce the same result, you&rsquo;re going to have a bad time.</p>
<h2 id="setting-up-with-controller-runtime">Setting up with controller-runtime</h2>
<p>I use  <a href="https://github.com/kubernetes-sigs/controller-runtime"
   
   
target="_blank" rel="noopener">controller-runtime</a>
 directly. Kubebuilder generates scaffolding on top of it, and that&rsquo;s fine for getting started, but I prefer knowing exactly what&rsquo;s under me. Less magic, fewer surprises when something breaks.</p>
<p>Here&rsquo;s the skeleton. I&rsquo;ll use a simplified version of one of our operators: a <code>CloudVolume</code> that provisions storage on a provider API and attaches it to a node.</p>
<p>First, the types:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">v1alpha1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> <span style="color:#a6e22e">metav1</span> <span style="color:#e6db74">&#34;k8s.io/apimachinery/pkg/apis/meta/v1&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CloudVolumeSpec</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">SizeGB</span>   <span style="color:#66d9ef">int</span>    <span style="color:#e6db74">`json:&#34;sizeGB&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Region</span>   <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;region&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">NodeName</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;nodeName&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CloudVolumeStatus</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">VolumeID</span>   <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;volumeID,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Phase</span>      <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;phase,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">AttachedTo</span> <span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;attachedTo,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// +kubebuilder:object:root=true</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// +kubebuilder:subresource:status</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CloudVolume</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">TypeMeta</span>   <span style="color:#e6db74">`json:&#34;,inline&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">ObjectMeta</span> <span style="color:#e6db74">`json:&#34;metadata,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Spec</span>              <span style="color:#a6e22e">CloudVolumeSpec</span>   <span style="color:#e6db74">`json:&#34;spec,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Status</span>            <span style="color:#a6e22e">CloudVolumeStatus</span> <span style="color:#e6db74">`json:&#34;status,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Small spec. Obvious fields. No <code>map[string]interface{}</code> anywhere. I can&rsquo;t stress this enough: the CRD is your product interface. If a user has to read your source code to figure out what to put in the spec, you&rsquo;ve already lost.</p>
<h2 id="the-reconciliation-loop">The reconciliation loop</h2>
<p>Here&rsquo;s the real thing. Not a toy example. This is close to what runs in our production clusters, simplified for clarity but structurally identical.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CloudVolumeReconciler</span>) <span style="color:#a6e22e">Reconcile</span>(<span style="color:#a6e22e">req</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">log</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Log</span>.<span style="color:#a6e22e">WithValues</span>(<span style="color:#e6db74">&#34;cloudvolume&#34;</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">NamespacedName</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">vol</span> <span style="color:#a6e22e">v1alpha1</span>.<span style="color:#a6e22e">CloudVolume</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">NamespacedName</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">vol</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{}, <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">IgnoreNotFound</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Handle deletion first. Always.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">DeletionTimestamp</span>.<span style="color:#a6e22e">IsZero</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">reconcileDelete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">vol</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Ensure finalizer is present</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">containsFinalizer</span>(<span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Finalizers</span>, <span style="color:#a6e22e">finalizerName</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Finalizers</span> = append(<span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Finalizers</span>, <span style="color:#a6e22e">finalizerName</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Update</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">vol</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{}, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{<span style="color:#a6e22e">Requeue</span>: <span style="color:#66d9ef">true</span>}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Provision if we don&#39;t have a volume ID yet</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Status</span>.<span style="color:#a6e22e">VolumeID</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">reconcileProvision</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">log</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">vol</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Attach if not attached</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Status</span>.<span style="color:#a6e22e">AttachedTo</span> <span style="color:#f92672">!=</span> <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Spec</span>.<span style="color:#a6e22e">NodeName</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">reconcileAttach</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">log</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">vol</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Steady state. Check back in 5 minutes.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{<span style="color:#a6e22e">RequeueAfter</span>: <span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span>}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Notice the structure. Deletion check first, then finalizer setup, then provision, then attach, then idle. Each branch returns early. No nested ifs six levels deep. No giant switch statement. The reconciler reads top to bottom like a decision tree, and every path is idempotent.</p>
<p>This structure isn&rsquo;t clever. It&rsquo;s boring. That&rsquo;s the point.</p>
<h2 id="the-provision-step">The provision step</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CloudVolumeReconciler</span>) <span style="color:#a6e22e">reconcileProvision</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">log</span> <span style="color:#a6e22e">logr</span>.<span style="color:#a6e22e">Logger</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">vol</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">v1alpha1</span>.<span style="color:#a6e22e">CloudVolume</span>,
</span></span><span style="display:flex;"><span>) (<span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Info</span>(<span style="color:#e6db74">&#34;provisioning volume&#34;</span>, <span style="color:#e6db74">&#34;sizeGB&#34;</span>, <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Spec</span>.<span style="color:#a6e22e">SizeGB</span>, <span style="color:#e6db74">&#34;region&#34;</span>, <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Spec</span>.<span style="color:#a6e22e">Region</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">volumeID</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Provider</span>.<span style="color:#a6e22e">CreateVolume</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Spec</span>.<span style="color:#a6e22e">SizeGB</span>, <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Spec</span>.<span style="color:#a6e22e">Region</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">err</span>, <span style="color:#e6db74">&#34;failed to provision volume&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">setPhase</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">vol</span>, <span style="color:#e6db74">&#34;ProvisionFailed&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Don&#39;t return the error. Requeue with backoff instead.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{<span style="color:#a6e22e">RequeueAfter</span>: <span style="color:#ae81ff">30</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Status</span>.<span style="color:#a6e22e">VolumeID</span> = <span style="color:#a6e22e">volumeID</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">setPhase</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">vol</span>, <span style="color:#e6db74">&#34;Provisioned&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Status</span>().<span style="color:#a6e22e">Update</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">vol</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{}, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Info</span>(<span style="color:#e6db74">&#34;volume provisioned&#34;</span>, <span style="color:#e6db74">&#34;volumeID&#34;</span>, <span style="color:#a6e22e">volumeID</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{<span style="color:#a6e22e">Requeue</span>: <span style="color:#66d9ef">true</span>}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Two things I learned the hard way here.</p>
<p><strong>Don&rsquo;t return errors from external API calls.</strong> If you <code>return ctrl.Result{}, err</code>, controller-runtime uses exponential backoff, which is fine. But for provider API failures, I want explicit control over retry timing. A provider outage shouldn&rsquo;t hammer their API with exponential retries that max out at 16 minutes. I return <code>RequeueAfter</code> with a fixed delay and handle the error myself.</p>
<p><strong>Update status immediately after the side effect.</strong> If I provision the volume but crash before writing the status, the next reconciliation will call <code>CreateVolume</code> again. That&rsquo;s why our provider client is idempotent: it checks if a volume with the same labels already exists and returns it. But the status update should still happen as fast as possible to minimize the window.</p>
<h2 id="deletion-and-finalizers">Deletion and finalizers</h2>
<p>This is where most operators break. I see people skip finalizers because &ldquo;it&rsquo;s just Kubernetes resources, they&rsquo;ll get garbage collected.&rdquo; Sure, for in-cluster stuff. But we&rsquo;re provisioning cloud resources. If someone deletes the <code>CloudVolume</code> CR and we don&rsquo;t clean up the actual volume, we&rsquo;re leaking money.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">finalizerName</span> = <span style="color:#e6db74">&#34;cloudvolume.example.com/cleanup&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CloudVolumeReconciler</span>) <span style="color:#a6e22e">reconcileDelete</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">vol</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">v1alpha1</span>.<span style="color:#a6e22e">CloudVolume</span>,
</span></span><span style="display:flex;"><span>) (<span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">log</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Log</span>.<span style="color:#a6e22e">WithValues</span>(<span style="color:#e6db74">&#34;cloudvolume&#34;</span>, <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Name</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">containsFinalizer</span>(<span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Finalizers</span>, <span style="color:#a6e22e">finalizerName</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Status</span>.<span style="color:#a6e22e">VolumeID</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Info</span>(<span style="color:#e6db74">&#34;deleting external volume&#34;</span>, <span style="color:#e6db74">&#34;volumeID&#34;</span>, <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Status</span>.<span style="color:#a6e22e">VolumeID</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Provider</span>.<span style="color:#a6e22e">DeleteVolume</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Status</span>.<span style="color:#a6e22e">VolumeID</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> !<span style="color:#a6e22e">isNotFound</span>(<span style="color:#a6e22e">err</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">err</span>, <span style="color:#e6db74">&#34;failed to delete external volume&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{<span style="color:#a6e22e">RequeueAfter</span>: <span style="color:#ae81ff">15</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Remove finalizer</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Finalizers</span> = <span style="color:#a6e22e">removeFinalizer</span>(<span style="color:#a6e22e">vol</span>.<span style="color:#a6e22e">Finalizers</span>, <span style="color:#a6e22e">finalizerName</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Update</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">vol</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{}, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Info</span>(<span style="color:#e6db74">&#34;cleanup complete&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Result</span>{}, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>isNotFound</code> check matters. If the volume was already deleted externally (someone cleaned it up in the cloud console, provider auto-deleted it, whatever), we still need to remove the finalizer. Without that check, the CR gets stuck in <code>Terminating</code> forever. Ask me how I know.</p>
<h2 id="wiring-it-up">Wiring it up</h2>
<p>The main setup is straightforward. This is where you tell controller-runtime what to watch and how to build the manager.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">metricsAddr</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">StringVar</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">metricsAddr</span>, <span style="color:#e6db74">&#34;metrics-addr&#34;</span>, <span style="color:#e6db74">&#34;:8080&#34;</span>, <span style="color:#e6db74">&#34;metrics endpoint&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">flag</span>.<span style="color:#a6e22e">Parse</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">mgr</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">NewManager</span>(<span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">GetConfigOrDie</span>(), <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Options</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Scheme</span>:             <span style="color:#a6e22e">scheme</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">MetricsBindAddress</span>: <span style="color:#a6e22e">metricsAddr</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Port</span>:               <span style="color:#ae81ff">9443</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">LeaderElection</span>:     <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">LeaderElectionID</span>:   <span style="color:#e6db74">&#34;cloudvolume-controller&#34;</span>,
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">setupLog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">err</span>, <span style="color:#e6db74">&#34;unable to start manager&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> (<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">CloudVolumeReconciler</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Client</span>:   <span style="color:#a6e22e">mgr</span>.<span style="color:#a6e22e">GetClient</span>(),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Log</span>:      <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Log</span>.<span style="color:#a6e22e">WithName</span>(<span style="color:#e6db74">&#34;cloudvolume&#34;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Scheme</span>:   <span style="color:#a6e22e">mgr</span>.<span style="color:#a6e22e">GetScheme</span>(),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Provider</span>: <span style="color:#a6e22e">provider</span>.<span style="color:#a6e22e">NewClient</span>(),
</span></span><span style="display:flex;"><span>    }).<span style="color:#a6e22e">SetupWithManager</span>(<span style="color:#a6e22e">mgr</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">setupLog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">err</span>, <span style="color:#e6db74">&#34;unable to create controller&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">mgr</span>.<span style="color:#a6e22e">Start</span>(<span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">SetupSignalHandler</span>()); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">setupLog</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">err</span>, <span style="color:#e6db74">&#34;problem running manager&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Exit</span>(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>LeaderElection: true</code>: don&rsquo;t skip this. Without it, running two replicas means two controllers fighting over the same resources. We had this bug for exactly one deploy before someone noticed duplicate volumes showing up.</p>
<h2 id="testing-operators">Testing operators</h2>
<p>Testing operators is painful. I won&rsquo;t pretend otherwise. But <code>envtest</code> from controller-runtime makes it tolerable. It spins up a real API server and etcd in-process. No mocks for the Kubernetes API layer.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestReconcileProvision</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">env</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">envtest</span>.<span style="color:#a6e22e">Environment</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">CRDDirectoryPaths</span>: []<span style="color:#66d9ef">string</span>{<span style="color:#a6e22e">filepath</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#e6db74">&#34;..&#34;</span>, <span style="color:#e6db74">&#34;config&#34;</span>, <span style="color:#e6db74">&#34;crd&#34;</span>, <span style="color:#e6db74">&#34;bases&#34;</span>)},
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cfg</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">Start</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">require</span>.<span style="color:#a6e22e">NoError</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">env</span>.<span style="color:#a6e22e">Stop</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">k8sClient</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">cfg</span>, <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">Options</span>{<span style="color:#a6e22e">Scheme</span>: <span style="color:#a6e22e">scheme</span>})
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">require</span>.<span style="color:#a6e22e">NoError</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Create a CloudVolume</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">vol</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">v1alpha1</span>.<span style="color:#a6e22e">CloudVolume</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ObjectMeta</span>: <span style="color:#a6e22e">metav1</span>.<span style="color:#a6e22e">ObjectMeta</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Name</span>:      <span style="color:#e6db74">&#34;test-vol&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Namespace</span>: <span style="color:#e6db74">&#34;default&#34;</span>,
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Spec</span>: <span style="color:#a6e22e">v1alpha1</span>.<span style="color:#a6e22e">CloudVolumeSpec</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">SizeGB</span>:   <span style="color:#ae81ff">100</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Region</span>:   <span style="color:#e6db74">&#34;eu-west-1&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">NodeName</span>: <span style="color:#e6db74">&#34;worker-1&#34;</span>,
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">require</span>.<span style="color:#a6e22e">NoError</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">k8sClient</span>.<span style="color:#a6e22e">Create</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">TODO</span>(), <span style="color:#a6e22e">vol</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Run reconciler with a fake provider</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">reconciler</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">CloudVolumeReconciler</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Client</span>:   <span style="color:#a6e22e">k8sClient</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Log</span>:      <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Log</span>.<span style="color:#a6e22e">WithName</span>(<span style="color:#e6db74">&#34;test&#34;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Scheme</span>:   <span style="color:#a6e22e">scheme</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Provider</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">fakeProvider</span>{<span style="color:#a6e22e">volumeID</span>: <span style="color:#e6db74">&#34;vol-abc123&#34;</span>},
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">reconciler</span>.<span style="color:#a6e22e">Reconcile</span>(<span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Request</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">NamespacedName</span>: <span style="color:#a6e22e">types</span>.<span style="color:#a6e22e">NamespacedName</span>{<span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;test-vol&#34;</span>, <span style="color:#a6e22e">Namespace</span>: <span style="color:#e6db74">&#34;default&#34;</span>},
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">require</span>.<span style="color:#a6e22e">NoError</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">assert</span>.<span style="color:#a6e22e">True</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">result</span>.<span style="color:#a6e22e">Requeue</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Verify status was updated</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">updated</span> <span style="color:#a6e22e">v1alpha1</span>.<span style="color:#a6e22e">CloudVolume</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">require</span>.<span style="color:#a6e22e">NoError</span>(<span style="color:#a6e22e">t</span>, <span style="color:#a6e22e">k8sClient</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">TODO</span>(), <span style="color:#a6e22e">types</span>.<span style="color:#a6e22e">NamespacedName</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Name</span>: <span style="color:#e6db74">&#34;test-vol&#34;</span>, <span style="color:#a6e22e">Namespace</span>: <span style="color:#e6db74">&#34;default&#34;</span>,
</span></span><span style="display:flex;"><span>    }, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">updated</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">assert</span>.<span style="color:#a6e22e">Equal</span>(<span style="color:#a6e22e">t</span>, <span style="color:#e6db74">&#34;vol-abc123&#34;</span>, <span style="color:#a6e22e">updated</span>.<span style="color:#a6e22e">Status</span>.<span style="color:#a6e22e">VolumeID</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Mock the external provider, not Kubernetes. The fake provider interface is trivial: <code>CreateVolume</code>, <code>DeleteVolume</code>, <code>AttachVolume</code>. Maybe five methods total. This gives you fast, reliable tests that catch real API interaction bugs.</p>
<p>Unit tests for pure logic (like &ldquo;given this spec and this status, what phase should we be in?&rdquo;) are even simpler. Regular table-driven Go tests. No envtest needed.</p>
<h2 id="mistakes-ive-made-so-you-dont-have-to">Mistakes I&rsquo;ve made (so you don&rsquo;t have to)</h2>
<p><strong>Mutating spec in the reconciler.</strong> I did this once to &ldquo;normalize&rdquo; a field. Created an infinite reconcile loop because every spec update triggered a new event. Took me an embarrassingly long time to figure out why CPU was pinned at 100%.</p>
<p><strong>Not watching owned resources.</strong> Our volume operator creates a PersistentVolume as a downstream resource. If someone manually deletes the PV, the operator needs to know. Forgot to add it to the watch and spent a day debugging why volumes were &ldquo;attached&rdquo; but pods couldn&rsquo;t mount them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CloudVolumeReconciler</span>) <span style="color:#a6e22e">SetupWithManager</span>(<span style="color:#a6e22e">mgr</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">Manager</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctrl</span>.<span style="color:#a6e22e">NewControllerManagedBy</span>(<span style="color:#a6e22e">mgr</span>).
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">For</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">v1alpha1</span>.<span style="color:#a6e22e">CloudVolume</span>{}).
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// PVs are cluster-scoped, so they can&#39;t carry an owner reference to a</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// namespaced CloudVolume. Map them back through labels we set on create.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Watches</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">source</span>.<span style="color:#a6e22e">Kind</span>{<span style="color:#a6e22e">Type</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">corev1</span>.<span style="color:#a6e22e">PersistentVolume</span>{}},
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">handler</span>.<span style="color:#a6e22e">EnqueueRequestsFromMapFunc</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">ToRequests</span>: <span style="color:#a6e22e">handler</span>.<span style="color:#a6e22e">ToRequestsFunc</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">obj</span> <span style="color:#a6e22e">handler</span>.<span style="color:#a6e22e">MapObject</span>) []<span style="color:#a6e22e">reconcile</span>.<span style="color:#a6e22e">Request</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">labels</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">obj</span>.<span style="color:#a6e22e">Meta</span>.<span style="color:#a6e22e">GetLabels</span>()
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">name</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">labels</span>[<span style="color:#e6db74">&#34;cloudvolume.example.com/name&#34;</span>]
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">return</span> []<span style="color:#a6e22e">reconcile</span>.<span style="color:#a6e22e">Request</span>{{<span style="color:#a6e22e">NamespacedName</span>: <span style="color:#a6e22e">types</span>.<span style="color:#a6e22e">NamespacedName</span>{
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">Name</span>:      <span style="color:#a6e22e">name</span>,
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">Namespace</span>: <span style="color:#a6e22e">labels</span>[<span style="color:#e6db74">&#34;cloudvolume.example.com/namespace&#34;</span>],
</span></span><span style="display:flex;"><span>                    }}}
</span></span><span style="display:flex;"><span>                }),
</span></span><span style="display:flex;"><span>            }).
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">r</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Writing status before the side effect completes.</strong> Optimistic status updates sound reasonable until your provider call fails after you already told the world the volume exists. Then you have phantom resources in your status that don&rsquo;t exist anywhere. Always: do the thing, then update the status.</p>
<p><strong>No rate limiting on requeue.</strong> Early versions of our operator requeued immediately on every provider error. During a 30-minute cloud outage, we sent thousands of requests to a failing API. Our provider rate-limited us, which made the recovery take even longer. Fixed it by adding <code>RequeueAfter: 30 * time.Second</code> minimum for any external call failure.</p>
<h2 id="when-not-to-write-an-operator">When not to write an operator</h2>
<p>Operators are the wrong tool when the thing you&rsquo;re managing doesn&rsquo;t have meaningful lifecycle beyond deploy-and-forget. A stateless web app with a Deployment, Service, and Ingress? Helm chart. Done.</p>
<p>I write an operator when:</p>
<ul>
<li>The thing has external state that Kubernetes doesn&rsquo;t know about</li>
<li>Day-2 ops involve multi-step coordination (backup, failover, resize)</li>
<li>Humans currently follow a runbook that involves <code>kubectl</code> and another API</li>
</ul>
<p>If none of those apply, you&rsquo;re adding complexity for no reason. And complexity in controllers is expensive because it runs 24/7 and fails silently.</p>
<h2 id="where-we-are-now">Where we are now</h2>
<p>Six operators. Three manage cloud resources (volumes, networks, VMs). Two handle internal platform concerns (cert rotation, DNS). One manages database provisioning. The three cloud operators are the most complex because external APIs are unreliable and slow. The internal ones are surprisingly simple once you get the pattern down.</p>
<p>Total lines of Go across all six: roughly 12,000. Most of that&rsquo;s tests. The actual reconciler logic is small in every case. If your reconciler is more than 200-300 lines, you&rsquo;re probably doing too much in one controller.</p>
<p>The operator pattern works. But it demands discipline. Idempotent reconciliation. Small CRDs. Defensive deletion. Boring code that a sleepy on-call engineer can read at 3am and understand what went wrong.</p>
<p>That last part is the real test.</p>
]]></content:encoded></item><item><title>Kubernetes Requests and Limits: How I Size Resources</title><link>https://lawzava.com/blog/2020-06-01-kubernetes-resource-management/</link><pubDate>Mon, 01 Jun 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-06-01-kubernetes-resource-management/</guid><description>Most K8s clusters I audit are either wildly overprovisioned or one bad deploy away from eviction storms. Here&amp;amp;rsquo;s how I set requests, limits, and guardrails.</description><content:encoded><![CDATA[<p>Your requests decide scheduling. Your limits decide who dies under pressure. Get them wrong and you&rsquo;re either burning money or getting paged at 3am.</p>
<p>Between running infrastructure at my infrastructure startup and working with other teams, there&rsquo;s one pattern I see in almost every cluster I look at: resource settings are either copy-pasted from a tutorial, set to absurdly high values &ldquo;just to be safe,&rdquo; or missing entirely.</p>
<p>The result is always the same. Either you&rsquo;re paying for  <a href="/blog/2019-04-08-cloud-cost-optimization/"
   
   >3x the compute you actually need</a>
, or your pods are getting evicted during the one traffic spike that matters.</p>
<h2 id="requests-vs-limits-two-different-jobs">Requests vs limits: two different jobs</h2>
<p>People treat these as the same thing. They&rsquo;re not.</p>
<p><strong>Requests</strong> are what the scheduler sees. When you set <code>requests.cpu: 200m</code>, you&rsquo;re telling Kubernetes &ldquo;this pod needs at least 200 millicores to run, find me a node that has that available.&rdquo; The scheduler uses requests to bin-pack pods onto nodes.</p>
<p><strong>Limits</strong> are runtime caps. The kernel enforces these. Hit your CPU limit and you get throttled. Hit your memory limit and you get OOM-killed. Very different consequences.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;200m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;1&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;512Mi&#34;</span>
</span></span></code></pre></div><p>One thing that trips people up: if you set a limit but skip the request, Kubernetes sets the request equal to the limit. If you set neither, congratulations, you&rsquo;ve got a BestEffort pod. First to die when the node gets tight.</p>
<h2 id="cpu-throttling-vs-oom-kills">CPU throttling vs OOM kills</h2>
<p>This is the part that bites people.</p>
<p>CPU is compressible. Your container hits its CPU limit? It gets throttled. Slower responses, higher latency, but it stays alive. Annoying but survivable.</p>
<p>Memory isn&rsquo;t compressible. Your container exceeds its memory limit? The kernel kills it. No negotiation. The pod restarts (if your restart policy allows it), your users see errors, and you get an alert.</p>
<p>I&rsquo;ve seen teams set memory limits too close to actual usage, then wonder why they&rsquo;re getting random OOM kills during garbage collection spikes. Leave real headroom on memory. I&rsquo;d rather waste 50Mi per pod than deal with cascading restarts.</p>
<h2 id="qos-classes-matter-more-than-you-think">QoS classes matter more than you think</h2>
<p>Kubernetes assigns every pod a Quality of Service class based on its resource settings. The class shapes eviction order when a node runs out of resources.</p>
<p><strong>Guaranteed:</strong> requests equal limits for both CPU and memory, in every container. These pods get evicted last. Use this for anything you actually care about in production.</p>
<p><strong>Burstable:</strong> requests are set but limits are higher. Most pods in real clusters end up here. Fine for most workloads, but know that you&rsquo;re in the middle of the eviction queue.</p>
<p><strong>BestEffort:</strong> no requests, no limits, no guarantees. First to get evicted. I only use this for batch jobs I don&rsquo;t care about.</p>
<p>The mistake I see constantly: teams running critical APIs as BestEffort because nobody set resource fields in the deployment spec. Then a memory spike on the node evicts those pods before the logging agent that has Guaranteed QoS. Backwards.</p>
<h2 id="how-i-actually-size-resources">How I actually size resources</h2>
<p>I don&rsquo;t guess. I measure, then set values based on real data.</p>
<p>Start with <code>kubectl top</code> to get a quick snapshot:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl top pods -n production
</span></span><span style="display:flex;"><span>kubectl top pods -n production --containers
</span></span></code></pre></div><p>But a snapshot isn&rsquo;t enough. You need percentile data over days or weeks. Pull p50, p75, p95, and p99 from whatever metrics system you&rsquo;re running: Prometheus, Datadog, whatever.</p>
<p>Then I set values like this:</p>
<ul>
<li><strong>CPU request</strong>: p75 usage plus a small buffer. This is your &ldquo;normal operating range.&rdquo;</li>
<li><strong>Memory request</strong>: p95 usage plus a buffer. Memory spikes are less forgiving.</li>
<li><strong>CPU limit</strong>: 2-3x the request. Let it burst for short periods.</li>
<li><strong>Memory limit</strong>: request plus enough headroom for GC spikes and transient allocations. But not so much that a leak runs unchecked.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;200m&#34;</span>      <span style="color:#75715e"># p75 + headroom</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;300Mi&#34;</span>  <span style="color:#75715e"># p95 + headroom</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span>      <span style="color:#75715e"># room for bursts</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;450Mi&#34;</span>  <span style="color:#75715e"># room for GC, not for leaks</span>
</span></span></code></pre></div><p>Review these after any significant traffic change. Resource settings should evolve with your workload, not stay frozen from the day someone first deployed the service.</p>
<h2 id="autoscaling-helps-but-isnt-magic">Autoscaling helps but isn&rsquo;t magic</h2>
<p>HPA (Horizontal Pod Autoscaler) scales replicas based on CPU utilization relative to your requests. If your requests are wrong, your scaling behavior is wrong. Garbage in, garbage out.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">autoscaling/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">HorizontalPodAutoscaler</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-hpa</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">scaleTargetRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">minReplicas</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">maxReplicas</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">targetCPUUtilizationPercentage</span>: <span style="color:#ae81ff">70</span>
</span></span></code></pre></div><p>VPA (Vertical Pod Autoscaler) is interesting but still beta. I run it in recommendation mode only: let it tell me what it thinks the requests should be, then I decide whether to apply that.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">autoscaling.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">VerticalPodAutoscaler</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-vpa</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">targetRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">updatePolicy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">updateMode</span>: <span style="color:#e6db74">&#34;Off&#34;</span>
</span></span></code></pre></div><p>I don&rsquo;t trust VPA to mutate running pods in production yet. Maybe next year.</p>
<h2 id="limitranges-and-resourcequotas-the-guardrails-you-need">LimitRanges and ResourceQuotas: the guardrails you need</h2>
<p>Every namespace in a shared cluster should have a LimitRange. Full stop. Without one, any developer can deploy a pod with no resource settings, and now you&rsquo;ve got a BestEffort pod sitting in production waiting to get evicted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">LimitRange</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">default-limits</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Container</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">defaultRequest</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;100m&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;128Mi&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">default</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span></code></pre></div><p>This gives every container sane defaults even if the developer forgot to set them. It&rsquo;s a safety net, not a replacement for proper resource specs.</p>
<p>ResourceQuotas cap total consumption per namespace. Essential for multi-tenant clusters where you don&rsquo;t want one team eating the whole cluster.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ResourceQuota</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">team-quota</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hard</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests.cpu</span>: <span style="color:#e6db74">&#34;10&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests.memory</span>: <span style="color:#e6db74">&#34;20Gi&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limits.cpu</span>: <span style="color:#e6db74">&#34;20&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limits.memory</span>: <span style="color:#e6db74">&#34;40Gi&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pods</span>: <span style="color:#e6db74">&#34;50&#34;</span>
</span></span></code></pre></div><h2 id="what-to-actually-monitor">What to actually monitor</h2>
<p>Set it and forget it doesn&rsquo;t work here. Watch these:</p>
<ul>
<li><strong>CPU throttling rate:</strong> if it&rsquo;s consistently high, your CPU limit is too low</li>
<li><strong>OOM kill count:</strong> any non-zero number means your memory limit needs attention</li>
<li><strong>Request-to-usage ratio:</strong> if you&rsquo;re requesting 4x what you use, you&rsquo;re wasting cluster capacity</li>
<li><strong>Allocatable vs requested:</strong> tells you how much room the cluster actually has</li>
<li><strong>Eviction events:</strong> if pods are getting evicted, something is wrong with your resource math</li>
</ul>
<p>I check these weekly for the clusters I manage. It takes ten minutes and prevents most resource-related incidents before they happen. It belongs on the same  <a href="/blog/2019-01-14-kubernetes-best-practices-2019/"
   
   >boring checklist</a>
 as probes and upgrades.</p>
<p>Nobody writes conference talks about setting CPU requests correctly. Get it wrong anyway and a BestEffort pod gets evicted at 3am and takes your API gateway with it.</p>
]]></content:encoded></item><item><title>gRPC in Production with Go: Protos, Deadlines, Errors</title><link>https://lawzava.com/blog/2020-05-11-grpc-best-practices/</link><pubDate>Mon, 11 May 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-05-11-grpc-best-practices/</guid><description>gRPC patterns from moving a startup&amp;amp;rsquo;s internal APIs off REST: proto design, Go servers and clients, status codes, testing, and mistakes that cost weekends.</description><content:encoded><![CDATA[<p>Stop treating gRPC like REST with a different wire format. Design protos for evolution from day one, always set deadlines, use interceptors for everything cross-cutting, and test with real servers.</p>
<p>We moved my infrastructure startup&rsquo;s internal APIs to gRPC about eight months ago. Before that, everything was REST with hand-rolled JSON serialization, version mismatches everywhere, and a weekly ritual of &ldquo;why is the client sending a string where we expect an int.&rdquo; Moving to gRPC fixed the type safety problem. It also introduced a whole new category of problems I didn&rsquo;t anticipate.</p>
<p>This is what I wish someone had written down before we started, with the Go code we actually run.</p>
<h2 id="why-we-picked-grpc-and-where-we-didnt">Why we picked gRPC (and where we didn&rsquo;t)</h2>
<p>Short version: internal service-to-service calls where latency matters and both sides are services we control. That&rsquo;s it. That&rsquo;s the use case.</p>
<p>We kept REST for:</p>
<ul>
<li>Anything browser-facing. gRPC-Web exists but it&rsquo;s a subset and adds a proxy layer. Not worth it for us.</li>
<li>Third-party integrations. Nobody wants to learn your proto schema.</li>
<li>Simple CRUD admin tools. <code>curl</code> is a better debugger than <code>grpcurl</code> and I don&rsquo;t care what anyone says.</li>
</ul>
<p>The real win was code generation. We have Go, Python, and a small Rust service. Generating typed clients from one <code>.proto</code> file eliminated an entire class of integration bugs. Before gRPC, every language had its own hand-written client that drifted independently. Now drift is a compilation error.</p>
<h2 id="proto-design-get-this-wrong-and-youll-pay-for-years">Proto design: get this wrong and you&rsquo;ll pay for years</h2>
<p>Field numbers are forever. I mean that literally. Once you ship a proto, those field numbers are carved into every binary that&rsquo;s ever been compiled against it. Get the schema design wrong early and you&rsquo;re living with it or doing a painful migration.</p>
<p>Here&rsquo;s what our service definitions actually look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-protobuf" data-lang="protobuf"><span style="display:flex;"><span>syntax <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;proto3&#34;</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#f92672">package</span> infra<span style="color:#f92672">.</span>nodes.v1;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">import</span> <span style="color:#e6db74">&#34;google/protobuf/timestamp.proto&#34;</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">service</span> NodeService {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">rpc</span> GetNode(GetNodeRequest) <span style="color:#66d9ef">returns</span> (Node);<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">rpc</span> ListNodes(ListNodesRequest) <span style="color:#66d9ef">returns</span> (ListNodesResponse);<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">rpc</span> WatchNodeStatus(WatchNodeStatusRequest) <span style="color:#66d9ef">returns</span> (stream NodeStatusEvent);<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">GetNodeRequest</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">string</span> node_id <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">ListNodesRequest</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">int32</span> page_size <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">string</span> page_token <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NodeFilter filter <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">ListNodesResponse</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">repeated</span> Node nodes <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">string</span> next_page_token <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">NodeFilter</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">repeated</span> <span style="color:#66d9ef">string</span> regions <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NodeStatus status <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">Node</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">string</span> id <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">string</span> hostname <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NodeStatus status <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  google.protobuf.Timestamp created_at <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  google.protobuf.Timestamp last_heartbeat <span style="color:#f92672">=</span> <span style="color:#ae81ff">5</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NodeResources resources <span style="color:#f92672">=</span> <span style="color:#ae81ff">6</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  reserved <span style="color:#ae81ff">7</span>, <span style="color:#ae81ff">8</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  reserved <span style="color:#e6db74">&#34;legacy_provider&#34;</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">NodeResources</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">int64</span> cpu_millicores <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">int64</span> memory_bytes <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">int64</span> disk_bytes <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">enum</span> NodeStatus {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NODE_STATUS_UNSPECIFIED <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NODE_STATUS_PROVISIONING <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NODE_STATUS_READY <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NODE_STATUS_DRAINING <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NODE_STATUS_OFFLINE <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">WatchNodeStatusRequest</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">string</span> node_id <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">message</span> <span style="color:#a6e22e">NodeStatusEvent</span> {<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  <span style="color:#66d9ef">string</span> node_id <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NodeStatus previous <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  NodeStatus current <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>  google.protobuf.Timestamp occurred_at <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>}<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>A few things worth noting:</p>
<p><strong>Every RPC gets its own request and response messages.</strong> Even if <code>GetNodeRequest</code> only has one field today. You will add fields later. If you use a raw <code>string</code> as your request type, you&rsquo;ve locked yourself out of adding filters, field masks, or anything else without a breaking change.</p>
<p><strong>Enums start with <code>_UNSPECIFIED = 0</code>.</strong> Proto3 defaults to zero. If you put a meaningful value at zero, you can&rsquo;t distinguish &ldquo;the client explicitly set this&rdquo; from &ldquo;the client didn&rsquo;t set it.&rdquo; We learned this the hard way with a status enum that defaulted to <code>ACTIVE</code> at zero. Debugging phantom active nodes wasn&rsquo;t fun.</p>
<p><strong>Reserve removed fields.</strong> See that <code>reserved 7, 8</code> and <code>reserved &quot;legacy_provider&quot;</code>? Those are fields we removed during a redesign. Without the reservation, someone could reuse field number 7 for a completely different type and corrupt data in old clients that still have the old schema cached.</p>
<p><strong>Use <code>Timestamp</code> not <code>int64</code>.</strong> I&rsquo;ve seen people use epoch millis as int64 fields. Looks fine until you&rsquo;re debugging across time zones and nobody remembers whether it&rsquo;s seconds or milliseconds or which epoch.</p>
<h2 id="go-server-implementation">Go server implementation</h2>
<p>Here&rsquo;s a stripped-down but realistic server. This is close to what we actually run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#f92672">package</span> <span style="color:#a6e22e">main</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;context&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;log&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;net&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;google.golang.org/grpc&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;google.golang.org/grpc/codes&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;google.golang.org/grpc/health&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">healthpb</span> <span style="color:#e6db74">&#34;google.golang.org/grpc/health/grpc_health_v1&#34;</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;google.golang.org/grpc/status&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">pb</span> <span style="color:#e6db74">&#34;example.com/infra/api/nodes/v1&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">nodeServer</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">UnimplementedNodeServiceServer</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">store</span> <span style="color:#a6e22e">NodeStore</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">nodeServer</span>) <span style="color:#a6e22e">GetNode</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">GetNodeRequest</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">Node</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">GetNodeId</span>() <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">InvalidArgument</span>, <span style="color:#e6db74">&#34;node_id is required&#34;</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">node</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">GetNodeId</span>())
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">Internal</span>, <span style="color:#e6db74">&#34;store lookup failed: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">node</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">NotFound</span>, <span style="color:#e6db74">&#34;node %q not found&#34;</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">GetNodeId</span>())
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">node</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">nodeServer</span>) <span style="color:#a6e22e">WatchNodeStatus</span>(<span style="color:#a6e22e">req</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">WatchNodeStatusRequest</span>, <span style="color:#a6e22e">stream</span> <span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">NodeService_WatchNodeStatusServer</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">GetNodeId</span>() <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">InvalidArgument</span>, <span style="color:#e6db74">&#34;node_id is required&#34;</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">events</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Subscribe</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">GetNodeId</span>())
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">store</span>.<span style="color:#a6e22e">Unsubscribe</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">GetNodeId</span>(), <span style="color:#a6e22e">events</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">select</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Context</span>().<span style="color:#a6e22e">Done</span>():
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">event</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">events</span>:
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>			}
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">stream</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">event</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>			}
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">main</span>() {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">lis</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Listen</span>(<span style="color:#e6db74">&#34;tcp&#34;</span>, <span style="color:#e6db74">&#34;:9090&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;failed to listen: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">srv</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">NewServer</span>(
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">ChainUnaryInterceptor</span>(
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">loggingInterceptor</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">recoveryInterceptor</span>,
</span></span><span style="display:flex;"><span>		),
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">RegisterNodeServiceServer</span>(<span style="color:#a6e22e">srv</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">nodeServer</span>{<span style="color:#a6e22e">store</span>: <span style="color:#a6e22e">NewNodeStore</span>()})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">hsrv</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">health</span>.<span style="color:#a6e22e">NewServer</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">healthpb</span>.<span style="color:#a6e22e">RegisterHealthServer</span>(<span style="color:#a6e22e">srv</span>, <span style="color:#a6e22e">hsrv</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">hsrv</span>.<span style="color:#a6e22e">SetServingStatus</span>(<span style="color:#e6db74">&#34;infra.nodes.v1.NodeService&#34;</span>, <span style="color:#a6e22e">healthpb</span>.<span style="color:#a6e22e">HealthCheckResponse_SERVING</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;listening on :9090&#34;</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">Serve</span>(<span style="color:#a6e22e">lis</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;failed to serve: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A few patterns here that took us a while to settle on:</p>
<p><strong>Embed <code>UnimplementedNodeServiceServer</code>.</strong> This is the forward-compatible pattern. When you add a new RPC to the proto, the server still compiles. It returns <code>Unimplemented</code> for the new method until you add the handler. Without this, adding a method to the proto breaks every server binary.</p>
<p><strong>Register the health service.</strong> This isn&rsquo;t optional. Kubernetes liveness and readiness probes need it (through <code>grpc_health_probe</code>, since Kubernetes has no native gRPC probe). Our deploy pipeline rejects services that don&rsquo;t register the gRPC health check. No exceptions.</p>
<p><strong>Chain interceptors.</strong> Logging, recovery, metrics: all cross-cutting concerns go in interceptors, not in every handler. The <code>ChainUnaryInterceptor</code> API landed relatively recently and it&rsquo;s much cleaner than the old single-interceptor pattern where you&rsquo;d nest them manually.</p>
<h2 id="client-patterns-that-dont-break-at-3am">Client patterns that don&rsquo;t break at 3am</h2>
<p>The client side is where most people get sloppy. Here&rsquo;s what we enforce:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">newNodeClient</span>(<span style="color:#a6e22e">addr</span> <span style="color:#66d9ef">string</span>) (<span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">NodeServiceClient</span>, <span style="color:#66d9ef">func</span>(), <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#ae81ff">5</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">DialContext</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">addr</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">WithTransportCredentials</span>(<span style="color:#a6e22e">loadTLSCredentials</span>()),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">WithDefaultCallOptions</span>(
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">MaxCallRecvMsgSize</span>(<span style="color:#ae81ff">4</span><span style="color:#f92672">*</span><span style="color:#ae81ff">1024</span><span style="color:#f92672">*</span><span style="color:#ae81ff">1024</span>),
</span></span><span style="display:flex;"><span>		),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">WithChainUnaryInterceptor</span>(
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">retryInterceptor</span>(<span style="color:#ae81ff">3</span>, <span style="color:#ae81ff">100</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Millisecond</span>),
</span></span><span style="display:flex;"><span>		),
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;dial %s: %w&#34;</span>, <span style="color:#a6e22e">addr</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">cleanup</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">conn</span>.<span style="color:#a6e22e">Close</span>() }
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">NewNodeServiceClient</span>(<span style="color:#a6e22e">conn</span>), <span style="color:#a6e22e">cleanup</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">getNodeWithDeadline</span>(<span style="color:#a6e22e">client</span> <span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">NodeServiceClient</span>, <span style="color:#a6e22e">nodeID</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">Node</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#ae81ff">3</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">node</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">GetNode</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">GetNodeRequest</span>{<span style="color:#a6e22e">NodeId</span>: <span style="color:#a6e22e">nodeID</span>})
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">st</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Convert</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">Code</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">NotFound</span>:
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// not an error, just missing</span>
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">DeadlineExceeded</span>:
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;node lookup timed out after 3s&#34;</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;node lookup failed: %s&#34;</span>, <span style="color:#a6e22e">st</span>.<span style="color:#a6e22e">Message</span>())
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">node</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Always set deadlines.</strong> Every single call. No exceptions. A call without a deadline is a goroutine leak waiting to happen. We had an incident where a downstream service hung and our caller accumulated ~40,000 goroutines because nobody set a timeout. The fix was one line. The outage was three hours.</p>
<p><strong>Handle status codes explicitly.</strong> Don&rsquo;t just check <code>err != nil</code>. A <code>NotFound</code> is fundamentally different from <code>Internal</code>. Your retry policy, your alerting, and your user-facing message all depend on the code. <code>status.Convert(err)</code> gives you the code. Use it.</p>
<p><strong>Retry with backoff, but only on the right codes.</strong> We retry on <code>Unavailable</code> and <code>DeadlineExceeded</code>. We do <em>not</em> retry on <code>InvalidArgument</code> or <code>NotFound</code>. Retrying a bad request is just a faster way to burn your quota.</p>
<h2 id="interceptors-the-grpc-middleware-pattern">Interceptors: the gRPC middleware pattern</h2>
<p>This is where gRPC shines versus REST. Interceptors are typed and composable, with separate hooks for unary and streaming RPCs. Here are our unary logging and recovery interceptors:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">loggingInterceptor</span>(
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">req</span> <span style="color:#66d9ef">interface</span>{},
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">info</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">UnaryServerInfo</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">handler</span> <span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">UnaryHandler</span>,
</span></span><span style="display:flex;"><span>) (<span style="color:#66d9ef">interface</span>{}, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">start</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">handler</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">duration</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Since</span>(<span style="color:#a6e22e">start</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">code</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">OK</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">code</span> = <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Code</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;method=%s code=%s duration=%s&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">info</span>.<span style="color:#a6e22e">FullMethod</span>, <span style="color:#a6e22e">code</span>, <span style="color:#a6e22e">duration</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">recoveryInterceptor</span>(
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">req</span> <span style="color:#66d9ef">interface</span>{},
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">info</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">UnaryServerInfo</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">handler</span> <span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">UnaryHandler</span>,
</span></span><span style="display:flex;"><span>) (<span style="color:#a6e22e">resp</span> <span style="color:#66d9ef">interface</span>{}, <span style="color:#a6e22e">err</span> <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">defer</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> recover(); <span style="color:#a6e22e">r</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;panic in %s: %v&#34;</span>, <span style="color:#a6e22e">info</span>.<span style="color:#a6e22e">FullMethod</span>, <span style="color:#a6e22e">r</span>)
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">Internal</span>, <span style="color:#e6db74">&#34;internal error&#34;</span>)
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>	}()
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">handler</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The recovery interceptor has saved us more than once. A nil pointer dereference in a handler used to crash the entire server. Now it returns <code>Internal</code> and logs the panic. The server stays up. Other RPCs keep working. We still fix the bug, but we don&rsquo;t page the on-call team at 3am for a panic in a non-critical endpoint.</p>
<h2 id="error-handling-the-part-everyone-gets-wrong">Error handling: the part everyone gets wrong</h2>
<p>I&rsquo;ve seen codebases where every gRPC error is <code>codes.Internal</code>. That&rsquo;s like every HTTP response being a 500. Useless for clients. Useless for monitoring.</p>
<p>Our rule is simple: pick the status code that tells the client what to <em>do</em>.</p>
<table>
  <thead>
      <tr>
          <th>Situation</th>
          <th>Code</th>
          <th>Client action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Bad input</td>
          <td><code>InvalidArgument</code></td>
          <td>Fix the request, don&rsquo;t retry</td>
      </tr>
      <tr>
          <td>Missing resource</td>
          <td><code>NotFound</code></td>
          <td>Don&rsquo;t retry, maybe create it</td>
      </tr>
      <tr>
          <td>Duplicate creation</td>
          <td><code>AlreadyExists</code></td>
          <td>Probably idempotent, check state</td>
      </tr>
      <tr>
          <td>Auth missing/expired</td>
          <td><code>Unauthenticated</code></td>
          <td>Re-authenticate</td>
      </tr>
      <tr>
          <td>Auth valid but insufficient</td>
          <td><code>PermissionDenied</code></td>
          <td>Don&rsquo;t retry, escalate</td>
      </tr>
      <tr>
          <td>Server overloaded</td>
          <td><code>Unavailable</code></td>
          <td>Retry with backoff</td>
      </tr>
      <tr>
          <td>Bug or unknown</td>
          <td><code>Internal</code></td>
          <td>Alert, escalate</td>
      </tr>
  </tbody>
</table>
<p><code>Internal</code> means &ldquo;the server has a bug.&rdquo; If you&rsquo;re returning <code>Internal</code> for a missing resource or bad input, you&rsquo;re lying to your monitoring system. Your alerts will fire for things that aren&rsquo;t server bugs. Alert fatigue follows.</p>
<h2 id="proto-evolution-the-non-obvious-rules">Proto evolution: the non-obvious rules</h2>
<p>Adding fields is safe. Removing them isn&rsquo;t. That much is obvious. The non-obvious part:</p>
<p><strong>Changing a field from <code>string</code> to <code>bytes</code> is wire-compatible but semantically different.</strong> We did this once with a field that held a UUID. Wire format was identical. But the generated Go code changed from <code>string</code> to <code>[]byte</code> and broke every caller at compile time. &ldquo;Wire compatible&rdquo; and &ldquo;API compatible&rdquo; are different things.</p>
<p><strong>Removing a field without <code>reserved</code> is a time bomb.</strong> Six months later, someone reuses that field number for a different type. Old clients that haven&rsquo;t recompiled send the old type. The new server interprets it as the new type. Data corruption that only manifests in production with old client versions. Good luck debugging that.</p>
<p><strong>Version at the package level.</strong> It&rsquo;s the same idea as  <a href="/blog/2020-02-03-api-versioning-strategies/"
   
   >URL path versioning for REST</a>
. When you need a breaking change:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-protobuf" data-lang="protobuf"><span style="display:flex;"><span><span style="color:#75715e">// Old clients still work
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#f92672">package</span> infra<span style="color:#f92672">.</span>nodes.v1;<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#75715e">// New clients use v2
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#f92672">package</span> infra<span style="color:#f92672">.</span>nodes.v2;<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Run both versions side by side. Migrate clients one at a time. Kill v1 when the last client is gone. We track this with a Grafana dashboard that shows request counts per proto package version.</p>
<h2 id="testing-skip-the-mocks">Testing: skip the mocks</h2>
<p>We don&rsquo;t mock gRPC clients. We spin up a real <code>grpc.Server</code> in tests with <code>bufconn</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">setupTest</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) <span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">NodeServiceClient</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Helper</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">lis</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">bufconn</span>.<span style="color:#a6e22e">Listen</span>(<span style="color:#ae81ff">1024</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">1024</span>)
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">srv</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">NewServer</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">RegisterNodeServiceServer</span>(<span style="color:#a6e22e">srv</span>, <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">nodeServer</span>{<span style="color:#a6e22e">store</span>: <span style="color:#a6e22e">NewMemoryStore</span>()})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">Serve</span>(<span style="color:#a6e22e">lis</span>) }()
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Cleanup</span>(<span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">srv</span>.<span style="color:#a6e22e">GracefulStop</span>() })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">DialContext</span>(
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">WithContextDialer</span>(<span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">_</span> <span style="color:#66d9ef">string</span>) (<span style="color:#a6e22e">net</span>.<span style="color:#a6e22e">Conn</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">lis</span>.<span style="color:#a6e22e">DialContext</span>(<span style="color:#a6e22e">ctx</span>)
</span></span><span style="display:flex;"><span>		}),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">WithInsecure</span>(),
</span></span><span style="display:flex;"><span>	)
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;dial bufconn: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Cleanup</span>(<span style="color:#66d9ef">func</span>() { <span style="color:#a6e22e">conn</span>.<span style="color:#a6e22e">Close</span>() })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">NewNodeServiceClient</span>(<span style="color:#a6e22e">conn</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">TestGetNode</span>(<span style="color:#a6e22e">t</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">T</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">client</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">setupTest</span>(<span style="color:#a6e22e">t</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Happy path</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">node</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">GetNode</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">GetNodeRequest</span>{<span style="color:#a6e22e">NodeId</span>: <span style="color:#e6db74">&#34;node-1&#34;</span>})
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Fatalf</span>(<span style="color:#e6db74">&#34;unexpected error: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">node</span>.<span style="color:#a6e22e">GetHostname</span>() <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;worker-01.example.com&#34;</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;hostname = %q, want %q&#34;</span>, <span style="color:#a6e22e">node</span>.<span style="color:#a6e22e">GetHostname</span>(), <span style="color:#e6db74">&#34;worker-01.example.com&#34;</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Not found</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">GetNode</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">GetNodeRequest</span>{<span style="color:#a6e22e">NodeId</span>: <span style="color:#e6db74">&#34;nonexistent&#34;</span>})
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Code</span>(<span style="color:#a6e22e">err</span>) <span style="color:#f92672">!=</span> <span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">NotFound</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;code = %v, want NotFound&#34;</span>, <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Code</span>(<span style="color:#a6e22e">err</span>))
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Validation</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">client</span>.<span style="color:#a6e22e">GetNode</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">pb</span>.<span style="color:#a6e22e">GetNodeRequest</span>{})
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Code</span>(<span style="color:#a6e22e">err</span>) <span style="color:#f92672">!=</span> <span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">InvalidArgument</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">t</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;code = %v, want InvalidArgument&#34;</span>, <span style="color:#a6e22e">status</span>.<span style="color:#a6e22e">Code</span>(<span style="color:#a6e22e">err</span>))
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>bufconn</code> gives you an in-memory listener. No ports, no flaky tests from port conflicts, no network overhead. But you still test the full gRPC stack: serialization, interceptors, status codes, deadlines. Mocking the client interface skips all of that. We found real bugs with <code>bufconn</code> that mocks would have hidden: serialization of <code>oneof</code> fields, deadline propagation through interceptors, and metadata handling.</p>
<h2 id="load-balancing-the-http2-gotcha">Load balancing: the HTTP/2 gotcha</h2>
<p>gRPC runs on HTTP/2, which multiplexes requests over a single TCP connection. This means traditional L4 load balancers don&rsquo;t work. All requests go to whichever backend received the connection. One hot server, N-1 idle servers.</p>
<p>We use client-side balancing with a service registry. The gRPC resolver API lets you plug in your own discovery:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">conn</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">Dial</span>(
</span></span><span style="display:flex;"><span>	<span style="color:#e6db74">&#34;dns:///nodes.example.internal:9090&#34;</span>,
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">WithTransportCredentials</span>(<span style="color:#a6e22e">loadTLSCredentials</span>()),
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">grpc</span>.<span style="color:#a6e22e">WithDefaultServiceConfig</span>(<span style="color:#e6db74">`{&#34;loadBalancingPolicy&#34;:&#34;round_robin&#34;}`</span>),
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>The <code>dns:///</code> scheme tells the resolver to use DNS. <code>round_robin</code> distributes across all A records. For us this works because we run headless Kubernetes services that return pod IPs.</p>
<p>If you&rsquo;re behind  <a href="/blog/2018-11-26-service-mesh-istio-practical-guide/"
   
   >Envoy or Istio</a>
, make sure they&rsquo;re configured for HTTP/2, not HTTP/1.1 upgrade. I&rsquo;ve seen service meshes silently downgrade gRPC to HTTP/1.1 and nobody notices until streaming breaks.</p>
<h2 id="what-id-do-differently">What I&rsquo;d do differently</h2>
<p>Eight months in, this is what I&rsquo;d change if we started over:</p>
<p><strong>Invest in proto linting from day one.</strong> We use <code>buf</code> now but adopted it late. Early protos have inconsistent naming, missing field reservations, and enum values that don&rsquo;t follow the <code>TYPE_NAME_VALUE</code> pattern. Fixing these in a live system is painful.</p>
<p><strong>Put <code>buf</code> next to <code>protoc</code> from the start.</strong> The <code>protoc</code> plugin ecosystem is a maze, and we still generate code with it. But <code>buf</code> handles linting and breaking change detection in one tool. Should have started there.</p>
<p><strong>Don&rsquo;t  <a href="/blog/2016-01-15-why-microservices-arent-always-the-answer/"
   
   >over-decompose services</a>
.</strong> Our first instinct was one proto per entity. Node service. Deployment service. Billing service. Network service. That&rsquo;s fine in theory. In practice, most operations touch three or four services, so every user-facing action became a cascade of RPCs. We&rsquo;ve since consolidated the ones that always move together.</p>
<p>gRPC is a great tool for internal APIs. Use it where it helps and REST where that&rsquo;s simpler.</p>
]]></content:encoded></item><item><title>VPN Overload in 2020: Why Scaling the VPN Is the Wrong Fix</title><link>https://lawzava.com/blog/2020-05-04-vpn-scaling-challenges/</link><pubDate>Mon, 04 May 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-05-04-vpn-scaling-challenges/</guid><description>COVID broke everyone&amp;amp;rsquo;s VPN. More licenses stop the bleeding and split tunneling buys time. The real fix is moving apps behind identity-based access.</description><content:encoded><![CDATA[<p>Your VPN is on fire. So was the one at a team I worked with this spring, and so was every other company&rsquo;s that treated VPN as its entire remote access strategy.</p>
<p>What happened: organizations sized their VPN for maybe 20% concurrent usage. Sales people on the road. The odd remote day. Then overnight, 100% of the workforce went remote and stayed there. Five times the sessions, five times the bandwidth, encryption overhead pegging every CPU on the gateway. Licensing servers falling over because nobody ever imagined needing that many concurrent seats.</p>
<p>I&rsquo;m not going to pretend this was some unforeseeable event. It wasn&rsquo;t. We all knew these VPN setups were fragile. We just got away with it because the failure condition never arrived. Until it did.</p>
<h2 id="everything-broke-at-once">Everything broke at once</h2>
<p>The fun part about VPN failures is how they cascade. It&rsquo;s never just one thing.</p>
<p>License checks hit a ceiling first. Then the gateway CPU maxes out from encryption overhead. Then someone notices that all SaaS traffic (Teams, Slack, G Suite, everything) is hairpinning through the data center because full-tunnel was the default. So now your WAN link is saturated too. Then RADIUS or your directory service starts timing out because it was never load-tested for this volume. Then MFA starts queuing.</p>
<p>Meanwhile, you have one gateway and one ISP, because &ldquo;we&rsquo;ll add redundancy next quarter&rdquo; has been on the roadmap for three years.</p>
<p>The help desk tickets pile up. Leadership asks why people can&rsquo;t work. You explain that the VPN was designed for a different world. Nobody wants to hear that.</p>
<h2 id="just-scale-the-vpn-is-the-wrong-answer">&ldquo;Just scale the VPN&rdquo; is the wrong answer</h2>
<p>This is where I get annoyed. The knee-jerk response I keep seeing is: buy more licenses, add more gateways, throw bandwidth at it. And yes, that stops the bleeding. We did it too. But if your takeaway from this crisis is &ldquo;we need a bigger VPN,&rdquo; you&rsquo;ve missed the point entirely.</p>
<p>VPNs grant network-level access. You connect, you&rsquo;re on the network, you can reach things. The implicit trust model is insane if you think about it for more than thirty seconds. An employee&rsquo;s compromised laptop gets VPN access and now it&rsquo;s sitting on your corporate network with lateral movement potential. That was always the problem. COVID just made it impossible to ignore because now every employee&rsquo;s home network is your attack surface. The same pressure is  <a href="/blog/2020-04-27-cloud-security-rapid-scaling/"
   
   >wrecking cloud security defaults</a>
 too.</p>
<p>The &ldquo;just scale it&rdquo; crowd is essentially saying: let&rsquo;s take this fundamentally flawed security model and make it bigger. No thanks.</p>
<h2 id="split-tunneling-buys-time-nothing-more">Split tunneling buys time, nothing more</h2>
<p>I&rsquo;ll give split tunneling credit for being the single most impactful quick fix. Stop routing SaaS traffic through your data center. Keep internal resources on the tunnel, let everything else go direct. VPN load drops dramatically.</p>
<p>But look at what you&rsquo;re doing. You&rsquo;re acknowledging that the VPN was never needed for half the traffic flowing through it. Which should make you ask: what else doesn&rsquo;t actually need to be on the VPN?</p>
<p>The answer, for most organizations, is almost everything. Most of your apps are already SaaS or could be accessed through an identity-aware proxy. The VPN is protecting a shrinking set of truly internal resources while adding overhead and fragility to everything else.</p>
<h2 id="where-this-actually-needs-to-go">Where this actually needs to go</h2>
<p> <a href="/blog/2018-02-19-zero-trust-security-architecture/"
   
   >Zero trust</a>
. Identity-based access. Whatever you want to call it, it&rsquo;s the model where you authenticate and authorize at the application layer instead of the network layer. Where a compromised device doesn&rsquo;t get you lateral movement because there&rsquo;s no implicit network trust to exploit.</p>
<p>I realize I sound like a vendor pitch right now, and I hate that. But the principle is sound even if the marketing around it&rsquo;s insufferable. Stop granting network access. Start granting application access. Verify the device, verify the identity, verify the context, grant access to exactly what&rsquo;s needed.</p>
<p>You don&rsquo;t have to rip out VPN tomorrow. That&rsquo;s unrealistic. But you should be moving your most critical applications behind identity-aware access right now, and treating VPN as the legacy fallback for the stuff you haven&rsquo;t migrated yet. Not the other way around.</p>
<h2 id="what-we-did-on-that-team">What we did on that team</h2>
<p>Short term, the same ugly stuff everyone else did. Activated emergency licenses. Deployed additional gateways. Enabled split tunneling with endpoint posture checks. Added a second ISP. Staggered login windows to smooth the peaks.</p>
<p>None of it was elegant. All of it was necessary.</p>
<p>But the real work started after the fires were out. Moving applications behind identity-based access. Treating VPN as a shrinking perimeter, not an expanding one. Setting up monitoring that warns you before users start complaining (session counts trending up, auth failure rates, gateway CPU) instead of post-mortem log analysis.</p>
<p>The goal is simple: every month, fewer things should require VPN. If that number isn&rsquo;t going down, you&rsquo;re just waiting for the next crisis to expose the same structural weakness all over again.</p>
]]></content:encoded></item><item><title>AWS Security While Scaling Fast: Make Secure the Default</title><link>https://lawzava.com/blog/2020-04-27-cloud-security-rapid-scaling/</link><pubDate>Mon, 27 Apr 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-04-27-cloud-security-rapid-scaling/</guid><description>Scaling AWS overnight? Manual security review collapses under that pressure. Secure defaults hold: IAM boundaries, private Terraform modules, GuardDuty.</description><content:encoded><![CDATA[<p>When you scale fast, security doesn&rsquo;t degrade gradually. It collapses. Lock down IAM with permissions boundaries, make everything private by default in Terraform modules, turn on CloudTrail/Config/GuardDuty before you add a single new service, and treat <code>0.0.0.0/0</code> in a security group like a production incident.</p>
<p>I&rsquo;m watching companies triple their AWS footprint in weeks right now. Entire engineering teams went remote overnight. New services are shipping without review. IAM policies are getting <code>*</code> slapped on them because someone needed to unblock a deploy at 11pm.</p>
<p>I&rsquo;ve seen this pattern before, in cyber-defense exercises rather than startups. The scenario is always the same: operational tempo increases, shortcuts happen, and adversaries walk through the gaps those shortcuts create. The difference is that in a cyber-defense exercise, you get a debrief. In production, you get a breach notification.</p>
<p>At the cloud infrastructure company I run, I build tooling for companies that are already scaling hard. The ones I&rsquo;ve seen get through it without security incidents made the secure path the easy path. Security team size has little to do with it. That&rsquo;s the entire thesis of this post.</p>
<h2 id="permissive-defaults-are-the-problem">Permissive Defaults Are the Problem</h2>
<p>Everyone wants to blame the pace. &ldquo;We&rsquo;re moving too fast to do security properly.&rdquo; Nonsense.</p>
<p>The problem is that most cloud environments are configured to be permissive by default. When things are calm, humans manually tighten things up. Reviews happen. Someone catches the overly broad IAM role before it ships. But when you&rsquo;re scaling fast, those manual catches disappear. And now you&rsquo;re running on whatever the defaults are.</p>
<p>If your defaults are permissive, you&rsquo;re exposed. Period.</p>
<p>I dealt with this at the fintech startup when we were scaling our data infrastructure. More process didn&rsquo;t fix it. Better defaults did.</p>
<h2 id="iam-where-most-breaches-actually-start">IAM: Where Most Breaches Actually Start</h2>
<p>I&rsquo;ll say it bluntly: if you&rsquo;re not using IAM permissions boundaries right now, you&rsquo;re gambling. Permissions boundaries are the ceiling on what any role can do, regardless of what policies are attached to it. They&rsquo;re the single most underused security control in AWS.</p>
<p>Here&rsquo;s a permissions boundary I enforce on every account that creates IAM roles:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;Version&#34;</span>: <span style="color:#e6db74">&#34;2012-10-17&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;Statement&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Sid&#34;</span>: <span style="color:#e6db74">&#34;AllowSpecificServices&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Effect&#34;</span>: <span style="color:#e6db74">&#34;Allow&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Action&#34;</span>: [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;s3:*&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;dynamodb:*&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;sqs:*&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;sns:*&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;logs:*&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;cloudwatch:*&#34;</span>
</span></span><span style="display:flex;"><span>      ],
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Resource&#34;</span>: <span style="color:#e6db74">&#34;*&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Sid&#34;</span>: <span style="color:#e6db74">&#34;DenyIAMEscalation&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Effect&#34;</span>: <span style="color:#e6db74">&#34;Deny&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Action&#34;</span>: [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iam:CreateUser&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iam:CreateRole&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iam:AttachRolePolicy&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iam:PutRolePolicy&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;iam:CreateAccessKey&#34;</span>
</span></span><span style="display:flex;"><span>      ],
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Resource&#34;</span>: <span style="color:#e6db74">&#34;*&#34;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Sid&#34;</span>: <span style="color:#e6db74">&#34;DenyOrganizationChanges&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Effect&#34;</span>: <span style="color:#e6db74">&#34;Deny&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Action&#34;</span>: <span style="color:#e6db74">&#34;organizations:*&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Resource&#34;</span>: <span style="color:#e6db74">&#34;*&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Even if a developer attaches <code>AdministratorAccess</code> to their role (and they will, at 11pm, to unblock a deploy), the permissions boundary prevents privilege escalation. They can&rsquo;t create new roles, attach policies, or generate access keys. The blast radius is capped.</p>
<p>A common shortcut is giving every Lambda execution role full S3 access because &ldquo;it&rsquo;s easier.&rdquo; Then a single compromised function can read every bucket in the account. Permissions boundaries would contain that to the specific service&rsquo;s buckets.</p>
<h2 id="security-groups-the-00000-problem">Security Groups: The <code>0.0.0.0/0</code> Problem</h2>
<p>I run a script on every new project that searches for <code>0.0.0.0/0</code> in security group ingress rules. It always finds something. Always.</p>
<p>Here&rsquo;s the Terraform pattern I enforce in  <a href="/blog/2019-09-23-terraform-at-scale/"
   
   >shared modules</a>
:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group&#34; &#34;app&#34;</span> {
</span></span><span style="display:flex;"><span>  name_prefix <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;app-&#34;</span>
</span></span><span style="display:flex;"><span>  vpc_id      <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">vpc_id</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # No inline ingress rules. Force explicit definition.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # This is intentional: default deny.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">egress</span> {
</span></span><span style="display:flex;"><span>    from_port   <span style="color:#f92672">=</span> <span style="color:#ae81ff">443</span>
</span></span><span style="display:flex;"><span>    to_port     <span style="color:#f92672">=</span> <span style="color:#ae81ff">443</span>
</span></span><span style="display:flex;"><span>    protocol    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
</span></span><span style="display:flex;"><span>    cidr_blocks <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>]
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HTTPS outbound only&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">lifecycle</span> {
</span></span><span style="display:flex;"><span>    create_before_destroy <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  tags <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    ManagedBy <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform&#34;</span>
</span></span><span style="display:flex;"><span>    Review    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;required&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}<span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Separate rule resources: auditable, reviewable, deletable
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_security_group_rule&#34; &#34;app_from_alb&#34;</span> {
</span></span><span style="display:flex;"><span>  type                     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ingress&#34;</span>
</span></span><span style="display:flex;"><span>  from_port                <span style="color:#f92672">=</span> <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>  to_port                  <span style="color:#f92672">=</span> <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>  protocol                 <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tcp&#34;</span>
</span></span><span style="display:flex;"><span>  source_security_group_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">alb</span>.<span style="color:#66d9ef">id</span>
</span></span><span style="display:flex;"><span>  security_group_id        <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">app</span>.<span style="color:#66d9ef">id</span>
</span></span><span style="display:flex;"><span>  description              <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ALB to app on 8080&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Notice: no ingress defined inline. Every ingress rule is a separate <code>aws_security_group_rule</code> resource with a <code>description</code> field. When someone adds a rule, it shows up clearly in the Terraform plan. When you need to audit, you can grep your codebase for <code>aws_security_group_rule</code> and get a complete inventory. Try doing that with inline rules.</p>
<p>This pattern came directly from a cyber-defense exercise where we had to rapidly audit network access during a simulated attack. The team that had structured, auditable firewall rules found the compromised path in minutes. The team with ad-hoc rules was still mapping their network topology when the exercise ended.</p>
<h2 id="make-encryption-non-negotiable">Make Encryption Non-Negotiable</h2>
<p>Every data store module should enforce encryption. Not suggest it. Enforce it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_db_instance&#34; &#34;main&#34;</span> {
</span></span><span style="display:flex;"><span>  publicly_accessible    <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  storage_encrypted      <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  deletion_protection    <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  kms_key_id             <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">kms_key_arn</span>
</span></span><span style="display:flex;"><span>  iam_database_authentication_enabled <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # No default security group: force explicit assignment
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  vpc_security_group_ids <span style="color:#f92672">=</span> [<span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">db_security_group_id</span>]
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_s3_bucket&#34; &#34;data&#34;</span> {
</span></span><span style="display:flex;"><span>  bucket_prefix <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;app-data-&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">server_side_encryption_configuration</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">rule</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">apply_server_side_encryption_by_default</span> {
</span></span><span style="display:flex;"><span>        sse_algorithm     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;aws:kms&#34;</span>
</span></span><span style="display:flex;"><span>        kms_master_key_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">kms_key_arn</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_s3_bucket_public_access_block&#34; &#34;data&#34;</span> {
</span></span><span style="display:flex;"><span>  bucket <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_s3_bucket</span>.<span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">id</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  block_public_acls       <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  block_public_policy     <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  ignore_public_acls      <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  restrict_public_buckets <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The S3 public access block is the one I see missing most often. People enable encryption but forget to block public access at the bucket level. Then someone uploads an object with a public ACL and you&rsquo;re in the news.</p>
<h2 id="detection-turn-it-on-before-you-scale">Detection: Turn It On Before You Scale</h2>
<p>This is the mistake I see repeatedly. Teams add services, then plan to &ldquo;add monitoring later.&rdquo; Later never comes.</p>
<p>Before you provision a single new service, these should be running across every account in your organization:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># AWS Config rules: deploy via StackSets across all accounts</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">config_rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">s3-bucket-public-read-prohibited</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">s3-bucket-public-write-prohibited</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">encrypted-volumes</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">cloudtrail-enabled</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">root-account-mfa-enabled</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">iam-user-no-policies-check</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">iam-root-access-key-check</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">restricted-ssh</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">vpc-flow-logs-enabled</span>
</span></span></code></pre></div><p>CloudTrail, Config, and GuardDuty. All three. Non-negotiable. GuardDuty in particular has gotten good enough that it catches IAM anomalies (unusual API calls, credential usage from unexpected IPs) with minimal tuning.</p>
<p>The trick is filtering. GuardDuty will generate findings. Most of them are low severity. Set up an SNS topic that only fires on HIGH and CRITICAL findings. Pipe those to a Slack channel or PagerDuty. Everything else goes to a weekly review.</p>
<p>Treat this as a baseline for every account. The teams that actually review the weekly findings catch configuration drift before it becomes exploitable. The teams that ignore them lose that early warning.</p>
<h2 id="remote-access-kill-the-vpn-use-session-manager">Remote Access: Kill the VPN, Use Session Manager</h2>
<p>Everyone&rsquo;s spinning up VPNs right now. I get it. But VPNs are a liability. They&rsquo;re a single point of trust. Once you&rsquo;re on the VPN, you&rsquo;re &ldquo;inside.&rdquo; That model is broken.</p>
<p>For AWS access, use SSM Session Manager instead. It gives you shell access to EC2 instances through the AWS API. No SSH keys to manage. No inbound security group rules. Full session logging to CloudTrail and S3.</p>
<p>For application access, put services behind an identity-aware proxy. Verify identity and device state on every request. The network boundary isn&rsquo;t your security boundary. That&rsquo;s the core of  <a href="/blog/2018-02-19-zero-trust-security-architecture/"
   
   >zero trust</a>
.</p>
<p>I learned this the hard way during a cyber-defense exercise. The red team&rsquo;s first move was always to compromise VPN credentials. Once inside, they had lateral movement across the entire network. The teams that had segmented access (per-service authentication, no implicit trust from network position) held up. The teams running flat networks behind a VPN got owned.</p>
<h2 id="the-seven-item-checklist">The Seven-Item Checklist</h2>
<p>I keep this short because nobody follows long checklists under pressure.</p>
<ol>
<li><strong>IAM permissions boundaries</strong> on every account that creates roles. Deploy today.</li>
<li><strong>S3 public access blocks</strong> on every bucket. <code>aws s3api put-public-access-block</code> across all accounts. Takes five minutes.</li>
<li><strong>Security groups</strong>: audit for <code>0.0.0.0/0</code> ingress. Remove or justify every one.</li>
<li><strong>CloudTrail + Config + GuardDuty</strong>: enabled in every account, every region. Use AWS Organizations and StackSets.</li>
<li><strong>Encryption by default</strong>: KMS keys provisioned, Terraform modules enforce <code>storage_encrypted = true</code> and <code>publicly_accessible = false</code>.</li>
<li><strong>SSM Session Manager</strong> instead of SSH. Kill inbound port 22.</li>
<li><strong>Weekly access review</strong>: who has access to what, and why. Remove what&rsquo;s not justified.</li>
</ol>
<p>That&rsquo;s it. Seven items. You can get through most of them in a day if you&rsquo;re motivated.</p>
<h2 id="stop-treating-security-as-a-phase">Stop Treating Security as a Phase</h2>
<p>The framing I keep hearing is &ldquo;we&rsquo;ll fix security after we stabilize.&rdquo; That&rsquo;s backwards. Security is a property of how you scale, and you don&rsquo;t get to it afterwards.</p>
<p>If your Terraform modules are secure by default, every new service you deploy is secure. If your IAM boundaries are in place, every new role is constrained. If your detection is running, every new account is monitored.</p>
<p>The work is in the defaults. Get those right and scaling fast stays scaling fast instead of scaling into a breach.</p>
]]></content:encoded></item><item><title>Scaling WebRTC Video: SFUs, Simulcast, and TURN Costs</title><link>https://lawzava.com/blog/2020-03-30-scaling-video-infrastructure/</link><pubDate>Mon, 30 Mar 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-03-30-scaling-video-infrastructure/</guid><description>Most companies building video calling are making the same architecture mistakes. What I keep seeing, and how to fix it before your SFUs fall over.</description><content:encoded><![CDATA[<p>SFUs beat MCUs for 90% of video use cases. Simulcast is non-negotiable. Your TURN bill will surprise you. And if you&rsquo;re not measuring time-to-first-media, you&rsquo;re flying blind.</p>
<p>Two weeks ago, someone called me in a panic. Their video platform, built for 10,000 concurrent users, was handling 80,000. SFUs at 94% CPU. TURN servers hemorrhaging bandwidth. The whole thing held together by one engineer who hadn&rsquo;t slept in three days.</p>
<p>This is March 2020. I&rsquo;ve had four conversations like this in the last ten days. The pattern is always the same: the architecture was fine for the old world, and the old world ended overnight.</p>
<p>What follows is what I keep telling these teams: the decisions that matter when your video infra is on fire.</p>
<h2 id="video-isnt-http">Video Isn&rsquo;t HTTP</h2>
<p>I keep meeting backend engineers who treat video like a REST API with bigger payloads. It&rsquo;s not.</p>
<p>A video call is a continuous bidirectional stream that has to feel natural to a human brain. That brain notices latency above ~150ms. It notices audio glitches before video glitches. It definitely notices when your echo cancellation is broken and everyone hears themselves with a half-second delay.</p>
<ul>
<li><strong>Latency under 150ms</strong> or the conversation feels wrong. People start talking over each other.</li>
<li><strong>Audio is sacred.</strong> Users tolerate blurry video for minutes. They hang up after five seconds of choppy audio.</li>
<li><strong>NAT traversal is the norm.</strong> Corporate firewalls, hotel wifi, mobile networks: assume the worst.</li>
<li><strong>Packet loss kills you silently.</strong> Bandwidth can look fine while jitter makes the whole thing unwatchable.</li>
</ul>
<h2 id="p2p-sfu-mcu-pick-the-right-one">P2P, SFU, MCU: Pick the Right One</h2>
<p>I see teams agonize over this choice like it&rsquo;s a religious decision. It shouldn&rsquo;t be.</p>
<p><strong>P2P</strong> works for 2-4 people. Direct connection, lowest latency, almost no server cost. The catch: every participant uploads their stream to every other participant. Past 4-5 people, uplinks die.</p>
<p><strong>SFU (Selective Forwarding Unit)</strong> is the right answer for almost everything else. It receives streams and forwards them selectively. No mixing, no transcoding on the server. This is what Zoom, Google Meet, and basically every serious platform runs.</p>
<p><strong>MCU (Multipoint Control Unit)</strong> mixes everything into a single composed stream per participant. Sounds nice. In practice, the server-side CPU and GPU cost is brutal. Only for very low-capability clients or massive broadcast rooms.</p>
<p>Most production systems are hybrid. One system I worked on ran P2P for 1:1 calls and SFU for everything larger. Simple rule, huge savings.</p>
<p>The rough guidelines I give every team:</p>
<ul>
<li><strong>2-4 people:</strong> P2P. Don&rsquo;t overthink it.</li>
<li><strong>5-20 people:</strong> SFU with simulcast.</li>
<li><strong>20+ people:</strong> SFU with aggressive active-speaker optimization.</li>
<li><strong>Webinars / broadcasts:</strong> SFU for the audience, dedicated pipeline for presenters.</li>
</ul>
<h2 id="scaling-sfus-without-losing-your-mind">Scaling SFUs Without Losing Your Mind</h2>
<p>SFUs scale horizontally in theory. In practice, calls are stateful. Every participant in a room has to land on the same SFU instance. This isn&rsquo;t a stateless web tier you can throw behind a load balancer.</p>
<p><strong>Session stickiness</strong> first. Your load balancer needs to understand rooms, not just connections. Get this wrong and you&rsquo;ll spend days debugging &ldquo;ghost participants&rdquo; and one-way audio.</p>
<p><strong>Room size caps</strong> are a safety valve. One team removed their 50-person cap &ldquo;because sales wanted it&rdquo; and promptly discovered a single 200-person room could saturate an SFU. Cap it. Enforce it. Explain to sales later.</p>
<p><strong>Cascading SFUs</strong> across regions is how you handle  <a href="/blog/2017-10-02-building-multi-region-applications/"
   
   >distributed calls</a>
 without routing Sydney&rsquo;s traffic through Virginia. Each region gets its own SFU. They talk over backbone links. Users connect locally.</p>
<pre tabindex="0"><code>Sydney user -&gt; Sydney SFU &lt;--backbone--&gt; London SFU &lt;- London user
</code></pre><p>This keeps last-mile latency low and contains failure domains. When the Sydney SFU has issues, London keeps running.</p>
<h2 id="simulcast-is-non-negotiable">Simulcast Is Non-Negotiable</h2>
<p>If you&rsquo;re running an SFU without simulcast, stop reading and go implement it. I&rsquo;m serious.</p>
<p>Clients send multiple quality layers simultaneously:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">simulcast</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">high</span>: <span style="color:#ae81ff">1280x720 @ 30fps, ~1500kbps</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mid</span>:  <span style="color:#ae81ff">640x360 @ 30fps, ~700kbps</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">low</span>:  <span style="color:#ae81ff">320x180 @ 15fps, ~200kbps</span>
</span></span></code></pre></div><p>The SFU forwards the appropriate layer per receiver based on bandwidth and screen size. Active speaker gets high quality. Thumbnail tiles get low. Someone on hotel wifi gets mid or low for everything.</p>
<p>Without simulcast, you&rsquo;re either crushing constrained clients with full-resolution streams or wasting bandwidth by downscaling everything. No middle ground.</p>
<p>Pair simulcast with adaptive bitrate control. When the SFU detects rising packet loss or sustained jitter, it signals the sender to drop resolution. Fastest lever you have to stabilize a degrading call.</p>
<h2 id="the-turn-server-problem">The TURN Server Problem</h2>
<p>TURN is the thing nobody budgets for and everybody needs.</p>
<p>STUN lets peers discover their public IP and try a direct connection. When that fails (corporate firewall, symmetric NAT, hotel wifi blocking UDP), you need TURN. TURN relays all media through your server. Every stream hits your infra twice (in and out). At scale, it becomes your single largest cost.</p>
<p>I worked with a team whose TURN relay ratio was 40%. Their bandwidth bill was obscene. We got it down to 15% by improving ICE candidate gathering, adding more TURN regions, and implementing better network detection on the client side.</p>
<p>Monitor your TURN relay ratio religiously. Anything above 20% means something is wrong with your connectivity story. More TURN regions with latency-aware routing helps. Anycast or geo-DNS for region selection is table stakes.</p>
<h2 id="what-to-actually-monitor">What to Actually Monitor</h2>
<p>I&rsquo;ve walked into too many video platforms where the dashboard shows CPU, memory, and &ldquo;active calls.&rdquo; That tells you nothing about whether calls are good.</p>
<p>The metrics I insist on:</p>
<p><strong>Time to first media.</strong> How long from clicking &ldquo;join&rdquo; until the user sees/hears someone? This is the single most important UX metric. If it&rsquo;s over 3 seconds, people think it&rsquo;s broken.</p>
<p><strong>Join success rate.</strong> What percentage of join attempts actually result in a connected call? I&rsquo;ve seen rates as low as 85%. That&rsquo;s 15% of users clicking join and getting nothing.</p>
<p><strong>Packet loss and jitter per stream.</strong> Not aggregated across the SFU. Per stream. You need to know which participants are suffering.</p>
<p><strong>TURN relay ratio.</strong> Track it. Trend it. Freak out if it&rsquo;s climbing.</p>
<p><strong>SFU CPU and egress bandwidth.</strong> Per instance, not averaged. One overloaded SFU ruins everyone on it while the average looks fine.</p>
<h2 id="operating-it-under-pressure">Operating It Under Pressure</h2>
<p>The architecture is the easy part. The hard part is operating it under pressure.</p>
<p>You need admission control. When an SFU is at 80% CPU, stop putting new rooms on it. Almost nobody does this until after their first outage.</p>
<p>You need  <a href="/blog/2019-05-06-designing-for-failure/"
   
   >graceful degradation</a>
. When things get bad, drop to audio-only automatically. A stable voice call beats a stuttering video call every time.</p>
<p>You need fast regional failover. When a region goes down, redirect new joins to the nearest healthy region. Don&rsquo;t wait for someone to notice at 2 AM.</p>
<p>And you need someone who owns this. Not &ldquo;the platform team&rdquo; collectively. A specific person who gets paged when quality drops. The team I mentioned at the start had a bigger ownership problem than an architecture problem: nobody owned the system end-to-end until it was already on fire.</p>
<h2 id="where-this-goes">Where This Goes</h2>
<p>We&rsquo;re two weeks into what I think is a permanent shift. The companies that figure out SFU scaling, simulcast, and TURN optimization now will have a massive advantage.</p>
<p>The ones still treating video as a feature rather than infrastructure will keep making panicked calls at 11 PM.</p>
]]></content:encoded></item><item><title>WebAssembly Outside the Browser: Real Promise, Real Gaps</title><link>https://lawzava.com/blog/2020-03-02-webassembly-beyond-browser/</link><pubDate>Mon, 02 Mar 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-03-02-webassembly-beyond-browser/</guid><description>WebAssembly outside the browser is interesting for edge, plugins, and sandboxing. But WASI and the tooling gaps are bigger than the hype admits.</description><content:encoded><![CDATA[<p>Everyone keeps telling me WebAssembly is going to replace containers. I don&rsquo;t buy it. Not yet. But I&rsquo;m paying attention, because the parts that work already work really well.</p>
<h2 id="what-actually-makes-sense">What actually makes sense</h2>
<p>Wasm outside the browser has a few properties that are hard to get anywhere else. Near-instant cold starts. True sandboxing where the host decides exactly what the guest can touch. Tiny binaries you can ship anywhere. If you&rsquo;re running short-lived functions at the edge or loading untrusted plugin code, these properties matter more than anything Docker gives you.</p>
<p> <a href="/blog/2019-11-18-edge-computing-architecture/"
   
   >Edge computing</a>
 is the obvious winner here. Cloudflare Workers already proved the model: run user code in tight sandboxes, spin up fast, tear down fast. Wasm formalizes that. Plugin systems are the other sweet spot. I&rsquo;ve dealt with enough &ldquo;just run their code in a subprocess&rdquo; architectures to appreciate a sandbox that doesn&rsquo;t require a container per tenant.</p>
<h2 id="the-gaps-wasi-threads-go-support">The gaps: WASI, threads, Go support</h2>
<p>WASI networking is basically nonexistent right now. You can read files if the host lets you. That&rsquo;s about it. Threading support is patchy. Debugging is painful. If your service needs to open a socket or talk to a database, you&rsquo;re going to have a bad time.</p>
<p>I&rsquo;ve been following the Go side of this closely. Go can compile to Wasm, but only for the browser, and the binaries are big. TinyGo gets you much smaller binaries, but it has no WASI target yet, and it&rsquo;s a subset of Go. Not all standard library packages work. Reflection is limited. You hit walls fast.  <a href="/blog/2018-03-05-rust-for-backend-services/"
   
   >Rust</a>
 is in a much better position here: it already has a WASI target and the tooling is good. Go&rsquo;s story will get there, but we&rsquo;re not there yet.</p>
<h2 id="the-hype-im-skeptical-of">The hype I&rsquo;m skeptical of</h2>
<p>Blockchain smart contracts in Wasm. Replacing Kubernetes with Wasm runtimes. Running your entire backend as Wasm modules. These are conference talk ideas, not production architectures. The gap between &ldquo;cool demo&rdquo; and &ldquo;thing I&rsquo;d run at 3am&rdquo; is enormous.</p>
<p>I watched the same pattern play out with microservices. The concept was sound. The early adopters built real things. Then everyone decided they needed it for their CRUD app and created distributed monoliths. Wasm outside the browser is at the &ldquo;concept is sound&rdquo; stage. Let&rsquo;s not skip ahead.</p>
<h2 id="what-im-actually-doing-with-it">What I&rsquo;m actually doing with it</h2>
<p>Experimenting. I compiled a small data transformation service to Wasm and ran it in Wasmtime. Cold start was impressive: under a millisecond. Binary was tiny. But I hit the WASI wall the moment I needed HTTP. Ended up passing data through stdin/stdout like it was 1985.</p>
<p>The capability-based security model is the part I keep coming back to. A process that can&rsquo;t access anything you didn&rsquo;t hand it beats one you trust not to escape. That&rsquo;s a better model for multi-tenant anything.</p>
<h2 id="when-id-use-it">When I&rsquo;d use it</h2>
<p>For edge functions, plugin systems, and sandboxed execution, Wasm outside the browser is usable today. For everything else, it&rsquo;s a promising prototype. I&rsquo;d start experimenting now, but I wouldn&rsquo;t migrate anything critical until WASI networking lands and the tooling matures. That&rsquo;s not happening next quarter.</p>
]]></content:encoded></item><item><title>Terraform Testing: Static Checks, Policy, and Integration</title><link>https://lawzava.com/blog/2020-02-17-infrastructure-testing-patterns/</link><pubDate>Mon, 17 Feb 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-02-17-infrastructure-testing-patterns/</guid><description>I tested Terraform modules with static checks, policy engines, and integration runs side by side: what each layer catches, what it misses, when to run it.</description><content:encoded><![CDATA[<p>Most infra teams either test nothing or test everything the slow way. The sweet spot is layering three approaches (static/unit, policy, and integration) and knowing which one catches which class of bug.</p>
<p>I broke production with a Terraform typo last year. Changed a security group rule, fat-fingered a CIDR block, and opened port 22 to the internet for about forty minutes. Nobody caught it in review. Our CI pipeline at the time was <code>terraform plan</code> and vibes.</p>
<p>That was the wake-up call. I spent the next six months building out infrastructure testing at my infrastructure startup, trying every approach I could find. Unit tests for IaC. Policy engines. Full integration runs with real resources. Some of it was worth the effort. Some wasn&rsquo;t.</p>
<h2 id="three-approaches-three-different-jobs">Three Approaches, Three Different Jobs</h2>
<p>The mistake I see most teams make is treating &ldquo;infrastructure testing&rdquo; as one thing. There are three fundamentally different approaches, and they catch fundamentally different problems.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th><strong>Static / Unit Tests</strong></th>
          <th><strong>Policy / Compliance Tests</strong></th>
          <th><strong>Integration Tests</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>What it checks</strong></td>
          <td>Syntax, structure, basic correctness</td>
          <td>Security rules, org standards, regulatory requirements</td>
          <td>Actual deployed resources behaving correctly</td>
      </tr>
      <tr>
          <td><strong>Speed</strong></td>
          <td>Seconds</td>
          <td>Seconds to low minutes</td>
          <td>5-30 minutes</td>
      </tr>
      <tr>
          <td><strong>Cost</strong></td>
          <td>Zero (no resources created)</td>
          <td>Zero (evaluates plans/manifests)</td>
          <td>Real money (spins up real infra)</td>
      </tr>
      <tr>
          <td><strong>Catches</strong></td>
          <td>Typos, invalid configs, format drift</td>
          <td>Policy violations, misconfigs that scanners know about</td>
          <td>Networking issues, IAM problems, broken dependencies</td>
      </tr>
      <tr>
          <td><strong>Misses</strong></td>
          <td>Anything behavioral</td>
          <td>Novel misconfigs, runtime behavior</td>
          <td>Little, but too slow and expensive to run on every commit</td>
      </tr>
      <tr>
          <td><strong>Run when</strong></td>
          <td>Every commit, pre-commit hooks</td>
          <td>Every PR, every <code>terraform plan</code></td>
          <td>Nightly, or on merges to main</td>
      </tr>
      <tr>
          <td><strong>Tools</strong></td>
          <td><code>terraform validate</code>, <code>tflint</code>, <code>kubeval</code></td>
          <td>OPA/Conftest, Checkov, tfsec</td>
          <td>Terratest, Kitchen-Terraform</td>
      </tr>
  </tbody>
</table>
<p>That table is the whole thesis. Each layer fills in what the previous one misses.</p>
<h2 id="static-and-unit-tests-the-fast-floor">Static and Unit Tests: The Fast Floor</h2>
<p>These are your <code>terraform fmt</code>, <code>terraform validate</code>, <code>tflint</code>, <code>kubeval</code> checks. They run in seconds. They cost nothing. They should be on every single commit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>terraform fmt -check -recursive
</span></span><span style="display:flex;"><span>terraform validate
</span></span><span style="display:flex;"><span>tflint terraform/
</span></span><span style="display:flex;"><span>kubeval --strict k8s/*.yaml
</span></span></code></pre></div><p>What surprised me: these catch more than you&rsquo;d think. At my infrastructure startup, roughly 40% of our infra PRs had at least one issue caught at this layer. Mostly formatting, occasionally an actual invalid reference or deprecated resource argument.</p>
<p>What they won&rsquo;t catch: anything that&rsquo;s syntactically valid but semantically wrong. That security group I opened to the world? Perfectly valid HCL. <code>terraform validate</code> had no opinion.</p>
<h2 id="policy-tests-where-the-real-value-is">Policy Tests: Where the Real Value Is</h2>
<p>This is the layer I underestimated. OPA with Conftest lets you write rules against Terraform plan JSON, Kubernetes manifests, basically any structured config.</p>
<p>The SSH rule that would have saved me forty minutes of panic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-rego" data-lang="rego"><span style="display:flex;"><span><span style="color:#66d9ef">package</span> <span style="color:#a6e22e">terraform</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>deny[<span style="color:#a6e22e">msg</span>] {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">r</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">input</span><span style="color:#f92672">.</span><span style="color:#a6e22e">resource_changes</span>[<span style="color:#a6e22e">_</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">r</span><span style="color:#f92672">.</span><span style="color:#a6e22e">type</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;aws_security_group_rule&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">r</span><span style="color:#f92672">.</span><span style="color:#a6e22e">change</span><span style="color:#f92672">.</span><span style="color:#a6e22e">after</span><span style="color:#f92672">.</span><span style="color:#a6e22e">cidr_blocks</span>[<span style="color:#a6e22e">_</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;0.0.0.0/0&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">r</span><span style="color:#f92672">.</span><span style="color:#a6e22e">change</span><span style="color:#f92672">.</span><span style="color:#a6e22e">after</span><span style="color:#f92672">.</span><span style="color:#a6e22e">from_port</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">22</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">msg</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">&#34;SSH must not be open to the world&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Small. Specific. Tied to an actual incident. That&rsquo;s the pattern.</p>
<p>I tried building comprehensive policy libraries. Dozens of rules covering every possible misconfiguration. Bad idea. Teams bypass large generic policy sets. They add exceptions, disable checks, or just stop running them. The policies that stick are the ones written after something went wrong. Each one represents a scar.</p>
<p>We also run Checkov as a complement. It only came out in December, but its list of built-in checks for common misconfigs is growing fast. Good for catching things you haven&rsquo;t been burned by yet. But it&rsquo;s a floor, not a ceiling.</p>
<p><strong>My rule of thumb:</strong> if a scanner catches it, great. If it doesn&rsquo;t, and it bit you, write a policy for it. Your policy library should grow from incidents, not from a wish list.</p>
<h2 id="integration-tests-expensive-and-worth-it-sometimes">Integration Tests: Expensive and Worth It (Sometimes)</h2>
<p>Integration tests create real AWS resources, run assertions against them, then tear everything down. Terratest is what we use. Go-based, works well with our existing stack.</p>
<p>These are the only tests that catch networking and IAM problems. You can validate a Terraform plan all day and it won&rsquo;t tell you that your NAT gateway routing is broken or that your RDS instance can&rsquo;t actually reach the VPC endpoint it needs.</p>
<p>But they&rsquo;re slow. Our VPC module integration test takes about twelve minutes. Our EKS test takes twenty-five. And they cost money. Not a lot per run, but it adds up if you&rsquo;re running them on every PR.</p>
<table>
  <thead>
      <tr>
          <th><strong>What we integration test</strong></th>
          <th><strong>Runtime</strong></th>
          <th><strong>Why</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>VPC module (routing, NAT, DNS)</td>
          <td>~12 min</td>
          <td>Networking bugs are invisible until deploy</td>
      </tr>
      <tr>
          <td>EKS cluster (deploy, healthcheck)</td>
          <td>~25 min</td>
          <td>Too many moving parts to validate statically</td>
      </tr>
      <tr>
          <td>RDS module (connectivity, encryption)</td>
          <td>~8 min</td>
          <td>Encryption settings are easy to get wrong</td>
      </tr>
  </tbody>
</table>
<p>We don&rsquo;t integration test every module. That&rsquo;s the trap. Test the ones where a static check can&rsquo;t tell you if it works. Our S3 bucket module? Unit tests and policy checks are enough. Our networking module? Needs the real thing.</p>
<p><strong>The cleanup problem</strong> is real. Terratest has <code>defer terraform.Destroy()</code> but it&rsquo;s not bulletproof. We&rsquo;ve had orphaned resources from failed tests sit around for weeks. I wrote a nightly cleanup job that tags test resources and nukes anything older than 24 hours. Unsexy but necessary.</p>
<h2 id="how-we-wire-it-together">How We Wire It Together</h2>
<p>Our CI pipeline at my infrastructure startup:</p>
<ol>
<li><strong>Pre-commit hooks</strong>: <code>terraform fmt</code>, basic linting. Runs locally in seconds.</li>
<li><strong>PR checks</strong>: <code>terraform validate</code>, <code>tflint</code>, Conftest policies, Checkov. Blocks merge on failure. Takes about 90 seconds.</li>
<li><strong>Nightly on main</strong>: Integration tests for critical modules. Results posted to Slack. Takes about 45 minutes total.</li>
<li><strong>Post-deploy</strong>: Smoke tests hitting real endpoints. Health checks, latency thresholds, basic connectivity. Alerts on failure.</li>
</ol>
<p>Fast checks on every commit, expensive checks on a schedule. That split is what makes the  <a href="/blog/2019-09-23-terraform-at-scale/"
   
   >pipeline scale with the codebase</a>
. I&rsquo;ve seen teams try to run Terratest on every PR and give up within a month because the feedback loop is too slow.</p>
<h2 id="practical-stuff-that-matters">Practical Stuff That Matters</h2>
<p>A few things I wish someone had told me six months ago:</p>
<ul>
<li><strong>Tag everything</strong> with a test identifier. When tests fail mid-run and leave resources behind, you need to find them.</li>
<li><strong>Set hard cost limits.</strong> We capped our test AWS account at $200/month. Hit it once. Fixed the test that was spinning up m5.4xlarge instances.</li>
<li><strong>Never put real credentials in test fixtures.</strong> Sounds obvious. I&rsquo;ve reviewed code where people did it anyway.</li>
<li><strong>Store plan outputs.</strong> When something breaks in production, being able to diff against the last tested plan is invaluable.</li>
</ul>
<h2 id="where-we-stand-after-six-months">Where We Stand After Six Months</h2>
<p>Static checks are table stakes. There&rsquo;s no excuse not to have them. Policy tests are the highest ROI layer and the one most teams skip. Integration tests are worth it for complex modules but not for everything.</p>
<p>If I were starting from scratch tomorrow, I&rsquo;d add static checks on day one, write my first policy rule on day two after reading our incident history, and add integration tests only for the modules that scare me.</p>
<p>Perfect coverage was never the goal. The goal is knowing that the categories of mistakes that have hurt you before won&rsquo;t hurt you again.</p>
]]></content:encoded></item><item><title>2019 in Review: The Year I Quit, Built, and Started Over</title><link>https://lawzava.com/blog/2019-12-16-year-in-review-2019/</link><pubDate>Mon, 16 Dec 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-12-16-year-in-review-2019/</guid><description>Leaving the fintech startup, joining a founder program, starting a cloud infrastructure company, and every lesson the year threw at me.</description><content:encoded><![CDATA[<p>I don&rsquo;t usually do year-in-review posts. They tend to read like LinkedIn humble-brags. But 2019 genuinely changed the trajectory of what I&rsquo;m doing, and I want to write it down while the details are still sharp. I left my job at a fintech startup, joined an intense founder program, started a company, and learned that building something from zero is a different skill from scaling something that already exists.</p>
<h2 id="leaving-the-fintech-startup">Leaving the fintech startup</h2>
<p>I spent a good stretch at a fintech startup. Good team. Interesting problems. We built systems that handled enormous volumes of financial data with tight latency requirements. I learned a lot about what it means to run infrastructure that can&rsquo;t go down. When money is involved, a 99.95% uptime target stops being an academic exercise and starts being your weekend.</p>
<p>But by early 2019, I knew I wanted to build something of my own. Not because the fintech startup was the wrong place. It was the right place for the stage I was in. The itch was something else: I wanted to own the problem, not just the solution.</p>
<p>So I left.</p>
<h2 id="joining-a-founder-program">Joining a founder program</h2>
<p>I joined a founder program&rsquo;s cohort this year. The expectation is that you find a co-founder and build a company. In weeks.</p>
<p>It&rsquo;s as intense as it sounds.</p>
<p>The previous year I&rsquo;d gone through  <a href="/blog/2018-12-24-year-in-review-2018-tech/"
   
   >a startup accelerator</a>
, which was a great experience but structured differently. The accelerator gives you resources and mentorship around an existing idea. The program strips you back to zero. You walk in with credentials and walk out with either a company or a very clear understanding of why you didn&rsquo;t start one.</p>
<p>What the program teaches you fast: your technical skills are table stakes. The hard part is finding someone whose strengths complement yours, whose risk tolerance matches, and who you can argue with productively at 11pm on a Tuesday. The co-founder search is brutal. You&rsquo;re essentially speed-dating for the most important professional relationship of your life.</p>
<h2 id="founding-a-company">Founding a company</h2>
<p>Out of the program, I founded a cloud infrastructure company on my own. The thesis was straightforward: cloud infrastructure is absurdly complex, and most teams are paying for complexity they don&rsquo;t need. I wanted to build tooling that made cloud operations simpler without dumbing them down.</p>
<p>This wasn&rsquo;t an abstract idea for me. I&rsquo;d spent years in Go, contributing to open-source projects and building cloud-native systems. I&rsquo;d watched teams at the startups where I&rsquo;d worked struggle with the same problems. Kubernetes was maturing, sure. But &ldquo;maturing&rdquo; in practice meant the operational tax was becoming clearer, not smaller. Managed control planes helped, but day-2 operations (upgrades, policy, incident response) were still eating engineering time that should have gone to product work.</p>
<p>The company was born from that frustration. I wasn&rsquo;t trying to replace Kubernetes or any specific tool. I was trying to reduce the cognitive load of operating in the cloud.</p>
<p>Starting a company from scratch is humbling, even the second time. At the fintech startup, I inherited systems, processes, a team. At the new company, I had a laptop and a pitch deck that needed rewriting every week. The feedback loops are different. The loneliness is real. And the decisions hit different when it&rsquo;s your name on the incorporation documents.</p>
<h2 id="what-i-learned-this-year">What I Learned This Year</h2>
<p><strong>Building from zero versus building on top of something are fundamentally different skills.</strong> I had founded a startup before, but the years since had gone into building technology inside other companies. Building the company itself is not the same thing, and I had to relearn it.</p>
<p><strong>The industry&rsquo;s complexity problem is real and getting worse.</strong> I watched the cloud-native ecosystem grow more powerful and more impenetrable simultaneously. TypeScript was becoming the default for frontend at scale. Go was dominating infrastructure tooling. Rust was expanding beyond systems niches. Good trends, all of them. But the surface area of &ldquo;things you need to know to ship a product&rdquo; kept expanding.</p>
<p><strong>Simplicity is a harder sell than it should be.</strong> The &ldquo;monolith first&rdquo; argument went mainstream in 2019. People started publicly defending boring technology choices. But in practice, teams still reached for distributed architectures before they needed them. Resume-driven development is a powerful force.</p>
<p><strong>The best founders I met in the program weren&rsquo;t the smartest people in the room.</strong> They were the most honest about what they didn&rsquo;t know. That&rsquo;s a different skill than intelligence, and it&rsquo;s harder to fake.</p>
<h2 id="what-i-noticed-in-the-industry">What I Noticed in the Industry</h2>
<p>A few things I noticed from the outside looking in, which shaped how I thought about the company:</p>
<p>Service meshes gained real production usage but proved heavy. The value was clear for large distributed systems: mutual TLS, canary deployments, consistent observability. For everyone else, it was another layer to debug.</p>
<p>Edge computing found its feet. CDN providers turned their points of presence into compute platforms, and real workloads started running there: personalization, auth at the perimeter, feature flagging close to the user. For most teams it&rsquo;s still  <a href="/blog/2019-11-18-edge-computing-architecture/"
   
   >premature optimization</a>
, but the stateless CDN cases are real.</p>
<p> <a href="/blog/2019-02-11-gitops-principles-and-practices/"
   
   >GitOps</a>
 moved from blog posts to actual deployment models. Git as the single source of truth for environments. Pull-based deployments. Declarative infrastructure. This was the kind of operational maturity the industry needed.</p>
<p>Security conversations got more concrete. Zero trust shifted from strategy decks to implementation work. High-profile cloud incidents, above all the Capital One breach disclosed in July, drove home that configuration risk is as real as application risk. IAM policies are code, and they deserve the same rigor.</p>
<p>Remote work kept becoming more normal, at least for engineering teams.</p>
<h2 id="what-im-carrying-into-2020">What I&rsquo;m Carrying Into 2020</h2>
<p>I&rsquo;m ending 2019 as a founder again. A year ago I was at a fintech company. Now I&rsquo;m trying to build something from nothing, with the conviction that cloud infrastructure can be simpler and the awareness that conviction alone doesn&rsquo;t ship products.</p>
<p>The things that stayed hard: operating Kubernetes well, managing dependency sprawl, treating security as engineering rather than compliance theater, and tying cloud costs back to architecture decisions. These are the problems I&rsquo;m now trying to solve professionally, which means I get to be frustrated by them in a whole new way.</p>
<p>2019 wasn&rsquo;t a comfortable year. It was a good one. There&rsquo;s a difference.</p>
]]></content:encoded></item><item><title>Edge Computing Is Usually Premature Optimization</title><link>https://lawzava.com/blog/2019-11-18-edge-computing-architecture/</link><pubDate>Mon, 18 Nov 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-11-18-edge-computing-architecture/</guid><description>Edge computing is real, but most teams adopting it don&amp;amp;rsquo;t have an edge problem. They have an architecture problem they&amp;amp;rsquo;re solving with geography.</description><content:encoded><![CDATA[<p>I keep seeing startups pitch edge computing like it&rsquo;s the next container revolution. Investors nod. Architects draw diagrams with little boxes at &ldquo;the edge.&rdquo; Nobody asks the obvious question: do you actually have a latency problem that geography can fix?</p>
<p>At a mobility startup we processed GPS telemetry from thousands of vehicles across the city. Real IoT. Real device data. Real volume. You&rsquo;d think that&rsquo;s a textbook edge case. It wasn&rsquo;t. A well-tuned  <a href="/blog/2019-09-09-message-queues-patterns/"
   
   >message queue</a>
 and a couple of regional cloud instances handled everything we needed. The same logic applies to  <a href="/blog/2019-06-17-multi-region-architecture/"
   
   >multi-region</a>
: add geography when measurements demand it. Sub-second was easy without edge infrastructure.</p>
<h2 id="the-edge-is-a-spectrum">The edge is a spectrum</h2>
<p>Device. Network edge. Regional edge. Cloud. Each hop adds latency, sure. But each hop closer to the device also adds operational pain: deployment complexity, consistency headaches, debugging in the dark.</p>
<p>The math only works when you have a genuine constraint:</p>
<ul>
<li><strong>Latency below 50ms</strong> on a critical path. Not aspirational latency. Measured, user-impacting latency.</li>
<li><strong>Bandwidth costs that actually hurt.</strong> If you&rsquo;re shipping raw video or sensor streams, processing locally makes sense. If you&rsquo;re sending JSON, it doesn&rsquo;t.</li>
<li><strong>Unreliable connectivity.</strong> Factory floors, moving vehicles, rural deployments. The network genuinely can&rsquo;t be trusted.</li>
<li><strong>Data residency requirements.</strong> Regulation says the data stays put. Fine. That&rsquo;s a real constraint.</li>
</ul>
<p>If none of those apply, you&rsquo;re adding distributed systems complexity for a problem you don&rsquo;t have.</p>
<h2 id="what-edge-computing-costs-you">What edge computing costs you</h2>
<p>Consistency goes out the window. You&rsquo;re now designing for eventual convergence, conflict resolution, and partial failures at every node. Your deployment pipeline needs to handle hundreds of locations instead of a handful of regions. Observability becomes sampling and aggregation because you can&rsquo;t ship raw telemetry from every edge node without defeating the purpose.</p>
<p>Security surface area multiplies. Every edge node is a potential compromise point. You&rsquo;re trusting code running in locations you don&rsquo;t physically control.</p>
<p>Now I&rsquo;m building cloud infrastructure tooling at my own startup. I see teams adopt edge patterns and then spend months building the operational scaffolding that a centralized architecture gives you for free. The edge didn&rsquo;t solve their problem. It replaced one set of problems with a harder set.</p>
<h2 id="when-its-worth-it">When it&rsquo;s worth it</h2>
<p>CDN edge compute is the one place where the tradeoff is almost always favorable. Auth checks, request routing, and simple personalization are stateless, short-lived operations running on infrastructure someone else manages. Cloudflare Workers, Lambda@Edge, and similar products nailed this by keeping the programming model simple and the blast radius small.</p>
<p>Beyond CDN, edge computing earns its complexity in exactly three scenarios: real-time industrial control, high-volume media processing at the source, and latency-sensitive interactive applications where every millisecond is measurable in revenue.</p>
<p>Everything else? Run it in the cloud. Optimize your queries. Use a CDN. Treating edge as a default is resume-driven development dressed up as forward thinking. Start with the simplest thing that works, measure where it doesn&rsquo;t, and push compute to the edge only when the numbers force your hand.</p>
]]></content:encoded></item><item><title>Terraform at Scale: Splitting the State Monolith</title><link>https://lawzava.com/blog/2019-09-23-terraform-at-scale/</link><pubDate>Mon, 23 Sep 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-09-23-terraform-at-scale/</guid><description>How to split a monolithic Terraform state into something teams can work with: state layout by domain, small pinned modules, plan review, and drift checks.</description><content:encoded><![CDATA[<p>I&rsquo;ve managed Terraform across three companies now. At the fintech startup we started the way everyone starts: one state file, a handful of resources, <code>terraform apply</code> from someone&rsquo;s laptop, the same way  <a href="/blog/2016-06-20-why-i-moved-our-infrastructure-to-terraform/"
   
   >I first moved our infrastructure to Terraform</a>
 years earlier. It was fine. Then it wasn&rsquo;t.</p>
<p>By the time I started building my own infrastructure startup in a founder program this year, I&rsquo;d already learned this lesson the hard way. The problem is never Terraform itself. The problem is that infrastructure grows quietly. You add a few resources here, a new environment there, and one morning you run <code>terraform plan</code> and it takes eleven minutes and touches things you forgot existed. That&rsquo;s the moment you realize your Terraform setup doesn&rsquo;t scale. And by then, untangling it is painful.</p>
<p>Stop dumping everything into one state file. Split by domain, pin everything, automate plan-and-apply, and treat <code>-target</code> like a fire extinguisher. The rest of this post is what I wish I&rsquo;d known before the first time a <code>plan</code> returned 200+ changes because someone refactored a module without understanding what depended on it.</p>
<h2 id="the-breaking-point">The breaking point</h2>
<p>Small Terraform setups are a joy. You describe what you want, you apply it, it exists. Beautiful.</p>
<p>At scale, the problems are predictable:</p>
<ul>
<li><strong>Plans take forever.</strong> API rate limits turn a 30-second plan into a 10-minute gamble.</li>
<li><strong>State becomes a bottleneck.</strong> Two engineers run <code>apply</code> at the same time. One wins, one gets a lock error and has to wait.</li>
<li><strong>Blast radius grows silently.</strong> A change to a security group touches the same state as your database config. One bad merge and you&rsquo;re having a very bad afternoon.</li>
<li><strong>Drift accumulates.</strong> Resources nobody has looked at in months quietly diverge from what Terraform thinks they are.</li>
</ul>
<p>None of these kill you immediately. They just make everything slower and scarier until one day someone is afraid to run <code>apply</code> at all.</p>
<h2 id="split-state-by-domain-not-by-convenience">Split state by domain, not by convenience</h2>
<p>The single most impactful thing you can do is break your state apart. Not by team, not by project. By domain.</p>
<pre tabindex="0"><code>terraform/
|-- network/
|-- security/
|-- data/
|-- kubernetes/
`-- services/
    |-- api/
    `-- web/
</code></pre><p>Each directory gets its own state backend. Each one can be planned and applied independently. A change to your VPC config doesn&rsquo;t require Terraform to also reconcile your Kubernetes cluster and all your application services.</p>
<p>For environments, use separate directories and backends. I know workspaces exist. I&rsquo;ve used them. They work fine when it&rsquo;s just you. They get confusing fast when three engineers are working across staging and production and nobody remembers which workspace they&rsquo;re in. Separate directories are boring. Boring is good.</p>
<h3 id="remote-state-with-locking-always">Remote state with locking. Always.</h3>
<p>If you&rsquo;re running Terraform at scale and your state is local, stop reading this and go fix that first.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">terraform</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">backend</span> <span style="color:#e6db74">&#34;s3&#34;</span> {
</span></span><span style="display:flex;"><span>    bucket         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-state&#34;</span>
</span></span><span style="display:flex;"><span>    key            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;network/terraform.tfstate&#34;</span>
</span></span><span style="display:flex;"><span>    region         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;us-east-1&#34;</span>
</span></span><span style="display:flex;"><span>    dynamodb_table <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-locks&#34;</span>
</span></span><span style="display:flex;"><span>    encrypt        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>DynamoDB for locking. Encryption on. Versioning on the S3 bucket so you can recover when (not if) someone corrupts state. This is infrastructure for your infrastructure. Treat it seriously.</p>
<h3 id="cross-state-references">Cross-state references</h3>
<p>When stacks need to talk to each other, use remote state data sources. Keep the outputs stable: changing an output name in your network stack shouldn&rsquo;t break your services stack.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#e6db74">&#34;terraform_remote_state&#34; &#34;network&#34;</span> {
</span></span><span style="display:flex;"><span>  backend <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;s3&#34;</span>
</span></span><span style="display:flex;"><span>  config <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    bucket <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-state&#34;</span>
</span></span><span style="display:flex;"><span>    key    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;network/terraform.tfstate&#34;</span>
</span></span><span style="display:flex;"><span>    region <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;us-east-1&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_subnet&#34; &#34;app&#34;</span> {
</span></span><span style="display:flex;"><span>  vpc_id     <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">terraform_remote_state</span>.<span style="color:#66d9ef">network</span>.<span style="color:#66d9ef">outputs</span>.<span style="color:#66d9ef">vpc_id</span>
</span></span><span style="display:flex;"><span>  cidr_block <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10.0.20.0/24&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Think of outputs as your stack&rsquo;s API. You wouldn&rsquo;t change an API contract without versioning. Same principle.</p>
<h2 id="modules-small-versioned-boring">Modules: small, versioned, boring</h2>
<p>I&rsquo;ve seen Terraform modules that try to do everything. A &ldquo;vpc&rdquo; module that also creates subnets, NAT gateways, route tables, security groups, and somehow a Lambda function. Don&rsquo;t do this.</p>
<p>Keep modules focused on one thing.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">module</span> <span style="color:#e6db74">&#34;vpc&#34;</span> {
</span></span><span style="display:flex;"><span>  source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;./modules/vpc&#34;</span>
</span></span><span style="display:flex;"><span>  cidr   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10.0.0.0/16&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> <span style="color:#e6db74">&#34;public_subnet&#34;</span> {
</span></span><span style="display:flex;"><span>  source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;./modules/subnet&#34;</span>
</span></span><span style="display:flex;"><span>  vpc_id <span style="color:#f92672">=</span> <span style="color:#66d9ef">module</span>.<span style="color:#66d9ef">vpc</span>.<span style="color:#66d9ef">vpc_id</span>
</span></span><span style="display:flex;"><span>  cidr   <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;10.0.1.0/24&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Pin versions. Pin everything. Terraform version, provider versions, module versions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">terraform</span> {
</span></span><span style="display:flex;"><span>  required_version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 0.12&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">provider</span> <span style="color:#e6db74">&#34;aws&#34;</span> {
</span></span><span style="display:flex;"><span>  version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.28&#34;</span>
</span></span><span style="display:flex;"><span>  region  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;us-east-1&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> <span style="color:#e6db74">&#34;vpc&#34;</span> {
</span></span><span style="display:flex;"><span>  source  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-aws-modules/vpc/aws&#34;</span>
</span></span><span style="display:flex;"><span>  version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2.15.0&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I learned this one when an unpinned provider upgrade changed the default behavior of an AWS resource and our <code>plan</code> suddenly wanted to destroy and recreate a production database. Nobody was hurt. My heart rate was.</p>
<h2 id="the-review-workflow">The review workflow</h2>
<p>At the fintech startup I watched us go from &ldquo;just apply it&rdquo; to a proper review flow, and the number of infrastructure incidents dropped to near zero. The process isn&rsquo;t exciting:</p>
<ol>
<li>Branch for the change.</li>
<li><code>terraform fmt</code> and <code>terraform validate</code> locally.</li>
<li>CI runs <code>plan</code> and posts the output to the PR.</li>
<li>The team that owns that stack reviews the plan.</li>
<li><code>apply</code> runs only after approval, then the PR merges.</li>
</ol>
<p>We used Atlantis for this and it worked well:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># atlantis.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">projects</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">network</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dir</span>: <span style="color:#ae81ff">terraform/network</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow</span>: <span style="color:#ae81ff">default</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">workflows</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">default</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">plan</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">init</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">plan</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">apply</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">apply</span>
</span></span></code></pre></div><p>The plan output is the thing being reviewed, not just the code diff. A four-line HCL change can produce a 200-line plan. The plan is what matters.</p>
<h2 id="the-stuff-that-will-save-you-at-2-am">The stuff that will save you at 2 AM</h2>
<p><strong>Plan files.</strong> Always save the plan and apply from the saved file. What was reviewed should be exactly what gets applied.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>terraform plan -out<span style="color:#f92672">=</span>plan.tfplan
</span></span><span style="display:flex;"><span>terraform apply plan.tfplan
</span></span></code></pre></div><p><strong><code>-target</code> is for emergencies only.</strong> It&rsquo;s tempting to use <code>-target</code> to apply just one resource when you&rsquo;re in a hurry. Don&rsquo;t make it a habit. It skips dependency resolution and creates drift. Use it when something is on fire, then follow up with a full plan to clean up.</p>
<p><strong>Drift detection.</strong> Schedule <code>terraform plan -detailed-exitcode</code> to run regularly. Surface drift before it surprises you during an incident. Exit code 2 means there&rsquo;s drift. Pipe that into an alert.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>terraform plan -detailed-exitcode
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Exit 0: no changes</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Exit 1: error</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Exit 2: changes detected (drift)</span>
</span></span></code></pre></div><p><strong>Parallelism tuning.</strong> Default is 10. You can bump it to 20 for faster applies, but watch for API rate limit errors. More parallelism isn&rsquo;t always faster.</p>
<h2 id="guard-the-gates">Guard the gates</h2>
<p>Treat Terraform like application code. Lint it. Format it. Run policy checks before it reaches production.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># .pre-commit-config.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">repos</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">https://github.com/antonbabenko/pre-commit-terraform</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rev</span>: <span style="color:#ae81ff">v1.19.0</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hooks</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">terraform_fmt</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">terraform_validate</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">terraform_tflint</span>
</span></span></code></pre></div><p>Pre-commit hooks catch the easy mistakes. For the hard ones, like someone trying to open port 22 to the world or creating an unencrypted S3 bucket, use Sentinel or OPA. Policy as code is one of those things that feels like overhead until the first time it stops a bad change from reaching production.</p>
<h2 id="own-your-stacks">Own your stacks</h2>
<p>Define CODEOWNERS by directory. The network team reviews network changes. The platform team reviews Kubernetes changes. Nobody should be able to merge infrastructure changes without review from someone who understands the blast radius.</p>
<p>Keep documentation minimal and close to the code. A README in each stack directory with the inputs, outputs, and any non-obvious decisions. Not a wiki that nobody updates.</p>
<p>Terraform scales fine. The part that doesn&rsquo;t scale is humans making ad hoc decisions about where things go and how changes flow. The  <a href="/blog/2018-10-29-infrastructure-as-code-patterns/"
   
   >IaC patterns</a>
 I wrote about last year still hold; the state layout is where most teams need to start.</p>
]]></content:encoded></item><item><title>Kubernetes Security Hardening: Fixing Insecure Defaults</title><link>https://lawzava.com/blog/2019-04-22-kubernetes-security-hardening/</link><pubDate>Mon, 22 Apr 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-04-22-kubernetes-security-hardening/</guid><description>Kubernetes defaults optimize for fast adoption, not safety. A hardening checklist from running production clusters at three startups.</description><content:encoded><![CDATA[<p>Taking part in cyber-defense exercises taught me a principle that stuck: attackers only need one open door. You need to lock all of them. Kubernetes, out of the box, leaves most of them wide open.</p>
<p>I ran into this the hard way at a fintech startup. We spun up our first production cluster in a hurry: tight deadlines, the usual. Anonymous auth on the API server? Enabled. etcd encrypted at rest? No. NetworkPolicy? We didn&rsquo;t even know it existed yet. We were one exposed port away from giving our financial data to anyone with <code>kubectl</code> and an internet connection.</p>
<p>That experience made me paranoid in a productive way. By the time I was setting up infrastructure at my infrastructure startup during the founder-program cohort, I had a hardening checklist I ran through before any cluster touched real traffic. This is that checklist, updated for early 2019. The short version: lock down the API server, encrypt etcd, enforce RBAC and PodSecurityPolicy, add NetworkPolicy, and stop trusting your container images.</p>
<h2 id="lock-down-the-control-plane">Lock Down the Control Plane</h2>
<h3 id="api-server">API Server</h3>
<p>Three flags you should set immediately:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>--<span style="color:#ae81ff">anonymous-auth=false</span>
</span></span><span style="display:flex;"><span>--<span style="color:#ae81ff">authorization-mode=Node,RBAC</span>
</span></span><span style="display:flex;"><span>--<span style="color:#ae81ff">audit-log-path=/var/log/kubernetes/audit.log</span>
</span></span></code></pre></div><p>Disable anonymous access. Require RBAC. Turn on audit logging so you can actually answer &ldquo;who did what&rdquo; during an incident. Keep the API server on a private network. If it&rsquo;s reachable from the public internet, you&rsquo;ve already lost.</p>
<p>Enable <code>NodeRestriction</code> and <code>PodSecurityPolicy</code> admission controllers. These are your baseline. Without them, any pod can request privileges it shouldn&rsquo;t have.</p>
<h3 id="etcd">etcd</h3>
<p>etcd holds your cluster state and your Secrets in plaintext by default. Treat it like a credentials database, because that&rsquo;s what it is.</p>
<p>Run it on control plane nodes only. Firewall it from pod networks. Require TLS for both peer and client connections. And encrypt Secrets at rest:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apiserver.config.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">EncryptionConfiguration</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">secrets</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">providers</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">aescbc</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">keys</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">key1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">secret</span>: <span style="color:#ae81ff">&lt;base64-encoded-key&gt;</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">identity</span>: {}
</span></span></code></pre></div><p>The <code>identity</code> provider at the end is a fallback for reading old unencrypted secrets. Remove it once everything is re-encrypted.</p>
<h3 id="controller-manager-and-scheduler">Controller Manager and Scheduler</h3>
<p>Run these only on control plane nodes. Enable leader election. Give each controller its own service account with tight permissions. The principle of least privilege applies to cluster internals too.</p>
<h2 id="harden-the-nodes">Harden the Nodes</h2>
<h3 id="host-os">Host OS</h3>
<p>Minimal OS. Patched. SSH access restricted and ideally behind MFA. Only required ports open. Disable swap. Don&rsquo;t relax kernel defaults for legacy workloads. If something needs a relaxed seccomp profile, make it an exception, not a default.</p>
<h3 id="kubelet">Kubelet</h3>
<p>The kubelet is the most powerful thing on each node, and it ships with the door unlocked:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>--<span style="color:#ae81ff">anonymous-auth=false</span>
</span></span><span style="display:flex;"><span>--<span style="color:#ae81ff">authorization-mode=Webhook</span>
</span></span><span style="display:flex;"><span>--<span style="color:#ae81ff">read-only-port=0</span>
</span></span></code></pre></div><p>Anonymous kubelet access is how you get remote code execution on every node in your cluster. Disable it. Use webhook authorization. Kill the read-only port.</p>
<h2 id="rbac-less-is-more">RBAC: Less Is More</h2>
<p>Use namespace-scoped <code>Role</code> and <code>RoleBinding</code> by default. <code>ClusterRole</code> and <code>ClusterRoleBinding</code> should be rare and audited.</p>
<p>Never grant <code>cluster-admin</code> to human users. Never grant it to CI. I&rsquo;ve seen CI pipelines with cluster-admin running untrusted code from pull requests. That&rsquo;s not a deployment pipeline, that&rsquo;s a breach pipeline.</p>
<p>One service account per workload. Disable auto-mounting on the default service account. That token sitting in every pod by default is a lateral movement gift.</p>
<h2 id="workload-security">Workload Security</h2>
<h3 id="podsecuritypolicy">PodSecurityPolicy</h3>
<p>In 2019, PSP is the built-in way to prevent pods from running as root, mounting host paths, or using host networking. Use it. Block privileged containers by default. Grant exceptions only with documented justification.</p>
<h3 id="security-contexts">Security Contexts</h3>
<p>Every workload should run with:</p>
<ul>
<li>Non-root user</li>
<li>All capabilities dropped</li>
<li><code>allowPrivilegeEscalation: false</code></li>
<li>Read-only root filesystem when possible</li>
</ul>
<p>These four lines stop a surprising number of container escape techniques.</p>
<h3 id="resource-limits">Resource Limits</h3>
<p>Set CPU and memory requests and limits on everything. This isn&rsquo;t just a reliability thing. Without limits, a compromised pod can starve the node and take down every other workload on it. Crypto miners love clusters without resource limits.</p>
<h2 id="network-isolation">Network Isolation</h2>
<p>Default-deny <code>NetworkPolicy</code> for both ingress and egress. Then add explicit allow rules. If you haven&rsquo;t done this, every pod can talk to every other pod. I covered the default-deny policy in my  <a href="/blog/2019-01-14-kubernetes-best-practices-2019/"
   
   >Kubernetes production checklist</a>
.</p>
<p>Keep control plane components on separate subnets from application pods. The API server shouldn&rsquo;t be reachable from your web frontend pod.</p>
<h2 id="secrets-management">Secrets Management</h2>
<p>Encrypt at rest. Scope access through RBAC. Never put secrets in images, ConfigMaps, or environment variables you can see with <code>kubectl describe pod</code>.</p>
<p>Mount secrets as read-only files. Rotate them. For anything high-value, use Vault or your cloud provider&rsquo;s KMS and inject at runtime. Kubernetes Secrets are base64-encoded, which is not encryption.</p>
<h2 id="image-supply-chain">Image Supply Chain</h2>
<p>At the fintech startup we once deployed a container image pulled from Docker Hub that hadn&rsquo;t been updated in 18 months. It had three known CVEs, one critical. We found out during a penetration test, not before.</p>
<p>Limit deployments to trusted registries. Pin images by digest, not tag; <code>latest</code> isn&rsquo;t a version. Scan images in CI with Clair or Anchore. Block deploys with high-severity findings. Enable Docker Content Trust for workloads where provenance matters. More on this in  <a href="/blog/2018-08-20-container-security-beyond-basics/"
   
   >container security beyond the basics</a>
.</p>
<h2 id="audit-and-monitoring">Audit and Monitoring</h2>
<p>Collect audit logs and ship them somewhere the cluster can&rsquo;t delete them. Alert on:</p>
<ul>
<li><code>clusterrolebinding</code> creation</li>
<li><code>exec</code> into pods</li>
<li>Changes to PodSecurityPolicy</li>
<li>Any access to Secrets</li>
</ul>
<p>Falco adds runtime visibility at the container level. It&rsquo;s worth the setup time.</p>
<h2 id="do-it-this-week">Do It This Week</h2>
<p>If you&rsquo;re running Kubernetes in production and haven&rsquo;t gone through a hardening pass, do it this week. Not next sprint. The defaults are designed for getting started fast, and they succeed at that. They aren&rsquo;t designed to keep your data safe.</p>
]]></content:encoded></item><item><title>Cloud Cost Optimization: Cut Waste Before Buying Discounts</title><link>https://lawzava.com/blog/2019-04-08-cloud-cost-optimization/</link><pubDate>Mon, 08 Apr 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-04-08-cloud-cost-optimization/</guid><description>Cloud cost optimization strategies ranked by effort and savings: tagging, idle resources, right-sizing, reserved and spot instances, and data transfer.</description><content:encoded><![CDATA[<p>I&rsquo;ve managed cloud spend at three startups now. A fintech startup where every penny in margin mattered. A mobility startup where we ran real-time services on a shoestring. And currently my own infrastructure startup, where, ironically, the whole product is about making cloud infrastructure sane.</p>
<p>I keep seeing the same pattern. Someone gets a scary AWS bill. Leadership asks engineering to &ldquo;optimize cloud costs.&rdquo; Engineering buys reserved instances, saves 20%, and calls it done. Meanwhile, half the staging environments are running 24/7, there are unattached EBS volumes left over from instances terminated months ago, and nobody knows which team owns the NAT gateway that&rsquo;s costing $400/month.</p>
<p>Reserved instances are a pricing mechanism. Most teams burn money on idle resources they can&rsquo;t even find, then negotiate a discount on the waste. Fix the waste first. Below are the strategies that work, ranked by effort against impact.</p>
<h2 id="strategies-compared">Strategies Compared</h2>
<table>
  <thead>
      <tr>
          <th>Strategy</th>
          <th>Effort</th>
          <th>Typical Savings</th>
          <th>When It Helps</th>
          <th>When It&rsquo;s a Trap</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kill idle resources</td>
          <td>Low</td>
          <td>15-30%</td>
          <td>Always. Literally always.</td>
          <td>Never a trap. Just do it.</td>
      </tr>
      <tr>
          <td>Right-size instances</td>
          <td>Medium</td>
          <td>10-25%</td>
          <td>After you have utilization data</td>
          <td>Before you have data (you&rsquo;ll guess wrong)</td>
      </tr>
      <tr>
          <td>Storage lifecycle policies</td>
          <td>Low</td>
          <td>5-15%</td>
          <td>When you have &gt;1TB of objects</td>
          <td>When access patterns are unpredictable</td>
      </tr>
      <tr>
          <td>Reserved instances / committed-use discounts</td>
          <td>Low (to buy)</td>
          <td>20-40% off on-demand</td>
          <td>Steady, predictable workloads</td>
          <td>Fast-changing services, early-stage products</td>
      </tr>
      <tr>
          <td>Spot / preemptible instances</td>
          <td>High</td>
          <td>60-90% off on-demand</td>
          <td>CI, batch jobs, stateless workers</td>
          <td>Anything stateful or latency-sensitive</td>
      </tr>
      <tr>
          <td>Architecture changes</td>
          <td>Very high</td>
          <td>10-50%</td>
          <td>When compute patterns are fundamentally wasteful</td>
          <td>When the org isn&rsquo;t ready for the migration cost</td>
      </tr>
  </tbody>
</table>
<p>That table tells a story most FinOps consultants won&rsquo;t: the highest-ROI work is the boring stuff at the top.</p>
<h2 id="start-with-visibility-not-discounts">Start With Visibility, Not Discounts</h2>
<p>At the fintech startup, I inherited an AWS account with zero tagging discipline. Costs were allocated to &ldquo;engineering&rdquo; as a single line item. Completely useless. My first week, I enforced four tags on every resource:</p>
<pre tabindex="0"><code>Environment: production | staging | dev
Team: platform | data | product
Service: api | worker | web | ml-pipeline
Owner: &lt;person who provisioned it&gt;
</code></pre><p>Within two weeks, we found three full staging environments nobody was using. One had been running for five months. That single discovery saved more than the next quarter&rsquo;s reserved instance commitment would have.</p>
<p>Tags are free. Use them.</p>
<h2 id="right-sizing-the-unsexy-middle-ground">Right-Sizing: The Unsexy Middle Ground</h2>
<p>Most instances are oversized. This isn&rsquo;t controversial. Every cloud provider&rsquo;s own tooling will tell you this. The question is what to do about it.</p>
<p>My approach, applied at every place I&rsquo;ve worked:</p>
<ol>
<li><strong>Collect two weeks of utilization data.</strong> Not one day. Not peak hour. Two weeks across normal traffic patterns.</li>
<li><strong>Start with non-production.</strong> Dev and staging environments are almost always 2-4x oversized because someone copied the production config.</li>
<li><strong>Target p95 CPU below 60% after resizing.</strong> You want headroom. This isn&rsquo;t about running hot.</li>
</ol>
<table>
  <thead>
      <tr>
          <th>Instance Type</th>
          <th>Monthly Cost</th>
          <th>Avg CPU</th>
          <th>p95 CPU</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>m5.2xlarge</td>
          <td>$280</td>
          <td>8%</td>
          <td>15%</td>
          <td>Downsize to m5.large ($70)</td>
      </tr>
      <tr>
          <td>r5.xlarge</td>
          <td>$183</td>
          <td>45%</td>
          <td>72%</td>
          <td>Keep. Good fit.</td>
      </tr>
      <tr>
          <td>c5.4xlarge</td>
          <td>$496</td>
          <td>12%</td>
          <td>20%</td>
          <td>Downsize to c5.xlarge ($124)</td>
      </tr>
      <tr>
          <td>t3.medium</td>
          <td>$30</td>
          <td>3%</td>
          <td>5%</td>
          <td>Ask if this service is still needed</td>
      </tr>
  </tbody>
</table>
<p>That last column is the one that matters most. Sometimes the right size is zero.</p>
<h2 id="spot-instances-high-reward-high-effort">Spot Instances: High Reward, High Effort</h2>
<p>I&rsquo;m a fan of spot capacity for the right workloads. At my infrastructure startup, I run all CI builds on spot instances. That saves roughly 70% on build infrastructure. But I wouldn&rsquo;t touch spot for anything user-facing or stateful.</p>
<p>How I decide what goes on spot:</p>
<table>
  <thead>
      <tr>
          <th>Workload</th>
          <th>Spot-safe?</th>
          <th>Why / Why Not</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CI/CD pipelines</td>
          <td>Yes</td>
          <td>Builds are idempotent. Retry is cheap.</td>
      </tr>
      <tr>
          <td>Batch ETL jobs</td>
          <td>Yes</td>
          <td>Checkpointable. Interruption adds minutes, not risk.</td>
      </tr>
      <tr>
          <td>Dev environments</td>
          <td>Yes</td>
          <td>Nobody cares if dev goes down for 2 minutes.</td>
      </tr>
      <tr>
          <td>Stateless API workers</td>
          <td>Maybe</td>
          <td>Only if you have enough replicas and proper drain handling.</td>
      </tr>
      <tr>
          <td>Databases</td>
          <td>No</td>
          <td>Just&hellip; no.</td>
      </tr>
      <tr>
          <td>Single-instance anything</td>
          <td>No</td>
          <td>Spot + single instance = eventual outage</td>
      </tr>
  </tbody>
</table>
<h2 id="data-transfer-costs">Data Transfer Costs</h2>
<p>Cross-AZ traffic on AWS is $0.01/GB each way, one of the  <a href="/blog/2017-07-03-hidden-costs-of-cloud-services/"
   
   >hidden cloud costs</a>
 that never shows up in the pricing calculator. Doesn&rsquo;t sound like much until you&rsquo;re pushing 10TB/month between services in different availability zones. That&rsquo;s $200/month just for services talking to each other inside your own VPC.</p>
<p>Three fixes that cost nothing but attention:</p>
<ul>
<li><strong>Keep chatty services in the same AZ.</strong> Schedule pods to co-locate when latency and bandwidth matter.</li>
<li><strong>Use VPC endpoints for AWS services.</strong> S3 traffic through a NAT gateway is money on fire.</li>
<li><strong>Cache aggressively at the edge.</strong> A CDN in front of your API responses (where appropriate) cuts both latency and egress.</li>
</ul>
<p>At the mobility startup, data transfer was our third-highest line item after compute and RDS. Moving our real-time location services into a single AZ and adding a VPC endpoint for S3 cut that bill by 40% in one sprint.</p>
<h2 id="my-cost-routine">My Cost Routine</h2>
<p>I don&rsquo;t have a FinOps team. At startup scale, cost discipline is an engineering habit, not a department. Here&rsquo;s my actual routine:</p>
<p><strong>Weekly:</strong> Glance at the cost dashboard. Look for anything that jumped more than 20% week-over-week. If something spiked, find it before the month closes.</p>
<p><strong>Monthly:</strong> Review top 10 services by cost. Check for idle resources: unattached volumes, orphaned load balancers, forgotten test clusters. Five minutes of cleanup saves hundreds.</p>
<p><strong>Before any commitment:</strong> Right-size first. Always. Buying a reserved instance for an oversized instance just locks in the waste at a discount.</p>
<p>Cloud cost optimization has no finish line. Like writing tests or reviewing pull requests, you either build the habit or pay the tax forever. The teams that manage spend well are the ones where every engineer can see what their service costs and cares enough to keep it reasonable.</p>
]]></content:encoded></item><item><title>GitOps with Flux and Argo CD: Stop Deploying From a Laptop</title><link>https://lawzava.com/blog/2019-02-11-gitops-principles-and-practices/</link><pubDate>Mon, 11 Feb 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-02-11-gitops-principles-and-practices/</guid><description>How to move a team off ad-hoc kubectl deploys to Git-driven Kubernetes with Flux and Argo CD: repo layout, secrets, rollbacks, and my mistakes.</description><content:encoded><![CDATA[<p>If your deployment process involves someone running kubectl on a laptop, you don&rsquo;t have a deployment process. You have a person. GitOps replaces that person with a Git repo and a controller that remembers what it did.</p>
<p>At the fintech startup, our &ldquo;deployment pipeline&rdquo; for the first few months was me running <code>kubectl apply</code> from my MacBook at 11pm. I had a checklist in Notes.app. It worked until I forgot step 4 one night and took down the price feed for 45 minutes.</p>
<p>The outage wasn&rsquo;t catastrophic, but it made me realize that if I got hit by a bus, nobody else could deploy. The entire process lived in my head and my shell history.</p>
<p>GitOps fixed that. It took a while, and it stuck.</p>
<h2 id="what-gitops-is">What GitOps is</h2>
<p>GitOps is a principle: <strong>Git is the single source of truth for what your infrastructure should look like, and a controller continuously reconciles reality to match.</strong></p>
<p>That&rsquo;s it. Everything else is implementation detail.</p>
<p>The word gets thrown around a lot in 2019, mostly by vendors trying to sell dashboards. Strip away the marketing and you get three rules:</p>
<ol>
<li>Declare your desired state in  <a href="/blog/2018-10-29-infrastructure-as-code-patterns/"
   
   >files checked into Git</a>
</li>
<li>Run a controller that watches those files and applies changes automatically</li>
<li>Never touch the cluster directly</li>
</ol>
<p>If a change isn&rsquo;t in Git, it didn&rsquo;t happen. Full stop.</p>
<h2 id="why-pull-based-beats-push-based">Why pull-based beats push-based</h2>
<p>Most  <a href="/blog/2016-06-06-continuous-deployment-without-chaos/"
   
   >CI/CD pipelines</a>
 are push-based. Jenkins builds your image, then runs <code>kubectl apply</code> against the cluster. The problem: Jenkins now needs cluster credentials, network access, and the ability to mutate production state. Your CI system becomes a god-mode attack surface.</p>
<p>Pull-based GitOps flips this. The controller runs <em>inside</em> the cluster. It watches a Git repo and pulls changes when it sees a new commit. The cluster reaches out to Git, not the other way around.</p>
<pre tabindex="0"><code>Push model:  CI --&gt; kubectl apply --&gt; Cluster
Pull model:  Cluster &lt;-- controller watches &lt;-- Git repo
</code></pre><p>The pull model wins on security first. No cluster credentials in CI. No network path from build servers to production. And because the controller keeps comparing the cluster with Git, drift shows up instead of accumulating quietly.</p>
<h2 id="setting-it-up-a-real-example">Setting it up: a real example</h2>
<p>Here is the repo layout I settled on after trying three different approaches. It&rsquo;s for Go microservices on  <a href="/blog/2019-01-14-kubernetes-best-practices-2019/"
   
   >Kubernetes</a>
, so it&rsquo;s Kustomize-flavored.</p>
<pre tabindex="0"><code>infra/
  apps/
    api-gateway/
      base/
        deployment.yaml
        service.yaml
        kustomization.yaml
      overlays/
        staging/
          kustomization.yaml
          replicas.yaml
        production/
          kustomization.yaml
          replicas.yaml
          hpa.yaml
    auth-service/
      base/
        ...
      overlays/
        ...
  platform/
    namespaces/
    network-policies/
    rbac/
</code></pre><p>The <code>apps/</code> directory is owned by service teams. The <code>platform/</code> directory is owned by me (or whoever is on infra that week). This separation matters more than any specific tool choice.</p>
<h3 id="the-base-deployment">The base deployment</h3>
<p>Nothing exotic here. A standard Kubernetes deployment with explicit resource requests, health checks, and a pinned image tag. No <code>latest</code>. Ever.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># apps/api-gateway/base/deployment.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-gateway</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">api-gateway</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">app</span>: <span style="color:#ae81ff">api-gateway</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-gateway</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image</span>: <span style="color:#ae81ff">registry.example.com/api-gateway:v0.8.3</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#f92672">containerPort</span>: <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;100m&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;128Mi&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">livenessProbe</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">httpGet</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/healthz</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">port</span>: <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">initialDelaySeconds</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">readinessProbe</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">httpGet</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/ready</span>
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">port</span>: <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">initialDelaySeconds</span>: <span style="color:#ae81ff">5</span>
</span></span></code></pre></div><h3 id="the-production-overlay">The production overlay</h3>
<p>Production gets more replicas, an HPA, and a specific image tag. The overlay touches only what differs.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># apps/api-gateway/overlays/production/kustomization.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">kustomize.config.k8s.io/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Kustomization</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">../../base</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">hpa.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">patchesStrategicMerge</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">replicas.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">images</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">registry.example.com/api-gateway</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">newTag</span>: <span style="color:#ae81ff">v0.8.3</span>
</span></span></code></pre></div><p>This is the file that changes on every deploy. A version bump is a one-line diff in a pull request. Reviewable. Revertable. Boring.</p>
<h2 id="choosing-a-controller-flux-vs-argo-cd">Choosing a controller: Flux vs Argo CD</h2>
<p>In early 2019, the two serious options are Flux and Argo CD. I&rsquo;ve run both.</p>
<p><strong>Flux</strong> is simple. It watches a Git repo, syncs to a cluster, done. Installing it is one Helm chart that runs a Deployment pointed at your repo:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>helm repo add weaveworks https://weaveworks.github.io/flux
</span></span><span style="display:flex;"><span>helm install --name flux <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set git.url<span style="color:#f92672">=</span>git@github.com:example/infra.git <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set git.branch<span style="color:#f92672">=</span>master <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set git.path<span style="color:#f92672">=</span>apps/ <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --namespace flux <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  weaveworks/flux
</span></span></code></pre></div><p>Flux feels like a Unix tool. It does one thing. If you want a UI or multi-cluster visibility, look elsewhere.</p>
<p><strong>Argo CD</strong> is heavier but more featureful. You define Applications as custom resources:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">argoproj.io/v1alpha1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Application</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-gateway</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">argocd</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">project</span>: <span style="color:#ae81ff">default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">source</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">repoURL</span>: <span style="color:#ae81ff">https://github.com/example/infra</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">targetRevision</span>: <span style="color:#ae81ff">master</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#ae81ff">apps/api-gateway/overlays/production</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">destination</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server</span>: <span style="color:#ae81ff">https://kubernetes.default.svc</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">syncPolicy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">automated</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">prune</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Automated sync with <code>prune: true</code> makes a merged commit the deploy, deletions included. And when someone runs a manual <code>kubectl edit</code> in production, the UI marks the application OutOfSync, so drift is visible to the whole team. That alone is worth the setup cost.</p>
<p>My recommendation: start with Flux if you have one cluster and want simplicity. Move to Argo CD when you need multi-cluster, a UI for the team, or you&rsquo;re tired of explaining to people what state the cluster is in.</p>
<p>We started with Flux. Moved to Argo CD three months later. No regrets on either decision.</p>
<h2 id="the-deployment-workflow">The deployment workflow</h2>
<p>Here is how a deploy actually works once this is set up:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Developer bumps the image tag after CI builds a new version</span>
</span></span><span style="display:flex;"><span>git checkout -b deploy/api-gateway-v0.8.4
</span></span><span style="display:flex;"><span>vim apps/api-gateway/overlays/production/kustomization.yaml
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Change newTag: v0.8.3 -&gt; v0.8.4</span>
</span></span><span style="display:flex;"><span>git add -A <span style="color:#f92672">&amp;&amp;</span> git commit -m <span style="color:#e6db74">&#34;deploy: api-gateway v0.8.4&#34;</span>
</span></span><span style="display:flex;"><span>git push origin deploy/api-gateway-v0.8.4
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Open PR, get review, merge</span>
</span></span></code></pre></div><p>After merge, the controller picks up the change on its next poll (every few minutes by default; the interval is configurable). The rollout happens. Monitoring confirms. Done.</p>
<p>Compare that to the old world:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># The old way -- do not do this</span>
</span></span><span style="display:flex;"><span>kubectl set image deployment/api-gateway <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  api-gateway<span style="color:#f92672">=</span>registry.example.com/api-gateway:v0.8.4 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -n production
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Hope you remember what you just did</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Hope the next person knows too</span>
</span></span></code></pre></div><p>The first approach gives you an author, a reviewer, a timestamp, a diff, and a one-command rollback (<code>git revert</code>). The second gives you nothing.</p>
<h2 id="secrets-the-hard-part">Secrets: the hard part</h2>
<p>Secrets are where GitOps gets uncomfortable. You want everything in Git, but you can&rsquo;t commit plaintext credentials. In 2019, the options are:</p>
<p><strong>Mozilla SOPS</strong> encrypts values in-place using KMS or PGP. The file structure stays readable, only the values are encrypted. This is what I use.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># secrets.enc.yaml (encrypted with SOPS)</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Secret</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-gateway-secrets</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">data</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">DATABASE_URL</span>: <span style="color:#ae81ff">ENC[AES256_GCM,data:abc123...,type:str]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">API_KEY</span>: <span style="color:#ae81ff">ENC[AES256_GCM,data:def456...,type:str]</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">sops</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">kms</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">arn</span>: <span style="color:#ae81ff">arn:aws:kms:eu-west-1:123:key/abc-def</span>
</span></span></code></pre></div><p><strong>Bitnami Sealed Secrets</strong> encrypts secrets with a cluster-side key. Safe to commit the encrypted form. Decryption only happens inside the cluster.</p>
<p><strong>External secret managers</strong> such as Vault or AWS Secrets Manager. The secret reference lives in Git, the actual value lives elsewhere. More moving parts, but better for large orgs.</p>
<p>Pick one. Don&rsquo;t commit plaintext. I&rsquo;ve seen production database passwords in public GitHub repos more times than I want to admit.</p>
<h2 id="handling-emergencies">Handling emergencies</h2>
<p>Sometimes you need to bypass the process. A P1 incident at 3am isn&rsquo;t the time to open a pull request.</p>
<p>The rule: <strong>fix it now, commit it immediately after.</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1. Fix the fire</span>
</span></span><span style="display:flex;"><span>kubectl rollout undo deployment/api-gateway -n production
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. Immediately capture in Git (before you forget or go back to sleep)</span>
</span></span><span style="display:flex;"><span>git checkout -b hotfix/api-gateway-rollback
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Update manifests to match the rolled-back state</span>
</span></span><span style="display:flex;"><span>git commit -m <span style="color:#e6db74">&#34;hotfix: rollback api-gateway to v0.8.3 (incident #47)&#34;</span>
</span></span><span style="display:flex;"><span>git push <span style="color:#f92672">&amp;&amp;</span> <span style="color:#75715e"># open PR</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3. Write a note in the incident channel explaining why you bypassed GitOps</span>
</span></span></code></pre></div><p>If you skip step 2, the controller will eventually reconcile and re-deploy the broken version. I learned this the hard way: we had a demo the next morning and I rolled back manually without committing. Woke up to the broken version redeployed by Flux at 6am. Fun times.</p>
<h2 id="what-drift-detection-tells-you">What drift detection tells you</h2>
<p>When you first enable GitOps, the controller will fight you. It will revert manual changes, flag resources you forgot about, and generally make noise.</p>
<p>Good. That noise is telling you things:</p>
<ul>
<li>A developer ran <code>kubectl edit</code> because the PR process felt too slow. Fix the process.</li>
<li>A resource exists in the cluster that isn&rsquo;t in Git. Either add it to Git or delete it.</li>
<li>Someone created a secret manually during an incident. Capture it properly.</li>
</ul>
<p>Drift detection is your audit system running in real time. Treat every drift event as a process fix to make, and don&rsquo;t suppress it.</p>
<h2 id="repo-structure-mono-vs-split">Repo structure: mono vs split</h2>
<p>I tried three approaches before settling:</p>
<p><strong>Mono-repo (app + infra together).</strong> Simple for tiny teams. Falls apart when you have more than ~5 services because every deploy touches the same repo and branch protection gets weird.</p>
<p><strong>Split repos (one for app, one for infra).</strong> Clean ownership boundaries. The infra repo becomes the system of record. Cross-cutting changes require two PRs, which is annoying but forces you to think about coupling. This is what I recommend for most teams.</p>
<p><strong>Per-app manifests in app repos.</strong> Each service owns its deployment config. Great for autonomy, terrible for platform-wide changes like &ldquo;update the sidecar version on every service.&rdquo; You will end up writing a bot to open 30 PRs.</p>
<p>For a team of 3-10 engineers, split repos with Kustomize overlays is the sweet spot. You can always migrate later; your manifests are just files in Git.</p>
<h2 id="what-i-got-wrong">What I got wrong</h2>
<p>A few mistakes worth sharing:</p>
<p><strong>I over-automated image updates.</strong> I set up Flux&rsquo;s auto-image-update feature, which watches a registry and commits new tags automatically. Sounds great. In practice, it deployed a broken build at 2am because CI had a flaky test that passed on a bad commit. Explicit tag bumps via PRs are slower but safer. I turned off auto-updates within a week.</p>
<p><strong>I didn&rsquo;t enforce branch protection early enough.</strong> For the first month, anyone could push directly to master on the infra repo. One accidental force-push later, I added required reviews and status checks. Should have been day one.</p>
<p><strong>I skipped the README.</strong> New engineers had no idea how to deploy. The process was obvious to me because I built it. It wasn&rsquo;t obvious to anyone else. A one-page doc explaining &ldquo;how to deploy a new version&rdquo; saved hours of Slack questions.</p>
<h2 id="getting-started">Getting started</h2>
<p>If you&rsquo;re reading this and still deploying with kubectl from a laptop, here is what I would do tomorrow:</p>
<ol>
<li>Create an infra repo. Move your manifests there.</li>
<li>Install Flux and point it at the repo.</li>
<li>Enable branch protection on master. Require one review.</li>
<li>Deploy something small through a PR. Watch it sync.</li>
<li>Delete kubectl access for your CI system. If CI can&rsquo;t touch the cluster, it won&rsquo;t.</li>
</ol>
<p>You can do steps 1-4 in an afternoon. Step 5 takes courage, but it&rsquo;s the one that makes the whole thing stick.</p>
]]></content:encoded></item><item><title>Kubernetes Production Checklist: The Boring Basics</title><link>https://lawzava.com/blog/2019-01-14-kubernetes-best-practices-2019/</link><pubDate>Mon, 14 Jan 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-01-14-kubernetes-best-practices-2019/</guid><description>Most Kubernetes outages come from skipped basics: limits, probes, network policies, RBAC, upgrades, etcd restores. The checklist I run on every cluster.</description><content:encoded><![CDATA[<p>Kubernetes won the orchestration debate. Winning adoption doesn&rsquo;t mean most teams run it well. I&rsquo;ve operated production clusters at a fintech startup, and the failure pattern I keep seeing is the same: teams skip the fundamentals, bolt on complexity, then blame Kubernetes when things break. Resource limits, probes, network policies, RBAC, and boring upgrades prevent most of that pain.</p>
<p>This is the checklist I use, not a getting-started guide. Everything here exists because I&rsquo;ve been paged at 3am for the opposite.</p>
<h2 id="upgrades-should-be-boring">Upgrades Should Be Boring</h2>
<p>If your Kubernetes upgrade process feels heroic, something is wrong.  <a href="/blog/2018-01-22-kubernetes-in-production-lessons-learned/"
   
   >Two years of running clusters</a>
 taught me that the boring parts are the hard parts. Stay on a supported minor version. Have a rollback plan. Run it on a schedule, not as a fire drill.</p>
<p>I keep a dead-simple rule: if we&rsquo;re more than one minor version behind, it&rsquo;s a P1. Not because something is broken, but because something <em>will</em> break and we&rsquo;ll be debugging with stale docs and missing patches.</p>
<h2 id="resource-requests-and-limits-on-everything">Resource Requests and Limits. On Everything.</h2>
<p>This is the single most impactful thing you can do. No exceptions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;250m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;512Mi&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;1000m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;1Gi&#34;</span>
</span></span></code></pre></div><p>Requests are your scheduling guarantee. Limits are your safety net. Without both, you&rsquo;re one misbehaving pod away from cascading node pressure. I&rsquo;ve seen a single Go service with no memory limit take down a shared node at the fintech startup because someone forgot to close a channel and goroutines piled up. That was a fun Friday evening.</p>
<p>Size these from actual metrics, not guesswork. The metrics server and Prometheus give you enough signal. Start conservative, then adjust.</p>
<h2 id="namespaces-with-actual-guardrails">Namespaces With Actual Guardrails</h2>
<p>Namespaces without resource quotas are just labels. Enforce boundaries at the namespace level so one team&rsquo;s experiment can&rsquo;t starve another team&rsquo;s production workload.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ResourceQuota</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">production-quota</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hard</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests.cpu</span>: <span style="color:#e6db74">&#34;20&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">requests.memory</span>: <span style="color:#ae81ff">40Gi</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limits.cpu</span>: <span style="color:#e6db74">&#34;40&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">limits.memory</span>: <span style="color:#ae81ff">80Gi</span>
</span></span></code></pre></div><h2 id="probes-are-mandatory">Probes Are Mandatory</h2>
<p>Not optional. Not &ldquo;we&rsquo;ll add them later.&rdquo; Mandatory.</p>
<p>Liveness probes restart broken processes. Readiness probes keep unready pods out of the Service&rsquo;s endpoints, so they get no traffic. Without them, you&rsquo;re routing traffic to containers that are technically running but functionally dead.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">livenessProbe</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">httpGet</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/healthz</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">port</span>: <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">initialDelaySeconds</span>: <span style="color:#ae81ff">15</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">periodSeconds</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">readinessProbe</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">httpGet</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#ae81ff">/ready</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">port</span>: <span style="color:#ae81ff">8080</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">initialDelaySeconds</span>: <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">periodSeconds</span>: <span style="color:#ae81ff">5</span>
</span></span></code></pre></div><p>One thing I always add: make <code>/healthz</code> and <code>/ready</code> do different things. Your liveness check should be &ldquo;is the process alive.&rdquo; Your readiness check should be &ldquo;can this instance serve traffic right now&rdquo;: database connected, caches warm, dependencies reachable.</p>
<h2 id="handle-shutdown-properly">Handle Shutdown Properly</h2>
<p>Your container will get SIGTERM&rsquo;d. Handle it. Stop accepting new work, drain in-flight requests, exit clean. Set <code>terminationGracePeriodSeconds</code> to match your actual drain time, not the default 30 seconds.</p>
<p>In Go (which is what I write most services in), this means catching the signal and giving your HTTP server a context-bounded shutdown. It&rsquo;s ten lines of code. There&rsquo;s no excuse.</p>
<h2 id="secrets-deserve-respect">Secrets Deserve Respect</h2>
<p>Two rules:</p>
<ol>
<li><strong>Mount secrets as files, not environment variables.</strong> Env vars leak into crash dumps, child processes, and logging. Files don&rsquo;t.</li>
<li><strong>Enable encryption at rest in etcd.</strong> If you&rsquo;re on a managed provider, verify this is actually on. Don&rsquo;t assume.</li>
</ol>
<p>For anything beyond toy projects, use an external secret manager. Kubernetes native secrets are base64-encoded, not encrypted. Big difference.</p>
<h2 id="default-deny-networking">Default-Deny Networking</h2>
<p>Without network policies, every pod can talk to every other pod. That&rsquo;s not a cluster, it&rsquo;s a flat network with extra steps.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">NetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">default-deny</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">podSelector</span>: {}
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyTypes</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">Ingress</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">Egress</span>
</span></span></code></pre></div><p>Start with deny-all, then explicitly allow what&rsquo;s needed, the same default-deny idea behind  <a href="/blog/2018-02-19-zero-trust-security-architecture/"
   
   >the zero trust setup we built</a>
. Yes, it&rsquo;s more work upfront. But &ldquo;every service can reach every other service plus the internet&rdquo; isn&rsquo;t a security posture I&rsquo;m comfortable with, and you shouldn&rsquo;t be either.</p>
<p>My order of priorities is always the same: security, stability, performance. In that order. Network policies fall squarely in bucket one.</p>
<h2 id="rbac-and-pod-security">RBAC and Pod Security</h2>
<p>Run as non-root. Drop capabilities. Use read-only root filesystems where you can. Enforce these with PodSecurityPolicy so it&rsquo;s not just a suggestion.</p>
<p>For RBAC: if any service account has <code>cluster-admin</code>, fix that first. Service accounts get the verbs and resources they need. Nothing more.</p>
<h2 id="roll-out-safely">Roll Out Safely</h2>
<p>Use rolling updates with sensible surge and unavailable settings. Add Pod Disruption Budgets so node drains and maintenance don&rsquo;t accidentally kill your service. Spread replicas across nodes and zones with anti-affinity rules.</p>
<p>None of this is glamorous. All of it prevents 2am pages.</p>
<h2 id="observability-that-helps">Observability That Helps</h2>
<p>Three things:</p>
<p><strong>Structured logs.</strong> JSON, consistent fields, correlation IDs. If your logs are unstructured strings, your incident response is <code>grep</code> and prayer.</p>
<p><strong>Application metrics tied to SLOs.</strong> Service-level objectives tell you whether users are happy; vanity dashboards don&rsquo;t. Expose the metrics with Prometheus and build dashboards that answer questions.</p>
<p><strong>Consistent labels.</strong> Every resource gets <code>app</code>, <code>team</code>, <code>version</code>, <code>environment</code>. Those labels are what make cost allocation, debugging, and incident response possible at any scale beyond one person.</p>
<h2 id="back-up-etcd-test-the-restore">Back Up etcd. Test the Restore.</h2>
<p>A backup you&rsquo;ve never restored is a hope, not a backup. Schedule restores. Verify them. The five minutes this takes will save you from the worst day of your career.</p>
<h2 id="the-pattern">The Pattern</h2>
<p>The teams I&rsquo;ve seen run Kubernetes well aren&rsquo;t doing anything clever. They do the basics consistently and spend their creativity on the product instead of firefighting infrastructure.</p>
]]></content:encoded></item><item><title>Istio 1.0 in Practice: Powerful, Painful, Often Overkill</title><link>https://lawzava.com/blog/2018-11-26-service-mesh-istio-practical-guide/</link><pubDate>Mon, 26 Nov 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-11-26-service-mesh-istio-practical-guide/</guid><description>Evaluating Istio 1.0 at a fintech startup: what it gives you, what it costs in overhead and complexity, and why most teams should think twice.</description><content:encoded><![CDATA[<p>I wanted to hate Istio. We&rsquo;d been running microservices at the fintech startup for a while, and every few weeks someone would bring up service meshes like they were the answer to problems we hadn&rsquo;t even articulated yet.  <a href="/blog/2017-11-27-service-mesh-do-you-actually-need-one/"
   
   >A year ago I walked away unconvinced</a>
. Istio hit 1.0 in July, so I spent real time evaluating it again. Deploying it. Fighting with it. My conclusion is complicated.</p>
<p>Istio is impressive technology. It&rsquo;s also a complexity bomb that most teams have no business adopting.</p>
<h2 id="what-it-does">What it does</h2>
<p>The pitch is simple. You have microservices, they all need retries, timeouts, mTLS, and observability. Instead of implementing that in every service, you push it to a sidecar proxy layer. Istio manages those proxies. Fine.</p>
<p>In practice you get three things:</p>
<p><strong>Traffic control.</strong> Route by version, headers, percentage. Canary deployments become a YAML change instead of a code change.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.istio.io/v1alpha3</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">VirtualService</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hosts</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">http</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">route</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">destination</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">host</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">subset</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">90</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">destination</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">host</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">subset</span>: <span style="color:#ae81ff">v2</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">weight</span>: <span style="color:#ae81ff">10</span>
</span></span></code></pre></div><p><strong>Mutual TLS without touching application code.</strong> This is nice. You flip a policy and suddenly everything&rsquo;s encrypted in transit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">authentication.istio.io/v1alpha1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Policy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">default</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">peers</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">mtls</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">mode</span>: <span style="color:#ae81ff">PERMISSIVE</span>
</span></span></code></pre></div><p><strong>Uniform observability.</strong> Every request goes through Envoy, so you get consistent metrics and traces everywhere. Request rates, error rates, latency percentiles, distributed traces, all without instrumenting each service individually.</p>
<p>That&rsquo;s the good stuff. And if you stopped reading here, you&rsquo;d think Istio is a no-brainer.</p>
<h2 id="how-it-works">How it works</h2>
<p>The architecture is a control plane plus a data plane. Data plane: Envoy sidecar proxies injected into every pod, intercepting all traffic. Control plane: four components, Pilot (routing), Mixer (policy and telemetry), Citadel (certificates), Galley (config processing).</p>
<pre tabindex="0"><code>App -&gt; Envoy -&gt; network -&gt; Envoy -&gt; App
</code></pre><p>Four control plane components for what is essentially a proxy configurator. That should tell you something about the operational surface area you&rsquo;re signing up for.</p>
<h2 id="the-part-nobody-talks-about-at-conferences">The part nobody talks about at conferences</h2>
<p>This is what I found evaluating it at the fintech startup.</p>
<p>Every pod now has a sidecar. That&rsquo;s extra CPU, extra memory, extra things that can fail. We saw meaningful resource overhead. Not catastrophic, but not nothing, and it scales linearly with your pod count.</p>
<p>Timeout alignment is a nightmare. You set a 2-second timeout at the mesh layer, but the upstream service has a 5-second timeout, and the downstream expects responses in 1 second. Now you&rsquo;ve got three layers of timeout logic interacting in ways that are hard to reason about. We spent more time debugging timeout cascades than we saved by having mesh-level retries.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.istio.io/v1alpha3</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">VirtualService</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hosts</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">http</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">2s</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">retries</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">attempts</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">perTryTimeout</span>: <span style="color:#ae81ff">1s</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">route</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">destination</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">host</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">subset</span>: <span style="color:#ae81ff">v1</span>
</span></span></code></pre></div><p>Partial adoption is worse than no adoption. If half your services are in the mesh and half aren&rsquo;t, you&rsquo;ve got blind spots everywhere. Policies don&rsquo;t apply uniformly. Your observability has gaps. You&rsquo;re paying the complexity cost without getting the full benefit.</p>
<p>Egress is its own project. By default, pods in the mesh can&rsquo;t reach anything outside the cluster until you add a ServiceEntry for every external API they call, or carve IP ranges out of the sidecar&rsquo;s interception. We found this out the fun way.</p>
<p>And upgrades? Every Istio upgrade changes CRDs, defaults, sometimes both. You rehearse them or you regret it.</p>
<h2 id="when-its-worth-it">When it&rsquo;s worth it</h2>
<p>If you&rsquo;re running dozens of services on Kubernetes, you need consistent traffic policy across all of them, and you have the team to operate the mesh, Istio delivers. The RBAC model is solid:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">rbac.istio.io/v1alpha1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ServiceRole</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-reader</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">api.production.svc.cluster.local</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">methods</span>: [<span style="color:#e6db74">&#34;GET&#34;</span>, <span style="color:#e6db74">&#34;POST&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">paths</span>: [<span style="color:#e6db74">&#34;/api/*&#34;</span>]
</span></span><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">rbac.istio.io/v1alpha1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ServiceRoleBinding</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-reader-binding</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">namespace</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">subjects</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">user</span>: <span style="color:#e6db74">&#34;cluster.local/ns/production/sa/frontend&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">roleRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ServiceRole</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-reader</span>
</span></span></code></pre></div><p>The observability story (golden signals per service, distributed traces, service topology maps) is better than bolting together per-service monitoring.</p>
<p>But if you&rsquo;re running five services? Ten? Just write a shared library for retries and use Prometheus directly. You don&rsquo;t need this.</p>
<h2 id="how-to-trial-it">How to trial it</h2>
<p>Start in staging. Not production. Not &ldquo;let&rsquo;s just try it on one production namespace.&rdquo; Staging.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kubectl apply -f install/kubernetes/istio-demo.yaml
</span></span><span style="display:flex;"><span>kubectl label namespace staging istio-injection<span style="color:#f92672">=</span>enabled
</span></span></code></pre></div><p>Get comfortable with the abstractions. <code>DestinationRule</code> for defining service subsets, <code>VirtualService</code> for traffic splitting. Understand that these are two separate resources that reference each other and both need to be correct or nothing works.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.istio.io/v1alpha3</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">DestinationRule</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">host</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">subsets</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">version</span>: <span style="color:#ae81ff">v1</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">v2</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">version</span>: <span style="color:#ae81ff">v2</span>
</span></span></code></pre></div><p>Start with permissive mTLS. Watch your metrics. Only move to strict when you&rsquo;re confident nothing breaks. Then add RBAC rules one service pair at a time.</p>
<p>The teams I&rsquo;ve seen fail with Istio all did the same thing: they adopted everything at once because the demo looked cool. The teams that succeeded treated it like any other piece of infrastructure: incrementally, skeptically, with rollback plans and runbooks.</p>
<p>Istio is a powerful tool. Whether it&rsquo;s the right tool for you is a different question entirely. Our trial at the fintech startup involved a lot of late nights and some colorful Slack messages that I won&rsquo;t reproduce here.</p>
]]></content:encoded></item><item><title>Terraform in Production: Repo Layout, Modules, and State</title><link>https://lawzava.com/blog/2018-10-29-infrastructure-as-code-patterns/</link><pubDate>Mon, 29 Oct 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-10-29-infrastructure-as-code-patterns/</guid><description>Opinionated Terraform patterns from a fintech startup: repo layout, small modules, per-environment state, CI plans, drift detection, and secrets.</description><content:encoded><![CDATA[<p>Most IaC advice reads like a textbook nobody asked for. This is what I&rsquo;ve learned running Terraform at the fintech startup, where our entire AWS stack is codified and any manual console change gets treated like a bug. (The  <a href="/blog/2016-06-20-why-i-moved-our-infrastructure-to-terraform/"
   
   >first time I moved a stack to Terraform</a>
 was at a mobility startup, after a bad night.)</p>
<h2 id="declarative-or-go-home">Declarative or go home</h2>
<p>Imperative scripts describe steps. Declarative config describes outcomes. Huge difference. Terraform handles ordering, diffs, idempotency. You just tell it what you want.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">resource</span> <span style="color:#e6db74">&#34;aws_instance&#34; &#34;web&#34;</span> {
</span></span><span style="display:flex;"><span>  ami           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${var.ami_id}&#34;</span>
</span></span><span style="display:flex;"><span>  instance_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t3.micro&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  tags <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    Name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;web&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>At the fintech startup we stopped writing bash provisioning scripts the week we adopted Terraform. Never looked back.</p>
<h2 id="immutable-over-mutable-always">Immutable over mutable. Always.</h2>
<p>Replace, don&rsquo;t patch. When config changes, tear down and rebuild. Sounds wasteful until you realize in-place mutations are where drift lives. Rollback becomes &ldquo;redeploy the previous version&rdquo; instead of &ldquo;figure out which of 47 manual tweaks broke something.&rdquo;</p>
<h2 id="if-its-not-in-git-it-doesnt-exist">If it&rsquo;s not in git, it doesn&rsquo;t exist</h2>
<p>I mean this literally. We reject any infrastructure change that isn&rsquo;t a PR. No exceptions. The diff is the documentation.</p>
<h2 id="repo-layout">Repo layout</h2>
<p>We use a monorepo. One repo, all infra. Refactors are easier, search is easier, onboarding is easier. The risk is blast radius, which is what small state files and good reviews are for.</p>
<p>Structure by concern, not by team:</p>
<pre tabindex="0"><code>terraform/
  global/
  network/
  data/
  services/
    api/
    web/
</code></pre><p>Each directory maps to its own state file. Smaller state means faster plans and smaller explosions when someone makes a mistake.</p>
<h2 id="modules-that-dont-suck">Modules that don&rsquo;t suck</h2>
<p>A few rules I enforce:</p>
<p><strong>One job per module.</strong> A network module. A database module. Not an everything-module with 30 variables and a prayer.</p>
<p><strong>Explicit inputs, focused outputs.</strong> If your module takes 15 required variables, it&rsquo;s doing too much. Outputs should be stable values other modules can depend on without breaking.</p>
<p><strong>Sensible defaults.</strong> Most consumers should be able to use your module with minimal config. Overrides available, not required.</p>
<p><strong>Compose, don&rsquo;t inherit.</strong> Build bigger systems by wiring small modules together. Adding flags to one giant module is how you get a 2000-line <code>main.tf</code> that nobody wants to touch.</p>
<p><strong>Pin your versions.</strong> Unpinned modules are a time bomb. We learned this the hard way when an upstream module update silently changed our security group rules. Pin everything, upgrade deliberately.</p>
<h2 id="environments">Environments</h2>
<p>One directory per environment. Shared modules underneath. Each environment gets its own state.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#75715e"># environments/production/main.tf
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">module</span> <span style="color:#e6db74">&#34;app&#34;</span> {
</span></span><span style="display:flex;"><span>  source         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;../../modules/app&#34;</span>
</span></span><span style="display:flex;"><span>  environment    <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;production&#34;</span>
</span></span><span style="display:flex;"><span>  instance_count <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Environments differ by variables, not by code. Promotion is dev to staging to production, same Terraform, different tfvars. Simple.</p>
<p>Workspaces? Fine for side projects. For a real team, explicit directories win. Workspaces hide differences and make it too easy to apply to the wrong environment.</p>
<h2 id="ci-pipeline">CI pipeline</h2>
<p>Treat infra code like app code. Our pipeline:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>terraform fmt -check
</span></span><span style="display:flex;"><span>terraform init
</span></span><span style="display:flex;"><span>terraform validate
</span></span><span style="display:flex;"><span>terraform plan -no-color
</span></span></code></pre></div><p>Plan output goes into the PR as a comment. Apply is gated behind manual approval for production. Fast automation, human control where it counts.</p>
<h2 id="drift-detection">Drift detection</h2>
<p>People will click things in the console. It happens. We run scheduled plans and alert on any diff. The goal is getting manual fixes back into code before they become invisible tech debt. Nobody gets punished.</p>
<h2 id="secrets-and-state">Secrets and state</h2>
<p>Three things I&rsquo;m strict about:</p>
<p><strong>Secrets never go in <code>.tf</code> files.</strong> Use AWS Secrets Manager, Vault, injected env vars. Whatever. Just not in your repo.</p>
<p><strong>State files are sensitive.</strong> They contain plaintext values for things you&rsquo;d rather keep private. Remote backend, encryption on, locking on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">terraform</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">backend</span> <span style="color:#e6db74">&#34;s3&#34;</span> {
</span></span><span style="display:flex;"><span>    bucket         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-state&#34;</span>
</span></span><span style="display:flex;"><span>    key            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prod/terraform.tfstate&#34;</span>
</span></span><span style="display:flex;"><span>    region         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;us-east-1&#34;</span>
</span></span><span style="display:flex;"><span>    encrypt        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    dynamodb_table <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-locks&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Least privilege for CI.</strong> Your automation role shouldn&rsquo;t be able to delete your account. Scope it tight to the resources it manages. Broad permissions hide mistakes until they become outages.</p>
<h2 id="the-boring-part-is-the-point">The boring part is the point</h2>
<p>Good IaC is boring. Small diffs, predictable plans, easy reviews, easy rollbacks. That&rsquo;s the goal. At the fintech startup this approach has kept our infra stable through multiple scaling phases, and I&rsquo;d use the same patterns anywhere else.</p>
]]></content:encoded></item><item><title>Kubernetes Operators: Powerful, but Overhyped</title><link>https://lawzava.com/blog/2018-04-02-kubernetes-operators-extending-the-platform/</link><pubDate>Mon, 02 Apr 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-04-02-kubernetes-operators-extending-the-platform/</guid><description>Kubernetes operators are useful for Day 2 operations, but writing a good one is hard. When to adopt an existing operator, when to build your own.</description><content:encoded><![CDATA[<p>Operators are having a moment. Every Kubernetes talk, every blog post, every Slack channel: someone&rsquo;s pitching operators as the answer to all your stateful workload problems. They&rsquo;re a clever pattern. But I think the community is overselling them, and most teams jumping on the bandwagon are setting themselves up for pain.</p>
<p>At the fintech startup, we run our infrastructure on Kubernetes. We&rsquo;ve evaluated operators, built internal tooling around them, and dealt with the sharp edges firsthand.</p>
<h2 id="what-an-operator-is">What an Operator Is</h2>
<p>Dead simple concept. An operator is a controller that watches a custom resource, compares desired state to actual state, and reconciles the difference. Same pattern as built-in K8s controllers, just applied to your specific application.</p>
<p>You define a CRD like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">example.com/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Database</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">orders-db</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">engine</span>: <span style="color:#ae81ff">postgres</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">version</span>: <span style="color:#e6db74">&#34;10&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">replicas</span>: <span style="color:#ae81ff">3</span>
</span></span></code></pre></div><p>The operator sees that and creates the StatefulSets, Services, ConfigMaps, backup jobs, whatever the system needs. Intent-driven. Declarative. Very Kubernetes-native.</p>
<p>The real value is Day 2 operations. Scaling, rolling upgrades, backups, failover, drift correction. The stuff that happens after you deploy. That&rsquo;s where operators shine and where manual runbooks fall apart at 3am.</p>
<h2 id="where-the-hype-breaks-down">Where the Hype Breaks Down</h2>
<p>What the conference talks skip: writing a good operator is hard. Writing a bad one is easy. And a bad operator running with cluster-level RBAC is a liability.</p>
<p>I&rsquo;ve seen teams spend weeks building custom operators for problems that a well-written Helm chart and a cron job would have solved. The pattern is seductive (&ldquo;we&rsquo;ll encode all our operational knowledge into code!&rdquo;) but the implementation details are brutal.</p>
<p>Your reconciliation loop has to be idempotent. Truly idempotent. Not &ldquo;works most of the time&rdquo; idempotent. You need proper owner references for garbage collection. You need finalizers if you touch anything outside the cluster. You need status conditions so someone debugging at 2am can see what&rsquo;s going on. Most first-attempt operators skip half of this.</p>
<h2 id="build-vs-adopt-usually-adopt">Build vs. Adopt (Usually Adopt)</h2>
<p>At the fintech startup, we evaluated building custom operators early on. For most of our use cases, adopting existing ones was the right call. The etcd operator and the Prometheus operator come from CoreOS, which introduced the operator pattern in 2016, and they have real production hours behind them. Our custom needs weren&rsquo;t special enough to justify the maintenance burden.</p>
<p>Build your own only if your system is genuinely proprietary, your operational rules can&rsquo;t be expressed with existing operators, or you need tighter control than third-party code allows. Otherwise, use what exists.</p>
<h2 id="if-you-write-one-anyway">If You Write One Anyway</h2>
<p>A few things will save you:</p>
<ul>
<li><strong>Idempotent reconciliation.</strong> Compute desired state from the CR every single time. No &ldquo;run once&rdquo; actions without guards.</li>
<li><strong>Owner references.</strong> Let K8s garbage collection do its job.</li>
<li><strong>Status fields.</strong> If <code>kubectl describe</code> can&rsquo;t tell me what&rsquo;s broken, your operator is incomplete.</li>
<li><strong>Least privilege RBAC.</strong> Operators often end up with way too many permissions. Start minimal.</li>
<li><strong>Conservative defaults.</strong> Easier to loosen limits than to recover from an operator that went aggressive on scaling.</li>
</ul>
<p>Test against a real cluster. Unit tests alone won&rsquo;t catch the weird timing issues that only show up when the API server is under load.</p>
<h2 id="where-i-land">Where I Land</h2>
<p>Operators are the right abstraction for complex stateful workloads on Kubernetes. They&rsquo;re not magic, and they&rsquo;re not always the right tool. Most teams I talk to would be better served by adopting a mature community operator than by building one from scratch. We still keep our own databases  <a href="/blog/2018-01-22-kubernetes-in-production-lessons-learned/"
   
   >outside the cluster</a>
.</p>
<p>The pattern will mature, and the tooling for writing operators will get better. Right now, in 2018, approach with clear eyes about what you&rsquo;re signing up for.</p>
]]></content:encoded></item><item><title>Zero Trust Architecture: Building It at a Fintech Startup</title><link>https://lawzava.com/blog/2018-02-19-zero-trust-security-architecture/</link><pubDate>Mon, 19 Feb 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-02-19-zero-trust-security-architecture/</guid><description>How I replaced castle-and-moat security at a fintech startup with zero trust: identity-first access, an access proxy, and micro-segmentation.</description><content:encoded><![CDATA[<p>One thing cyber-defense exercises hammered into me: the perimeter is a lie. Networks get breached. Insiders go rogue. A &ldquo;trusted&rdquo; subnet is just a subnet you haven&rsquo;t caught an attacker on yet.</p>
<p>So when I started building the fintech startup&rsquo;s infrastructure, I refused to use the castle-and-moat model. Flat internal networks with a firewall at the edge? That&rsquo;s how you get lateral movement from a single compromised laptop to your production database in under four minutes. I&rsquo;ve seen it happen. Not theoretically. In exercises, in real incident reports, in postmortems that never should have been necessary.</p>
<p>Zero trust was the alternative. Not a product we bought. A posture we adopted, with Google&rsquo;s published BeyondCorp work as the proof that it runs at scale.</p>
<h3 id="what-zero-trust-means">What zero trust means</h3>
<p>Every request gets authenticated and authorized. Doesn&rsquo;t matter if it comes from an office IP, a developer&rsquo;s home Wi-Fi, or a service running in our own cluster. No one gets a free pass because they logged in once. No service gets implicit access because it sits on the same VLAN.</p>
<p>You design as if a breach already happened. Then you make it hard for that breach to go anywhere.</p>
<p>At the fintech startup, this meant shifting the security boundary from the network to identity. Users prove who they are. Services prove who they are. Devices prove they&rsquo;re healthy. Policies decide what&rsquo;s allowed <em>right now</em>, not based on some access grant from three months ago that nobody reviewed.</p>
<h3 id="the-pieces-that-matter">The pieces that matter</h3>
<p><strong>Never trust, always verify.</strong> Each call gets validated with fresh signals: user identity, service identity, device posture, risk context. Not &ldquo;you authenticated an hour ago, good enough.&rdquo; Fresh. Every time.</p>
<p><strong>Assume breach.</strong> We removed implicit trust paths, encrypted all internal traffic, and put hard guardrails around lateral movement. If one service gets popped, it can&rsquo;t crawl to the next.</p>
<p><strong>Least privilege, enforced ruthlessly.</strong> This isn&rsquo;t a checkbox on an audit form. It&rsquo;s constant pruning. People and services get exactly what they need. Nothing more. And those grants expire.</p>
<p>Micro-segmentation made this real for us. Instead of a handful of broad network zones, we defined explicit service-to-service dependencies and enforced them. Our frontend could reach the API it depended on. It couldn&rsquo;t reach the database. Period. Moving between segments required the same authentication you&rsquo;d demand at the edge.</p>
<p>Device trust was the other critical piece. A valid user on a compromised laptop is still a threat. We treated device health as a first-class signal. Managed, patched, compliant devices got deeper access. Unknown or outdated machines got restricted. Sessions expired. Risk-based challenges kicked in when behavior looked unusual.</p>
<h3 id="how-we-made-it-concrete">How we made it concrete</h3>
<p>We put an access proxy in front of every internal application. Users hit the proxy over HTTPS, it checked identity and device state, evaluated policy, then decided whether to let traffic through. No VPN needed. Same control plane for every app. Developers could work from anywhere without us losing visibility or control.</p>
<p>Inside the network, services talked with authenticated identities, connections were encrypted end-to-end, and policies were enforced consistently. No team had to reinvent auth.</p>
<p> <a href="/blog/2016-12-19-securing-apis-authentication-authorization-patterns/"
   
   >API gateways</a>
 centralized authn/authz for external traffic, but backend services still validated the identity they received. That was non-negotiable. Trusting the gateway blindly would&rsquo;ve just recreated a perimeter inside the network. Defeats the entire point.</p>
<h3 id="getting-there-without-burning-everything-down">Getting there without burning everything down</h3>
<p>We didn&rsquo;t flip a switch. We started by mapping what we actually had. Asset inventory, data classification, real access patterns. What happened on the wire, not what the diagrams said.</p>
<p>Then we prioritized. Critical data first. High-risk access paths. Places where we could tighten things without rewriting the entire stack. Visibility came before enforcement. We ran in audit mode for weeks, watching what would get blocked, fixing false positives before they became outages.</p>
<p>Soft enforcement first. Hard enforcement only after we trusted the policies.</p>
<p>The cultural part was harder than the technical part. Developers had to treat service-to-service auth as normal work, not a security team annoyance. Ops had to run a more intentional network. Everyone had to understand <em>why</em> the extra checks existed. We explained the reasoning, automated everything we could, and made the secure path the easy path.</p>
<h3 id="legacy-systems-complexity-and-latency">Legacy systems, complexity, and latency</h3>
<p>Legacy systems. We had components that couldn&rsquo;t speak modern auth protocols. So we put proxies in front of them and drew tighter network boundaries as compensating controls. Not elegant. Effective.</p>
<p>Complexity scales fast. Without policy automation, consistent tooling, and solid observability, you drown. We invested early in all three. Not optional.</p>
<p>Performance concerns came up constantly. Engineers worried about latency from all the extra auth checks. Valid concern. We solved it with aggressive caching, efficient token validation, and connection reuse. The overhead ended up negligible.</p>
<p>The biggest risk is developer experience. If the secure path is painful, people route around it. Every time. So we built self-service tooling, published clear patterns, and made the defaults safe. Security that fights the developer loses.</p>
<h3 id="where-we-ended-up">Where we ended up</h3>
<p>There&rsquo;s no finish line. What we got at the fintech startup was a smaller blast radius, clear visibility into who accessed what and when, and an architecture that matched how distributed systems work instead of a perimeter pretending the world hasn&rsquo;t changed. We keep tightening it.</p>
]]></content:encoded></item><item><title>Two Years of Kubernetes in Production: The Boring Parts</title><link>https://lawzava.com/blog/2018-01-22-kubernetes-in-production-lessons-learned/</link><pubDate>Mon, 22 Jan 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-01-22-kubernetes-in-production-lessons-learned/</guid><description>Year two of running Kubernetes in production: network policies, DNS, resource requests, PodDisruptionBudgets, upgrades, and RBAC.</description><content:encoded><![CDATA[<p>Last Tuesday at 3 AM, I got paged because a node drain took out our entire API tier. All three replicas. Same node. No PodDisruptionBudget. I&rsquo;d been running Kubernetes in production for two years and still hadn&rsquo;t set one up for our most critical service. That&rsquo;s the thing about year two. You think you know the platform. You don&rsquo;t. You just know a different set of things than you did in year one.</p>
<p>I wrote about our  <a href="/blog/2017-01-16-kubernetes-in-production-year-of-lessons/"
   
   >first year with Kubernetes</a>
 a year ago. The tone was &ldquo;we survived and it was worth it.&rdquo; That&rsquo;s still true. But year two taught me that survival and maturity are very different things.</p>
<h3 id="the-networking-tax">The networking tax</h3>
<p>I said networking was hard last year. Understatement.</p>
<p>We started with a basic CNI and no network policies. Fine for a while. Then we added more services, more teams started deploying, and suddenly every pod could talk to every other pod. No segmentation. No audit trail of who talks to what. Fixing this retroactively was brutal. You can&rsquo;t just flip on network policies without mapping every legitimate connection first. We spent two weeks doing traffic analysis before we could write a single policy.</p>
<p>DNS was the other trap. Kube-dns worked great at our initial scale. Ten services, light traffic, no complaints. Then we added a data pipeline that hammered DNS with lookups on every request. Intermittent failures. Slow responses that looked like application bugs. Took us days to trace it back to DNS resolution under load. The fix was trivial: ndots configuration and a local DNS cache. The debugging wasn&rsquo;t.</p>
<p>Treat networking as its own project. First-class. With a budget. Not something you&rsquo;ll &ldquo;figure out later.&rdquo;</p>
<h3 id="resource-requests-stopped-being-guesses">Resource requests stopped being guesses</h3>
<p>Year one, our resource requests were vibes. Somebody would eyeball a service, pick a number, ship it. We got away with it because we had plenty of headroom.</p>
<p>Year two, the cluster got denser. Services started competing for resources. OOMKills on one service, wasted capacity on another. The scheduler was doing exactly what we told it to. The problem was that we told it garbage.</p>
<p>We started profiling every service under realistic load in staging. Measured actual memory and CPU usage over a week. Then set requests based on the P95, not the average, not a guess. Kept memory limits close to requests. Dropped CPU limits entirely unless a service had a known runaway pattern.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;100m&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;512Mi&#34;</span>
</span></span></code></pre></div><p>That YAML block looks boring. It&rsquo;s also the difference between a stable cluster and a 3 AM page. The numbers matter. Profile them.</p>
<h3 id="stateful-workloads-still-nope-mostly">Stateful workloads: still nope (mostly)</h3>
<p>We tried running Elasticsearch inside the cluster for about a month. Storage behavior was unpredictable. Backup and restore required custom tooling. Recovery after a node failure was slow and manual. We moved it back to managed infrastructure and haven&rsquo;t looked back.</p>
<p>Postgres and Redis stayed outside the cluster all of year two. I know people run stateful workloads on Kubernetes successfully. I also know it requires an operational investment we weren&rsquo;t ready to make. Managed services aren&rsquo;t free. They just shift the cost from your on-call to your invoice. For us, that trade was obviously correct.</p>
<h3 id="poddisruptionbudgets-the-lesson-i-learned-the-hard-way">PodDisruptionBudgets: the lesson I learned the hard way</h3>
<p>Back to that 3 AM page. A single node drain shouldn&rsquo;t be able to take down a production service. That&rsquo;s the whole point of running multiple replicas. But without a PodDisruptionBudget, Kubernetes will happily drain every pod from a node regardless of what service they belong to. If all your replicas happen to land on the same node (and the scheduler will do this if your anti-affinity rules are weak), one drain takes you to zero.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">policy/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">PodDisruptionBudget</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-pdb</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">minAvailable</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">selector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">api</span>
</span></span></code></pre></div><p>We now treat PDBs as part of the deployment contract. You don&rsquo;t get to ship a production service without one. Period.</p>
<h3 id="upgrades-arent-routine">Upgrades aren&rsquo;t routine</h3>
<p>Upgraded the cluster twice in year two. Both times were more work than expected.</p>
<p>Deprecations broke manifests. API versions we were using got dropped with one minor release warning. Node draining surfaced resource issues we&rsquo;d been ignoring. Control plane changes rippled into unexpected places. One upgrade changed default admission controller behavior and three deploys failed before we figured out why.</p>
<p>The process that worked: read every release note cover to cover. Rehearse in staging with production-like load. Not &ldquo;run the tests.&rdquo; Push real traffic through it. Schedule a real maintenance window. Don&rsquo;t YOLO a cluster upgrade on a Friday afternoon. (We did this once. Never again.)</p>
<p>Multiple clusters helped. We moved to a setup where we could upgrade one cluster while the other kept serving. Longer to provision, but the blast radius dropped to near zero.</p>
<h3 id="git-as-the-source-of-truth">Git as the source of truth</h3>
<p>This was already working in year one, but by year two it became non-negotiable culture. Every manifest in version control. Every deployment triggered by a merge. No <code>kubectl apply</code> from someone&rsquo;s laptop. No exceptions.</p>
<p>The payoff isn&rsquo;t just auditability. It&rsquo;s rollbacks. Something breaks, you revert a commit. No guessing what changed. No &ldquo;who ran what command in production.&rdquo; The answer is always in the git log. This single practice eliminated more incidents than any other operational improvement we made.</p>
<h3 id="rbac-and-secrets-the-wake-up-call">RBAC and secrets: the wake-up call</h3>
<p>Year one, everyone had cluster-admin. Year two, we finally locked it down. Created service accounts per application, scoped access narrowly, and spent an uncomfortable afternoon discovering how many things broke when we removed broad permissions. Every one of those breakages was a security hole we&rsquo;d been ignoring.</p>
<p>Secrets were the other wake-up call. Base64 isn&rsquo;t encryption. It&rsquo;s encoding. Anyone with API read access could decode every secret in the cluster. We moved to pulling secrets from Vault at deploy time and encrypting etcd at rest. Should have done it in month one. Didn&rsquo;t.</p>
<h3 id="java-dns-and-autoscaling">Java, DNS, and autoscaling</h3>
<p>Some workloads need special treatment. Our Java services ignored container memory limits by default and allocated memory like they owned the whole node. Took a few OOMKills to learn that the JVM needs explicit container-aware flags.</p>
<p>DNS bit us again here. Services making too many lookups created a bottleneck that looked like network latency. Horizontal pod autoscaling only worked correctly once our base resource requests were accurate. Garbage in, garbage scaling decisions out.</p>
<h3 id="what-id-tell-someone-starting-today">What I&rsquo;d tell someone starting today</h3>
<p>Use managed Kubernetes. Don&rsquo;t run your own control plane unless you have a very specific reason. GKE is mature, and AWS and Azure both announced managed offerings late last year. The time you save goes directly into product work.</p>
<p>Invest in developer experience from day one. The platform can be rock solid and still feel terrible if developers can&rsquo;t get a fast local feedback loop. Minikube, namespaced dev environments, whatever works. Budget time for it early.</p>
<p>Don&rsquo;t touch  <a href="/blog/2017-11-27-service-mesh-do-you-actually-need-one/"
   
   >service mesh</a>
 until the basics are boring. I&rsquo;ve seen teams adopt Istio before they had working health checks. Get networking, observability, and resource management right first. Then decide if you need the complexity.</p>
<h3 id="year-three">Year three</h3>
<p>The cluster is boring now. Boring in the good way. Deploys happen multiple times a day without drama. Incidents are rarer and smaller. The platform does what we need it to do.</p>
<p>But it took two full years of grinding to get here. None of that&rsquo;s glamorous. None of it makes for a good conference talk. It&rsquo;s just the work.</p>
<p>Kubernetes is a platform that rewards the teams willing to operate it seriously. Two years in, I&rsquo;m convinced it&rsquo;s the right foundation. I&rsquo;m also convinced that most of the value comes from the boring operational work that nobody wants to do.</p>
]]></content:encoded></item><item><title>Spectre and Meltdown: What We Patched in the First Five Days</title><link>https://lawzava.com/blog/2018-01-08-spectre-meltdown-first-five-days/</link><pubDate>Mon, 08 Jan 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-01-08-spectre-meltdown-first-five-days/</guid><description>Five days after the Spectre and Meltdown disclosure: what we patched at a fintech startup, what KPTI costs, and what it means for shared infrastructure.</description><content:encoded><![CDATA[<p>January 3rd. I&rsquo;m on holiday. Phone buzzes. Then buzzes again. Then doesn&rsquo;t stop.</p>
<p>Spectre and Meltdown dropped and I spent the next 48 hours glued to my laptop instead of doing literally anything else. First thing I did was SSH into every production box at the fintech startup. Not because I thought someone was actively exploiting this. Because I needed to know what we were exposed to. Fintech. Customer data. Financial feeds. The stakes don&rsquo;t leave room for &ldquo;I&rsquo;ll check Monday.&rdquo;</p>
<p>What made my stomach drop: these aren&rsquo;t software bugs. They&rsquo;re baked into the silicon. Speculative execution, the thing that makes modern CPUs fast, is the attack surface. Decades of chip design. Every major vendor. That&rsquo;s not a patch-and-forget situation.</p>
<h3 id="what-happened">What happened</h3>
<p>Meltdown lets unprivileged code read kernel memory. The CPU speculatively executes instructions before permission checks finish. The architectural state rolls back but the cache doesn&rsquo;t. So you can infer secrets from other processes. Neat trick if you&rsquo;re an attacker. Nightmare if you&rsquo;re running anything multi-tenant.</p>
<p>Spectre is worse in some ways. Two variants. One bypasses bounds checks to read past array limits. The other injects branch targets to hijack speculative execution. Cross-process data leakage. Affects Intel, AMD, ARM. Basically everything.</p>
<p>Taking part in cyber-defense exercises left me more scared of hardware vulnerabilities than software ones. Software you can patch, rewrite, isolate. Hardware you can mitigate with microcode and kernel workarounds, but the flaw stays until you physically replace the chip. And nation-states have known about classes of side-channel attacks for years. The fact that this is now public and weaponizable by anyone with a browser and some JavaScript should terrify every CTO reading this.</p>
<h3 id="what-we-patched-at-the-fintech-startup">What we patched at the fintech startup</h3>
<p>Patched the OS on every server: Linux kernel updates with KPTI for Meltdown. Spectre Variant 2 is messier. Google published its retpoline technique on the 4th, but kernels built with it haven&rsquo;t reached the stable trees yet, so that one is on the watch list. Pushed browser updates across the team because yes, JavaScript in a browser tab can exploit this. Then started the firmware conversation with our hosting providers, which is still ongoing because microcode updates require reboots and coordination.</p>
<p>Quick sanity check on every box:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep -q <span style="color:#e6db74">&#34;page_table_isolation=on&#34;</span> /proc/cmdline <span style="color:#f92672">&amp;&amp;</span> echo <span style="color:#e6db74">&#34;KPTI enabled&#34;</span>
</span></span><span style="display:flex;"><span>dmesg | grep -i retpoline
</span></span></code></pre></div><p>The first line only matches if someone forced KPTI on at boot. Patched kernels turn it on by default, so <code>dmesg | grep -i isolation</code> is the more reliable check. The retpoline line will stay empty until you&rsquo;re running a kernel built with it. If KPTI isn&rsquo;t showing up, you have work to do tonight.</p>
<p>Cloud providers are rebooting instances and patching hypervisors on their own schedules. If you&rsquo;re on AWS, GCP, Azure, check your provider&rsquo;s advisory page. Don&rsquo;t assume they&rsquo;ve already handled your instances. Verify.</p>
<h3 id="the-performance-hit-is-real">The performance hit is real</h3>
<p>KPTI isn&rsquo;t free. It adds overhead to every system call because the kernel page tables get unmapped from user space. Extra TLB flushes on every context switch. I/O heavy workloads get hit hardest. Databases especially.</p>
<p>We saw measurable regression on our PostgreSQL instances. Not catastrophic, but enough that I had to plan for additional capacity. If you&rsquo;re running near your limits already, this patch just pushed you over. Scale out now. Don&rsquo;t wait for the alerts.</p>
<p>Compute-heavy workloads with few syscalls? Barely noticeable. But most real applications aren&rsquo;t pure compute. They talk to databases, hit APIs, read files. The early reports say 5-30% overhead depending on workload. Measure your own systems. Benchmarks from Intel&rsquo;s blog are useless for your specific stack.</p>
<h3 id="the-bigger-problem">The bigger problem</h3>
<p>These vulnerabilities destroy a core assumption: that hardware isolation works. In multi-tenant environments, containers sharing a kernel, VMs sharing a host, a malicious neighbor can now potentially read your memory. That&rsquo;s cloud computing&rsquo;s dirty secret laid bare.</p>
<p>I&rsquo;m not saying abandon the cloud. I&rsquo;m saying: know where your sensitive data lives. If you&rsquo;re processing financial data on shared infrastructure with no dedicated tenancy, reconsider that decision today. Not next quarter.</p>
<p>Also think about hardware diversity. If your entire fleet is Intel, you just concentrated all your risk on one vendor&rsquo;s architecture. The next disclosure might hit differently. Spread the bets.</p>
<h3 id="this-isnt-over">This isn&rsquo;t over</h3>
<p>Spectre and Meltdown are the first. Not the last. Speculative execution is a rich attack surface and researchers are just getting started. Your patching pipeline needs to be fast, automated, and tested. If applying an emergency kernel update takes your team a week of manual work, that&rsquo;s the real vulnerability.  <a href="/blog/2017-05-15-wannacry-postmortem-lessons-for-every-organization/"
   
   >WannaCry</a>
 made the same point last year from the software side.</p>
<p>Future CPUs will have hardware fixes. But silicon refresh cycles are measured in years. Whatever you&rsquo;re running today, you&rsquo;re running for a while. Plan accordingly, and update your threat model: the CPU itself is now part of the attack surface. Welcome to 2018.</p>
]]></content:encoded></item><item><title>Container Security Beyond the Basics: What We Hardened</title><link>https://lawzava.com/blog/2017-12-04-container-security-beyond-the-basics/</link><pubDate>Mon, 04 Dec 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-12-04-container-security-beyond-the-basics/</guid><description>Containers share the host kernel, so they aren&amp;amp;rsquo;t a security boundary. How we hardened images, runtimes, network policies, and RBAC at a fintech startup.</description><content:encoded><![CDATA[<p>Containers ship code. They don&rsquo;t ship security. Harden the image, lock the runtime, segment the network, or accept that one escape owns your entire node.</p>
<h3 id="containers-arent-a-security-boundary">Containers Aren&rsquo;t a Security Boundary</h3>
<p>I need to say this bluntly because I keep hearing it wrong: a container isn&rsquo;t a VM. It isn&rsquo;t a sandbox. It shares a kernel with the host. If something breaks out, the blast radius is the whole node. Full stop.</p>
<p>At the fintech startup we learned this the hard way during an  <a href="/blog/2017-01-16-kubernetes-in-production-year-of-lessons/"
   
   >early Kubernetes migration</a>
. We had a service running as root inside its container (&ldquo;just for debugging,&rdquo; someone said), and a misconfigured volume mount gave it read access to the host&rsquo;s <code>/etc</code>. Nobody exploited it. We caught it in review. But it sat there for two weeks before anyone noticed, and that scared me more than an actual breach would have.</p>
<p>A principle from cyber-defense exercises that applies everywhere: assume the perimeter is already broken. Then design so that a breach in one layer doesn&rsquo;t cascade. Containers are just another layer. Treat them that way.</p>
<h3 id="ship-less-expose-less">Ship Less, Expose Less</h3>
<p>The single highest-ROI thing you can do for container security is shrink your images. Every package you include is attack surface. Every binary is a tool an attacker can use post-exploitation. Multi-stage builds are your best friend here.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">golang:1.9</span> <span style="color:#66d9ef">AS</span> <span style="color:#e6db74">builder</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span> <span style="color:#e6db74">/app</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> . .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> CGO_ENABLED<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> GOOS<span style="color:#f92672">=</span>linux go build -o service .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">alpine:3.6</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> adduser -D appuser<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span> <span style="color:#e6db74">appuser</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> --from<span style="color:#f92672">=</span>builder /app/service /service<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENTRYPOINT</span> [<span style="color:#e6db74">&#34;/service&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>This is almost exactly what we use at the fintech startup for our Go services. Builder stage compiles everything. Final image has Alpine, a non-root user, and the binary. That&rsquo;s it. No curl, no bash, no wget. If an attacker gets code execution inside this container, they have almost nothing to work with.</p>
<p>Two more things that matter:</p>
<p><strong>Pin to digests, not tags.</strong> <code>alpine:3.6</code> can change underneath you. <code>alpine@sha256:abc123...</code> can&rsquo;t. We had a CI build pass on Tuesday and fail on Thursday because the base image got a patch that broke our libc expectations. Tags are lies. Digests are truth.</p>
<p><strong>Scan continuously.</strong> Not just at build time. Vulnerabilities get published weeks or months after an image ships. We run Clair against our registry on a cron. Anything with a critical CVE gets flagged and the owning team has 48 hours to patch or explain why not.</p>
<h3 id="runtime-where-mistakes-become-incidents">Runtime: Where Mistakes Become Incidents</h3>
<p>A clean image means nothing if you run it with <code>--privileged</code>. I&rsquo;ve seen production deployments where containers had <code>SYS_ADMIN</code> capabilities because someone copy-pasted a Stack Overflow answer during a late-night deploy. That one capability basically gives you root on the host.</p>
<p>Strip everything. Start from zero and add back only what you need.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">runAsNonRoot</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">readOnlyRootFilesystem</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">capabilities</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">drop</span>: [<span style="color:#e6db74">&#34;ALL&#34;</span>]
</span></span></code></pre></div><p>Three lines of YAML. That&rsquo;s it. <code>runAsNonRoot</code> stops the &ldquo;just for debugging&rdquo; excuse. <code>readOnlyRootFilesystem</code> prevents an attacker from dropping binaries. Dropping all capabilities means no mount, no raw sockets, no ptrace. If your app genuinely needs a specific capability, like <code>NET_BIND_SERVICE</code> to bind port 443, add that one back explicitly and document why.</p>
<p>Pair this with seccomp profiles if your runtime supports them. Default Docker seccomp blocks around 44 syscalls. For most web services, you can block even more. We profiled our API gateway&rsquo;s syscall usage over a week, built a whitelist, and cut the available syscall surface by 60%.</p>
<p>The other half is detection. You need to know when something unexpected happens inside a container. A new shell process. An outbound connection to an IP that isn&rsquo;t in your dependency list. A binary that wasn&rsquo;t in the original image. We keep this simple: sysdig + alerting rules. Nothing fancy. But it works because we actually look at the alerts.</p>
<h3 id="network-segmentation-kill-flat-networks">Network Segmentation: Kill Flat Networks</h3>
<p>Default Kubernetes networking is flat. Every pod can talk to every other pod. This is the container equivalent of putting every server in the same VLAN with no firewall rules. Insane, but it&rsquo;s the default.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">networking.k8s.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">NetworkPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-allow-frontend</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">app</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">policyTypes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">Ingress</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">ingress</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">from</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">podSelector</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">matchLabels</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">app</span>: <span style="color:#ae81ff">frontend</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">port</span>: <span style="color:#ae81ff">8080</span>
</span></span></code></pre></div><p>This policy says: the API pod accepts traffic from frontend pods on port 8080. Everything else is denied. Lateral movement goes from trivial to hard: the attacker now has to compromise the frontend service too.</p>
<p>At the fintech startup we started by mapping every service-to-service dependency, then wrote deny-all policies and added explicit allows. Took about a week for our cluster. Found three services that were talking to things they had no business talking to, leftovers from an old feature that got half-removed. Network policies are a security tool and an architecture audit tool.</p>
<p> <a href="/blog/2017-11-27-service-mesh-do-you-actually-need-one/"
   
   >Service meshes</a>
 with mutual TLS are starting to appear (Istio, Linkerd), but they&rsquo;re early. We&rsquo;re watching, not adopting yet. For secrets, keep them out of your images. Kubernetes Secrets with encryption at rest (alpha since 1.7), or Vault if you want something more robust. Never <code>ENV</code> a database password into a Dockerfile. I&rsquo;ve seen it. In production. At companies you&rsquo;ve heard of.</p>
<h3 id="rbac-and-audit-logging">RBAC and Audit Logging</h3>
<p>Kubernetes RBAC isn&rsquo;t optional. Neither is PodSecurityPolicy. These are the controls that prevent a developer with deploy access from accidentally (or intentionally) escalating to cluster-admin. Scope permissions to namespaces. Give teams access to their stuff and nothing else.</p>
<p>Audit logging closes the loop. When something goes wrong, and it will, you need to answer: who did what, when, and to which resource. Without audit logs, incident response is guesswork. With them, it&rsquo;s a timeline.</p>
<h3 id="a-checklist-applied-every-time">A Checklist, Applied Every Time</h3>
<p>None of this is exotic. Slim images, pinned builds, non-root containers, read-only filesystems, dropped capabilities, network policies, RBAC, audit logs. It&rsquo;s a checklist. The hard part is doing it consistently, across every service, on every deploy, even when someone says &ldquo;we&rsquo;ll fix it after launch.&rdquo;</p>
<p>Stack these controls and a container escape becomes a dead end instead of a highway to your entire infrastructure.</p>
]]></content:encoded></item><item><title>Multi-Region Architecture: What I Wish Someone Had Told Me</title><link>https://lawzava.com/blog/2017-10-02-building-multi-region-applications/</link><pubDate>Mon, 02 Oct 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-10-02-building-multi-region-applications/</guid><description>What I learned evaluating multi-region at the fintech startup: the patterns that work, the ones that burn you, and when you should even bother.</description><content:encoded><![CDATA[<p>Multi-region buys you latency and resilience. It costs you sanity. Know which one you&rsquo;re trading before you start.</p>
<p>At the fintech startup we serve real-time financial data to users spread across Europe and increasingly beyond. Our backend runs in a single AWS region right now. It works. But I&rsquo;ve spent the last few months seriously evaluating what a multi-region setup would look like for us, and I want to lay out what I&rsquo;ve found: the architecture patterns and the ugly operational reality nobody warns you about.</p>
<h3 id="physics-doesnt-care-about-your-sla">Physics doesn&rsquo;t care about your SLA</h3>
<p>A request from London to <code>eu-west-1</code> is fast. A request from Singapore to <code>eu-west-1</code> isn&rsquo;t. We&rsquo;re talking close to 200ms of round-trip network latency before your application code even runs. Stack a few API calls on top of that, add a database query, and suddenly the page takes two seconds to load. Users don&rsquo;t file bug reports for slow pages. They just leave.</p>
<p>I measured this myself with a cheap VPS in each continent. London to Dublin: ~10ms. Tokyo to Dublin: ~240ms. That&rsquo;s a completely different product experience.</p>
<h3 id="three-patterns-three-sets-of-problems">Three patterns, three sets of problems</h3>
<p><strong>Active-passive</strong> is the one everyone starts with. Your primary region handles all traffic. A standby region sits there with replicated data, waiting. You get disaster recovery. You don&rsquo;t get latency improvement for remote users. The tricky part is failover. If you haven&rsquo;t rehearsed it, your &ldquo;30-second failover&rdquo; is actually a 45-minute scramble at 3am with someone SSHing into the wrong box.</p>
<p><strong>Active-active</strong> is the dream. Multiple regions serving traffic simultaneously, users routed to the nearest one. Fast everywhere. But you&rsquo;ve just signed up for distributed consensus problems. Two users updating the same record in two regions at the same time? Now you need conflict resolution. Last-write-wins is easy to implement and will silently eat data. Domain-specific merge logic is correct and will take you months to build. Pick your pain.</p>
<p><strong>Read-local, write-central</strong> is what I keep coming back to for our use case. Reads hit the nearest region. Writes go to a single primary. For a read-heavy system like ours, where users consume financial content far more than they create it, this is the sweet spot. Remote users still eat write latency, but writes are a small fraction of our traffic. The tradeoff is acceptable.</p>
<h3 id="data-is-where-it-gets-ugly">Data is where it gets ugly</h3>
<p>Every multi-region discussion eventually becomes a data discussion, and this is where the CAP theorem stops being academic and starts making your on-call engineer cry.</p>
<p>Async replication is the pragmatic choice. Your writes are fast because they don&rsquo;t wait for the replica. But a user who just updated their watchlist and immediately reads it back might see stale data. For financial content, that&rsquo;s bad. For user preferences, it&rsquo;s annoying but survivable. You have to know your data well enough to make that call per table, sometimes per column.</p>
<p>Sync replication gives you consistency. It also means every write waits for a cross-region round trip. One slow region drags the whole system down. I&rsquo;ve seen this kill write throughput in practice. Unless your write volume is very low and your tolerance for tail latency is very high, avoid it.</p>
<p>The approach I like best is <strong>regional data affinity</strong>. A UK user&rsquo;s data lives in the EU region. Period. We only replicate the things that truly need to be everywhere: shared reference data, global config, that sort of thing. This sidesteps most consistency headaches because you&rsquo;re not actually doing multi-master for user data. You&rsquo;re doing single-master per user, distributed across regions. Much simpler failure modes.</p>
<h3 id="getting-users-to-the-right-place">Getting users to the right place</h3>
<p>DNS geo-routing is the obvious first step. Route53 does it, Cloudflare does it, everyone does it. It works well enough. The catch is DNS caching. Resolvers don&rsquo;t always honor your TTLs, and ISPs will cache your records for longer than you want, which means failover via DNS is slower than you&rsquo;d hope. Minutes, not seconds.</p>
<p>Anycast is faster for failover but harder to operate. You need BGP-level control. Most startups, us included, don&rsquo;t have the network engineering chops for this. It&rsquo;s the right answer at scale. It&rsquo;s premature complexity at our stage.</p>
<p>CDNs solve the easy part (static assets, edge caching) and punt on the hard part (dynamic requests still need to reach an origin). Worth doing regardless of your multi-region strategy, but don&rsquo;t confuse a CDN with actual multi-region architecture.</p>
<h3 id="the-operational-tax-nobody-budgets-for">The operational tax nobody budgets for</h3>
<p>Here&rsquo;s where most multi-region proposals die, and most of them should.</p>
<p><strong>Deployments</strong> get harder. You can&rsquo;t just <code>kubectl apply</code> and walk away. You need to decide: do you deploy to all regions simultaneously? Serially? Use one region as a canary? We&rsquo;d probably do serial deployment with the secondary region first, watch it for 15 minutes, then roll to primary. That means every deploy takes longer. Every rollback is more complex. Your CI/CD pipeline just doubled in scope.</p>
<p><strong>Monitoring</strong> has to be per-region AND global. You need to know that latency is high in <code>ap-southeast-1</code> specifically, not just that &ldquo;p99 latency is elevated somewhere.&rdquo; If your monitoring runs in the same region as your primary, then when that region dies, so does your ability to see that it died.</p>
<p><strong>Failover</strong> is a muscle you have to keep exercising. We do  <a href="/blog/2017-08-21-chaos-engineering-for-mortals/"
   
   >game days</a>
 at the fintech startup for our current single-region setup, and even those surface surprises. A multi-region failover you&rsquo;ve never tested is a multi-region failover that doesn&rsquo;t work. Full stop. The first time you practice it, something will break that you didn&rsquo;t expect. Better to find that on a Tuesday afternoon than during an actual incident.</p>
<h3 id="when-multi-region-is-worth-it">When multi-region is worth it</h3>
<p>Most teams shouldn&rsquo;t. Not yet.</p>
<p>If your users are in one geography and your uptime requirements are met by a single region with good practices (multi-AZ, proper backups, tested restores), multi-region is complexity you&rsquo;re borrowing against future needs. That debt has interest.</p>
<p>For us at the fintech startup, the calculus is shifting. We&rsquo;re getting real users in Asia.  <a href="/blog/2017-02-27-preparing-for-gdpr-technical-checklist/"
   
   >GDPR</a>
 takes effect next May and already shapes where we plan to keep certain data. And our customers are making trading decisions based on our content, so downtime costs them money, not just patience. We&rsquo;ll probably go read-local, write-central within the next year. But I&rsquo;m going in with my eyes open about what it costs.</p>
<p>Build your system so migration is possible. Abstract your data layer. Don&rsquo;t hardcode region assumptions. Use infrastructure-as-code so spinning up a new region isn&rsquo;t a six-week project. Then wait until the numbers actually justify the move.</p>
<p>The worst multi-region architectures I&rsquo;ve seen were built by teams who wanted the résumé bullet point. The best were built by teams who had no choice.</p>
]]></content:encoded></item><item><title>The Business Case for Infrastructure: Pitch It in Money</title><link>https://lawzava.com/blog/2017-09-04-the-business-case-for-infrastructure-investment/</link><pubDate>Mon, 04 Sep 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-09-04-the-business-case-for-infrastructure-investment/</guid><description>Boards fund money, risk, and speed. How I pitched infrastructure investment at a fintech startup: business framing, rough ROI, and a skimmable proposal.</description><content:encoded><![CDATA[<p>Nobody on your board woke up thinking about database migrations. They woke up thinking about revenue, burn rate, and whether the next funding round will close. If you walk into that room talking about latency percentiles, you&rsquo;ve already lost.</p>
<p>I learned this the hard way at the fintech startup. We needed a serious infrastructure overhaul. I knew it. My team knew it. But our investors didn&rsquo;t speak in uptime and throughput. They spoke in money. So I had to learn their language.</p>
<h3 id="talk-money-or-dont-talk">Talk money or don&rsquo;t talk</h3>
<p>Every infra pitch boils down to one of four things: it makes money, saves money, reduces risk, or lets us ship faster. That&rsquo;s it. Pick the frame that fits and lead with it. Not with the technology. Not with the architecture diagram. With the business outcome.</p>
<p>At the fintech startup, we were heading into a period where marketing had big traffic projections. Our system couldn&rsquo;t handle it. I didn&rsquo;t pitch &ldquo;we need to re-architect our data pipeline.&rdquo; I pitched &ldquo;we&rsquo;ll lose paying users in Q3 if we don&rsquo;t act now.&rdquo; Different sentence. Same project. Completely different reaction in the room.</p>
<h3 id="risk-is-the-easiest-sell">Risk is the easiest sell</h3>
<p>When downside is obvious, risk framing writes itself. Quantify the probability of an outage. Estimate the cost. Multiply. That number gets attention fast because nobody wants to be the one who ignored a preventable disaster.</p>
<p>Growth constraints work too, but you need credible projections. Vague &ldquo;we might need to scale&rdquo; doesn&rsquo;t cut it. Show the wall you&rsquo;re about to hit and the revenue sitting on the other side.</p>
<p>Velocity framing is underrated. If your  <a href="/blog/2016-06-06-continuous-deployment-without-chaos/"
   
   >deploys take two hours</a>
 and engineers sit idle waiting, that&rsquo;s burned salary. Translate it. &ldquo;We&rsquo;re losing 40 engineer-hours a week to deployment bottlenecks&rdquo; hits harder than &ldquo;our CI/CD pipeline needs work.&rdquo;</p>
<h3 id="do-the-math-even-if-its-rough">Do the math, even if it&rsquo;s rough</h3>
<p>A proposal without numbers is just an opinion. I&rsquo;ve seen smart engineers lose budget requests because they couldn&rsquo;t answer &ldquo;what does this cost us today?&rdquo; Document the current state in dollars and hours, the same way you&rsquo;d  <a href="/blog/2016-02-22-the-true-cost-of-technical-debt/"
   
   >price technical debt</a>
. Then describe what changes, in the same units.</p>
<p>Keep the ROI dead simple. Investment. Expected annual benefit. Payback period. If your assumptions are honest and explicit, rough math beats no math every single time.</p>
<h3 id="structure-for-skimmers">Structure for skimmers</h3>
<p>Executives don&rsquo;t read proposals. They skim them. One paragraph up top: what you want, why, and what it&rsquo;s worth. Then a short problem statement in business terms. Proposed solution with minimal technical detail. Quantified impact. Cost. Timeline. Risks and mitigations. Alternatives you considered and rejected.</p>
<p>That order matters. Bury the ask on page three and it never gets read.</p>
<h3 id="where-ive-seen-pitches-die">Where I&rsquo;ve seen pitches die</h3>
<p>Being too technical. Your CFO doesn&rsquo;t need to understand container orchestration. They need to understand what it changes for the business.</p>
<p>Skipping quantification. &ldquo;Better reliability&rdquo; means nothing. &ldquo;$200K in avoided incident costs per year&rdquo; means something.</p>
<p>Ignoring tradeoffs. If this delays a feature, say so. Explain why the trade is still worth it. Pretending there&rsquo;s no cost makes you look naive.</p>
<p>Asking for everything at once. Phase it. Get a win. Use that win to fund the next phase.</p>
<p>And the killer: not following up. If you don&rsquo;t report results after getting funded, you&rsquo;ve just burned your credibility for the next ask.</p>
<h3 id="keep-the-conversation-going">Keep the conversation going</h3>
<p>Don&rsquo;t wait for budget season. Share metrics regularly. When an incident happens, quantify the damage. When delivery speeds up after an investment, show the numbers. Make infrastructure value visible all the time, so the next request feels like an obvious yes instead of a surprise ask.</p>
<p>Infrastructure is a business investment. Treat it like one and it gets funded. Treat it like a technical hobby and it doesn&rsquo;t.</p>
]]></content:encoded></item><item><title>Hidden AWS Costs: Why the Bill Doesn't Match the Calculator</title><link>https://lawzava.com/blog/2017-07-03-hidden-costs-of-cloud-services/</link><pubDate>Mon, 03 Jul 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-07-03-hidden-costs-of-cloud-services/</guid><description>Our AWS bill came in near double the calculator. Where the money went (transfer, storage, zombies, RDS) and what cut it: tags, weekly reviews, RIs, spot.</description><content:encoded><![CDATA[<p>I opened our AWS bill last month and thought there was an error. We had budgeted for compute. What showed up was compute plus a zoo of line items I never agreed to. Data transfer fees. Storage request charges. Snapshot costs for things nobody remembered creating. The bill was nearly double what the pricing calculator said it would be.</p>
<p>This wasn&rsquo;t the first time. At the fintech startup we process a lot of financial data. At a mobility startup we were shuffling GPS pings across regions. Both times the same gut punch: the pricing page and the invoice are two completely different documents. Last year I argued that  <a href="/blog/2016-07-05-the-real-cost-of-running-your-own-servers/"
   
   >the cloud beats colocation for a startup</a>
. It still does, but only if you read the bill.</p>
<h3 id="the-pricing-page-is-a-fantasy">The pricing page is a fantasy</h3>
<p>Every team I&rsquo;ve worked with starts the same way. You go to the AWS calculator, punch in your instance types, maybe add an RDS box, and land on a number that feels reasonable. Then the invoice arrives and it&rsquo;s stuffed with charges you never consciously chose.</p>
<p>That&rsquo;s by design. AWS is dozens of services, each with its own meter running.</p>
<h3 id="where-the-money-goes">Where the money goes</h3>
<p><strong>Data transfer.</strong> This one got us at the fintech startup. We were pulling market data, processing it, and pushing results to users. Egress charges per gigabyte. Cross-AZ traffic. NAT gateway fees. None of it obvious until the bill. A workload that looks cheap on compute can bleed money on transfer alone.</p>
<p><strong>Storage that never stops billing.</strong> You pay for capacity, sure. But also for provisioned IOPS, for every snapshot that piles up because nobody set a retention policy, for every API call against S3. Cold storage looks cheap until you actually need to retrieve something. Then the retrieval fees hit.</p>
<p><strong>Zombie resources.</strong> Stopped instances still bill for their EBS volumes. Elastic IPs sitting unused. Load balancers running with nothing behind them. Serverless invocations that seem free individually but cost real money at scale. I&rsquo;ve found forgotten dev environments running for months. Months.</p>
<p><strong>Managed databases.</strong> RDS is convenient but expensive. Multi-AZ doubles your compute and storage cost. Backups cost money. Provisioned IOPS costs more money. ElastiCache bills per node hour whether your hit rate is 99% or 5%. DynamoDB charges for capacity units and storage separately.</p>
<p><strong>Support and monitoring.</strong> AWS support is a percentage of your total bill. Your bill goes up, support goes up. CloudWatch charges for custom metrics, log ingestion, dashboards. Config rules cost per active rule. Observability isn&rsquo;t free. Not even close.</p>
<h3 id="tag-everything-or-drown">Tag everything or drown</h3>
<p>Before you optimize anything, you need to know what costs what. The answer is tagging. Boring, tedious, absolutely essential.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">Resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">MyInstance</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">Type</span>: <span style="color:#ae81ff">AWS::EC2::Instance</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">Properties</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">Tags</span>:
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">Key</span>: <span style="color:#ae81ff">Environment</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">Value</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">Key</span>: <span style="color:#ae81ff">Team</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">Value</span>: <span style="color:#ae81ff">platform</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">Key</span>: <span style="color:#ae81ff">Service</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">Value</span>: <span style="color:#ae81ff">api</span>
</span></span></code></pre></div><p>Tag every resource. Environment, team, service. No exceptions. If something isn&rsquo;t tagged, you can&rsquo;t attribute the cost, and if you can&rsquo;t attribute it, nobody owns it. Unowned costs grow forever.</p>
<p>Set up weekly cost reviews. Not monthly. By the time a monthly review catches a spike, you have already burned four weeks of money. Set budget alerts. Use Cost Explorer daily. Make cost visible to the people creating the resources.</p>
<h3 id="what-saves-money">What saves money</h3>
<p><strong>Reserved instances</strong> for anything with a stable baseline. Don&rsquo;t over-commit. One-year terms first. Only go multi-year when you&rsquo;re dead certain the workload isn&rsquo;t moving.</p>
<p><strong>Spot instances</strong> for batch jobs, CI builds, anything that can handle interruption. We used these heavily for data processing at the fintech startup. Massive savings, but you have to design for the instance disappearing mid-job.</p>
<p><strong>Right-sizing.</strong> This is the boring, never-ending work. Pull actual CPU and memory utilization, compare to what you&rsquo;re paying for, downsize. Most instances are over-provisioned because someone picked a size &ldquo;just in case&rdquo; and never looked again.</p>
<p><strong>Cleanup.</strong> Delete unused snapshots. Remove unattached volumes. Kill forgotten dev environments. Set lifecycle policies on S3. This isn&rsquo;t glamorous work. It saves real money.</p>
<p><strong>Architecture decisions.</strong> Keep compute and data in the same region. Use CloudFront for static content. Route S3 traffic through a VPC endpoint instead of a NAT gateway. These choices compound.</p>
<h3 id="make-cost-someones-problem">Make cost someone&rsquo;s problem</h3>
<p>The single biggest thing you can do is make cost visible and owned. A shared dashboard where every team sees their spend. A weekly review where you discuss it like you discuss uptime. Cost as a non-functional requirement, right next to  <a href="/blog/2016-12-12-production-monitoring-metrics-that-matter/"
   
   >latency and availability</a>
.</p>
<p>Nobody optimizes what nobody sees. Make it visible. Make it owned. That&rsquo;s the whole trick.</p>
]]></content:encoded></item><item><title>Kubernetes in Production: What Paid Off and What Bit Us</title><link>https://lawzava.com/blog/2017-01-16-kubernetes-in-production-year-of-lessons/</link><pubDate>Mon, 16 Jan 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-01-16-kubernetes-in-production-year-of-lessons/</guid><description>Running Kubernetes in production: what paid off, what bit us (networking, secrets, YAML sprawl), and who should adopt it.</description><content:encoded><![CDATA[<p>We moved our first workload to Kubernetes a while back. The pitch was simple: declarative deployments, self-healing services, no more SSH-into-prod-and-pray. I can now confirm all of that&rsquo;s true. I can also confirm that the path there involved a DNS outage at 2am, a deployment that silently ate 4GB of RAM, and me reading the kube-proxy source code on a Saturday.</p>
<p>Worth it? Yes. Painless? Not even close.</p>
<h3 id="the-stuff-that-pays-off-immediately">The stuff that pays off immediately</h3>
<p>The declarative model is the real win. You describe what you want, Kubernetes converges toward it. No more runbooks for &ldquo;what if the service dies on box 3.&rdquo; It just comes back. Deployments get boring, which is exactly what you want deployments to be.</p>
<p>Resource requests and limits are the other quick win, but only once you actually set them properly. We ran for weeks without limits on one of our data ingestion services. It worked great until it didn&rsquo;t, and then it took down two neighbors on the same node.</p>
<p>Don&rsquo;t skip this part:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;250m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;256Mi&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;500m&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;512Mi&#34;</span>
</span></span></code></pre></div><p>Those numbers are a starting point. Profile under real load. Revisit quarterly. The defaults are lies.</p>
<p>Rolling updates deserve a mention too. Going from &ldquo;schedule maintenance window, hold breath, deploy, pray&rdquo; to incremental rollouts with automatic rollback changed how the whole team thinks about shipping. People deploy more often because it feels safe. That feedback loop alone justified the migration.</p>
<h3 id="where-it-bites-you">Where it bites you</h3>
<p><strong>Networking.</strong> Every single time.</p>
<p>Service discovery looks dead simple in the docs. Then you hit DNS caching issues, or a network policy blocks something you didn&rsquo;t expect, or your ingress controller does something creative with header forwarding. Kubernetes hides the network behind a nice abstraction until that abstraction leaks, and then you&rsquo;re reading iptables rules at midnight.</p>
<p>We had a bug where intermittent 503s on one service turned out to be a kube-dns cache TTL mismatch with our upstream provider. Took three days to find. The fix was two lines of config. That&rsquo;s Kubernetes networking in a nutshell.</p>
<p><strong>Resource tuning</strong> is the slow burn. Set requests too low and the scheduler packs your nodes like a clown car, then starts evicting pods when things get tight. Set limits too low and your service gets CPU-throttled under load, which looks exactly like an application bug from the outside. The only way through is profiling under production traffic patterns and adjusting. There&rsquo;s no shortcut here.</p>
<p><strong>Stateful workloads</strong> are possible, but we have kept Postgres and Redis outside the cluster so far. Storage classes, PV lifecycle, backup and restore: it all works, but the operational surface area is large. If you&rsquo;re just getting started, keep your databases where they are. Move them in later when you actually understand what you&rsquo;re signing up for.</p>
<p><strong>Debugging gets weird.</strong> Something fails and the cause could be the application code, the container image, the scheduler, the network plugin, the node itself, or some combination. I&rsquo;ve seen a deployment fail because a node had a full disk from container image garbage collection not running. Good luck finding that without decent logging and tracing. <code>kubectl describe</code> is your best friend, but sometimes your best friend doesn&rsquo;t know the answer either.</p>
<h3 id="the-hard-lessons">The hard lessons</h3>
<p>Treating Kubernetes like a black box works until you need to upgrade the cluster. Or recover from an etcd failure. Or figure out why the scheduler won&rsquo;t place a pod. Someone on your team needs to actually understand the control plane. Not &ldquo;watched a conference talk&rdquo; understand. &ldquo;Can read the API server logs and make sense of them&rdquo; understand.</p>
<p>Running your own control plane is expensive, and the currency is attention more than money. If your cloud provider offers managed Kubernetes, take the deal. We spent months on control plane operations that could have been spent on product work. Google Container Engine is solid enough now. If your provider has no managed option yet, budget real engineering time for the control plane instead of pretending it&rsquo;s free.</p>
<p>YAML sprawl is the death of a thousand paper cuts. You start with clean manifests for three services. Six months later you have forty files that are 80% identical and nobody remembers which copy is canonical. Helm helps, but be careful. I&rsquo;ve seen teams replace YAML sprawl with template sprawl, which is somehow worse because now you need to understand Go templates to deploy a config change.</p>
<p>Secrets handling is genuinely bad out of the box. Kubernetes Secrets are base64-encoded. That&rsquo;s encoding, not encryption. Anyone with API access can read them. We ended up pulling secrets from Vault at deploy time and treating the built-in Secrets as a transport layer. If you&rsquo;re storing database passwords as Kubernetes Secrets and calling it done, stop.</p>
<p>Local development is the thing nobody budgets time for. Your cluster isn&rsquo;t a laptop. Developers need fast feedback without blowing up shared environments. Minikube and isolated namespaces help, but they need investment. We lost a solid week of productivity before we built a decent local dev story.</p>
<h3 id="habits-that-actually-stuck">Habits that actually stuck</h3>
<p>Git is the source of truth. Every manifest in version control. Deployments triggered by merges. No <code>kubectl apply</code> from someone&rsquo;s laptop in production. This gives you an audit trail, makes rollbacks trivial, and kills snowflake configurations dead.</p>
<p>Health checks as production code. A bad liveness probe will kill your service faster than a bad deploy. We had a probe that checked a database connection with a 1-second timeout. Database had a slow moment, probe failed, Kubernetes restarted the pod, pod came up and hit the still-slow database, probe failed again. Restart loop. Cascading failure from a health check. Write your probes carefully.</p>
<p>Kubernetes-aware monitoring. Watching application metrics is necessary but not sufficient. You need to see what the scheduler is doing, how the nodes look, what the control plane health is. Without that, incidents are guesswork. With it, incidents become analysis.</p>
<h3 id="should-you-adopt-it">Should you adopt it?</h3>
<p>If you have multiple services, deploy frequently, and need real scheduling and resilience, yes. The investment pays off.</p>
<p>If you have a small team,  <a href="/blog/2016-01-15-why-microservices-arent-always-the-answer/"
   
   >a monolith</a>
, or two services behind a load balancer, probably not yet. A  <a href="/blog/2016-10-17-container-orchestration-swarm-kubernetes-mesos/"
   
   >simpler orchestrator like Swarm</a>
 is a gentler start. Kubernetes doesn&rsquo;t remove operational complexity. It trades one kind for another, and the new kind requires specific expertise. A team that&rsquo;s struggling with basic deployments won&rsquo;t be saved by Kubernetes. They&rsquo;ll just struggle with more abstraction layers.</p>
<h3 id="where-it-leaves-me">Where it leaves me</h3>
<p>Kubernetes feels less like magic now and more like a system I can reason about. That&rsquo;s the milestone that matters: I understand why it works, and I understand why it breaks.</p>
<p>The teams I&rsquo;ve seen succeed treat Kubernetes as a product they operate, with on-call, with runbooks, with regular investment. The teams that struggle treat it like something they installed once and forgot about. Same as any other infrastructure, really. The tool doesn&rsquo;t care about your intentions. It cares about your configuration.</p>
]]></content:encoded></item><item><title>Why Every Developer Should Understand Networking</title><link>https://lawzava.com/blog/2016-09-19-why-every-developer-should-understand-networking/</link><pubDate>Mon, 19 Sep 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-09-19-why-every-developer-should-understand-networking/</guid><description>Three production bugs (DNS caching, TIME_WAIT port exhaustion, shallow health checks) that basic networking knowledge prevents, and where to start.</description><content:encoded><![CDATA[<p>Learn how DNS, TCP, and load balancers actually work, or keep debugging the wrong layer at 2 AM.</p>
<h3 id="the-network-isnt-magic">The network isn&rsquo;t magic</h3>
<p>I keep having the same conversation at a mobility startup. A developer opens a ticket: &ldquo;the API is randomly slow.&rdquo; I ask them to run <code>ss -tan</code> on the box. Blank stare. They have never heard of socket states.</p>
<p>This isn&rsquo;t a knowledge gap I can ignore. If you write software that talks over a network (and in 2016, that&rsquo;s almost all software), networking is part of your job. Not optional. Not &ldquo;nice to have.&rdquo; Part of the job.</p>
<h3 id="three-bugs-that-keep-showing-up">Three bugs that keep showing up</h3>
<p><strong>DNS caching gone wrong.</strong> We had a service that cached DNS lookups forever because the HTTP client&rsquo;s default resolver never respected TTL. We rotated an IP behind a load balancer, and that one service kept hammering the old address for hours. The fix was two lines of config. The outage was forty-five minutes of confusion because nobody thought to check resolution.</p>
<p><strong>TIME_WAIT exhaustion.</strong> A microservice opened a new TCP connection for every request to our mapping provider. Under load, the box ran out of ephemeral ports. Connections backed up, timeouts cascaded, and the vehicle unlock flow broke. The developer who wrote it had no idea that TCP connections linger in TIME_WAIT after close.  <a href="/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/"
   
   >Connection pooling</a>
 fixed it. Understanding why connection pooling matters would have prevented it.</p>
<p><strong>Load balancer health checks.</strong> We added a new backend behind an Nginx upstream. The  <a href="/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/"
   
   >health check</a>
 was hitting a path that returned 200 even when the database was unreachable. Traffic routed to a box that couldn&rsquo;t serve real requests. Fifteen minutes of partial outage because nobody understood what the load balancer was actually checking.</p>
<p>None of these required deep packet analysis. None required a networking degree. They required knowing the basics well enough to ask the right question.</p>
<h3 id="why-developers-avoid-it">Why developers avoid it</h3>
<p>Networking feels like someone else&rsquo;s problem. There&rsquo;s an infrastructure team, or a cloud provider, or an abstraction layer that&rsquo;s supposed to handle it. And those things help. But abstractions leak. They always leak. When they do, the developer staring at the logs needs to know which layer is broken.</p>
<p>&ldquo;It works on my machine&rdquo; is almost always a networking statement. Different DNS, different routes, different timeouts, different connection behavior. If you can&rsquo;t reason about the network, you can&rsquo;t debug the gap between your laptop and production.</p>
<h3 id="where-to-start">Where to start</h3>
<p>You don&rsquo;t need to read RFCs. You need a working mental model. These resources got our team there:</p>
<ul>
<li><strong>&ldquo;TCP/IP Illustrated, Volume 1&rdquo; by Stevens.</strong> Dense but precise. Read the chapters on TCP connection states and you will understand half the production issues you have ever seen.</li>
<li><strong>Julia Evans&rsquo; blog posts on networking and debugging tools.</strong> Approachable and surprisingly deep.</li>
<li><strong><code>ss</code>, <code>dig</code>, <code>curl -v</code>, <code>tcpdump</code>.</strong> Run them. Break things on purpose. Watch what happens when you kill a connection mid-handshake or poison a DNS cache.</li>
</ul>
<p>The network isn&rsquo;t going to become simpler. The developers who understand it will debug faster, design better systems, and stop filing tickets that say &ldquo;the API is randomly slow.&rdquo;</p>
]]></content:encoded></item><item><title>Log Aggregation at Scale: ELK vs Alternatives</title><link>https://lawzava.com/blog/2016-09-05-log-aggregation-at-scale-elk-vs-alternatives/</link><pubDate>Mon, 05 Sep 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-09-05-log-aggregation-at-scale-elk-vs-alternatives/</guid><description>ELK is powerful and a second full-time job. What running it at a mobility startup taught me, and the hosted or simpler options I&amp;amp;rsquo;d pick instead.</description><content:encoded><![CDATA[<p>Once you have more than a handful of services, SSH-and-grep stops working. A single user request at a mobility startup touches the mobile API, the fleet service, the payment layer, and at least two background workers. When something breaks, I need to search one place for all the related events. That&rsquo;s the baseline.</p>
<p>So we set up  <a href="/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/"
   
   >ELK</a>
. Elasticsearch, Logstash, Kibana. The pitch was compelling: open source, flexible, great full-text search, a plugin for everything. We stood up a three-node cluster, pointed Logstash at it, gave the team Kibana dashboards. For the first few weeks, it felt like a superpower.</p>
<p>Then the cluster started misbehaving. And I spent the next several months learning a painful lesson about the gap between &ldquo;powerful&rdquo; and &ldquo;worth the operational cost.&rdquo;</p>
<p><strong>If you don&rsquo;t have someone who wants to babysit Elasticsearch full time, don&rsquo;t run ELK yourself.</strong> Hosted Elasticsearch, Graylog, or even Splunk will save you more engineering hours than they cost.</p>
<h3 id="the-operational-tax-nobody-warns-you-about">The Operational Tax Nobody Warns You About</h3>
<p>Elasticsearch isn&rsquo;t a database you deploy and forget. It&rsquo;s a distributed system that demands constant attention. Shard allocation, index rotation and retention, JVM heap tuning, split-brain prevention, disk watermarks. Every one of these will bite you, and they will bite you at 3 AM.</p>
<p>At the mobility startup, our log volume was moderate. Maybe a few gigabytes a day. Nothing that should stress a three-node cluster. But Elasticsearch doesn&rsquo;t care about your expectations. It cares about index design, merge policies, and whether you remembered to set <code>bootstrap.mlockall</code>. We spent more time keeping the logging infrastructure healthy than we spent on some of our actual product services.</p>
<p>The worst part is that when your logging system goes down, you  <a href="/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/"
   
   >lose visibility into everything else</a>
 at the same time. Your safety net disappears exactly when you need it most.</p>
<h3 id="what-elk-actually-gets-right">What ELK Actually Gets Right</h3>
<p>I&rsquo;m not going to pretend it&rsquo;s all bad. Elasticsearch&rsquo;s search is genuinely excellent. When the cluster is healthy, the ability to run arbitrary queries across millions of log lines with sub-second response times is hard to match. Logstash can parse almost any log format into structured fields. Kibana dashboards gave our product team visibility they never had before.</p>
<p>The ecosystem is real. Beats shippers are lightweight, community plugins cover most integrations, and the documentation is solid. If you have the operational muscle, ELK is the most flexible open source logging stack available.</p>
<h3 id="what-id-do-differently">What I&rsquo;d Do Differently</h3>
<p>If I were starting over, I wouldn&rsquo;t self-host Elasticsearch for logging. Full stop.</p>
<p><strong>Hosted Elasticsearch</strong> removes the worst of the operational burden. You keep the same query model, the same Kibana dashboards, the same integrations. You lose some control over cluster configuration and you pay more per gigabyte, but you gain back the engineering hours you were burning on cluster babysitting. For most teams, that tradeoff is obvious.</p>
<p><strong>Graylog</strong> is worth a look if you want Elasticsearch search without the full DIY build. It wraps Elasticsearch in a more opinionated log management experience with built-in alerting and stream routing. Less flexible than raw ELK, but faster to get running and easier to keep running.</p>
<p><strong>Splunk</strong> is the enterprise answer. Powerful, mature, battle-tested. Also expensive enough to make your finance team flinch. If budget isn&rsquo;t the constraint, Splunk is a safe bet. For a startup, it rarely makes sense.</p>
<p><strong>Cloud provider logging</strong> is the lowest-effort option. AWS CloudWatch Logs, for example, integrates deeply with everything else in AWS and requires zero operational overhead. The query capabilities are basic compared to Elasticsearch, but basic is often enough. You can always export to something more powerful later.</p>
<h3 id="structured-logging-matters-more-than-the-tool">Structured Logging Matters More Than the Tool</h3>
<p>Regardless of which aggregation tool you pick, the single best investment is structured logging. A JSON log line with consistent fields turns debugging from archaeology into search.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;timestamp&#34;</span>: <span style="color:#e6db74">&#34;2016-09-05T10:23:45Z&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;level&#34;</span>: <span style="color:#e6db74">&#34;error&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;payment-api&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;trace_id&#34;</span>: <span style="color:#e6db74">&#34;abc123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Payment processing failed&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;error&#34;</span>: <span style="color:#e6db74">&#34;Connection timeout&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;customer_id&#34;</span>: <span style="color:#e6db74">&#34;cust_456&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Carry a trace ID through every service. Use consistent field names. Emit JSON instead of free-form text. These decisions pay off no matter what sits behind the ingestion pipeline. If you do nothing else, do this.</p>
<h3 id="put-a-buffer-in-front-of-ingestion">Put a Buffer in Front of Ingestion</h3>
<p>One more thing I learned the hard way: put a queue between your log shippers and your aggregation layer. Kafka, Redis, even a simple file buffer. Traffic spikes will happen. Deploys will happen. If your pipeline has no buffer, you drop logs during the exact moments you need them most.</p>
<h3 id="pick-your-logging-battles">Pick your logging battles</h3>
<p>ELK is powerful software with brutal operational costs. Most teams underestimate how much work Elasticsearch is to run, and they find out at the worst possible time. If you can afford someone who genuinely enjoys tuning JVM garbage collection and shard allocation, go for it. Otherwise, pay for a hosted solution or pick a simpler tool.</p>
]]></content:encoded></item><item><title>Cloud vs Colocation for Startups: The Real Cost in 2016</title><link>https://lawzava.com/blog/2016-07-05-the-real-cost-of-running-your-own-servers/</link><pubDate>Tue, 05 Jul 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-07-05-the-real-cost-of-running-your-own-servers/</guid><description>Colocated servers look cheaper than EC2 until you add rack space, gear, and the engineer to run them. For a 2016 startup the cloud wins by a wide margin.</description><content:encoded><![CDATA[<p>Your startup isn&rsquo;t Netflix. Use managed cloud services and ship your product.</p>
<h3 id="stop-pretending">Stop Pretending</h3>
<p>I keep meeting startup founders who want to run their own servers. They pull up a spreadsheet showing that a dedicated box costs less per month than the equivalent EC2 instance. They are right about that one number and wrong about everything else.</p>
<p>Running your own hardware means you&rsquo;re now in two businesses: whatever your startup actually does, and running a small data center. Most teams can&rsquo;t do one of those well. Trying to do both is how you burn six months and ship nothing.</p>
<h3 id="the-math-nobody-wants-to-do">The Math Nobody Wants to Do</h3>
<p>A decent server costs around $5,000. Two for redundancy, so $10,000 up front. Now add the parts people skip.</p>
<p>Colocation: $500 to $1,000 a month for power, cooling, and rack space. Call it $750.</p>
<p>An infrastructure engineer who can keep those boxes alive, patched, and secure. In 2016 that&rsquo;s $120,000 a year minimum, fully loaded. Probably more. Even part-time, you&rsquo;re spending real money on someone whose job isn&rsquo;t your product.</p>
<p>Networking gear, firewalls, backups, monitoring. Another few thousand a year if you&rsquo;re doing it properly. More if you aren&rsquo;t, because you will pay for it later in downtime and data loss.</p>
<p>Add it up for one year: $10,000 hardware plus $9,000 colo plus $120,000 engineer plus maybe $5,000 in miscellaneous gear and software. That&rsquo;s roughly $144,000 to run two servers.</p>
<p>The same capacity on AWS runs maybe $300 to $500 a month depending on instance type and reservations. Call it $5,000 a year. Even with managed database, load balancer, and S3 you&rsquo;re probably under $15,000. And you didn&rsquo;t hire anyone whose job is keeping hardware alive.</p>
<p>The gap isn&rsquo;t subtle. It&rsquo;s an order of magnitude.</p>
<h3 id="but-what-about-scale">But What About Scale</h3>
<p>Yes, at serious scale the economics shift. If you&rsquo;re running hundreds of servers at  <a href="/blog/2016-03-28-aws-lambda-when-serverless-makes-sense/"
   
   >steady load around the clock</a>
, owned hardware can make sense. That&rsquo;s a real conversation for companies with predictable workloads and existing ops teams.</p>
<p>You&rsquo;re a startup. You don&rsquo;t have predictable workloads. You don&rsquo;t have an ops team. You might not have product-market fit yet. Optimizing infrastructure cost isn&rsquo;t your problem. Shipping fast enough to survive is your problem.</p>
<h3 id="the-hidden-cost-that-kills-you">The Hidden Cost That Kills You</h3>
<p>The bigger cost is time.</p>
<p>Provisioning a new server in your colo takes days or weeks. Ordering, shipping, racking, imaging, hardening. On AWS you have a new instance in minutes. When you need to test a hypothesis, run an experiment, or scale for a launch, that difference is existential.</p>
<p>I&rsquo;ve watched startups spend months building infrastructure that AWS would have given them out of the box. Months they didn&rsquo;t have. Some of them are gone now.</p>
<h3 id="when-i-would-consider-it">When I Would Consider It</h3>
<p>You have a stable, profitable business with predictable load. You already employ infrastructure engineers. You have compliance requirements that make cloud genuinely harder. You&rsquo;re processing data volumes where transfer costs dominate.</p>
<p>If none of those apply, you shouldn&rsquo;t be thinking about this.</p>
<p>For a startup in 2016, running your own servers is almost always vanity dressed up as frugality. The cloud isn&rsquo;t cheap, but it&rsquo;s cheaper than hiring ops staff, renting rack space, and losing months of engineering time to problems that are already solved.</p>
<p>Use AWS. Use  <a href="/blog/2016-04-12-postgres-vs-mysql-practical-comparison/"
   
   >managed Postgres</a>
. Use managed Redis. Ship your product. Worry about optimizing infrastructure costs when you have the kind of revenue that makes it worth optimizing.</p>
]]></content:encoded></item><item><title>Ansible vs Puppet vs Chef: Why Ansible Won for Us</title><link>https://lawzava.com/blog/2016-04-25-configuration-management-ansible-puppet-chef/</link><pubDate>Mon, 25 Apr 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-04-25-configuration-management-ansible-puppet-chef/</guid><description>Puppet, Chef, and Ansible compared from production use. Ansible wins on least ceremony; Puppet and Chef still fit large or Ruby-fluent infra teams.</description><content:encoded><![CDATA[<p>I&rsquo;ve run all three in production. Puppet at one gig, Chef at another, Ansible across several. Here&rsquo;s where I landed.</p>
<h3 id="puppet">Puppet</h3>
<p>Puppet&rsquo;s declarative DSL is solid. The agent-server model gives you continuous enforcement, which compliance-heavy orgs love. But you&rsquo;re running agent infrastructure on every node, learning a custom DSL that isn&rsquo;t quite a programming language, and debugging catalog compilation errors that give you nothing useful.</p>
<p>For a 500-node fleet with a dedicated infra team and compliance requirements? Puppet earns its keep. For anything smaller, it&rsquo;s overhead you don&rsquo;t need.</p>
<h3 id="chef">Chef</h3>
<p>Chef lets you write real Ruby. That&rsquo;s its greatest strength and its biggest trap. I&rsquo;ve watched teams build incredibly clever Chef cookbooks that nobody else could maintain. The testing story is good if you invest in it, but most teams don&rsquo;t.</p>
<p>If your infra team already thinks in Ruby, Chef can be powerful. If they don&rsquo;t, you&rsquo;re paying the Ruby learning tax on top of the config management learning tax. That&rsquo;s two taxes too many.</p>
<h3 id="ansible">Ansible</h3>
<p>Ansible is YAML, SSH, no agents. You write a playbook, you run it, machines converge. Done.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">hosts</span>: <span style="color:#ae81ff">web</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">tasks</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">install nginx</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">apt</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">nginx</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">state</span>: <span style="color:#ae81ff">present</span>
</span></span></code></pre></div><p>That&rsquo;s readable by anyone on the team. The new hire understands it on day one. The on-call engineer at 3am can follow it. That matters more than any feature comparison chart.</p>
<p>The tradeoff is real: Ansible is push-based, not continuously enforcing. At serious scale you&rsquo;ll bolt on Ansible Tower for scheduling and reporting. But for most teams, &ldquo;run the playbook&rdquo; is the right model.</p>
<h3 id="my-take">My take</h3>
<p>The config management wars of 2016 are mostly a distraction.  <a href="/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/"
   
   >Docker</a>
 and immutable infrastructure are already eating into this space. But you still need something for the hosts that run your containers, for your legacy systems, for the stuff that isn&rsquo;t going anywhere.</p>
<p>For that, I want the tool with the least ceremony. The one where I spend time on the actual infrastructure problem, not on the tool&rsquo;s own complexity.</p>
<p>That&rsquo;s Ansible.</p>
<p>Pick it, version your playbooks, write roles that are boring and readable, and get back to work that actually matters.</p>
]]></content:encoded></item><item><title>Docker in Production: Lessons from Running Containers</title><link>https://lawzava.com/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/</link><pubDate>Mon, 08 Feb 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/</guid><description>What running Docker in production at a mobility startup taught us about image builds, tagging, networking, logs, resource limits, and non-root security.</description><content:encoded><![CDATA[<p>Docker won&rsquo;t fix your ops. It will make bad ops louder and faster. But if you treat images like release artifacts, lock down your Dockerfiles, and build real observability around your containers, it&rsquo;s the best deployment tool available right now.</p>
<h3 id="we-broke-production-three-times-in-the-first-month">We Broke Production Three Times in the First Month</h3>
<p>At a mobility startup, we moved our backend services into Docker containers because we were tired of deploy scripts that worked on one machine and failed on another. The promise was simple: build once, run anywhere. That part was true. What nobody told us was how many new ways we would find to break things.</p>
<p>The first week, a developer pushed a container built on their laptop. It had a different glibc than the host. Segfault in production. The second week, we ran out of disk because nobody was cleaning up old images. The third week, a container ran as root and wrote a config file to a bind mount that the host process couldn&rsquo;t read back. Three incidents, three categories of failure, all in 30 days.</p>
<p>Every one of those was our fault. Docker did exactly what we told it to do. We just hadn&rsquo;t learned to tell it the right things yet.</p>
<h3 id="dockerfiles-that-actually-work">Dockerfiles That Actually Work</h3>
<p>Most Dockerfile tutorials produce images that are 800MB and run as root. That&rsquo;s fine for a demo. It isn&rsquo;t fine when you&rsquo;re pushing 15 deploys a day across 4 services.</p>
<p>Here is the pattern we settled on for our Go services at the mobility startup:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#75715e"># Dockerfile.build: compiles the binary in a throwaway Go image</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">golang:1.5</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span> <span style="color:#e6db74">/go/src/github.com/acme/location-svc</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> . .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> go get -d ./... <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> CGO_ENABLED<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> GOOS<span style="color:#f92672">=</span>linux go build -a -installsuffix cgo <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>       -ldflags<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-s -w&#34;</span> -o /app ./cmd/server<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#75715e"># Dockerfile: the runtime image, Alpine plus the binary</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">alpine:3.3</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apk add --no-cache ca-certificates tzdata <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    <span style="color:#f92672">&amp;&amp;</span> adduser -D -u <span style="color:#ae81ff">1000</span> appuser<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> app /usr/local/bin/app<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span> <span style="color:#e6db74">appuser</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">EXPOSE</span> <span style="color:#e6db74">8080</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENTRYPOINT</span> [<span style="color:#e6db74">&#34;app&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker build -t location-svc-build -f Dockerfile.build .
</span></span><span style="display:flex;"><span>docker create --name extract location-svc-build
</span></span><span style="display:flex;"><span>docker cp extract:/app ./app
</span></span><span style="display:flex;"><span>docker rm extract
</span></span><span style="display:flex;"><span>docker build -t acme/location-svc:<span style="color:#66d9ef">$(</span>git rev-parse --short HEAD<span style="color:#66d9ef">)</span> .
</span></span></code></pre></div><p>A few things to notice. One Dockerfile can&rsquo;t compile the code and then leave the compiler behind, so we split the job in two: a throwaway Go image builds the binary, and a short script copies it into the runtime image. Only the static binary ends up on the Alpine image, which keeps it under 20MB for most of our services. We create a non-root user. We strip debug symbols with <code>-ldflags=&quot;-s -w&quot;</code>. We use the exec form of <code>ENTRYPOINT</code>, not a shell wrapper, so the binary runs as PID 1 and receives signals directly.</p>
<p>For our Python services (we had one analytics pipeline that wasn&rsquo;t worth rewriting), the pattern looked different:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> <span style="color:#e6db74">python:3.5-slim</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> groupadd -r app <span style="color:#f92672">&amp;&amp;</span> useradd -r -g app -d /app -s /sbin/nologin app<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span> <span style="color:#e6db74">/app</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> requirements.txt .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> pip install --no-cache-dir -r requirements.txt<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> . .<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> chown -R app:app /app<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">USER</span> <span style="color:#e6db74">app</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">EXPOSE</span> <span style="color:#e6db74">5000</span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">CMD</span> [<span style="color:#e6db74">&#34;gunicorn&#34;</span>, <span style="color:#e6db74">&#34;--bind&#34;</span>, <span style="color:#e6db74">&#34;0.0.0.0:5000&#34;</span>, <span style="color:#e6db74">&#34;--workers&#34;</span>, <span style="color:#e6db74">&#34;2&#34;</span>, <span style="color:#e6db74">&#34;wsgi:app&#34;</span>]<span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Same principles, plus dependencies installed before the code is copied, so Docker caches that layer. Non-root user. Minimal base image. Explicit port. No <code>latest</code> tag anywhere.</p>
<h3 id="image-tagging-latest-isnt-a-version">Image Tagging: latest Isn&rsquo;t a Version</h3>
<p>We tag every image with the git commit SHA. Not the branch name, not <code>latest</code>, not a date string. The commit SHA.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker build -t acme/location-svc:<span style="color:#66d9ef">$(</span>git rev-parse --short HEAD<span style="color:#66d9ef">)</span> .
</span></span><span style="display:flex;"><span>docker push acme/location-svc:<span style="color:#66d9ef">$(</span>git rev-parse --short HEAD<span style="color:#66d9ef">)</span>
</span></span></code></pre></div><p>When something breaks at 2 AM, the first question is: what is running right now? With SHA tags, you can answer that in seconds. You can diff the exact code. You can roll back to the previous SHA. You can see who merged what. <code>latest</code> tells you nothing.</p>
<p>We store image metadata as labels too:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#66d9ef">ARG</span> GIT_SHA<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ARG</span> BUILD_DATE<span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">LABEL</span> build.sha<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GIT_SHA<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>      build.date<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>BUILD_DATE<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>      build.ci<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;jenkins&#34;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>It sounds like overkill. It isn&rsquo;t. During an incident last month, we traced a memory leak to a specific commit because the container label told us exactly what was deployed.</p>
<h3 id="docker-compose-for-local-development">docker-compose for Local Development</h3>
<p>Our services talk to PostgreSQL, Redis, and each other. Getting all of that running locally used to mean a two-page setup guide that was always out of date. Now it&rsquo;s a single file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#e6db74">&#39;2&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">location-svc</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">build</span>: <span style="color:#ae81ff">./services/location</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8080:8080&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">DATABASE_URL=postgres://app:dev@db:5432/app?sslmode=disable</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">REDIS_URL=redis://cache:6379</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">db</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">cache</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">trip-svc</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">build</span>: <span style="color:#ae81ff">./services/trip</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;8081:8080&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">DATABASE_URL=postgres://app:dev@db:5432/app?sslmode=disable</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">LOCATION_SVC_URL=http://location-svc:8080</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">depends_on</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">db</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">location-svc</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">db</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">postgres:9.5</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">POSTGRES_USER=app</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">POSTGRES_PASSWORD=dev</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">POSTGRES_DB=app</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">pgdata:/var/lib/postgresql/data</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cache</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">redis:3.0-alpine</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pgdata</span>:
</span></span></code></pre></div><p>New engineers run <code>docker-compose up</code> and have a working environment in under two minutes. No installing Go. No configuring Postgres. No hunting for the right Redis version. The compose file is checked into the repo, so it stays current.</p>
<p>One thing we learned the hard way: <code>depends_on</code> doesn&rsquo;t mean &ldquo;wait until the service is ready.&rdquo; It means &ldquo;start this container after that one.&rdquo; Postgres takes a few seconds to accept connections. Your application will crash if it tries to connect immediately. We added retry logic to every service&rsquo;s database connection code. Not elegant, but reliable.</p>
<h3 id="networking-stop-hardcoding-ips">Networking: Stop Hardcoding IPs</h3>
<p>Docker&rsquo;s default bridge network assigns IPs dynamically. If you hardcode an IP in a config file, it will work until it doesn&rsquo;t, and then you will spend an hour figuring out why service A can&rsquo;t find service B.</p>
<p>On a single host, Docker Compose gives you DNS resolution for free. Service names resolve to container IPs. <code>http://location-svc:8080</code> just works. That&rsquo;s good enough for development and for small production setups.</p>
<p>For multi-host, the story is more complicated. We use overlay networks, which let containers on different hosts communicate as if they were on the same network. It works, but it adds latency. We measured an extra 1-2ms per hop on our setup. For most services that&rsquo;s noise. For the real-time vehicle location tracking, it was noticeable. We kept that service on a single host with host networking.</p>
<h3 id="logs-stdout-or-nothing">Logs: stdout or Nothing</h3>
<p>Every one of our services logs to stdout. No log files inside the container. No custom logging directories. stdout.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">SetOutput</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdout</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">SetFlags</span>(<span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Ldate</span> | <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Ltime</span> | <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Lmicroseconds</span> | <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">LUTC</span>)
</span></span></code></pre></div><p>Docker captures stdout and makes it available via <code>docker logs</code>. We run a log forwarder on each host that ships container logs to a central ELK stack. Each log line includes the container ID, image name, and service name as structured fields. When something goes wrong, we can filter by service, time range, and container in Kibana within seconds.</p>
<p>The mistake we made early was letting one service log at DEBUG level in production. It wrote 2GB of logs in an hour, filled up the Docker log driver&rsquo;s buffer, and slowed down every other container on the host. We now enforce log level via environment variables and default to WARN in production.</p>
<h3 id="resource-limits-arent-optional">Resource Limits Aren&rsquo;t Optional</h3>
<p>A container without resource limits will happily consume all available memory on the host and take everything else down with it. We learned this when our analytics service hit a data spike and OOM-killed the Postgres container running on the same host.</p>
<p>Every container gets explicit limits:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">location-svc</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">mem_limit</span>: <span style="color:#ae81ff">256m</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">memswap_limit</span>: <span style="color:#ae81ff">256m</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cpu_shares</span>: <span style="color:#ae81ff">512</span>
</span></span></code></pre></div><p>The <code>memswap_limit</code> matching <code>mem_limit</code> prevents the container from using swap. If it runs out of memory, it dies. That sounds harsh, but a container thrashing on swap is worse than a container that restarts cleanly. Our health checks pick up the restart within seconds.</p>
<h3 id="security-run-as-non-root-or-dont-ship">Security: Run as Non-Root or Don&rsquo;t Ship</h3>
<p>I mentioned this in the Dockerfile section but it bears repeating. Every container runs as a non-root user. No exceptions. If a library or tool requires root, we find a different library or tool.</p>
<p>Beyond the user, we also:</p>
<ul>
<li>Use <code>--read-only</code> filesystem where possible, mounting only specific writable directories for temp files</li>
<li>Drop all capabilities and add back only what is needed: <code>--cap-drop=ALL --cap-add=NET_BIND_SERVICE</code></li>
<li>Never mount the Docker socket into a container (I&rsquo;ve seen this in tutorials and it terrifies me)</li>
<li>Scan images for known vulnerabilities before pushing to our private registry</li>
</ul>
<p>Our private registry runs behind TLS with basic auth. Every push and pull is authenticated. The registry itself runs in a container, which is a fun bit of recursion, but it works.</p>
<h3 id="health-checks-and-restarts">Health Checks and Restarts</h3>
<p>Every service exposes a <code>/healthz</code> endpoint that returns 200 if the service can reach its dependencies:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">healthHandler</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">Ping</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusServiceUnavailable</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprintf</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;db: %v&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">w</span>.<span style="color:#a6e22e">WriteHeader</span>(<span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusOK</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Fprint</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;ok&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Our load balancer polls this endpoint every 5 seconds. If a container fails three consecutive checks, it gets pulled from the rotation. Docker&rsquo;s restart policy (<code>--restart=on-failure:5</code>) handles restarting the process. If it fails 5 times in a row, it stays down and pages us. At that point, something is fundamentally wrong and automatic restarts won&rsquo;t fix it.</p>
<h3 id="what-i-would-tell-myself-six-months-ago">What I Would Tell Myself Six Months Ago</h3>
<p>Start with one service. Not the most important one and not the least important one. Pick something that deploys frequently and has good test coverage. Containerize it, run it in production for two weeks, and fix every sharp edge you find. Then do the next one.</p>
<p>Don&rsquo;t try to containerize your database early. We keep Postgres on a dedicated host with proper backups, WAL archiving, and monitoring. The application containers are stateless and disposable. The database is neither of those things.</p>
<p>Write your Dockerfiles like they will be read by someone debugging a production incident at 3 AM. Because they will be.</p>
<p>Treat image builds as CI artifacts. Build them in CI, tag them with the commit, push them to a registry, deploy from the registry. Never build on a developer laptop and push to production. We burned ourselves on that one.</p>
<p>Docker changed how we ship software at the mobility startup. Deploys went from 20 minutes of SSH-and-pray to 45 seconds of pull-and-start. But the speed only came after we did the unglamorous work of writing good Dockerfiles, setting up log aggregation, adding health checks, and enforcing resource limits. The container is the easy part. The ops around it is the job.</p>
]]></content:encoded></item></channel></rss>