Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
51c8e38
Merge branch 'main' into v-next
fboucher Jun 4, 2026
a31bb9b
upgrade packages
fboucher Jun 4, 2026
3794de1
aspire upgrade
fboucher Jul 10, 2026
9a133eb
Enhance Azure Storage integration and update package versions
fboucher Jul 24, 2026
95d2ea2
test new structures
fboucher Jul 31, 2026
ed3327c
starting point
fboucher Aug 7, 2026
c0c0041
clean reboot
fboucher Aug 7, 2026
ada812b
step
fboucher Aug 7, 2026
c792cca
cleaning up
fboucher Aug 7, 2026
18f31e6
good progress it deploy but missing a data storage
fboucher Aug 7, 2026
f4df21e
feat: Add storage accounts with identity-based access for Function Ap…
fboucher Aug 8, 2026
cec471d
draft
fboucher Aug 12, 2026
3b791dd
style: Refactor layout structure for improved readability and consist…
michaelmsonne Jul 13, 2026
b20a1af
It's 100 pourcent working
fboucher Aug 14, 2026
3af8952
Merge branch 'add-sidecar' into azure-options
fboucher Aug 14, 2026
c93c454
Merge branch 'azure-options' into frank/upgrade-time
fboucher Aug 14, 2026
cfea74f
updading packages
fboucher Aug 14, 2026
4a0641a
Modernize codebase with nullable reference types and dependency updates
fboucher Sep 4, 2026
7bc5d90
Merge pull request #25 from fboucher/frank/upgrade-time
fboucher Sep 4, 2026
07e2020
Merge pull request #21 from michaelmsonne/michaelmsonne-footer-layout…
fboucher Sep 4, 2026
a4c11b2
Merge pull request #26 from fboucher/all-contributors/add-michaelmsonne
fboucher Sep 4, 2026
c083187
adding docs to deploy the budget version
fboucher Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-BADGE:START - Do not remove or modify this section -->
[![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-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->


Expand Down Expand Up @@ -116,6 +116,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ddematheu2"><img src="https://avatars.githubusercontent.com/u/43075365?v=4?s=100" width="100px;" alt="ddematheu2"/><br /><sub><b>ddematheu2</b></sub></a><br /><a href="https://github.com/microsoft/AzUrlShortener/issues?q=author%3Addematheu2" title="Bug reports">🐛</a> <a href="https://github.com/microsoft/AzUrlShortener/commits?author=ddematheu2" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://davidop.code.blog/"><img src="https://avatars.githubusercontent.com/u/7433346?v=4?s=100" width="100px;" alt="David Oliva Paredes"/><br /><sub><b>David Oliva Paredes</b></sub></a><br /><a href="https://github.com/microsoft/AzUrlShortener/commits?author=davidop" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.sonnes.cloud"><img src="https://avatars.githubusercontent.com/u/49366751?v=4?s=100" width="100px;" alt="Michael Morten Sonne &#124; Microsoft MVP"/><br /><sub><b>Michael Morten Sonne &#124; Microsoft MVP</b></sub></a><br /><a href="https://github.com/microsoft/AzUrlShortener/issues?q=author%3Amichaelmsonne" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>
Expand Down
1 change: 1 addition & 0 deletions doc/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions doc/how-deploy-budget-solution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@


# Deploy the budget solution

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
```

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
```

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 <admin-app-name> \
--resource-group <resource-group-name>.
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 `<admin-app-name>` and `<resource-group-name>` 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.
11 changes: 11 additions & 0 deletions prompt.txt
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions src/.dockerignore
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.azure
publish/
3 changes: 2 additions & 1 deletion src/Api/Cloud5mins.ShortenerTools.Api.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<UserSecretsId>500bdb23-5095-412f-b1e9-421be27fb629</UserSecretsId>
</PropertyGroup>

<ItemGroup>
Expand Down
32 changes: 32 additions & 0 deletions src/Api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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.
# 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"]
2 changes: 1 addition & 1 deletion src/Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ILogger>(sp =>
{
Expand Down
3 changes: 1 addition & 2 deletions src/Api/ShortenerEnpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/AppHost/Cloud5mins.ShortenerTools.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Aspire.AppHost.Sdk/13.1.0">
<Project Sdk="Aspire.AppHost.Sdk/13.4.6">

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand Down
7 changes: 7 additions & 0 deletions src/AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Projects.Cloud5mins_ShortenerTools_FunctionsLight>("azfunc-light")
Expand Down
48 changes: 48 additions & 0 deletions src/AppHost/infra/admin.tmpl.yaml
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions src/AppHost/infra/api.tmpl.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading