Migrate hosting from Azure Container Apps to Azure App Service - #511
Migrate hosting from Azure Container Apps to Azure App Service#511rvanmaanen wants to merge 5 commits into
Conversation
Summary: Replaces two pay-as-you-go Container Apps (plus their Container Apps Environment and Standard Load Balancer, ~€16/mo) with a single Basic B1 App Service Plan hosting the API and Web sites as separate Web Apps for Containers, and a shared Basic B1 Plan for PR previews. Cuts recurring Azure cost while keeping VNet isolation, managed identity auth, and health checks. Implementation: - Added infra/modules/appServicePlan.bicep + rewrote api.bicep/web.bicep as Web Apps for Containers (VNet-integrated, alwaysOn, healthCheckPath, API locked to the Web app's subnet via ipSecurityRestrictions); network.bicep gained snet-app-service and snet-app-service-pr subnets (1 subnet : 1 Plan) - Rewrote pr-applications.bicep onto a shared, persistent PR App Service Plan with deterministic *.azurewebsites.net hostnames, removing all Container-Apps-environment-domain lookup logic - Updated deploy scripts (Deploy-Application(s).ps1, Deploy-PrPreview.ps1, Teardown-PrEnvironments.ps1, Renew-WildcardCertificates.ps1, Setup-UserSecrets.ps1) to use az webapp/appsettings instead of az containerapp; wildcardCert.bicep is now redeployed standalone per domain on renewal since Microsoft.Web/certificates doesn't auto-refresh from KV - Redesigned ci.yml so PR preview build/deploy/E2E only run on manual workflow_dispatch (pr_number input) since the PR Plan is now shared and persistent rather than one environment per PR; quality-gate jobs stay on pull_request as before - Removed dead containerApps.bicep/wildcardCertificates.bicep modules and the now-inapplicable Container-Apps restart-storm log alert; updated infra/AGENTS.md, scripts/AGENTS.md, and docs (repository-structure, network-architecture, ci-cd-pipeline, wildcard-certificates, health-checks) to describe the App Service model Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Summary: The infrastructure was migrated from Azure Container Apps to Azure App Service, but code comments, docs, and Bicep templates still referenced the old platform. This updates all remaining mentions to keep documentation and in-code explanations accurate. Implementation: - Reworded Container Apps-specific comments in Program.cs, ServiceDefaults, health checks, and E2E test helpers to describe App Service concepts (health-check path, always-on, clientAffinityEnabled, App Service Plan capacity) instead of replicas/probes/scale-to-zero - Removed the unused logAnalyticsWorkspaceId parameter from alerts.bicep and its former Container App restart-storm log query alert, no longer applicable on App Service - Updated README.md, technology-stack.md, admin-authentication.md, and url-routing.md to describe Azure App Service instead of Container Apps
There was a problem hiding this comment.
Pull request overview
Migrates TechHub’s hosting model from Azure Container Apps to Azure App Service (Web Apps for Containers) while keeping the existing container-image deployment approach, updating IaC, scripts, CI/CD, tests, and docs to reflect the new platform primitives (plans/sites, VNet integration, access restrictions, health-check path, client affinity, Key Vault references).
Changes:
- Replace Container Apps resources with App Service Plans + Web Apps for Containers, including VNet integration, access restrictions, health checks, and Key Vault reference app settings.
- Update deployment automation and GitHub Actions workflows to use
az webappflows and make PR preview deploy/E2E manual-only viaworkflow_dispatch. - Update application comments, tests, and documentation to reflect App Service behavior (always-on/healthCheckPath/client affinity) instead of Container Apps probes/replicas/scale-to-zero.
Reviewed changes
Copilot reviewed 52 out of 52 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/TechHub.E2E.Tests/Web/SectionCardCustomPagesTests.cs | Updates sticky-session/circuit messaging to App Service client affinity terminology. |
| tests/TechHub.E2E.Tests/Helpers/NetworkThrottling.cs | Updates WAN profile documentation to reference App Service targets. |
| tests/TechHub.E2E.Tests/Helpers/BlazorHelpers.cs | Adjusts navigation retry rationale for App Service deploy warmup behavior. |
| tests/TechHub.E2E.Tests/AGENTS.md | Updates E2E network profile documentation for App Service WAN simulation. |
| src/TechHub.Web/Services/SectionCacheHealthCheck.cs | Updates readiness/health-check commentary for App Service. |
| src/TechHub.Web/Program.cs | Updates health-check and forwarded-headers comments to App Service reverse proxy model/capacity assumptions. |
| src/TechHub.ServiceDefaults/Extensions.cs | Updates OpenTelemetry filtering/enrichment comments for App Service health checks and proxy IP behavior. |
| src/TechHub.Infrastructure/Services/AiCompletionClient.cs | Updates managed identity hosting references to App Service. |
| src/TechHub.Infrastructure/Data/NpgsqlDataSourceFactory.cs | Updates managed identity hosting reference to App Service. |
| src/TechHub.Api/Program.cs | Updates forwarded header trust comment to App Service proxy model. |
| scripts/Wait-ForLiveVersion.ps1 | Updates version-poll/warmup documentation and examples for App Service hostnames. |
| scripts/TechHubRunner.psm1 | Updates “production-like” hosting reference to App Service. |
| scripts/Teardown-PrEnvironments.ps1 | Switches PR environment discovery/deletion from Container Apps to App Service sites. |
| scripts/Sync-KeyVaultSecrets.ps1 | Updates description/output messaging for App Service Key Vault reference behavior. |
| scripts/Setup-UserSecrets.ps1 | Switches production config fetch from Container Apps env-vars to App Service app settings. |
| scripts/Renew-WildcardCertificates.ps1 | Reworks wildcard renewal to redeploy Microsoft.Web/certificates imports tied to an App Service Plan. |
| scripts/Migrate-KeyVaultSecrets.ps1 | Updates one-time migration guidance for App Service deployment order. |
| scripts/Deploy-PrPreview.ps1 | Reworks PR preview lifecycle to deploy App Service sites (and diagnostics) instead of Container Apps. |
| scripts/Deploy-Infrastructure.ps1 | Updates documentation to clarify infra phase excludes App Service sites. |
| scripts/Deploy-Applications.ps1 | Updates Phase 2 deployment messaging for App Service sites. |
| scripts/Deploy-Application.ps1 | Reworks fast-path deployment from az containerapp update to az webapp config set + app settings. |
| scripts/AGENTS.md | Updates script catalog entries to App Service equivalents. |
| README.md | Updates infra stack reference from Container Apps to App Service. |
| infra/pr-applications.bicep | Reworks PR preview IaC to App Service plan/sites + VNet integration subnet + tags. |
| infra/parameters/prod-infrastructure.bicepparam | Updates prod infra parameters for App Service plans and new subnets. |
| infra/parameters/prod-applications.bicepparam | Updates prod apps parameters for App Service plan + app names. |
| infra/modules/wildcardCertificates.bicep | Removes Container Apps wildcard-cert orchestration module. |
| infra/modules/wildcardCert.bicep | Changes wildcard cert import target from CAE certificates to Microsoft.Web/certificates via Key Vault. |
| infra/modules/web.bicep | Replaces Container App web module with App Service site configuration (container settings, KV refs, VNet integration, hostname bindings). |
| infra/modules/postgres.bicep | Updates comments to reflect App Service/private endpoint usage. |
| infra/modules/openai.bicep | Updates comments to reflect App Service/private endpoint usage. |
| infra/modules/network.bicep | Replaces Container Apps subnet with App Service plan VNet-integration subnets (prod + PR). |
| infra/modules/monitoring.bicep | Updates commentary about server-side telemetry source (App Service). |
| infra/modules/kvSecretsUserRole.bicep | Updates role assignment comment for App Service usage. |
| infra/modules/keyVault.bicep | Updates comments about private endpoint usage for App Service. |
| infra/modules/containerApps.bicep | Removes Container Apps Environment module. |
| infra/modules/appServicePlan.bicep | Adds shared App Service Plan module (Basic B1 Linux). |
| infra/modules/api.bicep | Replaces Container App API module with App Service site config (KV refs, VNet route-all, CORS, IP restrictions, VNet integration). |
| infra/modules/alerts.bicep | Removes Container Apps restart-storm alert and LAW param dependency. |
| infra/infrastructure.bicep | Replaces CAE deployment with App Service Plan(s) provisioning and updated networking outputs. |
| infra/applications.bicep | Reworks Phase 2 orchestration for App Service sites (plan + VNet integration subnet + cert binding flow). |
| infra/AGENTS.md | Updates infra-phase guidance to reflect App Service Plan/sites split. |
| docs/wildcard-certificates.md | Updates certificate import/binding/renewal model for App Service. |
| docs/url-routing.md | Updates forwarded-header and probe/health-check references for App Service. |
| docs/technology-stack.md | Updates infra stack listing. |
| docs/repository-structure.md | Updates repo structure descriptions for App Service IaC/modules. |
| docs/network-architecture.md | Updates network architecture diagrams and access model for App Service plans/sites and subnets. |
| docs/health-checks.md | Updates health-check documentation to App Service healthCheckPath/alwaysOn model and tradeoffs. |
| docs/ci-cd-pipeline.md | Updates CI/CD docs for App Service deployment model and manual PR preview dispatch. |
| docs/admin-authentication.md | Updates secret/app-setting flow from Container Apps env vars to App Service app settings/KV refs. |
| .github/workflows/pr-env-nightly-teardown.yml | Updates nightly teardown discovery from Container Apps to App Service sites. |
| .github/workflows/ci.yml | Adds manual workflow_dispatch PR preview flow; keeps quality gate on PR events; updates URLs and teardown triggers. |
Suppressed comments (1)
scripts/Teardown-PrEnvironments.ps1:128
- Same issue here: suppressing stderr on the PostgreSQL list risks silently skipping the scan when Azure CLI fails, leaving orphaned PR databases behind.
Summary: Two E2E test groups assumed recent content would always exist under the app's default/explicit date filters. The local dev database is a periodically refreshed snapshot that can lag behind the system clock, so narrow date windows returned zero or too-few items, causing 5 deterministic failures. Also fixed a markdownlint MD012 violation. Implementation: - DateRangeSliderTests: widen DateRangeSlider_PreservesDateInTagUrl's date window from 30 to 90 days to match the app's own default-filter assumption, guaranteeing the tag cloud has content to click - LoadMoreButtonTests: add explicit wide (3-year) date range query params to all /ai/blogs navigations so the Load More button reliably appears regardless of default 90-day filter content freshness - docs/network-architecture.md: remove trailing blank line to satisfy markdownlint MD012
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 54 out of 54 changed files in this pull request and generated no new comments.
Suppressed comments (4)
scripts/Deploy-Application.ps1:94
- The banner output has a duplicated word ("Web Web App"), which looks like a copy/paste typo and makes script output harder to scan.
infra/modules/api.bicep:59 - In this API module, the tags parameter description says "Tags applied to the Web App", which is misleading (this module deploys the API site). This can cause confusion when reusing the module or wiring parameters.
@description('Tags applied to the Web App')
param tags object = {}
infra/modules/api.bicep:65
- The fallback for newsletterWebsiteBaseUrl uses the API siteName (https://.azurewebsites.net), which is not a valid public website base URL and would produce incorrect links if webFqdns is ever omitted/empty. Since all current callers pass a non-empty webFqdns, it’s safer to fail fast by always taking webFqdns[0].
var newsletterWebsiteBaseUrl = !empty(webFqdns) ? 'https://${webFqdns[0]}' : 'https://${siteName}.azurewebsites.net'
.github/workflows/ci.yml:565
- workflow_dispatch pr_number is used to form refs/pull/<pr_number>/head and passed into Deploy-PrPreview.ps1. With no validation, a non-numeric input yields a confusing checkout/deploy failure. Add an explicit numeric check before the first checkout so errors are clear and early.
- name: Checkout PR head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: refs/pull/${{ inputs.pr_number }}/head
…airs, fix banner typo, clarify network docs, update AGENTS.md KV ref syntax
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 54 out of 54 changed files in this pull request and generated no new comments.
Suppressed comments (10)
scripts/Deploy-Application.ps1:208
- This
az webapp showcall suppresses stderr via2>$null. If the CLI fails, the script reports only "Could not retrieve Web App hostname" without showing the underlying Azure CLI error. Prefer--only-show-errors(and avoid redirecting stderr away) to keep failures actionable.
infra/applications.bicep:205 - Typo in the section header comment: "Web Web App" → "Web App".
// Web Web App
infra/modules/api.bicep:67
corsOriginsincludeshttps://*.azurewebsites.net, which is both broader than needed (you already pass explicitwebFqdns) and may not be accepted by App Service CORS settings (they typically expect concrete origins). Using only the explicit web FQDNs keeps the rule deterministic and avoids potential deployment/runtime CORS issues.
var corsOrigins = union(['https://*.azurewebsites.net'], customOrigins)
scripts/Deploy-Application.ps1:154
- This
az webapp showcall suppresses stderr via2>$null. If the CLI fails (RBAC/subscription/app missing), the script will silently skip the API health gate and proceed. Prefer surfacing errors (e.g.--only-show-errors) so a hostname lookup failure is visible and diagnosable.
This issue also appears on line 204 of the same file.
infra/applications.bicep:26
- Typo in the parameter description: "Web Web App name" → "Web App name".
This issue also appears on line 205 of the same file.
@description('Web Web App name')
infra/AGENTS.md:37
- Typo in the Phase 2 inventory list: "Web Web App" → "Web App".
- Web Web App (Web App for Containers, public-facing)
infra/AGENTS.md:144
keyVaultUrlis a Container Apps concept; this section is describing App Service. To avoid confusing future contributors, describe Key Vault reference app settings (@Microsoft.KeyVault(SecretUri=...)) instead.
- **Key Vault** - App Service sites reference secrets via `keyVaultUrl` (Key Vault references) with managed identity
infra/modules/kvSecretsUserRole.bicep:11
- This comment is now inaccurate for the App Service model: Key Vault Secrets User is used broadly for Key Vault reference resolution (app settings), not specifically for "certificate secrets". Updating the comment avoids implying the wrong dependency.
// Key Vault Secrets User — allows reading certificate secrets (needed by App Service sites)
docs/wildcard-certificates.md:74
- This bullet misstates which identity needs Key Vault access for certificate import.
Microsoft.Web/certificatesimports are performed by the App Service certificate provider (see the note ininfra/modules/wildcardCert.bicep), not by the app's managed identity.
- **Key Vault access**: `infra/modules/kvSecretsUserRole.bicep` grants the managed identity Key Vault Secrets User role so it can read certificate secrets
infra/AGENTS.md:36
- This sentence ties API reachability to
WEBSITES_PORT, butWEBSITES_PORTonly tells App Service which container port to route to; it doesn’t explain the actual network isolation (VNet integration +ipSecurityRestrictions). Updating the wording avoids implyingWEBSITES_PORTis a security/control mechanism.
- API Web App (Web App for Containers, VNet-integrated, not publicly accessible — reached only via the Web app's `WEBSITES_PORT`/private call)
Migrate hosting from Azure Container Apps to Azure App Service
Problem
TechHub ran on Azure Container Apps. This migration moves the API and Web sites to Azure App Service (Web Apps for Containers, Basic B1 Linux plan) for simpler, more cost-predictable hosting of a low-traffic workload, while keeping the existing container image-based deployment model.
Solution
@Microsoft.KeyVault(...)), Regional VNet Integration + IP restrictions to keep the API non-public,healthCheckPath/alwaysOninstead of liveness/readiness/startup probes, and Docker registry app settings for GHCR image pullsinfra/modules/containerApps.bicepand updatedinfrastructure.bicep,applications.bicep,pr-applications.bicep, and the monitoring/alerts modules accordingly (dropped the Container-Apps-only restart-storm log alert and unusedlogAnalyticsWorkspaceIdparam)Deploy-Application.ps1,Deploy-Applications.ps1,Deploy-Infrastructure.ps1,Deploy-PrPreview.ps1,Teardown-PrEnvironments.ps1, etc.) to useaz webappcommands instead ofaz containerapp.github/workflows/ci.yml) so PR-preview build/deploy/E2E jobs run only via manualworkflow_dispatch(shared PR-preview App Service Plan is a cost/load control point), while the quality-gate jobs still run automatically on every pull requestProgram.cs,ServiceDefaults, health checks, and E2E test helpers to describe App Service concepts (health-check path, always-on,clientAffinityEnabled, single-instance capacity) instead of Container Apps replicas/probes/scale-to-zerodocs/network-architecture.mdDateRangeSliderTests.DateRangeSlider_PreservesDateInTagUrland 4 tests inLoadMoreButtonTests) that assumed the local dev database always has content within a narrow recent date window. Since the local snapshot can lag behind the system clock, those tests now use wider, safer date ranges (90 days / 3 years) so they no longer depend on content freshnessImpact