Create Express.js virtual machine using Azure CLI

In this tutorial, create a Linux virtual machine (VM) for an Express.js app. The VM is configured with a cloud-init configuration file and includes NGINX and a GitHub repository for an Express.js app. Connect to the VM with SSH, change the web app to including trace logging, and view the public Express.js server app in a web browser.

This tutorial includes the following tasks:

  • Sign in to Azure with Azure CLI
  • Create Azure Linux VM resource with Azure CLI
    • Open public port 80
    • Install demo Express.js web app from a GitHub repository
    • Install web app dependencies
    • Start web app
  • Create Azure Monitoring resource with Azure CLI
    • Connect to VM with SSH
    • Install Azure SDK client library with npm
    • Add Application Insights client library code to create custom tracing
  • View web app from browser
    • Request /trace route to generate custom tracing in Application Insights log
    • View count of traces collected in log with Azure CLI
    • View list of traces with Azure portal
  • Remove resources with Azure CLI

Create or use an existing Azure subscription

You'll need an Azure user account with an active subscription. Create one for free.

Prerequisites

  • SSH to connect to the VM: Use Azure Cloud Shell or a modern terminal such as bash shell, which includes SSH.

1. Create Application Insights resource for web pages

Create an Azure resource group for all your Azure resources and a Monitor resource to collect your web app's log files to the Azure cloud. Creating a resource group allows you to easily find the resources, and delete them when you're done. Azure Monitor is the name of the Azure service, while Application Insights is the name of the client library the tutorial uses.

  1. Optional, if you've more than one subscription, use az account set to set the default subscription before completing the remaining commands.

    az account set \
        --subscription "ACCOUNT NAME OR ID" 
    
  2. Create an Azure resource group with az group create. Use the name rg-demo-vm-eastus:

    az group create \
        --location eastus \
        --name rg-demo-vm-eastus 
    

Create Azure Monitor resource with Azure CLI

  1. Install Application Insights extension to the Azure CLI.

    az extension add -n application-insights
    
  2. Use the following command to create a monitoring resource, with az monitor app-insights component create:

    az monitor app-insights component create \
      --app demoWebAppMonitor \
      --location eastus \
      --resource-group rg-demo-vm-eastus \
      --query instrumentationKey --output table
    
  3. Copy the Result from the output, you'll need that value as your instrumentationKey later.

  4. Leave the terminal open, you'll use it in the next step.

2. Create Linux virtual machine using Azure CLI

Uses a cloud-init configuration file to create both the NGINX reverse proxy server and the Express.js server. NGINX is used to forward the Express.js port (3000) to the public port (80).

  1. Create a local file named cloud-init-github.txt and save the following contents to the file or you can save the repository's file to your local computer. The cloud-init formatted file needs to exist in the same folder as the terminal path for your Azure CLI commands.

    #cloud-config
    package_upgrade: true
    packages:
      - nginx
    write_files:
      - owner: www-data:www-data
        path: /etc/nginx/sites-available/default
        content: |
          server {
            listen 80 default_server;
            server_name _;
            location / {
              # First, try if the file exists locally, otherwise request it from the app
              try_files $uri @app;
            }
            location @app {
              proxy_pass http://localhost:3000;
              proxy_http_version 1.1;
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection 'upgrade';
              proxy_set_header X-Forwarded-For $remote_addr;
              proxy_set_header Host $host;
              proxy_cache_bypass $http_upgrade;
            }
          }
    runcmd:
      # install Node.js
      - 'curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -'
      - 'sudo apt-get install -y nodejs'
      # clone GitHub Repo into myapp directory
      - 'cd /home/azureuser'
      - git clone "https://github.com/Azure-Samples/js-e2e-vm" myapp
      # Start app
      - 'cd myapp && npm install && npm start'
      # restart NGINX
      - systemctl restart nginx
    
  2. Review the runcmd section of file to understand what it does.

    The runcmd has several tasks:

    • Download Node.js, and install it
    • Clone the sample Express.js repository from GitHub into myapp directory
    • Install the application dependencies
    • Start the Express.js app with PM2

Create a virtual machine resource

  1. Enter the Azure CLI command, az vm create, at a terminal to create an Azure resource of a Linux virtual machine. The command creates the VM from the cloud-init file and generates the SSH keys for you. The running command displays where the keys are stored.

    az vm create \
      --resource-group rg-demo-vm-eastus \
      --name demo-vm \
      --location eastus \
      --public-ip-sku Standard \
      --image UbuntuLTS \
      --admin-username azureuser \
      --generate-ssh-keys \
      --custom-data cloud-init-github.txt
    
  2. Wait while the process may take a few minutes.

  3. Keep the publicIpAddress value from the response, it's needed to view the web app in a browser and to connect to the VM. If you lose this IP, use the Azure CLI command, az vm list-ip-addresses to get it again.

  4. The process created SSH keys and but them in a location stated in the response.

  5. Go to that location and create the authorized_keys file:

    cd <SSH-KEY-LOCATION> && cat id_rsa >> authorized_keys
    

Open port for virtual machine

When first created, the virtual machine has no open ports. Open port 80 with the following Azure CLI command, az vm open-port so the web app is publicly available:

az vm open-port \
  --port 80 \
  --resource-group rg-demo-vm-eastus \
  --name demo-vm

Browse to web site

  1. Use the public IP address in a web browser to make sure the virtual machine is available and running. Change the URL to use the value from publicIpAddress.

    http://YOUR-VM-PUBLIC-IP-ADDRESS
    
  2. If the resource fails with a gateway error, try again in a minute, the web app may take a minute to start.

  3. The virtual machine's web app returns the following information:

    • VM name
    • Your client IP
    • Current Date/Time

    Screenshot of web browser showing simple app served from Linus virtual machine on Azure.

  4. The initial code file for the web app has a single route, which passed through the NGINX proxy.

    const os = require('os');
    const express = require('express')
    const app = express()
    
    app.use('/public', express.static('public'))
    app.get('/', function (req, res) {
    
        const clientIP = req.headers['x-forwarded-for'];
        const msg = `HostName: ${os.hostname()}<br>ClientIP: ${clientIP}<br>DateTime: ${new Date()}<br><img width='200' height='200' src='/public/leaves.jpg' alt='flowers'>`
        console.log(msg)
    
        res.send(msg)
    })
    app.listen(3000, function () {
        console.log(`Hello world app listening on port 3000! ${Date.now()}`)
    })
    

3. Connect to Linux virtual machine using SSH

In this section of the tutorial, use SSH in a terminal to connect to your virtual machine. SSH is a common tool provided with many modern shells, including the Azure Cloud Shell.

Connect with SSH and change web app

  1. Connect to your remote virtual machine with the following command.

    Replace YOUR-VM-PUBLIC-IP with your own virtual machine's public IP.

    ssh azureuser@YOUR-VM-PUBLIC-IP
    

    This process assumes that your SSH client can find your SSH keys, created as part of your VM creation and placed on your local machine.

  2. If you're asked if you're sure you want to connect, answer y or yes to continue.

  3. Use the following command to understand where you are on the virtual machine. You should be at the azureuser root: /home/azureuser.

    pwd
    
  4. When the connection is complete, the terminal prompt should change to indicate the username and resource name of remote virtual machine.

    azureuser@demo-vm:
    
  5. Your web app is in the subdirectory, myapp. Change to the myapp directory and list the contents:

    cd myapp && ls -l
    
  6. You should see contents representing the GitHub repository cloned into the virtual machine and the npm package files:

    -rw-r--r--   1 root root   891 Nov 11 20:23 cloud-init-github.txt
    -rw-r--r--   1 root root  1347 Nov 11 20:23 index-logging.js
    -rw-r--r--   1 root root   282 Nov 11 20:23 index.js
    drwxr-xr-x 190 root root  4096 Nov 11 20:23 node_modules
    -rw-r--r--   1 root root 84115 Nov 11 20:23 package-lock.json
    -rw-r--r--   1 root root   329 Nov 11 20:23 package.json
    -rw-r--r--   1 root root   697 Nov 11 20:23 readme.md
    

Install Monitoring SDK

  1. In the SSH terminal, which is connected to your virtual machine, install the Azure SDK client library for Application Insights.

    sudo npm install --save applicationinsights
    
  2. Wait until the command completes before continuing.

Add Monitoring instrumentation key

  1. In the SSH terminal, which is connected to your virtual machine, use the Nano editor to open the package.json file.

    sudo nano package.json
    
  2. Add a APPINSIGHTS_INSTRUMENTATIONKEY environment variable to the beginning of your Start script. In the following example, replace REPLACE-WITH-YOUR-KEY with your instrumentation key value.

    "start": "APPINSIGHTS_INSTRUMENTATIONKEY=REPLACE-WITH-YOUR-KEY pm2 start index.js --watch --log /var/log/pm2.log"
    
  3. Still in the SSH terminal, save the file in the Nano editor with control + X.

  4. If prompted in the Nano editor, enter Y to save.

  5. If prompted in the Nano editor, accept the file name when prompted.

Stop VM to change application

The Azure client library is now in your node_modules directory and the key is passed into the app as an environment variable. The next step programmatically uses Application Insights.

  1. Stop PM2, which is a production process manager for Node.js applications, with the following commands:

    sudo npm run-script stop 
    
  2. Replace original index.js with file using Application Insights.

    sudo npm run-script appinsights
    
  3. The client library and logging code is provided for you.

    const express = require('express')
    const app = express()
    const os = require('os');
    
    console.log(JSON.stringify(process.env));
    
    const AppInsights = require('applicationinsights');
    
    if (process.env.APPINSIGHTS_INSTRUMENTATIONKEY) {
        console.log(`AppInsights configured with key ${process.env.APPINSIGHTS_INSTRUMENTATIONKEY}`);
    } else{
        console.log(`AppInsights not configured`);
    }
    
    AppInsights.setup(process.env.APPINSIGHTS_INSTRUMENTATIONKEY)
        .setAutoDependencyCorrelation(true)
        .setAutoCollectRequests(true)
        .setAutoCollectPerformance(true, true)
        .setAutoCollectExceptions(true)
        .setAutoCollectDependencies(true)
        .setAutoCollectConsole(true)
        .setUseDiskRetryCaching(true)
        .setSendLiveMetrics(false)
        .setDistributedTracingMode(AppInsights.DistributedTracingModes.AI)
        .start();
    
    const AppInsightsClient = AppInsights.defaultClient;
    
    
    app.get('/trace', (req, res) => {
    
        const clientIP = req.headers['x-forwarded-for'];
        const msg = `trace route ${os.hostname()} ${clientIP} ${new Date()}`;
    
        console.log(msg)
    
        if (process.env.APPINSIGHTS_INSTRUMENTATIONKEY) {
            AppInsightsClient.trackPageView();
            AppInsightsClient.trackTrace({ message: msg })
            AppInsightsClient.flush();
        } else {
            msg += ' AppInsights not configured';
        }
    
        res.send(`${msg}`)
    })
    
    app.get('/', function (req, res) {
    
        const clientIP = req.headers['x-forwarded-for'];
        const msg = `root route ${os.hostname()} ${clientIP} ${new Date()}`
    
        console.log(msg)
    
        res.send(msg)
    
    })
    app.listen(3000, function () {
        console.log(`Hello world app listening on port 3000! ${os.hostname()}`)
    })
    
  4. Restart the app with PM2 to pick up the next environment variable.

    sudo npm start
    

Use app to verify logging

  1. In a web browser, test the app with the new trace route:

    http://YOUR-VM-PUBLIC-IP-ADDRESS/trace
    

    The browser displays the response, trace route demo-vm YOUR-CLIENT-IP VM-DATE-TIME with your IP address.

Viewing the log for NGINX

The virtual machine (VM) collects logs for NGINX, which are available to view.

Service Log location
NGINX /var/log/nginx/access.log
  1. Still in the SSH terminal, view VM log for the NGINX proxy service with the following command to view the log:
cat /var/log/nginx/access.log
  1. The log includes the call from your local computer.
"GET /trace HTTP/1.1" 200 10 "-"

Viewing the log for PM2

The virtual machine collects logs for PM2, which are available to view.

Service Log location
PM2 /var/log/pm2.log
  1. View VM log for the PM2 service, which is your Express.js Node web app. In the same bash shell, use the following command to view the log:

    cat /var/log/pm2.log
    
  2. The log includes the call from your local computer.

    grep "Hello world app listening on port 3000!" /var/log/pm2.log
    
  3. The log also includes your environment variables, including your ApplicationInsights key, passed in the npm start script. use the following grep command to verify your key is in the environment variables.

    grep APPINSIGHTS_INSTRUMENTATIONKEY /var/log/pm2.log
    

    This displays your PM2 log with APPINSIGHTS_INSTRUMENTATIONKEY highlighted in a different color.

VM logging and cloud logging

In this application, using console.log writes the messages into the PM2 logs found on the VM only. If you delete the logs or the VM, you lose that information.

If you want to retain the logs beyond the lifespan of your virtual machine, use Application Insights.

5. Clean up resources

Once you've completed this tutorial, you need to remove the resource group, which includes all its resources to make sure you aren't billed for any more usage.

In the same terminal, use the Azure CLI command, az group delete, to delete the resource group:

az group delete --name rg-demo-vm-eastus -y

This command takes a few minutes.

Troubleshooting

If you have issues, use the following table to understand how to resolve your issue:

Problem Resolution
502 Gateway error This could indicate your index.js or package.js file has an error. View your PM2 logs at /var/log/pm2.log for more information. The most recent error is at the bottom of the file. If you're sure those files are correct, stop and start the PM2 using the npm scripts in package.json.

Sample code

Next steps