<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kubernetes v2 on Alperen Doc</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/</link><description>Recent content in Kubernetes v2 on Alperen Doc</description><generator>Hugo</generator><language>en</language><atom:link href="https://blog.ulutasalperen.com/docs/kubernetes_v2/index.xml" rel="self" type="application/rss+xml"/><item><title>helm</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/helm/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/helm/</guid><description>&lt;h1 id="helm"&gt;Helm&lt;a class="anchor" href="#helm"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;In the &lt;a href="../../kubernetes/kubernetes-yaml-configurations/"&gt;v1 notes&lt;/a&gt; we wrote every manifest by hand: deployments, configmaps, services, gateways. That&amp;rsquo;s fine for a handful of resources you own. It falls apart the moment you want Prometheus + Grafana + Loki - three interdependent applications that together ship &lt;em&gt;hundreds&lt;/em&gt; of YAML lines, with version matrices between them.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://helm.sh/docs/"&gt;Helm&lt;/a&gt; is &lt;em&gt;the&lt;/em&gt; package manager for Kubernetes. Instead of raw manifests you install &lt;em&gt;charts&lt;/em&gt;: versioned, templatized bundles that ship with sane defaults you can override. Think apt/npm, but the package installs into your cluster.&lt;/p&gt;</description></item><item><title>kustomize</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/kustomize/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/kustomize/</guid><description>&lt;h1 id="kustomize"&gt;Kustomize&lt;a class="anchor" href="#kustomize"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;Helm solves packaging with templates and values. &lt;a href="https://kustomize.io/"&gt;Kustomize&lt;/a&gt; refuses templates entirely: keep your plain YAML as the &lt;strong&gt;base&lt;/strong&gt;, and build &lt;strong&gt;overlays&lt;/strong&gt; that patch in environment differences. Since v1.27 &lt;code&gt;kubectl&lt;/code&gt; ships with it built in - no extra binary:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl apply -k ./my-app&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-mental-model"&gt;The Mental Model&lt;a class="anchor" href="#the-mental-model"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;k8s-manifests/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── base/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── api-deployment.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── api-configmap.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── web-deployment.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── kustomization.yaml # lists resources&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── overlays/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── dev/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ │ └── kustomization.yaml # patches: replicas: 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── prod/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ ├── kustomization.yaml&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;│ └── patch-replicas.yaml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;base&lt;/strong&gt; = the truth of the application (works as-is on any cluster)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;overlay&lt;/strong&gt; = a set of patches on top of the base (environment-specific deltas)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The rule that makes it work: overlays only &lt;em&gt;touch&lt;/em&gt; fields they declare. Everything else passes through unchanged.&lt;/p&gt;</description></item><item><title>cluster-extensions</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/cluster-extensions/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/cluster-extensions/</guid><description>&lt;h1 id="extension-interfaces"&gt;Extension Interfaces&lt;a class="anchor" href="#extension-interfaces"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;Everything we&amp;rsquo;ve installed so far - the &lt;a href="../multi-node-kubeadm/"&gt;CNI plugin&lt;/a&gt; that gives pods their network, the &lt;a href="../longhorn/"&gt;Longhorn&lt;/a&gt; CSI driver that gives them disks, the &lt;a href="../multi-node-kubeadm/"&gt;containerd&lt;/a&gt; runtime that runs them - shares one thing in common: none of them are &amp;ldquo;Kubernetes&amp;rdquo;. They&amp;rsquo;re plugins, and Kubernetes just provides the sockets they plug into.&lt;/p&gt;&#10;&lt;p&gt;Those sockets are the extension interfaces, and knowing them turns &amp;ldquo;the cluster is broken&amp;rdquo; into &amp;ldquo;the &lt;em&gt;network plugin&lt;/em&gt; is broken&amp;rdquo;:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Arayüz&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Ne işe yarar&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Kim implement eder&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CNI&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Pod&amp;rsquo;lara IP ve network connectivity&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Calico, Flannel, Cilium, Weave&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CSI&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Volume provisioning, attach, mount&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Longhorn, cloud disk drivers, Ceph&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CRI&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Container&amp;rsquo;ları gerçekten çalıştırmak&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;containerd, CRI-O, (dockershim gitti)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;The contract matters more than the vendor. CNI says &amp;ldquo;here&amp;rsquo;s a pod, give it an IP and a veth&amp;rdquo;; every plugin answers that the same &lt;em&gt;way of thinking&lt;/em&gt;, differently in the details. Same for CSI (&lt;code&gt;CreateVolume&lt;/code&gt;/&lt;code&gt;Attach&lt;/code&gt;/&lt;code&gt;Mount&lt;/code&gt; are verbs every driver must know) and CRI (&lt;code&gt;RunPodSandbox&lt;/code&gt;/&lt;code&gt;CreateContainer&lt;/code&gt;/&lt;code&gt;StopContainer&lt;/code&gt;).&lt;/p&gt;</description></item><item><title>network-policy</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/network-policy/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/network-policy/</guid><description>&lt;h1 id="networkpolicy"&gt;NetworkPolicy&lt;a class="anchor" href="#networkpolicy"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;When we built custom bridge networks in &lt;a href="../../docker/docker-network/"&gt;Docker&lt;/a&gt;, the goal was isolation: containers on &lt;code&gt;network A&lt;/code&gt; can&amp;rsquo;t see containers on &lt;code&gt;network B&lt;/code&gt;. &lt;a href="https://kubernetes.io/docs/concepts/services-networking/network-policies/"&gt;NetworkPolicy&lt;/a&gt; is the cluster-level equivalent - with a twist that trips everyone up at first.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The default is &amp;ldquo;allow everything&amp;rdquo;&lt;/strong&gt;. No policy = every pod can talk to every pod, everywhere. But the moment a pod is selected by &lt;em&gt;any&lt;/em&gt; NetworkPolicy (Ingress or Egress, even an empty one), that flips: all traffic to/from that pod is denied unless another policy explicitly allows it. Policies are &lt;strong&gt;additive&lt;/strong&gt;: the union of all rules selecting a pod defines its allowed traffic. There is no &amp;ldquo;deny&amp;rdquo; rule - you deny by omission.&lt;/p&gt;</description></item><item><title>security-context</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/security-context/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/security-context/</guid><description>&lt;h1 id="security-context"&gt;Security Context&lt;a class="anchor" href="#security-context"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;In &lt;a href="../../docker/manage-docker-as-non-root-user/"&gt;manage-docker-as-non-root-user&lt;/a&gt; we made the case for not running containers as root on Docker. Kubernetes has the same problem: most container images still run their process as &lt;code&gt;uid 0&lt;/code&gt;. A &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/"&gt;Security Context&lt;/a&gt; sets the security options for a pod or container - and there&amp;rsquo;s a whole family of them, not just &amp;ldquo;the user&amp;rdquo;.&lt;/p&gt;&#10;&lt;h2 id="the-field-set"&gt;The Field Set&lt;a class="anchor" href="#the-field-set"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Container level&lt;/strong&gt; (per container in &lt;code&gt;spec.containers[].securityContext&lt;/code&gt;):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;securityContext&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runAsNonRoot&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runAsUser&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;runAsGroup&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1000&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;allowPrivilegeEscalation&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;readOnlyRootFilesystem&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;capabilities&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;drop&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;ALL&amp;#34;&lt;/span&gt;]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;seccompProfile&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;type&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;RuntimeDefault&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;runAsNonRoot: true&lt;/code&gt; - kubelet &lt;em&gt;refuses to start&lt;/em&gt; the container if the image&amp;rsquo;s effective user resolves to 0. The most valuable single line in this chapter&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;runAsUser&lt;/code&gt;/&lt;code&gt;runAsGroup&lt;/code&gt; - override the image&amp;rsquo;s user with numeric UID/GID (names don&amp;rsquo;t work; &lt;code&gt;id -u&lt;/code&gt; of the image user is what you need)&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;allowPrivilegeEscalation: false&lt;/code&gt; - blocks &lt;code&gt;setuid&lt;/code&gt; binaries and capability-gain inside the container&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;readOnlyRootFilesystem: true&lt;/code&gt; - container filesystem becomes immutable; writes must target mounted volumes&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;capabilities.drop: [&amp;quot;ALL&amp;quot;]&lt;/code&gt; - drops Linux capabilities; add back only what the app genuinely needs (&lt;code&gt;add: [&amp;quot;NET_BIND_SERVICE&amp;quot;]&lt;/code&gt; for binding ports &amp;lt; 1024)&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;seccompProfile: RuntimeDefault&lt;/code&gt; - applies the container runtime&amp;rsquo;s default syscall filter. In restricted PSA this is required, and most runtimes&amp;rsquo; default is a reasonable baseline&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;strong&gt;Pod level&lt;/strong&gt; (&lt;code&gt;spec.securityContext&lt;/code&gt;) - applies to every container and to volumes:&lt;/p&gt;</description></item><item><title>kubernetes-troubleshooting</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/kubernetes-troubleshooting/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/kubernetes-troubleshooting/</guid><description>&lt;h1 id="troubleshooting"&gt;Troubleshooting&lt;a class="anchor" href="#troubleshooting"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;Everything that follows builds on one rule: &lt;strong&gt;status codes first, logs second, &lt;code&gt;describe&lt;/code&gt; always&lt;/strong&gt;. After writing dozens of deployments in the &lt;a href="../../kubernetes/"&gt;v1 notes&lt;/a&gt;, here&amp;rsquo;s the operator-grade field guide.&lt;/p&gt;&#10;&lt;h2 id="pod-status-quick-reference"&gt;Pod Status Quick Reference&lt;a class="anchor" href="#pod-status-quick-reference"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Status&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Anlamı&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;İlk bakılacak yer&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;Pending&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Node&amp;rsquo;a schedule edilemedi&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;describe&lt;/code&gt; → Events (resources? taints? affinity? quota?)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ImagePullBackOff&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Image çekilemiyor (tag/repo/auth)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;describe&lt;/code&gt; → Events + image adı&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;CrashLoopBackOff&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Başlıyor, çöküyor, üstel beklemeyle tekrar&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;logs --previous&lt;/code&gt; + exit code&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ErrImagePull&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ImagePullBackOff&lt;/code&gt;&amp;lsquo;un ilk hali&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Aynı&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;Running&lt;/code&gt; ama broken&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Process yaşıyor, app ölü&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;logs&lt;/code&gt; + &lt;code&gt;port-forward&lt;/code&gt; ile elle dene&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;OOMKilled&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Memory limiti aşıldı&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;describe&lt;/code&gt; → &lt;code&gt;Last State&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h2 id="exit-code-cheat-sheet"&gt;Exit Code Cheat Sheet&lt;a class="anchor" href="#exit-code-cheat-sheet"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The exit code in &lt;code&gt;Last State: Terminated&lt;/code&gt; usually tells the whole story:&lt;/p&gt;</description></item><item><title>control-plane-troubleshooting</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/control-plane-troubleshooting/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/control-plane-troubleshooting/</guid><description>&lt;h1 id="control-plane-components"&gt;Control Plane Components&lt;a class="anchor" href="#control-plane-components"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;In the &lt;a href="../../kubernetes/kubernetes-namespaces/"&gt;namespaces&lt;/a&gt; notes we listed what lives in &lt;code&gt;kube-system&lt;/code&gt; and said &amp;ldquo;you don&amp;rsquo;t want to mess with it&amp;rdquo;. Good advice - but understanding it is non-negotiable, because when the control plane misbehaves &lt;em&gt;every&lt;/em&gt; debugging tool you own stops working. &lt;code&gt;kubectl get pods&lt;/code&gt; that returns &lt;code&gt;the connection to the server was refused&lt;/code&gt; is not a pod problem.&lt;/p&gt;&#10;&lt;p&gt;The four components, and what they own:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Bileşen&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Ne yapar&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Nasıl koşar&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kube-apiserver&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Tek API yüzeyi; her &lt;code&gt;kubectl&lt;/code&gt; buraya gider&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;static pod&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;etcd&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Tüm cluster state&amp;rsquo;inin tek kaynağı(key-value storage)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;static pod&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kube-scheduler&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Pod&amp;rsquo;ları node&amp;rsquo;lara (birden fazla kube-scheduler olusturulabilir) yerleştirir&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;static pod&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;kube-controller-manager&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Reconcile döngüleri (deployment, rs, node&amp;hellip;)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;static pod&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;On a &lt;a href="../multi-node-kubeadm/"&gt;kubeadm&lt;/a&gt; cluster these run as &lt;strong&gt;static pods&lt;/strong&gt; - not Deployments, not systemd units. The kubelet reads YAML files from &lt;code&gt;/etc/kubernetes/manifests/&lt;/code&gt; and keeps whatever is in there alive, forever. Delete a manifest and the component dies; restore the file and it comes back. That directory is the single most important path on a control-plane node.&lt;/p&gt;</description></item><item><title>gitops-argocd</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/gitops-argocd/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/gitops-argocd/</guid><description>&lt;h1 id="gitops"&gt;GitOps&lt;a class="anchor" href="#gitops"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;My site deploys with GitHub Actions + rsync over SSH (see &lt;a href="../../version_control_systems/github_workflow/"&gt;github_workflow&lt;/a&gt;). That&amp;rsquo;s a &lt;strong&gt;push&lt;/strong&gt; model: CI runs, then &lt;em&gt;pushes&lt;/em&gt; files to the server. GitOps flips it: the cluster &lt;em&gt;pulls&lt;/em&gt; its own desired state from git and reconciles itself continuously.&lt;/p&gt;&#10;&lt;p&gt;The four principles (from &lt;a href="https://opengitops.dev/"&gt;OpenGitOps&lt;/a&gt;):&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Declarative&lt;/strong&gt; - the entire desired state is expressed declaratively (manifests, charts, kustomize overlays)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Versioned and immutable&lt;/strong&gt; - state lives in git; every change is a commit, every deploy is a merge&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Pulled automatically&lt;/strong&gt; - an agent inside the cluster pulls; nothing outside pushes into the cluster&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Continuously reconciled&lt;/strong&gt; - actual state is compared with desired state &lt;em&gt;all the time&lt;/em&gt;, and drift is corrected&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;That fourth principle is what push-based CD structurally cannot give you. With push CD, the 2am &lt;code&gt;kubectl edit&lt;/code&gt; sticks forever - nobody re-runs the pipeline for a change they didn&amp;rsquo;t make. With pull CD, drift is detected within seconds and reverted.&lt;/p&gt;</description></item><item><title>vpa-cluster-autoscaler</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/vpa-cluster-autoscaler/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/vpa-cluster-autoscaler/</guid><description>&lt;h1 id="the-three-autoscalers"&gt;The Three Autoscalers&lt;a class="anchor" href="#the-three-autoscalers"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;In the &lt;a href="../../kubernetes/kubernetes-scaling-horizontal/"&gt;scaling notes&lt;/a&gt; we built an HPA. But HPA is one of &lt;em&gt;three&lt;/em&gt; autoscalers, and they operate at different layers - mixing them up is a classic design mistake:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Autoscaler&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Katman&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Soru&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Ne yapar&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;HPA&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Pod sayısı&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;ldquo;Kaç pod?&amp;rdquo;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Metriğe göre replica sayısını değiştirir&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;VPA&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Pod kaynakları&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;ldquo;Bir pod ne kadar ister?&amp;rdquo;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;requests&lt;/code&gt;/&lt;code&gt;limits&lt;/code&gt;&amp;lsquo;i otomatik ayarlar (restart ile)&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;Cluster Autoscaler&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Node sayısı&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&amp;ldquo;Kaç node?&amp;rdquo;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Pending pod&amp;rsquo;lar için node ekler, boş node&amp;rsquo;u çıkarır&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;The layering matters because they solve &lt;em&gt;different&lt;/em&gt; problems: HPA handles variable load for a fixed-per-pod footprint; VPA fixes wrong resource estimates; CA fixes fleet capacity.&lt;/p&gt;</description></item><item><title>taints-affinity-quotas</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/taints-affinity-quotas/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/taints-affinity-quotas/</guid><description>&lt;h1 id="taints-and-tolerations"&gt;Taints and Tolerations&lt;a class="anchor" href="#taints-and-tolerations"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;On a single-node Minikube cluster scheduling is trivial - there&amp;rsquo;s nowhere else to put a pod. The moment you add nodes (&lt;a href="../multi-node-kubeadm/"&gt;multi-node notes&lt;/a&gt; are coming), scheduling decisions become real. &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/"&gt;Taints&lt;/a&gt; mark a node as &amp;ldquo;keep away&amp;rdquo;, and &lt;em&gt;tolerations&lt;/em&gt; let specific pods opt in anyway:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kubectl taint nodes &amp;lt;node-name&amp;gt; workload&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ai:NoSchedule&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The three effects behave differently:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;NoSchedule&lt;/code&gt;: new pods without a matching toleration can&amp;rsquo;t be scheduled here (running pods are left alone)&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;PreferNoSchedule&lt;/code&gt;: soft version - scheduler &lt;em&gt;tries&lt;/em&gt; to avoid, but won&amp;rsquo;t reject&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;NoExecute&lt;/code&gt;: also &lt;em&gt;evicts&lt;/em&gt; pods already running there that lack the toleration - the aggressive one&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The matching toleration in a pod spec:&lt;/p&gt;</description></item><item><title>pod-priority-disruption</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/pod-priority-disruption/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/pod-priority-disruption/</guid><description>&lt;h1 id="daemonset"&gt;DaemonSet&lt;a class="anchor" href="#daemonset"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;Deployments spread replicas wherever there&amp;rsquo;s room. Sometimes &amp;ldquo;wherever there&amp;rsquo;s room&amp;rdquo; is the wrong answer - a log shipper, a CSI node agent, or a CNI plugin needs exactly one pod per node, no more, no less. That&amp;rsquo;s a &lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/"&gt;DaemonSet&lt;/a&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;apps/v1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;DaemonSet&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;node-agent&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;selector&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;matchLabels&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;app&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;node-agent&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;template&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;labels&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;app&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;node-agent&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;tolerations&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;operator&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Exists&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;containers&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;agent&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;image&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;busybox:1.36&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;command&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;sh&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;sleep infinity&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Look at that toleration - &lt;code&gt;operator: Exists&lt;/code&gt; tolerates &lt;em&gt;everything&lt;/em&gt;, which is exactly what you want when the DaemonSet must land on control-plane nodes too. Most system DaemonSets (kube-proxy, Longhorn managers, Calico&amp;rsquo;s node agents) ship with this, because a plugin that avoids the control plane is a plugin that half-works.&lt;/p&gt;</description></item><item><title>multi-node-kubeadm</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/multi-node-kubeadm/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/multi-node-kubeadm/</guid><description>&lt;h1 id="multi-node-from-minikube-to-a-real-cluster"&gt;Multi-Node: From Minikube to a Real Cluster&lt;a class="anchor" href="#multi-node-from-minikube-to-a-real-cluster"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;Minikube is a single-node cluster - we&amp;rsquo;ve repeated that warning since the very first &lt;a href="../../kubernetes/kubernetes-nodes-basic/"&gt;v1 notes&lt;/a&gt;. For homelab work the standard path to &lt;em&gt;real&lt;/em&gt; multi-node Kubernetes is &lt;code&gt;kubeadm&lt;/code&gt; - vanilla k8s, everything in your hands, the same tool the managed distros are built on. This note walks through a kubeadm install on VMs, from node prep to a joined, working cluster, with &lt;a href="https://cilium.io/"&gt;Cilium&lt;/a&gt; as the CNI - eBPF datapath, working &lt;a href="../network-policy/"&gt;NetworkPolicy&lt;/a&gt; enforcement and no kube-proxy.&lt;/p&gt;</description></item><item><title>kubeadm-upgrade-etcd</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/kubeadm-upgrade-etcd/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/kubeadm-upgrade-etcd/</guid><description>&lt;h1 id="cluster-upgrades"&gt;Cluster Upgrades&lt;a class="anchor" href="#cluster-upgrades"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;The &lt;a href="../multi-node-kubeadm/"&gt;multi-node&lt;/a&gt; notes ended with a one-liner: &amp;ldquo;upgrade the control plane first, one minor version at a time&amp;rdquo;. That&amp;rsquo;s the policy - this is the practice.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/kubeadm/kubeadm-upgrade/"&gt;Upgrading&lt;/a&gt; a cluster is the most common maintenance task you will ever run, and the most common way to break one. The rules that keep it boring:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;One minor version at a time.&lt;/strong&gt; &lt;code&gt;1.30&lt;/code&gt; → &lt;code&gt;1.31&lt;/code&gt;, never &lt;code&gt;1.30&lt;/code&gt; → &lt;code&gt;1.32&lt;/code&gt;. The version skew policy allows the apiserver to lead kubelets by one minor; anything more is unsupported and the upgrade will refuse&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Control plane first, workers after.&lt;/strong&gt; The new apiserver must understand old kubelets, not the reverse&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;One node at a time.&lt;/strong&gt; Drain → upgrade → uncordon → next&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Read the output.&lt;/strong&gt; &lt;code&gt;kubeadm upgrade plan&lt;/code&gt; exists precisely because upgrades are not a single command&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;blockquote class='book-hint '&gt;&#10;&lt;p&gt;The apt packages are version-pinned with &lt;code&gt;apt-mark hold&lt;/code&gt; from the install notes. Every upgrade starts with &lt;code&gt;apt-mark unhold&lt;/code&gt; and ends with &lt;code&gt;apt-mark hold&lt;/code&gt; again - forgetting the re-hold is how a future &lt;code&gt;apt upgrade&lt;/code&gt; silently moves your cluster a minor version.&lt;/p&gt;&#10;&lt;/blockquote&gt;</description></item><item><title>ha-control-plane</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/ha-control-plane/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/ha-control-plane/</guid><description>&lt;h1 id="high-availability"&gt;High Availability&lt;a class="anchor" href="#high-availability"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;The &lt;a href="../multi-node-kubeadm/"&gt;multi-node&lt;/a&gt; notes left a check on the wall: &lt;code&gt;--control-plane-endpoint=&amp;lt;VIP&amp;gt;&lt;/code&gt; &amp;ldquo;makes later HA possible&amp;rdquo;. This is later.&lt;/p&gt;&#10;&lt;p&gt;A cluster with one control-plane node has one single point of failure: kill that node and &lt;code&gt;kubectl&lt;/code&gt; stops working, the scheduler stops placing pods, and every controller stops reconciling. The &lt;em&gt;workloads&lt;/em&gt; keep running (kubelets and containers don&amp;rsquo;t need the apiserver to stay alive), but the cluster stops being managed. For a homelab that&amp;rsquo;s an annoyance; for anything real it&amp;rsquo;s an outage.&lt;/p&gt;</description></item><item><title>devops-control-plane</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/devops-control-plane/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/devops-control-plane/</guid><description>&lt;h1 id="who-owns-the-control-plane"&gt;Who Owns the Control Plane&lt;a class="anchor" href="#who-owns-the-control-plane"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;Everything until now assumed &lt;em&gt;we&lt;/em&gt; own the cluster: we built it on &lt;a href="../multi-node-kubeadm/"&gt;multi-node VMs&lt;/a&gt;, we &lt;a href="../control-plane-troubleshooting/"&gt;SSH&amp;rsquo;d into it&lt;/a&gt; when it broke, we &lt;a href="../ha-control-plane/"&gt;joined control-plane nodes&lt;/a&gt; by hand. That&amp;rsquo;s the homelab model. The moment a second team shows up, the question changes from &amp;ldquo;how do I run this&amp;rdquo; to &lt;strong&gt;&amp;ldquo;who is allowed to do what&amp;rdquo;&lt;/strong&gt; - and that&amp;rsquo;s a platform engineering question, not a kubectl one.&lt;/p&gt;&#10;&lt;p&gt;The split that every real org lands on eventually:&lt;/p&gt;</description></item><item><title>cert-manager</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/cert-manager/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/cert-manager/</guid><description>&lt;h1 id="cert-manager"&gt;cert-manager&lt;a class="anchor" href="#cert-manager"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;In the v1 &lt;a href="../../kubernetes/kubernetes-ingress/"&gt;ingress&lt;/a&gt; notes we said that real clusters &amp;ldquo;get this issued automatically by cert-manager instead of hand-rolling it&amp;rdquo;. That was a promise we haven&amp;rsquo;t paid off yet. Let&amp;rsquo;s pay it off.&lt;/p&gt;&#10;&lt;p&gt;Here is what a TLS certificate actually requires, every single time:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;a private key&lt;/li&gt;&#10;&lt;li&gt;a certificate signing request built from that key&lt;/li&gt;&#10;&lt;li&gt;someone - a Certificate Authority - to sign it&lt;/li&gt;&#10;&lt;li&gt;a renewal before it expires (Let&amp;rsquo;s Encrypt certs last 90 days)&lt;/li&gt;&#10;&lt;li&gt;the result copied into a &lt;code&gt;Secret&lt;/code&gt; in the right namespace, in the right format&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Do that by hand for two hostnames and you have already lost an afternoon. &lt;a href="https://cert-manager.io/"&gt;cert-manager&lt;/a&gt; is a controller that does all five as a Kubernetes reconciliation loop: you declare &lt;em&gt;what&lt;/em&gt; you want, it keeps it true forever - including the renewal.&lt;/p&gt;</description></item><item><title>cert-manager-production</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/cert-manager-production/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/cert-manager-production/</guid><description>&lt;h1 id="cert-manager-in-production"&gt;cert-manager in Production&lt;a class="anchor" href="#cert-manager-in-production"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;The previous chapter taught the mechanism: &lt;code&gt;Issuer&lt;/code&gt;, &lt;code&gt;Certificate&lt;/code&gt;, &lt;code&gt;Secret&lt;/code&gt;, and a Gateway that serves it. Everything worked because we controlled every variable - a fake domain, a self-signed root, a laptop that trusts whatever we tell it to.&lt;/p&gt;&#10;&lt;p&gt;Production removes that control. The domain is public and someone else owns the DNS. The certificate has to be trusted by browsers you don&amp;rsquo;t administer. It expires on a schedule you don&amp;rsquo;t choose. And when the automation silently stops working, the outage looks like &amp;ldquo;the website is down&amp;rdquo; rather than &amp;ldquo;a controller is in a crash loop&amp;rdquo;.&lt;/p&gt;</description></item><item><title>prometheus-stack</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/prometheus-stack/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/prometheus-stack/</guid><description>&lt;h1 id="monitoring-with-kube-prometheus-stack"&gt;Monitoring with kube-prometheus-stack&lt;a class="anchor" href="#monitoring-with-kube-prometheus-stack"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;We&amp;rsquo;ve touched monitoring twice and never actually &lt;em&gt;ran&lt;/em&gt; it. The &lt;a href="../helm/"&gt;helm&lt;/a&gt; notes installed the plain &lt;code&gt;prometheus&lt;/code&gt; chart to teach values precedence, and &lt;a href="../cluster-extensions/"&gt;cluster-extensions&lt;/a&gt; installed &lt;code&gt;kube-prometheus-stack&lt;/code&gt; to show what an operator looks like. Neither taught you to operate a monitoring stack.&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s this chapter. By the end you&amp;rsquo;ll have Prometheus scraping the cluster, Grafana showing it, Alertmanager paging you when it matters, and - the part people skip - a &lt;code&gt;ServiceMonitor&lt;/code&gt; for your own application so its metrics land next to the system ones.&lt;/p&gt;</description></item><item><title>victoriametrics</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/victoriametrics/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/victoriametrics/</guid><description>&lt;h1 id="victoriametrics"&gt;VictoriaMetrics&lt;a class="anchor" href="#victoriametrics"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;The &lt;a href="../prometheus-stack/"&gt;prometheus-stack&lt;/a&gt; chapter gave you a working monitoring stack. This one asks a different question: if you were starting over, would you still pick Prometheus as the storage engine?&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://docs.victoriametrics.com/"&gt;VictoriaMetrics&lt;/a&gt; is a metrics database that speaks the same scrape protocol and mostly the same query language, but is built differently underneath - columnar storage, aggressive compression, no local TSDB churn. The usual reasons people move: less memory per series, longer retention on the same disk, and cardinality explosions that don&amp;rsquo;t take the database down with them.&lt;/p&gt;</description></item><item><title>longhorn</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/longhorn/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/longhorn/</guid><description>&lt;h1 id="longhorn"&gt;Longhorn&lt;a class="anchor" href="#longhorn"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;Everything we&amp;rsquo;ve done with storage so far lived on &lt;em&gt;one&lt;/em&gt; node. In the &lt;a href="../../kubernetes/kubernetes-persistent-volumes/"&gt;v1 notes&lt;/a&gt; a PVC was a local disk on (the only) Minikube node, and in the &lt;a href="../multi-node-kubeadm/"&gt;multi-node notes&lt;/a&gt; our workers each keep their own local directories. That means the day &lt;code&gt;kworker1&lt;/code&gt; dies, every PVC hosted on it dies with it - block storage was the last single point of failure in our homelab.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://longhorn.io/docs/"&gt;Longhorn&lt;/a&gt; fixes that: it&amp;rsquo;s a distributed block storage system built as a CSI driver, purpose-made for Kubernetes. Every volume it serves is split into an &lt;strong&gt;engine&lt;/strong&gt; plus &lt;strong&gt;N replicas&lt;/strong&gt; (default 3) that live on &lt;em&gt;different&lt;/em&gt; nodes - scheduled exactly like pods are.&lt;/p&gt;</description></item><item><title>postgresql-statefulset-replication</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/postgresql-statefulset-replication/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/postgresql-statefulset-replication/</guid><description>&lt;h1 id="postgresql-streaming-replication-the-manual-way"&gt;PostgreSQL Streaming Replication (The Manual Way)&lt;a class="anchor" href="#postgresql-streaming-replication-the-manual-way"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;In the &lt;a href="../../kubernetes/kubernetes-statefulsets/"&gt;StatefulSet notes&lt;/a&gt; we ran a single-replica PostgreSQL: pod dies, new pod mounts the same PVC, data survives. But there&amp;rsquo;s still exactly &lt;em&gt;one&lt;/em&gt; copy of the data, and if the primary goes down, your database is down until it comes back. On a 4-node cluster with &lt;a href="../longhorn/"&gt;Longhorn&lt;/a&gt; underneath, we can finally do the real thing: a primary plus streaming replicas.&lt;/p&gt;&#10;&lt;h2 id="two-different-layers-of-protection"&gt;Two Different Layers of Protection&lt;a class="anchor" href="#two-different-layers-of-protection"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This is the concept to get straight before touching YAML, because people conflate them constantly:&lt;/p&gt;</description></item><item><title>postgresql-longhorn-read-replicas</title><link>https://blog.ulutasalperen.com/docs/kubernetes_v2/postgresql-longhorn-read-replicas/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.ulutasalperen.com/docs/kubernetes_v2/postgresql-longhorn-read-replicas/</guid><description>&lt;h1 id="postgresql-read-replicas-with-cloudnativepg"&gt;PostgreSQL Read Replicas with CloudNativePG&lt;a class="anchor" href="#postgresql-read-replicas-with-cloudnativepg"&gt;#&lt;/a&gt;&lt;/h1&gt;&#10;&lt;p&gt;The &lt;a href="../postgresql-statefulset-replication/"&gt;manual setup&lt;/a&gt; taught us the mechanics: WAL, basebackup, standby. It also showed the catch - failover is a human job, and humans don&amp;rsquo;t get paged at 3am for fun. &lt;a href="https://cloudnative-pg.io/"&gt;CloudNativePG&lt;/a&gt; (CNPG) is the operator that automates the whole lifecycle: primary election, failover, replica management, backups - as a declarative &lt;code&gt;Cluster&lt;/code&gt; CRD. On top of &lt;a href="../longhorn/"&gt;Longhorn&lt;/a&gt; it turns our 4-node homelab into a genuinely production-shaped database platform.&lt;/p&gt;&#10;&lt;h2 id="what-the-operator-buys-you"&gt;What the Operator Buys You&lt;a class="anchor" href="#what-the-operator-buys-you"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Elle StatefulSet&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;CloudNativePG&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Replication kurulumu&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;pg_basebackup&lt;/code&gt;, &lt;code&gt;pg_hba&lt;/code&gt;, init script&amp;rsquo;ler&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;instances: 3&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Failover&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Manuel &lt;code&gt;pg_promote()&lt;/code&gt; + split-brain riski&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Otomatik: primary ölürse en güncel replica promote edilir&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Primary DNS&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kendi service şemanı yazarsın&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;-rw&lt;/code&gt; / &lt;code&gt;-ro&lt;/code&gt; / &lt;code&gt;-r&lt;/code&gt; service üçlüsü hazır&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Pg upgrade/resize&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kendin&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Operator rolling update yapar&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Monitoring&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Kendin&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Hazır metrics endpoint&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;The service trio is the part your apps touch - and it&amp;rsquo;s the read-replica payoff:&lt;/p&gt;</description></item></channel></rss>