Resolving Deployment Issues with Ts-node and Azure Development Pipelines

Written by nuralem | Published 2023/05/25
Tech Story Tags: ts-node | azure | nodejs | typescript | bugs | bug-fixing | web-app-development | web-app

TLDRIn 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. We'll take a practical, step-by-step approach, investigating these issues, delving into their root causes, and outlining strategies for fixing them.via the TL;DR App

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.

Problem

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 compilerwith 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.

Tiny ts-node project

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 addedpathsin 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: [email protected]
      inputs:
        versionSpec: '18.x'
      displayName: 'Install Node.js'

    - script: |
        npm install yarn
        yarn
      displayName: 'npm install, build and test'

    - task: [email protected]
      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: [email protected]
            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: [email protected]
      inputs:
        versionSpec: '18.x'
      displayName: 'Install Node.js'

    - script: |
        npm install yarn
        yarn
        yarn build
      displayName: 'npm install, build and test'

    - task: [email protected]
      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: [email protected]
            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!

Conclusion

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!


Written by nuralem | Software engineer
Published by HackerNoon on 2023/05/25