When writing Server-Side Rendering logic in Next.js in a microservices architecture in Kubernetes, there are two interesting situations we face that I want to address in this article.
This post is inspired by the audiophile project which you can find on my GitHub account here: https://github.com/mmaksi/audiophile
Kubernetes for Microservices
We often use Kubernetes to orchestrate our microservices that run inside isolated environments known as pods, which themselves run inside a Kubernetes cluster. By the word "orchestrating", it means that we want those pods that contain our microservices to scale up and down on demand.
Let's suppose we are building an e-commerce application and we have two microservices, one is auth
and one is payment
. The requests that come to the payment microservice might be more than those coming to the auth
microservice. So in this case, we want only the payment
microservice to scale. And that's the purpose of Kubernetes.
Making a Request to a Microservice From The Browser in Next.js
Let's suppose that we have a Next.js application running on a local server (localhost) on a domain called audiophile.com (we can achieve that in the hosts file). That application has a signup page which makes requests to a microservice called auth
to signup a new user. I want to examine what happens from the moment we write audiophile.com in the browser and click enter.
When the Next.js app first loads on the browser, the Next.js server makes HTTP a request to the root route which is audiophile.com in this case. That request reaches the ingress-controller
which is responsible for routing our requests when working with Kubernetes. The ingress controller routes the request to the Next.js pod and a fully-rendered HTML page is returned to the browser.
Now when we navigate to the Sign Up page and we fill the user form and click "submit", an HTTP request is going to be sent to /api/users/signup
by Axios. By default, the browser attaches that path to the current URL that we are on which is localhost or audiophile.com. As a result, the request goes to audiophile.com/api/users/signup
.
Remember that the ingress controller (our routing system) is listening to any request coming to our Kubernetes cluster and it decides how to map incoming requests to the correct pods. When the ingress controller sees that the request is coming to /api/users/*
it will direct it to the auth
microservice and the microservice will do apply the right logic to sign up the user and store their data in the database.
Everything is working smoothly so far. Now let's assume that in the first step, when the app renders the home page. What if at the first moment we want the application to know whether the user is signed-in or not and renders a different page accordingly.
In Next.js version 14, we can directly access the database directly from the server before sending any response to the browser through what is known as "server actions". Let's examine what happens when an axios request from a server action to /api/users/current-user
.
As discussed earlier, the /api/users/current-user
is attached to the current URL which is localhost port 80. But what is localhost in this case? It is the application which runs inside a container inside the Pod because every container is its own virtual machine. So we are connecting to port 80 while there is nothing listening on port 80. That causes the request to fail.
To make that request succeed, we must send the request to the ingress controller, the centralized routing place. And the controller itself directs the route to the right path, which is the auth
pod.
Now the question is, how can we connect to the ingress controller? We can connect to the ingress controller from our browser by connecting to port 80 on localhost. But we want to connect to it from inside the Kubernetes cluster. To see how that is possible, let's look deeper into what is inside a Kubernetes cluster.
Cross-Namespace Communication in Kubernetes
Inside Kubernetes, objects (pods, services, etc..) are organized into namespaces. To see the namespaces inside your Kubernetes cluster, you can run kubectl get namespaces
.
By default all pods that we create are created inside the default
namespace, while on the other hand, the ingress controller is created inside ingress-nginx
namespace. To connect to a service inside the same namespace, we connect to http://SERVICE_NAME
. But to connect a pod to a service in a default namespace we follow this URL: http://SERVICE_NAME.NAMESPACE.svc.cluster.local
.
List All Services inside a Namespace
To see all the services running inside the ingress-nginx namespace, we type: kubectl get services -n ingress-nginx
and we see we have a service called ingress-nginx-controller
.
The Final URL
So, to connect from the client Pod from the default namespace with the ingress-nginx service inside the ingress-nginx namespace, we send requests to this URL: http://ingress-nginx-controller.ingress-nginx.svc.cluster.local
and then we attach the route, sot the final URL is:
ingress-nginx-controller.ingress-nginx.svc...
Specifying Host Name
Now that the URL is known, the ingress controller is responsible for routing on a specific domain. But a single ingress controller can manage different domains. So we have to tell the ingress controller what domain we want our request to be made for. That is very easy in Next.js and axios:
const INGRESS_BASE_URL =
'http://ingress-nginx-controller.ingress-nginx.svc.cluster.local';
axios.get(INGRESS_BASE_URL, {
headers: {
Host: 'audiophile.com',
}
})
We don't have to specify the host when sending requests from the browser simply because the localhost is already attached to audiophile.com
Attaching Cookies
Cookies are automatically sent from the browser in HTTP or HTTPS requests. But here we are not in a browser environment, so we can't expect the cookies to be automatically sent with the request. Luckily, in Next.js version 14 accessing the session from the cookies inside a server component is done by simply importing this function:
import { cookies } from 'next/headers';
const session = cookies().get('session');
So the final request should look something like this:
import { cookies } from 'next/headers';
const INGRESS_BASE_URL =
'http://ingress-nginx-controller.ingress-nginx.svc.cluster.local';
const session = cookies().get('session');
axios.get(INGRESS_BASE_URL, {
headers: {
Host: 'audiophile.com',
Cookie: `session=${session?.value}`,
}
})
If you want to learn everything about cookies and sessions, please refer to my other blog post here:
https://markmaksi.hashnode.dev/fundamental-authentication-concepts
I know it was a little bit complicated to wire it up, but I hope I made everything crystal clear as much as I could. If you like my style of explaining concepts in blogging, you could also like my style of explaining big web development concepts in easy to understand tutorials, which is the purpose of my YouTube channel:
Thank you for reaching this far!