<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[substack.yannick.dev]]></title><description><![CDATA[Articles about technology, IT infrastructure, programming languages. Written by a human.]]></description><link>https://substack.yannick.dev</link><image><url>https://substackcdn.com/image/fetch/$s_!g7Pw!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e41cc30-8464-4486-baf8-53177a089189_133x133.jpeg</url><title>substack.yannick.dev</title><link>https://substack.yannick.dev</link></image><generator>Substack</generator><lastBuildDate>Wed, 30 Sep 2026 17:22:12 GMT</lastBuildDate><atom:link href="https://substack.yannick.dev/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Yannick]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[yannickdev@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[yannickdev@substack.com]]></itunes:email><itunes:name><![CDATA[Yannick]]></itunes:name></itunes:owner><itunes:author><![CDATA[Yannick]]></itunes:author><googleplay:owner><![CDATA[yannickdev@substack.com]]></googleplay:owner><googleplay:email><![CDATA[yannickdev@substack.com]]></googleplay:email><googleplay:author><![CDATA[Yannick]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Lessons from Azure Kubernetes Service]]></title><description><![CDATA[Learnings and Opinions]]></description><link>https://substack.yannick.dev/p/lessons-from-azure-kubernetes-service</link><guid isPermaLink="false">https://substack.yannick.dev/p/lessons-from-azure-kubernetes-service</guid><dc:creator><![CDATA[Yannick]]></dc:creator><pubDate>Thu, 13 Aug 2026 18:55:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!g7Pw!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e41cc30-8464-4486-baf8-53177a089189_133x133.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is all from a medium size company we don&#8217;t use Kubernetes for the scaling, we use it because we can.</p><h2>It is a simple and stable life, but</h2><ul><li><p>ingress-nginx was deprecated, so we have to replace it, probably with traefik</p></li><li><p>kubenet networking on Azure was deprecated, Azure wants us to re-do our production cluster by 2028</p></li></ul><h2>Single Node Cluster works just fine</h2><p>For years we run single node cluster for the data-warehouse. There were no problems.</p><h2>Default StorageClass with ZRS and Retain</h2><p>The default StorageClass may use LRS and reclaimPolicy: Delete. Which means it creates the disks in a specific zone, and it will remove the disks automatically.</p><p>You want to create a StorageClass that uses ZRS and reclaimPolicy: Retain.</p><blockquote><p>We used LRS, one day the cluster moved to another zone, a zone different from the disks.</p></blockquote><h2>Avoid CPU limits</h2><p>CPU Limits are tricky and hard to get right. We don&#8217;t set them. <a href="https://github.com/inevolin/k8s-cpu-limits-analyzed">[more]</a></p><blockquote><p>One services used up all its CPU time and got stuck in GC pauses.</p></blockquote><h2>AKS Reader Role with Metrics</h2><p>Per default there is no Azure AKS role for developers. We create a new reader role that can access the metrics, pods, configmaps, but not secrets.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
        "permissions": [
            {
                "actions": [
                    "Microsoft.Authorization/*/read",
                    "Microsoft.ContainerService/managedClusters/listClusterUserCredential/action",
                    "Microsoft.Insights/alertRules/*",
                    "Microsoft.Resources/subscriptions/operationresults/read",
                    "Microsoft.Resources/subscriptions/read",
                    "Microsoft.Resources/subscriptions/resourceGroups/read",
                    "Microsoft.Support/*"
                ],
                "notActions": [],
                "dataActions": [
                    "Microsoft.ContainerService/managedClusters/apps/controllerrevisions/read",
                    "Microsoft.ContainerService/managedClusters/apps/daemonsets/read",
                    "Microsoft.ContainerService/managedClusters/apps/deployments/read",
                    "Microsoft.ContainerService/managedClusters/apps/replicasets/read",
                    "Microsoft.ContainerService/managedClusters/apps/statefulsets/read",
                    "Microsoft.ContainerService/managedClusters/autoscaling/horizontalpodautoscalers/read",
                    "Microsoft.ContainerService/managedClusters/batch/cronjobs/read",
                    "Microsoft.ContainerService/managedClusters/batch/jobs/read",
                    "Microsoft.ContainerService/managedClusters/configmaps/read",
                    "Microsoft.ContainerService/managedClusters/endpoints/read",
                    "Microsoft.ContainerService/managedClusters/events.k8s.io/events/read",
                    "Microsoft.ContainerService/managedClusters/events/read",
                    "Microsoft.ContainerService/managedClusters/extensions/daemonsets/read",
                    "Microsoft.ContainerService/managedClusters/extensions/deployments/read",
                    "Microsoft.ContainerService/managedClusters/extensions/ingresses/read",
                    "Microsoft.ContainerService/managedClusters/extensions/networkpolicies/read",
                    "Microsoft.ContainerService/managedClusters/extensions/replicasets/read",
                    "Microsoft.ContainerService/managedClusters/limitranges/read",
                    "Microsoft.ContainerService/managedClusters/namespaces/read",
                    "Microsoft.ContainerService/managedClusters/networking.k8s.io/ingresses/read",
                    "Microsoft.ContainerService/managedClusters/networking.k8s.io/networkpolicies/read",
                    "Microsoft.ContainerService/managedClusters/persistentvolumeclaims/read",
                    "Microsoft.ContainerService/managedClusters/pods/read",
                    "Microsoft.ContainerService/managedClusters/policy/poddisruptionbudgets/read",
                    "Microsoft.ContainerService/managedClusters/replicationcontrollers/read",
                    "Microsoft.ContainerService/managedClusters/resourcequotas/read",
                    "Microsoft.ContainerService/managedClusters/serviceaccounts/read",
                    "Microsoft.ContainerService/managedClusters/services/read",
                    "Microsoft.ContainerService/managedClusters/metrics/read",
                    "Microsoft.ContainerService/managedClusters/metrics.k8s.io/nodes/read",
                    "Microsoft.ContainerService/managedClusters/metrics.k8s.io/pods/read",
                    "Microsoft.ContainerService/managedClusters/resetMetrics/read",
                    "Microsoft.ContainerService/managedClusters/apis/metrics.k8s.io/read",
                    "Microsoft.ContainerService/managedClusters/nodes/read"
                ],
                "notDataActions": []
            }
        ]
    }
}</code></pre></div><h2>Weird Cost Optimisations</h2><ul><li><p>on AKS the amount of memory that is available for normal pods is tied to the max-pods configuration, per default it is 110, if you tune it down you get more memory</p></li><li><p>Azure AKS system pods request a lot of CPU resources (I think it is 1.5 CPU). This can become costly with a lot of small system nodes in your test environments.</p></li><li><p>Turn off log collection if not needed <a href="https://learn.microsoft.com/en-us/azure/azure-monitor/containers/kubernetes-data-collection-configmap#filter-container-logs">link</a></p></li></ul><h2>Other Weird Things</h2><p>You still need a preStop sleep 10 on every deployment, to give the ingress and traffic time to drain.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sleep
                  - '10'</code></pre></div><p></p><p>We name our Kubernetes ready endpoints <code>/kubernetes/ready</code> to send a clear message, this endpoint belongs to Kubernetes, you respond 200 when you want to receive traffic. The endpoint <code>/health/ready</code> was misunderstood and used for all kind of health checks (thanks Microsoft).</p><p>Liveness probes are only used for applications that have a history of hanging/freeze.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">          readinessProbe:
            initialDelaySeconds: 5
            periodSeconds: 10
            failureThreshold: 3
            successThreshold: 1
            httpGet:
              path: /kubernetes/ready
              port: 8080</code></pre></div><p></p><p>If a deployment has dependencies to secrets they are noted in a label, so the relationship can be queried withe the kubectl cli in scripts.</p><h2>Nodepool Image Hydration</h2><p>When we add a new nodepool we download all the images before we move workload over. The bash script</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">#!/usr/bin/env bash
set -euo pipe fail
# Note: each image must contain the true binary, otherwise it will fail

NAMESPACE="${1:-default}"

echo "Fetching images from namespace: $NAMESPACE"
init_containers=$(kubectl get pods -n "$NAMESPACE" \
  -o jsonpath="{.items[*].spec['initContainers','containers'][*].image}" \
  | tr ' ' '\n' | sort -u \
  | jq -Rn '[inputs | select(length &gt; 0)] | to_entries | map({
      name: ("prepuller-" + (.key + 1 | tostring)),
      image: .value,
      command: ["true"]
    })')

echo "Found $(echo "$init_containers" | jq length) images, generating prepuller.json"

jq -n --argjson ic "$init_containers" '{
  apiVersion: "apps/v1",
  kind: "DaemonSet",
  metadata: { name: "prepuller" },
  spec: {
    selector: { matchLabels: { name: "prepuller" } },
    template: {
      metadata: { labels: { name: "prepuller" } },
      spec: {
        initContainers: $ic,
        containers: [{
          name: "pause",
          image: "registry.k8s.io/pause:latest"
        }]
      }
    }
  }
}' &gt; prepuller.json

echo "Applying the generated prepuller.json to start pulling images..."
echo "  kubectl apply -n $NAMESPACE -f prepuller.json"
kubectl apply -n $NAMESPACE -f prepuller.json

echo "Waiting for the prepuller DaemonSet to be ready..."
echo "  kubectl wait -n $NAMESPACE --for=condition=ready pod -l name=prepuller --timeout=300s"
kubectl wait -n $NAMESPACE --for=condition=ready pod -l name=prepuller --timeout=300s

echo "Deleting puller DaemonSet..."
echo "  kubectl delete -n $NAMESPACE daemonset prepuller"
kubectl delete -n $NAMESPACE daemonset prepuller

rm prepuller.json
echo "Finished."</code></pre></div><h2>Simple Helm Deployments</h2><p>We keep our Kubernetes definition simple, only pass in the image URL during deployment. Helm does a good job and with the atomic option it rolls back on any error during deployment. </p><p>In bigger projects we generate the Helm Charts (which are simple Kubernetes YAML files) from custom YAML templates, with configurations, written in YAML.</p><p>The ingress configurations (the configuration of what URL maps to which deployment) used to live next to the code, but we decouple it to prevent older branches from re-deploying older ingress. </p><blockquote><p>We are a small company so simple solution go a long way.</p></blockquote><h2>Helm Chart</h2><p>An example of a very simple and flat helm chart.</p><p>values.yaml</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">deploymentName: NAME
TargetEnvironment: prod
port: 4200
replicas: 2
requests:
  cpu: 500m
  memory: 2000Mi
limits:
  memory: 2000Mi
</code></pre></div><p>deployment.txt (it is not valid yaml)</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: '{{ .Values.deploymentName }}'
  labels:
    app.kubernetes.io/name: '{{ .Values.deploymentName }}'
    app.kubernetes.io/part-of: backend
spec:
  replicas: {{ .Values.replicas }}
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: '{{ .Values.deploymentName }}'
  template:
    metadata:
      labels:
        app.kubernetes.io/name: '{{ .Values.deploymentName }}'
        app.kubernetes.io/part-of: backend
    spec:
      automountServiceAccountToken: false
      enableServiceLinks: false
      terminationGracePeriodSeconds: 60
      securityContext:
        runAsNonRoot: true
      containers:
        - image: '{{ .Values.image }}'
          imagePullPolicy: IfNotPresent
          name: '{{ .Values.deploymentName }}'
          env:
            - name: TARGET_ENVIRONMENT
              value: '{{ .Values.TargetEnvironment }}'
            - name: PORT
              value: '{{ .Values.port }}'
            - name: POD_DEPLOYMENT_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.labels['app']
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            - name: POD_MEMORY_LIMIT
              valueFrom:
                resourceFieldRef:
                  resource: limits.memory
                  divisor: 1Mi
          ports:
            - containerPort: {{ .Values.port }}
          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sleep
                  - '10'
          readinessProbe:
            initialDelaySeconds: 1
            periodSeconds: 5
            failureThreshold: 3
            successThreshold: 1
            httpGet:
              path: /kubernetes/ready
              port: {{ .Values.port }}
          resources:
            requests:
              cpu: {{ .Values.requests.cpu }}
              memory: {{ .Values.requests.memory }}
            limits:
              memory: {{ .Values.limits.memory }}
</code></pre></div><p>service.txt</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;yaml&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: '{{ .Values.deploymentName }}'
  labels:
    app.kubernetes.io/part-of: backend
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: {{ .Values.port }}
  selector:
    app.kubernetes.io/name: '{{ .Values.deploymentName }}'
</code></pre></div><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Node.js and JavaScript in 2026]]></title><description><![CDATA[Learnings and Opinions]]></description><link>https://substack.yannick.dev/p/nodejs-and-javascript-in-2026</link><guid isPermaLink="false">https://substack.yannick.dev/p/nodejs-and-javascript-in-2026</guid><dc:creator><![CDATA[Yannick]]></dc:creator><pubDate>Tue, 04 Aug 2026 19:11:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!g7Pw!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8e41cc30-8464-4486-baf8-53177a089189_133x133.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It is 2026, I still us nvm to manage my local node installation, most of the time I just run <code>nvm install --lts</code></p><p>I tried pnpm, it seems faster, but it introduces a new lock file, so I stick with npm. </p><p>To be more secure disable post install scripts in the <code>.npmrc</code> configuration.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">save-exact=true
min-release-age=5
ignore-scripts=true
</code></pre></div><h1>Notes on Packages</h1><p>dotenv is not needed anymore, node can handle an env file with node <code>--env-file-if-exists=file</code></p><p>A few other packages can be replaced:</p><ul><li><p>node-fetch &#8594; use the built in fetch()</p></li><li><p>Test frameworks &#8594; node:test</p></li><li><p>chalk / kleur &#8594; util.styleText()</p></li><li><p>glob &#8594; fs.glob()</p></li><li><p>rimraf &#8594; fs.rm({ recursive: true })</p></li><li><p>mkdirp &#8594; fs.mkdir({ recursive: true })</p></li><li><p>uuid (v4) &#8594; crypto.randomUUID()</p></li><li><p>Nodemon &#8594; node can watch files for changes with &#8212;watch</p></li></ul><p>Standard library has now a node: prefix so use</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
...</code></pre></div><h1>knip</h1><p>I love the tool knip, it highlights all unused imports, packages. With this you can do a good deep cleanup. example config knip.json</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "entry": ["src/index.tsx"],
  "project": ["src/**"],
  "ignore": [
    "/src/openapi/**",
  ],
  "exclude": ["enumMembers"]
}</code></pre></div><h1>Other Tools</h1><ul><li><p>check what is outdated: npx npm-check-updates -i --format group </p></li><li><p>check for critical: npm audit --audit-level=critical </p></li><li><p>start a debugger with: node --inspect index.js</p></li></ul><h1>Other Notes</h1><ul><li><p><code>structuredClone()</code> to get a deep copy</p></li><li><p>BigInt if your numbers are bigger than Number.MAX_SAFE_INTEGER</p></li><li><p>node can directly import a json</p><ul><li><p><code>import data from &#8216;./data.json&#8217; with { type: &#8216;json&#8217; }</code></p><p></p></li></ul></li></ul><h1>Frameworks</h1><p>Vue.js is still a great choice.</p><h1>TypeScript and node.js</h1><p>During development we use tsc and node with the watch parameter, we can start them both as side by side terminal with a vscode task.</p><p>package.json</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">    "dev:tsc": "tsc --watch --preserveWatchOutput",
    "dev:node": "node --watch-path=./dist dist/index.js"</code></pre></div><h1>TypeScript without TypeScript</h1><p>TypeScript has syntax that does not exist in JavaScript, if you avoid them you don&#8217;t need the build/transpiling step. Without the special syntax you can strip the types from the .ts files and run them as JavaScript.</p><p>With <code>tsc --erasableSyntaxOnly</code> you can check if any TypeScript special syntax is used in the project. <a href="https://www.typescriptlang.org/tsconfig/#erasableSyntaxOnly">erasableSyntaxOnly</a></p><h1>JavaScript with JSDoc checked by TypeScript</h1><p>A JavaScript project can still use TypeScript for checking the code and catching bugs. The type information can be provided as JSDoc.  </p><p>Checking JavaScript files</p><p><code>tsc --noEmit --allowJs --checkJs &lt;file&gt;  </code></p><h1>Snippets</h1><p>Testing with the node standard library.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">// node --test file.js
import {describe, it} from "node:test"
import assert from "node:assert"

function isEmailValid(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

describe("isEmailValid function", () =&gt; {
&#9;it("returns true for a valid email", () =&gt; {
&#9;&#9;assert.ok(isEmailValid("john@example.org"))
&#9;})
  it("returns false for a invalid email", () =&gt; {
    assert.ok(!isEmailValid("john@example"))
  })&#9;
})
</code></pre></div><p></p>]]></content:encoded></item></channel></rss>