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", ... })— seecore/apps/devices-simulator/front-end/src/app/core/ws/simulator-ws.service.ts. Thepathvalue 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_proxyhandles 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-devbuilds the image with Kaniko, tagging it${CI_COMMIT_SHA}and${CI_COMMIT_REF_SLUG}. Runs on every non-proddeploy pipeline.simulator-back:promoteuses crane to retag the SHA image with${CI_COMMIT_REF_SLUG}andlatest. Runs onprod-*tag pipelines.terraform:applyconsumesvar.docker_image_tag, which is set byinfrastructure/gitlab/terraform.gitlab-ci.extend.ymlto$CI_COMMIT_SHAfor branch pipelines and$CI_COMMIT_REF_SLUGfor tag pipelines. The App Service points tosimulator-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.