A sample demonstrating how to implement a containerized multitenant facility management and accommodation booking application. It uses microservices architecture and relies on a bunch of Azure services.
- 4 bounded contexts
- CQRS and DDD (with reliable domain events)
- ASP.NET Core 6, Blazor, gRPC
- Kubernetes and Docker Compose
- GitHub actions
- Azure Container Registry
- Helm
- Multitenant Azure AD organizational accounts
- Azure AD B2C authentication for customers
- OpenID Connect and OAuth2
- Azure Kubernetes Service
- Azure Container Registry
- Azure SQL databases
- Azure Storage
- Azure Service Bus
- Azure Search
- Application Insights
- Azure KeyVault
The application can be run locally in Docker Compose to facilitate VS debugging experience. In Staging/Production modes it's supposed to be run in a Kubernetes cluster.
docker-compose -f "docker-compose.yml" -f "docker-compose.development.yml" --no-ansi build
docker-compose -f "docker-compose.yml" -f "docker-compose.development.yml" --no-ansi up -d --no-build --force-recreate --remove-orphans
There are GitHub workflows to build and deploy the application. It is expected that the following variables are set in GitHub secrets:
REGISTRY_NAME
- name of the docker registry to push images to (e.g. myregistry.azurecr.io)REGISTRY_USERNAME
- username to log in to the registryREGISTRY_PASSWORD
- password to log in to the registryKEYVAULT_NAME
- name of the KeyVault (see below)USER_ASSIGNED_CLIENT_ID
- user-assigned managed identity that is configured to access KeyVault (list and get permissions)ACME_EMAIL
- a valid email address (Let's Encrypt will use this to contact you about expiring certificates and issues related to your account)DNS_BACKEND
- a URL for the backend API (e.g. backend.51.138.82.127.nip.io)DNS_FILES
- a URL for the backend API (e.g. files.51.138.82.127.nip.io)CLUSTER_RESOURCE_GROUP
- resource group name of the AKS clusterCLUSTER_NAME
- name of the AKS clusterAZURE_CREDENTIALS
- credentials for the AKS set context action
The credentials can be obtained by creating a new principal:
az ad sp create-for-rbac --name "myApp" --role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
--sdk-auth
# The command should output a JSON object similar to this:
{
"clientId": "<GUID>",
"clientSecret": "<STRING>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
"resourceManagerEndpointUrl": "<URL>"
}
If you already have a principal you can construct the credentials JSON using appropriate values. For the resourceManagerEndpointUrl
use https://management.azure.com/
.
The application supports Development, Staging and Production environments. In Development it relies on User Secrets and in Staging/Production it uses Azure KeyVault.
Here's a short description of configuration parameters:
{
"ApplicationInsights": {
"InstrumentationKey": "Application Insights resource instrumentation key"
},
"Authentication": {
"AzureAd": {
"Instance": "Your Azure AD instance, e.g. https://login.microsoftonline.com/",
"Audience": "BookFast API AppId in Azure AD, e.g. https://devunleashed.onmicrosoft.com/book-fast-api",
"ValidIssuers": "Comma separated list of tenant identifiers, e.g. https://sts.windows.net/490789ec-b183-4ba5-97cf-e69ec8870130/,https://sts.windows.net/f418e7eb-0dcd-40be-9b81-c58c87c57d9a/"
}
},
"ServiceBus": {
"ConnectionString": "Connection string to Service Bus topic",
"Facility": {
"NotificationQueueConnection": "Connection string to the Facility service notification queue"
},
"Booking": {
"NotificationQueueConnection": "Connection string to the Booking service notification queue"
}
},
"Data": {
"Azure": {
"Storage": {
"ConnectionString": "Connection string to an Azure storage account"
}
},
"DefaultConnection": {
"ConnectionString": "Connection string to a SQL database"
}
},
"Search": {
"QueryKey": "Azure Search query key",
"AdminKey": "Azure Search admin key",
"ServiceEndpoint": "Azure Search service endpoint",
"IndexName": "Azure Search index"
}
}
All services are configured to use the same User Secrets ID for simplicity. So it's enough to configure User Secrets with the settings shown above once to be used in Development environment.
Accessing KeyVault depends on how you run the application (in Kubernetes or in Docker Compose).
The application has been designed to run in Azure Kubernetes Service (AKS). In Staging and Production modes services are configured to use pod identity called my-pod-identity
(see deployment files). This article provides details on how to set up pod identity in the application namespace.
The pod identity in question is expected to reference a user-assigned managed identity that is configured to access KeyVault (list and get permissions).
Currently Docker Compose is supposed to be used locally on a dev box. In Production mode (that is used for demo purpose only) the following directory gets mounted to containers: ${OneDrive}/dev/BookFast/KeyVault
The directory is expected to contain 4 plain text files:
- tenantId
- clientId
- clientSecret
- keyVaultName
File names are pretty much self explonatory. Just make sure they don't contain extra spaces or line feeds. The app is going to try to create a ClientSecretCredential
to authenticate to KeyVault.
Alternatively it can also try to use a DefaultAzureCredential
if there is a KeyVaultName
environment variable. But in this case make sure to also define the following variables for DefaultAzureCredential
to work:
- AZURE_CLIENT_ID
- AZURE_CLIENT_SECRET
- AZURE_TENANT_ID
In both cases you need to have a corresponding principal in your Azure AD tenant that is also configured to access KeyVault secrets (list and get permissions).
Azure AD is used for organizational accounts of facility providers. You will need two applications in Azure AD: one for the APIs (Book Fast API app) and one for the web (BookFast app). Both applications should have multitenant support enabled. BookFast should have a delegated permission to access BookFast API app. If you're new to Azure AD the following post are going to help you out:
- Protecting your APIs with Azure Active Directory
- Enabling multitenant support in you Azure AD protected applications
Both apps have a user role called 'Facility Provider' that should be assigned to users to enable them to edit facilities. Please have a look at this post to understand how application and user roles are configured in Azure AD.
Customer accounts are managed in Azure AD B2C. It supports self sign up, profile editing and 3rd part identity providers.
You will need to create a B2C tenant and an app. You will also need to policies:
- Sign in or sign up policy
- Profile edit policy
You may also find this post useful when setting you your application.
BookFast.Facility.Data
and BookFast.Booking.Data
projects contain EFCore migrations to set up you SQL database schema.
Azure Service Bus is used as a message broker for integration events.
Please make sure to provision a single topic with 3 subscriptions:
- Booking
- Facility
- SearchIndexer
Also provision 2 notification queues:
- bookfast-facility-notifications
- bookfast-booking-notifications
BookFast.Search.Adapter can be run from the command line as dotnet run provision
in order to create an index in your Azure Search service. It will require the following parameters to be defined in user secrets:
- Search:ServiceEndpoint
- Search:AdminKey
- Search:IndexName