Running ReactJS application with HTTPS and backend APIs behind a Kubernetes Ingress - reactjs

I am developing a ReactJS application that is calling REST APIs running in kubernetes.
The setup is as follows:
ReactJS being developed/debugged locally and ran with "npm start" because nothing beats how fast the local development server detects changes and reload the browser when changes are detected.
ReactJS API requests are done with axios
Backend APIs written in GO running as separate deployment/services locally in minikube.
There is an Ingress installed locally in minikube to forward requests from urlshortner.local to the respective k8s service.
The basic idea is the following:
ReactJS -> k8s ingress -> GO REST API
Now the problem starts when I try to set secure httpOnly cookies. Because the cookie needs to be secure, I created a self signed ssl certificate and applied it to be used by the ingress. I also enabled CORS settings in the ingress configuration. I also configured axios to not reject self signed certificates.
For some reason that is unknown to me I can't success in making the request.
Below are my relevant config files and code snippets:
k8s ingress:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: url-shortner-backend-services
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://localhost:4000"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
spec:
tls:
- secretName: urlshortner-local-tls
hosts:
- urlshortner.local
rules:
- host: urlshortner.local
http:
paths:
- path: /shortner(/|$)(.*)
backend:
serviceName: url-shortener-service
servicePort: 3000
- path: /auth(/|$)(.*)
backend:
serviceName: auth-service
servicePort: 3000
The react application start scripts:
PORT=4000 SSL_CRT_FILE=tls.crt SSL_KEY_FILE=tls.key react-scripts start
The axios code snippet that creates an axios instance that is used to issue a POST request
import axios from "axios";
import https from "https";
export default axios.create({
baseURL: 'https://urlshortner.local',
withCredentials: true,
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
When a POST request is made, I see the following error in the browser console/network tab even though when I first load the page I am accepting the certificate warning and adding it as a trusted certificate:
The end result that I would like to achieve is to be able to set a cookie and read the cookie on subsequent requests.
The cookie is being set as follows:
c.SetSameSite(http.SameSiteNoneMode)
c.SetCookie("token", resp.Token, 3600, "/", "localhost:4000", true, true)
What is missing? What am I doing wrong?
Thanks in advance

I finally managed to fix this issue and the good news is that you don't need to create a self signed certificate.
The steps are the following:
set a HOST environment variable before starting your development react server.
adjust /etc/hosts so that 127.0.0.1 points to the value set in the HOST environment variable
adjust your k8s ingress CORS settings to allow "cors-allow-origin" from the domain set in the HOST environment variable
setting cookies should now work as expected.
Below are the relevant code snippets:
npm start script
"scripts": {
"start": "PORT=4000 HOST=app.urlshortner.local react-scripts start",
}
notice the HOST environment variable, the PORT environment variable is optional, I'm using it because the default port 3000 is already taken.
/etc/hosts
127.0.0.1 app.urlshortner.local
192.168.99.106 urlshortner.local
note that 192.168.99.106 is my local minikube ip address.
Kubernetes ingress configuration
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: url-shortner-backend-services
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "http://app.urlshortner.local:4000"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
spec:
rules:
- host: urlshortner.local
http:
paths:
- path: /shortner(/|$)(.*)
backend:
serviceName: url-shortener-service
servicePort: 3000
- path: /auth(/|$)(.*)
backend:
serviceName: auth-service
servicePort: 3000
What matters here is the following:
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "http://app.urlshortner.local:4000"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
axios instance used
import axios from "axios";
let baseURL = '';
if (process.env.NODE_ENV === 'development') {
baseURL = 'http://urlshortner.local';
}
export default axios.create({
baseURL,
withCredentials: true
});
How the cookie is set:
c.SetCookie("token", resp.Token, 3600, "/", ".urlshortner.local", false, true)
note the domain used. It starts with a "."
I hope this helps someone.

Related

Connect React Client running on local PC to Kubernetes Cluster on Azure

I have 2 deployments + services running on Azure: react client and nodejs auth.
I have registered a public IP on Azure which I added to my windows host file (= myexample.com).
Typing the URL in the browser, the client opens and requests go to auth service as expected.
Now I want to run the client locally (with npm start) but connect to auth service still running on Azure.
I removed the client from the cloud deployment (= the deployment+the service) and use the domain (=myexample.cloud) as the base URL in my axios client in my React client. To confirm, on Azure my ingress-nginx-controller of type Load_Balancer shows the aforementioned public IP as its external IP plus ports 80:30819/TCP,443:31077/TCP.
When I ran the Client locally, it shows the correct request URL (http://myexample.cloud/api/users/signin) but I get a 403 Forbidden answer.
What am I missing? I should be able to connect to my cloud service by using the public IP? There error is caused by my client because Azure is not putting road blocks in place. I mean it is a pubic IP, correct?
Update 1
Just to clarify, the 403 Forbidden is not caused by me trying to sign in with incorrect credentials. I have another api/users/health-ckeck route that is giving me the same error
My cloud ingress deployment. I have also tried to remove the client part (last 7 lines) to no effect.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-service
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: myexample.cloud
http:
paths:
- path: /api/users/?(.*)
pathType: Prefix
backend:
service:
name: auth-srv
port:
number: 3000
- path: /
pathType: Prefix
backend:
service:
name: client-srv
port:
number: 3000
my client cloud deployment+service that worked when client was running in cloud
apiVersion: apps/v1
kind: Deployment
metadata:
name: client
spec:
replicas: 1
selector:
matchLabels:
app: client
template:
metadata:
labels:
app: client
spec:
containers:
- name: client
image: client
---
apiVersion: v1
kind: Service
metadata:
name: client
spec:
selector:
app: client
ports:
- name: client
protocol: TCP
port: 3000
targetPort: 3000
my auth deployment + service
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
spec:
replicas: 1
selector:
matchLabels:
app: auth
template:
metadata:
labels:
app: auth
spec:
containers:
- name: auth
image: auth
apiVersion: v1
kind: Service
metadata:
name: auth
spec:
selector:
app: auth
ports:
- name: auth
protocol: TCP
port: 3000
targetPort: 3000
The problem was actually CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin'
but my browser did not tell me.
After switching from Chrome to Firefox, the problem became apperant.
I had to add annotations to my ingress controller as described here: express + socket.io + kubernetes Access-Control-Allow-Origin' header

Why can I not open a React web app in a browser through an ingress when deploying it in a Kubernetes cluster

I have a very simple React web app (using Argon Dashboard Pro Template) deployed in a Kubernetes cluster. The Docker image of it works locally as well as in the cluster when exposing it via nodeport. But exposing it via NGINX ingress doesn't work, although the ingress itself is tested for other services and applications which expose REST endpoints. The content of the web page is not built correctly, because some js files are not found, although this is the case when they are exposed via nodeport.
Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: react-deployment
namespace: support
labels:
app: react
stage: dev
spec:
replicas: 1
template:
metadata:
labels:
app: react
spec:
containers:
- name: react
image: fmaus/react-test:argon
ports:
- containerPort: 3000
name: react-port
imagePullPolicy: Always
restartPolicy: Always
selector:
matchLabels:
app: react
Kubernetes Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: react-ingress
namespace: support
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header Accept-Encoding "";
more_set_headers "Content-Type: text/javascript; charset=UTF-8";
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /test(/|$)(.*)
pathType: Prefix
backend:
service:
name: react-service
port:
number: 3000
Kubernetes Service:
apiVersion: v1
kind: Service
metadata:
name: "react-service"
namespace: support
spec:
selector:
app: "react"
ports:
- port: 3000
type: ClusterIP
This Code can also be found in my GitHub Repository: https://github.com/fmaus/react-kubernetes-test
To reproduce the problem, just apply these Kubernetes resources to a cluster and try to reach the web app through a browser via ingress (http://host/subpath). I have the resources deployed here: http://c105-164.cloud.gwdg.de:31600/test
The error messages can be visited in the console of the browser (F12 when using Firefox):
Loading failed for the <script> with source “http://c105-164.cloud.gwdg.de:31600/static/js/bundle.js”. subpath:61:1
Loading failed for the <script> with source “http://c105-164.cloud.gwdg.de:31600/static/js/vendors~main.chunk.js”. subpath:61:1
Loading failed for the <script> with source “http://c105-164.cloud.gwdg.de:31600/static/js/main.chunk.js”.
I use Mozilla Firefox and the following NGINX ingress controller: https://kubernetes.github.io/ingress-nginx/
I think you have two issues in place here:
You set the content type to javascript, so the html is not interpreted correctly by the browser. F.e. http://c105-164.cloud.gwdg.de:31600/test/index.html is shown as source
You need to make sure the resources are referenced including the sub path, or a 404 will result
For example
<script src="/static/js/bundle.js"></script><script src="/static/js/vendors~main.chunk.js"></script><script src="/static/js/main.chunk.js"></script></body>
Needs to load the bundle from /subpath/static/js/bundle.js since it is an absolute link.

Kubernetes Host and Service Ingress Mapping using TCP

While working with Kubernetes for some months now, I found a nice way to use one single existing domain name and expose the cluster-ip through a sub-domain but also most of the microservices through different sub-sub-domains using the ingress controller.
My ingress example code:
kind: Ingress
apiVersion: networking.k8s.io/v1beta1
metadata:
name: cluster-ingress-basic
namespace: ingress-basic
selfLink: >-
/apis/networking.k8s.io/v1beta1/namespaces/ingress-basic/ingresses/cluster-ingress-basic
uid: 5d14e959-db5f-413f-8263-858bacc62fa6
resourceVersion: '42220492'
generation: 29
creationTimestamp: '2021-06-23T12:00:16Z'
annotations:
kubernetes.io/ingress.class: nginx
managedFields:
- manager: Mozilla
operation: Update
apiVersion: networking.k8s.io/v1beta1
time: '2021-06-23T12:00:16Z'
fieldsType: FieldsV1
fieldsV1:
'f:metadata':
'f:annotations':
.: {}
'f:kubernetes.io/ingress.class': {}
'f:spec':
'f:rules': {}
- manager: nginx-ingress-controller
operation: Update
apiVersion: networking.k8s.io/v1beta1
time: '2021-06-23T12:00:45Z'
fieldsType: FieldsV1
fieldsV1:
'f:status':
'f:loadBalancer':
'f:ingress': {}
spec:
rules:
- host: microname1.subdomain.domain.com
http:
paths:
- pathType: ImplementationSpecific
backend:
serviceName: kylin-job-svc
servicePort: 7070
- host: microname2.subdomain.domain.com
http:
paths:
- pathType: ImplementationSpecific
backend:
serviceName: superset
servicePort: 80
- {}
status:
loadBalancer:
ingress:
- ip: xx.xx.xx.xx
With this configuration:
microname1.subdomain.domain.com is pointing into Apache Kylin
microname2.subdomain.domain.com is pointing into Apache Superset
This way all microservices can be exposed using the same Cluster-Load-Balancer(IP) but the different sub-sub domains.
I tried to do the same for the SQL Server but this is not working, not sure why but I have the feeling that the reason is that the SQL Server communicates using TCP and not HTTP.
- host: microname3.subdomain.domain.com
http:
paths:
- pathType: ImplementationSpecific
backend:
serviceName: mssql-linux
servicePort: 1433
Any ideas on how I can do the same for TCP services?
Your understanding is good, by default NGINX Ingress Controller only supports HTTP and HTTPs traffic configuration (Layer 7) so probably your SQL server is not working because of this.
Your SQL service is operating using TCP connections so it is does not take into consideration custom domains that you are trying to setup as they are using same IP address anyway .
The solution for your issue is not use custom sub-domain(s) for this service but to setup exposing TCP service in NGINX Ingress Controller. For example you can setup this SQL service to be available on ingress IP on port 1433:
Ingress controller uses the flags --tcp-services-configmap and --udp-services-configmap to point to an existing config map where the key is the external port to use and the value indicates the service to expose using the format: <namespace/service name>:<service port>:[PROXY]:[PROXY]
To setup it you can follow steps provided in official NGINX Ingress documentation but there are also some more detailed instructions on StackOverflow, for example this one.

Configuring k8s nginx ingress to route React SPA and backend apis

My backend services is working great with ingress nginx.
I'm trying without success to add a frontend SPA react app to my ingress.
I did manage to get it work but I can't get both my backend AND front end to works.
My ingress yml is
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-service
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/use-regex: 'true'
nginx.ingress.kubernetes.io/rewrite-target: /$2
#nginx.ingress.kubernetes.io/add-base-url: "true"
spec:
rules:
- host: accounting.easydeal.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: frontend-srv
port:
number: 3000
- host: api.easydeal.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: docker-hello-world-svc
port:
number: 8088
- path: /accounting(/|$)(.*)
pathType: Prefix
backend:
service:
name: accounting-srv
port:
number: 80
- path: /company(/|$)(.*)
pathType: Prefix
backend:
service:
name: dealers-srv
port:
number: 80
With this yml above i'm able to poke my backend like so -> api.easydeal.dev/helloword or
api.easydeal.dev/company/* and it work !.
However my react app (accounting.easydeal.dev) end up with a white page a console log with this error ->
Uncaught SyntaxError: Unexpected token '<'
The only way i'm able to make my react app work is to change rewrite-target: /$2 to / . However doing so prevent to route correctly my other apis.
I did set the homepage for the react app to "." but still have the error and I also try to set path to /?(*) for my front end
here is my dockerfile
# pull the base image
FROM node:alpine
# set the working direction
WORKDIR /app
# add `/app/node_modules/.bin` to $PATH
ENV PATH /app/node_modules/.bin:$PATH
# install app dependencies
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . ./
EXPOSE 3000
CMD ["npm", "start"]
As pointed in the comments by original poster:
Doing 2 ingress services sold this issue.
The solution to this issue was to create 2 separate Ingress resources.
The underlying issue was that the workload required 2 different nginx.ingress.kubernetes.io/rewrite-target: parameters.
Above annotations can be set per Ingress resource and not per path.
You can create 2 Ingress resources that will be separate entities (will have different annotations) and they will work "together".
More reference can be found in the links below:
Stackoverflow.com: Answer: Apply nginx-ingress annotations at path level
Kubernetes.github.io: Ingress nginx: User guide: Basic usage
Being specific to nginx-ingress:
By default when you provision/deploy NGINX Ingress controller you are telling your Kubernetes cluster to create Service of type LoadBalancer. This Service will requests the IP address from the cloud provider (GKE, EKS, AKS) and will route the traffic from this IP to your Ingress controller where the requests will be evaluated and send further according to your Ingress resources definitions.
A side note!
By default was not used without a reason as there are other methods to expose your Ingress controller to the external traffic. You can read more about it by following below link:
Kubernetes.github.io: Ingress nginx: Deploy: Baremetal
Your Ingress controller will have single IP address to expose your workload:
$ kubectl get service -n ingress-nginx ingress-nginx-controller
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.32.6.63 AA.BB.CC.DD 80:30828/TCP,443:30664/TCP 19m
Ingress resource that are using kubernetes.io/ingress.class: "nginx" will use that address.
Ingress resources created in this way will look like following when issuing:
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
goodbye-ingress goodbye.domain.name AA.BB.CC.DD 80 19m
hello-ingress hello.domain.name AA.BB.CC.DD 80 19m
A second side note!
If you are using a managed Kubernetes cluster, please refer to it's documentation for more reference on using Ingress resources as there could be major differences.

Frontend can't resolve backend name within k8s cluster

I'm trying to deploy a simple Angular/Express app on GKE and the http requests from the frontend can't find the name of the express app.
Here's an example of one get requests. I changed the request from 'localhost' to 'express' because that is the name of the clusterIP service setup in the cluster. Also, I'm able to curl this url from the angular pod and get json returned as expected.
getPups(){
this.http.get<{message:string, pups: any}>("http://express:3000/pups")
.pipe(map((pupData)=>{
return pupData.pups.map(pup=>{
return{
name: pup.name,
breed: pup.breed,
quote: pup.quote,
id: pup._id,
imagePath: pup.imagePath,
rates: pup.rates
}
});
}))
.subscribe((transformedPups)=>{
this.pups = transformedPups
this.pupsUpdated.next([...this.pups])
});
}
Here's the angular deployment.
apiVersion: apps/v1
kind: Deployment
metadata:
name: puprate-deployment
spec:
replicas: 1
selector:
matchLabels:
component: web
template:
metadata:
labels:
component: web
spec:
containers:
- name: puprate
image: brandonjones085/puprate
ports:
- containerPort: 4200
---
apiVersion: v1
kind: Service
metadata:
name: puprate-cluster-ip-service
spec:
type: ClusterIP
selector:
component: web
ports:
- port: 4200
targetPort: 4200
And the express deployment.
apiVersion: apps/v1
kind: Deployment
metadata:
name: express
spec:
replicas: 3
selector:
matchLabels:
component: server
template:
metadata:
labels:
component: server
spec:
containers:
- name: server
image: brandonjones085/puprate-express
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: express
spec:
type: ClusterIP
selector:
component: server
ports:
- port: 3000
targetPort: 3000
Your frontend app is making the call from outside your cluster, and therefor needs a way to reach it. Because you are serving http, the best way to set that up will be with an ingress.
First, make sure you have an ingress controller set up in your cluster ( e.g. nginx ingress controller) https://kubernetes.github.io/ingress-nginx/deploy/#gce-gke
Then, set up your express with a service (from your question, I see you already have that set up on port 3000, that's good, though in the service I recommend to change the port to 80 - though not critical)
With that, set up your ingress:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: express
spec:
rules:
- host: <a domain you own>
http:
paths:
# NOTICE!! have you express app listen for that path, or set up nginx rewrite rules (I recommend the former, it's much easier to understand)
- path: /api
backend:
serviceName: express
servicePort: 3000 # or 80 if you decide to change that
Do the same for your web deployment, so you can serve your frontend directly:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: web
spec:
rules:
- host: <a domain you own>
http:
paths:
- path: /
backend:
serviceName: web
servicePort: 4200 # or 80 if you decide to change that
Notice that both ingresses are using the same host but different paths - that's important for what's coming next
in your angular app, change that:
this.http.get<{message:string, pups: any}>("http://express:3000/pups")
to that:
this.http.get<{message:string, pups: any}>("/api/pups")
Browsers will parse that to <domain in your address bar>/api/pups
Since you are using GKE, when you set up the ingress controller you will generate a load balancer in the google cloud - make sure that has a DNS entry that directs there.
I'm assuming you already own a domain, but if you don't yet, just add the ip you got to your personal hosts file until you get one like so:
<ip of load balancer> <domain you want>
# for example
45.210.10.15 awesome-domain.com
So now, use the browser to go to the domain you own - you should get the frontend served to you - and since you are calling your api with an address that starts with /, your api call will go to the same host, and redirected by your ingress to your express app this time, instead of the frontend server.
Angular is running in your browser, not in the pod inside the cluster.
The requests will originate therefore externally and the URL must point to the Ingress or LoadBalancer of your backend service.

Resources