Auto-update blog content from Obsidian: 2025-08-19 14:01:03
All checks were successful
Blog Deployment / Check-Rebuild (push) Successful in 7s
Blog Deployment / Build (push) Has been skipped
Blog Deployment / Deploy-Staging (push) Successful in 12s
Blog Deployment / Test-Staging (push) Successful in 3s
Blog Deployment / Merge (push) Successful in 8s
Blog Deployment / Deploy-Production (push) Successful in 12s
Blog Deployment / Test-Production (push) Successful in 4s
Blog Deployment / Clean (push) Has been skipped
Blog Deployment / Notify (push) Successful in 4s

This commit is contained in:
Gitea Actions
2025-08-19 14:01:03 +00:00
parent 1a8ca54ada
commit 6e6d16e474
3 changed files with 669 additions and 41 deletions

View File

@@ -0,0 +1,634 @@
---
slug: expose-kubernetes-pods-externally-ingress-tls
title: Exposer des Pods Kubernetes en externe avec Ingress et TLS
description: Découvrez comment exposer des pods Kubernetes en externe avec Services, Ingress et TLS grâce à BGP, NGINX et Cert-Manager dans un homelab.
date: 2025-08-19
draft: false
tags:
- kubernetes
- helm
- bgp
- opnsense
- cilium
- nginx-ingress-controller
- cert-manager
categories:
- homelab
---
## Intro
Après avoir construit mon propre cluster Kubernetes dans mon homelab avec `kubeadm` dans [cet article]({{< ref "post/8-create-manual-kubernetes-cluster-kubeadm" >}}), mon prochain défi est dexposer un pod simple à lextérieur, accessible via une URL et sécurisé avec un certificat TLS validé par Lets Encrypt.
Pour y parvenir, jai besoin de configurer plusieurs composants :
- **Service** : Expose le pod à lintérieur du cluster et fournit un point daccès.
- **Ingress** : Définit des règles de routage pour exposer des services HTTP(S) à lextérieur.
- **Ingress Controller** : Surveille les ressources Ingress et gère réellement le routage du trafic.
- **Certificats TLS** : Sécurisent le trafic en HTTPS grâce à des certificats délivrés par Lets Encrypt.
Cet article vous guide pas à pas pour comprendre comment fonctionne laccès externe dans Kubernetes dans un environnement homelab.
C'est parti.
---
## Helm
Jutilise **Helm**, le gestionnaire de paquets de facto pour Kubernetes, afin dinstaller des composants externes comme lIngress Controller ou cert-manager.
### Pourquoi Helm
Helm simplifie le déploiement et la gestion des applications Kubernetes. Au lieu décrire et de maintenir de longs manifestes YAML, Helm permet dinstaller des applications en une seule commande, en sappuyant sur des charts versionnés et configurables.
### Installer Helm
Jinstalle Helm sur mon hôte bastion LXC, qui dispose déjà dun accès au cluster Kubernetes :
```bash
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt update
sudo apt install helm
```
---
## Services Kubernetes
Avant de pouvoir exposer un pod à lextérieur, il faut dabord le rendre accessible à lintérieur du cluster. Cest là quinterviennent les **Services Kubernetes**.
Les Services agissent comme un pont entre les pods et le réseau, garantissant que les applications restent accessibles même si les pods sont réordonnés ou redéployés.
Il existe plusieurs types de Services Kubernetes, chacun avec un objectif différent :
- **ClusterIP** expose le Service sur une IP interne au cluster, uniquement accessible depuis lintérieur.
- **NodePort** expose le Service sur un port statique de lIP de chaque nœud, accessible depuis lextérieur du cluster.
- **LoadBalancer** expose le Service sur une IP externe, généralement via une intégration cloud (ou via BGP dans un homelab).
---
## Exposer un Service `LoadBalancer` avec BGP
Au départ, jai envisagé dutiliser **MetalLB** pour exposer les adresses IP des services sur mon réseau local. Cest ce que jutilisais auparavant quand je dépendais de la box de mon FAI comme routeur principal. Mais après avoir lu cet article, [Use Cilium BGP integration with OPNsense](https://devopstales.github.io/kubernetes/cilium-opnsense-bgp/), je réalise que je peux obtenir le même résultat (voire mieux) en utilisant **BGP** avec mon routeur **OPNsense** et **Cilium**, mon CNI.
### Quest-ce que BGP ?
BGP (_Border Gateway Protocol_) est un protocole de routage utilisé pour échanger des routes entre systèmes. Dans un homelab Kubernetes, BGP permet à tes nœuds Kubernetes dannoncer directement leurs IPs à ton routeur ou firewall. Ton routeur sait alors exactement comment atteindre les adresses IP gérées par ton cluster.
Au lieu que MetalLB gère lallocation dIP et les réponses ARP, tes nœuds disent directement à ton routeur : « Hé, cest moi qui possède ladresse 192.168.1.240 ».
### Lapproche MetalLB classique
Sans BGP, MetalLB en mode Layer 2 fonctionne comme ceci :
- Il assigne une adresse IP `LoadBalancer` (par exemple `192.168.1.240`) depuis un pool.
- Un nœud répond aux requêtes ARP pour cette IP sur ton LAN.
Oui, MetalLB peut aussi fonctionner avec BGP, mais pourquoi lutiliser si mon CNI (Cilium) le gère déjà nativement ?
### BGP avec Cilium
Avec Cilium + BGP, tu obtiens :
- Lagent Cilium du nœud annonce les IPs `LoadBalancer` via BGP.
- Ton routeur apprend ces routes et les envoie au bon nœud.
- Plus besoin de MetalLB.
### Configuration BGP
BGP est désactivé par défaut, aussi bien sur OPNsense que sur Cilium. Activons-le des deux côtés.
#### Sur OPNsense
Daprès la [documentation officielle OPNsense](https://docs.opnsense.org/manual/dynamic_routing.html#bgp-section), lactivation de BGP nécessite dinstaller un plugin.
Va dans `System` > `Firmware` > `Plugins` et installe le plugin **os-frr** :
![ ](img/opnsense-add-os-frr-plugin.png)
Installer le plugin `os-frr` dans OPNsense
Une fois installé, active le plugin dans `Routing` > `General` :
![ ](img/opnsense-enable-routing-frr-plugin.png)
Activer le routage dans OPNsense
Ensuite, rends-toi dans la section **BGP**. Dans longlet **General** :
- Coche la case pour activer BGP.
- Défini ton **ASN BGP**. Jai choisi `64512`, le premier ASN privé de la plage réservée (voir [ASN table](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\)#ASN_Table)) :
![ ](img/opnsense-enable-bgp.png)
Ajoute ensuite tes voisins BGP. Je ne fais le peering quavec mes **nœuds workers** (puisque seuls eux hébergent des workloads). Pour chaque voisin :
- Mets lIP du nœud dans `Peer-IP`.
- Utilise `64513` comme **Remote AS** (celui de Cilium).
- Configure `Update-Source Interface` sur `Lab`.
- Coche `Next-Hop-Self`.
![ ](img/opnsense-bgp-create-neighbor.png)
Voici la liste de mes voisins une fois configurés :
![ ](img/opnsense-bgp-neighbor-list.png)
Liste des voisins BGP
Noublie pas la règle firewall pour autoriser BGP (port `179/TCP`) depuis le VLAN **Lab** vers le firewall :
![ ](img/opnsense-create-firewall-rule-bgp-peering.png)
Autoriser TCP/179 de Lab vers OPNsense
#### Dans Cilium
Jai déjà Cilium installé et je nai pas trouvé comment activer BGP avec la CLI, donc je lai simplement réinstallé avec loption BGP :
```bash
cilium uninstall
cilium install --set bgpControlPlane.enabled=true
```
Je configure uniquement les **nœuds workers** pour établir le peering BGP en les labellisant avec un `nodeSelector` :
```bash
kubectl label node apex-worker node-role.kubernetes.io/worker=""
kubectl label node vertex-worker node-role.kubernetes.io/worker=""
kubectl label node zenith-worker node-role.kubernetes.io/worker=""
```
```plaintext
NAME STATUS ROLES AGE VERSION
apex-master Ready control-plane 5d4h v1.32.7
apex-worker Ready worker 5d1h v1.32.7
vertex-master Ready control-plane 5d1h v1.32.7
vertex-worker Ready worker 5d1h v1.32.7
zenith-master Ready control-plane 5d1h v1.32.7
zenith-worker Ready worker 5d1h v1.32.7
```
Pour la configuration BGP complète, jai besoin de :
- **CiliumBGPClusterConfig** : paramètres BGP pour le cluster Cilium, incluant son ASN local et son pair.
- **CiliumBGPPeerConfig** : définit les timers, le redémarrage gracieux et les routes annoncées.
- **CiliumBGPAdvertisement** : indique quels services Kubernetes annoncer via BGP.
- **CiliumLoadBalancerIPPool** : définit la plage dIPs attribuées aux services `LoadBalancer`.
```yaml
---
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPClusterConfig
metadata:
name: bgp-cluster
spec:
nodeSelector:
matchLabels:
node-role.kubernetes.io/worker: "" # Only for worker nodes
bgpInstances:
- name: "cilium-bgp-cluster"
localASN: 64513 # Cilium ASN
peers:
- name: "pfSense-peer"
peerASN: 64512 # OPNsense ASN
peerAddress: 192.168.66.1 # OPNsense IP
peerConfigRef:
name: "bgp-peer"
---
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPPeerConfig
metadata:
name: bgp-peer
spec:
timers:
holdTimeSeconds: 9
keepAliveTimeSeconds: 3
gracefulRestart:
enabled: true
restartTimeSeconds: 15
families:
- afi: ipv4
safi: unicast
advertisements:
matchLabels:
advertise: "bgp"
---
apiVersion: cilium.io/v2alpha1
kind: CiliumBGPAdvertisement
metadata:
name: bgp-advertisement
labels:
advertise: bgp
spec:
advertisements:
- advertisementType: "Service"
service:
addresses:
- LoadBalancerIP
selector:
matchExpressions:
- { key: somekey, operator: NotIn, values: [ never-used-value ] }
---
apiVersion: "cilium.io/v2alpha1"
kind: CiliumLoadBalancerIPPool
metadata:
name: "dmz"
spec:
blocks:
- start: "192.168.55.20" # LB Range Start IP
stop: "192.168.55.250" # LB Range End IP
```
Applique la configuration :
```bash
kubectl apply -f bgp.yaml
ciliumbgpclusterconfig.cilium.io/bgp-cluster created
ciliumbgppeerconfig.cilium.io/bgp-peer created
ciliumbgpadvertisement.cilium.io/bgp-advertisement created
ciliumloadbalancerippool.cilium.io/dmz created
```
Si tout fonctionne, tu devrais voir les sessions BGP **établies** avec tes workers :
```bash
cilium bgp peers
Node Local AS Peer AS Peer Address Session State Uptime Family Received Advertised
apex-worker 64513 64512 192.168.66.1 established 6m30s ipv4/unicast 1 2
vertex-worker 64513 64512 192.168.66.1 established 7m9s ipv4/unicast 1 2
zenith-worker 64513 64512 192.168.66.1 established 6m13s ipv4/unicast 1 2
```
### Déployer un Service `LoadBalancer` avec BGP
Validons rapidement que la configuration fonctionne en déployant un `Deployment` de test et un `Service` de type `LoadBalancer` :
```yaml
---
apiVersion: v1
kind: Service
metadata:
name: test-lb
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
selector:
svc: test-lb
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
svc: test-lb
template:
metadata:
labels:
svc: test-lb
spec:
containers:
- name: web
image: nginx
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
```
Vérifions si le service obtient une IP externe :
```bash
kubectl get services test-lb
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
test-lb LoadBalancer 10.100.167.198 192.168.55.20 80:31350/TCP 169m
```
Le service a récupéré la première IP du pool défini : `192.168.55.20`.
Depuis nimporte quel appareil du LAN, on peut tester laccès sur le port 80 :
![Test LoadBalancer service with BGP](img/k8s-test-loadbalancer-service-with-bgp.png)
✅ Notre pod est joignable via une IP `LoadBalancer` routée en BGP. Première étape réussie !
---
## Kubernetes Ingress
Nous avons réussi à exposer un pod en externe en utilisant un service `LoadBalancer` et une adresse IP attribuée via BGP. Cette approche fonctionne très bien pour les tests, mais elle ne fonctionne pas à léchelle.
Imagine avoir 10, 20 ou 50 services différents. Est-ce que je voudrais vraiment allouer 50 adresses IP et encombrer mon firewall ainsi que mes tables de routage avec 50 entrées BGP ? Certainement pas.
Cest là quintervient **Ingress**.
### Quest-ce quun Kubernetes Ingress ?
Un Kubernetes **Ingress** est un objet API qui gère **laccès externe aux services** dun cluster, généralement en HTTP et HTTPS, le tout via un point dentrée unique.
Au lieu dattribuer une IP par service, on définit des règles de routage basées sur :
- **Des noms dhôtes** (`app1.vezpi.me`, `blog.vezpi.me`, etc.)
- **Des chemins** (`/grafana`, `/metrics`, etc.)
Avec Ingress, je peux exposer plusieurs services via la même IP et le même port (souvent 443 pour HTTPS), et Kubernetes saura comment router la requête vers le bon service backend.
Voici un exemple simple d`Ingress`, qui route le trafic de `test.vezpi.me` vers le service `test-lb` sur le port 80 :
```yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress
spec:
rules:
- host: test.vezpi.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-lb
port:
number: 80
```
### Ingress Controller
Un Ingress, en soi, nest quun ensemble de règles de routage. Il ne traite pas réellement le trafic. Pour le rendre fonctionnel, il faut un **Ingress Controller**, qui va :
- Surveiller lAPI Kubernetes pour détecter les ressources `Ingress`.
- Ouvrir les ports HTTP(S) via un service `LoadBalancer` ou `NodePort`.
- Router le trafic vers le bon `Service` selon les règles de lIngress.
Parmi les contrôleurs populaires, on retrouve NGINX, Traefik, HAProxy, et dautres encore. Comme je cherchais quelque chose de simple, stable et largement adopté, jai choisi le **NGINX Ingress Controller**.
### Installer NGINX Ingress Controller
Jutilise Helm pour installer le contrôleur, et je définis `controller.ingressClassResource.default=true` pour que tous mes futurs ingress lutilisent par défaut :
```bash
helm install ingress-nginx \
--repo=https://kubernetes.github.io/ingress-nginx \
--namespace=ingress-nginx \
--create-namespace ingress-nginx \
--set controller.ingressClassResource.default=true \
--set controller.config.strict-validate-path-type=false
```
Le contrôleur est déployé et expose un service `LoadBalancer`. Dans mon cas, il récupère la deuxième adresse IP disponible dans la plage BGP :
```bash
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
ingress-nginx-controller LoadBalancer 10.106.236.13 192.168.55.21 80:31195/TCP,443:30974/TCP 75s app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
```
### Réserver une IP statique pour le contrôleur
Je veux massurer que lIngress Controller reçoive toujours la même adresse IP. Pour cela, jai créé deux pools dIP Cilium distincts :
- Un réservé pour lIngress Controller avec une seule IP.
- Un pour tout le reste.
```yaml
---
# Pool for Ingress Controller
apiVersion: cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
name: ingress-nginx
spec:
blocks:
- cidr: 192.168.55.55/32
serviceSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/component: controller
---
# Default pool for other services
apiVersion: cilium.io/v2alpha1
kind: CiliumLoadBalancerIPPool
metadata:
name: default
spec:
blocks:
- start: 192.168.55.100
stop: 192.168.55.250
serviceSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: NotIn
values:
- ingress-nginx
```
Après avoir remplacé le pool partagé par ces deux pools, lIngress Controller reçoit bien lIP dédiée `192.168.55.55`, et le service `test-lb` obtient `192.168.55.100` comme prévu :
```bash
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default test-lb LoadBalancer 10.100.167.198 192.168.55.100 80:31350/TCP 6h34m
ingress-nginx ingress-nginx-controller LoadBalancer 10.106.236.13 192.168.55.55 80:31195/TCP,443:30974/TCP 24m
```
### Associer un Service à un Ingress
Maintenant, connectons un service à ce contrôleur.
Je commence par mettre à jour le service `LoadBalancer` dorigine pour le convertir en `ClusterIP` (puisque cest désormais lIngress Controller qui lexposera en externe) :
```yaml
---
apiVersion: v1
kind: Service
metadata:
name: test-lb
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
name: http
selector:
svc: test-lb
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress
spec:
rules:
- host: test.vezpi.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-lb
port:
number: 80
```
Ensuite, japplique le manifeste `Ingress` pour exposer le service en HTTP.
Comme jutilise le plugin **Caddy** dans OPNsense, jai encore besoin dun routage local de type Layer 4 pour rediriger le trafic de `test.vezpi.me` vers ladresse IP de lIngress Controller (`192.168.55.55`). Je crée donc une nouvelle règle dans le plugin Caddy.
![Create Layer4 router in Caddy plugin for OPNsense](img/opnsense-caddy-create-layer4-route-http.png)
Puis je teste laccès dans le navigateur :
![ ](img/ingress-controller-nginx-test-simple-webserver.png)
Test dun Ingress en HTTP
✅ Mon pod est désormais accessible via son URL HTTP en utilisant un Ingress. Deuxième étape complétée !
---
## Connexion sécurisée avec TLS
Exposer des services en HTTP simple est suffisant pour des tests, mais en pratique nous voulons presque toujours utiliser **HTTPS**. Les certificats TLS chiffrent le trafic et garantissent lauthenticité ainsi que la confiance pour les utilisateurs.
### Cert-Manager
Pour automatiser la gestion des certificats dans Kubernetes, nous utilisons **Cert-Manager**. Il peut demander, renouveler et gérer les certificats TLS sans intervention manuelle.
#### Installer Cert-Manager
Nous le déployons avec Helm dans le cluster :
```bash
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
```
#### Configurer Cert-Manager
Ensuite, nous configurons un **ClusterIssuer** pour Lets Encrypt. Cette ressource indique à Cert-Manager comment demander des certificats :
```yaml
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: <email>
privateKeySecretRef:
name: letsencrypt-staging-key
solvers:
- http01:
ingress:
ingressClassName: nginx
```
Ici, je définis le serveur **staging** de Lets Encrypt ACME pour les tests. Les certificats de staging ne sont pas reconnus par les navigateurs, mais ils évitent datteindre les limites strictes de Lets Encrypt lors du développement.
Appliquez-le :
```bash
kubectl apply -f clusterissuer.yaml
```
Vérifiez si votre `ClusterIssuer` est `Ready` :
```bash
kubectl get clusterissuers.cert-manager.io
NAME READY AGE
letsencrypt-staging True 14m
```
Sil ne devient pas `Ready`, utilisez `kubectl describe` sur la ressource pour le diagnostiquer.
### Ajouter TLS dans un Ingress
Nous pouvons maintenant sécuriser notre service avec TLS en ajoutant une section `tls` dans la spécification `Ingress` et en référençant le `ClusterIssuer` :
```yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress-https
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
tls:
- hosts:
- test.vezpi.me
secretName: test-vezpi-me-tls
rules:
- host: test.vezpi.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-lb
port:
number: 80
```
En arrière-plan, Cert-Manager suit ce flux pour émettre le certificat :
- Détecte l`Ingress` avec `tls` et le `ClusterIssuer`.
- Crée un CRD **Certificate** décrivant le certificat souhaité + lemplacement du Secret.
- Crée un CRD **Order** pour représenter une tentative démission avec Lets Encrypt.
- Crée un CRD **Challenge** (par ex. validation HTTP-01).
- Met en place un Ingress/Pod temporaire pour résoudre le challenge.
- Crée un CRD **CertificateRequest** et envoie le CSR à Lets Encrypt.
- Reçoit le certificat signé et le stocke dans un Secret Kubernetes.
- LIngress utilise automatiquement ce Secret pour servir en HTTPS.
✅ Une fois ce processus terminé, votre Ingress est sécurisé avec un certificat TLS.
![Certificat TLS validé avec le serveur de staging de Lets Encrypt](img/k8s-test-deploy-service-tls-certificate-staging-lets-encrypt.png)
### Passer aux certificats de production
Une fois que le staging fonctionne, nous pouvons passer au serveur **production** ACME pour obtenir un certificat Lets Encrypt reconnu :
```yaml
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <email>
privateKeySecretRef:
name: letsencrypt-key
solvers:
- http01:
ingress:
ingressClassName: nginx
```
Mettez à jour l`Ingress` pour pointer vers le nouveau `ClusterIssuer` :
```yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress-https
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
tls:
- hosts:
- test.vezpi.me
secretName: test-vezpi-me-tls
rules:
- host: test.vezpi.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test-lb
port:
number: 80
```
Comme le certificat de staging est encore stocké dans le Secret, je le supprime pour forcer une nouvelle demande en production :
```bash
kubectl delete secret test-vezpi-me-tls
```
🎉 Mon `Ingress` est désormais sécurisé avec un certificat TLS valide délivré par Lets Encrypt. Les requêtes vers `https://test.vezpi.me` sont chiffrées de bout en bout et routées par le NGINX Ingress Controller jusquà mon pod `nginx` :
![Ingress HTTPS avec certificat validé par Lets Encrypt](img/k8s-deploy-test-service-tls-certificate-lets-encrypt.png)
---
## Conclusion
Dans ce parcours, je suis parti des bases, en exposant un simple pod avec un service `LoadBalancer`, puis jai construit étape par étape une configuration prête pour la production :
- Compréhension des **Services Kubernetes** et de leurs différents types.
- Utilisation du **BGP avec Cilium** et OPNsense pour attribuer des IP externes directement depuis mon réseau.
- Introduction des **Ingress** pour mieux passer à léchelle, en exposant plusieurs services via un point dentrée unique.
- Installation du **NGINX Ingress Controller** pour gérer le routage.
- Automatisation de la gestion des certificats avec **Cert-Manager**, afin de sécuriser mes services avec des certificats TLS Lets Encrypt.
🚀 Résultat : mon pod est maintenant accessible via une véritable URL, sécurisé en HTTPS, comme nimporte quelle application web moderne.
Cest une étape importante dans mon aventure Kubernetes en homelab. Dans le prochain article, je souhaite explorer le stockage persistant et connecter mon cluster Kubernetes à mon setup **Ceph** sous **Proxmox**.
A la prochaine !

View File

@@ -1,9 +1,9 @@
--- ---
slug: slug: expose-kubernetes-pods-externally-ingress-tls
title: Template title: Exposing Kubernetes Pods externally with Ingress and TLS
description: description: Learn how to expose Kubernetes pods externally with Services, Ingress, and TLS using BGP, NGINX, and Cert-Manager in a homelab setup.
date: date: 2025-08-19
draft: true draft: false
tags: tags:
- kubernetes - kubernetes
- helm - helm
@@ -13,6 +13,7 @@ tags:
- nginx-ingress-controller - nginx-ingress-controller
- cert-manager - cert-manager
categories: categories:
- homelab
--- ---
## Intro ## Intro
@@ -25,20 +26,22 @@ To achieve this, I needed to configure several components:
- **Ingress Controller**: Listen to Ingress resources and handles actual traffic routing. - **Ingress Controller**: Listen to Ingress resources and handles actual traffic routing.
- **TLS Certificates**: Secure traffic with HTTPS using certificates from Lets Encrypt. - **TLS Certificates**: Secure traffic with HTTPS using certificates from Lets Encrypt.
This post will guide you through each step, to understand how external access works in Kubernetes, in a homelab environment. This post guides you through each step to understand how external access works in Kubernetes in a homelab environment.
Lets dive in. Lets dive in.
--- ---
## Helm ## Helm
To install the external components needed in this setup (like the Ingress controller or cert-manager), Ill use **Helm**, the de facto package manager for Kubernetes. I use **Helm**, the de facto package manager for Kubernetes, to install external components like the Ingress controller or cert-manager.
### Why Helm ### Why Helm
Helm simplifies the deployment and management of Kubernetes applications. Instead of writing and maintaining large YAML manifests, Helm lets you install applications with a single command, using versioned and configurable charts. Helm simplifies the deployment and management of Kubernetes applications. Instead of writing and maintaining large YAML manifests, Helm lets you install applications with a single command, using versioned and configurable charts.
### Install Helm ### Install Helm
I installed Helm on my LXC bastion host, which already has access to the Kubernetes cluster: I install Helm on my LXC bastion host, which already has access to the Kubernetes cluster:
```bash ```bash
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
@@ -51,35 +54,26 @@ sudo apt install helm
Before we can expose a pod externally, we need a way to make it reachable inside the cluster. Thats where Kubernetes Services come in. Before we can expose a pod externally, we need a way to make it reachable inside the cluster. Thats where Kubernetes Services come in.
A Service provides a stable, abstracted network endpoint for a set of pods. This abstraction ensures that even if the pods IP changes (for example, when it gets restarted), the Service IP remains constant. Services act as the bridge between pods and the network, making sure applications remain reachable even as pods are rescheduled.
There are several types of Kubernetes Services, each serving a different purpose: There are several types of Kubernetes Services, each serving a different purpose:
- **ClusterIP** exposes the Service on a cluster-internal IP, only accessible inside the cluster.
#### ClusterIP - **NodePort** exposes the Service on a static port on each nodes IP, accessible from outside the cluster.
- **LoadBalancer** exposes the Service on an external IP, typically using cloud integrations (or BGP in a homelab).
This is the default type. It exposes the Service on a cluster-internal IP. It is only accessible from within the cluster. Use this when your application does not need to be accessed externally.
#### NodePort
This type exposes the Service on a static port on each nodes IP. You can access the service from outside the cluster using `http://<NodeIP>:<NodePort>`. Its simple to set up, great for testing.
#### LoadBalancer
This type provisions an external IP to access the Service. It usually relies on cloud provider integration, but in a homelab (or bare-metal setup), we can achieve the same effect using BGP.
--- ---
## Expose a `LoadBalancer` Service with BGP ## Expose a `LoadBalancer` Service with BGP
Initially, I considered using **MetalLB** to expose service IPs to my home network. Thats what I used in the past when relying on my ISP box as the main router. But after reading this post, [Use Cilium BGP integration with OPNsense](https://devopstales.github.io/kubernetes/cilium-opnsense-bgp/), I realized I could achieve the same (or even better) using BGP with my **OPNsense** router and **Cilium**, my CNI. Initially, I considered using **MetalLB** to expose service IPs to my home network. Thats what I used in the past when relying on my ISP box as the main router. But after reading this post, [Use Cilium BGP integration with OPNsense](https://devopstales.github.io/kubernetes/cilium-opnsense-bgp/), I realize I can achieve the same (or even better) using BGP with my **OPNsense** router and **Cilium**, my CNI.
### What Is BGP? ### What Is BGP?
BGP (Border Gateway Protocol) is a routing protocol used to exchange network routes between systems. In the Kubernetes homelab context, BGP allows your Kubernetes nodes to advertise IPs directly to your network router or firewall. Your router then knows how to reach the IPs managed by your cluster. BGP (Border Gateway Protocol) is a routing protocol used to exchange network routes between systems. In the Kubernetes homelab context, BGP allows your Kubernetes nodes to advertise IPs directly to your network router or firewall. Your router then knows how to reach the IPs managed by your cluster.
So instead of MetalLB managing IP allocation and ARP replies, your nodes directly tell your router: Hey, I own 192.168.1.240. So instead of MetalLB managing IP allocation and ARP replies, your nodes directly tell your router: « Hey, I own 192.168.1.240 ».
### Legacy MetalLB Approach ### Legacy MetalLB Approach
Without BGP, MetalLB in Layer 2 mode works like this: Without BGP, MetalLB in Layer 2 mode works like this:
- Assigns a LoadBalancer IP (e.g., `192.168.1.240`) from a pool. - Assigns a `LoadBalancer` IP (e.g., `192.168.1.240`) from a pool.
- One node responds to ARP for that IP on your LAN. - One node responds to ARP for that IP on your LAN.
Yes, MetalLB can also work with BGP, but what if my CNI (Cilium) can handle it out of the box? Yes, MetalLB can also work with BGP, but what if my CNI (Cilium) can handle it out of the box?
@@ -92,7 +86,7 @@ With Cilium + BGP, you get:
### BGP Setup ### BGP Setup
By default, BGP is disabled by default, both on my OPNsense router and in Cilium. Lets enable it on both ends. BGP is disabled by default on both OPNsense and Cilium. Lets enable it on both ends.
#### On OPNsense #### On OPNsense
@@ -120,8 +114,8 @@ Now create your BGP neighbors. Im only peering with my **worker nodes** (sinc
![ ](img/opnsense-bgp-create-neighbor.png) ![ ](img/opnsense-bgp-create-neighbor.png)
BGP neighbor configuration in OPNsense BGP neighbor configuration in OPNsense
Heres how my neighbor list looks once complete: Heres how my neighbors list looks once complete:
![ ](img/opnsense-bgp-nieghbor-list.png) ![ ](img/opnsense-bgp-neighbor-list.png)
BGP neighbor list BGP neighbor list
Dont forget to create a firewall rule allowing BGP (port `179/TCP`) from the **Lab** VLAN to the firewall: Dont forget to create a firewall rule allowing BGP (port `179/TCP`) from the **Lab** VLAN to the firewall:
@@ -130,14 +124,14 @@ Allow TCP/179 from Lab to OPNsense
#### In Cilium #### In Cilium
I already had Cilium installed and couldnt find a way to enable BGP with the CLI, so I simply reinstalled it with the BGP option: I already have Cilium installed and couldnt find a way to enable BGP with the CLI, so I simply reinstall it with the BGP option:
```bash ```bash
cilium uninstall cilium uninstall
cilium install --set bgpControlPlane.enabled=true cilium install --set bgpControlPlane.enabled=true
``` ```
Next, I want only **worker nodes** to establish BGP peering. I add a label to each one for the future `nodeSelector`: I configure only worker nodes to establish BGP peering by labeling them for the `nodeSelector`:
```bash ```bash
kubectl label node apex-worker node-role.kubernetes.io/worker="" kubectl label node apex-worker node-role.kubernetes.io/worker=""
kubectl label node vertex-worker node-role.kubernetes.io/worker="" kubectl label node vertex-worker node-role.kubernetes.io/worker=""
@@ -313,7 +307,7 @@ Thats where **Ingress** kicks in.
### What Is a Kubernetes Ingress? ### What Is a Kubernetes Ingress?
A **Kubernetes Ingress** is an API object that manages **external access to services** in a cluster, typically HTTP and HTTPS, all through a single entry point. A Kubernetes **Ingress** is an API object that manages **external access to services** in a cluster, typically HTTP and HTTPS, all through a single entry point.
Instead of assigning one IP per service, you define routing rules based on: Instead of assigning one IP per service, you define routing rules based on:
- **Hostnames** (`app1.vezpi.me`, `blog.vezpi.me`, etc.) - **Hostnames** (`app1.vezpi.me`, `blog.vezpi.me`, etc.)
@@ -344,15 +338,16 @@ spec:
### Ingress Controller ### Ingress Controller
On its own, an Ingress is just a set of routing rules. It doesnt actually handle traffic. To bring it to life, I need an **Ingress Controller** which will: On its own, an Ingress is just a set of routing rules. It doesnt actually handle traffic. To bring it to life, I need an **Ingress Controller**, which will:
- Watches the Kubernetes API for `Ingress` resources. - Watches the Kubernetes API for `Ingress` resources.
- Opens HTTP(S) ports on a `LoadBalancer` or `NodePort` service. - Opens HTTP(S) ports on a `LoadBalancer` or `NodePort` service.
- Routes traffic to the correct `Service` based on the `Ingress` rules. - Routes traffic to the correct `Service` based on the `Ingress` rules.
Popular controllers include NGINX, Traefik, HAProxy, and more. Since I was looking for something simple, stable, and widely adopted, I picked the **NGINX Ingress Controller**. Popular controllers include NGINX, Traefik, HAProxy, and more. Since I was looking for something simple, stable, and widely adopted, I picked the **NGINX Ingress Controller**.
### Install NGINX Ingress Controller ### Install NGINX Ingress Controller
I used Helm to install the controller, and I set `controller.ingressClassResource.default=true` so that all my future ingresses use it by default: I use Helm to install the controller, and I set `controller.ingressClassResource.default=true` so that all my future ingresses use it by default:
```bash ```bash
helm install ingress-nginx \ helm install ingress-nginx \
--repo=https://kubernetes.github.io/ingress-nginx \ --repo=https://kubernetes.github.io/ingress-nginx \
@@ -362,7 +357,7 @@ helm install ingress-nginx \
--set controller.config.strict-validate-path-type=false --set controller.config.strict-validate-path-type=false
``` ```
The controller is deployed and exposes a `LoadBalancer` service. In my setup, it picked the second available IP in the BGP range: The controller is deployed and exposes a `LoadBalancer` service. In my setup, it picks the second available IP in the BGP range:
```bash ```bash
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
ingress-nginx-controller LoadBalancer 10.106.236.13 192.168.55.21 80:31195/TCP,443:30974/TCP 75s app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx ingress-nginx-controller LoadBalancer 10.106.236.13 192.168.55.21 80:31195/TCP,443:30974/TCP 75s app.kubernetes.io/component=controller,app.kubernetes.io/instance=ingress-nginx,app.kubernetes.io/name=ingress-nginx
@@ -370,7 +365,7 @@ ingress-nginx-controller LoadBalancer 10.106.236.13 192.168.55.21 80:311
### Reserving a Static IP for the Controller ### Reserving a Static IP for the Controller
I want to make sure the Ingress Controller always receives the same IP address. To do this, I created **two separate Cilium IP pools**: I want to make sure the Ingress Controller always receives the same IP address. To do this, I created two separate Cilium IP pools:
- One dedicated for the Ingress Controller with a single IP. - One dedicated for the Ingress Controller with a single IP.
- One for everything else. - One for everything else.
```yaml ```yaml
@@ -405,7 +400,7 @@ spec:
- ingress-nginx - ingress-nginx
``` ```
After replacing the previous shared pool with these two, the Ingress Controller got the desired IP `192.168.55.55`, and the `test-lb` service picked `192.168.55.100` as expected: After replacing the previous shared pool with these two, the Ingress Controller gets the desired IP `192.168.55.55`, and the `test-lb` service picks `192.168.55.100` as expected:
```bash ```bash
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default test-lb LoadBalancer 10.100.167.198 192.168.55.100 80:31350/TCP 6h34m default test-lb LoadBalancer 10.100.167.198 192.168.55.100 80:31350/TCP 6h34m
@@ -465,7 +460,7 @@ Test Ingress on HTTP
--- ---
## Secure Connection with TLS ## Secure Connection with TLS
Exposing services over plain HTTP is fine for testing, but in practice we almost always want **HTTPS**. TLS certificates encrypt traffic and provides authenticity and trust to users. Exposing services over plain HTTP is fine for testing, but in practice we almost always want **HTTPS**. TLS certificates encrypt traffic and provide authenticity and trust to users.
### Cert-Manager ### Cert-Manager
@@ -497,7 +492,7 @@ spec:
server: https://acme-staging-v02.api.letsencrypt.org/directory server: https://acme-staging-v02.api.letsencrypt.org/directory
email: <email> email: <email>
privateKeySecretRef: privateKeySecretRef:
name: letsencrypt--key name: letsencrypt-staging-key
solvers: solvers:
- http01: - http01:
ingress: ingress:
@@ -560,9 +555,8 @@ Behind the scenes, Cert-Manager goes through this workflow to issue the certific
- Receives the signed certificate and stores it in a Kubernetes Secret. - Receives the signed certificate and stores it in a Kubernetes Secret.
- The Ingress automatically uses the Secret to serve HTTPS. - The Ingress automatically uses the Secret to serve HTTPS.
✅ Once this process completes, your Ingress is secured with a TLS certificate. ✅ Once this process completes, your Ingress is secured with a TLS certificate.
![TLS certificate verified with the staging Let's Encrypt server](img/k8s-test-deploy-service-tls-certificate-staging-lets-encrypt.png) ![Certificat TLS validé avec le serveur de staging de Lets Encrypt](img/k8s-test-deploy-service-tls-certificate-staging-lets-encrypt.png)
### Switch to Production Certificates ### Switch to Production Certificates
@@ -618,7 +612,7 @@ kubectl delete secret test-vezpi-me-tls
``` ```
🎉 My `Ingress` is now secured with a valid TLS certificate from Lets Encrypt. Requests to `https://test.vezpi.me` are encrypted end-to-end and routed by the NGINX Ingress Controller to my `nginx` pod: 🎉 My `Ingress` is now secured with a valid TLS certificate from Lets Encrypt. Requests to `https://test.vezpi.me` are encrypted end-to-end and routed by the NGINX Ingress Controller to my `nginx` pod:
![Ingress HTTPS with certificate verified by Let's Encrypt](img/k8s-deploy-test-service-tls-certificate-lets-encrypt.png) ![Ingress HTTPS avec certificat validé par Lets Encrypt](img/k8s-deploy-test-service-tls-certificate-lets-encrypt.png)
--- ---
@@ -633,4 +627,4 @@ In this journey, I started from the basics, exposing a single pod with a `LoadBa
🚀 The result: my pod is now reachable at a real URL, secured with HTTPS, just like any modern web application. 🚀 The result: my pod is now reachable at a real URL, secured with HTTPS, just like any modern web application.
This is a huge milestone in my homelab Kubernetes journey, in the next article, I'd like to explore persistent storage to be able to use my **Ceph** cluster on **Proxmox**. This is a huge milestone in my homelab Kubernetes journey. In the next article, I want to explore persistent storage and connect my Kubernetes cluster to my **Ceph** setup on **Proxmox**.

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB