Virtual machines (VM) offer great flexibility for hosting web applications. As a developer or engineer, you can configure and control every piece of software and every setting that the application needs to run. Azure, one of the largest cloud hosting platforms, has virtual machine offerings for both Linux and Windows-based operating systems.

By implementing continuous integration and continuous deployment (CI/CD), you can fully leverage the scalability and agility of cloud-based virtual machines, automatically updating your applications in response to changes in the codebase without manual intervention. In this tutorial, you will learn how to set up a continuous deployment pipeline to deploy a Node.js application to an Azure virtual machine.

Prerequisites

To follow this post, a few things are required:

  1. Node.js installed on your system (version >= 10.3)
  2. An Azure account
  3. A CircleCI account
  4. A GitHub account
  5. Azure CLI installed on your system

With all these installed and set up, you can begin the tutorial.

Cloning the Node.js project

First you need to clone the project that you will be deploying to the Azure VM. This project is a basic Node.js API with a single endpoint for returning an array of todo objects. Go to the location where you want to store the project and clone it:

git clone --single-branch --branch base-project https://github.com/CIRCLECI-GWP/cd-node-azure-vm.git

Once the project has been cloned, go to the root of the project and install dependencies:

cd cd-node-azure-vm
npm install

Run the application using the npm run dev command. The application will start up at the address http://localhost:3000. When the application is up and running, enter http://localhost:3000/todos in your browser to review the list of todos.

Todos endpoint - Node app

Now, go to the package.json file of your project and add these scripts in the scripts sections:

"scripts": {
    .....,
    "stop": "pm2 kill",
    "start": "pm2 start server.js"
}

The start and stop scripts will use the pm2 process manager to start and stop the Node.js application on the VM. The pm2 script will be installed globally on the VM when it has been set up.

At the root of the project, run the rm -rf .git command to remove any .git history. Then push the project to GitHub. Make sure that this is the GitHub account connected to your CircleCI account.

Setting up a virtual machine on Azure to run Node.js

Next, create a new VM on Azure and set its environment up for hosting the Node.js application. These are the steps:

  1. Create a new VM instance.
  2. Install nginx.
  3. Configure nginx to act as a proxy server. Route all traffic to port 80 on your VM to the running instance of the Node.js application on port 3000.
  4. Install Node.js on the VM and clone the app from the GitHub repo into a folder in the VM.
  5. Install pm2 globally.

Do not be intimidated by the complexity of these steps! You can complete all five with one command. At the root of your project, create a new file named cloud-init-github.txt; it is a cloud-init file. Cloud-init is an industry-standard method for cloud instance initialization.

In the cloud-init file, enter:

#cloud-config
package_upgrade: true
packages:
  - nginx
write_files:
  - owner: www-data:www-data
    path: /etc/nginx/sites-available/default
    content: |
      server {
        listen 80;
        location / {
          proxy_pass http://localhost:3000;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection keep-alive;
          proxy_set_header X-Forwarded-For $remote_addr;
          proxy_set_header Host $host;
          proxy_cache_bypass $http_upgrade;
        }
      }
runcmd:
  # install Node.js
  - 'curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -'
  - 'sudo apt-get install -y nodejs'
  # clone GitHub Repo into myapp directory
  - 'cd /home/azureuser'
  - git clone "https://github.com/CIRCLECI-GWP/nodejs-azure-vm" myapp
  # Install pm2
  - 'sudo npm install pm2 -g'
  # restart NGINX
  - service nginx restart

All five of the steps I listed earlier are included in this file. Make sure you replace the sample GitHub repo with the name of your repository in the git clone command. Note that you are cloning the project in a myapp folder inside the home directory: /home/azureuser.

Next, use the configuration in the file above to create a new VM instance on Azure. Make sure that you are logged in to Azure on your CLI (run az login to log in):

First, you need a resource group. Run the following command to create one:

az group create --name Demos-Group --location eastus

Note: You don’t have to create a new resource group for this. You can use an existing one if you prefer.

Next, run this command to create the VM instance:

az vm create --resource-group Demos-Group --name node-vm --image Ubuntu2204 --admin-username azureuser --custom-data cloud-init-github.txt --generate-ssh-keys

Note the value of each parameter set in the command:

  • Demos-Group is the Azure resource group you are creating the VM in.
  • node-vm is the name of your VM.
  • eastus is the region you are creating the VM in.
  • Ubuntu2204 is the VM operating system.
  • azureuser is the admin user for the VM; it will be used to connect via SSH into the VM.
  • cloud-init-github.txt is the cloud VM configuration file you just wrote.

When the command is done running, a response object is printed to the screen. Make sure you save the privateIpAddress property of the object. privateIpAddress is the IP address you will use to access your application in the browser and via ssh.

Go to your resources page in the Azure portal. Click Virtual Machines to review your VM instance.

VM - Azure

Web port 80 is not open by default on the VM. You need to open this port explicitly to allow web requests to hit the server. To open port 80, run:

az vm open-port --port 80 --resource-group Demos-Group --name node-vm

Note that the VM name and resource group are passed in this command. When the command is done, a big chunk of JSON is printed on your CLI. We are not using it in the tutorial, so feel free to ignore it.

Generating SSH keys on the server

Your next step is to generate ssh keys on the server to give the continuous deployment pipeline script access to the VM.

SSH into your server, making sure to replace YOUR-PUBLIC-IP-ADDRESS with what was returned when you set up the VM:

ssh azureuser@YOUR-PUBLIC-IP-ADDRESS

This command gets you into the home directory. Go to the .ssh folder by running cd .ssh. Generate the ssh keys:

ssh-keygen

Press Enter to accept the default location with id_rsa as the file name. CircleCI requires an empty passphrase for access, so press Enter twice in response to the passphrase and confirm passphrase prompts.

Next, append the contents of the public key to the authorized_keys file by running:

cat id_rsa.pub >> authorized_keys

Then, print out the contents of the private key:

cat id_rsa

Copy this information and save it in a safe location. You will be adding it to CircleCI later on.

Assigning permissions to the VM user

When the project folder was cloned during the set-up process, the root user created the myapp folder. That means that your VM’s admin user, azureuser, cannot update the contents of the myapp folder. You need to give your VM’s admin user the permissions to update the myapp folder.

To begin, make sure that you have moved out of the .ssh folder back to the home directory containing myapp. Change azureuser to have root as its default group using the command:

sudo usermod -g root azureuser

Next, you need to change the owner and group of all of the files in myapp to azureuser and root. Use the command:

sudo chown -R azureuser:root myapp

Now, if you run ls -l myapp, you will see the output indicating that azureuser has ownership of myapp and its content.

User permissions - VM

You can now set the privileges on all folders and files to read/write/execute for owner and group and to nothing for public. Enter:

sudo chmod -R 770 myapp

Configure the deployment pipeline

At the root of your project, create a folder and name it .circleci. In that folder, create a file named config.yml. Inside config.yml, enter this code:

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: cimg/node:18.10.0
    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - $AZURE_VM_SSH_FINGERPRINT
      - run:
          name: Copy updated files to VM
          command: scp -o StrictHostKeyChecking=no -r ./* $AZURE_VM_USER@$AZURE_VM_IP:~/myapp

  deploy:
    machine:
      enabled: true
    steps:
      - run:
          name: Deploy Over SSH
          command: |
            ssh $AZURE_VM_USER@$AZURE_VM_IP "cd myapp && sudo npm run-script stop && sudo npm install && sudo npm start"

workflows:
  version: 2
  build:
    jobs:
      - build:
        filters:
          branches:
            only: main
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: main

Your new config.yml file contains two jobs.

The build job checks out the code and uses the SSH key to securely copy the updated files from the application to the Azure VM myapp folder, using the scp command. The StrictHostKeyChecking=no part of the command suppresses the prompt asking for a confirmation check for the host key. This keeps the prompt from blocking the automated process.

The deploy job then deploys the application over SSH. It goes into the myapp folder, stops the app, installs dependencies, and restarts the application.

The config.yml file contains a workflow definition that makes sure the build job completes successfully before deploy is run. The workflow also makes sure that deployments take place only when code is pushed to the main branch. That prevents deploying the application when team members are pushing to feature branches.

The next step is to update the repository with the new update. Review Pushing a project to GitHub for instructions.

Add the project to CircleCI

To begin, make sure that you have pushed the latest updates to your project to GitHub. Next, log in to your CircleCI account. If you signed up with your GitHub account, all your repositories will be available on your project’s dashboard.

Locate your nodejs-azure-vm project and click Set Up Project.

Add Project - CircleCI

Enter main as the name of the GitHub branch containing your CircleCI configuration when prompted, then click Set Up Project.

Add Config - CircleCI

CircleCI will then start the pipeline, which will run the tests but fail to deploy. This build failed because you have not yet set up the configuration file with the variables for your virtual machine on Azure.

Build failed

Create pipeline configuration environment variables

To fix this, start by adding the SSH key you saved earlier (the private key you copied to a secure location). Go to the Project Settings page of your project. From the side menu, click SSH Keys . Scroll down to the Additional SSH Keys section and click Add SSH Key. When prompted, enter your public IP in the Hostname field and your private key in the Private Key field. Click Add SSH Key to save the information.

Add SSH Key - CircleCI

After adding the key, it will be shown in a Hostname/Fingerprint pair on a table in the Additional SSH Keys section. Copy this fingerprint. It will be required in the pipeline configuration file.

Because the VM user and public IP will be used in the pipeline configuration, it is good practice to make them environment variables. From the side menu, click Environment Variables and enter the following information:

  • For AZURE_VM_SSH_FINGERPRINT, enter the fingerprint that was generated after you added the SSH key.
  • For AZURE_VM_USER enter the VM admin, azureuser.
  • In AZURE_VM_IP enter your VM public IP address.

Go back to the dashboard. Click Rerun Workflow from Failed. This will trigger the workflow, which should build successfully this time.

Build successful - CircleCI

Click the build job to review the details.

Build details - CircleCI

Next, click the deploy job to view its details.

Deploy details - CircleCI

So the workflow is running smoothly. That is great, but a more convincing test of the process is to open the deployed application in the browser. In your browser, load the endpoint http://[YOUR_PUBLIC_IP]/todos. Make sure you replace the placeholder YOUR_PUBLIC_IP with the correct information.

Todos Live - Browser

Now, that is convincing.

Next, add some more todo objects to your todos.js file:

module.exports = [
  ...,
  {
    id: 4,
    task: "Make Dinner"
  },
  {
    id: 5,
    task: "Take out the trash"
  }
];

Commit and push your updates to your repository to run the pipeline again. When the workflow is complete, reload your browser to check on your changes.

Todos updated - browser

Conclusion

Virtual machines come with great power and flexibility, and Azure provides one of the most reliable VM offerings in the market. Automating the deployment of your applications to an Azure virtual machine, as demonstrated in this tutorial, combines the reliability of Azure with the ease-of-use of CircleCI. Your team will benefit from having reliable web applications that are easier to deploy.

Happy coding!


Fikayo Adepoju is a LinkedIn Learning (Lynda.com) Author, Full-stack developer, technical writer, and tech content creator proficient in Web and Mobile technologies and DevOps with over 10 years experience developing scalable distributed applications. With over 40 articles written for CircleCI, Twilio, Auth0, and The New Stack blogs, and also on his personal Medium page, he loves to share his knowledge to as many developers as would benefit from it. You can also check out his video courses on Udemy.

Read more posts by Fikayo Adepoju