<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Fatih Koç</title>
    <description>The latest articles on DEV Community by Fatih Koç (@fatihkoc).</description>
    <link>https://dev.to/fatihkoc</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3507198%2Fce4ce2a6-38b6-406c-a45e-ae6f9225f701.jpeg</url>
      <title>DEV Community: Fatih Koç</title>
      <link>https://dev.to/fatihkoc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fatihkoc"/>
    <language>en</language>
    <item>
      <title>From Signals to Reliability: SLOs, Runbooks and Post-Mortems</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Sun, 02 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/from-signals-to-reliability-slos-runbooks-and-post-mortems-1gg5</link>
      <guid>https://dev.to/fatihkoc/from-signals-to-reliability-slos-runbooks-and-post-mortems-1gg5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;All configuration examples, templates and alert rules are in the &lt;a href="https://github.com/fatihkc/kubernetes-observability" rel="noopener noreferrer"&gt;kubernetes-observability&lt;/a&gt; repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can build perfect observability infrastructure. Deploy &lt;a href="https://dev.to/posts/opentelemetry-kubernetes-pipeline/"&gt;unified OpenTelemetry pipelines&lt;/a&gt;, add &lt;a href="https://dev.to/posts/kubernetes-security-observability/"&gt;security telemetry&lt;/a&gt;, implement &lt;a href="https://dev.to/posts/ebpf-parca-observability/"&gt;continuous profiling&lt;/a&gt;. Instrument every service. Collect every metric, log and trace. Build beautiful Grafana dashboards.&lt;/p&gt;

&lt;p&gt;And still struggle during incidents.&lt;/p&gt;

&lt;p&gt;The missing piece isn’t technical. It’s organizational. When alerts fire during incidents, your team needs to answer four questions instantly: How severe is this? What actions should we take? Who needs to be involved? When is this resolved?&lt;/p&gt;

&lt;p&gt;Without Service Level Objectives, severity becomes subjective. Different engineers will have different opinions about whether a 5% error rate is acceptable or catastrophic. Without runbooks, incident response becomes improvisation. Each engineer follows their own mental model, leading to inconsistent outcomes. Without structured post-mortems, teams fix symptoms but miss root causes, hitting the same issues repeatedly.&lt;/p&gt;

&lt;p&gt;The gap between observability and reliability isn’t about collecting more data. It’s about giving teams the frameworks to act on that data systematically. SLOs define shared understanding of what “working” means. Runbooks codify collective knowledge about remediation. Post-mortems create organizational learning from failures.&lt;/p&gt;

&lt;p&gt;This post focuses on the human systems that turn observability into reliability. You’ll see how to define SLOs that drive decisions, build runbooks that scale team knowledge, structure post-mortems that generate improvements and embed these practices into engineering culture without adding bureaucracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why observability alone doesn’t prevent incidents
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://dev.to/posts/opentelemetry-kubernetes-pipeline/"&gt;OpenTelemetry pipeline post&lt;/a&gt; showed how to unify metrics, logs and traces. The &lt;a href="https://dev.to/posts/kubernetes-security-observability/"&gt;security observability post&lt;/a&gt; added audit logs and runtime detection. The &lt;a href="https://dev.to/posts/ebpf-parca-observability/"&gt;profiling post&lt;/a&gt; covered performance optimization. You have visibility into everything.&lt;/p&gt;

&lt;p&gt;But visibility doesn’t equal reliability.&lt;/p&gt;

&lt;p&gt;Consider a payment service processing transactions. Your observability stack shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request rate: 1,200 req/sec&lt;/li&gt;
&lt;li&gt;Error rate: 2.3%&lt;/li&gt;
&lt;li&gt;P99 latency: 450ms&lt;/li&gt;
&lt;li&gt;CPU: 65%&lt;/li&gt;
&lt;li&gt;Active database connections: 180&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is this good or bad? Without defined objectives, you’re guessing. Some teams would panic at 2.3% errors. Others wouldn’t wake up an engineer until it hit 15%. The decision becomes political instead of systematic.&lt;/p&gt;

&lt;p&gt;Even worse, when alerts fire, engineers are left improvising. The alert says “high latency” but doesn’t tell you whether to restart pods, scale horizontally, check the database, or roll back the last deployment. Every incident becomes a research project.&lt;/p&gt;

&lt;p&gt;And without structured retrospectives, you fix the immediate problem but miss the systemic causes. The database connection pool was too small. Configuration changes don’t require approval. Deployment rollbacks aren’t automated. You’ll hit similar issues repeatedly because you’re not learning.&lt;/p&gt;

&lt;p&gt;SLOs, runbooks and post-mortems solve these problems. They transform observability from passive data collection into active reliability improvement. I’ve watched teams cut their mean time to resolution by 60% within three months of implementing these practices, not because they collected more data but because they knew how to act on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service Level Indicators define what actually matters
&lt;/h2&gt;

&lt;p&gt;Service Level Indicators are the specific metrics that measure user-facing reliability. Not internal metrics like CPU or memory. Not infrastructure metrics like pod count. User-facing behavior that customers actually experience.&lt;/p&gt;

&lt;p&gt;The four golden signals provide a starting framework: latency (how fast), traffic (how much demand), errors (how many failures) and saturation (how full your critical resources are—CPU, memory, thread/connection pools, queue depth, disk/network I/O). These apply to almost any service, but you need to make them concrete for your specific workload.&lt;/p&gt;

&lt;p&gt;For a REST API, your SLIs might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt; : Percentage of requests that return 2xx or 3xx status codes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; : 99th percentile response time for successful requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Throughput&lt;/strong&gt; : Requests per second the service can handle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a data pipeline, your SLIs are different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Freshness&lt;/strong&gt; : Time between data generation and availability in the warehouse&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correctness&lt;/strong&gt; : Percentage of records processed without data quality errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completeness&lt;/strong&gt; : Percentage of expected source records present in output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is measuring what users experience, not what infrastructure does. Users don’t care if your pods are using 80% CPU. They care whether their checkout succeeded and how long it took.&lt;/p&gt;

&lt;p&gt;Implement availability SLI using data from your OpenTelemetry pipeline. If you set &lt;code&gt;namespace: traces.spanmetrics&lt;/code&gt; as above, the span-metrics will be available as &lt;code&gt;traces_spanmetrics_*&lt;/code&gt; in Prometheus. If you use a different namespace, adjust the metric names accordingly. Example query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Availability SLI: percentage of successful requests
sum(rate(traces_spanmetrics_calls_total{
  service_name="checkout-service",
  status_code=~"2..|3.."
}[5m]))
/
sum(rate(traces_spanmetrics_calls_total{
  service_name="checkout-service"
}[5m]))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For latency, use histogram quantiles from your instrumented request duration metrics. With &lt;code&gt;namespace: traces.spanmetrics&lt;/code&gt;, the duration histogram is exposed as &lt;code&gt;traces_spanmetrics_duration_bucket&lt;/code&gt; with accompanying &lt;code&gt;_sum&lt;/code&gt; and &lt;code&gt;_count&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Latency SLI: 99th percentile response time
histogram_quantile(
  0.99,
  sum by (le) (
    rate(traces_spanmetrics_duration_bucket{
      service_name="checkout-service",
      status_code=~"2.."
    }[5m])
  )
)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OpenTelemetry Collector’s spanmetrics connector automatically generates these metrics from traces (older releases exposed this as a processor). You instrument once, get both detailed traces for debugging and aggregated metrics for SLOs. Metric names depend on the connector &lt;code&gt;namespace&lt;/code&gt; you set, and dots are converted to underscores in Prometheus (e.g., &lt;code&gt;traces.spanmetrics&lt;/code&gt; → &lt;code&gt;traces_spanmetrics&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Example configuration aligning metric names used below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connectors:
  spanmetrics:
    namespace: traces.spanmetrics

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [spanmetrics]
    metrics:
      receivers: [spanmetrics]
      exporters: [prometheusremotewrite]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don’t try to create SLIs for everything. Start with 2-3 indicators for your most critical user journeys. For an e-commerce platform, that’s probably browse products, add to cart and complete checkout. Each journey gets availability and latency SLIs. That’s six total. Manageable.&lt;/p&gt;

&lt;p&gt;Avoid vanity metrics disguised as SLIs. “Average response time” is a terrible SLI because it hides outliers. One request taking 30 seconds while 99 others take 100ms averages to 400ms, which looks fine but represents a terrible user experience. Use percentiles instead. P50, P95, P99.&lt;/p&gt;

&lt;p&gt;Also avoid internal metrics that don’t map to user experience. “Kafka consumer lag” isn’t an SLI unless you can translate it into user impact. If lag means users see stale data, then “data freshness” is your SLI. Measure the user-facing symptom, not the internal cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service Level Objectives turn metrics into reliability targets
&lt;/h2&gt;

&lt;p&gt;SLOs are the targets you set for your SLIs. “99.9% of requests will succeed” or “99% of requests will complete in under 500ms.” These targets become the contract between your service and its users.&lt;/p&gt;

&lt;p&gt;The right SLO balances user expectations with engineering cost. Setting a 99.99% availability target sounds great until you realize it allows only 4.38 minutes of downtime per month. Achieving that requires redundancy, automation and operational overhead that might not be worth it for an internal tool.&lt;/p&gt;

&lt;p&gt;The process for setting SLOs is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Measure current performance for 2-4 weeks&lt;/li&gt;
&lt;li&gt;Identify what users actually need (talk to them)&lt;/li&gt;
&lt;li&gt;Set objectives slightly better than current but achievable&lt;/li&gt;
&lt;li&gt;Iterate based on error budget consumption&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example approach for setting SLOs:&lt;/p&gt;

&lt;p&gt;If current performance shows 99.7% availability and P99 latency of 800ms, but user research indicates that occasional slowness is acceptable while failures are not, you might set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt; : 99.5% of requests succeed (more conservative than current, providing error budget)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; : 99% of requests complete in under 1000ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These SLOs translate to quantifiable budgets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0.5% error budget = 14.4 hours of downtime per month&lt;/li&gt;
&lt;li&gt;1% of requests can exceed latency target&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates clear decision guardrails. When burning error budget faster than expected, teams slow feature releases and focus on reliability. With remaining error budget, teams can take calculated risks on innovation.&lt;/p&gt;

&lt;p&gt;Implement SLOs as Prometheus recording rules. This pre-computes the SLI values and makes dashboards faster. The latency example below assumes spanmetrics duration unit is milliseconds and a 1s threshold (le=“1000”):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;groups:
- name: checkout-service-slo
  interval: 30s
  rules:
  # Availability SLI
  - record: sli:availability:ratio_rate5m
    expr: |
      sum(rate(traces_spanmetrics_calls_total{
        service_name="checkout-service",
        status_code=~"2..|3.."
      }[5m]))
      /
      sum(rate(traces_spanmetrics_calls_total{
        service_name="checkout-service"
      }[5m]))      

  # Latency SLI (percentage of requests under threshold)
  - record: sli:latency:ratio_rate5m
    expr: |
      sum(rate(traces_spanmetrics_duration_bucket{
        service_name="checkout-service",
        le="1000"
      }[5m]))
      /
      sum(rate(traces_spanmetrics_duration_count{
        service_name="checkout-service"
      }[5m]))      

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a Grafana dashboard that shows SLO compliance over time. Add a gauge showing current error budget remaining. When error budget drops below 20%, make it red. This gives teams a visual indicator of risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error budgets as reliability currency
&lt;/h2&gt;

&lt;p&gt;Error budgets flip the reliability conversation. Instead of “we need 100% uptime” (impossible), you get “we have a budget for failures, spend it on innovation instead of panic.”&lt;/p&gt;

&lt;p&gt;If your SLO is 99.5% availability over 30 days, your error budget is 0.5% of requests. At 1M requests per day, that’s 5,000 failed requests per day or 150,000 per month. Every actual failure reduces your remaining budget.&lt;/p&gt;

&lt;p&gt;Calculate error budget burn rate to catch problems before you exhaust your budget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Current error rate vs error budget rate
# If this exceeds 1.0, you're burning budget faster than planned
(1 - sli:availability:ratio_rate5m) / (1 - 0.995)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A burn rate of 1.0 means you’re consuming error budget at exactly the rate your SLO allows. A burn rate of 10 means you’ll exhaust your monthly budget in 3 days at current failure rates. A burn rate of 0.5 means you have headroom.&lt;/p&gt;

&lt;p&gt;Multi-window, multi-burn-rate alerting prevents both false positives and slow detection. The Google SRE workbook recommends alerting on two conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fast burn (2% budget consumed in 1 hour) means page immediately&lt;/li&gt;
&lt;li&gt;Slow burn (5% budget consumed in 6 hours) means ticket for investigation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s the alert rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;groups:
- name: checkout-service-slo-alerts
  rules:
  # Fast burn: 14.4x burn rate over 1 hour AND 2 minutes
  - alert: CheckoutServiceErrorBudgetFastBurn
    expr: |
      (1 - sli:availability:ratio_rate5m{service_name="checkout-service"}) / (1 - 0.995) &amp;gt; 14.4
      and
      (1 - sli:availability:ratio_rate5m{service_name="checkout-service"}) / (1 - 0.995) &amp;gt; 14.4 offset 2m      
    for: 2m
    labels:
      severity: critical
      team: payments
    annotations:
      summary: "Checkout service burning error budget at 14.4x rate"
      description: "At current error rate, monthly error budget will be exhausted in {{ $value | humanizeDuration }}. Current availability: {{ $labels.availability }}%"
      runbook_url: "https://runbooks.internal/payments/checkout-error-budget-burn"

  # Slow burn: 6x burn rate over 6 hours
  - alert: CheckoutServiceErrorBudgetSlowBurn
    expr: |
      (1 - avg_over_time(sli:availability:ratio_rate5m{service_name="checkout-service"}[6h])) / (1 - 0.995) &amp;gt; 6      
    for: 15m
    labels:
      severity: warning
      team: payments
    annotations:
      summary: "Checkout service burning error budget at 6x rate"
      description: "Error budget consumption is elevated. Review recent changes."
      runbook_url: "https://runbooks.internal/payments/checkout-error-budget-burn"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These thresholds balance false positives against detection time. Fast burn alerts catch severe outages immediately. Slow burn alerts catch gradual degradation before it exhausts your budget.&lt;/p&gt;

&lt;p&gt;Error budgets also drive policy decisions. Many teams implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Green&lt;/strong&gt; (&amp;gt;75% budget remaining): Ship features freely, take risks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yellow&lt;/strong&gt; (25-75% remaining): Review change requests, prefer low-risk improvements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red&lt;/strong&gt; (&amp;lt;25% remaining): Feature freeze, focus on reliability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This policy is enforced through engineering process, not tooling. When your dashboard shows 18% error budget remaining, the team lead knows to defer that risky refactor until next month. I’ve seen this framework completely change the product-engineering dynamic. Instead of arguing about whether a release is “too risky,” teams look at the error budget dashboard and make data-driven decisions in under five minutes.&lt;/p&gt;

&lt;p&gt;Error budgets tell you when to act. Runbooks tell you how to act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runbooks transform alerts into action
&lt;/h2&gt;

&lt;p&gt;Runbooks transform alerts from “something is broken” into “here’s exactly what to do.” Every alert should link to a runbook. No exceptions.&lt;/p&gt;

&lt;p&gt;The runbook structure should be consistent across all services:&lt;/p&gt;

&lt;h3&gt;
  
  
  Runbook Template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# [Service Name] - [Alert Name]

## Summary
One-sentence description of what this alert means and user-facing impact.

## Severity
Critical / Warning / Info

## Diagnosis
1. Check current SLO status: [Grafana dashboard link]
2. Review recent traces: {service_name="checkout-service", status="error"}
3. Correlate with deployments: [ArgoCD/Flux dashboard link]
4. Review metrics: [Grafana dashboard link]

## Mitigation
1. Rollback: `kubectl rollout undo deployment/checkout-service -n production`
2. Scale up: `kubectl scale deployment/checkout-service --replicas=10 -n production`
3. Disable feature: `kubectl set env deployment/checkout-service FEATURE_X=false`

## Escalation
- On-call engineer (15 min) → Service owner → Incident commander → Executive
- Contact: [PagerDuty link]

## Investigation
After mitigation: Check traces, [profiling data](/posts/ebpf-parca-observability/), [audit logs](/posts/kubernetes-security-observability/) and database logs.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This template connects directly to your observability stack. The diagnosis section uses the &lt;a href="https://dev.to/posts/opentelemetry-kubernetes-pipeline/"&gt;unified OpenTelemetry pipeline&lt;/a&gt; to correlate signals. The investigation section references &lt;a href="https://dev.to/posts/ebpf-parca-observability/"&gt;profiling&lt;/a&gt; and &lt;a href="https://dev.to/posts/kubernetes-security-observability/"&gt;security observability&lt;/a&gt; when needed.&lt;/p&gt;

&lt;p&gt;Runbooks live in Git alongside your service code. Treat them as code: version controlled, peer reviewed, tested. When you deploy a new feature, update the runbook. When an incident reveals a gap, file a PR to improve it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting runbooks to alerts
&lt;/h3&gt;

&lt;p&gt;Remember the &lt;code&gt;runbook.url&lt;/code&gt; annotation we added to the OpenTelemetry pipeline? Now it pays off. Your alerts automatically include runbook links:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;annotations:
  summary: "{{ $labels.service_name }} error rate above threshold"
  description: "Current error rate: {{ $value }}%. SLO allows 0.5%."
  runbook_url: '{{ $labels.runbook_url }}'
  dashboard: 'https://grafana.internal/d/service-overview?var-service={{ $labels.service_name }}'
  traces: 'https://grafana.internal/explore?queries=[{"datasource":"tempo","query":"{{ $labels.service_name }}","queryType":"traceql"}]'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the PagerDuty notification arrives, it contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What’s broken (service name, error rate)&lt;/li&gt;
&lt;li&gt;Current vs expected (SLO threshold)&lt;/li&gt;
&lt;li&gt;Where to look (dashboard link, trace query)&lt;/li&gt;
&lt;li&gt;What to do (runbook link)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The on-call engineer clicks the runbook link and follows the steps. No guessing. No Slack archaeology trying to remember what worked last time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Post-mortems drive learning from failures
&lt;/h2&gt;

&lt;p&gt;Post-mortems (also called incident retrospectives or after-action reviews) turn incidents into systemic improvements. The goal isn’t to blame individuals. It’s to identify process, tooling, or architecture gaps that let the incident happen.&lt;/p&gt;

&lt;p&gt;The key principle is &lt;strong&gt;blameless culture&lt;/strong&gt;. You assume people made reasonable decisions given the information available at the time. If someone deployed broken code, the question isn’t “why did they do that?” It’s “why didn’t our testing catch it?” and “why could they deploy to production without review?”&lt;/p&gt;

&lt;h3&gt;
  
  
  Post-Mortem Template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Incident: [Brief description]

**Date** : [Date] | **Duration** : [Duration] | **Severity** : [Critical/High/Medium]  
**Commander** : [Name] | **Responders** : [Names]

## Impact
- Users affected: [Number/Percentage]
- Revenue impact: [Amount if applicable]
- SLO impact: [Error budget consumed]

## Timeline
Key events: Alert fired → Investigation began → Discovery → Mitigation → Recovery → Resolution

## Root Cause
What changed, why it caused the problem, why safeguards didn't prevent it.

## What Went Well / Poorly
**Well** : Fast detection, effective collaboration, good tooling use  
**Poorly** : Missing alerts, unclear ownership, inadequate testing, manual processes

## Action Items
| Action | Owner | Priority | Due Date | Status |
|--------|-------|----------|----------|--------|
| [Specific improvement] | [Team] | P0-P2 | [Date] | [Status] |

**P0** : Prevents similar incidents | **P1** : Improves detection/mitigation | **P2** : Nice to have

## Lessons Learned
System-level insights, team practice changes, observability gaps identified.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post-mortems happen within 48 hours of incident resolution while details are fresh. Schedule a 60-minute meeting with all responders plus relevant stakeholders. Use the template to guide discussion.&lt;/p&gt;

&lt;p&gt;The action items section is critical. These must have owners, due dates and tracking. Follow up in sprint planning to ensure they’re prioritized. Otherwise post-mortems become theater where everyone nods, writes “we should monitor better” and changes nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Common Post-Mortem Anti-Patterns
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Blame disguised as process&lt;/strong&gt; : “Should have known better” is wrong (ask why the system allowed it). &lt;strong&gt;Vague action items&lt;/strong&gt; : “Improve monitoring” is useless (be specific with dates and metrics). &lt;strong&gt;No follow-through&lt;/strong&gt; : Make action items sprint backlog priorities. &lt;strong&gt;Learning in silos&lt;/strong&gt; : Share post-mortems across engineering. &lt;strong&gt;Incident theater&lt;/strong&gt; : If you’re not implementing action items, stop writing post-mortems (you’re wasting time).&lt;/p&gt;

&lt;p&gt;A common anti-pattern: teams mark post-mortem action items “complete” yet only patch immediate symptoms, leaving systemic fixes undone. That’s not learning. That’s paperwork.&lt;/p&gt;

&lt;h3&gt;
  
  
  External resources and real incident reports
&lt;/h3&gt;

&lt;p&gt;Strengthen SEO and give readers practical references with these authoritative links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google SRE Workbook&lt;/strong&gt; : guidance on blameless post-mortems and error budgets — &lt;a href="https://sre.google/workbook/" rel="noopener noreferrer"&gt;sre.google/workbook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atlassian Incident Management&lt;/strong&gt; : incident handbook and templates — &lt;a href="https://www.atlassian.com/incident-management" rel="noopener noreferrer"&gt;atlassian.com/incident-management&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PagerDuty Incident Response Guide&lt;/strong&gt; : practical postmortem guidance — &lt;a href="https://response.pagerduty.com/" rel="noopener noreferrer"&gt;response.pagerduty.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab incidents (public)&lt;/strong&gt;: searchable incident issues and retrospectives — &lt;a href="https://gitlab.com/gitlab-com/gl-infra/production/-/issues?label_name%5B%5D=incident" rel="noopener noreferrer"&gt;gitlab.com/gitlab-com/gl-infra/production/-/issues?label_name[]=incident&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare incidents&lt;/strong&gt; : engineering blog write-ups — &lt;a href="https://blog.cloudflare.com/tag/outage/" rel="noopener noreferrer"&gt;blog.cloudflare.com/tag/outage&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub engineering&lt;/strong&gt; : reliability and incident engineering posts — &lt;a href="https://github.blog/engineering/" rel="noopener noreferrer"&gt;github.blog/engineering&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP incident history&lt;/strong&gt; : cloud provider public incident reports — &lt;a href="https://status.cloud.google.com/" rel="noopener noreferrer"&gt;status.cloud.google.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure status history&lt;/strong&gt; : historical incidents — &lt;a href="https://azure.status.microsoft/en-us/status/history/" rel="noopener noreferrer"&gt;azure.status.microsoft/en-us/status/history&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmortem Library (ilert)&lt;/strong&gt;: curated real-world incidents — &lt;a href="https://www.ilert.com/postmortems" rel="noopener noreferrer"&gt;ilert.com/postmortems&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How to find other companies’ incident reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search engineering blogs for tags like “incident”, “postmortem”, or “outage” (e.g., &lt;code&gt;site:company.com/engineering incident&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Check public status pages for “history” or “post-incident” sections.&lt;/li&gt;
&lt;li&gt;Look in product/infra repos for labels like &lt;code&gt;incident&lt;/code&gt;, &lt;code&gt;postmortem&lt;/code&gt;, or &lt;code&gt;root-cause&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building a reliability culture that sticks
&lt;/h2&gt;

&lt;p&gt;SLOs, runbooks and post-mortems fail when they’re mandated top-down without team buy-in. You need to embed these practices into daily work, not layer them on as bureaucracy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adoption strategy
&lt;/h3&gt;

&lt;p&gt;Pick your most critical user journey. Define 2-3 SLIs, set SLOs, build the dashboard. When the next incident hits, the team will see immediate value (clear error budget impact, guided response via runbook, concrete improvements from post-mortem). Success with one service creates organic demand. Teams resist until they watch another team resolve an incident in 15 minutes with clear runbooks and error budget data.&lt;/p&gt;

&lt;p&gt;Don’t create a central “reliability team” that owns all SLOs and runbooks (it doesn’t scale). Provide templates (Prometheus recording rules, Grafana dashboards, runbook and post-mortem templates) and let service teams customize for their needs. Payments cares about transaction success rate, search cares about result freshness. Same framework, different metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tie it to incentives
&lt;/h3&gt;

&lt;p&gt;What gets measured gets managed. If your promotion criteria include “delivered 5 features” but not “maintained 99.9% SLO,” engineers will optimize for features.&lt;/p&gt;

&lt;p&gt;Include reliability metrics in team goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintain SLO compliance (&amp;gt;95% of time periods meet target)&lt;/li&gt;
&lt;li&gt;Zero incidents without runbooks&lt;/li&gt;
&lt;li&gt;All critical incidents get post-mortems within 48 hours&lt;/li&gt;
&lt;li&gt;Action items from post-mortems completed within 30 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Celebrate reliability wins like you celebrate feature launches. When a team goes three months without exhausting error budget, that’s worth recognition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrate with existing workflows
&lt;/h3&gt;

&lt;p&gt;Embed reliability into existing processes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sprint planning&lt;/strong&gt; : Review error budget consumption. &lt;strong&gt;Stand-ups&lt;/strong&gt; : Report SLO status alongside feature progress. &lt;strong&gt;Code reviews&lt;/strong&gt; : Check for instrumentation and runbook updates. &lt;strong&gt;Retrospectives&lt;/strong&gt; : Use post-mortem template for incidents that impacted SLOs.&lt;/p&gt;

&lt;p&gt;Assign one engineer per team as “observability champion,” the point person for defining SLIs/SLOs, keeping runbooks updated, facilitating post-mortems and sharing practices. Champions meet monthly to share patterns and standardize tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Psychological safety as foundation
&lt;/h3&gt;

&lt;p&gt;None of this works without psychological safety. Blameless culture means focusing accountability on systems and processes, not individuals. When broken code deploys, ask “Why didn’t our CI catch this?” not “Why didn’t you test properly?” When someone makes a poor architectural decision, ask “What information would have helped?” Leaders set the tone: “what can we learn?” not “who screwed up?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing it all together
&lt;/h2&gt;

&lt;p&gt;Observability without reliability practices is data for data’s sake. Reliability practices without observability are guesswork. Together, they transform reactive firefighting into proactive reliability engineering.&lt;/p&gt;

&lt;p&gt;These practices create organizational capabilities beyond tooling. &lt;strong&gt;Shared understanding&lt;/strong&gt; of reliability (SLO compliance, error budgets), &lt;strong&gt;collective knowledge&lt;/strong&gt; about remediation (runbooks that scale), &lt;strong&gt;correlated signals&lt;/strong&gt; from unified observability (metrics, logs and traces) and &lt;strong&gt;systematic learning&lt;/strong&gt; from failures (post-mortems that drive improvement).&lt;/p&gt;

&lt;p&gt;Start small. One service, two SLIs, three runbooks. Prove value, then scale. The 60% MTTR improvements, the 5-minute risk decisions, the prevention of repeat incidents aren’t aspirational. They’re achievable within months of adopting these practices. The infrastructure is ready. Now turn signals into reliability through human systems and team practices.&lt;/p&gt;

</description>
      <category>sre</category>
      <category>observability</category>
      <category>runbook</category>
      <category>devops</category>
    </item>
    <item>
      <title>eBPF Observability and Continuous Profiling with Parca</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Mon, 27 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/ebpf-observability-and-continuous-profiling-with-parca-1k8c</link>
      <guid>https://dev.to/fatihkoc/ebpf-observability-and-continuous-profiling-with-parca-1k8c</guid>
      <description>&lt;p&gt;Your monitoring shows CPU hovering at 80%. Prometheus metrics tell you which pods are consuming resources. Your &lt;a href="https://dev.to/posts/opentelemetry-kubernetes-pipeline/"&gt;OpenTelemetry pipeline&lt;/a&gt; connects traces to logs. Grafana dashboards show the symptoms. But you still can’t answer the most basic question during an incident: which function in your code is actually burning the CPU?&lt;/p&gt;

&lt;p&gt;This is the instrumentation tax. You can add more metrics, more logs, more traces. But unless you instrument every function with custom spans (which no one does), you’re still guessing. You grep through code looking for suspects. You deploy experimental fixes and hope CPU drops. You waste hours when the answer should take seconds.&lt;/p&gt;

&lt;p&gt;eBPF profiling changes this. It samples stack traces directly from the kernel without touching your application code. No SDK. No recompilation. No deployment changes. You get CPU and memory profiles showing exactly which functions consume resources, across any language, in production, with negligible overhead.&lt;/p&gt;

&lt;p&gt;I’m focusing on Parca in this post because continuous profiling is the missing piece in the observability story so far. We covered &lt;a href="https://dev.to/posts/opentelemetry-kubernetes-pipeline/"&gt;metrics and traces&lt;/a&gt; and &lt;a href="https://dev.to/posts/kubernetes-security-observability/"&gt;security observability&lt;/a&gt;. Profiling fills the performance optimization gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What eBPF actually solves
&lt;/h2&gt;

&lt;p&gt;eBPF lets you run sandboxed programs in the Linux kernel without changing kernel source code or loading modules. For observability, this means you can hook into system calls, network events and CPU scheduling to collect telemetry automatically.&lt;/p&gt;

&lt;p&gt;There are three major categories of eBPF observability tools. Cilium and Hubble provide network flow visibility. I covered this in the &lt;a href="https://dev.to/posts/kubernetes-security-observability/"&gt;security observability post&lt;/a&gt; when discussing network policy enforcement and detecting lateral movement. Pixie offers automatic distributed tracing by capturing application-level protocols (HTTP, gRPC, DNS) directly from kernel network buffers. You get traces without adding OpenTelemetry SDKs to your code.&lt;/p&gt;

&lt;p&gt;But here’s what neither of those do: tell you which functions inside your application are the bottlenecks.&lt;/p&gt;

&lt;p&gt;Parca is continuous profiling. It samples stack traces at regular intervals (19 times per second per logical CPU) and aggregates them into flamegraphs. When CPU spikes, you open the flamegraph and see the exact function call hierarchy consuming cycles. Not “this service is slow,” but “json.Marshal in the checkout handler is taking 73% of CPU time because someone passed a 50MB payload.”&lt;/p&gt;

&lt;p&gt;Understanding the difference between traces and profiles is important. Traces show request flow across services with timing and context. They’re great for understanding “why is this specific request slow?” Profiles show aggregate behavior over time. They answer “what is my application spending CPU on overall?” You need both. Traces for debugging individual requests. Profiles for optimization and cost reduction.&lt;/p&gt;

&lt;p&gt;What eBPF profiling doesn’t give you is business context. It can’t tell you which team owns the hot code path or what the downstream impact is. It won’t correlate profiles with user-facing SLOs. That’s still OpenTelemetry’s job. eBPF collects the low-level truth. OTel normalizes and correlates it with the rest of your observability stack.&lt;/p&gt;

&lt;p&gt;This is why I don’t buy the “eBPF replaces instrumentation” narrative. It extends it. You still need explicit instrumentation for ownership metadata, trace correlation and custom business metrics. eBPF gives you the system-level data you can’t easily instrument yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous profiling without the overhead
&lt;/h2&gt;

&lt;p&gt;Prometheus scrapes metrics every 15-30 seconds and stores time series. Parca samples stack traces 19 times per second per logical CPU and aggregates them into profiles stored as time series of stack samples. The mental model is similar but the data is different.&lt;/p&gt;

&lt;p&gt;When you query Prometheus, you get a number over time. CPU percentage. Request rate. Error count. When you query Parca, you get a flamegraph showing which functions were on the stack during that time window. The width of each box in the flamegraph represents how much CPU time that function consumed relative to everything else.&lt;/p&gt;

&lt;p&gt;The sampling overhead is low when properly configured, though actual resource consumption varies significantly based on your workload, number of cores and configuration settings. Compare that to APM agents doing full tracing on every request, which typically add higher overhead depending on the tool and configuration. The reason profiling is cheap is it’s statistical. Missing a few samples doesn’t matter. Over time, patterns emerge even with this sampling approach.&lt;/p&gt;

&lt;p&gt;In production, you run Parca as a DaemonSet. Each agent samples its node’s processes using eBPF, then forwards aggregated profiles to a central Parca server. The server stores them and exposes an API for querying. Grafana can display Parca profiles directly, or you use Parca’s own UI.&lt;/p&gt;

&lt;p&gt;Integration with OpenTelemetry comes in three flavors. The simplest is running parallel stacks. Parca for profiling, OTel for everything else. You manually cross-reference when debugging. Not ideal but it works.&lt;/p&gt;

&lt;p&gt;Better is the Prometheus bridge. Parca can export summary metrics like “top 10 functions by CPU” as Prometheus metrics. Your OpenTelemetry Collector scrapes them alongside everything else. Now your unified metrics backend includes profiling data, even if the flamegraphs still live in Parca’s UI. You can build Grafana dashboards that show CPU metrics from Prometheus next to top functions from Parca, with links to drill into full profiles.&lt;/p&gt;

&lt;p&gt;The future path is the &lt;a href="https://opentelemetry.io/blog/2024/state-profiling/" rel="noopener noreferrer"&gt;OpenTelemetry Profiling SIG&lt;/a&gt;. They’re working on standardizing profile data in OTLP, the same protocol that carries metrics, logs and traces today. When that’s ready, Parca and other profilers will export profiles directly to OTel Collectors, and you’ll have true unified pipelines. This started as experimental work in 2024, and while the direction is clear, full production readiness is still evolving.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1hal2ce7iwiqubfrle88.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1hal2ce7iwiqubfrle88.webp" alt="Parca Architecture and Integration" width="800" height="539"&gt;&lt;/a&gt;&lt;em&gt;Parca deployment architecture showing DaemonSet agents, central server, and integration options&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you adopt continuous profiling now
&lt;/h2&gt;

&lt;p&gt;Most teams adopt profiling too early. They hear “low overhead visibility” and deploy it cluster-wide before they’ve fixed basic observability gaps. Then they have flamegraphs nobody looks at because alerts still don’t link to runbooks and logs aren’t correlated with traces.&lt;/p&gt;

&lt;p&gt;If you haven’t built the foundation from the earlier posts in this series, profiling won’t save you. Fix metrics, logs and trace correlation first. Profiling is an optimization tool, not a debugging tool for mysteries.&lt;/p&gt;

&lt;p&gt;That said, some teams need it immediately. If compute costs are a major line item and you’re looking for optimization targets, profiling pays for itself fast. The typical pattern is finding unexpected bottlenecks in libraries you assumed were optimized. JSON serialization, regex matching, logging formatters - these often consume 30-40% of CPU without anyone noticing. Once identified, switching implementations or caching results can cut node counts significantly.&lt;/p&gt;

&lt;p&gt;You should adopt profiling if you have recurring performance incidents where the root cause is unclear, especially in polyglot environments where instrumenting every service consistently is hard. eBPF works across languages. A Go service and a Python service produce comparable profiles. You don’t need language-specific APM agents.&lt;/p&gt;

&lt;p&gt;Continuous profiling also helps with noisy neighbor problems in multi-tenant clusters. When a pod starts consuming unexpected CPU, profiles show whether it’s legitimate workload growth or runaway code. This is particularly useful for catching infinite loops in production that would take hours to debug with logs alone.&lt;/p&gt;

&lt;p&gt;Wait if your team is small and you’re still building observability basics. Profiling adds another system to maintain. The Parca server needs storage. Retention policies need planning. Someone has to own triage workflows when profiles show hotspots. If you don’t have SRE capacity for this, delay.&lt;/p&gt;

&lt;p&gt;Skip profiling entirely if you’re running serverless or frontend-heavy workloads where compute cost isn’t significant. Also skip if your organization has strict eBPF policies. Some security teams block eBPF entirely due to the kernel-level access it requires. You’ll need to make the case for CAP_BPF and CAP_PERFMON capabilities before deploying.&lt;/p&gt;

&lt;p&gt;Managed Kubernetes makes this easier. Most modern &lt;a href="https://aws.amazon.com/blogs/containers/empowering-kubernetes-observability-with-ebpf-on-amazon-eks/" rel="noopener noreferrer"&gt;node images support eBPF&lt;/a&gt;. EKS, GKE and AKS all work with Parca as long as you’re running recent kernel versions (5.10 or newer recommended, 4.19 minimum). Test in a dev cluster first because older node groups might have restrictions.&lt;/p&gt;

&lt;p&gt;The retention question matters for cost planning. Profiling data is smaller than traces but not trivial. A large production cluster generates gigabytes of profile data daily. Most teams keep 30-90 days. Parca supports object storage backends (S3, GCS) so older data can be archived cheaply. Budget accordingly and set lifecycle policies early.&lt;/p&gt;

&lt;p&gt;Who owns profiling outcomes? If SREs look at profiles and file tickets for service teams to optimize their code, adoption fails. Service teams need direct access to profiles for their own namespaces. Build dashboards that show “your service’s top CPU functions this week” and make it self-service. Optimization becomes part of the normal development cycle instead of a special SRE project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually get from profiling
&lt;/h2&gt;

&lt;p&gt;Here’s what the process typically looks like. When you first add Parca to a production cluster, profiles show expected patterns. Most CPU goes to business logic, some to JSON parsing, some to database client libraries. Nothing shocking.&lt;/p&gt;

&lt;p&gt;The value comes when you filter by the most expensive services measured by node hours per week. Common findings include checkout services burning CPU in logging libraries that pretty-print JSON on every request. Inventory services with caching layers doing more work than just hitting the database. Search services running regex matching in loops that should be precompiled.&lt;/p&gt;

&lt;p&gt;Fixing these issues typically yields 20-40% CPU reductions per service. When applied across a cluster, total CPU utilization drops enough to justify downsizing node pools. At scale, even modest optimizations translate to thousands in monthly savings.&lt;/p&gt;

&lt;p&gt;The ROI on profiling isn’t always that dramatic but it’s usually positive. Even small optimizations add up when you multiply by request volume. A function that takes 10ms instead of 15ms doesn’t sound impressive until you realize it runs 10 million times a day.&lt;/p&gt;

&lt;p&gt;Secondary benefits are harder to quantify but real. HPA oscillation decreases when services have smoother CPU profiles. You get fewer false-positive CPU alerts because you can filter out expected spikes (like scheduled batch jobs). Root cause analysis for performance incidents gets faster when you can jump straight to profiles instead of inferring from metrics.&lt;/p&gt;

&lt;p&gt;But here’s where teams screw it up. They deploy Parca, look at flamegraphs during incidents, then do nothing with the information. Profiles become “nice visualizations” that nobody acts on. You need ownership.&lt;/p&gt;

&lt;p&gt;I recommend tagging services in Grafana with the team annotation (same one you added to the OpenTelemetry pipeline in the earlier post). Build a weekly report that shows each team’s top CPU-consuming functions. Make it visible. Some organizations add “optimize one hotspot per quarter” to team goals. That’s heavy-handed but it works.&lt;/p&gt;

&lt;p&gt;Another mistake is enabling profiling for everything. Start with your most expensive services by compute cost. Profile those for 2-4 weeks. Find and fix the top 3 hotspots. Measure the impact. Then expand to more services. Treat profiling as a targeted optimization tool, not a passive monitoring layer.&lt;/p&gt;

&lt;p&gt;Don’t expect profiling to replace distributed tracing. Some teams mistakenly think “we have flamegraphs now, we don’t need traces.” Wrong. Traces show request flow and timing across services. Profiles show where each service spends CPU. A slow request might have a fast profile (it’s waiting on I/O). A fast request might have an expensive profile (it’s CPU-bound but the overall latency is fine). Use both.&lt;/p&gt;

&lt;p&gt;If you’re comparing tools, Pyroscope (now part of Grafana after &lt;a href="https://techcrunch.com/2023/03/15/grafana-acquires-pyroscope-and-merges-it-with-its-phlare-continuous-profiling-database/" rel="noopener noreferrer"&gt;their March 2023 acquisition&lt;/a&gt;) is the other major continuous profiler. &lt;a href="https://www.parca.dev/" rel="noopener noreferrer"&gt;Parca&lt;/a&gt; and &lt;a href="https://grafana.com/oss/pyroscope/" rel="noopener noreferrer"&gt;Grafana Pyroscope&lt;/a&gt; are similar in capability. Parca has stronger eBPF support and remains fully open-source with a cleaner Prometheus integration. Grafana Pyroscope has better multi-tenancy, alerting, and native Grafana Cloud integration. Try both and pick based on your workflow. The profiling concepts are the same.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Parca&lt;/th&gt;
&lt;th&gt;Grafana Pyroscope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;eBPF Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native, first-class&lt;/td&gt;
&lt;td&gt;Via agent integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Self-hosted only&lt;/td&gt;
&lt;td&gt;Self-hosted + managed (Grafana Cloud)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prometheus Integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native metrics export&lt;/td&gt;
&lt;td&gt;Via Grafana integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Alerting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Via external tools&lt;/td&gt;
&lt;td&gt;Built-in alerting rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI/Visualization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standalone + Grafana&lt;/td&gt;
&lt;td&gt;Native Grafana integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Object storage (S3, GCS)&lt;/td&gt;
&lt;td&gt;Object storage + Grafana Cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Kubernetes-first, open-source preference&lt;/td&gt;
&lt;td&gt;Existing Grafana stack users&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  AI tools for performance optimization
&lt;/h2&gt;

&lt;p&gt;The intersection of profiling and AI isn’t just about reading flamegraphs. AI coding assistants like GitHub Copilot, Cursor, and Sourcegraph Cody can suggest more efficient implementations when you’re fixing hotspots. Point them at an expensive function from your profile, and they’ll propose alternatives using faster algorithms, better data structures or optimized libraries.&lt;/p&gt;

&lt;p&gt;Static analysis tools enhanced with AI can now correlate their findings with runtime profiling data. Tools like Snyk Code and SonarQube are starting to flag performance issues not just based on code patterns but on actual resource consumption in production. When a function appears in profiles as expensive, these tools surface it in code review with severity weighted by real impact.&lt;/p&gt;

&lt;p&gt;Cost modeling improves when you combine profiles with infrastructure spending. If a service consumes $800/month in compute and profiling shows 40% of that goes to one function, you know optimizing that function could save $320/month. Multiply across services and you have a prioritized optimization roadmap driven by actual financial impact. Some FinOps platforms are building this correlation automatically.&lt;/p&gt;

&lt;p&gt;Automated performance testing tools like k6 and Gatling now integrate with profilers. Run load tests, collect profiles during the test and AI models flag performance regressions by comparing profiles across commits. This catches optimizations that accidentally got rolled back or new code paths that are unexpectedly expensive before they hit production.&lt;/p&gt;

&lt;p&gt;For incident response, LLMs help with flamegraph interpretation. Feed a profile into current models like GPT-5, Claude Sonnet and ask “what’s expensive here?” You get a natural language summary pointing to hotspots with context. Faster than training every engineer to read flamegraphs fluently, though you should verify the analysis against the raw data.&lt;/p&gt;

&lt;p&gt;ChatGPT Atlas, OpenAI’s new Chromium-based browser with integrated AI, takes this further. When you see an expensive third-party library function in profiles, Atlas can research it in agent mode - pulling documentation, known issues, and optimization guides automatically while you continue analyzing. The browser memory feature learns from your profiling workflows, so it starts recognizing patterns specific to your stack over time. This turns debugging sessions from manual research into assisted investigation.&lt;/p&gt;

&lt;p&gt;Adaptive profiling is coming. Instead of sampling all services equally, the profiler learns which services have variable performance and increases sampling there. Services that run stable profiles get sampled less. You get better visibility where it matters while keeping overall overhead low. The &lt;a href="https://ebpf.foundation/" rel="noopener noreferrer"&gt;eBPF Foundation&lt;/a&gt; is driving standardization of these advanced profiling techniques across the ecosystem.&lt;/p&gt;

&lt;p&gt;Natural language queries across observability data are improving. “Show me traces and logs related to the high CPU in the payments service” should surface correlated data across metrics, logs, traces and profiles. We’re not quite there yet but the tooling is converging. When this works reliably, debugging shifts from manual correlation to asking questions.&lt;/p&gt;

&lt;p&gt;What’s not ready is fully autonomous optimization. AI can suggest fixes based on profile changes, but it can’t understand your business logic or deployment history. It doesn’t know that the hotspot in your service is expected because you just onboarded a major customer. Human judgment still matters. AI proposes, engineers decide.&lt;/p&gt;

&lt;p&gt;Guardrails are critical for any AI integration. Redact sensitive symbols or function names before sending profiles to external LLMs. Use self-hosted models or VPC endpoints when possible. Control costs by summarizing first and only sending deltas for analysis. Keep humans in the loop for approval. Log decisions for compliance.&lt;/p&gt;

&lt;p&gt;The future vision is eBPF collects unbiased performance data, OpenTelemetry normalizes and correlates it and AI layers on top to spot patterns and point to runbooks. Full production-grade maturity is still 1-2 years out. But the pieces are coming together.&lt;/p&gt;

&lt;p&gt;For now, the practical approach is deploying Parca, integrating it with your existing OTel stack via the Prometheus bridge and building workflows where profiles surface during incidents. Start with manual analysis. Add anomaly detection once you understand normal patterns. Experiment with LLM summaries but verify outputs.&lt;/p&gt;

&lt;p&gt;Profiling is where your observability stack shifts from reactive (what broke?) to proactive (how do we optimize before it breaks?). Combined with the unified pipeline from the OpenTelemetry post and the security telemetry from the audit logs post, you’re building something close to full visibility.&lt;/p&gt;

&lt;p&gt;The next step is turning all this visibility into faster incident resolution. In the next post, I’ll cover SLOs, runbooks and the operational practices that close the loop from signal to action. Because collecting data is the easy part. Using it to ship faster and break less is the hard part.&lt;/p&gt;

</description>
      <category>ebpf</category>
      <category>parca</category>
      <category>observability</category>
      <category>devops</category>
    </item>
    <item>
      <title>Security Observability in Kubernetes Goes Beyond Logs</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Fri, 24 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/security-observability-in-kubernetes-goes-beyond-logs-6kd</link>
      <guid>https://dev.to/fatihkoc/security-observability-in-kubernetes-goes-beyond-logs-6kd</guid>
      <description>&lt;p&gt;Most Kubernetes security tools tell you about vulnerabilities before deployment. Many can detect what’s happening during an attack, but they work in isolation without the correlation needed to piece together the full story.&lt;/p&gt;

&lt;p&gt;The typical security stack includes vulnerability scanners, Pod Security Standards, network policies, and runtime detection tools. But when a real incident occurs, teams often struggle to understand what happened because each tool generates signals in isolation. Audit logs show API calls. Falco catches suspicious behavior. Prometheus exposes metrics you can use to spot network spikes. But these signals live in different systems with different timestamps and zero shared context.&lt;/p&gt;

&lt;p&gt;The problem is correlation.&lt;/p&gt;

&lt;p&gt;That’s what observability is about.&lt;/p&gt;

&lt;p&gt;Security observability is different from application observability. You’re not debugging slow queries or memory leaks. You’re answering “Did someone just try to escalate privileges?” and “Which pods are making unexpected API calls?” in real time. This requires audit logs, runtime behavior detection, network flow analysis, and the ability to correlate security events with application traces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What security observability actually means
&lt;/h2&gt;

&lt;p&gt;Security observability means you can answer these questions in under 60 seconds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which pods accessed secrets in the last hour?&lt;/li&gt;
&lt;li&gt;Did any container spawn an unexpected shell process?&lt;/li&gt;
&lt;li&gt;What API calls did this suspicious service account make?&lt;/li&gt;
&lt;li&gt;Which workloads are communicating outside their expected network boundaries?&lt;/li&gt;
&lt;li&gt;Can I trace this security event back to the specific user request that triggered it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security observability gives you investigation superpowers through correlation and context.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/posts/opentelemetry-kubernetes-pipeline/"&gt;OpenTelemetry pipeline post&lt;/a&gt;, I showed how to unify metrics, logs, and traces. This post extends that foundation to security signals. You’ll get Kubernetes audit logs flowing through your observability pipeline, runtime security events from Falco correlated with traces and security metrics that let you spot attacks as they happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes audit logs are underutilized
&lt;/h2&gt;

&lt;p&gt;Kubernetes audit logs record every API server request. User authentication, pod creation, secret access, RBAC decisions, admission webhook results. Everything that touches the API server gets logged. Most teams either disable audit logs (which is insane from a security standpoint) or dump them to S3 where they’re essentially useless during an active incident. You can’t correlate S3 logs with live application traces when you need answers in under a minute.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/" rel="noopener noreferrer"&gt;Kubernetes audit documentation&lt;/a&gt; provides comprehensive guidance on audit logging configuration and best practices.&lt;/p&gt;

&lt;p&gt;Here’s what you’re missing when audit logs aren’t in your observability platform:&lt;/p&gt;

&lt;p&gt;A service account suddenly starts listing secrets across all namespaces. A user creates a pod with &lt;code&gt;hostPath&lt;/code&gt; mounts in production. Someone deletes a critical ConfigMap. An unknown source IP hits the API server with repeated authentication failures. Without audit logs in a queryable backend with correlation to application telemetry, you’re investigating these incidents with &lt;code&gt;kubectl&lt;/code&gt; and guessing.&lt;/p&gt;

&lt;p&gt;I’ve seen teams spend 30 minutes trying to figure out who deleted a deployment. With audit logs in Loki and correlated by user/namespace/timestamp, it takes 15 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring useful audit logs
&lt;/h3&gt;

&lt;p&gt;The default audit policy logs everything at the RequestResponse level, which means you’ll capture full request and response bodies for every API call. This generates gigabytes per day in any moderately active cluster and most of it is noise.&lt;/p&gt;

&lt;p&gt;You want a policy that captures security-relevant events at appropriate detail levels while dropping low value spam like constant healthcheck requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Don't log read-only requests to common resources
  - level: None
    verbs: ["get", "list", "watch"]
    resources:
      - group: ""
        resources: ["pods", "pods/status", "nodes", "nodes/status"]

  # Log secret access with metadata (who, when, which secret)
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets", "configmaps"]

  # Log RBAC changes with full request details
  - level: RequestResponse
    verbs: ["create", "update", "patch", "delete"]
    resources:
      - group: "rbac.authorization.k8s.io"
        resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]

  # Log pod create/delete with request body (captures specs)
  - level: Request
    verbs: ["create", "delete"]
    resources:
      - group: ""
        resources: ["pods"]

  # Catch privilege escalations and authentication failures
  - level: RequestResponse
    omitStages: ["RequestReceived"]
    users: ["system:anonymous"]

  # Default: log metadata for everything else
  - level: Metadata
    omitStages: ["RequestReceived"]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy logs secret access, RBAC changes, pod mutations, and authentication anomalies while dropping noisy read-only requests. It cuts audit volume by 70-80% compared to logging everything.&lt;/p&gt;

&lt;p&gt;For managed Kubernetes (EKS, GKE, AKS), audit logs are available through cloud provider logging services. For self-managed clusters, you’ll configure the API server flags in the shipping options below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping audit logs to your observability pipeline
&lt;/h2&gt;

&lt;p&gt;Audit logs are structured JSON that you can send to any OTLP receiver or log aggregator. Choose the approach that fits your cluster setup:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: File-based logging with Fluent Bit (simplest)
&lt;/h3&gt;

&lt;p&gt;For most setups, file-based audit logging with Fluent Bit is simpler than running a webhook server. Configure the API server to write audit logs to a file, then use Fluent Bit (already running as a DaemonSet in most observability setups) to tail and forward them.&lt;/p&gt;

&lt;p&gt;Configure API server for file-based logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--audit-policy-file=/etc/kubernetes/audit-policy.yaml
--audit-log-path=/var/log/kubernetes/audit/audit.log
--audit-log-format=json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure Fluent Bit to parse and forward audit logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  namespace: observability
data:
  parsers.conf: |
    [PARSER]
        Name k8s-audit
        Format json
        Time_Key requestReceivedTimestamp
        Time_Format %Y-%m-%dT%H:%M:%S.%LZ    

  fluent-bit.conf: |
    [INPUT]
        Name tail
        Path /var/log/kubernetes/audit/*.log
        Parser k8s-audit
        Tag k8s.audit
        Refresh_Interval 5
        Mem_Buf_Limit 50MB
        Skip_Long_Lines On

    [FILTER]
        Name modify
        Match k8s.audit
        Add service.name kubernetes-audit
        Add signal.type security

    [OUTPUT]
        Name forward
        Match k8s.audit
        Host otel-gateway.observability.svc.cluster.local
        Port 24224
        Require_ack_response true    

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires mounting &lt;code&gt;/var/log/kubernetes/audit/&lt;/code&gt; from the host into the Fluent Bit DaemonSet pods. No additional services needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; : The OpenTelemetry Collector receiving these logs must have the &lt;code&gt;fluentforward&lt;/code&gt; receiver enabled on port 24224 (shown in the correlation section below).&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Cloud provider native audit logs (for managed Kubernetes)
&lt;/h3&gt;

&lt;p&gt;For managed Kubernetes, use the cloud provider’s native audit log integration. EKS sends audit logs to CloudWatch Logs, GKE to Cloud Logging, and AKS to Azure Monitor. Forward them to your observability backend using the OpenTelemetry Collector’s cloud-specific receivers (CloudWatch, Google Cloud Logging, Azure Monitor) or cloud-native export mechanisms like Pub/Sub or Event Hubs. This approach avoids running additional infrastructure but creates vendor lock-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Webhook backend (for specific use cases)
&lt;/h3&gt;

&lt;p&gt;Webhooks allow the API server to send audit events to an HTTP endpoint in real time. Use this only if you need custom transformation logic before forwarding to OpenTelemetry. Deploy a simple HTTP service that receives audit event batches from the API server and forwards them to your OTLP endpoint. Configure the API server with &lt;code&gt;--audit-webhook-config-file&lt;/code&gt; pointing to your webhook. Most teams are better served by Option 1 or Option 2. Webhooks add operational complexity without clear benefits for typical use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which option should you choose?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-managed clusters (kubeadm, kops, etc.):&lt;/strong&gt; Use &lt;strong&gt;Option 1&lt;/strong&gt; (Fluent Bit). You already have file access and likely run Fluent Bit for application logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed Kubernetes with existing observability stack:&lt;/strong&gt; Use &lt;strong&gt;Option 2&lt;/strong&gt; (cloud provider native). Simplest integration with no additional infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed Kubernetes requiring vendor neutrality:&lt;/strong&gt; Use &lt;strong&gt;Option 3&lt;/strong&gt; (webhook) only if you need real-time streaming and can’t use &lt;strong&gt;Option 1&lt;/strong&gt; (file-based logging).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need custom enrichment or transformation:&lt;/strong&gt; Use &lt;strong&gt;Option 3&lt;/strong&gt; (webhook) to add custom logic before forwarding to OpenTelemetry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Falco catches what static scans miss
&lt;/h2&gt;

&lt;p&gt;Falco is a CNCF runtime security tool that watches system calls (via eBPF or kernel module) and triggers alerts on suspicious behavior. Shell spawned in a container. Sensitive file access. Unexpected network connections. Privilege escalation attempts. These are behavioral signals that only appear at runtime. Vulnerability scanners won’t catch these behaviors because they only happen during execution, and Falco is purpose-built to detect them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Falco with OpenTelemetry export
&lt;/h3&gt;

&lt;p&gt;Falco can export alerts to syslog, HTTP endpoints, or gRPC. You want alerts flowing into your observability pipeline as structured logs with correlation context.&lt;/p&gt;

&lt;p&gt;Install Falco with Helm and configure JSON output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update

helm install falco falcosecurity/falco \
  --namespace falco \
  --create-namespace \
  --set tty=true \
  --set driver.kind=modern_ebpf \
  --set falco.json_output=true \
  --set falco.file_output.enabled=true \
  --set falco.file_output.filename=/var/run/falco/events.log \
  --set falco.file_output.keep_alive=false

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure Fluent Bit (or the OpenTelemetry Filelog Receiver) to tail Falco’s output and forward to your observability backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INPUT]
    Name tail
    Path /var/run/falco/events.log
    Parser json
    Tag falco.events
    Refresh_Interval 5

[FILTER]
    Name modify
    Match falco.events
    Add service.name falco
    Add signal.type security

[OUTPUT]
    Name forward
    Match falco.events
    Host otel-gateway.observability.svc.cluster.local
    Port 24224

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Falco will now send alerts as JSON logs. Each alert includes pod name, namespace, process details, and the rule that triggered.&lt;/p&gt;

&lt;p&gt;Tuning Falco rules is critical. Out of the box, you’ll get alerts for legitimate admin activity. Create a custom rules file to suppress expected behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- rule: Terminal shell in container
  desc: A shell was spawned in a container
  condition: &amp;gt;
    spawned_process and container and 
    shell_procs and proc.tty != 0 and 
    not user_known_terminal_shell_activity    
  output: &amp;gt;
    Shell spawned in container (user=%user.name container=%container.name 
    shell=%proc.name parent=%proc.pname cmdline=%proc.cmdline)    
  priority: WARNING

- macro: user_known_terminal_shell_activity
  condition: &amp;gt;
    (container.image.repository = "my-debug-image") or
    (k8s.ns.name = "development" and user.name = "admin@example.com")    

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This custom rule allows shells in debug images and development namespaces while alerting on everything else. Start with WARNING priority, review alerts weekly, and gradually tighten rules as you understand normal behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correlating security events with application traces
&lt;/h2&gt;

&lt;p&gt;Correlation requires shared context: &lt;code&gt;trace_id&lt;/code&gt;, &lt;code&gt;namespace&lt;/code&gt;, &lt;code&gt;pod&lt;/code&gt;, &lt;code&gt;service.name&lt;/code&gt;. When your application logs include trace IDs and your security logs (audit + Falco) include the same pod/namespace/service metadata, you can navigate from a suspicious API call to the exact request that caused it.&lt;/p&gt;

&lt;p&gt;Here’s the key insight: use the OpenTelemetry Collector’s &lt;code&gt;k8sattributes&lt;/code&gt; processor to enrich all signals with Kubernetes metadata, then ensure applications inject trace context into every log line.&lt;/p&gt;

&lt;p&gt;The gateway Collector config from the &lt;a href="https://dev.to/posts/opentelemetry-kubernetes-pipeline/"&gt;OpenTelemetry post&lt;/a&gt; already has &lt;code&gt;k8sattributes&lt;/code&gt; enrichment. Extend it to process security logs. The &lt;a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/k8sattributesprocessor" rel="noopener noreferrer"&gt;k8sattributes processor documentation&lt;/a&gt; provides detailed configuration options and performance considerations.&lt;/p&gt;

&lt;p&gt;Ensure the OpenTelemetry Collector has RBAC permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: ServiceAccount
metadata:
  name: otel-collector
  namespace: observability
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: otel-collector
rules:
- apiGroups: [""]
  resources: ["pods", "namespaces", "nodes"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["replicasets", "deployments"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: otel-collector
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: otel-collector
subjects:
- kind: ServiceAccount
  name: otel-collector
  namespace: observability

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure the k8sattributes processor and ensure the Collector receives Fluent Bit’s forward output on port 24224:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;receivers:
  otlp:
    protocols: {grpc: {}, http: {}}
  fluentforward:
    endpoint: 0.0.0.0:24224

processors:
  k8sattributes:
    auth_type: serviceAccount
    extract:
      metadata: [k8s.namespace.name, k8s.pod.name, k8s.deployment.name, k8s.node.name]
      annotations:
        - tag_name: team
          key: team
          from: pod
      labels:
        - tag_name: app
          key: app
          from: pod

  # Add security-specific attributes
  attributes/security:
    actions:
      - key: signal.type
        value: security
        action: insert
      - key: is_security_event
        value: true
        action: insert

service:
  pipelines:
    logs:
      receivers: [fluentforward, otlp]
      processors: [k8sattributes, attributes/security, batch]
      exporters: [loki]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every security log (audit events, Falco alerts) gets enriched with pod name, namespace, deployment, and custom labels. When you query Loki for security events, you can filter by &lt;code&gt;k8s.namespace.name="production"&lt;/code&gt; and &lt;code&gt;signal.type="security"&lt;/code&gt; to see only production security logs.&lt;/p&gt;

&lt;p&gt;With K8s metadata enrichment in place, you can now correlate security events with application traces. When suspicious behavior occurs, jump directly from the Falco alert to the trace showing the full request context.&lt;/p&gt;

&lt;p&gt;This requires injecting &lt;code&gt;trace_id&lt;/code&gt; into application logs. Not all security events will have trace IDs (e.g., direct &lt;code&gt;kubectl&lt;/code&gt; commands), but application-triggered events should.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a security observability dashboard
&lt;/h2&gt;

&lt;p&gt;Raw logs and traces are useful for investigation, but you need high-level dashboards that show security posture and alert on anomalies.&lt;/p&gt;

&lt;p&gt;Here’s what makes sense in a Grafana security dashboard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit log metrics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API request rate by user/namespace/verb&lt;/li&gt;
&lt;li&gt;Failed authentication attempts over time&lt;/li&gt;
&lt;li&gt;Secret access events (who accessed which secrets)&lt;/li&gt;
&lt;li&gt;RBAC change events (role bindings created/deleted)&lt;/li&gt;
&lt;li&gt;Pod creation events with privileged specs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Falco metrics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alert rate by priority (INFO/WARNING/CRITICAL)&lt;/li&gt;
&lt;li&gt;Top triggered rules&lt;/li&gt;
&lt;li&gt;Alert count by pod/namespace&lt;/li&gt;
&lt;li&gt;Shell spawn events in production namespaces&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Correlation panel:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recent security events with links to traces&lt;/li&gt;
&lt;li&gt;Anomaly detection (API request rates outside normal range)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use Loki queries to extract metrics from security logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Count secret access events per user
count_over_time({service_name="kubernetes-audit"} | json | objectRef_resource="secrets" [5m]) by (user_username)

# Count Falco alerts by priority
count_over_time({service_name="falco"} | json | priority="CRITICAL" [5m])

# Failed authentication attempts
count_over_time({service_name="kubernetes-audit"} | json | verb="create" | responseStatus_code &amp;gt;= 400 [5m])

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a correlation panel that shows recent security events with drill-down links. When an alert fires, you should be able to click through to the audit log, see the associated Falco alert if any, and jump to the application trace if the event came from an app request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retention policies for security logs
&lt;/h3&gt;

&lt;p&gt;Security logs have different retention requirements than application logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audit logs&lt;/strong&gt; : 1 year minimum for most compliance frameworks. PCI-DSS requires 1 year with the last 90 days immediately available. HIPAA requires 6 years for documentation, which many organizations apply to audit logs as well. These are your legal and compliance record of who did what and when.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falco alerts&lt;/strong&gt; : 30-90 days is typical. You need enough history to investigate incidents and establish baseline behavior patterns, but runtime alerts are less critical for compliance than audit logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network flows&lt;/strong&gt; : 7-30 days given the massive volume. Keep longer retention only for compliance-required namespaces or use sampling to reduce volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider tiered storage in Loki: recent data (last 7 days) in fast storage for active investigation, older data in object storage for compliance queries. Set up log lifecycle policies to automatically expire logs based on these retention requirements. Budget for storage accordingly—audit logs and network flows can easily reach terabytes per year in production clusters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operationalizing security observability
&lt;/h2&gt;

&lt;p&gt;Security observability fails when it becomes another tool nobody checks. You need to integrate it into on-call workflows and incident response runbooks.&lt;/p&gt;

&lt;p&gt;Here are approaches that work well:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Include security signals in standard dashboards.&lt;/strong&gt; Don’t isolate security metrics in a separate dashboard that only the security team sees. Add a “Security Events” panel to the main application dashboard. When developers see their service triggering Falco alerts, they investigate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automate correlation in alerts.&lt;/strong&gt; When a Falco alert fires, include the pod name and namespace in the alert. Add a link directly to the Loki query that shows related audit logs. Include the Grafana Explore URL with pre-filled filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make security logs accessible to developers.&lt;/strong&gt; Grant read access to audit logs and Falco alerts in Loki. Developers should be able to query “Which pods in my namespace accessed secrets today?” without filing a ticket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test your setup with attack simulations.&lt;/strong&gt; Simulate privilege escalation and container escape attempts in a test environment. Verify that your dashboards show the activity and alerts fire. This builds confidence and identifies gaps before real incidents happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending to network security observability
&lt;/h2&gt;

&lt;p&gt;Audit logs and runtime alerts cover control plane and process behavior. But network traffic is another attack vector. Unexpected egress traffic, lateral movement between pods, data exfiltration attempts. You need network flow visibility.&lt;/p&gt;

&lt;p&gt;Kubernetes Network Policies define allowed traffic, but they don’t give you observability into actual traffic. You need flow logs.&lt;/p&gt;

&lt;p&gt;Tools like Cilium (with Hubble) or Calico (with flow logs) export network flow data. These can feed into your observability pipeline as metrics or logs.&lt;/p&gt;

&lt;p&gt;Cilium Hubble exposes flow logs to files, which you can then forward to your observability pipeline. The &lt;a href="https://docs.cilium.io/en/stable/observability/hubble/" rel="noopener noreferrer"&gt;Cilium Hubble documentation&lt;/a&gt; covers flow export configuration and filtering options. Configure Hubble to export flows to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm upgrade cilium cilium/cilium \
  --namespace kube-system \
  --set hubble.enabled=true \
  --set hubble.export.static.enabled=true \
  --set hubble.export.static.filePath=/var/run/cilium/hubble/events.log \
  --set "hubble.export.static.fieldMask={time,source.namespace,source.pod_name,destination.namespace,destination.pod_name,verdict,l4,IP}"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure Fluent Bit to tail the Hubble flow logs and forward them to OpenTelemetry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INPUT]
    Name tail
    Path /var/run/cilium/hubble/events.log
    Parser json
    Tag hubble.flows
    Refresh_Interval 5
    Mem_Buf_Limit 50MB

[FILTER]
    Name modify
    Match hubble.flows
    Add service.name cilium-hubble
    Add signal.type network

[OUTPUT]
    Name forward
    Match hubble.flows
    Host otel-gateway.observability.svc.cluster.local
    Port 24224

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Network flows include source/dest pod, namespace, ports, protocols, verdict (allowed/denied). You can build dashboards showing denied connections (potential policy violations), unexpected egress destinations (possible data exfiltration), and high-volume pod-to-pod traffic (lateral movement).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warning about volume&lt;/strong&gt; : Network flow logging generates massive data volume. A large production cluster can produce tens to hundreds of gigabytes of flow logs daily, depending on workload patterns. Every TCP connection, every DNS query, every service-to-service call creates a flow record.&lt;/p&gt;

&lt;p&gt;Use aggressive filtering with Hubble’s &lt;code&gt;allowList&lt;/code&gt; and &lt;code&gt;denyList&lt;/code&gt; to focus on security-relevant flows (denied connections, external egress, cross-namespace traffic) and exclude high-volume internal service mesh traffic. Consider sampling for non-compliance workloads.&lt;/p&gt;

&lt;p&gt;For most teams, enabling flow logging selectively for production namespaces or during incident investigation is more practical than continuous full-cluster flow capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can actually build with this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Investigation speed increases dramatically.&lt;/strong&gt; Tracking down who modified a ClusterRole binding with kubectl and grep can take anywhere from minutes to hours, depending on what logs you have. With audit logs in Loki filtered by &lt;code&gt;{service_name="kubernetes-audit"} | json | objectRef_resource="clusterrolebindings" | verb="update"&lt;/code&gt;, you get the answer in seconds. User name, timestamp, source IP, the exact change. Done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See the full attack chain.&lt;/strong&gt; Audit logs show the API calls (listing secrets, creating pods). Falco catches the shell spawn. You see the complete sequence of events instead of isolated alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance audits get faster.&lt;/strong&gt; “Show me all secret access in Q3 for PCI-scoped namespaces” can take hours of manual reconstruction if logs are scattered across different systems. With Loki, it’s a single query with CSV export. Done in minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert fatigue reduces when you have context.&lt;/strong&gt; A Falco alert fires for shell activity in production. Is it an attack or someone running &lt;code&gt;kubectl exec&lt;/code&gt; to debug? With correlation, you see the audit log showing which user ran the exec command, their role bindings, and whether it aligns with normal behavior patterns. Real incidents stand out because you can filter out expected activity.&lt;/p&gt;

&lt;p&gt;This doesn’t replace preventive controls. You still need vulnerability scanning, Pod Security Standards, network policies, and the practices from &lt;a href="https://dev.to/posts/shift-left-security-devsecops/"&gt;Shift Left Security&lt;/a&gt;. But when those controls fail and an incident happens, correlated security observability changes investigation time from hours to minutes.&lt;/p&gt;

&lt;p&gt;The goal isn’t perfect visibility. It’s actionable visibility. Can you answer “What happened?” when an alert fires? Can you trace a security event back to the request that caused it? If yes, you have enough. If not, add the missing signal.&lt;/p&gt;

&lt;p&gt;This post covered getting security signals into your observability pipeline and correlating them. The next one explores where this is heading—eBPF-native approaches, AI-assisted investigation, and the convergence of security and platform observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enterprise alternatives
&lt;/h2&gt;

&lt;p&gt;The open-source approach gives you full control and flexibility, but requires ongoing maintenance. Enterprise platforms bundle these capabilities with managed infrastructure, pre-built dashboards, and support.&lt;/p&gt;

&lt;p&gt;If you’re looking at commercial options, consider Kubernetes-native platforms (Sysdig Secure, Aqua Security, Prisma Cloud), cloud provider tools (AWS GuardDuty, Google Cloud Security Command Center, Azure Defender), or SIEM platforms with Kubernetes integrations (Elastic Security, Datadog Security Monitoring, Sumo Logic). Many teams use a mix: cloud provider tools for basic monitoring, open-source for custom correlation and deep investigation, and SIEM when compliance requires centralized reporting.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>observability</category>
    </item>
    <item>
      <title>Building a Unified OpenTelemetry Pipeline in Kubernetes</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Tue, 14 Oct 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/building-a-unified-opentelemetry-pipeline-in-kubernetes-87n</link>
      <guid>https://dev.to/fatihkoc/building-a-unified-opentelemetry-pipeline-in-kubernetes-87n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;All configurations, instrumentation examples, and testing scripts are in the &lt;a href="https://github.com/fatihkc/kubernetes-observability" rel="noopener noreferrer"&gt;kubernetes-observability&lt;/a&gt; repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Last year during a production incident, I debugged a payment failure with all the standard tools open. Grafana showed CPU spikes. CloudWatch had logs scattered across three services. Jaeger displayed 50 similar-looking traces. Twenty minutes in, I still couldn’t answer the basic question: “Which trace is the actual failing request?” The alert told us payments were broken. The logs showed errors. The traces existed. But nothing connected them. I ended up searching request IDs across log groups until I found the culprit.&lt;/p&gt;

&lt;p&gt;The problem wasn’t tools or data. We had plenty of both. The problem was correlation, or the complete lack of it.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/posts/monitoring-kube-prometheus-stack/"&gt;first post about kube-prometheus-stack&lt;/a&gt;, I showed why monitoring dashboards aren’t observability. This post shows you how to actually build observability with OpenTelemetry. You’ll get metrics, logs, and traces flowing through a unified pipeline with shared context that lets you jump from an alert to the exact failing trace in seconds, not hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenTelemetry solved vendor lock-in (and a bunch of other problems)
&lt;/h2&gt;

&lt;p&gt;OpenTelemetry is a &lt;a href="https://www.cncf.io/projects/opentelemetry/" rel="noopener noreferrer"&gt;CNCF graduated project&lt;/a&gt; that gives you vendor-neutral instrumentation libraries and a Collector that receives, processes, and exports telemetry at scale. Instead of coupling your application code to specific vendors, you instrument once with OTel SDKs and route telemetry wherever you need.&lt;/p&gt;

&lt;p&gt;In a freelance project, I migrated from kube-prometheus-stack to OTel. We needed custom metrics, logs, and traces. But vendor lock-in was the real concern. Kube-prometheus-stack worked for basic Prometheus metrics, but adding distributed tracing meant bolting on separate systems. And vendors get expensive fast.&lt;/p&gt;

&lt;p&gt;With OTel, I instrumented applications once and kept the flexibility to evaluate backends without touching code. We started with self-hosted Grafana, then tested a commercial vendor for two weeks by changing just the Collector’s exporter config. Zero application changes. That flexibility is the win.&lt;/p&gt;

&lt;p&gt;But vendor flexibility isn’t even the main benefit. The real value is centralized enrichment and correlation. Every signal that passes through the Collector gets the same Kubernetes metadata (pod, namespace, team annotations), the same sampling decisions, and the same trace context. This means your logs have the same &lt;code&gt;service.name&lt;/code&gt; and &lt;code&gt;trace_id&lt;/code&gt; as your traces, which have the same attributes as your metrics.&lt;/p&gt;

&lt;p&gt;When everything shares context, you can finally navigate between signals during an incident instead of manually correlating timestamps and guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to deploy the Collector
&lt;/h2&gt;

&lt;p&gt;Most teams deploy collectors wrong. They either sidecar everything and watch YAML explode, or they DaemonSet everything and wonder why nodes run out of memory.&lt;/p&gt;

&lt;p&gt;You can run OTel Collectors as sidecars, DaemonSets, or centralized gateways. Each pattern has trade-offs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Pattern&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sidecar&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One Collector container per pod&lt;/td&gt;
&lt;td&gt;Strong isolation, per-service control, lowest latency&lt;/td&gt;
&lt;td&gt;More YAML per workload, harder to scale config&lt;/td&gt;
&lt;td&gt;High-security workloads or latency-sensitive apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DaemonSet (Agent)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One Collector per node&lt;/td&gt;
&lt;td&gt;Simple ops, collects host + pod telemetry, fewer manifests&lt;/td&gt;
&lt;td&gt;Limited CPU/memory for heavy processing&lt;/td&gt;
&lt;td&gt;Broad cluster coverage with light transforms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gateway (Deployment)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Centralized Collector service&lt;/td&gt;
&lt;td&gt;Centralized config, heavy processing, easy fan-out&lt;/td&gt;
&lt;td&gt;Extra network hop, potential bottleneck&lt;/td&gt;
&lt;td&gt;Central policy, sampling, multi-backend routing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I use DaemonSet agents on each node for collection plus a gateway Deployment for processing. The agent forwards raw signals to the gateway, which applies enrichment, sampling, and routing.&lt;/p&gt;

&lt;p&gt;This keeps node resources light and centralizes the complex configuration in one place. I’ve seen teams try to do heavy processing in DaemonSet agents and then wonder why their nodes run out of memory. Don’t do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the OpenTelemetry Operator in Kubernetes
&lt;/h2&gt;

&lt;p&gt;You can deploy Collectors with raw manifests, but the &lt;a href="https://opentelemetry.io/docs/kubernetes/operator/" rel="noopener noreferrer"&gt;OpenTelemetry Operator&lt;/a&gt; gives you a &lt;code&gt;OpenTelemetryCollector&lt;/code&gt; CRD that handles service discovery and RBAC automatically. The Operator needs cert-manager for its admission webhooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install cert-manager first
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.0/cert-manager.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait about a minute for cert-manager to be ready. Then install the Operator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update

helm install opentelemetry-operator open-telemetry/opentelemetry-operator \
  --namespace opentelemetry-operator \
  --create-namespace \
  --set manager.replicas=2

kubectl -n opentelemetry-operator get pods

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;manager.replicas=2&lt;/code&gt; ensures high availability. Once installed, you define Collectors as custom resources and the Operator provisions everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenTelemetry Gateway Collector configuration
&lt;/h2&gt;

&lt;p&gt;The gateway receives OTLP (OpenTelemetry Protocol) signals, the standard wire protocol that carries metrics, logs, and traces over gRPC (port 4317) or HTTP (port 4318). It enriches them with Kubernetes metadata, applies intelligent sampling, and exports to backends. Here’s the key piece, the k8sattributes processor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;processors:
  k8sattributes:
    auth_type: serviceAccount
    extract:
      metadata: [k8s.namespace.name, k8s.pod.name, k8s.deployment.name]
      annotations:
        - tag_name: team
          key: team
          from: pod
        - tag_name: runbook.url
          key: runbook-url
          from: pod

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This automatically adds Kubernetes metadata to every signal. The annotations block extracts custom pod annotations like &lt;code&gt;team&lt;/code&gt; and &lt;code&gt;runbook-url&lt;/code&gt;, so every trace, metric, and log includes ownership and a link to remediation steps. During an incident, this saves you from hunting through wikis or Slack to figure out who owns the failing service.&lt;/p&gt;

&lt;p&gt;For sampling, I use tail-based sampling that keeps 100% of errors and slow requests. If your app processes 10 million requests per day and you store every trace, you’ll burn through storage and query performance.&lt;/p&gt;

&lt;p&gt;Sampling keeps a percentage of traces while discarding the rest. The problem with basic probabilistic sampling is it treats all traces equally. You might sample 10% of everything and miss critical error traces.&lt;/p&gt;

&lt;p&gt;Tail-based sampling is smarter. It waits until the trace completes, then decides based on rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors-first
        type: status_code
        status_code: {status_codes: [ERROR]}
      - name: slow-requests
        type: latency
        latency: {threshold_ms: 2000}
      - name: probabilistic-sample
        type: probabilistic
        probabilistic: {sampling_percentage: 10}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps 100% of errors, 100% of requests over 2 seconds, and 10% of everything else. You get full visibility into problems while reducing storage by 80-90%. Start conservative at 10% and increase sampling for high-value flows as you understand query patterns.&lt;/p&gt;

&lt;p&gt;The memory_limiter processor prevents OOM kills by back-pressuring receivers when memory usage approaches limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  memory_limiter:
    check_interval: 1s
    limit_mib: 3072
    spike_limit_mib: 800

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete gateway configuration with all receivers, exporters, and resource limits is in &lt;a href="https://github.com/fatihkc/kubernetes-observability/blob/main/opentelemetry/basic-pipeline/gateway.yaml" rel="noopener noreferrer"&gt;gateway.yaml&lt;/a&gt;. Deploy it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl create namespace observability
kubectl apply -f gateway.yaml
kubectl -n observability get pods -l app.kubernetes.io/name=otel-gateway

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you export metrics to Prometheus via the &lt;code&gt;prometheusremotewrite&lt;/code&gt; exporter, ensure Prometheus is started with &lt;code&gt;--web.enable-remote-write-receiver&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Alternatives: target a backend that supports remote write ingestion natively (e.g., Grafana Mimir, Cortex, Thanos), or use the Collector’s &lt;code&gt;prometheus&lt;/code&gt; exporter and configure Prometheus to scrape it instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  DaemonSet agent configuration
&lt;/h2&gt;

&lt;p&gt;The agent config is minimal. Just receive, batch, and forward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spec:
  mode: daemonset
  config: |
    receivers:
      otlp:
        protocols: {http: {endpoint: 0.0.0.0:4318}, grpc: {endpoint: 0.0.0.0:4317}}
      hostmetrics:
        scrapers: [cpu, memory, disk, network]

    processors:
      batch: {timeout: 5s}
      memory_limiter:
        check_interval: 1s
        limit_mib: 400
        spike_limit_mib: 100

    exporters:
      otlp:
        endpoint: otel-gateway.observability.svc.cluster.local:4317

    service:
      pipelines:
        traces: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlp]}
        metrics: {receivers: [otlp, hostmetrics], processors: [memory_limiter, batch], exporters: [otlp]}
        logs: {receivers: [otlp], processors: [memory_limiter, batch], exporters: [otlp]}    

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent collects host metrics plus OTLP signals from pods, batches them, and forwards to the gateway. Keep processing minimal to preserve node resources. Full configuration in &lt;a href="https://github.com/fatihkc/kubernetes-observability/blob/main/opentelemetry/basic-pipeline/agent.yaml" rel="noopener noreferrer"&gt;agent.yaml&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f agent.yaml
kubectl -n observability get pods -l app.kubernetes.io/name=otel-agent -o wide

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Instrumenting applications
&lt;/h2&gt;

&lt;p&gt;Applications must emit signals for collectors to work. Here’s a Python Flask app with OpenTelemetry tracing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from flask import Flask, request
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor

resource = Resource.create({
    "service.name": "checkout-service",
    "service.version": "v1.0.0",
    "deployment.environment": "production",
    "team": "payments",
})

trace_provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter(
    endpoint="http://otel-agent.observability.svc.cluster.local:4318/v1/traces"
)
trace_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(trace_provider)

app = Flask( __name__ )
FlaskInstrumentor().instrument_app(app)

@app.route("/checkout", methods=["POST"])
def checkout():
    with trace.get_tracer( __name__ ).start_as_current_span("checkout") as span:
        span.set_attribute("user.id", request.json.get("user_id"))
        # Business logic here
        return {"status": "success"}, 200

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy with pod annotations for the Collector to discover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metadata:
  annotations:
    team: "payments"
    runbook-url: "https://runbooks.internal/payments/checkout"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every trace now includes &lt;code&gt;service.name&lt;/code&gt;, &lt;code&gt;team&lt;/code&gt;, and &lt;code&gt;runbook.url&lt;/code&gt;. During incidents, you can filter by team in Grafana and get instant access to remediation docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correlation is everything
&lt;/h2&gt;

&lt;p&gt;A unified pipeline only matters if you can actually navigate between signals during an incident. You see an alert fire for high error rates. You need the logs for that service. Then you need the exact trace that failed. Without correlation, you’re manually matching timestamps across three different tools and hoping you found the right request. With proper correlation, you click through from alert to logs to trace in seconds. This requires three things: consistent resource attributes like &lt;code&gt;service.name&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt; across all signals, trace context (&lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;) injected into every log line, and data sources configured in Grafana to link between them.&lt;/p&gt;

&lt;p&gt;Add trace context to logs with the OTel SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import logging
from flask import Flask, request
from opentelemetry import trace
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter

# Reuse the resource defined earlier for tracing
# resource = Resource.create({"service.name": "checkout-service", ...})

log_exporter = OTLPLogExporter(
    endpoint="http://otel-agent.observability.svc.cluster.local:4318/v1/logs"
)
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))

handler = LoggingHandler(logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)

tracer = trace.get_tracer( __name__ )

@app.route("/checkout", methods=["POST"])
def checkout():
    with tracer.start_as_current_span("checkout") as span:
        span_context = trace.get_current_span().get_span_context()
        logging.info("Processing checkout", extra={
            "trace_id": format(span_context.trace_id, "032x"),
            "span_id": format(span_context.span_id, "016x"),
            "user_id": request.json.get("user_id")
        })
        # Business logic here
        return {"status": "success"}, 200

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;trace_id&lt;/code&gt; in logs, you build a Grafana dashboard that shows a Prometheus alert for high error rate, Loki logs filtered by &lt;code&gt;service.name&lt;/code&gt; and &lt;code&gt;trace_id&lt;/code&gt;, and the Tempo trace showing the full request flow. Click the alert, see logs, jump to trace. Incident resolution drops from hours to minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before you go to production
&lt;/h2&gt;

&lt;p&gt;Before production, validate each pipeline with a smoke test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl -n observability port-forward svc/otel-gateway 4318:4318

curl -X POST http://localhost:4318/v1/traces \
  -H "Content-Type: application/json" \
  -d '{
    "resourceSpans": [{
      "resource": {"attributes": [{"key": "service.name", "value": {"stringValue": "test-service"}}]},
      "scopeSpans": [{
        "spans": [{
          "traceId": "5b8aa5a2d2c872e8321cf37308d69df2",
          "spanId": "051581bf3cb55c13",
          "name": "test-span",
          "kind": 1,
          "startTimeUnixNano": "1609459200000000000",
          "endTimeUnixNano": "1609459200500000000"
        }]
      }]
    }]
  }'

kubectl -n monitoring port-forward svc/tempo 3100:3100
curl http://localhost:3100/api/search?q=test-service

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If traces, metrics, and logs all reach their backends, you’re ready. Full validation scripts including metrics, logs, and traces are in &lt;a href="https://github.com/fatihkc/kubernetes-observability/blob/main/opentelemetry/basic-pipeline/testing/test-pipeline.sh" rel="noopener noreferrer"&gt;test-pipeline.sh&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operations and scaling
&lt;/h2&gt;

&lt;p&gt;Treat OTel Collector config like application code. Store manifests in Git, require PR approval for config changes, deploy to dev then staging then production, and alert on Collector health (queue size, drop rate, CPU/memory).&lt;/p&gt;

&lt;p&gt;Enable HPA for the gateway based on CPU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: otel-gateway
  namespace: observability
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: otel-gateway
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor Collector-specific metrics exposed on &lt;code&gt;:8888/metrics&lt;/code&gt; like &lt;code&gt;otelcol_receiver_accepted_spans&lt;/code&gt;, &lt;code&gt;otelcol_receiver_refused_spans&lt;/code&gt;, &lt;code&gt;otelcol_exporter_sent_spans&lt;/code&gt;, &lt;code&gt;otelcol_exporter_send_failed_spans&lt;/code&gt;, and &lt;code&gt;otelcol_processor_batch_batch_send_size&lt;/code&gt;. Alert if refused or send_failed metrics spike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grafana cross-navigation
&lt;/h2&gt;

&lt;p&gt;To enable click-through from metrics to logs to traces, configure Grafana data source correlations. In the Tempo data source, add a trace-to-logs link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "datasourceUid": "loki-uid",
  "tags": [{"key": "service.name", "value": "service_name"}],
  "query": "{service_name=\"${ __field.labels.service_name}\"} |~ \"${__ span.traceId}\""
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Loki data source, add a logs-to-trace link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "datasourceUid": "tempo-uid",
  "field": "trace_id",
  "url": "/explore?left={\"datasource\":\"tempo-uid\",\"queries\":[{\"query\":\"${__value.raw}\"}]}"
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you view a trace in Tempo, you can click “Logs for this trace” and see all related log lines. From Loki, you can click a &lt;code&gt;trace_id&lt;/code&gt; field and jump directly to the trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually got from this migration
&lt;/h2&gt;

&lt;p&gt;In a production migration I led last year, we consolidated three separate agents (Prometheus exporter, Fluentd, Jaeger agent) into a single OTel pipeline. After 3 months:&lt;/p&gt;

&lt;p&gt;Incident resolution time dropped from 90 minutes (median) to 25 minutes. Engineers stopped jumping between four tools. One Grafana dashboard with cross-links was enough. Trace sampling reduced storage by 85% with no loss in debug capability. The &lt;code&gt;team&lt;/code&gt; attribute and &lt;code&gt;runbook.url&lt;/code&gt; in every signal eliminated “who owns this?” questions.&lt;/p&gt;

&lt;p&gt;The biggest win wasn’t technical. It was operational.&lt;/p&gt;

&lt;p&gt;On-call engineers stopped guessing and started following a clear path: alert, dashboard, trace, runbook. When you can click through from a Prometheus alert to Loki logs to the exact failing trace in Tempo, observability stops being theoretical and starts being useful.&lt;/p&gt;

&lt;p&gt;Deploy the gateway and agent, instrument one service, and test cross-navigation in Grafana. Once you see it work, you’ll understand why unified pipelines matter. The next post in this series covers extending this pipeline for security observability (preprocessing Kubernetes audit logs, integrating with SIEMs, correlating security events with application traces). For now, get the basic pipeline working. Understanding &lt;a href="https://dev.to/posts/k8s-deployment-guide/"&gt;Kubernetes deployment patterns&lt;/a&gt; helps when running monitoring infrastructure reliably, especially when you need to ensure collectors stay running during cluster upgrades.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>opentelemetry</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Observability Gap with kube-prometheus-stack in Kubernetes</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Sun, 05 Oct 2025 10:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/the-observability-gap-with-kube-prometheus-stack-in-kubernetes-2ik7</link>
      <guid>https://dev.to/fatihkoc/the-observability-gap-with-kube-prometheus-stack-in-kubernetes-2ik7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgo4tewsapvpsvpwxodnu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgo4tewsapvpsvpwxodnu.webp" alt="observability-gap-kube-prometheus-stack" width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
Observability in Kubernetes has become a hot topic in recent years. Teams everywhere deploy the popular &lt;strong&gt;kube-prometheus-stack&lt;/strong&gt; , which bundles Prometheus and Grafana into an opinionated setup for monitoring Kubernetes workloads. On the surface, it looks like the answer to all your monitoring needs. But here is the catch: &lt;strong&gt;monitoring is not observability&lt;/strong&gt;. And if you confuse the two, you will hit a wall when your cluster scales or your incident response gets messy.&lt;/p&gt;

&lt;p&gt;In this first post of my observability series, I want to break down the real difference between monitoring and observability, highlight the gaps in kube-prometheus-stack, and suggest how we can move toward true Kubernetes observability.&lt;/p&gt;
&lt;h2&gt;
  
  
  The question I keep hearing
&lt;/h2&gt;

&lt;p&gt;I worked with a team running microservices on Kubernetes. They had kube-prometheus-stack deployed, beautiful Grafana dashboards, and alerts configured. Everything looked great until 3 AM on a Tuesday when API requests started timing out.&lt;/p&gt;

&lt;p&gt;The on-call engineer got paged. Prometheus showed CPU spikes. Grafana showed pod restarts. When the team jumped on Slack, they asked me: “Do you have tools for understanding what causes these timeouts?” They spent two hours manually correlating logs across CloudWatch, checking recent deployments, and guessing at database queries before finding the culprit: a batch job with an unoptimized query hammering the production database.&lt;/p&gt;

&lt;p&gt;I had seen this pattern before. Their monitoring stack told them something was broken, but not why. With distributed tracing, they would have traced the slow requests back to that exact query in minutes, not hours. This is the observability gap I keep running into: teams confuse monitoring dashboards with actual observability. The lesson for them was clear: monitoring answers “what broke” while observability answers “why it broke.” And fixing this requires shared ownership. Developers need to instrument their code for visibility. DevOps engineers need to provide the infrastructure to capture and expose that behavior. When both sides own observability together, incidents get resolved faster and systems become more reliable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Monitoring vs Observability
&lt;/h2&gt;

&lt;p&gt;Most engineers use the terms interchangeably, but they are not the same. Monitoring tells you when something is wrong, while observability helps you understand why it went wrong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; : Answers “what is happening?” You collect predefined metrics (CPU, memory, disk) and set alerts when thresholds are breached. Your alert fires: “CPU usage is 95%.” Now what?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; : Answers “why is this happening?” You investigate using interconnected data you didn’t know you’d need. Which pod is consuming CPU? What user request triggered it? Which database query is slow? What changed in the last deployment?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The classic definition of observability relies on the &lt;strong&gt;three pillars&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt; : Numerical values over time (CPU, latency, request counts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt; : Unstructured text for contextual events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; : Request flow across services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prometheus and Grafana excel at metrics, but Kubernetes observability requires all three pillars working together. The &lt;a href="https://landscape.cncf.io/guide#observability-and-analysis--observability" rel="noopener noreferrer"&gt;CNCF observability landscape&lt;/a&gt; shows how the ecosystem has evolved beyond simple monitoring. If you only deploy kube-prometheus-stack, you will only get one piece of the puzzle.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Dominance of kube-prometheus-stack
&lt;/h2&gt;

&lt;p&gt;Let’s be fair. kube-prometheus-stack is the default for a reason. It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prometheus&lt;/strong&gt; for metrics scraping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; for dashboards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alertmanager&lt;/strong&gt; for rule-based alerts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node Exporter&lt;/strong&gt; for hardware and OS metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Helm, you can set it up in minutes. This is why it dominates Kubernetes monitoring setups today. But it’s not the full story.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within minutes, you’ll have Prometheus scraping metrics, Grafana running on port 3000, and a collection of pre-configured dashboards. It feels like magic at first.&lt;/p&gt;

&lt;p&gt;Access Grafana to see your dashboards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3000:80

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default credentials are &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;prom-operator&lt;/code&gt;. You’ll immediately see dashboards for Kubernetes cluster monitoring, node exporter metrics, and pod resource usage. The data flows in automatically.&lt;/p&gt;

&lt;p&gt;In many projects, I’ve seen teams proudly display dashboards full of red and green panels yet still struggle during incidents. Why? Because the dashboards told them &lt;em&gt;what&lt;/em&gt; broke, not &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Pitfalls with kube-prometheus-stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Metric Cardinality Explosion
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Cardinality&lt;/strong&gt; is the number of unique time series created by combining a metric name with all possible label value combinations. Each unique combination creates a separate time series that Prometheus must store and query. The &lt;a href="https://prometheus.io/docs/practices/naming/" rel="noopener noreferrer"&gt;Prometheus documentation on metric and label naming&lt;/a&gt; provides official guidance on avoiding cardinality issues.&lt;/p&gt;

&lt;p&gt;Prometheus loves labels, but too many labels can crash your cluster. If you add dynamic labels like &lt;code&gt;user_id&lt;/code&gt; or &lt;code&gt;transaction_id&lt;/code&gt;, you end up with &lt;strong&gt;millions of time series&lt;/strong&gt;. This causes both storage and query performance issues. I’ve witnessed a production cluster go down not because of the application but because Prometheus itself was choking.&lt;/p&gt;

&lt;p&gt;Here’s a bad example that will destroy your Prometheus instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from prometheus_client import Counter

# BAD: High cardinality labels
http_requests = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'user_id', 'transaction_id'] # AVOID!
)

# With 1000 users and 10000 transactions per user, you get:
# 5 methods * 20 endpoints * 1000 users * 10000 transactions = 1 billion time series

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, use low-cardinality labels and track high-cardinality data elsewhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from prometheus_client import Counter

# GOOD: Low cardinality labels
http_requests = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['method', 'endpoint', 'status_code'] # Limited set of values
)

# Now you have: 5 methods * 20 endpoints * 5 status codes = 500 time series

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check your cardinality with this PromQL query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;count({ __name__ =~".+"}) by ( __name__ )

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see metrics with hundreds of thousands of series, you’ve found your culprit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lack of Scalability
&lt;/h3&gt;

&lt;p&gt;In small clusters, a single Prometheus instance works fine. In large enterprises with multiple clusters, it becomes a nightmare. Without federation or sharding, Prometheus does not scale well. If you’re building multi-cluster infrastructure, understanding &lt;a href="https://dev.to/posts/k8s-deployment-guide/"&gt;Kubernetes deployment patterns&lt;/a&gt; becomes critical for running monitoring components reliably.&lt;/p&gt;

&lt;p&gt;For multi-cluster setups, you’ll need Prometheus federation according to the &lt;a href="https://prometheus.io/docs/prometheus/latest/federation/" rel="noopener noreferrer"&gt;Prometheus federation documentation&lt;/a&gt;. Here’s a basic configuration for a global Prometheus instance that scrapes from cluster-specific instances:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scrape_configs:
  - job_name: 'federate'
    scrape_interval: 15s
    honor_labels: true
    metrics_path: '/federate'
    params:
      'match[]':
        - '{job="kubernetes-pods"}'
        - '{ __name__ =~"job:.*"}'
    static_configs:
      - targets:
        - 'prometheus-cluster-1.monitoring:9090'
        - 'prometheus-cluster-2.monitoring:9090'
        - 'prometheus-cluster-3.monitoring:9090'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with federation, you hit storage limits. A single Prometheus instance struggles beyond 10-15 million active time series.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert Fatigue
&lt;/h3&gt;

&lt;p&gt;Kube-prometheus-stack ships with a bunch of default alerts. While they are useful at first, they quickly generate &lt;strong&gt;alert fatigue&lt;/strong&gt;. Engineers drown in notifications that don’t actually help them resolve issues.&lt;/p&gt;

&lt;p&gt;Check your current alert rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get prometheusrules -n monitoring

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll likely see dozens of pre-configured alerts. Here’s an example of a noisy alert that fires too often:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- alert: KubePodCrashLooping
  annotations:
    description: 'Pod {{ $labels.namespace }}/{{ $labels.pod }} is crash looping'
    summary: Pod is crash looping.
  expr: |
    max_over_time(kube_pod_container_status_waiting_reason{reason="CrashLoopBackOff"}[5m]) &amp;gt;= 1    
  for: 15m
  labels:
    severity: warning

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem? This fires for every pod in CrashLoopBackOff, including those in development namespaces or expected restarts during deployments. You end up with alert spam.&lt;/p&gt;

&lt;p&gt;A better approach is to tune alerts based on criticality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- alert: CriticalPodCrashLooping
  annotations:
    description: 'Critical pod {{ $labels.namespace }}/{{ $labels.pod }} is crash looping'
    summary: Production-critical pod is failing.
  expr: |
    max_over_time(kube_pod_container_status_waiting_reason{
      reason="CrashLoopBackOff",
      namespace=~"production|payment|auth"
    }[5m]) &amp;gt;= 1    
  for: 5m
  labels:
    severity: critical

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you only get alerted for crashes in critical namespaces, and you can respond faster because the signal-to-noise ratio is higher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboards That Show What but Not Why
&lt;/h3&gt;

&lt;p&gt;Grafana panels look impressive, but most of them only highlight symptoms. High CPU, failing pods, dropped requests. They don’t explain the underlying cause. This is the observability gap.&lt;/p&gt;

&lt;p&gt;Here’s a typical PromQL query you’ll see in Grafana dashboards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Shows CPU usage percentage
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you &lt;strong&gt;what&lt;/strong&gt; : CPU is at 95%. But it doesn’t tell you &lt;strong&gt;why&lt;/strong&gt;. Which process? Which pod? What triggered the spike?&lt;/p&gt;

&lt;p&gt;You can try drilling down with more queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Top 10 pods by CPU usage
topk(10, rate(container_cpu_usage_seconds_total[5m]))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even this shows you the pod name, but not the request path, user action, or external dependency that caused the spike. Without distributed tracing, you’re guessing. You end up in Slack asking, “Did anyone deploy something?” or “Is the database slow?”&lt;/p&gt;

&lt;h2&gt;
  
  
  Why kube-prometheus-stack Alone Is Not Enough for Kubernetes Observability
&lt;/h2&gt;

&lt;p&gt;Here is the opinionated part: kube-prometheus-stack is &lt;strong&gt;monitoring, not observability&lt;/strong&gt;. It’s a foundation, but not the endgame. Kubernetes observability requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt; (e.g., Loki, Elasticsearch)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; (e.g., Jaeger, Tempo)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlated context&lt;/strong&gt; (not isolated metrics)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without these, you will continue firefighting with partial visibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Path Toward Observability
&lt;/h2&gt;

&lt;p&gt;So, how do we close the observability gap?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with kube-prometheus-stack, but &lt;strong&gt;acknowledge its limits&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add a &lt;strong&gt;centralized logging solution&lt;/strong&gt; (Loki, Elasticsearch, or your preferred stack).&lt;/li&gt;
&lt;li&gt;Adopt &lt;strong&gt;distributed tracing&lt;/strong&gt; with Jaeger or Tempo.&lt;/li&gt;
&lt;li&gt;Prepare for the next step: &lt;strong&gt;OpenTelemetry&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how to add Loki for centralized logging alongside your existing Prometheus setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Install Loki for log aggregation
helm install loki grafana/loki \
  --namespace monitoring \
  --create-namespace

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For distributed tracing, Tempo integrates seamlessly with Grafana:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install Tempo for traces
helm install tempo grafana/tempo \
  --namespace monitoring

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now configure Grafana to use Loki and Tempo as data sources. In your Grafana UI, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: 1
datasources:
  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
  - name: Tempo
    type: tempo
    access: proxy
    url: http://tempo:3100

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this setup, you can jump from a metric spike in Prometheus to related logs in Loki and traces in Tempo. This is when monitoring starts becoming observability.&lt;/p&gt;

&lt;p&gt;OpenTelemetry introduces a vendor-neutral way to capture metrics, logs, and traces in a single pipeline. Instead of bolting together siloed tools, you get a unified foundation. I’ll cover this in detail in the &lt;a href="https://dev.to/posts/opentelemetry-kubernetes-centralized-observability"&gt;next post on OpenTelemetry in Kubernetes&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Kubernetes observability is more than Prometheus and Grafana dashboards. Kube-prometheus-stack gives you a strong monitoring foundation, but it leaves critical gaps in logs, traces, and correlation. If you only rely on it, you will face cardinality explosions, alert fatigue, and dashboards that tell you what went wrong but not why.&lt;/p&gt;

&lt;p&gt;True Kubernetes observability requires a mindset shift. You’re not just collecting metrics anymore. You’re building a system that helps you ask questions you didn’t know you’d need to answer. When an incident happens at 3 AM, you want to trace a slow API call from the user request, through your microservices, down to the database query that’s timing out. Prometheus alone won’t get you there.&lt;/p&gt;

&lt;p&gt;To build true Kubernetes observability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept kube-prometheus-stack as monitoring, not observability&lt;/li&gt;
&lt;li&gt;Add logs and traces into your pipeline&lt;/li&gt;
&lt;li&gt;Watch out for metric cardinality and alert noise&lt;/li&gt;
&lt;li&gt;Move toward OpenTelemetry pipelines for a unified solution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The monitoring foundation you build today shapes how quickly you can respond to incidents tomorrow. Start with kube-prometheus-stack, acknowledge its limits, and plan your path toward full observability. Your future self (and your on-call team) will thank you.&lt;/p&gt;

&lt;p&gt;In the next part of this series, I will show how to deploy OpenTelemetry in Kubernetes for centralized observability. That is where the real transformation begins.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>observability</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
    <item>
      <title>Shift Left Security Practices Developers Like</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Tue, 16 Sep 2025 20:09:33 +0000</pubDate>
      <link>https://dev.to/fatihkoc/shift-left-security-practices-developers-like-5f16</link>
      <guid>https://dev.to/fatihkoc/shift-left-security-practices-developers-like-5f16</guid>
      <description>&lt;p&gt;Security is often treated as a late stage gate. In a cloud native world, that's a tax on velocity. &lt;strong&gt;Shift Left Security&lt;/strong&gt; flips the script. We integrate security earlier. During design, coding, and CI—so developers get fast, actionable feedback without leaving their flow.&lt;/p&gt;

&lt;p&gt;In this guide, I'll share &lt;strong&gt;developer friendly&lt;/strong&gt; practices I've used across teams, plus &lt;strong&gt;ready to copy code examples&lt;/strong&gt; you can paste into repos today. I'll also call out common traps and how to avoid "security theater."&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick story
&lt;/h2&gt;

&lt;p&gt;On a microservices project, a customer had layered on too many security tools. Builds slowed, false positives spiked while observability lagged. We shifted feedback to the IDE and pre commit, moved deep scans to nightly, and added auto fix hints. Two sprints later: faster merges, fewer vulnerabilities reaching staging, and a happier team. Developer experience and timing beat raw coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developer friendly means
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast feedback&lt;/strong&gt;: seconds, not minutes, for inner loop checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low noise&lt;/strong&gt;: start with high signal rules; phase in stricter ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In flow&lt;/strong&gt;: IDE, pre-commit, PR checks—no context switching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent&lt;/strong&gt;: policies as code; exceptions time bound and auditable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning oriented&lt;/strong&gt;: every failure teaches the fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For broader context, see &lt;a href="https://owasp.org/www-project-application-security-verification-standard/" rel="noopener noreferrer"&gt;&lt;strong&gt;OWASP ASVS&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://csrc.nist.gov/Projects/ssdf" rel="noopener noreferrer"&gt;&lt;strong&gt;NIST SSDF&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Also see the &lt;a href="https://owasp.org/www-project-devsecops-guideline/latest/" rel="noopener noreferrer"&gt;OWASP DevSecOps Guidelines&lt;/a&gt; for practical ways to align velocity with safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shift Left Security in practice (with code)
&lt;/h2&gt;

&lt;p&gt;Below are &lt;strong&gt;plug and play snippets&lt;/strong&gt; that respect the inner loop. Start small, pick two and expand.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Pre‑commit essentials: secrets + basic SAST
&lt;/h3&gt;

&lt;p&gt;We’ve all seen the “oops, someone committed a token” alert. By the time PR or nightly scans catch it, it’s already in history. Pre‑commit hooks are fast, local, and stop the embarrassing stuff early. Keep them under a few seconds in duration and gate only on high‑confidence issues so developers don't disable them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml&lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/gitleaks/gitleaks&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v8.24.2&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detect"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--redact"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--no-banner"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/semgrep/pre-commit&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v1.136.0'&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--config"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p/ci"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--quiet"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# start with high-signal rules&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pre-commit/pre-commit-hooks&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v6.0.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check-yaml&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;end-of-file-fixer&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trailing-whitespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: Gate only on &lt;strong&gt;critical or high-confidence&lt;/strong&gt; findings at commit time. Expand to medium/low in PR or nightly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Optional: tighten Gitleaks with custom allow lists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitleaks.toml (example)&lt;/span&gt;
&lt;span class="nn"&gt;[allowlist]&lt;/span&gt;
  &lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"allow test tokens"&lt;/span&gt;
  &lt;span class="py"&gt;regexes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"GH_TEST_[0-9A-F]{20}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Fast PR checks + deeper nightly scans
&lt;/h3&gt;

&lt;p&gt;Pull requests should answer one question: Is this safe to merge right now? That’s it. Fast jobs for secrets, lightweight SAST, and basic IaC checks keep PRs flowing. Then at night, when no one’s waiting on feedback, run deep scans. Full rule sets, dependency scans, and container/IaC checks. That way, developers aren’t stuck waiting 15 minutes just to land a comment fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/secure-pr.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secure-pr&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fast-guardrails&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secrets scan (Gitleaks)&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Semgrep&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install --upgrade semgrep&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SAST (Semgrep high-signal)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep --config p/ci --sarif --output semgrep.sarif --quiet --metrics=off&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload SARIF&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nightly: Run deeper SAST, SCA, container/IaC scans.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/nightly-security.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nightly-security&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deep-scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Semgrep&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install --upgrade semgrep&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SAST (Semgrep full)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;semgrep \&lt;/span&gt;
            &lt;span class="s"&gt;--config p/r2c-security-audit \&lt;/span&gt;
            &lt;span class="s"&gt;--config p/secrets \&lt;/span&gt;
            &lt;span class="s"&gt;--config p/docker \&lt;/span&gt;
            &lt;span class="s"&gt;--sarif --output semgrep.sarif --quiet --metrics=off&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload SARIF&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;semgrep.sarif&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SCA (npm/yarn example)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if [ -f package-lock.json ]; then npm audit --audit-level=high; fi&lt;/span&gt;
          &lt;span class="s"&gt;if [ -f yarn.lock ] &amp;amp;&amp;amp; command -v yarn &amp;gt;/dev/null; then \&lt;/span&gt;
            &lt;span class="s"&gt;YVER=$(yarn -v | cut -d. -f1); \&lt;/span&gt;
            &lt;span class="s"&gt;if [ "$YVER" -ge 2 ]; then yarn npm audit --audit-level=high; else yarn audit --level high; fi; \&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Container &amp;amp; IaC scan (Trivy)&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@0.28.0&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;scan-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fs"&lt;/span&gt;
          &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;table"&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL,HIGH"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Policy as Code with OPA: block risky images in CI
&lt;/h3&gt;

&lt;p&gt;Unwritten security rules only surface during review. OPA turns them into testable, versioned policy. A small Rego rule like “only signed images from our registry” makes the decision explicit and produces clear pass/fail reasons.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="c1"&gt;# policy/image.rego&lt;/span&gt;
&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;ci&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;

&lt;span class="c1"&gt;# Reasons to deny; empty -&amp;gt; allowed&lt;/span&gt;
&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"registry.example.com/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s2"&gt;"image must come from registry.example.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signature_verified&lt;/span&gt;
  &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s2"&gt;"image signature not verified"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it into a small CI step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# scripts/opa_check.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;:?image&lt;span class="p"&gt; required&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;sig_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;:?signature_verified&lt;span class="p"&gt; required&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;  &lt;span class="c"&gt;# true/false&lt;/span&gt;

&lt;span class="c"&gt;# Create JSON input for OPA and evaluate policy&lt;/span&gt;
&lt;span class="nv"&gt;violations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; i &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--argjson&lt;/span&gt; s &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sig_ok&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{image: $i, signature_verified: $s}'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  opa &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; - &lt;span class="nt"&gt;-d&lt;/span&gt; policy/ &lt;span class="nt"&gt;-f&lt;/span&gt; json &lt;span class="s1"&gt;'data.ci.image.deny'&lt;/span&gt; | &lt;span class="se"&gt;\&lt;/span&gt;
  jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.result[0].expressions[0].value[]?'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$violations&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Policy violations:"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$violations&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^/ - /'&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Policy passed"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/opa_check.sh registry.example.com/app@sha256:... &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any violations are returned, print them and fail the job. For more on secure CI pipelines, see the &lt;a href="https://www.cncf.io/blog/2025/03/18/open-policy-agent-best-practices-for-a-secure-deployment/" rel="noopener noreferrer"&gt;CNCF blog on OPA best practices&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Kubernetes: Pod Security Standards via labels (quick win)
&lt;/h3&gt;

&lt;p&gt;Kubernetes defaults allow risky capabilities like privileged pods, host mounts, running as root. Most apps don't need them. Namespace level Pod Security Admission labels that enforce the Pod Security Standards are the fastest way to shut off bad defaults. Label the namespace and whole classes of risk disappear. Some workloads will need exceptions, but those become explicit decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# namespaces/restricted.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Namespace&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pod-security.kubernetes.io/enforce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restricted"&lt;/span&gt;
    &lt;span class="na"&gt;pod-security.kubernetes.io/audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;restricted"&lt;/span&gt;
    &lt;span class="na"&gt;pod-security.kubernetes.io/warn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baseline"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lock down common Pod risks with a default template.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Safer Dockerfile (small changes, big impact)
&lt;/h3&gt;

&lt;p&gt;Many Dockerfiles run as root and include unnecessary packages. Prefer a distroless runtime and a non‑root user to ship a smaller, safer image. You’ll cut CVEs and attack surface, reduce registry storage and network transfer, and speed image pulls. Build times also drop when you prune dev dependencies, shrink the build context, and leverage layer caching. Distroless itself doesn’t make builds faster. Debugging is harder without a shell, so keep a separate -debug image for staging.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distroless runtime → smaller image, fewer CVEs, faster pulls, lower registry storage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USER&lt;/code&gt; non‑root → safer by default.&lt;/li&gt;
&lt;li&gt;Multi‑stage build + prune dev deps → smaller runtime and better cache reuse (faster builds).&lt;/li&gt;
&lt;li&gt;Note: native modules build faster on &lt;code&gt;node:22-slim&lt;/code&gt; than Alpine; still use distroless for runtime. BuildKit cache mounts speed npm/yarn installs.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="c"&gt;# Build stage&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm prune &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev

&lt;span class="c"&gt;# Runtime (distroless)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/nodejs22-debian12&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nonroot:nonroot&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["dist/server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6) Threat modeling as code (lightweight)
&lt;/h3&gt;

&lt;p&gt;Threat models drift when they live outside the repo. Keep a small YAML file next to the code so it evolves with each change. When an API or trust boundary changes, update the model in the same PR. It won’t cover everything, but it keeps risks visible and makes design decisions explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docs/threat-model.yaml&lt;/span&gt;
&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-api&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public-api&lt;/span&gt;
&lt;span class="na"&gt;assets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A001&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;card-data&lt;/span&gt;
    &lt;span class="na"&gt;classification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sensitive&lt;/span&gt;
&lt;span class="na"&gt;trust_boundaries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-gateway&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-api&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-api&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bank-gateway&lt;/span&gt;
&lt;span class="na"&gt;threats&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;T001&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SQL injection on /charge&lt;/span&gt;
    &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STRIDE.Tampering&lt;/span&gt;
    &lt;span class="na"&gt;risk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;High&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mitigated&lt;/span&gt;
    &lt;span class="na"&gt;mitigations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;parameterized-queries&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;input-validation&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;waf-rule-123&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;T002&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secrets leakage via logs&lt;/span&gt;
    &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STRIDE.InformationDisclosure&lt;/span&gt;
    &lt;span class="na"&gt;risk&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Medium&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Open&lt;/span&gt;
    &lt;span class="na"&gt;mitigations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;structured-logging&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;log-scrubbers&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;disable-debug-in-prod&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;owners&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-champion&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alice&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tech-lead&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render it in CI (to HTML/diagram) for visibility and require a short rationale when accepting risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  7) Minimum viable SBOM + signature
&lt;/h3&gt;

&lt;p&gt;You can’t patch what you can’t find, and you can’t trust what you can’t verify. An SBOM (via Syft or similar) inventories what’s in your image, and a Cosign signature + SBOM attestation proves who built it and with what. When “Are we affected by CVE‑XXXX?” arrives, this turns hours into minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sbom+sign.sh&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;IMAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"registry.example.com/app:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_SHA&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Build image&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
docker push &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# SBOM (SPDX via Syft)&lt;/span&gt;
syft packages &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; spdx-json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sbom.spdx.json

&lt;span class="c"&gt;# Sign image + attest SBOM (Cosign)&lt;/span&gt;
cosign sign &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
cosign attest &lt;span class="nt"&gt;--predicate&lt;/span&gt; sbom.spdx.json &lt;span class="nt"&gt;--type&lt;/span&gt; spdx &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Verify signature and attestation (adjust identity/issuer for your CI)&lt;/span&gt;
cosign verify &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-identity&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REPOSITORY&lt;/span&gt;&lt;span class="k"&gt;:-}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-oidc-issuer&lt;/span&gt; &lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

cosign verify-attestation &lt;span class="nt"&gt;--type&lt;/span&gt; spdx &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Signature and SBOM attestation verified"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then extend your OPA policy to require a valid attestation.&lt;/p&gt;

&lt;h3&gt;
  
  
  8) IaC guardrails: Terraform checks in PRs
&lt;/h3&gt;

&lt;p&gt;Cloud misconfigurations are the sneakiest bugs. They look harmless in code review, then suddenly you’ve got a public S3 bucket in prod. Running tfsec on the Terraform plan catches those before apply. It’s cheap insurance, and it makes reviewers more confident: “yep, this plan doesn’t open the blast doors.” Sure, you’ll have to tune a few noisy rules, but the net is positive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/tf-guardrails.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tf-guardrails&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tfsec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Validate &amp;amp; plan&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform init -input=false&lt;/span&gt;
          &lt;span class="s"&gt;terraform validate -no-color&lt;/span&gt;
          &lt;span class="s"&gt;terraform plan -out plan.out -no-color&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tfsec (critical only)&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/tfsec-action@v1.0.11&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;tfsec_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--severity&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CRITICAL&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--format&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sarif&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--out&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tfsec.sarif"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload SARIF&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github/codeql-action/upload-sarif@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tfsec.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common pitfalls (and fixes)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;False positive fatigue&lt;/strong&gt; → start with &lt;strong&gt;high confidence&lt;/strong&gt; rules; add suppressions with context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow pipelines&lt;/strong&gt; → parallelize; cache dependencies; schedule deep scans &lt;strong&gt;nightly&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opaque decisions&lt;/strong&gt; → keep policies as code; require rationale on exceptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;“Security says no” culture&lt;/strong&gt; → create &lt;strong&gt;security champions&lt;/strong&gt; within dev teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late requirements&lt;/strong&gt; → add &lt;strong&gt;threat modeling&lt;/strong&gt; to planning; codify standards in templates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tools &amp;amp; when to use them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Fast inner loop&lt;/th&gt;
&lt;th&gt;PR/CI guardrail&lt;/th&gt;
&lt;th&gt;Scheduled/deep&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secrets leakage&lt;/td&gt;
&lt;td&gt;pre-commit + Gitleaks&lt;/td&gt;
&lt;td&gt;Gitleaks Action&lt;/td&gt;
&lt;td&gt;Org/repo-wide secret scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code vulns&lt;/td&gt;
&lt;td&gt;Semgrep targeted rules&lt;/td&gt;
&lt;td&gt;Semgrep CI (SARIF upload)&lt;/td&gt;
&lt;td&gt;Semgrep full rulesets + CodeQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;npm/pnpm audit; pip-audit&lt;/td&gt;
&lt;td&gt;Audit in CI (fail-on=high)&lt;/td&gt;
&lt;td&gt;Renovate/Dependabot + license allowlists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Containers&lt;/td&gt;
&lt;td&gt;Trivy (fs)&lt;/td&gt;
&lt;td&gt;Trivy (image) in CI&lt;/td&gt;
&lt;td&gt;Trivy + Cosign/Sigstore attestations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IaC (Terraform)&lt;/td&gt;
&lt;td&gt;tfsec or Checkov locally&lt;/td&gt;
&lt;td&gt;tfsec/Checkov in CI&lt;/td&gt;
&lt;td&gt;Conftest/OPA (Rego) against &lt;code&gt;terraform plan&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What’s the difference between Shift Left Security and DevSecOps?&lt;/strong&gt;&lt;br&gt;
Shift Left is the practice (earlier checks), DevSecOps is the culture/process shift enabling it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Shift Left Security slow developers down?&lt;/strong&gt;&lt;br&gt;
Only if you push heavy checks into the inner loop. Keep fast checks local/PR; move heavy ones to nightly. Most teams recoup time via fewer hotfixes and less rework.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do developers need to be security experts?&lt;/strong&gt;&lt;br&gt;
No. They need sharp guardrails and actionable feedback. Security champions and short, focused trainings beat long policy docs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we handle false positives?&lt;/strong&gt;&lt;br&gt;
Tune rules with suppressions and allowlists in-repo; require justification in PRs; review exceptions monthly and prune stale ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if a tool blocks a release?&lt;/strong&gt;&lt;br&gt;
Use severity thresholds (e.g., fail on high/critical). Allow time bound waivers with an owner and due date; track them in issues and audit regularly.&lt;/p&gt;

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

&lt;p&gt;Shift Left Security succeeds when it respects developer time. Keep fast checks in the inner loop, move heavy analysis to nightly, and encode policy so decisions are visible and auditable. Favor modular, open source pieces so any tool can be swapped without lock in; upgrade to enterprise where it clearly pays off.&lt;/p&gt;

&lt;p&gt;Enterprise options to evaluate: Prisma Cloud, SonarQube/SonarCloud, Snyk, Wiz, Aqua, Lacework, GitHub Advanced Security/GitLab Ultimate.&lt;/p&gt;

&lt;p&gt;Curious how to apply this to your platform? &lt;strong&gt;Ping me via the &lt;a href="https://fatihkoc.net/contact/" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page&lt;/strong&gt;—I'm happy to tailor a developer friendly rollout for your stack. &lt;/p&gt;

&lt;p&gt;This article originally published at &lt;a href="https://fatihkoc.net/posts/shift-left-security-devsecops/" rel="noopener noreferrer"&gt;fatihkoc.net&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>devsecops</category>
      <category>kubernetes</category>
      <category>docker</category>
    </item>
    <item>
      <title>Elastic Kubernetes Service Cost Optimization: A Comprehensive Guide - Part Two</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Tue, 21 May 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/elastic-kubernetes-service-cost-optimization-a-comprehensive-guide-part-two-bj8</link>
      <guid>https://dev.to/fatihkoc/elastic-kubernetes-service-cost-optimization-a-comprehensive-guide-part-two-bj8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction: Deep Diving into AWS EKS Cost Optimization
&lt;/h2&gt;

&lt;p&gt;When companies or projects try to choose the right distribution for Kubernetes, they know most have similar features. High availability, resilience, support, addons for storage, security, etc. It comes to the single most important thing when making a decision. &lt;strong&gt;Cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://fatihkoc.net/posts/eks-cost-optimization-1/" rel="noopener noreferrer"&gt;first part&lt;/a&gt; of this series, we discussed general Kubernetes cost optimization strategies. Now, further delve into Amazon Web Services (AWS) specific strategies to optimize your Elastic Kubernetes Service (EKS) costs. This part focuses on leveraging AWS services, understanding pricing models, and implementing AWS-specific features for cost-effective EKS management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the AWS EKS Pricing Model
&lt;/h2&gt;

&lt;p&gt;AWS charges for EKS based on the control plane usage and EC2 instances or Fargate for worker nodes. Familiarize yourself with the pricing details, including per-hour charges for the EKS control plane and costs associated with EC2 instances or Fargate usage. Keep an eye on price changes and consider reserved instances or savings plans for predictable workloads. Using the &lt;a href="https://calculator.aws/" rel="noopener noreferrer"&gt;AWS Pricing Calculator&lt;/a&gt; is essential for price prediction. Try to use Spot &amp;amp; All upfront paid Reserved instances as much as possible.&lt;/p&gt;

&lt;p&gt;For predictable workloads, consider purchasing Reserved Instances or Savings Plans. These options provide significant discounts compared to on-demand pricing in exchange for a commitment to a certain level of usage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbcc2cgs6nt8wydklk6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdbcc2cgs6nt8wydklk6u.png" alt="AWS Cost Calculator for EC2" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimizing EC2 Instances for Worker Nodes
&lt;/h2&gt;

&lt;p&gt;Choose the right EC2 instance types based on your application’s needs. Utilize Spot Instances for non-critical or flexible workloads to save up to 90% compared to on-demand prices. Use Auto Scaling Groups (ASGs) to dynamically adjust capacity, ensuring you pay only for what you need.&lt;/p&gt;

&lt;p&gt;Node auto-scaling is really essential for Kubernetes workloads. Especially in cloud environments where pay-as-you-use models are used, dynamically increasing and decreasing node sizes are crucial. &lt;a href="https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler" rel="noopener noreferrer"&gt;Cluster-autoscaler&lt;/a&gt; is a de facto tool for this job. However, this tool is designed for multiple cloud and bare metal environments, so it does not focus on AWS workloads. Slow starts, nongraceful shutdowns, and various problems with APIs are creating a need for an AWS-focused auto-scaling tool.&lt;/p&gt;

&lt;p&gt;Karpenter is only focusing on AWS workloads. You can create new instances and run pods on them within seconds. Create templates for capacity types(spot, on-demand), zone, architecture, instance category, type, etc. Check out the &lt;a href="https://karpenter.sh/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; for more details. When a pod can not find enough resources to run, Karpenter immediately creates a new node, and the pod can run within seconds. Also whenever you have more nodes than you need, Karpenter can delete nodes for you. Don’t forget to install an &lt;a href="https://github.com/aws/aws-node-termination-handler" rel="noopener noreferrer"&gt;aws-node-termination-handler&lt;/a&gt; for graceful shutdown. Karpenter is also focusing on cost optimization. If you can replace an on-demand node for a spot node, then Karpenter will &lt;a href="https://aws.amazon.com/blogs/containers/optimizing-your-kubernetes-compute-costs-with-karpenter-consolidation/" rel="noopener noreferrer"&gt;replace&lt;/a&gt; them. Or it can change its size as well.&lt;/p&gt;

&lt;p&gt;I prefer to use &lt;a href="https://github.com/awslabs/eks-node-viewer" rel="noopener noreferrer"&gt;eks-node-viewer&lt;/a&gt; to understand node scaling and resource allocations. Karpenter is updating very regularly. They keep adding new features and fixing bugs frequently, so check official documentation before using it and keep it up-to-date.&lt;/p&gt;

&lt;p&gt;AWS Fargate allows you to run containers without managing servers or clusters. It’s a great option for workloads with variable resource requirements, as you pay per vCPU and memory used. Fargate can simplify operations and potentially reduce costs for suitable workloads. This one is operation cost vs. compute resource cost. It can be efficient for new projects for head start.&lt;/p&gt;

&lt;h2&gt;
  
  
  EKS Networking and its Impact on Costs
&lt;/h2&gt;

&lt;p&gt;Understand and optimize networking components in EKS to control costs. Choose the right VPC and subnet strategies and consider using AWS PrivateLink to reduce data transfer costs. Be mindful of network traffic between availability zones and regions, as these can incur additional charges.&lt;/p&gt;

&lt;p&gt;For simple development environments, a single AZ can reduce network costs. EKS control planes run on multi-AZ architecture, but node groups and Fargate instances can run on a single AZ. Also, fewer instance numbers can mean less network communication, reducing costs. Choose instance types and sizes wisely.&lt;/p&gt;

&lt;p&gt;Use cache mechanisms as much as possible. The cache can reduce network load and response times. For static workloads, use CloudFront. This can also be useful for web applications. CloudFront + ALB can be used together. Once the user enters the AWS network infrastructure, requests will be faster than before. Caching responses from databases can affect network usage and performance as well. Use it wisely because it might increase resource usage unless properly planned. Don’t forget, cache is king.&lt;/p&gt;

&lt;p&gt;If you are using other AWS services like S3, DynamoDB, ECR etc. then use VPC Endpoints. Normally, when you send a request to S3 inside EKS pods, it traverses the internet. AWS charges you for sending the packets to the internet, and it is a less secure and slower way to access a service. VPC endpoints allow you to access some AWS services without traversing the internet. Which is faster, cheaper, and more secure.&lt;/p&gt;

&lt;p&gt;NAT Gateways can be really expensive, especially if you have a multi-AZ architecture. There is a trick here. If you use one NAT Gateway for each AZ, you can reduce inter-AZ communication. However, this will increase the price for NAT Gateway usage. You must check whether NAT Gateways or inter-AZ communications are cheaper.&lt;/p&gt;

&lt;p&gt;VPC to VPC communication is expensive. You can use public internet, but it comes with a price. Use VPC Peering(2 VPC) and Transit Gateway(+3 VPC) as much as possible. VPC Peering is a great choice for the same AZ because there are no network charges.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1hvv5qexd2myfzo5oar.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1hvv5qexd2myfzo5oar.png" alt="AWS VPC Network" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage Optimization
&lt;/h2&gt;

&lt;p&gt;Optimize storage costs by choosing the appropriate Elastic Block Store (EBS) or Elastic File System (EFS) for your needs. Use gp2 or gp3 volumes for a balance between performance and cost. Delete unattached volumes and snapshots regularly, and consider lifecycle policies for automated management.&lt;/p&gt;

&lt;p&gt;For increased performance for workloads like databases, EC2 instance stores are really powerful choices. Instance store disks can be used with EC2 user-data to format disks. EKS can use them as HostPath persistent volumes. &lt;a href="https://github.com/kubernetes-sigs/sig-storage-local-static-provisioner" rel="noopener noreferrer"&gt;Local Persistent Volume Static Provisioner&lt;/a&gt; is also another alternative for local disks. Be careful with them because once you lose your instance, the disks are gone as well. It is useful for high performance for less cost.&lt;/p&gt;

&lt;p&gt;Minimize container image sizes and use Amazon Elastic Container Registry (ECR) to store and manage your Docker images. Implement lifecycle policies in ECR to automatically clean up unused images, reducing storage costs.&lt;/p&gt;

&lt;p&gt;EKS control plane logs can be disabled for test environments. They can create network workload and storage usage. Storing logs in S3 with the correct tiering mechanism and configuring retention periods are also important.&lt;/p&gt;

&lt;p&gt;What about backups? Amazon Data Lifecycle Manager(DLM) can be leveraged to manage automated backups and retention policies. EBS snapshots and AMIs can be controlled with DLM. However, currently DLM is not working via EBS CSI Driver. They can be used seperately. Open-source solutions like &lt;a href="https://velero.io/" rel="noopener noreferrer"&gt;Velero&lt;/a&gt; can be used for automated backups and retention policies inside EKS.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Cost Management Tools
&lt;/h2&gt;

&lt;p&gt;Leverage AWS-native tools like AWS Cost Explorer, Budgets, and Trusted Advisor to monitor and optimize EKS costs. These tools provide insights into your spending patterns, resource utilization, and recommendations for cost savings. Using Cost Explorer with hourly changes can help you understand how your changes affected costs.&lt;/p&gt;

&lt;p&gt;Billing and Budget &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html" rel="noopener noreferrer"&gt;alerts&lt;/a&gt; must be used on day 1. Otherwise, it will be late once you use more resources than you need. AWS Cost Explorer is updating daily. Hourly price changes can be opt-in. Tagging all of the resources will improve the observability of cost management. With the automation tools like AWS Cloudformation and Terraform, it is much easier. AWS Trusted Advisor is also a must-do operation. Check recommendations and take actions. It will help you to increase usage level of reserved instances and saving plans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and Compliance on a Budget
&lt;/h2&gt;

&lt;p&gt;Implement security best practices without breaking the bank. Use AWS Identity and Access Management (IAM) roles and policies for granular control over EKS resources. Leverage AWS Certificate Manager for SSL/TLS certificates and AWS Key Management Service (KMS) for encryption, optimizing costs while maintaining security.&lt;/p&gt;

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

&lt;p&gt;Price optimization can be tricky. You need capacity, faster communication, and highly available workloads, but it is expensive. Try to use this blog post as a guide and figure out your infrastructure needs. Use fewer resources for testing and development environments and don’t think much about high availability, resilience, etc. Production environments are something else so be prepared for what is coming. Use VPC Endpoints and regularly check network costs and what is causing them.&lt;/p&gt;

&lt;p&gt;Lastly, AWS Well-Architected Framework helps customers to make decisions about their workloads. Cost Optimization Piller whitepaper can help you with cost reduction. Check out the official &lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/cost-optimization-pillar/welcome.html" rel="noopener noreferrer"&gt;whitepaper&lt;/a&gt;. Practice Cloud Financial Management can give you an idea about AWS cost management.&lt;/p&gt;

&lt;p&gt;This article is also available on &lt;a href="https://medium.com/vngrs/elastic-kubernetes-service-cost-optimization-a-comprehensive-guide-part-two-17077e59aede" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>eks</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>costoptimization</category>
    </item>
    <item>
      <title>Elastic Kubernetes Service Cost Optimization: A Comprehensive Guide - Part One</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Tue, 07 May 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/elastic-kubernetes-service-cost-optimization-a-comprehensive-guide-part-one-19go</link>
      <guid>https://dev.to/fatihkoc/elastic-kubernetes-service-cost-optimization-a-comprehensive-guide-part-one-19go</guid>
      <description>&lt;p&gt;Elastic Kubernetes Service (EKS) stands out in the Kubernetes ecosystem for its powerful cloud-based solutions. However, many organizations struggle with managing costs effectively. This &lt;a href="https://fatihkoc.net/posts/eks-cost-optimization-2/" rel="noopener noreferrer"&gt;two-part&lt;/a&gt; blog series dives deep into strategies for reducing expenses on both Kubernetes and AWS sides. We start with universally applicable Kubernetes tips, enriched with contextual understanding and real-world scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring and Logging: The First Step in Cost Management
&lt;/h2&gt;

&lt;p&gt;Effective cost management begins with robust monitoring and logging. Start with cloud provider dashboards and virtualization technology interfaces for basic insights. Install metrics-server to leverage commands like &lt;code&gt;kubectl top pods&lt;/code&gt; and &lt;code&gt;kubectl top nodes&lt;/code&gt; for real-time data. For comprehensive infrastructure monitoring, consider the &lt;a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack" rel="noopener noreferrer"&gt;kube-prometheus-stack&lt;/a&gt;, which includes Grafana, Prometheus, node-exporter, Alertmanager, and optionally Thanos.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install metrics-server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install kube-prometheus-stack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While some prefer the ELK stack for logging, its resource intensity for simple syslogs is a drawback. A more efficient alternative for pod logs is the combination of Loki and Promtail, seamlessly integrating with Grafana for monitoring and logging and significantly reducing storage requirements.&lt;/p&gt;

&lt;p&gt;Enterprise APM solutions like Dynatrace, Datadog, New Relic, Splunk, etc. can be used for monitoring solutions, but since we are discussing cost management, they are not necessary for small clusters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6n18d71bsdmt6sr8dtnq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6n18d71bsdmt6sr8dtnq.png" alt="kube-prometheus-stack dashboard" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource Management: Tuning for Efficiency
&lt;/h2&gt;

&lt;p&gt;Now, we have a monitoring and logging stack. Which pods or nodes are using how many resources can be seen? Node types and sizes that fit our applications can be chosen. If you are new to Kubernetes, you can see some blog posts discussing whether to use requests/limits for containers. Long story short, start with requests. Watch your containers, check average usage, and add %25 more requests for containers average usage. Using limits can create massive chaos in your environment unless you know why you are using it. If your application needs that limit, then use it. If you are not sure about that, don’t use limits.&lt;/p&gt;

&lt;p&gt;Requests are allocation resources for your containers, so the kube-scheduler can decide which nodes can run your applications while scheduling. Pods are running with guaranteed resources. Limits basically kill your pods when they reach limits, so if you have a problem with your application, they will keep dying. High risk, low reward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Horizontal Pod Autoscaler(HPA): Scaling with Demand
&lt;/h2&gt;

&lt;p&gt;HPA is pivotal in cloud environments where payment is based on usage. It automatically adjusts pod numbers in response to resource demand changes. Custom metrics can trigger scaling activities, for which &lt;a href="https://keda.sh/" rel="noopener noreferrer"&gt;KEDA&lt;/a&gt; offers a versatile solution. Effective HPA implementation ensures you scale resources efficiently, aligning costs with actual needs.&lt;/p&gt;

&lt;p&gt;Pod count is important because it affects your node count as well. Unfortunately, there are limits for container counts. The maximum number of pods can be limited to Container Network Interface plugins, IP address allocations, network interfaces, resource constraints etc.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Managing Workloads: Strategic Updates and Scaling
&lt;/h2&gt;

&lt;p&gt;Regular updates and pruning of deployments and rolling updates contribute to cost optimization. Cluster auto-scalers, a feature supported differently by various cloud providers, can significantly reduce costs by dynamically adjusting resources. The &lt;a href="https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler" rel="noopener noreferrer"&gt;Cluster Autoscaler&lt;/a&gt; project is a valuable tool in this regard. The next blog post will cover the specifics of AWS’s approach.&lt;/p&gt;

&lt;p&gt;For bare metal installations, you don’t have too many options. You can use autoscalers for infrastructures like OpenStack. At best, you can reduce resource usage, but license costs will always be the same. For example, if you are using bare metal Openshift, you still pay a license fee for worker machines whether you use them or not.&lt;/p&gt;

&lt;p&gt;On the other hand, cloud providers are using the pay-as-you-use concept. Even one less worker node can affect machine, network, disk, and operation costs. Most of the cloud providers offer autoscalers designed for Kubernetes workloads like Karpenter. With the right configuration, the impact can be huge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optimize Images: Balancing Efficiency and Security
&lt;/h2&gt;

&lt;p&gt;We have discussed the infrastructure part; now, we can focus on our applications and images. Minimizing images can provide efficiency, security, speed, and cost reduction. You don’t want to pull a 200 GB Ubuntu image when you can do the same job with a 20 MB stretch image, right? This is especially important for security. Fewer binaries and libraries mean a lower attach surface. Besides, storing images can be costly as well. Try to use scratch or alpine images as much as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Namespace Management and Resource Quotas: Effective Segregation
&lt;/h2&gt;

&lt;p&gt;Segregating different environments like dev, staging, and production within the same cluster can help monitor and set resource quotas. Setting quotas on namespaces to prevent a single team or project from consuming all cluster resources. A development namespace with a set quota to ensure that resource-intensive test workloads don’t impact production services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-resources
spec:
  hard:
    requests.cpu: "1"
    requests.memory: 1Gi
    limits.cpu: "2"
    limits.memory: 2Gi
    requests.nvidia.com/gpu: 4

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hidden Enemy: Network Costs
&lt;/h2&gt;

&lt;p&gt;Using multiple region availability zones can be good for high availability, but it comes with a price. Be aware of costs between regions, availability zones, and virtual machines. Every provider has a different policy about network costs. Caching strategies can also reduce network traffic and data transfer costs.&lt;/p&gt;

&lt;p&gt;Continuous monitoring and making simple improvements daily is the most efficient way to decrease network costs. Use cost calculator applications for cloud providers while deciding on infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Cost Optimization: Tools
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2eqit49efmotdokg8jqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2eqit49efmotdokg8jqd.png" alt="eks-node-viewer" width="800" height="138"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;eWe have discussed most of the tips and tricks about Kubernetes cost management. Still, we want to be sure about everything. Cost monitoring tools like OpenCost and Kubecost can be used. For understanding cluster-wide resource usage, &lt;a href="https://github.com/awslabs/eks-node-viewer" rel="noopener noreferrer"&gt;eks-node-viewer&lt;/a&gt; is a really useful tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part Two
&lt;/h2&gt;

&lt;p&gt;Kubernetes cost optimization can be used for every platform, including cloud and bare-metal infrastructures. In the &lt;a href="https://fatihkoc.net/posts/eks-cost-optimization-2/" rel="noopener noreferrer"&gt;next chapter&lt;/a&gt;, we will discuss the cost optimization of AWS components. We will focus on leveraging AWS services, understanding pricing models, and implementing AWS-specific features for cost-effective EKS management.&lt;/p&gt;

&lt;p&gt;This article is also available on &lt;a href="https://medium.com/vngrs/elastic-kubernetes-service-cost-optimization-a-comprehensive-guide-part-one-ef51b3c64ed5" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>eks</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>costoptimization</category>
    </item>
    <item>
      <title>Ultimate Kubernetes Deployment Guide</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Sat, 03 Sep 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/ultimate-kubernetes-deployment-guide-1o5p</link>
      <guid>https://dev.to/fatihkoc/ultimate-kubernetes-deployment-guide-1o5p</guid>
      <description>&lt;p&gt;With the rising of cloud technologies, companies had a chance to create, deploy and manage their applications without paying upfront. In the old days, you need to buy some rack, network cables, servers, coolers, etc. It was taking too much time, and generally, huge tech companies took advantage of their vendor-locking technology stacks. You didn’t have much choice, right? With the free software movement and foundations like &lt;a href="https://www.cncf.io/" rel="noopener noreferrer"&gt;CNCF&lt;/a&gt;, standardization of the technologies becomes much more important. Nobody wants vendor-locking because it kills disruptive ideas.&lt;/p&gt;

&lt;p&gt;Then suddenly Docker became popular(or is &lt;a href="https://blog.aquasec.com/a-brief-history-of-containers-from-1970s-chroot-to-docker-2016" rel="noopener noreferrer"&gt;it?&lt;/a&gt;) and companies realized they don’t need to use the same stack for every problem with containerization technologies. You can choose your programming language, database, caching mechanisms, etc. If it works once, it works every time. Right? We all know it is not true. Distributed systems give us scalability, agility, availability, and all of the other good advantages. But what was the price for it? Operational costs and bigger complexity problems. You can run your container with whatever you want but in the end, you have a much more complex system than ever. How can you trace, monitor, and gets logs from every container? How about authentication, authorization, secret management, traffic management, and access control?&lt;/p&gt;

&lt;p&gt;Those problems created solutions like &lt;a href="https://kubernetes.io/" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt;. With this blog post and simple template &lt;a href="https://github.com/fatihkc/ultimate-k8s-deployment-guide" rel="noopener noreferrer"&gt;project&lt;/a&gt;, we can learn more about Kubernetes deployments. It is a huge area to explore but I think deployments are a great place to start. At the end of the day, you need to enter this world with just a simple deployment. Kubernetes is generally used for companies that are using microservices and none of them changed their infrastructure in a day. They all started with a simple deployment. You don’t need to think about tracing, monitoring, secret management, etc. Don’t worry. Kubernetes will lead you to these problems. Focus on them one at a time.&lt;/p&gt;

&lt;p&gt;Before reading the rest of the post, be sure that you have an idea about Kubernetes &lt;a href="https://kubernetes.io/docs/concepts/overview/components/" rel="noopener noreferrer"&gt;components&lt;/a&gt; and how they are working with each other. I am just gonna focus on the deployment side of the Kubernetes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;I can’t write every single aspect of Deployments in a simple blog post. My goal is to give you a simple template project that you can use for your projects. I am gonna explain the template project in detail. You can use it as a reference for your projects. Don’t worry there will be lots of tips and tricks along the way. Also, I’m gonna keep updating the project with new features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo Requirements:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/fatihkc/ultimate-k8s-deployment-guide" rel="noopener noreferrer"&gt;Check template project&lt;/a&gt;. It can be slightly different from blog post.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/engine/install/ubuntu/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kind.sigs.k8s.io/docs/user/quick-start/#installation" rel="noopener noreferrer"&gt;Kind&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://helm.sh/docs/intro/install/" rel="noopener noreferrer"&gt;Helm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clone template project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/fatihkc/ultimate-k8s-deployment-guide.git

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Diagram
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqsjsmp1oqtfanyhu842.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiqsjsmp1oqtfanyhu842.png" alt="Diagram" width="429" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Kind
&lt;/h2&gt;

&lt;p&gt;Kind is a great tool for creating Kubernetes clusters without losing time. Normally you need virtual machines for installation but Kind is using containers as virtual machines. That is brilliant technology. Simple to use, need much fewer resources, and is fast. You can also use it for testing different Kubernetes versions and checking if your application is ready for an upgrade or not. You can always choose &lt;a href="https://github.com/kelseyhightower/kubernetes-the-hard-way" rel="noopener noreferrer"&gt;hard way&lt;/a&gt;. It is really good for understanding what is going on with your cluster. 3 years ago I was installing Kubernetes with Ansible and Vagrant. Check &lt;a href="https://github.com/fatihkc/end-to-end-devops" rel="noopener noreferrer"&gt;this&lt;/a&gt; project if you want to know more about it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kind create cluster --config kind/cluster.yaml --name guide --image=kindest/node:v1.23.6

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Helm
&lt;/h2&gt;

&lt;p&gt;Helm is a package manager for Kubernetes applications. If you are new to Helm, I don’t recommend creating a default template(helm create chart). Because it is more complicated than it needs to be. I am gonna write about important things for deployments and explain them one by one. Let’s start with our Chart.yaml file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v2
name: helm-chart
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.0.0"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All you need to focus on is the version and appVersion field. Why do we have two different version variables? Let’s say you have an application that runs with 1.0.0. You can increase it via semantic versioning tools and then pass it to the Helm chart. I recommend increasing the chart version is very important for this scenario. Also using appVersion for your image version tag is recommended. But you can update your chart without increasing the application version too. You can add or create new YAML files, and make improvements and appVersion can stay the same. Then you should only increase the version variable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Templates
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04rjnjtytmkz53xyyn0r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F04rjnjtytmkz53xyyn0r.png" alt="Templates" width="408" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Templates are your YAML files that use to create Kubernetes resources. The important thing is to divide them by their resource type and name them resource-type.yaml. Let’s create a simple deployment and check what they are using for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: apps/v1 # API version to use for all resources in the manifest
kind: Deployment # Kind of the resource to create
metadata:
  name: {{ .Release.Name }} # Name of the resource to create
  namespace: {{ .Release.Namespace }} # Namespace of the resource to create
  labels:
    app: {{ .Values.deployment.name }} # Label to apply to the resource
spec:
  replicas: {{ .Values.deployment.replicas }} # Number of replicas to create
  selector:
    matchLabels:
      app: {{ .Values.deployment.name }} # Label to select the resource
  template:
    metadata:
      labels:
        app: {{ .Values.deployment.name }} # Label to apply to the resource
    spec:
      containers:
        - name: {{ .Values.deployment.container.name }} # Name of the container in the pod
          image: {{ .Values.deployment.container.image }} # Image to use for the container
          imagePullPolicy: {{ .Values.deployment.container.imagePullPolicy }} # Image pull policy to use for the container
          ports:
            - containerPort: {{ .Values.deployment.container.port }} # Port to expose on the container
              protocol: {{ .Values.deployment.container.protocol }} # Protocol to use for the port

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This might look a little bit complicated. How do we know where to write these keywords? There is two spec, multiple labels, and so many brackets. Well, you just need to check the &lt;a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; and learn how to read a YAML file. YAML files are all about spaces and keywords. As you can see, we say that I need to use apps/v1 API for my Deployment kind of resource. Kubernetes is just a big API server. Don’t forget that. There are many API’s in Kubernetes. Check them with;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl api-version
kubectl api-resources

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we give a name to the resource. Some people use names like “ReleaseName-deployment” but I prefer keeping it the same with the release name. Deployment is responsible for running multiple containers so we choose how many replicas we want. Selectors using for finding which pods we are gonna manage with Deployment. In the background, a Replica Set will be created it will be responsible for running the pods. If you are not familiar with Replica Sets, check &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/" rel="noopener noreferrer"&gt;this&lt;/a&gt; article.&lt;/p&gt;

&lt;p&gt;Then we gave information about our containers. I only have one but it is enough for now. You can declare more containers in a pod.&lt;/p&gt;

&lt;h2&gt;
  
  
  Values
&lt;/h2&gt;

&lt;p&gt;What about the values that are all over the deployment.yaml? Well, they are saving so much time and you can able to use different values files for different environment files. You use most of the things as values and easily change them with values.yaml file. Like cluster.yaml for Kind. You can use them with {{ .Values.deployment.name }}. Just check &lt;a href="https://github.com/fatihkc/ultimate-k8s-deployment-guide/blob/main/helm-chart/values.yaml" rel="noopener noreferrer"&gt;values.yaml&lt;/a&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment strategy
&lt;/h2&gt;

&lt;p&gt;Deployment strategy is a way to control how many replicas are created. You can use different strategies for different deployments. I prefer &lt;a href="https://kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/" rel="noopener noreferrer"&gt;RollingUpdate&lt;/a&gt; for seamless upgrades.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  strategy:
    type: RollingUpdate # Type of the deployment strategy
    rollingUpdate:
      maxSurge: {{ .Values.deployment.strategy.rollingUpdate.maxSurge }} # Maximum number of pods to create
      maxUnavailable: {{ .Values.deployment.strategy.rollingUpdate.maxUnavailable }} # Maximum number of pods to delete

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Environment variables
&lt;/h2&gt;

&lt;p&gt;Environment variables are used to pass information to the containers. You can use them in your containers with $VARIABLE_NAME. I am using it for changing the USER variable. It will affect my application output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  env:
    - name: USER
      value: {{ .Values.deployment.env.USER }} # Value of the environment variable

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ConfigMap
&lt;/h2&gt;

&lt;p&gt;ConfigMap and environment variables are very similar. The only difference is when you change your environment variables changes, the pods are gonna restart and then take the new value. But ConfigMap is not gonna restart your pod. You need to restart it manually. If your application can handle it, it is a good idea to use ConfigMap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}
  namespace: {{ .Release.Namespace }}
data:
  USER: "{{ .Values.USER }}"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Secrets and Volumes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    volumeMounts:
    - mountPath: "/tmp" # Mount path for the container
      name: test-volume # Name of the volume mount
      readOnly: true # Whether the volume is read-only
volumes:
  - name: test-volume # Name of the volume
    secret: # Volume to use for the secret
      secretName: test # Name of the secret to use for the volume

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secrets are very similar to environment variables with small differences. If your variable contains sensitive information like database passwords and credential files for third-party services, you can use secrets. All of your YAML files are stored in etcd but secrets are encrypted. ConfigMaps are not encrypted. When you list your secret in Kubernetes you will see it is base64 encoded. You can use base64 decode to get the original value. So where is encryption and why does base64 encode? Let’s say your application uses credential files like Firebase. It has multiple lines with different syntax than just a simple password. Encoding it keeps the spaces and new lines. If you want to completely hide your secret, you can use solutions like Hashicorp &lt;a href="https://www.vaultproject.io/" rel="noopener noreferrer"&gt;Vault&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I could use it like ConfigMap but I prefer using volumes. Because your application logs can show your environment variables like secrets. If you prefer volumes, it is a long shot to expose secrets. If you have a configuration file, then it will be the perfect fit. You don’t use volume as your main storage. Instead, use persistent volumes. It is a really deep dive, just check &lt;a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/" rel="noopener noreferrer"&gt;this&lt;/a&gt; article. One last thing, don’t use secret.yaml like me. It is not a good practice. Don’t keep it in your repository. You can use it as a template and create it with Helm.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ k exec -it webserver-5d7d6ccc8d-l8ftz cat /tmp/secret-file.txt
top secret

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Container resources
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resources:
  requests:
    cpu: {{ .Values.deployment.container.resources.requests.cpu }} # CPU to request for the container
    memory: {{ .Values.deployment.container.resources.requests.memory }} # Memory to request for the container
  limits:
    cpu: {{ .Values.deployment.container.resources.limits.cpu }} # CPU to limit for the container
    memory: {{ .Values.deployment.container.resources.limits.memory }} # Memory to limit for the container

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This part is a little bit tricky. Kube-scheduler is responsible for a simple decision. Which pod, which node? If you have a request for a pod, make sure that you will have enough CPU and memory in a node. It allocates your requests. You can use much more resources in a node but minimums are clear for kube-scheduler. Limits are responsible for top usage. Do you need them? If you are not an expert in this area, simply no. Because in a high traffic situation where pods need more resources, you are limiting it and that means not giving a response to high demand. We don’t want that right? Unless you have a different situation with your infrastructure. I am gonna use it for demo purposes. Use &lt;a href="https://github.com/kubernetes-sigs/metrics-server" rel="noopener noreferrer"&gt;metrics-server&lt;/a&gt; for monitoring your pod’s usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health probes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;livenessProbe:
  httpGet:
    path: {{ .Values.deployment.container.livenessProbe.path}} # Path to check for liveness
    port: {{ .Values.deployment.container.livenessProbe.port }} # Port to check for liveness
  initialDelaySeconds: {{ .Values.deployment.container.livenessProbe.initialDelaySeconds }} # Initial delay before liveness check
  timeoutSeconds: {{ .Values.deployment.container.livenessProbe.timeoutSeconds }} # Timeout before liveness check
readinessProbe:
  httpGet:
    path: {{ .Values.deployment.container.readinessProbe.path }} # Path to check for readiness
    port: {{ .Values.deployment.container.readinessProbe.port }} # Port to check for readiness
  initialDelaySeconds: {{ .Values.deployment.container.readinessProbe.initialDelaySeconds }} # Initial delay before readiness check
  timeoutSeconds: {{ .Values.deployment.container.readinessProbe.timeoutSeconds }} # Timeout before readiness check

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Health probes are really important for the availability of your application. You don’t want to send a request to the failed pod, right? Liveness probes are used for understanding whether or not your pod can accept traffic. If it fails, it kills the pod and restarts it. Let’s say it is ready for accepting connections. But is it ready for action? Readiness probes are used for checking third-party dependencies. Can you reach the database? Is another related service alive or not? Can pod achieve its job?&lt;/p&gt;

&lt;p&gt;Liveness probes must be simple like sending a ping. On the other hand, readiness probes must be sure that they can accept traffic. Otherwise, other ready pods will handle the traffic. A piece of advice, don’t check third-party dependencies in liveness because it can kill all of your applications. Check this awesome &lt;a href="https://blog.colinbreck.com/kubernetes-liveness-and-readiness-probes-how-to-avoid-shooting-yourself-in-the-foot/" rel="noopener noreferrer"&gt;article&lt;/a&gt; about health probes. These probes are not coming out of the box, unfortunately. Your application code must handle them by exposing the application’s health status. We have HTTP health probes. What about gRPC connections? Well, that’s another &lt;a href="https://github.com/grpc-ecosystem/grpc-health-probe" rel="noopener noreferrer"&gt;adventure&lt;/a&gt; to discover.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Context
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  runAsNonRoot: true
  runAsUser: 1000
  capabilities:
    drop:
    - ALL

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The security context is one of the most important things about deployment. This mechanism is changing with the new Kubernetes releases but the idea is still the same. Do you want to allow privilege escalation? No. Read-only filesystem? Hell yeah. And more things like that. Don’t forget to drop all capabilities. Check out &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; about security contexts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Affinity
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;affinity:
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 10 # Weight of the node affinity
        preference:
          matchExpressions:
          - key: kubernetes.io/arch # Key of the node affinity
            operator: In # Operator to use for the node affinity
            values:
            - arm64 # Value of the node affinity
      - weight: 10 # Weight of the node affinity
        preference:
          matchExpressions:
          - key: kubernetes.io/os # Key of the node affinity
            operator: In # Operator to use for the node affinity
            values:
            - linux # Value of the node affinity

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Affinity is really good for large environments. You can have different types of nodes. They can have different architecture, operating systems, sizes, etc. For example, I’m using an affinity for &lt;a href="https://karpenter.sh/" rel="noopener noreferrer"&gt;Karpenter&lt;/a&gt;. Karpenter allows you to scale out within 60 seconds. If your pod needs resources and can’t find them, Karpenter creates a new node and assign your pod. I’m using EC2 Spot instances for this purpose. You just need to choose your deployments and make them scalable with Karpenter. Affinity is making sure that this pod will run on the nodes that have required labels. In our example, I used architecture and operating system but If you have solutions like Karpenter, It becomes much more important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Topology Spread Constraints
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;topologySpreadConstraints:
  - maxSkew: 1 # Maximum number of pods to spread
    topologyKey: "topology.kubernetes.io/zone" # Key to use for spreading
    whenUnsatisfiable: ScheduleAnyway # Action to take if the constraint is not satisfied
    labelSelector:
      matchLabels:
        app: {{ .Release.Name }} # Label to select the resource
  - maxSkew: 1
    topologyKey: "kubernetes.io/hostname" # Key to use for spreading
    whenUnsatisfiable: ScheduleAnyway # Action to take if the constraint is not satisfied
    labelSelector:
      matchLabels:
        app: {{ .Release.Name }} # Label to select the resource

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we are sure that our application will run on arm64 architecture with Linux operating system. What if all of our pods run on the same node? If that node is terminated then our application will not available. We must spread them. Topology spread constraints allow us to make sure our pod will run on different hosts, zone, or any other topology. For demo purposes I only chose hostname.&lt;/p&gt;

&lt;h2&gt;
  
  
  Service
&lt;/h2&gt;

&lt;p&gt;Service is a Kubernetes resource that allows you to expose your application. It is a load balancer for your pods. You can use it for internal or external traffic. I choose NodePort for my service. It is a simple way to expose your application. You can use it for testing purposes. It exposes a port on each node. You can access your application with the node’s IP address and the port.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }} # Name of the service
  namespace: {{ .Release.Namespace }} # Namespace of the service
spec:
  type: NodePort
  selector:
    app: {{ .Values.service.selector.name }}
  ports:
    - protocol: {{ .Values.service.ports.protocol }}
      port: {{ .Values.service.ports.port }}
      targetPort: {{ .Values.service.ports.targetPort }}
      nodePort: {{ .Values.service.ports.nodePort }}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Action
&lt;/h2&gt;

&lt;p&gt;Now we are ready to deploy our application. We have a chart and resource templates. We can use the helm install command for that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm upgrade --install webserver helm-chart -f helm-chart/values.yaml -n $NAMESPACE

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I generally use “upgrade –install” commands instead of “install” because I can use the same command for updating my application. If anything is missing, it will install it. If something changed, it will update it. Let’s check our resources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get all -n $NAMESPACE

NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/webserver-5d7d6ccc8d-l8ftz 1/1 Running 0 83m 10.244.1.11 guide-worker &amp;lt;none&amp;gt; &amp;lt;none&amp;gt;
pod/webserver-5d7d6ccc8d-ncdxq 1/1 Running 0 83m 10.244.2.7 guide-worker2 &amp;lt;none&amp;gt; &amp;lt;none&amp;gt;
pod/webserver-5d7d6ccc8d-xpljk 1/1 Running 0 82m 10.244.1.13 guide-worker &amp;lt;none&amp;gt; &amp;lt;none&amp;gt;

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/kubernetes ClusterIP 10.96.0.1 &amp;lt;none&amp;gt; 443/TCP 46h &amp;lt;none&amp;gt;
service/webserver NodePort 10.96.29.110 &amp;lt;none&amp;gt; 8080:30000/TCP 23h app=webserver

NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/webserver 3/3 3 3 23h webserver fatihkoc/app:latest app=webserver

NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
replicaset.apps/webserver-5d7d6ccc8d 3 3 3 83m webserver fatihkoc/app:latest app=webserver,pod-template-hash=5d7d6ccc8d
replicaset.apps/webserver-68667fc8c7 0 0 0 23h webserver fatihkoc/app:latest app=webserver,pod-template-hash=68667fc8c7
replicaset.apps/webserver-689c788945 0 0 0 23h webserver fatihkoc/app:latest app=webserver,pod-template-hash=689c788945

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything looks ready. Let’s access our app and see how it works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl http://localhost:30000
Hello, Fatih! Your secret is: top secret

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might think why I used port 30000. Well, the easiest way to access your application is to use NodePort. You can use LoadBalancer or Ingress but I don’t want to make it complicated. In Kind configuration, I exposed port 8080 to 30000. You can change it on your own.&lt;/p&gt;

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

&lt;p&gt;In this blog post, we checked most of the components about Deployment. Of course, there are tons of things to learn. I tried to make it simple and easy to understand. I hope you enjoyed it. If you have any questions, feel free to &lt;a href="https://www.fatihkoc.net/contact/" rel="noopener noreferrer"&gt;ask&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>cloud</category>
      <category>devops</category>
      <category>helm</category>
    </item>
    <item>
      <title>Cloud Resume Challenge</title>
      <dc:creator>Fatih Koç</dc:creator>
      <pubDate>Mon, 22 Aug 2022 00:00:00 +0000</pubDate>
      <link>https://dev.to/fatihkoc/cloud-resume-challenge-3chc</link>
      <guid>https://dev.to/fatihkoc/cloud-resume-challenge-3chc</guid>
      <description>&lt;p&gt;Hello everyone, I am Fatih. I am working in the cloud area for over two years. Currently working as a DevOps Engineer at FTech Labs, we’re building a super app for crypto exchanges. Today our topic is &lt;a href="https://cloudresumechallenge.dev/" rel="noopener noreferrer"&gt;Cloud Resume Challenge&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;I had a &lt;a href="https://github.com/fatihkc/" rel="noopener noreferrer"&gt;website&lt;/a&gt; built with Django. I’ve dockerized it and deployed it to the EC2 instance. Turns out it keeps failing with a new error every time. Most of the time docker-compose can not manage to make it available on 7/24. I decided to keep it shut down. I kept thinking about creating a new website in a serverless way but it never happened. Always too busy with something and do not want to waste my time on a simple website. Then I found out Cloud Resume Challenge. It’s giving you the basic steps of a serverless portfolio website for beginners but there are so many people doing it with over +10 years of experience in IT. I thought I can test my skills with a challenge and make a website that doesn’t waste my time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloud Resume Challenge Steps
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0sntvp1d7t608afzkgh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm0sntvp1d7t608afzkgh.jpg" alt="Cloud Resume Challenge Steps" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can check the steps on the &lt;a href="https://cloudresumechallenge.dev/docs/the-challenge/" rel="noopener noreferrer"&gt;website&lt;/a&gt;. They change in time. It seems basic right? You can probably finish it within a month just using your spare time. Also, there are additional steps for making it amazing. You can also check out the book written for this challenge. It is really helpful for getting a cloud job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technologies
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmozap8uofxz12d5b5yi9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmozap8uofxz12d5b5yi9.png" alt="Diagram" width="540" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can check my GitHub &lt;a href="https://github.com/fatihkc" rel="noopener noreferrer"&gt;repository&lt;/a&gt; for this challenge. I am not gonna give the details about technical things. You can check the repo and read the comments. I skipped the certification part. I will explain it later.&lt;/p&gt;

&lt;p&gt;I knew most of the technical stuff in the challenge, so I decided to use Terraform for giving a little bit of excitement to the challenge.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Hugo: I chose Hugo for static website generation because it uses Go and I love it. I have a little repository for my Golang practices. It is easy to use and fast. I could use HTML+CSS for my resume but I wanted to add an image. I like the way it looks and I already knew a few things about HTML, CSS, Javascript, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS: I am using it for my job and It is the leader of the cloud providers. Just don’t forget to create an IAM user and billing alerts. You don’t want to pay thousands of dollars for a simple website, right?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Terraform: I am huge fan of Terraform. In FTech Labs, we are creating, configuring, and scaling with IaC tools. Manual things are banned because they can cause damage and it becomes harder to fix them. Don’t forget to create an S3 bucket for states and DynamoDB for state locking. After that, you can create your cloud infrastructure with HCL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GitHub Actions: I used different CI tools but never used GH Actions before. It is fast, easy to configure, and free for public repositories. I am managing my build, deployment, and test steps. Terraform is triggering manually because I don’t want to use it every time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What did I learn?
&lt;/h2&gt;

&lt;p&gt;I used new tools and turns out I am a fast learner. A few years ago this challenge can take forever. However, the main thing about this challenge is the &lt;a href="https://cloudresumechallenge.dev/book/" rel="noopener noreferrer"&gt;book&lt;/a&gt; itself. I learned a few tricks about understanding technologies, documentation reading, and career changes. I also started blogging with this post. I have another blog idea and I am excited about it.&lt;/p&gt;

&lt;p&gt;I figured out my resume had a huge problem. I was generally working alone with infrastructures and used so many different tools, architectures, etc. But I couldn’t prove it. GitHub repositories were created years ago, I don’t have a certificate, I don’t blog about anything, etc. If you want to get a cloud job, your resume must be sexy. If you can’t pass the HR resume process, your technical knowledge is not that important. Nobody will ask you about your previous work, test your skills or give assignments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next?
&lt;/h2&gt;

&lt;p&gt;That is why I am going to focus on blogging, certificates, and making more repositories about my skills. I passed the certification process in the challenge because I wanted to focus on them later on. The challenge gave me a moral boost and a good project for my resume. Thanks to &lt;a href="https://jamesclear.com/atomic-habits" rel="noopener noreferrer"&gt;Atomic Habits&lt;/a&gt;, I just start a thing and see how it goes. I am going to keep blogging and create a project for the blog posts. The most important thing is certifications. So many people are arguing about it whether you need to take it or leave. Most of the job posts are not demanding but I talked to a lot of HR people and they say I will give you a boost depending on the certification. Especially in consulting world, you must have a few certificates to improve your company’s partnership level.&lt;/p&gt;

&lt;p&gt;I got my first job before graduation. Technical interviews were generally easy because when they asked about my GNU/Linux skills, I told them I gave a few lectures in different boot camps. I added them to my resume. I wasn’t planning to become an instructor but when people see your resume they will know that you know a few things about basic GNU/Linux administration. Generally, people were skipping GNU/Linux questions. Certificates are the same. When you have them, most people won’t even ask about them, and HR people like them very much. If you prepared and learned things about certificate topics during the preparation, interviews will be much easier than before.&lt;/p&gt;

&lt;p&gt;AWS Cloud Practitioner, AWS Solutions Architect - Associate, and Certified Kubernetes Administrator are my main targets for now. Why Cloud Practitioner? I can easily get it without even looking at example questions. My main goal is to learn the process of certification in AWS. It won’t be hard and I won’t be stressed about it. That way, Solutions Architect - Associate will be easier than before. Also, an additional certificate won’t hurt. CKA will be last one and then I will create a new roadmap for my career.&lt;/p&gt;

&lt;p&gt;I hope you enjoyed my first blog post. Let’s create another one!&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>cloudnative</category>
      <category>serverless</category>
      <category>hugo</category>
    </item>
  </channel>
</rss>
