From 54b1604bb0922f8150f0cafd0dba2e0cd6094c72 Mon Sep 17 00:00:00 2001 From: Paolo Salvatori Date: Thu, 10 Sep 2026 22:00:28 +0200 Subject: [PATCH 1/7] Add the Web App with App Configuration and Key Vault sample (Python and .NET) A permutation of the PostgreSQL flexible server sample in which the five PostgreSQL connection settings leave the Web App's app settings: PG_HOST, PG_PORT and PG_DATABASE become key-values of an Azure App Configuration store, PG_USER and PG_PASSWORD become Key Vault secrets exposed through App Configuration Key Vault references, and the app loads them at startup with the App Configuration provider, authenticating to the store and to Key Vault with a user-assigned managed identity over Private Endpoints. All three provisioning modes (Azure CLI scripts, Bicep, Terraform with AzAPI for the key-values) create the same topology in both languages, are enrolled in run-samples.sh and were run on the emulator and on Azure. The root README gets one Outline row. Co-Authored-By: Claude Fable 5.1 --- README.md | 1 + run-samples.sh | 6 + .../dotnet/README.md | 210 + .../dotnet/bicep/README.md | 142 + .../dotnet/bicep/deploy.sh | 507 + .../dotnet/bicep/main.bicep | 482 + .../dotnet/bicep/main.bicepparam | 27 + .../bicep/modules/app-configuration.bicep | 126 + .../bicep/modules/app-service-plan.bicep | 154 + .../dotnet/bicep/modules/key-vault.bicep | 180 + .../dotnet/bicep/modules/log-analytics.bicep | 45 + .../bicep/modules/managed-identity.bicep | 31 + .../modules/postgresql-flexible-server.bicep | 176 + .../bicep/modules/private-dns-zone.bicep | 41 + .../bicep/modules/private-endpoint.bicep | 71 + .../bicep/modules/virtual-network.bicep | 238 + .../dotnet/bicep/modules/web-app.bicep | 215 + .../dotnet/images/architecture.png | Bin 0 -> 99609 bytes .../dotnet/images/vacation-planner.png | Bin 0 -> 85690 bytes .../dotnet/scripts/README.md | 86 + .../dotnet/scripts/call-web-app.sh | 201 + .../dotnet/scripts/deploy.sh | 1867 +++ .../dotnet/scripts/validate.sh | 299 + .../dotnet/src/Models/Activity.cs | 4 + .../dotnet/src/Pages/Delete.cshtml | 2 + .../dotnet/src/Pages/Delete.cshtml.cs | 23 + .../dotnet/src/Pages/Index.cshtml | 265 + .../dotnet/src/Pages/Index.cshtml.cs | 48 + .../dotnet/src/Pages/_ViewImports.cshtml | 4 + .../dotnet/src/Program.cs | 61 + .../dotnet/src/Services/ActivityId.cs | 15 + .../src/Services/AppConfigurationSettings.cs | 158 + .../dotnet/src/Services/IActivityStore.cs | 21 + .../src/Services/PostgresActivityStore.cs | 118 + .../dotnet/src/Services/PostgresOptions.cs | 33 + .../dotnet/src/Services/StoreInitializer.cs | 33 + .../dotnet/src/VacationPlanner.csproj | 13 + .../dotnet/src/appsettings.json | 9 + .../dotnet/src/wwwroot/favicon.ico | Bin 0 -> 36791 bytes .../dotnet/src/wwwroot/style.css | 341 + .../dotnet/terraform/README.md | 115 + .../dotnet/terraform/deploy.sh | 409 + .../dotnet/terraform/main.tf | 481 + .../modules/app_service_plan/main.tf | 25 + .../modules/app_service_plan/outputs.tf | 19 + .../modules/app_service_plan/variables.tf | 42 + .../terraform/modules/log_analytics/main.tf | 14 + .../terraform/modules/log_analytics/output.tf | 30 + .../modules/log_analytics/variables.tf | 37 + .../terraform/modules/nat_gateway/main.tf | 42 + .../terraform/modules/nat_gateway/output.tf | 14 + .../modules/nat_gateway/variables.tf | 43 + .../modules/network_security_group/main.tf | 53 + .../modules/network_security_group/outputs.tf | 9 + .../network_security_group/variables.tf | 51 + .../modules/postgres_flexible_server/main.tf | 56 + .../postgres_flexible_server/outputs.tf | 15 + .../postgres_flexible_server/variables.tf | 81 + .../modules/private_dns_zone/main.tf | 27 + .../modules/private_dns_zone/outputs.tf | 9 + .../modules/private_dns_zone/variables.tf | 20 + .../modules/private_endpoint/main.tf | 26 + .../modules/private_endpoint/outputs.tf | 19 + .../modules/private_endpoint/variables.tf | 61 + .../terraform/modules/virtual_network/main.tf | 58 + .../modules/virtual_network/outputs.tf | 19 + .../modules/virtual_network/variables.tf | 40 + .../dotnet/terraform/modules/web_app/main.tf | 72 + .../terraform/modules/web_app/outputs.tf | 24 + .../terraform/modules/web_app/variables.tf | 106 + .../dotnet/terraform/outputs.tf | 55 + .../dotnet/terraform/providers.tf | 60 + .../dotnet/terraform/terraform.tfvars | 3 + .../dotnet/terraform/variables.tf | 263 + .../dotnet/visio/architecture.vsdx | Bin 0 -> 542962 bytes .../python/README.md | 206 + .../python/bicep/README.md | 142 + .../python/bicep/deploy.sh | 507 + .../python/bicep/main.bicep | 482 + .../python/bicep/main.bicepparam | 27 + .../bicep/modules/app-configuration.bicep | 126 + .../bicep/modules/app-service-plan.bicep | 154 + .../python/bicep/modules/key-vault.bicep | 180 + .../python/bicep/modules/log-analytics.bicep | 45 + .../bicep/modules/managed-identity.bicep | 31 + .../modules/postgresql-flexible-server.bicep | 176 + .../bicep/modules/private-dns-zone.bicep | 41 + .../bicep/modules/private-endpoint.bicep | 71 + .../bicep/modules/virtual-network.bicep | 238 + .../python/bicep/modules/web-app.bicep | 215 + .../python/images/architecture.png | Bin 0 -> 99609 bytes .../python/images/vacation-planner.png | Bin 0 -> 85690 bytes .../python/scripts/README.md | 86 + .../python/scripts/call-web-app.sh | 201 + .../python/scripts/deploy.sh | 1867 +++ .../python/scripts/validate.sh | 299 + .../python/src/app.py | 100 + .../python/src/database.py | 138 + .../python/src/gunicorn.conf.py | 18 + .../python/src/requirements.txt | 7 + .../python/src/settings.py | 129 + .../static/bootstrap/css/bootstrap-grid.css | 5051 +++++++ .../bootstrap/css/bootstrap-grid.css.map | 1 + .../bootstrap/css/bootstrap-grid.min.css | 7 + .../bootstrap/css/bootstrap-grid.min.css.map | 1 + .../bootstrap/css/bootstrap-grid.rtl.css | 5050 +++++++ .../bootstrap/css/bootstrap-grid.rtl.css.map | 1 + .../bootstrap/css/bootstrap-grid.rtl.min.css | 7 + .../css/bootstrap-grid.rtl.min.css.map | 1 + .../static/bootstrap/css/bootstrap-reboot.css | 485 + .../bootstrap/css/bootstrap-reboot.css.map | 1 + .../bootstrap/css/bootstrap-reboot.min.css | 8 + .../css/bootstrap-reboot.min.css.map | 1 + .../bootstrap/css/bootstrap-reboot.rtl.css | 482 + .../css/bootstrap-reboot.rtl.css.map | 1 + .../css/bootstrap-reboot.rtl.min.css | 8 + .../css/bootstrap-reboot.rtl.min.css.map | 1 + .../bootstrap/css/bootstrap-utilities.css | 4866 +++++++ .../bootstrap/css/bootstrap-utilities.css.map | 1 + .../bootstrap/css/bootstrap-utilities.min.css | 7 + .../css/bootstrap-utilities.min.css.map | 1 + .../bootstrap/css/bootstrap-utilities.rtl.css | 4857 +++++++ .../css/bootstrap-utilities.rtl.css.map | 1 + .../css/bootstrap-utilities.rtl.min.css | 7 + .../css/bootstrap-utilities.rtl.min.css.map | 1 + .../src/static/bootstrap/css/bootstrap.css | 11266 ++++++++++++++++ .../static/bootstrap/css/bootstrap.css.map | 1 + .../static/bootstrap/css/bootstrap.min.css | 7 + .../bootstrap/css/bootstrap.min.css.map | 1 + .../static/bootstrap/css/bootstrap.rtl.css | 11242 +++++++++++++++ .../bootstrap/css/bootstrap.rtl.css.map | 1 + .../bootstrap/css/bootstrap.rtl.min.css | 7 + .../bootstrap/css/bootstrap.rtl.min.css.map | 1 + .../static/bootstrap/js/bootstrap.bundle.js | 6812 ++++++++++ .../bootstrap/js/bootstrap.bundle.js.map | 1 + .../bootstrap/js/bootstrap.bundle.min.js | 7 + .../bootstrap/js/bootstrap.bundle.min.js.map | 1 + .../src/static/bootstrap/js/bootstrap.esm.js | 4999 +++++++ .../static/bootstrap/js/bootstrap.esm.js.map | 1 + .../static/bootstrap/js/bootstrap.esm.min.js | 7 + .../bootstrap/js/bootstrap.esm.min.js.map | 1 + .../src/static/bootstrap/js/bootstrap.js | 5046 +++++++ .../src/static/bootstrap/js/bootstrap.js.map | 1 + .../src/static/bootstrap/js/bootstrap.min.js | 7 + .../static/bootstrap/js/bootstrap.min.js.map | 1 + .../python/src/static/favicon.ico | Bin 0 -> 36791 bytes .../python/src/static/style.css | 341 + .../python/src/templates/index.html | 260 + .../python/terraform/README.md | 115 + .../python/terraform/deploy.sh | 409 + .../python/terraform/main.tf | 481 + .../modules/app_service_plan/main.tf | 25 + .../modules/app_service_plan/outputs.tf | 19 + .../modules/app_service_plan/variables.tf | 42 + .../terraform/modules/log_analytics/main.tf | 14 + .../terraform/modules/log_analytics/output.tf | 30 + .../modules/log_analytics/variables.tf | 37 + .../terraform/modules/nat_gateway/main.tf | 42 + .../terraform/modules/nat_gateway/output.tf | 14 + .../modules/nat_gateway/variables.tf | 43 + .../modules/network_security_group/main.tf | 53 + .../modules/network_security_group/outputs.tf | 9 + .../network_security_group/variables.tf | 51 + .../modules/postgres_flexible_server/main.tf | 56 + .../postgres_flexible_server/outputs.tf | 15 + .../postgres_flexible_server/variables.tf | 81 + .../modules/private_dns_zone/main.tf | 27 + .../modules/private_dns_zone/outputs.tf | 9 + .../modules/private_dns_zone/variables.tf | 20 + .../modules/private_endpoint/main.tf | 26 + .../modules/private_endpoint/outputs.tf | 19 + .../modules/private_endpoint/variables.tf | 61 + .../terraform/modules/virtual_network/main.tf | 58 + .../modules/virtual_network/outputs.tf | 19 + .../modules/virtual_network/variables.tf | 40 + .../python/terraform/modules/web_app/main.tf | 72 + .../terraform/modules/web_app/outputs.tf | 24 + .../terraform/modules/web_app/variables.tf | 101 + .../python/terraform/outputs.tf | 55 + .../python/terraform/providers.tf | 60 + .../python/terraform/terraform.tfvars | 3 + .../python/terraform/variables.tf | 263 + .../python/visio/architecture.vsdx | Bin 0 -> 542962 bytes 183 files changed, 77388 insertions(+) create mode 100644 samples/web-app-app-configuration/dotnet/README.md create mode 100644 samples/web-app-app-configuration/dotnet/bicep/README.md create mode 100755 samples/web-app-app-configuration/dotnet/bicep/deploy.sh create mode 100644 samples/web-app-app-configuration/dotnet/bicep/main.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/main.bicepparam create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/app-configuration.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/app-service-plan.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/key-vault.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/log-analytics.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/managed-identity.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/postgresql-flexible-server.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/private-dns-zone.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/private-endpoint.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/virtual-network.bicep create mode 100644 samples/web-app-app-configuration/dotnet/bicep/modules/web-app.bicep create mode 100644 samples/web-app-app-configuration/dotnet/images/architecture.png create mode 100644 samples/web-app-app-configuration/dotnet/images/vacation-planner.png create mode 100644 samples/web-app-app-configuration/dotnet/scripts/README.md create mode 100755 samples/web-app-app-configuration/dotnet/scripts/call-web-app.sh create mode 100755 samples/web-app-app-configuration/dotnet/scripts/deploy.sh create mode 100755 samples/web-app-app-configuration/dotnet/scripts/validate.sh create mode 100644 samples/web-app-app-configuration/dotnet/src/Models/Activity.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Pages/Delete.cshtml create mode 100644 samples/web-app-app-configuration/dotnet/src/Pages/Delete.cshtml.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Pages/Index.cshtml create mode 100644 samples/web-app-app-configuration/dotnet/src/Pages/Index.cshtml.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Pages/_ViewImports.cshtml create mode 100644 samples/web-app-app-configuration/dotnet/src/Program.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Services/ActivityId.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Services/AppConfigurationSettings.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Services/IActivityStore.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Services/PostgresActivityStore.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Services/PostgresOptions.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/Services/StoreInitializer.cs create mode 100644 samples/web-app-app-configuration/dotnet/src/VacationPlanner.csproj create mode 100644 samples/web-app-app-configuration/dotnet/src/appsettings.json create mode 100644 samples/web-app-app-configuration/dotnet/src/wwwroot/favicon.ico create mode 100644 samples/web-app-app-configuration/dotnet/src/wwwroot/style.css create mode 100644 samples/web-app-app-configuration/dotnet/terraform/README.md create mode 100755 samples/web-app-app-configuration/dotnet/terraform/deploy.sh create mode 100644 samples/web-app-app-configuration/dotnet/terraform/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/app_service_plan/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/app_service_plan/outputs.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/app_service_plan/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/log_analytics/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/log_analytics/output.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/log_analytics/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/nat_gateway/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/nat_gateway/output.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/nat_gateway/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/network_security_group/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/network_security_group/outputs.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/network_security_group/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/postgres_flexible_server/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/postgres_flexible_server/outputs.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/postgres_flexible_server/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/private_dns_zone/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/private_dns_zone/outputs.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/private_dns_zone/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/private_endpoint/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/private_endpoint/outputs.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/private_endpoint/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/virtual_network/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/virtual_network/outputs.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/virtual_network/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/web_app/main.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/web_app/outputs.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/modules/web_app/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/outputs.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/providers.tf create mode 100644 samples/web-app-app-configuration/dotnet/terraform/terraform.tfvars create mode 100644 samples/web-app-app-configuration/dotnet/terraform/variables.tf create mode 100644 samples/web-app-app-configuration/dotnet/visio/architecture.vsdx create mode 100644 samples/web-app-app-configuration/python/README.md create mode 100644 samples/web-app-app-configuration/python/bicep/README.md create mode 100644 samples/web-app-app-configuration/python/bicep/deploy.sh create mode 100644 samples/web-app-app-configuration/python/bicep/main.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/main.bicepparam create mode 100644 samples/web-app-app-configuration/python/bicep/modules/app-configuration.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/app-service-plan.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/key-vault.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/log-analytics.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/managed-identity.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/postgresql-flexible-server.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/private-dns-zone.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/private-endpoint.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/virtual-network.bicep create mode 100644 samples/web-app-app-configuration/python/bicep/modules/web-app.bicep create mode 100644 samples/web-app-app-configuration/python/images/architecture.png create mode 100644 samples/web-app-app-configuration/python/images/vacation-planner.png create mode 100644 samples/web-app-app-configuration/python/scripts/README.md create mode 100755 samples/web-app-app-configuration/python/scripts/call-web-app.sh create mode 100644 samples/web-app-app-configuration/python/scripts/deploy.sh create mode 100644 samples/web-app-app-configuration/python/scripts/validate.sh create mode 100644 samples/web-app-app-configuration/python/src/app.py create mode 100644 samples/web-app-app-configuration/python/src/database.py create mode 100644 samples/web-app-app-configuration/python/src/gunicorn.conf.py create mode 100644 samples/web-app-app-configuration/python/src/requirements.txt create mode 100644 samples/web-app-app-configuration/python/src/settings.py create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-grid.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-grid.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-grid.min.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-grid.min.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-grid.rtl.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-grid.rtl.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-grid.rtl.min.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-grid.rtl.min.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-reboot.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-reboot.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-reboot.min.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-reboot.min.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-reboot.rtl.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-reboot.rtl.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-reboot.rtl.min.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-reboot.rtl.min.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-utilities.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-utilities.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-utilities.min.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-utilities.min.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-utilities.rtl.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-utilities.rtl.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-utilities.rtl.min.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap-utilities.rtl.min.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap.min.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap.min.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap.rtl.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap.rtl.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap.rtl.min.css create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/css/bootstrap.rtl.min.css.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.bundle.js create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.bundle.js.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.bundle.min.js create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.bundle.min.js.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.esm.js create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.esm.js.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.esm.min.js create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.esm.min.js.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.js create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.js.map create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.min.js create mode 100644 samples/web-app-app-configuration/python/src/static/bootstrap/js/bootstrap.min.js.map create mode 100644 samples/web-app-app-configuration/python/src/static/favicon.ico create mode 100644 samples/web-app-app-configuration/python/src/static/style.css create mode 100644 samples/web-app-app-configuration/python/src/templates/index.html create mode 100644 samples/web-app-app-configuration/python/terraform/README.md create mode 100644 samples/web-app-app-configuration/python/terraform/deploy.sh create mode 100644 samples/web-app-app-configuration/python/terraform/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/app_service_plan/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/app_service_plan/outputs.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/app_service_plan/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/log_analytics/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/log_analytics/output.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/log_analytics/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/nat_gateway/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/nat_gateway/output.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/nat_gateway/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/network_security_group/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/network_security_group/outputs.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/network_security_group/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/postgres_flexible_server/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/postgres_flexible_server/outputs.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/postgres_flexible_server/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/private_dns_zone/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/private_dns_zone/outputs.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/private_dns_zone/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/private_endpoint/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/private_endpoint/outputs.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/private_endpoint/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/virtual_network/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/virtual_network/outputs.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/virtual_network/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/web_app/main.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/web_app/outputs.tf create mode 100644 samples/web-app-app-configuration/python/terraform/modules/web_app/variables.tf create mode 100644 samples/web-app-app-configuration/python/terraform/outputs.tf create mode 100644 samples/web-app-app-configuration/python/terraform/providers.tf create mode 100644 samples/web-app-app-configuration/python/terraform/terraform.tfvars create mode 100644 samples/web-app-app-configuration/python/terraform/variables.tf create mode 100644 samples/web-app-app-configuration/python/visio/architecture.vsdx diff --git a/README.md b/README.md index a82c529..71db00d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Each sample is a self-contained project with its own README, Azure CLI scripts a | Web App and Managed Identities ([Python](./samples/web-app-managed-identity/python/README.md), [.NET](./samples/web-app-managed-identity/dotnet/README.md)) | The *Vacation Planner* single-page web app on an Azure Web App, storing each activity as a blob in an `activities` container and accessing Blob Storage through a user-assigned or system-assigned managed identity rather than connection strings. | | Web App and SQL Database ([Python](./samples/web-app-sql-database/python/README.md), [.NET](./samples/web-app-sql-database/dotnet/README.md)) | The *Vacation Planner* single-page web app on an Azure Web App, storing activities in an Azure SQL Database. The connection string and the HTTPS certificate are read from Azure Key Vault, and an API endpoint verifies the Key Vault certificate. | | Web App and PostgreSQL Database ([Python](./samples/web-app-postgresql-flexible-server/python/README.md), [.NET](./samples/web-app-postgresql-flexible-server/dotnet/README.md)) | The *Vacation Planner* single-page web app on an Azure Web App, storing activities in an Azure Database for PostgreSQL flexible server injected into a virtual network (delegated subnet and private DNS zone), with the database and application user created by the deploy scripts. | +| Web App, App Configuration and Key Vault ([Python](./samples/web-app-app-configuration/python/README.md), [.NET](./samples/web-app-app-configuration/dotnet/README.md)) | The *Vacation Planner* single-page web app on an Azure Web App, storing activities in an Azure Database for PostgreSQL flexible server and reading its connection settings from an Azure App Configuration store whose secrets are Key Vault references, authenticating to both stores with a user-assigned managed identity through private endpoints. | | Web App and MySQL Database ([Python](./samples/web-app-mysql-flexible-server/python/README.md), [.NET](./samples/web-app-mysql-flexible-server/dotnet/README.md)) | The *Vacation Planner* single-page web app on an Azure Web App, storing activities in an Azure Database for MySQL flexible server injected into a virtual network, using TLS-only connections, with the database and application user created by the deploy scripts. | | Web App with Custom Docker Image ([Python](./samples/web-app-custom-image/python/README.md), [.NET](./samples/web-app-custom-image/dotnet/README.md)) | A web app that runs a custom container image built locally and pushed to Azure Container Registry; the web app pulls it with a managed identity (AcrPull) through a VNet-integrated network and reports its image and host name on `/api/status`. | | [ACI and Blob Storage (Python)](./samples/aci-blob-storage/python/README.md) | A containerized Flask web app on Azure Container Instances, with its image in Azure Container Registry, its secrets in Key Vault and its data in Blob Storage. | diff --git a/run-samples.sh b/run-samples.sh index 9009023..62c9f25 100755 --- a/run-samples.sh +++ b/run-samples.sh @@ -47,6 +47,8 @@ SAMPLES=( "samples/web-app-mysql-flexible-server/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh" "samples/web-app-postgresql-flexible-server/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh" "samples/web-app-postgresql-flexible-server/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh" + "samples/web-app-app-configuration/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh" + "samples/web-app-app-configuration/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh" "samples/web-app-custom-image/python|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh" "samples/web-app-custom-image/dotnet|bash scripts/deploy.sh|bash scripts/validate.sh && bash scripts/call-web-app.sh" "samples/aci-blob-storage/python|bash scripts/deploy.sh|bash scripts/validate.sh" @@ -72,6 +74,8 @@ TERRAFORM_SAMPLES=( "samples/web-app-mysql-flexible-server/dotnet/terraform|bash deploy.sh" "samples/web-app-postgresql-flexible-server/python/terraform|bash deploy.sh" "samples/web-app-postgresql-flexible-server/dotnet/terraform|bash deploy.sh" + "samples/web-app-app-configuration/python/terraform|bash deploy.sh" + "samples/web-app-app-configuration/dotnet/terraform|bash deploy.sh" "samples/aci-blob-storage/python/terraform|bash deploy.sh" "samples/container-apps-blob-storage/python/terraform|bash deploy.sh" "samples/url-shortener/python/terraform|bash deploy.sh|bash ../scripts/validate.sh" @@ -94,6 +98,8 @@ BICEP_SAMPLES=( "samples/web-app-mysql-flexible-server/dotnet/bicep|bash deploy.sh" "samples/web-app-postgresql-flexible-server/python/bicep|bash deploy.sh" "samples/web-app-postgresql-flexible-server/dotnet/bicep|bash deploy.sh" + "samples/web-app-app-configuration/python/bicep|bash deploy.sh" + "samples/web-app-app-configuration/dotnet/bicep|bash deploy.sh" "samples/aci-blob-storage/python/bicep|bash deploy.sh" "samples/container-apps-blob-storage/python/bicep|bash deploy.sh" "samples/url-shortener/python/bicep|bash deploy.sh|bash ../scripts/validate.sh" diff --git a/samples/web-app-app-configuration/dotnet/README.md b/samples/web-app-app-configuration/dotnet/README.md new file mode 100644 index 0000000..ae02003 --- /dev/null +++ b/samples/web-app-app-configuration/dotnet/README.md @@ -0,0 +1,210 @@ +# Azure Web App with Azure App Configuration and Azure Key Vault + +This sample demonstrates an ASP.NET Core Razor Pages single-page web application called *Vacation Planner* hosted on an [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview). The app runs on an Azure App Service Plan and stores activity data in the `activities` table of the `PlannerDB` database on an [Azure Database for PostgreSQL flexible server](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/overview), reached through a [Private Endpoint](https://learn.microsoft.com/azure/private-link/private-endpoint-overview). What sets this permutation apart from the [PostgreSQL sample](../../web-app-postgresql-flexible-server/dotnet/README.md) it derives from is where the app reads its connection settings from: the host, port and database name live in an [Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview) store, the application role name and password live in [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) and reach the app through App Configuration [Key Vault references](https://learn.microsoft.com/en-us/azure/azure-app-configuration/use-key-vault-references-dotnet-core), and the app authenticates to both stores with a [user-assigned managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) authorized through Azure RBAC. No connection string, access key or database credential is stored in the Web App's app settings. + +## Architecture + +![Architecture Diagram](./images/architecture.png) + +The web app enables users to plan and manage vacation activities; all data is persisted in PostgreSQL. The solution is composed of the following Azure resources: + +1. [Azure Resource Group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-cli): A logical container scoping all resources in this sample. +2. [Azure Virtual Network](https://learn.microsoft.com/azure/virtual-network/virtual-networks-overview): Hosts two subnets: + - *app-subnet*: Delegated to `Microsoft.Web/serverFarms` for regional VNet integration of the Web App. + - *pe-subnet*: Hosts the three Private Endpoints to the PostgreSQL flexible server, the App Configuration store and the Key Vault. +3. [Azure Private DNS Zones](https://learn.microsoft.com/azure/dns/private-dns-privatednszone) `privatelink.postgres.database.azure.com`, `privatelink.azconfig.io` and `privatelink.vaultcore.azure.net`, each linked to the VNet with a `link-to-vnet` virtual network link. The DNS zone group (`default`) of each Private Endpoint registers the `A` record of its target, so the Web App resolves the three services to private IP addresses. +4. [Azure Private Endpoints](https://learn.microsoft.com/azure/private-link/private-endpoint-overview): `-postgres-pe-` (group `postgresqlServer`), `-appconfig-pe-` (group `configurationStores`) and `-keyvault-pe-` (group `vault`). +5. [Azure NAT Gateway](https://learn.microsoft.com/azure/nat-gateway/nat-overview): Deterministic outbound connectivity for the Web App subnet. +6. [Azure Network Security Group](https://learn.microsoft.com/en-us/azure/virtual-network/network-security-groups-overview): One NSG per subnet. +7. [Azure Log Analytics Workspace](https://learn.microsoft.com/azure/azure-monitor/logs/log-analytics-overview): Centralizes the diagnostic logs and metrics of every resource, the store and the vault included. +8. [Azure Database for PostgreSQL flexible server](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/overview): Public-access server hosting the `PlannerDB` database. Burstable `Standard_B1ms`, version 16, 32 GiB storage, 7-day backup retention, HA disabled. A permissive firewall rule (`0.0.0.0` to `255.255.255.255`) lets the deploy machine run the post-create psql bootstrap; the Web App itself reaches the server through the Private Endpoint. +9. [PostgreSQL database](https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-servers) `PlannerDB`: Created at provisioning time; the post-deploy psql step creates the `activities` table and seeds the demo rows. +10. [Azure App Configuration store](https://learn.microsoft.com/en-us/azure/azure-app-configuration/overview) `-appconfig-` (Standard tier): Holds the five connection settings as key-values, see [Configuration design](#configuration-design). Access keys stay enabled (the default) because the deployment scripts and templates write the key-values with them; the Web App never uses them. +11. [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/general/overview) `-keyvault-` with the [Azure RBAC permission model](https://learn.microsoft.com/en-us/azure/key-vault/general/rbac-guide): Holds the secrets `pg-user` and `pg-password` with the credentials of the PostgreSQL application role. +12. [User-assigned managed identity](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) `-identity-`, assigned to the Web App, with two [role assignments](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles): [App Configuration Data Reader](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/integration#app-configuration-data-reader) on the store and [Key Vault Secrets User](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/security#key-vault-secrets-user) on the vault. +13. [Azure App Service Plan](https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans): The underlying compute tier that hosts the web application. +14. [Azure Web App](https://learn.microsoft.com/en-us/azure/app-service/overview): Runs the ASP.NET Core *Vacation Planner* app with regional VNet integration into *app-subnet* and the user-assigned managed identity. Its app settings hold the store endpoint (`Endpoints__AppConfiguration`), the identity client id (`AZURE_CLIENT_ID`), `LOGIN_NAME`, `WEBSITES_PORT` and the Oryx build flags. No `PG_*` app setting exists: the app connects to PostgreSQL with the dedicated application role (`testuser`) it reads from the store, and the server-admin login is never used at runtime. +15. [App Service Source Control](https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/create-or-update-source-control?view=rest-appservice-2024-11-01): *(Optional)* Configures continuous deployment from a public GitHub repository. + +The deploy scripts and templates follow the same pattern as the sibling [`web-app-postgresql-flexible-server`](../../web-app-postgresql-flexible-server/dotnet/) sample: after provisioning, they (i) connect as the server admin via the public endpoint and the firewall rule, (ii) create the application role `testuser` with its own password, (iii) grant minimum schema privileges on `PlannerDB`, (iv) create the `activities` table and (v) seed the sample rows. The difference is step (vi): the role name and password are not written onto the Web App's app settings; they are stored in Key Vault and referenced from the App Configuration store at provisioning time, and the app loads them from there. + +### Configuration design + +The original sample hands the five PostgreSQL settings to the Web App as app settings. This sample partitions them by sensitivity: + +| Setting | Lives in | Key or secret name | Content type | +| ------------- | ------------------------------------------ | --------------------------- | -------------------------------------------------------------------- | +| `PG_HOST` | App Configuration key-value | `PG_HOST` | none | +| `PG_PORT` | App Configuration key-value | `PG_PORT` | none | +| `PG_DATABASE` | App Configuration key-value | `PG_DATABASE` | none | +| `PG_USER` | Key Vault secret, referenced from the store | `PG_USER` referencing `pg-user` | `application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8` | +| `PG_PASSWORD` | Key Vault secret, referenced from the store | `PG_PASSWORD` referencing `pg-password` | `application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8` | + +The keys keep the names of the former environment variables, carry no label, and the two Key Vault references hold the versionless identifier of their secret (`{"uri":"https://.vault.azure.net/secrets/pg-user"}`), so they always follow the latest version. Key Vault secret names allow only alphanumerics and hyphens, which is why the secrets are called `pg-user` and `pg-password`. The App Configuration store is the app's single configuration source: a reader of the store sees where every setting comes from, and the secrets never leave Key Vault except when the authorized identity resolves them. + +There are two ways for an App Service app to consume such a store: + +1. **In-process App Configuration provider** (the path this sample uses, on Azure and on the emulator). The app receives only the store endpoint and the identity client id, loads the `PG_*` keys itself with the [Azure App Configuration .NET provider](https://learn.microsoft.com/en-us/azure/azure-app-configuration/reference-dotnet-provider) (`Microsoft.Azure.AppConfiguration.AspNetCore`) and lets the provider resolve the Key Vault references with the same credential. One `DefaultAzureCredential` serves both stores: on App Service it resolves to the user-assigned managed identity selected by `AZURE_CLIENT_ID`, on a developer machine to the signed-in Azure CLI user. See `src/Services/AppConfigurationSettings.cs`: + + ```csharp + var credential = new DefaultAzureCredential(); + var settings = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.Connect(new Uri(endpoint), credential) + .Select("PG_*") + .ConfigureKeyVault(keyVault => keyVault.SetCredential(credential)) + .ConfigureStartupOptions(startup => startup.Timeout = TimeSpan.FromSeconds(60)); + }) + .Build(); + builder.Configuration.AddConfiguration(settings); + ``` + + The load is retried for a few minutes, so a role assignment that has not propagated yet produces log lines rather than a crash loop, and the app logs which keys it loaded and how many Key Vault references it resolved, never the values. The app reads the endpoint as `Endpoints:AppConfiguration`, which the .NET configuration system maps from the `Endpoints__AppConfiguration` app setting; the Python variant reads the same app setting, so both languages share one name. + +2. **App Service configuration references** (Azure only, documented here as the alternative, not used by the deployed sample). App Service can resolve app settings of the form `@Microsoft.AppConfiguration(Endpoint=https://.azconfig.io; Key=; Label=