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
- Request
- Remove resources with Azure CLI
Prerequisites
- An Azure user account and subscription: create a free subscription.
- SSH to connect to the VM: Use Azure Cloud Shell or a modern terminal such as bash shell, which includes SSH.
Use the Bash environment in Azure Cloud Shell. For more information, see Quickstart for Bash in Azure Cloud Shell.
If you prefer to run CLI reference commands locally, install the Azure CLI. If you're running on Windows or macOS, consider running Azure CLI in a Docker container. For more information, see How to run the Azure CLI in a Docker container.
If you're using a local installation, sign in to the Azure CLI by using the az login command. To finish the authentication process, follow the steps displayed in your terminal. For other sign-in options, see Sign in with the Azure CLI.
When you're prompted, install the Azure CLI extension on first use. For more information about extensions, see Use extensions with the Azure CLI.
Run az version to find the version and dependent libraries that are installed. To upgrade to the latest version, run az upgrade.
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.
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"
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
Install Application Insights extension to the Azure CLI.
az extension add -n application-insights
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
Copy the Result from the output, you'll need that value as your
instrumentationKey
later.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).
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
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
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
Wait while the process may take a few minutes.
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.
The process created SSH keys and but them in a location stated in the response.
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
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
If the resource fails with a gateway error, try again in a minute, the web app may take a minute to start.
The virtual machine's web app returns the following information:
- VM name
- Your client IP
- Current Date/Time
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
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.
If you're asked if you're sure you want to connect, answer
y
oryes
to continue.Use the following command to understand where you are on the virtual machine. You should be at the azureuser root:
/home/azureuser
.pwd
When the connection is complete, the terminal prompt should change to indicate the username and resource name of remote virtual machine.
azureuser@demo-vm:
Your web app is in the subdirectory,
myapp
. Change to themyapp
directory and list the contents:cd myapp && ls -l
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
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
Wait until the command completes before continuing.
Add Monitoring instrumentation key
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
Add a
APPINSIGHTS_INSTRUMENTATIONKEY
environment variable to the beginning of your Start script. In the following example, replaceREPLACE-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"
Still in the SSH terminal, save the file in the Nano editor with control + X.
If prompted in the Nano editor, enter Y to save.
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.
Stop PM2, which is a production process manager for Node.js applications, with the following commands:
sudo npm run-script stop
Replace original
index.js
with file using Application Insights.sudo npm run-script appinsights
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()}`) })
Restart the app with PM2 to pick up the next environment variable.
sudo npm start
Use app to verify logging
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 |
- 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
- 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 |
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
The log includes the call from your local computer.
grep "Hello world app listening on port 3000!" /var/log/pm2.log
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 . |