Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .github/workflows/AdminWebpage-Deploy-WF.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: AdminWebpage-Deploy-WF

# Provisions the Admin Web App App Service via Terraform, then builds and
# deploys the React SPA to it. Auth uses the same OIDC federated identity
# Phil configured for the BotNet API workflow, so no new secrets are needed.

on:
workflow_dispatch:
pull_request:
branches: [main]
paths:
- "admin-webapp/**"
- "Iac/admin-webapp/**"
- ".github/workflows/AdminWebpage-Deploy-WF.yml"
push:
branches: [main]
paths:
- "admin-webapp/**"
- "Iac/admin-webapp/**"
- ".github/workflows/AdminWebpage-Deploy-WF.yml"

permissions:
id-token: write
contents: read

env:
RESOURCE_GROUP: ewu-deliverybotsystem-rg
APP_SERVICE_NAME: WA-DeliveryBot-Admin-dev
TFSTATE_STORAGE_ACCOUNT: dbstfstate01
TFSTATE_CONTAINER: tfstate
BOTNET_API_URL: https://ewu-deliverybotsystem-api.mangocoast-332176b0.westus2.azurecontainerapps.io
SIMULATOR_API_URL: https://deliverybot-robot-simulator.mangocoast-332176b0.westus2.azurecontainerapps.io

jobs:
provision-and-deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

# ── 1. Authenticate to Azure via OIDC ────────────────────────────────
- 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 }}

# ── 2. Ensure the Terraform state container exists ────────────────────
# `az storage container create` is idempotent; safe to run every time.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor out and leverage common TF workflow

- name: Ensure TF state container exists
run: |
az storage container create \
--name "$TFSTATE_CONTAINER" \
--account-name "$TFSTATE_STORAGE_ACCOUNT" \
--auth-mode login \
--only-show-errors

# ── 3. Provision App Service via Terraform ────────────────────────────
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.9.5"

- name: Terraform Init
working-directory: ./Iac/admin-webapp
env:
ARM_USE_OIDC: "true"
ARM_USE_AZUREAD: "true"
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
run: terraform init -input=false

- name: Terraform Apply
working-directory: ./Iac/admin-webapp
env:
ARM_USE_OIDC: "true"
ARM_USE_AZUREAD: "true"
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
run: terraform apply -input=false -auto-approve

# ── 4. Build the SPA with upstream URLs baked in ──────────────────────
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.x"
cache: "npm"
cache-dependency-path: admin-webapp/package-lock.json

- name: Install dependencies
working-directory: ./admin-webapp
run: npm install

- name: Run unit tests
working-directory: ./admin-webapp
run: npm test

- name: Build React app
working-directory: ./admin-webapp
env:
VITE_BOTNET_API_URL: ${{ env.BOTNET_API_URL }}
VITE_SIMULATOR_API_URL: ${{ env.SIMULATOR_API_URL }}
run: npm run build

# ── 5. Deploy the build to the App Service ────────────────────────────
- name: Deploy to Azure App Service
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.APP_SERVICE_NAME }}
package: ./admin-webapp/dist

- name: Print deployment URL
run: |
FQDN=$(az webapp show \
--name "$APP_SERVICE_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query defaultHostName -o tsv)
echo "========================================"
echo " Admin Web App deployed!"
echo " URL: https://${FQDN}"
echo "========================================"
67 changes: 67 additions & 0 deletions Iac/admin-webapp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Admin Web App — Terraform

Provisions the Azure App Service that hosts the [Admin & Maintenance App](../../admin-webapp/) (issue #18).

## Structure

Per the [project guidelines](../../docs/project-guidelines.md), the App Service is defined in a reusable module that the root config consumes.

```
Iac/admin-webapp/
├── providers.tf # terraform block, azurerm backend + provider (root only)
├── main.tf # calls module "admin_webapp"
├── variables.tf # root inputs + defaults
├── outputs.tf # re-exports the module's outputs
└── modules/
└── webapp/ # reusable App Service module
├── main.tf # data sources (RG, plan) + azurerm_linux_web_app
├── variables.tf # module inputs
└── outputs.tf # name, hostname, url
```

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).

## What it creates

| Resource | Notes |
|---|---|
| `module.admin_webapp.azurerm_linux_web_app.admin` | `WA-DeliveryBot-Admin-dev`, Node 22 Linux, `pm2 serve` startup, System Assigned Managed Identity |

## What it reuses (data sources, not managed)

| Resource | Why |
|---|---|
| `azurerm_resource_group.rg` (`ewu-deliverybotsystem-rg`) | Team's shared RG |
| `azurerm_service_plan.plan` (`ASP-RGDeliveryBotdev-8b82`) | Shared with Customer site — no duplicate plan cost |

## State

Stored in Azure Blob:

- Storage account: `dbstfstate01` (pre-existing in the RG)
- Container: `tfstate`
- Key: `admin-webapp.tfstate` (unique to this module — won't collide with PR #74's shared Iac)

## How it runs

The [`AdminWebpage-Deploy-WF.yml`](../../.github/workflows/AdminWebpage-Deploy-WF.yml) workflow:

1. Authenticates to Azure via OIDC federated identity (existing repo secrets `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`)
2. Ensures the `tfstate` container exists in `dbstfstate01`
3. Runs `terraform init` + `apply -auto-approve` against this directory
4. Builds the React app and deploys to the App Service Terraform just created/updated

## Local execution (rarely needed)

If you want to run this locally you'll need `terraform`, `az` CLI, and an Azure session:

```bash
az login
terraform init
terraform plan
terraform apply
```

## Migration note

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.
24 changes: 24 additions & 0 deletions Iac/admin-webapp/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Root configuration for the Admin & Maintenance App infrastructure.
#
# Composes the reusable ./modules/webapp module. Backend + provider config
# live in providers.tf; inputs and their defaults live in variables.tf.

module "admin_webapp" {
source = "./modules/webapp"

resource_group_name = var.resource_group_name
app_service_plan_name = var.app_service_plan_name
app_service_name = var.app_service_name
node_version = var.node_version
botnet_api_url = var.botnet_api_url
simulator_api_url = var.simulator_api_url
tags = var.tags
}

# The App Service was originally declared at the root before the module
# refactor. Tell Terraform it simply moved addresses so the existing live
# resource is preserved instead of destroyed and recreated.
moved {
from = azurerm_linux_web_app.admin
to = module.admin_webapp.azurerm_linux_web_app.admin
}
65 changes: 65 additions & 0 deletions Iac/admin-webapp/modules/webapp/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Reusable module: a Linux App Service that hosts a static SPA via pm2.
#
# Reuses an existing resource group and App Service Plan (passed by name) so
# the team isn't billed for a duplicate plan. The only managed resource is the
# App Service itself.

terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}

data "azurerm_resource_group" "rg" {
name = var.resource_group_name
}

data "azurerm_service_plan" "plan" {
name = var.app_service_plan_name
resource_group_name = data.azurerm_resource_group.rg.name
}

resource "azurerm_linux_web_app" "admin" {
name = var.app_service_name
resource_group_name = data.azurerm_resource_group.rg.name
location = data.azurerm_service_plan.plan.location
service_plan_id = data.azurerm_service_plan.plan.id
https_only = true

identity {
type = "SystemAssigned"
}

site_config {
always_on = false
app_command_line = "pm2 serve /home/site/wwwroot --no-daemon --spa"

application_stack {
node_version = var.node_version
}

# Allow the GitHub Actions workflow to push builds.
scm_use_main_ip_restriction = true
}

# Build-time URLs are baked into the SPA bundle, so these app settings
# exist mainly as a record of which upstreams this deployment talks to.
# If the SPA gains a runtime config layer, switch to reading these.
app_settings = {
"WEBSITE_NODE_DEFAULT_VERSION" = "~22"
"BOTNET_API_URL" = var.botnet_api_url
"SIMULATOR_API_URL" = var.simulator_api_url
}

tags = var.tags

lifecycle {
ignore_changes = [
# Deployments overwrite the build artifact; don't fight the workflow.
app_settings["WEBSITE_RUN_FROM_PACKAGE"],
]
}
}
14 changes: 14 additions & 0 deletions Iac/admin-webapp/modules/webapp/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
output "app_service_name" {
description = "Name of the provisioned App Service."
value = azurerm_linux_web_app.admin.name
}

output "default_hostname" {
description = "Default hostname of the App Service."
value = azurerm_linux_web_app.admin.default_hostname
}

output "app_url" {
description = "HTTPS URL of the App Service."
value = "https://${azurerm_linux_web_app.admin.default_hostname}"
}
35 changes: 35 additions & 0 deletions Iac/admin-webapp/modules/webapp/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
variable "resource_group_name" {
description = "Resource group that hosts the team's DeliveryBot resources."
type = string
}

variable "app_service_plan_name" {
description = "Existing App Service Plan to reuse (shared with the Customer site to keep cost down)."
type = string
}

variable "app_service_name" {
description = "Globally-unique name for the App Service."
type = string
}

variable "node_version" {
description = "Node runtime version used by the SPA host (pm2 serve)."
type = string
}

variable "botnet_api_url" {
description = "Public URL of the BotNet API (Container App), baked into the SPA at build time."
type = string
}

variable "simulator_api_url" {
description = "Public URL of the Robot Simulator (Container App), baked into the SPA at build time."
type = string
}

variable "tags" {
description = "Common tags applied to the App Service."
type = map(string)
default = {}
}
14 changes: 14 additions & 0 deletions Iac/admin-webapp/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
output "app_service_name" {
description = "Name of the provisioned App Service."
value = module.admin_webapp.app_service_name
}

output "default_hostname" {
description = "Default hostname of the Admin Web App."
value = module.admin_webapp.default_hostname
}

output "app_url" {
description = "HTTPS URL of the Admin Web App."
value = module.admin_webapp.app_url
}
37 changes: 37 additions & 0 deletions Iac/admin-webapp/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Provider + state backend for the Admin Web App App Service.
#
# Auth: assumed to be set by the surrounding GitHub Actions workflow via
# `azure/login@v2` (OIDC) and the ARM_USE_OIDC / ARM_USE_AZUREAD env vars.
# State: lives in the team's pre-existing storage account `dbstfstate01`
# under a unique key so we don't collide with Bill's root-level Iac (#74).

terraform {
required_version = ">= 1.6.0"

required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}

backend "azurerm" {
resource_group_name = "ewu-deliverybotsystem-rg"
storage_account_name = "dbstfstate01"
container_name = "tfstate"
key = "admin-webapp.tfstate"
use_oidc = true
use_azuread_auth = true
}
}

provider "azurerm" {
features {}
use_oidc = true

# The CI service principal has scoped roles (RG Contributor + Blob Data
# Contributor) but no subscription-level resource-provider registration
# rights. Microsoft.Web is already registered for the subscription, so skip
# the provider's default auto-registration to avoid a 403 on apply.
resource_provider_registrations = "none"
}
Loading
Loading