# 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" } env { name = "KEEP_ALIVE" value = "1" } env { name = "IMU_HZ" value = "20" } 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" } env { name = "SCAN_INTERVAL_S" value = "30" } env { name = "METRIC_FLIGHT_WINDOW" value = "5" } port { container_port = 9105 } resources { requests = { cpu = "50m", memory = "64Mi" } limits = { cpu = "500m", memory = "256Mi" } } 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" } env { name = "VIEW_REFRESH_S" value = "30" } env { name = "TREE_CACHE_S" value = "15" } env { name = "METRIC_FLIGHT_WINDOW" value = "5" } port { container_port = 8088 } resources { requests = { cpu = "50m", memory = "64Mi" } limits = { cpu = "750m", memory = "256Mi" } } 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: 15s scrape_configs: - job_name: swarm static_configs: - targets: ["exporter:9105"] - job_name: explorer static_configs: - targets: ["explorer:8088"] - job_name: node static_configs: - targets: ["node-exporter:9100"] 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 deleteDatasources: - name: Prometheus orgId: 1 datasources: - name: Prometheus uid: swarm-prom 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") "swarm-platform.json" = file("${path.module}/../../../simulator/monitoring/grafana/dashboards/swarm-platform.json") } } # Host metrics for the k3d node (CPU / memory on the platform dashboard) resource "kubernetes_deployment" "node_exporter" { metadata { name = "node-exporter" namespace = kubernetes_namespace.swarm.metadata[0].name labels = local.labels } spec { replicas = 1 selector { match_labels = { app = "node-exporter" } } template { metadata { labels = merge(local.labels, { app = "node-exporter" }) } spec { host_network = true host_pid = true container { name = "node-exporter" image = "prom/node-exporter:v1.8.2" args = [ "--path.procfs=/host/proc", "--path.sysfs=/host/sys", "--path.rootfs=/host/root", "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)", ] port { container_port = 9100 } volume_mount { name = "proc" mount_path = "/host/proc" read_only = true } volume_mount { name = "sys" mount_path = "/host/sys" read_only = true } volume_mount { name = "root" mount_path = "/host/root" read_only = true } resources { requests = { cpu = "20m", memory = "32Mi" } limits = { cpu = "200m", memory = "64Mi" } } } volume { name = "proc" host_path { path = "/proc" } } volume { name = "sys" host_path { path = "/sys" } } volume { name = "root" host_path { path = "/" } } } } } } resource "kubernetes_service" "node_exporter" { metadata { name = "node-exporter" namespace = kubernetes_namespace.swarm.metadata[0].name } spec { selector = { app = "node-exporter" } port { port = 9100 target_port = 9100 } } } 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 } } }