Deploying a Node application to Azure with Bicep and Azure Pipelines

Yannick Haas
medialesson
Published in
4 min readApr 2, 2024

This is part of a multi-post series detailing how to consume environment variables in an Angular application, replacing the traditional environment.ts files. The other posts can be found here:

For this post, we’re going to use the application we created in here. The pipeline also applies to simple Node applications, which don’t serve static content, of course.

I’m going to assume you’ve already created the resource group in Azure. If not, you can read about how to create one here.

Bundle the Node application

By default Nx will not bundle Node applications and will instead only compile main.ts and create a package.json . However, uploading potentially thousands of files is going to require way longer than just having it all in one single file. So we need to tell Nx (or esbuild to be more specific) to bundle our application along with third party dependencies.

We do this by adding/changing the following options:

// apps/runtime-host/project.json
{
// ...
"targets": {
"build": {
// ...
"options": {
// ...
"bundle": true,
"thirdParty": true,
"generatePackageJson": false,
"esbuildOptions": {
"minify": true,
// ...
}
},
// ...
},
// ...
},
// ...
}

Building and Deploying

Now we’re going to add the script, which builds our application, to package.json , so we can easily modify it, if necessary, without having to modify the pipeline itself.

// package.json
{
// ...
"scripts": {
// ...
"ci:build": "nx run-many -t build"
},
// ...
}

Next, we’ll add our Bicep file inside a folder called .bicep . That way we can easily deploy to multiple environments without having to configure it ourselves. Again, this file is almost identical to the usual setup. We only have to change appCommandLine .

// .bicep/main.bicep
targetScope = 'resourceGroup'

param location string = resourceGroup().location
param envName string
// If set to development, Angular will run in development mode
@allowed(['production', 'development'])
param angularEnvironment string = 'production'
param appApiUrl string
param appBackgroundColor string
var productName = 'angular-environment-variables' // this is global fix
var appServicePlanName = 'plan-${productName}-linux-${envName}' // we expect this plan always exists!
var webAppName = 'app-${productName}-${envName}'
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: appServicePlanName
location: location
sku: {
name: 'F1'
}
properties: {
reserved: true
}
kind: 'linux'
}
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: webAppName
location: location
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
linuxFxVersion: 'NODE|18-LTS'
appCommandLine: 'pm2 start /home/site/wwwroot/main.js --no-daemon'
http20Enabled: true
webSocketsEnabled: false
autoHealEnabled: true
detailedErrorLoggingEnabled: true
ftpsState: 'Disabled'
}
httpsOnly: true
clientAffinityEnabled: false
}
}
resource webAppConnectionStrings 'Microsoft.Web/sites/config@2023-01-01' = {
name: 'appsettings'
parent: webApp
properties: {
ANGULAR_ENV: angularEnvironment
API_URL: appApiUrl
BACKGROUND_COLOR: appBackgroundColor
}
}

The steps required to build our software in Azure are:

  • Install npm packages with npm ci
  • Build Runtime Host and client
  • Copy files and upload as Pipeline Artifacts
  • Optional: Create infrastructure with Bicep
  • Get Pipeline Artifacts and deploy them to our Web App

If your application doesn’t serve static files, you simply have to remove the “Copy client” step from the azure-pipelines.yml . So this is what our Pipeline should look like:

# .azure/azure-pipelines.yml
# Replace <YOUR-RG-NAME> and <YOUR-SUBSCRIPTION> with appropriate values
trigger:
- main

pool:
vmImage: 'ubuntu-latest'

variables:
# Pipeline variables
# These should match the output folders in dist
PIPELINE_RUNTIME_HOST_DIST_DIR: 'runtime-host'
PIPELINE_CLIENT_DIR: 'angular-environment-variables/browser'
PIPELINE_BICEP_DROP_NAME: 'drop-bicep'
PIPELINE_APP_DROP_NAME: 'drop-app'
RESOURCE_GROUP_NAME: '<YOUR-RG-NAME>'
RESOURCE_GROUP_ENV: 'production'
# Variables used for the environment configuration set by bicep
# Ideally you want to have these in a variable group instead
APP_ANGULAR_ENVIRONMENT: 'production'
APP_API_URL: 'https://random.dog'
APP_BACKGROUND_COLOR: 'lime'

stages:
- stage: Build
displayName: Build App
jobs:
- job: WebApp
displayName: Build WebApp
steps:
- checkout: self
clean: 'true'
fetchDepth: 0

- task: NodeTool@0
displayName: Ensure Node is installed
inputs:
versionSource: 'spec'
versionSpec: '18.x'

# Check, if our bicep file is valid (optional, might still fail in deployment phase)
- script: az bicep build --file ./.bicep/main.bicep
displayName: Bicep build

# Install packages
- task: Npm@1
displayName: Install dependencies
inputs:
command: 'ci'

# Build Runtime Host and client
- task: Npm@1
displayName: Build applications
inputs:
command: 'custom'
customCommand: 'run ci:build'

# Copy bicep file
- task: CopyFiles@2
displayName: Copy Bicep files
inputs:
SourceFolder: '$(Build.SourcesDirectory)/.bicep'
Contents: '**'
TargetFolder: '$(Build.ArtifactStagingDirectory)/bicep'

# Copy Runtime Host
- task: CopyFiles@2
displayName: Copy Runtime Host files
inputs:
SourceFolder: '$(Build.SourcesDirectory)/dist/apps/$(PIPELINE_RUNTIME_HOST_DIST_DIR)'
Contents: '**'
TargetFolder: '$(Build.ArtifactStagingDirectory)/app'

# Copy client to sub-folder of Runtime Host
- task: CopyFiles@2
displayName: Copy client
inputs:
SourceFolder: '$(Build.SourcesDirectory)/dist/apps/$(PIPELINE_CLIENT_DIR)'
Contents: '**'
TargetFolder: '$(Build.ArtifactStagingDirectory)/app/client'

- task: PublishBuildArtifacts@1
displayName: Publish bicep drop
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/bicep'
ArtifactName: $(PIPELINE_BICEP_DROP_NAME)
publishLocation: 'Container'

- task: PublishBuildArtifacts@1
displayName: Publish app drop
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)/app'
ArtifactName: $(PIPELINE_APP_DROP_NAME)
publishLocation: 'Container'

- stage: deploy
displayName: Deploy WebApp
dependsOn: Build
jobs:
- deployment: deploy
displayName: Deploy WebApp
environment: angular-environment-variables
strategy:
runOnce:
deploy:
steps:
- download: current
displayName: Download artifacts

- task: AzureCLI@2
displayName: Run Bicep
inputs:
azureSubscription: '<YOUR-SUBSCRIPTION>'
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group create `
--resource-group $Env:RESOURCE_GROUP_NAME `
--name "DeployApp" `
--template-file "$Env:PIPELINE_WORKSPACE/$Env:PIPELINE_BICEP_DROP_NAME/main.bicep" `
--parameters envName="$Env:RESOURCE_GROUP_ENV" `
angularEnvironment="$Env:APP_ANGULAR_ENVIRONMENT" `
appApiUrl="$Env:APP_API_URL" `
appBackgroundColor="$Env:APP_BACKGROUND_COLOR"

- task: AzureWebApp@1
displayName: Deploy WebApp
inputs:
azureSubscription: '<YOUR-SUBSCRIPTION>'
appType: 'webAppLinux'
appName: 'app-angular-environment-variables-$(RESOURCE_GROUP_ENV)'
package: '$(Pipeline.Workspace)/$(PIPELINE_APP_DROP_NAME)'

To add this pipeline in Azure DevOps click “Create Pipeline”, select where your code is located and which repository you want to use. Then select “Existing Pipeline” and select the appropriate path (Azure DevOps should automatically detect your pipeline file).

If you navigate to your application now, you should see a random dog picture with the background color you’ve chosen.

--

--