<?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>DevOps | Law Zava</title><link>https://lawzava.com/topics/devops/</link><description>Delivery practice, automation, and the operational habits that separate shipping from firefighting.</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/devops/index.xml" rel="self" type="application/rss+xml"/><item><title>AI Workflow Automation: Let the Model Decide, Let Code Act</title><link>https://lawzava.com/blog/2025-08-04-ai-workflow-automation/</link><pubDate>Mon, 04 Aug 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-08-04-ai-workflow-automation/</guid><description>The trick to AI workflow automation is simple: let the model decide, let deterministic code act, and never confuse the two.</description><content:encoded><![CDATA[<p>I&rsquo;ve seen this failure more than once. It starts when a company automates invoice processing with an  <a href="/blog/2024-04-01-agentic-workflows-production/"
   
   >AI agent</a>
. The agent reads invoices, extracts line items, matches them to purchase orders, and approves payments. End to end. No human in the loop.</p>
<p>It works well for a few months. Then the agent approves a six-figure payment to a vendor who submits a duplicate invoice with slightly different formatting. The model treats it as new. The validation layer doesn&rsquo;t exist because &ldquo;the AI handles it.&rdquo;</p>
<p>A six-figure loss, because someone treated a probabilistic system like a deterministic one.</p>
<p>Seeing it more than once crystallized a principle I come back to often: AI decides, deterministic code acts. Never the other way around.</p>
<h2 id="the-architecture-that-survives">The Architecture That Survives</h2>
<p>The separation is simple in concept and surprisingly rare in practice. The AI component receives structured context, produces a structured decision with a rationale, and stops there. Everything after that (validation, side effects, and the actual work) is deterministic code with explicit rules.</p>
<p>The flow:</p>
<ol>
<li><strong>Trigger</strong> arrives with metadata (a ticket, a document, an event)</li>
<li><strong>AI decision</strong> produces  <a href="/blog/2024-04-29-structured-output-patterns/"
   
   >structured output</a>
: classification, extraction, routing recommendation, confidence score, and a short explanation of why</li>
<li><strong>Deterministic validation</strong> checks the decision against hard policy rules, allowlists, deny lists, and threshold constraints</li>
<li><strong>Action or escalation.</strong> If validation passes and confidence is high, execute. If not, route to human review with the full context attached</li>
<li><strong>Audit trail</strong> stores the input, the decision, the rationale, the validation result, and the final action</li>
</ol>
<p>Every step is logged. Every decision is replayable. If something goes wrong, you can trace exactly where and why.</p>
<h2 id="confidence-tiers-arent-optional">Confidence Tiers Aren&rsquo;t Optional</h2>
<p>Not every AI decision deserves the same treatment. A classification the model is 95% sure about is different from one it&rsquo;s 60% sure about. Your automation should know the difference.</p>
<p>I use three tiers everywhere:</p>
<ul>
<li><strong>High confidence:</strong> auto-approve, execute the action, log for periodic review</li>
<li><strong>Medium confidence:</strong> queue for human review with the AI&rsquo;s recommendation and rationale attached</li>
<li><strong>Low confidence:</strong> escalate immediately, flag for manual handling, don&rsquo;t proceed</li>
</ul>
<p>Thresholds depend on your domain. For invoice processing, I set the bar high because the cost of a wrong action is real money. For ticket triage, I set it lower because a misrouted ticket is annoying but recoverable.</p>
<p>The point is that uncertainty is a normal operating state. It isn&rsquo;t a bug. Your system should be designed to handle it gracefully instead of pretending every decision is confident.</p>
<h2 id="context-discipline">Context Discipline</h2>
<p>Feed the AI the minimum context needed to make a good decision. Not a raw database dump. Not the entire ticket history. Use a structured package: the specific document or event, the relevant policy excerpt, and a few representative examples of how similar cases were decided.</p>
<p>When teams dump everything into the  <a href="/blog/2024-07-22-context-window-strategies/"
   
   >context window</a>
, two things happen: token costs explode, and the model starts hallucinating connections between unrelated data points. More context isn&rsquo;t better context. Be deliberate about what matters for a specific decision.</p>
<h2 id="where-ai-automation-fits">Where AI Automation Fits</h2>
<p>Good fits: request triage, document classification, data extraction from messy formats, policy-based routing where ambiguity is expected and escalation is normal.</p>
<p>Bad fits: anything safety-critical, anything requiring hard real-time guarantees, anything where a wrong decision is irreversible and expensive. If you can&rsquo;t tolerate occasional uncertainty, don&rsquo;t automate with a probabilistic system.</p>
<p>From what I&rsquo;ve seen, the most successful automation projects started with a single workflow that already had a manual review path. They ran the AI in  <a href="/blog/2025-04-14-ai-testing-production/"
   
   >shadow mode</a>
 first, compared its decisions to the human decisions, measured agreement rates, and only then moved to live execution, with review still in place for the first few weeks.</p>
<h2 id="why-the-duplicate-payment-went-through">Why the Duplicate Payment Went Through</h2>
<p>That duplicate payment isn&rsquo;t a model failure. The model does exactly what it was designed to do: it classifies the invoice and approves it. The failure is architectural. Nobody built the validation layer that should have caught a duplicate vendor-amount-date combination. Nobody defined the hard boundaries.</p>
<p>AI automation works when you respect what it is: a probabilistic decision engine. Wrap it with  <a href="/blog/2024-11-11-ai-safety-production/"
   
   >deterministic guardrails</a>
, log everything, and keep humans in the loop for anything your business can&rsquo;t afford to get wrong.</p>
<p>Guardrails beat talent. Always.</p>
]]></content:encoded></item><item><title>Agentic Workflows in Production: Constrain the Blast Radius</title><link>https://lawzava.com/blog/2024-04-01-agentic-workflows-production/</link><pubDate>Mon, 01 Apr 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-04-01-agentic-workflows-production/</guid><description>AI agents that take actions carry real blast radius. Policy allowlists, structured workflows, idempotent steps, tracing, and a shadow-mode rollout.</description><content:encoded><![CDATA[<p>An agent that can read data and change state is a system with real blast radius. Constrain it with explicit policies, prefer structured workflows over free-form loops, and invest in observability before you invest in capabilities.</p>
<p>There&rsquo;s a moment in every agentic AI demo that makes the audience gasp. The agent reads a database, reasons about the results, drafts an email, and sends it. Autonomously. It feels like magic.</p>
<p>Then someone asks: &ldquo;What happens if it sends the wrong email?&rdquo; And the room gets quiet.</p>
<p>I&rsquo;ve been  <a href="/blog/2023-09-18-agent-architecture-patterns/"
   
   >building agentic systems</a>
 for several months now. The demo-to-production gap here is wider than almost anywhere else in AI engineering. A chatbot that hallucinates is annoying. An agent that hallucinates and then <em>acts on the hallucination</em> is a liability.</p>
<p>The difference between teams that ship agents successfully and teams that revert after a week comes down to three things: boundaries, structure, and boring reliability work.</p>
<h2 id="boundaries-first-capabilities-second">Boundaries first, capabilities second</h2>
<p>Almost every team starts with capabilities. &ldquo;What tools should the agent have? What actions can it take?&rdquo; Wrong starting point.</p>
<p>Start with constraints. What is the agent <em>not</em> allowed to do? What&rsquo;s the maximum blast radius of a single run? What happens when it goes wrong?</p>
<p>A policy config is the simplest way to make these constraints explicit and auditable:</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">agent_policy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">allowed_tools</span>: [<span style="color:#ae81ff">read_db, write_ticket, send_email_draft]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">max_steps</span>: <span style="color:#ae81ff">8</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">max_runtime_seconds</span>: <span style="color:#ae81ff">120</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">max_cost_usd</span>: <span style="color:#ae81ff">0.50</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">approval_required</span>: [<span style="color:#ae81ff">send_email, issue_refund, modify_production]</span>
</span></span></code></pre></div><p>Treat this as the foundation. The allowed tools list is an allowlist: the agent can only use what&rsquo;s explicitly permitted. Step and time limits prevent runaway loops. Cost caps prevent a single request from draining your budget. The approval list separates actions that are safe to automate from actions that need a human in the loop.</p>
<p>At one company I worked with, a team skipped the approval step for &ldquo;low-risk&rdquo; actions. One of those low-risk actions turned out to be updating customer records. An agent misinterpreted a support request and bulk-updated addresses for a batch of orders. The fix took two days. The approval gate would have taken two seconds.</p>
<p>If the policy feels too restrictive, relax it intentionally and document why. If you can&rsquo;t explain why a tool is on the allowed list, it shouldn&rsquo;t be there.</p>
<h2 id="structured-workflows-beat-free-form-loops">Structured workflows beat free-form loops</h2>
<p>The temptation with agents is to give them a goal and let them figure out the steps. This works beautifully in demos. In production, it creates systems that are impossible to debug, test, or audit.</p>
<p>I prefer structured workflows with a small number of decision points. The model chooses among defined paths. Deterministic logic handles state transitions. The result is a system you can trace, test, and explain.</p>
<p>Think of it as a state machine where the model influences transitions but doesn&rsquo;t control them entirely. The model might decide whether a customer inquiry needs escalation or can be handled automatically. But the escalation path itself (what happens, in what order, and with what approvals) is defined in code, not improvised by the model.</p>
<p>When a task genuinely doesn&rsquo;t fit a clean workflow, isolate it. Put the free-form reasoning in a narrow, heavily instrumented sandbox with tight constraints. Don&rsquo;t make it the default path for everything.</p>
<h2 id="the-boring-reliability-checklist">The boring reliability checklist</h2>
<p>I know this section won&rsquo;t go viral. That&rsquo;s fine. It&rsquo;s the section that keeps your agent from becoming an incident.</p>
<p><strong>Idempotent steps.</strong> If a step fails and retries, it shouldn&rsquo;t duplicate work. The agent shouldn&rsquo;t send two emails because the first one timed out after actually sending. Design every action to be safe to retry.</p>
<p><strong>Checkpointing.</strong> Long-running workflows should save their state at each step. If the process crashes or the model call times out, the workflow should resume from the last checkpoint, not start over.</p>
<p><strong>Time and step caps.</strong> Hard limits. Non-negotiable. An agent stuck in a reasoning loop should hit a wall after N steps or M seconds, return whatever partial results it has, and report the failure. I set these conservatively and loosen them only after seeing production data.</p>
<p><strong>Retry discipline.</strong> Retry on clearly transient failures such as rate limits and network timeouts. Don&rsquo;t retry on semantic failures, where the model misunderstood the task or the tool returned an error because the input was wrong. Retrying bad logic just wastes money and time.</p>
<h2 id="observability-isnt-optional">Observability isn&rsquo;t optional</h2>
<p>If you can&rsquo;t  <a href="/blog/2023-08-21-llm-observability/"
   
   >trace what an agent did</a>
 (every tool call, every model response, every decision point), you can&rsquo;t debug it. And you <em>will</em> need to debug it.</p>
<p>Structured logging for every step:</p>
<ul>
<li>What tool was called and with what inputs</li>
<li>What the model returned and what confidence signal it provided</li>
<li>Whether an approval was required and who approved it</li>
<li>How long each step took and how many tokens it consumed</li>
<li>The final outcome and whether it matched the intent</li>
</ul>
<p>This log is also your feedback loop. It tells you which prompts need refinement, which tools are unreliable, which workflows cost too much, and where the model consistently makes bad decisions.</p>
<p>One caution: be disciplined about what you log. Inputs and outputs may contain sensitive data. Define retention policies and access controls before you ship, not after an auditor asks.</p>
<h2 id="rolling-out-without-regret">Rolling out without regret</h2>
<p>The teams that succeed with agentic workflows share a rollout pattern:</p>
<ol>
<li><strong>Shadow mode first.</strong> The agent runs alongside the existing process but doesn&rsquo;t take any actions. Log what it <em>would</em> have done. Compare to what the human actually did. This gives you real quality data without any risk.</li>
<li><strong>Low-risk tasks with clear success criteria.</strong> Start with internal tasks where a mistake is inconvenient, not catastrophic. Ticket triage. Data enrichment. Report drafting.</li>
<li><strong>Expand only after stability.</strong> Once reliability, cost, and quality are stable for the initial scope, add more tools or more complex workflows. One step at a time.</li>
</ol>
<p>This pacing is unglamorous. It&rsquo;s also the only approach I&rsquo;ve seen work consistently.</p>
<h2 id="agents-act-so-the-bar-is-higher">Agents act, so the bar is higher</h2>
<p>Agents are powerful. They&rsquo;re also the highest-risk AI feature you can ship. Every other AI feature is advisory: the model suggests, the user decides. An agent <em>acts</em>. That means every bug, every hallucination, every misunderstanding has real consequences.</p>
<p>Treat agents as systems engineering, not prompt engineering. Define the blast radius. Build the constraints. Invest in the observability. Ship slow.</p>
<p>The teams that move carefully are the ones still running agents in production six months later. The teams that rush are the ones writing postmortems.</p>
]]></content:encoded></item><item><title>Terraform Patterns That Scale: Modules, State, Promotion</title><link>https://lawzava.com/blog/2022-11-21-infrastructure-as-code-patterns/</link><pubDate>Mon, 21 Nov 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-11-21-infrastructure-as-code-patterns/</guid><description>Practical Terraform patterns for teams past the tutorial stage: module design, state management, environment promotion, and policy enforcement.</description><content:encoded><![CDATA[<p>Writing Terraform is easy. Keeping it maintainable across fifty services, four environments, and a dozen contributors is where things fall apart. Structure your modules tightly, treat state as a product feature, promote changes through environments instead of rebuilding, and enforce policy in CI.</p>
<p>Everyone can write a Terraform file. The problem starts around month six, when you have fifteen modules nobody remembers writing, state files that three people are afraid to touch, and a staging environment that has drifted so far from production that testing there is theater.</p>
<p>I&rsquo;ve seen this pattern at multiple organizations, and I wrote about  <a href="/blog/2018-10-29-infrastructure-as-code-patterns/"
   
   >IaC patterns back in 2018</a>
. The IaC starts clean. Then it grows. Then it becomes the thing everyone is scared of. The patterns below are what I&rsquo;ve seen work to prevent that decay.</p>
<h2 id="module-design-one-job-per-module">Module Design: One Job Per Module</h2>
<p>The single most important structural decision is module scope. A module should do one thing. Not &ldquo;provision a service and its database and its DNS and its monitoring.&rdquo; One thing.</p>
<p>Here is what a focused module looks like for an application service:</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"># modules/ecs-service/main.tf
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">variable</span> <span style="color:#e6db74">&#34;service_name&#34;</span> {
</span></span><span style="display:flex;"><span>  type        <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Name of the ECS service&#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">variable</span> <span style="color:#e6db74">&#34;cluster_arn&#34;</span> {
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</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">variable</span> <span style="color:#e6db74">&#34;container_image&#34;</span> {
</span></span><span style="display:flex;"><span>  type        <span style="color:#f92672">=</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Docker image URI&#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">variable</span> <span style="color:#e6db74">&#34;cpu&#34;</span> {
</span></span><span style="display:flex;"><span>  type    <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span>
</span></span><span style="display:flex;"><span>  default <span style="color:#f92672">=</span> <span style="color:#ae81ff">256</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">variable</span> <span style="color:#e6db74">&#34;memory&#34;</span> {
</span></span><span style="display:flex;"><span>  type    <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span>
</span></span><span style="display:flex;"><span>  default <span style="color:#f92672">=</span> <span style="color:#ae81ff">512</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">variable</span> <span style="color:#e6db74">&#34;desired_count&#34;</span> {
</span></span><span style="display:flex;"><span>  type    <span style="color:#f92672">=</span> <span style="color:#66d9ef">number</span>
</span></span><span style="display:flex;"><span>  default <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</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">variable</span> <span style="color:#e6db74">&#34;subnet_ids&#34;</span> {
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#66d9ef">list</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">variable</span> <span style="color:#e6db74">&#34;security_group_ids&#34;</span> {
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#66d9ef">list</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">resource</span> <span style="color:#e6db74">&#34;aws_ecs_service&#34; &#34;this&#34;</span> {
</span></span><span style="display:flex;"><span>  name            <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">service_name</span>
</span></span><span style="display:flex;"><span>  cluster         <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">cluster_arn</span>
</span></span><span style="display:flex;"><span>  task_definition <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_ecs_task_definition</span>.<span style="color:#66d9ef">this</span>.<span style="color:#66d9ef">arn</span>
</span></span><span style="display:flex;"><span>  desired_count   <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">desired_count</span>
</span></span><span style="display:flex;"><span>  launch_type     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;FARGATE&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">network_configuration</span> {
</span></span><span style="display:flex;"><span>    subnets         <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">subnet_ids</span>
</span></span><span style="display:flex;"><span>    security_groups <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">security_group_ids</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_ecs_task_definition&#34; &#34;this&#34;</span> {
</span></span><span style="display:flex;"><span>  family                   <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">service_name</span>
</span></span><span style="display:flex;"><span>  requires_compatibilities <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;FARGATE&#34;</span>]
</span></span><span style="display:flex;"><span>  network_mode             <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;awsvpc&#34;</span>
</span></span><span style="display:flex;"><span>  cpu                      <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">cpu</span>
</span></span><span style="display:flex;"><span>  memory                   <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">memory</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  container_definitions <span style="color:#f92672">=</span> <span style="color:#66d9ef">jsonencode</span>([{
</span></span><span style="display:flex;"><span>    name  <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">service_name</span>
</span></span><span style="display:flex;"><span>    image <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">container_image</span>
</span></span><span style="display:flex;"><span>    portMappings <span style="color:#f92672">=</span> [{
</span></span><span style="display:flex;"><span>      containerPort <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>    }]
</span></span><span style="display:flex;"><span>  }])
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Clean inputs, clean outputs, one responsibility. The database, the DNS record, the monitoring: those are separate modules composed at a higher level. When the ECS module changes, you know exactly what&rsquo;s affected.</p>
<p>The moment a module becomes a grab bag, it stops being reusable. I&rsquo;ve seen &ldquo;service&rdquo; modules with 40 input variables and 200 lines of conditional logic. Nobody wants to touch them. Nobody trusts them. They become legacy infrastructure inside your infrastructure code.</p>
<h2 id="repository-layout">Repository Layout</h2>
<p>Your repo structure should reflect ownership, not technology. A layout I&rsquo;ve used successfully:</p>
<pre tabindex="0"><code>infrastructure/
  modules/
    ecs-service/
    rds-instance/
    cloudfront-distribution/
  environments/
    production/
      main.tf
      terraform.tfvars
    staging/
      main.tf
      terraform.tfvars
    dev/
      main.tf
      terraform.tfvars
</code></pre><p>Each environment directory composes the same modules with different variables. This makes differences between environments explicit and small instead of hidden and surprising.</p>
<h2 id="state-management-isnt-a-detail">State Management Isn&rsquo;t a Detail</h2>
<p>State management is where IaC projects actually fail. Not in the HCL. In the state.</p>
<p>Rules that have saved me from pain:</p>
<p><strong>Remote state with locking.</strong> Always. No exceptions. Two people running <code>terraform apply</code> at the same time against the same state file is how you get a 3am incident.</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;mycompany-terraform-state&#34;</span>
</span></span><span style="display:flex;"><span>    key            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;production/api-service/terraform.tfstate&#34;</span>
</span></span><span style="display:flex;"><span>    region         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;eu-west-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>Segment state by blast radius.</strong> Don&rsquo;t put your entire production environment in one state file (I covered  <a href="/blog/2021-12-06-terraform-at-scale/"
   
   >splitting state by blast radius</a>
 last year). When you run <code>terraform plan</code> and it wants to touch 200 resources, you aren&rsquo;t going to review that carefully. Nobody does. Split by service or component so each plan is small and reviewable.</p>
<p><strong>Encrypt everything.</strong> State files contain secrets whether you want them to or not. Database passwords, API keys, connection strings: they end up in state. Encryption at rest is the minimum.</p>
<p>At a real-time messaging platform I worked with, we segmented state per service per environment. A change to the messaging service&rsquo;s production infrastructure touched exactly that service&rsquo;s state. Nothing else. This made reviews fast, plans small, and rollbacks contained.</p>
<h2 id="environment-promotion-over-rebuilds">Environment Promotion Over Rebuilds</h2>
<p>The safest deployment model is promotion. A change that passed in dev should be the exact same change in staging, which should be the exact same change in production. Not &ldquo;similar.&rdquo; 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-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></span><span style="display:flex;"><span><span style="color:#66d9ef">module</span> <span style="color:#e6db74">&#34;api_service&#34;</span> {
</span></span><span style="display:flex;"><span>  source <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;../../modules/ecs-service&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  service_name       <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;api-service&#34;</span>
</span></span><span style="display:flex;"><span>  cluster_arn        <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">cluster_arn</span>
</span></span><span style="display:flex;"><span>  container_image    <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">api_image</span><span style="color:#75715e">  # promoted from staging
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  cpu                <span style="color:#f92672">=</span> <span style="color:#ae81ff">512</span>
</span></span><span style="display:flex;"><span>  memory             <span style="color:#f92672">=</span> <span style="color:#ae81ff">1024</span>
</span></span><span style="display:flex;"><span>  desired_count      <span style="color:#f92672">=</span> <span style="color:#ae81ff">4</span>
</span></span><span style="display:flex;"><span>  subnet_ids         <span style="color:#f92672">=</span> <span style="color:#66d9ef">var</span>.<span style="color:#66d9ef">private_subnet_ids</span>
</span></span><span style="display:flex;"><span>  security_group_ids <span style="color:#f92672">=</span> [<span style="color:#66d9ef">aws_security_group</span>.<span style="color:#66d9ef">api</span>.<span style="color:#66d9ef">id</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The module source is the same across environments. Only the variables change. When you promote, you update the image tag and any environment-specific sizing, and the rest stays locked.</p>
<p>Avoid long-lived branches per environment. I&rsquo;ve seen teams with a <code>staging</code> branch and a <code>production</code> branch that diverged months ago. Nobody knows what the differences are. Nobody wants to find out. Use one branch, different variable files.</p>
<h2 id="testing-layers-not-afterthoughts">Testing: Layers, Not Afterthoughts</h2>
<p>IaC testing is closer to software testing than most teams realize. Three layers that work:</p>
<p><strong>Static analysis.</strong> Run <code>terraform validate</code>, <code>tflint</code>, and a policy tool like OPA or Sentinel in CI. These catch syntax errors, deprecated patterns, and policy violations before anyone runs a plan. Fast and cheap.</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"># policy/require_encryption.rego
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">package</span> <span style="color:#66d9ef">terraform</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">deny</span>[<span style="color:#66d9ef">msg</span>] {
</span></span><span style="display:flex;"><span>  resource :<span style="color:#f92672">=</span> <span style="color:#66d9ef">input</span>.<span style="color:#66d9ef">resource_changes</span>[<span style="color:#66d9ef">_</span>]
</span></span><span style="display:flex;"><span>  resource.type <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;aws_s3_bucket&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">not</span> <span style="color:#66d9ef">resource</span>.<span style="color:#66d9ef">change</span>.<span style="color:#66d9ef">after</span>.<span style="color:#66d9ef">server_side_encryption_configuration</span>
</span></span><span style="display:flex;"><span>  msg :<span style="color:#f92672">=</span> <span style="color:#66d9ef">sprintf</span>(<span style="color:#e6db74">&#34;S3 bucket %s must have encryption enabled&#34;</span>, [<span style="color:#66d9ef">resource</span>.<span style="color:#66d9ef">address</span>])
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>With AWS provider v4 and later, bucket encryption lives in a separate <code>aws_s3_bucket_server_side_encryption_configuration</code> resource, so adapt a rule like this to the provider version you run.</p>
<p><strong>Plan review.</strong> Every change gets a <code>terraform plan</code> output attached to the PR. Reviewers look at the plan, not just the code. The code tells you what you intended. The plan tells you what will actually happen. These aren&rsquo;t always the same thing.</p>
<p><strong>Integration tests for critical modules.</strong> For modules that provision core infrastructure, use something like Terratest to actually create and destroy resources in a test account. This is slow and expensive, so reserve it for the modules where a mistake costs real money.</p>
<h2 id="policy-as-code">Policy as Code</h2>
<p>Policy documents nobody reads aren&rsquo;t policy. Automated checks that block bad changes are policy.</p>
<p>Encode your security baselines, encryption requirements, and network boundaries as code. Run them in CI before any apply. When a developer opens a PR that creates an unencrypted S3 bucket, the pipeline catches it, not a security audit six months later.</p>
<h2 id="managing-drift">Managing Drift</h2>
<p>Drift is inevitable. Someone will fix something in the console during an incident. An auto-scaling event will leave resources in a state Terraform didn&rsquo;t expect. External automation will modify tags.</p>
<p>The answer isn&rsquo;t &ldquo;never touch the console.&rdquo; The answer is detect drift early and reconcile it regularly. Run <code>terraform plan</code> on a schedule. Alert on unexpected changes. Treat drift like tech debt: small amounts are fine, accumulated drift is dangerous.</p>
<h2 id="secrets-keep-them-out">Secrets: Keep Them Out</h2>
<p>Secrets don&rsquo;t belong in <code>.tfvars</code> files, in version control, or in state files if you can avoid it. Use a secrets manager and reference values at runtime:</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;aws_secretsmanager_secret_version&#34; &#34;db_password&#34;</span> {
</span></span><span style="display:flex;"><span>  secret_id <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;production/api-service/db-password&#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">resource</span> <span style="color:#e6db74">&#34;aws_db_instance&#34; &#34;main&#34;</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"></span>  password <span style="color:#f92672">=</span> <span style="color:#66d9ef">data</span>.<span style="color:#66d9ef">aws_secretsmanager_secret_version</span>.<span style="color:#66d9ef">db_password</span>.<span style="color:#66d9ef">secret_string</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Mark outputs as sensitive. Review plan output for accidental exposure. This isn&rsquo;t paranoia. I&rsquo;ve seen database credentials committed to state files in shared S3 buckets with overly broad access policies.</p>
<h2 id="treat-terraform-like-application-code">Treat Terraform Like Application Code</h2>
<p>The teams I&rsquo;ve seen manage infrastructure well at scale all share the same trait: they treat their Terraform code with the same rigor they treat their application code. Reviews, tests, clear ownership, and a healthy fear of clever abstractions. Boring infrastructure is reliable infrastructure.</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>Monorepo vs. Polyrepo: A Practical Decision Guide</title><link>https://lawzava.com/blog/2022-10-31-monorepo-vs-polyrepo/</link><pubDate>Mon, 31 Oct 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-10-31-monorepo-vs-polyrepo/</guid><description>Monorepo or polyrepo depends on coupling, team shape, and your appetite for build tooling. Here is how to decide without getting religious about it.</description><content:encoded><![CDATA[<p>If your code changes together, put it together. If your teams release independently, keep them separate. Everything else is tooling cost and organizational preference. Stop arguing about it on Twitter.</p>
<p>This debate generates more heat than it deserves. I&rsquo;ve worked in both models: a monorepo at my infrastructure startup where every change could touch everything, and polyrepos at larger companies where dozens of teams needed autonomy. Both worked. Both had pain points. The difference was never the repo structure itself. It was whether the structure matched how the code actually changed.</p>
<h2 id="three-questions-that-decide-it">Three Questions That Decide It</h2>
<p>Forget &ldquo;monorepo vs. polyrepo&rdquo; as an identity choice. Ask these questions instead:</p>
<ol>
<li>How often do changes span multiple services or libraries?</li>
<li>How independent are your team release cycles?</li>
<li>How much build infrastructure are you willing to own?</li>
</ol>
<p>That&rsquo;s basically it. The answers determine the tradeoff surface.</p>
<h2 id="the-tradeoff-table">The Tradeoff Table</h2>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>Monorepo</th>
          <th>Polyrepo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Cross-cutting changes</strong></td>
          <td>Single commit, single review</td>
          <td>Multiple PRs, version coordination, prayer</td>
      </tr>
      <tr>
          <td><strong>Build complexity</strong></td>
          <td>Needs smart builds, caching, selective testing</td>
          <td>Standard CI per repo, but dependency sync is your problem</td>
      </tr>
      <tr>
          <td><strong>Team autonomy</strong></td>
          <td>Lower: shared standards, shared tooling</td>
          <td>Higher: own stack, own cadence, own mistakes</td>
      </tr>
      <tr>
          <td><strong>Shared code</strong></td>
          <td>Easy to update, hard to ignore</td>
          <td>Easy to fork, hard to keep in sync</td>
      </tr>
      <tr>
          <td><strong>Onboarding</strong></td>
          <td>Everything is discoverable in one place</td>
          <td>Context is scattered across repos, wikis, tribal knowledge</td>
      </tr>
      <tr>
          <td><strong>Access control</strong></td>
          <td>Coarser: everyone sees everything (usually)</td>
          <td>Fine-grained per repo</td>
      </tr>
      <tr>
          <td><strong>Release coordination</strong></td>
          <td>Natural: everything moves together</td>
          <td>Requires explicit compatibility contracts</td>
      </tr>
      <tr>
          <td><strong>Failure blast radius</strong></td>
          <td>A bad merge can block everyone</td>
          <td>Contained per repo</td>
      </tr>
  </tbody>
</table>
<p>Neither column is universally better. It depends on your situation.</p>
<h2 id="when-monorepo-wins">When Monorepo Wins</h2>
<p>Monorepo shines when coupling is real, not aspirational. If your backend API change requires a frontend update and a shared library bump, doing that across three repos with three PRs and a release dance is painful. A monorepo makes it one commit with one review.</p>
<p>It also works when you want consistency. Shared linting, shared CI, shared tooling: all easier when there&rsquo;s one source of truth. At one company with fifteen microservices, we moved to a monorepo and immediately found three services using different logging libraries for no reason. Consolidation happened naturally once the code was visible.</p>
<p>The cost is real though. You need smart builds. Running all tests for every change in a repo with fifty services isn&rsquo;t viable. Tools like Bazel, Nx, or Turborepo exist for this reason, but they aren&rsquo;t free to set up or maintain. The repository itself becomes infrastructure, and someone has to own it.</p>
<h2 id="when-polyrepo-wins">When Polyrepo Wins</h2>
<p>Polyrepo wins when teams are genuinely independent. If your payments team and your notifications team share an API contract but otherwise never touch each other&rsquo;s code, separate repos give them freedom to move at their own pace. Different languages, different deploy schedules, different testing strategies: all fine.</p>
<p>Permissions are simpler too. Contractors working on one service don&rsquo;t need to see the entire codebase. Compliance teams are happier when access boundaries are clean.</p>
<p>The cost is coordination. Cross-repo changes require  <a href="/blog/2022-03-07-api-versioning-strategies/"
   
   >versioning</a>
, compatibility planning, and discipline. Shared code gets forked, diverges, and becomes a source of subtle bugs. I&rsquo;ve seen organizations with thirty repos where &ldquo;shared utilities&rdquo; existed in five slightly different versions across teams, each with its own bugs.</p>
<h2 id="the-hybrid-reality">The Hybrid Reality</h2>
<p>Most organizations I&rsquo;ve worked with end up somewhere in the middle. A few domain-aligned monorepos (one for the core platform, one for data pipelines, one for customer-facing apps) with polyrepo isolation between domains. This is fine as long as it&rsquo;s deliberate.</p>
<p>The danger is the accidental hybrid: some code is in the monorepo because someone put it there, some is in separate repos because someone else preferred that, and nobody wrote down why. That&rsquo;s how you get the worst of both worlds.</p>
<h2 id="decision-checklist">Decision Checklist</h2>
<p>Answer these:</p>
<ul>
<li><strong>Cross-service changes happen weekly or more?</strong> Lean monorepo.</li>
<li><strong>Teams need independent release schedules?</strong> Lean polyrepo.</li>
<li><strong>You have (or will build) dedicated build tooling capacity?</strong> Monorepo is feasible.</li>
<li><strong>Permissions and isolation are a hard requirement?</strong> Polyrepo is simpler.</li>
<li><strong>Shared code is a core part of delivery speed?</strong> Monorepo makes it visible and maintainable.</li>
</ul>
<p>If you&rsquo;re split 3-2 either way, you&rsquo;re probably fine with either model. Pick one and commit to owning the tradeoffs.</p>
<h2 id="migration-is-possible-but-expensive">Migration Is Possible but Expensive</h2>
<p>I&rsquo;ve helped teams move in both directions. It&rsquo;s never a weekend project. You need a clear boundary map, a dependency inventory, and a staged migration plan. Treat it like a product initiative with dedicated ownership, not a &ldquo;we&rsquo;ll do it gradually&rdquo; background task. Gradual migrations without ownership die quietly and leave you in the worst-of-both-worlds hybrid.</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>Kubernetes Security Hardening: Pods, RBAC, Network, Secrets</title><link>https://lawzava.com/blog/2022-02-07-kubernetes-security-hardening/</link><pubDate>Mon, 07 Feb 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-02-07-kubernetes-security-hardening/</guid><description>Kubernetes defaults favor convenience over security. A layered hardening guide covering pods, RBAC, network policies, secrets, and the control plane.</description><content:encoded><![CDATA[<p>Kubernetes ships with defaults designed to get your workloads running. Not to resist a motivated attacker. Not to survive a leaked credential. Not to contain a compromised container. If you&rsquo;re running a production cluster with default settings, you&rsquo;re running a cluster that trusts everything by default. Lock down pods, network, RBAC, secrets, and the control plane as separate layers, and assume every layer will be tested.</p>
<p>I&rsquo;ve hardened clusters across multiple organizations and I keep seeing the same gaps I wrote about  <a href="/blog/2019-04-22-kubernetes-security-hardening/"
   
   >in 2019</a>
. This post walks through the layers that matter, with config you can actually apply. Some of this comes from taking part in cyber-defense exercises, where &ldquo;assume breach&rdquo; was the operating model and defense in depth was the only design that survived contact with reality.</p>
<h2 id="start-with-a-threat-model-seriously">Start with a threat model (seriously)</h2>
<p>I know. Nobody wants to do this part. But every security control you skip because &ldquo;we&rsquo;re not a bank&rdquo; is a control an attacker won&rsquo;t encounter. You don&rsquo;t need a 40-page document. You need answers to three questions:</p>
<ol>
<li>What data would hurt you if it leaked?</li>
<li>Who has access to your cluster, and how?</li>
<li>If a container gets compromised, what can the attacker reach?</li>
</ol>
<p>If you can&rsquo;t answer these quickly, start there before touching any YAML.</p>
<h2 id="pod-security-the-non-negotiable-baseline">Pod security: the non-negotiable baseline</h2>
<p>Every container should run with the least privilege possible. Full stop. Here is what that looks like in practice:</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">Pod</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">app</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">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">runAsUser</span>: <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">fsGroup</span>: <span style="color:#ae81ff">1000</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">app</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">image</span>: <span style="color:#ae81ff">myregistry/app:v1.2.3@sha256:abc123...</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">allowPrivilegeEscalation</span>: <span style="color:#66d9ef">false</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><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></code></pre></div><p>Key points:</p>
<ul>
<li><strong><code>runAsNonRoot: true</code></strong>: if your app needs root, fix the app. I&rsquo;ve seen exactly one legitimate case for running as root in a non-system container in the last three years.</li>
<li><strong>Drop ALL capabilities</strong>, then add back only what&rsquo;s needed. Most applications need zero Linux capabilities.</li>
<li><strong>Read-only root filesystem</strong>: forces you to use volumes for writable paths, which is where you want writes anyway.</li>
<li><strong>Resource limits</strong>: not just for cost. An unbound container can starve the node and affect every other workload on it.</li>
<li><strong>Pin image digests</strong>: <code>latest</code> isn&rsquo;t a version. Pin to a SHA256 digest for anything that matters.</li>
</ul>
<p>PodSecurityPolicy was deprecated in 1.21 and is scheduled for removal in 1.25. Replace it. Pod Security Admission (beta in 1.23) implements three levels: privileged, baseline, restricted. At minimum, enforce <code>baseline</code> cluster-wide and <code>restricted</code> on sensitive namespaces:</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">Namespace</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</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">pod-security.kubernetes.io/enforce</span>: <span style="color:#ae81ff">restricted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/audit</span>: <span style="color:#ae81ff">restricted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">pod-security.kubernetes.io/warn</span>: <span style="color:#ae81ff">restricted</span>
</span></span></code></pre></div><p>For more granular control, OPA Gatekeeper or Kyverno let you write custom policies. I prefer Kyverno for readability, but both work.</p>
<h2 id="network-policies-default-deny-then-allow">Network policies: default deny, then allow</h2>
<p>Kubernetes networking is flat by default. Every pod can talk to every other pod. That&rsquo;s fantastic for an attacker who compromises one service and wants to pivot laterally.</p>
<p>Start with a default deny policy on every namespace:</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-all</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">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>Then explicitly allow what&rsquo;s needed:</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">allow-app-to-db</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">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">backend</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">Egress</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">protocol</span>: <span style="color:#ae81ff">TCP</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">5432</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">to</span>:  <span style="color:#75715e"># DNS</span>
</span></span><span style="display:flex;"><span>        - <span style="color:#f92672">namespaceSelector</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">protocol</span>: <span style="color:#ae81ff">UDP</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">port</span>: <span style="color:#ae81ff">53</span>
</span></span></code></pre></div><p>A critical gotcha: NetworkPolicy only works if your CNI supports it. Flannel doesn&rsquo;t enforce network policies. Calico, Cilium, and Weave do. Verify enforcement in every environment, including staging. I&rsquo;ve seen teams spend weeks writing policies that were silently ignored because the CNI was wrong.</p>
<p>Don&rsquo;t forget egress. Ingress gets all the attention, but an attacker who compromises a pod and can make outbound HTTPS calls to the internet has an exfiltration path. Restrict egress to known destinations.</p>
<h2 id="rbac-strict-and-boring">RBAC: strict and boring</h2>
<p>Good RBAC is boring. It should be so restrictive that nobody notices it until they try to do something they shouldn&rsquo;t.</p>
<ul>
<li>One service account per workload. Not one per namespace. Not the default service account.</li>
<li>Scope roles to namespaces and specific resources. Cluster-wide roles are for platform teams, not applications.</li>
<li>Disable automatic service account token mounting for pods that don&rsquo;t need to talk to the API server:</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">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">ServiceAccount</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">app-sa</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">automountServiceAccountToken</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p>For human access, use OIDC with your identity provider. Short-lived tokens from <code>kubectl</code> via OIDC are vastly better than long-lived kubeconfig certificates. Nobody should have <code>cluster-admin</code> in production unless they&rsquo;re actively performing cluster operations, and even then, consider just-in-time access.</p>
<h2 id="secrets-they-arent-encrypted">Secrets: they aren&rsquo;t encrypted</h2>
<p>This surprises people. Kubernetes Secrets are base64-encoded in etcd. That&rsquo;s encoding, not encryption. Anyone with etcd access can read every secret in your cluster.</p>
<p>Fix this in layers:</p>
<ol>
<li><strong>Encrypt etcd at rest.</strong> Configure <code>EncryptionConfiguration</code> on the API server with <code>aescbc</code> or <code>secretbox</code> providers.</li>
<li><strong>Use an external secrets manager.</strong> HashiCorp Vault, AWS Secrets Manager, or GCP Secret Manager with an operator like External Secrets. The secret value never lives in etcd.</li>
<li><strong>Scope access.</strong> Only the service accounts that need a secret should be able to read it. RBAC applies to secrets too.</li>
<li><strong>Rotate.</strong> If you can&rsquo;t rotate a secret without a deployment, fix your rotation process before you fix the secret.</li>
</ol>
<p>Never bake secrets into container images. Never put them in ConfigMaps. Never log them. These sound obvious, but I find violations in almost every cluster audit.</p>
<h2 id="control-plane-hygiene">Control plane hygiene</h2>
<p>The API server is the front door to everything. Lock it down:</p>
<ul>
<li>Disable anonymous authentication (<code>--anonymous-auth=false</code>).</li>
<li>Enable audit logging. You need to know who did what and when.</li>
<li>Restrict API server access to trusted networks. If your API server is reachable from the public internet, you have a bigger problem than this blog post covers.</li>
<li>Keep etcd on a private network, TLS-encrypted, and only reachable by the API server.</li>
<li>Treat kubeconfig files like production database credentials. Because that&rsquo;s what they are.</li>
</ul>
<p>If you&rsquo;re on a managed service (EKS, GKE, AKS), the provider handles some of this. But &ldquo;managed&rdquo; doesn&rsquo;t mean &ldquo;secure.&rdquo; Review what they expose and what they lock down. I&rsquo;ve seen managed clusters with surprisingly permissive defaults.</p>
<h2 id="image-supply-chain">Image supply chain</h2>
<p>Stop using <code>latest</code>. Stop using mutable tags. Every production image should be:</p>
<ul>
<li>Built from a minimal base (distroless, Alpine, or scratch for Go binaries).</li>
<li>Scanned for vulnerabilities in CI before it reaches a registry.</li>
<li>Signed, ideally with cosign or Notary.</li>
<li>Pulled from a private registry with access controls.</li>
</ul>
<p>If you&rsquo;re pulling public images directly into production, you&rsquo;re trusting that Docker Hub (or whoever) hasn&rsquo;t been compromised. That trust should make you uncomfortable.</p>
<h2 id="node-level-protections">Node-level protections</h2>
<p>Nodes are part of your trust boundary. A compromised node means compromised pods.</p>
<ul>
<li>Use a minimal OS (Bottlerocket, Flatcar, Talos). Less software means fewer attack vectors.</li>
<li>Patch regularly. Automate node rotation if you can.</li>
<li>Lock down the kubelet API. Disable anonymous kubelet auth.</li>
<li>Apply seccomp profiles. The <code>RuntimeDefault</code> profile is a good starting point and blocks a significant number of syscalls:</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">securityContext</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">seccompProfile</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RuntimeDefault</span>
</span></span></code></pre></div><p> <a href="/blog/2020-11-30-container-runtime-security/"
   
   >Runtime detection</a>
 tools like Falco are worth considering. They won&rsquo;t prevent an attack, but they can alert you when something unexpected happens: a shell spawning inside a container, an unusual network connection, a binary that shouldn&rsquo;t exist being executed.</p>
<h2 id="monitoring-and-incident-response">Monitoring and incident response</h2>
<p>Hardening without visibility is just hoping. You need:</p>
<ul>
<li>Centralized audit logs from the API server.</li>
<li>Alerts on failed authentication attempts and privilege escalations.</li>
<li>Monitoring for unexpected image pulls or outbound connections.</li>
<li>A written incident response playbook. Not a novel. A one-page runbook that answers: who gets paged, what gets isolated, how do we preserve evidence.</li>
</ul>
<p>Write the playbook before you need it. During an actual incident isn&rsquo;t the time to figure out who has cluster access.</p>
<h2 id="the-operating-discipline">The operating discipline</h2>
<p>None of this works as a one-time project. Security is operational:</p>
<ul>
<li>All cluster config changes go through code review. GitOps is your friend here.</li>
<li>Stay on supported Kubernetes versions. Running an EOL version means running with known unpatched vulnerabilities.</li>
<li>Test your controls in staging. I&rsquo;ve seen policies that worked perfectly in staging and failed silently in production because the namespace labels were wrong.</li>
<li>Run backup and restore drills. Not &ldquo;we have Velero installed.&rdquo; Actually restore from backup and verify the cluster works.</li>
</ul>
<p>Kubernetes hardening is a set of defaults you enforce continuously, across pods, network, identities, secrets, and the control plane. When those defaults are solid, incidents become containable instead of catastrophic.</p>
]]></content:encoded></item><item><title>DORA Metrics: Keep Them Off Performance Reviews</title><link>https://lawzava.com/blog/2022-01-24-dora-metrics-implementation/</link><pubDate>Mon, 24 Jan 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-01-24-dora-metrics-implementation/</guid><description>DORA metrics work until someone puts them on a performance review. How to define, collect, and use them at team level without gaming.</description><content:encoded><![CDATA[<p>I keep seeing the same pattern. A VP reads the DORA report, gets excited, mandates dashboards for all teams, and within two months the metrics are either gamed, ignored, or weaponized in performance reviews. A genuinely useful framework gets turned into a surveillance tool. I&rsquo;ve made the same argument about  <a href="/blog/2020-08-31-developer-productivity-metrics/"
   
   >developer productivity metrics</a>
 before.</p>
<p>DORA metrics (deployment frequency, lead time for changes, change failure rate, time to restore) are good <em>because</em> they measure system outcomes. Not individual output. Not lines of code. Not story points. System outcomes. The moment you point them at individual engineers, you&rsquo;ve already lost the plot.</p>
<h2 id="the-four-metrics-briefly">The four metrics, briefly</h2>
<p>The 2021 State of DevOps report also tracks reliability as a fifth measure, but the original four are what most teams mean by &ldquo;DORA.&rdquo;</p>
<p><strong>Deployment frequency</strong>: how often code reaches production. Higher is generally better because it implies smaller batches and faster feedback. But a team deploying empty feature flags ten times a day isn&rsquo;t actually faster.</p>
<p><strong>Lead time for changes</strong>: the time from commit to production. This one is revealing. When lead time is high, the bottleneck is almost never &ldquo;engineers are slow.&rdquo; It&rsquo;s reviews, environments, approval gates, flaky tests. Infrastructure problems disguised as people problems.</p>
<p><strong>Change failure rate</strong>: the percentage of deployments that cause incidents or need rollback. The quality counterbalance to speed. If you&rsquo;re deploying constantly but breaking things, you aren&rsquo;t winning.</p>
<p><strong>Time to restore</strong>: how quickly you recover after a failure. I care about this one the most. Your system <em>will</em> break. The question is whether you can fix it in minutes or hours.</p>
<h2 id="the-measurement-trap">The measurement trap</h2>
<p>Here is where teams go wrong. They start measuring before they agree on definitions.</p>
<p>What counts as a deployment? Does a config change count? A feature flag flip? What about a deployment to staging that auto-promotes? Is a deployment &ldquo;to production&rdquo; when it hits the first canary or when it reaches 100% of traffic?</p>
<p>What counts as a failure? Only P1 incidents? Any rollback? A hotfix that ships within an hour?</p>
<p>If you don&rsquo;t write these definitions down and get agreement, every team will interpret the metrics differently. Then someone will compare teams, the numbers will be meaningless, and trust in the whole system collapses.</p>
<p>I watched exactly this happen at one company. Two teams reported wildly different deployment frequencies. Turned out one team counted every Kubernetes pod restart as a deployment. The other only counted manual releases. Same dashboard, completely different realities.</p>
<h2 id="definitions-medians-and-team-level-numbers">Definitions, medians, and team-level numbers</h2>
<p>Use rolling 30-day medians, not averages. Averages lie when you have one outlier incident that takes a week to resolve. Medians are boring and that&rsquo;s the point.</p>
<p>Keep the metrics at the team or service level. Never at the individual level. The second you rank individuals by lead time, someone will start gaming commit timestamps. I&rsquo;ve seen it happen.</p>
<p>Track trends against a team&rsquo;s own baseline. Comparing Team A to Team B is almost always misleading. Teams have different codebases, different customer profiles, different risk tolerances. A payments team deploying twice a week with zero failures is doing better than a marketing team deploying daily with a 15% failure rate, even though the dashboard would suggest otherwise.</p>
<p>Automate collection. If someone has to manually tag deployments or log incidents for the metrics to work, the data will be inconsistent within a month. Pull from your CI/CD system, your version control, and your incident tracker. If those systems don&rsquo;t have the events you need, fix that first.</p>
<h2 id="the-traps-to-avoid">The traps to avoid</h2>
<p><strong>Don&rsquo;t use DORA for performance reviews.</strong> I can&rsquo;t say this enough. The moment engineers know their deployment frequency affects their bonus, they will split PRs into meaningless fragments. You&rsquo;ll get higher numbers and worse software.</p>
<p><strong>Don&rsquo;t optimize one metric in isolation.</strong> Deployment frequency without change failure rate is just recklessness. Lead time without quality is just speed. They work as a set or not at all.</p>
<p><strong>Don&rsquo;t hide incidents.</strong> If the change failure rate shows up in some leadership scorecard, teams will reclassify incidents as &ldquo;expected behavior&rdquo; or &ldquo;not caused by the deployment.&rdquo; I&rsquo;ve seen teams debate for twenty minutes whether something was &ldquo;really an incident&rdquo; just to protect their number.</p>
<p><strong>Don&rsquo;t expect precision.</strong> These are directional signals. Trends matter. A lead time that drops from days to hours over a quarter is meaningful. The difference between 2.3 hours and 2.7 hours is noise. Treat it as noise.</p>
<h2 id="pair-the-numbers-with-conversations">Pair the numbers with conversations</h2>
<p>Pair DORA metrics with qualitative feedback. Ask teams: &ldquo;What&rsquo;s slowing you down this sprint?&rdquo; The metrics should confirm what teams already feel. If the numbers say everything is fine but engineers are miserable, the numbers are wrong, or more likely they&rsquo;re measuring the wrong thing.</p>
<p>Use the metrics to fund improvements. Lead time is high because the test suite takes 40 minutes? That&rsquo;s a concrete investment case. Change failure rate is climbing? Maybe the team needs better staging environments, not a talking-to.</p>
<p>DORA is a compass. It tells you roughly which direction you&rsquo;re heading. It doesn&rsquo;t tell you where every pothole is. Use it that way and it&rsquo;s genuinely valuable. Overfit to the numbers and you&rsquo;ll optimize for the dashboard while the actual engineering culture rots underneath.</p>
]]></content:encoded></item><item><title>Terraform at Scale: What Changed Since 2019</title><link>https://lawzava.com/blog/2021-12-06-terraform-at-scale/</link><pubDate>Mon, 06 Dec 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-12-06-terraform-at-scale/</guid><description>Two years after my first Terraform-at-scale post: what held up, what broke, and what I do differently now with modules, workspaces, CI, and policy.</description><content:encoded><![CDATA[<p>Two years after my first Terraform-at-scale post, the core advice holds: split state by ownership, version your modules, run everything in CI. What changed: workspaces are still a trap, Terraform Cloud is worth considering, OPA/Conftest policy enforcement is now table stakes, and your module registry is your most important internal product. If you&rsquo;re still running <code>terraform apply</code> from your laptop, we need to talk.</p>
<p>I wrote about  <a href="/blog/2019-09-23-terraform-at-scale/"
   
   >Terraform patterns back in 2019</a>
 when I was mostly dealing with teams of 5-15 engineers sharing a handful of state files. Since then I&rsquo;ve helped scale Terraform at organizations with hundreds of engineers, dozens of AWS accounts, and thousands of managed resources.</p>
<p>Some of what I wrote in 2019 held up. Some of it was naive. Here is the updated version.</p>
<h2 id="what-held-up">What Held Up</h2>
<p><strong>Split state by ownership and blast radius.</strong> This is still the single most important Terraform pattern. One state file per component, scoped by team ownership and failure domain. Networking is separate from application infrastructure. Shared data stores are separate from service-specific resources.</p>
<p>The reason hasn&rsquo;t changed: a <code>terraform plan</code> that touches resources owned by three different teams is a plan nobody wants to review and nobody wants to approve. Small states, clear ownership, independent deploys.</p>
<p><strong>Remote state with locking is non-negotiable.</strong> S3 + DynamoDB for AWS. Still the standard baseline in 2021. Encrypt state at rest, restrict access to the minimum set of IAM roles, and never store state locally.</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;company-terraform-state&#34;</span>
</span></span><span style="display:flex;"><span>    key            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prod/networking/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>Explicit cross-component dependencies.</strong> Publish outputs. Consume them through <code>terraform_remote_state</code> data sources or (better) through SSM parameters or a service registry. Don&rsquo;t assume. Don&rsquo;t hardcode. If many stacks need the same value, treat it as a versioned contract.</p>
<h2 id="what-i-do-differently-now">What I Do Differently Now</h2>
<h3 id="modules-are-a-product-not-a-library">Modules Are a Product, Not a Library</h3>
<p>In 2019 I treated  <a href="/blog/2018-10-29-infrastructure-as-code-patterns/"
   
   >modules</a>
 as code reuse. Wrong framing. Modules at scale are a contract between the platform team and everyone else. The interface matters more than the implementation.</p>
<p>A good module in 2021:</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;service&#34;</span> {
</span></span><span style="display:flex;"><span>  source  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;company/ecs-service/aws&#34;</span>
</span></span><span style="display:flex;"><span>  version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 2.1&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders-api&#34;</span>
</span></span><span style="display:flex;"><span>  environment <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;production&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">  # everything else has sane defaults
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p>Narrow input surface. Strong defaults. Documented outputs. Pinned versions. If a product team has to read the module source to use it, the module has failed.</p>
<p>I now recommend running an internal module registry. Terraform Cloud has one built in. If you&rsquo;re self-hosting, a simple Git-based registry with tagged releases works. The point is discoverability and versioning. Without it, module consumption is copy-paste and you lose the ability to push improvements to consumers.</p>
<h3 id="workspaces-are-still-a-trap">Workspaces Are Still a Trap</h3>
<p>I was cautious about workspaces in 2019. I&rsquo;m more forceful now: don&rsquo;t use them for environment separation. Directories per environment are uglier but dramatically safer.</p>
<p>With directories, each environment has its own backend config, its own variable files, and its own state. You can review a production change without worrying about accidentally picking the wrong workspace. You can apply to staging without touching production. The blast radius is obvious from the file path.</p>
<p>Workspaces hide this separation behind a CLI flag. I&rsquo;ve seen production outages caused by <code>terraform workspace select</code> picking the wrong environment. Twice at different companies. Both times the engineer was experienced and careful. The tool made a catastrophic mistake easy.</p>
<p>Use workspaces for ephemeral environments (PR preview environments, load test clusters) where the lifecycle is short and the blast radius is low.</p>
<h3 id="ci-is-the-only-operator">CI Is the Only Operator</h3>
<p>In 2019 I said Terraform should run in CI. In 2021 I say nobody should have <code>terraform apply</code> credentials on their laptop. Period.</p>
<p>The workflow:</p>
<ol>
<li>Developer opens a PR with infrastructure changes</li>
<li>CI runs <code>terraform plan</code> and posts the output as a PR comment</li>
<li>A reviewer reads the plan (the actual plan output, not just the code diff)</li>
<li>Merge to main triggers <code>terraform apply</code></li>
<li>Short-lived credentials via OIDC federation. No long-lived AWS keys anywhere.</li>
</ol>
<p>This gives you an audit trail, consistent environments, peer review of actual changes, and no &ldquo;I ran apply from my laptop and I&rsquo;m not sure what happened&rdquo; incidents.</p>
<h3 id="policy-enforcement-is-table-stakes">Policy Enforcement Is Table Stakes</h3>
<p>In 2019, policy enforcement felt optional. In 2021, after watching too many &ldquo;someone opened 0.0.0.0/0 on a production security group&rdquo; incidents, it&rsquo;s mandatory.</p>
<p>OPA with Conftest is my go-to for self-hosted setups. Terraform Cloud has Sentinel. Either works. The goal is the same: block known-bad patterns before they reach production.</p>
<p>Policies I enforce everywhere:</p>
<ul>
<li>No public ingress on security groups without explicit approval</li>
<li>All resources must have required tags (team, environment, service)</li>
<li>No inline IAM policies; use managed policies</li>
<li>S3 buckets must have encryption and versioning enabled</li>
<li>RDS instances must not be publicly accessible</li>
</ul>
<p>These aren&rsquo;t business logic. They&rsquo;re guardrails that prevent the easy mistakes.</p>
<h3 id="secrets-management-got-better">Secrets Management Got Better</h3>
<p>State files contain sensitive data. This was true in 2019 and it&rsquo;s still true in 2021. The difference is that the tooling around it improved.</p>
<p>Use a secrets manager (AWS Secrets Manager, HashiCorp Vault) and pass sensitive values at runtime. Mark outputs as <code>sensitive</code> so they don&rsquo;t leak in plan output or CI logs. Never put credentials, API keys, or certificates in <code>.tfvars</code> files.</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">output</span> <span style="color:#e6db74">&#34;database_password&#34;</span> {
</span></span><span style="display:flex;"><span>  value     <span style="color:#f92672">=</span> <span style="color:#66d9ef">aws_db_instance</span>.<span style="color:#66d9ef">main</span>.<span style="color:#66d9ef">password</span>
</span></span><span style="display:flex;"><span>  sensitive <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="drift-detection-isnt-optional">Drift Detection Isn&rsquo;t Optional</h3>
<p>Terraform assumes it&rsquo;s the only thing changing your infrastructure. It isn&rsquo;t. Console clicks, auto-scaling events, AWS service updates: drift happens.</p>
<p>Run <code>terraform plan</code> on a schedule (daily for production, weekly for staging) and alert on unexpected changes. This catches two things: manual changes that should have been codified, and resource changes that indicate a problem.</p>
<h2 id="the-operational-routine">The Operational Routine</h2>
<p>Terraform isn&rsquo;t deploy-and-forget. It needs maintenance.</p>
<ul>
<li><strong>Weekly:</strong> Review plan output for drift in production stacks</li>
<li><strong>Monthly:</strong> Update provider versions (pin them, but don&rsquo;t let them go stale for months)</li>
<li><strong>Quarterly:</strong> Audit state access, review module usage, clean up orphaned resources</li>
<li><strong>Every PR:</strong> Review the plan output, not just the HCL diff</li>
</ul>
<h2 id="what-i-would-tell-2019-me">What I Would Tell 2019 Me</h2>
<p>Invest in the module registry earlier. It&rsquo;s the highest-leverage thing you can build for Terraform at scale. Every team that consumes a well-designed module is a team that isn&rsquo;t reinventing infrastructure patterns.</p>
<p>Stop fighting workspaces. Just use directories. The extra files are worth the safety.</p>
<p>Get apply off of laptops on day one, not &ldquo;when we have time.&rdquo; The incident that happens before you migrate to CI will be more expensive than the migration itself.</p>
<p>Terraform scales fine. The syntax isn&rsquo;t the hard part. Coordination, ownership, and safety are the hard parts. Get those right and the HCL takes care of itself.</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>Feature Flags at Scale: Ownership, Expiry, and Cleanup</title><link>https://lawzava.com/blog/2021-09-06-feature-flags-at-scale/</link><pubDate>Mon, 06 Sep 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-09-06-feature-flags-at-scale/</guid><description>847 feature flags, about 200 with owners. Lessons from production codebases on flag types, ownership rules, fail modes, and cleanup.</description><content:encoded><![CDATA[<p>Last year, on a platform I worked on, I ran a script to count our feature flags. We had 847. Active, production feature flags spread across every service we ran. I knew we had a lot. I didn&rsquo;t know we had that many.</p>
<p>The worst part: when I cross-referenced with our flag metadata, only about 200 had an owner listed. The rest were orphans. Nobody knew who created them, what they controlled, or whether they were safe to remove. One flag had been set to &ldquo;false&rdquo; for 14 months. The code path behind it was unreachable. But nobody dared delete it because nobody knew what it did.</p>
<p>That&rsquo;s what feature flags look like at scale when you don&rsquo;t have discipline. And I&rsquo;m writing this post because I don&rsquo;t want you to end up there.</p>
<h2 id="flags-are-great-stale-flags-are-debt">Flags are great. Stale flags are debt.</h2>
<p>The pitch for feature flags is solid. Separate deployment from release. Ship code to production behind a flag. Roll out  <a href="/blog/2021-02-08-gitops-progressive-delivery/"
   
   >gradually</a>
. Roll back instantly. Run experiments. Gate features by customer plan. Use kill switches during incidents.</p>
<p>All of that works. I&rsquo;m a believer. Flags have saved me multiple times during incidents. Flipping a kill switch is faster than deploying a rollback. Having a gradual rollout catch a regression at 5% of traffic instead of 100% is valuable.</p>
<p>The problem isn&rsquo;t the concept. The problem is lifecycle management. Flags are easy to create. Nobody wants to remove them.</p>
<h2 id="categorize-your-flags-or-drown">Categorize your flags or drown</h2>
<p>After the 847-flag incident, we introduced four categories:</p>
<p><strong>Release flags</strong> are short-lived. Control rollout of a new feature. Expected lifetime: days to weeks. Must be removed once the feature is fully rolled out.</p>
<p><strong>Experiment flags</strong> are medium-lived. Power A/B tests. Expected lifetime: duration of the experiment. Must be retired with a clear outcome documented.</p>
<p><strong>Operational flags</strong> are potentially long-lived. Kill switches, circuit breakers, graceful degradation. These can stay, but they need periodic review.</p>
<p><strong>Permission flags</strong> are permanent. Gate access by customer plan or contract. These are really entitlements, not feature flags, and should be treated differently in your code.</p>
<p>The categories matter because the cleanup rules are different. A release flag that&rsquo;s still around after a month is a problem. An operational kill switch that&rsquo;s been around for a year is fine.</p>
<h2 id="the-rules-that-saved-us">The rules that saved us</h2>
<p>After the cleanup, we implemented a simple policy. Every new flag must have:</p>
<ol>
<li>An owner (the person who will remove it)</li>
<li>A type (release, experiment, operational, permission)</li>
<li>An expected removal date (except operational and permission flags)</li>
<li>A documented default value and failure mode</li>
</ol>
<p>If any of those are missing, the flag can&rsquo;t be created. We enforced this in our flag management wrapper.</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">Flag</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>       <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Owner</span>      <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Type</span>       <span style="color:#a6e22e">FlagType</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">RemoveBy</span>   <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</span> <span style="color:#75715e">// zero for operational/permission</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">DefaultVal</span> <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">FailMode</span>   <span style="color:#a6e22e">FailMode</span>  <span style="color:#75715e">// FailOpen or FailClosed</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">FlagService</span>) <span style="color:#a6e22e">Create</span>(<span style="color:#a6e22e">f</span> <span style="color:#a6e22e">Flag</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">Owner</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">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;flag must have an owner&#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">f</span>.<span style="color:#a6e22e">Type</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">Release</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">f</span>.<span style="color:#a6e22e">RemoveBy</span>.<span style="color:#a6e22e">IsZero</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">errors</span>.<span style="color:#a6e22e">New</span>(<span style="color:#e6db74">&#34;release flags must have a removal date&#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></span></code></pre></div><p>We also added a weekly report: flags past their removal date, flags with no evaluations in the last 30 days, and flags where the &ldquo;off&rdquo; path has never been exercised in production. That last one is subtle but important: if you&rsquo;ve never seen the fallback path in production, you don&rsquo;t actually know it works.</p>
<h2 id="fail-open-or-fail-closed">Fail-open or fail-closed</h2>
<p>This is the decision most teams skip. What happens when the flag service is down?</p>
<p>For a payment flow, you probably want fail-closed. Better to show an error than process a payment through an untested code path.</p>
<p>For a UI experiment, fail-open makes sense. Show the default experience. Nobody notices.</p>
<p>At one enterprise, the flag service went down for 20 minutes and their checkout flow broke because every flag defaulted to &ldquo;false&rdquo;, including the flag that enabled their only working checkout flow. They&rsquo;d never tested what &ldquo;all flags off&rdquo; looked like. Don&rsquo;t be that team.</p>
<h2 id="the-cleanup-problem-is-a-people-problem">The cleanup problem is a people problem</h2>
<p>Technical solutions help. Expiration dates, automated reports, lint rules that flag dead code paths. But the real problem is incentives.</p>
<p>Nobody gets credit for removing a feature flag. Shipping a new feature is visible. Cleaning up after it isn&rsquo;t. So flags accumulate, like any other  <a href="/blog/2017-12-18-technical-debt-triage-framework-for-prioritization/"
   
   >unowned tech debt</a>
.</p>
<p>On that platform, we made flag cleanup part of the definition of done. A feature isn&rsquo;t finished when it&rsquo;s rolled out to 100%. It&rsquo;s finished when the flag is removed and the old code path is deleted. We tracked &ldquo;flag debt&rdquo; on our engineering dashboard alongside other health metrics.</p>
<p>At larger organizations, I recommend a quarterly flag review. Pull up the full inventory. For every flag without a recent evaluation, ask: is this still needed? For every flag past its removal date, assign cleanup as a sprint task. It&rsquo;s unglamorous work. It prevents the kind of mess that makes your codebase feel hostile to new engineers.</p>
<h2 id="keep-it-simple">Keep it simple</h2>
<p>The best feature flag setup I&rsquo;ve seen is also the simplest. Boolean flags with deterministic evaluation. Clear categories. Mandatory ownership. Aggressive cleanup.</p>
<p>The worst setups are the ones with complex targeting rules, flags that depend on other flags, and evaluation logic spread across three different layers. If a single flag requires a whiteboard to explain, break it into smaller flags or put the complexity in application code where it can be tested properly.</p>
<p>Feature flags are a powerful tool. They&rsquo;re also a maintenance commitment. Treat them like production configuration, because that&rsquo;s what they are.</p>
]]></content:encoded></item><item><title>Observability-Driven Development: Instrument Before You Ship</title><link>https://lawzava.com/blog/2021-06-14-observability-driven-development/</link><pubDate>Mon, 14 Jun 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-06-14-observability-driven-development/</guid><description>Observability-driven development without the jargon: structured logs, RED metrics, traces, and SLO alerts that ship with each feature.</description><content:encoded><![CDATA[<p>&ldquo;Observability-Driven Development&rdquo; has entered the conference talk circuit and I already hate the name. It sounds like a methodology. Like you need a certification or a Slack channel or a retro format.</p>
<p>It isn&rsquo;t a methodology. It means instrumenting your code before you ship it. That&rsquo;s the whole thing.</p>
<p>And yet I keep walking into systems, at startups and enterprises alike, where observability was bolted on after the first production fire. The logs are a mess. Every service logs differently. Metrics exist for some endpoints but not others. Nobody has traces. The dashboards are either empty or full of vanity charts that nobody looks at.</p>
<h2 id="the-actual-problem">The actual problem</h2>
<p>When observability comes last, every team invents their own approach. Service A logs JSON with <code>request_id</code>. Service B logs plain text with <code>req_id</code>. Service C doesn&rsquo;t log request IDs at all. You find this out at 2am during an outage while trying to correlate a failure across three services.</p>
<p>I&rsquo;ve lived this. At the fintech startup we had a period where our financial data pipeline logs were useless for debugging cross-service issues because every team had picked their own field names. Fixing that retroactively took weeks. If we&rsquo;d agreed on a format before writing the services, it would&rsquo;ve taken an afternoon.</p>
<h2 id="what-odd-actually-means-in-practice">What &ldquo;ODD&rdquo; actually means in practice</h2>
<p>Before you write a feature, answer three questions:</p>
<ol>
<li>How will I know this is working correctly in production?</li>
<li>How will I know this is slow?</li>
<li>How will I know this is broken?</li>
</ol>
<p>If you can&rsquo;t answer those, you&rsquo;re not ready to write the code. That&rsquo;s the whole framework. No acronym needed.</p>
<h2 id="structured-logs-or-nothing">Structured logs or nothing</h2>
<p>Your logs should be structured JSON with consistent field names across every service. Non-negotiable.</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 style="color:#f92672">&#34;level&#34;</span>:<span style="color:#e6db74">&#34;info&#34;</span>,<span style="color:#f92672">&#34;event&#34;</span>:<span style="color:#e6db74">&#34;order_created&#34;</span>,<span style="color:#f92672">&#34;request_id&#34;</span>:<span style="color:#e6db74">&#34;abc-123&#34;</span>,<span style="color:#f92672">&#34;order_id&#34;</span>:<span style="color:#e6db74">&#34;ord-456&#34;</span>,<span style="color:#f92672">&#34;duration_ms&#34;</span>:<span style="color:#ae81ff">42</span>}
</span></span></code></pre></div><p>Every log line gets a <code>request_id</code>. Every log line gets an <code>event</code> name. Every log line gets a <code>level</code>. If you&rsquo;re logging <code>fmt.Println(&quot;something happened&quot;)</code> in production Go code, we need to talk.</p>
<p>Pick your field names once. Write them down. Enforce them in code review. This is boring work. It pays off every single incident.</p>
<h2 id="metrics-red-and-stop">Metrics: RED and stop</h2>
<p>For services, use  <a href="/blog/2016-12-12-production-monitoring-metrics-that-matter/"
   
   >RED metrics</a>
. Rate, Errors, Duration. For every endpoint. That&rsquo;s your baseline.</p>
<p>I see teams go wild with custom metrics on day one. Thirty metrics per service, half of them never queried. Meanwhile they&rsquo;re missing basic error rate tracking on their most critical endpoint. Start with RED. Add custom metrics when you have a specific question that RED can&rsquo;t answer.</p>
<p>One thing that will absolutely burn you: high-cardinality labels. If you&rsquo;re putting user IDs or full URLs into metric labels, you&rsquo;re building a cost bomb. I saw one team&rsquo;s Prometheus storage costs triple in a month because someone added a <code>path</code> label that included query parameters. Keep labels to things like <code>method</code>, <code>status_code</code>, <code>service</code>. Low and predictable.</p>
<h2 id="traces-arent-optional">Traces aren&rsquo;t optional</h2>
<p>Distributed tracing used to feel like a luxury. It&rsquo;s not. If you&rsquo;re running more than two services, you need traces. Full stop.</p>
<p>Every inbound request starts a trace. Every outbound call propagates the trace context. This is a few lines of middleware in Go. It&rsquo;s trivial. And it&rsquo;s the difference between &ldquo;I think the problem is in the payment service&rdquo; and &ldquo;I can see the exact call that took 4 seconds.&rdquo;</p>
<p>Sample if you need to for cost reasons. But sample consistently. Don&rsquo;t sample 100% on staging and 1% on production and then wonder why you can never find the trace you need.</p>
<h2 id="make-it-part-of-code-review">Make it part of code review</h2>
<p>This is where it sticks or falls apart. If observability isn&rsquo;t in your code review checklist, it won&rsquo;t happen.</p>
<p>When I review a PR that adds a new endpoint, I look for:</p>
<ul>
<li>Does the handler emit RED metrics?</li>
<li>Are key events logged with stable fields?</li>
<li>Does the trace propagate to downstream calls?</li>
<li>Are the labels safe?</li>
</ul>
<p>If the answer is no, the PR isn&rsquo;t ready. Same as missing tests. Same as missing error handling. Observability isn&rsquo;t a follow-up ticket. It ships with the feature.</p>
<h2 id="alerts-that-dont-suck">Alerts that don&rsquo;t suck</h2>
<p>Most alerts are terrible. They fire on every blip, train everyone to ignore them, and then nobody notices when something actually breaks.</p>
<p>Alert on symptoms, not causes. Alert on &ldquo;error rate is above X% for Y minutes,&rdquo; not &ldquo;one request returned a 500.&rdquo; Better yet, use  <a href="/blog/2019-05-20-effective-slos/"
   
   >SLO-based alerts</a>
. Set an error budget. Alert when you&rsquo;re burning through it too fast. This single change cut our alert noise at my infrastructure startup by something like 80%.</p>
]]></content:encoded></item><item><title>DevSecOps in Practice: The Pipeline Controls I Implement</title><link>https://lawzava.com/blog/2021-04-19-devsecops-practices/</link><pubDate>Mon, 19 Apr 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-04-19-devsecops-practices/</guid><description>Pre-commit hooks, CI security stages, OPA Gatekeeper policies, and a triage system: the DevSecOps controls I set up so developers don&amp;amp;rsquo;t route around them.</description><content:encoded><![CDATA[<p>Security as a final gate doesn&rsquo;t work at enterprise scale. I&rsquo;ve been building DevSecOps pipelines at large telecom companies, drawing on what cyber-defense exercises taught me. What follows is what I implement: pre-commit hooks for secrets, CI pipeline security stages with real configs, container scanning, runtime policy enforcement with OPA, and the triage system that makes all of it sustainable. Every control has to be fast enough that developers don&rsquo;t route around it.</p>
<p>I have a rule when I start working with a new team: before I recommend anything, I ask the team to show me how code gets from a developer&rsquo;s laptop to production. Every step. Every tool. Every credential.</p>
<p>The answer is almost always incomplete. Not because people are hiding things, but because nobody has mapped the full path recently. That incomplete picture is where security gaps live.</p>
<p>DevSecOps is the practice of embedding security checks into the workflow developers already use. You can&rsquo;t buy it. If it slows them down, they&rsquo;ll circumvent it. If it&rsquo;s invisible and fast, they&rsquo;ll barely notice it&rsquo;s there. That&rsquo;s the target.</p>
<h2 id="layer-1-the-developers-machine">Layer 1: the developer&rsquo;s machine</h2>
<p>The cheapest place to catch a security issue is before it ever leaves the developer&rsquo;s laptop. Pre-commit hooks are the first line.</p>
<p>Here is what I install on every project:</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">local</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">gitleaks</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">gitleaks</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">entry</span>: <span style="color:#ae81ff">gitleaks --path=. --unstaged -v</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">language</span>: <span style="color:#ae81ff">system</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pass_filenames</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">https://github.com/hadolint/hadolint</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">rev</span>: <span style="color:#ae81ff">v2.1.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">hadolint</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">args</span>: [<span style="color:#e6db74">&#39;--ignore&#39;</span>, <span style="color:#e6db74">&#39;DL3008&#39;</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.48.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_validate</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">terraform_tfsec</span>
</span></span></code></pre></div><p>Gitleaks catches secrets before they hit Git history. Hadolint catches Dockerfile anti-patterns. Terraform validation and tfsec catch infrastructure misconfigurations.</p>
<p>The critical design principle: these hooks must run in under 10 seconds. If a pre-commit hook takes 30 seconds, developers disable it. I&rsquo;ve seen this happen at every organization I&rsquo;ve worked with. Speed isn&rsquo;t optional.</p>
<p>For Go projects specifically, I also add <code>go vet</code>, <code>staticcheck</code>, and <code>gosec</code> as pre-commit checks. Gosec catches common security issues like SQL injection patterns and hardcoded credentials.</p>
<h2 id="layer-2-ci-pipeline-security-stages">Layer 2: CI pipeline security stages</h2>
<p>Pre-commit hooks are a safety net, not a gate. The real enforcement happens in CI. Here is the pipeline structure I implement, using GitHub Actions as the example:</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"># .github/workflows/security.yml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Security Checks</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>: [<span style="color:#ae81ff">push, pull_request]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">secrets-scan</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">fetch-depth</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">zricethezav/gitleaks-action@v1.5.0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">sast</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Run gosec</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">securego/gosec@master</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">args</span>: <span style="color:#e6db74">&#39;-exclude-generated ./...&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Run Semgrep</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">returntocorp/semgrep-action@v1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">config</span>: &gt;-<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            p/owasp-top-ten
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            p/golang</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">dependency-scan</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Run Trivy for dependencies</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">aquasecurity/trivy-action@master</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">scan-type</span>: <span style="color:#e6db74">&#39;fs&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">severity</span>: <span style="color:#e6db74">&#39;CRITICAL,HIGH&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">exit-code</span>: <span style="color:#e6db74">&#39;1&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">container-scan</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">needs</span>: [<span style="color:#ae81ff">sast]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build image</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">docker build -t app:${{ github.sha }} .</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Scan image</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">aquasecurity/trivy-action@master</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">image-ref</span>: <span style="color:#e6db74">&#39;app:${{ github.sha }}&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">severity</span>: <span style="color:#e6db74">&#39;CRITICAL,HIGH&#39;</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">exit-code</span>: <span style="color:#e6db74">&#39;1&#39;</span>
</span></span></code></pre></div><p>Four jobs: secrets scanning, static analysis, and dependency scanning run in parallel, and container scanning runs once SAST passes. There&rsquo;s no point scanning an image if the code has critical issues.</p>
<p>The key decision is the gating policy. I use a two-tier approach:</p>
<ul>
<li><strong>Critical and High severity</strong>: block the pipeline. The PR can&rsquo;t merge.</li>
<li><strong>Medium and Low</strong>: create a tracking issue automatically, assign it to the team, set a remediation deadline. Don&rsquo;t block the pipeline.</li>
</ul>
<p>This is the balance that makes DevSecOps sustainable. If you block on everything, teams stop caring about the signal because it&rsquo;s always red. If you block on nothing, the findings pile up and nobody fixes them.</p>
<p>At one telecom company, we went from zero automated security checks to full pipeline coverage in three weeks. The first week was brutal: the initial scan found over 200 findings across their repositories. We triaged them into buckets, fixed the critical ones immediately, and created a backlog for the rest. Within a month, new PRs were consistently clean.</p>
<h2 id="layer-3-infrastructure-and-runtime-controls">Layer 3: infrastructure and runtime controls</h2>
<p>Static analysis catches what it can see. It can&rsquo;t see runtime behavior, configuration drift, or privilege escalation. That&rsquo;s where policy-as-code and runtime controls come in.</p>
<p>I use Open Policy Agent (OPA) with Gatekeeper for Kubernetes-based deployments, as part of the  <a href="/blog/2019-04-22-kubernetes-security-hardening/"
   
   >baseline hardening</a>
 every cluster should get. Here is a constraint that prevents containers from running as root:</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">constraints.gatekeeper.sh/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">K8sPSPAllowedUsers</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">require-non-root</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">match</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kinds</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">apiGroups</span>: [<span style="color:#e6db74">&#34;&#34;</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Pod&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">excludedNamespaces</span>: [<span style="color:#e6db74">&#34;kube-system&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runAsUser</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">rule</span>: <span style="color:#ae81ff">MustRunAsNonRoot</span>
</span></span></code></pre></div><p>And one that requires resource limits on every container:</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">constraints.gatekeeper.sh/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">K8sContainerLimits</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">require-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">match</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kinds</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">apiGroups</span>: [<span style="color:#e6db74">&#34;&#34;</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Pod&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;2&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;2Gi&#34;</span>
</span></span></code></pre></div><p>These constraints are enforced at admission time. If a deployment tries to run a container as root, the API server rejects it. No exceptions. No &ldquo;we&rsquo;ll fix it later.&rdquo; The cluster enforces the policy.</p>
<p>For network policies, I default to deny-all and explicitly allow what is needed:</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-all</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">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>Then each service gets a specific policy that allows only its known dependencies. This is tedious to set up and worth every minute. When a service gets compromised, lateral movement is limited by the network policy. I first saw this pattern in cyber-defense exercises. The principle is the same regardless of scale. Default deny. Explicit allow. No implicit trust.</p>
<h2 id="layer-4-triage-and-ownership">Layer 4: triage and ownership</h2>
<p>This is where most DevSecOps implementations die. The tools work. The scans run. The findings pile up. Nobody owns them.</p>
<p>My triage system is simple:</p>
<ul>
<li><strong>Critical</strong>: fix within 24 hours. Page the on-call if necessary. These are actively exploitable issues in production code.</li>
<li><strong>High</strong>: fix within one sprint. Track in the regular backlog. Team lead owns the follow-up.</li>
<li><strong>Medium</strong>: fix within 30 days. Batch these into security-focused work blocks.</li>
<li><strong>Low</strong>: fix when convenient, or accept the risk with documentation.</li>
</ul>
<p>Every finding gets an owner. Not a team: a person. &ldquo;The security team will handle it&rdquo; is how findings go to die.</p>
<p>I also set up a weekly triage meeting. Fifteen minutes. Review new findings, check progress on open items, and escalate anything that&rsquo;s stuck. At one company, this meeting reduced the median age of open vulnerabilities from 47 days to 11 days in two months. Not because we added more tools. Because someone was consistently asking &ldquo;is this fixed yet?&rdquo;</p>
<h2 id="secrets-management">Secrets management</h2>
<p>Secrets in source control are the most common security finding I see.  Hardcoded API keys, database passwords in config files, AWS credentials in test fixtures. It happens everywhere.</p>
<p>The fix is structural, not behavioral. Don&rsquo;t tell developers &ldquo;be careful with secrets.&rdquo; Give them a system that makes it hard to leak secrets in the first place.</p>
<ul>
<li>All secrets live in a vault (HashiCorp Vault, AWS Secrets Manager, whatever fits your stack).</li>
<li>Applications pull secrets at runtime, never at build time.</li>
<li>CI credentials are scoped to individual jobs and rotate automatically, the same rule I apply when  <a href="/blog/2021-01-11-supply-chain-security-practices/"
   
   >hardening build pipelines</a>
.</li>
<li>Pre-commit hooks catch anything that slips through.</li>
<li>Git history scanning runs on the full repository, not just the diff. Because secrets removed in a later commit are still in the history.</li>
</ul>
<h2 id="what-ive-learned-from-doing-this-repeatedly">What I&rsquo;ve learned from doing this repeatedly</h2>
<p>The technical setup is the easy part. The hard part is culture.</p>
<p>Developers aren&rsquo;t the enemy of security. They&rsquo;re the delivery mechanism for security. If you treat them as adversaries (locking everything down, blocking every build, requiring approvals for trivial changes), they&rsquo;ll find workarounds. Shadow builds. Personal accounts. Manual deployments that skip the pipeline entirely.</p>
<p>The organizations that succeed with DevSecOps are the ones that make secure the default and make the default fast. Secure container base images that are pre-approved. Templates that include security configs out of the box. Scanning that runs in parallel with tests instead of after them.</p>
<p>Security should feel like good tooling. When it does, adoption is automatic.</p>
]]></content:encoded></item><item><title>MLOps Fundamentals: Fix the Basics Before Buying Tools</title><link>https://lawzava.com/blog/2021-03-22-mlops-fundamentals/</link><pubDate>Mon, 22 Mar 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-03-22-mlops-fundamentals/</guid><description>Most teams buying MLOps tools can&amp;amp;rsquo;t version their training data. Start with dataset versioning, a release checklist, and one monitoring signal.</description><content:encoded><![CDATA[<p>Every other pitch deck I see this year includes &ldquo;MLOps&rdquo; somewhere on the roadmap. Platform teams want to build ML infrastructure. Data science teams want self-service model deployment. VPs want dashboards showing model performance in production.</p>
<p>Almost none of them are ready for any of it.</p>
<p>I say this as someone who has watched  <a href="/blog/2018-02-05-machine-learning-for-backend-engineers/"
   
   >ML projects</a>
 go sideways in multiple organizations. I keep seeing the same pattern. A data science team trains a model in a notebook. It works great in evaluation. Someone manually deploys it. It slowly degrades over months because nobody is monitoring whether the real world still looks like the training data. Eventually a stakeholder notices the predictions are garbage and the team scrambles to retrain.</p>
<p>That&rsquo;s not an MLOps problem. That&rsquo;s a &ldquo;we don&rsquo;t even have the basics&rdquo; problem.</p>
<h2 id="what-mlops-actually-requires">What MLOps actually requires</h2>
<p>ML systems aren&rsquo;t just model files. They are  <a href="/blog/2017-04-24-building-data-pipelines-that-dont-break/"
   
   >data pipelines</a>
, training code, feature stores, serving infrastructure, and monitoring, all tightly coupled in ways that traditional software isn&rsquo;t. A code change might not matter. A data distribution shift always matters. The output is probabilistic. Degradation is silent.</p>
<p>MLOps is the discipline that keeps this entire system reliable. But discipline implies prerequisites.</p>
<p>Can your team reproduce a model from six months ago? Not just the code, but the exact dataset, the feature definitions, the hyperparameters, the environment? If no, you&rsquo;re not ready for MLOps. You&rsquo;re ready for version control.</p>
<p>Can you answer &ldquo;what data was this model trained on?&rdquo; If that question requires someone to check their memory or dig through a shared drive, you&rsquo;re not ready for automated retraining pipelines. You&rsquo;re ready for data documentation.</p>
<p>Can you tell when a model&rsquo;s predictions are getting worse? Not when a stakeholder complains, but when the drift actually starts? If you have no monitoring on prediction distributions or feature distributions, you&rsquo;re not ready for canary deployments of models. You&rsquo;re ready for basic observability.</p>
<h2 id="mlops-v0-where-to-start">MLOps v0: where to start</h2>
<p>Here is what I tell teams who want MLOps:</p>
<p>Pick your single most important model. The one that actually affects revenue or user experience. Write down its data dependencies. All of them. Where does the training data come from? Who owns those sources? What happens if a schema changes upstream?</p>
<p>Then add three things:</p>
<ol>
<li>
<p><strong>Dataset versioning.</strong> When you train, snapshot the data. Tag it. Store it alongside the model artifact. You need to be able to say &ldquo;model v7 was trained on dataset snapshot X from date Y.&rdquo;</p>
</li>
<li>
<p><strong>A release checklist.</strong> Before a model goes to production, someone checks it against a known baseline on held-out data. Latency and memory usage are within serving targets. Known edge cases are tested. This is a manual checklist. That&rsquo;s fine. Manual and consistent beats automated and nonexistent.</p>
</li>
<li>
<p><strong>One monitoring signal.</strong> Pick one metric that reflects whether the model is doing its job in the real world. Not offline accuracy; something tied to a business outcome. Monitor it. Set an alert threshold. When it degrades, retrain.</p>
</li>
</ol>
<p>That&rsquo;s it. That&rsquo;s your MLOps v0. It&rsquo;s not glamorous. It won&rsquo;t impress anyone at a conference. But it prevents the silent failure mode that kills most ML projects.</p>
<h2 id="when-youre-actually-ready-for-more">When you&rsquo;re actually ready for more</h2>
<p>Once those basics are reliable (and I mean actually reliable, not &ldquo;we set it up three months ago and haven&rsquo;t checked since&rdquo;), then you can start adding automation. Automated retraining triggers. Feature stores. Shadow deployments. A/B testing infrastructure. Model registries with governance controls.</p>
<p>But every layer you add assumes the previous layer works. Automated retraining is useless if your data isn&rsquo;t versioned. A feature store is overhead if your feature definitions aren&rsquo;t documented. A/B testing is theater if you can&rsquo;t measure business outcomes.</p>
<p>The MLOps vendor market is exploding right now. Everyone is selling platforms. Most of them solve problems that come after the problems most teams actually have.</p>
<p>Fix the boring stuff first. The tooling can wait.</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>Observability for Small Distributed Teams: A Minimal Stack</title><link>https://lawzava.com/blog/2020-09-14-observability-distributed-teams/</link><pubDate>Mon, 14 Sep 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-09-14-observability-distributed-teams/</guid><description>Most observability advice targets 500-engineer orgs. For a small distributed team: structured logs, a request_id, one dashboard per service, few alerts.</description><content:encoded><![CDATA[<p>You don&rsquo;t need Datadog&rsquo;s enterprise tier. You need structured logs, one good dashboard per service, alerts that don&rsquo;t cry wolf, and a <code>request_id</code> on everything. That&rsquo;s 80% of it.</p>
<p>I&rsquo;ve been working with distributed teams for most of this year. Some are five people across three time zones. Some are thirty across eight. The pattern I keep seeing: they either have zero observability or they went full enterprise cargo cult and now nobody can find anything.</p>
<p>There&rsquo;s a middle ground.</p>
<h2 id="why-distributed-teams-feel-the-gaps-first">Why distributed teams feel the gaps first</h2>
<p>In an office, someone notices something is slow. They say it out loud. Someone else goes &ldquo;oh yeah, I deployed ten minutes ago.&rdquo; Problem found in under a minute.</p>
<p>Remote? That same issue sits in a Slack thread for 45 minutes while people in different time zones wake up, read context, and try to figure out what changed. I&rsquo;ve watched this happen. Repeatedly.</p>
<p>More tools won&rsquo;t fix it. What fixes it is making your systems capable of answering basic questions without requiring a human to be online at the right moment. That&rsquo;s the gap between  <a href="/blog/2018-07-09-observability-beyond-monitoring/"
   
   >monitoring and observability</a>
.</p>
<p>Three questions. That&rsquo;s it:</p>
<ol>
<li>Is this thing broken right now?</li>
<li>What changed recently?</li>
<li>Where do I look next?</li>
</ol>
<p>If your setup can answer those, you&rsquo;re ahead of most teams I&rsquo;ve worked with.</p>
<h2 id="enterprise-observability-isnt-your-observability">Enterprise observability isn&rsquo;t your observability</h2>
<p>Google has an entire SRE organization. They built custom everything. When you read their SRE book and try to implement the same stack with your team of eight, you end up with:</p>
<ul>
<li>A Prometheus instance nobody configured alerts for</li>
<li>Grafana dashboards copied from a blog post that don&rsquo;t match your services</li>
<li>Jaeger running but with 0.1% sampling so traces are useless when you actually need them</li>
<li>An ELK stack eating 40% of your infrastructure budget</li>
</ul>
<p>I&rsquo;ve seen this exact setup at three different companies this year. Not exaggerating.</p>
<h2 id="what-you-actually-need">What you actually need</h2>
<p>Here&rsquo;s my stack recommendation for a small distributed team. Opinionated, yes. But it works.</p>
<p><strong>Logs: Structured JSON to a managed service.</strong> Loki if you&rsquo;re cheap. Papertrail if you want simple. The key is structured, not the tool. Every log line should look roughly 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-json" data-lang="json"><span style="display:flex;"><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;msg&#34;</span>: <span style="color:#e6db74">&#34;payment failed&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;checkout&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;request_id&#34;</span>: <span style="color:#e6db74">&#34;7f3c2c4d&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;user_id&#34;</span>: <span style="color:#e6db74">&#34;u_123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;error&#34;</span>: <span style="color:#e6db74">&#34;card_declined&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;duration_ms&#34;</span>: <span style="color:#ae81ff">340</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Same field names. Every service. No exceptions. The <code>request_id</code> alone will save you hours of debugging per incident. I can&rsquo;t stress this enough. Propagate it through HTTP headers, queue messages, background jobs. Everywhere.</p>
<p><strong>Metrics: Prometheus + Grafana.</strong> Still the best bang for buck. But don&rsquo;t build 30 dashboards. Build one per service. Three panels:</p>
<ol>
<li>Request rate and error rate (tells you if something is broken)</li>
<li>Latency percentiles (tells you if it&rsquo;s degrading)</li>
<li>Recent deploys and config changes overlaid on the graphs (tells you what caused it)</li>
</ol>
<p>That&rsquo;s your dashboard. If a new engineer can&rsquo;t look at it and understand the health of the service in 30 seconds, strip it down further.</p>
<p><strong>Traces: Jaeger or Zipkin, but only if you have more than two services.</strong> If you&rsquo;re a monolith with a database and a cache, traces are overhead you don&rsquo;t need yet. Just use request IDs in your logs. Seriously.</p>
<p>When you do need traces, bump sampling to at least 10% on critical paths. 0.1% default sampling means you&rsquo;ll never have a trace for the request that actually broke.</p>
<p><strong>Alerts: Less is more.</strong> Every alert that pages someone at 3am and isn&rsquo;t actionable is erosion of trust. Once your team stops trusting alerts, you&rsquo;ve lost. They&rsquo;ll start muting channels. I&rsquo;ve seen it happen.</p>
<p>My rule: every alert needs three things.</p>
<ul>
<li>A condition that&rsquo;s actually abnormal (not &ldquo;CPU above 60%&rdquo;)</li>
<li>A link to the relevant dashboard</li>
<li>A link to a runbook that says what to do first</li>
</ul>
<p>If you can&rsquo;t write those three things for an alert, the alert shouldn&rsquo;t exist.</p>
<h2 id="the-request_id-sermon">The request_id sermon</h2>
<p>I keep coming back to this because it&rsquo;s the single highest-leverage thing you can do.</p>
<p>At the fintech startup we had services talking to services talking to queues talking to workers. When something went wrong without correlation IDs, the debugging process was: check this log, then that log, then maybe this other log, hope the timestamps line up, piece it together manually. Took forever.</p>
<p>After we standardized on a single <code>request_id</code> header propagated everywhere? Same investigation. One search. Done.</p>
<p>The implementation is trivial. Middleware that reads <code>X-Request-ID</code> from incoming requests. Generates a UUID if missing. Passes it along. Logs it on every line. Takes an afternoon to implement across your whole stack.</p>
<p>An afternoon of work for months of saved debugging time. That&rsquo;s the kind of trade I like.</p>
<h2 id="runbooks-the-unsexy-high-leverage-tool">Runbooks: the unsexy high-leverage tool</h2>
<p>Nobody wants to write runbooks. I get it. But here&rsquo;s the scenario: it&rsquo;s 2am in your time zone. The alert fires. The person on call is in a different country. They&rsquo;ve been on the team for three weeks.</p>
<p>Without a runbook, they&rsquo;re messaging people, waiting for responses, guessing. With a runbook, they open it, follow the steps, and either fix it or know exactly who to escalate to.</p>
<p>Keep them short. Keep them next to the code. Update them after every incident. A runbook that says &ldquo;check the database connection pool, then check Redis, then check the upstream API timeout&rdquo; is worth more than a 50-page incident response process document nobody has read.</p>
<h2 id="mistakes-i-keep-seeing">Mistakes I keep seeing</h2>
<p><strong>Collecting everything.</strong> Storage is cheap. Cardinality explosions aren&rsquo;t. I watched a team&rsquo;s Prometheus instance fall over because they added a <code>user_id</code> label to a counter. Millions of time series. Dead monitoring system. During an outage. Ironic.</p>
<p><strong>Dashboard graveyards.</strong> Thirty dashboards, twenty-eight of which nobody has looked at in months. Two of which are actually useful but you can&rsquo;t remember which ones.  <a href="/blog/2016-12-12-production-monitoring-metrics-that-matter/"
   
   >Delete aggressively</a>
.</p>
<p><strong>Happy path instrumentation only.</strong> Your error paths need more instrumentation than your happy paths. The happy path works. You know this because nobody is complaining. The error paths are where surprises live.</p>
<p><strong>Separate conventions per team.</strong> One team calls it <code>user_id</code>, another calls it <code>userId</code>, a third calls it <code>uid</code>. Now your cross-service queries are a mess. Pick a convention. Enforce it in code review. This is boring work that pays off enormously.</p>
<h2 id="what-to-measure-about-your-observability-itself">What to measure about your observability itself</h2>
<p>One meta-metric I track: time from &ldquo;something seems wrong&rdquo; to &ldquo;I know what changed and where to look.&rdquo; If that number is going down over time, your observability is working. If it&rsquo;s flat or going up, you&rsquo;re adding complexity without adding clarity.</p>
<p>The other one: how many alerts fired this week that didn&rsquo;t need a human response? If it&rsquo;s more than 20%, you have a noise problem.</p>
<h2 id="start-here">Start here</h2>
<p>If you&rsquo;re starting from scratch with a small distributed team, do this in order:</p>
<ol>
<li>Structured JSON logs with a shared <code>request_id</code>. One week of work, max.</li>
<li>One Grafana dashboard per service with the three panels I mentioned. Another week.</li>
<li>Three to five alerts that are actually actionable. A few days.</li>
<li>Short runbooks for those alerts. A day.</li>
</ol>
<p>That&rsquo;s a month of work spread across your team. After that, you have a system that answers the three questions. Everything else (traces,  <a href="/blog/2019-05-20-effective-slos/"
   
   >SLOs</a>
, error budgets, custom metrics) can wait until you feel the pain.</p>
<p>Don&rsquo;t let perfect be the enemy of &ldquo;I can actually debug production at 2am without waking up three people.&rdquo;</p>
]]></content:encoded></item><item><title>GitHub Actions in Production: Matrix, Caching, Secrets</title><link>https://lawzava.com/blog/2020-07-20-github-actions-advanced/</link><pubDate>Mon, 20 Jul 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-07-20-github-actions-advanced/</guid><description>Matrix builds, dependency caching, gated deploys, and the security gotchas I hit building a startup&amp;amp;rsquo;s CI/CD pipeline on GitHub Actions.</description><content:encoded><![CDATA[<p>I migrated our CI/CD at my infrastructure startup from a janky CircleCI setup to GitHub Actions about four months ago. The basic stuff was trivial. Push code, run tests, green checkmark. Fine.</p>
<p>Then I needed matrix builds across three OSes, caching that doesn&rsquo;t invalidate every other commit, gated production deploys, and secrets that don&rsquo;t leak into fork PRs. That&rsquo;s where it got interesting. These are the patterns that survived, each learned by breaking something at least once.</p>
<h2 id="matrix-builds-cover-ground-without-burning-minutes">Matrix builds: cover ground without burning minutes</h2>
<p>The matrix strategy is the single best feature in Actions. One job definition, multiple OS and runtime combinations. But the defaults will burn through your free minutes fast if you&rsquo;re not careful.</p>
<p>Here&rsquo;s what our test workflow looks 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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">ci</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#ae81ff">main]</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull_request</span>:
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">test</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">${{ matrix.os }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">15</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">strategy</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">fail-fast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">matrix</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">os</span>: [<span style="color:#ae81ff">ubuntu-latest, macos-latest, windows-latest]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: [<span style="color:#ae81ff">12</span>, <span style="color:#ae81ff">14</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exclude</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">windows-latest</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">node</span>: <span style="color:#ae81ff">12</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@v1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">node-version</span>: <span style="color:#ae81ff">${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm ci</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm test</span>
</span></span></code></pre></div><p>Few things to note.</p>
<p><strong><code>fail-fast: false</code>.</strong> I used to leave this at the default (<code>true</code>). Then we&rsquo;d get a flaky Windows test that killed the whole matrix, and we&rsquo;d lose the Linux and macOS results entirely. Set it to false. Let everything run. You want the full picture.</p>
<p><strong>Exclude dead combos.</strong> We dropped Node 10 entirely and excluded Node 12 on Windows because we don&rsquo;t ship Windows builds on that version. Every combination you exclude saves real minutes. We were running 9 combinations, trimmed it to 5. The monthly bill noticed.</p>
<p><strong><code>timeout-minutes</code> on everything.</strong> I learned this one the hard way. A hung test on macOS once ran for 6 hours before I noticed. GitHub&rsquo;s default timeout is also 6 hours. That&rsquo;s not a default, that&rsquo;s a trap. Set 15 minutes. If your tests take longer than that, you have a different problem.</p>
<h2 id="caching-the-difference-between-8-minutes-and-90-seconds">Caching: the difference between 8 minutes and 90 seconds</h2>
<p>Caching npm dependencies turned our builds from &ldquo;go get coffee&rdquo; to &ldquo;it&rsquo;s already done.&rdquo; But the cache key design matters more than you&rsquo;d think.</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">name</span>: <span style="color:#ae81ff">Cache node modules</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/cache@v1</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: <span style="color:#ae81ff">~/.npm</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">key</span>: <span style="color:#ae81ff">${{ runner.os }}-node-${{ hashFiles(&#39;**/package-lock.json&#39;) }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">restore-keys</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      ${{ runner.os }}-node-</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">Install dependencies</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm ci</span>
</span></span></code></pre></div><p>The <code>restore-keys</code> fallback is critical. Without it, any change to <code>package-lock.json</code> means a complete cache miss and a full install. With the fallback, you get the last close-enough cache and <code>npm ci</code> only fetches the diff. Went from 4-minute installs to about 20 seconds on cache hit.</p>
<p>One gotcha: <code>actions/cache@v1</code> has a 5GB limit per repo. We hit it after a few weeks because we were caching <code>node_modules</code> directly. Big mistake: cache the npm cache directory instead and let <code>npm ci</code> do the linking. Switched to caching <code>~/.npm</code> and the size dropped by 80%.</p>
<h2 id="job-orchestration-build-once-deploy-from-artifacts">Job orchestration: build once, deploy from artifacts</h2>
<p>This is the pattern I&rsquo;m most opinionated about. Never rebuild in your deploy step. Build once, upload the artifact, download it in deploy. Guarantees you deploy exactly what you tested.</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">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">build</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Cache npm</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/cache@v1</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">path</span>: <span style="color:#ae81ff">~/.npm</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">key</span>: <span style="color:#ae81ff">${{ runner.os }}-node-${{ hashFiles(&#39;**/package-lock.json&#39;) }}</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">restore-keys</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            ${{ runner.os }}-node-</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm ci</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm run build</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm test</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@v2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">dist</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">path</span>: <span style="color:#ae81ff">dist/</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">deploy-staging</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">needs</span>: <span style="color:#ae81ff">build</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">if</span>: <span style="color:#ae81ff">github.ref == &#39;refs/heads/main&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/download-artifact@v2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">dist</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy to staging</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/deploy.sh staging</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">DEPLOY_TOKEN</span>: <span style="color:#ae81ff">${{ secrets.STAGING_DEPLOY_TOKEN }}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">deploy-production</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">needs</span>: <span style="color:#ae81ff">deploy-staging</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">if</span>: <span style="color:#ae81ff">github.ref == &#39;refs/heads/main&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/download-artifact@v2</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">dist</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy to production</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/deploy.sh production</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">DEPLOY_TOKEN</span>: <span style="color:#ae81ff">${{ secrets.PROD_DEPLOY_TOKEN }}</span>
</span></span></code></pre></div><p>Watch artifact storage. Artifacts are kept for 90 days and there&rsquo;s no per-artifact retention setting yet. You don&rsquo;t need three months of build artifacts. You need maybe a week for debugging. We hit 12GB of stored artifacts before I noticed.</p>
<p>The <code>needs</code> chain (<code>build</code> -&gt; <code>deploy-staging</code> -&gt; <code>deploy-production</code>) gives you a natural pipeline. Staging has to pass before production even starts. Actions has no built-in approval gate, which is why our real production deploys go through a manual trigger (below). That keeps us at  <a href="/blog/2016-06-06-continuous-deployment-without-chaos/"
   
   >continuous delivery</a>
 for prod: the artifact is always ready, the last step is a human decision.</p>
<h2 id="trigger-filtering-stop-running-ci-on-readme-changes">Trigger filtering: stop running CI on README changes</h2>
<p>This was annoying before I figured it out. Every docs edit, every README typo fix, every changelog update triggered a full CI run. Waste.</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">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#ae81ff">main]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">paths</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;src/**&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;package.json&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;package-lock.json&#39;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;.github/workflows/**&#39;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull_request</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">paths</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#39;src/**&#39;</span>
</span></span></code></pre></div><p>Only run when code actually changes. I also include <code>.github/workflows/**</code> in the push paths because I want CI to test itself when I modify the workflow files. Learned that one after pushing a broken workflow that I didn&rsquo;t catch for two days because the paths filter excluded the workflows directory.</p>
<p>For production deploys, we use <code>workflow_dispatch</code>, which GitHub shipped this month, so nobody accidentally deploys on merge:</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">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">description</span>: <span style="color:#e6db74">&#39;Deploy target&#39;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>: <span style="color:#e6db74">&#39;staging&#39;</span>
</span></span></code></pre></div><p>Manual button in the Actions tab. Feels old-school. Works perfectly.</p>
<h2 id="secrets-and-fork-prs-the-security-stuff-that-bit-me">Secrets and fork PRs: the security stuff that bit me</h2>
<p>The rule nobody states clearly enough: <strong>fork pull requests run your workflows, but secrets aren&rsquo;t injected</strong>. This is correct behavior and you shouldn&rsquo;t try to work around it.</p>
<p>I see people &ldquo;fixing&rdquo; the fork PR problem anyway, by finding some other way to hand secrets to a job that runs the PR&rsquo;s code. Don&rsquo;t. Any setup where untrusted PR code runs with access to your secrets is a supply chain attack waiting to happen.</p>
<p>Our approach:</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">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">test</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm ci</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm test</span>
</span></span><span style="display:flex;"><span>      <span style="color:#75715e"># No secrets needed for tests</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">deploy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">needs</span>: <span style="color:#ae81ff">test</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">if</span>: <span style="color:#ae81ff">github.event_name == &#39;push&#39; &amp;&amp; github.ref == &#39;refs/heads/main&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Deploy</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/deploy.sh</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">env</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">DEPLOY_TOKEN</span>: <span style="color:#ae81ff">${{ secrets.DEPLOY_TOKEN }}</span>
</span></span></code></pre></div><p>Tests run on PRs without secrets. Deploy only runs on push to main, where secrets are available and the code is trusted. Simple separation.</p>
<p>Also: always mask secrets in logs.</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">name</span>: <span style="color:#ae81ff">Mask token</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">run</span>: <span style="color:#ae81ff">echo &#34;::add-mask::${{ secrets.API_TOKEN }}&#34;</span>
</span></span></code></pre></div><p>GitHub does this automatically for secrets referenced directly, but if you derive a value from a secret (substring, base64 encode, whatever), the derived value isn&rsquo;t masked. Found that out when a deploy URL containing an embedded token showed up in our build logs. Fun morning.</p>
<h2 id="self-hosted-runners-probably-not-yet">Self-hosted runners: probably not yet</h2>
<p>We considered self-hosted runners for builds that need access to our internal Docker registry. Decided against it. The maintenance overhead of patching, isolating, and monitoring runner machines wasn&rsquo;t worth it for our team size. We just push to a public registry and pull in deploy.</p>
<p>If you do need 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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">build</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">self-hosted</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout-minutes</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v2</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">make build</span>
</span></span></code></pre></div><p>Just know that self-hosted runners don&rsquo;t get automatic cleanup between jobs like hosted runners do. Previous job&rsquo;s files, environment variables, running processes: all still there. You need to handle that yourself or you&rsquo;ll get extremely confusing cross-contamination bugs.</p>
<h2 id="four-months-in">Four months in</h2>
<p>None of this is groundbreaking. But I spent a couple months learning each of these the hard way, so maybe this saves you the same debugging sessions. GitHub Actions is good in 2020. The YAML is verbose. The ecosystem is young. But the fundamentals are solid and the GitHub integration is unbeatable.</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>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>Kubernetes Predictions for 2020: Day-Two Operations Win</title><link>https://lawzava.com/blog/2020-01-06-kubernetes-predictions-2020/</link><pubDate>Mon, 06 Jan 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-01-06-kubernetes-predictions-2020/</guid><description>The adoption debate is over. 2020 is about operating Kubernetes well: managed control planes, GitOps by default, and policy enforcement.</description><content:encoded><![CDATA[<p>Half the Kubernetes predictions I&rsquo;ve seen for 2020 are recycled 2019 predictions that never materialized. Service mesh everywhere. Databases on k8s. Serverless killing containers. None of that happened at scale, and most of it won&rsquo;t this year either. These are the changes I expect to matter.</p>
<h2 id="the-adoption-argument-is-finished">The adoption argument is finished</h2>
<p>Nobody is debating whether to use Kubernetes anymore. That conversation ended sometime mid-2019. Every team I&rsquo;ve worked with lately has clusters in production. The question now is whether they&rsquo;re running it well. Spoiler: most aren&rsquo;t.</p>
<p>The real work in 2020 is day-two operations: upgrades, multi-tenant governance, cost control, and not breaking things on Friday afternoon. Boring stuff. Important stuff.</p>
<p>I&rsquo;ve been living this reality since I started building cloud infrastructure tooling after a founder program. The gap between &ldquo;we have Kubernetes&rdquo; and &ldquo;we operate Kubernetes well&rdquo; is enormous. That gap is where most of the pain lives.</p>
<h2 id="managed-control-planes-win-stop-fighting-it">Managed control planes win. Stop fighting it.</h2>
<p>If you&rsquo;re still self-managing your control plane without a very specific reason, I don&rsquo;t understand what you&rsquo;re doing. Cloud providers handle patching, availability, and scaling. The operational cost of doing it yourself is absurd for most teams.</p>
<p>Yes, there are exceptions. Air-gapped environments. Weird regulatory constraints. Hybrid topologies that managed services can&rsquo;t reach. But these are exceptions, not defaults. The default in 2020 should be managed.</p>
<h2 id="gitops-becomes-the-normal-way-to-deploy">GitOps becomes the normal way to deploy</h2>
<p>This is probably my highest-confidence prediction.  <a href="/blog/2019-02-11-gitops-principles-and-practices/"
   
   >GitOps</a>
 moves from &ldquo;cool thing the early adopters do&rdquo; to just how you deploy. Argo CD and Flux are solid. The model is proven.</p>
<p>Pull-based delivery, audit trails in git, rollbacks that don&rsquo;t require someone SSHing into a bastion host at 2 a.m. It works. Multi-cluster GitOps is still rough, but single-cluster is basically solved.</p>
<p>If you&rsquo;re starting fresh in 2020 and not using GitOps, you&rsquo;re choosing to have a worse time later.</p>
<h2 id="security-shifts-from-checkboxes-to-enforcement">Security shifts from checkboxes to enforcement</h2>
<p>RBAC and network policies should already be in place. If they&rsquo;re not, that&rsquo;s a different conversation (and you should be worried). The 2020 shift is toward policy as code: admission control that actually prevents bad configurations from reaching the cluster.</p>
<p>OPA Gatekeeper is the one to watch 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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">constraints.gatekeeper.sh/v1beta1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">K8sRequiredLabels</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">require-team-label</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">match</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kinds</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">apiGroups</span>: [<span style="color:#e6db74">&#34;&#34;</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Namespace&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">parameters</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>: [<span style="color:#e6db74">&#34;team&#34;</span>, <span style="color:#e6db74">&#34;cost-center&#34;</span>]
</span></span></code></pre></div><p>Simple example, but the idea matters. Don&rsquo;t just document policies. Enforce them automatically so humans can&rsquo;t skip them when they&rsquo;re in a rush.</p>
<h2 id="service-mesh-is-overrated-for-now">Service mesh is overrated (for now)</h2>
<p>Hot take that shouldn&rsquo;t be a hot take: most teams don&rsquo;t need a service mesh in 2020. The mTLS and traffic shaping benefits are real, but the operational overhead is significant. I&rsquo;ve watched teams spend weeks  <a href="/blog/2018-11-26-service-mesh-istio-practical-guide/"
   
   >debugging Istio sidecars</a>
 instead of shipping features.</p>
<p>For critical services with strict security requirements, use a mesh. For your internal CRUD APIs and batch jobs, you&rsquo;re adding complexity for conference talk credibility.</p>
<p>eBPF-based networking like Cilium is interesting and may eventually take over some of what sidecars do, but it&rsquo;s early. Check back in 2021.</p>
<h2 id="running-databases-on-k8s-still-mostly-dont">Running databases on k8s: still mostly don&rsquo;t</h2>
<p>The operator ecosystem is improving. Zalando&rsquo;s Postgres operator, Strimzi for Kafka: these are credible projects. CSI is maturing. But &ldquo;technically possible&rdquo; and &ldquo;good idea for your team&rdquo; are very different things.</p>
<p>From my time at the fintech startup handling strict data requirements, I learned that stateful workloads need operational confidence you only get through battle-testing. Most teams haven&rsquo;t battle-tested their k8s storage layer enough to bet production data on it. Give it another year or two before it goes mainstream.</p>
<h2 id="internal-platforms-are-the-underrated-trend">Internal platforms are the underrated trend</h2>
<p>This is the one nobody&rsquo;s hyping enough. Kubernetes is powerful and terrible as a developer interface. The teams that win in 2020 will build  <a href="/blog/2019-03-11-building-internal-developer-platforms/"
   
   >internal platforms</a>
 that hide the complexity. Opinionated templates. Self-service environments. Clear boundaries between app teams and platform teams.</p>
<p>The point is to let developers ship faster without accidentally creating cost or security problems.</p>
<h2 id="what-wont-happen">What won&rsquo;t happen</h2>
<p>Service mesh everywhere won&rsquo;t happen. Neither will databases migrating wholesale to k8s, or the mythical &ldquo;NoOps&rdquo; future some people keep promising.</p>
<p>Kubernetes in 2020 is about operating it well. Teams that treat operations, security, and developer experience as actual product work, not afterthoughts, will be fine. Everyone else will keep filing the same incident reports they filed last year.</p>
]]></content:encoded></item><item><title>Zero-Downtime Deployments: Migrations, Probes, and Habits</title><link>https://lawzava.com/blog/2019-10-21-zero-downtime-deployments/</link><pubDate>Mon, 21 Oct 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-10-21-zero-downtime-deployments/</guid><description>Zero-downtime deploys depend less on tooling than on expand-and-contract migrations, backward-compatible code, readiness probes, and graceful shutdown.</description><content:encoded><![CDATA[<p>Two production outages I caused still stick with me. One was at the fintech startup, when a database migration locked a table for eleven minutes during market open. The other was during my founder-program cohort, when I pushed a config change that broke backward compatibility with in-flight requests. Both times, the deploy tooling worked perfectly. The problem was me.</p>
<p>No deploy tool gives you zero downtime on its own. It&rsquo;s a discipline across code, database migrations, and infrastructure, and the hardest part is the schema changes, not the YAML.</p>
<h2 id="what-zero-downtime-means">What Zero Downtime Means</h2>
<p>Two things. That&rsquo;s it.</p>
<ol>
<li>Capacity never drops below what traffic needs.</li>
<li>Running code stays compatible with in-flight requests and existing data.</li>
</ol>
<p>If both hold, your deploy is invisible to users. If either breaks, you have an outage regardless of how fancy your canary setup is.</p>
<h2 id="database-migrations-are-the-hard-part">Database Migrations Are the Hard Part</h2>
<p>Rolling updates, blue-green, canary: pick your favorite. They all handle the application layer fine. The hard part is always the database.</p>
<p>A schema change that locks a table or drops a column still in use will take you down no matter what deployment pattern you&rsquo;re running. I learned this the painful way with Postgres at the fintech startup, where our users table had enough rows that a naive <code>ALTER TABLE</code> would hold a lock for minutes.</p>
<p>The fix is boring, and I covered it in more depth in  <a href="/blog/2016-08-15-database-migrations-without-downtime/"
   
   >database migrations without downtime</a>
. Expand, migrate, contract. In that order, across separate deploys.</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">-- Deploy 1: Expand (add the new 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> users <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> phone VARCHAR(<span style="color:#ae81ff">20</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Deploy 2: Migrate (backfill in batches by id range, not one giant UPDATE)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> users <span style="color:#66d9ef">SET</span> phone <span style="color:#f92672">=</span> legacy_phone
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span> <span style="color:#66d9ef">AND</span> id <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">1000</span> <span style="color:#66d9ef">AND</span> phone <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 the next id range until done
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Deploy 3: Contract (drop old column after all code uses the new one)
</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> users <span style="color:#66d9ef">DROP</span> <span style="color:#66d9ef">COLUMN</span> legacy_phone;
</span></span></code></pre></div><p>Three deploys for one column rename. Annoying? Yes. But nobody notices. That&rsquo;s the point.</p>
<p>On MySQL, tools like <code>gh-ost</code> or <code>pt-online-schema-change</code> do background table copies to avoid long locks. On Postgres, the work is mostly careful DDL: nullable columns without defaults, <code>CREATE INDEX CONCURRENTLY</code>, a short <code>lock_timeout</code>, and backfills in batches (Postgres <code>UPDATE</code> has no <code>LIMIT</code>, so batch by primary key range). Worth learning if you&rsquo;re running anything with real traffic.</p>
<h2 id="pick-a-rollout-pattern-and-keep-it-simple">Pick a Rollout Pattern and Keep It Simple</h2>
<h3 id="rolling-updates">Rolling Updates</h3>
<p>The default and usually the right choice. Set <code>maxUnavailable: 0</code> so you never lose capacity during the rollout.</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">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">strategy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">type</span>: <span style="color:#ae81ff">RollingUpdate</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">rollingUpdate</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">maxUnavailable</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">maxSurge</span>: <span style="color:#ae81ff">25</span><span style="color:#ae81ff">%</span>
</span></span></code></pre></div><p>This works when old and new versions can run side by side. Which means your APIs need to be backward compatible. If that sounds like extra work, it is. But it&rsquo;s the kind of extra work that prevents 3 a.m. pages.</p>
<h3 id="canary">Canary</h3>
<p>Send a small slice of traffic to the new version. Watch your error rate. If it&rsquo;s clean, ramp up. If it spikes, roll back automatically.</p>
<p>The key word there is &ldquo;automatically.&rdquo; A canary release without automated rollback is just a smaller blast radius with manual intervention. You might as well flip a coin.</p>
<h3 id="blue-green">Blue-Green</h3>
<p>Run two environments, switch traffic when the new one is verified. Fast rollback, but you&rsquo;re paying for double infrastructure. Fine for critical services. Overkill for most things.</p>
<h2 id="the-stuff-between-the-deploys">The Stuff Between the Deploys</h2>
<p>The patterns above are table stakes. What actually makes deploys boring (in a good way) is the plumbing around them.</p>
<p><strong>Readiness vs. liveness probes.</strong> These aren&rsquo;t the same thing. Liveness says &ldquo;is this process alive.&rdquo; Readiness says &ldquo;can this process handle traffic right now.&rdquo; If your readiness check passes before caches are warm and dependencies are verified, you&rsquo;re sending users to a half-alive instance.</p>
<p><strong>Graceful shutdown.</strong> When Kubernetes sends SIGTERM (see my  <a href="/blog/2019-01-14-kubernetes-best-practices-2019/"
   
   >Kubernetes production checklist</a>
), your app needs to stop accepting new connections, finish in-flight requests, then exit. Sounds obvious. Roughly half the Go services I&rsquo;ve reviewed get this wrong, usually by not waiting long enough for the load balancer to drain.</p>
<p><strong>Connection draining.</strong> Set a realistic termination grace period. If your longest request takes 30 seconds, a 5-second grace period is going to drop connections.</p>
<p><strong>DNS TTLs.</strong> Lower them before a cutover, raise them after. I&rsquo;ve seen teams spend hours debugging a &ldquo;failed deploy&rdquo; that was actually stale DNS.</p>
<h2 id="my-pre-deploy-checklist">My Pre-Deploy Checklist</h2>
<p>The questions I ask before every deploy:</p>
<ul>
<li>Can the new code read data written by the old code? Can the old code read data written by the new code?</li>
<li>Does the database migration need a lock? How long?</li>
<li>Do the readiness checks actually verify dependencies, or do they just return 200?</li>
<li>If this deploy goes wrong, can I roll back in under a minute?</li>
</ul>
<p>If any answer is &ldquo;I&rsquo;m not sure,&rdquo; that&rsquo;s where the work is. Not in the deploy tool.</p>
<h2 id="why-this-stays-hard">Why this stays hard</h2>
<p>Zero downtime deploys aren&rsquo;t hard technically. They&rsquo;re hard culturally. They require everyone on the team to think about backward compatibility, schema evolution, and graceful degradation before they write the code. Not after.</p>
<p>The teams I&rsquo;ve seen do this well, at the fintech startup, at startups in my founder-program cohort, and on other teams I&rsquo;ve worked with, all share one trait. Deploys are boring. Nobody watches them. Nobody holds their breath. The monitoring catches problems and the rollback is automatic.</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>Developer Experience: Internal Platforms vs Ad-Hoc Tooling</title><link>https://lawzava.com/blog/2019-08-12-developer-experience-platform/</link><pubDate>Mon, 12 Aug 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-08-12-developer-experience-platform/</guid><description>Purpose-built internal platforms versus the scripts and Makefiles teams grow themselves, and the team size and pain at which each one wins.</description><content:encoded><![CDATA[<p>I&rsquo;ve  <a href="/blog/2019-03-11-building-internal-developer-platforms/"
   
   >built internal platforms</a>
. I&rsquo;ve also inherited codebases where a pile of shell scripts and a well-maintained Makefile outperformed every &ldquo;developer portal&rdquo; I&rsquo;d seen at the time. The difference between those outcomes was never the technology. It was whether the team actually needed a platform or just needed their existing tools to stop breaking.</p>
<p>At the fintech startup, the number of services and repos grew fast. We hit a point where onboarding a new engineer meant a full day of tribal knowledge transfer: which repo to clone first, which env vars to set, which Docker Compose file was current. The ad-hoc scripts that got us through the first year became the bottleneck. That&rsquo;s when a platform starts to make sense. Not before. If your developers are already shipping fast with scripts and Makefiles, a shiny platform might just slow them down.</p>
<h2 id="the-two-approaches">The Two Approaches</h2>
<p><strong>Ad-hoc tooling</strong> is what every team starts with. Makefiles, shell scripts, a README that someone updates when they remember. It&rsquo;s fast to create, easy to understand, and perfectly tuned to the team that wrote it.</p>
<p><strong>An internal platform</strong> is what you build when the ad-hoc approach stops scaling. A curated set of tools, templates, and workflows that standardize the path from code to production.</p>
<p>Right now, a lot of the conference conversation favors platforms. I think that framing skips the part where most teams aren&rsquo;t ready for one.</p>
<h2 id="where-ad-hoc-tooling-wins">Where Ad-Hoc Tooling Wins</h2>
<p>Small teams. Early-stage products. Anything where the domain is still shifting.</p>
<p>When I was at a mobility startup building our backend, we had four engineers. A <code>Makefile</code> with targets for <code>build</code>, <code>test</code>, <code>deploy-staging</code>, and <code>deploy-prod</code> covered about 90% of our workflow. Total maintenance cost: maybe an hour a month. Everyone understood every line because everyone had written at least one target.</p>
<p>Ad-hoc tooling wins when:</p>
<ul>
<li>The team is under ten engineers and everyone knows the stack.</li>
<li>Requirements change weekly, so a rigid platform would just get in the way.</li>
<li>The &ldquo;platform&rdquo; would have exactly one consumer team. That&rsquo;s a library, not a platform.</li>
<li>You can still onboard a new engineer in under two hours with a README and a pair programming session.</li>
</ul>
<p>The advantage is speed and ownership. Nobody files a ticket to change a Makefile. Nobody waits for &ldquo;the platform team&rdquo; to prioritize a feature.</p>
<h2 id="where-ad-hoc-tooling-breaks-down">Where Ad-Hoc Tooling Breaks Down</h2>
<p>It breaks down at the seams. Not within one team, but across teams.</p>
<p>The symptoms are consistent. I&rsquo;ve seen them at every company past about fifteen engineers:</p>
<ul>
<li>New hires take days to get a working local environment because every team&rsquo;s setup script assumes different things.</li>
<li>&ldquo;Works on my machine&rdquo; becomes a weekly standup topic.</li>
<li>Three teams have three different CI configs that do roughly the same thing with slightly different bugs.</li>
<li>Nobody knows how to deploy the service they inherited from the team that left.</li>
</ul>
<p>The failure mode isn&rsquo;t that any single script is bad. It&rsquo;s that the collection of scripts has no coherence. Each one optimizes for its author&rsquo;s context, and that context drifts.</p>
<h2 id="where-a-platform-earns-its-keep">Where a Platform Earns Its Keep</h2>
<p>A platform earns its keep when it removes decisions that slow people down without removing decisions that matter.</p>
<p>Good platform DX looks like this:</p>
<ul>
<li><code>platform init</code> scaffolds a new service with CI, monitoring, and deployment already wired up. The developer writes business logic on day one.</li>
<li><code>git push</code> triggers a pipeline that builds, tests, and deploys to staging. No YAML archaeology required.</li>
<li>Logs, metrics, and traces are correlated by default. When something breaks at 2am, the on-call engineer doesn&rsquo;t need to be an expert in the observability stack.</li>
</ul>
<p>Bad platform DX looks like this:</p>
<ul>
<li>A portal with a service catalog that nobody updates.</li>
<li>Mandatory migration to &ldquo;the platform way&rdquo; with no clear improvement in daily workflow.</li>
<li>A platform team that ships features on their own roadmap instead of fixing the paper cuts developers actually report.</li>
</ul>
<p>I&rsquo;ve been in a founder-program cohort this year, building a cloud infrastructure startup, and one thing the startup environment hammers home is this: your internal customers are just as fickle as external ones. If the platform isn&rsquo;t obviously better than what developers already have, they will route around it. Every time.</p>
<h2 id="the-comparison-that-matters">The Comparison That Matters</h2>
<p>Here is how I think about the tradeoff in practice:</p>
<p><strong>Onboarding time.</strong> Ad-hoc: depends entirely on documentation quality, which is usually poor. Platform: can be excellent if <code>platform init</code> and the paved-road defaults work. Can be terrible if the platform itself needs onboarding.</p>
<p><strong>Speed of change.</strong> Ad-hoc: instant for the team that owns the script, impossible for everyone else. Platform: slower to change but the change applies everywhere.</p>
<p><strong>Reliability.</strong> Ad-hoc: varies wildly per team. Platform: consistent baseline, but a platform bug affects everyone simultaneously.</p>
<p><strong>Cost.</strong> Ad-hoc: hidden in every team&rsquo;s sprint. Platform: visible headcount on a dedicated team. Leadership often prefers the visible cost, which is ironic because it&rsquo;s usually higher.</p>
<p><strong>Autonomy.</strong> This is the one people underweight. Developers care about autonomy. A platform that takes away the ability to customize a build step or add a deployment hook will face resistance no matter how polished it is. The best platforms I&rsquo;ve seen treat defaults as suggestions and overrides as first-class features.</p>
<h2 id="my-rule-of-thumb">My Rule of Thumb</h2>
<p>If you have fewer than three teams consuming shared infrastructure, invest in better scripts and a good README. A <code>Makefile</code> with ten well-named targets and a one-page setup guide will outperform a half-built platform every time.</p>
<p>If you have more than three teams and you&rsquo;re seeing the same problems solved differently in each one, start building a platform. But start with the one workflow that causes the most pain, not with a portal and a service catalog.</p>
<p>One platform rollout I watched work started with deployment. Just deployment. One command to deploy any service to staging, with automatic rollback if health checks failed. That single workflow converted the skeptics faster than any roadmap slide could have. Then we added CI standardization. Then observability. Each step earned trust before the next one started.</p>
<h2 id="where-platform-teams-fail">Where platform teams fail</h2>
<p>Most internal platforms I&rsquo;ve seen fail because the team builds what it finds interesting instead of what developers need. I&rsquo;ve been guilty of this. At one point I spent two weeks building a slick CLI for environment management when the actual pain point was that our staging database kept running out of disk space.</p>
<p>Talk to your developers before you build anything. Sit with them. Watch them work. The friction you need to fix is almost never the friction you assumed.</p>
<p>Good DX, whether it comes from a platform or a pile of scripts, is measured by how fast a developer goes from idea to running code in production, without fear.</p>
]]></content:encoded></item><item><title>Security Incident Response: Drills Beat Written Plans</title><link>https://lawzava.com/blog/2019-07-15-security-incident-response/</link><pubDate>Mon, 15 Jul 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-07-15-security-incident-response/</guid><description>Most incident response plans are shelf-ware. What matters in a breach: simple severity levels, the first 30 minutes, four roles, evidence, and drills.</description><content:encoded><![CDATA[<p>I&rsquo;ve sat in rooms where people were genuinely unsure whether they were being breached or just seeing noisy logs. That moment of paralysis, when nobody knows who decides, who calls whom, who touches what, is where the real damage happens. Not the exploit itself. The confusion.</p>
<p>Nobody reads the 40-page incident response PDF until the breach is already happening. The plan that survives first contact is the one your team has rehearsed, and it has to work at 2am with half your team asleep. I wrote about  <a href="/blog/2016-05-23-security-incident-response-for-startups/"
   
   >incident response for startups</a>
 a few years ago; this is what I&rsquo;d add now.</p>
<h2 id="the-problem-with-most-ir-plans">The problem with most IR plans</h2>
<p>Every company I&rsquo;ve worked with has some version of an incident response document. At the fintech startup, we had one. At a mobility startup, we had one. They were fine documents. Well-structured. Thorough. And completely useless the first time something real happened.</p>
<p>The issue isn&rsquo;t the content. The issue is that nobody has practiced it. An IR plan is like a fire drill: the value is in everyone having walked the route, more than in the map of the exits.</p>
<p>During a cyber-defense exercise I participated in, the teams that performed best weren&rsquo;t the ones with the most sophisticated tooling. They were the ones where every person knew their role cold. The analyst knew to preserve logs before touching anything. The incident commander knew to start a timeline before asking questions. The comms lead had templates ready. Muscle memory.</p>
<h2 id="severity-stop-overthinking-it">Severity: stop overthinking it</h2>
<p>I&rsquo;ve seen teams spend 20 minutes debating whether something is a SEV-2 or SEV-3 while the attacker is still active. Here is a simple rule that works for startups and mid-size teams:</p>
<p><strong>SEV-1</strong>: You know or strongly suspect data has been accessed by someone who shouldn&rsquo;t have it. Customer data, credentials, financial records. Page everyone.</p>
<p><strong>SEV-2</strong>: Something is clearly wrong (unauthorized access, exploitation of a known vuln, weird lateral movement), but you don&rsquo;t know the scope yet. Page the security lead and an incident commander.</p>
<p><strong>SEV-3</strong>: Suspicious activity that needs investigation but shows no evidence of compromise. Next business hours is fine.</p>
<p>That&rsquo;s it. Three levels. The distinction between 1 and 2 is &ldquo;do we know what they got?&rdquo; If yes, SEV-1. If no, SEV-2. Stop debating and start working.</p>
<h2 id="the-first-30-minutes-decide-everything">The first 30 minutes decide everything</h2>
<p>I have a strong opinion on this: the first 30 minutes of an incident determine whether you spend three days cleaning up or three weeks. Here is what has to happen in that window:</p>
<p><strong>Minute 0-5: Acknowledge and assign.</strong> Someone claims the incident commander role. Not &ldquo;can someone look at this?&rdquo; but an explicit &ldquo;I&rsquo;m IC for this.&rdquo; Open a dedicated channel. Name it something obvious like <code>incident-2019-07-15</code>.</p>
<p><strong>Minute 5-15: Triage and preserve.</strong> Before you touch anything, snapshot what you can. Disk images, memory dumps, log exports. On one team I worked with, we learned this the hard way when someone rebooted a compromised container to &ldquo;fix it&rdquo; and wiped the volatile evidence we needed. Never again.</p>
<p>The key questions at this stage:</p>
<ul>
<li>Is the attacker still active right now?</li>
<li>What systems are potentially in scope?</li>
<li>What is the worst-case data exposure?</li>
</ul>
<p><strong>Minute 15-30: Contain.</strong> Isolate affected hosts. Revoke compromised credentials. Block known-bad IPs. Kill active sessions from suspicious accounts. The goal is to stop the bleeding; understanding everything comes later.</p>
<p>A useful mental model: containment is a tourniquet. You apply it fast and ugly. Eradication is surgery. That comes later.</p>
<h2 id="four-roles-that-matter">Four roles that matter</h2>
<p>Skip the org chart with 12 named roles. For a team under 50 engineers, you need four:</p>
<p><strong>Incident Commander (IC)</strong>: Owns the incident. Makes decisions when there&rsquo;s disagreement. Controls the communication cadence. The IC coordinates and doesn&rsquo;t debug. The single hardest skill here is resisting the urge to jump into a terminal. I struggle with this every time.</p>
<p><strong>Technical Lead</strong>: Drives the investigation. Validates containment. Recommends eradication steps. This person lives in the logs and the systems.</p>
<p><strong>Scribe</strong>: Documents everything with timestamps. Decisions, actions, findings, who said what. This role feels bureaucratic until you need to write the post-mortem or talk to a regulator. Then it&rsquo;s the most valuable person in the room.</p>
<p><strong>Comms Lead</strong>: Handles internal updates and external messaging. Gets legal involved early if disclosure might be required. At a startup, this is often the CEO or co-founder. Fine. Just make sure they aren&rsquo;t also trying to be the IC.</p>
<h2 id="investigation-follow-the-trail-not-your-assumptions">Investigation: follow the trail, not your assumptions</h2>
<p>The biggest mistake in incident investigation is anchoring on the first hypothesis. &ldquo;It was probably a leaked API key&rdquo; becomes the only thing anyone investigates, while the actual entry point, a vulnerable dependency three services deep, goes unexamined.</p>
<p>Ask these questions systematically:</p>
<ul>
<li>How did initial access occur?</li>
<li>What was the dwell time? (How long were they in before we noticed?)</li>
<li>Was there lateral movement?</li>
<li>What data was accessed, modified, or exfiltrated?</li>
</ul>
<p>Where to look:</p>
<ul>
<li>Application logs (request patterns, auth failures, unusual API calls)</li>
<li>Cloud audit logs (IAM changes, new resources, API activity)</li>
<li>Network flow data (unexpected outbound connections, data transfer volumes)</li>
<li>Auth logs (session creation, privilege escalation, MFA bypasses)</li>
</ul>
<p>Build your timeline as you go. Not after. Reconstructing a timeline from memory 48 hours later is fiction writing, not forensics.</p>
<h2 id="evidence-the-chain-of-custody-problem">Evidence: the chain-of-custody problem</h2>
<p>This is the part most startup engineers have never thought about. If your incident turns into a legal matter, and breaches involving customer data often do, you need evidence that holds up.</p>
<p>Practical rules:</p>
<ul>
<li>Snapshot before you fix. Always.</li>
<li>Hash everything you collect. SHA-256 is fine.</li>
<li>Record who collected what, when, and where it&rsquo;s stored.</li>
<li>Don&rsquo;t modify originals. Work on copies.</li>
</ul>
<p>A minimal evidence log looks 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">evidence</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">item</span>: <span style="color:#ae81ff">api-server-memory.raw</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">collected_by</span>: <span style="color:#ae81ff">law</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">collected_at</span>: <span style="color:#e6db74">2019-07-15T10:45:00Z</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">hash</span>: <span style="color:#ae81ff">sha256:3e0bfd4a4c8662dbe8f663b96b7e5e1bc7bd8281ec39abcffb342d056c88194e</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">storage</span>: <span style="color:#ae81ff">s3://incident-evidence/2019-07-15/</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">notes</span>: <span style="color:#ae81ff">captured before container restart</span>
</span></span></code></pre></div><p>Is this overkill for most startups? Maybe. But the one time you need it and don&rsquo;t have it, you will wish you had spent the extra five minutes.</p>
<h2 id="communication-silence-isnt-a-strategy">Communication: silence isn&rsquo;t a strategy</h2>
<p>Internal communication during an incident should follow a predictable cadence. SEV-1 gets updates every 30 minutes. SEV-2 every hour. Use a simple template:</p>
<pre tabindex="0"><code>Incident: checkout-auth-bypass
Severity: SEV-1
Status: containment
IC: law
Last update: 2019-07-15 11:00 UTC
Summary: Unauthorized API access via expired OAuth tokens.
         Revoked all active sessions. Investigating scope.
Next update: 11:30 UTC
</code></pre><p>External communication is harder and scarier. My advice: get legal involved before you send anything to customers. Not because lawyers make things better, but because disclosure obligations vary by jurisdiction and getting it wrong creates more problems than the breach itself. GDPR gives you 72 hours to notify the supervisory authority. US obligations vary by state and sector; New York&rsquo;s DFS cybersecurity rule, for example, also sets a 72-hour clock for covered financial companies. Know your obligations before the incident, not during.</p>
<h2 id="the-post-mortem-where-the-real-value-lives">The post-mortem: where the real value lives</h2>
<p>Run a blameless post-mortem within 48 hours. Not a week later. Not &ldquo;when things calm down.&rdquo; Memory degrades fast.</p>
<p>Structure it simply:</p>
<ol>
<li><strong>Timeline</strong>: What happened, in order, with timestamps.</li>
<li><strong>Root cause</strong>: Not &ldquo;human error.&rdquo; The systemic reason. Why was it possible for a human to make that error?</li>
<li><strong>What worked</strong>: Reinforce the things that went right.</li>
<li><strong>What broke</strong>: Be honest about gaps.</li>
<li><strong>Action items</strong>: With owners and deadlines. No orphaned tasks.</li>
</ol>
<p>The post-mortem is how you convert an expensive incident into durable organizational learning. If your team is afraid to write honest post-mortems, that&rsquo;s a bigger problem than any security incident.</p>
<h2 id="preparation-the-boring-stuff-that-saves-you">Preparation: the boring stuff that saves you</h2>
<p>None of this works if you haven&rsquo;t done the groundwork:</p>
<ul>
<li><strong>Run tabletop exercises quarterly.</strong> Describe a scenario. Walk through the response. Find the gaps before they find you. Keep each one to 30 minutes. Low overhead. High value.</li>
<li><strong>Test your backups.</strong> Not &ldquo;verify they exist.&rdquo; Actually restore from them. I&rsquo;ve seen teams discover their backup pipeline had been silently failing for weeks.</li>
<li><strong>Ensure logging is actually on.</strong> Audit trails, cloud API logs, application logs. If it isn&rsquo;t being collected, it doesn&rsquo;t exist during an incident.</li>
<li><strong>Keep a contact list current.</strong> Who do you call at your cloud provider? Your legal counsel? Your insurance carrier? Your biggest customer&rsquo;s security team? Write it down.</li>
</ul>
<h2 id="practice-before-the-first-breach">Practice before the first breach</h2>
<p>Most startups won&rsquo;t invest seriously in incident response until after their first real breach. That&rsquo;s human nature. But the gap between &ldquo;we have a plan&rdquo; and &ldquo;we&rsquo;ve practiced the plan&rdquo; is the gap between a manageable incident and a company-threatening one. The plan doesn&rsquo;t need to be perfect. It needs to be practiced.</p>
]]></content:encoded></item><item><title>Internal Developer Platforms: Treat Them as Products</title><link>https://lawzava.com/blog/2019-03-11-building-internal-developer-platforms/</link><pubDate>Mon, 11 Mar 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-03-11-building-internal-developer-platforms/</guid><description>Most internal developer platforms fail because nobody treated them as a product. Lessons from building, and scrapping, platform tooling at three startups.</description><content:encoded><![CDATA[<p>I&rsquo;ve built internal tooling at three companies now: a mobility startup, a fintech startup, and most recently the early groundwork at my infrastructure startup. At least once, I got it badly wrong. The lesson: stop building internal platforms nobody asked for, and talk to your engineers first. If they&rsquo;d rather copy-paste bash scripts than use your platform, that tells you everything.</p>
<p>At the fintech startup, we built a deployment system that was technically sound and architecturally clean. Nobody used it. Engineers kept SSHing into boxes and running scripts manually because our &ldquo;platform&rdquo; added steps instead of removing them. That was humbling.</p>
<p>The mistake was obvious in hindsight. We built what we thought engineers <em>should</em> want instead of solving what actually slowed them down.</p>
<h2 id="platforms-are-products">Platforms Are Products</h2>
<p>This is the part most engineering leaders get backwards. You wouldn&rsquo;t ship a product feature without talking to users. But somehow, platform teams spin up for months in isolation, emerge with a Kubernetes abstraction layer, and then act surprised when adoption is flat.</p>
<p>Your developers are your users. They have deadlines, context-switching overhead, and zero patience for tools that add friction. If your platform doesn&rsquo;t make their Tuesday afternoon measurably better, it&rsquo;s shelf-ware.</p>
<p>At the mobility startup, we got this right. We started by sitting with engineers and listing every manual step in their deploy process. Not what we imagined the pain points were. What they actually were. Turned out the biggest time sink wasn&rsquo;t CI/CD at all. It was environment provisioning. Engineers were burning hours configuring staging environments that drifted from production within a week.</p>
<p>So we fixed that one thing first. Standardized environments, one command to spin up, automatic teardown. Adoption was immediate because it solved a real problem engineers hated.</p>
<h2 id="paved-roads-not-walls">Paved Roads, Not Walls</h2>
<p>The best platforms are opinionated but not authoritarian. You set strong defaults, make the common case trivial, and leave an escape hatch for the 10% of services that genuinely need something different.</p>
<p>The trap is mandating usage. The moment you force teams onto your platform without it being obviously better, you&rsquo;ve created resentment and shadow infrastructure. I&rsquo;ve seen this happen. Engineers are resourceful. They will find workarounds, and those workarounds will be worse than whatever you were trying to replace.</p>
<h2 id="the-only-metric-that-matters-early-on">The Only Metric That Matters Early On</h2>
<p>Forget deployment frequency dashboards and adoption funnels. Early on, there&rsquo;s one question: <strong>would engineers choose your platform if it wasn&rsquo;t required?</strong></p>
<p>If the answer is no, you have a product problem, not an adoption problem. No amount of migration mandates or executive sponsorship fixes bad developer experience.</p>
<h2 id="ship-small-listen-hard">Ship Small, Listen Hard</h2>
<p>Start with the most painful thing. Fix it. Ship it. Watch what happens. Then find the next most painful thing. That&rsquo;s it. That&rsquo;s the strategy.</p>
<p>I&rsquo;m three weeks into the founder-program cohort now, starting my infrastructure startup from scratch. Even at this stage, with just me writing the code, I&rsquo;m thinking about what &ldquo;the default path to production&rdquo; looks like. Not because I need a platform. Because the habits you set in week one calcify fast, and undoing bad tooling decisions at 20 engineers is a nightmare I&rsquo;ve already lived through while  <a href="/blog/2018-11-12-scaling-engineering-teams/"
   
   >scaling an engineering team</a>
.</p>
<p>Build for the workflow your team already has. Then make the default path undeniably better.</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>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>Container Security in 2018: PSP, Distroless, Image Signing</title><link>https://lawzava.com/blog/2018-08-20-container-security-beyond-basics/</link><pubDate>Mon, 20 Aug 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-08-20-container-security-beyond-basics/</guid><description>Eight months after my first container security post: PodSecurityPolicy, distroless images, image signing, and Vault at a fintech startup.</description><content:encoded><![CDATA[<p>Last December I wrote about  <a href="/blog/2017-12-04-container-security-beyond-the-basics/"
   
   >container security fundamentals</a>
: slim images, dropped capabilities, network policies. That post got more traction than I expected. I got a fair amount of &ldquo;we do none of this&rdquo; in my inbox. So this is where things stand eight months later, both in the ecosystem and at the fintech startup.</p>
<h3 id="podsecuritypolicy-is-real-now">PodSecurityPolicy is real now</h3>
<p>When I wrote the first post, PodSecurityPolicy existed, but almost nobody used it. That changed fast. We rolled it out at the fintech startup in Q1, and it caught issues immediately. A dev tried to deploy a container with <code>hostNetwork: true</code> for a quick port-forwarding hack. PSP rejected it. Before PSP, that would have gone through and sat there until someone noticed, or didn&rsquo;t.</p>
<p>The enforcement model is rough. You define policies, bind them via RBAC, and hope the interaction between multiple policies does what you think it does. We spent a solid week debugging why certain pods wouldn&rsquo;t schedule. But the alternative, trusting every deploy not to escalate privileges, is worse.</p>
<h3 id="we-moved-past-alpine">We moved past Alpine</h3>
<p>In the 2017 post I showed an Alpine-based final image. We&rsquo;ve since moved most Go services to <code>scratch</code> or <code>distroless</code>. No shell. No package manager. No <code>/etc/passwd</code>. An attacker who gets code execution inside one of these containers gets almost nothing. They can&rsquo;t spawn a shell because there isn&rsquo;t one.</p>
<p>The tradeoff is debugging. You can&rsquo;t <code>kubectl exec</code> into a scratch container and poke around. We lean on better structured logging instead. Worth it.</p>
<h3 id="image-signing-went-from-nice-to-have-to-required">Image signing went from nice-to-have to required</h3>
<p>Docker Content Trust plus Notary. We gate all production pulls on signature verification now. This came out of a near-miss: a CI pipeline pulled from an internal registry without verifying the image hadn&rsquo;t been tampered with. Nobody tampered with it. But nobody would have known if they had.</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>export DOCKER_CONTENT_TRUST<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>docker trust sign registry.internal/api:2.4.1
</span></span></code></pre></div><p>Simple. We should have done it a year ago.</p>
<h3 id="network-policies-still-matter-more-than-service-meshes">Network policies still matter more than service meshes</h3>
<p>Istio is getting a lot of attention, and it hit 1.0 at the end of July. We&rsquo;re still not running it. NetworkPolicy with Calico does what we need: default-deny, explicit allows, and no extra control plane to babysit. When Istio matures, maybe. For now, we keep it simple.</p>
<h3 id="secrets-from-kubernetes-secrets-to-vault">Secrets: from Kubernetes Secrets to Vault</h3>
<p>We switched from Kubernetes Secrets to Vault-backed dynamic credentials for database access. Short-lived tokens that rotate automatically. The ops overhead was real (Vault isn&rsquo;t a simple system), but the security posture improvement was immediate. No more long-lived database passwords sitting in etcd.</p>
<h3 id="where-we-are">Where we are</h3>
<p>Eight months of tightening. Every service runs non-root, read-only filesystem, all capabilities dropped. PSP enforces it cluster-wide. Images are signed. Network policies are default-deny. Secrets come from Vault.</p>
<p>Most of it&rsquo;s YAML and policy files. But the gap between &ldquo;we know what to do&rdquo; and &ldquo;we do it everywhere&rdquo; is where breaches live. We&rsquo;re closing that gap, one deploy at a time.</p>
]]></content:encoded></item><item><title>Monitoring vs Observability: Lessons From a Silent Outage</title><link>https://lawzava.com/blog/2018-07-09-observability-beyond-monitoring/</link><pubDate>Mon, 09 Jul 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-07-09-observability-beyond-monitoring/</guid><description>After an outage our dashboards couldn&amp;amp;rsquo;t explain, I rebuilt a fintech startup&amp;amp;rsquo;s telemetry around metrics, logs, and traces tied together by request IDs.</description><content:encoded><![CDATA[<p>It was 2 AM on a Wednesday, and our news ingestion pipeline at the fintech startup had gone silent. No errors in the logs. No alerts firing. CPU fine, memory fine, disk fine. Every dashboard said the system was healthy. But users were seeing stale financial data, some of it hours old.</p>
<p>I spent forty minutes SSH-ing into boxes, tailing logs, and grepping for exceptions. Nothing. The pipeline just&hellip; stopped processing. It took me another hour to discover the root cause: a third-party API we depended on had started returning empty 200 responses instead of actual data. Our monitoring checked for errors. It checked for timeouts. It never checked for &ldquo;success that contains nothing useful.&rdquo;</p>
<p>That night changed how I think about production systems.</p>
<h2 id="monitoring-checks-boxes-observability-answers-questions">Monitoring Checks Boxes. Observability Answers Questions.</h2>
<p> <a href="/blog/2017-03-20-why-observability-matters-more-than-monitoring/"
   
   >Monitoring is built around things you already know can go wrong</a>
. Threshold crossed, alert fires, runbook engaged. It works great for predictable problems: disk filling up, CPU pegged, error rate spiking. We had all of that at the fintech startup. Grafana dashboards everywhere. PagerDuty wired up. It felt safe.</p>
<p>But distributed systems don&rsquo;t fail in predictable ways. They fail in weird, combinatorial, never-seen-this-before ways. And that&rsquo;s where monitoring falls apart. You can&rsquo;t write an alert for a failure mode you haven&rsquo;t imagined yet.</p>
<p>Observability flips the model. Instead of predefining what questions the system can answer, you instrument it richly enough that you can ask <em>new</em> questions on the fly. During an incident. At 2 AM. Without deploying anything.</p>
<p>The difference: monitoring tells you something is wrong. Observability helps you figure out <em>why</em>.</p>
<h2 id="three-signals-one-story">Three Signals, One Story</h2>
<p>After the empty-200 incident, I started rebuilding our telemetry around three pillars.</p>
<p><strong>Metrics</strong> give you the bird&rsquo;s-eye view. Aggregated numbers over time. At the fintech startup, we track request rates, error rates, and latency distributions for every service using Prometheus.</p>
<pre tabindex="0"><code>http_requests_total{method=&#34;GET&#34;, endpoint=&#34;/api/stories&#34;, status=&#34;200&#34;} 15234
http_request_duration_seconds_bucket{endpoint=&#34;/api/stories&#34;, le=&#34;0.5&#34;} 421
</code></pre><p>Metrics are cheap to store and great for alerting. But they can&rsquo;t tell you why a specific request failed or what user it affected. They&rsquo;re the smoke detector, not the fire investigator.</p>
<p><strong>Logs</strong> capture the details. Each event, each error, each decision the code made. We moved early to structured JSON logs because free-form strings are nearly useless at scale.</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 style="color:#f92672">&#34;timestamp&#34;</span>:<span style="color:#e6db74">&#34;2018-07-09T10:30:45Z&#34;</span>,<span style="color:#f92672">&#34;level&#34;</span>:<span style="color:#e6db74">&#34;error&#34;</span>,<span style="color:#f92672">&#34;service&#34;</span>:<span style="color:#e6db74">&#34;ingestion&#34;</span>,<span style="color:#f92672">&#34;request_id&#34;</span>:<span style="color:#e6db74">&#34;abc123&#34;</span>,<span style="color:#f92672">&#34;message&#34;</span>:<span style="color:#e6db74">&#34;Empty response from provider&#34;</span>,<span style="color:#f92672">&#34;provider&#34;</span>:<span style="color:#e6db74">&#34;reuters&#34;</span>,<span style="color:#f92672">&#34;duration_ms&#34;</span>:<span style="color:#ae81ff">340</span>}
</span></span></code></pre></div><p>That structured format meant we could  <a href="/blog/2016-09-05-log-aggregation-at-scale-elk-vs-alternatives/"
   
   >query logs in Kibana</a>
 instead of grepping through them. That alone made incident response faster.</p>
<p><strong>Traces</strong> show you the journey of a single request across services. This was the missing piece for us. The fintech startup&rsquo;s architecture involves an ingestion service talking to NLP processors talking to a ranking engine talking to the API layer. When something is slow, you need to see the whole chain.</p>
<pre tabindex="0"><code>Trace abc123
- POST /ingest/batch (120ms)
  - NLP enrichment (45ms)
  - Relevance scoring (30ms)
  - db.write stories (35ms)
</code></pre><p>We started with Zipkin. Not perfect, but suddenly I could see that our NLP service was adding 200ms of latency on certain content types. That was invisible before.</p>
<h2 id="the-glue-correlation-ids">The Glue: Correlation IDs</h2>
<p>Each signal alone is useful. Together, connected by a shared identifier, they&rsquo;re powerful.</p>
<p>The workflow that matters: a latency alert fires from metrics. You pull up the trace for a slow request. The trace shows a specific span taking too long. You jump to the logs for that span using the same request ID and find the exact query and error.</p>
<pre tabindex="0"><code>X-Request-ID: 5f3c9e86c2c84e1b
X-B3-TraceId: 4d1e00a3b9bd1d42
X-B3-SpanId: 6df3a1c2b93f6b1a
</code></pre><p>We made it a rule: every log line includes the request ID, and every trace propagates context headers. No exceptions. It took weeks to retrofit across all our services, but the payoff during the next incident was immediate. Instead of an hour of archaeology, I had a clear thread to pull.</p>
<h2 id="instrument-the-critical-path-first">Instrument the Critical Path First</h2>
<p>When I started adding instrumentation at the fintech startup, the temptation was to instrument everything. Don&rsquo;t do that. You&rsquo;ll drown in data and your storage costs will spike.</p>
<p>Start with what matters: the critical path your users depend on.</p>
<p>For us that meant inbound API handlers, the news ingestion pipeline, database calls, and every external API dependency. We used RED (rate, errors, duration) for our services and USE (utilization, saturation, errors) for infrastructure resources.</p>
<p>Tracing has a sampling problem. Capturing every single request is expensive. We settled on keeping 100% of errors and sampling successful requests at about 5%. During incidents, we crank sampling up. Good enough for debugging, affordable enough to run continuously.</p>
<h2 id="designing-systems-that-can-be-debugged">Designing Systems That Can Be Debugged</h2>
<p>Observability isn&rsquo;t something you bolt on after the fact. It&rsquo;s a design choice.</p>
<p>Structured logs over free-form strings. Always. If you can&rsquo;t query it, it&rsquo;s useless when you&rsquo;re under pressure at 2 AM.</p>
<p>Watch your cardinality. Metrics with unbounded label values (like user IDs) will destroy your Prometheus instance. High-cardinality data belongs in logs and traces, not metrics.</p>
<p>Add context that matters. A trace that only shows timings is half the story. Include the request type, the tenant, the result count. When something goes wrong, that context is the difference between a five-minute fix and a two-hour hunt.</p>
<h2 id="our-stack-in-2018">Our Stack in 2018</h2>
<p>For anyone building this out now, this is what we run:</p>
<ul>
<li><strong>Metrics</strong>: Prometheus with Alertmanager, Grafana for dashboards</li>
<li><strong>Logs</strong>: Fluentd piping into Elasticsearch, Kibana for exploration</li>
<li><strong>Tracing</strong>: Zipkin with OpenTracing instrumentation</li>
</ul>
<p>OpenTracing is getting solid adoption and keeps us from being locked into one vendor. OpenCensus is emerging as an alternative worth watching. Both are pushing toward a world where trace context propagation just works out of the box.</p>
<p>There are commercial options that bundle everything behind one query layer. We looked at them. For our scale, the open source stack made more sense. That calculus changes depending on team size and how much operational overhead you can absorb.</p>
<h2 id="what-that-2-am-incident-taught-me">What That 2 AM Incident Taught Me</h2>
<p>That silent pipeline failure exposed a blind spot in how I thought about production readiness. Having dashboards isn&rsquo;t the same as having understanding.</p>
<p>We still have incidents at the fintech startup. But now when something breaks in a way we&rsquo;ve never seen before, we have the telemetry to figure it out fast.</p>
]]></content:encoded></item><item><title>SRE Principles for Small Teams: Skip the Cargo Cult</title><link>https://lawzava.com/blog/2018-04-30-site-reliability-engineering-principles/</link><pubDate>Mon, 30 Apr 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-04-30-site-reliability-engineering-principles/</guid><description>Teams copy Google&amp;amp;rsquo;s SRE playbook without asking if it fits. What matters for small teams: one SLO, error budgets, toil, alerts, and postmortems.</description><content:encoded><![CDATA[<p>Every conference talk I&rsquo;ve sat through this year has someone breathlessly explaining SRE like it&rsquo;s a revelation. SLOs! Error budgets! Toil reduction! The crowd nods along, takes notes, goes back to the office, and promptly cargo-cults the whole thing without stopping to think about whether any of it makes sense at their scale.</p>
<p>The principles behind SRE are good. I don&rsquo;t dispute that. What drives me up the wall is watching teams of fifteen people set up elaborate error budget policies and multi-stage canary deployments when they have three services and a hundred users. You don&rsquo;t need Google&rsquo;s playbook. You need common sense with a bit of structure.</p>
<h2 id="what-sre-gets-right">What SRE gets right</h2>
<p>The core idea is dead simple: treat reliability as a feature, not a side effect. Measure it. Set targets. Make trade-offs explicit instead of pretending you can ship fast and never break anything.</p>
<p>At the fintech startup, I started by picking our most critical service, the real-time financial data API, and defining one SLO for it. Not twelve. One. &ldquo;99.9% of requests succeed under 200ms.&rdquo; That single number gave us a shared language between engineering and product. When product wanted to ship something risky, we could point at the error budget and have a real conversation instead of gut-feel arguments.</p>
<p>That&rsquo;s the part people skip. The error budget isn&rsquo;t a fancy dashboard widget. It&rsquo;s a negotiation tool. Budget healthy? Ship bold. Budget burning? Slow down. Simple.</p>
<h2 id="slos-arent-a-religion">SLOs aren&rsquo;t a religion</h2>
<p>This is where the cargo-culting starts. I&rsquo;ve seen teams spend weeks building SLO tracking infrastructure before they even know what their users care about. They&rsquo;ll measure fourteen signals and set targets for all of them because the Google SRE book said so.</p>
<p>Pick the one or two things that matter to your users. For us it was latency and availability of the news feed. That&rsquo;s it. We didn&rsquo;t need saturation metrics for a service running on three nodes. We didn&rsquo;t need a formal SLA document when our &ldquo;SLA&rdquo; was &ldquo;don&rsquo;t let the paying customers notice.&rdquo;</p>
<p>Start with what hurts. The rest can wait.</p>
<h2 id="toil-is-real-but-so-is-over-engineering">Toil is real, but so is over-engineering</h2>
<p>The toil concept is the best thing to come out of SRE thinking. Manual, repetitive work that scales with the system and produces no lasting value. Kill it. Automate it. Eliminate the need for it entirely if you can.</p>
<p>At the fintech startup we had a deployment process that involved SSH-ing into machines and running scripts in a specific order. Classic toil. We automated it with a simple CI pipeline. Nothing fancy. No custom deployment orchestrator, no multi-stage canary with automatic rollback. A pipeline that ran tests and deployed if they passed.</p>
<p>The temptation is always to build the Rolls-Royce version. Resist it. Automate the thing that&rsquo;s wasting the most time right now. Move on. Come back later if you need more.</p>
<h2 id="monitoring-stop-alerting-on-nonsense">Monitoring: stop alerting on nonsense</h2>
<p>I had an alert that fired when CPU hit 80%. It went off constantly. Nobody cared. It trained the whole team to ignore alerts, which is the single worst outcome a monitoring system can produce.</p>
<p>We ripped out the noise and replaced it with  <a href="/blog/2016-12-12-production-monitoring-metrics-that-matter/"
   
   >alerts tied to user impact</a>
. P99 latency above our SLO threshold for ten minutes? That pages someone. CPU is high but latency is fine? That&rsquo;s a graph you check during business hours.</p>
<p>The four golden signals (latency, traffic, errors, saturation) are a fine starting framework. But &ldquo;framework&rdquo; is the key word. You don&rsquo;t need all four on day one. You need the ones that tell you when users are hurting.</p>
<h2 id="incidents-and-postmortems">Incidents and postmortems</h2>
<p>This part most teams do need, regardless of scale. When something breaks, have  <a href="/blog/2017-10-23-incident-management-for-growing-teams/"
   
   >a plan</a>
 that isn&rsquo;t &ldquo;the person who knows the most panics the hardest.&rdquo;</p>
<p>We kept it simple. Mitigate first, investigate later. Communicate during, not after. And when it&rsquo;s over, write up what happened without blaming anyone. Blameless postmortems aren&rsquo;t some warm-fuzzy HR initiative. They&rsquo;re how you actually learn things, because people tell the truth when they&rsquo;re not afraid of getting fired.</p>
<p>The part nobody talks about: follow through on the action items. I&rsquo;ve seen more postmortem documents gather dust than I care to admit. If you&rsquo;re not going to fix the thing, don&rsquo;t bother writing it down.</p>
<h2 id="gradual-rollouts-are-just-good-sense">Gradual rollouts are just good sense</h2>
<p>Deploy to 1%, watch the metrics, expand to 10%, watch again, go to 100%. This isn&rsquo;t rocket science. It&rsquo;s the engineering equivalent of &ldquo;taste the soup before serving it.&rdquo; But you don&rsquo;t always need automated canary analysis and a dedicated deployment pipeline to do it. Sometimes a feature flag and a pair of eyes on a dashboard is plenty.</p>
<p>Rollback capability matters more than rollout sophistication. Can you get back to the last known good state quickly? If yes, you&rsquo;re in decent shape. If no, fix that before you build anything else.</p>
<p>SRE gives you a vocabulary for thinking about reliability, and that&rsquo;s valuable. Treating the Google SRE book like scripture and implementing every practice wholesale because a conference speaker said so is not.</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>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>Building a Platform Team: Lessons from Our First Year</title><link>https://lawzava.com/blog/2017-12-28-building-platform-teams/</link><pubDate>Thu, 28 Dec 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-12-28-building-platform-teams/</guid><description>Standing up a small platform team at a fintech startup: tight scope, infrastructure run as a product, paved roads over mandates, and what I&amp;amp;rsquo;d change.</description><content:encoded><![CDATA[<p>Looking back at 2017, the single best engineering decision we made at the fintech startup was pulling infrastructure work out of the product teams and giving it a home. Not a big, formal reorg. Just me at first, then two of us, saying: we&rsquo;re going to own CI/CD, deployment, and monitoring so the rest of the team can stop reinventing it every sprint.</p>
<p>That was March. By December, we had a small platform team that actually worked. Getting there was messy.</p>
<h3 id="the-problem-was-obvious">The problem was obvious</h3>
<p>We had product engineers writing their own deployment scripts. Plural. Each service had its own way of getting to production. Some used shell scripts checked into the repo. One team had a guy who just SSH&rsquo;d into the box. Logging was inconsistent. Monitoring was whatever someone remembered to set up. When something broke at 2 AM, diagnosing it meant guessing which service did what and where its logs lived.</p>
<p>This is fine when you have three services. We had more than three services.</p>
<h3 id="what-we-built">What we built</h3>
<p>I kept the scope ruthless. Four areas, nothing else.</p>
<p><strong>Developer experience.</strong>  <a href="/blog/2016-06-06-continuous-deployment-without-chaos/"
   
   >CI/CD pipelines</a>
, a shared staging environment, deployment tooling that didn&rsquo;t require tribal knowledge. This was the first thing we tackled because it was the loudest pain point.</p>
<p><strong>Runtime infrastructure.</strong> Databases, caches, message queues. Standardized. Documented. Not six different Postgres configurations floating around.</p>
<p><strong>Observability.</strong>  <a href="/blog/2017-03-20-why-observability-matters-more-than-monitoring/"
   
   >Centralized logging and monitoring</a>
. One place to look when things go wrong. This alone saved us hours every incident.</p>
<p><strong>Security defaults.</strong> Secrets management that wasn&rsquo;t &ldquo;put it in an environment variable and hope.&rdquo; Auth tooling that product teams could plug into without reading a novel.</p>
<h3 id="treat-it-like-a-product-or-it-dies">Treat it like a product or it dies</h3>
<p>What I didn&rsquo;t expect: building the platform was the easy part. Getting people to use it was the actual job.</p>
<p>We had product engineers with muscle memory. They knew their janky deploy scripts. They didn&rsquo;t trust our new pipeline. Fair enough. So I stopped thinking of the platform as infrastructure and started thinking of it as a product. Our users were the product teams. If they didn&rsquo;t adopt what we built, we failed. Full stop.</p>
<p>This meant sitting with teams. Watching them work. Asking dumb questions like &ldquo;why did you just do that manually?&rdquo; and then going back and automating whatever that was. It meant writing docs that were actually useful, not docs that checked a box. It meant having a Slack channel where people got answers fast.</p>
<p>Self-service was non-negotiable. If a team needed a new database, they should get it in minutes. Not file a ticket. Not wait for me to wake up. The moment we became a ticket queue, we became the bottleneck we were trying to eliminate.</p>
<h3 id="how-we-built-the-team">How we built the team</h3>
<p>I started alone, then pulled in one more person with strong ops background. That was the right call. You need someone who has been paged at 3 AM and someone who knows how to build tooling that doesn&rsquo;t feel like punishment to use. Pure infra people build things that work but nobody can figure out. Pure app developers build things that look nice but fall over under load.</p>
<p>We stayed small. Two people, then three by Q4. Attached to engineering leadership, sitting next to the product teams. Physically close. That proximity matters more than any process. When you overhear someone complaining about deploys, you fix deploys. When you&rsquo;re in a different building, you build things nobody asked for.</p>
<h3 id="paved-roads-over-mandates">Paved roads over mandates</h3>
<p>We never mandated anything. I hate mandates. Instead, we built paved roads, to borrow Netflix&rsquo;s term: a service template, a default pipeline config, a monitoring setup that worked out of the box. You could ignore it all and do your own thing. But the paved road was so much easier that nobody bothered.</p>
<p>That&rsquo;s the trick. Make the right thing the easy thing. Don&rsquo;t write policies. Write code that makes the policy unnecessary.</p>
<p>We exposed platform capabilities through simple APIs. Want a database? Here&rsquo;s a YAML spec, submit it, done. Want to deploy? Push to master. Want logs? They are already where you expect 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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">platform.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">billing-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">postgresql</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">size</span>: <span style="color:#ae81ff">small</span>
</span></span></code></pre></div><p>Nothing fancy. Deliberately boring. Boring infrastructure is good infrastructure.</p>
<h3 id="what-i-would-do-differently">What I would do differently</h3>
<p>I waited too long to write documentation. The first three months, everything lived in my head and in Slack threads. That doesn&rsquo;t scale, even at our size. Write the docs early, even if they are rough.</p>
<p>I also underestimated how much time support would eat. By September, I was spending most of my week answering questions and helping teams migrate. Almost no time left to build new capabilities. We fixed this by setting explicit office hours and protecting two full days per week for building. Should have done that from day one.</p>
<h3 id="where-this-goes-next">Where this goes next</h3>
<p>We&rsquo;re a small company. This setup works for our size. If the fintech startup doubles its engineering team, the platform group will need to split into focused sub-teams: developer experience, infrastructure, security. But not yet. Right now the right move is staying lean and staying close to the people we serve.</p>
<p>A platform team is a product team that happens to build infrastructure. The moment you forget the product part, you&rsquo;re just an ops team with a fancier name.</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>Incident Management for Growing Teams: What to Change</title><link>https://lawzava.com/blog/2017-10-23-incident-management-for-growing-teams/</link><pubDate>Mon, 23 Oct 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-10-23-incident-management-for-growing-teams/</guid><description>How incident response changed as our fintech startup outgrew five people: incident roles, severity levels, sane on-call, mitigation first, real follow-up.</description><content:encoded><![CDATA[<p>3 AM on a Tuesday. Our primary data pipeline is down. Financial data is stale. Institutional clients are waking up expecting fresh data and getting yesterday&rsquo;s numbers. I&rsquo;m on a call with two engineers, both talking over each other, both working on different theories, neither aware of what the other has already tried. A third person pushes a fix to production that conflicts with what engineer number two is doing. The pipeline stays down for another forty minutes.</p>
<p>That incident cost us a client conversation I never want to have again.</p>
<p>At the fintech startup, we were five people for a long time. Incident response was simple: someone noticed a problem, yelled about it, and whoever was closest fixed it. Worked great. Then we grew. More services, more people, more time zones. That informal model didn&rsquo;t scale. It just produced chaos with more participants.</p>
<h2 id="roles-kill-ambiguity">Roles Kill Ambiguity</h2>
<p>The single biggest improvement we made was assigning roles during incidents. Not permanently, per incident.</p>
<p><strong>Incident commander.</strong> One person owns coordination. They decide severity, pull in the right people, and keep things moving. They don&rsquo;t debug. They run the show.</p>
<p><strong>Technical lead.</strong> The person actually investigating and fixing the problem. They talk to the incident commander, not to Slack at large.</p>
<p><strong>Comms lead.</strong> Someone handles customer updates and internal stakeholders. Separating this from the technical work was huge for us. Engineers hate writing status updates mid-crisis, and it shows.</p>
<p><strong>Scribe.</strong> Captures the timeline. What happened when, what was tried, what worked. Without this, your postmortem is fiction.</p>
<p>For smaller incidents, one person wears multiple hats. Fine. But for anything SEV-1, staff every role. Context switching during a crisis is where mistakes compound.</p>
<h2 id="severity-levels-are-a-decision-framework">Severity Levels Are a Decision Framework</h2>
<p>We use four levels. SEV-1 is full outage or data loss: all hands, immediate external comms. SEV-2 is partial degradation: on-call team owns it with escalation paths. SEV-3 hits a subset of users: on-call handles it. SEV-4 goes through normal workflow.</p>
<p>The numbering matters less than everyone agreeing on what warrants waking someone up at 3 AM versus filing a ticket. Without that agreement, you get two failure modes: either every alert is treated as a fire drill, or real emergencies get a slow response because the team is desensitized.</p>
<h2 id="on-call-that-doesnt-destroy-people">On-Call That Doesn&rsquo;t Destroy People</h2>
<p>I&rsquo;ve seen on-call rotations burn out good engineers in months. We run week-long rotations with explicit handoffs and a secondary on-call for escalation. The handoff is a real conversation, not a calendar entry.</p>
<p>Three rules we follow:</p>
<p>Response time expectations are written down. Not &ldquo;ASAP&rdquo;, actual numbers. Acknowledge within ten minutes, active investigation within thirty.</p>
<p> <a href="/blog/2016-12-12-production-monitoring-metrics-that-matter/"
   
   >Noisy alerts</a>
 get fixed or deleted. Every alert that wakes someone up without being actionable erodes trust in the system. We review alert volume monthly.</p>
<p>Compensation exists. Time off after a rough night. No one should eat the cost of a 3 AM page out of team loyalty.</p>
<h2 id="the-first-five-minutes">The First Five Minutes</h2>
<p>When a page fires, the response should be boring. Acknowledge it. Assess severity. Open a dedicated channel. Page the people who need to be there. Start a timeline with what you know, even if what you know is almost nothing.</p>
<p>This sounds obvious. It isn&rsquo;t. Without a checklist, people skip steps when adrenaline kicks in. They jump straight to debugging and forget to tell anyone what&rsquo;s happening.</p>
<h2 id="mitigation-before-root-cause">Mitigation Before Root Cause</h2>
<p>This one took me too long to learn. When something is broken, your first job is to stop the bleeding. Roll back the deploy. Restart the service. Disable the feature. Redirect traffic. Whatever makes the user impact stop.</p>
<p>Root cause analysis is important. It&rsquo;s also a luxury you earn after users stop being affected. I&rsquo;ve watched engineers spend thirty minutes debugging while customers churned. Fix it first, understand it later.</p>
<h2 id="postmortems-that-change-things">Postmortems That Change Things</h2>
<p>We do  <a href="/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/"
   
   >blameless postmortems</a>
 for every significant incident. Timeline, impact, root cause, contributing factors, action items. The blameless part matters. If people think they&rsquo;ll get punished, they&rsquo;ll hide what actually happened, and your postmortem becomes useless.</p>
<p>What I care about more than the postmortem document: <strong>action items that ship.</strong> Every action item gets an owner and a deadline. We review them in our regular engineering meetings. An item is closed when the change is deployed, not when someone says they&rsquo;ll get to it.</p>
<p>I can&rsquo;t overstate this. If your postmortem action items rot in a backlog, you will have the same incident again. Guaranteed.</p>
<h2 id="patterns-over-individual-fires">Patterns Over Individual Fires</h2>
<p>Track your incidents. Frequency by service, time to detect, time to resolve, on-call load per team. Individual incidents teach you something. Patterns teach you where to invest. If one service generates 60% of your pages, you have an engineering priority problem, and no on-call tweak will fix it.</p>
<h2 id="pick-tools-your-team-will-use">Pick Tools Your Team Will Use</h2>
<p>I don&rsquo;t care which paging system you run. PagerDuty, Opsgenie, whatever. I care that your team actually uses it consistently. Same for your status page, your incident channels, your postmortem docs. The best incident management tool is the one nobody has to be reminded to check.</p>
<p>We keep it simple at the fintech startup. Dedicated Slack channels per incident, a status page for external comms, and a shared doc template for postmortems. Nothing fancy. All of it used every time.</p>
<h2 id="predictability-is-the-goal">Predictability Is the Goal</h2>
<p>Systems break. What separates good teams from drowning teams is predictability in the response: everyone knows their role, everyone knows the severity, communication flows to the right places, and the things you learn actually make it into production.</p>
]]></content:encoded></item><item><title>Chaos Engineering for Small Teams: No Netflix Required</title><link>https://lawzava.com/blog/2017-08-21-chaos-engineering-for-mortals/</link><pubDate>Mon, 21 Aug 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-08-21-chaos-engineering-for-mortals/</guid><description>Chaos engineering for a small team: start with whiteboard game days, then staging experiments with kill and tc. Always a hypothesis, always a stop button.</description><content:encoded><![CDATA[<p>You don&rsquo;t need a chaos team, a million users, or Netflix&rsquo;s infrastructure. You need a hypothesis, a kill command, and the guts to run it during work hours.</p>
<p>Every talk I&rsquo;ve seen on chaos engineering opens with the same slide: Netflix&rsquo;s Chaos Monkey. And then everyone in the audience nods, thinks &ldquo;cool, but we&rsquo;re not Netflix,&rdquo; and goes back to hoping their services don&rsquo;t fall over on a Friday afternoon.</p>
<p>I get it. I was there too. At the fintech startup we had a small engineering team, a handful of microservices, and real users depending on real-time financial data. Not exactly the scale where you hire a dedicated Site Reliability team. But that&rsquo;s precisely why we needed chaos experiments. We couldn&rsquo;t afford a mystery outage at 2 AM with no runbook and no idea which service just took everything else down with it.</p>
<h3 id="what-chaos-engineering-is">What chaos engineering is</h3>
<p>Strip away the branding and chaos engineering is dead simple. You pick something that could break. You guess what will happen when it does. You break it. Then you check if you were right.</p>
<p>That&rsquo;s the whole thing. Hypothesis, experiment, observation. Science fair stuff.</p>
<p>The part most people skip is the hypothesis. They just kill a pod and watch the fireworks. Without a hypothesis, that&rsquo;s sabotage. The hypothesis is what makes it useful. &ldquo;If service A goes down, service B should degrade gracefully and return cached results within 500ms.&rdquo; Now you have something testable. Now you&rsquo;ll actually learn something when the answer is no.</p>
<h3 id="start-with-a-game-day-before-any-tool">Start with a game day before any tool</h3>
<p>Forget Chaos Monkey and the tooling around it. At least for now.</p>
<p>What worked for us was embarrassingly low-tech. We&rsquo;d pick a Thursday afternoon, gather around a whiteboard, and ask: &ldquo;What happens if Postgres becomes unreachable for 30 seconds?&rdquo; Then we&rsquo;d talk through it. Just talk. Who gets paged? What errors do users see? Does the queue back up? Does it recover automatically or does someone need to SSH in and restart something?</p>
<p>Half the time, the answer was &ldquo;nobody knows.&rdquo; That&rsquo;s the finding. That&rsquo;s the value. You don&rsquo;t even need to touch a terminal yet.</p>
<p>When you&rsquo;re ready to actually inject failures, staging is your friend. We ran our first real experiments there. Killed processes, added latency with <code>tc</code>, dropped DNS. Found out our  <a href="/blog/2017-01-16-kubernetes-in-production-year-of-lessons/"
   
   >health checks were lying to us</a>
. Found out one service had a retry loop that would hammer a dead dependency 200 times a second. In staging. Before any customer saw it.</p>
<h3 id="the-failures-that-teach-you-the-most">The failures that teach you the most</h3>
<p>Kill a process. See if it restarts. See if anyone gets alerted. You&rsquo;d be surprised how many teams discover their alerting doesn&rsquo;t work during their first chaos experiment. We did.</p>
<p>Add network latency. 100ms, then 500ms, then 2 seconds. Watch what happens to upstream callers.  <a href="/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/"
   
   >Timeouts configured wrong</a>
 will cascade faster than you can type <code>kubectl get pods</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Kill a process, see if your system notices</span>
</span></span><span style="display:flex;"><span>kill -9 $PID
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Add 100ms latency, see who complains</span>
</span></span><span style="display:flex;"><span>tc qdisc add dev eth0 root netem delay 100ms
</span></span></code></pre></div><p>Starve a service of memory or CPU. We had a data ingestion job at the fintech startup that would occasionally eat all available memory on the box. We only discovered this pattern because we simulated it first. Fixed it with proper resource limits before it hit production hard.</p>
<p>Cut off a dependency. What does your app do when Redis is gone? If the answer is &ldquo;500 errors for everyone,&rdquo; you&rsquo;ve got work to do. If the answer is &ldquo;falls back to the database, slightly slower,&rdquo; you&rsquo;re in good shape.</p>
<h3 id="the-only-rule-you-must-be-able-to-stop-it">The only rule: you must be able to stop it</h3>
<p>This is non-negotiable. If you can&rsquo;t kill the experiment in seconds, don&rsquo;t run it. Blast radius has to be small. Rollback has to be instant. Everyone on the team has to know the experiment is happening.</p>
<p>We had a simple rule: no experiments without a Slack message first, and no experiments that touch more than one service at a time. Boring, sure. But nobody got surprised, and we never turned a controlled test into a real incident.</p>
<p>Progress looks like: staging first, then one production instance, then a few, then wider. If staging experiments are still surprising you, you&rsquo;re not ready for production. Stay there. There&rsquo;s no shame in it.</p>
<h3 id="make-it-boring">Make it boring</h3>
<p>The goal is for chaos experiments to become routine. Wednesday afternoon, we break something. Thursday morning, we file tickets for what we found. Next week, we verify the fixes. Repeat.</p>
<p>The best outcome is when experiments stop being interesting. When you kill a service and everything just&hellip; works. Traffic reroutes. Alerts fire. Dashboards light up. Recovery happens automatically. Boring. Beautiful.</p>
<p>Resilience means knowing exactly what happens when things break. Any team can get there. You just have to be willing to break things on purpose first.</p>
]]></content:encoded></item><item><title>Security Automation in CI: Stop Reviewing by Hand</title><link>https://lawzava.com/blog/2017-07-17-security-automation-from-manual-to-continuous/</link><pubDate>Mon, 17 Jul 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-07-17-security-automation-from-manual-to-continuous/</guid><description>Manual security review can&amp;amp;rsquo;t keep up with continuous delivery. How I moved secret scanning, dependency audits, SAST, and DAST into a fintech CI pipeline.</description><content:encoded><![CDATA[<p>Automate the boring security checks. Put humans on the hard problems. Ship faster with fewer holes.</p>
<p>Classified government environments are rigid, process-heavy, approval-driven. Everything gated. Everything slow. For classified systems, that makes sense.</p>
<p>Fintech isn&rsquo;t classified systems.</p>
<p>At the fintech startup we  <a href="/blog/2016-06-06-continuous-deployment-without-chaos/"
   
   >ship multiple times a day</a>
. We handle financial data, user accounts, payment flows. The attack surface is real. But if I made every deploy wait for a manual security review, we&rsquo;d ship once a week. Maybe. And the devs would hate me. Rightfully so.</p>
<p>So I stopped pretending manual review scales and started building security into the pipeline itself.</p>
<h2 id="the-problem-with-manual-gates">The Problem with Manual Gates</h2>
<p>A manual security review at a startup doing continuous delivery looks like this: a Slack message saying &ldquo;hey can you look at this PR?&rdquo; followed by me context-switching from whatever I&rsquo;m actually working on, skimming the diff, and approving it because I&rsquo;ve got six other things on fire.</p>
<p>That&rsquo;s security theater.</p>
<p>Manual review is slow. It&rsquo;s inconsistent. I catch different things depending on whether I&rsquo;ve had coffee. It doesn&rsquo;t leave an audit trail beyond a thumbs-up emoji. And it absolutely can&rsquo;t cover every commit when you&rsquo;re pushing code dozens of times a day.</p>
<p>The goal is to stop wasting security engineers&rsquo; time on things a script can catch.</p>
<h2 id="what-i-built-layered-pipeline-checks">What I Built: Layered Pipeline Checks</h2>
<p>Security in a pipeline comes in layers. You want cheap, fast checks early and heavier analysis later. The key is that none of it requires a human to manually trigger.</p>
<p><strong>Pre-commit: catch secrets before they leave the laptop.</strong> This is the single highest-value automation I&rsquo;ve ever set up. One leaked API key in a public repo and you&rsquo;re having a very bad week. At the fintech startup, dealing with financial APIs, a leaked key could mean real money walking out the door.</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-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/sh
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">## .git/hooks/pre-commit</span>
</span></span><span style="display:flex;"><span>./scripts/scan-secrets.sh <span style="color:#f92672">||</span> exit <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>Dead simple. Runs in under a second. Saves you from the kind of incident that makes the news.</p>
<p><strong>PR checks: block the merge if it&rsquo;s dirty.</strong> Static analysis, dependency audit, container scan. All automated. All required to pass. Never <code>allow_failure: true</code>, which turns a gate into a suggestion.</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">security-checks</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">script</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">./scripts/sast.sh</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">./scripts/dependency-audit.sh</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">./scripts/container-scan.sh</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">allow_failure</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p>Cyber-defense exercises drill in the idea of &ldquo;deny by default.&rdquo; Same principle here. The merge doesn&rsquo;t happen unless the checks are green. No exceptions, no &ldquo;I&rsquo;ll fix it later&rdquo; PRs.</p>
<p><strong>Post-merge: the heavy stuff.</strong> DAST against staging. Security-focused integration tests. Things like &ldquo;can an unauthenticated user hit this admin endpoint?&rdquo; and &ldquo;are the CORS headers actually set right?&rdquo; Static analysis can&rsquo;t catch these. You need a running application.</p>
<p>In production, runtime monitoring and log analysis close the loop. Security becomes a continuous signal instead of a checkbox someone ticked three sprints ago.</p>
<h2 id="making-it-stick">Making It Stick</h2>
<p>Most teams screw up here: they turn on everything at once, get 400 findings on the first run, and the entire dev team starts ignoring the output within a week.</p>
<p>Don&rsquo;t do that.</p>
<p>Start with one check. Make it reliable. Make the failure messages useful. Instead of &ldquo;vulnerability found&rdquo;, say &ldquo;this dependency has a known RCE, upgrade to version X, here&rsquo;s the CVE link.&rdquo; The difference between a useful pipeline and an annoying one is whether the developer knows what to do when it fails.</p>
<p>At the fintech startup I rolled checks out one at a time over a few months. Secret scanning first. Then dependency auditing. Then SAST. Each one tuned, false positives suppressed, team trained on the output before adding the next. Boring, and effective.</p>
<p>Build a suppression process too. Some findings are false positives. Some are accepted risks. That&rsquo;s fine. But track the suppressions and review them. A suppression list that only grows is a red flag.</p>
<h2 id="humans-on-the-hard-problems">Humans on the Hard Problems</h2>
<p>Scanners catch known patterns. SQL injection signatures, outdated libraries, hardcoded credentials. Important stuff. But they can&rsquo;t think.</p>
<p>Threat modeling is a human job. So is reviewing the authentication architecture of a new feature, and penetration testing that simulates an attacker&rsquo;s creativity.  <a href="/blog/2016-05-23-security-incident-response-for-startups/"
   
   >Incident response</a>
 when something real happens at 2 AM is very much a human job.</p>
<p>The whole point of automating the repetitive checks is to free up time for this work. In cyber-defense exercises, dedicated teams handle threat analysis without getting bogged down running vulnerability scanners by hand. Same principle at a startup, except the &ldquo;dedicated team&rdquo; is me and maybe one other person. Which is exactly why automation matters even more.</p>
<h2 id="the-payoff">The Payoff</h2>
<p>Before automation, security at the fintech startup was me trying to review everything and inevitably missing things. After, every single commit gets checked. Every dependency gets audited. Every container gets scanned. And I spend my time on architecture reviews and threat modeling instead of eyeballing diffs.</p>
<p>The pipeline doesn&rsquo;t get tired. It doesn&rsquo;t get distracted. It doesn&rsquo;t approve a PR because it&rsquo;s Friday afternoon and everyone wants to go home.</p>
]]></content:encoded></item><item><title>Observability vs Monitoring: Why Dashboards Aren't Enough</title><link>https://lawzava.com/blog/2017-03-20-why-observability-matters-more-than-monitoring/</link><pubDate>Mon, 20 Mar 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-03-20-why-observability-matters-more-than-monitoring/</guid><description>Monitoring answers questions you predicted. After splitting into microservices, we needed structured logs, trace IDs, and tracing to debug what broke.</description><content:encoded><![CDATA[<p>Most teams think they have monitoring figured out. Dashboards, thresholds, PagerDuty. Something spikes, someone gets paged, someone fixes it. Works great when you have a monolith and three endpoints.</p>
<p>It falls apart the second you split into services.</p>
<p>I learned this the hard way. We had  <a href="/blog/2016-12-12-production-monitoring-metrics-that-matter/"
   
   >decent Grafana dashboards</a>
. Reasonable alerts. Then we started  <a href="/blog/2016-01-15-why-microservices-arent-always-the-answer/"
   
   >breaking things into microservices</a>
 and deploying multiple times a day. A request would fail somewhere in a chain of five services and our dashboards would just&hellip; look fine. Every individual service reported healthy metrics. The problem lived in the gaps between them.</p>
<p>That&rsquo;s the core issue with monitoring. It answers questions you already thought to ask. Latency on this endpoint? Sure. Error rate on that queue? Got it. But the failure you actually hit in production is the one you never predicted. Your dashboards have no panel for it.</p>
<h3 id="observability-is-a-different-mindset">Observability is a different mindset</h3>
<p>Observability means you instrument your system so you can ask <em>new</em> questions after something breaks. Not just &ldquo;is it up&rdquo; but &ldquo;why did this specific user&rsquo;s request take 8 seconds at 3am on Tuesday.&rdquo;</p>
<p>Three signals make this work: metrics for trends and alerts, logs for detail, and traces for stitching a single request across every service it touches. Separately they are useful. Together they let you follow one request from the edge to the failure.</p>
<p>Structured logging is the foundation. Stop writing free-text log lines. Make every log entry a JSON object with a trace ID, service name, version, and whatever fields you actually need to filter 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-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;2017-03-20T10: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;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;trace_id&#34;</span>: <span style="color:#e6db74">&#34;abc123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;payment-service&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;2.1.3&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then propagate context. Generate a trace ID at the edge and carry it through every downstream call. This is the single most important thing you can do. Without it you&rsquo;re just grepping logs and praying.</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-text" data-lang="text"><span style="display:flex;"><span>X-Trace-Id: abc123
</span></span><span style="display:flex;"><span>X-Span-Id: def456
</span></span><span style="display:flex;"><span>X-Parent-Span-Id: ghi789
</span></span></code></pre></div><h3 id="how-incidents-change">How incidents change</h3>
<p>Alert fires. Pull the trace. Follow the slow span to the source. Read the logs for that trace. Confirm with metrics whether it&rsquo;s one user or everyone. Done. Five minutes instead of forty-five minutes of bouncing between dashboards while your Slack channel fills with &ldquo;any update?&rdquo;</p>
<p>Same thing with performance. User says it&rsquo;s slow. Pull their trace. Compare to a healthy one. See the difference: a cache miss, an extra database round-trip, a third-party call timing out. You fix the actual cause instead of guessing.</p>
<h3 id="the-tools-dont-matter-that-much">The tools don&rsquo;t matter that much</h3>
<p>Prometheus, InfluxDB,  <a href="/blog/2016-09-05-log-aggregation-at-scale-elk-vs-alternatives/"
   
   >ELK</a>
, Zipkin: pick whatever fits your stack. Commercial platforms can save time. But the tooling isn&rsquo;t the hard part. The hard part is disciplined instrumentation. Consistent field names. Trace IDs everywhere. Every team following the same conventions.</p>
<p>No purchase gets you there. You stop staring at dashboards waiting for red and start asking questions about behavior you didn&rsquo;t expect. When every team ships services independently, that&rsquo;s the only way I&rsquo;ve found to stay sane.</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>Production Monitoring: Why We Deleted 42 Grafana Panels</title><link>https://lawzava.com/blog/2016-12-12-production-monitoring-metrics-that-matter/</link><pubDate>Mon, 12 Dec 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-12-12-production-monitoring-metrics-that-matter/</guid><description>We cut 47 Grafana panels to five metrics and three paging alerts. The production metrics that matter for a startup backend, and how to prune the rest.</description><content:encoded><![CDATA[<p>At a mobility startup we went through the classic monitoring arc. Started with nothing, panicked after an outage, installed StatsD and Grafana, then spent two weeks shipping every metric we could find into dashboards nobody looked at. We had 47 Grafana panels. Forty-seven. And when the next incident hit, we still couldn&rsquo;t figure out what was broken for twenty minutes.</p>
<p>The problem wasn&rsquo;t tooling. The problem was noise. So we deleted 42 of those panels. Kept five metrics. Built real alerts around them. Our on-call engineer started sleeping again.</p>
<h3 id="most-monitoring-is-waste">Most monitoring is waste</h3>
<p>Here is what I&rsquo;ve learned  <a href="/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/"
   
   >running production systems</a>
: if you&rsquo;re watching more than a handful of metrics, you&rsquo;re watching none of them. Human attention doesn&rsquo;t scale. Twenty dashboards with eight panels each means you have zero dashboards, because nobody is actually reading them under pressure.</p>
<p>The instinct after an outage is always &ldquo;we need more visibility.&rdquo; Wrong. You need better visibility. Those are different things.</p>
<h3 id="five-numbers-nothing-else">Five numbers, nothing else</h3>
<p>For a startup backend serving real users, these are the only metrics I care about on a daily basis:</p>
<p><strong>1. Request latency at p95 and p99.</strong> Not average. Averages lie. A service averaging 40ms with a p99 at 3 seconds is broken for one request in a hundred, and the user behind it is filing the support ticket. We tracked this per service and alerted on it.</p>
<p><strong>2. Error rate by type.</strong> Not just &ldquo;5xx count.&rdquo; Break it down. A spike in 401s is a different problem than a spike in 503s. One is probably a bad deploy or a client bug. The other is your database falling over. The distinction matters because the response is completely different.</p>
<p><strong>3. Saturation of your bottleneck resource.</strong> For us that was PostgreSQL connection pool utilization. For you it might be memory, disk I/O, or worker threads. The point is: know which resource will kill you first and watch that one. Not all resources. That one.</p>
<p><strong>4. Request throughput.</strong> Traffic going up is good. Traffic dropping to zero at 2pm on a Tuesday is very bad. This metric is less about the number and more about detecting sudden changes. A 50% drop in requests is an incident whether or not errors are firing.</p>
<p><strong>5. Deployment markers.</strong> Not a metric in the traditional sense, but I overlay every deploy on our Grafana dashboards. Half the incidents I&rsquo;ve investigated started with &ldquo;someone shipped something.&rdquo; Correlating metrics with  <a href="/blog/2016-06-06-continuous-deployment-without-chaos/"
   
   >deploys</a>
 is the fastest diagnostic shortcut I know.</p>
<p>That&rsquo;s it. Five things.</p>
<h3 id="alerts-should-wake-you-up-for-a-reason">Alerts should wake you up for a reason</h3>
<p>We had a rule: if an alert fires and the on-call engineer can&rsquo;t take a meaningful action within five minutes, delete the alert. Brief CPU spikes? Not actionable. Disk at 80%? Only actionable if there&rsquo;s a runbook. &ldquo;Something might be wrong&rdquo; isn&rsquo;t an alert. It&rsquo;s anxiety.</p>
<p>Our paging alerts were:</p>
<ul>
<li>p99 latency above threshold for 5 minutes</li>
<li>Error rate above 1% for 3 minutes</li>
<li>Primary database connection pool above 90%</li>
</ul>
<p>Three alerts. That was it for paging. Everything else went to a Slack channel that people checked during business hours.</p>
<h3 id="the-discipline-is-in-what-you-remove">The discipline is in what you remove</h3>
<p>After I stripped our monitoring down, the team pushed back. &ldquo;What if we miss something?&rdquo; My answer: we were already missing things. Forty-seven panels meant nobody looked at any of them carefully. Five panels meant the on-call engineer actually understood the state of the system at a glance.</p>
<p>Monitoring isn&rsquo;t a collection problem. It&rsquo;s an attention problem. Pick the five metrics that reflect what your users experience, alert only where someone can act, and delete everything else.</p>
]]></content:encoded></item><item><title>Container Orchestration: Docker Swarm vs Kubernetes vs Mesos</title><link>https://lawzava.com/blog/2016-10-17-container-orchestration-swarm-kubernetes-mesos/</link><pubDate>Mon, 17 Oct 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-10-17-container-orchestration-swarm-kubernetes-mesos/</guid><description>Docker Swarm, Kubernetes, and Mesos compared side by side at a mobility startup in late 2016. Kubernetes will win, but its operational tax is real.</description><content:encoded><![CDATA[<p>We tested all three at a mobility startup. Kubernetes won on capability but nearly lost on operational cost.</p>
<h3 id="the-evaluation">The evaluation</h3>
<p>At the mobility startup we run location-tracking services, payment processing, and a fleet management backend. All containerized. When we outgrew  <a href="/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/"
   
   >single-host Docker Compose</a>
, I needed an orchestrator that could schedule across hosts, handle rolling deploys, and not require a dedicated platform team to babysit.</p>
<p>I spent three weeks running Swarm, Kubernetes, and Mesos with Marathon side by side on identical three-node clusters. Same workload: our core API, a background worker, and a Redis instance. Here is what I found.</p>
<h3 id="the-comparison">The comparison</h3>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>Docker Swarm</th>
          <th>Kubernetes</th>
          <th>Mesos + Marathon</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Setup time</strong></td>
          <td>~30 minutes. <code>docker swarm init</code> and you&rsquo;re running.</td>
          <td>Hours to days depending on how you bootstrap. kubeadm helps but isn&rsquo;t production-ready yet.</td>
          <td>Half a day minimum. ZooKeeper, Mesos masters, Marathon framework. Lots of moving parts.</td>
      </tr>
      <tr>
          <td><strong>Learning curve</strong></td>
          <td>Low. If you know Docker, you know Swarm.</td>
          <td>Steep. Pods, services, deployments, ingress, RBAC, namespaces. Real concepts, but many of them.</td>
          <td>Medium-high. The two-layer model (Mesos offers resources, Marathon schedules) is elegant but unfamiliar.</td>
      </tr>
      <tr>
          <td><strong>Rolling deploys</strong></td>
          <td>Basic. Works, but limited control over rollout speed and health checks.</td>
          <td>Excellent. Deployment objects give you fine-grained rollout control, pause, and rollback.</td>
          <td>Good. Marathon handles health-check-gated rolling restarts well.</td>
      </tr>
      <tr>
          <td><strong>Service discovery</strong></td>
          <td>Built-in DNS. Simple, sufficient for most cases.</td>
          <td>Built-in DNS plus label selectors. Powerful, composable.</td>
          <td>Mesos-DNS or external tools. More wiring required.</td>
      </tr>
      <tr>
          <td><strong>Networking</strong></td>
          <td>Overlay network out of the box. Straightforward.</td>
          <td>CNI plugins. Flexible but you&rsquo;re choosing and configuring a network plugin on day one.</td>
          <td>Depends on configuration. Bridge, host, or CNI. Less opinionated, more work.</td>
      </tr>
      <tr>
          <td><strong>Ecosystem &amp; community</strong></td>
          <td>Tied to Docker Inc. Smaller community, fewer third-party tools.</td>
          <td>Growing fast. Google backing, CNCF formation, new tooling every week.</td>
          <td>Mature at scale (Twitter, Apple), but losing mindshare to Kubernetes in the container-specific space.</td>
      </tr>
      <tr>
          <td><strong>Operational overhead</strong></td>
          <td>Low. Few components, familiar tooling.</td>
          <td>High. etcd cluster, API server, scheduler, controller manager, kube-proxy, kubelet. Each one can fail.</td>
          <td>High. ZooKeeper quorum, Mesos masters, Marathon instances. Proven, but operationally heavy.</td>
      </tr>
      <tr>
          <td><strong>Scaling ceiling</strong></td>
          <td>Fine for dozens of services. Unclear beyond that in late 2016.</td>
          <td>Designed for thousands of pods. Already proven at Google-scale lineage.</td>
          <td>Built for tens of thousands of tasks. This is where Mesos genuinely excels.</td>
      </tr>
      <tr>
          <td><strong>Maturity</strong></td>
          <td>Young. Swarm mode shipped months ago in Docker 1.12.</td>
          <td>Young but accelerating. v1.4 is solid, v1.5 incoming. Rapid release cycle.</td>
          <td>Mature. Years of production use at large organizations.</td>
      </tr>
  </tbody>
</table>
<h3 id="my-take">My take</h3>
<p>Kubernetes is going to win this market. The abstraction model is right. Pods, services, and declarative desired-state are the correct primitives for running containers at scale. The community momentum is unmistakable. Every infrastructure vendor is building Kubernetes integrations, not Swarm or Mesos ones.</p>
<p>But the operational cost in late 2016 is genuinely high. I burned two full days debugging etcd quorum problems on a three-node cluster. Networking with flannel required reading source code, not documentation. RBAC is immature. Monitoring the control plane itself is a project unto itself.</p>
<p>Swarm is the practical answer for small teams today. We used it at the mobility startup before migrating to Kubernetes, and that stretch was productive. The API is Docker&rsquo;s API. The mental model is simple. You can ship with it and migrate later without regret.</p>
<p>Mesos is the right choice if you already run it or if you genuinely need to schedule containers alongside Spark jobs and batch workloads on the same cluster. For a startup running pure container workloads, Mesos is overbuilt. The ZooKeeper dependency alone adds operational surface area that a small team doesn&rsquo;t need.</p>
<h3 id="what-we-actually-did">What we actually did</h3>
<p>We started on Swarm and shipped to production on it. When we hit the limits of Swarm&rsquo;s rolling deploy controls and needed better service discovery for inter-service communication, we migrated to Kubernetes. The migration was painful but contained. It took about a week of focused work.</p>
<p>I would make the same decision again. Swarm bought us time to ship features instead of fighting infrastructure. Kubernetes gave us a platform we could grow into. Trying to start with Kubernetes on day one with our team size would have been a mistake.</p>
<h3 id="pick-for-your-team-not-the-hype">Pick for your team, not the hype</h3>
<p>Pick based on your team, not the feature matrix. Swarm if you need to ship now with minimal operational burden. Kubernetes if you can absorb the learning curve and want the platform that will matter in two years. Mesos if you&rsquo;re already running it or have workloads that genuinely require its scheduling model.</p>
<p>Kubernetes is going to dominate. That doesn&rsquo;t mean you have to adopt it today.</p>
]]></content:encoded></item><item><title>Building a Security-First Engineering Culture</title><link>https://lawzava.com/blog/2016-10-03-building-security-first-engineering-culture/</link><pubDate>Mon, 03 Oct 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-10-03-building-security-first-engineering-culture/</guid><description>Security culture is habits leadership enforces: no secrets in code, a security question on every PR, least privilege, patch deadlines, and champions.</description><content:encoded><![CDATA[<p>Stop treating security as a gate at the end of the pipeline. Embed it into how your team thinks, designs, and ships. This requires specific policies, visible leadership, and the willingness to slow down a release when the answer to &ldquo;what happens if this gets compromised?&rdquo; is &ldquo;I don&rsquo;t know.&rdquo;</p>
<h3 id="security-culture-isnt-a-project">Security culture isn&rsquo;t a project</h3>
<p>I&rsquo;ve watched teams try to buy their way into security. They purchase a scanner, run a penetration test once a year, and call it done. Then a junior engineer hardcodes an API key, pushes it to a public repo, and suddenly the scanner doesn&rsquo;t matter.</p>
<p>Security culture is the habit that prevents that push in the first place.</p>
<p>One team I worked with handled sensitive user data, including payment information. There&rsquo;s no version of &ldquo;we&rsquo;ll fix it later&rdquo; that regulators or users will accept. Security had to be foundational from the start, not bolted on after the first scare.</p>
<h3 id="what-cyber-defense-exercises-taught-me">What cyber-defense exercises taught me</h3>
<p>A principle I carry from cyber-defense exercises: the biggest lesson was organizational. The teams that performed well were the ones where every person understood the threat model and acted on it without waiting for permission. Tooling mattered less than that.</p>
<p>That stuck with me. Security at scale is a culture problem first.</p>
<h3 id="the-policies-i-actually-enforce">The policies I actually enforce</h3>
<p>Theory is cheap. Here are the specific rules I&rsquo;ve implemented across teams.</p>
<p><strong>No secrets in code. Ever.</strong> We use environment variables and a secrets manager. The CI pipeline fails if it detects anything that looks like a key or credential in the codebase. This is automated, not optional, and not overridable without a written justification that I personally review.</p>
<p><strong>Every pull request gets a security question.</strong> Not a full threat model. Just one question: &ldquo;What is the worst thing that happens if this code is exploited?&rdquo; If the author can&rsquo;t answer it, the PR doesn&rsquo;t merge. This forces engineers to think about attack surface as part of their daily work, not as a separate exercise.</p>
<p><strong>Least privilege by default.</strong> New services start with zero permissions and add only what they need. New employees get read access to the repositories they work on and nothing else. Escalation requires a request and a reason. I review access quarterly and revoke anything that isn&rsquo;t actively justified.</p>
<p><strong>Dependency updates aren&rsquo;t optional.</strong> We track dependencies weekly. Known vulnerabilities get patched within 48 hours for critical severity, one week for high. This is a policy, not a suggestion. I&rsquo;ve delayed feature work to meet these windows.</p>
<p><strong>Incident response is rehearsed.</strong> We run a  <a href="/blog/2016-05-23-security-incident-response-for-startups/"
   
   >tabletop exercise</a>
 every quarter. Not a checkbox drill. A real scenario where I throw a curveball halfway through to see how the team adapts. The ones who have done this three or four times respond to real incidents with calm instead of panic.</p>
<h3 id="the-hard-part-is-consistency">The hard part is consistency</h3>
<p>Any team can write a security policy document. The hard part is enforcing it on the days when you&rsquo;re behind on a deadline and the shortcut is right there.</p>
<p>I&rsquo;ve blocked releases. I&rsquo;ve told product managers that a feature would ship a week late because the authentication flow wasn&rsquo;t reviewed. Those conversations are uncomfortable. They are also the moments that define whether your culture is real or performative.</p>
<p>Engineers watch what leadership does, not what leadership says. If the CTO merges a PR that skips the security review because &ldquo;we need to ship,&rdquo; every engineer on the team learns that security is negotiable. It takes one exception to undo months of habit building.</p>
<h3 id="security-champions-scale-your-coverage">Security champions scale your coverage</h3>
<p>I can&rsquo;t review every line of code. No security team can. The answer isn&rsquo;t to hire more security people. It&rsquo;s to make every engineer a little bit dangerous.</p>
<p>The model I like: one engineer on each team acts as a security champion. Not a full-time role. Maybe 10-15% of their time. They attend a monthly session to review recent vulnerabilities in the stack, discuss attack patterns relevant to the business, and update the threat model.</p>
<p>These champions become the first line of defense in code reviews. They catch the obvious issues before they reach me, and they raise the questions that nobody else on the team would think to ask.</p>
<p>The key is that being a security champion is respected. It counts in performance reviews. It isn&rsquo;t extra work on top of their real job. It&rsquo;s part of their real job.</p>
<h3 id="make-the-secure-path-the-easy-path">Make the secure path the easy path</h3>
<p>If doing the right thing is harder than doing the wrong thing, people will do the wrong thing. That&rsquo;s ordinary human behavior.</p>
<p>We built internal libraries that handle authentication, input validation, and encryption correctly. When an engineer needs to call an external API, they use our wrapper that handles TLS verification, credential injection, and audit logging automatically. The secure path is also the path with less code to write.</p>
<p>We templated our infrastructure so that new services deploy with network isolation, encrypted storage, and logging enabled by default. An engineer has to go out of their way to deploy something insecure. That friction is intentional.</p>
<h3 id="incidents-are-data-not-blame">Incidents are data, not blame</h3>
<p>When something goes wrong, and it will, the response determines whether your culture strengthens or collapses.</p>
<p>We run  <a href="/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/"
   
   >blameless postmortems</a>
, because blame destroys information flow. If engineers fear punishment, they hide mistakes. Hidden mistakes compound. The breach that takes down a company is rarely the first failure. It&rsquo;s the tenth failure that nobody reported because the first person who reported one got burned.</p>
<p>Every postmortem produces exactly two outputs: a timeline of what happened, and a list of specific changes to prevent recurrence. Not &ldquo;be more careful.&rdquo; Specific changes. A new automated check. A revised permission. A policy update.</p>
<h3 id="security-is-a-daily-decision">Security is a daily decision</h3>
<p>Security culture is discipline. It&rsquo;s the same decision made correctly a thousand times, even when it&rsquo;s inconvenient. Especially when it&rsquo;s inconvenient.</p>
<p>You build it by enforcing the standards you set, including on yourself.</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>Zero-Downtime PostgreSQL Migrations: Expand and Contract</title><link>https://lawzava.com/blog/2016-08-15-database-migrations-without-downtime/</link><pubDate>Mon, 15 Aug 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-08-15-database-migrations-without-downtime/</guid><description>How to change PostgreSQL schemas without maintenance windows: expand and contract, batched backfills, NOT VALID constraints, and concurrent indexes.</description><content:encoded><![CDATA[<p>If your migration strategy is &ldquo;take the site down, run ALTER TABLE, pray, bring it back up,&rdquo; you&rsquo;re doing it wrong. Every schema change can be decomposed into steps that keep old and new code running simultaneously. It takes more discipline but zero heroics.</p>
<h3 id="the-problem-with-just-run-the-migration">The problem with &ldquo;just run the migration&rdquo;</h3>
<p>One system I worked on was a high-volume data API. Its clients didn&rsquo;t pause because the engineering team needed a maintenance window, and a feed that goes dark is a feed that loses subscribers. When I took over database operations there, the existing pattern was to schedule migrations at 2 AM on Sundays, take the API offline, run the DDL, and hope the application came back cleanly. It worked until it didn&rsquo;t.</p>
<p>The breaking point was a column type change on a table with 40 million rows of historical data. The migration ran for 47 minutes. During that time the table was locked, the API returned errors, and a downstream consumer silently switched to a stale cache it never recovered from. We spent the next two days cleaning up data consistency issues.</p>
<p>After that I decided every migration would be zero-downtime or it wouldn&rsquo;t ship.</p>
<h3 id="the-expand-and-contract-pattern">The expand-and-contract pattern</h3>
<p>The core idea is simple. Instead of making a breaking change in one step, you split it into phases where the schema is always compatible with whatever application code is currently running.</p>
<p><strong>Expand</strong>: add the new structure alongside the old one. Both coexist.</p>
<p><strong>Migrate</strong>: deploy code that writes to both old and new, reads from new. Backfill historical data.</p>
<p><strong>Contract</strong>: once every running instance uses the new structure, remove the old one.</p>
<p>This pattern handles nearly every schema change. The details vary, but the rhythm stays the same.</p>
<h3 id="adding-a-column-safely">Adding a column safely</h3>
<p>The simplest case. A nullable column with no default doesn&rsquo;t rewrite the table in PostgreSQL. It&rsquo;s metadata-only and takes a lock for milliseconds.</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:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> orders <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> processed_date DATE;
</span></span></code></pre></div><p>Fast. Safe. Old code ignores the column. New code starts writing to it.</p>
<p>If you need a NOT NULL constraint, don&rsquo;t add it in the same statement. PostgreSQL will scan the entire table to verify the constraint, holding an ACCESS EXCLUSIVE lock the whole time. Instead:</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> processed_date DATE;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Step 2: backfill in batches (see below)
</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 without full table scan
</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">CONSTRAINT</span> orders_processed_not_null
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">CHECK</span> (processed_date <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">VALID</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Step 4: validate separately (only takes a SHARE UPDATE EXCLUSIVE lock)
</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 VALIDATE <span style="color:#66d9ef">CONSTRAINT</span> orders_processed_not_null;
</span></span></code></pre></div><p>The <code>NOT VALID</code> trick is critical on large tables. It tells PostgreSQL to enforce the constraint on new writes immediately but skip the full table scan. The <code>VALIDATE</code> step runs later with a weaker lock that doesn&rsquo;t block writes. On our 40-million-row tables, this was the difference between a 200-millisecond migration and a 20-minute outage.</p>
<h3 id="renaming-a-column">Renaming a column</h3>
<p>You don&rsquo;t rename columns in production. Full stop.</p>
<p>What you actually do is expand-and-contract. Here is the concrete sequence, shown on an example <code>orders</code> table where <code>price</code> becomes <code>unit_price</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Phase 1: Expand
</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> unit_price NUMERIC(<span style="color:#ae81ff">18</span>,<span style="color:#ae81ff">8</span>);
</span></span></code></pre></div><p>Deploy code that writes to both <code>price</code> and <code>unit_price</code>. Reads come from <code>unit_price</code> with a fallback to <code>price</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Phase 2: Backfill
</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> unit_price <span style="color:#f92672">=</span> price
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> unit_price <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span> id <span style="color:#66d9ef">BETWEEN</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">AND</span> <span style="color:#ae81ff">100000</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Repeat 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> unit_price <span style="color:#f92672">=</span> price
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> unit_price <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span> id <span style="color:#66d9ef">BETWEEN</span> <span style="color:#ae81ff">100001</span> <span style="color:#66d9ef">AND</span> <span style="color:#ae81ff">200000</span>;
</span></span></code></pre></div><p>Once backfill is complete and every application instance reads from <code>unit_price</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-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Phase 3: Contract
</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">DROP</span> <span style="color:#66d9ef">COLUMN</span> price;
</span></span></code></pre></div><p>Three deployments minimum. That&rsquo;s the cost. The benefit is zero downtime and a clean rollback at every step.</p>
<h3 id="changing-a-column-type">Changing a column type</h3>
<p>Same pattern, different details. We had a case where an external identifier was stored as <code>INTEGER</code> but needed to become <code>TEXT</code> to support a new ID format.</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">-- Expand
</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> products <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> external_id_new TEXT;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Backfill
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> products <span style="color:#66d9ef">SET</span> external_id_new <span style="color:#f92672">=</span> external_id::TEXT
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> external_id_new <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span> id <span style="color:#66d9ef">BETWEEN</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">AND</span> <span style="color:#ae81ff">50000</span>;
</span></span></code></pre></div><p>Deploy dual-write code. Cut over reads. Drop the old column.</p>
<p>The temptation is to use <code>ALTER COLUMN ... TYPE TEXT</code> which rewrites the entire table under an exclusive lock. On a table with millions of rows that&rsquo;s actively being queried, that isn&rsquo;t an option.</p>
<h3 id="batched-backfills-that-dont-kill-the-database">Batched backfills that don&rsquo;t kill the database</h3>
<p>Large backfills are where most zero-downtime migrations go wrong. A single <code>UPDATE ... WHERE condition</code> on 40 million rows will generate enormous WAL, bloat the table, and hold locks that block concurrent operations.</p>
<p>The pattern 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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>set -e
</span></span><span style="display:flex;"><span>batch_size<span style="color:#f92672">=</span><span style="color:#ae81ff">5000</span>
</span></span><span style="display:flex;"><span>max_id<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>psql -Atc <span style="color:#e6db74">&#34;SELECT COALESCE(MAX(id), 0) FROM orders&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#f92672">((</span>current_id <span style="color:#f92672">=</span> 0; current_id &lt; max_id; current_id <span style="color:#f92672">+=</span> batch_size<span style="color:#f92672">))</span>; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  psql -c <span style="color:#e6db74">&#34;UPDATE orders
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">           SET unit_price = price
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">           WHERE id &gt; </span>$current_id<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">             AND id &lt;= </span>$current_id<span style="color:#e6db74"> + </span>$batch_size<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">             AND unit_price IS NULL&#34;</span>
</span></span><span style="display:flex;"><span>  sleep 0.1  <span style="color:#75715e"># breathe</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>Each <code>psql</code> call is its own transaction, so every batch commits and releases its locks before the next one starts. A <code>DO</code> block can&rsquo;t do that: it runs as a single transaction from start to finish.</p>
<p>The pause matters. Without it, you saturate I/O and  <a href="/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/"
   
   >replication lag</a>
 spikes. On that system our replicas served read traffic for dashboards and analytics. A backfill that caused 30 seconds of replication lag would show stale data to every user watching a dashboard. We settled on batches of 5,000 rows with 100ms pauses. The backfill took longer but production stayed healthy.</p>
<h3 id="indexes-without-locking">Indexes without locking</h3>
<p><code>CREATE INDEX</code> on PostgreSQL takes a SHARE lock on the table, which blocks writes for the duration of the build. On a large table, that can be minutes.</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:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_orders_processed <span style="color:#66d9ef">ON</span> orders(processed_date);
</span></span></code></pre></div><p><code>CONCURRENTLY</code> builds the index without blocking writes. It takes longer and does two table scans instead of one, but it&rsquo;s the only option for production tables. There are two caveats worth knowing:</p>
<p>First, it can&rsquo;t run inside a transaction. If your migration tool wraps everything in a transaction, you need to handle this case separately.</p>
<p>Second, if it fails partway through, it leaves an invalid index behind. Find it in <code>pg_index</code> and drop it before retrying.</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:#66d9ef">SELECT</span> indexrelid::regclass, indisvalid
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_index
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">NOT</span> indisvalid;
</span></span></code></pre></div><h3 id="foreign-keys-without-blocking">Foreign keys without blocking</h3>
<p>Adding a foreign key constraint with <code>ADD CONSTRAINT ... FOREIGN KEY</code> scans the whole table while holding a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes for the duration. Same trick as NOT NULL:</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:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> orders
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">CONSTRAINT</span> fk_orders_product
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">FOREIGN</span> <span style="color:#66d9ef">KEY</span> (product_id) <span style="color:#66d9ef">REFERENCES</span> products(id)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">VALID</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> orders VALIDATE <span style="color:#66d9ef">CONSTRAINT</span> fk_orders_product;
</span></span></code></pre></div><p>The first statement adds the constraint for new writes only, taking a brief lock. The second validates existing rows with a much weaker lock.</p>
<h3 id="the-deployment-sequence">The deployment sequence</h3>
<p>The order matters. Getting it wrong means you have a window where the application and schema disagree.</p>
<ol>
<li>
<p><strong>Run expand migration.</strong> New columns, new indexes (concurrently), new constraints (NOT VALID). Schema is now compatible with both old and new code.</p>
</li>
<li>
<p><strong>Deploy new application code.</strong> Writes to both old and new columns. Reads from new with fallback to old.</p>
</li>
<li>
<p><strong>Run backfill.</strong> In batches, during low traffic if possible. Monitor replication lag and query latency.</p>
</li>
<li>
<p><strong>Verify completeness.</strong> <code>SELECT COUNT(*) FROM orders WHERE unit_price IS NULL</code> should be zero.</p>
</li>
<li>
<p><strong>Deploy read-cutover code.</strong> Reads exclusively from new columns. Still writes to both.</p>
</li>
<li>
<p><strong>Run contract migration.</strong> Drop old columns, drop temporary constraints. Schema is clean.</p>
</li>
</ol>
<p>Each step is independently reversible. If the backfill causes problems, pause it. If the new code has bugs, roll back the deployment. The old schema is still there. This is the entire point.</p>
<h3 id="what-this-costs">What this costs</h3>
<p>More deployments. More code that handles two schemas simultaneously. Migrations that used to be one PR become three or four. Application code carries temporary dual-write logic that gets cleaned up in the contract phase.</p>
<p>It&rsquo;s more work. But on that system, the alternative was telling API consumers that we needed a maintenance window during their business hours. That conversation never goes well.</p>
<p>The discipline pays off in a different way too. When every migration follows expand-and-contract,  <a href="/blog/2016-06-06-continuous-deployment-without-chaos/"
   
   >deployments become boring</a>
. Nobody pages you at 2 AM for a schema change. Nobody holds their breath during a release. The process is mechanical and predictable.</p>
<p>The best migration is the one nobody notices.</p>
]]></content:encoded></item><item><title>Why I Moved Our AWS Infrastructure to Terraform</title><link>https://lawzava.com/blog/2016-06-20-why-i-moved-our-infrastructure-to-terraform/</link><pubDate>Mon, 20 Jun 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-06-20-why-i-moved-our-infrastructure-to-terraform/</guid><description>We moved from console-driven, script-heavy infrastructure to Terraform so changes are reviewed, reproducible, and recoverable from code.</description><content:encoded><![CDATA[<p>After a botched production recovery at a mobility startup exposed how little we actually knew about our own infrastructure, I moved everything to Terraform. It wasn&rsquo;t fast and it wasn&rsquo;t glamorous, but now our AWS environments live in version control and I sleep better.</p>
<h3 id="the-night-that-changed-my-mind">The Night That Changed My Mind</h3>
<p>I got paged at 1 AM because an EC2 instance hosting our vehicle-availability API had died. No big deal, I thought. Spin up a new one. Except nobody could tell me the exact instance type, the security group rules, or the IAM role attached to it. The wiki said one thing. The console said another. The CloudFormation stack that supposedly managed it had drifted so far from reality that applying it would have torn down half the VPC.</p>
<p>We recovered in about four hours. Four hours for one instance. That was the moment I decided our infrastructure couldn&rsquo;t live in people&rsquo;s heads anymore.</p>
<h3 id="what-we-were-running-before">What We Were Running Before</h3>
<p>The mobility startup&rsquo;s AWS setup was a patchwork. Some resources lived in a CloudFormation stack written by a contractor who had since left. Other pieces were created by hand in the console during late-night debugging sessions and never documented. A handful of bash scripts in a repo called <code>infra-scripts</code> handled things like spinning up staging environments, but they were brittle and nobody trusted them enough to run without reading every line first.</p>
<p>CloudFormation itself wasn&rsquo;t the problem exactly. The problem was that we treated it as a one-time setup tool instead of a living definition. We would write a template, deploy it, then immediately start making manual changes on top. Within weeks the template was fiction.</p>
<h3 id="why-terraform-won">Why Terraform Won</h3>
<p>I evaluated three options: doubling down on CloudFormation, adopting  <a href="/blog/2016-04-25-configuration-management-ansible-puppet-chef/"
   
   >Ansible</a>
 for infrastructure provisioning, or trying Terraform. Terraform was still young in mid-2016, somewhere around version 0.6, but it had two properties that mattered to me.</p>
<p>First, the declarative model. You describe what you want, not the steps to get there. After years of imperative scripts where ordering and idempotency were my problem, this was a relief. I could define a VPC, subnets, security groups, and instances in HCL and let Terraform figure out the dependency graph.</p>
<p>Second, the plan step. Before Terraform touches anything, it shows you exactly what it intends to do. Create this, modify that, destroy the other. At the mobility startup we were a small team and we couldn&rsquo;t afford surprises. The plan became our pre-flight checklist.</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;vehicle_api&#34;</span> {
</span></span><span style="display:flex;"><span>  ami           <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ami-0a2e1043&#34;</span>
</span></span><span style="display:flex;"><span>  instance_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;t2.small&#34;</span>
</span></span><span style="display:flex;"><span>  subnet_id     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${aws_subnet.private.id}&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  vpc_security_group_ids <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;${aws_security_group.api.id}&#34;</span>,
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  iam_instance_profile <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${aws_iam_instance_profile.vehicle_api.name}&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">tags</span> {
</span></span><span style="display:flex;"><span>    Name        <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vehicle-api-${var.environment}&#34;</span>
</span></span><span style="display:flex;"><span>    Service     <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;vehicle-availability&#34;</span>
</span></span><span style="display:flex;"><span>    Environment <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;${var.environment}&#34;</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>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That snippet is close to our actual vehicle-availability API definition. Nothing clever. Readable. Reviewable. If someone asks how production is configured, the answer is in Git.</p>
<h3 id="the-migration">The Migration</h3>
<p>I didn&rsquo;t attempt a big bang. That would have been reckless with a small engineering team and live users on the platform.</p>
<p>Week one: all new infrastructure goes into Terraform. No exceptions. If you need a new security group, you write HCL.</p>
<p>Week two through four: I started bringing existing resources under Terraform. Terraform 0.6 has no import command, so this is the tedious part: write the HCL for a resource, get it into state, run <code>plan</code>, and iterate until plan shows no changes. For our setup it meant adopting roughly forty resources across the VPC, subnets, route tables, security groups, instances, RDS, and an Elastic Load Balancer.</p>
<p>I did this environment by environment. Development first, where mistakes were cheap. Then staging. Production came last, after I had already made every dumb mistake in a place where it didn&rsquo;t matter. By the time I brought the production VPC under Terraform, the process was mechanical.</p>
<p>Modules helped keep things sane. I built a small VPC module and a service module. Every environment became a composition of those two building blocks with different variables. The upfront effort paid back immediately in consistency. Staging actually looked like production for the first time.</p>
<h3 id="state-the-sharp-edge">State: The Sharp Edge</h3>
<p>Terraform&rsquo;s state file is where it tracks the mapping between your HCL and real AWS resources. It&rsquo;s powerful and it&rsquo;s dangerous. Early on I kept state in a local file and committed it to Git. That worked fine for a solo operator but fell apart as soon as a second engineer touched the repo.</p>
<p>We moved state to S3 remote state within the first month. Terraform has no state locking yet, so two people applying at once is a real risk: concurrent applies can corrupt state and leave you in a worse position than having no IaC at all. The state file also contains secrets in plain text, things like RDS passwords and API keys. Encryption at rest on the S3 bucket and tight IAM policies aren&rsquo;t optional. We treat state like production credentials because that&rsquo;s what it contains.</p>
<h3 id="what-changed-operationally">What Changed Operationally</h3>
<p>The biggest shift was cultural. Before Terraform, infrastructure changes happened whenever someone had console access and a reason. After Terraform, every change went through a pull request. Run <code>terraform plan</code>, paste the output into the PR, get a review, merge, apply. Simple process. Hard to skip.</p>
<p>We caught real problems this way. A plan once showed that changing a subnet CIDR would destroy and recreate every instance in the subnet. That would have been a fifteen-minute outage discovered at 2 AM instead of during a calm code review on a Tuesday afternoon.</p>
<p>Recovery got faster too. When that same vehicle-availability API instance died again two months later, rebuilding it was a <code>terraform apply</code> away. Five minutes instead of four hours. That alone justified the migration.</p>
<h3 id="trade-offs">Trade-offs</h3>
<p>Terraform isn&rsquo;t free. The learning curve is real, especially around state management and provider quirks. HCL is readable but limited. Complex logic feels awkward compared to a real programming language. And in mid-2016, the provider ecosystem still has gaps. We hit bugs. We read source code. We worked around things.</p>
<p>But the alternative was worse. The alternative was guessing at 1 AM.</p>
<h3 id="where-we-stand">Where We Stand</h3>
<p>Every piece of the mobility startup&rsquo;s AWS infrastructure is now defined in Terraform and stored in version control. Environments are reproducible. Changes are reviewed. Recovery is predictable. The migration took about six weeks of focused work alongside normal feature development.</p>
<p>If your infrastructure still lives in consoles and half-remembered scripts, make the move. Terraform isn&rsquo;t perfect, but the cost of not knowing what you&rsquo;re running is higher than you think. You will find out exactly how high at 1 AM on a Tuesday.</p>
]]></content:encoded></item><item><title>Continuous Deployment Without the Chaos</title><link>https://lawzava.com/blog/2016-06-06-continuous-deployment-without-chaos/</link><pubDate>Mon, 06 Jun 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-06-06-continuous-deployment-without-chaos/</guid><description>Continuous deployment is a discipline problem. How we ship a mobility startup&amp;amp;rsquo;s backend many times a day: trusted tests, small changes, fast rollback.</description><content:encoded><![CDATA[<p>The teams that deploy often without breaking things aren&rsquo;t smarter or better staffed. They are more disciplined. Continuous deployment is a set of habits enforced by automation.</p>
<h3 id="deploying-many-times-a-day">Deploying Many Times a Day</h3>
<p>At a mobility startup we deploy our backend services multiple times a day. Not because we decided to be trendy. Because our business requires it. When you&rsquo;re running a shared mobility platform, a broken deploy means vehicles that don&rsquo;t unlock, payments that don&rsquo;t process, and users who open a competitor&rsquo;s app. The cost of a bad release is immediate and measurable.</p>
<p>That urgency forced us to take continuous deployment seriously much earlier than I expected. We were a small team, moving fast, and the only way to ship that fast without constant fires was to build discipline into the pipeline itself.</p>
<h3 id="cd-is-a-discipline-problem">CD Is a Discipline Problem</h3>
<p>Most teams that fail at continuous deployment fail because they treat it as a tooling problem. They install Jenkins, wire up a webhook, and wonder why production keeps breaking on Friday afternoon.</p>
<p>The tooling is the easy part. Jenkins works. GitLab CI is getting better by the month. The hard part is the set of habits that make frequent deploys safe. Habits like writing tests that actually catch regressions. Habits like reviewing changes with deployment risk in mind. Habits like keeping every change small enough to reason about when something goes wrong at midnight.</p>
<p>I&rsquo;ve watched teams with excellent pipelines ship broken code because nobody enforced the discipline around the pipeline. And I&rsquo;ve watched teams with mediocre tooling ship reliably because their habits were strong. Discipline beats tooling every time.</p>
<h3 id="the-pipeline-we-actually-run">The Pipeline We Actually Run</h3>
<p>Our Jenkins setup is straightforward. On every push, it runs the unit tests. If those pass, it builds the Docker image,  <a href="/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/"
   
   >tags it with the commit SHA</a>
, and pushes it to our private registry. Integration tests run against that image. If everything is green, it deploys to staging automatically. Production deploys happen after a one-click approval (strictly speaking, that makes our production step continuous delivery), and the click takes about ten seconds because the person approving has already seen the diff, the test results, and the staging behavior.</p>
<p>The whole cycle from push to production takes under fifteen minutes on a good day. That speed matters. When the feedback loop is short, developers catch their own mistakes before context switches away. When the loop is long, commits pile up, responsibility diffuses, and nobody knows which change caused the problem.</p>
<p>We also run a nightly full regression suite that catches the slower, more expensive test cases. Those don&rsquo;t block individual deploys, but a red nightly build stops all production deploys the next morning until someone fixes it. No exceptions.</p>
<h3 id="tests-you-can-trust">Tests You Can Trust</h3>
<p>A continuous deployment pipeline is only as strong as the tests it runs. Flaky tests are poison. A test that fails randomly teaches the team to ignore failures. Once that habit forms, real failures get waved through too.</p>
<p>We spent weeks stabilizing our test suite before we trusted it to gate production deploys. That meant removing tests that depended on timing, isolating tests that shared state, and replacing slow end-to-end tests with faster contract tests where possible. It was unglamorous work. It was also the single highest-leverage investment we made in our deployment process.</p>
<p>The rule is simple: if a test fails, it means something is wrong with the code, not with the test. Any test that violates that rule gets fixed or deleted. There&rsquo;s no middle ground.</p>
<h3 id="small-changes-fast-rollback">Small Changes, Fast Rollback</h3>
<p>Every change we deploy is small. Not because we have a policy document that says so. Because small changes are easier to review, easier to test, and critically easier to roll back.</p>
<p>Rollback isn&rsquo;t an afterthought in our process. It&rsquo;s a first-class operation. We keep the previous three Docker images tagged and ready. Rolling back means pointing the load balancer at the previous image. It takes less than a minute. We practice it regularly, not just when things are on fire.</p>
<p>The worst continuous deployment failures I&rsquo;ve seen happened because rollback was theoretical. Someone wrote a wiki page describing the rollback steps, nobody ever tested them, and when the moment came, the steps didn&rsquo;t work. A rollback plan that hasn&rsquo;t been exercised isn&rsquo;t a plan. It&rsquo;s a wish.</p>
<h3 id="monitoring-that-closes-the-loop">Monitoring That Closes the Loop</h3>
<p>Deploying fast without watching the result is just shipping bugs faster. After every production deploy, we watch error rates, response latency, and a handful of business metrics for at least ten minutes. If anything moves in the wrong direction, we roll back first and investigate second.</p>
<p>This is where most teams cut corners. They deploy, see green in the pipeline, and move on. But the pipeline only tells you the tests passed. It doesn&rsquo;t tell you that the new code path is three times slower under real traffic, or that an edge case in the mobile client is now returning 500s.</p>
<p>We use basic Grafana dashboards with alerts that fire if error rate or p99 latency crosses a threshold within the first fifteen minutes after deploy. Nothing sophisticated. Just enough signal to catch the obvious problems before users do.</p>
<h3 id="when-cd-doesnt-fit">When CD Doesn&rsquo;t Fit</h3>
<p>I&rsquo;m not a purist about this. Some systems shouldn&rsquo;t deploy on every green build. Anything that touches payment processing gets an extra review gate. Schema migrations go through a separate, more careful process. And we don&rsquo;t pretend that mobile app releases can follow the same cadence as backend services when the App Store review cycle exists.</p>
<p>The point of continuous deployment isn&rsquo;t to deploy everything all the time. It&rsquo;s to make deployment a non-event for the systems where speed matters. For everything else, continuous delivery, where the artifact is always ready but the final step is manual, is perfectly fine.</p>
<h3 id="ship-the-habits-first">Ship the habits first</h3>
<p>After months of running this process, what matters most is the team&rsquo;s relationship with production. When developers  <a href="/blog/2016-03-10-building-devops-culture-from-scratch/"
   
   >own the deploy and own the monitoring</a>
, they write smaller changes and better tests, and they think about rollback before they write the feature.</p>
<p>If you want continuous deployment, start with discipline. Make your tests trustworthy. Make your changes small. Make rollback fast and practiced. Make monitoring non-negotiable. The tooling will follow. Jenkins, GitLab CI, or whatever comes next won&rsquo;t matter if the habits aren&rsquo;t there first.</p>
]]></content:encoded></item><item><title>Security Incident Response Playbook for Startups</title><link>https://lawzava.com/blog/2016-05-23-security-incident-response-for-startups/</link><pubDate>Mon, 23 May 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-05-23-security-incident-response-for-startups/</guid><description>An incident response playbook for small teams, learned the hard way: define incidents, name owners, contain carefully, and communicate clearly.</description><content:encoded><![CDATA[<p>It was a Tuesday in February, around 11 PM. I was finishing a late sprint at our shared mobility startup when our monitoring lit up. Unusual outbound traffic from one of our API servers. Not a spike in user traffic. Not a deploy gone wrong. Something else entirely.</p>
<p>I pulled up the logs and saw authenticated requests hitting an internal endpoint we had deprecated months ago but never torn down. The requests were coming from a session token that belonged to an engineer who had left the company six weeks earlier. His credentials should have been revoked. They weren&rsquo;t. Somewhere between the offboarding checklist and our actual IAM setup, we had a gap.</p>
<p>The next four hours taught me more about incident response than any exercise or drill ever could. Not because the technical problem was hard. It wasn&rsquo;t. The deprecated endpoint was read-only, the data it exposed was limited, and the token hadn&rsquo;t been used for anything beyond what looked like automated probing. But the organizational response was chaos. Nobody knew who was supposed to make decisions. Our CEO was calling me every ten minutes asking for updates I didn&rsquo;t have. One engineer started revoking tokens across the board, which knocked real users offline. Another started restoring from backups before we understood what had actually happened.</p>
<p>We contained it within a few hours. The blast radius was small. But the recovery took three days because of the collateral damage from our own panic. The token revocation took down a payment processing integration. The premature backup restore overwrote six hours of legitimate transaction data. We spent more time cleaning up our response than we spent on the actual incident.</p>
<p>That night changed how I think about security incidents. The technical compromise was a footnote. The damage came from not having a plan: people acting fast without acting together.</p>
<p>The lesson I took from it: the plan doesn&rsquo;t need to be sophisticated. It needs to exist, it needs to be practiced, and it needs to be clear about who does what. That&rsquo;s the bar. Most startups don&rsquo;t clear it.</p>
<p>Here is what I&rsquo;ve learned.</p>
<h3 id="the-line-between-event-and-incident">The line between event and incident</h3>
<p>Draw it before you need it. A security event is suspicious activity that might be nothing. A security incident is confirmed or strongly suspected unauthorized access that demands immediate action.</p>
<p>When you&rsquo;re unsure, treat it as an incident and downgrade later. This is a lesson reinforced in cyber-defense exercises, and it applies just as much to a twelve-person startup. You can walk back an overreaction. You can&rsquo;t walk back a delayed containment that let an attacker pivot to your production database.</p>
<p>The distinction matters because it determines who gets woken up and how fast. If everything is an incident, people stop responding. If nothing is, you miss the real ones.</p>
<h3 id="ownership-before-the-alarm-goes-off">Ownership before the alarm goes off</h3>
<p>At our mobility startup, the problem wasn&rsquo;t that nobody cared. Everyone cared. That was the problem. Five people all making independent decisions in a crisis is worse than one person making imperfect decisions with authority.</p>
<p>You need three roles defined ahead of time:</p>
<p><strong>Incident commander.</strong> This person makes decisions and keeps the response coordinated. They don&rsquo;t need to be the most technical person. They need to be calm, organized, and empowered to say &ldquo;stop&rdquo; when someone is about to make things worse.</p>
<p><strong>Technical lead.</strong> This person drives the investigation and remediation. They decide what logs to pull, what systems to isolate, and what the containment strategy looks like.</p>
<p><strong>Communications lead.</strong> This person keeps internal and external messaging accurate. At a five-person startup, this might be the CEO. At a fifty-person company, it might be someone in ops or legal.</p>
<p>At a small startup, one person wears multiple hats. Fine. But the ownership has to be explicit. Write it down. Put it in the wiki. And for the love of everything, make sure the contact information works at 11 PM on a Tuesday. Test it quarterly. Phone numbers change. People leave.  <a href="/blog/2016-03-10-building-devops-culture-from-scratch/"
   
   >On-call lists</a>
 rot faster than you think.</p>
<h3 id="severity-in-plain-language">Severity in plain language</h3>
<p>Fancy severity matrices are a waste of time at a startup. You need four levels that everyone understands without looking anything up:</p>
<p><strong>Critical.</strong> Active data exfiltration or production systems are compromised. Drop everything.</p>
<p><strong>High.</strong> Confirmed unauthorized access, scope unclear. Assemble the response team now.</p>
<p><strong>Medium.</strong> Suspected compromise, limited scope. Investigate within hours, not days.</p>
<p><strong>Low.</strong> Suspicious activity that needs a closer look. Triage it during business hours.</p>
<p>The most important thing about your severity levels is that people actually use them. If your incident commander has to consult a decision tree to figure out whether an incident is a P1 or a P2, you have failed.</p>
<h3 id="containment-is-where-most-startups-break">Containment is where most startups break</h3>
<p>Containment is the step where panic does the most damage. The goal is to stop the bleeding without cutting off the patient&rsquo;s blood supply.</p>
<p>Revoke compromised credentials immediately. Isolate affected systems from the network, but preserve evidence while you do it. Cut off active data exfiltration paths. If malware is involved, isolate the system and keep the sample for analysis.</p>
<p>Every containment action has a tradeoff. Revoking all tokens is safe but might take down production. Isolating a server preserves evidence but removes capacity. These are real decisions with real business impact. Make them intentionally, communicate the impact, and document why you chose what you chose.</p>
<p>The engineer at my startup who revoked every token in the system wasn&rsquo;t wrong to act fast. He was wrong to act alone, without telling anyone, during an active incident. By the time I realized what had happened, our payment integration was down and we were fielding angry support tickets alongside the security investigation. Two fires instead of one.</p>
<h3 id="investigation-slow-down-before-you-speed-up">Investigation: slow down before you speed up</h3>
<p>Once the immediate bleeding stops, resist the urge to fix everything at once. Investigation is about building a timeline and understanding the blast radius.</p>
<p>Pull logs across authentication, application, and network layers. Preserve forensic images before you start making changes. Build a timeline of attacker activity. Collect indicators of compromise so you can sweep for related access you might have missed.</p>
<p>Don&rsquo;t rush to restore service before you understand how the attacker got in. I&rsquo;ve seen startups bring compromised systems back online with the same vulnerability still open because they were in a hurry to get back to normal. That&rsquo;s a second incident waiting to happen.</p>
<h3 id="eradication-and-recovery">Eradication and recovery</h3>
<p>Eradication means removing every foothold. Close the vulnerability that allowed the initial access. Remove any malware or backdoors. Reset credentials that could have been compromised. Verify that no unauthorized accounts were created.</p>
<p>Attackers who know what they are doing leave multiple persistence mechanisms. A revoked token isn&rsquo;t enough if they also dropped an SSH key or created a service account. Validate your cleanup thoroughly.</p>
<p>Recovery means rebuilding from known-good sources when possible. Restore data from clean backups if needed. Bring systems back gradually and watch closely for signs of re-entry. The monitoring should be tighter after an incident, not looser.</p>
<h3 id="communication-during-a-crisis">Communication during a crisis</h3>
<p>Internal communication should be regular, calm, and honest. Keep leadership aligned on severity and business risk. Inform affected teams. Separate confirmed facts from open questions in every update. Nothing erodes trust faster than a retraction.</p>
<p>External communication should be coordinated with legal counsel, especially if customer data is involved. Move quickly, but don&rsquo;t speculate. Explain what happened, what data was affected, what you&rsquo;re doing about it, and what customers should do. If you&rsquo;re in fintech or health, map your regulatory notification requirements now. Not during the incident. Now.</p>
<p>At the shared mobility startup, we got this part right almost by accident. Our CEO was a former lawyer and insisted on reviewing every external statement. It slowed us down by an hour. It saved us from saying something incorrect that would have required a correction, which always looks worse than a slight delay.</p>
<h3 id="documentation-as-you-go">Documentation as you go</h3>
<p>Write it down in real time. Decisions, actions, evidence, timestamps. You will need this record for compliance, for legal protection, and for learning. It also prevents confusion when responders rotate or when you need to brief someone new.</p>
<p>Use a shared document. A Google Doc is fine. A dedicated incident management tool is better. The format matters less than the habit. If you aren&rsquo;t documenting during the incident, you won&rsquo;t remember accurately afterward. Memory under stress is unreliable.</p>
<h3 id="preparation-that-costs-almost-nothing">Preparation that costs almost nothing</h3>
<p><strong>Know your critical assets.</strong> What systems hold sensitive data? What access paths could cause maximum damage? If you can&rsquo;t answer these questions in under a minute, you aren&rsquo;t ready.</p>
<p><strong>Build detection that actually works.</strong>  <a href="/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/"
   
   >Centralized logs</a>
. Alerts on unusual patterns. At minimum, you should know when someone authenticates from a new location or when an API sees traffic outside normal bounds. This doesn&rsquo;t require expensive tooling. It requires attention.</p>
<p><strong>Write draft communication templates.</strong> Under pressure, writing coherent messages is harder than you think. Have templates for internal updates, customer notifications, and regulatory disclosures. Fill in the specifics during the incident, but don&rsquo;t start from a blank page.</p>
<p><strong>Run tabletop exercises.</strong> Pick a plausible scenario: stolen laptop, exposed database, compromised former employee credentials. Walk through the response as a team. These discussions take an hour and reveal gaps that would cost you days during a real incident.</p>
<p>The former employee scenario isn&rsquo;t hypothetical. It&rsquo;s what hit us. If we had run that exercise once, someone would have asked whether our offboarding process actually revoked all access. We would have checked. We would have found the gap. The incident would never have happened.</p>
<h3 id="cheap-insurance">Cheap insurance</h3>
<p>Incident response planning is the cheapest insurance a startup can buy. A simple playbook, three named roles, and one tabletop exercise per quarter will put you ahead of most companies your size.</p>
<p>You will still have incidents. The goal is to respond with discipline instead of panic, contain the damage instead of amplifying it, and come out the other side with a team that trusts each other more.</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>Building a DevOps Culture from Scratch</title><link>https://lawzava.com/blog/2016-03-10-building-devops-culture-from-scratch/</link><pubDate>Thu, 10 Mar 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-03-10-building-devops-culture-from-scratch/</guid><description>Hiring a DevOps engineer won&amp;amp;rsquo;t end the dev vs ops fight. Put developers on the pager, pilot with one team, and fix incentives before tools.</description><content:encoded><![CDATA[<p>I keep seeing the same pattern. A company posts a job listing for a &ldquo;DevOps engineer,&rdquo; hires someone smart, hands them a Jira board, and waits for magic. Six months later the deploy pipeline is marginally better but the teams still don&rsquo;t trust each other. The hire burns out. Everyone blames the tools.</p>
<p>DevOps isn&rsquo;t a role. It isn&rsquo;t Jenkins. It isn&rsquo;t even a set of practices you can copy from a blog post. It&rsquo;s a cultural shift where the people who build software and the people who run it share the same goals, the same pain, and the same wins. Fix the incentives that make dev and ops fight each other first. Tools come second.</p>
<p>I learned this the hard way at a mobility startup.</p>
<h2 id="what-happened-at-the-mobility-startup">What Happened at the Mobility Startup</h2>
<p>When I joined, we had a classic split. Three backend engineers shipping features as fast as possible. One ops person, call him Max, who kept the lights on. The engineers would push code on Friday afternoon, Max would get paged at 2 a.m. Saturday, and Monday morning would start with a blame session disguised as a standup.</p>
<p>Max was good at his job. The engineers were good at theirs. The system between them was broken.</p>
<p>The fix wasn&rsquo;t a tool. It was a conversation. I put the entire team in a room and said: starting next week, the person who ships the code carries the pager for it. Not forever, just for the first 48 hours after deploy. If your code wakes someone up, that someone is you.</p>
<p>The reaction was roughly what you would expect. The engineers hated it. Max loved it. But within two weeks, something shifted. Deploys got smaller. Engineers started writing  <a href="/blog/2016-02-08-docker-in-production-lessons-from-running-containers-at-scale/"
   
   >health checks</a>
 without being asked. Someone added a runbook for the payment service, unprompted. The Friday afternoon deploys stopped entirely.</p>
<p>Nobody told them to do any of that. The incentive structure changed, and the behavior followed.</p>
<h2 id="start-with-shared-pain-not-shared-tools">Start With Shared Pain, Not Shared Tools</h2>
<p>That experience taught me something I keep coming back to: if you want DevOps culture, start by making everyone feel the consequences of their decisions.</p>
<p>Developers who never see production alerts write code that&rsquo;s hard to operate. Ops people who never sit in planning meetings treat every deploy as a threat. The gap is empathy.</p>
<p>The cheapest thing you can do is cross-pollinate. Have a developer shadow on-call for a week. Have the ops person sit in sprint planning. Run a postmortem together after the next incident and focus on the system, not the person. I&rsquo;ve never seen a team do this and not come out with a better understanding of each other.</p>
<h2 id="pick-a-pilot-team-and-let-them-own-it">Pick a Pilot Team and Let Them Own It</h2>
<p>Don&rsquo;t try to transform the whole company at once. Pick one team. Give them ownership of their deploy pipeline and their on-call. Let them figure out what works. Measure what happens.</p>
<p>At the mobility startup, that pilot was the team running the vehicle availability service. Small blast radius, clear metrics, leadership willing to experiment. They went from deploying once a week to deploying daily within a month. Not because I told them to, but because shorter cycles meant smaller changes meant fewer pages.</p>
<p>The rest of the engineering org watched this happen. Nobody had to sell them on DevOps after that. They just asked when they could start.</p>
<h2 id="the-technical-stuff-matters-but-it-isnt-first">The Technical Stuff Matters, But It Isn&rsquo;t First</h2>
<p>Once the culture is moving, you need the technical foundations to support it. Infrastructure as code so environments are reproducible. A CI pipeline so every commit gets tested. Monitoring that answers &ldquo;is this thing working&rdquo; without requiring tribal knowledge.</p>
<p>In 2016, that means Ansible or Chef for config management, Jenkins or CircleCI for builds, and something like the ELK stack or Graphite for observability. The specific tools matter less than the principle: everything should be automated, version-controlled, and visible to the whole team.</p>
<p>I&rsquo;ve seen teams with perfect pipelines and terrible culture. The pipeline doesn&rsquo;t fix trust. It just makes deploys faster. If the team still throws code over the wall and blames each other when it breaks, a faster pipeline just accelerates the dysfunction.</p>
<h2 id="traps-ive-walked-into">Traps I&rsquo;ve Walked Into</h2>
<p><strong>The hero trap.</strong> At the mobility startup, Max was the hero. He could fix anything at 3 a.m. That felt like a strength until he took a week off and nobody knew how to restart the payment gateway. Heroes are a single point of failure wearing a cape. Spread the knowledge.</p>
<p><strong>Tool obsession.</strong> We spent two weeks evaluating orchestration platforms when the real problem was that nobody knew what was running in production. A shared spreadsheet would have been more useful than Kubernetes at that point. Solve the problem you actually have.</p>
<p><strong>Burnout disguised as ownership.</strong> If you give developers pager duty without reducing their feature load, you aren&rsquo;t building DevOps culture. You&rsquo;re just distributing the suffering more evenly. Shared responsibility means shared capacity planning too.</p>
<h2 id="its-a-slow-game">It&rsquo;s a Slow Game</h2>
<p>DevOps culture doesn&rsquo;t happen in a quarter. At the mobility startup, it took about six months before the dev-vs-ops tension actually went away. The team went from monthly releases with weekend war rooms to daily deploys that nobody thought twice about. But it was gradual, messy, and full of setbacks.</p>
<p>The wins compound though. Smaller deploys mean fewer incidents. Fewer incidents mean less firefighting. Less firefighting means more time for the work that actually matters. Once that flywheel starts turning, it&rsquo;s hard to stop.</p>
<p>If you&rsquo;re starting from zero, that&rsquo;s fine. Most teams are. Start with the incentives, not the tools. Make people feel what production actually looks like. Pick a small team and let them prove the model. The culture will follow the work.</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>