- Published on
Bit Bucket Pipelines - Part 4: Deploying a Function App to Azure with Bicep
6 min read- Authors
- Name
- Daniel Mackay
- @daniel_mackay
Series
- Part 1: Building and Testing ASP.NET Core
- Part 2: Deploying ASP.NET Core to Azure Web App
- Part 3: Deploying a React App to Azure Blob Storage
- Part 4: Deploying a Function App to Azure with Bicep
Intro
I started this Bit Bucket Series over 2 years ago and recently got the chance to build upon the knowledge I had by introducing Bicep and the Azure CLI into Bit Bucket Pipelines. This process was not nearly as smooth as I would have liked. All the moving parts worked fine locally, but once running within Bit Bucket Pipelines I ran into a slew of errors.
Let's dive into the problems I faced and the solutions I found to overcome them.
Problems
Microsoft Pipes Are Not supported
In the past, I have used Microsoft provided pipes to deploy Azure Functions. However, these have not been touched for years and are no longer supported.
Atlassian Pipes Are Buggy
I discovered that Atlassian has taken over support of the older Microsoft Pipes. At first, I thought this would solve some of the bugs I encountered, but alas it did not.
One of the key problems when running the azure-cli-run pipe is that it uses a public GitHub API under the hood to check for the latest version of Bicep. It turns out this API is rate limited and only allows 60 calls per hour from a single IP address. You can imagine how well this works on shared infrastructure (i.e. Bit Bucket Pipelines). A lot of people are unhappy about this. 🤦
Overhead With Using Pipes
Even if you get lucky to make it past the GitHub API throttling issue above, there is still an overhead with using a pipe like azure-cli-run pipe. Each AZ CLI command you need to run must provision the pipe, authenticate, and can only run a single command. All that takes about 30 seconds, which is not great if you need to run several AZ CLI commands.
Solutions
Manually Install the Azure CLI
Instead of using the Azure pipes, I decided to run the Azure CLI directly. The first option I tried was to use the microsoft-azure-cli docker image to run the commands. Unfortunately, that suffered from the same GitHub API problem above.
The most reliable way I found was to install the Azure CLI directly via:
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
Run multiple Commands in a Single Step
Installing the Azure CLI manually does come with an overhead, but if we need to run multiple commands in a single step the extra time spent with the install pays off as each command after that runs quickly. Doing this gave me a net reduction in the overall time needed to run the pipeline.
We could improve this even more by creating a custom docker image that contained the latest .NET SDK, PowerShell, Azure CLI and Bicep. Such an image would contain all tools we would possibly need to build and deploy .NET applications without having to spend time manually installing tools during each pipeline run.
Pipeline Configuration
With all of the above in mind, the final pipeline looked as follows:
image: mcr.microsoft.com/dotnet/sdk:7.0
definitions:
steps:
- step: &build
name: Build
caches:
- dotnetcore
script:
- dotnet restore
- dotnet build --no-restore --configuration Release -warnaserror
- step: &package
name: Package
caches:
- dotnetcore
script:
- apt-get update
- apt-get install zip -y
- dotnet build --configuration Release -warnaserror
- dotnet publish ./src/My.Project/My.Project.csproj -c Release -o ./publish
- cd ./publish
- zip -r func.zip .
artifacts:
- publish/func.zip
- step: &deploy
name: Deploy
script:
- curl -sL https://aka.ms/InstallAzureCLIDeb | bash
- az login --service-principal -u $AZURE_APP_ID -p $AZURE_PASSWORD --tenant $AZURE_TENANT_ID
- az group create --name $RG_NAME --location $LOCATION
- az deployment group create --resource-group $RG_NAME --template-file ./deploy/main.bicep --parameters location=australiaeast environment="$AZURE_ENV"
- az functionapp deployment source config-zip -g $RG_NAME -n $FUNCTION_NAME --src "publish/func.zip"
pipelines:
pull-requests:
'**':
- step: *build
branches:
main:
- step: *package
- stage:
name: Deploy to DEV
deployment: DEV
steps:
- step: *deploy
- stage:
name: Deploy to QA
deployment: QA
trigger: manual
steps:
- step: *deploy
- stage:
name: Deploy to PROD
deployment: PROD
trigger: manual
steps:
- step: *deploy
Bicep Configuration
The bicep file was as follows:
param location string
param environment string
var applicationName = 'my-app'
var suffix = '${applicationName}-${environment}'
var storageAccountName = 'stmyapp${environment}'
var functionAppName = 'func-${suffix}'
var hostingPlanName = 'plan-${suffix}'
var appInsightsName = 'appi-${suffix}'
resource functionApp 'Microsoft.Web/sites@2022-03-01' = {
name: functionAppName
kind: 'functionapp,linux'
location: location
properties: {
siteConfig: {
netFrameworkVersion: '7.0'
appSettings: [
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'dotnet-isolated'
}
{
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
value: appInsights.properties.InstrumentationKey
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(storageAccount.id, '2019-06-01').keys[0].value};EndpointSuffix=core.windows.net'
}
{
name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(storageAccount.id, '2019-06-01').keys[0].value};EndpointSuffix=core.windows.net'
}
{
name: 'WEBSITE_CONTENTSHARE'
value: '${toLower(functionAppName)}b614'
}
{
name: 'AZURE_FUNCTIONS_ENVIRONMENT'
value: dotNetEnvironment
}
]
}
clientAffinityEnabled: false
httpsOnly: true
publicNetworkAccess: 'Enabled'
}
dependsOn: [
hostingPlan
]
}
resource hostingPlan 'Microsoft.Web/serverfarms@2022-03-01' = {
name: hostingPlanName
location: location
kind: 'linux'
tags: {
}
sku: {
tier: 'Dynamic'
name: 'Y1'
}
}
resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: appInsightsName
location: location
tags: {
}
kind: 'web'
properties: {
Application_Type: 'web'
}
dependsOn: []
}
resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' = {
name: storageAccountName
location: location
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
properties: {
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
}
}
Summary
In summary, I faced multiple problems while introducing Bicep and Azure CLI into Bit Bucket Pipelines, including the unsupported nature of the Microsoft provided pipes and the bugs with Atlassian pipes. Additionally, using pipes caused overhead and further rate-limiting problems with a GitHub API. I ultimately decided to manually install the Azure CLI, which involved a little overhead but allowed running multiple commands in a single step, making the pipeline run faster. In the future, we may be able to create a custom docker image to speed up pipelines even further.
The final pipeline and bicep configuration showed an end-to-end example of building, packaging, and deploying a function app to DEV, QA, and PROD environments.
I hope this will help you to get up and running faster with deployment-related Bit Bucket Pipelines, and that you don't have to spend days figuring out the above as I did. 😅