In this article, we'll explore common pitfalls and potential solutions when working with TypeScript (using ts-node) in Azure Serverless Development Pipelines. This piece is particularly valuable for developers encountering problems during the deployment process, often characterized by obscure error messages and puzzling behavior. We'll take a practical, step-by-step approach, investigating these issues, delving into their root causes, and outlining strategies for fixing them.
Whether you're a seasoned developer, DevOps engineer, or a curious beginner eager to learn more about TypeScript and Azure DevOps, this comprehensive guide will serve as a valuable tool in navigating the intricacies of ts-node deployments in Azure pipelines.
Working on Restart and building our backend system on different cloud solutions (GCP, Azure) our team has faced several issues on serverless deployment.
Using ts-node as a runtime in-memory ts-js compiler
with the latest TypeScript 5.0.4 and nodejs18, we got errors after successfully building our app using dev.azure.com pipelines (Image 1).
The errors during app startup:
2023-05-25T10:52:24.326355260Z _____
2023-05-25T10:52:24.326400861Z / _ \ __________ _________ ____
2023-05-25T10:52:24.326406761Z / /_\ \\___ / | \_ __ \_/ __ \
2023-05-25T10:52:24.326410361Z / | \/ /| | /| | \/\ ___/
2023-05-25T10:52:24.326413761Z \____|__ /_____ \____/ |__| \___ >
2023-05-25T10:52:24.326417661Z \/ \/ \/
2023-05-25T10:52:24.326420961Z A P P S E R V I C E O N L I N U X
2023-05-25T10:52:24.326424361Z
2023-05-25T10:52:24.326427461Z Documentation: http://aka.ms/webapp-linux
2023-05-25T10:52:24.326430661Z NodeJS quickstart: https://aka.ms/node-qs
2023-05-25T10:52:24.326433761Z NodeJS Version : v18.16.0
2023-05-25T10:52:24.326436961Z Note: Any data outside '/home' is not persisted
2023-05-25T10:52:24.326440161Z
2023-05-25T10:52:26.504451955Z Starting OpenBSD Secure Shell server: sshd.
2023-05-25T10:52:26.807519399Z Starting periodic command scheduler: cron.
2023-05-25T10:52:26.863716274Z Cound not find build manifest file at '/home/site/wwwroot/oryx-manifest.toml'
2023-05-25T10:52:26.866811884Z Could not find operation ID in manifest. Generating an operation id...
2023-05-25T10:52:26.868247288Z Build Operation ID: 796ee1ba-542e-43f1-9f6c-1e7a5e2e9815
2023-05-25T10:52:27.139574733Z Environment Variables for Application Insight's IPA Codeless Configuration exists..
2023-05-25T10:52:27.156085084Z Writing output script to '/opt/startup/startup.sh'
2023-05-25T10:52:27.229878114Z Running #!/bin/sh
2023-05-25T10:52:27.229938514Z
2023-05-25T10:52:27.229945514Z # Enter the source directory to make sure the script runs where the user expects
2023-05-25T10:52:27.229950214Z cd "/home/site/wwwroot"
2023-05-25T10:52:27.229954114Z
2023-05-25T10:52:27.229957814Z export NODE_PATH=/usr/local/lib/node_modules:$NODE_PATH
2023-05-25T10:52:27.229979514Z if [ -z "$PORT" ]; then
2023-05-25T10:52:27.230057715Z export PORT=8080
2023-05-25T10:52:27.230063815Z fi
2023-05-25T10:52:27.230067915Z
2023-05-25T10:52:27.246143265Z PATH="$PATH:/home/site/wwwroot" yarn start
2023-05-25T10:52:30.491250324Z yarn run v1.17.3
2023-05-25T10:52:30.896237571Z $ ts-node src/app.ts
2023-05-25T10:52:31.521499168Z node:internal/modules/cjs/loader:1078
2023-05-25T10:52:31.577313557Z throw err;
2023-05-25T10:52:31.577384758Z ^
2023-05-25T10:52:31.577391958Z
2023-05-25T10:52:31.577396558Z Error: Cannot find module './util'
2023-05-25T10:52:31.577400858Z Require stack:
2023-05-25T10:52:31.577404958Z - /home/site/wwwroot/node_modules/.bin/ts-node
2023-05-25T10:52:31.577484858Z at Module._resolveFilename (node:internal/modules/cjs/loader:1075:15)
2023-05-25T10:52:31.577494058Z at Module._load (node:internal/modules/cjs/loader:920:27)
2023-05-25T10:52:31.577498458Z at Module.require (node:internal/modules/cjs/loader:1141:19)
2023-05-25T10:52:31.577502658Z at require (node:internal/modules/cjs/helpers:110:18)
2023-05-25T10:52:31.577506858Z at Object.<anonymous> (/home/site/wwwroot/node_modules/.bin/ts-node:9:16)
2023-05-25T10:52:31.577511558Z at Module._compile (node:internal/modules/cjs/loader:1254:14)
2023-05-25T10:52:31.577551858Z at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
2023-05-25T10:52:31.577557158Z at Module.load (node:internal/modules/cjs/loader:1117:32)
2023-05-25T10:52:31.577561358Z at Module._load (node:internal/modules/cjs/loader:958:12)
2023-05-25T10:52:31.577565458Z at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) {
2023-05-25T10:52:31.577569658Z code: 'MODULE_NOT_FOUND',
2023-05-25T10:52:31.577573658Z requireStack: [ '/home/site/wwwroot/node_modules/.bin/ts-node' ]
2023-05-25T10:52:31.577577758Z }
2023-05-25T10:52:31.577586558Z
2023-05-25T10:52:31.577591058Z Node.js v18.16.0
2023-05-25T10:52:31.684203528Z error Command failed with exit code 1.
The key points of these logs are:
Error: Cannot find module './util'
/home/site/wwwroot/node_modules/.bin/ts-node
requireStack: [ '/home/site/wwwroot/node_modules/.bin/ts-node' ]
error Command failed with exit code 1.
Spending hours of googling and searching for similar problems on Stack Overflow, we cannot
define the nature of the issue. Trying different pipeline setups, and switching from Linux based web app to Windows does not help. The same code works fine in the case of serverless Google Cloud, but for some reason does not work on serverless Azure.
Finally, we got a solution: We switch “ts-node”
to a basic “tsc“
compiler and everything worked fine.
In this article, I will explain some details of switching your existing project, that uses ts-node
to a native tsc
compiler.
First, we need a basic project that has only one file and ts-node on it (Image 2).
The src
folder contains our app.ts file:
import * as http from 'http';
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello Hackernoon. We are using ts on Azure!\n');
});
server.listen(8080, '0.0.0.0', () => {
console.log('Server running at http://0.0.0.0:8080/');
});
In .gitignore
we have added dist
folder and node_modules
because compiling from ts
to js
will be inside the Azure pipeline process.
The content of package.json:
{
"name": "ts-nodejs",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "ts-node src/app.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"devDependencies": {
"@types/node": "^20.2.3"
}
}
We have installed only ts-node and typescript packages.
First, we need to modify the start
command and create a new command and add it here:
{
"name": "tsc-nodejs",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "node dist/app.js",
"build": "tsc"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"devDependencies": {
"@types/node": "^20.2.3"
}
}
All project pre-compiled files will be stored in the dist
folder, which nodejs
will execute our entry point of the app (app.js). Build
the command executes or tsc
compiler. It gets settings from tsconfig.json
file.
Our settings will be:
{
"compilerOptions": {
"target": "ES2019",
"module": "commonjs",
"esModuleInterop": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": false,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"removeComments": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"baseUrl": "./src",
"rootDir": "src",
"paths": {
"@extensions/*": ["handlers/extensions/*"],
"@services/*": ["services/*"],
"@docs/*": ["docs/*"],
"@server/*": ["server/*"],
"@handlers/*": ["handlers/*"],
"@app/*": ["*"],
},
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"typeRoots": [
"./types",
"./node_modules/@types"
],
"allowSyntheticDefaultImports": true,
"lib": ["es5", "es6", "dom"],
"moduleResolution": "node",
},
"include": [
"**/*"
],
"exclude": [
"dist",
"node_modules"
]
}
The most important parts of the tsconfig.json:
"noEmit": false,
"include": [
"**/*"
],
"outDir": "dist",
"baseUrl": "./src",
"rootDir": "src",
Be sure, that noEmit
is false (by default is false), outDir
is the name of the folder, where tsc
will save precompiled files, baseUrl
and rootDir
is src
, where your project is (don’t save project files outside src
folder, because tsc
will ignore them).
In this example, I also have addedpaths
in case you have it on an existing project, but this tiny project does not use its allies, so you can ignore them. If you use allies on your project, please change them inside package.json
from:
"_moduleAliases": {
"@extensions": "src/handlers/extensions",
"@services": "src/services",
"@docs": "src/docs",
"@server": "src/server",
"@handlers": "src/handlers",
"@app": "src"
}
to this:
"_moduleAliases": {
"@extensions": "dist/handlers/extensions",
"@services": "dist/services",
"@docs": "dist/docs",
"@server": "dist/server",
"@handlers": "dist/handlers",
"@app": "dist"
}
Finally, you need to change basic azure-pipelines.yaml
config from:
# Node.js Express Web App to Linux on Azure
# Build a Node.js Express app and deploy it to Azure as a Linux web app.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
- master
variables:
# Azure Resource Manager connection created during pipeline creation
azureSubscription: '<Your sub>'
# Web app name
webAppName: '<Your webAppName>'
# Environment name
environmentName: 'Your webAppName>'
# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
displayName: 'Install Node.js'
- script: |
npm install yarn
yarn
displayName: 'npm install, build and test'
- task: ArchiveFiles@2
displayName: 'Archive files'
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
includeRootFolder: false
archiveType: zip
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
replaceExistingArchive: true
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
artifact: drop
- stage: Deploy
displayName: Deploy stage
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Deploy
displayName: Deploy
environment: $(environmentName)
pool:
vmImage: $(vmImageName)
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Azure Web App Deploy: Your webAppName>'
inputs:
azureSubscription: $(azureSubscription)
appType: webAppLinux
appName: $(webAppName)
runtimeStack: 'NODE|18-lts'
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
startUpCommand: 'yarn start'
to this:
# Node.js Express Web App to Linux on Azure
# Build a Node.js Express app and deploy it to Azure as a Linux web app.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
- master
variables:
# Azure Resource Manager connection created during pipeline creation
azureSubscription: 'Your sub'
# Web app name
webAppName: 'Your webAppName>'
# Environment name
environmentName: 'Your webAppName>'
# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
displayName: 'Install Node.js'
- script: |
npm install yarn
yarn
yarn build
displayName: 'npm install, build and test'
- task: ArchiveFiles@2
displayName: 'Archive files'
inputs:
rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
includeRootFolder: false
archiveType: zip
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
replaceExistingArchive: true
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
artifact: drop
- stage: Deploy
displayName: Deploy stage
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Deploy
displayName: Deploy
environment: $(environmentName)
pool:
vmImage: $(vmImageName)
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Azure Web App Deploy: Your webAppName>'
inputs:
azureSubscription: $(azureSubscription)
appType: webAppLinux
appName: $(webAppName)
runtimeStack: 'NODE|18-lts'
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
startUpCommand: 'yarn start'
Finally, git push
and see what will happen!
Hope this article will save a lot of time during debugging process in case of using ts-node
with Azure Node.js Serverless Apps
. It's clear that the complexities of TypeScript deployments within Azure DevOps pipelines can present unique challenges. However, by taking a systematic approach and learning to understand the underpinnings of ts-node and Azure pipelines, we can effectively troubleshoot and resolve these obstacles.
In this article, we dove into the most common issues developers encounter, broke down the intricacies of these problems, and provided step-by-step solutions. Remember, it's not about avoiding problems altogether – it's about developing the ability to diagnose, troubleshoot, and solve them effectively when they inevitably arise.
Moving forward, take this knowledge and apply it to your DevOps processes. Not only will you increase your productivity, but you'll also be contributing to a smoother, more efficient pipeline for your entire team. There's always more to learn in this ever-evolving field, and this guide is just one step towards mastering Azure DevOps with TypeScript.
Stay curious, keep learning, and happy coding!