From a31bb9b6a8e4103d2279b5c4b0cfde939016b8bd Mon Sep 17 00:00:00 2001 From: fboucher Date: Thu, 4 Jun 2026 14:07:41 -0400 Subject: [PATCH 01/17] upgrade packages --- src/Directory.Packages.props | 68 ++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b23f817d..72b23866 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,39 +1,33 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 3794de10a80b8326eec2d7693e0205af23b03f7d Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 10 Jul 2026 09:49:40 -0400 Subject: [PATCH 02/17] aspire upgrade --- src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj | 2 +- src/Directory.Packages.props | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj b/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj index 0f8183cf..4801274d 100644 --- a/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj +++ b/src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 72b23866..b58565f9 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,14 +4,14 @@ - - + + - + From 9a133ebf4edfe06619667d337d0c103b6dc5d2cd Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 24 Jul 2026 10:00:17 -0400 Subject: [PATCH 03/17] Enhance Azure Storage integration and update package versions --- src/AppHost/Program.cs | 7 +++++++ src/Directory.Packages.props | 1 - src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep | 4 +++- src/infra/main.bicep | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/AppHost/Program.cs b/src/AppHost/Program.cs index 073ad77f..5d7a1a9f 100644 --- a/src/AppHost/Program.cs +++ b/src/AppHost/Program.cs @@ -6,13 +6,20 @@ var customDomain = builder.AddParameter("CustomDomain"); var defaultRedirectUrl = builder.AddParameter("DefaultRedirectUrl"); +// To use an existing storage account, you can provide the name and resource group of the existing storage account. +// var existingStorageName = builder.AddParameter("existingStorageName"); +// var existingStorageResourceGroup = builder.AddParameter("existingStorageResourceGroup"); + var urlStorage = builder.AddAzureStorage("url-data"); + // .AsExisting(existingStorageName, existingStorageResourceGroup); if (builder.Environment.IsDevelopment()) { urlStorage.RunAsEmulator(); } + + var strTables = urlStorage.AddTables("strTables"); var azFuncLight = builder.AddAzureFunctionsProject("azfunc-light") diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b58565f9..6799cdac 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -7,7 +7,6 @@ - diff --git a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep index 01fb9a78..77337b54 100644 --- a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep +++ b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep @@ -72,6 +72,8 @@ resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/rol output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob +output dataLakeEndpoint string = funcstoragea17ca.properties.primaryEndpoints.dfs + output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue -output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table \ No newline at end of file +output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table diff --git a/src/infra/main.bicep b/src/infra/main.bicep index b43172b8..adf882bb 100644 --- a/src/infra/main.bicep +++ b/src/infra/main.bicep @@ -63,6 +63,7 @@ output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CO output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint +output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint output FUNCSTORAGEA17CA_TABLEENDPOINT string = funcstoragea17ca.outputs.tableEndpoint output URL_DATA_TABLEENDPOINT string = url_data.outputs.tableEndpoint From 95d2ea21f628d6627bfc11fb89f4f4d272463984 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 31 Jul 2026 10:43:25 -0400 Subject: [PATCH 04/17] test new structures --- .../admin-appservice/azure.admin.yaml | 8 ++ .../redirect/azure.redirect.yaml | 8 ++ src/.gitignore | 1 + ...d5mins.ShortenerTools.AppHost.Admin.csproj | 21 ++++ src/AppHost.Admin/Program.cs | 33 ++++++ src/AppHost.Admin/appsettings.json | 13 +++ ...ins.ShortenerTools.AppHost.Redirect.csproj | 21 ++++ src/AppHost.Redirect/Program.cs | 22 ++++ src/AppHost.Redirect/appsettings.json | 12 +++ src/AzUrlShortener.sln | 102 +++++++++++++++++- src/deploy-admin.sh | 33 ++++++ src/deploy-redirect.sh | 33 ++++++ .../funcstoragea17ca.module.bicep | 79 -------------- src/infra/main.bicep | 69 ------------ src/infra/main.parameters.json | 22 ---- src/infra/resources.bicep | 94 ---------------- src/infra/url-data/url-data.module.bicep | 67 ------------ 17 files changed, 303 insertions(+), 335 deletions(-) create mode 100644 deployment/split-resources/admin-appservice/azure.admin.yaml create mode 100644 deployment/split-resources/redirect/azure.redirect.yaml create mode 100644 src/AppHost.Admin/Cloud5mins.ShortenerTools.AppHost.Admin.csproj create mode 100644 src/AppHost.Admin/Program.cs create mode 100644 src/AppHost.Admin/appsettings.json create mode 100644 src/AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj create mode 100644 src/AppHost.Redirect/Program.cs create mode 100644 src/AppHost.Redirect/appsettings.json create mode 100755 src/deploy-admin.sh create mode 100755 src/deploy-redirect.sh delete mode 100644 src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep delete mode 100644 src/infra/main.bicep delete mode 100644 src/infra/main.parameters.json delete mode 100644 src/infra/resources.bicep delete mode 100644 src/infra/url-data/url-data.module.bicep diff --git a/deployment/split-resources/admin-appservice/azure.admin.yaml b/deployment/split-resources/admin-appservice/azure.admin.yaml new file mode 100644 index 00000000..57a07af8 --- /dev/null +++ b/deployment/split-resources/admin-appservice/azure.admin.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: src.admin +services: + app: + language: dotnet + project: ../../../src/AppHost/AppHost.csproj + host: appservice diff --git a/deployment/split-resources/redirect/azure.redirect.yaml b/deployment/split-resources/redirect/azure.redirect.yaml new file mode 100644 index 00000000..5f11f3c6 --- /dev/null +++ b/deployment/split-resources/redirect/azure.redirect.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: src.redirect +services: + app: + language: dotnet + project: ../../../AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj + host: function diff --git a/src/.gitignore b/src/.gitignore index 8e843802..1c037d0f 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1 +1,2 @@ .azure +publish/ diff --git a/src/AppHost.Admin/Cloud5mins.ShortenerTools.AppHost.Admin.csproj b/src/AppHost.Admin/Cloud5mins.ShortenerTools.AppHost.Admin.csproj new file mode 100644 index 00000000..eba5d294 --- /dev/null +++ b/src/AppHost.Admin/Cloud5mins.ShortenerTools.AppHost.Admin.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + true + true + + + + + + + + + + + + diff --git a/src/AppHost.Admin/Program.cs b/src/AppHost.Admin/Program.cs new file mode 100644 index 00000000..d49de86f --- /dev/null +++ b/src/AppHost.Admin/Program.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +var customDomain = builder.AddParameter("CustomDomain"); +var defaultRedirectUrl = builder.AddParameter("DefaultRedirectUrl"); + +// To share the same storage as AppHost.Redirect, replace the storage creation below with: +// var existingStorageName = builder.AddParameter("existingStorageName"); +// var existingStorageRg = builder.AddParameter("existingStorageResourceGroup"); +// var urlStorage = builder.AddAzureStorage("url-data") +// .AsExisting(existingStorageName, existingStorageRg); + +var urlStorage = builder.AddAzureStorage("url-data"); + +if (builder.Environment.IsDevelopment()) +{ + urlStorage.RunAsEmulator(); +} + +var strTables = urlStorage.AddTables("strTables"); + +var manAPI = builder.AddProject("api") + .WithReference(strTables) + .WaitFor(strTables) + .WithEnvironment("CustomDomain", customDomain) + .WithEnvironment("DefaultRedirectUrl", defaultRedirectUrl); + +builder.AddProject("admin") + .WithExternalHttpEndpoints() + .WithReference(manAPI); + +builder.Build().Run(); diff --git a/src/AppHost.Admin/appsettings.json b/src/AppHost.Admin/appsettings.json new file mode 100644 index 00000000..00c52bac --- /dev/null +++ b/src/AppHost.Admin/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + }, + "Parameters":{ + "CustomDomain": "https://c5m.ca", + "DefaultRedirectUrl": "https://azure.com" + } +} diff --git a/src/AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj b/src/AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj new file mode 100644 index 00000000..09f35a8c --- /dev/null +++ b/src/AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + true + true + + + + + + + + + + + + diff --git a/src/AppHost.Redirect/Program.cs b/src/AppHost.Redirect/Program.cs new file mode 100644 index 00000000..153ff4a6 --- /dev/null +++ b/src/AppHost.Redirect/Program.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Hosting; + +var builder = DistributedApplication.CreateBuilder(args); + +var defaultRedirectUrl = builder.AddParameter("DefaultRedirectUrl"); + +var urlStorage = builder.AddAzureStorage("url-data"); + +if (builder.Environment.IsDevelopment()) +{ + urlStorage.RunAsEmulator(); +} + +var strTables = urlStorage.AddTables("strTables"); + +var azFuncLight = builder.AddAzureFunctionsProject("azfunc-light") + .WithReference(strTables) + .WaitFor(strTables) + .WithEnvironment("DefaultRedirectUrl", defaultRedirectUrl) + .WithExternalHttpEndpoints(); + +builder.Build().Run(); diff --git a/src/AppHost.Redirect/appsettings.json b/src/AppHost.Redirect/appsettings.json new file mode 100644 index 00000000..d72f83d2 --- /dev/null +++ b/src/AppHost.Redirect/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + }, + "Parameters":{ + "DefaultRedirectUrl": "https://azure.com" + } +} diff --git a/src/AzUrlShortener.sln b/src/AzUrlShortener.sln index a000c143..b8d4e9eb 100644 --- a/src/AzUrlShortener.sln +++ b/src/AzUrlShortener.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 @@ -15,38 +15,132 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.TinyBlazorAdmin", "TinyBlazorAdmin\Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj", "{88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppHost.Redirect", "AppHost.Redirect", "{48743B4B-BDEA-DC59-E766-996CB80EA03B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.AppHost.Redirect", "AppHost.Redirect\Cloud5mins.ShortenerTools.AppHost.Redirect.csproj", "{1C276950-388A-4CCF-998F-308534197198}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FunctionsLight", "FunctionsLight", "{978765B0-30B3-FFB1-F51A-527AE8C5D709}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppHost.Admin", "AppHost.Admin", "{5FE8D6DD-1E2C-845E-148A-9E14D73F3A3B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.AppHost.Admin", "AppHost.Admin\Cloud5mins.ShortenerTools.AppHost.Admin.csproj", "{FFDD6A77-8DCF-4137-9566-9E15EE0358ED}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{D82C56D6-B2CD-F611-9E31-04638F7EC30E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TinyBlazorAdmin", "TinyBlazorAdmin", "{E0AB1A1F-3E51-5469-E2E5-E04CC29A76AB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x64.ActiveCfg = Debug|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x64.Build.0 = Debug|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x86.ActiveCfg = Debug|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Debug|x86.Build.0 = Debug|Any CPU {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|Any CPU.ActiveCfg = Release|Any CPU {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|Any CPU.Build.0 = Release|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x64.ActiveCfg = Release|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x64.Build.0 = Release|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x86.ActiveCfg = Release|Any CPU + {99DF620C-9FF5-4233-B556-E5E9E143AB60}.Release|x86.Build.0 = Release|Any CPU {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x64.Build.0 = Debug|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Debug|x86.Build.0 = Debug|Any CPU {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|Any CPU.Build.0 = Release|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x64.ActiveCfg = Release|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x64.Build.0 = Release|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x86.ActiveCfg = Release|Any CPU + {1C59956F-01A4-4F22-8B75-39CC48A8290C}.Release|x86.Build.0 = Release|Any CPU {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x64.ActiveCfg = Debug|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x64.Build.0 = Debug|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x86.ActiveCfg = Debug|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Debug|x86.Build.0 = Debug|Any CPU {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|Any CPU.ActiveCfg = Release|Any CPU {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|Any CPU.Build.0 = Release|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x64.ActiveCfg = Release|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x64.Build.0 = Release|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x86.ActiveCfg = Release|Any CPU + {3DC7E590-FE84-4D0B-B989-9CD6B31B325A}.Release|x86.Build.0 = Release|Any CPU {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x64.ActiveCfg = Debug|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x64.Build.0 = Debug|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x86.ActiveCfg = Debug|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Debug|x86.Build.0 = Debug|Any CPU {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|Any CPU.ActiveCfg = Release|Any CPU {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|Any CPU.Build.0 = Release|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x64.ActiveCfg = Release|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x64.Build.0 = Release|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x86.ActiveCfg = Release|Any CPU + {6567F739-8BCC-4B4B-8CCC-EEEB9B28465F}.Release|x86.Build.0 = Release|Any CPU {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x64.ActiveCfg = Debug|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x64.Build.0 = Debug|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x86.ActiveCfg = Debug|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Debug|x86.Build.0 = Debug|Any CPU {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|Any CPU.Build.0 = Release|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x64.ActiveCfg = Release|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x64.Build.0 = Release|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x86.ActiveCfg = Release|Any CPU + {85C6D9A8-EB37-4FA3-9E83-7178825C9EA5}.Release|x86.Build.0 = Release|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x64.ActiveCfg = Debug|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x64.Build.0 = Debug|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x86.ActiveCfg = Debug|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Debug|x86.Build.0 = Debug|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|Any CPU.ActiveCfg = Release|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|Any CPU.Build.0 = Release|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x64.ActiveCfg = Release|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x64.Build.0 = Release|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x86.ActiveCfg = Release|Any CPU + {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x86.Build.0 = Release|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Debug|x64.ActiveCfg = Debug|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Debug|x64.Build.0 = Debug|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Debug|x86.ActiveCfg = Debug|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Debug|x86.Build.0 = Debug|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Release|Any CPU.Build.0 = Release|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Release|x64.ActiveCfg = Release|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Release|x64.Build.0 = Release|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Release|x86.ActiveCfg = Release|Any CPU + {1C276950-388A-4CCF-998F-308534197198}.Release|x86.Build.0 = Release|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|x64.ActiveCfg = Debug|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|x64.Build.0 = Debug|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|x86.ActiveCfg = Debug|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|x86.Build.0 = Debug|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|Any CPU.Build.0 = Release|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|x64.ActiveCfg = Release|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|x64.Build.0 = Release|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|x86.ActiveCfg = Release|Any CPU + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1C276950-388A-4CCF-998F-308534197198} = {48743B4B-BDEA-DC59-E766-996CB80EA03B} + {FFDD6A77-8DCF-4137-9566-9E15EE0358ED} = {5FE8D6DD-1E2C-845E-148A-9E14D73F3A3B} EndGlobalSection EndGlobal diff --git a/src/deploy-admin.sh b/src/deploy-admin.sh new file mode 100755 index 00000000..1d9553e1 --- /dev/null +++ b/src/deploy-admin.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy Api + TinyBlazorAdmin as an App Service +# +# For a single App Service, both projects need to be merged into one deployable +# web application. Until then, they can be deployed as separate App Services +# within the same App Service Plan. +# +# Usage: +# ./deploy-admin.sh + +RG="${1:-rg-azurl-admin}" +LOC="${2:-eastus2}" +ENV_NAME="admin-${LOC}" + +echo "=== Provisioning infrastructure ===" +az deployment group create \ + --resource-group "$RG" \ + --template-file infra/admin/main.bicep \ + --parameters environmentName="$ENV_NAME" location="$LOC" + +ADMIN_NAME=$(az deployment group show --resource-group "$RG" -n main --query properties.outputs.WEB_APP_NAME.value -o tsv) + +echo "=== Deploy Api ===" +cd Api +dotnet publish -c Release -o ../publish/admin +cd .. +zip -j publish/admin.zip publish/admin/* +az webapp deploy --resource-group "$RG" --name "$ADMIN_NAME" --type zip --src-path publish/admin.zip + +echo "=== Done ===" +echo "API: https://${ADMIN_NAME}.azurewebsites.net" diff --git a/src/deploy-redirect.sh b/src/deploy-redirect.sh new file mode 100755 index 00000000..dddd0faf --- /dev/null +++ b/src/deploy-redirect.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Deploy FunctionsLight as a real Azure Function App +# Prerequisites: +# - Azure CLI logged in (az login) +# - Resource group created +# +# Usage: +# ./deploy-redirect.sh + +RG="${1:-rg-azurl-redirect}" +LOC="${2:-eastus2}" +ENV_NAME="redirect-${LOC}" + +echo "=== Provisioning infrastructure ===" +az deployment group create \ + --resource-group "$RG" \ + --template-file infra/redirect/main.bicep \ + --parameters environmentName="$ENV_NAME" location="$LOC" DefaultRedirectUrl="https://azure.com" + +# Get outputs +FUNC_NAME=$(az deployment group show --resource-group "$RG" -n main --query properties.outputs.FUNCTION_APP_NAME.value -o tsv) + +echo "=== Publishing FunctionsLight ===" +cd FunctionsLight +dotnet publish -c Release -o ../publish/redirect +cd .. + +func azure functionapp publish "$FUNC_NAME" --dotnet-isolated + +echo "=== Done ===" +echo "Function App: https://${FUNC_NAME}.azurewebsites.net" diff --git a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep deleted file mode 100644 index 77337b54..00000000 --- a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep +++ /dev/null @@ -1,79 +0,0 @@ -@description('The location for the resource(s) to be deployed.') -param location string = resourceGroup().location - -param principalId string - -param principalType string - -resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = { - name: take('funcstoragea17ca${uniqueString(resourceGroup().id)}', 24) - kind: 'StorageV2' - location: location - sku: { - name: 'Standard_GRS' - } - properties: { - accessTier: 'Hot' - allowSharedKeyAccess: false - minimumTlsVersion: 'TLS1_2' - networkAcls: { - defaultAction: 'Allow' - } - } - tags: { - 'aspire-resource-name': 'funcstoragea17ca' - } -} - -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: funcstoragea17ca -} - -resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') - principalType: principalType - } - scope: funcstoragea17ca -} - -resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') - principalType: principalType - } - scope: funcstoragea17ca -} - -resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') - principalType: principalType - } - scope: funcstoragea17ca -} - -resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') - principalType: principalType - } - scope: funcstoragea17ca -} - -output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob - -output dataLakeEndpoint string = funcstoragea17ca.properties.primaryEndpoints.dfs - -output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue - -output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table diff --git a/src/infra/main.bicep b/src/infra/main.bicep deleted file mode 100644 index adf882bb..00000000 --- a/src/infra/main.bicep +++ /dev/null @@ -1,69 +0,0 @@ -targetScope = 'subscription' - -@minLength(1) -@maxLength(64) -@description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') -param environmentName string - -@minLength(1) -@description('The location used for all deployed resources') -param location string - -@description('Id of the user or app to assign application roles') -param principalId string = '' - -param CustomDomain string -param DefaultRedirectUrl string - -var tags = { - 'azd-env-name': environmentName -} - -resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { - name: 'rg-${environmentName}' - location: location - tags: tags -} -module resources 'resources.bicep' = { - scope: rg - name: 'resources' - params: { - location: location - tags: tags - principalId: principalId - } -} - -module funcstoragea17ca 'funcstoragea17ca/funcstoragea17ca.module.bicep' = { - name: 'funcstoragea17ca' - scope: rg - params: { - location: location - principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID - principalType: 'ServicePrincipal' - } -} -module url_data 'url-data/url-data.module.bicep' = { - name: 'url-data' - scope: rg - params: { - location: location - principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID - principalType: 'ServicePrincipal' - } -} - -output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID -output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME -output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = resources.outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT -output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = resources.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID -output AZURE_CONTAINER_REGISTRY_NAME string = resources.outputs.AZURE_CONTAINER_REGISTRY_NAME -output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME -output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID -output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN -output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint -output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint -output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint -output FUNCSTORAGEA17CA_TABLEENDPOINT string = funcstoragea17ca.outputs.tableEndpoint -output URL_DATA_TABLEENDPOINT string = url_data.outputs.tableEndpoint diff --git a/src/infra/main.parameters.json b/src/infra/main.parameters.json deleted file mode 100644 index b89be02d..00000000 --- a/src/infra/main.parameters.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "principalId": { - "value": "${AZURE_PRINCIPAL_ID}" - }, - "CustomDomain": { - "value": "${AZURE_CUSTOM_DOMAIN}" - }, - "DefaultRedirectUrl": { - "value": "${AZURE_DEFAULT_REDIRECT_URL}" - }, - "environmentName": { - "value": "${AZURE_ENV_NAME}" - }, - "location": { - "value": "${AZURE_LOCATION}" - } - } - } - \ No newline at end of file diff --git a/src/infra/resources.bicep b/src/infra/resources.bicep deleted file mode 100644 index 6bedbd68..00000000 --- a/src/infra/resources.bicep +++ /dev/null @@ -1,94 +0,0 @@ -@description('The location used for all deployed resources') -param location string = resourceGroup().location -@description('Id of the user or app to assign application roles') -param principalId string = '' - - -@description('Tags that will be applied to all resources') -param tags object = {} - -var resourceToken = uniqueString(resourceGroup().id) - -resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: 'mi-${resourceToken}' - location: location - tags: tags -} - -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { - name: replace('acr-${resourceToken}', '-', '') - location: location - sku: { - name: 'Basic' - } - tags: tags -} - -resource caeMiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerRegistry.id, managedIdentity.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) - scope: containerRegistry - properties: { - principalId: managedIdentity.properties.principalId - principalType: 'ServicePrincipal' - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') - } -} - -resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { - name: 'law-${resourceToken}' - location: location - properties: { - sku: { - name: 'PerGB2018' - } - } - tags: tags -} - -resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = { - name: 'cae-${resourceToken}' - location: location - properties: { - workloadProfiles: [{ - workloadProfileType: 'Consumption' - name: 'consumption' - }] - appLogsConfiguration: { - destination: 'log-analytics' - logAnalyticsConfiguration: { - customerId: logAnalyticsWorkspace.properties.customerId - sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey - } - } - } - tags: tags - - resource aspireDashboard 'dotNetComponents' = { - name: 'aspire-dashboard' - properties: { - componentType: 'AspireDashboard' - } - } - -} - -resource explicitContributorUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(containerAppEnvironment.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) - scope: containerAppEnvironment - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') - } -} - -output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId -output MANAGED_IDENTITY_NAME string = managedIdentity.name -output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId -output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = logAnalyticsWorkspace.name -output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = logAnalyticsWorkspace.id -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer -output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = managedIdentity.id -output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.name -output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = containerAppEnvironment.name -output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = containerAppEnvironment.id -output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = containerAppEnvironment.properties.defaultDomain diff --git a/src/infra/url-data/url-data.module.bicep b/src/infra/url-data/url-data.module.bicep deleted file mode 100644 index 248c278c..00000000 --- a/src/infra/url-data/url-data.module.bicep +++ /dev/null @@ -1,67 +0,0 @@ -@description('The location for the resource(s) to be deployed.') -param location string = resourceGroup().location - -param principalId string - -param principalType string - -resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = { - name: take('urldata${uniqueString(resourceGroup().id)}', 24) - kind: 'StorageV2' - location: location - sku: { - name: 'Standard_GRS' - } - properties: { - accessTier: 'Hot' - allowSharedKeyAccess: false - minimumTlsVersion: 'TLS1_2' - networkAcls: { - defaultAction: 'Allow' - } - } - tags: { - 'aspire-resource-name': 'url-data' - } -} - -resource blobs 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { - name: 'default' - parent: url_data -} - -resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') - principalType: principalType - } - scope: url_data -} - -resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') - principalType: principalType - } - scope: url_data -} - -resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) - properties: { - principalId: principalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') - principalType: principalType - } - scope: url_data -} - -output blobEndpoint string = url_data.properties.primaryEndpoints.blob - -output queueEndpoint string = url_data.properties.primaryEndpoints.queue - -output tableEndpoint string = url_data.properties.primaryEndpoints.table \ No newline at end of file From ed3327c530849501a482b894f5363d29f52d1986 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 7 Aug 2026 09:41:30 -0400 Subject: [PATCH 05/17] starting point --- .../admin-appservice/azure.admin.yaml | 8 -------- .../split-resources/redirect/azure.redirect.yaml | 8 -------- src/AppHost.Admin/Program.cs | 14 ++++---------- src/deploy-1/azure.yaml | 15 +++++++++++++++ 4 files changed, 19 insertions(+), 26 deletions(-) delete mode 100644 deployment/split-resources/admin-appservice/azure.admin.yaml delete mode 100644 deployment/split-resources/redirect/azure.redirect.yaml create mode 100644 src/deploy-1/azure.yaml diff --git a/deployment/split-resources/admin-appservice/azure.admin.yaml b/deployment/split-resources/admin-appservice/azure.admin.yaml deleted file mode 100644 index 57a07af8..00000000 --- a/deployment/split-resources/admin-appservice/azure.admin.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json - -name: src.admin -services: - app: - language: dotnet - project: ../../../src/AppHost/AppHost.csproj - host: appservice diff --git a/deployment/split-resources/redirect/azure.redirect.yaml b/deployment/split-resources/redirect/azure.redirect.yaml deleted file mode 100644 index 5f11f3c6..00000000 --- a/deployment/split-resources/redirect/azure.redirect.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json - -name: src.redirect -services: - app: - language: dotnet - project: ../../../AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj - host: function diff --git a/src/AppHost.Admin/Program.cs b/src/AppHost.Admin/Program.cs index d49de86f..5b5af3c3 100644 --- a/src/AppHost.Admin/Program.cs +++ b/src/AppHost.Admin/Program.cs @@ -6,17 +6,11 @@ var defaultRedirectUrl = builder.AddParameter("DefaultRedirectUrl"); // To share the same storage as AppHost.Redirect, replace the storage creation below with: -// var existingStorageName = builder.AddParameter("existingStorageName"); -// var existingStorageRg = builder.AddParameter("existingStorageResourceGroup"); -// var urlStorage = builder.AddAzureStorage("url-data") -// .AsExisting(existingStorageName, existingStorageRg); +var existingStorageName = builder.AddParameter("existingStorageName"); +var existingStorageResourceGroup = builder.AddParameter("existingStorageResourceGroup"); -var urlStorage = builder.AddAzureStorage("url-data"); - -if (builder.Environment.IsDevelopment()) -{ - urlStorage.RunAsEmulator(); -} +var urlStorage = builder.AddAzureStorage("url-data") + .AsExisting(existingStorageName, existingStorageResourceGroup); var strTables = urlStorage.AddTables("strTables"); diff --git a/src/deploy-1/azure.yaml b/src/deploy-1/azure.yaml new file mode 100644 index 00000000..4aa1faeb --- /dev/null +++ b/src/deploy-1/azure.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +# azure.yaml +name: AzUrlShortener +services: + azfunc-light: + project: ../FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj + language: dotnet + host: function # <-- Tells AZD this is a native Azure Function App! + + # App Service for Api + Admin (Main + Sidecar or Multi-container) + admin: + project: ../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj + language: dotnet + host: appservice \ No newline at end of file From c0c00414f93e0d91b8838abd9f84e4d7a9077e5e Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 7 Aug 2026 09:42:14 -0400 Subject: [PATCH 06/17] clean reboot --- ...d5mins.ShortenerTools.AppHost.Admin.csproj | 21 --------------- src/AppHost.Admin/Program.cs | 27 ------------------- src/AppHost.Admin/appsettings.json | 13 --------- ...ins.ShortenerTools.AppHost.Redirect.csproj | 21 --------------- src/AppHost.Redirect/Program.cs | 22 --------------- src/AppHost.Redirect/appsettings.json | 12 --------- 6 files changed, 116 deletions(-) delete mode 100644 src/AppHost.Admin/Cloud5mins.ShortenerTools.AppHost.Admin.csproj delete mode 100644 src/AppHost.Admin/Program.cs delete mode 100644 src/AppHost.Admin/appsettings.json delete mode 100644 src/AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj delete mode 100644 src/AppHost.Redirect/Program.cs delete mode 100644 src/AppHost.Redirect/appsettings.json diff --git a/src/AppHost.Admin/Cloud5mins.ShortenerTools.AppHost.Admin.csproj b/src/AppHost.Admin/Cloud5mins.ShortenerTools.AppHost.Admin.csproj deleted file mode 100644 index eba5d294..00000000 --- a/src/AppHost.Admin/Cloud5mins.ShortenerTools.AppHost.Admin.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - Exe - net10.0 - enable - enable - true - true - - - - - - - - - - - - diff --git a/src/AppHost.Admin/Program.cs b/src/AppHost.Admin/Program.cs deleted file mode 100644 index 5b5af3c3..00000000 --- a/src/AppHost.Admin/Program.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Extensions.Hosting; - -var builder = DistributedApplication.CreateBuilder(args); - -var customDomain = builder.AddParameter("CustomDomain"); -var defaultRedirectUrl = builder.AddParameter("DefaultRedirectUrl"); - -// To share the same storage as AppHost.Redirect, replace the storage creation below with: -var existingStorageName = builder.AddParameter("existingStorageName"); -var existingStorageResourceGroup = builder.AddParameter("existingStorageResourceGroup"); - -var urlStorage = builder.AddAzureStorage("url-data") - .AsExisting(existingStorageName, existingStorageResourceGroup); - -var strTables = urlStorage.AddTables("strTables"); - -var manAPI = builder.AddProject("api") - .WithReference(strTables) - .WaitFor(strTables) - .WithEnvironment("CustomDomain", customDomain) - .WithEnvironment("DefaultRedirectUrl", defaultRedirectUrl); - -builder.AddProject("admin") - .WithExternalHttpEndpoints() - .WithReference(manAPI); - -builder.Build().Run(); diff --git a/src/AppHost.Admin/appsettings.json b/src/AppHost.Admin/appsettings.json deleted file mode 100644 index 00c52bac..00000000 --- a/src/AppHost.Admin/appsettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Aspire.Hosting.Dcp": "Warning" - } - }, - "Parameters":{ - "CustomDomain": "https://c5m.ca", - "DefaultRedirectUrl": "https://azure.com" - } -} diff --git a/src/AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj b/src/AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj deleted file mode 100644 index 09f35a8c..00000000 --- a/src/AppHost.Redirect/Cloud5mins.ShortenerTools.AppHost.Redirect.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - Exe - net10.0 - enable - enable - true - true - - - - - - - - - - - - diff --git a/src/AppHost.Redirect/Program.cs b/src/AppHost.Redirect/Program.cs deleted file mode 100644 index 153ff4a6..00000000 --- a/src/AppHost.Redirect/Program.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.Extensions.Hosting; - -var builder = DistributedApplication.CreateBuilder(args); - -var defaultRedirectUrl = builder.AddParameter("DefaultRedirectUrl"); - -var urlStorage = builder.AddAzureStorage("url-data"); - -if (builder.Environment.IsDevelopment()) -{ - urlStorage.RunAsEmulator(); -} - -var strTables = urlStorage.AddTables("strTables"); - -var azFuncLight = builder.AddAzureFunctionsProject("azfunc-light") - .WithReference(strTables) - .WaitFor(strTables) - .WithEnvironment("DefaultRedirectUrl", defaultRedirectUrl) - .WithExternalHttpEndpoints(); - -builder.Build().Run(); diff --git a/src/AppHost.Redirect/appsettings.json b/src/AppHost.Redirect/appsettings.json deleted file mode 100644 index d72f83d2..00000000 --- a/src/AppHost.Redirect/appsettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Aspire.Hosting.Dcp": "Warning" - } - }, - "Parameters":{ - "DefaultRedirectUrl": "https://azure.com" - } -} From ada812bcc57145ee5b05c0b6ab7ad3001e96dddc Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 7 Aug 2026 10:20:27 -0400 Subject: [PATCH 07/17] step --- ...ud5mins.ShortenerTools.FunctionsLight.csproj | 3 ++- ...d5mins.ShortenerTools.TinyBlazorAdmin.csproj | 5 +++-- src/azure-aca.yaml | 8 ++++++++ src/azure.yaml | 17 ++++++++++++----- src/deploy-1/azure.yaml | 15 --------------- 5 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 src/azure-aca.yaml delete mode 100644 src/deploy-1/azure.yaml diff --git a/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj b/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj index d6e1e138..ced86731 100644 --- a/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj +++ b/src/FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj @@ -1,4 +1,4 @@ - + net10.0 v4 @@ -6,6 +6,7 @@ enable enable true + 713be2c6-6051-4bcf-ac1c-2ab228a0e42d diff --git a/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj b/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj index 25d231f4..d332ee3a 100644 --- a/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj +++ b/src/TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj @@ -1,14 +1,15 @@ - + net10.0 enable enable true + 5a6db536-4d9e-4bfd-8006-85420b82f175 - + diff --git a/src/azure-aca.yaml b/src/azure-aca.yaml new file mode 100644 index 00000000..929a3b2b --- /dev/null +++ b/src/azure-aca.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: src +services: + app: + language: dotnet + project: ./AppHost/Cloud5mins.ShortenerTools.AppHost.csproj + host: containerapp diff --git a/src/azure.yaml b/src/azure.yaml index 929a3b2b..0c86938e 100644 --- a/src/azure.yaml +++ b/src/azure.yaml @@ -1,8 +1,15 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json -name: src -services: - app: + +name: budget-azUrl +services: + azfunc-light: + project: ./FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj + language: dotnet + host: function + + + admin: + project: ./TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj language: dotnet - project: ./AppHost/Cloud5mins.ShortenerTools.AppHost.csproj - host: containerapp + host: appservice \ No newline at end of file diff --git a/src/deploy-1/azure.yaml b/src/deploy-1/azure.yaml deleted file mode 100644 index 4aa1faeb..00000000 --- a/src/deploy-1/azure.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json - -# azure.yaml -name: AzUrlShortener -services: - azfunc-light: - project: ../FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj - language: dotnet - host: function # <-- Tells AZD this is a native Azure Function App! - - # App Service for Api + Admin (Main + Sidecar or Multi-container) - admin: - project: ../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj - language: dotnet - host: appservice \ No newline at end of file From c792cca5e1da7bd0049ab4a35abfb64170436d9c Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 7 Aug 2026 10:21:58 -0400 Subject: [PATCH 08/17] cleaning up --- src/deploy-admin.sh | 33 ------------------- src/deploy-redirect.sh | 33 ------------------- src/next-steps.md | 74 ------------------------------------------ 3 files changed, 140 deletions(-) delete mode 100755 src/deploy-admin.sh delete mode 100755 src/deploy-redirect.sh delete mode 100644 src/next-steps.md diff --git a/src/deploy-admin.sh b/src/deploy-admin.sh deleted file mode 100755 index 1d9553e1..00000000 --- a/src/deploy-admin.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Deploy Api + TinyBlazorAdmin as an App Service -# -# For a single App Service, both projects need to be merged into one deployable -# web application. Until then, they can be deployed as separate App Services -# within the same App Service Plan. -# -# Usage: -# ./deploy-admin.sh - -RG="${1:-rg-azurl-admin}" -LOC="${2:-eastus2}" -ENV_NAME="admin-${LOC}" - -echo "=== Provisioning infrastructure ===" -az deployment group create \ - --resource-group "$RG" \ - --template-file infra/admin/main.bicep \ - --parameters environmentName="$ENV_NAME" location="$LOC" - -ADMIN_NAME=$(az deployment group show --resource-group "$RG" -n main --query properties.outputs.WEB_APP_NAME.value -o tsv) - -echo "=== Deploy Api ===" -cd Api -dotnet publish -c Release -o ../publish/admin -cd .. -zip -j publish/admin.zip publish/admin/* -az webapp deploy --resource-group "$RG" --name "$ADMIN_NAME" --type zip --src-path publish/admin.zip - -echo "=== Done ===" -echo "API: https://${ADMIN_NAME}.azurewebsites.net" diff --git a/src/deploy-redirect.sh b/src/deploy-redirect.sh deleted file mode 100755 index dddd0faf..00000000 --- a/src/deploy-redirect.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Deploy FunctionsLight as a real Azure Function App -# Prerequisites: -# - Azure CLI logged in (az login) -# - Resource group created -# -# Usage: -# ./deploy-redirect.sh - -RG="${1:-rg-azurl-redirect}" -LOC="${2:-eastus2}" -ENV_NAME="redirect-${LOC}" - -echo "=== Provisioning infrastructure ===" -az deployment group create \ - --resource-group "$RG" \ - --template-file infra/redirect/main.bicep \ - --parameters environmentName="$ENV_NAME" location="$LOC" DefaultRedirectUrl="https://azure.com" - -# Get outputs -FUNC_NAME=$(az deployment group show --resource-group "$RG" -n main --query properties.outputs.FUNCTION_APP_NAME.value -o tsv) - -echo "=== Publishing FunctionsLight ===" -cd FunctionsLight -dotnet publish -c Release -o ../publish/redirect -cd .. - -func azure functionapp publish "$FUNC_NAME" --dotnet-isolated - -echo "=== Done ===" -echo "Function App: https://${FUNC_NAME}.azurewebsites.net" diff --git a/src/next-steps.md b/src/next-steps.md deleted file mode 100644 index 4a558546..00000000 --- a/src/next-steps.md +++ /dev/null @@ -1,74 +0,0 @@ -# Next Steps after `azd init` - -## Table of Contents - -1. [Next Steps](#next-steps) -2. [What was added](#what-was-added) -3. [Billing](#billing) -4. [Troubleshooting](#troubleshooting) - -## Next Steps - -### Provision infrastructure and deploy application code - -Run `azd up` to provision your infrastructure and deploy to Azure in one step (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! - -To troubleshoot any issues, see [troubleshooting](#troubleshooting). - -### Configure CI/CD pipeline - -1. Create a workflow pipeline file locally. The following starters are available: - - [Deploy with GitHub Actions](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.github/workflows/azure-dev.yml) - - [Deploy with Azure Pipelines](https://github.com/Azure-Samples/azd-starter-bicep/blob/main/.azdo/pipelines/azure-dev.yml) -2. Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. - -## What was added - -### Infrastructure configuration - -To describe the infrastructure and application, an `azure.yaml` was added with the following directory structure: - -```yaml -- azure.yaml # azd project configuration -``` - -This file contains a single service, which references your project's App Host. When needed, `azd` generates the required infrastructure as code in memory and uses it. - -If you would like to see or modify the infrastructure that `azd` uses, run `azd infra synth` to persist it to disk. - -If you do this, some additional directories will be created: - -```yaml -- infra/ # Infrastructure as Code (bicep) files - - main.bicep # main deployment module - - resources.bicep # resources shared across your application's services -``` - -In addition, for each project resource referenced by your app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. This file contains the infrastructure as code for running the project on Azure Container Apps. - -*Note*: Once you have synthesized your infrastructure to disk, changes made to your App Host will not be reflected in the infrastructure. You can re-generate the infrastructure by running `azd infra synth` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra synth` to overwrite the files without prompting. - -*Note*: `azd infra synth` is currently an alpha feature and must be explicitly enabled by running `azd config set alpha.infraSynth on`. You only need to do this once. - -## Billing - -Visit the *Cost Management + Billing* page in Azure Portal to track current spend. For more information about how you're billed, and how you can monitor the costs incurred in your Azure subscriptions, visit [billing overview](https://learn.microsoft.com/azure/developer/intro/azure-developer-billing). - -## Troubleshooting - -Q: I visited the service endpoint listed, and I'm seeing a blank page, a generic welcome page, or an error page. - -A: Your service may have failed to start, or it may be missing some configuration settings. To investigate further: - -1. Run `azd show`. Click on the link under "View in Azure Portal" to open the resource group in Azure Portal. -2. Navigate to the specific Container App service that is failing to deploy. -3. Click on the failing revision under "Revisions with Issues". -4. Review "Status details" for more information about the type of failure. -5. Observe the log outputs from Console log stream and System log stream to identify any errors. -6. If logs are written to disk, use *Console* in the navigation to connect to a shell within the running container. - -For more troubleshooting information, visit [Container Apps troubleshooting](https://learn.microsoft.com/azure/container-apps/troubleshooting). - -### Additional information - -For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert). From 18f31e681778b51175c17a17e3f518142a34b053 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 7 Aug 2026 11:38:00 -0400 Subject: [PATCH 09/17] good progress it deploy but missing a data storage --- doc/how-deploy-budget-solution.md | 5 + src/Api/Cloud5mins.ShortenerTools.Api.csproj | 3 +- src/AppHost/infra/admin.tmpl.yaml | 48 ++ src/AppHost/infra/api.tmpl.yaml | 51 ++ src/AppHost/infra/azfunc-light.tmpl.yaml | 66 ++ src/azure-aca.yaml | 8 - src/azure.yaml | 17 +- src/budget-deployment/azure.yaml | 22 + src/budget-deployment/infra/main.bicep | 88 +++ .../infra/main.parameters.json | 21 + src/budget-deployment/infra/resources.bicep | 203 ++++++ .../funcstoragea17ca-roles.module.bicep | 52 ++ .../funcstoragea17ca.module.bicep | 35 + src/infra/main.bicep | 85 +++ src/infra/main.json | 685 ++++++++++++++++++ src/infra/main.parameters.json | 22 + src/infra/resources.bicep | 85 +++ .../url-data-roles.module.bicep | 42 ++ src/infra/url-data/url-data.module.bicep | 35 + src/next-steps.md | 73 ++ 20 files changed, 1625 insertions(+), 21 deletions(-) create mode 100644 doc/how-deploy-budget-solution.md create mode 100644 src/AppHost/infra/admin.tmpl.yaml create mode 100644 src/AppHost/infra/api.tmpl.yaml create mode 100644 src/AppHost/infra/azfunc-light.tmpl.yaml delete mode 100644 src/azure-aca.yaml create mode 100644 src/budget-deployment/azure.yaml create mode 100644 src/budget-deployment/infra/main.bicep create mode 100644 src/budget-deployment/infra/main.parameters.json create mode 100644 src/budget-deployment/infra/resources.bicep create mode 100644 src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep create mode 100644 src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep create mode 100644 src/infra/main.bicep create mode 100644 src/infra/main.json create mode 100644 src/infra/main.parameters.json create mode 100644 src/infra/resources.bicep create mode 100644 src/infra/url-data-roles/url-data-roles.module.bicep create mode 100644 src/infra/url-data/url-data.module.bicep create mode 100644 src/next-steps.md diff --git a/doc/how-deploy-budget-solution.md b/doc/how-deploy-budget-solution.md new file mode 100644 index 00000000..9346428b --- /dev/null +++ b/doc/how-deploy-budget-solution.md @@ -0,0 +1,5 @@ + + +cd src/budget-deployment + +azd up \ No newline at end of file diff --git a/src/Api/Cloud5mins.ShortenerTools.Api.csproj b/src/Api/Cloud5mins.ShortenerTools.Api.csproj index 19e21d56..e492bbf0 100644 --- a/src/Api/Cloud5mins.ShortenerTools.Api.csproj +++ b/src/Api/Cloud5mins.ShortenerTools.Api.csproj @@ -1,10 +1,11 @@ - + net10.0 enable enable true + 500bdb23-5095-412f-b1e9-421be27fb629 diff --git a/src/AppHost/infra/admin.tmpl.yaml b/src/AppHost/infra/admin.tmpl.yaml new file mode 100644 index 00000000..4a85ff1d --- /dev/null +++ b/src/AppHost/infra/admin.tmpl.yaml @@ -0,0 +1,48 @@ +api-version: 2024-02-02-preview +location: {{ .Env.AZURE_LOCATION }} +identity: + type: UserAssigned + userAssignedIdentities: + ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" + : {} +properties: + environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} + configuration: + activeRevisionsMode: single + runtime: + dotnet: + autoConfigureDataProtection: true + ingress: + external: true + targetPort: {{ targetPortOrDefault 8080 }} + transport: http + allowInsecure: false + registries: + - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} + identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} + template: + containers: + - image: {{ .Image }} + name: admin + env: + - name: AZURE_CLIENT_ID + value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} + - name: API_HTTP + value: http://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + - name: API_HTTPS + value: https://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED + value: "true" + - name: HTTP_PORTS + value: '{{ targetPortOrDefault 0 }}' + - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY + value: in_memory + - name: services__api__http__0 + value: http://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + - name: services__api__https__0 + value: https://api.internal.{{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN }} + scale: + minReplicas: 1 +tags: + azd-service-name: admin + aspire-resource-name: admin diff --git a/src/AppHost/infra/api.tmpl.yaml b/src/AppHost/infra/api.tmpl.yaml new file mode 100644 index 00000000..d7e65517 --- /dev/null +++ b/src/AppHost/infra/api.tmpl.yaml @@ -0,0 +1,51 @@ +api-version: 2024-02-02-preview +location: {{ .Env.AZURE_LOCATION }} +identity: + type: UserAssigned + userAssignedIdentities: + ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" + : {} +properties: + environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} + configuration: + activeRevisionsMode: single + runtime: + dotnet: + autoConfigureDataProtection: true + ingress: + external: false + targetPort: {{ targetPortOrDefault 8080 }} + transport: http + allowInsecure: true + registries: + - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} + identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} + secrets: + - name: connectionstrings--strtables + value: '{{ .Env.URL_DATA_TABLEENDPOINT }}' + template: + containers: + - image: {{ .Image }} + name: api + env: + - name: AZURE_CLIENT_ID + value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} + - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED + value: "true" + - name: CustomDomain + value: '{{ parameter "CustomDomain" }}' + - name: DefaultRedirectUrl + value: '{{ parameter "DefaultRedirectUrl" }}' + - name: HTTP_PORTS + value: '{{ targetPortOrDefault 0 }}' + - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY + value: in_memory + - name: STRTABLES_URI + value: '{{ .Env.URL_DATA_TABLEENDPOINT }}' + - name: ConnectionStrings__strTables + secretRef: connectionstrings--strtables + scale: + minReplicas: 1 +tags: + azd-service-name: api + aspire-resource-name: api diff --git a/src/AppHost/infra/azfunc-light.tmpl.yaml b/src/AppHost/infra/azfunc-light.tmpl.yaml new file mode 100644 index 00000000..f48e7e30 --- /dev/null +++ b/src/AppHost/infra/azfunc-light.tmpl.yaml @@ -0,0 +1,66 @@ +api-version: 2024-02-02-preview +location: {{ .Env.AZURE_LOCATION }} +identity: + type: UserAssigned + userAssignedIdentities: + ? "{{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}" + : {} +properties: + environmentId: {{ .Env.AZURE_CONTAINER_APPS_ENVIRONMENT_ID }} + configuration: + activeRevisionsMode: single + runtime: + dotnet: + autoConfigureDataProtection: true + ingress: + external: true + targetPort: 8080 + transport: http + allowInsecure: false + registries: + - server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }} + identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }} + template: + containers: + - image: {{ .Image }} + name: azfunc-light + env: + - name: AZURE_CLIENT_ID + value: {{ .Env.MANAGED_IDENTITY_CLIENT_ID }} + - name: ASPNETCORE_FORWARDEDHEADERS_ENABLED + value: "true" + - name: ASPNETCORE_URLS + value: http://+:8080 + - name: Aspire__Azure__Data__Tables__AzureWebJobsStorage__ServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_TABLEENDPOINT }}' + - name: Aspire__Azure__Data__Tables__strTables__ServiceUri + value: '{{ .Env.URL_DATA_TABLEENDPOINT }}' + - name: Aspire__Azure__Storage__Blobs__AzureWebJobsStorage__ServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_BLOBENDPOINT }}' + - name: Aspire__Azure__Storage__Files__DataLake__AzureWebJobsStorage__ServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_DATALAKEENDPOINT }}' + - name: Aspire__Azure__Storage__Queues__AzureWebJobsStorage__ServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_QUEUEENDPOINT }}' + - name: AzureFunctionsJobHost__telemetryMode + value: OpenTelemetry + - name: AzureWebJobsStorage__blobServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_BLOBENDPOINT }}' + - name: AzureWebJobsStorage__dataLakeServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_DATALAKEENDPOINT }}' + - name: AzureWebJobsStorage__queueServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_QUEUEENDPOINT }}' + - name: AzureWebJobsStorage__tableServiceUri + value: '{{ .Env.FUNCSTORAGEA17CA_TABLEENDPOINT }}' + - name: DefaultRedirectUrl + value: '{{ parameter "DefaultRedirectUrl" }}' + - name: FUNCTIONS_WORKER_RUNTIME + value: dotnet-isolated + - name: OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY + value: in_memory + - name: strTables__tableServiceUri + value: '{{ .Env.URL_DATA_TABLEENDPOINT }}' + scale: + minReplicas: 1 +tags: + azd-service-name: azfunc-light + aspire-resource-name: azfunc-light diff --git a/src/azure-aca.yaml b/src/azure-aca.yaml deleted file mode 100644 index 929a3b2b..00000000 --- a/src/azure-aca.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json - -name: src -services: - app: - language: dotnet - project: ./AppHost/Cloud5mins.ShortenerTools.AppHost.csproj - host: containerapp diff --git a/src/azure.yaml b/src/azure.yaml index 0c86938e..f0ab76bb 100644 --- a/src/azure.yaml +++ b/src/azure.yaml @@ -1,15 +1,8 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json - -name: budget-azUrl -services: - azfunc-light: - project: ./FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj - language: dotnet - host: function - - - admin: - project: ./TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj +name: azurlshortener +services: + app: language: dotnet - host: appservice \ No newline at end of file + project: ./AppHost/Cloud5mins.ShortenerTools.AppHost.csproj + host: containerapp diff --git a/src/budget-deployment/azure.yaml b/src/budget-deployment/azure.yaml new file mode 100644 index 00000000..bf1f546c --- /dev/null +++ b/src/budget-deployment/azure.yaml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + + +name: budget-azUrl +infra: + provider: bicep + path: ./infra +services: + azfunc-light: + project: ../FunctionsLight/Cloud5mins.ShortenerTools.FunctionsLight.csproj + language: dotnet + host: function + + api: + project: ../Api/Cloud5mins.ShortenerTools.Api.csproj + language: dotnet + host: appservice + + admin: + project: ../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj + language: dotnet + host: appservice \ No newline at end of file diff --git a/src/budget-deployment/infra/main.bicep b/src/budget-deployment/infra/main.bicep new file mode 100644 index 00000000..0b8b0cee --- /dev/null +++ b/src/budget-deployment/infra/main.bicep @@ -0,0 +1,88 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') +param environmentName string + +@minLength(1) +@description('The location used for all deployed resources') +param location string + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +param CustomDomain string +param DefaultRedirectUrl string + +var tags = { + 'azd-env-name': environmentName +} + +resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { + name: 'rg-${environmentName}' + location: location + tags: tags +} + +module resources './resources.bicep' = { + scope: rg + name: 'resources' + params: { + location: location + tags: tags + principalId: principalId + CustomDomain: CustomDomain + DefaultRedirectUrl: DefaultRedirectUrl + } +} + +module funcstoragea17ca '../../infra/funcstoragea17ca/funcstoragea17ca.module.bicep' = { + name: 'funcstoragea17ca' + scope: rg + params: { + location: location + } +} + +module funcstoragea17ca_roles '../../infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep' = { + name: 'funcstoragea17ca-roles' + scope: rg + params: { + funcstoragea17ca_outputs_name: funcstoragea17ca.outputs.name + location: location + principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID + principalType: 'ServicePrincipal' + } +} + +module url_data '../../infra/url-data/url-data.module.bicep' = { + name: 'url-data' + scope: rg + params: { + location: location + } +} + +module url_data_roles '../../infra/url-data-roles/url-data-roles.module.bicep' = { + name: 'url-data-roles' + scope: rg + params: { + location: location + principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID + principalType: 'ServicePrincipal' + url_data_outputs_name: url_data.outputs.name + } +} + +output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID +output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME +output AZURE_APP_SERVICE_PLAN_NAME string = resources.outputs.AZURE_APP_SERVICE_PLAN_NAME +output AZURE_FUNCTION_APP_NAME string = resources.outputs.AZURE_FUNCTION_APP_NAME +output AZURE_API_APP_NAME string = resources.outputs.AZURE_API_APP_NAME +output AZURE_ADMIN_APP_NAME string = resources.outputs.AZURE_ADMIN_APP_NAME +output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint +output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint +output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint +output FUNCSTORAGEA17CA_TABLEENDPOINT string = funcstoragea17ca.outputs.tableEndpoint +output URL_DATA_TABLEENDPOINT string = url_data.outputs.tableEndpoint diff --git a/src/budget-deployment/infra/main.parameters.json b/src/budget-deployment/infra/main.parameters.json new file mode 100644 index 00000000..396f07bc --- /dev/null +++ b/src/budget-deployment/infra/main.parameters.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "CustomDomain": { + "value": "${AZURE_CUSTOM_DOMAIN}" + }, + "DefaultRedirectUrl": { + "value": "${AZURE_DEFAULT_REDIRECT_URL}" + }, + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + } + } +} diff --git a/src/budget-deployment/infra/resources.bicep b/src/budget-deployment/infra/resources.bicep new file mode 100644 index 00000000..7dd502be --- /dev/null +++ b/src/budget-deployment/infra/resources.bicep @@ -0,0 +1,203 @@ +@description('The location used for all deployed resources') +param location string = resourceGroup().location + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +@description('Tags that will be applied to all resources') +param tags object = {} + +@description('Custom domain passed to API service') +param CustomDomain string + +@description('Default redirect URL passed to API and function services') +param DefaultRedirectUrl string + +var resourceToken = uniqueString(resourceGroup().id) + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: 'mi-${resourceToken}' + location: location + tags: tags +} + +resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { + name: 'asp-${resourceToken}' + location: location + sku: { + name: 'B1' + tier: 'Basic' + size: 'B1' + family: 'B' + capacity: 1 + } + kind: 'app' + properties: { + reserved: false + } + tags: union(tags, { + 'aspire-resource-name': 'budget-appservice-plan' + }) +} + +resource functionConsumptionPlan 'Microsoft.Web/serverfarms@2023-12-01' = { + name: 'asp-func-${resourceToken}' + location: location + sku: { + name: 'Y1' + tier: 'Dynamic' + size: 'Y1' + family: 'Y' + capacity: 0 + } + kind: 'functionapp' + properties: { + reserved: false + } + tags: union(tags, { + 'aspire-resource-name': 'budget-functions-plan' + }) +} + +resource functionSite 'Microsoft.Web/sites@2023-12-01' = { + name: 'func-${resourceToken}' + location: location + kind: 'functionapp' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + serverFarmId: functionConsumptionPlan.id + httpsOnly: true + siteConfig: { + ftpsState: 'Disabled' + minTlsVersion: '1.2' + appSettings: [ + { + name: 'FUNCTIONS_EXTENSION_VERSION' + value: '~4' + } + { + name: 'FUNCTIONS_WORKER_RUNTIME' + value: 'dotnet-isolated' + } + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } + { + name: 'DefaultRedirectUrl' + value: DefaultRedirectUrl + } + ] + } + } + tags: union(tags, { + 'azd-service-name': 'azfunc-light' + 'aspire-resource-name': 'azfunc-light' + }) +} + +resource apiSite 'Microsoft.Web/sites@2023-12-01' = { + name: 'api-${resourceToken}' + location: location + kind: 'app' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + serverFarmId: appServicePlan.id + httpsOnly: true + siteConfig: { + ftpsState: 'Disabled' + minTlsVersion: '1.2' + appSettings: [ + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } + { + name: 'CustomDomain' + value: CustomDomain + } + { + name: 'DefaultRedirectUrl' + value: DefaultRedirectUrl + } + ] + } + } + tags: union(tags, { + 'azd-service-name': 'api' + 'aspire-resource-name': 'api' + }) +} + +resource adminSite 'Microsoft.Web/sites@2023-12-01' = { + name: 'admin-${resourceToken}' + location: location + kind: 'app' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + serverFarmId: appServicePlan.id + httpsOnly: true + siteConfig: { + ftpsState: 'Disabled' + minTlsVersion: '1.2' + appSettings: [ + { + name: 'AZURE_CLIENT_ID' + value: managedIdentity.properties.clientId + } + { + name: 'API_HTTP' + value: 'https://${apiSite.properties.defaultHostName}' + } + { + name: 'API_HTTPS' + value: 'https://${apiSite.properties.defaultHostName}' + } + { + name: 'services__api__http__0' + value: 'https://${apiSite.properties.defaultHostName}' + } + { + name: 'services__api__https__0' + value: 'https://${apiSite.properties.defaultHostName}' + } + ] + } + } + tags: union(tags, { + 'azd-service-name': 'admin' + 'aspire-resource-name': 'admin' + }) +} + +resource principalRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) { + name: guid(resourceGroup().id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')) + scope: resourceGroup() + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') + } +} + +output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId +output MANAGED_IDENTITY_NAME string = managedIdentity.name +output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId +output AZURE_APP_SERVICE_PLAN_NAME string = appServicePlan.name +output AZURE_FUNCTION_APP_NAME string = functionSite.name +output AZURE_API_APP_NAME string = apiSite.name +output AZURE_ADMIN_APP_NAME string = adminSite.name diff --git a/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep b/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep new file mode 100644 index 00000000..a3d4d0de --- /dev/null +++ b/src/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep @@ -0,0 +1,52 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param funcstoragea17ca_outputs_name string + +param principalType string + +param principalId string + +resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: funcstoragea17ca_outputs_name +} + +resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') + principalType: principalType + } + scope: funcstoragea17ca +} \ No newline at end of file diff --git a/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep new file mode 100644 index 00000000..335395d2 --- /dev/null +++ b/src/infra/funcstoragea17ca/funcstoragea17ca.module.bicep @@ -0,0 +1,35 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('funcstoragea17ca${uniqueString(resourceGroup().id)}', 24) + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_GRS' + } + properties: { + accessTier: 'Hot' + allowSharedKeyAccess: false + isHnsEnabled: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + defaultAction: 'Allow' + } + } + tags: { + 'aspire-resource-name': 'funcstoragea17ca' + } +} + +output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob + +output dataLakeEndpoint string = funcstoragea17ca.properties.primaryEndpoints.dfs + +output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue + +output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table + +output name string = funcstoragea17ca.name + +output id string = funcstoragea17ca.id \ No newline at end of file diff --git a/src/infra/main.bicep b/src/infra/main.bicep new file mode 100644 index 00000000..25faaf92 --- /dev/null +++ b/src/infra/main.bicep @@ -0,0 +1,85 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-') +param environmentName string + +@minLength(1) +@description('The location used for all deployed resources') +param location string + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +param CustomDomain string +param DefaultRedirectUrl string + +var tags = { + 'azd-env-name': environmentName +} + +resource rg 'Microsoft.Resources/resourceGroups@2022-09-01' = { + name: 'rg-${environmentName}' + location: location + tags: tags +} +module resources 'resources.bicep' = { + scope: rg + name: 'resources' + params: { + location: location + tags: tags + principalId: principalId + } +} + +module funcstoragea17ca 'funcstoragea17ca/funcstoragea17ca.module.bicep' = { + name: 'funcstoragea17ca' + scope: rg + params: { + location: location + } +} +module funcstoragea17ca_roles 'funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep' = { + name: 'funcstoragea17ca-roles' + scope: rg + params: { + funcstoragea17ca_outputs_name: funcstoragea17ca.outputs.name + location: location + principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID + principalType: 'ServicePrincipal' + } +} +module url_data 'url-data/url-data.module.bicep' = { + name: 'url-data' + scope: rg + params: { + location: location + } +} +module url_data_roles 'url-data-roles/url-data-roles.module.bicep' = { + name: 'url-data-roles' + scope: rg + params: { + location: location + principalId: resources.outputs.MANAGED_IDENTITY_PRINCIPAL_ID + principalType: 'ServicePrincipal' + url_data_outputs_name: url_data.outputs.name + } +} + +output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CLIENT_ID +output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME +output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = resources.outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = resources.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID +output AZURE_CONTAINER_REGISTRY_NAME string = resources.outputs.AZURE_CONTAINER_REGISTRY_NAME +output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME +output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID +output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = resources.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN +output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint +output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint +output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint +output FUNCSTORAGEA17CA_TABLEENDPOINT string = funcstoragea17ca.outputs.tableEndpoint +output URL_DATA_TABLEENDPOINT string = url_data.outputs.tableEndpoint diff --git a/src/infra/main.json b/src/infra/main.json new file mode 100644 index 00000000..91a34417 --- /dev/null +++ b/src/infra/main.json @@ -0,0 +1,685 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "11546803755779573508" + } + }, + "parameters": { + "environmentName": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "metadata": { + "description": "Name of the environment that can be used as part of naming resource convention, the name of the resource group for your application will use this name, prefixed with rg-" + } + }, + "location": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "The location used for all deployed resources" + } + }, + "principalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Id of the user or app to assign application roles" + } + }, + "CustomDomain": { + "type": "string" + }, + "DefaultRedirectUrl": { + "type": "string" + } + }, + "variables": { + "tags": { + "azd-env-name": "[parameters('environmentName')]" + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2022-09-01", + "name": "[format('rg-{0}', parameters('environmentName'))]", + "location": "[parameters('location')]", + "tags": "[variables('tags')]" + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "resources", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + }, + "principalId": { + "value": "[parameters('principalId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "17949110531045892075" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location used for all deployed resources" + } + }, + "principalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Id of the user or app to assign application roles" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags that will be applied to all resources" + } + } + }, + "variables": { + "resourceToken": "[uniqueString(resourceGroup().id)]" + }, + "resources": [ + { + "type": "Microsoft.App/managedEnvironments/dotNetComponents", + "apiVersion": "2024-02-02-preview", + "name": "[format('{0}/{1}', format('cae-{0}', variables('resourceToken')), 'aspire-dashboard')]", + "properties": { + "componentType": "AspireDashboard" + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken')))]" + ] + }, + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[format('mi-{0}', variables('resourceToken'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-07-01", + "name": "[replace(format('acr-{0}', variables('resourceToken')), '-', '')]", + "location": "[parameters('location')]", + "sku": { + "name": "Basic" + }, + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', ''))]", + "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', '')), resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d'))]", + "properties": { + "principalId": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').principalId]", + "principalType": "ServicePrincipal", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', ''))]", + "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken')))]" + ] + }, + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[format('law-{0}', variables('resourceToken'))]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + } + }, + "tags": "[parameters('tags')]" + }, + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2024-02-02-preview", + "name": "[format('cae-{0}', variables('resourceToken'))]", + "location": "[parameters('location')]", + "properties": { + "workloadProfiles": [ + { + "workloadProfileType": "Consumption", + "name": "consumption" + } + ], + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken'))), '2022-10-01').customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken'))), '2022-10-01').primarySharedKey]" + } + } + }, + "tags": "[parameters('tags')]", + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken')))]" + ] + } + ], + "outputs": { + "MANAGED_IDENTITY_CLIENT_ID": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').clientId]" + }, + "MANAGED_IDENTITY_NAME": { + "type": "string", + "value": "[format('mi-{0}', variables('resourceToken'))]" + }, + "MANAGED_IDENTITY_PRINCIPAL_ID": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken'))), '2023-01-31').principalId]" + }, + "AZURE_LOG_ANALYTICS_WORKSPACE_NAME": { + "type": "string", + "value": "[format('law-{0}', variables('resourceToken'))]" + }, + "AZURE_LOG_ANALYTICS_WORKSPACE_ID": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', format('law-{0}', variables('resourceToken')))]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', replace(format('acr-{0}', variables('resourceToken')), '-', '')), '2023-07-01').loginServer]" + }, + "AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format('mi-{0}', variables('resourceToken')))]" + }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "value": "[replace(format('acr-{0}', variables('resourceToken')), '-', '')]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_NAME": { + "type": "string", + "value": "[format('cae-{0}', variables('resourceToken'))]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_ID": { + "type": "string", + "value": "[resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken')))]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN": { + "type": "string", + "value": "[reference(resourceId('Microsoft.App/managedEnvironments', format('cae-{0}', variables('resourceToken'))), '2024-02-02-preview').defaultDomain]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "funcstoragea17ca", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "5132421617093321654" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resource(s) to be deployed." + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)]", + "kind": "StorageV2", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_GRS" + }, + "properties": { + "accessTier": "Hot", + "allowSharedKeyAccess": false, + "isHnsEnabled": false, + "minimumTlsVersion": "TLS1_2", + "networkAcls": { + "defaultAction": "Allow" + } + }, + "tags": { + "aspire-resource-name": "funcstoragea17ca" + } + } + ], + "outputs": { + "blobEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.blob]" + }, + "dataLakeEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.dfs]" + }, + "queueEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.queue]" + }, + "tableEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.table]" + }, + "name": { + "type": "string", + "value": "[take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24)]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', take(format('funcstoragea17ca{0}', uniqueString(resourceGroup().id)), 24))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "funcstoragea17ca-roles", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "funcstoragea17ca_outputs_name": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.name.value]" + }, + "location": { + "value": "[parameters('location')]" + }, + "principalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_PRINCIPAL_ID.value]" + }, + "principalType": { + "value": "ServicePrincipal" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "6014125912618429667" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resource(s) to be deployed." + } + }, + "funcstoragea17ca_outputs_name": { + "type": "string" + }, + "principalType": { + "type": "string" + }, + "principalId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('funcstoragea17ca_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "principalType": "[parameters('principalType')]" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca')]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "url-data", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "10282375894748324618" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resource(s) to be deployed." + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)]", + "kind": "StorageV2", + "location": "[parameters('location')]", + "sku": { + "name": "Standard_GRS" + }, + "properties": { + "accessTier": "Hot", + "allowSharedKeyAccess": false, + "isHnsEnabled": false, + "minimumTlsVersion": "TLS1_2", + "networkAcls": { + "defaultAction": "Allow" + } + }, + "tags": { + "aspire-resource-name": "url-data" + } + } + ], + "outputs": { + "blobEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.blob]" + }, + "dataLakeEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.dfs]" + }, + "queueEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.queue]" + }, + "tableEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)), '2024-01-01').primaryEndpoints.table]" + }, + "name": { + "type": "string", + "value": "[take(format('urldata{0}', uniqueString(resourceGroup().id)), 24)]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', take(format('urldata{0}', uniqueString(resourceGroup().id)), 24))]" + } + } + } + }, + "dependsOn": [ + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "url-data-roles", + "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "principalId": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_PRINCIPAL_ID.value]" + }, + "principalType": { + "value": "ServicePrincipal" + }, + "url_data_outputs_name": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data'), '2025-04-01').outputs.name.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.45.15.27210", + "templateHash": "5468789998190738231" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "The location for the resource(s) to be deployed." + } + }, + "url_data_outputs_name": { + "type": "string" + }, + "principalType": { + "type": "string" + }, + "principalId": { + "type": "string" + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "principalType": "[parameters('principalType')]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('url_data_outputs_name')), parameters('principalId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "principalType": "[parameters('principalType')]" + } + } + ] + } + }, + "dependsOn": [ + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources')]", + "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]", + "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data')]" + ] + } + ], + "outputs": { + "MANAGED_IDENTITY_CLIENT_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_CLIENT_ID.value]" + }, + "MANAGED_IDENTITY_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.MANAGED_IDENTITY_NAME.value]" + }, + "AZURE_LOG_ANALYTICS_WORKSPACE_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_LOG_ANALYTICS_WORKSPACE_NAME.value]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT.value]" + }, + "AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID.value]" + }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_REGISTRY_NAME.value]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_NAME": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_NAME.value]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_ID": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID.value]" + }, + "AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'resources'), '2025-04-01').outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN.value]" + }, + "FUNCSTORAGEA17CA_BLOBENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.blobEndpoint.value]" + }, + "FUNCSTORAGEA17CA_DATALAKEENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.dataLakeEndpoint.value]" + }, + "FUNCSTORAGEA17CA_QUEUEENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.queueEndpoint.value]" + }, + "FUNCSTORAGEA17CA_TABLEENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'funcstoragea17ca'), '2025-04-01').outputs.tableEndpoint.value]" + }, + "URL_DATA_TABLEENDPOINT": { + "type": "string", + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'url-data'), '2025-04-01').outputs.tableEndpoint.value]" + } + } +} \ No newline at end of file diff --git a/src/infra/main.parameters.json b/src/infra/main.parameters.json new file mode 100644 index 00000000..b89be02d --- /dev/null +++ b/src/infra/main.parameters.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "CustomDomain": { + "value": "${AZURE_CUSTOM_DOMAIN}" + }, + "DefaultRedirectUrl": { + "value": "${AZURE_DEFAULT_REDIRECT_URL}" + }, + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + } + } + } + \ No newline at end of file diff --git a/src/infra/resources.bicep b/src/infra/resources.bicep new file mode 100644 index 00000000..61533054 --- /dev/null +++ b/src/infra/resources.bicep @@ -0,0 +1,85 @@ +@description('The location used for all deployed resources') +param location string = resourceGroup().location +@description('Id of the user or app to assign application roles') +param principalId string = '' + + +@description('Tags that will be applied to all resources') +param tags object = {} + +var resourceToken = uniqueString(resourceGroup().id) + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: 'mi-${resourceToken}' + location: location + tags: tags +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: replace('acr-${resourceToken}', '-', '') + location: location + sku: { + name: 'Basic' + } + tags: tags +} + +resource caeMiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(containerRegistry.id, managedIdentity.id, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')) + scope: containerRegistry + properties: { + principalId: managedIdentity.properties.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } +} + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: 'law-${resourceToken}' + location: location + properties: { + sku: { + name: 'PerGB2018' + } + } + tags: tags +} + +resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-preview' = { + name: 'cae-${resourceToken}' + location: location + properties: { + workloadProfiles: [{ + workloadProfileType: 'Consumption' + name: 'consumption' + }] + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspace.properties.customerId + sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey + } + } + } + tags: tags + + resource aspireDashboard 'dotNetComponents' = { + name: 'aspire-dashboard' + properties: { + componentType: 'AspireDashboard' + } + } + +} + +output MANAGED_IDENTITY_CLIENT_ID string = managedIdentity.properties.clientId +output MANAGED_IDENTITY_NAME string = managedIdentity.name +output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId +output AZURE_LOG_ANALYTICS_WORKSPACE_NAME string = logAnalyticsWorkspace.name +output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = logAnalyticsWorkspace.id +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer +output AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID string = managedIdentity.id +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.name +output AZURE_CONTAINER_APPS_ENVIRONMENT_NAME string = containerAppEnvironment.name +output AZURE_CONTAINER_APPS_ENVIRONMENT_ID string = containerAppEnvironment.id +output AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN string = containerAppEnvironment.properties.defaultDomain diff --git a/src/infra/url-data-roles/url-data-roles.module.bicep b/src/infra/url-data-roles/url-data-roles.module.bicep new file mode 100644 index 00000000..e029cdc0 --- /dev/null +++ b/src/infra/url-data-roles/url-data-roles.module.bicep @@ -0,0 +1,42 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param url_data_outputs_name string + +param principalType string + +param principalId string + +resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: url_data_outputs_name +} + +resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalType: principalType + } + scope: url_data +} + +resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalType: principalType + } + scope: url_data +} + +resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalType: principalType + } + scope: url_data +} \ No newline at end of file diff --git a/src/infra/url-data/url-data.module.bicep b/src/infra/url-data/url-data.module.bicep new file mode 100644 index 00000000..6da8ab17 --- /dev/null +++ b/src/infra/url-data/url-data.module.bicep @@ -0,0 +1,35 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('urldata${uniqueString(resourceGroup().id)}', 24) + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_GRS' + } + properties: { + accessTier: 'Hot' + allowSharedKeyAccess: false + isHnsEnabled: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + defaultAction: 'Allow' + } + } + tags: { + 'aspire-resource-name': 'url-data' + } +} + +output blobEndpoint string = url_data.properties.primaryEndpoints.blob + +output dataLakeEndpoint string = url_data.properties.primaryEndpoints.dfs + +output queueEndpoint string = url_data.properties.primaryEndpoints.queue + +output tableEndpoint string = url_data.properties.primaryEndpoints.table + +output name string = url_data.name + +output id string = url_data.id \ No newline at end of file diff --git a/src/next-steps.md b/src/next-steps.md new file mode 100644 index 00000000..21e65176 --- /dev/null +++ b/src/next-steps.md @@ -0,0 +1,73 @@ +# Next Steps after `azd init` + +## Table of Contents + +1. [Next Steps](#next-steps) +2. [What was added](#what-was-added) +3. [Billing](#billing) +4. [Troubleshooting](#troubleshooting) + +## Next Steps + +### Provision infrastructure and deploy application code + +Run `azd up` to provision your infrastructure and deploy to Azure in one step (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! + +To troubleshoot any issues, see [troubleshooting](#troubleshooting). + +### Configure CI/CD pipeline + +Run `azd pipeline config -e ` to configure the deployment pipeline to connect securely to Azure. An environment name is specified here to configure the pipeline with a different environment for isolation purposes. Run `azd env list` and `azd env set` to reselect the default environment after this step. + +- Deploying with `GitHub Actions`: Select `GitHub` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration. + +- Deploying with `Azure DevOps Pipeline`: Select `Azure DevOps` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration. + +## What was added + +### Infrastructure configuration + +To describe the infrastructure and application, an `azure.yaml` was added with the following directory structure: + +```yaml +- azure.yaml # azd project configuration +``` + +This file contains a single service, which references your project's App Host. When needed, `azd` generates the required infrastructure as code in memory and uses it. + +If you would like to see or modify the infrastructure that `azd` uses, run `azd infra gen` to generate it to disk. + +If you do this, some additional directories will be created: + +```yaml +- infra/ # Infrastructure as Code (bicep) files + - main.bicep # main deployment module + - resources.bicep # resources shared across your application's services +``` + +In addition, for each project resource referenced by your app host, a `containerApp.tmpl.yaml` file will be created in a directory named `manifests` next the project file. This file contains the infrastructure as code for running the project on Azure Container Apps. + +*Note*: Once you have generated your infrastructure to disk, those files are the source of truth for azd. Any changes made to `azure.yaml` or your App Host will not be reflected in the infrastructure until you regenerate it with `azd infra gen` again. It will prompt you before overwriting files. You can pass `--force` to force `azd infra gen` to overwrite the files without prompting. + +## Billing + +Visit the *Cost Management + Billing* page in Azure Portal to track current spend. For more information about how you're billed, and how you can monitor the costs incurred in your Azure subscriptions, visit [billing overview](https://learn.microsoft.com/azure/developer/intro/azure-developer-billing). + +## Troubleshooting + +Q: I visited the service endpoint listed, and I'm seeing a blank page, a generic welcome page, or an error page. + +A: Your service may have failed to start, or it may be missing some configuration settings. To investigate further: + +1. Run `azd show`. Click on the link under "View in Azure Portal" to open the resource group in Azure Portal. +2. Navigate to the specific Container App service that is failing to deploy. +3. Click on the failing revision under "Revisions with Issues". +4. Review "Status details" for more information about the type of failure. +5. Observe the log outputs from Console log stream and System log stream to identify any errors. +6. If logs are written to disk, use *Console* in the navigation to connect to a shell within the running container. + +For more troubleshooting information, visit [Container Apps troubleshooting](https://learn.microsoft.com/azure/container-apps/troubleshooting). + +### Additional information + +For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert). From f4df21ed44f93696c674c6e1ac560d8539fe60f1 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 8 Aug 2026 17:37:38 -0400 Subject: [PATCH 10/17] feat: Add storage accounts with identity-based access for Function App and API Deploy dedicated storage for the Function App's `AzureWebJobsStorage` and a separate URL data store. Enable secure, identity-based access for both Function and API apps by configuring managed identity and service endpoints, thereby replacing connection strings. --- .../funcstoragea17ca-roles.module.bicep | 52 +++++++++++++++++++ .../funcstoragea17ca.module.bicep | 35 +++++++++++++ src/budget-deployment/infra/main.bicep | 12 +++-- src/budget-deployment/infra/resources.bicep | 43 +++++++++++++++ .../url-data-roles.module.bicep | 42 +++++++++++++++ .../infra/url-data/url-data.module.bicep | 35 +++++++++++++ 6 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep create mode 100644 src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep create mode 100644 src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep create mode 100644 src/budget-deployment/infra/url-data/url-data.module.bicep diff --git a/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep b/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep new file mode 100644 index 00000000..a3d4d0de --- /dev/null +++ b/src/budget-deployment/infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep @@ -0,0 +1,52 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param funcstoragea17ca_outputs_name string + +param principalType string + +param principalId string + +resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: funcstoragea17ca_outputs_name +} + +resource funcstoragea17ca_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalType: principalType + } + scope: funcstoragea17ca +} + +resource funcstoragea17ca_StorageAccountContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(funcstoragea17ca.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab') + principalType: principalType + } + scope: funcstoragea17ca +} \ No newline at end of file diff --git a/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep b/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep new file mode 100644 index 00000000..335395d2 --- /dev/null +++ b/src/budget-deployment/infra/funcstoragea17ca/funcstoragea17ca.module.bicep @@ -0,0 +1,35 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource funcstoragea17ca 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('funcstoragea17ca${uniqueString(resourceGroup().id)}', 24) + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_GRS' + } + properties: { + accessTier: 'Hot' + allowSharedKeyAccess: false + isHnsEnabled: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + defaultAction: 'Allow' + } + } + tags: { + 'aspire-resource-name': 'funcstoragea17ca' + } +} + +output blobEndpoint string = funcstoragea17ca.properties.primaryEndpoints.blob + +output dataLakeEndpoint string = funcstoragea17ca.properties.primaryEndpoints.dfs + +output queueEndpoint string = funcstoragea17ca.properties.primaryEndpoints.queue + +output tableEndpoint string = funcstoragea17ca.properties.primaryEndpoints.table + +output name string = funcstoragea17ca.name + +output id string = funcstoragea17ca.id \ No newline at end of file diff --git a/src/budget-deployment/infra/main.bicep b/src/budget-deployment/infra/main.bicep index 0b8b0cee..08e9d3a3 100644 --- a/src/budget-deployment/infra/main.bicep +++ b/src/budget-deployment/infra/main.bicep @@ -34,10 +34,14 @@ module resources './resources.bicep' = { principalId: principalId CustomDomain: CustomDomain DefaultRedirectUrl: DefaultRedirectUrl + urlDataTableEndpoint: url_data.outputs.tableEndpoint + funcStorageBlobEndpoint: funcstoragea17ca.outputs.blobEndpoint + funcStorageQueueEndpoint: funcstoragea17ca.outputs.queueEndpoint + funcStorageTableEndpoint: funcstoragea17ca.outputs.tableEndpoint } } -module funcstoragea17ca '../../infra/funcstoragea17ca/funcstoragea17ca.module.bicep' = { +module funcstoragea17ca './funcstoragea17ca/funcstoragea17ca.module.bicep' = { name: 'funcstoragea17ca' scope: rg params: { @@ -45,7 +49,7 @@ module funcstoragea17ca '../../infra/funcstoragea17ca/funcstoragea17ca.module.bi } } -module funcstoragea17ca_roles '../../infra/funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep' = { +module funcstoragea17ca_roles './funcstoragea17ca-roles/funcstoragea17ca-roles.module.bicep' = { name: 'funcstoragea17ca-roles' scope: rg params: { @@ -56,7 +60,7 @@ module funcstoragea17ca_roles '../../infra/funcstoragea17ca-roles/funcstoragea17 } } -module url_data '../../infra/url-data/url-data.module.bicep' = { +module url_data './url-data/url-data.module.bicep' = { name: 'url-data' scope: rg params: { @@ -64,7 +68,7 @@ module url_data '../../infra/url-data/url-data.module.bicep' = { } } -module url_data_roles '../../infra/url-data-roles/url-data-roles.module.bicep' = { +module url_data_roles './url-data-roles/url-data-roles.module.bicep' = { name: 'url-data-roles' scope: rg params: { diff --git a/src/budget-deployment/infra/resources.bicep b/src/budget-deployment/infra/resources.bicep index 7dd502be..d29216b5 100644 --- a/src/budget-deployment/infra/resources.bicep +++ b/src/budget-deployment/infra/resources.bicep @@ -13,6 +13,18 @@ param CustomDomain string @description('Default redirect URL passed to API and function services') param DefaultRedirectUrl string +@description('Table service endpoint of the url-data storage account (for URL shortener data)') +param urlDataTableEndpoint string + +@description('Blob service endpoint of the func storage account (AzureWebJobsStorage)') +param funcStorageBlobEndpoint string + +@description('Queue service endpoint of the func storage account (AzureWebJobsStorage)') +param funcStorageQueueEndpoint string + +@description('Table service endpoint of the func storage account (AzureWebJobsStorage)') +param funcStorageTableEndpoint string + var resourceToken = uniqueString(resourceGroup().id) resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { @@ -92,6 +104,32 @@ resource functionSite 'Microsoft.Web/sites@2023-12-01' = { name: 'DefaultRedirectUrl' value: DefaultRedirectUrl } + // AzureWebJobsStorage uses identity-based access (no connection string) + { + name: 'AzureWebJobsStorage__blobServiceUri' + value: funcStorageBlobEndpoint + } + { + name: 'AzureWebJobsStorage__queueServiceUri' + value: funcStorageQueueEndpoint + } + { + name: 'AzureWebJobsStorage__tableServiceUri' + value: funcStorageTableEndpoint + } + { + name: 'AzureWebJobsStorage__credential' + value: 'managedidentity' + } + { + name: 'AzureWebJobsStorage__clientId' + value: managedIdentity.properties.clientId + } + // strTables connection used by AddAzureTableClient("strTables") + { + name: 'ConnectionStrings__strTables' + value: urlDataTableEndpoint + } ] } } @@ -130,6 +168,11 @@ resource apiSite 'Microsoft.Web/sites@2023-12-01' = { name: 'DefaultRedirectUrl' value: DefaultRedirectUrl } + // strTables connection used by AddAzureTableClient("strTables") + { + name: 'ConnectionStrings__strTables' + value: urlDataTableEndpoint + } ] } } diff --git a/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep b/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep new file mode 100644 index 00000000..e029cdc0 --- /dev/null +++ b/src/budget-deployment/infra/url-data-roles/url-data-roles.module.bicep @@ -0,0 +1,42 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param url_data_outputs_name string + +param principalType string + +param principalId string + +resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { + name: url_data_outputs_name +} + +resource url_data_StorageBlobDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') + principalType: principalType + } + scope: url_data +} + +resource url_data_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') + principalType: principalType + } + scope: url_data +} + +resource url_data_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(url_data.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')) + properties: { + principalId: principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88') + principalType: principalType + } + scope: url_data +} \ No newline at end of file diff --git a/src/budget-deployment/infra/url-data/url-data.module.bicep b/src/budget-deployment/infra/url-data/url-data.module.bicep new file mode 100644 index 00000000..6da8ab17 --- /dev/null +++ b/src/budget-deployment/infra/url-data/url-data.module.bicep @@ -0,0 +1,35 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource url_data 'Microsoft.Storage/storageAccounts@2024-01-01' = { + name: take('urldata${uniqueString(resourceGroup().id)}', 24) + kind: 'StorageV2' + location: location + sku: { + name: 'Standard_GRS' + } + properties: { + accessTier: 'Hot' + allowSharedKeyAccess: false + isHnsEnabled: false + minimumTlsVersion: 'TLS1_2' + networkAcls: { + defaultAction: 'Allow' + } + } + tags: { + 'aspire-resource-name': 'url-data' + } +} + +output blobEndpoint string = url_data.properties.primaryEndpoints.blob + +output dataLakeEndpoint string = url_data.properties.primaryEndpoints.dfs + +output queueEndpoint string = url_data.properties.primaryEndpoints.queue + +output tableEndpoint string = url_data.properties.primaryEndpoints.table + +output name string = url_data.name + +output id string = url_data.id \ No newline at end of file From cec471d90f61a67b05890e47e7a629a07f01c0d9 Mon Sep 17 00:00:00 2001 From: fboucher Date: Wed, 12 Aug 2026 17:46:29 -0400 Subject: [PATCH 11/17] draft --- prompt.txt | 11 ++ src/.dockerignore | 13 ++ src/Api/Dockerfile | 31 +++++ src/budget-deployment/azure.yaml | 20 ++- .../hooks/deploy-api-sidecar.sh | 57 ++++++++ src/budget-deployment/infra/resources.bicep | 123 +++++++++++------- 6 files changed, 200 insertions(+), 55 deletions(-) create mode 100644 prompt.txt create mode 100644 src/.dockerignore create mode 100644 src/Api/Dockerfile create mode 100755 src/budget-deployment/hooks/deploy-api-sidecar.sh diff --git a/prompt.txt b/prompt.txt new file mode 100644 index 00000000..f2354469 --- /dev/null +++ b/prompt.txt @@ -0,0 +1,11 @@ +In this folder `src/budget-deployment` I have the deployment IaC to deploy my entire solution and it works. But right now it deploys the web site admin: `../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj` + +with it's api: `../Api/Cloud5mins.ShortenerTools.Api.csproj` in two individual app service. + +It would be better if they were deploy in the same App Service using sidecar. + +I would like to have the "api" NOT reachable from the internet. Like with a docker compose setup when a backend container is only accessible by the frontend container. + +Here admin is the frontend and api is the backend + +I saw that post that explains a few things about sidecar deployments in Azure App Service https://azure.github.io/AppService/2025/03/06/Sidecars-Deep-Dive-Part1.html \ No newline at end of file diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 00000000..2c5d0f71 --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,13 @@ +# Exclude build output and other large/unnecessary directories from the +# Docker build context (used by both local 'docker build' and 'az acr build'). +**/bin/ +**/obj/ +**/.git/ +**/.azure/ +**/node_modules/ +# Exclude other deployment configs – only src/ content is needed +budget-deployment/ +AppHost/ +infra/ +tools/ +*.md diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile new file mode 100644 index 00000000..6dba6996 --- /dev/null +++ b/src/Api/Dockerfile @@ -0,0 +1,31 @@ +# Build stage – uses the full SDK image +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# Copy NuGet central version management before restore +COPY ["Directory.Packages.props", "./"] + +# Copy project files and restore (layer-cache friendly) +COPY ["Api/Cloud5mins.ShortenerTools.Api.csproj", "Api/"] +COPY ["Core/Cloud5mins.ShortenerTools.Core.csproj", "Core/"] +COPY ["ServiceDefaults/Cloud5mins.ShortenerTools.ServiceDefaults.csproj", "ServiceDefaults/"] +RUN dotnet restore "Api/Cloud5mins.ShortenerTools.Api.csproj" + +# Copy everything and publish +COPY . . +RUN dotnet publish "Api/Cloud5mins.ShortenerTools.Api.csproj" \ + -c Release \ + -o /app/publish \ + /p:UseAppHost=false + +# Runtime stage – lightweight ASP.NET image +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app + +# Listen on HTTP only; App Service routes external traffic to the admin container. +# UseHttpsRedirection is a no-op when no HTTPS port is configured. +ENV ASPNETCORE_HTTP_PORTS=8080 +EXPOSE 8080 + +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "Cloud5mins.ShortenerTools.Api.dll"] diff --git a/src/budget-deployment/azure.yaml b/src/budget-deployment/azure.yaml index bf1f546c..b7d4112d 100644 --- a/src/budget-deployment/azure.yaml +++ b/src/budget-deployment/azure.yaml @@ -11,12 +11,22 @@ services: language: dotnet host: function - api: - project: ../Api/Cloud5mins.ShortenerTools.Api.csproj - language: dotnet - host: appservice + # The API is no longer a standalone App Service. + # It runs as a private sidecar container on the admin App Service + # (only reachable via http://localhost:8080 from within the same site unit). + # The sidecar image is built and pushed by the postprovision hook below. admin: project: ../TinyBlazorAdmin/Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj language: dotnet - host: appservice \ No newline at end of file + host: appservice + +hooks: + # After infrastructure is provisioned, build the API container image with + # ACR Tasks (no local Docker daemon required) and restart the admin site so + # App Service pulls the fresh image for the sidecar container. + postprovision: + posix: + shell: sh + run: hooks/deploy-api-sidecar.sh + interactive: false \ No newline at end of file diff --git a/src/budget-deployment/hooks/deploy-api-sidecar.sh b/src/budget-deployment/hooks/deploy-api-sidecar.sh new file mode 100755 index 00000000..ccd107b1 --- /dev/null +++ b/src/budget-deployment/hooks/deploy-api-sidecar.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# deploy-api-sidecar.sh +# +# Builds the API container image using ACR Tasks (no local Docker daemon needed), +# pushes it to the project's ACR, then restarts the admin App Service so that +# App Service pulls the fresh image for the API sidecar container. +# +# Environment variables provided by azd: +# AZURE_CONTAINER_REGISTRY_NAME – ACR name (output from Bicep) +# AZURE_ADMIN_APP_NAME – Admin App Service name (output from Bicep) +# AZURE_RESOURCE_GROUP – Resource group name (set by azd) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# Docker build context = src/ (solution root, two levels up from this script) +SRC_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" + +if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then + echo "ERROR: AZURE_CONTAINER_REGISTRY_NAME is not set. Run 'azd provision' first." + exit 1 +fi + +if [ -z "$AZURE_ADMIN_APP_NAME" ]; then + echo "ERROR: AZURE_ADMIN_APP_NAME is not set. Run 'azd provision' first." + exit 1 +fi + +if [ -z "$AZURE_RESOURCE_GROUP" ]; then + echo "ERROR: AZURE_RESOURCE_GROUP is not set. Run 'azd provision' first." + exit 1 +fi + +echo "" +echo "==> Building and pushing API sidecar image to ACR: $AZURE_CONTAINER_REGISTRY_NAME" +echo " Build context: $SRC_DIR" +echo " Dockerfile: Api/Dockerfile" +echo "" + +# 'az acr build' runs the Docker build inside Azure (no local Docker required). +# The Dockerfile path is relative to the build context (SRC_DIR = src/). +az acr build \ + --registry "$AZURE_CONTAINER_REGISTRY_NAME" \ + --image "api:latest" \ + --file "Api/Dockerfile" \ + "$SRC_DIR" + +echo "" +echo "==> Restarting admin site '$AZURE_ADMIN_APP_NAME' to pull the new API sidecar image..." +az webapp restart \ + --name "$AZURE_ADMIN_APP_NAME" \ + --resource-group "$AZURE_RESOURCE_GROUP" + +echo "" +echo "==> API sidecar deployment complete." +echo " The API is accessible only via http://localhost:8080 from within the" +echo " site unit and is NOT reachable from the internet." diff --git a/src/budget-deployment/infra/resources.bicep b/src/budget-deployment/infra/resources.bicep index d29216b5..ed7b0cdf 100644 --- a/src/budget-deployment/infra/resources.bicep +++ b/src/budget-deployment/infra/resources.bicep @@ -43,15 +43,41 @@ resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { family: 'B' capacity: 1 } - kind: 'app' + // Linux is required for App Service sidecar containers + kind: 'linux' properties: { - reserved: false + reserved: true } tags: union(tags, { 'aspire-resource-name': 'budget-appservice-plan' }) } +// Container registry to host the API sidecar image +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' = { + name: 'acr${resourceToken}' + location: location + sku: { + name: 'Basic' + } + properties: { + adminUserEnabled: false + } + tags: tags +} + +// Allow the managed identity to pull images from ACR +resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + // AcrPull built-in role: 7f951dda-4ed3-4680-a7ca-43fe172d538d + name: guid(containerRegistry.id, managedIdentity.id, '7f951dda-4ed3-4680-a7ca-43fe172d538d') + scope: containerRegistry + properties: { + principalId: managedIdentity.properties.principalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + principalType: 'ServicePrincipal' + } +} + resource functionConsumptionPlan 'Microsoft.Web/serverfarms@2023-12-01' = { name: 'asp-func-${resourceToken}' location: location @@ -139,10 +165,16 @@ resource functionSite 'Microsoft.Web/sites@2023-12-01' = { }) } -resource apiSite 'Microsoft.Web/sites@2023-12-01' = { - name: 'api-${resourceToken}' +// apiSite removed – the API is now deployed as a sidecar container on adminSite. +// See the apiSidecar sitecontainer resource below. + +// Admin site – Linux code-based app (main container receives all external traffic). +// The API runs as a sidecar container on the same site unit and is only +// reachable via localhost:8080 (never from the internet). +resource adminSite 'Microsoft.Web/sites@2023-12-01' = { + name: 'admin-${resourceToken}' location: location - kind: 'app' + kind: 'app,linux' identity: { type: 'UserAssigned' userAssignedIdentities: { @@ -156,10 +188,20 @@ resource apiSite 'Microsoft.Web/sites@2023-12-01' = { ftpsState: 'Disabled' minTlsVersion: '1.2' appSettings: [ + // ── Admin settings ────────────────────────────────────────────────── { name: 'AZURE_CLIENT_ID' value: managedIdentity.properties.clientId } + // Aspire service-discovery: route API calls to the sidecar on localhost. + // The HTTPS key is intentionally omitted so the client uses plain HTTP + // for localhost-to-localhost communication. + { + name: 'services__api__http__0' + value: 'http://localhost:8080' + } + // ── API settings (inherited by the sidecar via + // inheritAppSettingsAndConnectionStrings: true) ────────────────── { name: 'CustomDomain' value: CustomDomain @@ -177,55 +219,35 @@ resource apiSite 'Microsoft.Web/sites@2023-12-01' = { } } tags: union(tags, { - 'azd-service-name': 'api' - 'aspire-resource-name': 'api' + 'azd-service-name': 'admin' + 'aspire-resource-name': 'admin' }) } -resource adminSite 'Microsoft.Web/sites@2023-12-01' = { - name: 'admin-${resourceToken}' - location: location - kind: 'app' - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${managedIdentity.id}': {} - } - } +// API sidecar container – shares the same network namespace as adminSite. +// App Service only routes inbound internet traffic to the main (admin) container; +// this container is exclusively reachable via http://localhost:8080 from within +// the site unit. +resource apiSidecar 'Microsoft.Web/sites/sitecontainers@2024-04-01' = { + parent: adminSite + name: 'api' properties: { - serverFarmId: appServicePlan.id - httpsOnly: true - siteConfig: { - ftpsState: 'Disabled' - minTlsVersion: '1.2' - appSettings: [ - { - name: 'AZURE_CLIENT_ID' - value: managedIdentity.properties.clientId - } - { - name: 'API_HTTP' - value: 'https://${apiSite.properties.defaultHostName}' - } - { - name: 'API_HTTPS' - value: 'https://${apiSite.properties.defaultHostName}' - } - { - name: 'services__api__http__0' - value: 'https://${apiSite.properties.defaultHostName}' - } - { - name: 'services__api__https__0' - value: 'https://${apiSite.properties.defaultHostName}' - } - ] - } + // Image is pushed to ACR by the postprovision hook. + // 'latest' is refreshed by restarting the site after each push. + image: '${containerRegistry.properties.loginServer}/api:latest' + isMain: false + targetPort: '8080' + // Pull image using the site's user-assigned managed identity + authType: 'UserAssigned' + userManagedIdentityClientId: managedIdentity.properties.clientId + // All app settings on adminSite are inherited by the sidecar as env vars. + // This gives the API its AZURE_CLIENT_ID, CustomDomain, DefaultRedirectUrl, + // and ConnectionStrings__strTables without duplicating them. + inheritAppSettingsAndConnectionStrings: true } - tags: union(tags, { - 'azd-service-name': 'admin' - 'aspire-resource-name': 'admin' - }) + dependsOn: [ + acrPullRole + ] } resource principalRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(principalId)) { @@ -242,5 +264,6 @@ output MANAGED_IDENTITY_NAME string = managedIdentity.name output MANAGED_IDENTITY_PRINCIPAL_ID string = managedIdentity.properties.principalId output AZURE_APP_SERVICE_PLAN_NAME string = appServicePlan.name output AZURE_FUNCTION_APP_NAME string = functionSite.name -output AZURE_API_APP_NAME string = apiSite.name output AZURE_ADMIN_APP_NAME string = adminSite.name +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.name +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.properties.loginServer From 3b791dd0312e8105dd755416a8bbcb1213a1f7e5 Mon Sep 17 00:00:00 2001 From: Michael Morten Sonne <49366751+michaelmsonne@users.noreply.github.com> Date: Mon, 13 Jul 2026 08:58:44 +0200 Subject: [PATCH 12/17] style: Refactor layout structure for improved readability and consistency for the footer --- .../Components/Layout/MainLayout.razor | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor b/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor index 20a76e93..907a5cf2 100644 --- a/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor +++ b/src/TinyBlazorAdmin/Components/Layout/MainLayout.razor @@ -1,25 +1,28 @@ @inherits LayoutComponentBase - - Tiny Blazor Admin - - - - -
- @Body -
-
-
- - - - + + Tiny Blazor Admin + + + + +
+ @Body +
+
+
+ + + + Documentation and demos + + + + About Blazor + + +
@@ -27,13 +30,13 @@
- An unhandled error has occurred. - Reload - 🗙 + An unhandled error has occurred. + Reload + 🗙
@code { - public DesignThemeModes Mode { get; set; } - public OfficeColor? OfficeColor { get; set; } + public DesignThemeModes Mode { get; set; } + public OfficeColor? OfficeColor { get; set; } } \ No newline at end of file From b20a1af0b82a7018a393a9bc478e34bc64466716 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 14 Aug 2026 10:36:41 -0400 Subject: [PATCH 13/17] It's 100 pourcent working --- doc/how-deploy-budget-solution.md | 13 +++- src/Api/Dockerfile | 7 +- src/budget-deployment/azure.yaml | 2 +- .../hooks/deploy-api-sidecar.sh | 77 ++++++++++++++++--- src/budget-deployment/infra/main.bicep | 3 +- src/budget-deployment/infra/resources.bicep | 22 +++--- 6 files changed, 97 insertions(+), 27 deletions(-) diff --git a/doc/how-deploy-budget-solution.md b/doc/how-deploy-budget-solution.md index 9346428b..3e4f5bdf 100644 --- a/doc/how-deploy-budget-solution.md +++ b/doc/how-deploy-budget-solution.md @@ -2,4 +2,15 @@ cd src/budget-deployment -azd up \ No newline at end of file +azd up + + + + +az login +cd /home/frank/gh/AzUrlShortener/src/budget-deployment +azd deploy admin or azd deploy +if the API sidecar image is missing, run: +sh hooks/deploy-api-sidecar.sh +if needed, restart the app: +az webapp restart --name --resource-group \ No newline at end of file diff --git a/src/Api/Dockerfile b/src/Api/Dockerfile index 6dba6996..43ad5950 100644 --- a/src/Api/Dockerfile +++ b/src/Api/Dockerfile @@ -23,9 +23,10 @@ FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final WORKDIR /app # Listen on HTTP only; App Service routes external traffic to the admin container. -# UseHttpsRedirection is a no-op when no HTTPS port is configured. -ENV ASPNETCORE_HTTP_PORTS=8080 -EXPOSE 8080 +# Use a dedicated internal port so the main admin app (which listens on 8080) +# can coexist with the sidecar container in the same site unit. +ENV ASPNETCORE_HTTP_PORTS=8081 +EXPOSE 8081 COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "Cloud5mins.ShortenerTools.Api.dll"] diff --git a/src/budget-deployment/azure.yaml b/src/budget-deployment/azure.yaml index b7d4112d..ed36068a 100644 --- a/src/budget-deployment/azure.yaml +++ b/src/budget-deployment/azure.yaml @@ -13,7 +13,7 @@ services: # The API is no longer a standalone App Service. # It runs as a private sidecar container on the admin App Service - # (only reachable via http://localhost:8080 from within the same site unit). + # (only reachable via http://localhost:8081 from within the same site unit). # The sidecar image is built and pushed by the postprovision hook below. admin: diff --git a/src/budget-deployment/hooks/deploy-api-sidecar.sh b/src/budget-deployment/hooks/deploy-api-sidecar.sh index ccd107b1..e9873756 100755 --- a/src/budget-deployment/hooks/deploy-api-sidecar.sh +++ b/src/budget-deployment/hooks/deploy-api-sidecar.sh @@ -16,8 +16,49 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # Docker build context = src/ (solution root, two levels up from this script) SRC_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +# azd may not export these variables into the shell inside a hook, even when the +# environment file exists. Load the environment file explicitly from the project +# root's .azure directory, not from the hooks folder. +ENV_FILE="" +if [ -n "${AZURE_ENV_NAME:-}" ] && [ -f "$SCRIPT_DIR/../.azure/${AZURE_ENV_NAME}/.env" ]; then + ENV_FILE="$SCRIPT_DIR/../.azure/${AZURE_ENV_NAME}/.env" +fi + +if [ -z "$ENV_FILE" ]; then + for candidate in "$SCRIPT_DIR/../.azure"/*/.env; do + if [ -f "$candidate" ]; then + ENV_FILE="$candidate" + break + fi + done +fi + +if [ -n "$ENV_FILE" ]; then + set -a + . "$ENV_FILE" + set +a +fi + +# Fallback for azd hook shells that don't export env vars automatically. +if [ -z "$AZURE_RESOURCE_GROUP" ] && [ -n "$AZURE_ENV_NAME" ]; then + # This project names the resource group as rg- + AZURE_RESOURCE_GROUP="rg-${AZURE_ENV_NAME}" +fi + +if [ -z "$AZURE_RESOURCE_GROUP" ]; then + AZURE_RESOURCE_GROUP="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_RESOURCE_GROUP=/{print $2}' | tr -d '"' || true)" +fi + +if [ -z "$AZURE_ADMIN_APP_NAME" ]; then + AZURE_ADMIN_APP_NAME="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_ADMIN_APP_NAME=/{print $2}' | tr -d '"' || true)" +fi + if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then - echo "ERROR: AZURE_CONTAINER_REGISTRY_NAME is not set. Run 'azd provision' first." + AZURE_CONTAINER_REGISTRY_NAME="$(azd env get-values 2>/dev/null | awk -F= '/^AZURE_CONTAINER_REGISTRY_NAME=/{print $2}' | tr -d '"' || true)" +fi + +if [ -z "$AZURE_RESOURCE_GROUP" ]; then + echo "ERROR: AZURE_RESOURCE_GROUP is not set. Run 'azd provision' first." exit 1 fi @@ -26,24 +67,38 @@ if [ -z "$AZURE_ADMIN_APP_NAME" ]; then exit 1 fi -if [ -z "$AZURE_RESOURCE_GROUP" ]; then - echo "ERROR: AZURE_RESOURCE_GROUP is not set. Run 'azd provision' first." +if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then + AZURE_CONTAINER_REGISTRY_NAME="$(az acr list -g "$AZURE_RESOURCE_GROUP" --query "[0].name" -o tsv 2>/dev/null || true)" +fi + +if [ -z "$AZURE_CONTAINER_REGISTRY_NAME" ]; then + echo "ERROR: AZURE_CONTAINER_REGISTRY_NAME is not set and no registry was found in resource group '$AZURE_RESOURCE_GROUP'. Run 'azd provision' first." + exit 1 +fi + +DOCKERFILE_PATH="Api/Dockerfile" +if [ ! -f "$SRC_DIR/$DOCKERFILE_PATH" ]; then + echo "ERROR: Dockerfile not found at '$SRC_DIR/$DOCKERFILE_PATH'." exit 1 fi echo "" echo "==> Building and pushing API sidecar image to ACR: $AZURE_CONTAINER_REGISTRY_NAME" echo " Build context: $SRC_DIR" -echo " Dockerfile: Api/Dockerfile" +echo " Dockerfile: $DOCKERFILE_PATH" echo "" # 'az acr build' runs the Docker build inside Azure (no local Docker required). -# The Dockerfile path is relative to the build context (SRC_DIR = src/). -az acr build \ - --registry "$AZURE_CONTAINER_REGISTRY_NAME" \ - --image "api:latest" \ - --file "Api/Dockerfile" \ - "$SRC_DIR" +# Ensure the current working directory matches the build context so the relative +# Dockerfile path resolves consistently regardless of the hook's shell location. +( + cd "$SRC_DIR" + az acr build \ + --registry "$AZURE_CONTAINER_REGISTRY_NAME" \ + --image "api:latest" \ + --file "$DOCKERFILE_PATH" \ + . +) echo "" echo "==> Restarting admin site '$AZURE_ADMIN_APP_NAME' to pull the new API sidecar image..." @@ -53,5 +108,5 @@ az webapp restart \ echo "" echo "==> API sidecar deployment complete." -echo " The API is accessible only via http://localhost:8080 from within the" +echo " The API is accessible only via http://localhost:8081 from within the" echo " site unit and is NOT reachable from the internet." diff --git a/src/budget-deployment/infra/main.bicep b/src/budget-deployment/infra/main.bicep index 08e9d3a3..75d5f1d8 100644 --- a/src/budget-deployment/infra/main.bicep +++ b/src/budget-deployment/infra/main.bicep @@ -83,8 +83,9 @@ output MANAGED_IDENTITY_CLIENT_ID string = resources.outputs.MANAGED_IDENTITY_CL output MANAGED_IDENTITY_NAME string = resources.outputs.MANAGED_IDENTITY_NAME output AZURE_APP_SERVICE_PLAN_NAME string = resources.outputs.AZURE_APP_SERVICE_PLAN_NAME output AZURE_FUNCTION_APP_NAME string = resources.outputs.AZURE_FUNCTION_APP_NAME -output AZURE_API_APP_NAME string = resources.outputs.AZURE_API_APP_NAME output AZURE_ADMIN_APP_NAME string = resources.outputs.AZURE_ADMIN_APP_NAME +output AZURE_CONTAINER_REGISTRY_NAME string = resources.outputs.AZURE_CONTAINER_REGISTRY_NAME +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT output FUNCSTORAGEA17CA_BLOBENDPOINT string = funcstoragea17ca.outputs.blobEndpoint output FUNCSTORAGEA17CA_DATALAKEENDPOINT string = funcstoragea17ca.outputs.dataLakeEndpoint output FUNCSTORAGEA17CA_QUEUEENDPOINT string = funcstoragea17ca.outputs.queueEndpoint diff --git a/src/budget-deployment/infra/resources.bicep b/src/budget-deployment/infra/resources.bicep index ed7b0cdf..a9533435 100644 --- a/src/budget-deployment/infra/resources.bicep +++ b/src/budget-deployment/infra/resources.bicep @@ -185,6 +185,10 @@ resource adminSite 'Microsoft.Web/sites@2023-12-01' = { serverFarmId: appServicePlan.id httpsOnly: true siteConfig: { + // Linux App Service must be explicitly configured for the .NET runtime; + // otherwise it defaults to the generic PHP container and the admin app + // never receives the published Blazor app bundle. + linuxFxVersion: 'DOTNETCORE|10.0' ftpsState: 'Disabled' minTlsVersion: '1.2' appSettings: [ @@ -194,11 +198,11 @@ resource adminSite 'Microsoft.Web/sites@2023-12-01' = { value: managedIdentity.properties.clientId } // Aspire service-discovery: route API calls to the sidecar on localhost. - // The HTTPS key is intentionally omitted so the client uses plain HTTP - // for localhost-to-localhost communication. + // The main admin app listens on 8080, so the private sidecar needs its + // own internal port to avoid a bind conflict in the shared site unit. { name: 'services__api__http__0' - value: 'http://localhost:8080' + value: 'http://localhost:8081' } // ── API settings (inherited by the sidecar via // inheritAppSettingsAndConnectionStrings: true) ────────────────── @@ -226,7 +230,7 @@ resource adminSite 'Microsoft.Web/sites@2023-12-01' = { // API sidecar container – shares the same network namespace as adminSite. // App Service only routes inbound internet traffic to the main (admin) container; -// this container is exclusively reachable via http://localhost:8080 from within +// this container is exclusively reachable via http://localhost:8081 from within // the site unit. resource apiSidecar 'Microsoft.Web/sites/sitecontainers@2024-04-01' = { parent: adminSite @@ -236,14 +240,12 @@ resource apiSidecar 'Microsoft.Web/sites/sitecontainers@2024-04-01' = { // 'latest' is refreshed by restarting the site after each push. image: '${containerRegistry.properties.loginServer}/api:latest' isMain: false - targetPort: '8080' - // Pull image using the site's user-assigned managed identity + targetPort: '8081' + // Pull image using the site's user-assigned managed identity. + // App Service passes all app settings from the main site into the sidecar + // unless a specific override is defined. authType: 'UserAssigned' userManagedIdentityClientId: managedIdentity.properties.clientId - // All app settings on adminSite are inherited by the sidecar as env vars. - // This gives the API its AZURE_CLIENT_ID, CustomDomain, DefaultRedirectUrl, - // and ConnectionStrings__strTables without duplicating them. - inheritAppSettingsAndConnectionStrings: true } dependsOn: [ acrPullRole From cfea74f01af70c3aa81f94536ca71b712d48a079 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 14 Aug 2026 11:06:02 -0400 Subject: [PATCH 14/17] updading packages --- src/AzUrlShortener.sln | 42 ------------------------------------ src/Directory.Packages.props | 26 +++++++++++----------- 2 files changed, 13 insertions(+), 55 deletions(-) diff --git a/src/AzUrlShortener.sln b/src/AzUrlShortener.sln index b8d4e9eb..1e6500c2 100644 --- a/src/AzUrlShortener.sln +++ b/src/AzUrlShortener.sln @@ -15,20 +15,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.TinyBlazorAdmin", "TinyBlazorAdmin\Cloud5mins.ShortenerTools.TinyBlazorAdmin.csproj", "{88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppHost.Redirect", "AppHost.Redirect", "{48743B4B-BDEA-DC59-E766-996CB80EA03B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.AppHost.Redirect", "AppHost.Redirect\Cloud5mins.ShortenerTools.AppHost.Redirect.csproj", "{1C276950-388A-4CCF-998F-308534197198}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FunctionsLight", "FunctionsLight", "{978765B0-30B3-FFB1-F51A-527AE8C5D709}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppHost.Admin", "AppHost.Admin", "{5FE8D6DD-1E2C-845E-148A-9E14D73F3A3B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cloud5mins.ShortenerTools.AppHost.Admin", "AppHost.Admin\Cloud5mins.ShortenerTools.AppHost.Admin.csproj", "{FFDD6A77-8DCF-4137-9566-9E15EE0358ED}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{D82C56D6-B2CD-F611-9E31-04638F7EC30E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TinyBlazorAdmin", "TinyBlazorAdmin", "{E0AB1A1F-3E51-5469-E2E5-E04CC29A76AB}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,36 +97,8 @@ Global {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x64.Build.0 = Release|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x86.ActiveCfg = Release|Any CPU {88EAB6E9-CD20-4A6B-86A0-38CACA87FC7E}.Release|x86.Build.0 = Release|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Debug|x64.ActiveCfg = Debug|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Debug|x64.Build.0 = Debug|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Debug|x86.ActiveCfg = Debug|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Debug|x86.Build.0 = Debug|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Release|Any CPU.Build.0 = Release|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Release|x64.ActiveCfg = Release|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Release|x64.Build.0 = Release|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Release|x86.ActiveCfg = Release|Any CPU - {1C276950-388A-4CCF-998F-308534197198}.Release|x86.Build.0 = Release|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|x64.ActiveCfg = Debug|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|x64.Build.0 = Debug|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|x86.ActiveCfg = Debug|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Debug|x86.Build.0 = Debug|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|Any CPU.Build.0 = Release|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|x64.ActiveCfg = Release|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|x64.Build.0 = Release|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|x86.ActiveCfg = Release|Any CPU - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {1C276950-388A-4CCF-998F-308534197198} = {48743B4B-BDEA-DC59-E766-996CB80EA03B} - {FFDD6A77-8DCF-4137-9566-9E15EE0358ED} = {5FE8D6DD-1E2C-845E-148A-9E14D73F3A3B} - EndGlobalSection EndGlobal diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 6799cdac..42224a2b 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -2,31 +2,31 @@ - + - + - + - + - - + + - - - - - - - + + + + + + +
\ No newline at end of file From 4a0641adabacc8ff1ce8c7847fc23f4a97088f14 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 4 Sep 2026 09:54:42 -0400 Subject: [PATCH 15/17] Modernize codebase with nullable reference types and dependency updates This commit introduces widespread adoption of nullable reference types (NRTs) across domain models, messages, and services. Properties are now explicitly initialized to non-null defaults (e.g., `string.Empty`, `new List()`) or annotated as nullable to improve code robustness and prevent null reference exceptions. Additionally, Azure SDK client registration has been updated from `AddAzureTableClient` to `AddAzureTableServiceClient`, aligning with recent dependency changes. Minor adjustments to OpenAPI configuration and Blazor component injections reflect updated library conventions. --- src/Api/Program.cs | 2 +- src/Api/ShortenerEnpoints.cs | 3 +- src/Core/Domain/ClickDateList.cs | 5 ++-- src/Core/Domain/ClickStatsEntity.cs | 6 ++-- src/Core/Domain/ClickStatsEntityList.cs | 2 +- src/Core/Domain/DetailledBadRequest.cs | 2 +- src/Core/Domain/NextId.cs | 4 +-- src/Core/Domain/ShortUrlEntity.cs | 28 +++++++++---------- src/Core/Domain/ShortenerSettings.cs | 6 ++-- src/Core/Messages/ListResponse.cs | 2 +- src/Core/Messages/ShortRequest.cs | 8 +++--- src/Core/Messages/ShortResponse.cs | 6 ++-- src/Core/Messages/ShortUrlRequest.cs | 6 ++-- src/Core/Service/AzStrorageTablesService.cs | 13 ++++----- src/Core/Service/Utility.cs | 10 +++---- src/FunctionsLight/Program.cs | 2 +- .../Components/Pages/UrlManager.razor | 6 ++-- .../Components/Shared/ScheduleComponent.razor | 2 +- .../Shared/SchedulesComponent.razor | 2 +- src/TinyBlazorAdmin/UrlManagerClient.cs | 2 +- 20 files changed, 55 insertions(+), 62 deletions(-) diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 3a93d85a..de3bb9a0 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -6,7 +6,7 @@ // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); -builder.AddAzureTableClient("strTables"); +builder.AddAzureTableServiceClient("strTables"); builder.Services.AddTransient(sp => { diff --git a/src/Api/ShortenerEnpoints.cs b/src/Api/ShortenerEnpoints.cs index d5e702b3..b56c996a 100644 --- a/src/Api/ShortenerEnpoints.cs +++ b/src/Api/ShortenerEnpoints.cs @@ -10,8 +10,7 @@ public static class ShortenerEnpoints { public static void MapShortenerEnpoints(this IEndpointRouteBuilder app) { - var endpoints = app.MapGroup("api") - .WithOpenApi(); + var endpoints = app.MapGroup("api"); // GETS diff --git a/src/Core/Domain/ClickDateList.cs b/src/Core/Domain/ClickDateList.cs index 0cf13b43..27208e57 100644 --- a/src/Core/Domain/ClickDateList.cs +++ b/src/Core/Domain/ClickDateList.cs @@ -2,11 +2,10 @@ namespace Cloud5mins.ShortenerTools.Core.Domain { public class ClickDateList { - public List Items { get; set; } - public string Url { get; set; } + public List Items { get; set; } = new(); + public string Url { get; set; } = string.Empty; public ClickDateList() { - Url = string.Empty; } public ClickDateList(List list) { diff --git a/src/Core/Domain/ClickStatsEntity.cs b/src/Core/Domain/ClickStatsEntity.cs index da0a7943..94a59d17 100644 --- a/src/Core/Domain/ClickStatsEntity.cs +++ b/src/Core/Domain/ClickStatsEntity.cs @@ -5,9 +5,9 @@ namespace Cloud5mins.ShortenerTools.Core.Domain; public class ClickStatsEntity : ITableEntity { - public string Datetime { get; set; } - public string PartitionKey { get; set; } - public string RowKey { get; set; } + public string Datetime { get; set; } = string.Empty; + public string PartitionKey { get; set; } = string.Empty; + public string RowKey { get; set; } = string.Empty; public DateTimeOffset? Timestamp { get; set; } public ETag ETag { get; set; } diff --git a/src/Core/Domain/ClickStatsEntityList.cs b/src/Core/Domain/ClickStatsEntityList.cs index b3b25852..ca0b8c15 100644 --- a/src/Core/Domain/ClickStatsEntityList.cs +++ b/src/Core/Domain/ClickStatsEntityList.cs @@ -2,7 +2,7 @@ namespace Cloud5mins.ShortenerTools.Core.Domain { public class ClickStatsEntityList { - public List ClickStatsList { get; set; } + public List ClickStatsList { get; set; } = new(); public ClickStatsEntityList() { } public ClickStatsEntityList(List list) diff --git a/src/Core/Domain/DetailledBadRequest.cs b/src/Core/Domain/DetailledBadRequest.cs index c66c7d3d..26e9e32c 100644 --- a/src/Core/Domain/DetailledBadRequest.cs +++ b/src/Core/Domain/DetailledBadRequest.cs @@ -2,6 +2,6 @@ namespace Cloud5mins.ShortenerTools.Core.Domain { public class DetailedBadRequest { - public string Message { get; set; } + public string Message { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/src/Core/Domain/NextId.cs b/src/Core/Domain/NextId.cs index 8edc0f5f..19e7c9b3 100644 --- a/src/Core/Domain/NextId.cs +++ b/src/Core/Domain/NextId.cs @@ -6,8 +6,8 @@ namespace Cloud5mins.ShortenerTools.Core.Domain; public class NextId : ITableEntity { public int Id { get; set; } - public string PartitionKey { get; set; } - public string RowKey { get; set; } + public string PartitionKey { get; set; } = string.Empty; + public string RowKey { get; set; } = string.Empty; public DateTimeOffset? Timestamp { get; set; } public ETag ETag { get; set; } } diff --git a/src/Core/Domain/ShortUrlEntity.cs b/src/Core/Domain/ShortUrlEntity.cs index 4841bc5d..b355246a 100644 --- a/src/Core/Domain/ShortUrlEntity.cs +++ b/src/Core/Domain/ShortUrlEntity.cs @@ -2,16 +2,14 @@ using Azure.Data.Tables; using System.Runtime.Serialization; using System.Text.Json; -using Azure; -using Azure.Data.Tables; namespace Cloud5mins.ShortenerTools.Core.Domain { public class ShortUrlEntity : ITableEntity { - public string Url { get; set; } - private string _activeUrl { get; set; } + public string Url { get; set; } = string.Empty; + private string _activeUrl { get; set; } = string.Empty; public string ActiveUrl { @@ -24,16 +22,16 @@ public string ActiveUrl } - public string Title { get; set; } + public string Title { get; set; } = string.Empty; - public string ShortUrl { get; set; } + public string ShortUrl { get; set; } = string.Empty; public int Clicks { get; set; } public bool? IsArchived { get; set; } - public string SchedulesPropertyRaw { get; set; } + public string SchedulesPropertyRaw { get; set; } = string.Empty; - private List _schedules { get; set; } + private List _schedules { get; set; } = new(); //[IgnoreProperty] [IgnoreDataMember] @@ -49,7 +47,7 @@ public List Schedules } else { - _schedules = JsonSerializer.Deserialize(SchedulesPropertyRaw).ToList(); + _schedules = JsonSerializer.Deserialize(SchedulesPropertyRaw)?.ToList() ?? new List(); } } return _schedules; @@ -60,12 +58,12 @@ public List Schedules } } - public string PartitionKey { get; set; } - public string RowKey { get; set; } + public string PartitionKey { get; set; } = string.Empty; + public string RowKey { get; set; } = string.Empty; public DateTimeOffset? Timestamp { get; set; } public ETag ETag { get; set; } - public string CreatedDate { get; set; } + public string CreatedDate { get; set; } = string.Empty; @@ -76,17 +74,17 @@ public ShortUrlEntity(string longUrl, string endUrl) Initialize(longUrl, endUrl, string.Empty, null); } - public ShortUrlEntity(string longUrl, string endUrl, Schedule[] schedules) + public ShortUrlEntity(string longUrl, string endUrl, Schedule[]? schedules) { Initialize(longUrl, endUrl, string.Empty, schedules); } - public ShortUrlEntity(string longUrl, string endUrl, string title, Schedule[] schedules) + public ShortUrlEntity(string longUrl, string endUrl, string title, Schedule[]? schedules) { Initialize(longUrl, endUrl, title, schedules); } - private void Initialize(string longUrl, string endUrl, string title, Schedule[] schedules) + private void Initialize(string longUrl, string endUrl, string title, Schedule[]? schedules) { PartitionKey = endUrl.First().ToString(); RowKey = endUrl; diff --git a/src/Core/Domain/ShortenerSettings.cs b/src/Core/Domain/ShortenerSettings.cs index b702f8d4..ceb52e15 100644 --- a/src/Core/Domain/ShortenerSettings.cs +++ b/src/Core/Domain/ShortenerSettings.cs @@ -2,8 +2,8 @@ namespace Cloud5mins.ShortenerTools.Core.Domain { public class ShortenerSettings { - public string DefaultRedirectUrl { get; set; } - public string CustomDomain { get; set; } - public string DataStorage { get; set; } + public string DefaultRedirectUrl { get; set; } = string.Empty; + public string CustomDomain { get; set; } = string.Empty; + public string DataStorage { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/src/Core/Messages/ListResponse.cs b/src/Core/Messages/ListResponse.cs index a20619fc..66556c7e 100644 --- a/src/Core/Messages/ListResponse.cs +++ b/src/Core/Messages/ListResponse.cs @@ -4,7 +4,7 @@ namespace Cloud5mins.ShortenerTools.Core.Messages { public class ListResponse { - public List UrlList { get; set; } + public List UrlList { get; set; } = new(); public ListResponse() { } public ListResponse(List list) diff --git a/src/Core/Messages/ShortRequest.cs b/src/Core/Messages/ShortRequest.cs index c366338d..2cf5092c 100644 --- a/src/Core/Messages/ShortRequest.cs +++ b/src/Core/Messages/ShortRequest.cs @@ -4,12 +4,12 @@ namespace Cloud5mins.ShortenerTools.Core.Messages { public class ShortRequest { - public string Vanity { get; set; } + public string Vanity { get; set; } = string.Empty; - public string Url { get; set; } + public string Url { get; set; } = string.Empty; - public string Title { get; set; } + public string Title { get; set; } = string.Empty; - public Schedule[] Schedules { get; set; } + public Schedule[] Schedules { get; set; } = Array.Empty(); } } \ No newline at end of file diff --git a/src/Core/Messages/ShortResponse.cs b/src/Core/Messages/ShortResponse.cs index a4cf2df6..7a475688 100644 --- a/src/Core/Messages/ShortResponse.cs +++ b/src/Core/Messages/ShortResponse.cs @@ -2,9 +2,9 @@ namespace Cloud5mins.ShortenerTools.Core.Messages { public class ShortResponse { - public string ShortUrl { get; set; } - public string LongUrl { get; set; } - public string Title { get; set; } + public string ShortUrl { get; set; } = string.Empty; + public string LongUrl { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; public ShortResponse() { } public ShortResponse(string host, string longUrl, string endUrl, string title) diff --git a/src/Core/Messages/ShortUrlRequest.cs b/src/Core/Messages/ShortUrlRequest.cs index 0300e83e..32f76d30 100644 --- a/src/Core/Messages/ShortUrlRequest.cs +++ b/src/Core/Messages/ShortUrlRequest.cs @@ -7,7 +7,7 @@ namespace Cloud5mins.ShortenerTools.Core.Messages { public class ShortUrlRequest { - private string _vanity; + private string _vanity = string.Empty; public string? Title { get; set; } @@ -24,9 +24,9 @@ public string Vanity } [Required] - public string Url { get; set; } + public string Url { get; set; } = string.Empty; - private List _schedules; + private List _schedules = new(); public List Schedules { diff --git a/src/Core/Service/AzStrorageTablesService.cs b/src/Core/Service/AzStrorageTablesService.cs index 4d2ea59e..d78e0f47 100644 --- a/src/Core/Service/AzStrorageTablesService.cs +++ b/src/Core/Service/AzStrorageTablesService.cs @@ -111,7 +111,7 @@ public async Task UpdateShortUrlEntity(ShortUrlEntity urlEntity) public async Task GetShortUrlEntityByVanity(string vanity) { var tblUrls = GetUrlsTable(); - ShortUrlEntity shortUrlEntity = null; + ShortUrlEntity? shortUrlEntity = null; var result = tblUrls.QueryAsync(e => e.RowKey == vanity); await foreach (var entity in result) @@ -126,14 +126,13 @@ public async Task GetShortUrlEntity(ShortUrlEntity row) { TableClient tblUrls = GetUrlsTable(); var response = await tblUrls.GetEntityAsync(row.PartitionKey, row.RowKey); - ShortUrlEntity eShortUrl = response.Value as ShortUrlEntity; - return eShortUrl; + return response.Value; } public async Task IfShortUrlEntityExistByVanity(string vanity) { - ShortUrlEntity shortUrlEntity = await GetShortUrlEntityByVanity(vanity); + ShortUrlEntity? shortUrlEntity = await GetShortUrlEntityByVanity(vanity); return (shortUrlEntity != null); } @@ -155,11 +154,11 @@ public async Task ArchiveShortUrlEntity(ShortUrlEntity urlEntity - public async Task> GetAllStatsByVanity(string vanity, string startDate, string endDate) + public async Task> GetAllStatsByVanity(string vanity, string? startDate, string? endDate) { var tblStats = GetStatsTable(); - var sDate = DateOnly.ParseExact(startDate, "yyyy-MM-dd", CultureInfo.InvariantCulture); - var eDate = DateOnly.ParseExact(endDate, "yyyy-MM-dd", CultureInfo.InvariantCulture); + var sDate = DateOnly.ParseExact(startDate ?? DateOnly.MinValue.ToString("yyyy-MM-dd"), "yyyy-MM-dd", CultureInfo.InvariantCulture); + var eDate = DateOnly.ParseExact(endDate ?? DateOnly.MaxValue.ToString("yyyy-MM-dd"), "yyyy-MM-dd", CultureInfo.InvariantCulture); var lstUrlStats = new List(); AsyncPageable queryResult; diff --git a/src/Core/Service/Utility.cs b/src/Core/Service/Utility.cs index 68b14daa..a3c23e03 100644 --- a/src/Core/Service/Utility.cs +++ b/src/Core/Service/Utility.cs @@ -94,8 +94,8 @@ public async static Task ExtractUrlsDataFromCSV(string fileFullName) { data.NextId = new NextId { - PartitionKey = csv.GetField("PartitionKey"), - RowKey = csv.GetField("RowKey"), + PartitionKey = csv.GetField("PartitionKey") ?? String.Empty, + RowKey = csv.GetField("RowKey") ?? String.Empty, Id = csv.GetField("Id") }; } @@ -103,11 +103,11 @@ public async static Task ExtractUrlsDataFromCSV(string fileFullName) { var record = new ShortUrlEntity { - PartitionKey = csv.GetField("PartitionKey"), - RowKey = csv.GetField("RowKey"), + PartitionKey = csv.GetField("PartitionKey") ?? String.Empty, + RowKey = csv.GetField("RowKey") ?? String.Empty, Clicks = csv.GetField("Clicks") ?? 0, Title = csv.GetField("Title") ?? String.Empty, - Url = csv.GetField("Url"), + Url = csv.GetField("Url") ?? String.Empty, SchedulesPropertyRaw = csv.GetField("SchedulesPropertyRaw") ?? String.Empty, IsArchived = csv.GetField("IsArchived") ?? false }; diff --git a/src/FunctionsLight/Program.cs b/src/FunctionsLight/Program.cs index 6e68fedc..d236cb08 100644 --- a/src/FunctionsLight/Program.cs +++ b/src/FunctionsLight/Program.cs @@ -3,7 +3,7 @@ var builder = FunctionsApplication.CreateBuilder(args); -builder.AddAzureTableClient("strTables"); +builder.AddAzureTableServiceClient("strTables"); builder.ConfigureFunctionsWebApplication(); diff --git a/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor b/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor index 74f1dd42..10fa37af 100644 --- a/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor +++ b/src/TinyBlazorAdmin/Components/Pages/UrlManager.razor @@ -84,9 +84,8 @@ IQueryable? filteredUrlList => urlList?.Where(x => x.RowKey.Contains(vanityFilter, StringComparison.CurrentCultureIgnoreCase) && x.Title.Contains(titleFilter, StringComparison.CurrentCultureIgnoreCase) ); PaginationState pagination = new PaginationState { ItemsPerPage = 20 }; - ShortUrlRequest shortUrlRequest; - ShortUrlEntity editedUrl; - public List gridData { get; set; } + ShortUrlRequest shortUrlRequest = new(); + public List gridData { get; set; } = new(); GridSort sortByClicks = GridSort.ByDescending(p => p.Clicks); GridSort sortBySchedules = GridSort.ByDescending(p => p.Schedules.Count); @@ -156,7 +155,6 @@ } } - [Inject] public IJSRuntime JsRuntime { get; set; } public async Task CopyToClipboardAsync(string url) { await JSRuntime.InvokeVoidAsync("clipboardCopy.copyText", url); diff --git a/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor b/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor index c4348957..439341f6 100644 --- a/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor +++ b/src/TinyBlazorAdmin/Components/Shared/ScheduleComponent.razor @@ -33,7 +33,7 @@ @code { [Parameter] - public Schedule? schedule { get; set; } + public Schedule schedule { get; set; } = new(); private DateTime startDate; private DateTime startTime; diff --git a/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor b/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor index 45428eea..dea0dda7 100644 --- a/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor +++ b/src/TinyBlazorAdmin/Components/Shared/SchedulesComponent.razor @@ -37,7 +37,7 @@ @code { [Parameter] - public List schedules { get; set; } + public List schedules { get; set; } = new(); private void AddScheduleClick(){ diff --git a/src/TinyBlazorAdmin/UrlManagerClient.cs b/src/TinyBlazorAdmin/UrlManagerClient.cs index a2593c57..28a9f887 100644 --- a/src/TinyBlazorAdmin/UrlManagerClient.cs +++ b/src/TinyBlazorAdmin/UrlManagerClient.cs @@ -9,7 +9,7 @@ public class UrlManagerClient(HttpClient httpClient) public async Task?> GetUrls() { - IQueryable urlList = null; + IQueryable? urlList = null; try{ using var response = await httpClient.GetAsync("/api/UrlList"); if(response.IsSuccessStatusCode){ From a4c11b2d97a79736a538ed1028e1c1ec9495fd5b Mon Sep 17 00:00:00 2001 From: Frank Boucher <2404846+fboucher@users.noreply.github.com> Date: Fri, 4 Sep 2026 10:23:03 -0400 Subject: [PATCH 16/17] Merge pull request #26 from fboucher/all-contributors/add-michaelmsonne docs: add michaelmsonne as a contributor for bug --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0f9e3035..c29451ca 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -224,6 +224,15 @@ "contributions": [ "code" ] + }, + { + "login": "michaelmsonne", + "name": "Michael Morten Sonne | Microsoft MVP", + "avatar_url": "https://avatars.githubusercontent.com/u/49366751?v=4", + "profile": "https://blog.sonnes.cloud", + "contributions": [ + "bug" + ] } ], "commitConvention": "angular", diff --git a/README.md b/README.md index e0e3bcf6..6adb8e06 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![GitHub Release](https://img.shields.io/github/v/release/microsoft/AzUrlShortener) ![.NET](https://img.shields.io/badge/10.0-512BD4?logo=dotnet&logoColor=fff) [![Build](https://github.com/microsoft/AzUrlShortener/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/microsoft/AzUrlShortener/actions/workflows/build.yml) ![GitHub License](https://img.shields.io/github/license/microsoft/AzUrlShortener) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white)](https://discord.gg/6zA3jKw) -[![All Contributors](https://img.shields.io/badge/all_contributors-23-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-24-orange.svg?style=flat-square)](#contributors-) @@ -116,6 +116,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d ddematheu2
ddematheu2

🐛 💻 David Oliva Paredes
David Oliva Paredes

💻 + Michael Morten Sonne | Microsoft MVP
Michael Morten Sonne | Microsoft MVP

🐛 From c08318712b24fcb862e197beb8e8119d89834e96 Mon Sep 17 00:00:00 2001 From: fboucher Date: Fri, 4 Sep 2026 10:50:44 -0400 Subject: [PATCH 17/17] adding docs to deploy the budget version --- doc/faq.md | 1 + doc/how-deploy-budget-solution.md | 96 +++++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/doc/faq.md b/doc/faq.md index efd319d5..77565e15 100644 --- a/doc/faq.md +++ b/doc/faq.md @@ -10,6 +10,7 @@ - [How does it work?](./how-it-works.md) - [Security Considerations](./security-considerations.md) - [How to make the api public](./how-to-set-api-public.md) +- [How to deploy to App Service (budget friendlier version)](./how-deploy-budget-solution.md) ## How to run AzUrlShortener locally diff --git a/doc/how-deploy-budget-solution.md b/doc/how-deploy-budget-solution.md index 3e4f5bdf..9d2527e1 100644 --- a/doc/how-deploy-budget-solution.md +++ b/doc/how-deploy-budget-solution.md @@ -1,16 +1,100 @@ -cd src/budget-deployment +# Deploy the budget solution -azd up +This guide explains how to deploy the budget-friendly version of AzUrlShortener. This deployment uses an Azure App Service for the admin website, an Azure Functions app for redirects, and an API container running as a private sidecar in the admin App Service. +## Prerequisites +Install the following tools before you begin: +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) +- [Azure Developer CLI](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) +- Git, if you are cloning the repository +You also need: + +- An Azure subscription with permission to create resource groups and the resources in this deployment. +- A local copy of your fork or clone of the AzUrlShortener repository. +- The custom domain you will use for short URLs, including `https://`. +- The default redirect URL, including `https://`. + +## First deployment + +1. Sign in to Azure with both the Azure CLI and Azure Developer CLI: + + ```bash + az login + azd auth login + ``` + + Use the same account, or an account with access to the target subscription. + +1. Change to the budget deployment directory: + + ```bash + cd src/budget-deployment + ``` + + If you cloned the repository elsewhere, replace the path with your local repository path. + +1. Provision the Azure resources and deploy the application: + + ```bash + azd up + ``` + + During this command, `azd` asks you to select an Azure subscription and location, and to provide values for: + + - **Environment name**: Used in the resource group name. For example, an environment named `budget-prod` creates the resource group `rg-budget-prod`. + - **Custom domain**: The complete domain for your short URLs, such as `https://c5m.ca`. + - **Default redirect URL**: The destination used when a vanity URL does not exist or no vanity is supplied. + + `azd up` creates the resource group and required storage, App Service, Functions, and container registry resources. After provisioning, the post-provision hook builds the API image in Azure Container Registry and restarts the admin App Service so it can load the sidecar. + +1. Open the URLs printed by `azd` when deployment completes. The admin website URL is the App Service URL, and the redirect service URL is the Azure Functions URL. + +## Update an existing deployment + +After making application changes, sign in and move to the deployment directory: + +```bash az login +azd auth login cd /home/frank/gh/AzUrlShortener/src/budget-deployment -azd deploy admin or azd deploy -if the API sidecar image is missing, run: +``` + +Deploy only the admin website and its related application files with: + +```bash +azd deploy admin +``` + +To deploy all application services instead, use: + +```bash +azd deploy +``` + +Use `azd provision` when the infrastructure definition has changed and the Azure resources need to be updated. + +## Troubleshooting the API sidecar + +The API sidecar image is normally built automatically by the `azd up` post-provision hook. If the image is missing or the hook did not complete, run the hook manually from `src/budget-deployment`: + +```bash sh hooks/deploy-api-sidecar.sh -if needed, restart the app: -az webapp restart --name --resource-group \ No newline at end of file +``` + +The script builds the image with Azure Container Registry Tasks, so Docker does not need to be running locally. It then restarts the admin App Service. + +If the admin website still has the old sidecar after the image is available, restart it manually: + +```bash +az webapp restart \ + --name \ + --resource-group . + The API sidecar is private and is reachable only by the admin application through `http://localhost:8081`; it is not exposed directly to the internet. + + +Replace `` and `` with the values from the deployment output or the Azure portal. The API sidecar is private and is reachable only by the admin application through `http://localhost:8081`; it is not exposed directly to the internet. \ No newline at end of file