Skip to content

Commit ba880d5

Browse files
authored
Merge pull request #82 from IntelliTect-Samples/admin-and-maintenance-app
Admin & Maintenance App: bot fleet CRUD
2 parents 2da694e + 250c14f commit ba880d5

30 files changed

Lines changed: 6938 additions & 0 deletions
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: AdminWebpage-Deploy-WF
2+
3+
# Provisions the Admin Web App App Service via Terraform, then builds and
4+
# deploys the React SPA to it. Auth uses the same OIDC federated identity
5+
# Phil configured for the BotNet API workflow, so no new secrets are needed.
6+
7+
on:
8+
workflow_dispatch:
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- "admin-webapp/**"
13+
- "Iac/admin-webapp/**"
14+
- ".github/workflows/AdminWebpage-Deploy-WF.yml"
15+
push:
16+
branches: [main]
17+
paths:
18+
- "admin-webapp/**"
19+
- "Iac/admin-webapp/**"
20+
- ".github/workflows/AdminWebpage-Deploy-WF.yml"
21+
22+
permissions:
23+
id-token: write
24+
contents: read
25+
26+
env:
27+
RESOURCE_GROUP: ewu-deliverybotsystem-rg
28+
APP_SERVICE_NAME: WA-DeliveryBot-Admin-dev
29+
TFSTATE_STORAGE_ACCOUNT: dbstfstate01
30+
TFSTATE_CONTAINER: tfstate
31+
BOTNET_API_URL: https://ewu-deliverybotsystem-api.mangocoast-332176b0.westus2.azurecontainerapps.io
32+
SIMULATOR_API_URL: https://deliverybot-robot-simulator.mangocoast-332176b0.westus2.azurecontainerapps.io
33+
34+
jobs:
35+
provision-and-deploy:
36+
runs-on: ubuntu-latest
37+
38+
steps:
39+
- name: Checkout repository
40+
uses: actions/checkout@v4
41+
42+
# ── 1. Authenticate to Azure via OIDC ────────────────────────────────
43+
- name: Azure Login (OIDC)
44+
uses: azure/login@v2
45+
with:
46+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
47+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
48+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
49+
50+
# ── 2. Ensure the Terraform state container exists ────────────────────
51+
# `az storage container create` is idempotent; safe to run every time.
52+
- name: Ensure TF state container exists
53+
run: |
54+
az storage container create \
55+
--name "$TFSTATE_CONTAINER" \
56+
--account-name "$TFSTATE_STORAGE_ACCOUNT" \
57+
--auth-mode login \
58+
--only-show-errors
59+
60+
# ── 3. Provision App Service via Terraform ────────────────────────────
61+
- name: Setup Terraform
62+
uses: hashicorp/setup-terraform@v3
63+
with:
64+
terraform_version: "1.9.5"
65+
66+
- name: Terraform Init
67+
working-directory: ./Iac/admin-webapp
68+
env:
69+
ARM_USE_OIDC: "true"
70+
ARM_USE_AZUREAD: "true"
71+
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
72+
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
73+
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
74+
run: terraform init -input=false
75+
76+
- name: Terraform Apply
77+
working-directory: ./Iac/admin-webapp
78+
env:
79+
ARM_USE_OIDC: "true"
80+
ARM_USE_AZUREAD: "true"
81+
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
82+
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
83+
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
84+
run: terraform apply -input=false -auto-approve
85+
86+
# ── 4. Build the SPA with upstream URLs baked in ──────────────────────
87+
- name: Setup Node.js
88+
uses: actions/setup-node@v4
89+
with:
90+
node-version: "22.x"
91+
cache: "npm"
92+
cache-dependency-path: admin-webapp/package-lock.json
93+
94+
- name: Install dependencies
95+
working-directory: ./admin-webapp
96+
run: npm install
97+
98+
- name: Run unit tests
99+
working-directory: ./admin-webapp
100+
run: npm test
101+
102+
- name: Build React app
103+
working-directory: ./admin-webapp
104+
env:
105+
VITE_BOTNET_API_URL: ${{ env.BOTNET_API_URL }}
106+
VITE_SIMULATOR_API_URL: ${{ env.SIMULATOR_API_URL }}
107+
run: npm run build
108+
109+
# ── 5. Deploy the build to the App Service ────────────────────────────
110+
- name: Deploy to Azure App Service
111+
uses: azure/webapps-deploy@v3
112+
with:
113+
app-name: ${{ env.APP_SERVICE_NAME }}
114+
package: ./admin-webapp/dist
115+
116+
- name: Print deployment URL
117+
run: |
118+
FQDN=$(az webapp show \
119+
--name "$APP_SERVICE_NAME" \
120+
--resource-group "$RESOURCE_GROUP" \
121+
--query defaultHostName -o tsv)
122+
echo "========================================"
123+
echo " Admin Web App deployed!"
124+
echo " URL: https://${FQDN}"
125+
echo "========================================"

Iac/admin-webapp/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Admin Web App — Terraform
2+
3+
Provisions the Azure App Service that hosts the [Admin & Maintenance App](../../admin-webapp/) (issue #18).
4+
5+
## Structure
6+
7+
Per the [project guidelines](../../docs/project-guidelines.md), the App Service is defined in a reusable module that the root config consumes.
8+
9+
```
10+
Iac/admin-webapp/
11+
├── providers.tf # terraform block, azurerm backend + provider (root only)
12+
├── main.tf # calls module "admin_webapp"
13+
├── variables.tf # root inputs + defaults
14+
├── outputs.tf # re-exports the module's outputs
15+
└── modules/
16+
└── webapp/ # reusable App Service module
17+
├── main.tf # data sources (RG, plan) + azurerm_linux_web_app
18+
├── variables.tf # module inputs
19+
└── outputs.tf # name, hostname, url
20+
```
21+
22+
The root `main.tf` includes a `moved {}` block so the refactor into a module is a no-op against existing state (the live App Service is preserved, not recreated).
23+
24+
## What it creates
25+
26+
| Resource | Notes |
27+
|---|---|
28+
| `module.admin_webapp.azurerm_linux_web_app.admin` | `WA-DeliveryBot-Admin-dev`, Node 22 Linux, `pm2 serve` startup, System Assigned Managed Identity |
29+
30+
## What it reuses (data sources, not managed)
31+
32+
| Resource | Why |
33+
|---|---|
34+
| `azurerm_resource_group.rg` (`ewu-deliverybotsystem-rg`) | Team's shared RG |
35+
| `azurerm_service_plan.plan` (`ASP-RGDeliveryBotdev-8b82`) | Shared with Customer site — no duplicate plan cost |
36+
37+
## State
38+
39+
Stored in Azure Blob:
40+
41+
- Storage account: `dbstfstate01` (pre-existing in the RG)
42+
- Container: `tfstate`
43+
- Key: `admin-webapp.tfstate` (unique to this module — won't collide with PR #74's shared Iac)
44+
45+
## How it runs
46+
47+
The [`AdminWebpage-Deploy-WF.yml`](../../.github/workflows/AdminWebpage-Deploy-WF.yml) workflow:
48+
49+
1. Authenticates to Azure via OIDC federated identity (existing repo secrets `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`)
50+
2. Ensures the `tfstate` container exists in `dbstfstate01`
51+
3. Runs `terraform init` + `apply -auto-approve` against this directory
52+
4. Builds the React app and deploys to the App Service Terraform just created/updated
53+
54+
## Local execution (rarely needed)
55+
56+
If you want to run this locally you'll need `terraform`, `az` CLI, and an Azure session:
57+
58+
```bash
59+
az login
60+
terraform init
61+
terraform plan
62+
terraform apply
63+
```
64+
65+
## Migration note
66+
67+
This module deliberately stores its state in a unique key (`admin-webapp.tfstate`) rather than depending on Bill's PR #74 backend config. Once #74 lands and the team agrees on a backend convention, switch [`providers.tf`](providers.tf) to consume the shared backend.

Iac/admin-webapp/main.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Root configuration for the Admin & Maintenance App infrastructure.
2+
#
3+
# Composes the reusable ./modules/webapp module. Backend + provider config
4+
# live in providers.tf; inputs and their defaults live in variables.tf.
5+
6+
module "admin_webapp" {
7+
source = "./modules/webapp"
8+
9+
resource_group_name = var.resource_group_name
10+
app_service_plan_name = var.app_service_plan_name
11+
app_service_name = var.app_service_name
12+
node_version = var.node_version
13+
botnet_api_url = var.botnet_api_url
14+
simulator_api_url = var.simulator_api_url
15+
tags = var.tags
16+
}
17+
18+
# The App Service was originally declared at the root before the module
19+
# refactor. Tell Terraform it simply moved addresses so the existing live
20+
# resource is preserved instead of destroyed and recreated.
21+
moved {
22+
from = azurerm_linux_web_app.admin
23+
to = module.admin_webapp.azurerm_linux_web_app.admin
24+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Reusable module: a Linux App Service that hosts a static SPA via pm2.
2+
#
3+
# Reuses an existing resource group and App Service Plan (passed by name) so
4+
# the team isn't billed for a duplicate plan. The only managed resource is the
5+
# App Service itself.
6+
7+
terraform {
8+
required_providers {
9+
azurerm = {
10+
source = "hashicorp/azurerm"
11+
version = "~> 4.0"
12+
}
13+
}
14+
}
15+
16+
data "azurerm_resource_group" "rg" {
17+
name = var.resource_group_name
18+
}
19+
20+
data "azurerm_service_plan" "plan" {
21+
name = var.app_service_plan_name
22+
resource_group_name = data.azurerm_resource_group.rg.name
23+
}
24+
25+
resource "azurerm_linux_web_app" "admin" {
26+
name = var.app_service_name
27+
resource_group_name = data.azurerm_resource_group.rg.name
28+
location = data.azurerm_service_plan.plan.location
29+
service_plan_id = data.azurerm_service_plan.plan.id
30+
https_only = true
31+
32+
identity {
33+
type = "SystemAssigned"
34+
}
35+
36+
site_config {
37+
always_on = false
38+
app_command_line = "pm2 serve /home/site/wwwroot --no-daemon --spa"
39+
40+
application_stack {
41+
node_version = var.node_version
42+
}
43+
44+
# Allow the GitHub Actions workflow to push builds.
45+
scm_use_main_ip_restriction = true
46+
}
47+
48+
# Build-time URLs are baked into the SPA bundle, so these app settings
49+
# exist mainly as a record of which upstreams this deployment talks to.
50+
# If the SPA gains a runtime config layer, switch to reading these.
51+
app_settings = {
52+
"WEBSITE_NODE_DEFAULT_VERSION" = "~22"
53+
"BOTNET_API_URL" = var.botnet_api_url
54+
"SIMULATOR_API_URL" = var.simulator_api_url
55+
}
56+
57+
tags = var.tags
58+
59+
lifecycle {
60+
ignore_changes = [
61+
# Deployments overwrite the build artifact; don't fight the workflow.
62+
app_settings["WEBSITE_RUN_FROM_PACKAGE"],
63+
]
64+
}
65+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
output "app_service_name" {
2+
description = "Name of the provisioned App Service."
3+
value = azurerm_linux_web_app.admin.name
4+
}
5+
6+
output "default_hostname" {
7+
description = "Default hostname of the App Service."
8+
value = azurerm_linux_web_app.admin.default_hostname
9+
}
10+
11+
output "app_url" {
12+
description = "HTTPS URL of the App Service."
13+
value = "https://${azurerm_linux_web_app.admin.default_hostname}"
14+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
variable "resource_group_name" {
2+
description = "Resource group that hosts the team's DeliveryBot resources."
3+
type = string
4+
}
5+
6+
variable "app_service_plan_name" {
7+
description = "Existing App Service Plan to reuse (shared with the Customer site to keep cost down)."
8+
type = string
9+
}
10+
11+
variable "app_service_name" {
12+
description = "Globally-unique name for the App Service."
13+
type = string
14+
}
15+
16+
variable "node_version" {
17+
description = "Node runtime version used by the SPA host (pm2 serve)."
18+
type = string
19+
}
20+
21+
variable "botnet_api_url" {
22+
description = "Public URL of the BotNet API (Container App), baked into the SPA at build time."
23+
type = string
24+
}
25+
26+
variable "simulator_api_url" {
27+
description = "Public URL of the Robot Simulator (Container App), baked into the SPA at build time."
28+
type = string
29+
}
30+
31+
variable "tags" {
32+
description = "Common tags applied to the App Service."
33+
type = map(string)
34+
default = {}
35+
}

Iac/admin-webapp/outputs.tf

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
output "app_service_name" {
2+
description = "Name of the provisioned App Service."
3+
value = module.admin_webapp.app_service_name
4+
}
5+
6+
output "default_hostname" {
7+
description = "Default hostname of the Admin Web App."
8+
value = module.admin_webapp.default_hostname
9+
}
10+
11+
output "app_url" {
12+
description = "HTTPS URL of the Admin Web App."
13+
value = module.admin_webapp.app_url
14+
}

Iac/admin-webapp/providers.tf

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Provider + state backend for the Admin Web App App Service.
2+
#
3+
# Auth: assumed to be set by the surrounding GitHub Actions workflow via
4+
# `azure/login@v2` (OIDC) and the ARM_USE_OIDC / ARM_USE_AZUREAD env vars.
5+
# State: lives in the team's pre-existing storage account `dbstfstate01`
6+
# under a unique key so we don't collide with Bill's root-level Iac (#74).
7+
8+
terraform {
9+
required_version = ">= 1.6.0"
10+
11+
required_providers {
12+
azurerm = {
13+
source = "hashicorp/azurerm"
14+
version = "~> 4.0"
15+
}
16+
}
17+
18+
backend "azurerm" {
19+
resource_group_name = "ewu-deliverybotsystem-rg"
20+
storage_account_name = "dbstfstate01"
21+
container_name = "tfstate"
22+
key = "admin-webapp.tfstate"
23+
use_oidc = true
24+
use_azuread_auth = true
25+
}
26+
}
27+
28+
provider "azurerm" {
29+
features {}
30+
use_oidc = true
31+
32+
# The CI service principal has scoped roles (RG Contributor + Blob Data
33+
# Contributor) but no subscription-level resource-provider registration
34+
# rights. Microsoft.Web is already registered for the subscription, so skip
35+
# the provider's default auto-registration to avoid a 403 on apply.
36+
resource_provider_registrations = "none"
37+
}

0 commit comments

Comments
 (0)