feat: add IaC layer with shared var/t1 and var/t3 data paths

Ansible provisions the k3d cluster and Flux controllers; Terraform
modules deploy the simulated fleet and ground warehouse. Compose and
k3d share var/t1 (live lake) and var/t3 (warehouse). The prototype
gains a live mode fed by the explorer read-only SQL API.
This commit is contained in:
2026-07-08 20:17:32 +01:00
parent d58569b25c
commit cb4664f5ee
44 changed files with 8067 additions and 35 deletions
+439
View File
@@ -0,0 +1,439 @@
# Simulated fleet (T4 dev environment, docs/06) on the k3d cluster created
# by infra/ansible/sim-cluster.yml. Drones are StatefulSet replicas writing
# to a shared hostPath lake — the same /data contract as the on-board NVMe.
locals {
labels = { "app.kubernetes.io/part-of" = "swarm-sim" }
}
resource "kubernetes_namespace" "swarm" {
metadata {
name = var.namespace
labels = local.labels
}
}
# --- Fleet ------------------------------------------------------------------
resource "kubernetes_stateful_set" "drone" {
metadata {
name = "drone"
namespace = kubernetes_namespace.swarm.metadata[0].name
labels = local.labels
}
spec {
service_name = "drone"
replicas = var.drone_count
# A landed drone powers off; the next start is the next flight
pod_management_policy = "Parallel"
selector {
match_labels = { app = "drone" }
}
template {
metadata {
labels = merge(local.labels, { app = "drone" })
}
spec {
container {
name = "drone"
image = var.simulator_image
image_pull_policy = "Never" # imported via k3d image import
env {
name = "DRONE_ID"
value_from {
field_ref {
field_path = "metadata.name"
}
}
}
env {
name = "DURATION_S"
value = tostring(var.flight_duration_s)
}
env {
name = "SPEEDUP"
value = tostring(var.speedup)
}
env {
name = "DATA_DIR"
value = "/data"
}
volume_mount {
name = "lake"
mount_path = "/data"
}
resources {
requests = { cpu = "50m", memory = "128Mi" }
limits = { cpu = "500m", memory = "512Mi" }
}
}
volume {
name = "lake"
host_path {
path = var.data_host_path
}
}
}
}
}
}
# --- Exporter + Prometheus + Grafana (observability, docs/07) ----------------
resource "kubernetes_deployment" "exporter" {
metadata {
name = "exporter"
namespace = kubernetes_namespace.swarm.metadata[0].name
labels = local.labels
}
spec {
replicas = 1
selector {
match_labels = { app = "exporter" }
}
template {
metadata {
labels = merge(local.labels, { app = "exporter" })
}
spec {
container {
name = "exporter"
image = var.simulator_image
image_pull_policy = "Never"
command = ["python", "monitoring/exporter.py"]
env {
name = "DATA_DIR"
value = "/data"
}
env {
name = "EXPORTER_PORT"
value = "9105"
}
port {
container_port = 9105
}
volume_mount {
name = "lake"
mount_path = "/data"
read_only = true
}
}
volume {
name = "lake"
host_path {
path = var.data_host_path
}
}
}
}
}
}
resource "kubernetes_service" "exporter" {
metadata {
name = "exporter"
namespace = kubernetes_namespace.swarm.metadata[0].name
}
spec {
selector = { app = "exporter" }
port {
port = 9105
target_port = 9105
}
}
}
resource "kubernetes_deployment" "explorer" {
metadata {
name = "explorer"
namespace = kubernetes_namespace.swarm.metadata[0].name
labels = local.labels
}
spec {
replicas = 1
selector {
match_labels = { app = "explorer" }
}
template {
metadata {
labels = merge(local.labels, { app = "explorer" })
}
spec {
container {
name = "explorer"
image = var.simulator_image
image_pull_policy = "Never"
command = ["python", "explorer/server.py"]
env {
name = "DATA_DIR"
value = "/data"
}
env {
name = "EXPLORER_PORT"
value = "8088"
}
port {
container_port = 8088
}
volume_mount {
name = "lake"
mount_path = "/data"
read_only = true
}
}
volume {
name = "lake"
host_path {
path = var.data_host_path
}
}
}
}
}
}
resource "kubernetes_service" "explorer" {
metadata {
name = "explorer"
namespace = kubernetes_namespace.swarm.metadata[0].name
}
spec {
type = "NodePort"
selector = { app = "explorer" }
port {
port = 8088
target_port = 8088
node_port = var.node_ports.explorer
}
}
}
resource "kubernetes_config_map" "prometheus" {
metadata {
name = "prometheus-config"
namespace = kubernetes_namespace.swarm.metadata[0].name
}
data = {
"prometheus.yml" = <<-EOT
global:
scrape_interval: 5s
scrape_configs:
- job_name: swarm
static_configs:
- targets: ["exporter:9105"]
EOT
}
}
resource "kubernetes_deployment" "prometheus" {
metadata {
name = "prometheus"
namespace = kubernetes_namespace.swarm.metadata[0].name
labels = local.labels
}
spec {
replicas = 1
selector {
match_labels = { app = "prometheus" }
}
template {
metadata {
labels = merge(local.labels, { app = "prometheus" })
}
spec {
container {
name = "prometheus"
image = "prom/prometheus:v2.53.0"
port {
container_port = 9090
}
volume_mount {
name = "config"
mount_path = "/etc/prometheus"
}
}
volume {
name = "config"
config_map {
name = kubernetes_config_map.prometheus.metadata[0].name
}
}
}
}
}
}
resource "kubernetes_service" "prometheus" {
metadata {
name = "prometheus"
namespace = kubernetes_namespace.swarm.metadata[0].name
}
spec {
type = "NodePort"
selector = { app = "prometheus" }
port {
port = 9090
target_port = 9090
node_port = var.node_ports.prometheus
}
}
}
resource "kubernetes_config_map" "grafana_provisioning" {
metadata {
name = "grafana-provisioning"
namespace = kubernetes_namespace.swarm.metadata[0].name
}
data = {
"datasource.yml" = <<-EOT
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
EOT
"dashboards.yml" = <<-EOT
apiVersion: 1
providers:
- name: swarm
folder: ""
type: file
options:
path: /var/lib/grafana/dashboards
EOT
}
}
resource "kubernetes_config_map" "grafana_dashboard" {
metadata {
name = "grafana-dashboard"
namespace = kubernetes_namespace.swarm.metadata[0].name
}
data = {
"swarm.json" = file("${path.module}/../../../simulator/monitoring/grafana/dashboards/swarm.json")
}
}
resource "kubernetes_deployment" "grafana" {
metadata {
name = "grafana"
namespace = kubernetes_namespace.swarm.metadata[0].name
labels = local.labels
}
spec {
replicas = 1
selector {
match_labels = { app = "grafana" }
}
template {
metadata {
labels = merge(local.labels, { app = "grafana" })
}
spec {
container {
name = "grafana"
image = "grafana/grafana:11.1.0"
port {
container_port = 3000
}
env {
name = "GF_AUTH_ANONYMOUS_ENABLED"
value = "true"
}
env {
name = "GF_AUTH_ANONYMOUS_ORG_ROLE"
value = "Admin"
}
env {
name = "GF_AUTH_DISABLE_LOGIN_FORM"
value = "true"
}
# Air-gap hygiene — same flags as the Compose profile
env {
name = "GF_ANALYTICS_REPORTING_ENABLED"
value = "false"
}
env {
name = "GF_ANALYTICS_CHECK_FOR_UPDATES"
value = "false"
}
env {
name = "GF_ANALYTICS_CHECK_FOR_PLUGIN_UPDATES"
value = "false"
}
volume_mount {
name = "provisioning-datasources"
mount_path = "/etc/grafana/provisioning/datasources"
}
volume_mount {
name = "provisioning-dashboards"
mount_path = "/etc/grafana/provisioning/dashboards"
}
volume_mount {
name = "dashboards"
mount_path = "/var/lib/grafana/dashboards"
}
}
volume {
name = "provisioning-datasources"
config_map {
name = kubernetes_config_map.grafana_provisioning.metadata[0].name
items {
key = "datasource.yml"
path = "datasource.yml"
}
}
}
volume {
name = "provisioning-dashboards"
config_map {
name = kubernetes_config_map.grafana_provisioning.metadata[0].name
items {
key = "dashboards.yml"
path = "dashboards.yml"
}
}
}
volume {
name = "dashboards"
config_map {
name = kubernetes_config_map.grafana_dashboard.metadata[0].name
}
}
}
}
}
}
resource "kubernetes_service" "grafana" {
metadata {
name = "grafana"
namespace = kubernetes_namespace.swarm.metadata[0].name
}
spec {
type = "NodePort"
selector = { app = "grafana" }
port {
port = 3000
target_port = 3000
node_port = var.node_ports.grafana
}
}
}