Automating the build and deployment of an Angular application in the Azure DevOps (AzDO) ecosystem is as easy as any other code hosting service. AzDO has added task types to help deploy directly to Azure subscriptions. In this post, we will look at creating a build pipeline with caching, discuss deploying to Azure App Service, and pushing a container to the Azure Container Registry for use in AKS.

Building the Pipeline

AzDO does have a GUI to help set up a JavaScript-based project. The YAML generated is a great starting place for most applications. We noticed while running the generated build pipeline that the Node Package Manager (NPM) installation task was taking a significant amount of time, 15 minutes in some cases. The packages and versions have been decided at this point in development and were not expected to change often. AzDO allows for caching of files or artifacts during a build to be used in subsequent pipeline runs, saving the node modules folder and not installing if there are no changes. The package-lock.json file with its hash keys is the perfect file for checking if there have been changes.

The first part of the pipeline for installation and building is kept from the generated pipeline: install the latest node version, 16.14.0 is the latest stable version at the writing of this post; install the specific angular cli version globally needed for the application, 13.2.5; install packages from package.json file and install a library from a tar archive located in our repository. Finally, run ng build to create a dist folder of the angular application that can be deployed to the cloud.

steps:

- task: NodeTool@0
  inputs:
    versionSpec: '16.x'
  displayName: 'Install Node.js'

- task: Npm@1  
  displayName: 'Angular CLI 13.2.5'  
  inputs:  
    command: custom  
    verbose: false  
    customCommand: 'install -g @angular/cli@13.2.5'

- task: Npm@1  
  displayName: 'npm install'  
  inputs:
    command: custom  
    customCommand: 'install file:custom-angular-lib.tgz --legacy-peer-deps'  
  
- task: Npm@1  
  displayName: Build  
  inputs: 
    command: custom  
    verbose: false  
    customCommand: 'run build'  

Next, we wanted to update our job with caching to improve the run time of the pipeline. We added the npm_config_cache variable and used a “Cache” task type to set up the npm install. Using an environment variable to a path under $(Pipeline.Workspace) ensures the cache is accessible from container and non-container jobs. The logic on the cache task attempts to restore the cache, if successful the npm clean install step will not need to run.

variables:
  npm_config_cache: $(Pipeline.Workspace)/.npm

... 

- task: Cache@2
  inputs:
    key: 'npm | "$(Agent.OS)" | package-lock.json'
    restoreKeys: |
        npm | "$(Agent.OS)"
    path: $(npm_config_cache)
  displayName: Cache npm
- script: npm clean install

Lastly, building the Angular project with the ng build command and publish the created dist folder to our AzDO artifacts for use in the next stage, deployment.

- task: Npm@1  
      displayName: Build  
      inputs:
        command: custom  
        customCommand: 'run build'  
      
    - task: CopyPublishBuildArtifacts@1  
      displayName: 'Copy Publish Artifact: angular-web-app'  
      inputs:  
        CopyRoot: /dist  
        Contents: '**'  
        ArtifactName: angular-web-app 
        ArtifactType: Container

TOP 10 REASONS TO CHOOSE YAML FOR YOUR NEXT AZURE DEVOPS CI/CD PIPELINE
Discover reasons to choose YAML for your next Azure DevOps Pipeline and the benefits of a unified development experience.

Deploying the Pipeline

We had two deployment ideas in mind: Azure Kubernetes Service, AKS, and Azure App Service. Deploying to App Service requires the archived build artifact and pushing it to the Azure Subscription. Alternatively, if we wanted to use our application in AKS the application is bundled into a Docker image.

Before either deployment option, a task to sign in to the Azure subscription is required. More information on setting up to deploy to Azure can be found in Microsoft’s documentation.

- task: AzureCLI@2
      displayName: Az Login
      inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          echo "Login to AZ"
          az login --name <login name>

Deploy to App Service

The Azure Web Apps task can be used to deploy the Angular application artifact directly to the App Service. The task takes the artifact and pushes it to the App Service.

- task: AzureWebApp@1
    displayName: 'Deploy Azure Web App : angular-app'
    inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        appName: angular-app
        appType: webAppLinux
        package: $(Pipeline.Workspace)/angular-web-app 

Container for ACR and AKS

In order to deploy to an AKS cluster, the Angular application will need to be containerized and deployed to the Azure Container Registry, ACR. A basic Nginx container that hosts the dist folder is a common Angular dockerizing practice. Using the dockerfile in the repository, the pipeline will build the image and then push it to the ACR in our AZ subscription. Once the container is pushed to the registry, it can be used in any container service like AKS. See the Microsoft documentation on how to push to an Azure Container Registry.

- task: AzureCLI@2
      displayName: Az Container Registry Login
      inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          echo "Login to AZ"
          az acr login --name <login name>

- task: AzureCLI@2
      displayName: Docker Build angular-app
      inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          echo "Build image with docker"
          docker build -t angular-app:latest -f Dockerfile .

    - task: AzureCLI@2
      displayName: Docker Tag and Push angular-app
      inputs:
        azureSubscription: <Name of the Azure Resource Manager service connection>
        scriptType: bash
        scriptLocation: inlineScript
        inlineScript: |
          docker image tag angular-app:latest <name><endpoint>/<repo>/angular-app:latest
          docker image tag angular-app:latest <name><endpoint>/<repo>/angular-app:<build id>
          docker image push <name><endpoint>/<repo>/angular-app:<build id>
          docker image push <name><endpoint>/<repo>/angular-app:latest

Azure Artifacts vs. Caching

Caching was chosen as a solution over creating an artifact to improve build time by reusing files from earlier pipeline runs. The Microsoft documentation recommends using pipeline caching specifically for reducing build time and reusing files from previous runs. On the other hand, a pipeline artifact should be used if a job needs the files produced in a previous job in order to succeed. Since the installation of the node modules could be done even if a pulled cache fails, it made more sense to use caching.

Conclusion

By using AzDO pipelines, we were successful in creating a build pipeline with caching while having the flexibility to deploy to Azure App Service or AKS. The caching of the node modules sped up our installation task from 15 minutes to 3 minutes. The retrieval of the node modules still takes time but is faster overall, and possibly more cost-effective. Using the artifacts would not have benefited us in the same manner. Finally, the built-in Azure CLI tasks allow us to either deploy the Angular application directly to App Services or to build a Docker container, which allows us to push the image up to our Azure Registry for use in AKS or to use the container elsewhere.