Using Azure DevOps Pipeline to Build, Test, Scan, and Deploy a React Application to AKS
Tags: azure, azuredevops, aks, trivy
Published: 2023-04-14 20:41:00

In this blog post, we will walk through the process of setting up an Azure DevOps pipeline to build, test, scan, and deploy a React application to Azure Kubernetes Service (AKS). We will also integrate Trivy, an open-source vulnerability scanner, to scan the Docker image for security vulnerabilities.

## Prerequisites:

- An Azure DevOps account
- An Azure Kubernetes Service (AKS) cluster
- An Azure Container Registry
- Trivy installed on your Azure DevOps

## Project Architecture

![Project Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cxy0j26mqjn17vqwgqyw.png)
## 

## 1. Set up the Azure DevOps Pipeline
First, create a new pipeline in your Azure DevOps project. Then, add the following YAML code to your azure-pipelines.yml file. This pipeline has four stages: Test, Build, Scan and Push Image, and Deploy.

![Pipeline stages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jrfuax4zg0metw4n6vvh.png)
```
trigger:
  branches:
    include:
    - master

variables:
  acrName: 'acr234346'
  imageName: 'crypt-react-app'
  k8sNamespace: 'default'
  k8sDeploymentName: 'deployment'


stages:
- stage: Test
  displayName: 'Test stage'
  jobs:
  - job: Test
    displayName: 'Test Job'
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - checkout: self
    - task: NodeTool@0
      inputs:
        versionSpec: '16.x'
      displayName: 'Install Node.js'
    - script: |
        npm ci
        npm run test
      displayName: 'Run tests'


- stage: Build
  displayName: 'Build stage'
  dependsOn: Test
  condition: succeeded()
  jobs:
  - job: Build
    displayName: 'Build Job'
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - checkout: self

    - task: Docker@2
      displayName: 'Build and push Docker image'
      inputs:
        containerRegistry: 'docker-reg'
        repository: '$(imageName)'
        command: 'buildAndPush'
        Dockerfile: '**/Dockerfile'

    
- stage: Scan
  displayName: 'Scan Image'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - job: ScanImage
    displayName: 'Scan Image Job'
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: Docker@2
      displayName: 'Login to ACR'
      inputs:
        containerRegistry: 'docker-reg'
        repository: '$(imageName)'
        command: 'login'
    - task: trivy@1
      inputs:
        version: 'latest'
        docker: false
        loginDockerConfig: true
        image: $(acrName).azurecr.io/$(imageName):$(Build.BuildId)
        ignoreUnfixed: true
      displayName: Scan Docker image with Trivy

    
- stage: Deploy
  displayName: 'Deploy stage'
  dependsOn: Scan
  condition: succeeded()
  jobs:
  - deployment: Deploy
    displayName: 'Deploy Job'
    environment: 'production'
    pool:
      vmImage: 'ubuntu-latest'
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self
          - task: Kubernetes@1
            displayName: 'Deploy Deployment to AKS'
            inputs:
              connectionType: 'Azure Resource Manager'
              azureSubscriptionEndpoint: 'my-connection'
              azureResourceGroup: 'dev-rg'
              kubernetesCluster: 'demo-cluster'
              useClusterAdmin: true
              command: 'apply'
              useConfigurationFile: true
              configurationType: 'inline'
              inline: |
                apiVersion: apps/v1
                kind: Deployment
                metadata:
                  name: deployment
                  namespace: default
                spec:
                  replicas: 1
                  selector:
                    matchLabels:
                      app: crypto-app
                  template:
                    metadata:
                      labels:
                        app: crypto-app
                    spec:
                      containers:
                      - name: $(imageName)
                        image: $(acrName).azurecr.io/$(imageName):$(Build.BuildId)
                        ports:
                        - containerPort: 5000
              secretType: 'dockerRegistry'
              containerRegistryType: 'Azure Container Registry'

          - task: Kubernetes@1
            displayName: 'Deploy Service to AKS'
            inputs:
              connectionType: 'Azure Resource Manager'
              azureSubscriptionEndpoint: 'my-connection'
              azureResourceGroup: 'dev-rg'
              kubernetesCluster: 'demo-cluster'
              useClusterAdmin: true
              command: 'apply'
              useConfigurationFile: true
              configurationType: 'inline'
              inline: |
                apiVersion: v1
                kind: Service
                metadata:
                  name: crypto-app-service
                  namespace: default
                spec:
                  selector:
                    app: crypto-app
                  ports:
                    - protocol: TCP
                      port: 80
                      targetPort: 5000
                  type: LoadBalancer
              secretType: 'dockerRegistry'
              containerRegistryType: 'Azure Container Registry'
              
```

## 2. Configure the Kubernetes Deployment and Service
In the Deploy stage of the pipeline, we will deploy our application to AKS using Kubernetes Deployment and Service resources. The pipeline is configured to use inline YAML for these resources, making it easy to modify and maintain the configuration.

![AKS service](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vez8lv4tldkfnufkzjmn.png)

In the Deployment resource, we specify the Docker image to be used, pulled from the Azure Container Registry (ACR) created in the previous stage. The Service resource is set up as a LoadBalancer, allowing external access to the application.

Below is the code snippet for the Deploy stage, which includes the Deployment and Service resources:

```
- stage: Deploy
  displayName: 'Deploy stage'
  dependsOn: Scan
  condition: succeeded()
  jobs:
  - deployment: Deploy
    displayName: 'Deploy Job'
    environment: 'production'
    pool:
      vmImage: 'ubuntu-latest'
    strategy:
      runOnce:
        deploy:
          steps:
          - checkout: self
          - task: Kubernetes@1
            displayName: 'Deploy Deployment to AKS'
            inputs:
              connectionType: 'Azure Resource Manager'
              azureSubscriptionEndpoint: 'my-connection'
              azureResourceGroup: 'dev-rg'
              kubernetesCluster: 'demo-cluster'
              useClusterAdmin: true
              command: 'apply'
              useConfigurationFile: true
              configurationType: 'inline'
              inline: |
                apiVersion: apps/v1
                kind: Deployment
                metadata:
                  name: deployment
                  namespace: default
                spec:
                  replicas: 1
                  selector:
                    matchLabels:
                      app: crypto-app
                  template:
                    metadata:
                      labels:
                        app: crypto-app
                    spec:
                      containers:
                      - name: $(imageName)
                        image: $(acrName).azurecr.io/$(imageName):$(Build.BuildId)
                        ports:
                        - containerPort: 5000
              secretType: 'dockerRegistry'
              containerRegistryType: 'Azure Container Registry'

          - task: Kubernetes@1
            displayName: 'Deploy Service to AKS'
            inputs:
              connectionType: 'Azure Resource Manager'
              azureSubscriptionEndpoint: 'my-connection'
              azureResourceGroup: 'dev-rg'
              kubernetesCluster: 'demo-cluster'
              useClusterAdmin: true
              command: 'apply'
              useConfigurationFile: true
              configurationType: 'inline'
              inline: |
                apiVersion: v1
                kind: Service
                metadata:
                  name: crypto-app-service
                  namespace: default
                spec:
                  selector:
                    app: crypto-app
                  ports:
                    - protocol: TCP
                      port: 80
                      targetPort: 5000
                  type: LoadBalancer
              secretType: 'dockerRegistry'
              containerRegistryType: 'Azure Container Registry'
```

## 3. Scan Docker Image with Trivy
Trivy is an open-source scanner for vulnerabilities in container images, it detects vulnerabilities of OS packages (Alpine, Red Hat Universal Base Image, etc.) and application dependencies (Bundler, Composer, npm, yarn, etc.). It is highly configurable and easy to use. In this step, we will be integrating Trivy with our pipeline to scan our Docker image before pushing it to the ACR.

To use Trivy, we need to add a new job in our pipeline under the ScanAndPushImage stage. This job will run Trivy to scan the Docker image and report any vulnerabilities found. We will use the Trivy task provided by the Azure DevOps Marketplace.

The Trivy task needs the image name and tag to scan, and the scan results are saved in a JSON file. We can configure the task to fail if any critical vulnerabilities are found.

Here's the updated pipeline code with the Trivy job:

```
- stage: Scan
  displayName: 'Scan Image'
  dependsOn: Build
  condition: succeeded()
  jobs:
  - job: ScanImage
    displayName: 'Scan Image Job'
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: Docker@2
      displayName: 'Login to ACR'
      inputs:
        containerRegistry: 'docker-reg'
        repository: '$(imageName)'
        command: 'login'
    - task: trivy@1
      inputs:
        version: 'latest'
        docker: false
        loginDockerConfig: true
        image: $(acrName).azurecr.io/$(imageName):$(Build.BuildId)
        ignoreUnfixed: true
      displayName: Scan Docker image with Trivy
```

![Trivy result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6r5ergud3dh7z8uah1or.png)

## Conclusion
In conclusion, we have successfully set up a CI/CD pipeline for a React app that includes testing, building, scanning, and deploying. We started by creating a basic React app and setting up a GitHub repository to store the code. Then, we configured an Azure Pipeline that automatically builds, tests, and deploys the app to a Kubernetes cluster running on Azure Kubernetes Service (AKS). We used Docker to containerize the app and stored the container image in an Azure Container Registry (ACR). We also integrated the Trivy vulnerability scanner to scan the Docker image for security issues before deploying it to the cluster.

The pipeline consists of several stages, each with its own set of jobs and tasks. In the first stage, the app is tested using Jest and Enzyme, ensuring that it functions correctly before moving on to the next stages. In the second stage, the app is built and a Docker image is created. In the third stage, the Docker image is scanned for vulnerabilities using Trivy, and then pushed to the ACR. In the final stage, the app is deployed to the AKS cluster using Kubernetes.

Throughout this project, we have demonstrated how a CI/CD pipeline can help automate the software development process, reducing the chance of errors and increasing the speed of delivery. With the tools and techniques used here, we have also shown how security can be integrated into the pipeline, ensuring that the app is safe to use and deploy in a production environment.

In summary, this project provides a solid foundation for building and deploying modern web applications using continuous integration and deployment techniques.

Source code: [(https://github.com/rifkhan107/crypto-currency-sample-website.git)]