Today’s apps are usually deployed as Docker containers on remote machines and, as such, can be a bit challenging to develop and debug. In order to speed-up the development process, one of the must have features is the ability to conduct remote debugging.

This makes the development process much smoother and try/fail cycles are much faster. One of the most popular IDEs which offers that ability is VS Code. Let’s say you have an application written in Typescript Node.js, and you want to put it in a Docker container and deploy it on a Kubernetes cluster on AWS, but you also still want to debug that application and develop it as you go. Instead of having to log variables and errors into the container log, there’s a great option of connecting to the Kubernetes cluster via SSH and then remote debugging the containerized deployed application from there, setting up breakpoints before and during the execution. All of this is done in the comfort of your own local machine’s VS Code. So let’s start debugging!

Motivation (Why do we need remote debugging?)

In this day and age, code should be running well on all different kinds of platforms and environments. Yet, platforms have different characteristics and sometimes functionality is not easily achieved across different environments, and of course there’s that ultimate developers’ state of mind where we say: “Well, it works on my machine…”

Sometimes the code on the production server simply is not working the same as it works on our development environment and we need to know why. Troubleshooting a problem on a remote server is often a difficult task and one troubleshooting method is debugging the application code directly on the server. However, because it’s a bit challenging to achieve the functionality of remote debugging, especially when there’s Kubernetes and Docker involved in the story, I wrote this blog to make it easier for developers like myself that run into such problems and want to quickly get a hold of the configuration that will make their bug identification and fixing process a lot smoother.

Prerequisites

There are some prerequisites that are expected to be had or installed prior to setting up the debugging process. These are:

  1. Kubernetes cluster on AWS, on which the microservice will be deployed as a Kubernetes deployment.

  2. Visual Studio Code on your local machine or the Kubernetes cluster, with installed extension:
    –  Remote SSH – to connect to the Kubernetes cluster from the local machine.

  3. An application written in Typescript Node.js.

Setting up SSH connection to the Kubernetes cluster

Note: If you already have VS Code installed on your Kubernetes cluster and only want to debug the dockerized deployed app on the cluster, you can proceed to setting up the debug process (next headline).

The first step that should be done is to connect the Visual Studio Code installed on your local machine to the Kubernetes cluster which contains the code we want to put in a container, deploy as a Kubernetes deployment and then debug.

This can be achieved following a few steps, listed below.

  1. Remote SSH extension must be installed in VS Code. If you have just installed it, please reload your VS Code for the changes to be applied. The installation guide as well as a detailed explanation of the extension’s usage can be found here.

  2. After the extension has successfully been installed, type in Cmd + Shift + P in VS Code to open the configuration, then select the option: Remote SSH – Connect to Host.  You will be provided with the existing configurations (if there are any) and also the possibility to add a new SSH Host, as shown in the image below.
    If you are not given the option of Add New SSH Host… , choose Configure SSH Hosts to configure config files manually and skip to step 4.

  3. Select the option: Add New SSH Host and then type in ssh command to connect to the remote machine with Kubernetes cluster. An example of that ssh command, using a private key to authenticate to the machine, is:

    ssh -i ~/.ssh/private_key.pem username@ip_address_of_host

     

  4. After inputting the command, select a SSH configuration file to store this data into.

Select the first one (or any other one you prefer), and the new host information should be stored in that config file.

After you open that config file it should look like this:

Host ip_address_of_host 
   IdentityFile ~/.ssh/private_key.pem 
   User username_for_host_machine

You can now Connect to SSH Host by selecting that option when you enter Cmd+Shift+P again.

Now that you’re connected to the remote host through SSH, you can open an application folder on the machine from VS code to start setting up the debugging process for it.

Setting up the debug process

To set up the debug process, some config files in the application itself have to be changed. Those files include the package.json, tsconfig.json and launch.json files as well as the Dockerfile.

The application on which the configuration will be demonstrated on is written in Node.js Typescript.

Change the package.json file

The first thing to do is to add a new script in the app’s package.json file to support debugging.

The package.json file is a config file of javascript applications which lists the packages the project depends on with their versions, and also makes your build reproducible and therefore easier to share as an npm package. You can read more about this here.

For debugging purposes, we will setup the scripts property of this file.

The script will be called debug:

"debug": "node --inspect out/start.js"

–inspect option: activate inspector on host:port. Default is 127.0.0.1:9229.  If you want to change the port, it should be specified as: --inspect=0.0.0.0:PORT.

start.js script is the starting point of your app execution (main entrypoint file). Since our typescript transpiling folder will be ./out (this is set in tsconfig file), the path for the starting script is out/start.js

Change the tsconfig.json file

The tsconfig.json file specifies the root files and the compiler options required to compile the Typescript written project. In this case, the project is compiled by invoking tsc in Dockerfile with no input files, in which case the compiler searches for the tsconfig.json file starting in the current directory and continuing up the parent directory chain.

Important attributes for debugging are:

inlineSourceMap: must be set to true. Source maps are used to display your original JavaScript (.ts files) while debugging, which is a lot easier to look at than minified production code. There are two options for using source maps: inlineSourceMap and sourceMap. In case of remote debugging, inlineSourceMap should be used, because with this option, a single file with source maps is emitted instead of a separate corresponding .map file (with sourceMap option), which is easier for the VS Code debugger to find.

inlineSources: tsconfig compiler option to emit the source alongside the sourcemaps within a single file.

outDir: folder in which the transpiled js files are stored in.

{ 
   "compilerOptions": {
   "target": "es6",
   "module": "commonjs",
   "outDir": "./out", 
 "inlineSourceMap": true,
 "inlineSources": true 
}, 
"include": ["*.ts"], 
"exclude": ["node_modules"] 
}

Create a launch.json file

The next step is to create (or update) the launch.json file in .vscode folder of the application that will be used for the debug configuration of the app. More info about launch.json file can be found on the Visual Studio website.

Important attributes for our purposes that have to be set are:

port: port on which the debugger will attach to. This port should be the same as the one defined in package.json file (–inspect=0.0.0.0:PORT). Since by default the port is 9229 for debugging, that can be left unset in the package.json file, but should be clearly defined in this launch.json.

remoteRoot: directory in the container that contains the app source code, declared in Dockerfile. In this case, that is: /app folder.

localRoot: directory on the machine that contains application code, in this case the Kubernetes cluster, that contains the source code. That is the ${workspaceRoot}.

sourceMaps: this field marks the use of sourceMaps to map typescript files from local to remote, and should be set to true to be able to set breakpoints in .ts files in VS Code before and during the execution.

smartStep:  if set to true, the debugger will try to automatically step over code that doesn’t map to source files

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Attach",
      "port": 9229,
      "localRoot": "${workspaceRoot}",
      "remoteRoot": "/app",
      "sourceMaps": true,
      "smartStep": true,
      "internalConsoleOptions": "openOnSessionStart",
      "trace": "sm",
      "outDir": "${workspaceRoot}/out"
    }
  ]
}

Change the Dockerfile

To containerize the app, a Dockerfile must be written, and it’s important attributes for debugging are:

WORKDIR : to set the working dir which will contain the app’s source code in the container. This should be the same as the remoteRoot in the launch.json file.

ENTRYPOINT: entrypoint of execution must be set to the debug script defined in package.json file.

Application Dockerfile is shown in the code snippet below:

# Docker Parent Image with Node and Typescript
FROM node:10-alpine

# Create Directory for the Container
WORKDIR /app

COPY package.json .
# Copy the files we need to our new Directory
COPY . /app

#Install typescript
RUN npm install -g typescript

# Grab dependencies and transpile src directory to out folder defined in tsconfig
RUN npm install
RUN tsc

# Set debug mode, this env variable marks script name in package.json file
ARG MODE=debug
ENV MODE=$MODE

# Start the server in debug mode, configured in package.json
ENTRYPOINT ["npm","run", “${MODE}”]

Port forwarding

After setting up the application’s config files, and building and pushing the app’s Docker image to the Dockerhub, two steps are left to do on the (remote) Kubernetes cluster:

  1. Deploy the application in a Kubernetes deployment
  2. Expose the application pod’s port for debugging with command
    kubectl port-forward -n pod-namespace pod-name PORT*Instead of using this command to forward specific port for specific debug use, you can also create a Kubernetes service in which you can specify which port from the container of Kubernetes deployment to forward to which on the outside.

And voila! Your app is ready to be debugged. Just click on the little bug (?) icon in your VS code, attach the debugger with the launch.json file that you created and you can start debugging..

Conclusion

Having in mind that the number of dockerized applications which are run in the Kubernetes environment constantly rises, and TypeScript has also gained rapid traction ever since RxJS and Angular started adopting it, remote debugging of this kind is becoming a necessity more than an option with each day. I hope this blog helps you set up the environment for an easy and smoothless process, so you can focus on fixing bugs, creating great things and making your dockerized typescript Node.js app as perfect as it can be.

Leave a Reply