Aller au contenu

Temps réel & IoT

Les capteurs publient leurs mesures en MQTT sur EMQX. Un collecteur Telegraf s’abonne au broker et écrit chaque mesure dans une base PostgreSQL ; Grafana l’affiche, avec sa source de données et un tableau de bord déjà provisionnés. Aucun code à écrire ni à héberger : quatre images publiques et deux fichiers de configuration.

apiVersion: cassiope.eu/v1alpha2
kind: Blueprint
project:
slug: flotte-capteurs
name: Flotte de capteurs
environments:
- slug: production
name: Production
addons:
- slug: mesures
name: Mesures
templateKey: postgresql
version: "17"
resources: { cpuMillicores: 500, ramMb: 1024, diskGb: 20 }
backupEnabled: true
services:
- slug: emqx
name: Broker MQTT
type: web
source:
imageRef: emqx/emqx:5.10.5
portsExposes: "8083"
publiclyExposed: true
healthcheckCommand: curl -s -o /dev/null --max-time 3 http://127.0.0.1:18083/status
resources: { cpuMillicores: 1000, ramMb: 1024 }
env:
- key: EMQX_AUTHENTICATION__1__BACKEND
value: built_in_database
- key: EMQX_AUTHENTICATION__1__MECHANISM
value: password_based
- key: EMQX_AUTHENTICATION__1__USER_ID_TYPE
value: username
- key: EMQX_AUTHENTICATION__1__BOOTSTRAP_TYPE
value: plain
- key: EMQX_AUTHENTICATION__1__BOOTSTRAP_FILE
value: /opt/emqx/etc/auth-built-in-db-bootstrap.csv
- key: EMQX_DASHBOARD__DEFAULT_PASSWORD
fromSecret: true
- key: CAPTEURS_PASSWORD
fromSecret: true
- key: INGESTION_PASSWORD
fromSecret: true
files:
- mountPath: /opt/emqx/etc/auth-built-in-db-bootstrap.csv
content: |
user_id,password,is_superuser
capteurs,${CAPTEURS_PASSWORD},false
ingestion,${INGESTION_PASSWORD},false
exposure: { port: 1883, protocol: tcp }
- slug: ingestion
name: Collecteur MQTT vers PostgreSQL
type: worker
dependsOn: [emqx]
source:
imageRef: telegraf:1.40.2
resources: { cpuMillicores: 250, ramMb: 256 }
bindings:
- addon: mesures
envVarKey: MESURES_URL
files:
- mountPath: /etc/telegraf/telegraf.conf
content: |
[agent]
interval = "10s"
flush_interval = "10s"
omit_hostname = true
[[inputs.mqtt_consumer]]
servers = ["tcp://${MQTT_HOST}:${MQTT_PORT}"]
topics = ["capteurs/+/mesures"]
username = "ingestion"
password = "${INGESTION_PASSWORD}"
data_format = "json"
name_override = "mesures"
[[inputs.mqtt_consumer.topic_parsing]]
topic = "capteurs/+/mesures"
tags = "_/capteur/_"
[[outputs.postgresql]]
connection = "${MESURES_URL}"
- slug: supervision
name: Supervision
type: web
source:
imageRef: grafana/grafana:13.2.4
portsExposes: "3000"
publiclyExposed: true
resources: { cpuMillicores: 500, ramMb: 512 }
env:
- key: GF_SECURITY_ADMIN_PASSWORD
fromSecret: true
- key: GF_USERS_ALLOW_SIGN_UP
value: "false"
bindings:
- addon: mesures
envVarKey: MESURES
derive: split-postgres
files:
- mountPath: /etc/grafana/provisioning/datasources/mesures.yaml
content: |
apiVersion: 1
datasources:
- name: Mesures
uid: mesures
type: grafana-postgresql-datasource
url: ${MESURES_HOST}:${MESURES_PORT}
user: ${MESURES_USER}
isDefault: true
jsonData:
database: ${MESURES_DATABASE}
sslmode: disable
postgresVersion: 1700
secureJsonData:
password: ${MESURES_PASSWORD}
- mountPath: /etc/grafana/provisioning/dashboards/flotte.yaml
content: |
apiVersion: 1
providers:
- name: Flotte
type: file
options:
path: /etc/grafana/dashboards
- mountPath: /etc/grafana/dashboards/flotte.json
content: |
{
"title": "Flotte de capteurs",
"uid": "flotte-capteurs",
"schemaVersion": 39,
"time": { "from": "now-6h", "to": "now" },
"refresh": "30s",
"panels": [
{
"id": 1, "type": "timeseries", "title": "Température",
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 0 },
"datasource": { "type": "grafana-postgresql-datasource", "uid": "mesures" },
"targets": [{
"refId": "A", "format": "time_series", "editorMode": "code", "rawQuery": true,
"datasource": { "type": "grafana-postgresql-datasource", "uid": "mesures" },
"rawSql": "SELECT time, capteur AS metric, temperature FROM mesures WHERE $__timeFilter(time) ORDER BY 1"
}]
},
{
"id": 2, "type": "timeseries", "title": "Humidité",
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 0 },
"datasource": { "type": "grafana-postgresql-datasource", "uid": "mesures" },
"targets": [{
"refId": "A", "format": "time_series", "editorMode": "code", "rawQuery": true,
"datasource": { "type": "grafana-postgresql-datasource", "uid": "mesures" },
"rawSql": "SELECT time, capteur AS metric, humidite FROM mesures WHERE $__timeFilter(time) ORDER BY 1"
}]
},
{
"id": 3, "type": "table", "title": "Dernière mesure par capteur",
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 9 },
"datasource": { "type": "grafana-postgresql-datasource", "uid": "mesures" },
"targets": [{
"refId": "A", "format": "table", "editorMode": "code", "rawQuery": true,
"datasource": { "type": "grafana-postgresql-datasource", "uid": "mesures" },
"rawSql": "SELECT DISTINCT ON (capteur) capteur, time, temperature, humidite FROM mesures ORDER BY capteur, time DESC"
}]
}
]
}
links:
- from: ingestion
to: emqx
envKeyPrefix: MQTT
envCopies:
- from: INGESTION_PASSWORD

À l’import, trois secrets sont demandés : le mot de passe du tableau de bord EMQX (EMQX_DASHBOARD__DEFAULT_PASSWORD), celui des capteurs (CAPTEURS_PASSWORD) et le mot de passe d’administration de Grafana (GF_SECURITY_ADMIN_PASSWORD). Le mot de passe du collecteur (INGESTION_PASSWORD) est saisi une seule fois sur le broker : le lien le recopie chez le collecteur. Le broker et Grafana reçoivent chacun une adresse en cassiope.eu (visible dans l’onglet Domaines du service) ; pour utiliser votre propre nom, ajoutez-le ensuite dans cet onglet et faites-le pointer vers la plateforme.

{
"values": {
"production/emqx": {
"EMQX_DASHBOARD__DEFAULT_PASSWORD": "à générer",
"CAPTEURS_PASSWORD": "à générer",
"INGESTION_PASSWORD": "à générer"
},
"production/supervision": {
"GF_SECURITY_ADMIN_PASSWORD": "à générer"
}
}
}

Un capteur publie sur le sujet capteurs/<identifiant>/mesures un JSON à plat, par exemple {"temperature": 21.4, "humidite": 48}. Le segment du milieu devient la colonne capteur de la table mesures, chaque clé numérique devient une colonne ; Telegraf crée la table et les colonnes à la première mesure reçue, les panneaux Grafana restent donc en erreur jusque-là. Pour des grandeurs autres que temperature et humidite, adaptez les requêtes du tableau de bord.