<?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: Vuyisile Ndlovu</title>
    <description>The latest articles on DEV Community by Vuyisile Ndlovu (@vndlovu).</description>
    <link>https://dev.to/vndlovu</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%2F631893%2F5b472209-723c-444b-9e89-f4a0ffa937fb.jpeg</url>
      <title>DEV Community: Vuyisile Ndlovu</title>
      <link>https://dev.to/vndlovu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vndlovu"/>
    <language>en</language>
    <item>
      <title>How to use Azure Key Vault with the Azure CLI</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Wed, 30 Jul 2025 07:36:30 +0000</pubDate>
      <link>https://dev.to/vndlovu/how-to-use-azure-key-vault-with-the-azure-cli-ed4</link>
      <guid>https://dev.to/vndlovu/how-to-use-azure-key-vault-with-the-azure-cli-ed4</guid>
      <description>&lt;p&gt;Azure Key Vault is a secure secret storage service from Microsoft. You can use it to safeguard application credentials and SSH keys. In this post, I’ll show you how to create a Key Vault, and also how to add, retrieve and modify credentials in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Key Vault
&lt;/h2&gt;

&lt;p&gt;Create a resource group if you don’t have one&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az group create --name myResourceGroup --location westus2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create an Azure Key Vault&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az keyvault create --name &amp;lt;yourKeyVaultName&amp;gt; --resource-group myResourceGroup --location westus2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;yourKeyVaultName&lt;/code&gt; with your own name. Azure assigns DNS names for Key Vaults, so &lt;code&gt;yourKeyVaultName&lt;/code&gt; must be globally unique.&lt;/p&gt;

&lt;h2&gt;
  
  
  Insert a Secret
&lt;/h2&gt;

&lt;p&gt;To insert or set a new secret, use &lt;code&gt;az keyvault secret set&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;az keyvault secret set --vault-name &amp;lt;yourKeyVaultName&amp;gt; --name "MySecret" --value "SecretValue"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Retrieve a Secret
&lt;/h2&gt;

&lt;p&gt;To securely retrieve a secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az keyvault secret show --vault-name &amp;lt;yourKeyVaultName&amp;gt; --name "MySecret"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To retrieve only the secret’s value and no other metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az keyvault secret show --vault-name &amp;lt;yourKeyVaultName&amp;gt; --name "MySecret" --query value -o tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Update an Existing Secret
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az keyvault secret set --vault-name &amp;lt;yourKeyVaultName&amp;gt; --name "MySecret" --value "NewSecretValue"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  List All Secrets
&lt;/h2&gt;

&lt;p&gt;To list all secrets in the Key Vault:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az keyvault secret list --vault-name &amp;lt;yourKeyVaultName&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Delete a Secret
&lt;/h2&gt;

&lt;p&gt;To delete a secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az keyvault secret delete --vault-name &amp;lt;yourKeyVaultName&amp;gt; --name "MySecret"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command performs a soft-delete that’ll keep the secret for 90 days before it is purged.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>cli</category>
    </item>
    <item>
      <title>Self-Hosting Planka in Kubernetes: A Lightweight Trello Alternative</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Thu, 10 Jul 2025 10:29:11 +0000</pubDate>
      <link>https://dev.to/vndlovu/self-hosting-planka-in-kubernetes-a-lightweight-trello-alternative-m9p</link>
      <guid>https://dev.to/vndlovu/self-hosting-planka-in-kubernetes-a-lightweight-trello-alternative-m9p</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;Planka is a lightweight, open-source, kanban-style project management application similar to Trello. It’s built with React and Postgres, and its key features include a drag-and-drop interface, boards, lists, cards, notifications and markdown support.&lt;/p&gt;

&lt;p&gt;Kanban is a simple but powerful method for visualising and managing work. You break tasks into cards and move them through columns that represent stages of progress, such as To Do, In Progress and Done. This makes it easy to see what’s happening at a glance: what’s in the backlog, what’s currently in progress and what’s done. In my case, I’m often juggling projects in my homelab, client work, personal learning goals and content creation. A Kanban board helps me stay organised and focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I chose Planka
&lt;/h2&gt;

&lt;p&gt;I use and love Trello, but its software is proprietary, and their free plan has limitations on the number of boards you can create. One of my goals with my homelab is digital sovereignty — the ability to control my data, tools and infrastructure without depending on third parties. SaaS platforms are great, but they frequently change pricing, lock down features, or disappear entirely. I want the freedom to own and shape the systems I use every day.&lt;/p&gt;

&lt;p&gt;That’s why I self-host as many tools as I can, from DNS and backups to tools like Planka. There’s no shortage of self-hostable Kanban-style tools. I chose Planka because it is Open source, uses a Postgres database, is lightweight and relatively easy to run in Kubernetes. There are some things I don’t like about it, but the pros outweigh the cons. More on that later. Self hosting Planka gives full control over it, I control access to it, its uptime, backups and how it fits into the rest of my workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes Set Up
&lt;/h2&gt;

&lt;p&gt;Planka runs as a container and can be deployed to Kubernetes using its official &lt;a href="https://dev.to/vndlovu/an-intro-to-helm-1mo4"&gt;Helm chart&lt;/a&gt;. In my homelab, I manage all applications with a&lt;a href="https://vuyisile.com/tag/gitops/" rel="noopener noreferrer"&gt;GItOps approach using FluxCD&lt;/a&gt;. To deploy Planka with Flux CD, I created three manifest files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;HelmRepository&lt;/code&gt; resource pointing to the official Planka Helm chart, &lt;/li&gt;
&lt;li&gt;a &lt;code&gt;HelmRelease&lt;/code&gt; that defines how Planka should be deployed and configured. &lt;/li&gt;
&lt;li&gt;and an &lt;code&gt;ExternalSecret&lt;/code&gt; manifest to securely fetch credentials from my secret storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using this approach keeps my deployment reproducible, version-controlled, and easy to update.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining the Helm Repository
&lt;/h3&gt;

&lt;p&gt;In FluxCD terminology, a &lt;code&gt;HelmRepository&lt;/code&gt; points FluxCD to an upstream Helm chart. The Helm Repository below points Flux to the upstream source of the Planka Helm chart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
  name: planka
  namespace: flux-system
spec:
  interval: 60m
  url: http://plankanban.github.io/planka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;spec.interval&lt;/code&gt; field specifies how often Flux will check the upstream Helm repository for updates. This interval is useful for ensuring that the Planka in my cluster stays up-to-date with the latest versions of Helm charts in the repository without requiring manual intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing Secrets with &lt;code&gt;ExternalSecrets&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To keep sensitive data like database credentials and the secret key out of Git and plain Kubernetes manifests, I created the secrets in AWS and referenced the secrets using the &lt;code&gt;ExternalSecrets&lt;/code&gt; operator. It connects to a secure secrets vault — in my case, AWS SSM Parameter Store — fetches the values, and creates Kubernetes &lt;code&gt;Secret&lt;/code&gt; resources automatically inside the cluster.&lt;/p&gt;

&lt;p&gt;To create the secrets in the&lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" rel="noopener noreferrer"&gt;AWS SSM Parameter Store&lt;/a&gt;, I used the AWS CLI to define each key-value pair securely. For example, I created and stored the Planka PostgreSQL credentials using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export planka_db_username="planka_user"
export planka_db_name="planka_db"

aws ssm put-parameter --name "/K8s/Clusters/Prod/Planka/planka-postgres" \
\ --type "SecureString" \
\ --value "{\"username\": \"$planka_db_username\", \"password\": \"$(openssl rand -base64 16)\", \"database\": \"$planka_db_name\"}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This parameter name or secret is namespaced under “Planka” in AWS to keep things organised. Secrets are encrypted at rest using AWS KMS and are only accessible to the IAM role or user associated with the External Secrets provider. Once created and stored, the External Secrets Operator fetches the secrets and injects them into Kubernetes as native &lt;code&gt;Secret&lt;/code&gt; resources, making them available to the Planka Helm release without ever exposing the raw values in the Git Repo. Pretty cool!&lt;/p&gt;

&lt;p&gt;To make the secrets available inside Kubernetes, I created an &lt;code&gt;ExternalSecret&lt;/code&gt; resource that maps each AWS SSM parameter to a corresponding key in a Kubernetes &lt;code&gt;Secret&lt;/code&gt;. Here’s an example of the manifest I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: planka-secret
  namespace: default
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-parameter-store
    kind: ClusterSecretStore
  target:
    name: planka-secret
    creationPolicy: Owner
  data:
    - secretKey: secretkey
      remoteRef:
        key: /K8s/Clusters/Prod/Planka/planka-secretkey
        property: SECRET_KEY
    - secretKey: planka-db-username
      remoteRef:
        key: /K8s/Clusters/Prod/Planka/planka-postgres
        property: username
    - secretKey: planka-db-password
      remoteRef:
        key: /K8s/Clusters/Prod/Planka/planka-postgres
        property: password
    - secretKey: planka-db-name
      remoteRef:
        key: /K8s/Clusters/Prod/Planka/planka-postgres
        property: database
    - secretKey: planka-admin-email
      remoteRef:
        key: /K8s/Clusters/Prod/Planka/planka-admin
        property: admin_email
    - secretKey: planka-admin-password
      remoteRef:
        key: /K8s/Clusters/Prod/Planka/planka-admin
        property: admin_password
    - secretKey: planka-admin-username
      remoteRef:
        key: /K8s/Clusters/Prod/Planka/planka-admin
        property: admin_username
    - secretKey: planka-admin-adminname
      remoteRef:
        key: /K8s/Clusters/Prod/Planka/planka-admin
        property: admin_name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifest tells the External Secrets Operator to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pull secrets from a &lt;code&gt;ClusterSecretStore&lt;/code&gt; named &lt;code&gt;aws-parameter-store&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fetch parameters such as &lt;code&gt;SECRET_KEY&lt;/code&gt; and &lt;code&gt;planka-db-&lt;/code&gt;password from SSM&lt;/li&gt;
&lt;li&gt;Map each SSM parameter to a corresponding key in the resulting &lt;code&gt;Secret&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Planka Helm Release(defined in the next section) then references this &lt;code&gt;Secret&lt;/code&gt; to inject the values as environment variables into the container at run time, keeping the credentials secure and fully managed through GitOps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Helm Release
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;HelmRelease&lt;/code&gt; configures and deploys applications via Helm in a cluster. I used the release below to deploy Planka to my cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: planka
  namespace: default
spec:
  interval: 5m
  chart:
    spec:
      chart: planka
      version: "1.0.3"
      sourceRef:
        kind: HelmRepository
        name: planka
        namespace: flux-system
      interval: 60m
  valuesFrom:
    - kind: Secret
      name: planka-secret
      valuesKey: secretkey
      targetPath: secretkey

    - kind: Secret
      name: planka-secret
      valuesKey: planka-db-username
      targetPath: postgresql.auth.username

    - kind: Secret
      name: planka-secret
      valuesKey: planka-db-password
      targetPath: postgresql.auth.password

    - kind: Secret
      name: planka-secret
      valuesKey: planka-db-name
      targetPath: postgresql.auth.database

  values:
    base:
      baseUrl: "https://planka.ndlovucloud.co.zw"
    ingress:
      enabled: true
      className: nginx
      annotations:
        cert-manager.io/cluster-issuer: letsencrypt-prod
      hosts:
        - host: planka.ndlovucloud.co.zw
          paths:
            - path: /
              pathType: Prefix
      tls:
        - hosts:
            - planka.ndlovucloud.co.zw
          secretName: planka-tls

    # Values from planka-secret
    postgresql:
      enabled: true
    redis:
      enabled: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Helm Release references the Planka external secret from the previous section, pulls in the credentials and injects them into the Planka container at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I use Planka
&lt;/h2&gt;

&lt;p&gt;I haven’t started using Planka heavily yet, as I just finished setting it up, but I plan to use it a a central board for managing my personal growth, homelab activities and client projects.&lt;/p&gt;

&lt;p&gt;For personal projects, I’ll track blog posts, conference talks, and books I’m reading to stay on top of my goals. I’ll create a dedicated board for infrastructure tasks to log homelab issues, upgrades and experiments. Finally, I’ll use Planka to manage client tasks. Using kanban for this will keep my work organised and deadlines clear.&lt;/p&gt;

&lt;p&gt;Here’s a screenshot showing the board I’m using to track work I’m doing for a client:&lt;/p&gt;

&lt;p&gt;![Planka board set against an image background of a scenic European town. The board is categorised into five columns. From left to right, the columns are&lt;/p&gt;

&lt;p&gt;Backlog, Doing, Documentation and Complete](&lt;a href="https://vuyisile.com/wp-content/uploads/2025/07/planka-screenshot.png" rel="noopener noreferrer"&gt;https://vuyisile.com/wp-content/uploads/2025/07/planka-screenshot.png&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I don’t like about Planka
&lt;/h2&gt;

&lt;p&gt;I appreciate the work the developers put into Planka, but there are a few areas where I think it falls short.&lt;/p&gt;

&lt;p&gt;First, customising parts of the UI isn’t straightforward. Unlike Trello, where you can tweak the look and feel easily, Planka requires you to fork the codebase and edit the CSS to make visual changes to the UI. I’m technically capable of doing that, but I’d prefer an easier way to personalise the interface without maintaining a fork.&lt;/p&gt;

&lt;p&gt;Secondly, there’s no built-in search for cards –at least as of the version I’m using. That makes it hard to find specific cards. That’s not a problem for me now because my boards are still small, but as my boards grow, it might be an inconvenience.&lt;/p&gt;

&lt;p&gt;Finally, I haven’t found an easy way to export your data in JSON or other portable formats. This is something I’d like to see supported, both for backup and interoperability reasons. These aren’t deal breakers, but they are small annoyances worth noting.&lt;/p&gt;

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

&lt;p&gt;Planka isn’t the most feature-packed project management tool out there, but it’s simple and easy to self-host, perfect for someone like me who wants full control over their data without unnecessary complexity. I’m happy to have a private, minimal Kanban system that fits into my workflow. As my boards grow and usage continues, I may post more about it and how I use it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>cloudflare</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Rollbacks in ArgoCD</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Tue, 01 Jul 2025 12:26:59 +0000</pubDate>
      <link>https://dev.to/vndlovu/rollbacks-in-argocd-4n3a</link>
      <guid>https://dev.to/vndlovu/rollbacks-in-argocd-4n3a</guid>
      <description>&lt;p&gt;ArgoCD is a GitOps-based continuous delivery tool for Kubernetes that keeps applications in sync with their declared state in Git. I wrote about &lt;a href="https://dev.to/vndlovu/gitops-with-argocd-3nj6"&gt;ArgoCD&lt;/a&gt; a few days ago, explaining what it is and how to set it up. In a GitOps workflow, Git is the single source of truth for your Kubernetes infrastructure. ArgoCD continually syncs and reconciles what’s defined in your Git repository with your cluster.&lt;/p&gt;

&lt;p&gt;Deployments don’t always go according to plan, and changes can break the application. In this post, I’ll discuss how to perform a rollback using ArgoCD.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitOps Philosophy
&lt;/h2&gt;

&lt;p&gt;The idea behind GitOps is that your cluster should always reflect what’s defined in Git. Rather than making manual changes to deployed resources, you define the desired state of your application in code, and a tool like ArgoCD applies it to your cluster automatically. This approach has some advantages:  &lt;/p&gt;

&lt;p&gt;– You can audit changes with Git history&lt;br&gt;&lt;br&gt;
– You always know exactly what’s running&lt;br&gt;&lt;br&gt;
– Rolling back bad deployments is straight forward. You run a &lt;code&gt;git revert&lt;/code&gt; or an ArgoCD rollback command.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rollback in ArgoCD
&lt;/h2&gt;

&lt;p&gt;A rollback in ArgoCD means ArgoCD syncs a previous version of an application to the cluster using its Git commit. It checks out the last good commit and reapplies it to the cluster.&lt;/p&gt;

&lt;p&gt;In some instances, it may be a good idea to disable auto syncing when troubleshooting failed deployments. To do this, run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;argocd app set &amp;lt;app-name&amp;gt; --sync-policy none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect the deployment history, run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;argocd app history &amp;lt;app_name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;replacing &lt;code&gt;&amp;lt;app_name&amp;gt;&lt;/code&gt; with the name of your application. This command shows a list of numbered revisions. To rollback, take note of the number of the good revision and run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;argocd app rollback &amp;lt;app_name&amp;gt; &amp;lt;revision_number&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;replacing app_name and revision_number with the correct values for your application.&lt;/p&gt;

&lt;p&gt;This will reapply the good version and bring the cluster back up. To clean up the broken state in Git, you can undo the changes using git revert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git revert &amp;lt;bad-commit-sha&amp;gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that your Git history accurately reflects the fix. If you had disabled auto sync, re-enable it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;argocd app set &amp;lt;app-name&amp;gt; --sync-policy automated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;ArgoCD makes rollbacks simple — but only if your Git repo is well-maintained. To make rollbacks easy, ensure your Git commits are small and intentional.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>argocd</category>
      <category>git</category>
    </item>
    <item>
      <title>Setting Up a Remote Backend for Terraform Using Azure Storage</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Thu, 26 Jun 2025 09:36:47 +0000</pubDate>
      <link>https://dev.to/vndlovu/setting-up-a-remote-backend-for-terraform-using-azure-storage-g37</link>
      <guid>https://dev.to/vndlovu/setting-up-a-remote-backend-for-terraform-using-azure-storage-g37</guid>
      <description>&lt;h1&gt;
  
  
  Terraform Remote State Using Azure Storage
&lt;/h1&gt;

&lt;p&gt;Recently, I needed to set up a shared Terraform workflow where state could be safely stored and accessed by a team. I figured out how to use Azure Blob Storage as a remote backend for Terraform. Storing your Terraform state in a remote backend ensures consistency across teams and machines. This post walks you through setting up Azure Blob Storage as the backend.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Create Storage Account and Container
&lt;/h2&gt;

&lt;p&gt;Run the following in your terminal. Adjust variables as needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Set variables
RESOURCE_GROUP="tf-aks-rg"
STORAGE_ACCOUNT_NAME="tfstatestore$(openssl rand -hex 3)" # must be globally unique
CONTAINER_NAME="tfstate"
LOCATION="westus2"

# Create the storage account
az storage account create \
  --resource-group $RESOURCE_GROUP \
  --name $STORAGE_ACCOUNT_NAME \
  --sku Standard_LRS \
  --encryption-services blob \
  --kind StorageV2 \
  --location $LOCATION

# Get the storage account key
ACCOUNT_KEY=$(az storage account keys list \
  --resource-group $RESOURCE_GROUP \
  --account-name $STORAGE_ACCOUNT_NAME \
  --query '[0].value' -o tsv)

# Create the container
az storage container create \
  --name $CONTAINER_NAME \
  --account-name $STORAGE_ACCOUNT_NAME \
  --account-key $ACCOUNT_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Configure Terraform to Use the Remote Backend
&lt;/h2&gt;

&lt;p&gt;Create a file called &lt;code&gt;backend.tf&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;terraform {
  backend "azurerm" {
    resource_group_name = "tf-aks-rg"
    storage_account_name = "&amp;lt;your-storage-account-name&amp;gt;"
    container_name = "tfstate"
    key = "terraform.tfstate"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;your-storage-account-name&amp;gt;&lt;/code&gt; with the actual name.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Initialize Terraform
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll be prompted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do you want to copy the existing state to the new backend? [yes]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Answer &lt;code&gt;yes&lt;/code&gt; to migrate your local state to Azure.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Verify
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Confirm that &lt;code&gt;terraform.tfstate&lt;/code&gt; exists in the Azure container.&lt;/li&gt;
&lt;li&gt;Your local state file (&lt;code&gt;terraform.tfstate&lt;/code&gt;) should no longer be present or should be empty.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Next Step: Team Access
&lt;/h2&gt;

&lt;p&gt;Make sure your team members have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read/write access to the storage account&lt;/li&gt;
&lt;li&gt;Correct backend config in their Terraform project&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This was my first time setting up a remote backend with Azure, and it turned out to be more straightforward than I expected. If you’re using Azure and want to avoid local state headaches, this set up works well.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>GitOps with ArgoCD</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Fri, 13 Jun 2025 19:48:30 +0000</pubDate>
      <link>https://dev.to/vndlovu/gitops-with-argocd-3nj6</link>
      <guid>https://dev.to/vndlovu/gitops-with-argocd-3nj6</guid>
      <description>&lt;p&gt;I’m learning a set of new tools to make my skills more well-rounded. These include Azure, Terraform and ArgoCD. &lt;a href="https://argo-cd.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;ArgoCD&lt;/a&gt; is a GitOps continuous delivery tool for Kubernetes that’s similar to Flux CD, which I used in &lt;a href="https://dev.to/vndlovu/homelab-kubernetes-cluster-51cc"&gt;my Kubernetes homelab&lt;/a&gt;. Like Flux CD, it syncs your cluster with configuration data stored in Git. The way it works is that you push changes to Git and Argo CD picks them up and applies them to the cluster. Git becomes the single source of truth. If the cluster drifts from what’s in Git, Argo CD can either alert you or automatically sync to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Argo CD
&lt;/h2&gt;

&lt;p&gt;I installed Argo CD using &lt;a href="https://dev.to/vndlovu/an-intro-to-helm-4o8c-temp-slug-7818815"&gt;Helm&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;helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

kubectl create namespace argocd
helm install argocd argo/argo-cd --namespace argocd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Accessing the UI
&lt;/h2&gt;

&lt;p&gt;Unlike Flux CD which is CLI-first, Argo CD is a UI-first application. After installing it in the cluster, I accessed the argocd-server by port forwarding using this command:&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 svc/argocd-server -n argocd 8080:443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command lets me tunnel securely into the Argo CD pod from my local machine without setting up an ingress or load balancer. This is a screenshot from the Argo CD UI:&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%2F529m3il0hyqu69zg6hy1.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%2F529m3il0hyqu69zg6hy1.png" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the Default Admin Password
&lt;/h2&gt;

&lt;p&gt;Before I could login to the UI, I had to get the default admin password from the secret that was created during installation:&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 secret argocd-initial-admin-secret -n argocd -o jsonpath="{.data.password}" | base64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Concept: The Application Object
&lt;/h2&gt;

&lt;p&gt;So far, I’ve learned that the heart of Argo CD is the &lt;code&gt;Application&lt;/code&gt; object. It defines a&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Source&lt;/code&gt; : A Git repo with source files (Helm charts, Kustomize files or plain YAML manifests)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Destination&lt;/code&gt;: The target Kubernetes cluster and namespace&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Sync Policy&lt;/code&gt;: Whether or not this application should be manually or automatically synced to the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is an example of an ArgoCD Application to deploy Nginx via Helm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: nginx-helm
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://charts.bitnami.com/bitnami
    chart: nginx
    targetRevision: 15.3.2
    helm:
      values: |
        service:
          type: LoadBalancer
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

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

&lt;/div&gt;



&lt;p&gt;This config tells Argo CD to watch the Bitnami Nginx chart and auto-deploy changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts so far
&lt;/h2&gt;

&lt;p&gt;Argo CD was quick to install–I had it up and running in a few minutes. The UI is clean and easy to use. I haven’t explored the CLI yet, but I’ll test it as I deploy more applications. So far, it feels straightforward and promising.&lt;/p&gt;

</description>
      <category>uncategorised</category>
    </item>
    <item>
      <title>An intro to Helm</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Thu, 12 Jun 2025 12:23:16 +0000</pubDate>
      <link>https://dev.to/vndlovu/an-intro-to-helm-1mo4</link>
      <guid>https://dev.to/vndlovu/an-intro-to-helm-1mo4</guid>
      <description>&lt;p&gt;I’m currently learning Helm to improve how I deploy and manage Kubernetes applications. This post is a quick summary of what I’ve learned so far.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://helm.sh/" rel="noopener noreferrer"&gt;Helm&lt;/a&gt; is a package manager for Kubernetes. It simplifies deploying and managing Kubernetes resources by bundling them into reusable packages called charts. If you’ve used &lt;code&gt;apt&lt;/code&gt; for Debian-based systems, Helm serves a similar role, except for Kubernetes.&lt;/p&gt;

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

&lt;p&gt;I installed Helm on Linux using Snap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo snap install helm --classic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Concepts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chart&lt;/strong&gt; : A collection of YAML manifests that define a Kubernetes application (e.g Prometheus or a monitoring stack).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release:&lt;/strong&gt; A running instance of a chart, versioned and deployed into a cluster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Values:&lt;/strong&gt; Configurable parameters used to customise chart behavior during deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Use Helm
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Faster Installs: Deploy applications like Prometheus and Grafana in seconds&lt;/li&gt;
&lt;li&gt;Rollbacks: Easily revert to a previous release if something breaks&lt;/li&gt;
&lt;li&gt;Customisation: Tweak values without editing raw YAML files&lt;/li&gt;
&lt;li&gt;GitOps-friendly: Helm charts integrate well into GitOps workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I’m Doing Next
&lt;/h2&gt;

&lt;p&gt;I’m starting with simple charts, then I’ll work my way up to deploying complex stacks and applications such as ArgoCD with Helm. I’ll be sharing what I learn as I go in short blog posts like this one.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>apt</category>
      <category>helm</category>
    </item>
    <item>
      <title>From HDD to SSD: How I fixed an I/O Bottleneck in a Kubernetes Node</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Fri, 30 May 2025 11:34:38 +0000</pubDate>
      <link>https://dev.to/vndlovu/from-hdd-to-ssd-how-i-fixed-an-io-bottleneck-in-a-kubernetes-node-1fl0</link>
      <guid>https://dev.to/vndlovu/from-hdd-to-ssd-how-i-fixed-an-io-bottleneck-in-a-kubernetes-node-1fl0</guid>
      <description>&lt;h1&gt;
  
  
  Diagnosing and Fixing an I/O Bottleneck in My Kubernetes Node
&lt;/h1&gt;

&lt;p&gt;I’ve been experiencing some issues with my homelab servers. One was that the media server (Mac Mini hardware running Ubuntu) kept freezing and locking up, requiring a manual restart. This caused the Kubernetes containers running on it to fail. Kubernetes, in response, would move the failed apps to another node(HP laptop) and slightly overload it. I suspected the issue was I/O related since this machine used a hard drive with a spinning disk, which is quite slow compared to a Solid State Drive(SSD). The media server has significantly less memory than the HP, but it has a better processor, and its RAM never got maxed out, so CPU and RAM weren’t the issue. Before setting up Kubernetes on it, its performance was acceptable, so I figured the slowness was caused by the impact of Kubernetes components like logs, image pulls, and metrics hammering the disk heavily, causing it to slow down.&lt;/p&gt;

&lt;p&gt;I fixed the problem by cloning the system from an HDD to a new SSD drive and swapping the drives. In this post, I’ll walk you through how I did that with minimal downtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node froze often, especially during heavy disk usage.&lt;/li&gt;
&lt;li&gt;It had a decent CPU but less RAM than my HP635 node.&lt;/li&gt;
&lt;li&gt;Disk I/O seemed to be the bottleneck.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Plan
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Clone the HDD to a new SSD. &lt;/li&gt;
&lt;li&gt;Swap drives&lt;/li&gt;
&lt;li&gt;Boot from the SSD.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Steps I Took
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;I drained the node of all Kubernetes applications running on it. The apps moved to the HP node.&lt;/li&gt;
&lt;li&gt;Next, I connected the new SSD to the server using a USB-SATA connector.&lt;/li&gt;
&lt;li&gt;Checked the drive names with &lt;code&gt;lsblk&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sda&lt;/code&gt;: 149.1GB (old HDD)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sdb&lt;/code&gt;: 238.5GB (new SSD)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The new SSD was larger than the HDD, which made cloning much easier since I didn’t need to resize the contents of the source disk. I ensured that the SSD was connected but not mounted.&lt;/li&gt;
&lt;li&gt;Next, I estimated how long it would take to copy random data from the hard drive to itself using this command and the formula below:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo dd if=/dev/sda of=/dev/null bs=1M count=256 status=progress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Running the command above gave me a rough estimate of the disk’s speed. I then divided the disk size by the speed to get how long it’d take to copy the entire disk. (Formula: disk size (MB) ÷ speed (MB/s) = ETA)&lt;/li&gt;
&lt;li&gt;I knew that it would take longer to copy data from the HDD to the SSD through a USB cable, but running the test above allowed me to get a reasonable estimate of the time it’d take. My initial thought was that it’d take about 40 minutes to an hour to clone the drive.

&lt;ol&gt;
&lt;li&gt;The server wasn’t connected to a screen, and setting up a screen for it would have been inconvenient, so I cloned the drive over SSH.&lt;/li&gt;
&lt;li&gt;To prevent losing progress if the SSH connection dropped during the clone process, I started a&lt;a href="https://dev.to/vndlovu/tmux-3868"&gt;tmux session&lt;/a&gt; to keep the job running in the background:
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tmux new -s clone_disk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Next, I cloned the disk using &lt;code&gt;dd&lt;/code&gt; &lt;strong&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo dd if=/dev/sda of=/dev/sdb bs=1M status=progress conv=noerror,sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;where &lt;code&gt;if&lt;/code&gt; is the input device, &lt;code&gt;of&lt;/code&gt; is the output device, &lt;code&gt;bs&lt;/code&gt; is the block size, &lt;code&gt;status=progress&lt;/code&gt; displays a progress bar and &lt;code&gt;conv=noerror,sync&lt;/code&gt; tells &lt;code&gt;dd&lt;/code&gt; not to stop even if it encounters a read error in the input device. If it encounters any read errors, it should pad input blocks with zeroes to keep the output file properly aligned, block by block&lt;/li&gt;
&lt;li&gt;Cloning the drive took twice as long as I had initially thought — the process completed after 2 hours.

&lt;ol&gt;
&lt;li&gt;Once the disk was cloned, I ran a check on the cloned disk &lt;strong&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo fdisk -l /dev/sdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The check was mostly okay except for this warning:
&lt;code&gt;"The backup GPT table is not on the end of the device."&lt;/code&gt; This meant that the GPT (GUID Partition Table) wasn’t located at the physical end of the disk, which is where it’s supposed to be. Since the HDD was smaller than the SSD, &lt;code&gt;dd&lt;/code&gt; copied the main GPT header and partition table at the beginning of the disk and the backup GPT at the same position it was on the smaller disk(at the end). But since the SSD is bigger, the backup GPT wasn’t at the end of the new disk. I took note of the warning and moved on.

&lt;ol&gt;
&lt;li&gt;After swapping the drives, I booted from the SSD. The server booted up without errors and quicker than it had previously.&lt;/li&gt;
&lt;li&gt;Next, I fixed the GPT warning using &lt;code&gt;gdisk&lt;/code&gt; &lt;strong&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo gdisk /dev/sda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Entered &lt;code&gt;v&lt;/code&gt; to verify&lt;/li&gt;
&lt;li&gt;And &lt;code&gt;w&lt;/code&gt; to write the fix

&lt;ol&gt;
&lt;li&gt;I tried to resize the new drive’s volume by running &lt;strong&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo pvresize /dev/sda2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Checking the volume sizes using the commands below showed no change in the size of the drive; it was still showing up as a hundred and something GB drive &lt;strong&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo vgdisplaysudo lvdisplay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;To resolve this, I installed &lt;code&gt;cloud-guest-utils&lt;/code&gt;, grew the partition to take up the extra space, resized the physical volume and extended the logical volume to take up the extra space:

&lt;ul&gt;
&lt;li&gt;Installed the required tool:
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt install cloud-guest-utils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Grew the partition:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo growpart /dev/sda2sudo pvresize /dev/sda2sudo lvextend -l +100%FREE /dev/ubuntu-vg/rootsudo resize2fs /dev/ubuntu-vg/root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;After that, I confirmed that everything was the way it should have been by running:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lsblk /dev/sda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything looked good, the drive and its partitions were using up all available space.  &lt;/p&gt;




&lt;h2&gt;
  
  
  Replacing the drives
&lt;/h2&gt;

&lt;p&gt;After verifying that the new drive looked good and that all the data from the old drive was copied successfully, I popped the Mac Mini open, removed the HDD and replaced it with the SSD before closing it back up.&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%2F4m2x48wvj9ow8c3oedzf.jpeg" 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%2F4m2x48wvj9ow8c3oedzf.jpeg" alt="SSD Drive connected to a SATA- USB adapter" width="768" height="1020"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Copied data to the SSD first&lt;/em&gt;&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%2F2db1uevhiasd1le1by0u.jpeg" 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%2F2db1uevhiasd1le1by0u.jpeg" alt="Unopened Mac mini" width="768" height="1020"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Mac Mini before disassembly&lt;/em&gt;&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%2Figowx7ofgrx12hckr4s9.jpeg" 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%2Figowx7ofgrx12hckr4s9.jpeg" alt="Mac Mini with case removed" width="800" height="602"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Case removed&lt;/em&gt;&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%2Fzdfntu5xbv8b2s30i0wf.jpeg" 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%2Fzdfntu5xbv8b2s30i0wf.jpeg" alt="Disassembled Mac Mini with HDD still in connector" width="800" height="602"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Disassembled&lt;/em&gt;&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%2Fxyazwt5ziz87c18vt96s.jpeg" 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%2Fxyazwt5ziz87c18vt96s.jpeg" alt="Mac Mini Hard drive removed" width="768" height="1020"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;HDD removed&lt;/em&gt;&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%2F1fc4dr26ud3h459y3kqk.jpeg" 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%2F1fc4dr26ud3h459y3kqk.jpeg" alt="SSD drive installed in Mac Mini" width="768" height="1020"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;HDD replaced with SSD&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The Media Server now runs on a Solid State Drive, it’s faster and more stable now. To test it, I drained all Kubernetes applications from the HP control node to the Mac Mini and ran them for a few days. They ran beautifully, the CPU didn’t take a huge hit, and it didn’t lock up from the heavy I/O from the apps. One slight problem I have now with the server is that its CPU fan is much louder. When replacing the hard drive, I forgot to reinstall a temperature sensor in the Mac Mini that regulates how fast the fan runs. Now it probably thinks the sensor is damaged, so it runs at full throttle all the time, and it’s quite loud. Other than that, the server works perfectly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;I need to be more careful when opening Apple devices, it’s easy to miss replacing an important component. If you’re running a server on an old HDD and it keeps constantly freezing, locking up or running slow, consider swapping it out for an SSD. Clone your data between drives before replacing them with &lt;code&gt;dd&lt;/code&gt;, fix any GPT issues with &lt;code&gt;gdisk&lt;/code&gt;, and resize your volumes.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>homelab</category>
    </item>
    <item>
      <title>May AWS Tech Meet at FlexiWork</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Mon, 26 May 2025 13:53:59 +0000</pubDate>
      <link>https://dev.to/vndlovu/may-aws-tech-meet-at-flexiwork-5aga</link>
      <guid>https://dev.to/vndlovu/may-aws-tech-meet-at-flexiwork-5aga</guid>
      <description>&lt;p&gt;On Saturday, May 24, I attended an AWS meetup at FlexiWork. The meetup was three hours long, we networked and listed to talks with hands-on tips for AWS practitioners.&lt;/p&gt;

&lt;p&gt;Here’s how it went down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The venue: FlexiWork, a relaxed, coworking space
&lt;/h2&gt;

&lt;p&gt;The event was hosted at &lt;a href="https://flexiwork.co.zw/" rel="noopener noreferrer"&gt;FlexiWork, a beautiful co-working space&lt;/a&gt; in Bulawayo with secure parking, art along the hallways and a quiet garden just outside. Around 20 people attended the meetup, ranging from bootcamp students to working professionals.&lt;/p&gt;

&lt;p&gt;Tiffany, the FlexiWork Operations Manager, started us off with a quick tour of the venue and an icebreaker session to introduce ourselves. FlexiWork offered doughnuts, coffee and other treats. We settled in with Wi-Fi and got ready for the talks at around 11AM.&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%2Fjbrfybjq9iii9tw75rbb.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%2Fjbrfybjq9iii9tw75rbb.jpg" alt="FlexiWork Room" width="632" height="707"&gt;&lt;/a&gt;&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%2Fa0277gmn8j6dg74s378g.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%2Fa0277gmn8j6dg74s378g.jpg" alt="Flexi Work Garden" width="632" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hildah Machando — AWS Fundamentals: Your Guide to Certification, Cloud Building, and Security
&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%2F60siorgoszaexcjxs90c.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%2F60siorgoszaexcjxs90c.jpg" alt="Hildah Machando, prensenting at an AWS Meetup at FlexiWork" width="632" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/hilda-machando-43277b179/" rel="noopener noreferrer"&gt;Hildah&lt;/a&gt;, a 3x AWS and CISA certified security specialist opened the session with a beginner-friendly introduction to AWS. She covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Certification paths and AWS job prospects&lt;/li&gt;
&lt;li&gt;Tips on passing certification exams, including costs and resources to prep for them&lt;/li&gt;
&lt;li&gt;How to set up your first AWS account. This included steps on creating groups, roles and IAM policies.&lt;/li&gt;
&lt;li&gt;She broke down how EC2 works; covering the different types of instances, and strategies for using the free tier wisely e.g stopping and terminating instances after completing learning sessions to avoid incurring high bills.&lt;/li&gt;
&lt;li&gt;An explanation of Shared Responsibility, including

&lt;ul&gt;
&lt;li&gt;What AWS manages for you (instance patching, licensing)&lt;/li&gt;
&lt;li&gt;What you’re responsible for (security configurations, updates etc)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The talk was practical and gave new users a clear path to start experimenting with AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Talk — Cloud Skills, No Cloud Required
&lt;/h2&gt;

&lt;p&gt;I gave a talk titled “Cloud Skills, No Cloud Required”, where I shared how I built my own “cloud” using local hardware combined with AWS services. You can find the &lt;a href="https://speakerdeck.com/terrameijar/cloud-skills-no-cloud-required" rel="noopener noreferrer"&gt;slides from my presentation on speakerdeck&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I walked through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why I started a homelab — to learn by doing, keep costs low and to prove that it’s possible&lt;/li&gt;
&lt;li&gt;What software powers it: containers (Kubernetes), backups, monitoring, declarative tools like Ansible and CloudFormation&lt;/li&gt;
&lt;li&gt;AWS services I use within the homelab:

&lt;ul&gt;
&lt;li&gt;S3 for backups&lt;/li&gt;
&lt;li&gt;Route 53 for DNS and subdomains&lt;/li&gt;
&lt;li&gt;SSM Parameter Store for secure credentials storage&lt;/li&gt;
&lt;li&gt;IAM for access control and policy enforcement&lt;/li&gt;
&lt;li&gt;SES to send alerts when backups fail&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;I also mentioned the cloud-native principles I follow, even in a homelab such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rebuildability&lt;/strong&gt; : everything is declarative in YAML and version controlled in git&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security-first&lt;/strong&gt; : credentials aren’t hardcoded and backups are stored securely&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;Monitoring and alerts: *&lt;/em&gt; so I know when things go wrong&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;Low cost: *&lt;/em&gt; I use low cost hardware and low-cost/free services from AWS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I recommend a hybrid approach like this if you’re learning AWS, cloud engineering or DevOps but can’t afford to run full workloads in the cloud, it’s helped me gain real-world experience without the real-world bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conversations I Had
&lt;/h2&gt;

&lt;p&gt;One of the highlights of the day was connecting with students from the &lt;a href="https://uncommon.org/bulawayo" rel="noopener noreferrer"&gt;Uncommon bootcamp&lt;/a&gt;. Their enthusiasm for learning how code works, from variables to full applications reminded me of myself many years ago when I was a bootcamp student at Muzinda Hub.&lt;/p&gt;

&lt;p&gt;I shared advice with them on breaking into tech via open source. We talked about programs like Google Summer of Code, Outreachy, Summer of Docs and other internship opportunities.&lt;/p&gt;

&lt;p&gt;Another great moment was chatting with &lt;a href="https://www.linkedin.com/in/tapiwanashe-kanda-3636861a1/" rel="noopener noreferrer"&gt;Tapiwa Kanda&lt;/a&gt; from the AWS Zimbabwe community. We kicked around the idea of launching an AWS User group in Bulawayo. The group would be a place for local builders to collaborate, share and grow together. I’m looking forward to see where that leads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;I few things that stuck with me during the event:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email is deceptively hard to self-host. Somone asked why I don’t run my own mail server. We discussed the challenges; IP reputation problems, getting emails marked as spam, setting DNS records up and keeping the server secure. It sounds like a pain, but now I want to try it. I’m thinking of it as an interesting technical puzzle.&lt;/li&gt;
&lt;li&gt;There’s a hunger for tech. Whether it’s bootcamp students or hobbyists, people are eager to learn more, build real things and use tech to its full potential.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;After the event, and thinking about the discussions I had, I plan to experiment with self-hosted email to see how far I can push it. I also want to follow up with Tapiwa about starting an AWS User Group in Bulawayo.&lt;/p&gt;

&lt;p&gt;If you’re interested in homelabs, AWS or setting up cloud-native systems on a budget, reach out to me to ask questions. And if you’ve never been to a tech event, go. You’ll meet interesting new people, make lasting connections and leave with more than just knowledge.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>flexiwork</category>
      <category>homelab</category>
    </item>
    <item>
      <title>How A Bad Firewall Rule Broke My Network</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Wed, 14 May 2025 09:19:32 +0000</pubDate>
      <link>https://dev.to/vndlovu/how-a-bad-firewall-rule-broke-my-network-2287</link>
      <guid>https://dev.to/vndlovu/how-a-bad-firewall-rule-broke-my-network-2287</guid>
      <description>&lt;p&gt;While tightening up and securing the firewall rules in my network with UFW, I ran into a problem that completely broke DHCP. My goal was simple: to allow DHCP requests from devices in my LAN. So I added the following rule:&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%2Fshguttad3oontyocx8oq.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%2Fshguttad3oontyocx8oq.png" alt="sudo ufw allow from 192.168.120.0/24 to any port 67 proto udp" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On paper, it looked right. DHCP traffic should only be coming from devices in that subnet, but as soon as I applied it, clients stopped getting IP addresses and I only realised this a day later after some devices rebooted or tried to renew their IP addresses. Static IPs worked fine, but DHCP wasn’t working.&lt;/p&gt;

&lt;h2&gt;
  
  
  DHCP Protocol — Where I got it wrong
&lt;/h2&gt;

&lt;p&gt;DCHP is unique in that it uses broadcast IP addresses to communicate. When clients join a new network, they send a message requesting an IP address to &lt;code&gt;255.255.255.255&lt;/code&gt;. This is the equivalent of entering a room and shouting to get everyone’s attention. Since I had created a rule to only allow DHCP traffic on a specific subnet that isn’t 255.255.255.255, the rule didn’t match, and UFW silently dropped it and didn’t allow the messages to reach the DHCP server. In other words, my rule was too restrictive, and DHCP couldn’t allocate IP addresses to network devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;When I saw the problem on one of the devices in my network, I first thought the network cable was damaged, so I swapped the cable with one from another device that worked, but the problem didn’t go away, so I knew it had to be something else. I checked the network settings and noticed that the device didn’t have an IP address, which meant there was something wrong with DHCP. I logged into the DHCP server, ran a few troubleshooting commands and realised that the problem wasn’t on the server itself, so it had to be clients failing to reach the server.&lt;/p&gt;

&lt;p&gt;Whenever I encounter a problem in code or in my network, the first question I ask myself is, “What changed recently?”. I remembered modifying the firewall rules the day before, and when I looked at them it hit me, I had configured the firewall to only allow DHCP traffic on the subnet and not on the broadcast address.&lt;/p&gt;

&lt;p&gt;To fix it, I simply deleted the rule and added another without the subnet:&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%2Fl2q1189luph15t1pev7m.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%2Fl2q1189luph15t1pev7m.png" width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This accepts DHCP traffic from any address. The rule feels like a security hole, which is why I changed it in the first place, but it isn’t because of the way DHCP works. To get IP addresses via DHCP, devices have to already be in the same network; devices outside the network can’t send DHCP requests to the server, so I’m only opening up traffic that’s already on the network. If a malicious person is already there, then I have bigger problems.&lt;/p&gt;

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

&lt;p&gt;The lesson for me here is to spend more time thinking about the underlying protocols I want to implement firewall rules for before applying the firewall rules.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>dhcp</category>
      <category>firewall</category>
      <category>ufw</category>
    </item>
    <item>
      <title>Why my ingress-nginx failed after reboot, and how I fixed it with static IPs in MetalLB</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Tue, 06 May 2025 08:39:38 +0000</pubDate>
      <link>https://dev.to/vndlovu/why-my-ingress-nginx-failed-after-reboot-and-how-i-fixed-it-with-static-ips-in-metallb-50bc</link>
      <guid>https://dev.to/vndlovu/why-my-ingress-nginx-failed-after-reboot-and-how-i-fixed-it-with-static-ips-in-metallb-50bc</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;As you know, I run a &lt;a href="https://dev.to/vndlovu/homelab-kubernetes-cluster-51cc"&gt;self-hosted Kubernetes Homelab&lt;/a&gt;. Recently, I had a problem where I couldn’t reach some services running in the cluster after restarting the Kubernetes servers. The problem didn’t happen every time I restarted the servers, but whenever it did occur, it was always after a server restart. Ingress-nginx is the service that got affected specifically. I have it configured to request a specific IP address, but sometimes after the server restarts, it fails to get the IP address from &lt;a href="https://dev.to/vndlovu/building-a-kubernetes-cluster-from-scratch-with-k3s-and-metallb-11md"&gt;MetalLB&lt;/a&gt;, the tool I use to manage and allocate IP addresses to the different services running in the cluster. When I first set up the cluster, I configured MetalLB with a pool of IPs it could assign to services. There were enough IP addresses for all the services and more to spare, so this wasn’t an IP address exhaustion problem. A service might grab a previously assigned IP because of the way MetalLB assigns IPs; it allocates IPs from a shared pool, and on reboot, services might start in a different order, leading to another service taking ingress-nginx’s IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;To debug the problem, the first thing I did was to check if the cluster was healthy. All server nodes were up and running, deployments were running okay, and application and pod logs looked fine. I ran &lt;code&gt;kubectl get svc&lt;/code&gt; to check on the network services and realised that the ingress nginx controller’s IP address column was stuck in “pending” state. This meant that it had failed to get its reserved IP address because another service had taken the IP address it wanted. As a quick fix, I stopped the service that had taken up the ingress-nginx’s IP and the IP address was immediately assigned to ingress-nginx. This worked until the server restarted again, so I needed a permanent solution.&lt;/p&gt;

&lt;p&gt;To resolve this IP address allocation race condition, I created IP addresses for each load balancer in the cluster instead of having the load balancer get its IPs from a shared IP pool. This way, the load balancers have reliable IP addresses that survive server restarts. Here’s how I created a static IP for ingress-nginx in MetalLB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: ingress-nginx
  namespace: metallb-system
spec:
  addresses:
    - 10.0.0.8/32

---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-advertisement
  namespace: metallb-system
spec:
  ipAddressPools:
  - ingress-nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When adding individual IPs and not an IP range to a pool like what I did above, MetalLB requires the IP to have a CIDR. 10.0.0.8/32 represents a single IP.&lt;/p&gt;

&lt;p&gt;Next, I updated the ingress-nginx deployment to request that specific IP address:&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:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.12.1
  name: ingress-nginx-controller
  namespace: ingress-nginx
  annotations:
    metallb.universe.tf/address-pool: ingress-nginx

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

&lt;/div&gt;



&lt;p&gt;Adding the &lt;code&gt;metallb.universe.tf/address-pool: ingress-nginx&lt;/code&gt; annotation to the service makes it request the &lt;code&gt;10.0.0.8&lt;/code&gt; IP address from MetalLB&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic IPs can cause race conditions&lt;/strong&gt;. Relying on dynamically assigned IP addresses can lead to service conflicts after reboots due to the non-deterministic order in which services come online.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static IPs increase reliability&lt;/strong&gt;. Assigning static IPs to critical services like ingress controllers ensures predictable behavior and avoids conflicts on server or network reboot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MetalLB is a joy to work with&lt;/strong&gt;. MetalLB supports both shared IP pools and dedicated IP configurations, making it easy to set up and adjust to meet my needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosting is hard&lt;/strong&gt;. Unlike managed cloud services, self-hosting requires handling networking, IP management, hardware, power issues and fault tolerance. Thinking about and solving these issues deepens my understanding of system admin in general and Kubernetes internals specifically.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>ingressnginx</category>
      <category>ipaddress</category>
    </item>
    <item>
      <title>S3 Lifecycle Rules for WordPress Backups: Deleting files older than 90 days</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Sat, 03 May 2025 19:00:58 +0000</pubDate>
      <link>https://dev.to/vndlovu/s3-lifecycle-rules-for-wordpress-backups-deleting-files-older-than-90-days-80c</link>
      <guid>https://dev.to/vndlovu/s3-lifecycle-rules-for-wordpress-backups-deleting-files-older-than-90-days-80c</guid>
      <description>&lt;p&gt;Since I started my self-hosted homelab, I’ve been thinking of ways to back up all my data. The first thing I chose to back up automatically is this &lt;a href="https://github.com/terrameijar/DevOps-Snippets/tree/main/Scripts/Wordpress%20Backup" rel="noopener noreferrer"&gt;blog using a script&lt;/a&gt; I wrote. The script creates a weekly backup of the WordPress database and the media files into compressed archives and uploads them to &lt;a href="https://vuyisile.com/amazon-s3-object-storage/" rel="noopener noreferrer"&gt;AWS S3&lt;/a&gt;. At the time of this post, the backup files are approximately 1GB each, and S3 charges $0.02/GB, which is a good deal to me.&lt;/p&gt;

&lt;p&gt;To keep S3 costs low, I set up the bucket to automatically delete backup files more than 90 days old using AWS S3’s Lifecycle Management Rules. This way, even if the size of my backup files increases, I won’t have multiple old backups wasting space and raising storage costs in S3. I’ll show you how I set this all up in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Lifecycle Management
&lt;/h2&gt;

&lt;p&gt;In the AWS console, navigate to your S3 bucket and click on the &lt;strong&gt;Management Ta&lt;/strong&gt; b.&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%2Fe3x7gh17754jb3wvcypr.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%2Fe3x7gh17754jb3wvcypr.png" width="733" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under Lifecycle configuration, click “ &lt;strong&gt;Create lifecycle rule&lt;/strong&gt; ” to create a new lifecycle rule. Lifecycle rules are actions you want AWS S3 to take on your files during their lifetime, such as moving them between storage classes or deleting them after a while.&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%2Fq8kz1jkx2rivxil8kswh.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%2Fq8kz1jkx2rivxil8kswh.png" alt="AWS S3 lifecycle configuration page" width="800" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, give the lifecycle rule a descriptive name, and specify whether you want the rule to apply to the entire bucket or specific objects. If you choose to apply the rule to all bucket objects, click the check box that is presented and move on to the Lifecycle rule actions.&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%2F8r2pf6475125e5hunoiv.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%2F8r2pf6475125e5hunoiv.png" alt="AWS S3 create lifecycle rule menue" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check the “Expire current versions of objects” box to make AWS delete or expire the files after some time. In versioned buckets, expiring an object adds a delete marker to the object, similar to a soft delete. In buckets without versioning enabled, expiring an object deletes it from the bucket. In my case, the bucket isn’t versioned, so this option is good for me. In the next section, set the days to wait before expiring objects. In the image below, I chose 90, this means that files older than 90 days will be automatically deleted.&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%2Fh9836heckoe4wmfrq9ou.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%2Fh9836heckoe4wmfrq9ou.png" alt="AWS S3 expire current versions of objects menu" width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Deleting old backups using lifecycle management in S3 is cost-effective and simple to set up. I’ll probably use some version of this approach for future backups. For example, I could use lifecycle rules to automatically transition older backups to cheaper storage classes before expiring them. How do you rotate backups? Let me know in the comments.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>backup</category>
    </item>
    <item>
      <title>The History of Virtual Machines and Containers</title>
      <dc:creator>Vuyisile Ndlovu</dc:creator>
      <pubDate>Tue, 22 Apr 2025 10:00:24 +0000</pubDate>
      <link>https://dev.to/vndlovu/the-history-of-virtual-machines-and-containers-5084</link>
      <guid>https://dev.to/vndlovu/the-history-of-virtual-machines-and-containers-5084</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Virtual Machines(VMs) and containers are at the heart of modern computing, powering everything from cloud infrastructure to local development environments. The concepts we now know as VMs and containers have been around since the early days of computing and were shaped by decades of research, hardware constraints and industry needs.&lt;/p&gt;

&lt;p&gt;This post is my attempt to provide a brief history or timeline that covers the evolution of VMs and containers, their common origins and how they developed into the modern tools we use today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Concepts and Terminology
&lt;/h2&gt;

&lt;p&gt;VMs and containers were designed to enable &lt;strong&gt;multitenancy&lt;/strong&gt; — running multiple workloads on the same machine by abstracting the underlying host. They achieve this using different approaches:  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kernel&lt;/strong&gt; : The Operating System’s main component that interacts with the hardware. It manages system resources like CPU, RAM and connected devices. If an Operating System is a car, the kernel is its engine.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Virtual Machines&lt;/strong&gt; : Emulate physical machines and real hardware using control software called hypervisors.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Containers&lt;/strong&gt; : Share the host kernel but isolate applications at the process level.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Hypervisor&lt;/strong&gt; : Software used to manage virtual machines in a host&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Virtual Machine Monitor&lt;/strong&gt; : Another name for a hypervisor, software that allows running multiple virtual operating systems on the same physical hardware.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;x86&lt;/strong&gt; : A popular family of CPU architectures used in personal computers and servers.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Paravirtualization&lt;/strong&gt; : A virtualization technique where the guest OS is modified to make it aware of the virtualization layer, allowing it to make explicit calls to the Hypervisor for some instructions.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;POSIX&lt;/strong&gt; — Portable Operating System Interface, A set of standards for UNIX-based systems to ensure compatibility between operating systems.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Namespaces&lt;/strong&gt; — An isolation feature that allow processes to have their view of certain system resources such as filesystems, network interfaces and processes without being affected by other processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Early Foundations (1960s-1980s)
&lt;/h2&gt;

&lt;p&gt;In the early computing days, software was written for a particular hardware architecture. When the hardware changed, software had to be re-written and needless to say, this was unsustainable. To address this problem, researchers proposed keeping the kernel simple, stable and managed by a few experts. This would allow the kernel to handle hardware interactions while minimising what programmers needed to know.&lt;/p&gt;

&lt;p&gt;During this time, key developments included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Improved memory isolation in hardware.&lt;/li&gt;
&lt;li&gt;Early timesharing systems that emphasized isolation&lt;/li&gt;
&lt;li&gt;Rise of Personal Computers led to the decline in VM research&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Decline and Revival of Virtual Machines (1990s-2000s)
&lt;/h2&gt;

&lt;p&gt;Although VMs existed in the 1980s and 1990s, interest waned during this period. The term “virtual machine” was even repurposed by Java and Smalltalk, something that was indicative of how dead the original concept of a VM was in this period. However, the late 1990s saw renewed interest due to research projects that sought to make Operating Systems more portable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Disco Project (1997)
&lt;/h3&gt;

&lt;p&gt;A Stanford University project that explored using Virtual Machines as a way to run operating systems on different types of hardware without extensive modifications. The project prioritised portability over security or performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  VMWare (1999)
&lt;/h3&gt;

&lt;p&gt;The team behind the Disco Project started VMWare a year later and in 1999 released a workstation product and two server products (&lt;a href="https://en.wikipedia.org/wiki/VMware_Server" rel="noopener noreferrer"&gt;VMWare GSX&lt;/a&gt; Server and &lt;a href="https://en.wikipedia.org/wiki/VMware_ESXi" rel="noopener noreferrer"&gt;VMWare ESX&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;The VMWare team faced a challenge virtualising the x86 architecture of the time because x86 processors (from Intel and AMD) weren’t built for virtualization.&lt;br&gt;&lt;br&gt;
Normally, a Virtual Machine Monitor(VMM) or Hypervisor relies on a method called &lt;strong&gt;trap-and-execute&lt;/strong&gt; to control how guest operating systems interact with hardware. VMWare couldn’t trap all the instructions because some could bypass their VMM.&lt;/p&gt;

&lt;p&gt;The x86 architecture had a problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;some instructions it processed were &lt;strong&gt;sensitive&lt;/strong&gt; but not &lt;strong&gt;privileged&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Privileged instructions such as changing memory settings trigger an error ( &lt;strong&gt;a trap&lt;/strong&gt; ) when run in user mode. When this happens, the VMM catches the error and safely handles it( &lt;strong&gt;execute&lt;/strong&gt; ).&lt;/li&gt;
&lt;li&gt;Sensitive instructions have potential system stability and security issues. These instructions don’t always trigger a trap, so they could be run without the VMM noticing, introducing potential security issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;VMware’s solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trap-and-Execute&lt;/strong&gt; where possible. For privileged instructions that triggered traps, VMware would handle them using traditional virtualization techniques.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Binary Translation&lt;/strong&gt;. For sensitive and tricky instructions that didn’t always trap, VMware rewrote the instructions on the fly before execution. Doing this allowed guest operating systems to run more efficiently, without realising they were virtualized.&lt;/li&gt;
&lt;li&gt;This approach allowed running guest operating systems unmodified and was a breakthrough at the time. Later, Intel and AMD created processor extensions in their x86 CPUs to provide hardware support for virtualization making VMWare’s workarounds unnecessary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Denali (2002)
&lt;/h3&gt;

&lt;p&gt;A University of Washington project that introduced paravirtualization, a technique that modified a guest OS to work more efficiently with a hypervisor. Since x86 hardware lacked native virtualization support at the time, paravirtualization offered a workaround by modifying CPU instructions. By making the guest OS aware of the virtulization layer,it could communicate more efficiently with the hypervisor. Instead of attempting to execute restricted instructions directly, the modified guest OS would make explicit calls to the hypervisor, allowing it to handle operations that required hardware-level execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Xen (2003)
&lt;/h3&gt;

&lt;p&gt;A University of Cambridge project that used paravirtualization techniques with an emphasis on keeping user applications unmodified. Unsafe or privileged calls were replaced with its own hyper calls. Xen introduced features that allowed tracking the resource usage of each guest, something that directly led to the creation of AWS EC2 a few years later.&lt;/p&gt;

&lt;h3&gt;
  
  
  x86 Hardware Virtualization Extensions (2004/2005)
&lt;/h3&gt;

&lt;p&gt;Due to the rise in the popularity of virtual machines and the complexity associated with running them on x86 hardware, AMD and Intel introduced (independently of each other) extensions to their processors&lt;a href="https://en.wikipedia.org/wiki/X86_virtualization#Hardware-assisted_virtualization" rel="noopener noreferrer"&gt;in the mid 2000s enabling hardware support for virtualization&lt;/a&gt;. These extensions allowed running VM code directly and trapped any sensitive instructions making paravirtualization or binary translation unnecessary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AMD-V&lt;/strong&gt; – the virtualization extension added to AMD processors around 2005. AMD-V debuted with AMD Athlon 64, Opteron and Turion 64 processors. It aimed to eliminate the need for binary translation.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Intel VT-x&lt;/strong&gt; – the virtualization extension first introduced to Intel processors in 2005 in Pentium 4. It allowed running hypervisors like VMware and KVM efficiently without using complex workarounds like Binary Translation. VT-x allowed guest OSes to run in kernel mode safely and introduced features that intercepted and emulated privileged system calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hyper-V (2008)
&lt;/h3&gt;

&lt;p&gt;In 2008, Microsoft released Hyper-V; their own Hypervisor that took advantage of the x86 virtualization extensions created by Intel and AMD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decline of VMs and the Rise of Modern Containers
&lt;/h2&gt;

&lt;p&gt;By the late 2000s, container-based solutions started gaining traction, reducing the reliance on hypervisors like QEMU+KVM, VMWare, Hyper-V and Xen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rise of Modern Containers (1982-2013)
&lt;/h2&gt;

&lt;p&gt;Containers evolved from multiple isolation technologies that were developed around the same time as VMs, including &lt;strong&gt;chroot,&lt;/strong&gt;  &lt;strong&gt;FreeBSD Jails&lt;/strong&gt; , &lt;strong&gt;resource controls, namespaces&lt;/strong&gt; and &lt;strong&gt;zones.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  POSIX Capabilities (1990s)
&lt;/h3&gt;

&lt;p&gt;In the 1990s, the&lt;a href="https://en.wikipedia.org/wiki/POSIX" rel="noopener noreferrer"&gt;POSIX Group&lt;/a&gt; proposed the &lt;a href="https://www.gsp.com/cgi-bin/man.cgi?section=3&amp;amp;topic=posix1e" rel="noopener noreferrer"&gt;POSIX.1e standard&lt;/a&gt; to add capabilities to the traditional UNIX security permissions. Capabilities allowed users to start processes or perform tasks using a subset of root privileges. The goal of the standard was to split root permissions into units or subsets that could be managed easily and assigned selectively to processes.&lt;/p&gt;

&lt;p&gt;The proposal was never accepted and later abandoned. Despite this, the Linux kernel introduced capabilities in version 2.2 in 1999. The Linux implementation wasn’t an exact match to the &lt;strong&gt;POSIX.1e&lt;/strong&gt; proposal but it retained the core principle of splitting root privileges into smaller units or capabilities. Linux is the only major OS to implement in a form similar to &lt;strong&gt;POSIX.1e&lt;/strong&gt;. It expanded them over time to make them more useful to modern security models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Early Isolation Techniques
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Chroot and FreeBSD Jails (1982-2000)
&lt;/h4&gt;

&lt;p&gt;UNIX v7 introduced Chroot, a program that allowed creating virtual filesystems by changing the root directory for running processes. Chroot limits a process to a specific directory, effectively changing the process’ world view and limits the process and its children to that new directory and its subdirectories.&lt;/p&gt;

&lt;p&gt;In 2000, FreeBSD 4.0 introduced Jails, enhancements to &lt;strong&gt;chroot&lt;/strong&gt; that extended &lt;code&gt;chroot&lt;/code&gt; to isolate not just the file system, but also network interfaces, process visibility and user credentials. Each jail could have its own root user with full permissions inside the jail and no permissions outside the jail.&lt;/p&gt;

&lt;h4&gt;
  
  
  Linux VServer and Virtuozzo (Early 2000s)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Linux-VServer&lt;/strong&gt; and &lt;strong&gt;Virtuozzo&lt;/strong&gt; were early container-like virtualization technologies that introduced process isolation and resource control mechanisms. Linux-VServer was a lightweight virtualization project that enabled multiple isolated environments to run on a single Linux Kernel. Virtuozzo, developed by Parallels was a commercial container-based virtualization platform focused on resource sharing in hosting environments. They introduced mechanisms to set limits on resources such as CPU, RAM, Disk, and other resources.&lt;/p&gt;

&lt;p&gt;For example, the projects introduced the following isolation mechanisms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory Isolation&lt;/strong&gt; – Implemented control systems that prevented containers from accessing each other’s memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process Isolation&lt;/strong&gt; – Ensured that a process in one container could not see, interact or interfere with processes from other containers or the host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Isolation&lt;/strong&gt; – Enabled isolation of network addresses, allowing processes (or containers) to have their own sets of IP addresses and network interfaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isolation work laid the groundwork for modern containers.&lt;/p&gt;

&lt;h4&gt;
  
  
  File System Mount Namespaces (2002)
&lt;/h4&gt;

&lt;p&gt;Linux introduced its first namespace, the mount namespace in 2002. Mount namespaces enabled creating isolated file system environments where processes could mount and unmount file systems without affecting the host system or other processes. The ability to isolate filesystems this way enabled processes to have different views of the filesystem, a crucial step toward isolating container environments from one another.&lt;/p&gt;

&lt;h4&gt;
  
  
  Class Based Kernel Resource Management (2003)
&lt;/h4&gt;

&lt;p&gt;An early framework that introduced the idea of classifying processes into groups or classes and assigning resources to each class as opposed to per-process resource limit. This concept played a significant role towards modern containers by influencing kernel features like cgroups, and namespaces which form the foundation of containers today.&lt;/p&gt;

&lt;h4&gt;
  
  
  Solaris Zones (AKA Solaris Containers) (2004)
&lt;/h4&gt;

&lt;p&gt;At around the same time as the developments in Linux, Solaris, a UNIX operating system introduced &lt;a href="https://en.wikipedia.org/wiki/Solaris_Containers" rel="noopener noreferrer"&gt;Zones&lt;/a&gt;, also known as Solaris Containers that isolated processes into groups that could only interact with processes within the same group.&lt;/p&gt;

&lt;h4&gt;
  
  
  Process Containers (2006/2007)
&lt;/h4&gt;

&lt;p&gt;Google used a system called Borg(precursor to Kubernetes) to manage its massive infrastructure. Borg, developed in the early 2000s allowed Google to run many tasks across thousands of machines in isolated containers. They needed an efficient way to manage, control and isolate resources, so around 2006, Google engineers began developing &lt;strong&gt;process containers&lt;/strong&gt; , an early implementation of resource isolation and control for running workloads efficiently on shared infrastructure.&lt;/p&gt;

&lt;p&gt;This work on process containers evolved and was later renamed cgroups( Control Groups) when it became a part of the Linux kernel in 2007. cgroups allowed fine-grained control over CPU, memory, I/O and other resources at the group level. This “class-based” or group-based management approach ensured that resources in one group did not starve resources in another group or class. Cgroups became the foundation for Docker, Kubernetes and modern container runtimes like containerd and CRI-O.&lt;/p&gt;

&lt;h4&gt;
  
  
  Additional Linux Kernel Namespaces (2006-2013)
&lt;/h4&gt;

&lt;p&gt;Between 2006 and 2013, the Linux kernel introduced additional namespaces that allowed isolation in process IDs, inter-process communication, network stacks and user IDs. User namespaces were the last namespace to be added. They allow unprivileged users to create a namespace or isolated environment that allows running privileged processes within the namespace while disallowing running them outside the environment. This enables a process to have root-like privileges within its own namespace without actually having those privileges outside of it in the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Modern Container Ecosystem (2008-Present)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LXC (2008)&lt;/strong&gt; – One of the first Linux container implementations, providing a userspace interface for managing containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker (2013)&lt;/strong&gt; – Popularised container usage with an easy-to-use interface, standardized images and an ecosystem that simplified deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container Orchestration&lt;/strong&gt; (2014-2015)

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Swarm (2014)&lt;/strong&gt; – Docker began work on Swarm, an orchestration tool for managing containerised applications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes (2015)&lt;/strong&gt; – Google built and open-sourced Kubernetes in 2015, it is now the de facto standard for orchestrating large-scale containerised applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LXD (2016)&lt;/strong&gt; – Developed by Canonical, LXD extended LXC by introducing a more powerful container management interface with better security and networking features.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security Concerns
&lt;/h2&gt;

&lt;p&gt;Despite their advantages, VMs and containers are not immune to vulnerabilities. In 2018, critical flaws like &lt;a href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" rel="noopener noreferrer"&gt;Spectre&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" rel="noopener noreferrer"&gt;Meltdown&lt;/a&gt; exposed security risks in both technologies, emphasizing the need for ongoing improvements.&lt;/p&gt;

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

&lt;p&gt;I enjoyed looking into the history of VMs and containers and learning how the technology behind them developed at the same time. Let me know in the comments how you use containers or virtual machines. Thanks for reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;If you’re interested in learning more about the history of VMs and contaiers, consider looking at the resources listed below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/X86_virtualization#AMD_virtualization_(AMD-V)" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/X86_virtualization#AMD_virtualization_(AMD-V)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dl.acm.org/doi/fullHtml/10.1145/3365199" rel="noopener noreferrer"&gt;https://dl.acm.org/doi/fullHtml/10.1145/3365199&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Xen" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Xen&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Denali_(operating_system)" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Denali_(operating_system)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Hyper-V" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Hyper-V&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>computerscience</category>
      <category>devops</category>
      <category>container</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
