Skip to content

Devices Simulator

The devices simulator is an optional component that lets users emulate IoT devices against the Kamea platform — telemetries, configuration sync, commands — without provisioning physical hardware. It is disabled by default.

Components

The simulator is composed of three deployable units, all under core/apps/devices-simulator/:

  • simulator-back — a NestJS API + Socket.IO gateway. Built as a Docker image (core/apps/devices-simulator/back-end/Dockerfile) and hosted on an Azure Linux Web App.
  • simulator-front — an Angular SPA that controls simulated devices and visualizes their events in real-time. Built as a static bundle and served by Caddy from the shared frontend Azure File Share.
  • simulator-module — the Terraform module that provisions the App Service for the API.

The simulator's API depends on the same JWT issuer (JWT_ISSUER, JWT_AUDIENCE, JWT_ISSUER_WELL_KNOWN_URL) as the management API. It does not depend on the platform database, Service Bus, Redis, or IoT Hub — the only persistent state lives in memory per running instance.

The USE_SIMULATOR flag

A single GitLab CI variable, USE_SIMULATOR, gates every piece of the simulator end-to-end:

Layer Effect when USE_SIMULATOR=true Effect when USE_SIMULATOR=false (default)
simulator-back:test, :test-e2e:build Run Skipped (when: never)
simulator-back:deploy-dev, :promote Run Skipped
simulator-front:build, :deploy Run Skipped
terraform:apply Instantiates module.simulator and creates the App Service module.simulator has count = 0, no App Service is created

The flag is declared in .gitlab-ci.extend.yml with a default value of "false". Override it in the GitLab CI/CD variables of each environment that should run the simulator.

The flag is also piped to Terraform via infrastructure/gitlab/terraform.gitlab-ci.extend.yml (echo use_simulator=$USE_SIMULATOR >> terraform.tfvars), which sets the use_simulator Terraform variable in infrastructure/variables.tf.

Routing through Caddy

The simulator API is only reachable through Caddy, mirroring the management API. The App Service uses an IP-restriction allowlist of Caddy's outbound IPs, fed in via the Terraform module input caddy_outbound_ips (read from module.core-platform.azurerm_linux_web_app_caddy.outbound_ip_address_list).

Because the simulator is optional, the default checked-in core/apps/caddy/caddyfile.extension does not reference it. Deployments that enable the simulator must point their CADDY_EXTENSION_PATH CI variable at an extension that adds the simulator routes. The example overlay is at core/apps/caddy/caddyfile.extension.simulator.example.

The routing scheme is:

URL prefix Target Notes
/simulator/ Caddy serve_frontend snippet, files from the file share at frontend/simulator/ The SPA, deployed with --base-href "/simulator/".
/simulator-api/* reverse_proxy to ${project-name}-simulator-api.azurewebsites.net REST and Socket.IO traffic. handle_path strips the prefix, so the API receives /devices, /socket.io/?... etc.

/api/* is reserved for the management API and is not used by the simulator. The simulator's API path mirrors its App Service name (${project_name}-simulator-api).

Socket.IO co-existence with the management WebSocket

The platform already proxies the management WebSocket server (WSS) at /socket.io/* in the base Caddyfile. To avoid colliding, the simulator's Socket.IO traffic is nested inside /simulator-api/:

  • The simulator API runs Socket.IO at its default server path (/socket.io).
  • Browser-side, the front-end calls io("/simulator-devices", { path: "/simulator-api/socket.io", ... }) — see core/apps/devices-simulator/front-end/src/app/core/ws/simulator-ws.service.ts. The path value is read from the environment file (simulatorWsPath), so local-dev keeps the socket.io default /socket.io.
  • Caddy's handle_path /simulator-api/* { reverse_proxy ... } strips the prefix, so the simulator backend sees /socket.io/?... and matches its server. reverse_proxy handles WebSocket upgrades natively, no additional rewriting is needed.

Result: the simulator API ends up reachable as same-origin from the simulator SPA, so CORS is never exercised in deployments behind Caddy and the simulator backend does not configure any CORS headers.

Image lifecycle

The simulator-back image follows the same lifecycle as every other Kamea Docker image:

  • simulator-back:deploy-dev builds the image with Kaniko, tagging it ${CI_COMMIT_SHA} and ${CI_COMMIT_REF_SLUG}. Runs on every non-prod deploy pipeline.
  • simulator-back:promote uses crane to retag the SHA image with ${CI_COMMIT_REF_SLUG} and latest. Runs on prod-* tag pipelines.
  • terraform:apply consumes var.docker_image_tag, which is set by infrastructure/gitlab/terraform.gitlab-ci.extend.yml to $CI_COMMIT_SHA for branch pipelines and $CI_COMMIT_REF_SLUG for tag pipelines. The App Service points to simulator-back:${var.docker_image_tag}, so re-applying Terraform rolls the deployment.

The simulator-back:deploy-dev and simulator-back:promote jobs are declared as optional needs of terraform:apply (see infrastructure/gitlab/.gitlab-ci.yml), so Terraform always waits for the freshest image when the simulator is enabled.

Known coupling

The end-to-end test job simulator-back:test-e2e:build uses RabbitMQ as a service. When USE_SIMULATOR=true but USE_MQTT=false, the rabbitmq-server-e2e image is not built and the e2e test will fail to start. This is a pre-existing coupling, not specific to the simulator's gating model.