Aller au contenu
  1. Posts/

QFieldCloud auto-hébergé [A02] : sortir le stockage objet de la machine

Sommaire
QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 7: Cet article

À qui s’adresse cette annexe
#

L’instance de base embarque son stockage objet, et c’est très bien pour démarrer. Cette annexe le remplace par un stockage externe — la seule brique dont le déplacement change vraiment l’architecture.

Les fichiers qu’on touche
#

Deux, côté instance — le reste se passe sur le serveur Garage et dans mc. Chaque bloc de code plus bas rappelle en première ligne le fichier auquel il appartient.

Fichier Ce qu’on y met
.env STORAGES, qui porte l’endpoint, le bucket et les clés
docker-compose.override.yml le profil never sur rustfs et createbuckets

Pourquoi le sortir
#

Le composant livré par l’upstream s’appelle rustfs, en version 1.0.0-beta.11. Le numéro pose déjà la question, mais l’argument décisif est ailleurs : la colocalisation. Dans le profil standalone, base, objets, images et journaux vivent sur le même disque, dans le même conteneur. Une saturation les emporte tous les quatre ensemble, et « sauvegarder les objets » revient à copier un volume sur le disque qu’on est en train de sauvegarder.

La destination retenue ici est une instance Garage qui tourne sur une autre LXC. Garage est conçu pour être distribué, mais celle-ci n’a qu’un seul nœud — c’est ce que montre le garage status plus bas, et c’est une précision qui compte : sans second nœud, il n’y a aucune réplication. Ce qu’on gagne en sortant le stockage, c’est la séparation des domaines de panne, pas de la redondance. La sauvegarde du bucket reste donc entière, et fait l’objet de l’annexe A04.

N’importe quel stockage compatible S3 conviendrait : ce qui suit ne dépend pas de Garage, sauf là où c’est dit.

Préparer le stockage
#

$ garage bucket create qfieldcloud
$ garage key create qfieldcloud-app
==== ACCESS KEY INFORMATION ====
Key ID:              GK20c7ce2347b9d7c025f6664e
Can create buckets:  false
$ garage bucket allow --read --write --owner qfieldcloud --key qfieldcloud-app
$ garage bucket set-quotas qfieldcloud --max-size 5G

Can create buckets: false est volontaire : la clé applicative n’a aucune raison de pouvoir créer un bucket. On verra plus bas que cela condamne un service de la stack — et c’est tant mieux.

Attention : un quota se compare à la capacité déclarée du nœud, pas à la place libre du disque.

$ garage status
ID                Hostname  Zone      Capacity  DataAvail
0f6796f7b25ec365  allfabox  allfabox  4.0 GB    13.7 GB (80.0%)

Capacité annoncée : 4,0 Go. Un quota de 5 Go ne protège donc de rien — le nœud refusera les écritures bien avant que le quota ne soit atteint. C’est une erreur facile à commettre, et elle ne se voit qu’au moment où le disque se remplit.

Le déménagement
#

Les objets d’abord, la configuration ensuite. Jamais l’inverse.

mc travaille par alias : un nom court qui porte l’URL et les clés d’un stockage. Il en faut donc deux, l’ancien et le nouveau, avant toute autre commande. Ils vivent dans ~/.mc/config.json et n’ont besoin d’être déclarés qu’une fois :

$ mc alias set rustfs http://172.17.0.1:8009 "$OBJECT_STORAGE_ROOT_USER" "$OBJECT_STORAGE_ROOT_PASSWORD" --api S3v4
$ mc alias set garage http://10.10.20.21:3900 GK20c7ce2347b9d7c025f6664e "$SECRET" --api S3v4
$ mc alias list | grep -E 'rustfs|garage'

--api S3v4 n’est pas décoratif : sans lui, mc négocie parfois une signature que Garage refuse, avec un message qui parle de tout sauf de ça. Les deux noms — rustfs et garage — sont les miens ; ce sont eux qu’on retrouve à gauche des chemins dans toutes les commandes qui suivent.

$ mc mirror --overwrite rustfs/qfieldcloud-local garage/qfieldcloud
Total: 270.33 KiB   Transferred: 270.33 KiB   Duration: 00m00s

$ mc diff rustfs/qfieldcloud-local garage/qfieldcloud
$

Un mc diff qui ne dit rien est le seul feu vert acceptable. Puis la bascule, dans le .env :

# .env  la variable qui bascule tout
STORAGES='{
    "default": {
        "BACKEND": "qfieldcloud.filestorage.backend.QfcS3Boto3Storage",
        "OPTIONS": {
            "access_key": "GK20c7ce2347b9d7c025f6664e",
            "secret_key": "…",
            "bucket_name": "qfieldcloud",
            "region_name": "garage",
            "endpoint_url": "http://10.10.20.21:3900",
            "addressing_style": "path"
        }
    }
}'

Trois options méritent une explication, parce qu’aucune n’est dans la documentation de QFieldCloud — et que sans elles, aucun stockage objet non-AWS ne fonctionne.

addressing_style: "path"
#

C’est l’option qui évite la panne la plus bête. En style virtual-hosted — le défaut d’AWS — boto3 place le nom du bucket devant l’hôte : qfieldcloud.10.10.20.21. Ce nom n’existe pas et n’existera jamais. En style path, on obtient 10.10.20.21:3900/qfieldcloud/…, qui est ce que Garage attend. L’option est bien acceptée par django-storages 1.14.6, qui la transmet à la configuration du client boto3.

region_name
#

Doit correspondre exactement au s3_region déclaré par le serveur — "garage" dans le garage.toml de ce serveur. La signature SigV4 inclut la région : une divergence produit un SignatureDoesNotMatch parfaitement opaque, qui ne dit rien du vrai problème.

endpoint_url
#

C’est celle qui compte. Rappel du mécanisme établi à l’épisode 4 : Django ne sert jamais l’octet lui-même. Il demande à boto3 une URL présignée valable dix minutes, la renvoie à nginx dans X-Accel-Redirect, et nginx va la chercher.

Or boto3 ne résout rien : il concatène. L’URL présignée contient endpoint_url recopié caractère pour caractère. Deux conséquences :

  • ce host:port doit être joignable depuis le conteneur nginx et depuis les conteneurs QGIS éphémères, pas seulement depuis l’hôte. Une IP de LAN convient ; 127.0.0.1 ne conviendrait jamais ;
  • si le saut est en HTTP clair, les octets et la signature circulent en clair. Sur un VLAN interne c’est un compromis défendable, et surtout réversible sans toucher aux données : passer en TLS plus tard, c’est changer endpoint_url et rien d’autre. Aucun objet ne bouge, parce que rien dans le stockage ne mémorise l’URL par laquelle on l’a atteint.

Deux services de moins
#

Une fois le stockage dehors, rustfs n’a plus lieu d’être. Et createbuckets non plus — c’est le seul service de toute la stack qui en dépendait, et la clé applicative ne peut de toute façon pas créer de bucket. Tous deux se retirent sans toucher au sous-module :

# docker-compose.override.yml
  rustfs:
    profiles: ["never"]

  createbuckets:
    # RETIRÉ avec rustfs : le bucket est créé une fois à la main, et la clé
    # applicative porte `Can create buckets: false` — ce service échouerait
    # de toute façon.
    profiles: ["never"]

Astuce : docker compose up -d --remove-orphans ne supprime pas les conteneurs déjà en marche — Compose ne considère pas comme orpheline une instance exclue par un profil. Il faut les nommer :

$ cd src && docker compose --env-file ../.env --profile never rm -sf rustfs createbuckets

Gardez l’ancien volume quelques jours, comme repli. Il ne coûte que de la place, et il vaut cher le jour où quelque chose a été oublié.

La vérification qui compte vraiment
#

L’épisode 4 a établi que {"database":"ok","storage":"ok"} ne prouve presque rien : côté stockage, la sonde se contente de vérifier que le bucket existe. Un bucket vide et une instance parfaitement cassée renvoient la même chose. Deux tests réels s’imposent.

Lire — un fichier de projet, par l’API, à travers toute la chaîne :

$ curl -D- -H "Authorization: Token …" \
    https://qfieldcloud.example.org/api/v1/files/497a2200-…/chantier.qgs/
HTTP/2 200
etag: "2544d86297275c653b298a0c7b08973b"
content-length: 33527

$ md5sum chantier.qgs
2544d86297275c653b298a0c7b08973b

33 527 octets, empreinte conforme à celle que l’API annonce dans son listing. Les octets viennent bien de l’autre machine.

Écrire — un vrai job de packaging, déclenché par l’API. Six secondes plus tard il est finished, et dans le bucket :

$ mc ls --recursive garage/qfieldcloud | grep packages
[2026-09-04 14:25:05 UTC] 100KiB projects/497a2200-…/packages/d0cfa847-…/data.gpkg
[2026-09-04 14:25:05 UTC] 8.3KiB projects/497a2200-…/packages/d0cfa847-…/chantier_qfield.qgz

Le conteneur QGIS éphémère a téléchargé le projet par l’API publique, fabriqué le GeoPackage, et écrit le paquet sur la machine d’à côté. La boucle complète tient debout.

Astuce : le champ du POST /api/v1/jobs/ s’appelle type, pas job_type. Avec le mauvais nom, la permission reçoit None, lève un NotImplementedError côté serveur et renvoie un 500 habillé en {"code": "unknown_error"}. Le message ne dit rien ; le journal de app, lui, nomme le coupable.

Voir aussi
#

Un projet QGIS peut aussi stocker ses pièces jointes dans un S3 qu’il adresse lui-même, sans passer par QFieldCloud. C’est un tout autre mécanisme, avec ses propres pièges — dont celui de graver l’adresse du serveur dans la donnée.

Ce qu’il faut retenir
#

  • L’argument pour sortir le stockage n’est pas la maturité du composant, c’est la colocalisation.
  • Les objets d’abord, la configuration ensuite. Un mc diff vide est le seul feu vert.
  • addressing_style: "path" et region_name alignée sur le serveur : deux options absentes de la documentation, indispensables hors AWS.
  • L’endpoint_url est recopiée telle quelle dans les URL présignées, et doit être joignable depuis nginx et depuis les conteneurs QGIS éphémères.
  • Un quota se compare à la capacité déclarée du nœud, pas à la place libre du disque.
  • {"storage":"ok"} ne prouve rien : seules une lecture et une écriture réelles le font.

Image de couverture : Scott Blake sur Unsplash.

QFieldCloud auto-hébergé - Cet article fait partie d'une série.
Partie 7: Cet article

Articles connexes