Compare commits
3 Commits
preview
...
7d7b1aa627
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d7b1aa627 | |||
| e170345fdd | |||
| 44bf161765 |
@@ -24,7 +24,7 @@ jobs:
|
||||
docker_folder_changed: ${{ steps.docker_folder.outputs.changed }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
run: git clone --branch ${{ gitea.ref_name }} https://${{ secrets.REPO_TOKEN }}@git.vezpi.com/Vezpi/blog.git .
|
||||
run: git clone --branch preview https://${{ secrets.REPO_TOKEN }}@git.vezpi.com/Vezpi/blog.git .
|
||||
|
||||
- name: Check Latest Hugo Version
|
||||
id: get_latest
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
shell: sh
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
run: git clone --branch ${{ gitea.ref_name }} https://${{ secrets.REPO_TOKEN }}@git.vezpi.com/Vezpi/blog.git .
|
||||
run: git clone --branch preview https://${{ secrets.REPO_TOKEN }}@git.vezpi.com/Vezpi/blog.git .
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
cd /blog
|
||||
docker compose down ${CONTAINER_NAME}
|
||||
docker compose up -d ${CONTAINER_NAME}
|
||||
sleep 30
|
||||
sleep 5
|
||||
echo "- Displaying container logs"
|
||||
docker compose logs ${CONTAINER_NAME}
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
needs: Deploy-Staging
|
||||
runs-on: ubuntu
|
||||
env:
|
||||
URL: "https://blog-staging.vezpi.com/en/"
|
||||
URL: "https://blog-dev.vezpi.com/en/"
|
||||
steps:
|
||||
- name: Check HTTP Response
|
||||
run: |
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
|
||||
- name: Merge preview Branch on main
|
||||
run: |
|
||||
git merge --ff-only origin/${{ gitea.ref_name }}
|
||||
git merge --ff-only origin/preview
|
||||
git push origin main
|
||||
|
||||
Deploy-Production:
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
cd /blog
|
||||
docker compose down ${CONTAINER_NAME}
|
||||
docker compose up -d ${CONTAINER_NAME}
|
||||
sleep 30
|
||||
sleep 5
|
||||
echo "- Displaying container logs"
|
||||
docker compose logs ${CONTAINER_NAME}
|
||||
|
||||
@@ -194,10 +194,7 @@ jobs:
|
||||
steps:
|
||||
- name: Remove Old Docker Image
|
||||
run: |
|
||||
IMAGE_IDS=$(docker image ls "${DOCKER_IMAGE}" 2>/dev/null | awk '$NF != "U" && NR>1 {print $2}')
|
||||
if [ -n "$IMAGE_IDS" ]; then
|
||||
docker image rm $IMAGE_IDS
|
||||
fi
|
||||
docker image rm ${{ needs.Check-Rebuild.outputs.current_docker_image }} --force
|
||||
|
||||
Notify:
|
||||
needs: [Check-Rebuild, Build, Deploy-Staging, Test-Staging, Merge, Deploy-Production, Test-Production, Clean]
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
--build-arg HUGO_VERSION=${{ needs.Check-Rebuild.outputs.latest_hugo_version }} \
|
||||
--tag ${DOCKER_IMAGE}:${{ needs.Check-Rebuild.outputs.latest_hugo_version }} \
|
||||
.
|
||||
docker tag ${DOCKER_IMAGE}:${{ needs.Check-Rebuild.outputs.latest_hugo_version }} ${DOCKER_IMAGE}:dev
|
||||
docker tag ${DOCKER_IMAGE}:${{ needs.Check-Rebuild.outputs.latest_hugo_version }} ${DOCKER_IMAGE}:test
|
||||
|
||||
Deploy-Test:
|
||||
needs:
|
||||
@@ -102,14 +102,14 @@ jobs:
|
||||
run:
|
||||
shell: sh
|
||||
env:
|
||||
CONTAINER_NAME: blog_dev
|
||||
CONTAINER_NAME: blog_test
|
||||
steps:
|
||||
- name: Launch Blog Test Deployment
|
||||
run: |
|
||||
cd /blog
|
||||
docker compose down ${CONTAINER_NAME}
|
||||
BLOG_TEST_BRANCH=${{ gitea.ref_name }} docker compose up -d ${CONTAINER_NAME}
|
||||
sleep 30
|
||||
sleep 5
|
||||
echo "- Displaying container logs"
|
||||
docker compose logs ${CONTAINER_NAME}
|
||||
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
needs: Deploy-Test
|
||||
runs-on: ubuntu
|
||||
env:
|
||||
URL: "https://blog-dev.vezpi.com/en/"
|
||||
URL: "https://blog-test.vezpi.com/en/"
|
||||
steps:
|
||||
- name: Check HTTP Response
|
||||
run: |
|
||||
|
||||
@@ -3,15 +3,9 @@
|
||||
}
|
||||
|
||||
.lang-toggle-icon {
|
||||
margin-left: 0;
|
||||
margin-left: auto;
|
||||
svg {
|
||||
width: 64px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
.left-sidebar {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
@@ -122,9 +122,9 @@ La meilleure solution que j'ai trouvée a été de percer deux trous de 40 mm a
|
||||
|
||||
Voici à quoi ça ressemble :
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Stack Logicielle
|
||||
@@ -183,7 +183,7 @@ Cette configuration de proxy à deux couches centralise la gestion des certifica
|
||||
Pour un accès distant sécurisé, j'ai configuré **WireGuard** sur OPNsense. Ce VPN léger fournit une connectivité chiffrée à mon lab où que je sois, permettant ainsi de gérer tous mes services sans les exposer directement à Internet.
|
||||
#### Schéma Réseau
|
||||
|
||||

|
||||

|
||||
|
||||
### Application
|
||||
|
||||
|
||||
@@ -121,9 +121,9 @@ Inside the rack, I also added two 80mm fans to help with airflow. To keep everyt
|
||||
|
||||
Here what is look like:
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
---
|
||||
@@ -184,7 +184,7 @@ This two-layer proxy setup centralizes SSL certificate management in **Caddy** w
|
||||
For secure remote access, I configured **WireGuard** on OPNsense. This lightweight VPN provides encrypted connectivity to my lab from anywhere, allowing management of all my services without exposing them all directly to the internet.
|
||||
#### Network Diagram
|
||||
|
||||

|
||||

|
||||
### Application
|
||||
|
||||
Let's dive into the fun part! What started as a modest setup meant to serve a few personal needs quickly turned into a full ecosystem of open source services, each solving a specific need or just satisfying curiosity.
|
||||
|
||||
@@ -32,7 +32,7 @@ Tout d'abord, nous devons télécharger une image compatible cloud-init. Bien qu
|
||||
Trouvez des images compatibles cloud dans le [Guide des images OpenStack](https://docs.openstack.org/image-guide/obtain-images.html).
|
||||
|
||||
Dans Proxmox, accédez à **Storage > ISO Images > Upload** pour uploader l'image téléchargée.
|
||||

|
||||

|
||||
## Créer la VM
|
||||
|
||||
Ensuite, on crée une VM en utilisant la ligne de commande (CLI) depuis le nœud Proxmox avec la commande suivantes :
|
||||
@@ -32,7 +32,7 @@ First, we need to download an image with cloud-init support. Although Rocky Linu
|
||||
Find cloud-ready images from the [OpenStack Image Guide](https://docs.openstack.org/image-guide/obtain-images.html).
|
||||
|
||||
In Proxmox, navigate to **Storage > ISO Images > Upload** to upload the downloaded image.
|
||||

|
||||

|
||||
|
||||
## Create the VM
|
||||
|
||||
@@ -21,7 +21,7 @@ Ce genre d’exercice est la pire chose que vous souhaitez voir arriver, parce q
|
||||
|
||||
Ma box OPNsense tournait parfaitement depuis des mois. Routeur, pare-feu, DNS, DHCP, VLANs, VPN, reverse proxy et même contrôleur UniFi : toutes les pièces de mon homelab passe par elle. Mais pas seulement, elle fournit aussi Internet à la maison.
|
||||
|
||||

|
||||

|
||||
|
||||
Cette box est le cœur de mon réseau, sans elle, je ne peux quasiment rien faire. J’ai détaillé son fonctionnement dans ma section [Homelab]({{< ref "page/homelab" >}}). Tout “fonctionnait juste”, et je ne m’en inquiétait pas. J’étais confiant, sa sauvegarde vivait uniquement à l’intérieur de la machine…
|
||||
|
||||
@@ -61,7 +61,7 @@ pkg: sqlite error while executing iterator in file pkgdb_iterator.c:1110: databa
|
||||
```
|
||||
|
||||
🚨 Mon alarme interne s'est déclenchée. J’ai pensé aux sauvegardes et j’ai immédiatement téléchargé la dernière :
|
||||

|
||||

|
||||
|
||||
En cliquant sur le bouton `Download configuration`, j’ai récupéré le `config.xml` en cours d’utilisation. Je pensais que ça suffirait.
|
||||
|
||||
@@ -21,7 +21,7 @@ This kind of exercise is the worst thing you want to happen because it's never f
|
||||
|
||||
My OPNsense box had been running smoothly for months. Router, firewall, DNS, DHCP, VLANs, VPN, reverse proxy and even UniFi controller: all the pieces of my homelab run through it. but not only, it is also serving internet at home.
|
||||
|
||||

|
||||

|
||||
|
||||
This box is the heart of my network, without it, I can hardly do anything. I have detailed how this is working in my [Homelab]({{< ref "page/homelab" >}}) section. It was “just working,” and I wasn’t worried about it. I felt confident, its backup was living only inside the machine...
|
||||
|
||||
@@ -62,7 +62,7 @@ pkg: sqlite error while executing iterator in file pkgdb_iterator.c:1110: databa
|
||||
```
|
||||
|
||||
🚨 My internal alarm sensor triggered, I wondered about backups, I immediately decided to download the latest backup:
|
||||

|
||||

|
||||
|
||||
Clicking the `Download configuration` button, I downloaded the current `config.xml` in use my the instance, I though it was enough.
|
||||
|
||||
|
Before Width: | Height: | Size: 295 KiB |
@@ -13,7 +13,7 @@ categories:
|
||||
## Intro
|
||||
|
||||
Quand j’ai construit mon cluster **Proxmox VE 8** pour la première fois, le réseau n’était pas ma priorité. Je voulais simplement remplacer rapidement un vieux serveur physique, alors j’ai donné la même configuration de base à chacun de mes trois nœuds, créé le cluster et commencé à créer des VM :
|
||||

|
||||

|
||||
|
||||
Cela a bien fonctionné pendant un moment. Mais comme je prévois de virtualiser mon routeur **OPNsense**, j’ai besoin de quelque chose de plus structuré et cohérent. C’est là que la fonctionnalité **S**oftware-**D**efined **N**etworking (SDN) de Proxmox entre en jeu.
|
||||
|
||||
@@ -21,7 +21,7 @@ Cela a bien fonctionné pendant un moment. Mais comme je prévois de virtualiser
|
||||
## Mon Réseau Homelab
|
||||
|
||||
Par défaut, chaque nœud Proxmox dispose de sa propre zone locale, appelée `localnetwork`, qui contient le pont Linux par défaut (`vmbr0`) comme VNet :
|
||||

|
||||

|
||||
|
||||
C’est suffisant pour des configurations isolées, mais rien n’est coordonné au niveau du cluster.
|
||||
|
||||
@@ -61,29 +61,29 @@ Proxmox prend en charge plusieurs types de zones :
|
||||
- **EVPN** : VXLAN avec BGP pour du routage L3 dynamique
|
||||
|
||||
Comme mon réseau domestique utilise déjà des VLAN, j’ai créé une **zone VLAN** appelée `homelan`, en utilisant `vmbr0` comme pont et en l’appliquant à tout le cluster :
|
||||

|
||||

|
||||
|
||||
### VNets
|
||||
|
||||
Un **VNet** est un réseau virtuel à l’intérieur d’une zone. Dans une zone VLAN, chaque VNet correspond à un ID VLAN spécifique.
|
||||
|
||||
J’ai commencé par créer `vlan55` dans la zone `homelan` pour mon réseau DMZ :
|
||||

|
||||

|
||||
|
||||
Puis j’ai ajouté les VNets correspondant à la plupart de mes VLAN, puisque je prévois de les rattacher à une VM OPNsense :
|
||||

|
||||

|
||||
|
||||
Enfin, j’ai appliqué la configuration dans **Datacenter → SDN** :
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Test de la Configuration Réseau
|
||||
|
||||
Dans une vieille VM que je n'utilise plus, je remplace l'actuel `vmbr0` avec le VLAN tag 66 par mon nouveau VNet `vlan66`:
|
||||

|
||||

|
||||
|
||||
Après l'avoir démarrée, la VM obtient une IP du DHCP d'OPNsense sur ce VLAN, ce qui est super. J'essaye également de ping une autre machine et ça fonctionne :
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Mise à jour de Cloud-Init et Terraform
|
||||
@@ -92,7 +92,7 @@ Pour aller plus loin, j’ai mis à jour le pont réseau utilisé dans mon **tem
|
||||
Comme avec la VM précédente, j’ai remplacé `vmbr0` et le tag VLAN 66 par le nouveau VNet `vlan66`.
|
||||
|
||||
J’ai aussi adapté mon code **Terraform** pour refléter ce changement :
|
||||

|
||||

|
||||
|
||||
Ensuite, j’ai validé qu’aucune régression n’était introduite en déployant une VM de test :
|
||||
```bash
|
||||
@@ -129,7 +129,7 @@ vm_ip = "192.168.66.181"
|
||||
```
|
||||
|
||||
La création s’est déroulée sans problème, tout est bon :
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Conclusion
|
||||
@@ -13,7 +13,7 @@ categories:
|
||||
## Intro
|
||||
|
||||
When I first built my **Proxmox VE 8** cluster, networking wasn’t my main concern. I just wanted to replace an old physical server quickly, so I gave each of my three nodes the same basic config, created the cluster, and started running VMs:
|
||||

|
||||

|
||||
|
||||
That worked fine for a while. But as I plan to virtualize my **OPNsense** router, I need something more structured and consistent. This is where Proxmox **S**oftware-**D**efined **N**etworking (SDN) feature comes in.
|
||||
|
||||
@@ -21,7 +21,7 @@ That worked fine for a while. But as I plan to virtualize my **OPNsense** router
|
||||
## My Homelab Network
|
||||
|
||||
By default, every Proxmox node comes with its own local zone, called `localnetwork`, which contains the default Linux bridge (`vmbr0`) as a VNet:
|
||||

|
||||

|
||||
|
||||
That’s fine for isolated setups, but at the cluster level nothing is coordinated.
|
||||
|
||||
@@ -61,29 +61,29 @@ Proxmox supports several zone types:
|
||||
- **EVPN**: VXLAN with BGP to establish Layer 3 routing
|
||||
|
||||
Since my home network already relies on VLANs, I created a **VLAN Zone** named `homelan`, using `vmbr0` as the bridge and applying it cluster-wide:
|
||||

|
||||

|
||||
|
||||
### VNets
|
||||
|
||||
A **VNet** is a virtual network inside a zone. In a VLAN zone, each VNet corresponds to a specific VLAN ID.
|
||||
|
||||
I started by creating `vlan55` in the `homelan` zone for my DMZ network:
|
||||

|
||||

|
||||
|
||||
Then I added VNets for most of my VLANs, since I plan to attach them to an OPNsense VM:
|
||||

|
||||

|
||||
|
||||
Finally, I applied the configuration in **Datacenter → SDN**:
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Test the Network Configuration
|
||||
|
||||
In a old VM which I don't use anymore, I replace the current `vmbr0` with VLAN tag 66 to my new VNet `vlan66`:
|
||||

|
||||

|
||||
|
||||
After starting it, the VM gets an IP from the DHCP on OPNsense on that VLAN, which sounds good. I also try to ping another machine and it works:
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Update Cloud-Init Template and Terraform
|
||||
@@ -91,7 +91,7 @@ After starting it, the VM gets an IP from the DHCP on OPNsense on that VLAN, whi
|
||||
To go further, I update the bridge used in my **cloud-init** template, which I detailed the creation in that [post]({{< ref "post/1-proxmox-cloud-init-vm-template" >}}). Pretty much the same thing I've done with the VM, I replace the current `vmbr0` with VLAN tag 66 with my new VNet `vlan66`.
|
||||
|
||||
I also update the **Terrafom** code to take this change into account:
|
||||

|
||||

|
||||
|
||||
I quicky check if I don't have regression and can still deploy a VM with Terraform:
|
||||
```bash
|
||||
@@ -128,7 +128,7 @@ vm_ip = "192.168.66.181"
|
||||
```
|
||||
|
||||
The VM is deploying without any issue, everything is OK:
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Conclusion
|
||||
@@ -29,7 +29,7 @@ Au sommet de mon installation, mon modem FAI, une _Freebox_ en mode bridge, reli
|
||||
Ce switch relie également mes trois nœuds Proxmox, chacun sur un port trunk avec le même VLAN natif. Chaque nœud dispose de deux cartes réseau : une pour le trafic général, et l’autre dédiée au réseau de stockage Ceph, connecté à un switch séparé de 2,5 Gbps.
|
||||
|
||||
Depuis le crash d’OPNsense, j’ai simplifié l’architecture en supprimant le lien LACP, qui n’apportait pas de réelle valeur :
|
||||

|
||||

|
||||
|
||||
Jusqu’à récemment, le réseau Proxmox de mon cluster était très basique : chaque nœud était configuré individuellement sans véritable logique commune. Cela a changé après la découverte du SDN Proxmox, qui m’a permis de centraliser les définitions de VLAN sur l’ensemble du cluster. J’ai décrit cette étape dans [cet article]({{< ref "post/11-proxmox-cluster-networking-sdn" >}}).
|
||||
|
||||
@@ -43,7 +43,7 @@ Place au lab. Voici les étapes principales :
|
||||
4. Configurer la haute disponibilité
|
||||
5. Tester la bascule
|
||||
|
||||

|
||||

|
||||
|
||||
### Ajouter des VLANs dans mon homelab
|
||||
|
||||
@@ -53,7 +53,7 @@ Pour cette expérimentation, je crée trois nouveaux VLANs :
|
||||
- **VLAN 103** : _POC pfSync_
|
||||
|
||||
Dans l’interface Proxmox, je vais dans `Datacenter` > `SDN` > `VNets` et je clique sur `Create` :
|
||||

|
||||

|
||||
|
||||
Une fois les trois VLANs créés, j’applique la configuration.
|
||||
|
||||
@@ -114,7 +114,7 @@ La VM `fake-freebox` est maintenant prête à fournir du DHCP sur le VLAN 101, a
|
||||
### Construire les VMs OPNsense
|
||||
|
||||
Je commence par télécharger l’ISO d’OPNsense et je l’upload sur un de mes nœuds Proxmox :
|
||||

|
||||

|
||||
|
||||
#### Création de la VM
|
||||
|
||||
@@ -128,69 +128,69 @@ Je crée la première VM `poc-opnsense-1` avec les paramètres suivants :
|
||||
1. VLAN 101 (_POC WAN_)
|
||||
2. VLAN 102 (_POC LAN_)
|
||||
3. VLAN 103 (_POC pfSync_)
|
||||

|
||||

|
||||
|
||||
ℹ️ Avant de la démarrer, je clone cette VM pour préparer la seconde : `poc-opnsense-2`
|
||||
|
||||
Au premier démarrage, je tombe sur une erreur “access denied”. Pour corriger, j’entre dans le BIOS, **Device Manager > Secure Boot Configuration**, je décoche _Attempt Secure Boot_ et je redémarre :
|
||||

|
||||

|
||||
|
||||
#### Installation d’OPNsense
|
||||
|
||||
La VM démarre sur l’ISO, je ne touche à rien jusqu’à l’écran de login :
|
||||

|
||||

|
||||
|
||||
Je me connecte avec `installer` / `opnsense` et je lance l’installateur. Je sélectionne le disque QEMU de 20 Go comme destination et je démarre l’installation :
|
||||

|
||||

|
||||
|
||||
Une fois terminé, je retire l’ISO du lecteur et je redémarre la machine.
|
||||
|
||||
#### Configuration de Base d’OPNsense
|
||||
|
||||
Au redémarrage, je me connecte avec `root` / `opnsense` et j’arrive au menu CLI :
|
||||

|
||||

|
||||
|
||||
Avec l’option 1, je réassigne les interfaces :
|
||||

|
||||

|
||||
|
||||
L’interface WAN récupère bien `10.101.0.150/24` depuis la `fake-freebox`. Je configure le LAN sur `10.102.0.2/24` et j’ajoute un pool DHCP de `10.102.0.10` à `10.102.0.99` :
|
||||

|
||||

|
||||
|
||||
✅ La première VM est prête, je reproduis l’opération pour la seconde OPNsense `poc-opnsense-2`, qui aura l’IP `10.102.0.3`.
|
||||
|
||||
### Configurer OPNsense en Haute Disponibilité
|
||||
|
||||
Avec les deux VMs OPNsense opérationnelles, il est temps de passer à la configuration via le WebGUI. Pour y accéder, j’ai connecté une VM Windows au VLAN _POC LAN_ et ouvert l’IP de l’OPNsense sur le port 443 :
|
||||

|
||||

|
||||
|
||||
#### Ajouter l’Interface pfSync
|
||||
|
||||
La troisième carte réseau (`vtnet2`) est assignée à l’interface _pfSync_. Ce réseau dédié permet aux deux firewalls de synchroniser leurs états via le VLAN _POC pfSync_ :
|
||||

|
||||

|
||||
|
||||
J’active l’interface sur chaque instance et je leur attribue une IP statique :
|
||||
- **poc-opnsense-1** : `10.103.0.2/24`
|
||||
- **poc-opnsense-2** : `10.103.0.3/24`
|
||||
|
||||
Puis, j’ajoute une règle firewall sur chaque nœud pour autoriser tout le trafic provenant de ce réseau sur l’interface _pfSync_ :
|
||||

|
||||

|
||||
|
||||
#### Configurer la Haute Disponibilité
|
||||
|
||||
Direction `System` > `High Availability` > `Settings`.
|
||||
- Sur le master (`poc-opnsense-1`), je configure les `General Settings` et les `Synchronization Settings`.
|
||||
- Sur le backup (`poc-opnsense-2`), seuls les `General Settings` suffisent (on ne veut pas qu’il écrase la config du master).
|
||||

|
||||

|
||||
|
||||
Une fois appliqué, je vérifie la synchro dans l’onglet `Status` :
|
||||

|
||||

|
||||
|
||||
#### Créer une IP Virtuelle
|
||||
|
||||
Pour fournir une passerelle partagée aux clients, je crée une IP virtuelle (VIP) en **CARP** (Common Address Redundancy Protocol) sur l’interface LAN. L’IP est portée par le nœud actif et bascule automatiquement en cas de failover.
|
||||
|
||||
Menu : `Interfaces` > `Virtual IPs` > `Settings` :
|
||||

|
||||

|
||||
|
||||
Je réplique ensuite la config depuis `System > High Availability > Status` avec le bouton `Synchronize and reconfigure all`.
|
||||
|
||||
@@ -205,7 +205,7 @@ Sur le master :
|
||||
- `DHCP ranges` : cocher aussi `Disable HA sync`
|
||||
- `DHCP options` : ajouter l’option `router [3]` avec la valeur `10.102.0.1` (VIP LAN)
|
||||
- `DHCP options` : cloner la règle pour `dns-server [6]` vers la même VIP.
|
||||

|
||||

|
||||
|
||||
Sur le backup :
|
||||
- `Services` > `Dnsmasq DNS & DHCP` > `General` : cocher `Disable HA sync`
|
||||
@@ -262,7 +262,7 @@ if ($type === "MASTER") {
|
||||
Passons aux tests !
|
||||
|
||||
OPNsense propose un _CARP Maintenance Mode_. Avec le master actif, seul lui avait son WAN monté. En activant le mode maintenance, les rôles basculent : le master devient backup, son WAN est désactivé et celui du backup est activé :
|
||||

|
||||

|
||||
|
||||
Pendant mes pings vers l’extérieur, aucune perte de paquets au moment du basculement.
|
||||
|
||||
@@ -29,7 +29,7 @@ On top of my setup, my ISP modem, a *Freebox* in bridge mode, connects directly
|
||||
The switch also connects my three Proxmox nodes, each on trunk ports with the same native VLAN. Every node has two NICs: one for general networking and the other dedicated to the Ceph storage network, which runs through a separate 2.5 Gbps switch.
|
||||
|
||||
Since the OPNsense crash, I’ve simplified things by removing the LACP link, it wasn’t adding real value:
|
||||

|
||||

|
||||
|
||||
|
||||
Until recently, Proxmox networking on my cluster was very basic: each node was configured individually with no real overlay logic. That changed after I explored Proxmox SDN, where I centralized VLAN definitions across the cluster. I described that step in [this article]({{< ref "post/11-proxmox-cluster-networking-sdn" >}}).
|
||||
@@ -44,7 +44,7 @@ Time to move into the lab. Here are the main steps:
|
||||
4. Configure high availability
|
||||
5. Test failover
|
||||
|
||||

|
||||

|
||||
|
||||
### Add VLANs in my Homelab
|
||||
|
||||
@@ -54,7 +54,7 @@ For this experiment, I create 3 new VLANs:
|
||||
- **VLAN 103**: *POC pfSync*
|
||||
|
||||
In the Proxmox UI, I navigate to `Datacenter` > `SDN` > `VNets` and I click `Create`:
|
||||

|
||||

|
||||
|
||||
Once the 3 new VLAN have been created, I apply the configuration.
|
||||
|
||||
@@ -115,7 +115,7 @@ The `fake-freebox` VM is now ready to serve DHCP on VLAN 101 and serve only one
|
||||
### Build OPNsense VMs
|
||||
|
||||
First I download the OPNsense ISO and upload it to one of my Proxmox nodes:
|
||||

|
||||

|
||||
|
||||
#### VM Creation
|
||||
|
||||
@@ -129,69 +129,69 @@ I create the first VM `poc-opnsense-1`, with the following settings:
|
||||
1. VLAN 101 (POC WAN)
|
||||
2. VLAN 102 (POC LAN)
|
||||
3. VLAN 103 (POC pfSync)
|
||||

|
||||

|
||||
|
||||
ℹ️ Before booting it, I clone this VM to prepare the second one: `poc-opnsense-2`
|
||||
|
||||
On first boot, I hit an “access denied” error. To fix this, I enter the BIOS, go to **Device Manager > Secure Boot Configuration**, uncheck _Attempt Secure Boot_, and restart the VM:
|
||||

|
||||

|
||||
|
||||
#### OPNsense Installation
|
||||
|
||||
The VM boots on the ISO, I touch nothing until I get into the login screen:
|
||||

|
||||

|
||||
|
||||
I log in as `installer` / `opnsense` and launch the installer. I select the QEMU hard disk of 20GB as destination and launch the installation:
|
||||

|
||||

|
||||
|
||||
Once the installation is finished, I remove the ISO from the drive and restart the machine.
|
||||
|
||||
#### OPNsense Basic Configuration
|
||||
|
||||
After reboot, I log in as `root` / `opnsense` and get into the CLI menu:
|
||||

|
||||

|
||||
|
||||
Using option 1, I reassigned interfaces:
|
||||

|
||||

|
||||
|
||||
The WAN interface successfully pulled `10.101.0.150/24` from the `fake-freebox`. I set the LAN interface to `10.102.0.2/24` and configured a DHCP pool from `10.102.0.10` to `10.102.0.99`:
|
||||

|
||||

|
||||
|
||||
✅ The first VM is ready, I start over for the second OPNsense VM, `poc-opnsense-2` which will have the IP `10.102.0.3`
|
||||
|
||||
### Configure OPNsense Highly Available
|
||||
|
||||
With both OPNsense VMs operational, it’s time to configure them from the WebGUI. To access the interface, I connected a Windows VM into the _POC LAN_ VLAN and browsed to the OPNsense IP on port 443:
|
||||

|
||||

|
||||
|
||||
#### Add pfSync Interface
|
||||
|
||||
The third NIC (`vtnet2`) is assigned to the _pfSync_ interface. This dedicated network allows the two firewalls to synchronize states on the VLAN *POC pfSync*:
|
||||

|
||||

|
||||
|
||||
I enable the interface on each instance and configure it with a static IP address:
|
||||
- **poc-opnsense-1**: `10.103.0.2/24`
|
||||
- **poc-opnsense-2**: `10.103.0.3/24`
|
||||
|
||||
Then, I add a firewall rule on each node to allow all traffic coming from this network on that *pfSync* interface:
|
||||

|
||||

|
||||
|
||||
#### Setup High Availability
|
||||
|
||||
Next, in `System` > `High Availability` > `Settings`.
|
||||
- On the master (`poc-opnsense-1`), I configure both the `General Settings` and the `Synchronization Settings`.
|
||||
- On the backup (`poc-opnsense-2`), only `General Settings` are needed, you don't want your backup overwrite the master config.
|
||||

|
||||

|
||||
|
||||
Once applied, I verify synchronization on the `Status` page:
|
||||

|
||||

|
||||
|
||||
#### Create Virtual IP Address
|
||||
|
||||
To provide a shared gateway for clients, I create a CARP Virtual IP (VIP) on the LAN interface. It is using the Common Address Redundancy Protocol. This IP is claimed by the active node and automatically fails over.
|
||||
|
||||
Navigate to `Interfaces` > `Virtual IPs` > `Settings`:
|
||||

|
||||

|
||||
|
||||
To replicate the config, I go to `System > High Availability > Status` and click the button next to `Synchronize and reconfigure all`.
|
||||
|
||||
@@ -206,7 +206,7 @@ On the master:
|
||||
- `DHCP ranges`: also tick the `Disable HA sync` box
|
||||
- `DHCP options`: add the option `router [3]` with the value `10.102.0.1` (LAN VIP)
|
||||
- `DHCP options`: clone the rule for `router [6]` pointing to the same VIP.
|
||||

|
||||

|
||||
|
||||
On the backup:
|
||||
- `Services` > `Dnsmasq DNS & DHCP` > `General`: also tick the `Disable HA sync` box
|
||||
@@ -264,7 +264,7 @@ if ($type === "MASTER") {
|
||||
Time for the real test!
|
||||
|
||||
OPNsense provides a _CARP Maintenance Mode_. With the master active, WAN was enabled only on that node. Entering maintenance mode flipped the roles: the master became backup, its WAN disabled, while the backup enabled its WAN:
|
||||

|
||||

|
||||
|
||||
While pinging outside the network, I observed zero packet loss during the failover.
|
||||
|
||||
@@ -84,7 +84,7 @@ Il est temps de mettre à jour, dans `System` > `Firmware` > `Status`, je v
|
||||
Une fois mis à jour et redémarré, je vais dans `System` > `Firmware` > `Plugins`, je coche l'option pour afficher les plugins communautaires. J'installe que le **QEMU Guest Agent**, `os-qemu-guest-agent`, pour permettre la communication entre la VM et l'hôte Proxmox.
|
||||
|
||||
Cela nécessite un arrêt. Dans Proxmox, j'active le `QEMU Guest Agent` dans les options de la VM :
|
||||

|
||||

|
||||
|
||||
Finalement je redémarre la VM. Une fois démarrée, depuis la WebGUI de Proxmox, je peux voir les IPs de la VM ce qui confirme que le guest agent fonctionne.
|
||||
|
||||
@@ -92,7 +92,7 @@ Finalement je redémarre la VM. Une fois démarrée, depuis la WebGUI de Proxmox
|
||||
## Interfaces
|
||||
|
||||
Sur les deux pare‑feu, j'assigne les NIC restantes à de nouvelles interfaces en ajoutant une description. Les VMs ont 7 interfaces, je compare attentivement les adresses MAC pour éviter de mélanger les interfaces :
|
||||

|
||||

|
||||
|
||||
Au final, la configuration des interfaces ressemble à ceci :
|
||||
|
||||
@@ -160,13 +160,13 @@ La HA est configurée dans `System` > `High Availability` > `Settings`
|
||||
### Statut de la HA
|
||||
|
||||
Dans `System` > `High Availability` > `Status`, je peux vérifier si la synchronisation fonctionne. Sur cette page je peux répliquer un ou tous les services du master vers le nœud backup :
|
||||

|
||||

|
||||
|
||||
---
|
||||
## IPs Virtuelles
|
||||
|
||||
Maintenant que la HA est configurée, je peux attribuer à mes réseaux une IP virtuelle partagée entre mes nœuds. Dans `Interfaces` > `Virtual IPs` > `Settings`, je crée un VIP pour chacun de mes réseaux en utilisant **CARP** (Common Address Redundancy Protocol). L'objectif est de réutiliser les adresses IP utilisées par mon instance OPNsense actuelle, mais comme elle route encore mon réseau, j'utilise des IP différentes pour la phase de configuration :
|
||||

|
||||

|
||||
|
||||
ℹ️ OPNsense permet CARP par défaut, aucune règle de pare‑feu spéciale requise
|
||||
|
||||
@@ -242,7 +242,7 @@ Pour commencer, dans `Firewall` > `Groups`, je crée 2 zones pour regrouper m
|
||||
### Network Aliases
|
||||
|
||||
Ensuite, dans `Firewall` > `Aliases`, je crée un alias `InternalNetworks` pour regrouper tous mes réseaux internes :
|
||||

|
||||

|
||||
|
||||
### Règles de Pare-feu Rules
|
||||
|
||||
@@ -345,17 +345,17 @@ Sur le nœud backup, je le configure de la même manière, la seule différence
|
||||
### Plages DHCP
|
||||
|
||||
Ensuite je configure les plages DHCP. Les deux pare‑feu auront des plages différentes, le nœud backup aura des plages plus petites (10 baux devraient suffire). Sur le master, elles sont configurées comme suit :
|
||||

|
||||

|
||||
|
||||
### Options DHCP
|
||||
|
||||
Puis je définis quelques options DHCP pour chaque domaine : le `router`, le `dns-server` et le `domain-name`. Je pointe les adresses IP vers la VIP de l'interface :
|
||||

|
||||

|
||||
|
||||
### Hôtes
|
||||
|
||||
Enfin, dans l'onglet `Hosts`, je définis des mappings DHCP statiques mais aussi des IP statiques non gérées par le DHCP, pour qu'elles soient enregistrées dans le DNS :
|
||||

|
||||

|
||||
|
||||
---
|
||||
## DNS
|
||||
@@ -372,7 +372,7 @@ Unbound est le résolveur récursif, pour les zones locales j'effectue un forwar
|
||||
### Paramètres Généraux d'Unbound
|
||||
|
||||
Configurons-le, dans `Services` > `Unbound DNS` > `General` :
|
||||

|
||||

|
||||
|
||||
### Liste de Blocage DNS
|
||||
|
||||
@@ -383,7 +383,7 @@ Pour maintenir le service à jour, dans `System` > `Settings` > `Cron`, j'a
|
||||
### Transfert de Requêtes
|
||||
|
||||
Enfin je configure le transfert de requêtes pour mes domaines locaux vers Dnsmasq. Dans `Services` > `Unbound DNS` > `Query Forwarding`, j'ajoute chacun de mes domaines locaux avec leurs reverse lookups (enregistrements PTR) :
|
||||

|
||||

|
||||
|
||||
---
|
||||
## VPN
|
||||
@@ -92,7 +92,7 @@ Finally I restart the VM. Once started, from the Proxmox WebGUI, I can see the I
|
||||
## Interfaces
|
||||
|
||||
On both firewalls, I assign the remaining NICs to new interfaces adding a description. The VMs have 7 interfaces, I carefully compare MAC addresses to avoid mixing interfaces:
|
||||

|
||||

|
||||
|
||||
In the end, the interfaces configuration looks like this:
|
||||
|
||||
@@ -157,13 +157,13 @@ The HA is setup in `System` > `High Availability` > `Settings`
|
||||
### HA Status
|
||||
|
||||
In the section `System` > `High Availability` > `Status`, I can verify if the synchronization is working. On this page I can replicate any or all services from my master to my backup node:
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Virtual IPs
|
||||
|
||||
Now that HA is configured, I can give my networks a virtual IP shared across my nodes. In `Interfaces` > `Virtual IPs` > `Settings`, I create one VIP for each of my networks using **CARP** (Common Address Redundancy Protocol). The target is to reuse the IP addresses used by my current OPNsense instance, but as it is still routing my network, I use different IPs for the configuration phase:
|
||||

|
||||

|
||||
|
||||
ℹ️ OPNsense allows CARP by default, no special firewall rule required
|
||||
|
||||
@@ -239,7 +239,7 @@ To begin, in `Firewall` > `Groups`, I create 2 zones to regroup my interfaces:
|
||||
### Network Aliases
|
||||
|
||||
Next, in `Firewall` > `Aliases`, I create an alias `InternalNetworks` to regroup all my internal networks:
|
||||

|
||||

|
||||
|
||||
### Firewall Rules
|
||||
|
||||
@@ -343,17 +343,17 @@ On the backup node, I configure it the same, the only difference will be the **D
|
||||
### DHCP Ranges
|
||||
|
||||
Next I configure the DHCP ranges. Both firewalls will have different ranges, the backup node will have smaller ones (only 10 leases should be enough). On the master, they are configured as follow:
|
||||

|
||||

|
||||
|
||||
### DHCP Options
|
||||
|
||||
Then I set some DHCP options for each domain: the `router`, the `dns-server` and the `domain-name`. I'm pointing the IP addresses to the interface's VIP:
|
||||

|
||||

|
||||
|
||||
### Hosts
|
||||
|
||||
Finally in in the `Hosts` tab, I define static DHCP mappings but also static IP not managed by the DHCP, to have them registered in the DNS:
|
||||

|
||||

|
||||
|
||||
---
|
||||
## DNS
|
||||
@@ -370,7 +370,7 @@ Unbound is the recursive resolver, for local zones I forward queries to Dnsmasq.
|
||||
### Unbound General Settings
|
||||
|
||||
Let's configure it, in `Services` > `Unbound DNS` > `General`:
|
||||

|
||||

|
||||
|
||||
### DNS Blocklist
|
||||
|
||||
@@ -381,7 +381,7 @@ To maintain the service up to date, in `System` > `Settings` > `Cron`, I add my
|
||||
### Query Forwarding
|
||||
|
||||
Finally I configure query forwarding for my local domains to Dnsmasq. In `Services` > `Unbound DNS` > `Query Forwarding`, I add each of my local domains with their reverse lookup (PTR record):
|
||||

|
||||

|
||||
|
||||
---
|
||||
## VPN
|
||||
@@ -108,7 +108,7 @@ apt full-upgrade -y
|
||||
```
|
||||
|
||||
Après la mise à niveau sur le premier nœud, la version Ceph affiche maintenant `19.2.3`, je peux voir mes OSD apparaître comme obsolètes, les moniteurs nécessitent soit une mise à niveau soit un redémarrage :
|
||||

|
||||

|
||||
|
||||
Je poursuis et mets à niveau les paquets sur les 2 autres nœuds.
|
||||
|
||||
@@ -132,7 +132,7 @@ systemctl restart ceph-osd.target
|
||||
```
|
||||
|
||||
Je surveille le statut Ceph avec la WebGUI Proxmox. Après le redémarrage, elle affiche quelques couleurs fancy. J’attends juste que les PG redeviennent verts, cela prend moins d’une minute :
|
||||

|
||||

|
||||
|
||||
Un avertissement apparaît : `HEALTH_WARN: all OSDs are running squid or later but require_osd_release < squid`
|
||||
|
||||
@@ -108,7 +108,7 @@ apt full-upgrade -y
|
||||
```
|
||||
|
||||
After the upgrade on the first node, the Ceph version now shows `19.2.3`, I can see my OSDs appear as outdated, the monitors need either an upgrade or a restart:
|
||||

|
||||

|
||||
|
||||
I carry on and upgrade the packages on the 2 other nodes.
|
||||
|
||||
@@ -132,7 +132,7 @@ systemctl restart ceph-osd.target
|
||||
```
|
||||
|
||||
I monitor the Ceph status with the Proxmox WebGUI. After the restart, it is showing some fancy colors. I'm just waiting for the PGs to be back to green, it takes less than a minute:
|
||||

|
||||

|
||||
|
||||
A warning shows up: `HEALTH_WARN: all OSDs are running squid or later but require_osd_release < squid`
|
||||
|
||||
@@ -34,12 +34,12 @@ D'abord, je configure mon réseau de couche 2 qui est géré par UniFi. Là, je
|
||||
- _pfSync_ (44), communication entre mes nœuds OPNsense.
|
||||
|
||||
Dans le contrôleur UniFi, dans `Paramètres` > `Réseaux`, j'ajoute un `New Virtual Network`. Je le nomme `WAN` et lui donne l'ID VLAN 20 :
|
||||

|
||||

|
||||
|
||||
Je fais la même chose pour le VLAN `pfSync` avec l'ID VLAN 44.
|
||||
|
||||
Je prévois de brancher ma box FAI sur le port 15 de mon switch, qui est désactivé pour l'instant. Je l'active, définis le VLAN natif sur le nouveau `WAN (20)` et désactive le trunking :
|
||||

|
||||

|
||||
|
||||
Une fois ce réglage appliqué, je m'assure que seules les ports où sont connectés mes nœuds Proxmox propagent ces VLANs sur leur trunk.
|
||||
|
||||
@@ -50,7 +50,7 @@ J'ai fini la configuration UniFi.
|
||||
Maintenant que le VLAN peut atteindre mes nœuds, je veux le gérer dans le SDN de Proxmox. J'ai configuré le SDN dans [cet article]({{< ref "post/11-proxmox-cluster-networking-sdn" >}}).
|
||||
|
||||
Dans `Datacenter` > `SDN` > `VNets`, je crée un nouveau VNet, je l'appelle `vlan20` pour suivre ma propre convention de nommage, je lui donne l'alias _WAN_ et j'utilise le tag (ID VLAN) 20 :
|
||||

|
||||

|
||||
|
||||
Je crée aussi le `vlan44` pour le VLAN _pfSync_, puis j'applique cette configuration et nous avons terminé avec le SDN.
|
||||
|
||||
@@ -75,7 +75,7 @@ La première VM s'appelle `cerbere-head1` (je ne vous l'ai pas dit ? Mon firew
|
||||
6. `vlan55` _(DMZ)_
|
||||
7. `vlan66` _(Lab)_
|
||||
|
||||

|
||||

|
||||
|
||||
ℹ️ Maintenant je clone cette VM pour créer `cerbere-head2`, puis je procède à l'installation d'OPNsense. Je ne veux pas entrer trop dans les détails de l'installation d'OPNsense, je l'ai déjà documentée dans le [proof of concept]({{< ref "post/12-opnsense-virtualization-highly-available" >}}).
|
||||
|
||||
@@ -117,7 +117,7 @@ Dans Proxmox VE 8, il était possible de créer des groupes HA, en fonction de l
|
||||
Le cluster Proxmox est capable de fournir de la HA pour les ressources, mais vous devez définir les règles.
|
||||
|
||||
Dans `Datacenter` > `HA`, vous pouvez voir le statut et gérer les ressources. Dans le panneau `Resources` je clique sur `Add`. Je dois choisir la ressource à configurer en HA dans la liste, ici `cerbere-head1` avec l'ID 122. Puis dans l'infobulle je peux définir le maximum de redémarrages et de relocations, je laisse `Failback` activé et l'état demandé à `started` :
|
||||

|
||||

|
||||
|
||||
Le cluster Proxmox s'assurera maintenant que cette VM est démarrée. Je fais de même pour l'autre VM OPNsense, `cerbere-head2`.
|
||||
|
||||
@@ -126,7 +126,7 @@ Le cluster Proxmox s'assurera maintenant que cette VM est démarrée. Je fais de
|
||||
Super, mais je ne veux pas qu'elles tournent sur le même nœud. C'est là qu'intervient la nouvelle fonctionnalité des règles d'affinité HA de Proxmox VE 9. Proxmox permet de créer des règles d'affinité de nœud et de ressource. Peu m'importe sur quel nœud elles tournent, mais je ne veux pas qu'elles soient ensemble. J'ai besoin d'une règle d'affinité de ressource.
|
||||
|
||||
Dans `Datacenter` > `HA` > `Affinity Rules`, j'ajoute une nouvelle règle d'affinité de ressource HA. Je sélectionne les deux VMs et choisis l'option `Keep Separate` :
|
||||

|
||||

|
||||
|
||||
✅ Mes VMs OPNsense sont maintenant entièrement prêtes !
|
||||
|
||||
@@ -393,7 +393,7 @@ En entrant manuellement en mode maintenance CARP depuis l'interface WebGUI, aucu
|
||||
|
||||
Pour simuler un failover, je tue la VM OPNsense active. Ici j'observe une seule perte de paquet. Génial.
|
||||
|
||||

|
||||

|
||||
|
||||
3. **Reprise après sinistre**
|
||||
|
||||
@@ -33,12 +33,12 @@ First, I configure my layer 2 network which is managed by UniFi. There I need to
|
||||
- *pfSync* (44), communication between my OPNsense nodes.
|
||||
|
||||
In the UniFi controller, in `Settings` > `Networks`, I add a `New Virtual Network`. I name it `WAN` and give it the VLAN ID 20:
|
||||

|
||||

|
||||
|
||||
I do the same thing again for the `pfSync` VLAN with the VLAN ID 44.
|
||||
|
||||
I plan to plug my ISP box on the port 15 of my switch, which is disabled for now. I set it as active, set the native VLAN on the newly created one `WAN (20)` and disable trunking:
|
||||

|
||||

|
||||
|
||||
Once this setting applied, I make sure that only the ports where are connected my Proxmox nodes propagate these VLAN on their trunk.
|
||||
|
||||
@@ -49,7 +49,7 @@ I'm done with UniFi configuration.
|
||||
Now that the VLAN can reach my nodes, I want to handle it in the Proxmox SDN. I've configured the SDN in [that article]({{< ref "post/11-proxmox-cluster-networking-sdn" >}}).
|
||||
|
||||
In `Datacenter` > `SDN` > `VNets`, I create a new VNet, call it `vlan20` to follow my own naming convention, give it the *WAN* alias and use the tag (VLAN ID) 20:
|
||||

|
||||

|
||||
|
||||
I also create the `vlan44` for the *pfSync* VLAN, then I apply this configuration and we are done with the SDN.
|
||||
|
||||
@@ -74,7 +74,7 @@ The first VM is named `cerbere-head1` (I didn't tell you? My current firewall is
|
||||
6. `vlan55` *(DMZ)*
|
||||
7. `vlan66` *(Lab)*
|
||||
|
||||

|
||||

|
||||
|
||||
ℹ️ Now I clone that VM to create `cerbere-head2`, then I proceed with OPNsense installation. I don't want to go into much details about OPNsense installation, I already documented it in the [proof of concept]({{< ref "post/12-opnsense-virtualization-highly-available" >}}).
|
||||
|
||||
@@ -115,7 +115,7 @@ In Proxmox VE 8, It was possible to create HA groups, depending of their resourc
|
||||
The Proxmox cluster is able to provide HA for the resources, but you need to define the rules.
|
||||
|
||||
In `Datacenter` > `HA`, you can see the status and manage the resources. In the `Resources` panel I click on `Add`. I need to pick the resource to configure as HA in the list, here `cerbere-head1` with ID 122. Then in the tooltip I can define the maximum of restart and relocate, I keep `Failback` enabled and the requested state to `started`:
|
||||

|
||||

|
||||
|
||||
The Proxmox cluster will now make sure this VM is started. I do the same for the other OPNsense VM, `cerbere-head2`.
|
||||
|
||||
@@ -124,7 +124,7 @@ The Proxmox cluster will now make sure this VM is started. I do the same for the
|
||||
Great, but I don't want them on the same node. This is when the new feature HA affinity rules, of Proxmox VE 9, come in. Proxmox allows to create node affinity and resource affinity rules. I don't mind on which node they run, but I don't want them together. I need a resource affinity rule.
|
||||
|
||||
In `Datacenter` > `HA` > `Affinity Rules`, I add a new HA resource affinity rule. I select both VMs and pick the option `Keep Separate`:
|
||||

|
||||

|
||||
|
||||
✅ My OPNsense VMs are now fully ready!
|
||||
|
||||
@@ -390,7 +390,7 @@ When manually entering CARP maintenance mode from the WebGUI interface, no packe
|
||||
|
||||
To simulate a failover, I kill the active OPNsense VM. Here I observe only one packet dropped. Awesome.
|
||||
|
||||

|
||||

|
||||
|
||||
3. **Disaster Recovery**
|
||||
|
||||
@@ -109,23 +109,23 @@ Avec Semaphore en fonctionnement, faisons rapidement le tour de l'UI et connecto
|
||||
## Discovery
|
||||
|
||||
Après avoir démarré la stack, je peux atteindre la page de connexion à l'URL :
|
||||

|
||||

|
||||
|
||||
Pour me connecter, j'utilise les identifiants définis par `SEMAPHORE_ADMIN_NAME`/`SEMAPHORE_ADMIN_PASSWORD`.
|
||||
|
||||
Au premier accès, Semaphore me demande de créer un projet. J'ai créé le projet Homelab :
|
||||

|
||||

|
||||
|
||||
La première chose que je veux faire est d'ajouter mon dépôt _homelab_ (vous pouvez trouver son miroir sur Github [ici](https://github.com/Vezpi/homelab)). Dans `Repository`, je clique sur le bouton `New Repository`, et j'ajoute l'URL du repo. Je ne spécifie pas d'identifiants car le dépôt est public :
|
||||

|
||||

|
||||
|
||||
ℹ️ Avant de continuer, je déploie 3 VM à des fins de test : `sem01`, `sem02` et `sem03`. Je les ai créées avec Terraform via [ce projet](https://github.com/Vezpi/Homelab/tree/main/terraform/projects/semaphore-vms).
|
||||
|
||||
Pour interagir avec ces VM, je dois configurer des identifiants. Dans le `Key Store`, j'ajoute la première donnée d'identification, une clé SSH pour mon utilisateur :
|
||||

|
||||

|
||||
|
||||
Ensuite je crée un nouvel `Inventory`. J'utilise le format d'inventaire Ansible (le seul disponible). Je sélectionne la clé SSH créée précédemment et choisis le type `Static`. Dans les champs je renseigne les 3 hôtes créés avec leur FQDN :
|
||||

|
||||

|
||||
|
||||
✅ Avec un projet, un repo, des identifiants et un inventaire en place, je peux avancer et tester l'exécution d'un playbook Ansible.
|
||||
|
||||
@@ -172,20 +172,20 @@ Je veux tester quelque chose de simple : installer un serveur web avec une page
|
||||
```
|
||||
|
||||
Dans Semaphore UI, je peux maintenant créer mon premier `Task Template` pour un playbook Ansible. Je lui donne un nom, le chemin du playbook (depuis le dossier racine du repo), le dépôt et sa branche :
|
||||

|
||||

|
||||
|
||||
Il est temps de lancer le playbook ! Dans la liste des task templates, je clique sur le bouton ▶️ :
|
||||

|
||||

|
||||
|
||||
Le playbook se lance et je peux suivre la sortie en temps réel :
|
||||

|
||||

|
||||
|
||||
Je peux aussi consulter les exécutions précédentes :
|
||||

|
||||

|
||||
|
||||
|
||||
✅ Enfin, je peux confirmer que le travail est fini en vérifiant l'URL sur le port 80 (http) :
|
||||

|
||||

|
||||
|
||||
Gérer des playbooks Ansible dans Semaphore UI est assez simple et vraiment pratique. L'interface est très soignée.
|
||||
|
||||
@@ -233,19 +233,19 @@ Avec cela en place, le playbook a réussi et j'ai pu créer l'utilisateur :
|
||||
```
|
||||
|
||||
Ensuite je crée un variable group `pve_vm`. Un variable group me permet de définir plusieurs variables et secrets ensemble :
|
||||

|
||||

|
||||
|
||||
Puis je crée un nouveau task template, cette fois de type Terraform Code. Je lui donne un nom, le chemin du projet Terraform, un workspace, le dépôt avec sa branche et le variable group :
|
||||

|
||||

|
||||
|
||||
Lancer le template me donne quelques options supplémentaires liées à Terraform :
|
||||

|
||||

|
||||
|
||||
Après le plan Terraform, il me propose d'appliquer, d'annuler ou d'arrêter :
|
||||

|
||||

|
||||
|
||||
Enfin, après avoir cliqué sur ✅ pour appliquer, j'ai pu regarder Terraform construire les VM, comme avec le CLI. À la fin, les VM ont été déployées avec succès sur Proxmox :
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Conclusion
|
||||
@@ -109,23 +109,23 @@ With Semaphore running, let’s take a quick tour of the UI and wire it up to a
|
||||
## Discovery
|
||||
|
||||
After starting the stack, I can reach the login page at the URL:
|
||||

|
||||

|
||||
|
||||
To log in, I use the credentials defined by `SEMAPHORE_ADMIN_NAME`/`SEMAPHORE_ADMIN_PASSWORD`.
|
||||
|
||||
On first login, Semaphore prompt me to create a project. I created the Homelab project:
|
||||

|
||||

|
||||
|
||||
The first thing I want to do is to add my *homelab* repository (you can find its mirror on Github [here](https://github.com/Vezpi/homelab)). In `Repository`, I click the `New Repository` button, and add the repo URL. I don't specify credentials because the repo is public:
|
||||

|
||||

|
||||
|
||||
ℹ️ Before continue, I deploy 3 VMs for testing purpose: `sem01`, `sem02` and `sem03`. I created them using Terraform with [this project](https://github.com/Vezpi/Homelab/tree/main/terraform/projects/semaphore-vms).
|
||||
|
||||
To interact with these VMs I need to configure credentials. In the the `Key Store`, I add the first credential, a SSH key for my user:
|
||||

|
||||

|
||||
|
||||
Then I create a new `Inventory`. I'm using the Ansible inventory format (the only one available). I select the SSH key previously created and select the type as `Static`. In the fields I enter the 3 hosts created with their FQDN:
|
||||

|
||||

|
||||
|
||||
✅ With a project, repo, credentials, and inventory in place, I can move forward and test to run an Ansible playbook.
|
||||
|
||||
@@ -172,20 +172,20 @@ I want to test something simple, install a web server with a custom page on thes
|
||||
```
|
||||
|
||||
In Semaphore UI, I can now create my first `Task Template` for Ansible playbook. I give it a name, the playbook path (from the root folder of the repo), the repository and its branch:
|
||||

|
||||

|
||||
|
||||
Time to launch the playbook! In the task templates list, I click on the ▶️ button:
|
||||

|
||||

|
||||
|
||||
The playbook launches and I can follow the output in real time:
|
||||

|
||||

|
||||
|
||||
I can also review previous runs:
|
||||

|
||||

|
||||
|
||||
|
||||
✅ Finally I can confirm the job is done by checking the URL on port 80 (http):
|
||||

|
||||

|
||||
|
||||
Managing Ansible playbooks in Semaphore UI is pretty simple and really convenient. The interface is really sleek.
|
||||
|
||||
@@ -233,19 +233,19 @@ With that in place, the playbook succeeded and I could create the user:
|
||||
```
|
||||
|
||||
Next I create a variable group `pve_vm`. A variable group let me define multiple variables and secrets together:
|
||||

|
||||

|
||||
|
||||
Then I create a new task template, this time with the kind Terraform Code. I give it a name, the path of the terraform [project](https://github.com/Vezpi/Homelab/tree/main/terraform/projects/semaphore-vms), a workspace, the repository along with its branch and. the variable group:
|
||||

|
||||

|
||||
|
||||
Running the template gives me some additional options related to Terraform:
|
||||

|
||||

|
||||
|
||||
After the Terraform plan, I'm proposed to apply, cancel or stop:
|
||||

|
||||

|
||||
|
||||
Finally after hitting ✅ to apply, I could watch Terraform build the VMs, just like using the CLI. At the end, the VMs were successfully deployed on Proxmox:
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Conclusion
|
||||
@@ -68,7 +68,7 @@ J'ai considéré FreeNAS/TrueNAS, OpenMediaVault et Unraid. J'ai choisi TrueNAS
|
||||
L'installation ne s'est pas déroulée aussi bien que prévu...
|
||||
|
||||
J'utilise [Ventoy](https://www.ventoy.net/en/index.html) pour garder plusieurs ISOs sur une clé USB. J'étais en version 1.0.99, et l'ISO ne se lançait pas. La mise à jour vers 1.1.10 a résolu le problème :
|
||||

|
||||

|
||||
|
||||
Mais là j'ai rencontré un autre problème lors du lancement de l'installation sur mon périphérique de stockage eMMC :
|
||||
```
|
||||
@@ -77,16 +77,16 @@ Failed to find partition number 2 on mmcblk0
|
||||
|
||||
J'ai trouvé une solution sur ce [post](https://forums.truenas.com/t/installation-failed-on-emmc-odroid-h4/15317/12) :
|
||||
- Entrer dans le shell
|
||||

|
||||

|
||||
- Éditer le fichier `/lib/python3/dist-packages/truenas_installer/utils.py`
|
||||
- Déplacer la ligne `await asyncio.sleep(1)` juste sous `for _try in range(tries):`
|
||||
- Modifier la ligne 46 pour ajouter `+ 'p'` :
|
||||
`for partdir in filter(lambda x: x.is_dir() and x.name.startswith(device + 'p'), dir_contents):`
|
||||

|
||||

|
||||
- Quitter le shell et lancer l'installation sans redémarrer
|
||||
|
||||
L'installateur a finalement pu passer :
|
||||

|
||||

|
||||
|
||||
Une fois l'installation terminée, j'ai éteint la machine. Ensuite je l'ai installée dans mon rack au-dessus des 3 nœuds Proxmox VE. J'ai branché les deux câbles Ethernet depuis mon switch et je l'ai mise sous tension.
|
||||
|
||||
@@ -99,18 +99,18 @@ Par défaut, TrueNAS utilise DHCP. J'ai trouvé son adresse MAC dans mon interfa
|
||||
### Paramètres généraux
|
||||
|
||||
Pendant l'installation, je n'ai pas défini de mot de passe pour truenas_admin. La page de connexion m'a forcé à en choisir un :
|
||||

|
||||

|
||||
|
||||
Une fois le mot de passe mis à jour, j'arrive sur le tableau de bord. L'interface donne une bonne impression au premier abord :
|
||||

|
||||

|
||||
|
||||
J'explore rapidement l'interface, la première chose que je fais est de changer le hostname en `granite` et de cocher la case en dessous pour hériter du domaine depuis DHCP :
|
||||

|
||||

|
||||
|
||||
Dans les `General Settings`, je change les paramètres de `Localization`. Je mets le Console Keyboard Map sur `French (AZERTY)` et le Fuseau horaire sur `Europe/Paris`.
|
||||
|
||||
Je crée un nouvel utilisateur `vez`, avec le rôle `Full Admin` dans TrueNAS. J'autorise SSH uniquement pour l'authentification par clé, pas de mots de passe :
|
||||

|
||||

|
||||
|
||||
Finalement je retire le rôle admin de `truenas_admin` et verrouille le compte.
|
||||
|
||||
@@ -119,16 +119,16 @@ Finalement je retire le rôle admin de `truenas_admin` et verrouille le compte
|
||||
Dans TrueNAS, un pool est une collection de stockage créée en combinant plusieurs disques en un espace unifié géré par ZFS.
|
||||
|
||||
Dans la page `Storage`, je trouve mes `Disks`, où je peux confirmer que TrueNAS voit mon couple de NVMe :
|
||||

|
||||

|
||||
|
||||
De retour sur le `Storage Dashboard`, je clique sur le bouton `Create Pool`. Je nomme le pool `storage` parce que je suis vraiment inspiré pour lui donner un nom :
|
||||

|
||||

|
||||
|
||||
Puis je sélectionne la disposition `Mirror` :
|
||||

|
||||

|
||||
|
||||
J'explore rapidement les configurations optionnelles, mais les valeurs par défaut me conviennent : autotrim, compression, pas de dedup, etc. À la fin, avant de créer le pool, il y a une section `Review` :
|
||||

|
||||

|
||||
|
||||
Après avoir cliqué sur `Create Pool`, on m'avertit que tout sur les disques sera effacé, ce que je confirme. Finalement le pool est créé.
|
||||
|
||||
@@ -139,10 +139,10 @@ Un dataset est un système de fichiers à l'intérieur d'un pool. Il peut conten
|
||||
#### Partage SMB
|
||||
|
||||
Créons maintenant mon premier dataset `files` pour partager des fichiers sur le réseau pour mes clients Windows, comme des ISOs, etc :
|
||||

|
||||

|
||||
|
||||
Lors de la création de datasets SMB dans SCALE, définissez le Share Type sur SMB afin que les bons ACL/xattr par défaut s'appliquent. TrueNAS me demande alors de démarrer/activer le service SMB :
|
||||

|
||||

|
||||
|
||||
Depuis mon portable Windows, j'essaie d'accéder à mon nouveau partage `\\granite.mgmt.vezpi.com\files`. Comme prévu on me demande des identifiants.
|
||||
|
||||
@@ -157,7 +157,7 @@ Je crée un autre dataset : `media`, et un enfant `photos`. Je crée un partag
|
||||
Sur mon serveur NFS actuel, les fichiers photos sont possédés par `root` (gérés par _Immich_). Plus tard je verrai comment migrer vers une version sans root.
|
||||
|
||||
⚠️ Pour l'instant je définis, dans les `Advanced Options`, le `Maproot User` et le `Maproot Group` sur `root`. Cela équivaut à l'attribut NFS `no_squash_root`, le `root` local du client reste `root` sur le serveur, ne faites pas ça :
|
||||

|
||||

|
||||
|
||||
✅ Je monte le partage NFS sur un client, cela fonctionne bien.
|
||||
|
||||
@@ -178,14 +178,14 @@ J'ai mentionné les capacités VM dans mes exigences. Je ne couvrirais pas cela
|
||||
### Protection des données
|
||||
|
||||
Il est maintenant temps d'activer quelques fonctionnalités de protection des données :
|
||||

|
||||

|
||||
|
||||
Je veux créer des snapshots automatiques pour certains de mes datasets, ceux qui me tiennent le plus à cœur : mes fichiers cloud et les photos.
|
||||
|
||||
Créons des tâches de snapshot. Je clique sur le bouton `Add` à côté de `Periodic Snapshot Tasks` :
|
||||
- cloud : snapshots quotidiens, conserver pendant 2 mois
|
||||
- photos : snapshots quotidiens, conserver pendant 7 jours
|
||||

|
||||

|
||||
|
||||
Je pourrais aussi configurer une `Cloud Sync Task`, mais Duplicati gère déjà les sauvegardes hors site.
|
||||
|
||||
@@ -204,12 +204,12 @@ sudo rsync -a --info=progress2 /data/photo/ /new_photos
|
||||
```
|
||||
|
||||
À la fin, je pourrais décommissionner mon ancien serveur NFS sur le LXC. La disposition des datasets après migration ressemble à ceci :
|
||||

|
||||

|
||||
|
||||
### Application Android
|
||||
|
||||
Par curiosité, j'ai cherché sur le Play Store une application pour gérer une instance TrueNAS. J'ai trouvé [Nasdeck](https://play.google.com/store/apps/details?id=com.strtechllc.nasdeck&hl=fr&pli=1), qui est plutôt sympa. Voici quelques captures d'écran :
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Conclusion
|
||||
@@ -67,7 +67,7 @@ I considered FreeNAS/TrueNAS, OpenMediaVault, and Unraid. I chose TrueNAS SCALE
|
||||
The install didn’t go as smoothly as expected...
|
||||
|
||||
I use [Ventoy](https://www.ventoy.net/en/index.html) to keep multiple ISOs on one USB stick. I was in version 1.0.99, and the ISO wouldn't launch. Updating to 1.1.10 fixed it:
|
||||

|
||||

|
||||
|
||||
But here I encountered another problem when launching the installation on my eMMC storage device:
|
||||
```
|
||||
@@ -76,16 +76,16 @@ Failed to find partition number 2 on mmcblk0
|
||||
|
||||
I found a solution on this [post](https://forums.truenas.com/t/installation-failed-on-emmc-odroid-h4/15317/12):
|
||||
- Enter the shell
|
||||

|
||||

|
||||
- Edit the file `/lib/python3/dist-packages/truenas_installer/utils.py`
|
||||
- Move the line `await asyncio.sleep(1)` right beneath `for _try in range(tries):`
|
||||
- Edit line 46 to add `+ 'p'`:
|
||||
`for partdir in filter(lambda x: x.is_dir() and x.name.startswith(device + 'p'), dir_contents):`
|
||||

|
||||

|
||||
- Exit the shell and start the installation without reboot
|
||||
|
||||
The installer was finally able to get through:
|
||||

|
||||

|
||||
|
||||
Once the installation was complete, I shut down the machine. Then I installed it into my rack on top of the 3 Proxmox VE nodes. I plugged both Ethernet cables from my switch and powered it up.
|
||||
|
||||
@@ -97,18 +97,18 @@ By default, TrueNAS uses DHCP. I found its MAC address in my UniFi interface and
|
||||
### General Settings
|
||||
|
||||
During install, I didn’t set a password for truenas_admin. The login page forced me to pick one:
|
||||

|
||||

|
||||
|
||||
Once the password is updated, I land on the dashboard. The UI feels great at first glance:
|
||||

|
||||

|
||||
|
||||
I quickly explore the interface, the first thing I do is changing the hostname to `granite` and check the box below et it inherit domain from DHCP:
|
||||

|
||||

|
||||
|
||||
In the `General Settings`, I change the `Localization` settings. I set the Console Keyboard Map to `French (AZERTY)` and the Timezone to `Europe/Paris`.
|
||||
|
||||
I create a new user `vez`, with `Full Admin` role within TrueNAS. I allow SSH for key‑based auth only, no passwords:
|
||||

|
||||

|
||||
|
||||
Finally I remove the admin role from `truenas_admin` and lock the account.
|
||||
|
||||
@@ -117,16 +117,16 @@ Finally I remove the admin role from `truenas_admin` and lock the account.
|
||||
In TrueNAS, a pool is a storage collection created by combining multiple disks into a unified ZFS‑managed space.
|
||||
|
||||
In the `Storage` page, I can find my `Disks`, where I can confirm TrueNAS can see my couple of NVMe drives:
|
||||

|
||||

|
||||
|
||||
Back in the `Storage Dashboard`, I click the `Create Pool` button. I name the pool `storage` because I'm really inspired to give it a name:
|
||||

|
||||

|
||||
|
||||
Then I select the `Mirror` layout:
|
||||

|
||||

|
||||
|
||||
I explore quickly the optional configurations, but the defaults are fine to me: autotrim, compression, no dedup, etc. At the end, before creating the pool, there is a `Review` section:
|
||||

|
||||

|
||||
|
||||
After hitting `Create Pool`, I'm warned that everything on the disks will be wiped, which I confirm. Finally the pool is created.
|
||||
|
||||
@@ -137,10 +137,10 @@ A dataset is a filesystem inside a pool. It can contains files, directories and
|
||||
#### SMB share
|
||||
|
||||
Let's now create my first dataset `files` to share files over the network for my Windows clients, like ISOs, etc:
|
||||

|
||||

|
||||
|
||||
When creating SMB datasets in SCALE, set Share Type to SMB so the right ACL/xattr defaults apply. TrueNAS then prompts me to start/enable the SMB service:
|
||||

|
||||

|
||||
|
||||
From my Windows Laptop, I try to access my new share `\\granite.mgmt.vezpi.com\files`. As expected I'm prompt to give credentials.
|
||||
|
||||
@@ -155,7 +155,7 @@ I create another dataset: `media`, and a child `photos`. I create a NFS share fr
|
||||
On my current NFS server, the files for the photos are owned by `root` (managed by *Immich*). Later I'll see how I can migrate towards a root-less version.
|
||||
|
||||
⚠️ For now I set, in `Advanced Options`, the `Maproot User` and `Maproot Group` to `root`. This is equivalent to the NFS attribute `no_squash_root`, the local `root` of the client stays `root` on the server, don't do that:
|
||||

|
||||

|
||||
|
||||
✅ I mount the NFS share on a client, this works fine.
|
||||
|
||||
@@ -175,14 +175,14 @@ I mentioned VM capabilities in my requirements. I won't cover that is this post,
|
||||
### Data protection
|
||||
|
||||
Now time to enable some data protection features:
|
||||

|
||||

|
||||
|
||||
I want to create automatic snapshots for some of my datasets, those I care the most: my cloud files and photos.
|
||||
|
||||
Let's create snapshot tasks. I click on the `Add` button next to `Periodic Snapshot Tasks`:
|
||||
- cloud: daily snapshots, keep for 2 months
|
||||
- photos: daily snapshots, keep for 7 days
|
||||

|
||||

|
||||
|
||||
I could also set up a `Cloud Sync Task`, but Duplicati already handles offsite backups.
|
||||
|
||||
@@ -200,12 +200,12 @@ sudo rsync -a --info=progress2 /data/photo/ /new_photos
|
||||
```
|
||||
|
||||
At the end, I could decommission my old NFS server on the LXC. The dataset layout after migration looks like this:
|
||||

|
||||

|
||||
|
||||
### Android application
|
||||
|
||||
Out of curiosity, I've checked on the Google Play store for an app to manage a TrueNAS instance. I've found [Nasdeck](https://play.google.com/store/apps/details?id=com.strtechllc.nasdeck&hl=fr&pli=1), which is quite nice. Here some screenshots:
|
||||

|
||||

|
||||
|
||||
---
|
||||
## Conclusion
|
||||
298
content/post/19-migrate-passive-opnsense-node-to-truenas.md
Normal file
@@ -0,0 +1,298 @@
|
||||
---
|
||||
slug: migrate-passive-opnsense-node-to-truenas
|
||||
title: Migrate my Passive OPNsense Node to TrueNAS
|
||||
description: I migrated my passive OPNsense HA VM from Proxmox to TrueNAS to keep routing and firewalling available even when my Proxmox cluster is down.
|
||||
date: 2026-03-12
|
||||
draft: true
|
||||
tags:
|
||||
- opnsense
|
||||
- truenas
|
||||
- proxmox
|
||||
categories:
|
||||
- homelab
|
||||
---
|
||||
|
||||
## Intro
|
||||
|
||||
My router is the heart of my homelab. When it’s down, everything is down: internet, DNS, VLAN firewall, reverse proxy… the whole stack.
|
||||
|
||||
I’m running an [[OPNsense]] HA cluster made of **two virtual machines** inside my [[Proxmox]] VE cluster. It works great… except for one annoying edge case: when the Proxmox cluster is down (rare, but it happens), I suddenly have **no router left**.
|
||||
|
||||
Recently I installed a [[TrueNAS]] server ([[Build my NAS with TrueNAS]]), and TrueNAS can host virtual machines. So I decided to move **only the passive OPNsense node** to TrueNAS, so that if Proxmox goes dark, I still have a node alive that can take over and keep the network running.
|
||||
|
||||
The objective of this post is simple: explain what I migrated, why I did it, and what configuration choices made it work reliably.
|
||||
|
||||
---
|
||||
|
||||
## The Plan: Split the HA Pair Across Two Hypervisors
|
||||
|
||||
The goal was:
|
||||
|
||||
- Keep the **active** OPNsense node running on Proxmox VE (where it already lives).
|
||||
- Migrate the **passive** node to TrueNAS.
|
||||
- Validate that the HA cluster still behaves properly (CARP VIPs, sync, services, failover).
|
||||
|
||||
This way, a Proxmox outage no longer means “no routing at all”.
|
||||
|
||||
---
|
||||
|
||||
## What I Used
|
||||
|
||||
Quick overview of the pieces involved:
|
||||
|
||||
- **OPNsense**: https://opnsense.org/
|
||||
- **Proxmox VE** (current home of both OPNsense VMs): https://www.proxmox.com/en/proxmox-virtual-environment/overview
|
||||
- **TrueNAS** (new home of the passive node, and storage to transfer the VM disk): https://www.truenas.com/
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Make OPNsense Lighter (RAM Reduction)
|
||||
|
||||
TrueNAS on my side doesn’t have “infinite RAM”, so the first step was to reduce memory usage to something more reasonable.
|
||||
|
||||
I reduced the memory allocation of both OPNsense nodes in Proxmox:
|
||||
|
||||
- Shutdown passive node `cerbere-head2`
|
||||
- Reduce RAM, restart, verify HA
|
||||
- Swap services to the passive temporarily and test networking
|
||||
- Shutdown active node `cerbere-head1`
|
||||
- Reduce RAM, restart, verify HA again
|
||||
|
||||
This kept the cluster healthy while ensuring the VM would fit comfortably on the NAS.
|
||||
|
||||
(Details: [[Reduce the memory allocation of OPNsense nodes]])
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Prepare Networking on TrueNAS (Trunk + VLAN Strategy)
|
||||
|
||||
To host an OPNsense VM properly, TrueNAS must be able to present the right networks to the VM (Mgmt, VLANs, etc.). In my case, I needed a trunk configuration.
|
||||
|
||||
In TrueNAS, I went to `System` > `Network` and created VLAN interfaces (example with VLAN 13):
|
||||
|
||||

|
||||
|
||||
TrueNAS is nice here: changes aren’t applied blindly. You can **test** them and you get a rollback window, which is exactly what you want when you’re touching the network config remotely:
|
||||
|
||||

|
||||
|
||||
### Management bridge
|
||||
|
||||
I created a bridge `br1` for the management interface, shared between:
|
||||
|
||||
- TrueNAS itself
|
||||
- the future OPNsense VM
|
||||
|
||||
And moved the IP configuration to the bridge:
|
||||
|
||||

|
||||
|
||||
Final view before apply:
|
||||
|
||||

|
||||
|
||||
### Static IP vs DHCP (and why I stayed static)
|
||||
|
||||
I initially tried switching the management bridge to DHCP by updating the MAC address in OPNsense (Dnsmasq override):
|
||||
|
||||

|
||||
|
||||
Then I attempted to flip TrueNAS from static to DHCP:
|
||||
|
||||

|
||||
|
||||
But DHCP didn’t behave as I expected: it kept receiving random IPs from the pool. I suspected existing leases played a role. I even tried manually editing leases and restarting the service, but after another change, it still ended up with a random address again.
|
||||
|
||||
In the end, I gave up and kept **a static IP** for TrueNAS. It’s boring, but it’s predictable.
|
||||
|
||||
### The key decision: bridge VLANs (not just VLAN interfaces)
|
||||
|
||||
This became important later: I originally planned to attach VLAN interfaces directly to the OPNsense VM, but it didn’t behave well.
|
||||
|
||||
So I created **one bridge per VLAN** (ex: `br13` with `vlan13` as the only member), and used those bridges for the VM NICs:
|
||||
|
||||

|
||||
|
||||
That ended up being the difference between “split-brain chaos” and “stable HA”.
|
||||
|
||||
(Full notes: [[Configure the trunk in TrueNAS]])
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Move the VM Disk From Proxmox to TrueNAS
|
||||
|
||||
To migrate the VM cleanly, I exported the Proxmox disk to TrueNAS.
|
||||
|
||||
### Create a dataset and export it via NFS
|
||||
|
||||
I created a dataset (initially called `disk`) and exported it with NFS, restricting access to my three Proxmox nodes (by IP):
|
||||
|
||||
- 192.168.88.21
|
||||
- 192.168.88.22
|
||||
- 192.168.88.23
|
||||
|
||||
(Notes: [[Create a new dataset in TrueNAS to export Proxmox VM disk]])
|
||||
|
||||
### Export the passive OPNsense disk
|
||||
|
||||
On the Proxmox node hosting the passive VM (`cerbere-head2`), I mounted the NFS share:
|
||||
|
||||
```bash
|
||||
mount granite.mgmt.vezpi.com:/mnt/storage/disk /mnt
|
||||
```
|
||||
|
||||
Then I shut down the VM from Proxmox (HA enabled, so I didn’t do it from inside OPNsense), and converted/exported the main disk (not the EFI disk) from Ceph RBD to a qcow2 file:
|
||||
|
||||
```bash
|
||||
qemu-img convert -f raw -O qcow2 -p \
|
||||
rbd:ceph-workload/vm-123-disk-1 \
|
||||
/mnt/cerbere-head2.qcow2
|
||||
```
|
||||
|
||||
The conversion took around a minute for a 20GB disk.
|
||||
|
||||
(Notes: [[Export the passive OPNsense VM disk from Proxmox]])
|
||||
|
||||
### Dataset reorg (cleaner layout)
|
||||
|
||||
I reorganized datasets on TrueNAS side to something more VM-oriented:
|
||||
|
||||
- created `storage/vm`
|
||||
- renamed `storage/disk` to `storage/vm/files`
|
||||
|
||||
Commands used:
|
||||
|
||||
```bash
|
||||
zfs list
|
||||
sudo zfs create storage/vm
|
||||
sudo zfs rename storage/disk storage/vm/files
|
||||
```
|
||||
|
||||
(Notes: [[Reorganize the dataset in TrueNAS]])
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Create the OPNsense VM on TrueNAS (Import Disk + Rebuild NICs)
|
||||
|
||||
Now the fun part: recreating the VM on TrueNAS with the same “spirit” as the Proxmox VM.
|
||||
|
||||
From `Virtual Machines`:
|
||||
|
||||

|
||||
|
||||
### VM settings I used
|
||||
|
||||
I created a new VM with:
|
||||
|
||||
**Operating System**
|
||||
- Guest: FreeBSD
|
||||
- Name: `cerberehead2` (TrueNAS doesn’t like dashes)
|
||||
- Boot: UEFI
|
||||
- Secure Boot: Disabled
|
||||
- TPM: Disabled
|
||||
- Start on Boot: Enabled
|
||||
- VNC: Disabled
|
||||
|
||||
**CPU & Memory**
|
||||
- Virtual CPUs: 1
|
||||
- Cores: 2
|
||||
- Threads: 1
|
||||
- CPU Mode: Custom
|
||||
- CPU Model: `qemu64`
|
||||
- Memory: 2 GiB
|
||||
|
||||
**Disk**
|
||||
- Import image enabled
|
||||
- Source: `/mnt/storage/vm/files/cerbere-head2.qcow2`
|
||||
- Disk Type: VirtIO
|
||||
- Location: `storage/vm`
|
||||
- Size: 20 GiB
|
||||
|
||||
**Network**
|
||||
- Adapter: VirtIO
|
||||
- Attached to `br1` (Mgmt)
|
||||
- MAC: kept the generated one here
|
||||
|
||||
Summary screen:
|
||||
|
||||

|
||||
|
||||
After saving, TrueNAS converted the imported image into a Zvol:
|
||||
|
||||

|
||||
|
||||
### Adding the additional NICs
|
||||
|
||||
After the VM was created, I added the additional NICs in the VM device list:
|
||||
|
||||

|
||||
|
||||
At first, I attached VLAN interfaces directly and started the VM… and instantly broke my network (great success).
|
||||
|
||||
The VM itself booted fine though, and seeing OPNsense come up cleanly on TrueNAS was a good sign:
|
||||
|
||||

|
||||
|
||||
But HA-wise, it was a mess: split-brain symptoms, with the TrueNAS-hosted node thinking it was MASTER on almost everything except Mgmt.
|
||||
|
||||
The fix was the VLAN bridging approach mentioned earlier: once I switched the VM NICs to attach to **bridges (`br13`, `br20`, etc.) instead of VLAN interfaces**, the cluster came back to a healthy state.
|
||||
|
||||
Second try: stable. ✅
|
||||
|
||||
(Notes: [[Create the OPNsense VM in TrueNAS]])
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Validate HA: CARP, Sync, Services, Switchover and Failover
|
||||
|
||||
Once everything was in place, I validated the new setup with a proper checklist. I wanted to be sure the cluster worked exactly as before.
|
||||
|
||||
### Basic checks
|
||||
|
||||
- Ping each interface as relevant (Mgmt/User/IoT/pfSync/DMZ/Lab)
|
||||
- SSH access
|
||||
- Web UI access
|
||||
- CARP VIP status must be `BACKUP` on the passive node
|
||||
- HA status (active must be able to log into passive)
|
||||
- Services state + “Synchronize and reconfigure all”
|
||||
- Check updates availability (`System` > `Firmware` > `Check for updates`)
|
||||
|
||||
### Switchover test (graceful)
|
||||
|
||||
I started:
|
||||
- a SSH session to DockerVM (to check state keeping)
|
||||
- a ping to an IoT host from a laptop
|
||||
|
||||
Then tested:
|
||||
- CARP role switch
|
||||
- inter-VLAN routing
|
||||
- WAN ping to `8.8.8.8`
|
||||
- firewall state (SSH session stays alive)
|
||||
- DNS resolution (external + internal)
|
||||
- Caddy reverse proxy + layer4 proxy checks
|
||||
- Wireguard access from outside
|
||||
- mDNS discovery (printer visibility)
|
||||
|
||||
✅ Switchover successful.
|
||||
|
||||
### Failover test (hard)
|
||||
|
||||
Then I forced power off of the active node and repeated the same functional tests.
|
||||
|
||||
✅ Failover successful.
|
||||
|
||||
At the end: restarted the active VM, and the HA pair returned to normal operation.
|
||||
|
||||
One note: QEMU Guest Agent doesn’t bring value here because TrueNAS doesn’t implement it as a hypervisor (I still left it installed since it’s harmless).
|
||||
|
||||
(Full checklist and validation steps: [[Validate the new OPNsense VM and cluster state]])
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This project solved a real weakness in my homelab: my “highly available” router cluster was still depending on a single platform (Proxmox). By moving only the **passive OPNsense node** to **TrueNAS**, I now have a router that can survive a full Proxmox outage.
|
||||
|
||||
The biggest takeaway for me was networking on TrueNAS: attaching VLAN interfaces directly to the VM was not reliable in my setup, but bridging each VLAN (`br13`, `br20`, etc.) made the HA behavior stable and predictable.
|
||||
|
||||
Next step is to monitor the cluster for a few days before doing the cleanup of the migration on the Proxmox side.
|
||||
|
Before Width: | Height: | Size: 62 KiB |
@@ -1,283 +0,0 @@
|
||||
---
|
||||
slug: migrate-passive-opnsense-node-to-truenas
|
||||
title: Migrer mon nœud OPNsense HA passif vers TrueNAS
|
||||
description: J’ai migré ma VM OPNsense HA passive de Proxmox vers TrueNAS pour garder le routage et le firewalling disponibles même lorsque mon cluster Proxmox est arrêté.
|
||||
date: 2026-05-24
|
||||
draft: false
|
||||
tags:
|
||||
- opnsense
|
||||
- truenas
|
||||
- proxmox
|
||||
- high-availability
|
||||
categories:
|
||||
- homelab
|
||||
---
|
||||
## Intro
|
||||
|
||||
Mon réseau homelab est géré par un cluster OPNsense composé de deux nœuds VM. Ces deux VM fonctionnent dans mon cluster Proxmox VE. Vous pouvez trouver les détails dans cet [article]({{< ref "post/15-migration-opnsense-proxmox-highly-available" >}}).
|
||||
|
||||
Cette configuration fonctionne bien la plupart du temps. Le problème concerne plutôt les rares cas où le cluster Proxmox lui-même est arrêté. Quand cela arrive, les deux nœuds OPNsense sont indisponibles en même temps, ce qui signifie qu’il ne me reste aucun routeur, donc aucun réseau du tout.
|
||||
|
||||
Récemment, j’ai installé un serveur TrueNAS dans le lab, que j'ai documenté dans ce [post]({{< ref "post/18-create-nas-server-with-truenas" >}}). Il est principalement là pour agir comme NAS, mais il pourrait aussi héberger des machines virtuelles. Cela me donne une bonne opportunité d’améliorer la résilience de mon réseau sans changer toute la conception.
|
||||
|
||||
💡 L’idée est simple : garder le nœud OPNsense actif sur Proxmox, mais déplacer le nœud passif vers TrueNAS.
|
||||
|
||||
De cette façon, si le cluster Proxmox tombe, le nœud OPNsense passif peut toujours prendre le relais et garder le réseau fonctionnel.
|
||||
|
||||
---
|
||||
## Préparer les nœuds OPNsense
|
||||
|
||||
Avant de déplacer quoi que ce soit, je veux m’assurer que les VM OPNsense peuvent fonctionner avec moins de mémoire.
|
||||
|
||||
Le serveur TrueNAS n’a pas autant de RAM disponible que le cluster Proxmox, donc la première étape est de réduire l’allocation mémoire des nœuds OPNsense au minimum.
|
||||
|
||||
Je commence avec le nœud passif, `cerbere-head2` :
|
||||
|
||||
- Éteindre le nœud passif
|
||||
- Réduire son allocation mémoire de 4 à 2GB
|
||||
- Le redémarrer
|
||||
- Vérifier la santé du cluster
|
||||
- Basculer le service vers le nœud passif
|
||||
- Exécuter des vérifications réseau
|
||||
|
||||
Ensuite, je répète la même opération sur le nœud actif, `cerbere-head1`.
|
||||
|
||||
Le faire un nœud à la fois me permet de garder le cluster HA en bonne santé tout en validant que l’allocation mémoire réduite est toujours suffisante pour ma configuration.
|
||||
|
||||
---
|
||||
## Préparer le réseau TrueNAS
|
||||
|
||||
La partie la plus importante de cette migration n’est pas l’export du disque ni la création de la VM. C’est le réseau.
|
||||
|
||||
Une VM OPNsense n’est pas un simple serveur avec une seule interface de management. Elle a besoin d’accéder à plusieurs réseaux, incluant le management, le WAN, les réseaux utilisateurs, l’IoT, pfSync, la DMZ et les réseaux lab.
|
||||
|
||||
Du côté TrueNAS, je commence depuis `System` > `Network` et j’ajoute des interfaces VLAN.
|
||||
|
||||
La première est le VLAN utilisateur :
|
||||
|
||||
- Type : `VLAN`
|
||||
- Nom : `vlan13`
|
||||
- Description : `User`
|
||||
- Interface parente : `enp1s0`
|
||||
- Tag VLAN : `13`
|
||||
|
||||

|
||||
|
||||
J’ajoute ensuite les autres VLANs de la même manière.
|
||||
|
||||
TrueNAS n’applique pas les changements réseau directement. Il donne l’option de tester les changements d’abord, avec une courte fenêtre de validation. Si la configuration n’est pas confirmée à temps, il revient automatiquement en arrière.
|
||||
|
||||
C’est vraiment pratique lorsqu’on change la configuration réseau de la machine à laquelle on est actuellement connecté.
|
||||
|
||||

|
||||
|
||||
Pour le réseau de management, j’ai créé un bridge appelé `br1`.
|
||||
|
||||
Ce bridge porte la configuration IP de management de TrueNAS à la place de l’interface physique `enp1s0`, parce qu’elle doit aussi être partagée avec la VM OPNsense.
|
||||
|
||||

|
||||
|
||||
Après cela, je retire la configuration IP de l’interface physique et je la garde sur le bridge.
|
||||
|
||||

|
||||
|
||||
J’ai initialement essayé d’utiliser DHCP pour le bridge de management après avoir mis à jour l’adresse MAC dans Dnsmasq, mais j’ai finalement décidé de garder une adresse IP statique pour TrueNAS. Après certains changements réseau, DHCP a donné une autre adresse du pool, donc l’adressage statique était l’option la plus sûre et la plus simple pour ce serveur.
|
||||
|
||||
Pour la VM OPNsense, je crée un bridge pour chaque VLAN. Par exemple, `br13` utilise `vlan13`, je déplace aussi la description, comme `User`, de l’interface VLAN vers le bridge pour plus de clarté.
|
||||
|
||||
La configuration réseau finale de TrueNAS :
|
||||
|
||||

|
||||
|
||||
---
|
||||
## Créer un dataset d’export temporaire
|
||||
|
||||
Pour déplacer le disque de la VM OPNsense passive de Proxmox vers TrueNAS, j’ai d’abord besoin d’un endroit pour exporter l’image disque.
|
||||
|
||||
Dans TrueNAS, je crée un dataset nommé `storage/vm/disk`, puis je crée un partage NFS à partir de celui-ci.
|
||||
|
||||
Dans les options avancées du partage NFS, j’ai configuré :
|
||||
|
||||
- Utilisateur Maproot : `root`
|
||||
- Hôtes autorisés :
|
||||
- `192.168.88.21`
|
||||
- `192.168.88.22`
|
||||
- `192.168.88.23`
|
||||
|
||||
Ce sont les nœuds Proxmox VE autorisés à monter le partage.
|
||||
|
||||
Je ne crée pas manuellement de zvol à ce moment-là. Le processus de création de VM dans TrueNAS gère l’import et la conversion du disque.
|
||||
|
||||
---
|
||||
## Exporter le disque de la VM depuis Proxmox
|
||||
|
||||
Depuis l’interface web Proxmox VE, je localise le nœud qui héberge la VM OPNsense passive `cerbere-head2`, elle fonctionne sur `Zenith`.
|
||||
|
||||
Je me connecte à ce nœud Proxmox en SSH et je monte le partage NFS depuis TrueNAS :
|
||||
|
||||
```bash
|
||||
mount granite.mgmt.vezpi.com:/mnt/storage/vm/disk /mnt
|
||||
```
|
||||
|
||||
Ensuite, j’éteins la VM depuis l’interface Proxmox VE. Je ne l’éteins pas depuis l’intérieur d’OPNsense parce que la VM a la HA activée.
|
||||
|
||||
Une fois la VM arrêtée, j’exporte le disque principal en qcow2. Je n’exporte pas le disque EFI.
|
||||
|
||||
```bash
|
||||
qemu-img convert -f raw -O qcow2 -p \
|
||||
rbd:ceph-workload/vm-123-disk-1 \
|
||||
/mnt/cerbere-head2.qcow2
|
||||
```
|
||||
|
||||
La conversion a pris environ une minute pour un disque de 20 GB.
|
||||
|
||||
À ce stade, le disque OPNsense passif est disponible sur TrueNAS et prêt à être importé dans une nouvelle VM.
|
||||
|
||||
---
|
||||
## Recréer la VM OPNsense dans TrueNAS
|
||||
|
||||
L’étape suivante consiste à recréer la VM OPNsense passive dans TrueNAS avec des paramètres correspondant aussi étroitement que possible à la VM d’origine.
|
||||
|
||||
Depuis l’interface web TrueNAS, je vais dans la section `Virtual Machines`.
|
||||
|
||||

|
||||
|
||||
Je crée une nouvelle VM avec ces paramètres.
|
||||
|
||||
Pour le système d’exploitation :
|
||||
|
||||
- Système d’exploitation invité : `FreeBSD`
|
||||
- Nom : `cerberehead2`
|
||||
- Horloge système : `Local`
|
||||
- Méthode de démarrage : `UEFI`
|
||||
- Activer Secure Boot : désactivé
|
||||
- Activer Trusted Platform Module : désactivé
|
||||
- Timeout d’arrêt : `90`
|
||||
- Démarrer au boot : activé
|
||||
- Activer l’affichage VNC : désactivé
|
||||
|
||||
Le nom de la VM n’utilise pas de tirets parce que TrueNAS ne les autorise pas ici.
|
||||
|
||||
Pour le CPU et la mémoire :
|
||||
|
||||
- CPU virtuels : `1`
|
||||
- Cœurs : `2`
|
||||
- Threads : `1`
|
||||
- Mode CPU : `Custom`
|
||||
- Modèle CPU : `qemu64`
|
||||
- Taille mémoire : `2 GiB`
|
||||
|
||||
Pour le disque :
|
||||
|
||||
- Créer une nouvelle image disque
|
||||
- Importer une image : activé
|
||||
- Source de l’image : `/mnt/storage/vm/files/cerbere-head2.qcow2`
|
||||
- Type de disque : `VirtIO`
|
||||
- Emplacement de stockage : `storage/vm`
|
||||
- Taille : `20 GiB`
|
||||
|
||||
Pour la première interface réseau :
|
||||
|
||||
- Type d’adaptateur : `VirtIO`
|
||||
- Adresse MAC : garder celle proposée
|
||||
- Attacher la NIC : `br1: Mgmt`
|
||||
|
||||
Je passe le média d’installation et la configuration GPU, puis je confirme le résumé.
|
||||
|
||||

|
||||
|
||||
Après confirmation, TrueNAS convertit l’image qcow2 importée en zvol.
|
||||
|
||||

|
||||
|
||||
Une fois la VM créée, j’ouvre les détails de la VM et j’ajoute les NICs restantes.
|
||||
|
||||

|
||||
|
||||
Pour chaque NIC supplémentaire, j’ai utilisé VirtIO comme type d’adaptateur et je l’ai attachée au bridge correspondant.
|
||||
|
||||
Pour la NIC WAN, je copie l’ancienne adresse MAC parce que j’utilise une astuce avec une seule adresse IP WAN. J’incrémente aussi le chiffre dans l’ordre des périphériques pour garder le même que dans Proxmox.
|
||||
|
||||

|
||||
|
||||
🎉 Enfin, je peux démarrer la VM OPNsense dans TrueNAS.
|
||||
|
||||

|
||||
|
||||
---
|
||||
## Valider le cluster HA
|
||||
|
||||
Une fois que le nœud passif fonctionne sur TrueNAS, je dois valider que le cluster HA OPNsense se comporte toujours correctement.
|
||||
|
||||
Je commence par des vérifications de base sur le nœud passif :
|
||||
|
||||
- Ping de l’interface de management depuis le bastion : `192.168.88.3`
|
||||
- Ping de l’interface utilisateur depuis un laptop : `192.168.13.3`
|
||||
- Ping de l’interface IoT : `192.168.37.3`
|
||||
- Ping pfSync depuis l’autre nœud : `192.168.44.2`
|
||||
- Ping de l’interface DMZ : `192.168.55.3`
|
||||
- Ping de l’interface Lab depuis DockerVM : `192.168.66.3`
|
||||
|
||||
Je vérifie aussi que le nœud était accessible en SSH depuis mon laptop en utilisant `192.168.13.3`, et que l’interface web était joignable à :
|
||||
|
||||
```text
|
||||
https://192.168.13.3:4443
|
||||
```
|
||||
|
||||
Ensuite, je valide l’état HA d’OPNsense :
|
||||
|
||||
- Le statut des VIP CARP doit être `BACKUP` sur toutes les VIP
|
||||
- La page de statut HA doit montrer que le nœud actif peut se connecter au nœud passif
|
||||
- Les services doivent fonctionner comme attendu
|
||||
- La synchronisation des services HA doit fonctionner
|
||||
- Les vérifications de mise à jour du firmware doivent être accessibles
|
||||
|
||||
Depuis le nœud actif, j’utilise la page de statut HA et je force une synchronisation complète avec `Synchronize and reconfigure all`.
|
||||
|
||||
---
|
||||
## Tests de bascule contrôlée
|
||||
|
||||
Avant de tester le failover, je démarre une session SSH vers `dockerVM` pour confirmer que les états du firewall sont préservés entre les nœuds. Je démarre aussi un ping depuis un laptop vers `192.168.37.120`.
|
||||
|
||||
Pour le test de bascule contrôlée, j’active proprement le mode maintenance sur le nœud master.
|
||||
|
||||
Le nouveau nœud passif devient `MASTER`, et je valide les services importants :
|
||||
|
||||
- Routage VLAN supplémentaire avec un ping vers `192.168.37.120`
|
||||
- Accès WAN avec un ping vers `8.8.8.8`
|
||||
- États du firewall en gardant la session SSH active
|
||||
- Résolution DNS externe avec `host redhat.com`
|
||||
- Résolution DNS interne avec `host SLZB-06M.mgmt.vezpi.com`
|
||||
- Accès à une page internet aléatoire
|
||||
- Reverse proxy Caddy
|
||||
- Proxy layer4 Caddy
|
||||
- Accès Wireguard depuis l’extérieur
|
||||
- mDNS en vérifiant si l’imprimante est apparue
|
||||
|
||||
✅ La bascule contrôlée est réussie.
|
||||
|
||||
---
|
||||
## Tests de failover
|
||||
|
||||
Après le test de bascule contrôlée propre, je teste un scénario de failover plus direct en forçant un poweroff du nœud actif.
|
||||
|
||||
J’ai répété la même checklist de validation.
|
||||
|
||||
✅ Le failover est réussi.
|
||||
|
||||
Enfin, je redémarre la VM OPNsense active.
|
||||
|
||||
🎯 À ce stade, le cluster HA OPNsense est de nouveau opérationnel, avec le nœud passif qui fonctionne maintenant sur TrueNAS au lieu de Proxmox.
|
||||
|
||||
---
|
||||
## Conclusion
|
||||
|
||||
Cette migration est une petite mais importante amélioration pour mon homelab.
|
||||
|
||||
Avant, les deux nœuds OPNsense dépendaient du cluster Proxmox VE. Si le cluster était arrêté, toute ma couche de routage réseau était arrêtée avec lui.
|
||||
|
||||
Maintenant, le nœud actif fonctionne toujours sur Proxmox, mais le nœud passif fonctionne sur TrueNAS. Cela me donne une meilleure séparation entre le cluster de virtualisation et la couche de failover réseau.
|
||||
|
||||
Petit disclaimer, bien que TrueNAS offre des fonctionnalités de virtualisation, il n’est pas comparable à Proxmox VE en termes de clustering et de capacités de gestion d’infrastructure.
|
||||
|
||||
Une note à propos de QEMU Guest Agent, la VM OPNsense avait déjà QEMU Guest Agent installé avant l’export. Dans cette configuration, il ne semble pas utile parce que TrueNAS ne l’a pas implémenté comme fonctionnalité d’hyperviseur. Je l’ai gardé installé quand même, parce qu’il est inoffensif.
|
||||
@@ -1,283 +0,0 @@
|
||||
---
|
||||
slug: migrate-passive-opnsense-node-to-truenas
|
||||
title: Migrate my Passive OPNsense HA Node to TrueNAS
|
||||
description: I migrated my passive OPNsense HA VM from Proxmox to TrueNAS to keep routing and firewalling available even when my Proxmox cluster is down.
|
||||
date: 2026-05-24
|
||||
draft: false
|
||||
tags:
|
||||
- opnsense
|
||||
- truenas
|
||||
- proxmox
|
||||
- high-availability
|
||||
categories:
|
||||
- homelab
|
||||
---
|
||||
## Intro
|
||||
|
||||
My homelab network is handled by an OPNsense cluster composed of two VM nodes. Both of these VMs are running inside my Proxmox VE cluster. You can find details in this [article]({{< ref "post/15-migration-opnsense-proxmox-highly-available" >}}).
|
||||
|
||||
This setup works fine most of the time. The issue is more about the rare cases where the Proxmox cluster itself is down. When that happens, both OPNsense nodes are unavailable at the same time, which means I do not have any router left, so no network at all.
|
||||
|
||||
Recently, I installed a TrueNAS server in the labwhich I document in that [post]({{< ref "post/18-create-nas-server-with-truenas" >}}). It is mainly here to act as a NAS, but it could also host virtual machines. That give me a good opportunity to improve the resilience of my network without changing the whole design.
|
||||
|
||||
💡 The idea is simple: keep the active OPNsense node on Proxmox, but move the passive node to TrueNAS.
|
||||
|
||||
This way, if the Proxmox cluster goes down, the passive OPNsense node can still take over and keep the network alive.
|
||||
|
||||
---
|
||||
## Prepare the OPNsense Nodes
|
||||
|
||||
Before moving anything, I want to make sure the OPNsense VMs could run with less memory.
|
||||
|
||||
The TrueNAS server does not have as much RAM available as the Proxmox cluster, so the first step is to reduce the memory allocation of the OPNsense nodes to the minimum.
|
||||
|
||||
I start with the passive node, `cerbere-head2`:
|
||||
|
||||
- Shut down the passive node
|
||||
- Reduce its memory allocation from 4 to 2GB
|
||||
- Restart it
|
||||
- Verify the cluster health
|
||||
- Swap the service to the passive node
|
||||
- Run network checks
|
||||
|
||||
Then I repeat the same operation on the active node, `cerbere-head1`.
|
||||
|
||||
Doing it one node at a time allow me to keep the HA cluster healthy while validating that the reduced memory allocation is still enough for my setup.
|
||||
|
||||
---
|
||||
## Prepare the TrueNAS Network
|
||||
|
||||
The most important part of this migration is not the disk export or the VM creation. It is the network.
|
||||
|
||||
An OPNsense VM is not a simple server with one management interface. It needs access to several networks, including management, WAN, user networks, IoT, pfSync, DMZ and lab networks.
|
||||
|
||||
On the TrueNAS side, I start from `System` > `Network` and add VLAN interfaces.
|
||||
|
||||
The first one is the User VLAN:
|
||||
|
||||
- Type: `VLAN`
|
||||
- Name: `vlan13`
|
||||
- Description: `User`
|
||||
- Parent interface: `enp1s0`
|
||||
- VLAN tag: `13`
|
||||
|
||||

|
||||
|
||||
I then add the other VLANs in the same way.
|
||||
|
||||
TrueNAS does not apply network changes directly. It gives the option to test the changes first, with a short validation window. If the configuration is not confirmed in time, it rolls back automatically.
|
||||
|
||||
This is really convenient when changing the network configuration of the machine you are currently connected to.
|
||||
|
||||

|
||||
|
||||
For the management network, I created a bridge called `br1`.
|
||||
|
||||
This bridge holds the TrueNAS management IP configuration instead of the physical interface `enp1s0`, because it also needs to be shared with the OPNsense VM.
|
||||
|
||||

|
||||
|
||||
After that, I remove the IP configuration from the physical interface and keep it on the bridge.
|
||||
|
||||

|
||||
|
||||
I initially tried to use DHCP for the management bridge after updating the MAC address in Dnsmasq, but I finally decided to keep a static IP address for TrueNAS. After some network changes, DHCP gave another address from the pool, so static addressing was the safer and simpler option for this server.
|
||||
|
||||
For the OPNsense VM, I create a bridge for each VLAN. For example, `br13` uses `vlan13`, I also move the description, like `User`, from the VLAN interface to the bridge for clarity.
|
||||
|
||||
The final TrueNAS network configuration:
|
||||
|
||||

|
||||
|
||||
---
|
||||
## Create a Temporary Export Dataset
|
||||
|
||||
To move the passive OPNsense VM disk from Proxmox to TrueNAS, I first need a place to export the disk image.
|
||||
|
||||
In TrueNAS, I create a dataset named `storage/vm/disk`, then create a NFS share from it.
|
||||
|
||||
In the advanced options of the NFS share, I configured:
|
||||
|
||||
- Maproot user: `root`
|
||||
- Authorized hosts:
|
||||
- `192.168.88.21`
|
||||
- `192.168.88.22`
|
||||
- `192.168.88.23`
|
||||
|
||||
These are the Proxmox VE nodes allowed to mount the share.
|
||||
|
||||
I don't manually create a zvol at that point. The VM creation process in TrueNAS handle the disk import and conversion.
|
||||
|
||||
---
|
||||
## Export the VM Disk from Proxmox
|
||||
|
||||
From the Proxmox VE web interface, I locate the node hosting the passive OPNsense VM `cerbere-head2`, it is running on `Zenith`.
|
||||
|
||||
I log into that Proxmox node over SSH and mount the NFS share from TrueNAS:
|
||||
|
||||
```bash
|
||||
mount granite.mgmt.vezpi.com:/mnt/storage/vm/disk /mnt
|
||||
```
|
||||
|
||||
Then I shut down the VM from the Proxmox VE interface. I don't shut it down from inside OPNsense because the VM has HA enabled.
|
||||
|
||||
Once the VM is stopped, I export the main disk to qcow2. I don't export the EFI disk.
|
||||
|
||||
```bash
|
||||
qemu-img convert -f raw -O qcow2 -p \
|
||||
rbd:ceph-workload/vm-123-disk-1 \
|
||||
/mnt/cerbere-head2.qcow2
|
||||
```
|
||||
|
||||
The conversion took about one minute for a 20 GB disk.
|
||||
|
||||
At this point, the passive OPNsense disk is available on TrueNAS and ready to be imported into a new VM.
|
||||
|
||||
---
|
||||
## Recreate the OPNsense VM in TrueNAS
|
||||
|
||||
The next step is to recreate the passive OPNsense VM in TrueNAS with parameters matching the original VM as closely as possible.
|
||||
|
||||
From the TrueNAS web interface, I go to the `Virtual Machines` section.
|
||||
|
||||

|
||||
|
||||
I create a new VM with these settings.
|
||||
|
||||
For the operating system:
|
||||
|
||||
- Guest Operating System: `FreeBSD`
|
||||
- Name: `cerberehead2`
|
||||
- System Clock: `Local`
|
||||
- Boot Method: `UEFI`
|
||||
- Enable Secure Boot: disabled
|
||||
- Enable Trusted Platform Module: disabled
|
||||
- Shutdown Timeout: `90`
|
||||
- Start on Boot: enabled
|
||||
- Enable Display VNC: disabled
|
||||
|
||||
The VM name does not use dashes because TrueNAS do not allow them there.
|
||||
|
||||
For CPU and memory:
|
||||
|
||||
- Virtual CPUs: `1`
|
||||
- Cores: `2`
|
||||
- Threads: `1`
|
||||
- CPU Mode: `Custom`
|
||||
- CPU Model: `qemu64`
|
||||
- Memory Size: `2 GiB`
|
||||
|
||||
For the disk:
|
||||
|
||||
- Create new disk image
|
||||
- Import Image: enabled
|
||||
- Image source: `/mnt/storage/vm/files/cerbere-head2.qcow2`
|
||||
- Disk Type: `VirtIO`
|
||||
- Storage Location: `storage/vm`
|
||||
- Size: `20 GiB`
|
||||
|
||||
For the first network interface:
|
||||
|
||||
- Adapter Type: `VirtIO`
|
||||
- MAC Address: keep the proposed one
|
||||
- Attach NIC: `br1: Mgmt`
|
||||
|
||||
I skip installation media and GPU configuration, then confirm the summary.
|
||||
|
||||

|
||||
|
||||
After confirmation, TrueNAS convert the imported qcow2 image into a zvol.
|
||||
|
||||

|
||||
|
||||
Once the VM is created, I open the VM details and add the remaining NICs.
|
||||
|
||||

|
||||
|
||||
For each additional NIC, I used VirtIO as the adapter type and attach it to the corresponding bridge.
|
||||
|
||||
For the WAN NIC, I copy the old MAC address because I use a single WAN IP address trick. I also increment the digit in the Device Order to keep the same as in Proxmox.
|
||||
|
||||

|
||||
|
||||
🎉 Finally I can start the OPNsense VM in TrueNAS.
|
||||
|
||||

|
||||
|
||||
---
|
||||
## Validate the HA cluster
|
||||
|
||||
Once the passive node is running on TrueNAS, I need to validate that the OPNsense HA cluster is still behaving correctly.
|
||||
|
||||
I start with basic checks on the passive node:
|
||||
|
||||
- Management interface ping from the bastion: `192.168.88.3`
|
||||
- User interface ping from a laptop: `192.168.13.3`
|
||||
- IoT interface ping: `192.168.37.3`
|
||||
- pfSync ping from the other node: `192.168.44.2`
|
||||
- DMZ interface ping: `192.168.55.3`
|
||||
- Lab interface ping from DockerVM: `192.168.66.3`
|
||||
|
||||
I also check that the node was accessible over SSH from my laptop using `192.168.13.3`, and that the web interface was reachable at:
|
||||
|
||||
```text
|
||||
https://192.168.13.3:4443
|
||||
```
|
||||
|
||||
Then I validate the OPNsense HA state:
|
||||
|
||||
- CARP VIP status must be `BACKUP` on all VIPs
|
||||
- HA status page must show that the active node can log in to the passive node
|
||||
- Services must be running as expected
|
||||
- HA service synchronization must work
|
||||
- Firmware update checks must be accessible
|
||||
|
||||
From the active node, I use the HA status page and force a full synchronization with `Synchronize and reconfigure all`.
|
||||
|
||||
---
|
||||
## Switchover Tests
|
||||
|
||||
Before testing failover, I start a SSH session to `dockerVM` to confirm that firewall states are preserved across nodes. I also start a ping from a laptop to `192.168.37.120`.
|
||||
|
||||
For the switchover test, I gracefully enable maintenance mode on the master node.
|
||||
|
||||
The new passive node become `MASTER`, and I validate the important services:
|
||||
|
||||
- Extra VLAN routing with ping to `192.168.37.120`
|
||||
- WAN access with ping to `8.8.8.8`
|
||||
- Firewall states by keeping the SSH session alive
|
||||
- External DNS resolution with `host redhat.com`
|
||||
- Internal DNS resolution with `host SLZB-06M.mgmt.vezpi.com`
|
||||
- Access to a random internet page
|
||||
- Caddy reverse proxy
|
||||
- Caddy layer4 proxy
|
||||
- Wireguard access from outside
|
||||
- mDNS by checking if the printer showed up
|
||||
|
||||
✅ The switchover is successful.
|
||||
|
||||
---
|
||||
## Failover Tests
|
||||
|
||||
After the graceful switchover test, I test a more direct failover scenario by forcing a poweroff of the active node.
|
||||
|
||||
I repeated the same validation checklist.
|
||||
|
||||
✅ The failover is successful.
|
||||
|
||||
Finally, I restart the active OPNsense VM.
|
||||
|
||||
🎯 At that point, the OPNsense HA cluster is operational again, with the passive node now running on TrueNAS instead of Proxmox.
|
||||
|
||||
---
|
||||
## Conclusion
|
||||
|
||||
This migration is a small but important improvement for my homelab.
|
||||
|
||||
Before, both OPNsense nodes depended on the Proxmox VE cluster. If the cluster was down, my whole network routing layer was down with it.
|
||||
|
||||
Now, the active node still runs on Proxmox, but the passive node runs on TrueNAS. This gives me a better separation between the virtualization cluster and the network failover layer.
|
||||
|
||||
Little disclaimer, while TrueNAS offers virtualization features, it is not comparable to Proxmox VE in terms of clustering and infrastructure management capabilities.
|
||||
|
||||
A note about QEMU Guest Agent, the OPNsense VM already had the QEMU Guest Agent installed before expert. In this setup, it does not seem useful because TrueNAS does not have it implemented as a hypervisor feature. I kept it installed anyway, because it is harmless.
|
||||
@@ -60,7 +60,7 @@ L'idée est simple :
|
||||
|
||||
De cette façon, je n'ai plus besoin de copier manuellement de fichiers ni de gérer les déploiements. Tout se déroule, de l'écriture de Markdown dans Obsidian au déploiement complet du site web.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
## ⚙️ Implémentation
|
||||
@@ -101,17 +101,17 @@ container:
|
||||
```
|
||||
|
||||
Le runner apparaît dans `Administration Area`, sous `Actions`>`Runners`. Pour obtenir le token d'enrôlement , on clique sur le bouton `Create new Runner`
|
||||

|
||||

|
||||
|
||||
### Étape 3 : Configurer les Gitea Actions pour le dépôt Obsidian
|
||||
|
||||
J'ai d'abord activé les Gitea Actions. Celles-ci sont désactivées par défaut. Cochez la case `Enable Repository Actions` dans les paramètres de ce dépôt.
|
||||
|
||||
J'ai créé un nouveau PAT (Personal Access Token) avec autorisation RW sur les dépôts.
|
||||

|
||||

|
||||
|
||||
J'ai ajouté le token comme secret `REPO_TOKEN` dans le dépôt.
|
||||

|
||||

|
||||
|
||||
|
||||
J'ai dû créer le workflow qui lancera un conteneur et effectuera les opérations suivantes :
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
Obsidian utilise des liens de type wiki pour les images, comme `
|
||||
Obsidian utilise des liens de type wiki pour les images, comme `![[nom_image.png]]`, ce qui n'est pas compatible avec Hugo par défaut. Voici comment j'ai automatisé une solution de contournement dans un workflow Gitea Actions :
|
||||
- Je trouve toutes les références d'images utilisées dans des fichiers `.md`.
|
||||
- Pour chaque image référencée, je mets à jour le lien dans les fichiers `.md` correspondants, comme ``.
|
||||
- Je copie ensuite ces images utilisées dans le répertoire statique du blog en remplaçant les espaces par des underscores.
|
||||
@@ -59,7 +59,7 @@ The idea is simple:
|
||||
|
||||
This way, I never need to manually copy files or trigger deployments. Everything flows from writing markdown in Obsidian to having a fully deployed website.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
## ⚙️ Implementation
|
||||
@@ -100,17 +100,17 @@ container:
|
||||
```
|
||||
|
||||
The runner appears in the `Administration Area`, under `Actions`>`Runners`. To obtain the registration token, click on the `Create new Runner` button
|
||||

|
||||

|
||||
|
||||
### Step 3: Set up Gitea Actions for Obsidian Repository
|
||||
|
||||
First I enabled the Gitea Actions, this is disabled by default, tick the box `Enable Repository Actions` in the settings for that repository
|
||||
|
||||
I created a new PAT (Personal Access Token) with RW permission on the repositories
|
||||

|
||||

|
||||
|
||||
I added this token as secret `REPO_TOKEN` in the repository
|
||||

|
||||

|
||||
|
||||
|
||||
I needed to create the workflow that will spin-up a container and do the following:
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
Obsidian uses wiki-style links for images, like `
|
||||
Obsidian uses wiki-style links for images, like `![[image name.png]]`, which isn't compatible with Hugo out of the box. Here's how I automated a workaround in a Gitea Actions workflow:
|
||||
- I find all used image references in `.md` files.
|
||||
- For each referenced image, I update the link in relevant `.md` files like ``.
|
||||
- I then copy those used images to the blog's static directory while replacing white-spaces by underscores.
|
||||
@@ -698,7 +698,7 @@ vm_ip = "192.168.66.156"
|
||||
|
||||
✅ Voilà, on vient de créer une VM sur Proxmox en quelques minutes.
|
||||
|
||||

|
||||

|
||||
|
||||
### Connexion SSH
|
||||
|
||||
@@ -697,7 +697,7 @@ vm_ip = "192.168.66.156"
|
||||
|
||||
✅ Done! We’ve successfully created our first VM on Proxmox using Terraform in just a few minutes.
|
||||
|
||||

|
||||

|
||||
|
||||
### SSH Connection
|
||||
|
||||
@@ -22,13 +22,13 @@ Le blog étant redéployé de façon automatique à chaque modification du conte
|
||||
|
||||
Aujourd'hui mon blog se redéploie automatiquement à chaque modification de la branche `main` du [dépôt Git](https://git.vezpi.com/Vezpi/Blog) de mon instance **Gitea** via une **Gitea Actions**. Chaque modification apportée à mon vault **Obsidian** est poussée automatiquement dans cette branche.
|
||||
|
||||

|
||||

|
||||
|
||||
### Créer une Nouvelle Branche
|
||||
|
||||
La première partie, la plus simple, a donc été de créer une nouvelle branche qui allait recevoir ces modifications. J'ai donc crée la branche `preview` dans ce dépôt. Ensuite j'ai modifié la branche cible recevant les modifications dans le workflow de mon dépôt Git Obsidian.
|
||||
|
||||

|
||||

|
||||
|
||||
### Containeriser le Blog
|
||||
|
||||
@@ -211,7 +211,7 @@ Maintenant voici ce que le nouveau workflow fait :
|
||||
|
||||
Voici un exemple de déploiement après un commit automatique généré par **Obsidian**, on peut voir ici que l'image Docker n'a pas été reconstruire car il n'y avait pas de nouvelle version d'Hugo disponible et que le dossier `docker` n'avait pas été modifié, de ce fait, le dernier job `Clean` n'était pas non plus nécessaire.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Code
|
||||
|
||||
@@ -22,13 +22,13 @@ Since the blog is automatically redeployed every time I modify content in Obsidi
|
||||
|
||||
Currently, my blog redeploys automatically on every change to the `main` branch of the [Git repository](https://git.vezpi.com/Vezpi/Blog) hosted on my **Gitea** instance, using a **Gitea Actions** workflow. Every change made in my **Obsidian** vault is automatically pushed to this branch.
|
||||
|
||||

|
||||

|
||||
|
||||
### Create a New Branch
|
||||
|
||||
The first and easiest step was to create a new branch to receive these changes. So I created a `preview` branch in this repository and then updated the target branch in the workflow of my Obsidian Git repo.
|
||||
|
||||

|
||||

|
||||
|
||||
### Containerize the Blog
|
||||
|
||||
@@ -211,7 +211,7 @@ Now, here’s what the new workflow does:
|
||||
|
||||
Here’s an example of a deployment triggered by an automatic commit from **Obsidian**. You can see that the Docker image wasn’t rebuilt because no new Hugo version was available and the `docker` folder hadn’t changed, so the final `Clean` job wasn’t necessary either.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Code
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB |
@@ -95,10 +95,10 @@ $ docker compose up -d
|
||||
```
|
||||
|
||||
✅ Atteindre l’URL [https://gotify.vezpi.me](https://gotify.vezpi.me) m’affiche la page de connexion Gotify :
|
||||

|
||||

|
||||
|
||||
Après connexion, j’accède au tableau de bord, sans messages évidemment :
|
||||

|
||||

|
||||
|
||||
### Créer une Application
|
||||
|
||||
@@ -107,10 +107,10 @@ Pour permettre l’envoi de messages, je dois d’abord créer une application p
|
||||
- **REST-API**
|
||||
|
||||
Pour le test, j’utiliserai la WebUI, je clique sur le bouton `APPS` en haut puis `CREATE APPLICATION`. Je choisis un magnifique nom d'application et une description.
|
||||

|
||||

|
||||
|
||||
Une fois mon application créée, un token est généré pour celle-ci. Je peux modifier l’application pour changer quoi que ce soit, je peux aussi uploader une icône.
|
||||

|
||||

|
||||
|
||||
### Tests
|
||||
|
||||
@@ -122,15 +122,15 @@ curl "https://gotify.vezpi.me/message?token=<apptoken>" -F "title=Cooked!" -F "m
|
||||
Je reçois instantanément la notification sur mon mobile et dans mon navigateur.
|
||||
|
||||
Je renvoie un autre message mais avec une priorité plus basse : `-2`. Je ne reçois pas de notification dans mon navigateur, je remarque une légère différence entre les deux messages. Sur mon mobile, seule ma montre la reçoit, je ne la vois pas sur l’écran, mais je la retrouve dans le centre de notifications.
|
||||

|
||||

|
||||
|
||||
### Application Android
|
||||
|
||||
Voici quelques captures d’écran depuis mon appareil Android :
|
||||

|
||||

|
||||
|
||||
Pour une raison inconnue, une notification apparaît aléatoirement pour me dire que je suis connecté à Gotify :
|
||||

|
||||

|
||||
|
||||
### Conclusion
|
||||
|
||||
@@ -205,7 +205,7 @@ $ docker compose up -d
|
||||
```
|
||||
|
||||
✅ L’URL [https://ntfy.vezpi.me](https://ntfy.vezpi.me) me donne accès au tableau de bord Ntfy :
|
||||

|
||||

|
||||
|
||||
Au départ je n’ai aucun utilisateur et aucun n’est créé par défaut. Comme j’ai interdit tout accès anonyme dans la config, je dois en créer un.
|
||||
|
||||
@@ -228,7 +228,7 @@ Je peux maintenant me connecter à l’interface Web, et passer en mode sombre,
|
||||
### Topics
|
||||
|
||||
Dans Ntfy, il n’y a pas d’applications à créer, mais les messages sont regroupés dans des topics, plus lisibles qu’un token lors de l’envoi. Une fois le topic créé, je peux changer le nom d’affichage ou envoyer des messages de test. Sur l’interface Web, cependant, je ne trouve aucune option pour changer l’icône, alors que c’est possible depuis l’application Android, ce qui n’est pas très pratique.
|
||||

|
||||

|
||||
### Tests
|
||||
|
||||
Envoyer un message est en fait plus difficile que prévu. Comme j’ai activé l’authentification, je dois aussi m’authentifier pour envoyer des messages :
|
||||
@@ -244,7 +244,7 @@ curl \
|
||||
### Application Android
|
||||
|
||||
Voici quelques captures de l’application Android Ntfy :
|
||||

|
||||

|
||||
|
||||
### Conclusion
|
||||
|
||||
@@ -287,7 +287,7 @@ $ curl -u gitea_blog:<password> -d "Message test from gitea_blog!" https://ntfy.
|
||||
{"id":"xIgwz9dr1w9Z","time":1749587681,"expires":1749630881,"event":"message","topic":"blog","message":"Message test from gitea_blog!"}
|
||||
```
|
||||
|
||||

|
||||

|
||||
✅ Message reçu !
|
||||
|
||||
Je tente aussi un envoi sur mon topic de test :
|
||||
@@ -319,7 +319,7 @@ Maintenant que mes utilisateurs sont prêts, je veux ajouter un job `Notify` dan
|
||||
#### Créer un Secret
|
||||
|
||||
Pour permettre à mon Gitea Runner d’utiliser l’utilisateur `gitea_blog` dans ses jobs, je veux créer un secret. J’explore le dépôt Gitea `Blog` dans `Settings`, puis `Actions` > `Secrets` > `Add Secret`. J’y mets la valeur du secret au format `<utilisateur>:<password>` :
|
||||

|
||||

|
||||
|
||||
### Écrire le Code `Notify`
|
||||
|
||||
@@ -369,7 +369,7 @@ Si quelque chose échoue, je veux être notifié sur mon mobile avec une priorit
|
||||
```
|
||||
|
||||
✅ Test des deux cas, fonctionne comme prévu :
|
||||

|
||||

|
||||
|
||||
## Conclusion
|
||||
|
||||
@@ -95,10 +95,10 @@ $ docker compose up -d
|
||||
```
|
||||
|
||||
✅ Reaching the URL https://gotify.vezpi.me gives me the Gotify login page:
|
||||

|
||||

|
||||
|
||||
After login, I can access the dashboard, with no messages obviously:
|
||||

|
||||

|
||||
|
||||
### Creating an Application
|
||||
|
||||
@@ -107,10 +107,10 @@ To allow messages to be pushed, I before need to create an application for which
|
||||
- **REST-API**
|
||||
|
||||
For the test, I will use the WebUI, I click on the `APPS` button at the top and `CREATE APPLICATION`. I choose a wonderful application name and description.
|
||||

|
||||

|
||||
|
||||
Once my application in created, a token is generated for it. I can edit the application to change anything, I can also upload an icon.
|
||||

|
||||

|
||||
|
||||
### Testing
|
||||
|
||||
@@ -122,15 +122,15 @@ curl "https://gotify.vezpi.me/message?token=<apptoken>" -F "title=Cooked!" -F "m
|
||||
I instantly received the notification on my mobile and on my browser.
|
||||
|
||||
I retried to send another message but with a lower priority: `-2`. I didn't get any notification in my browser, I see a slight differences between the two messages. On my mobile, only my watch received it, I don't see it on my screen, but I can find it on the notification center.
|
||||

|
||||

|
||||
|
||||
### Android App
|
||||
|
||||
Here some screenshots from my Android device:
|
||||

|
||||

|
||||
|
||||
For some reason, a notification randomly pops up to tell me that I'm connected to Gotify:
|
||||

|
||||

|
||||
### Conclusion
|
||||
|
||||
On the [documentation](https://gotify.net/docs/msgextras), I found some extras features, like adding images or click actions. In summary, it does the job, that's it. Easy installation process, the utilization is not hard, but I need to create an application for a token, then add this token anytime I want to push messages there.
|
||||
@@ -204,7 +204,7 @@ $ docker compose up -d
|
||||
```
|
||||
|
||||
✅ The URL https://ntfy.vezpi.me gives me to the Ntfy dashboard:
|
||||

|
||||

|
||||
|
||||
At start I don't have any user and none is created by default, as I denied all access to anonymous in the config, I need to create one.
|
||||
|
||||
@@ -227,7 +227,7 @@ I can now login into the WebUI, and I can now switch to dark mode, my eyes are g
|
||||
### Topics
|
||||
|
||||
In Ntfy there are no applications to create, but messages are grouped into topics, more readable than a token when sending messages. When the topic is created I can change the display name or send test messages. On the WebUI though I don't find any option to change the icon, where I can find this option in the Android App which is not really convenient.
|
||||

|
||||

|
||||
|
||||
### Testing
|
||||
|
||||
@@ -244,7 +244,7 @@ curl \
|
||||
### Android App
|
||||
|
||||
Here are some screenshots of Ntfy Android App:
|
||||

|
||||

|
||||
### Conclusion
|
||||
|
||||
Ntfy is a beautiful application with a really strong [documentation](https://docs.ntfy.sh/). The possibilities are endless and the list of integration is impressive. The installation was not hard but required a bit of more setup. The needs for CLI to configure users and permissions is not really convenient.
|
||||
@@ -286,7 +286,7 @@ $ curl -u gitea_blog:<password> -d "Message test from gitea_blog!" https://ntfy.
|
||||
{"id":"xIgwz9dr1w9Z","time":1749587681,"expires":1749630881,"event":"message","topic":"blog","message":"Message test from gitea_blog!"}
|
||||
```
|
||||
|
||||

|
||||

|
||||
✅ Message received!
|
||||
|
||||
I also try to send a message on my test topic:
|
||||
@@ -318,7 +318,7 @@ Now my users are setup, I want to add a `Notify` job in my CI/CD pipeline for th
|
||||
#### Create a Secret
|
||||
|
||||
To allow my Gitea Runner to use my `gitea_blog` user in its job, I want to create a secret. I explore the `Blog` Gitea repository `Settings`, then `Actions` > `Secrets` > `Add Secret`. Here I set the secret value with the `<user>:<password>` format:
|
||||

|
||||

|
||||
|
||||
### Write the `Notify` Code
|
||||
|
||||
@@ -368,7 +368,7 @@ If anything fails, I want to be notified on my mobile with higher priority. Ntfy
|
||||
```
|
||||
|
||||
✅ Testing both cases, work as expected:
|
||||

|
||||

|
||||
|
||||
## Conclusion
|
||||
|
||||
@@ -37,7 +37,7 @@ Node-RED ne remplace pas Home Assistant, il le renforce. Je ne détaillerai pas
|
||||
## Ancien Workflow
|
||||
|
||||
J’avais déjà une solution plutôt efficace pour contrôler ma climatisation via Home Assistant et Node-RED, mais je voulais l’améliorer pour qu’elle prenne aussi en compte le taux d’humidité dans l’appartement. Mon workflow actuel, bien qu’il fonctionne, n’était pas vraiment évolutif et assez difficile à maintenir :
|
||||

|
||||

|
||||
|
||||
## Nouveau Workflow
|
||||
|
||||
@@ -54,12 +54,12 @@ Pour m’aider à faire tout ça, j’utilise 4 [capteurs de température et d
|
||||
### Workflow
|
||||
|
||||
Laissez-moi vous présenter mon nouveau workflow de climatisation dans Node-RED, et vous expliquer en détail comment il fonctionne :
|
||||

|
||||

|
||||
|
||||
#### #### 1. Capteurs de Température
|
||||
|
||||
Dans le premier nœud, j’ai regroupé tous les capteurs thermiques dans un seul `trigger state node`, en ajoutant non seulement la température mais aussi le taux d’humidité géré par chaque capteur. Ce nœud contient donc une liste de 8 entités (2 pour chaque capteur). À chaque fois qu’une de ces 8 valeurs change, le nœud est déclenché :
|
||||

|
||||

|
||||
|
||||
Chacun de mes capteurs thermiques porte un nom de couleur en français, car ils ont tous un autocollant coloré pour les distinguer :
|
||||
- **Jaune** : Salon
|
||||
@@ -93,7 +93,7 @@ return msg;
|
||||
```
|
||||
|
||||
Pour le dernier nœud, dans la majorité des cas, les capteurs envoient deux messages simultanés : l’un pour la température, l’autre pour l’humidité. J’ai donc ajouté un `join node` pour fusionner ces deux messages s’ils sont envoyés dans la même seconde :
|
||||

|
||||

|
||||
|
||||
#### 2. Notification
|
||||
|
||||
@@ -132,17 +132,17 @@ return null; // Don't send anything now
|
||||
```
|
||||
|
||||
Le second nœud est un `call service node` qui envoie une notification sur mon téléphone Android avec les informations fournies :
|
||||

|
||||

|
||||
|
||||
#### 3. Curseurs de Température
|
||||
|
||||
Pour pouvoir ajuster la température sans avoir à modifier tout le workflow, j’ai créé deux entrées (ou helper) Home Assistant, de type number, pour chaque unité de climatisation, ce qui me fait un total de 6 entrées :
|
||||

|
||||

|
||||
|
||||
Ces valeurs représentent la température de base utilisée pour le calcul des seuils, en fonction des offsets que je détaillerai plus loin.
|
||||
|
||||
Le premier nœud est un `trigger state node` qui regroupe les 6 entités. Si je modifie l’une de ces valeurs, le nœud est déclenché :
|
||||

|
||||

|
||||
|
||||
Le deuxième nœud est un `function node`, qui permet de déterminer la pièce concernée :
|
||||
```js
|
||||
@@ -164,17 +164,17 @@ return msg;
|
||||
Dans Home Assistant, j’utilise d’autres entrées, mais cette fois sous forme de booléens. Le plus important est celui dédié à la climatisation, qui me permet de désactiver manuellement tout le workflow. J’en ai d’autres qui sont automatisés, par exemple pour le moment de la journée ou la détection de présence à la maison.
|
||||
|
||||
J’utilise un autre `trigger state node` qui regroupe tous mes interrupteurs sous forme de booléens, y compris un bouton de test utilisé pour le débogage :
|
||||

|
||||

|
||||
|
||||
Comme ces interrupteurs impactent tout l’appartement (et non une seule unité), le nœud suivant est un `change node` qui définit la valeur de la pièce à `partout` :
|
||||

|
||||

|
||||
|
||||
#### 5. Fenêtres
|
||||
|
||||
Les derniers déclencheurs sont les fenêtres. Si j’ouvre ou ferme une fenêtre située près d’une unité, cela active le workflow. J’ai des capteurs d’ouverture sur certaines fenêtres, mais pour l’unité du couloir, j’utilise l’état des fenêtres Velux. Certaines pièces ayant plusieurs fenêtres, j’ai créé une entrée de type groupe pour les regrouper.
|
||||
|
||||
Le premier nœud est le dernier `trigger state node`. La valeur retournée est une string qu’il faudra ensuite convertir en booléen :
|
||||

|
||||

|
||||
|
||||
Juste après, un autre `function node` permet d’identifier la pièce concernée :
|
||||
```js
|
||||
@@ -195,20 +195,20 @@ return msg;
|
||||
Quand j’ouvre une fenêtre, ce n’est pas forcément pour la laisser ouverte longtemps. Je peux simplement faire sortir le chat ou jeter un œil au portail. Je ne veux pas que la climatisation se coupe dès que j’ouvre une fenêtre. Pour contourner cela, j’ai mis en place un watchdog pour chaque unité, afin de retarder l’envoi du message pendant un certain temps.
|
||||
|
||||
Le premier nœud est un `switch node`. En fonction de la pièce transmise par le nœud précédent, il envoie le message au _watchdog_ correspondant :
|
||||

|
||||

|
||||
|
||||
Viennent ensuite les _watchdogs_, des `trigger nodes`, qui retardent le message pendant un certain temps, et prolongent ce délai si un autre message est reçu entre-temps :
|
||||

|
||||

|
||||
|
||||
#### 7. Climatisation Activée ?
|
||||
|
||||
Tous ces déclencheurs arrivent maintenant dans la chaîne de traitement, qui va déterminer ce que le système doit faire. Mais avant cela, on vérifie si l’automatisation est activée. J’ai ajouté ce kill switch au cas où, même si je l’utilise rarement.
|
||||
|
||||
Le premier nœud est un `delay node` qui régule le débit des messages entrants à 1 message par seconde :
|
||||

|
||||

|
||||
|
||||
Le deuxième nœud est un `current state node` qui vérifie si le booléen `climatisation` est activé :
|
||||

|
||||

|
||||
|
||||
#### 8. Configuration des pièces
|
||||
|
||||
@@ -223,7 +223,7 @@ Les unités de climatisation disposent de 4 modes :
|
||||
Pour déterminer quel mode utiliser, j’utilise des seuils pour chaque mode et la vitesse de ventilation, avec différents offsets selon la situation. Je peux ainsi définir un offset spécifique la nuit ou en cas d’absence. Je peux aussi définir un offset sur `disabled`, ce qui forcera l’arrêt de l’unité.
|
||||
|
||||
Le premier nœud est un `switch node`, basé sur la valeur `room`, qui oriente le message vers la configuration associée. Si la pièce est `partout`, le message est dupliqué vers les 3 configurations de pièce :
|
||||

|
||||

|
||||
|
||||
Il est ensuite connecté à un `change node`, qui ajoute la configuration dans `room_config`. Voici un exemple avec la configuration du salon :
|
||||
```json
|
||||
@@ -445,13 +445,13 @@ return msg;
|
||||
```
|
||||
|
||||
Le troisième nœud est un `filter node`, qui ignore les messages suivants ayant un contenu similaire :
|
||||

|
||||

|
||||
|
||||
Le quatrième nœud vérifie si un verrou est actif à l’aide d’un `current state node`. On regarde si le minuteur associé à l’unité est inactif. Si ce n’est pas le cas, le message est ignoré :
|
||||

|
||||

|
||||
|
||||
Le dernier nœud est un autre `current state node` qui permet de récupérer l’état actuel de l’unité et ses propriétés :
|
||||

|
||||

|
||||
|
||||
#### 10. État Cible
|
||||
|
||||
@@ -608,17 +608,17 @@ return msg;
|
||||
#### 11. Choix de l'Action
|
||||
|
||||
En fonction de l’action à effectuer, le `switch node` va router le message vers le bon chemin :
|
||||

|
||||

|
||||
|
||||
#### 12. Démarrage
|
||||
|
||||
Lorsque l’action est `start`, il faut d’abord allumer l’unité. Cela prend entre 20 et 40 secondes selon le modèle, et une fois démarrée, l’unité est verrouillée pendant un court laps de temps pour éviter les messages suivants.
|
||||
|
||||
Le premier nœud est un `call service node` utilisant le service `turn_on` sur l’unité de climatisation :
|
||||

|
||||

|
||||
|
||||
Le second nœud est un autre `call service node` qui va démarrer un minuteur de verrouillage (lock timer) pour cette unité pendant 45 secondes :
|
||||

|
||||

|
||||
|
||||
Le dernier est un `delay node` de 5 secondes, pour laisser le temps à l’intégration Daikin de Home Assistant de refléter le nouvel état.
|
||||
|
||||
@@ -629,12 +629,12 @@ Le dernier est un `delay node` de 5 secondes, pour laisser le temps à l’inté
|
||||
L’action `change` est utilisée pour passer d’un mode à un autre, mais aussi juste après l’allumage.
|
||||
|
||||
Le premier nœud est un `call service node` utilisant le service `set_hvac_mode` sur l’unité de climatisation :
|
||||

|
||||

|
||||
|
||||
Le nœud suivant est un `delay node` de 5 secondes.
|
||||
|
||||
Le dernier vérifie, avec un `switch node`, si la température cible doit être définie. Cela n’est nécessaire que pour les modes `cool` et `heat` :
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -643,7 +643,7 @@ Le dernier vérifie, avec un `switch node`, si la température cible doit être
|
||||
La température cible est uniquement pertinente pour les modes `cool` et `heat`. Avec une climatisation classique, vous définissez une température à atteindre — c’est exactement ce qu’on fait ici. Mais comme chaque unité utilise son propre capteur interne pour vérifier cette température, je ne leur fais pas vraiment confiance. Si la température cible est déjà atteinte selon l’unité, elle ne soufflera plus du tout.
|
||||
|
||||
Le premier nœud est un autre `call service node` utilisant le service `set_temperature` :
|
||||

|
||||

|
||||
|
||||
Encore une fois, ce nœud est suivi d’un `delay node` de 5 secondes.
|
||||
|
||||
@@ -652,20 +652,20 @@ Encore une fois, ce nœud est suivi d’un `delay node` de 5 secondes.
|
||||
L’action `check` est utilisée presque tout le temps. Elle consiste uniquement à vérifier et comparer la vitesse de ventilation souhaitée, et à la modifier si nécessaire.
|
||||
|
||||
Le premier nœud est un `switch node` qui vérifie si la valeur `speed` est définie :
|
||||

|
||||

|
||||
|
||||
Le deuxième est un autre `switch node` qui compare la valeur `speed` avec la vitesse actuelle :
|
||||

|
||||

|
||||
|
||||
Enfin, le dernier nœud est un `call service node` utilisant le service `set_fan_mode` pour définir la vitesse du ventilateur :
|
||||

|
||||

|
||||
|
||||
#### 16. Arrêt
|
||||
|
||||
Lorsque l’action est `stop`, l’unité de climatisation est simplement arrêtée.
|
||||
|
||||
Le premier nœud est un `call service node` utilisant le service `turn_off` :
|
||||

|
||||

|
||||
|
||||
Le deuxième nœud est un autre `call service node` qui va démarrer le minuteur de verrouillage de cette unité pour 45 secondes.
|
||||
|
||||
@@ -675,7 +675,7 @@ Parfois, pour une raison ou une autre, on souhaite utiliser la climatisation man
|
||||
Node-RED utilise son propre utilisateur dans Home Assistant, donc si une unité change d’état sans cet utilisateur, c’est qu’une intervention manuelle a eu lieu.
|
||||
|
||||
Le premier nœud est un `trigger state node`, qui envoie un message dès qu’une unité AC change d’état :
|
||||

|
||||

|
||||
|
||||
Le deuxième est un `function node` qui associe l’unité avec son minuteur :
|
||||
```js
|
||||
@@ -690,13 +690,13 @@ return msg;
|
||||
```
|
||||
|
||||
Le troisième est un `switch node` qui laisse passer le message uniquement si le `user_id` **n’est pas** celui de Node-RED :
|
||||

|
||||

|
||||
|
||||
Le quatrième est un autre `switch node` qui vérifie que le champ `user_id` **est bien défini** :
|
||||

|
||||

|
||||
|
||||
Enfin, le dernier nœud est un `call service node` utilisant le service `start` sur le minuteur de l’unité, avec sa durée par défaut (60 minutes) :
|
||||

|
||||

|
||||
|
||||
## TL;DR
|
||||
|
||||
@@ -37,7 +37,7 @@ Node-RED does not replace Home Assistant, it empowers it. I won't cover the inst
|
||||
## Previous Workflow
|
||||
|
||||
I was already having a good solution to control my AC from Home Assistant with Node-RED, but I wanted to enhance it to also handle the humidity level at home. My current workflow, despite being functional, was not really scalable and quite hard to maintain:
|
||||

|
||||

|
||||
|
||||
## New Workflow
|
||||
|
||||
@@ -54,12 +54,12 @@ To help me achieve that, I'm using 4 [Aqara temperature and humidity sensors](ht
|
||||
### Workflow
|
||||
|
||||
Let me introduce my new AC workflow within Node-RED and explain what it does in detail:
|
||||

|
||||

|
||||
|
||||
#### 1. Temperature Sensors
|
||||
|
||||
In the first node, I combined all the temperature sensors together in one `trigger state node`, but I also added humidity levels in addition to the temperature, managed by the sensor. The node then contains 8 entities in a list (2 for each of my sensor). Each time one value change out of these 8 entities, the node is triggered:
|
||||

|
||||

|
||||
|
||||
Each of my temperature sensors are named with a color in French, because each has its own color sticker to distinguish them:
|
||||
- **Jaune**: Living room
|
||||
@@ -93,7 +93,7 @@ return msg;
|
||||
```
|
||||
|
||||
For the last node, most of the time, the sensors will send two messages at the same time, one containing the temperature value and the other, the humidity level. I added a `join node` to combined the two messages if they are sent within the same second:
|
||||

|
||||

|
||||
|
||||
#### 2. Notification
|
||||
|
||||
@@ -132,17 +132,17 @@ return null; // Don't send anything now
|
||||
```
|
||||
|
||||
The second node is a `call service node` which send a notification on my Android device with the value given:
|
||||

|
||||

|
||||
|
||||
#### 3. Temperature Sliders
|
||||
|
||||
To have a control over the temperature without having to change the workflow, I created two Home Assistant helper, as number, which I can adjust for each unit, giving me 6 helpers in total:
|
||||

|
||||

|
||||
|
||||
These values are the base temperature used for the calculation of the threshold, depending off the offset which I will detail further.
|
||||
|
||||
The first node is a `trigger state node`, with all 6 entities combined. If I change one value, the node is triggered:
|
||||

|
||||

|
||||
|
||||
The second node is a `function node`, to determine the room affected:
|
||||
```js
|
||||
@@ -164,17 +164,17 @@ return msg;
|
||||
In Home Assistant, I'm using other helper but as boolean, the most important is the AC one, where I can manually disable the whole workflow. I have other which are automated, for the time of the day or for detect presence at home.
|
||||
|
||||
I have another `trigger state node` with all my toggles as boolean, including a test button, for debug purpose:
|
||||

|
||||

|
||||
|
||||
As toggles affect the whole apartment and not a single unit, the next node is a `change node`, which set the room value to `partout` (everywhere):
|
||||

|
||||

|
||||
|
||||
#### 5. Windows
|
||||
|
||||
The last triggers are my windows, if I open or close a window next to my unit, it triggers the workflow. I have door sensor for some of my doors, but for the hallway unit, I'm using the Velux windows state. Some rooms have more than one, I created a group helper for them.
|
||||
|
||||
The first node is the last `trigger state node`, the returned value is a string which I will have to convert later into boolean:
|
||||

|
||||

|
||||
|
||||
Connected to it, again a `function node` to select the affect room:
|
||||
```js
|
||||
@@ -195,20 +195,20 @@ return msg;
|
||||
When I open a window, it is not necessarily to let it open for a long time. I could just let the cat out or having a look at my portal. I don't want my AC tuned off as soon as open it. To workaround that I created a watchdog for each unit, to delay the message for some time.
|
||||
|
||||
The first node is a `switch node`, based on the room given by the previous node, it will send the message to the associated watchdog:
|
||||

|
||||

|
||||
|
||||
After are the watchdogs, `trigger nodes`, which will delay the message by some time and extend the delay if another message if received:
|
||||

|
||||

|
||||
|
||||
#### 7. AC Enabled ?
|
||||
|
||||
All these triggers are now entering the computing pipeline, to determine what the system must do with the action. But before, it is checking if the automation is even enabled. I add this kill switch, just in case, but I rarely use it anyway.
|
||||
|
||||
The first node is a `delay node` which regulate the rate of every incoming messages to 1 per second:
|
||||

|
||||

|
||||
|
||||
The second node is a `current state node` which checks if the `climatisation` boolean is enabled:
|
||||

|
||||

|
||||
#### 8. Room Configuration
|
||||
|
||||
The idea here is to attach the configuration of the room to the message. Each room have their own configuration, which unit is used, which sensors and more importantly, when should they be turned on and off.
|
||||
@@ -222,7 +222,7 @@ AC units have 4 mode which can be used:
|
||||
To determine which mode should be used, I'm using threshold for each mode and unit fan's speed, with different offset depending the situation. I can then define a offset during the night or when I'm away. I can also set the offset to `disabled`, which will force the unit to shut down.
|
||||
|
||||
The first node is a `switch node`, based on the `room` value, which will route the message to the associated room configuration. When the room is `partout` (everywhere), the message is split to all 3 room configuration:
|
||||

|
||||

|
||||
|
||||
It is connected to a `change node` which will attach the configuration to the `room_config`, here an example with the living-room configuration:
|
||||
```json
|
||||
@@ -443,13 +443,13 @@ return msg;
|
||||
```
|
||||
|
||||
The third node is a `filter node`, which drops subsequent messages with similar payload:
|
||||

|
||||

|
||||
|
||||
The fourth node checks if any lock is set, with a `current state node`, we verify if the timer associated to the unit is idle. If not, the message is discarded:
|
||||

|
||||

|
||||
|
||||
The last node is another `current state node` which will fetch the unit state and properties:
|
||||

|
||||

|
||||
|
||||
#### 10. Target State
|
||||
|
||||
@@ -606,17 +606,17 @@ return msg;
|
||||
#### 11. Action Switch
|
||||
|
||||
Based on the action to take, the `switch node` will route the message accordingly:
|
||||

|
||||

|
||||
|
||||
#### 12. Start
|
||||
|
||||
When the action is `start`, we first need to turn the unit online, while this takes between 20 to 40 seconds depending on the unit model, it is also locking the unit for a short period for future messages.
|
||||
|
||||
The first node is a `call service node` using the `turn_on` service on the AC unit:
|
||||

|
||||

|
||||
|
||||
The second node is another `call service node` which will start the lock timer of this unit for 45 seconds:
|
||||

|
||||

|
||||
|
||||
The last one is a `delay node` of 5 seconds, to give the time to the Home Assistant Daikin integration to resolve the new state.
|
||||
|
||||
@@ -625,19 +625,19 @@ The last one is a `delay node` of 5 seconds, to give the time to the Home Assist
|
||||
The `change` action is used to change from one mode to another, but also used right after the start action.
|
||||
|
||||
The first node is a `call service node` using `the set_hvac_mode` service on the AC unit:
|
||||

|
||||

|
||||
|
||||
The following node is another delay of 5 seconds.
|
||||
|
||||
The last one verify with a `switch node` if the target temperature needs to be set, this is only required for the modes `cool` and `heat`:
|
||||

|
||||

|
||||
|
||||
#### 14. Set Target Temperature
|
||||
|
||||
The target temperature is only relevant for `cool` and `heat` mode, when you use a normal AC unit, you define a temperature to reach. This is exactly what is defined here. But because each unit is using its own internal sensor to verify, I don't trust it. If the value is already reached, the unit won't blow anything.
|
||||
|
||||
The first node is another `call service node` using the `set_temperature` service:
|
||||

|
||||

|
||||
|
||||
Again, this node is followed by a `delay node` of 5 seconds
|
||||
|
||||
@@ -646,20 +646,20 @@ Again, this node is followed by a `delay node` of 5 seconds
|
||||
The `check` action is almost used everytime, it is actually only checks and compare the desired fan speed, it changes the fan speed if needed.
|
||||
|
||||
The first node is a `switch node` which verify if the `speed` is defined:
|
||||

|
||||

|
||||
|
||||
The second is another `switch node` to compare the `speed` value with the current speed:
|
||||

|
||||

|
||||
|
||||
Finally the last node is a `call service node` using the `set_fan_mode` to set the fan speed:
|
||||

|
||||

|
||||
|
||||
#### 16. Stop
|
||||
|
||||
When the `action` is stop, the AC unit is simply turned off
|
||||
|
||||
The first node is a `call service noded` using the service `turn_off`:
|
||||

|
||||

|
||||
|
||||
The second node is another `call service node` which will start the lock timer of this unit for 45 seconds
|
||||
|
||||
@@ -668,7 +668,7 @@ The second node is another `call service node` which will start the lock timer o
|
||||
Sometime, for some reason, we want to use the AC manually. When we do, we don't want the workflow to change our manual setting, at least for some time. Node-RED is using its own user in Home Assistant, so when an AC unit change state without this user, this was manually done.
|
||||
|
||||
The first node is a `trigger state node`, which will send a message when any AC unit is changing state:
|
||||

|
||||

|
||||
|
||||
The second is a `function node` which willassociate the unit with its timer:
|
||||
```js
|
||||
@@ -683,13 +683,13 @@ return msg;
|
||||
```
|
||||
|
||||
The third is a `switch node` that will let through the message when the user_id is not the Node-RED user's one:
|
||||

|
||||

|
||||
|
||||
The fourth is another `switch node` which checks if there are any `user_id`:
|
||||

|
||||

|
||||
|
||||
Lastly, the final node is a `call service node` using `start` service on the unit's timer with its default duration (60 minutes):
|
||||

|
||||

|
||||
|
||||
## TL;DR
|
||||
|
||||
@@ -594,7 +594,7 @@ vm_ip = "192.168.66.159"
|
||||
|
||||
✅ La VM est maintenant prête !
|
||||
|
||||

|
||||

|
||||
|
||||
🕗 _Ne faites pas attention à l’uptime, j’ai pris la capture d’écran le lendemain._
|
||||
|
||||
@@ -589,7 +589,7 @@ vm_ip = "192.168.66.159"
|
||||
|
||||
✅ The VM is now ready!
|
||||
|
||||

|
||||

|
||||
|
||||
🕗 *Don't pay attention to the uptime, I took the screenshot the next day*
|
||||
|
||||
@@ -97,31 +97,31 @@ BGP est désactivé par défaut, aussi bien sur OPNsense que sur Cilium. Activon
|
||||
D’après la [documentation officielle OPNsense](https://docs.opnsense.org/manual/dynamic_routing.html#bgp-section), l’activation de BGP nécessite d’installer un plugin.
|
||||
|
||||
Va dans `System` > `Firmware` > `Plugins` et installe le plugin **os-frr** :
|
||||

|
||||

|
||||
Installer le plugin `os-frr` dans OPNsense
|
||||
|
||||
Une fois installé, active le plugin dans `Routing` > `General` :
|
||||

|
||||

|
||||
Activer le routage dans OPNsense
|
||||
|
||||
Ensuite, rends-toi dans la section **BGP**. Dans l’onglet **General** :
|
||||
- Coche la case pour activer BGP.
|
||||
- Défini ton **ASN BGP**. J’ai 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)) :
|
||||

|
||||

|
||||
|
||||
Ajoute ensuite tes voisins BGP. Je ne fais le peering qu’avec mes **nœuds workers** (puisque seuls eux hébergent des workloads). Pour chaque voisin :
|
||||
- Mets l’IP du nœud dans `Peer-IP`.
|
||||
- Utilise `64513` comme **Remote AS** (celui de Cilium).
|
||||
- Configure `Update-Source Interface` sur `Lab`.
|
||||
- Coche `Next-Hop-Self`.
|
||||

|
||||

|
||||
|
||||
Voici la liste de mes voisins une fois configurés :
|
||||

|
||||

|
||||
Liste des voisins BGP
|
||||
|
||||
N’oublie pas la règle firewall pour autoriser BGP (port `179/TCP`) depuis le VLAN **Lab** vers le firewall :
|
||||

|
||||

|
||||
Autoriser TCP/179 de Lab vers OPNsense
|
||||
|
||||
#### Dans Cilium
|
||||
@@ -294,7 +294,7 @@ 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 n’importe quel appareil du LAN, on peut tester l’accès sur le port 80 :
|
||||

|
||||

|
||||
|
||||
✅ Notre pod est joignable via une IP `LoadBalancer` routée en BGP. Première étape réussie !
|
||||
|
||||
@@ -451,10 +451,10 @@ Ensuite, j’applique le manifeste `Ingress` pour exposer le service en HTTP.
|
||||
|
||||
Comme j’utilise le plugin **Caddy** dans OPNsense, j’ai encore besoin d’un routage local de type Layer 4 pour rediriger le trafic de `test.vezpi.me` vers l’adresse IP de l’Ingress Controller (`192.168.55.55`). Je crée donc une nouvelle règle dans le plugin Caddy.
|
||||
|
||||

|
||||

|
||||
|
||||
Puis je teste l’accès dans le navigateur :
|
||||

|
||||

|
||||
Test d’un Ingress en HTTP
|
||||
|
||||
✅ Mon pod est désormais accessible via son URL HTTP en utilisant un Ingress. Deuxième étape complétée !
|
||||
@@ -558,7 +558,7 @@ En arrière-plan, Cert-Manager suit ce flux pour émettre le certificat :
|
||||
- L’Ingress utilise automatiquement ce Secret pour servir en HTTPS.
|
||||
|
||||
✅ Une fois ce processus terminé, votre Ingress est sécurisé avec un certificat TLS.
|
||||

|
||||

|
||||
|
||||
### Passer aux certificats de production
|
||||
|
||||
@@ -614,7 +614,7 @@ kubectl delete secret test-vezpi-me-tls
|
||||
```
|
||||
|
||||
🎉 Mon `Ingress` est désormais sécurisé avec un certificat TLS valide délivré par Let’s 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` :
|
||||

|
||||

|
||||
|
||||
|
||||
---
|
||||
@@ -93,17 +93,17 @@ BGP is disabled by default on both OPNsense and Cilium. Let’s enable it on bot
|
||||
According to the [official OPNsense documentation](https://docs.opnsense.org/manual/dynamic_routing.html#bgp-section), enabling BGP requires installing a plugin.
|
||||
|
||||
Head to `System` > `Firmware` > `Plugins` and install the `os-frr` plugin:
|
||||

|
||||

|
||||
Install `os-frr` plugin in OPNsense
|
||||
|
||||
Once installed, enable the plugin under `Routing` > `General`:
|
||||

|
||||

|
||||
Enable routing in OPNsense
|
||||
|
||||
Then navigate to the `BGP` section. In the **General** tab:
|
||||
- Tick the box to enable BGP.
|
||||
- Set your **BGP ASN**. I used `64512`, the first private ASN from the reserved range (see [ASN table](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\)#ASN_Table)):
|
||||

|
||||

|
||||
General BGP configuration in OPNsense
|
||||
|
||||
Now create your BGP neighbors. I’m only peering with my **worker nodes** (since only they run workloads). For each neighbor:
|
||||
@@ -111,15 +111,15 @@ Now create your BGP neighbors. I’m only peering with my **worker nodes** (sinc
|
||||
- Use `64513` as the **Remote AS** (Cilium’s ASN)
|
||||
- Set `Update-Source Interface` to `Lab`
|
||||
- Tick `Next-Hop-Self`:
|
||||

|
||||

|
||||
BGP neighbor configuration in OPNsense
|
||||
|
||||
Here’s how my neighbors list looks once complete:
|
||||

|
||||

|
||||
BGP neighbor list
|
||||
|
||||
Don’t forget to create a firewall rule allowing BGP (port `179/TCP`) from the **Lab** VLAN to the firewall:
|
||||

|
||||

|
||||
Allow TCP/179 from Lab to OPNsense
|
||||
|
||||
#### In Cilium
|
||||
@@ -292,7 +292,7 @@ test-lb LoadBalancer 10.100.167.198 192.168.55.20 80:31350/TCP 169m
|
||||
The service got the first IP from our defined pool: `192.168.55.20`.
|
||||
|
||||
Now from any device on the LAN, try to reach that IP on port 80:
|
||||

|
||||

|
||||
|
||||
✅ Our pod is reachable through BGP-routed `LoadBalancer` IP, first step successful!
|
||||
|
||||
@@ -449,10 +449,10 @@ Then I apply the `Ingress` manifest as shown earlier to expose the service over
|
||||
|
||||
Since I'm using the Caddy plugin on OPNsense, I still need a local Layer 4 route to forward traffic for `test.vezpi.me` to the NGINX Ingress Controller IP (`192.168.55.55`). I simply create a new rule in the Caddy plugin.
|
||||
|
||||

|
||||

|
||||
|
||||
Now let’s test it in the browser:
|
||||

|
||||

|
||||
Test Ingress on HTTP
|
||||
|
||||
✅ Our pod is now reachable on its HTTP URL using an Ingress. Second step complete!
|
||||
@@ -556,7 +556,7 @@ Behind the scenes, Cert-Manager goes through this workflow to issue the certific
|
||||
- The Ingress automatically uses the Secret to serve HTTPS.
|
||||
|
||||
✅ Once this process completes, your Ingress is secured with a TLS certificate.
|
||||

|
||||

|
||||
|
||||
### Switch to Production Certificates
|
||||
|
||||
@@ -612,7 +612,7 @@ kubectl delete secret test-vezpi-me-tls
|
||||
```
|
||||
|
||||
🎉 My `Ingress` is now secured with a valid TLS certificate from Let’s Encrypt. Requests to `https://test.vezpi.me` are encrypted end-to-end and routed by the NGINX Ingress Controller to my `nginx` pod:
|
||||

|
||||

|
||||
|
||||
|
||||
---
|
||||
@@ -27,6 +27,4 @@ Checklist:
|
||||
- [ ] Not Checked
|
||||
- [x] Checked
|
||||
|
||||
Look this is ~~strike~~ !
|
||||
|
||||
What else? A fix!
|
||||
Look this is ~~strike~~ !
|
||||
@@ -1,14 +1,14 @@
|
||||
baseURL: "https://blog.vezpi.com/"
|
||||
title: "Vezpi Lab"
|
||||
theme: "stack"
|
||||
locale: "en-us"
|
||||
languageCode: "en-us"
|
||||
enableGitInfo: true
|
||||
DefaultContentLanguage: "en"
|
||||
defaultContentLanguageInSubdir: true
|
||||
|
||||
languages:
|
||||
en:
|
||||
label: English
|
||||
languageName: English
|
||||
weight: 1
|
||||
menu:
|
||||
main:
|
||||
@@ -54,7 +54,7 @@ languages:
|
||||
lastUpdated: "Jan 2, 2006"
|
||||
|
||||
fr:
|
||||
label: Français
|
||||
languageName: Français
|
||||
weight: 2
|
||||
menu:
|
||||
main:
|
||||
|
||||
@@ -75,9 +75,4 @@ footer:
|
||||
other: " "
|
||||
|
||||
designedBy:
|
||||
other: " "
|
||||
|
||||
pagination:
|
||||
jumpToPage: "Jump to page"
|
||||
jump: "Go"
|
||||
pressEnter: "Press Enter to jump"
|
||||
other: " "
|
||||
@@ -74,9 +74,4 @@ footer:
|
||||
other: " "
|
||||
|
||||
designedBy:
|
||||
other: " "
|
||||
|
||||
pagination:
|
||||
jumpToPage: "Aller à la page"
|
||||
jump: "Aller"
|
||||
pressEnter: "Presser Entrée pour aller"
|
||||
other: " "
|
||||
@@ -1,13 +1,8 @@
|
||||
{{- $IsList := .IsList -}}
|
||||
{{- $Page := .Page -}}
|
||||
<div class="article-details">
|
||||
{{ if $Page.Params.categories }}
|
||||
{{ if .Params.categories }}
|
||||
<header class="article-category">
|
||||
{{ range ($Page.GetTerms "tags") }}
|
||||
{{ $color := partial "helper/color-from-str" .LinkTitle }}
|
||||
{{ $BackgroundColor := default $color.BackgroundColor .Params.style.background }}
|
||||
{{ $TextColor := default $color.TextColor .Params.style.color }}
|
||||
<a href="{{ $Page.RelPermalink }}" style="background-color: {{ $BackgroundColor | safeCSS }}; color: {{ $TextColor | safeCSS }};">
|
||||
{{ range (.GetTerms "tags") }}
|
||||
<a href="{{ .RelPermalink }}" {{ with .Params.style }}style="background-color: {{ .background }}; color: {{ .color }};"{{ end }}>
|
||||
{{ .LinkTitle }}
|
||||
</a>
|
||||
{{ end }}
|
||||
@@ -16,51 +11,50 @@
|
||||
|
||||
<div class="article-title-wrapper">
|
||||
<h2 class="article-title">
|
||||
<a href="{{ $Page.RelPermalink }}">
|
||||
{{- $Page.Title -}}
|
||||
<a href="{{ .RelPermalink }}">
|
||||
{{- .Title -}}
|
||||
</a>
|
||||
</h2>
|
||||
|
||||
{{ with $Page.Params.description }}
|
||||
{{ with .Params.description }}
|
||||
<h3 class="article-subtitle">
|
||||
{{ . }}
|
||||
</h3>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ $showReadingTime := $Page.Params.readingTime | default ($Page.Site.Params.article.readingTime) }}
|
||||
{{ $showDate := not $Page.Date.IsZero }}
|
||||
{{ $showTime := or $showDate $showReadingTime }}
|
||||
|
||||
{{ if $showTime }}
|
||||
<footer class="article-meta">
|
||||
<div class="inline-meta">
|
||||
{{ if $showDate }}
|
||||
{{ $showReadingTime := .Params.readingTime | default (.Site.Params.article.readingTime) }}
|
||||
{{ $showDate := not .Date.IsZero }}
|
||||
{{ $showFooter := or $showDate $showReadingTime }}
|
||||
{{ if $showFooter }}
|
||||
<footer class="article-time">
|
||||
{{ if $showDate }}
|
||||
<div>
|
||||
{{ partial "helper/icon" "date" }}
|
||||
<time class="article-time--published" datetime='{{ $Page.Date.Format "2006-01-02T15:04:05Z07:00" }}'>
|
||||
{{- $Page.Date | time.Format $Page.Site.Params.dateFormat.published -}}
|
||||
<time class="article-time--published">
|
||||
{{- .Date | time.Format (or .Site.Params.dateFormat.published "Jan 02, 2006") -}}
|
||||
</time>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if $showReadingTime }}
|
||||
{{ partial "helper/icon" "clock" }}
|
||||
{{ if $showReadingTime }}
|
||||
<div>
|
||||
{{ partial "helper/icon" "stopwatch" }}
|
||||
<time class="article-time--reading">
|
||||
{{ T "article.readingTime" $Page.ReadingTime }}
|
||||
{{ T "article.readingTime" .ReadingTime }}
|
||||
</time>
|
||||
{{ end }}
|
||||
|
||||
{{- $date := $Page.Date.Format "20060102" | int -}}
|
||||
{{- $lastmod := $Page.Lastmod.Format "20060102" | int -}}
|
||||
{{- if gt $lastmod $date -}}
|
||||
<div class="article-lastmod">
|
||||
{{ partial "helper/icon" "refresh" }}
|
||||
<time>
|
||||
{{ T "article.lastUpdatedOn" }} {{ $Page.Lastmod | time.Format ( or $Page.Site.Params.dateFormat.lastUpdated "Jan 02, 2006 15:04 MST" ) }}
|
||||
</time>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{ end }}
|
||||
{{- $date := .Date.Format "20060102" | int -}}
|
||||
{{- $lastmod := .Lastmod.Format "20060102" | int -}}
|
||||
{{- if gt $lastmod $date -}}
|
||||
<div class="article-lastmod">
|
||||
{{ partial "helper/icon" "refresh" }}
|
||||
<time>
|
||||
{{ T "article.lastUpdatedOn" }} {{ .Lastmod | time.Format ( or .Site.Params.dateFormat.lastUpdated "Jan 02, 2006 15:04 MST" ) }}
|
||||
</time>
|
||||
</div>
|
||||
{{- end -}}
|
||||
</footer>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 554 KiB After Width: | Height: | Size: 554 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 386 KiB After Width: | Height: | Size: 386 KiB |
|
Before Width: | Height: | Size: 295 KiB After Width: | Height: | Size: 295 KiB |
|
Before Width: | Height: | Size: 568 KiB After Width: | Height: | Size: 568 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 507 KiB After Width: | Height: | Size: 507 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |