Deploying NestJS to Google Kubernetes Engine (GKE)

Table of contents

Introduction

Kubernetes, an open-source platform designed by Google and now maintained by the Cloud Native Computing Foundation, revolutionizes the way we deploy, scale, and manage containerized applications across clusters of machines.

It automates various aspects of application deployment, scaling, and operations, enabling organizations to focus on their core product without worrying about the underlying infrastructure. NestJS, on the other hand, is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. By leveraging TypeScript and combining elements of Object-Oriented Programming, Functional Programming, and Functional Reactive Programming, NestJS provides an out-of-the-box application architecture that allows developers to create highly testable, scalable, loosely coupled, and easily maintainable applications.

Prerequisites

  • gcloud installed. More info here.
  • GKE cluster created. More info here. Make sure to authenticate kubectl against the cluster that has been created.
    gcloud container clusters get-credentials <clustername> --zone <zone> --project <gcp-project>
  • Setup Google Artifact Registry repository created. More info here.

Dockerize

Firstly you'd need a package your project into a docker container. As a starting point you can use the containerfile below. Place this file in the root of your project.

💡
Note this dockerfile has hardcoded PORT 3000.
ARG NODE_VERSION=18.12.1-alpine 

FROM node:$NODE_VERSION AS development

WORKDIR /usr/src/app

COPY package.json ./
COPY package-lock.json ./
COPY tsconfig.json tsconfig.json
COPY nest-cli.json nest-cli.json

RUN npm install

COPY . .

RUN npm run build

FROM node:alpine as production

ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}

WORKDIR /usr/src/app

COPY package.json ./
COPY package-lock.json ./

RUN npm install --prod

COPY --from=development /usr/src/app/dist ./dist

EXPOSE 3000

CMD ["node", "dist/main"]

Now open your terminal and cd into the directory and build this container by running.

💡
To be able to deploy your application to Google Kubernetes Engine (GKE) easiest way would be to push your built images to Google Cloud Artifact Registry. You can find on how to do it here. Replace LOCATION, PROJECT-ID, REPOSITORY and IMAGE with your settings.
💡
If you are on MacOS and using Docker engine make sure to pass --platform linux/amd64 flag
docker build -t LOCATION-docker.pkg.dev/PROJECT-ID/REPOSITORY/IMAGE -f .

after build has successfully completed push image to Artifact Registry by running

docker push LOCATION-docker.pkg.dev/PROJECT-ID/REPOSITORY/IMAGE

Deployment

To deploy to Kubernetes deployment file is required. It tells cluster how to create and manage your pods (containerized application) in the cluster.

     
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: whatabot
  template:
    metadata:
      labels:
        app: myapp
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: myapp
          image: LOCATION-docker.pkg.dev/PROJECT-ID/REPOSITORY/IMAGE #replace this with image URL
          imagePullPolicy: Always
          ports:
            - containerPort: 3000
              protocol: TCP

Apply with kubectl apply -f <deployment-file>.yaml
This is very generic deployment file. Ways it can be improved:

  • Add health check
  • Add readiness check
  • Resource constraints

They are not added this time for simplicity sake.

Service

Service is a method for exposing a network application that is running as one or more Pods in your cluster. Entry point for the application in short. Service file is required to tell Kubernetes how to route traffic internally across its services.

apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  type: ClusterIP
  ports:
    - targetPort: 3333
      port: 3000
  selector:
      app: myapp

apply by running kubectl apply -f <service-file>.yaml

To test if everything is working correctly on cluster side you can port forward and in browser if NestJS app is responding by running:

kubectl port-forward svc/myapp 3333:3000

Now you can use browser or any Rest API client on port 3333 to test your application. All in all this step could be considered as done if you'd like to to consume your service internally. To make it publicly available on Google Cloud we'd need to create a LoadBalancer, create SSL and point the domain to newly created cluster. Feel free to follow along.

Ingress

To expose your application (service) over the internet you should use the ingress object.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp #name of ingress
spec:
  rules:
  - http:
      paths:
      - path: /*
        pathType: ImplementationSpecific
        backend:
          service:
            name: myapp #name of your service object we created above. 
            port:
              number: 3000

And apply

kubectl apply -f <ingress>.yaml

This will provision load balancer behind the scenes and it will take some time.
Run this command to get IP address of a load balancer.

kubectl get ingress myapp

Once you see an IP adress in the "ADDRESS" section you can visit browser. Traffic is served over HTTP and not HTTPS, so it's not much use for this. Let's expose our app via HTTPS.

HTTPS

Before starting delete the ingress created before.

kubectl delete -f <ingress>.yaml

delete ingress

For this we will need a global IP address. We can get one by running gcloud command.

gcloud compute addresses create <name-of-ip> --global

create global ip adress

Remember the name of IP address.

To get IP actual address run following:

gcloud compute addresses describe <name-of-ip> --global  

get IP address

Create a managed SSL certificate by applying following manifest.

apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
  name: myapp-cert #reference to our certificate in the cluster
spec:
  domains:
    - myapp.example.com #replace with your DNS

create a managed certificate

and adjust ingress object by adding annotations.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp
  annotations:
    kubernetes.io/ingress.global-static-ip-name: myapp-ip #name of ip
    networking.gke.io/managed-certificates: myapp-cert #name of certificate 
spec:
  rules:
  - http:
      paths:
      - path: /*
        pathType: ImplementationSpecific
        backend:
          service:
            name: myapp #name of service to route traffic to
            port:
              number: 3000

Apply managed certificate and ingress manifests. While everything is provisioning make sure to point your domain A record to the global IP address you've created. After couple minutes visit your domain name and it should be secured by Google's Managed Certificate and be serving your traffic.

Conclusion

In this short tutorial we managed to build and deploy NestJS application to the Google Kubernetes Engine and secure it over HTTPS. Make sure to delete all resources if not used, because you will get charged.

You can find code here. If any questions - feel free to reach out!