<?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: Woulf</title>
    <description>The latest articles on DEV Community by Woulf (@woulf).</description>
    <link>https://dev.to/woulf</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%2F2908107%2Fbcc2feee-9008-4787-8908-f55037a155a7.jpg</url>
      <title>DEV Community: Woulf</title>
      <link>https://dev.to/woulf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/woulf"/>
    <language>en</language>
    <item>
      <title>From craftsmanship to industrialization: Multi-environment GitOps with Argo CD and Kustomize on MicroK8s</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Mon, 07 Jul 2025 12:51:43 +0000</pubDate>
      <link>https://dev.to/woulf/from-craftsmanship-to-industrialization-multi-environment-gitops-with-argo-cd-and-kustomize-on-205</link>
      <guid>https://dev.to/woulf/from-craftsmanship-to-industrialization-multi-environment-gitops-with-argo-cd-and-kustomize-on-205</guid>
      <description>&lt;p&gt;&lt;em&gt;Setting up a self-hosted multi-environment GitOps workflow using Argo CD, Kustomize, and cert-manager on MicroK8s: architecture, challenges, mistakes, and Kubernetes deployment automation.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🌍 Context: Why this project?
&lt;/h2&gt;

&lt;p&gt;This project is the logical continuation of a process started several months earlier. Initially, my infrastructure was managed manually: a few Kubernetes files stored in a repo, an Ingress to manage incoming traffic, a Let’s Encrypt certificate, a deployment exposed internally via a ClusterIP service, and a NodePort for external access.&lt;/p&gt;

&lt;p&gt;But this approach quickly showed its limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No multi-environment management
&lt;/li&gt;
&lt;li&gt;No overall view of the cluster's state
&lt;/li&gt;
&lt;li&gt;Updates via pipelines were limited (only resource creation or update, with the rest done manually), and no clear history
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted a more robust setup, closer to what’s done in real companies: full infrastructure versioning, multi-env deployment, GitOps, more complete monitoring (traces, logs), and security.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Technical Goals
&lt;/h2&gt;

&lt;p&gt;This new project aims to build a solid and maintainable foundation to deploy any application in a self-hosted Kubernetes cluster. The pillars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitOps&lt;/strong&gt; with Argo CD for declarative deployments
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-environment&lt;/strong&gt; (dev, staging, prod...) using kustomize
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS Certificates&lt;/strong&gt; automatically managed by cert-manager
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NGINX Ingress&lt;/strong&gt; for public HTTPS exposure
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full infra versioning&lt;/strong&gt;: Ingress, Cert-Manager, and Argo CD are installed via Helm, but managed with Kustomize
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One namespace per environment&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📊 Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MicroK8s&lt;/strong&gt;: lightweight single-node Kubernetes distribution
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helm&lt;/strong&gt;: chart manager for installing tools like Argo CD, cert-manager, ingress-nginx
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kustomize&lt;/strong&gt;: overlays for different environments (dev, prod)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argo CD&lt;/strong&gt;: GitOps engine for continuous deployment
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cert-manager + ClusterIssuer Let’s Encrypt&lt;/strong&gt;: public TLS certificates&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📂 Repo Structure
&lt;/h2&gt;

&lt;p&gt;The repository is structured as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├── environments/
│   ├── dev/         &lt;span class="c"&gt;# Development environment&lt;/span&gt;
│   ├── staging/     &lt;span class="c"&gt;# Pre-production environment&lt;/span&gt;
│   └── prod/        &lt;span class="c"&gt;# Simulated production&lt;/span&gt;
├── base/            &lt;span class="c"&gt;# K8s components common to all environments&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool is installed via Helm with a versioned &lt;code&gt;values.yaml&lt;/code&gt;, and the &lt;code&gt;dev/&lt;/code&gt;, &lt;code&gt;staging/&lt;/code&gt;, and &lt;code&gt;prod/&lt;/code&gt; environments are Kustomize overlays with targeted patches.&lt;/p&gt;

&lt;p&gt;Repo available here: &lt;a href="https://github.com/Wooulf/devops-bootcamp-ippon" rel="noopener noreferrer"&gt;github.com/Wooulf/devops-bootcamp-ippon&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Multi-Environment Management with Kustomize
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Kustomize&lt;/code&gt; is the tool I use to cleanly manage different environments (&lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt;) from a shared base of Kubernetes resources. Unlike Helm, it doesn’t rely on an external templating engine. It’s about composing declarative files rather than generating them dynamically.&lt;/p&gt;

&lt;p&gt;Each environment inherits common resources from &lt;code&gt;base/&lt;/code&gt;, and applies environment-specific patches (e.g., domain name, Docker image, namespace...).&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── base/
│   └── portfolio/
│       ├── kustomization.yaml
│       ├── deployment.yaml
│       ├── service.yaml
│       └── ingress.yaml
├── environments/
│   ├── dev/
│   │   ├── kustomization.yaml
│   │   └── patch-ingress-host.json
│   ├── staging/
│   └── prod/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each directory (&lt;code&gt;base&lt;/code&gt; or environment-specific like &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, or &lt;code&gt;prod&lt;/code&gt;) contains a mandatory &lt;code&gt;kustomization.yaml&lt;/code&gt; file. This file defines which Kubernetes resources to compose and which environment-specific modifications to apply (e.g., a JSON patch to change the Ingress host).&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;&lt;br&gt;
&lt;code&gt;environments/dev/kustomization.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../../base/portfolio&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;patch-ingress-host.json&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio-ingress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;patch-ingress-host.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/spec/tls/0/hosts/0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev.woulf.fr"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/spec/rules/0/host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev.woulf.fr"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 With this structure, I can deploy the same project in multiple environments by only changing context and a few target variables.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔍 Quick tip: to validate the local Kustomize output before handing over to Argo CD:&lt;br&gt;&lt;br&gt;
&lt;code&gt;kubectl kustomize environments/dev&lt;/code&gt;&lt;br&gt;&lt;br&gt;
This generates the final YAML as it would be applied in the cluster.  &lt;/p&gt;

&lt;p&gt;You can then apply it using:&lt;br&gt;&lt;br&gt;
&lt;code&gt;kubectl apply -k environments/dev&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  🚀 Why I Use Argo CD
&lt;/h3&gt;

&lt;p&gt;Argo CD is the heart of my GitOps strategy: it ensures the actual Kubernetes cluster state always matches the manifests in Git.&lt;/p&gt;

&lt;p&gt;What I like about Argo CD:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear visual interface of deployment states
&lt;/li&gt;
&lt;li&gt;Automatic (pull-based) deployment on commit
&lt;/li&gt;
&lt;li&gt;History tracking, rollback support
&lt;/li&gt;
&lt;li&gt;Automatic drift detection
&lt;/li&gt;
&lt;li&gt;Easy targeting of specific environments/namespaces&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚙️ Installing Argo CD with HTTPS
&lt;/h2&gt;

&lt;p&gt;Argo CD was installed via Helm in the &lt;code&gt;argocd&lt;/code&gt; namespace, with this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd.woulf.fr&lt;/span&gt;

&lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server.insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;

&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;certificate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd-tls&lt;/span&gt;
    &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd.woulf.fr&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager.io&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIssuer&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;

  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nginx.ingress.kubernetes.io/backend-protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP"&lt;/span&gt;
    &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;argocd.woulf.fr&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;argocd.woulf.fr&lt;/span&gt;
        &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd-tls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why &lt;code&gt;server.insecure: true&lt;/code&gt;? Because TLS termination is handled at the Ingress level. Internal traffic remains HTTP, which is acceptable in a local or single-tenant VPS cluster.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌐 Public Access via Ingress
&lt;/h2&gt;

&lt;p&gt;The TLS certificate is automatically generated by cert-manager from the &lt;code&gt;letsencrypt-prod&lt;/code&gt; ClusterIssuer. Argo CD is now publicly available at &lt;a href="https://argocd.woulf.fr" rel="noopener noreferrer"&gt;https://argocd.woulf.fr&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📌 Defining a GitOps Deployment with Argo CD
&lt;/h2&gt;

&lt;p&gt;To connect Argo CD to my Git repo and define what to sync, I created an &lt;code&gt;Application&lt;/code&gt; Kubernetes resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio-dev&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/Wooulf/devops-bootcamp-ippon&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/dev&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 The Argo CD Application file is also versioned in my Git repo: &lt;a href="https://github.com/Wooulf/devops-bootcamp-ippon/blob/main/argocd/applications/portfolio-dev.yaml" rel="noopener noreferrer"&gt;argocd/applications/portfolio-dev.yaml&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;source.repoURL + path + targetRevision&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Argo CD watches the &lt;code&gt;environments/dev&lt;/code&gt; directory of the repo &lt;code&gt;https://github.com/Wooulf/devops-bootcamp-ippon&lt;/code&gt;. Any modification triggers an automatic sync.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;destination&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The app is deployed in the local cluster (MicroK8s) using &lt;code&gt;https://kubernetes.default.svc&lt;/code&gt;, in the &lt;code&gt;dev&lt;/code&gt; namespace.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;syncPolicy.automated&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;automated&lt;/code&gt;: syncs automatically without manual actions
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prune&lt;/code&gt;: removes resources no longer defined in Git
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selfHeal&lt;/code&gt;: restores resources changed manually in the cluster&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;syncOptions.CreateNamespace=true&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Automatically creates the &lt;code&gt;dev&lt;/code&gt; namespace if it doesn’t exist, making the deployment autonomous and idempotent.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🔁 Final Result
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Fully automated GitOps deployment in &lt;code&gt;dev&lt;/code&gt; environment
&lt;/li&gt;
&lt;li&gt;Continuous compliance between Git and the cluster
&lt;/li&gt;
&lt;li&gt;Resource updates and deletions only controlled via Git
&lt;/li&gt;
&lt;li&gt;Dynamic namespace creation without manual preconfiguration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;dev&lt;/code&gt; portfolio is now auto-deployed with the latest Docker image tagged &lt;code&gt;latest&lt;/code&gt;, and publicly accessible at &lt;a href="https://dev.woulf.fr" rel="noopener noreferrer"&gt;https://dev.woulf.fr&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%2Fgrdu8ym9bdzfo7vwcn3m.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%2Fgrdu8ym9bdzfo7vwcn3m.png" alt="Argo CD view showing three deployed apps: portfolio-dev, portfolio-staging, and portfolio-prod — all Healthy and Synced, with their Git paths, namespaces, and sync info." width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎯 Argo CD interface: deployment state and sync across dev, staging, and prod environments.&lt;/p&gt;
&lt;/blockquote&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%2Fgivei7dqsoeem4195wyz.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%2Fgivei7dqsoeem4195wyz.png" alt="Detailed view of Argo CD app portfolio-dev: tree view of created Kubernetes resources (Deployment, Service, Ingress, Certificate, Pod), all Healthy and synced with Git." width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔍 Detailed view of the portfolio-dev app in the dev namespace — every resource is tracked, versioned, and synced to Git.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  🧨 Problems Encountered (and Solved)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistent self-signed certificates&lt;/strong&gt;
I had persistent invalid certificates due to &lt;strong&gt;two simultaneous creation mechanisms&lt;/strong&gt;: an annotation &lt;code&gt;cert-manager.io/cluster-issuer&lt;/code&gt; on the Ingress &lt;em&gt;and&lt;/em&gt; a &lt;code&gt;server.certificate&lt;/code&gt; config in Helm's &lt;code&gt;values.yaml&lt;/code&gt;.
👉 &lt;strong&gt;Solution&lt;/strong&gt;: keep only one source of truth, Helm config with &lt;code&gt;server.certificate&lt;/code&gt; in this case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Temporary certificate active after Let’s Encrypt provisioning&lt;/strong&gt;
Cert-manager sometimes installs a temporary cert before Let’s Encrypt issues the final one.
👉 Just wait, this is expected behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MicroK8s “losing” add-ons (Helm, DNS, etc.) after reboot&lt;/strong&gt;
Some MicroK8s add-ons were disabled after reboot.
👉 Required to manually restart certain services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClusterIssuer missing after cluster reboot&lt;/strong&gt;
After reboot, my ClusterIssuer wasn’t recognized, I had forgotten to version it in GitOps at the beginning.
👉 Solved by adding it explicitly in the Argo CD-managed manifests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS access errors despite seemingly correct config&lt;/strong&gt;
In my case, caused by an &lt;strong&gt;active temporary certificate&lt;/strong&gt;, or bad &lt;strong&gt;TLS termination on Ingress&lt;/strong&gt;.
👉 Solved by handling TLS only at the Ingress level.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧾 Summary
&lt;/h2&gt;

&lt;p&gt;In this article, I started transitioning toward a multi-environment GitOps infrastructure, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installing Argo CD via Helm in a dedicated namespace
&lt;/li&gt;
&lt;li&gt;Managing HTTPS with cert-manager and a Let’s Encrypt ClusterIssuer
&lt;/li&gt;
&lt;li&gt;Resolving common certificate issues (self-signed, temporary, double creation)
&lt;/li&gt;
&lt;li&gt;Exposing Argo CD via HTTPS using NGINX Ingress
&lt;/li&gt;
&lt;li&gt;First GitOps deployment of an app (&lt;code&gt;portfolio&lt;/code&gt;) in the &lt;code&gt;dev&lt;/code&gt; environment, dynamically patched via &lt;code&gt;kustomize&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This technical foundation now allows me to test, version, and secure deployments just like in production, while keeping maximum infrastructure control.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔜 Coming in the Next Article
&lt;/h2&gt;

&lt;p&gt;We’ll move forward by integrating &lt;strong&gt;Argo CD Image Updater&lt;/strong&gt; to automatically update deployed images, adding a &lt;strong&gt;security layer with SealedSecrets&lt;/strong&gt; to protect the API token used to interact with Argo CD, and making the system fully autonomous in true GitOps fashion.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>argocd</category>
      <category>gitops</category>
      <category>devops</category>
    </item>
    <item>
      <title>Article 6: Minimalist monitoring with Prometheus &amp; Grafana</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Mon, 07 Jul 2025 12:33:06 +0000</pubDate>
      <link>https://dev.to/woulf/article-6-minimalist-monitoring-with-prometheus-grafana-m2a</link>
      <guid>https://dev.to/woulf/article-6-minimalist-monitoring-with-prometheus-grafana-m2a</guid>
      <description>&lt;p&gt;&lt;em&gt;Integrating Prometheus &amp;amp; Grafana to monitor my self-hosted Kubernetes cluster with public HTTPS access, a customized default dashboard, and maintained security, all in an MVP GitOps logic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this sixth step, I’m integrating a monitoring system to supervise my Kubernetes cluster, keeping things minimalist, self-hosted, and avoiding unnecessary complexity.&lt;/p&gt;




&lt;h3&gt;
  
  
  Objective: simple and effective visibility
&lt;/h3&gt;

&lt;p&gt;I want to view the state of my cluster at a glance: CPU, memory, pods, network. No over-engineering, no email alerts, just a clear dashboard.&lt;/p&gt;

&lt;p&gt;I’m using the Helm chart &lt;a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack" rel="noopener noreferrer"&gt;kube-prometheus-stack&lt;/a&gt;, widely used in production, even though I’m underutilizing it here for educational purposes.&lt;/p&gt;




&lt;h3&gt;
  
  
  Installation with Helm
&lt;/h3&gt;



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

helm upgrade --install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --values prometheus-stack-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I disabled &lt;code&gt;alertmanager&lt;/code&gt; in the &lt;code&gt;values.yaml&lt;/code&gt; file, since I don’t want to manage alerts for this MVP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;alertmanager&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Why not use the MicroK8s Prometheus module?
&lt;/h3&gt;

&lt;p&gt;MicroK8s offers a &lt;code&gt;prometheus&lt;/code&gt; module that can be enabled in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;microk8s enable prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this module is a &lt;strong&gt;black box&lt;/strong&gt; and hard to integrate into a GitOps workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It’s &lt;strong&gt;not versioned&lt;/strong&gt; in your Git repo&lt;/li&gt;
&lt;li&gt;It offers &lt;strong&gt;almost no control&lt;/strong&gt; over configuration or versions&lt;/li&gt;
&lt;li&gt;It doesn’t separate Grafana and Prometheus cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By choosing the Helm chart &lt;code&gt;kube-prometheus-stack&lt;/code&gt;, I keep &lt;strong&gt;full control&lt;/strong&gt; over configuration via my &lt;code&gt;values.yaml&lt;/code&gt;, can &lt;strong&gt;version my infrastructure&lt;/strong&gt;, and make my setup &lt;strong&gt;portable&lt;/strong&gt; to any cloud or local Kubernetes cluster.&lt;/p&gt;




&lt;h3&gt;
  
  
  Grafana access
&lt;/h3&gt;

&lt;p&gt;I created an Ingress at &lt;code&gt;grafana.woulf.fr&lt;/code&gt; with an HTTPS certificate managed by &lt;code&gt;cert-manager&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Admin access is protected by a Kubernetes &lt;code&gt;Secret&lt;/code&gt; (not committed), defined like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;existingSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring-grafana&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Secret creation&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl -n monitoring create secret generic monitoring-grafana \
  --from-literal=admin-user=admin \
  --from-literal=admin-password=********
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;To enable easy public access, I allowed &lt;strong&gt;anonymous&lt;/strong&gt; access with the &lt;code&gt;Viewer&lt;/code&gt; role (read-only):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana.ini&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;auth.anonymous&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;org_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Main Org.&lt;/span&gt;
      &lt;span class="na"&gt;org_role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Viewer&lt;/span&gt;
      &lt;span class="na"&gt;hide_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Default dashboard
&lt;/h3&gt;

&lt;p&gt;I chose the built-in &lt;code&gt;Kubernetes / Compute Resources / Cluster&lt;/code&gt; dashboard, then exported and versioned it into a &lt;code&gt;ConfigMap&lt;/code&gt;, mounting it as the home dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana.ini&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dashboards&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default_home_dashboard_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/grafana/dashboards/grafana-dashboard-home/default.json&lt;/span&gt;
  &lt;span class="na"&gt;dashboardsConfigMaps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;grafana-dashboard-home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana-dashboard-home&lt;/span&gt;
  &lt;span class="na"&gt;sidecar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dashboards&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana_dashboard&lt;/span&gt;
      &lt;span class="na"&gt;searchNamespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ALL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The related &lt;code&gt;ConfigMap&lt;/code&gt; is versioned in my infra repo and labeled with &lt;code&gt;grafana_dashboard: "1"&lt;/code&gt; to be auto-loaded by Grafana’s sidecar.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📁 A &lt;code&gt;ConfigMap&lt;/code&gt; is a Kubernetes resource that lets you mount non-sensitive files into a pod.&lt;br&gt;
It is automatically reloaded when modified.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Grafana is publicly accessible over HTTPS&lt;/li&gt;
&lt;li&gt;Default dashboard is readable and useful&lt;/li&gt;
&lt;li&gt;No login required to monitor the cluster&lt;/li&gt;
&lt;li&gt;Admin account is secured via a Kubernetes Secret&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach follows DevOps best practices while staying simple and understandable for a visitor or recruiter.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 What about production?
&lt;/h2&gt;

&lt;p&gt;This setup is intentionally minimalist and educational, but in a production context, several aspects would be hardened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alertmanager&lt;/strong&gt; would be enabled with alert routing to services (email, Slack, etc.) to notify when components fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana access&lt;/strong&gt; wouldn’t be anonymous: it would be IP-restricted, proxied, or SSO/LDAP-protected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin passwords&lt;/strong&gt; wouldn’t be handled via static Secrets, but through Vault or a secrets manager (SealedSecrets, ExternalSecrets).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboards&lt;/strong&gt; would be provisioned via API or dedicated files with more modular versioning strategies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS certificates&lt;/strong&gt; would be managed via larger-scale automatic rotation mechanisms (wildcard DNS, ACME DNS challenge, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for this MVP, the setup offers a great balance between simplicity, readability, baseline security, and GitOps maintainability.&lt;/p&gt;




&lt;p&gt;⚡ Next step: adding &lt;code&gt;loki&lt;/code&gt; for centralized log collection? Or testing ArgoCD for advanced GitOps?&lt;/p&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

</description>
      <category>prometheus</category>
      <category>grafana</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
    <item>
      <title>Article 5: HTTPS setup and future improvements</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Mon, 07 Jul 2025 12:25:58 +0000</pubDate>
      <link>https://dev.to/woulf/article-5-https-setup-and-future-improvements-ki</link>
      <guid>https://dev.to/woulf/article-5-https-setup-and-future-improvements-ki</guid>
      <description>&lt;p&gt;&lt;em&gt;Setting up HTTPS certificates using Let’s Encrypt and cert-manager, with challenge configuration and security annotations in the Kubernetes Ingress.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this fifth step, I’ll go over how I enabled an SSL/TLS certificate to secure &lt;strong&gt;woulf.fr&lt;/strong&gt;, still within a DevOps mindset. We'll cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Why Let’s Encrypt&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why cert-manager&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Essential annotations&lt;/strong&gt; (&lt;code&gt;ssl-redirect&lt;/code&gt;, &lt;code&gt;hsts-max-age&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Project context
&lt;/h2&gt;

&lt;p&gt;Throughout the previous articles, I’ve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployed a Kubernetes cluster using &lt;strong&gt;MicroK8s&lt;/strong&gt; on a VPS
&lt;/li&gt;
&lt;li&gt;Containerized my application with &lt;strong&gt;Docker&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set up a &lt;strong&gt;GitHub Actions CI/CD&lt;/strong&gt; pipeline to automatically build and publish the Docker image
&lt;/li&gt;
&lt;li&gt;Split &lt;strong&gt;app code&lt;/strong&gt; and &lt;strong&gt;infrastructure&lt;/strong&gt; into separate Git repositories
&lt;/li&gt;
&lt;li&gt;Deployed everything to Kubernetes using a &lt;strong&gt;GitOps&lt;/strong&gt; approach
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final step was to &lt;strong&gt;secure the site with HTTPS&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Let’s Encrypt?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://letsencrypt.org" rel="noopener noreferrer"&gt;Let’s Encrypt&lt;/a&gt; is a free, automated, and open certificate authority. It’s the go-to solution for obtaining a valid SSL/TLS certificate easily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt;: no cost for issuing or renewing certificates
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated&lt;/strong&gt;: works well with DevOps tools and Kubernetes
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusted&lt;/strong&gt;: certificates are valid in all modern browsers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why cert-manager?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;cert-manager&lt;/strong&gt; is a Kubernetes operator built to automate certificate lifecycle management (issuing, renewal, rotation...).&lt;/p&gt;

&lt;p&gt;It allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create SSL/TLS certificates&lt;/strong&gt; using Kubernetes &lt;code&gt;Certificate&lt;/code&gt; resources
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle validation challenges&lt;/strong&gt; automatically with Let’s Encrypt
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manage renewals&lt;/strong&gt; with no manual intervention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It fits naturally into a GitOps workflow: certificates and their configuration can be versioned in the infra repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the HTTP-01 challenge works
&lt;/h2&gt;

&lt;p&gt;To verify domain ownership, Let’s Encrypt uses &lt;strong&gt;HTTP-based validation&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;cert-manager creates a &lt;code&gt;Challenge&lt;/code&gt; at a special URL:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://woulf.fr/.well-known/acme-challenge/&amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;It spins up a temporary &lt;strong&gt;&lt;code&gt;acmesolver&lt;/code&gt; pod&lt;/strong&gt; that responds with a key
&lt;/li&gt;
&lt;li&gt;Let’s Encrypt queries the URL:

&lt;ul&gt;
&lt;li&gt;If the correct key is returned, the certificate is issued
&lt;/li&gt;
&lt;li&gt;If not, validation fails&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By adding this annotation to the Ingress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;acme.cert-manager.io/http01-edit-in-place: "true"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the HTTP challenge is integrated directly into the main Ingress, avoiding conflicts (which I had with separate solver pods/Ingresses).&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding annotations for extra security
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;nginx.ingress.kubernetes.io/ssl-redirect&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx.ingress.kubernetes.io/ssl-redirect: "true"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Automatically redirects HTTP traffic to HTTPS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the certificate is active, this ensures users always connect over HTTPS.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;nginx.ingress.kubernetes.io/hsts-max-age&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx.ingress.kubernetes.io/hsts-max-age: "31536000"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Adds an HSTS (HTTP Strict Transport Security) header, telling browsers to &lt;strong&gt;refuse HTTP&lt;/strong&gt; for 1 year (31,536,000 seconds).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;⚠️ Enable this &lt;strong&gt;only after HTTPS is confirmed working&lt;/strong&gt;, as it prevents fallback to HTTP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final configuration example
&lt;/h2&gt;

&lt;p&gt;Here’s a snippet of the final &lt;code&gt;Ingress&lt;/code&gt; configuration with all best practices included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf-ingress&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
    &lt;span class="na"&gt;acme.cert-manager.io/http01-edit-in-place&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/ssl-redirect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/hsts-max-age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;31536000"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;woulf.fr&lt;/span&gt;
      &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf-fr-tls&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf.fr&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
            &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;✅ The site is now accessible via HTTPS, with a &lt;strong&gt;Let’s Encrypt&lt;/strong&gt; certificate fully managed by &lt;strong&gt;cert-manager&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Here’s what I’m planning next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: set up Prometheus / Grafana stack
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized logging&lt;/strong&gt;: using Loki or EFK
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced GitOps&lt;/strong&gt;: try out ArgoCD or Flux to continuously observe and reconcile cluster state&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This project is my first step toward a fully automated and maintainable infrastructure. And it’s only the beginning 👨‍🚀&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>certmanager</category>
      <category>https</category>
      <category>devops</category>
    </item>
    <item>
      <title>Article 4: Kubernetes deployment with GitOps</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Mon, 07 Jul 2025 12:21:25 +0000</pubDate>
      <link>https://dev.to/woulf/article-4-kubernetes-deployment-with-gitops-5gl0</link>
      <guid>https://dev.to/woulf/article-4-kubernetes-deployment-with-gitops-5gl0</guid>
      <description>&lt;p&gt;&lt;em&gt;Deploying my version-controlled Kubernetes infrastructure using GitOps, with automated application via a dedicated pipeline and public exposure of my portfolio.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After setting up the build and push of my Docker image, it’s time to take it further: &lt;strong&gt;versioning and automating the deployment of my Kubernetes infrastructure&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Separation of concerns: code vs infra
&lt;/h2&gt;

&lt;p&gt;To maintain a clean architecture and GitOps logic, I split the infrastructure from the application code into two separate Git repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Wooulf/forkfolio" rel="noopener noreferrer"&gt;&lt;code&gt;Wooulf/forkfolio&lt;/code&gt;&lt;/a&gt;: contains the website’s source code (Next.js), the &lt;code&gt;Dockerfile&lt;/code&gt;, and a CI pipeline for building and pushing the Docker image.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Wooulf/infra-k8s-terraform" rel="noopener noreferrer"&gt;&lt;code&gt;Wooulf/infra-k8s-terraform&lt;/code&gt;&lt;/a&gt;: contains &lt;strong&gt;all Kubernetes configuration files&lt;/strong&gt; (&lt;code&gt;deployment.yaml&lt;/code&gt;, &lt;code&gt;service.yaml&lt;/code&gt;, etc.), and a &lt;strong&gt;separate CI/CD pipeline&lt;/strong&gt; for applying them to the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🎯 This structure decouples application code from infrastructure, making it easier to maintain, review, and evolve both parts independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deploying the application on MicroK8s
&lt;/h2&gt;

&lt;p&gt;To run my app on the cluster and expose it cleanly to the public, I’ve configured the following components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Deployment&lt;/strong&gt;: handles pod lifecycle (updates, resilience, etc.)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;ClusterIP Service&lt;/strong&gt;: stabilizes internal communication to the pod&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Ingress&lt;/strong&gt;: routes incoming HTTP traffic to the correct app&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;NodePort Service&lt;/strong&gt;: exposes the Ingress Controller to the outside world&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One request, one path
&lt;/h2&gt;

&lt;p&gt;Together, these components route an incoming HTTP request through the cluster as follows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🌐 &lt;code&gt;Client&lt;/code&gt; → &lt;code&gt;VPS&lt;/code&gt; (port 80) → &lt;code&gt;NodePort&lt;/code&gt; → &lt;code&gt;Ingress&lt;/code&gt; → &lt;code&gt;ClusterIP&lt;/code&gt; → &lt;code&gt;Pod&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s a visual diagram of the flow:&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%2Fabuijcwf91jng0eyr5mf.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%2Fabuijcwf91jng0eyr5mf.jpg" alt="Kubernetes routing diagram to my portfolio app" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Kubernetes manifest files
&lt;/h2&gt;

&lt;p&gt;These are the versioned files in the infra repo:&lt;/p&gt;

&lt;h3&gt;
  
  
  🧱 Deployment
&lt;/h3&gt;

&lt;p&gt;Handles container deployment, updates, and redundancy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf/portfolio:latest&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🌐 ClusterIP Service
&lt;/h3&gt;

&lt;p&gt;Exposes the pod within the cluster via a stable internal IP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🌍 Ingress
&lt;/h3&gt;

&lt;p&gt;Links the domain name (&lt;code&gt;woulf.fr&lt;/code&gt;) to the correct internal service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf-ingress&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf.fr&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
            &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🛣️ NodePort Service to expose the Ingress Controller
&lt;/h3&gt;

&lt;p&gt;This service exposes the NGINX Ingress Controller pod to the public by opening a port on the VPS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-ingress-microk8s-controller&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ingress&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePort&lt;/span&gt;
  &lt;span class="na"&gt;externalIPs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;185.216.27.229&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-ingress-microk8s&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;nodePort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32180&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡 &lt;code&gt;externalIPs&lt;/code&gt; allows manual exposure of a service on a VPS’s public IP. It’s a functional approach for self-hosted setups, but in a cloud context, we’d use a LoadBalancer service, which integrates with the provider’s networking. On bare-metal clusters, MetalLB can simulate this behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  GitHub Actions pipeline in the infra repo
&lt;/h2&gt;

&lt;p&gt;Once versioned, the files are applied to my cluster automatically using a &lt;strong&gt;second CI/CD pipeline&lt;/strong&gt; in the &lt;code&gt;infra-k8s-terraform&lt;/code&gt; repository.&lt;/p&gt;

&lt;p&gt;It triggers on changes to the &lt;code&gt;k8s/&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;k8s/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;apply_k8s_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/k8s-set-context@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;kubeconfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;\${{ secrets.KUBECONFIG }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl apply -f k8s/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl get all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Result: with every config commit, &lt;strong&gt;the cluster is automatically synced&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;I could go further with a &lt;strong&gt;fully-fledged GitOps tool&lt;/strong&gt; like &lt;strong&gt;ArgoCD&lt;/strong&gt; or &lt;strong&gt;Flux&lt;/strong&gt;. These tools continuously monitor the Git repository and update the cluster &lt;strong&gt;without relying on a manual pipeline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;⚠️ This setup is minimalistic: it doesn’t include high availability or dynamic traffic management. Still, it’s more than enough for a self-hosted portfolio in MVP mode.&lt;/p&gt;

&lt;p&gt;🔄 This setup gives me fast feedback between commits and production, while keeping my infra code versioned and clean. It’s not quite GitOps-as-a-Service, but it’s close.&lt;/p&gt;




&lt;p&gt;💡 In the next article, I’ll talk about &lt;strong&gt;secret management&lt;/strong&gt;, the upcoming &lt;strong&gt;HTTPS setup&lt;/strong&gt;, some &lt;strong&gt;monitoring ideas&lt;/strong&gt;, and possible future improvements to my infrastructure.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>gitops</category>
      <category>devops</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>Article 3: Docker + GitHub Actions</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Mon, 07 Jul 2025 12:16:00 +0000</pubDate>
      <link>https://dev.to/woulf/article-3-docker-github-actions-4mj8</link>
      <guid>https://dev.to/woulf/article-3-docker-github-actions-4mj8</guid>
      <description>&lt;p&gt;&lt;em&gt;Automating the Docker build of my portfolio with GitHub Actions, publishing to Docker Hub, and seamless redeployment on Kubernetes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the build and push of a Node.js app
&lt;/h2&gt;

&lt;p&gt;In this third step, I tackle the &lt;strong&gt;containerization of my Next.js application&lt;/strong&gt; (personal portfolio), and the creation of a &lt;strong&gt;CI/CD pipeline&lt;/strong&gt; to automate the Docker image build, its push to Docker Hub, and the automatic redeployment to Kubernetes.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD: Building the Docker image
&lt;/h2&gt;

&lt;p&gt;The goal is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Produce a lightweight, optimized Docker image
&lt;/li&gt;
&lt;li&gt;Automatically publish it to &lt;strong&gt;Docker Hub&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Restart the deployment on the cluster &lt;strong&gt;with zero downtime&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is defined in a GitHub Actions pipeline located in the application repository, under &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Docker image
&lt;/h2&gt;

&lt;p&gt;Here is the &lt;code&gt;Dockerfile&lt;/code&gt; used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1: Base image for build and run&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:23-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_TELEMETRY_DISABLED=1&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: Install dependencies&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;deps&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="c"&gt;# Step 3: Build and fetch Dev.to articles securely&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_URL=https://woulf.fr&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_EMAIL=corentinboucardpro@gmail.com&lt;/span&gt;

&lt;span class="c"&gt;# Secure token injection via BuildKit&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret,id&lt;span class="o"&gt;=&lt;/span&gt;devto_token &lt;span class="se"&gt;\\&lt;/span&gt;
  DEVTO_API_KEY=\$(cat /run/secrets/devto_token) node scripts/fetchDevtoArticles.js &amp;amp;&amp;amp; npm run build

&lt;span class="c"&gt;# Step 4: Final runtime image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT=3000&lt;/span&gt;

&lt;span class="c"&gt;# Create non-root user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; nodejs &lt;span class="nt"&gt;-g&lt;/span&gt; 1001 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
    adduser -S nextjs -u 1001 -G nodejs &amp;amp;&amp;amp; \\
    mkdir .next &amp;amp;&amp;amp; chown nextjs:nodejs .next

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/public ./public&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=nextjs:nodejs /app/.next/standalone ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nextjs&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧠 Notes on the Dockerfile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage build&lt;/strong&gt;: separates base, dependencies, build, and runtime phases results in a clean and minimal image.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpine base (node:23-alpine)&lt;/strong&gt;: lightweight, fast to pull, smaller attack surface (more secure).
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copying &lt;code&gt;package*.json&lt;/code&gt; first&lt;/strong&gt;: leverages Docker cache when dependencies don’t change → drastically speeds up builds.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using &lt;code&gt;npm ci&lt;/code&gt;&lt;/strong&gt;: ensures a clean install based on &lt;code&gt;package-lock.json&lt;/code&gt; without re-resolving versions.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure token injection via BuildKit&lt;/strong&gt;: avoids exposing secrets in the image or logs. Token is available only during the build.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to article fetching during build&lt;/strong&gt;: keeps content updated without committing it to the repo.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-root user (&lt;code&gt;nextjs&lt;/code&gt;)&lt;/strong&gt;: improves runtime security.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copying only required folders&lt;/strong&gt; (&lt;code&gt;.next/standalone&lt;/code&gt;, &lt;code&gt;.next/static&lt;/code&gt;): recommended by Next.js for Docker deployments.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPOSE 3000&lt;/code&gt;&lt;/strong&gt;: informative for docs and some tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔐 Result: a lightweight, secure, fast-to-build Docker image ready for production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pipeline in the application repository
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;deploy.yml&lt;/code&gt; pipeline contains two jobs:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build &amp;amp; push&lt;/strong&gt; the image
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redeploy to the Kubernetes cluster&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Website&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up QEMU&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-qemu-action@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Docker Hub&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;\${{ secrets.DOCKER_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;\${{ secrets.DOCKER_PASSWORD }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf/portfolio:latest&lt;/span&gt;
          &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;"devto_token=\${{ secrets.DEVTO_TOKEN }}"&lt;/span&gt;

  &lt;span class="na"&gt;rollout_k8s&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Kubernetes context&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/k8s-set-context@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;kubeconfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;\${{ secrets.KUBECONFIG }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restart Deployment&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;kubectl rollout restart deployment/portfolio -n default&lt;/span&gt;
          &lt;span class="s"&gt;kubectl rollout status deployment/portfolio -n default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧠 Notes on the pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-trigger on &lt;code&gt;push&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt;: deploys automatically on merge, great for rolling releases.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular workflow&lt;/strong&gt;: two isolated jobs (&lt;code&gt;docker&lt;/code&gt;, &lt;code&gt;rollout_k8s&lt;/code&gt;) for clarity and maintainability.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official Docker action with BuildKit&lt;/strong&gt;: handles cache, secrets, multi-platform builds.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets via &lt;code&gt;secrets.*&lt;/code&gt;&lt;/strong&gt;: no credentials in code, tokens injected at build time only.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--mount=type=secret&lt;/code&gt;&lt;/strong&gt;: ensures temporary use of secrets without exposing them in the final image.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pushes to Docker Hub&lt;/strong&gt; (&lt;code&gt;woulf/portfolio:latest&lt;/code&gt;): versioned and publicly accessible.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes redeploy with zero downtime&lt;/strong&gt;: &lt;code&gt;kubectl rollout restart&lt;/code&gt; updates the pod smoothly.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment status check&lt;/strong&gt;: prevents continuing if deployment fails.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;needs: docker&lt;/code&gt;&lt;/strong&gt;: guarantees the image is pushed before attempting redeploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔄 Result: a robust, fully automated CI/CD pipeline that updates content, rebuilds your Docker image, and redeploys to Kubernetes, all from a single push.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secret management
&lt;/h2&gt;

&lt;p&gt;No credentials are hardcoded. Everything is securely stored via &lt;strong&gt;GitHub Secrets&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DOCKER_USERNAME&lt;/code&gt; / &lt;code&gt;DOCKER_PASSWORD&lt;/code&gt; → for Docker Hub
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KUBECONFIG&lt;/code&gt; → to connect to the MicroK8s cluster remotely
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEVTO_TOKEN&lt;/code&gt; → to fetch Dev.to articles automatically during build&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;✅ On every push:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dev.to articles are fetched
&lt;/li&gt;
&lt;li&gt;Docker image is rebuilt and pushed
&lt;/li&gt;
&lt;li&gt;App is redeployed &lt;strong&gt;with no interruption&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add Trivy scan
&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;HEALTHCHECK&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reduce production dependencies&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;➡️ In the next article, I’ll explain how I versioned the &lt;strong&gt;Kubernetes infrastructure&lt;/strong&gt; in a separate repository and set up a &lt;strong&gt;GitOps pipeline&lt;/strong&gt; to keep the cluster in sync.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Article 2: Installing and configuring MicroK8s</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Thu, 26 Jun 2025 09:57:00 +0000</pubDate>
      <link>https://dev.to/woulf/article-2-installing-and-configuring-microk8s-3oo7</link>
      <guid>https://dev.to/woulf/article-2-installing-and-configuring-microk8s-3oo7</guid>
      <description>&lt;p&gt;&lt;em&gt;Setting up my local Kubernetes cluster with MicroK8s – installation, network configuration, activation of key modules, and secure access through UFW.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now that the project foundation is laid, it’s time to set up the Kubernetes infrastructure on my VPS using MicroK8s.&lt;br&gt;&lt;br&gt;
This will be the technical base on which I’ll deploy my portfolio.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1 - Installing MicroK8s
&lt;/h2&gt;

&lt;p&gt;I start by installing MicroK8s using the &lt;code&gt;snap&lt;/code&gt; package manager (already present on Ubuntu):&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 microk8s --classic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2 - Adding My User
&lt;/h2&gt;

&lt;p&gt;By default, all &lt;code&gt;microk8s&lt;/code&gt; commands require &lt;code&gt;sudo&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
To get rid of that, I add my user to the &lt;code&gt;microk8s&lt;/code&gt; group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo usermod -a -G microk8s $USER
newgrp microk8s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3 - Verifying Installation
&lt;/h2&gt;

&lt;p&gt;I check that the cluster is properly installed and ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;microk8s status --wait-ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4 - Enabling Essential Modules
&lt;/h2&gt;

&lt;p&gt;I enable the following components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;microk8s enable dns ingress storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick explanations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dns&lt;/code&gt;: internal name resolution within the cluster
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ingress&lt;/code&gt;: Ingress Controller (nginx reverse proxy)
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;storage&lt;/code&gt;: management of persistent volumes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 5 - Testing the Cluster
&lt;/h2&gt;

&lt;p&gt;I verify everything is working with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;microk8s kubectl get all -A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6 - Using kubectl Without microk8s Prefix
&lt;/h2&gt;

&lt;p&gt;To use &lt;code&gt;kubectl&lt;/code&gt; directly without prefix, I export the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;microk8s config &amp;gt; ~/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 7 - Opening Ports with UFW
&lt;/h2&gt;

&lt;p&gt;I configure the firewall (&lt;code&gt;ufw&lt;/code&gt;) to allow necessary connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ufw allow 22/tcp     # SSH
sudo ufw allow 80,443/tcp # HTTP / HTTPS (Ingress)
sudo ufw allow 16443/tcp  # Kubernetes API
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cluster Ready 🚀
&lt;/h2&gt;

&lt;p&gt;The Kubernetes cluster is now ready to host my upcoming deployments.&lt;/p&gt;

&lt;p&gt;In the next article, I’ll focus on &lt;strong&gt;building the Docker image&lt;/strong&gt; for my site and &lt;strong&gt;setting up the first CI/CD pipeline&lt;/strong&gt; to automatically publish it.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>microk8s</category>
      <category>selfhosted</category>
      <category>devops</category>
    </item>
    <item>
      <title>Article 1: Setting up a minimalist GitOps deployment</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Thu, 26 Jun 2025 09:23:32 +0000</pubDate>
      <link>https://dev.to/woulf/article-1-setting-up-a-minimalist-gitops-deployment-8ci</link>
      <guid>https://dev.to/woulf/article-1-setting-up-a-minimalist-gitops-deployment-8ci</guid>
      <description>&lt;p&gt;&lt;em&gt;An introduction to my GitOps approach for self-hosting my portfolio, with code/infrastructure separation, Kubernetes deployment, and automated CI/CD.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Context
&lt;/h2&gt;

&lt;p&gt;As part of my shift towards &lt;strong&gt;DevOps&lt;/strong&gt;, I decided to automate the deployment of my personal website.&lt;/p&gt;

&lt;p&gt;The project follows a &lt;strong&gt;GitOps&lt;/strong&gt; model: in other words, the Git repository (hosted on GitHub) serves as the &lt;strong&gt;single source of truth&lt;/strong&gt;. Any change merged into the repository should be automatically reflected in the production environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repository Structure
&lt;/h2&gt;

&lt;p&gt;To follow this GitOps logic, I chose to &lt;strong&gt;separate the application code&lt;/strong&gt; from the &lt;strong&gt;infrastructure configuration&lt;/strong&gt; in two distinct Git repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;application repository&lt;/strong&gt;: &lt;a href="https://github.com/Wooulf/forkfolio" rel="noopener noreferrer"&gt;&lt;code&gt;Wooulf/forkfolio&lt;/code&gt;&lt;/a&gt;, which contains the website’s source code (Next.js), the &lt;code&gt;Dockerfile&lt;/code&gt;, and a CI pipeline to build and deploy the Docker image.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;infrastructure repository&lt;/strong&gt;: &lt;a href="https://github.com/Wooulf/infra-k8s-terraform" rel="noopener noreferrer"&gt;&lt;code&gt;Wooulf/infra-k8s-terraform&lt;/code&gt;&lt;/a&gt;, which includes all Kubernetes configuration files (&lt;code&gt;deployment.yaml&lt;/code&gt;, &lt;code&gt;service.yaml&lt;/code&gt;, &lt;code&gt;ingress.yaml&lt;/code&gt;, etc.), along with another pipeline that automatically applies changes to the cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔁 This separation improves maintainability, clarifies responsibilities, and follows GitOps best practices.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Goals
&lt;/h2&gt;

&lt;p&gt;The aim is to build a &lt;strong&gt;Minimum Viable Product (MVP)&lt;/strong&gt; with an agile mindset, focusing first on the essential features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A minimalist Kubernetes cluster using &lt;strong&gt;MicroK8s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Deployment of my portfolio as a &lt;strong&gt;Docker container&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Public exposure via a &lt;strong&gt;custom domain name&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;CI/CD pipeline&lt;/strong&gt; to automate updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic monitoring&lt;/strong&gt; to observe cluster health&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why MicroK8s?
&lt;/h2&gt;

&lt;p&gt;I chose &lt;strong&gt;MicroK8s&lt;/strong&gt; for its simplicity and lightweight setup.&lt;br&gt;&lt;br&gt;
It’s an ideal solution for testing environments, proof-of-concept setups, or personal projects like this one.&lt;/p&gt;

&lt;p&gt;Other options like &lt;strong&gt;K3s&lt;/strong&gt;, &lt;strong&gt;kind&lt;/strong&gt;, or &lt;strong&gt;minikube&lt;/strong&gt; may be considered later as the project evolves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Environment Used
&lt;/h2&gt;

&lt;p&gt;I’m using a &lt;strong&gt;VPS&lt;/strong&gt; already set up at &lt;strong&gt;PulseHeberg&lt;/strong&gt;, with the following specs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Processor&lt;/strong&gt;: Intel Xeon E5-2680v4
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM&lt;/strong&gt;: 8 GB DDR4
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: 1 TB RAID 10
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection&lt;/strong&gt;: 750 Mbps
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System&lt;/strong&gt;: Ubuntu 22.04 LTS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This VPS easily exceeds MicroK8s requirements:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;“MicroK8s runs in as little as 540MB of memory, but to accommodate workloads, we recommend a system with at least 20G of disk space and 4G of memory.”&lt;/em&gt;&lt;br&gt;&lt;br&gt;
- &lt;em&gt;Official documentation&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;In the next article, I’ll walk through the initial configuration of the MicroK8s cluster on this VPS, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installing MicroK8s&lt;/li&gt;
&lt;li&gt;Enabling required modules (DNS, ingress…)&lt;/li&gt;
&lt;li&gt;Setting up the firewall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔧 The idea is to lay the technical foundations on which the entire project will be built.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>gitops</category>
      <category>devops</category>
      <category>portfolio</category>
    </item>
    <item>
      <title>De l'artisanat à l'industrialisation : GitOps multi-env avec Argo CD et Kustomize sur MicroK8s</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Fri, 30 May 2025 15:15:17 +0000</pubDate>
      <link>https://dev.to/woulf/de-lartisanat-a-lindustrialisation-gitops-multi-env-avec-argo-cd-sur-microk8s-12kn</link>
      <guid>https://dev.to/woulf/de-lartisanat-a-lindustrialisation-gitops-multi-env-avec-argo-cd-sur-microk8s-12kn</guid>
      <description>&lt;p&gt;&lt;em&gt;Mise en place d’un GitOps multi-environnement auto-hébergé avec Argo CD, Kustomize et cert-manager sur MicroK8s : architecture, défis, erreurs, et automatisation des déploiements Kubernetes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🌍 Contexte : Pourquoi ce projet ?
&lt;/h2&gt;

&lt;p&gt;Ce projet est le prolongement logique d'une démarche amorcée plusieurs mois auparavant. Initialement, mon infrastructure était gérée de façon artisanale : quelques fichiers Kubernetes posés dans un repository, un Ingress pour gérer le trafic entrant, un certificat Let’s Encrypt, un déploiement de mon app exposée en interne via un service ClusterIP, ainsi qu'un NodePort pour l'exposition externe.&lt;/p&gt;

&lt;p&gt;Mais rapidement, cette approche a montré ses limites :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pas de gestion multi-environnement&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pas de vision d'ensemble de l'état du cluster&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mises à jour via pipelines mais limitées (uniquement création ou mise à jour de ressources, le reste devant se faire manuellement), sans historique clair&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Je voulais un setup plus robuste, plus proche de ce qui se fait en entreprise : versionnement complet de l'infrastructure, déploiement multi-env, GitOps, monitoring plus complet (traces, logs) et sécurité.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Objectifs techniques
&lt;/h2&gt;

&lt;p&gt;Ce nouveau projet vise à construire une base solide et maintenable pour déployer n'importe quelle application dans un cluster Kubernetes auto-hébergé. Les piliers :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitOps&lt;/strong&gt; avec Argo CD pour des déploiements déclaratifs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-environnement&lt;/strong&gt; (dev, staging, prod...) via kustomize&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Certificats TLS&lt;/strong&gt; gérés automatiquement par cert-manager&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ingress NGINX&lt;/strong&gt; pour l'exposition publique en HTTPS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Versionnement complet de l'infra&lt;/strong&gt; : Ingress, Cert-Manager, Argo CD sont installés via Helm, mais pilotés via Kustomize&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Namespace&lt;/strong&gt; par environnement&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📊 Stack technique
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MicroK8s&lt;/strong&gt; : distribution légère de Kubernetes, mono-nœud&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Helm&lt;/strong&gt; : gestionnaire de chart pour l'installation d'outils comme Argo CD, cert-manager, ingress-nginx&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kustomize&lt;/strong&gt; : overlay d'environnements (dev, prod)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Argo CD&lt;/strong&gt; : moteur GitOps pour le déploiement continu&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;cert-manager + ClusterIssuer Let's Encrypt&lt;/strong&gt; : certificats TLS publics&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📂 Organisation du repo
&lt;/h2&gt;

&lt;p&gt;Le repository est structuré comme suit :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
├── environments/
│   ├── dev/         &lt;span class="c"&gt;# Déploiement dédié au développement&lt;/span&gt;
│   ├── staging/     &lt;span class="c"&gt;# Environnement de préproduction&lt;/span&gt;
│   └── prod/        &lt;span class="c"&gt;# Production simulée&lt;/span&gt;
├── base/            &lt;span class="c"&gt;# Composants K8s communs à tous les environnements&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chaque outil est installé via Helm avec un values.yaml versionné, et les environnements dev/, staging/ et prod/ sont des overlays Kustomize avec des patchs ciblés.&lt;/p&gt;

&lt;p&gt;Le repo est disponible ici : &lt;a href="https://github.com/Wooulf/devops-bootcamp-ippon" rel="noopener noreferrer"&gt;github.com/Wooulf/devops-bootcamp-ippon&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📦 Gestion multi-environnement avec Kustomize
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Kustomize&lt;/code&gt; est l’outil que j’utilise pour gérer proprement mes différents environnements (&lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt;) à partir d’une base commune de ressources Kubernetes. Contrairement à Helm, il ne nécessite pas de moteur de templating externe. On parle ici de composition de fichiers déclaratifs plutôt que de génération dynamique.&lt;/p&gt;

&lt;p&gt;Chaque environnement hérite des ressources communes de &lt;code&gt;base/&lt;/code&gt;, et vient appliquer des patches spécifiques (ex : nom de domaine, image Docker, namespace...).&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── base/
│   └── portfolio/
│       ├── kustomization.yaml
│       ├── deployment.yaml
│       ├── service.yaml
│       └── ingress.yaml
├── environments/
│   ├── dev/
│   │   ├── kustomization.yaml
│   │   └── patch-ingress-host.json
│   ├── staging/
│   └── prod/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chaque répertoire (qu’il s’agisse de &lt;code&gt;base&lt;/code&gt; ou d’un environnement comme &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt; ou &lt;code&gt;prod&lt;/code&gt;) contient un fichier &lt;code&gt;kustomization.yaml&lt;/code&gt; obligatoire. Ce fichier décrit les ressources Kubernetes à assembler ainsi que les éventuelles modifications spécifiques à l’environnement (comme un patch JSON pour changer le host de l’Ingress).&lt;/p&gt;

&lt;p&gt;Cette approche permet une composition claire et modulaire des ressources, tout en gardant un contrôle précis sur les différences entre environnements.&lt;/p&gt;

&lt;p&gt;Exemple :&lt;br&gt;
&lt;code&gt;environments/dev/kustomization.yaml&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;../../base/portfolio&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;patch-ingress-host.json&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio-ingress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;patch-ingress-host.json&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/spec/tls/0/hosts/0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev.woulf.fr"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"op"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/spec/rules/0/host"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev.woulf.fr"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Avec cette structure, je peux déployer le même projet dans plusieurs environnements en changeant uniquement le contexte et quelques variables cibles.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔍 Petit tip : pour valider la sortie Kustomize en local avant de confier le déploiement à Argo CD, on peut utiliser :&lt;br&gt;
&lt;code&gt;kubectl kustomize environments/dev&lt;/code&gt;&lt;br&gt;
Cela génère le YAML final tel qu’il sera appliqué dans le cluster.&lt;/p&gt;

&lt;p&gt;Et on peut l'appliquer comme ça :&lt;br&gt;
&lt;code&gt;kubectl apply -k environments/dev&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  🚀 Pourquoi j’utilise Argo CD ?
&lt;/h3&gt;

&lt;p&gt;Argo CD est le cœur de ma stratégie GitOps : c’est lui qui veille à ce que l’état réel du cluster Kubernetes soit toujours synchronisé avec les manifests Git.&lt;/p&gt;

&lt;p&gt;Ce que j’apprécie dans Argo CD :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interface visuelle claire de l’état des déploiements&lt;/li&gt;
&lt;li&gt;Déploiement automatique (pull-based) dès qu’un commit est détecté&lt;/li&gt;
&lt;li&gt;Historique des modifications, gestion des rollback&lt;/li&gt;
&lt;li&gt;Vérification automatique de la dérive de configuration&lt;/li&gt;
&lt;li&gt;Facilité de ciblage d’environnements/namespaces spécifiques&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚙️ Installation d'Argo CD avec HTTPS
&lt;/h2&gt;

&lt;p&gt;Argo CD a été installé via Helm dans le namespace &lt;code&gt;argocd&lt;/code&gt;, avec cette configuration :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd.woulf.fr&lt;/span&gt;

&lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server.insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;

&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;certificate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd-tls&lt;/span&gt;
    &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd.woulf.fr&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager.io&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIssuer&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;

  &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nginx.ingress.kubernetes.io/backend-protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP"&lt;/span&gt;
    &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;argocd.woulf.fr&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;argocd.woulf.fr&lt;/span&gt;
        &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd-tls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pourquoi &lt;code&gt;server.insecure: true&lt;/code&gt; ? Car la terminaison TLS est gérée au niveau de l'Ingress. Le trafic interne reste en HTTP, ce qui est acceptable dans un cluster local/VPS non mutualisé.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌐 Accès public via Ingress
&lt;/h2&gt;

&lt;p&gt;Le certificat TLS est généré automatiquement par cert-manager à partir du ClusterIssuer &lt;code&gt;letsencrypt-prod&lt;/code&gt;. Argo CD est maintenant accessible publiquement via &lt;a href="https://argocd.woulf.fr" rel="noopener noreferrer"&gt;https://argocd.woulf.fr&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📌 Définir un déploiement GitOps avec Argo CD
&lt;/h2&gt;

&lt;p&gt;Pour connecter Argo CD à mon dépôt Git et lui indiquer quoi synchroniser dans le cluster, j'ai créé une ressource Kubernetes de type &lt;code&gt;Application&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio-dev&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/Wooulf/devops-bootcamp-ippon&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;environments/dev&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;prune&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateNamespace=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 Le fichier Application Argo CD est également versionné dans mon dépôt Git, visible ici : &lt;a href="https://github.com/Wooulf/devops-bootcamp-ippon/blob/main/argocd/applications/portfolio-dev.yaml" rel="noopener noreferrer"&gt;argocd/applications/portfolio-dev.yaml&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔍 Comment ça fonctionne
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;source.repoURL + path + targetRevision&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Argo CD surveille le répertoire &lt;code&gt;environments/dev&lt;/code&gt; du dépôt Git &lt;code&gt;https://github.com/Wooulf/devops-bootcamp-ippon&lt;/code&gt;. Toute modification (commit, push) dans ce dossier déclenche une synchronisation automatique.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;destination&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
L’application est déployée dans le cluster local (MicroK8s) via l’URL &lt;code&gt;https://kubernetes.default.svc&lt;/code&gt;, dans le namespace &lt;code&gt;dev&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;syncPolicy.automated&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;automated&lt;/code&gt;: synchronisation automatique sans action manuelle.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prune&lt;/code&gt;: suppression des ressources obsolètes qui ne sont plus déclarées dans Git.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selfHeal&lt;/code&gt;: restauration automatique si une ressource est modifiée manuellement dans le cluster.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;syncOptions.CreateNamespace=true&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Le namespace &lt;code&gt;dev&lt;/code&gt; est créé automatiquement s’il n’existe pas encore, rendant le déploiement autonome et idempotent.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🔁 Résultat final
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Déploiement GitOps complet et automatique dans l’environnement &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Conformité assurée en permanence entre Git et le cluster&lt;/li&gt;
&lt;li&gt;Mise à jour ou suppression de ressources pilotée uniquement depuis le dépôt Git&lt;/li&gt;
&lt;li&gt;Namespace &lt;code&gt;dev&lt;/code&gt; créé dynamiquement sans préconfiguration manuelle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Le portfolio &lt;code&gt;dev&lt;/code&gt; est maintenant déployé automatiquement avec la dernière image Docker taggée &lt;code&gt;latest&lt;/code&gt;, et accessible via &lt;a href="https://dev.woulf.fr" rel="noopener noreferrer"&gt;https://dev.woulf.fr&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%2Fgrdu8ym9bdzfo7vwcn3m.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%2Fgrdu8ym9bdzfo7vwcn3m.png" alt="Vue Argo CD listant trois applications déployées : portfolio-dev, portfolio-staging et portfolio-prod. Chacune est en état Healthy et Synced, avec leurs chemins Git, namespaces et dernières synchronisations visibles." width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎯 Interface Argo CD : état de santé et synchronisation des environnements dev, staging et prod.&lt;/p&gt;
&lt;/blockquote&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%2Fgivei7dqsoeem4195wyz.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%2Fgivei7dqsoeem4195wyz.png" alt="Vue détaillée de l'application Argo CD portfolio-dev : affichage arborescent des ressources Kubernetes créées (Deployment, Service, Ingress, Certificate, Pod), toutes en état Healthy et synchronisées avec Git." width="800" height="389"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔍 Vue détaillée de l'application portfolio-dev déployée dans le namespace dev — chaque ressource est traquée, versionnée, et synchronisée à Git.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  🧨 Problèmes rencontrés (et résolus)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Certificats auto-signés persistants&lt;/strong&gt;
J’ai eu des certificats non valides persistants (auto-signés) à cause de &lt;strong&gt;deux mécanismes de création simultanés&lt;/strong&gt; : une annotation &lt;code&gt;cert-manager.io/cluster-issuer&lt;/code&gt; sur l’Ingress &lt;em&gt;et&lt;/em&gt; une configuration &lt;code&gt;server.certificate&lt;/code&gt; dans les &lt;code&gt;values.yaml&lt;/code&gt; de Helm.
👉 &lt;strong&gt;Solution&lt;/strong&gt; : ne conserver qu’une seule source de vérité. Ici, la configuration Helm via &lt;code&gt;server.certificate&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Certificat temporaire actif après provisionnement Let's Encrypt&lt;/strong&gt;
Cert-manager installe parfois un certificat temporaire avant que Let's Encrypt émette le définitif.
👉 Il suffit d’attendre la mise à jour, c’est un comportement normal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MicroK8s qui "perd" des add-ons (Helm, DNS, etc.) après redémarrage&lt;/strong&gt;
J’ai constaté que certains add-ons de MicroK8s se désactivaient au reboot.
👉 Nécessité de relancer manuellement certains services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClusterIssuer manquant après reboot du cluster&lt;/strong&gt;
Après redémarrage, mon ClusterIssuer n’était plus reconnu. Cela venait d’un oubli de le versionner dans mon GitOps au début.
👉 Résolu en l’ajoutant explicitement dans les manifests surveillés par Argo CD.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Erreurs d’accès HTTPS malgré une config a priori correcte&lt;/strong&gt;
Liées dans mon cas à un &lt;strong&gt;certificat temporaire encore actif&lt;/strong&gt;, ou à une mauvaise &lt;strong&gt;terminaison TLS côté Ingress&lt;/strong&gt;.
👉 En fixant la gestion TLS uniquement au niveau Ingress, le problème a disparu.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧾 En résumé
&lt;/h2&gt;

&lt;p&gt;Dans cet article, j’ai amorcé la transition vers une infrastructure GitOps multi-environnement, avec :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;L’installation d’Argo CD via Helm dans un namespace dédié&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;La gestion du HTTPS avec cert-manager et un ClusterIssuer Let’s Encrypt&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;La résolution de problèmes classiques liés aux certificats (self-signed, temporaires, double création de certificat)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;L’exposition d’Argo CD en HTTPS via Ingress NGINX&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Le premier déploiement GitOps d’une application (&lt;code&gt;portfolio&lt;/code&gt;) dans l’environnement &lt;code&gt;dev&lt;/code&gt;, patché dynamiquement via &lt;code&gt;kustomize&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ce socle technique me permet désormais de tester, versionner et sécuriser mes déploiements comme en production, tout en gardant un maximum de contrôle sur l'infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔜 À venir dans le prochain article
&lt;/h2&gt;

&lt;p&gt;On poursuivra en intégrant &lt;strong&gt;Argo CD Image Updater&lt;/strong&gt; pour assurer la mise à jour automatique des images déployées, en ajoutant une couche de &lt;strong&gt;sécurité avec SealedSecrets&lt;/strong&gt; pour protéger le token d'API permettant d'interagir avec Argo CD. On verra aussi comment rendre ce système autonome, sans intervention manuelle, toujours dans une logique GitOps.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>argocd</category>
      <category>gitops</category>
      <category>devops</category>
    </item>
    <item>
      <title>Article 6 : Monitoring minimaliste avec Prometheus &amp; Grafana</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Sun, 18 May 2025 14:21:04 +0000</pubDate>
      <link>https://dev.to/woulf/article-6-monitoring-minimaliste-avec-prometheus-grafana-kdn</link>
      <guid>https://dev.to/woulf/article-6-monitoring-minimaliste-avec-prometheus-grafana-kdn</guid>
      <description>&lt;p&gt;&lt;em&gt;Intégration de Prometheus &amp;amp; Grafana pour monitorer mon cluster Kubernetes auto-hébergé, avec accès HTTPS public, dashboard par défaut personnalisé et sécurité maintenue, le tout dans une logique MVP GitOps.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dans cette sixième étape, j'intègre un système de monitoring pour superviser mon cluster Kubernetes, toujours dans une logique MVP, auto-hébergée, et sans complexité inutile.&lt;/p&gt;




&lt;h3&gt;
  
  
  Objectif : visibilité simple et efficace
&lt;/h3&gt;

&lt;p&gt;Je veux pouvoir visualiser l'état de mon cluster en un coup d’œil : CPU, mémoire, pods, réseau. Pas de sur-ingénierie, pas d’alerts emails, juste un dashboard clair.&lt;/p&gt;

&lt;p&gt;J'utilise le chart Helm &lt;a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack" rel="noopener noreferrer"&gt;kube-prometheus-stack&lt;/a&gt;, largement adopté en prod, même s’il est ici sous-utilisé dans un but pédagogique.&lt;/p&gt;




&lt;h3&gt;
  
  
  Installation via Helm
&lt;/h3&gt;



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

helm upgrade --install monitoring prometheus-community/kube-prometheus-stack \
  --namespace monitoring \
  --create-namespace \
  --values prometheus-stack-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;J'ai désactivé &lt;code&gt;alertmanager&lt;/code&gt; dans le &lt;code&gt;values.yaml&lt;/code&gt;, car je ne souhaite pas gérer d'alertes pour ce MVP :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;alertmanager&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Pourquoi ne pas utiliser le module Prometheus de MicroK8s ?
&lt;/h3&gt;

&lt;p&gt;MicroK8s propose un module &lt;code&gt;prometheus&lt;/code&gt; activable en une ligne :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;microk8s enable prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mais ce module reste une &lt;strong&gt;boîte noire&lt;/strong&gt; difficile à intégrer dans un workflow GitOps :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Il n'est &lt;strong&gt;pas versionné&lt;/strong&gt; dans le dépôt Git&lt;/li&gt;
&lt;li&gt;Il n'offre &lt;strong&gt;quasiment aucun contrôle&lt;/strong&gt; sur la configuration ou les versions&lt;/li&gt;
&lt;li&gt;Il ne permet pas de séparer proprement Grafana et Prometheus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En choisissant le chart Helm &lt;code&gt;kube-prometheus-stack&lt;/code&gt;, je garde la &lt;strong&gt;maîtrise complète de la configuration&lt;/strong&gt; via mon &lt;code&gt;values.yaml&lt;/code&gt;, je peux &lt;strong&gt;versionner mon infrastructure&lt;/strong&gt;, et je rends mon setup &lt;strong&gt;portatif&lt;/strong&gt; sur n'importe quel autre cluster Kubernetes cloud ou local.&lt;/p&gt;




&lt;h3&gt;
  
  
  Accès à Grafana
&lt;/h3&gt;

&lt;p&gt;J'ai créé un Ingress à &lt;code&gt;grafana.woulf.fr&lt;/code&gt; avec certificat HTTPS géré par &lt;code&gt;cert-manager&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;L'accès admin est protégé par un &lt;code&gt;Secret&lt;/code&gt; Kubernetes (non committé) défini comme :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;existingSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring-grafana&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Création du Secret&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl -n monitoring create secret generic monitoring-grafana \
  --from-literal=admin-user=admin \
  --from-literal=admin-password=********
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;p&gt;Et pour permettre un accès public simple, j’ai activé l'accès &lt;strong&gt;anonyme&lt;/strong&gt; avec le rôle &lt;code&gt;Viewer&lt;/code&gt; (lecture seule) :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana.ini&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;auth.anonymous&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;org_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Main Org.&lt;/span&gt;
      &lt;span class="na"&gt;org_role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Viewer&lt;/span&gt;
      &lt;span class="na"&gt;hide_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Dashboard par défaut
&lt;/h3&gt;

&lt;p&gt;J'ai choisi le dashboard &lt;code&gt;Kubernetes / Compute Resources / Cluster&lt;/code&gt; fourni par défaut dans le chart, puis je l’ai exporté, versionné dans un &lt;code&gt;ConfigMap&lt;/code&gt;, et monté comme dashboard d’accueil :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;grafana.ini&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dashboards&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;default_home_dashboard_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/lib/grafana/dashboards/grafana-dashboard-home/default.json&lt;/span&gt;
  &lt;span class="na"&gt;dashboardsConfigMaps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;grafana-dashboard-home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana-dashboard-home&lt;/span&gt;
  &lt;span class="na"&gt;sidecar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dashboards&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana_dashboard&lt;/span&gt;
      &lt;span class="na"&gt;searchNamespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ALL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le &lt;code&gt;ConfigMap&lt;/code&gt; correspondant est versionné dans le repo d’infra, et porte le label &lt;code&gt;grafana_dashboard: "1"&lt;/code&gt; pour être pris en compte automatiquement par le sidecar Grafana.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📁 Un &lt;code&gt;ConfigMap&lt;/code&gt; est une ressource Kubernetes qui permet de monter des fichiers non sensibles dans un pod.&lt;br&gt;
Il est rechargé automatiquement en cas de modification.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Résultat
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Grafana accessible publiquement, en HTTPS&lt;/li&gt;
&lt;li&gt;Dashboard par défaut lisible et utile&lt;/li&gt;
&lt;li&gt;Aucun login requis pour consulter l’état du cluster&lt;/li&gt;
&lt;li&gt;Compte admin sécurisé via Secret Kubernetes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cette approche respecte les bonnes pratiques DevOps, tout en restant simple et facilement compréhensible pour un visiteur ou un recruteur.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Et en production ?
&lt;/h2&gt;

&lt;p&gt;Ce setup est volontairement minimaliste et pédagogique, mais plusieurs aspects seraient renforcés dans un contexte de production :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alertmanager&lt;/strong&gt; serait activé, avec des routes d’alerte vers des services externes (email, Slack, etc.), pour être notifié dès qu’un composant tombe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L’accès Grafana&lt;/strong&gt; ne serait pas ouvert en anonyme : il serait restreint par IP, protégé par un proxy ou connecté à un SSO/LDAP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le mot de passe admin&lt;/strong&gt; ne serait pas géré via un &lt;code&gt;Secret&lt;/code&gt; statique, mais externalisé via Vault ou une solution de gestion de secrets (SealedSecrets, ExternalSecrets).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Les dashboards&lt;/strong&gt; seraient provisionnés via API ou fichiers dédiés, avec une stratégie de gestion de version plus modulaire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Le certificat TLS&lt;/strong&gt; serait géré via des mécanismes de rotation automatique à plus grande échelle (wildcard DNS, ACME DNS challenge...).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mais dans le cadre de ce MVP, cette configuration me donne un bon équilibre entre simplicité, lisibilité, sécurité de base et maintenabilité GitOps.&lt;/p&gt;




&lt;p&gt;⚡ Prochaine étape : ajout de &lt;code&gt;loki&lt;/code&gt; pour la collecte de logs centralisée ? Ou test d’ArgoCD pour GitOps avancé ?&lt;/p&gt;

&lt;p&gt;C'est ouvert !&lt;/p&gt;

</description>
      <category>prometheus</category>
      <category>grafana</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
    <item>
      <title>Article 5 : Finalisation de l’HTTPS et perspectives d'évolution</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Wed, 16 Apr 2025 08:58:00 +0000</pubDate>
      <link>https://dev.to/woulf/article-5-finalisation-de-lhttps-et-perspectives-devolution-54e2</link>
      <guid>https://dev.to/woulf/article-5-finalisation-de-lhttps-et-perspectives-devolution-54e2</guid>
      <description>&lt;p&gt;&lt;em&gt;Mise en place d’un certificat HTTPS via Let’s Encrypt et cert-manager, avec configuration des challenges et annotations de sécurité dans l’Ingress Kubernetes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Dans cette cinquième étape, je reviens sur la mise en place d’un certificat SSL/TLS pour sécuriser le site &lt;strong&gt;woulf.fr&lt;/strong&gt;, toujours dans une optique DevOps. Nous allons voir :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pourquoi Let’s Encrypt&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pourquoi cert-manager&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Les annotations essentielles&lt;/strong&gt; (&lt;code&gt;ssl-redirect&lt;/code&gt;, &lt;code&gt;hsts-max-age&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Contexte du projet
&lt;/h2&gt;

&lt;p&gt;Au fil des articles précédents, j’ai :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Déployé un cluster Kubernetes via &lt;strong&gt;MicroK8s&lt;/strong&gt; sur un VPS.&lt;/li&gt;
&lt;li&gt;Conteneurisé mon application avec &lt;strong&gt;Docker&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Mis en place une &lt;strong&gt;CI/CD GitHub Actions&lt;/strong&gt; pour builder et publier automatiquement l’image Docker.&lt;/li&gt;
&lt;li&gt;Séparé &lt;strong&gt;code applicatif&lt;/strong&gt; et &lt;strong&gt;infrastructure&lt;/strong&gt; dans deux dépôts Git distincts.&lt;/li&gt;
&lt;li&gt;Déployé le tout sur Kubernetes avec une logique &lt;strong&gt;GitOps&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La dernière étape consistait à &lt;strong&gt;sécuriser l’accès au site&lt;/strong&gt; avec HTTPS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pourquoi Let’s Encrypt ?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://letsencrypt.org" rel="noopener noreferrer"&gt;Let’s Encrypt&lt;/a&gt; est une autorité de certification gratuite, automatisée et ouverte. C’est aujourd’hui la référence pour obtenir facilement un certificat SSL/TLS valide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avantages :
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gratuit&lt;/strong&gt; : aucun coût d’émission ni de renouvellement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatisé&lt;/strong&gt; : compatible avec les outils DevOps et Kubernetes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnu&lt;/strong&gt; : les certificats sont valides pour tous les navigateurs modernes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Pourquoi cert-manager ?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;cert-manager&lt;/strong&gt; est un opérateur Kubernetes développé pour gérer automatiquement les certificats (émission, renouvellement, rotation…).&lt;/p&gt;

&lt;p&gt;Il permet de :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Créer un certificat SSL/TLS&lt;/strong&gt; via des objets &lt;code&gt;Certificate&lt;/code&gt; Kubernetes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effectuer la validation (challenge)&lt;/strong&gt; automatiquement auprès de Let’s Encrypt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gérer les renouvellements&lt;/strong&gt; sans aucune intervention manuelle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Il s’intègre très bien dans un workflow GitOps : on peut versionner les certificats et leur configuration dans le repo d’infra.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fonctionnement du challenge HTTP-01
&lt;/h2&gt;

&lt;p&gt;Pour vérifier que je suis bien propriétaire du nom de domaine, Let’s Encrypt effectue une &lt;strong&gt;vérification via HTTP&lt;/strong&gt; :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;cert-manager crée un &lt;code&gt;Challenge&lt;/code&gt; avec une URL spéciale sur le domaine :
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   http://woulf.fr/.well-known/acme-challenge/&amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Il lance un &lt;strong&gt;pod &lt;code&gt;acmesolver&lt;/code&gt;&lt;/strong&gt; temporaire qui répond à cette URL avec une clé unique.&lt;/li&gt;
&lt;li&gt;Let’s Encrypt interroge l’URL :

&lt;ul&gt;
&lt;li&gt;Si la bonne clé est retournée, le certificat est délivré.&lt;/li&gt;
&lt;li&gt;Sinon, la validation échoue.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Avec l’ajout de l’annotation suivante dans l’Ingress :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;acme.cert-manager.io/http01-edit-in-place: "true"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;le challenge HTTP est directement intégré dans l’Ingress principal, ce qui évite des conflits (que j'ai eu avec des Ingress/POD dédiés au solver).&lt;/p&gt;




&lt;h2&gt;
  
  
  Ajout d'annotations pour une sécurité supplémentaire
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;nginx.ingress.kubernetes.io/ssl-redirect&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx.ingress.kubernetes.io/ssl-redirect: "true"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Forcer automatiquement la redirection HTTP vers HTTPS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Une fois le certificat valide, on peut s'assurer que les utilisateurs accèdent toujours au site en HTTPS.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;nginx.ingress.kubernetes.io/hsts-max-age&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx.ingress.kubernetes.io/hsts-max-age: "31536000"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Ajoute un en-tête HSTS (HTTP Strict Transport Security) pour dire aux navigateurs de &lt;strong&gt;refuser toute future connexion en HTTP&lt;/strong&gt; pendant 1 an (31 536 000 secondes).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;⚠️ Attention à ne l’activer &lt;strong&gt;qu’après avoir vérifié que HTTPS fonctionne&lt;/strong&gt;, car cela empêche tout retour vers HTTP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exemple final de configuration
&lt;/h2&gt;

&lt;p&gt;Voici un extrait du fichier &lt;code&gt;Ingress&lt;/code&gt; après intégration des bonnes pratiques :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf-ingress&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cert-manager.io/cluster-issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
    &lt;span class="na"&gt;acme.cert-manager.io/http01-edit-in-place&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/ssl-redirect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/hsts-max-age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;31536000"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;woulf.fr&lt;/span&gt;
      &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf-fr-tls&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf.fr&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
            &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Ce qu’il reste à faire
&lt;/h2&gt;

&lt;p&gt;✅ Le site est maintenant accessible en HTTPS, avec un certificat &lt;strong&gt;Let’s Encrypt&lt;/strong&gt; automatiquement géré par &lt;strong&gt;cert-manager&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Voici ce que je prévois pour la suite :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt; : mettre en place un stack Prometheus / Grafana.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs centralisés&lt;/strong&gt; : avec Loki ou EFK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitOps avancé&lt;/strong&gt; : tester ArgoCD ou Flux pour observer et réconcilier les états du cluster.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Ce projet est un premier pas vers une infrastructure entièrement automatisée et maintenable. Et ce n’est que le début 👨‍🚀&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>certmanager</category>
      <category>https</category>
      <category>devops</category>
    </item>
    <item>
      <title>Article 4 : Déploiement Kubernetes avec GitOps</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Mon, 07 Apr 2025 12:09:18 +0000</pubDate>
      <link>https://dev.to/woulf/article-4-deploiement-kubernetes-avec-gitops-55gf</link>
      <guid>https://dev.to/woulf/article-4-deploiement-kubernetes-avec-gitops-55gf</guid>
      <description>&lt;p&gt;&lt;em&gt;Déploiement de mon infrastructure Kubernetes versionnée avec GitOps, avec application automatisée via une pipeline dédiée et exposition publique de mon portfolio.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Après avoir mis en place le build et le push de mon image Docker, il est temps d’aller plus loin : &lt;strong&gt;versionner et automatiser le déploiement de mon infrastructure Kubernetes&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Séparation des responsabilités : code vs infra
&lt;/h2&gt;

&lt;p&gt;Pour garder une architecture propre et maintenir une logique GitOps, j’ai séparé l’infrastructure du code applicatif dans deux dépôts Git différents :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Wooulf/forkfolio" rel="noopener noreferrer"&gt;&lt;code&gt;Wooulf/forkfolio&lt;/code&gt;&lt;/a&gt; : contient le code source de mon site (Next.js), le &lt;code&gt;Dockerfile&lt;/code&gt; et une pipeline CI pour builder + déployer l’image Docker.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Wooulf/infra-k8s-terraform" rel="noopener noreferrer"&gt;&lt;code&gt;Wooulf/infra-k8s-terraform&lt;/code&gt;&lt;/a&gt; : contient &lt;strong&gt;tous les fichiers de configuration Kubernetes&lt;/strong&gt; (&lt;code&gt;deployment.yaml&lt;/code&gt;, &lt;code&gt;service.yaml&lt;/code&gt;, etc.), et une &lt;strong&gt;autre pipeline CI/CD&lt;/strong&gt; dédiée à leur application sur le cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🎯 Ce découpage permet de découpler le code applicatif de l'infrastructure, ce qui facilite la maintenance, les revues de code ciblées, et l’évolution des deux parties de manière indépendante.&lt;/p&gt;




&lt;h2&gt;
  
  
  Déploiement de l'application sur MicroK8s
&lt;/h2&gt;

&lt;p&gt;Pour que mon application tourne sur le cluster et soit exposée proprement au public, j’ai mis en place les éléments suivants :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un &lt;strong&gt;Deployment&lt;/strong&gt; : qui gère le cycle de vie du pod (mises à jour, résilience…)&lt;/li&gt;
&lt;li&gt;Un &lt;strong&gt;Service ClusterIP&lt;/strong&gt; : pour stabiliser la communication interne vers le pod&lt;/li&gt;
&lt;li&gt;Un &lt;strong&gt;Ingress&lt;/strong&gt; : pour router les requêtes HTTP externes vers la bonne application&lt;/li&gt;
&lt;li&gt;Un &lt;strong&gt;Service NodePort&lt;/strong&gt; : pour exposer l’Ingress Controller au monde extérieur&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Une requête, un chemin
&lt;/h2&gt;

&lt;p&gt;L'ensemble de ces composants permet de faire transiter une requête HTTP entrante à travers le cluster, comme ceci :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🌐 &lt;code&gt;Client&lt;/code&gt; → &lt;code&gt;VPS&lt;/code&gt; (port 80) → &lt;code&gt;NodePort&lt;/code&gt; → &lt;code&gt;Ingress&lt;/code&gt; → &lt;code&gt;ClusterIP&lt;/code&gt; → &lt;code&gt;Pod&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Voici un schéma clair et visuel du fonctionnement :&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%2Fabuijcwf91jng0eyr5mf.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%2Fabuijcwf91jng0eyr5mf.jpg" alt="Schéma de routage Kubernetes vers mon app portfolio" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Fichiers de définition Kubernetes
&lt;/h2&gt;

&lt;p&gt;Voici les fichiers versionnés dans le repo d’infra :&lt;/p&gt;

&lt;h3&gt;
  
  
  🧱 Deployment
&lt;/h3&gt;

&lt;p&gt;Gère le déploiement du conteneur, les mises à jour, la redondance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf/portfolio:latest&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🌐 Service ClusterIP
&lt;/h3&gt;

&lt;p&gt;Expose le pod à l’intérieur du cluster via une IP stable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🌍 Ingress
&lt;/h3&gt;

&lt;p&gt;Fait le lien entre un nom de domaine (&lt;code&gt;woulf.fr&lt;/code&gt;) et le bon service interne.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf-ingress&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf.fr&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
            &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;portfolio&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🛣️ Service NodePort pour exposer l’Ingress Controller
&lt;/h3&gt;

&lt;p&gt;Ce Service expose le pod NGINX de l’Ingress Controller vers l’extérieur, via un port ouvert sur le VPS.&lt;br&gt;
Cela permet aux requêtes HTTP/HTTPS d’atteindre le cluster depuis l’extérieur.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-ingress-microk8s-controller&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ingress&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePort&lt;/span&gt;
  &lt;span class="na"&gt;externalIPs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;185.216.27.229&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx-ingress-microk8s&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;nodePort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32180&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;💡 externalIPs permet d’exposer manuellement un service sur l’IP publique d’un VPS. C’est une approche fonctionnelle en environnement auto-hébergé, mais dans un contexte cloud, on privilégiera les Service de type LoadBalancer, qui s’intègrent directement avec l’infrastructure réseau du fournisseur. Pour les clusters sans cloud provider, une solution comme MetalLB permet de simuler ce comportement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pipeline GitHub Actions dans le repo infra
&lt;/h2&gt;

&lt;p&gt;Une fois les fichiers versionnés, je les applique automatiquement sur mon cluster grâce à une &lt;strong&gt;deuxième pipeline CI/CD&lt;/strong&gt; dans le dépôt &lt;code&gt;infra-k8s-terraform&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Elle s’exécute dès qu’un fichier est modifié dans le dossier &lt;code&gt;k8s/&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;k8s/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;apply_k8s_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/k8s-set-context@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;kubeconfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.KUBECONFIG }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl apply -f k8s/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl get all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Résultat : à chaque commit de config, &lt;strong&gt;le cluster se synchronise automatiquement&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Et la suite ?
&lt;/h2&gt;

&lt;p&gt;Je pourrais aller encore plus loin avec un &lt;strong&gt;outil GitOps complet&lt;/strong&gt; comme &lt;strong&gt;ArgoCD&lt;/strong&gt; ou &lt;strong&gt;Flux&lt;/strong&gt;. Ces outils se chargeraient de &lt;strong&gt;surveiller le dépôt Git en continu&lt;/strong&gt; et de mettre à jour le cluster &lt;strong&gt;sans passer par une pipeline manuelle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;⚠️ Ce setup reste minimaliste : il n'inclut pas de haute disponibilité ni de gestion dynamique du trafic. Il est cependant suffisant pour un portfolio auto-hébergé, dans une logique de MVP.&lt;/p&gt;

&lt;p&gt;🔄 Ce setup me permet d’avoir une boucle de feedback rapide entre mes commits et le résultat en production, tout en gardant un code d’infra proprement versionné. Ce n’est pas encore du GitOps “as a Service”, mais on s’en approche.&lt;/p&gt;




&lt;p&gt;💡 Dans le prochain article, je parlerai de la &lt;strong&gt;gestion des secrets&lt;/strong&gt;, du futur passage en &lt;strong&gt;HTTPS&lt;/strong&gt;, des idées de &lt;strong&gt;monitoring&lt;/strong&gt;, et des prochaines évolutions possibles de mon infrastructure.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>gitops</category>
      <category>devops</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>Article 3 : Docker + GitHub Actions</title>
      <dc:creator>Woulf</dc:creator>
      <pubDate>Mon, 07 Apr 2025 12:00:33 +0000</pubDate>
      <link>https://dev.to/woulf/article-3-docker-github-actions-4jno</link>
      <guid>https://dev.to/woulf/article-3-docker-github-actions-4jno</guid>
      <description>&lt;p&gt;&lt;em&gt;Automatisation du build Docker de mon portfolio avec GitHub Actions, publication sur Docker Hub et redéploiement transparent sur Kubernetes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatiser le build et le push d’une app Node.js
&lt;/h2&gt;

&lt;p&gt;Dans cette troisième étape, je m’attaque à la &lt;strong&gt;conteneurisation de mon application&lt;/strong&gt; Next.js (portfolio personnel), et à la mise en place d’une &lt;strong&gt;pipeline CI/CD&lt;/strong&gt; pour automatiser le build de l’image Docker, son push sur Docker Hub, et le redéploiement automatique sur Kubernetes.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI/CD : création de l’image Docker
&lt;/h2&gt;

&lt;p&gt;L’objectif est de :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Produire une image Docker légère et optimisée&lt;/li&gt;
&lt;li&gt;La publier automatiquement sur &lt;strong&gt;Docker Hub&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Redémarrer le déploiement sur le cluster &lt;strong&gt;sans downtime&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tout est défini dans une pipeline GitHub Actions située dans le dépôt de mon application, sous &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Construction de l’image Docker
&lt;/h2&gt;

&lt;p&gt;Voici le &lt;code&gt;Dockerfile&lt;/code&gt; utilisé :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Étape 1 — Base d’image pour builder et runner&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:23-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_TELEMETRY_DISABLED=1&lt;/span&gt;

&lt;span class="c"&gt;# Étape 2 — Installation des dépendances&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;deps&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="c"&gt;# Étape 3 — Build avec récupération sécurisée des articles Dev.to&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=deps /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_URL=https://woulf.fr&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NEXT_PUBLIC_EMAIL=corentinboucardpro@gmail.com&lt;/span&gt;

&lt;span class="c"&gt;# Injection sécurisée du token Dev.to via BuildKit&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret,id&lt;span class="o"&gt;=&lt;/span&gt;devto_token &lt;span class="se"&gt;\\&lt;/span&gt;
  DEVTO_API_KEY=\$(cat /run/secrets/devto_token) node scripts/fetchDevtoArticles.js &amp;amp;&amp;amp; npm run build

&lt;span class="c"&gt;# Étape 4 — Image finale pour l’exécution&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PORT=3000&lt;/span&gt;

&lt;span class="c"&gt;# Création d’un utilisateur non-root&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; nodejs &lt;span class="nt"&gt;-g&lt;/span&gt; 1001 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\\&lt;/span&gt;
    adduser -S nextjs -u 1001 -G nodejs &amp;amp;&amp;amp; \\
    mkdir .next &amp;amp;&amp;amp; chown nextjs:nodejs .next

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/public ./public&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=nextjs:nodejs /app/.next/standalone ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nextjs&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧠 Quelques remarques sur le Dockerfile :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage build&lt;/strong&gt; : séparation claire entre les étapes de base, d'installation, de build, et de runtime, ce qui permet une image finale propre et minimale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base alpine (node:23-alpine)&lt;/strong&gt; : image légère, rapide à télécharger, avec une surface d’attaque réduite (bon point pour la sécurité).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copie du &lt;code&gt;package*.json&lt;/code&gt; avant le code source&lt;/strong&gt; : permet de &lt;strong&gt;tirer parti du cache Docker&lt;/strong&gt; tant que les dépendances ne changent pas → accélère drastiquement les builds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utilisation de &lt;code&gt;npm ci&lt;/code&gt;&lt;/strong&gt; : garantit une installation propre et rapide des dépendances en se basant uniquement sur le &lt;code&gt;package-lock.json&lt;/code&gt;, sans résoudre les versions à nouveau.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Injection sécurisée du token Dev.to avec BuildKit&lt;/strong&gt; : évite d’exposer des secrets sensibles dans l’image ou les logs. Le token est utilisé uniquement le temps du build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Téléchargement dynamique des articles Dev.to&lt;/strong&gt; : via un script exécuté &lt;strong&gt;pendant le build&lt;/strong&gt;, ce qui permet de maintenir le contenu à jour sans le committer dans le dépôt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Création d’un utilisateur non-root (&lt;code&gt;nextjs&lt;/code&gt;)&lt;/strong&gt; : renforce la sécurité à l’exécution en limitant les permissions du processus Node.js.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Répertoires &lt;code&gt;.next/standalone&lt;/code&gt; et &lt;code&gt;.next/static&lt;/code&gt;&lt;/strong&gt; : copiés depuis l'étape de build pour ne garder que le strict nécessaire à l’exécution (selon les recommandations Next.js pour le déploiement Docker).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;EXPOSE 3000&lt;/code&gt;&lt;/strong&gt; : uniquement informatif, mais utile pour la documentation, certains outils, et la lisibilité du Dockerfile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔐 Résultat : une image Docker légère, sécurisée, rapide à builder, et prête à être déployée en prod.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pipeline dans le dépôt applicatif
&lt;/h2&gt;

&lt;p&gt;La pipeline &lt;code&gt;deploy.yml&lt;/code&gt; contient deux jobs :  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Le &lt;strong&gt;build &amp;amp; push&lt;/strong&gt; de l’image
&lt;/li&gt;
&lt;li&gt;Le &lt;strong&gt;redéploiement sur le cluster Kubernetes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Website&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up QEMU&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-qemu-action@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Docker Buildx&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/setup-buildx-action@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Docker Hub&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;\${{ secrets.DOCKER_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;\${{ secrets.DOCKER_PASSWORD }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;woulf/portfolio:latest&lt;/span&gt;
          &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;"devto_token=\${{ secrets.DEVTO_TOKEN }}"&lt;/span&gt;

  &lt;span class="na"&gt;rollout_k8s&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Kubernetes context&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/k8s-set-context@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;kubeconfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;\${{ secrets.KUBECONFIG }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restart Deployment&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;kubectl rollout restart deployment/portfolio -n default&lt;/span&gt;
          &lt;span class="s"&gt;kubectl rollout status deployment/portfolio -n default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧠 Quelques remarques sur la pipeline :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Déclencheur automatique sur &lt;code&gt;push&lt;/code&gt; sur &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; : permet de publier les changements dès qu’ils sont mergés, sans action manuelle. Utile en mode rolling release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow modulaire&lt;/strong&gt; : deux jobs bien séparés (&lt;code&gt;docker&lt;/code&gt; et &lt;code&gt;rollout_k8s&lt;/code&gt;), ce qui isole les responsabilités entre la &lt;strong&gt;construction/push&lt;/strong&gt; et le &lt;strong&gt;déploiement&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utilisation de &lt;code&gt;docker/build-push-action@v5&lt;/code&gt;&lt;/strong&gt; : outil maintenu par Docker Inc. pour builder efficacement avec &lt;strong&gt;BuildKit&lt;/strong&gt;, gérer les caches, secrets, tags, et plateformes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets gérés par GitHub (&lt;code&gt;secrets.*&lt;/code&gt;)&lt;/strong&gt; : aucun mot de passe ou token dans le code. Le token Dev.to est passé via &lt;code&gt;secrets&lt;/code&gt; en tant que secret mount avec BuildKit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BuildKit + &lt;code&gt;--mount=type=secret&lt;/code&gt;&lt;/strong&gt; : permet d’injecter un token dans un &lt;code&gt;RUN&lt;/code&gt; &lt;strong&gt;temporairement&lt;/strong&gt; (jamais exposé dans l'image finale, ni dans les logs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push sur Docker Hub&lt;/strong&gt; avec tag &lt;code&gt;woulf/portfolio:latest&lt;/code&gt; : ton image est automatiquement versionnée et disponible en ligne pour un déploiement rapide sur n’importe quel cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redéploiement Kubernetes via &lt;code&gt;kubectl rollout restart&lt;/code&gt;&lt;/strong&gt; : déclenche un redéploiement du pod sans downtime (Kubernetes gère le remplacement progressif).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vérification du statut avec &lt;code&gt;kubectl rollout status&lt;/code&gt;&lt;/strong&gt; : empêche la pipeline de continuer si le déploiement échoue → fiabilise le process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utilisation de &lt;code&gt;needs: docker&lt;/code&gt; dans le job de déploiement&lt;/strong&gt; : garantit que l’image est bien poussée &lt;strong&gt;avant&lt;/strong&gt; le restart sur le cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔄 Résultat : un pipeline CI/CD robuste, 100% automatisé, qui met à jour ton contenu, ton image Docker, et ton déploiement Kubernetes en un seul push.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gestion des secrets
&lt;/h2&gt;

&lt;p&gt;Aucun identifiant n’est codé en dur. Tout est stocké de manière sécurisée via les &lt;strong&gt;GitHub Secrets&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DOCKER_USERNAME&lt;/code&gt; / &lt;code&gt;DOCKER_PASSWORD&lt;/code&gt; → pour Docker Hub&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KUBECONFIG&lt;/code&gt; → pour se connecter à mon cluster MicroK8s à distance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DEVTO_TOKEN&lt;/code&gt; → pour accéder à l’API Dev.to et télécharger les articles automatiquement pendant le build&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Résultat
&lt;/h2&gt;

&lt;p&gt;✅ À chaque push :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Les articles Dev.to sont récupérés&lt;/li&gt;
&lt;li&gt;L’image est reconstruite et poussée&lt;/li&gt;
&lt;li&gt;L’app est redéployée &lt;strong&gt;sans interruption&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Prochaines étapes :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ajouter scan Trivy&lt;/li&gt;
&lt;li&gt;Ajout d’un &lt;code&gt;HEALTHCHECK&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Réduction des dépendances côté prod&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;➡️ Dans le prochain article, je vous explique comment j’ai versionné &lt;strong&gt;l’infrastructure Kubernetes&lt;/strong&gt; dans un second dépôt, et mis en place une &lt;strong&gt;pipeline GitOps&lt;/strong&gt; pour garder le cluster synchronisé.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
