Skip to content

Migrate hosting from Azure Container Apps to Azure App Service - #511

Open
rvanmaanen wants to merge 5 commits into
mainfrom
rvanmaanen-fictional-carnival
Open

Migrate hosting from Azure Container Apps to Azure App Service#511
rvanmaanen wants to merge 5 commits into
mainfrom
rvanmaanen-fictional-carnival

Conversation

@rvanmaanen

@rvanmaanen rvanmaanen commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

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

  • Replaced the Container Apps Environment with a shared App Service Plan (infra/modules/appServicePlan.bicep) hosting both API and Web sites as Web Apps for Containers
  • Rewrote infra/modules/api.bicep and infra/modules/web.bicep (Microsoft.Web/sites) to replace Container Apps ingress/secrets/scaling with App Service equivalents: Key Vault reference app settings (@Microsoft.KeyVault(...)), Regional VNet Integration + IP restrictions to keep the API non-public, healthCheckPath/alwaysOn instead of liveness/readiness/startup probes, and Docker registry app settings for GHCR image pulls
  • Removed infra/modules/containerApps.bicep and updated infrastructure.bicep, applications.bicep, pr-applications.bicep, and the monitoring/alerts modules accordingly (dropped the Container-Apps-only restart-storm log alert and unused logAnalyticsWorkspaceId param)
  • Updated deployment scripts (Deploy-Application.ps1, Deploy-Applications.ps1, Deploy-Infrastructure.ps1, Deploy-PrPreview.ps1, Teardown-PrEnvironments.ps1, etc.) to use az webapp commands instead of az containerapp
  • Reworked the CI pipeline (.github/workflows/ci.yml) so PR-preview build/deploy/E2E jobs run only via manual workflow_dispatch (shared PR-preview App Service Plan is a cost/load control point), while the quality-gate jobs still run automatically on every pull request
  • Updated docs (README, technology-stack, admin-authentication, url-routing, ci-cd-pipeline, health-checks, network-architecture, repository-structure, wildcard-certificates) and in-code comments across Program.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-zero
  • Fixed a markdownlint MD012 violation (trailing blank line) in docs/network-architecture.md
  • Fixed 5 deterministically-failing E2E tests (DateRangeSliderTests.DateRangeSlider_PreservesDateInTagUrl and 4 tests in LoadMoreButtonTests) 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 freshness

Impact

  • No change to application behavior or the container-image-based deploy workflow — this is purely a hosting-platform swap
  • PR preview deployments now require an explicit manual trigger instead of running automatically on every PR push
  • E2E test suite is now reliably green (344/344) both locally and under slow3g network throttling, independent of how recently the local content snapshot was refreshed

rvanmaanen and others added 3 commits August 6, 2026 21:04
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
@rvanmaanen
rvanmaanen requested a review from rajbos as a code owner August 7, 2026 18:43
Copilot AI lite review requested due to automatic review settings August 7, 2026 18:43

Copilot AI left a comment

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.

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 webapp flows and make PR preview deploy/E2E manual-only via workflow_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.

Comment thread scripts/Deploy-Application.ps1 Outdated
Comment thread infra/modules/web.bicep Outdated
Comment thread scripts/Deploy-Application.ps1 Outdated
Comment thread scripts/Teardown-PrEnvironments.ps1 Outdated
Comment thread scripts/Deploy-PrPreview.ps1 Outdated
Comment thread docs/network-architecture.md Outdated
Comment thread infra/AGENTS.md Outdated
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
Copilot AI review requested due to automatic review settings August 7, 2026 19:27

Copilot AI left a comment

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.

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
Copilot AI review requested due to automatic review settings August 8, 2026 07:28

Copilot AI left a comment

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.

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 show call suppresses stderr via 2>$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

  • corsOrigins includes https://*.azurewebsites.net, which is both broader than needed (you already pass explicit webFqdns) 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 show call suppresses stderr via 2>$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

  • keyVaultUrl is 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/certificates imports are performed by the App Service certificate provider (see the note in infra/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, but WEBSITES_PORT only tells App Service which container port to route to; it doesn’t explain the actual network isolation (VNet integration + ipSecurityRestrictions). Updating the wording avoids implying WEBSITES_PORT is 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants