diff --git a/.github/workflows/simulator-deploy.yml b/.github/workflows/simulator-deploy.yml new file mode 100644 index 0000000..7a67d5e --- /dev/null +++ b/.github/workflows/simulator-deploy.yml @@ -0,0 +1,84 @@ +name: Build and Deploy Robot Simulator + +on: + push: + branches: [main] + paths: + - "RobotSimulator/**" + +# Required for OIDC federated identity authentication — no client secrets used +permissions: + id-token: write + contents: read + +env: + RESOURCE_GROUP: ewu-deliverybotsystem-rg + + # ACR — pre-created; admin credentials stored as Container App registry secret + ACR_NAME: DeliverybotCR + ACR_LOGIN_SERVER: deliverybotcr.azurecr.io + + # Container App — pre-created with system-assigned managed identity + CONTAINER_APP_NAME: deliverybot-robot-simulator + + IMAGE_NAME: deliverybot-robot-simulator + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + # ── 1. Checkout source code ─────────────────────────────────────────────── + - name: Checkout repository + uses: actions/checkout@v4 + + # ── 2. Authenticate to Azure via OIDC federated identity ───────────────── + - name: Azure Login (OIDC) + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + # ── 3. Build Docker image and push to ACR ──────────────────────────────── + # Build context is RobotSimulator/ to match the docker-compose convention. + # The Dockerfile copies src projects relative to that context root. + - name: Log in to Azure Container Registry + run: az acr login --name "$ACR_NAME" + + - name: Build and push Docker image + run: | + IMAGE_TAG="${ACR_LOGIN_SERVER}/${IMAGE_NAME}:${{ github.sha }}" + echo "Building: $IMAGE_TAG" + docker build \ + -f RobotSimulator/src/DeliveryBot.RobotSimulator.Api/Dockerfile \ + -t "$IMAGE_TAG" \ + RobotSimulator/ + docker push "$IMAGE_TAG" + echo "IMAGE_TAG=$IMAGE_TAG" >> "$GITHUB_ENV" + + # ── 4. Deploy updated image to Container App ───────────────────────────── + # Image-only update — environment variables and secrets already configured + # on the live Container App are intentionally left unchanged. + # Do not add --set-env-vars here; Event Hub transport settings are managed + # outside this workflow until they are brought under IaC. + - name: Update Container App image + run: | + az containerapp update \ + --name "$CONTAINER_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --image "$IMAGE_TAG" + + # ── 5. Output the live deployment URL ───────────────────────────────────── + - name: Print deployment URL + run: | + FQDN=$(az containerapp show \ + --name "$CONTAINER_APP_NAME" \ + --resource-group "$RESOURCE_GROUP" \ + --query properties.configuration.ingress.fqdn -o tsv) + echo "========================================" + echo " Deployment complete!" + echo " App URL : https://${FQDN}" + echo " Health : https://${FQDN}/health" + echo " Bots API : https://${FQDN}/bots" + echo "========================================" diff --git a/Iac/simulator/main.tf b/Iac/simulator/main.tf new file mode 100644 index 0000000..e0f5522 --- /dev/null +++ b/Iac/simulator/main.tf @@ -0,0 +1,114 @@ +# --------------------------------------------------------------------------- +# Data sources — reference existing Azure resources; nothing here is created +# or destroyed by this module. +# --------------------------------------------------------------------------- + +data "azurerm_resource_group" "rg" { + name = var.resource_group_name +} + +data "azurerm_container_app_environment" "env" { + name = var.container_app_env_name + resource_group_name = data.azurerm_resource_group.rg.name +} + +data "azurerm_container_registry" "acr" { + name = var.acr_name + resource_group_name = data.azurerm_resource_group.rg.name +} + +data "azurerm_eventhub_namespace" "evhns" { + name = var.event_hub_namespace_name + resource_group_name = data.azurerm_resource_group.rg.name +} + +# --------------------------------------------------------------------------- +# Robot Simulator Container App +# +# Ownership boundary (Balanced approach): +# OWNED by this module: +# - Container image reference +# - Ingress: external, port 8080 +# - Revision mode: single (stateful in-memory simulator; one replica only) +# - Scale: min 1, max 1 +# - System-assigned managed identity +# - ACR registry reference +# - Non-sensitive env vars: ASPNETCORE_ENVIRONMENT, ASPNETCORE_URLS +# +# NOT owned by this module (managed outside IaC): +# - EventTransport__Mode +# - EventTransport__ConnectionString +# - EventTransport__InputEventHubName +# - EventTransport__OutputEventHubName +# - EventTransport__ConsumerGroup +# - EventTransport__EnableInputConsumer +# - Any Container App secrets +# +# Reason: these env vars and secrets are already live on the Container App +# and must not be overwritten during early IaC rollout. They will be brought +# under IaC management when the project-wide IaC module is established. +# --------------------------------------------------------------------------- + +resource "azurerm_container_app" "simulator" { + name = var.container_app_name + resource_group_name = data.azurerm_resource_group.rg.name + container_app_environment_id = data.azurerm_container_app_environment.env.id + revision_mode = "Single" + + identity { + type = "SystemAssigned" + } + + registry { + server = data.azurerm_container_registry.acr.login_server + } + + template { + container { + name = var.container_app_name + image = "${data.azurerm_container_registry.acr.login_server}/${var.image_name}:${var.image_tag}" + cpu = 0.5 + memory = "1Gi" + + # Non-sensitive runtime environment variables. + # Event Hub transport settings are intentionally excluded — see ownership + # boundary comment above. + env { + name = "ASPNETCORE_ENVIRONMENT" + value = "Production" + } + + env { + name = "ASPNETCORE_URLS" + value = "http://+:8080" + } + } + + # Scale is fixed at exactly one replica. + # The simulator holds bot state in memory; multiple replicas would produce + # independent, conflicting bot fleets with no shared state. + min_replicas = 1 + max_replicas = 1 + } + + ingress { + external_enabled = true + target_port = 8080 + + traffic_weight { + percentage = 100 + latest_revision = true + } + } + + lifecycle { + # Prevent Terraform from overwriting env vars or secrets that are managed + # outside this module (e.g., Event Hub connection settings set via portal + # or CLI). Remove this ignore block once those settings are brought under + # IaC management. + ignore_changes = [ + template[0].container[0].env, + secret, + ] + } +} diff --git a/Iac/simulator/outputs.tf b/Iac/simulator/outputs.tf new file mode 100644 index 0000000..ec9f8c6 --- /dev/null +++ b/Iac/simulator/outputs.tf @@ -0,0 +1,39 @@ +output "container_app_name" { + description = "Name of the simulator Container App." + value = azurerm_container_app.simulator.name +} + +output "container_app_fqdn" { + description = "Fully-qualified domain name of the simulator Container App ingress." + value = azurerm_container_app.simulator.ingress[0].fqdn +} + +output "container_app_url" { + description = "Base HTTPS URL for the simulator." + value = "https://${azurerm_container_app.simulator.ingress[0].fqdn}" +} + +output "health_url" { + description = "Health check URL for the simulator." + value = "https://${azurerm_container_app.simulator.ingress[0].fqdn}/health" +} + +output "acr_login_server" { + description = "ACR login server used for the simulator image." + value = data.azurerm_container_registry.acr.login_server +} + +output "image_reference" { + description = "Full image reference deployed to the Container App." + value = "${data.azurerm_container_registry.acr.login_server}/${var.image_name}:${var.image_tag}" +} + +output "event_hub_namespace_name" { + description = "Name of the Event Hub namespace referenced by the simulator." + value = data.azurerm_eventhub_namespace.evhns.name +} + +output "event_hub_namespace_fqdn" { + description = "Fully-qualified Event Hub namespace host (for reference)." + value = "${data.azurerm_eventhub_namespace.evhns.name}.servicebus.windows.net" +} diff --git a/Iac/simulator/providers.tf b/Iac/simulator/providers.tf new file mode 100644 index 0000000..0fce778 --- /dev/null +++ b/Iac/simulator/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.5" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.110" + } + } +} + +provider "azurerm" { + features {} +} diff --git a/Iac/simulator/terraform.tfvars b/Iac/simulator/terraform.tfvars new file mode 100644 index 0000000..f22929d --- /dev/null +++ b/Iac/simulator/terraform.tfvars @@ -0,0 +1,8 @@ +resource_group_name = "ewu-deliverybotsystem-rg" +location = "westus" +container_app_env_name = "managedEnvironment-ewudeliverybots-aa2f" +acr_name = "DeliverybotCR" +event_hub_namespace_name = "DeliverybotSimulator-EVHNS" +container_app_name = "deliverybot-robot-simulator" +image_name = "deliverybot-robot-simulator" +image_tag = "latest" diff --git a/Iac/simulator/variables.tf b/Iac/simulator/variables.tf new file mode 100644 index 0000000..2d5e859 --- /dev/null +++ b/Iac/simulator/variables.tf @@ -0,0 +1,42 @@ +variable "resource_group_name" { + description = "Name of the existing resource group that contains all simulator resources." + type = string +} + +variable "location" { + description = "Azure region of the resource group." + type = string +} + +variable "container_app_env_name" { + description = "Name of the existing Container Apps managed environment." + type = string +} + +variable "acr_name" { + description = "Name of the existing Azure Container Registry (without .azurecr.io)." + type = string +} + +variable "event_hub_namespace_name" { + description = "Name of the existing Event Hub namespace used by the simulator." + type = string +} + +variable "container_app_name" { + description = "Name of the simulator Container App." + type = string + default = "deliverybot-robot-simulator" +} + +variable "image_name" { + description = "Container image name (without registry prefix or tag)." + type = string + default = "deliverybot-robot-simulator" +} + +variable "image_tag" { + description = "Container image tag. Typically the deploying commit SHA." + type = string + default = "latest" +} diff --git a/docs/simulator-Iac-integration-guide b/docs/simulator-Iac-integration-guide new file mode 100644 index 0000000..6e6f418 --- /dev/null +++ b/docs/simulator-Iac-integration-guide @@ -0,0 +1,130 @@ +**Simulator IaC Integration Guide** + +This document describes what must change to merge the standalone simulator Terraform module into the eventual project-wide IaC. + +## Current state + +The simulator IaC currently lives in `Iac/simulator/` as a self-contained Terraform setup. It: + +- references existing Azure resources with `data` sources +- owns the simulator Container App's structural configuration +- intentionally excludes the live Event Hub env vars and secrets +- uses local state and a module-local provider config +- is deployed independently from the rest of the project + +That shape was chosen to unblock simulator infrastructure work before the full IaC stack exists. + +## What needs to change later + +### 1. Move the simulator module under the project root IaC + +The simulator should become a child module of the eventual root Terraform configuration. + +Likely changes: + +- move `Iac/simulator/` into the project's module layout +- remove `providers.tf` from the child module +- remove `terraform.tfvars` from the child module +- call the simulator module from the root module + +Example: + +```hcl +module "simulator" { + source = "./modules/simulator" + + resource_group_name = var.resource_group_name + location = var.location + container_app_env_name = var.container_app_env_name + acr_name = var.acr_name + event_hub_namespace_name = var.event_hub_namespace_name + container_app_name = var.simulator_container_app_name + image_name = var.simulator_image_name + image_tag = var.simulator_image_tag +} +``` + +### 2. Add remote state + +The current module has no backend. The final IaC should use a remote backend, likely Azure Storage. + +Required steps: + +- define the backend at the root level +- migrate local state if any exists +- keep the simulator module stateless except for managed resources + +### 3. Bring Event Hub settings into Terraform + +The biggest merge item is the simulator's Event Hub configuration. + +The following env vars are currently excluded and must eventually be declared in IaC: + +- `EventTransport__Mode` +- `EventTransport__InputEventHubName` +- `EventTransport__OutputEventHubName` +- `EventTransport__ConsumerGroup` +- `EventTransport__EnableInputConsumer` +- `EventTransport__ConnectionString` or its managed-identity replacement + +Recommended end state: + +- non-secret values become plain Terraform env vars +- the connection string moves to a secret or is removed entirely by switching to managed identity +- the `ignore_changes` block is removed once Terraform fully owns the env set + +### 4. Import the live Container App + +The existing `deliverybot-robot-simulator` Container App already exists in Azure. + +When the root IaC is ready, import it into state before first apply: + +```powershell +terraform import module.simulator.azurerm_container_app.simulator /subscriptions//resourceGroups/ewu-deliverybotsystem-rg/providers/Microsoft.App/containerApps/deliverybot-robot-simulator +``` + +This avoids Terraform trying to create a duplicate resource. + +### 5. Align workflow deployment with Terraform ownership + +The current workflow updates the Container App image only. + +Later, it should be adjusted to match the final IaC ownership model: + +- keep image tag updates sourced from CI +- decide whether Terraform or the workflow is the deploy authority +- remove the image-only `az containerapp update` path if Terraform becomes the single source of truth + +### 6. Add project-wide conventions + +The final IaC will likely standardize: + +- provider configuration +- variable naming +- backend configuration +- tagging +- naming conventions +- secret handling + +The simulator module should be adapted to those conventions rather than preserved as a standalone exception. + +## What can stay the same + +- the simulator Container App sizing and ingress choices +- the existing Azure resource names +- the module's general variable intent +- the workflow trigger on `RobotSimulator/**` +- the image naming convention + +## Cleanup list when merging + +- remove simulator-local `providers.tf` +- remove simulator-local `terraform.tfvars` +- remove `lifecycle.ignore_changes` once env vars are managed in Terraform +- migrate state to remote backend +- import the live Container App +- update the workflow to match the final Terraform deployment flow + +## Goal + +The end state should be a single project-wide IaC stack where the simulator is just one module, not a separate deployment island. \ No newline at end of file diff --git a/docs/simulator-deployment.md b/docs/simulator-deployment.md new file mode 100644 index 0000000..3d320d0 --- /dev/null +++ b/docs/simulator-deployment.md @@ -0,0 +1,121 @@ +# Simulator Deployment + +## Overview + +The Robot Simulator is deployed as a Container App (`deliverybot-robot-simulator`) in the `ewu-deliverybotsystem-rg` resource group. Deployment is handled by a GitHub Actions workflow that builds a new container image and updates the running Container App image only. + +Infrastructure configuration for the simulator is codified in `Iac/simulator/`. + +--- + +## IaC Module (`Iac/simulator/`) + +The simulator Terraform module uses a **balanced ownership** approach. It owns the Container App's structural configuration while deliberately excluding the live Event Hub transport settings. + +### What this module owns + +| Resource | Managed properties | +|---|---| +| `azurerm_container_app` | Image reference, ingress (external, port 8080), revision mode (Single), scale (min 1 / max 1), system-assigned identity, ACR registry reference, `ASPNETCORE_ENVIRONMENT`, `ASPNETCORE_URLS` | + +### What this module does NOT own + +The following settings are currently managed outside IaC (via Azure Portal or CLI) and are intentionally excluded from the Terraform module: + +| Setting | Reason | +|---|---| +| `EventTransport__Mode` | Already configured on live Container App | +| `EventTransport__ConnectionString` | Secret — managed outside IaC | +| `EventTransport__InputEventHubName` | Already configured on live Container App | +| `EventTransport__OutputEventHubName` | Already configured on live Container App | +| `EventTransport__ConsumerGroup` | Already configured on live Container App | +| `EventTransport__EnableInputConsumer` | Already configured on live Container App | +| Container App secrets | Managed outside IaC | + +A `lifecycle { ignore_changes }` block in `main.tf` ensures Terraform does not overwrite these settings when applying changes. + +These settings will be brought under IaC management when the project-wide Terraform module is established. + +### Referenced existing resources + +These resources are read via `data` sources; they are not created or destroyed by this module: + +| Resource | Name | +|---|---| +| Resource group | `ewu-deliverybotsystem-rg` | +| Container Apps environment | `managedEnvironment-ewudeliverybots-aa2f` | +| Azure Container Registry | `DeliverybotCR` | +| Event Hub namespace | `DeliverybotSimulator-EVHNS` | + +### Running the module manually + +Prerequisites: +- Terraform >= 1.5 +- Azure CLI authenticated (`az login`) with sufficient permissions on `ewu-deliverybotsystem-rg` + +```powershell +cd Iac/simulator + +terraform init +terraform plan +terraform apply +``` + +To deploy a specific image tag: + +```powershell +terraform apply -var="image_tag=" +``` + +--- + +## GitHub Actions Workflow + +File: `.github/workflows/simulator-deploy.yml` + +### Trigger + +The workflow runs automatically on push to `main` when any file under `RobotSimulator/` changes. + +```yaml +on: + push: + branches: [main] + paths: + - "RobotSimulator/**" +``` + +### What the workflow does + +1. Checks out the repository +2. Authenticates to Azure using OIDC federated identity (no client secrets) +3. Logs in to `DeliverybotCR` container registry +4. Builds the simulator Docker image from: + - Dockerfile: `RobotSimulator/src/DeliveryBot.RobotSimulator.Api/Dockerfile` + - Build context: `RobotSimulator/` + - Tag: `deliverybotcr.azurecr.io/deliverybot-robot-simulator:` +5. Pushes the image to ACR +6. Updates the `deliverybot-robot-simulator` Container App image **only** +7. Prints the Container App URL and health endpoint + +### What the workflow does NOT change + +The workflow uses `az containerapp update --image` with no `--set-env-vars`. This means all existing environment variables and secrets on the live Container App (including Event Hub transport settings) are preserved exactly as-is after every deployment. + +### Required GitHub secrets + +These secrets must be configured in the repository before the workflow can run. They are already used by the existing `botNetApi-deploy.yml` workflow. + +| Secret | Description | +|---|---| +| `AZURE_CLIENT_ID` | Client ID of the Azure AD app registration used for OIDC | +| `AZURE_TENANT_ID` | Azure AD tenant ID | +| `AZURE_SUBSCRIPTION_ID` | Azure subscription ID | + +--- + +## Known Limitations + +- **Event Hub env vars are not in IaC.** If the Container App is recreated from scratch using this Terraform module, the Event Hub transport settings will not be present. They must be re-applied manually or via CLI until they are brought under IaC. +- **No Terraform remote state configured.** The module currently has no `backend` block. A remote backend (e.g., Azure Storage) should be added before team use. +- **Image tag in tfvars defaults to `latest`.** For production deployments, always supply the specific commit SHA as `image_tag`. diff --git a/readme.md b/readme.md index 9af78b1..a3a9caa 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,8 @@ Autonomous "food" delivery robots deliver refreshments to patrons wherever they This repo includes all solution components including the robot software, web applications, backend services and infrastructure as code. +![Delivvery Bot](./frontend/customer-webapp/public/hero.png) + ![Delivvery Bot](./frontend/public/hero.png) See the [definition of done](./docs/definition-of-done.md) for more details on how to complete user stories.