Deploying docker image to Azure with yml and bicep through Github Actions
How to deploy a Blazor Server with PostgreSQL docker image and infrastructure to Azure using GitHub Actions
Built on the sholders of giants
But before I continue I want to point out I used lot of information from Anto Subsh blog (at one point I was almost giving up and wanted to buy few hours of consulting from him) but I'm quite stubborn so I didn't give up even though this took me over two weeks to finish (nb. I'm on a holiday and not spending all day on this)!
But I also used ChatGPT, the bing chat and Code Pilot extensively and from that experience I can say that I don't feel like its likely that programmers will loose their jobs to AI anytime soon. These tools helped me allot to get going but often they took me on some wild tangent of "two steps ahead and seven back".
I'm getting much better at wielding these new technologies but there are two things you need to have to succeed in using it where the first it to create good context and then form the question in a good way and then its its being super critical of what you are offered and don't just accept it right away just because you are not the expert in it.
But this is the future and who doesn't want to have 1-2 extra hands helping you writing the boring code.
CI/CD steps from A-Z
Here I'm going to share with you the steps I had to take to make my CI/CD work so that when I check in code to a branch it will run tests and build the code. And when I merge to main
it will deploy to my dev environment and when I tag
the main
branch it will deploy to production.
Create a Github Action Workflow yml file
Navigate to the folder where your .git
folder is and create a .github
folder and in that one create a workflows
folder and create the file docker-build-and-deploy.yml
and paste in the following code.
Note that you need to manually create the Azure Container Registry in your Azure portal for this to work.
name: Version, Build, Provision Infrastructure, and Deploy
on:
push:
branches:
- main
tags:
- 'v*' # This will match tags like v1.0, v2.0.1, etc. and is used to deploy to production when the main branch is tagged
pull_request:
branches:
- main
env:
BICEP_FILE_PATH: TodoApp/LF.TodoApp/aspnet-core/Main.bicep
# Development Environment Configuration
DEV_APPSERVICE_PLAN: B1
DEV_RESOURCE_GROUP: todo-acr-dev-rg
DEV_LOCATION: northeurope
DEV_AZURE_WEBAPP_NAME: td-d-TodoApp
DEV_AZURE_CONTAINER_REGISTRY: todoacrdev.azurecr.io
DEV_POSTGRESQL_SERVER_NAME: todo-d-ser # max 10 letters
DEV_POSTGRESQL_DATABASE_NAME: TodoAppdb
DEV_POSTGRESQL_ADMIN_LOGIN: ${{ secrets.DEV_POSTGRESQL_ADMIN_LOGIN }}
DEV_POSTGRESQL_ADMIN_PASSWORD: ${{ secrets.DEV_POSTGRESQL_ADMIN_PASSWORD }}
DEV_POSTGRESQL_SKU_NAME: Standard_B1ms
DEV_POSTGRESQL_SKU_TIER: 'burstable' # or 'generalpurpose' or 'memoryoptimized'
DEV_POSTGRESQL_STORAGE: 32 # GB
# Production Environment Configuration
PROD_APPSERVICE_PLAN: B1
PROD_RESOURCE_GROUP: your-prod-resource-group
PROD_LOCATION: your-prod-location
PROD_AZURE_WEBAPP_NAME: td-p-TodoApp #prod
PROD_AZURE_CONTAINER_REGISTRY: your-prod-acr-name.azurecr.io
PROD_POSTGRESQL_DATABASE_NAME: todo-db-prod
PROD_POSTGRESQL_SERVER_NAME: todo-postgresql-prod
PROD_POSTGRESQL_ADMIN_LOGIN: ${{ secrets.PROD_POSTGRESQL_ADMIN_LOGIN }}
PROD_POSTGRESQL_ADMIN_PASSWORD: ${{ secrets.PROD_POSTGRESQL_ADMIN_PASSWORD }}
PROD_POSTGRESQL_SKU_NAME: Standard_B1ms
PROD_POSTGRESQL_SKU_TIER: 'generalpurpose' # or 'burstable' or 'memoryoptimized'
PROD_POSTGRESQL_STORAGE: 32 # GB
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run All Tests
run: |
dotnet test TodoApp/LF.TodoApp/aspnet-core/test/LF.TodoApp.Application.Tests/LF.TodoApp.Application.Tests.csproj --logger "trx;LogFileName=ApplicationTestResults.trx" --results-directory test/results
dotnet test TodoApp/LF.TodoApp/aspnet-core/test/LF.TodoApp.Domain.Tests/LF.TodoApp.Domain.Tests.csproj --logger "trx;LogFileName=DomainTestResults.trx" --results-directory test/results
dotnet test TodoApp/LF.TodoApp/aspnet-core/test/LF.TodoApp.EntityFrameworkCore.Tests/LF.TodoApp.EntityFrameworkCore.Tests.csproj --logger "trx;LogFileName=EntityFrameworkCoreTestResults.trx" --results-directory test/results
echo "Tests have been executed successfully"
build_and_upload:
name: Build and Upload to build Artifacts
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Only build the images once and then promote them to each environment by their tags!
# Build the Blazor UI Docker image
- name: Build the Blazor UI Docker image
run: docker build . --file TodoApp/LF.TodoApp/aspnet-core/src/LF.TodoApp.Blazor/Dockerfile --tag blazor:${{ github.sha }}
# Build the Migrator Docker image
- name: Build the Migrator Docker image
run: docker build . --file TodoApp/LF.TodoApp/aspnet-core/src/LF.TodoApp.DbMigrator/Dockerfile --tag dbmigrator:${{ github.sha }}
# Save Docker image as a tar file so it can be used in the next job
- name: Save Docker image
run: |
docker save blazor:${{ github.sha }} | gzip > blazor.tar.gz
docker save dbmigrator:${{ github.sha }} | gzip > dbmigrator.tar.gz
# Upload Docker image as a build artifact
- name: Upload Docker image
uses: actions/upload-artifact@v3.1.2
with:
name: docker-images
path: |
blazor.tar.gz
dbmigrator.tar.gz
push-docker-dev:
name: Push Docker Images to ACR (Development)
needs: build_and_upload
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download Docker image
uses: actions/download-artifact@v2
with:
name: docker-images
- name: Load Docker images
run: |
gunzip -c blazor.tar.gz | docker load
gunzip -c dbmigrator.tar.gz | docker load
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.DEV_AZURE_CREDENTIALS }}
- name: Login to ACR (Development)
run: |
az acr login --name ${{ env.DEV_AZURE_CONTAINER_REGISTRY }}
- name: Push Docker Images to ACR (Development)
run: |
docker tag blazor:${{ github.sha }} ${{ env.DEV_AZURE_CONTAINER_REGISTRY }}/blazor:${{ github.sha }}
docker push ${{ env.DEV_AZURE_CONTAINER_REGISTRY }}/blazor:${{ github.sha }}
docker tag dbmigrator:${{ github.sha }} ${{ env.DEV_AZURE_CONTAINER_REGISTRY }}/dbmigrator:${{ github.sha }}
docker push ${{ env.DEV_AZURE_CONTAINER_REGISTRY }}/dbmigrator:${{ github.sha }}
- name: Verify Images in ACR (Development)
run: |
blazor_image_check=$(az acr repository show-tags --name ${{ env.DEV_AZURE_CONTAINER_REGISTRY }} --repository blazor --output tsv | grep ${{ github.sha }})
dbmigrator_image_check=$(az acr repository show-tags --name ${{ env.DEV_AZURE_CONTAINER_REGISTRY }} --repository dbmigrator --output tsv | grep ${{ github.sha }})
if [[ -z "$blazor_image_check" ]]; then echo "Blazor image not found in ACR" && exit 1; fi
if [[ -z "$dbmigrator_image_check" ]]; then echo "DBMigrator image not found in ACR" && exit 1; fi
push-docker-prod:
name: Push Docker Images to ACR (Development)
needs: build_and_upload
if: startsWith(github.ref, 'refs/tags/v') && github.event.base_ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.PROD_AZURE_CREDENTIALS }}
- name: Login to ACR (Production)
run: |
az acr login --name ${{ env.PROD_AZURE_CONTAINER_REGISTRY }}
- name: Push Docker Images to ACR (Production)
run: |
docker tag blazor:${{ github.sha }} ${{ env.PROD_AZURE_CONTAINER_REGISTRY }}/blazor:${{ github.sha }}
docker push ${{ env.PROD_AZURE_CONTAINER_REGISTRY }}/blazor:${{ github.sha }}
docker tag dbmigrator:${{ github.sha }} ${{ env.PROD_AZURE_CONTAINER_REGISTRY }}/dbmigrator:${{ github.sha }}
docker push ${{ env.PROD_AZURE_CONTAINER_REGISTRY }}/dbmigrator:${{ github.sha }}
- name: Verify Images in ACR (Production)
run: |
blazor_image_check=$(az acr repository show-tags --name ${{ env.PROD_AZURE_CONTAINER_REGISTRY }} --repository blazor --output tsv | grep ${{ github.sha }})
dbmigrator_image_check=$(az acr repository show-tags --name ${{ env.PROD_AZURE_CONTAINER_REGISTRY }} --repository dbmigrator --output tsv | grep ${{ github.sha }})
if [[ -z "$blazor_image_check" ]]; then echo "Blazor image not found in ACR" && exit 1; fi
if [[ -z "$dbmigrator_image_check" ]]; then echo "DBMigrator image not found in ACR" && exit 1; fi
create-infrastructure-dev:
name: Create Infrastructure (Development)
needs: build_and_upload
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.DEV_AZURE_CREDENTIALS }}
- name: Create Resource Group (Development)
run: |
az group create --name ${{ env.DEV_RESOURCE_GROUP }} --location ${{ env.DEV_LOCATION }}
- name: Deploy Infrastructure (Development)
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ secrets.DEV_AZURE_SUBSCRIPTION_ID }}
resourceGroupName: ${{ env.DEV_RESOURCE_GROUP }}
template: ${{ env.BICEP_FILE_PATH }}
parameters: >-
webAppName=${{ env.DEV_AZURE_WEBAPP_NAME }}
aspnetcoreEnvironment=Dev
location=${{ env.DEV_LOCATION }}
appServicePlanSkuName=${{ env.DEV_APPSERVICE_PLAN }}
netFrameworkVersion=v7.0
postgresFlexibleServersName=${{ env.DEV_POSTGRESQL_SERVER_NAME }}
postgresqlAdminLogin=${{ env.DEV_POSTGRESQL_ADMIN_LOGIN }}
postgresqlAdminPassword=${{ env.DEV_POSTGRESQL_ADMIN_PASSWORD }}
postgresFlexibleServersSkuTier=${{env.DEV_POSTGRESQL_SKU_TIER }}
postgresFlexibleServersSkuName=${{env.DEV_POSTGRESQL_SKU_NAME }}
postgresqlServerStorage=${{ env.DEV_POSTGRESQL_STORAGE }}
databaseName=${{ env.DEV_POSTGRESQL_DATABASE_NAME }}
create-infrastructure-prod:
name: Create Infrastructure (Production)
needs: build_and_upload
if: startsWith(github.ref, 'refs/tags/v') && github.event.base_ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.PROD_AZURE_CREDENTIALS }}
- name: Create Resource Group (Production)
run: |
az group create --name ${{ env.PROD_RESOURCE_GROUP }} --location ${{ env.PROD_LOCATION }}
- name: Deploy Infrastructure (Production)
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ secrets.PROD_AZURE_SUBSCRIPTION_ID }}
resourceGroupName: ${{ env.PROD_RESOURCE_GROUP }}
template: ${{ env.BICEP_FILE_PATH }}
parameters: >-
webAppName=${{ env.PROD_AZURE_WEBAPP_NAME }}
aspnetcoreEnvironment=Production
location=${{ env.PROD_LOCATION }}
appServicePlanSkuName=${{ env.PROD_APPSERVICE_PLAN }}
netFrameworkVersion=v7.0
postgresFlexibleServersName=${{ env.PROD_POSTGRESQL_SERVER_NAME }}
postgresqlAdminLogin=${{ env.PROD_POSTGRESQL_ADMIN_LOGIN }}
postgresqlAdminPassword=${{ env.PROD_POSTGRESQL_ADMIN_PASSWORD }}
postgresFlexibleServersSkuTier= ${{env.PROD_POSTGRESQL_SKU_TIER }}
postgresFlexibleServersSkuName= ${{env.PROD_POSTGRESQL_SKU_NAME }}
postgresqlServerStorage=${{ env.PROD_POSTGRESQL_STORAGE }}
databaseName=${{ env.PROD_POSTGRESQL_DATABASE_NAME }}
deploy-development:
name: Deploy to Development
needs: [build_and_upload, create-infrastructure-dev,push-docker-dev]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.DEV_AZURE_CREDENTIALS }}
- name: Login to ACR (Development)
run: az acr login --name ${{ env.DEV_AZURE_CONTAINER_REGISTRY }}
- name: Deploy Blazor Docker Image to Azure Web App (Development)
run: |
az webapp config container set \
--name ${{ env.DEV_AZURE_WEBAPP_NAME }} \
--resource-group ${{ env.DEV_RESOURCE_GROUP }} \
--docker-custom-image-name ${{ env.DEV_AZURE_CONTAINER_REGISTRY }}/blazor:${{ github.sha }} \
--docker-registry-server-url https://${{ env.DEV_AZURE_CONTAINER_REGISTRY }}
- name: Run Migrations (Development)
run: |
docker pull ${{ env.DEV_AZURE_CONTAINER_REGISTRY }}/dbmigrator:${{ github.sha }}
# Create a connection string for the database and pass it to the container
connection_string="Server=${{ env.DEV_POSTGRESQL_SERVER_NAME }}.postgres.database.azure.com;Database=${{ env.DEV_POSTGRESQL_DATABASE_NAME }};Port=5432;User Id=${{ env.DEV_POSTGRESQL_ADMIN_LOGIN }};Password=${{ env.DEV_POSTGRESQL_ADMIN_PASSWORD }};Ssl Mode=Require;Trust Server Certificate=true;"
# Run Migrations
docker run --rm \
-e ConnectionStrings:Default="$connection_string" \
${{ env.DEV_AZURE_CONTAINER_REGISTRY }}/dbmigrator:${{ github.sha }}
deploy-production:
name: Deploy to Production
needs: [build_and_upload, create-infrastructure-prod, push-docker-prod]
if: startsWith(github.ref, 'refs/tags/v') && github.event.base_ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.PROD_AZURE_CREDENTIALS }}
- name: Login to ACR (Production)
run: az acr login --name ${{ env.PROD_AZURE_CONTAINER_REGISTRY }}
- name: Check Docker Images in ACR (Production)
run: |
blazor_manifests=$(az acr repository show-manifests --name ${{ env.PROD_AZURE_CONTAINER_REGISTRY }} --repository blazor --orderby time_desc)
dbmigrator_manifests=$(az acr repository show-manifests --name ${{ env.PROD_AZURE_CONTAINER_REGISTRY }} --repository dbmigrator --orderby time_desc)
if [[ $blazor_manifests != *"${{ github.sha }}"* || $dbmigrator_manifests != *"${{ github.sha }}"* ]]; then
echo "Docker images not found in ACR (Production)"
exit 1
fi
- name: Deploy Blazor Docker Image to Azure Web App (Production)
run: |
az webapp config container set \
--name ${{ env.PROD_AZURE_WEBAPP_NAME }} \
--resource-group ${{ env.PROD_RESOURCE_GROUP }} \
--docker-custom-image-name ${{ env.PROD_AZURE_CONTAINER_REGISTRY }}/blazor:${{ github.sha }} \
--docker-registry-server-url https://${{ env.PROD_AZURE_CONTAINER_REGISTRY }}
- name: Run Migrations (Development)
run: |
docker pull ${{ env.PROD_AZURE_CONTAINER_REGISTRY }}/dbmigrator:${{ github.sha }}
connection_string="Server=${{ env.PROD_POSTGRESQL_SERVER_NAME }}.postgres.database.azure.com;Database=${{ env.PROD_POSTGRESQL_DATABASE_NAME }};Port=5432;User Id=${{ env.PROD_POSTGRESQL_ADMIN_LOGIN }};Password=${{ env.PROD_POSTGRESQL_ADMIN_PASSWORD }};Ssl Mode=VerifyFull;"
# Run Migrations
docker run --rm \
-e ConnectionStrings:Default="$connection_string" \
${{ env.PROD_AZURE_CONTAINER_REGISTRY }}/dbmigrator:${{ github.sha }}
Add the secrets to GitHub
You need to add the following values
secrets.DEV_POSTGRESQL_ADMIN_LOGIN
secrets.DEV_POSTGRESQL_ADMIN_PASSWORD
secrets.DEV_AZURE_SUBSCRIPTION_ID
secrets.DEV_AZURE_CREDENTIALS
But to create the DEV_AZURE_CREDENTIALS
you need to run the following command
az ad sp create-for-rbac --name {yourServicePrincipalName} --role contributor --scopes /subscriptions/{yourSubscriptionId}/resourceGroups/{yourResourceGroup}
It will give you this
{
"appId": "a487e0c1-82af-47d9-9a0b-af184eb87646d",
"displayName": "myServicePrincipal",
"name": "http://myServicePrincipal",
"password": "879sd8-3435t-3478-58394-8893",
"tenant": "ad8c9dd3-6d4b-45c3-b9c6-5c4b1f8e5eb6"
}
that you should copy to this format
{
"clientId": "a487e0c1-82af-47d9-9a0b-af184eb87646d",
"clientSecret": "879sd8-3435t-3478-58394-8893",
"subscriptionId": "yourSubscriptionId",
"tenantId": "ad8c9dd3-6d4b-45c3-b9c6-5c4b1f8e5eb6"
}
and that JSON you should save into DEV_AZURE_CREDENTIALS
in Github
Create a Main.bicep
Now we need to create the infrastructure file so navigate to TodoApp/LF.TodoApp/aspnet-core/
and create Main.bicep
and paste in the following code.
This environment setup is just the most simple one just to get you started with something. Its not thought to be a super secure production environment! Please feel free to add and change this code as you like. I would love to see some gists
please.
// Parameters START
@description('Name of the web application')
param webAppName string
@description('ASP.NET Core environment')
param aspnetcoreEnvironment string
@description('Location for all resources.')
param location string = resourceGroup().location
@description('The SKU of App Service Plan')
@allowed(['B1', 'B2', 'B3', 'F1'])
param appServicePlanSkuName string = 'B1'
@description('The .NET Framework version for the web app')
@allowed(['v7.0','v8.0'])
param netFrameworkVersion string = 'v7.0'
@description('The tier of the particular SKU, e.g. Burstable')
@allowed(['burstable', 'generalpurpose', 'memoryoptimized'])
param postgresFlexibleServersSkuTier string = 'burstable'
@description('The SKU of the PostgreSQL server')
@allowed(['Standard_B1ms']) // Add more when going to production
param postgresFlexibleServersSkuName string = 'Standard_B1ms'
@description('The version of a PostgreSQL server')
@allowed([ '13' ])
param postgresFlexibleServersversion string = '13'
@description('Name of the PostgreSQL server')
param postgresFlexibleServersName string
@description('Admin login for the PostgreSQL server')
param postgresqlAdminLogin string
@description('Admin password for the PostgreSQL server')
@secure()
param postgresqlAdminPassword string
@description('The mode to create a new PostgreSQL server')
@allowed([ 'Create', 'Default', 'PointInTimeRestore', 'Update' ])
param createMode string = 'Default'
@description('The size of storage for the PostgreSQL server')
@allowed([32,64,128,256,512,1024,2084])
param postgresqlServerStorage int = 32
@description('Name of the PostgreSQL database')
param databaseName string
// Parameters END
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
name: '${webAppName}serviceplan'
location: location
kind: 'linux'
properties: {
reserved: true
}
sku: {
name: appServicePlanSkuName
}
}
//TODO: Make this work with KeyVault so we don´t have the password in plain text
@description('Azure Postgresql connection string')
var postgresqlConnection = 'Server=${postgresFlexibleServersName}.postgres.database.azure.com;Database=${databaseName};Port=5432;User Id=${postgresqlAdminLogin};Password=${postgresqlAdminPassword};Ssl Mode=Require;Trust Server Certificate=true;'
resource postgresFlexibleServers 'Microsoft.DBforPostgreSQL/flexibleServers@2022-12-01' = {
name: postgresFlexibleServersName
location: location
sku: {
name: postgresFlexibleServersSkuName
tier: postgresFlexibleServersSkuTier
}
properties: {
administratorLogin: postgresqlAdminLogin
administratorLoginPassword: postgresqlAdminPassword
createMode:createMode
storage: {
storageSizeGB: postgresqlServerStorage
}
version: postgresFlexibleServersversion
}
}
// Here is a list of GitHub IP´s that can be added https://api.github.com/meta
var GitHubIps = ['192.30.252.0','185.199.108.0', '140.82.112.0', '143.55.64.0', '20.201.28.148', '20.205.243.168', '20.87.225.211', '20.248.137.49', '20.207.73.85', '20.27.177.116', '20.200.245.245','20.233.54.49']
var firewallRuleNames = [for i in range(0, length(GitHubIps)): 'github-${postgresFlexibleServersName}-firewall-${i}']
@description('Firewall rules to allow GitHub Actions to access the PostgreSQL server to run the migrations')
resource firewallRules 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-03-01-preview' = [for i in range(0, length(GitHubIps)): {
name: firewallRuleNames[i]
parent: postgresFlexibleServers
properties: {
startIpAddress: GitHubIps[i]
endIpAddress: GitHubIps[i]
}
}]
@description('Firewall rule to allow Azure Services to access the PostgreSQL server')
resource postgresFlexibleServersFirewallRule 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2022-12-01' = {
name: 'AllowAzureServices'
parent: postgresFlexibleServers
properties: {
startIpAddress: '0.0.0.0'
endIpAddress: '0.0.0.0'
}
}
@description('Azure AppService')
resource webApplication 'Microsoft.Web/sites@2022-09-01' = {
name: webAppName
kind: 'app'
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
netFrameworkVersion: netFrameworkVersion
ftpsState: 'FtpsOnly'
connectionStrings: [
{
name: 'ConnectionStrings__Default'
connectionString: postgresqlConnection
type: 'PostgreSQL'
}
]
appSettings: [
{
name: 'ASPNETCORE_ENVIRONMENT'
value: aspnetcoreEnvironment
}
]
}
httpsOnly: true
publicNetworkAccess: 'Enabled'
}
}
// Output help information
output appServicePlanOutput string = appServicePlan.id
output postgresFlexibleServersOutput string = postgresFlexibleServers.id
output postgresqlServerOutput string = postgresFlexibleServers.id
output storageSizeMessage string = 'The storage size is: ${postgresqlServerStorage} GB'
output webApplicationOutput string = webApplication.properties.defaultHostName
There are few things in there to take note of.
Firewall rules: I had issues being able to connect from Github to the PostgreSQL and needed to add these rules. I tried lots of other suggestions (actions and identity etc.) but none of them worked so if you know of a better way please let me know.
Plain text connection string: Since I just wanted to get my code up and running on dev so I could start to code I wasn´t worried about this now. I will update this code to point to Azure KeyVault for the user/pass in the connection string before long.
Add a appsettings.Dev.json file
This is the file used in your dev environment and the Dev
part comes from the aspnetcoreEnvironment
passed into the bicep file that sets the ASPNETCORE_ENVIRONMENT
that will read from this file.
{
"App": {
"SelfUrl": "https://td-d-TodoApp.azurewebsites.net",
"RedirectAllowedUrls": "https://td-d-TodoApp.azurewebsites.net",
"DisablePII": "false"
},
"ConnectionStrings": {
"Default": "Server=todo-d-ser.postgres.database.azure.com;Database=TodoAppdb;Port=5432;User Id=YourAdmin;Password=YourPassword;Ssl Mode=Require;Trust Server Certificate=true;"
},
"AuthServer": {
"Authority": "https://td-d-TodoApp.azurewebsites.net",
"RequireHttpsMetadata": "true"
},
"StringEncryption": {
"DefaultPassPhrase": "ramdom text"
},
"MyAppCertificate": {
"X590": "some GUID is good"
}
}
Docker file update
Update your Dockerfile in the Blazor Server project like this with information from Anto Subash - Abp Dockerfile blog post. If you don't you will have a 500 error in the service.
# Base Image
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
ENV ASPNETCORE_URLS=http://+:80
# Build Image
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
# Install Node.js
ENV NODE_VERSION 16.13.0
ENV NODE_DOWNLOAD_URL https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz
ENV NODE_DOWNLOAD_SHA 589b7e7eb22f8358797a2c14a0bd865459d0b44458b8f05d2721294dacc7f734
RUN curl -SL "$NODE_DOWNLOAD_URL" --output nodejs.tar.gz \
&& echo "$NODE_DOWNLOAD_SHA nodejs.tar.gz" | sha256sum -c - \
&& tar -xzf "nodejs.tar.gz" -C /usr/local --strip-components=1 \
&& rm nodejs.tar.gz \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
# Install gnupg for verifying signatures
RUN apt update && apt -y install gnupg
# Install Yarn
ENV YARN_VERSION 1.22.15
RUN set -ex \
&& wget -qO- https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --import \
&& curl -fSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz" \
&& curl -fSLO --compressed "https://yarnpkg.com/downloads/$YARN_VERSION/yarn-v$YARN_VERSION.tar.gz.asc" \
&& gpg --batch --verify yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz \
&& mkdir -p /opt/yarn \
&& tar -xzf yarn-v$YARN_VERSION.tar.gz -C /opt/yarn --strip-components=1 \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarnpkg \
&& rm yarn-v$YARN_VERSION.tar.gz.asc yarn-v$YARN_VERSION.tar.gz
# Copy project files
COPY ["TodoApp/TD.TodoApp/aspnet-core/NuGet.Config", "."]
COPY ["TodoApp/TD.TodoApp/aspnet-core/src/TD.TodoApp.Blazor/TD.TodoApp.Blazor.csproj", "src/TD.TodoApp.Blazor/"]
COPY ["TodoApp/TD.TodoApp/aspnet-core/src/TD.TodoApp.Application/TD.TodoApp.Application.csproj", "src/TD.TodoApp.Application/"]
COPY ["TodoApp/TD.TodoApp/aspnet-core/src/TD.TodoApp.Domain/TD.TodoApp.Domain.csproj", "src/TD.TodoApp.Domain/"]
COPY ["TodoApp/TD.TodoApp/aspnet-core/src/TD.TodoApp.Domain.Shared/TD.TodoApp.Domain.Shared.csproj", "src/TD.TodoApp.Domain.Shared/"]
COPY ["TodoApp/TD.TodoApp/aspnet-core/src/TD.TodoApp.Application.Contracts/TD.TodoApp.Application.Contracts.csproj", "src/TD.TodoApp.Application.Contracts/"]
COPY ["TodoApp/TD.TodoApp/aspnet-core/src/TD.TodoApp.HttpApi/TD.TodoApp.HttpApi.csproj", "src/TD.TodoApp.HttpApi/"]
COPY ["TodoApp/TD.TodoApp/aspnet-core/src/TD.TodoApp.EntityFrameworkCore/TD.TodoApp.EntityFrameworkCore.csproj", "src/TD.TodoApp.EntityFrameworkCore/"]
# Restore and Install ABP CLI
RUN dotnet restore "src/TD.TodoApp.Blazor/TD.TodoApp.Blazor.csproj"
RUN dotnet tool install -g Volo.Abp.Cli
# Set environment path for ABP CLI
ENV PATH="${PATH}:/root/.dotnet/tools"
# Copy remaining files and install ABP libraries
COPY . .
WORKDIR "/src/TodoApp/TD.TodoApp/aspnet-core/src/TD.TodoApp.Blazor"
RUN abp install-libs
# Build the project
RUN dotnet build "TD.TodoApp.Blazor.csproj" -c Release -o /app/build
# Publish the project
FROM build AS publish
RUN dotnet publish "TD.TodoApp.Blazor.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Final Image
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "TD.TodoApp.Blazor.dll"]
Code changes
500.30 or pfx error
If you get this error you might need to follow this blog-post Deploying abp.io to an Azure AppService
Go to your Blazor project TodoAppBlazorModule.cs file and paste in the following code over GetSigningCertificate()
.
private X509Certificate2 GetSigningCertificate(
IWebHostEnvironment hostingEnv,
IConfiguration configuration)
{
var fileName = $"cert-signing.pfx";
var passPhrase = configuration["MyAppCertificate:X590:PassPhrase"];
var file = Path.Combine(hostingEnv.ContentRootPath, fileName);
if (File.Exists(file))
{
var created = File.GetCreationTime(file);
var days = (DateTime.Now - created).TotalDays;
if (days > 180)
File.Delete(file);
else
return new X509Certificate2(file, passPhrase,
X509KeyStorageFlags.MachineKeySet);
}
// file doesn't exist or was deleted because it expired
using var algorithm = RSA.Create(keySizeInBits: 2048);
var subject = new X500DistinguishedName("CN=LawFull.ai Signing Certificate");
var request = new CertificateRequest(subject, algorithm,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(
X509KeyUsageFlags.DigitalSignature, critical: true));
var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow.AddYears(2));
File.WriteAllBytes(file, certificate.Export(X509ContentType.Pfx, string.Empty));
return new X509Certificate2(file, passPhrase,
X509KeyStorageFlags.MachineKeySet);
}
private X509Certificate2 GetEncryptionCertificate(
IWebHostEnvironment hostingEnv,
IConfiguration configuration)
{
var fileName = $"cert-encryption.pfx";
var passPhrase = configuration["MyAppCertificate:X590:PassPhrase"];
var file = Path.Combine(hostingEnv.ContentRootPath, fileName);
if (File.Exists(file))
{
var created = File.GetCreationTime(file);
var days = (DateTime.Now - created).TotalDays;
if (days > 180)
File.Delete(file);
else
return new X509Certificate2(file, passPhrase,
X509KeyStorageFlags.MachineKeySet);
}
// file doesn't exist or was deleted because it expired
using var algorithm = RSA.Create(keySizeInBits: 2048);
var subject = new X500DistinguishedName("CN=LawFull.ai Encryption Certificate");
var request = new CertificateRequest(subject, algorithm,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(
X509KeyUsageFlags.KeyEncipherment, critical: true));
var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow,
DateTimeOffset.UtcNow.AddYears(2));
File.WriteAllBytes(file, certificate.Export(X509ContentType.Pfx, string.Empty));
return new X509Certificate2(file, passPhrase, X509KeyStorageFlags.MachineKeySet);
}
you will also need to update its usage at the top
PreConfigure<OpenIddictServerBuilder>(builder =>
{
// In production, it is recommended to use two RSA certificates,
// one for encryption, one for signing.
builder.AddSigningCertificate(GetSigningCertificate(hostingEnvironment, configuration));
builder.AddEncryptionCertificate(GetEncryptionCertificate(hostingEnvironment, configuration));
builder.SetIssuer(new Uri(configuration["AuthServer:Authority"]!));
});
Update the DBMigrator to accept env ConnectionString
I could not get the "Run Migrations" step to work in the yml by passing in the connectionStrings:Default value until I added the following code to DbMigratorHostedService.cs
public async Task StartAsync(CancellationToken cancellationToken)
{
var envConnectionString = Environment.GetEnvironmentVariable("ConnectionStrings:Default");
if (!string.IsNullOrEmpty(envConnectionString))
{
_configuration["ConnectionStrings:Default"] = envConnectionString;
Log.Logger.Information("Using ConnectionStrings:Default from environmental variable");
// rest of code
}
It would be great if somebody could share with me a solution to this.
Todo´s
ConnectionString from appsettings.json issue
There is one issue I'm still having problem with getting the AppService to read the ConnectionString from the Azure UI. It seems to want to read from the appsettings.Dev.json file and not allow the UI to override it as it should do!
I will figure this out (or somebody of you will let me know why it didn't work).
Add keyVault support for ConnectionString
Now the bicep code puts the connectionString into the UI in clear text but should add it with KeyVault connection. So what needs to be done is to add KeyVault to the bicep and then point the connectionString to it so it.
Azure Developer CLI (azd) for abp.io?
I wish somebody would create a AZD Template and share it with us. Here is the documentation to create these templates.
It would be super helpful for us to have many people working on these templates together for all the different kinds of abp.io projects.
Final words
I just wanted to share this with you quickly and not waist time making it "perfect". I will 100% iterate this code as I go further and will probably update this code if it changes allot.
But please let me know if you have some issues or if you have improvements or tips for me.