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

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.

Related

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.

Problems with a react application deployed on k8s

I have a problem with a simple react app that was created using npx create-react-app react-app. Once deployed on k8s, I got this:
Uncaught SyntaxError: Unexpected token '<'
However, if I would to kubectl port-forward to the pod and view the app at localhost:3000 (container's pod is at 3000, cluster ip service listening on 3000 and forwarding to 3000) no problem at all.
The ingress routing looks to be fine as I can get to other services to work within the cluster but not to the app. Some help would be greatly appreciated.
Deployment yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: react-app-deployment
# namespace: gitlab-managed-apps
spec:
replicas: 1
selector:
matchLabels:
component: react-app
template:
metadata:
labels:
component: react-app
spec:
imagePullSecrets:
- name: simpleweb-token-namespace
containers:
- name: react-app
image: registry.gitlab.com/mttlong/sample/react-app
env:
- name: "PORT"
value: "3000"
ports:
- containerPort: 3000
Cluster ip service:
apiVersion: v1
kind: Service
metadata:
name: react-app-cluster-ip-service
spec:
type: ClusterIP
selector:
component: react-app
ports:
- port: 3000
targetPort: 3000
Dockerfile:
FROM node:10.15.3-alpine as builder
WORKDIR '/app'
COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx
EXPOSE 3000
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/build /usr/share/nginx/html
Ingress Service:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: orion-ingress-service
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: horizon.zeezum.com
http:
paths:
- path: /
backend:
serviceName: react-app-cluster-ip-service
servicePort: 3000
- path: /api(/|$)(.*)
backend:
serviceName: simple-api-nodeport-service
servicePort: 3050
I ran into the same issue as you have described. I solved it by splitting up the Ingress for the front-end and the API.
In your case this would look something like this:
Front-end ingress service (without rewrite target):
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: orion-ingress-frontend-service
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: horizon.zeezum.com
http:
paths:
- path: /
backend:
serviceName: react-app-cluster-ip-service
servicePort: 3000
Back-end ingress service (with the /$2 rewrite-target):
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: orion-ingress-backend-service
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- host: horizon.zeezum.com
http:
paths:
- path: /api(/|$)(.*)
backend:
serviceName: simple-api-nodeport-service
servicePort: 3050
The rest of you configuration should be good.
Remove nginx.ingress.kubernetes.io/rewrite-target: /$2 this line and it should work
This is most likely the result of a 404 page or a redirect to a page that serves regular html instead of the expected JavaScript files. (A HTML page starts with <html> or a <!DOCTYPE...> therefore the < as unexpected token)
Make sure that you have correctly build the image and you access the page correctly. You can verify by manually accessing the URL with the browser or look into the network tab of your browser development tools to inspect the response.
(I assume that either you have cached the index.html and try to access old assets, check your cache header, or that the paths to not match. In that case inspect your image and access the URLs manually)
I had a similar thing. I got rid of the Ingress altogether, and in my Service yaml file changed my Service to type: Nodeport and added the fields protocol: TCP and nodePort: <insert nodeport> under each port.
I got the values ofthe nodeports by running kubectl expose deployment <insert deployment name> --type=NodePort --name=example-service. Which creates a service and assigns the ports. You can then run kubectl describe services example-service to display all the nodeport values. You can then copy the values to your Service yaml file.
When its all running you would access the website from the browser using the nodeport that was generated, ie localhost:31077for example, and not the actual port 3000.
I then changed any url routes I had to use the nodeport instead of the actual ports and boom.

Serving a static react application behind a Kubernetes Ingress

I'm currently trying to setup a react SPA as a deployment/service on kubernetes. Like the backend services I have running currently, I want to be able to hit it behind an Ingress.
Because the SPA is just a bunch of static files, I'm exposing the files through nginx. The container that runs in the deployment has nginx installed to serve the static assets (nginx -g daemon off in the docker file). This works completely fine if I expose the deployment with a LoadBalancer, but if I use an Ingress, I get no response. Are there any special things to consider when serving static assets behind an ingress? Or any known references/resources for doing this?
Here's my ingress.yml:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: web-ingress
annotations:
kubernetes.io/ingress.global-static-ip-name: web-static-ip
nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
rules:
- host: api.my-domain.com
http:
paths:
- backend:
serviceName: web-backend-service
servicePort: 80
- host: app.my-domain.com
http:
paths:
- backend:
serviceName: web-frontend-service
servicePort: 80
You need to have an ingress controller deployed on your cluster for an ingress resource to actually take effect. Here is the installation guide of Nginx ingress controller.An example ingress to serve static content would look like below.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: web-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/add-base-url: "true"
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: api.my-domain.com
http:
paths:
- backend:
serviceName: web-backend-service
servicePort: 80
- host: app.my-domain.com
http:
paths:
- backend:
serviceName: web-frontend-service
servicePort: 80
Here is a guide on how to serve angular 8 application on Minikube using Nginx ingress.

ReactJS app displays whitescreen using Kubernetes Ingress

I am trying to get hands around ingress routing to deploy multiple ReactJS application using one public ip address. Using SpeedyMath app available at https://github.com/pankajladhar/speedy-math.git with below routing file trying to access this app as http://myapps.centralus.cloudapp.azure.com/speedymath displays a white screen. From browser logs what i see is http://myapps.centralus.cloudapp.azure.com/static/js/bundle.js net::ERR_ABORTED 404 (Not Found). I notice index.html getting loaded but errors "Failed to load resource" at line <script type="text/javascript" src="/static/js/bundle.js"></script></body>
ingress-routing.yml:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: myapps-ingress
annotations:
nginx.org/server-snippet: "proxy_ssl_verify off;"
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /speedymath
backend:
serviceName: speedymath-service
servicePort: 80
The same application loads properly when the routing file is updated for path from "/speedymath" to "/". But this does not help me in building different routing rules based on incoming url
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: myapps-ingress
annotations:
nginx.org/server-snippet: "proxy_ssl_verify off;"
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: speedymath-service
servicePort: 80
Appreciate your help here
My issue got resolved with couple of things:
As #mk_sta stated, path as path: /speedymath(/|$)(.*) and nginx.ingress.kubernetes.io/rewrite-target: /$2
To handle the context issue with ReactJS app, updated package.json to include "homepage": ".". This will update all links and paths to refer from current directory.
I've managed to reproduce the issue in the similar user case with Nginx Ingress controller 0.25.1 version:
$ kubectl exec -it $(kubectl get po -l component=controller|awk '{print $1}'| sed '1d') -- /nginx-ingress-controller
A few concerns that might help you to resolve a problem:
FYI, since 0.22.0 version of Nginx Ingress was announced, some significant changes in Rewrite annotations being propagated that are not compatible with previous configurations, read more here.
If you are using more latest Ingress controller release, then you would probably need to slightly improve the origin Ingress definition, according to the documentation:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: myapps-ingress
annotations:
nginx.org/server-snippet: "proxy_ssl_verify off;"
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
rules:
- http:
paths:
- path: /speedymath(/|$)(.*)
backend:
serviceName: speedymath-service
servicePort: 80
Afterwards, you might be able to reach the application endpoint, however I have added Trailing slash in my target URL, instructing Nginx engine to properly serve some static content; looking at your URL:
http://myapps.centralus.cloudapp.azure.com/speedymath/
Following this discussion, you can even redirect all requests to a non trailing location, adjusting appropriate Configuration snippet annotation to the source Ingress resource:
nginx.ingress.kubernetes.io/configuration-snippet: |
rewrite ^(/speedymath)$ $1/ permanent;
The last time I did this my annotation was a bit different. That's the only difference I see between what worked for me.
annotations:
ingress.kubernetes.io/rewrite-target: /
I also had this problem. It's caused because you specified a path in your ingress (in your case speedymath) but the react application will try to access the root path.
This can be solved by adding an environment variable (PUBLIC_URL) assigned with the same path you specified on ingress (would be \speedymath\ in your case) in your deployment (or pod) yaml like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
env:
- name: PUBLIC_URL
value: "/<MY_INGRESS_PATH>/"
image: myapp/image:latest
ports:
- containerPort: 80
Hope this can help others!

Kubernetes(minikube) + React Frontend + .netcore api + Cluster IP service + ingress + net::ERR_NAME_NOT_RESOLVED

Not able to resolve an API hosted as a ClusterIP service on Minikube when calling from the React JS frontend.
The basic architecture of my application is as follows
React --> .NET core API
Both these components are hosted as ClusterIP services. I have created an ingress service with http paths pointing to React component and the .NET core API.
However when I try calling it from the browser, react application renders, but the call to the API fails with
net::ERR_NAME_NOT_RESOLVED
Below are the .yml files for
1. React application
apiVersion: v1
kind: Service
metadata:
name: frontend-clusterip
spec:
type: ClusterIP
ports:
- port: 59000
targetPort: 3000
selector:
app: frontend
2. .NET core API
apiVersion: v1
kind: Service
metadata:
name: backend-svc-nodeport
spec:
type: ClusterIP
selector:
app: backend-svc
ports:
- port: 5901
targetPort: 59001
3. ingress service
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-service
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
rules:
- http:
paths:
- path: /?(.*)
backend:
serviceName: frontend-clusterip
servicePort: 59000
- path: /api/?(.*)
backend:
serviceName: backend-svc-nodeport
servicePort: 5901
4. frontend deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
selector:
matchLabels:
app: frontend
replicas: 1
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: upendra409/tasks_tasks.frontend
ports:
- containerPort: 3000
env:
- name: "REACT_APP_ENVIRONMENT"
value: "Kubernetes"
- name: "REACT_APP_BACKEND"
value: "http://backend-svc-nodeport"
- name: "REACT_APP_BACKENDPORT"
value: "5901"
This is the error I get in the browser:
xhr.js:166 GET
http://backend-svc-nodeport:5901/api/tasks net::ERR_NAME_NOT_RESOLVED
I installed curl in the frontend container to get in the frontend pod to try to connect the backend API using the above URL, but the command doesn't work
C:\test\tasks [develop ≡ +1 ~6 -0 !]> kubectl exec -it frontend-655776bc6d-nlj7z --curl http://backend-svc-nodeport:5901/api/tasks
Error: unknown flag: --curl
You are getting this error from local machine because ClusterIP service is wrong type for accessing from outside of the cluster. As mentioned in kubernetes documentation ClusterIP is only reachable from within the cluster.
Publishing Services (ServiceTypes)
For some parts of your application (for example, frontends) you may
want to expose a Service onto an external IP address, that’s outside
of your cluster.
Kubernetes ServiceTypes allow you to specify what kind of Service
you want. The default is ClusterIP.
Type values and their behaviors are:
ClusterIP: Exposes the Service on a cluster-internal IP. Choosing this value makes the Service only reachable from within the
cluster. This is the default ServiceType.
NodePort:
Exposes the Service on each Node’s IP at a static port (the
NodePort). A ClusterIP Service, to which the NodePort
Service routes, is automatically created. You’ll be able to contact
the NodePort Service, from outside the cluster, by requesting
<NodeIP>:<NodePort>.
LoadBalancer:
Exposes the Service externally using a cloud provider’s load balancer.
NodePort and ClusterIP Services, to which the external load
balancer routes, are automatically created.
ExternalName:
Maps the Service to the contents of the externalName field (e.g.
foo.bar.example.com), by returning a CNAME record
with its value. No proxying of any kind is set up.
Note: You need CoreDNS version 1.7 or higher to use the ExternalName type.
I suggest using NodePort or LoadBalancer service type instead.
Refer to above documentation links for examples.

Resources