8

I have written an application that runs a FastAPI server inside a Kubernetes pod. The external communication with the pod goes through an nginx ingress controller in a separate pod. I am running nginx:1.17.0.

When it is all up and running I can use curl calls to interact with the app server through the ingress address, and access all the simple GET paths as well as address/openapi.json in my browser. I can also access the interactive documentation page if I use the internal ip of the app service in Kubernetes. However trying to reach the interactive documentation page (address/docs#/default/) gives me an error regarding /openapi.json.

enter image description here

Since the curl calls work as expected I do not think the problem is necessarily in the ingress definition but as using the internal ip of the app also works fine the issue should not be inside the app.
I have included the ingress definition file below.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80

---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - host: my-host.info
    http:
      paths:
      - path: /server(/|$)(.*)
        backend:
          serviceName: my-app-service # This is the service that runs my fastAPI server pod
          servicePort: 80

EDIT
This is the service.yaml file

apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  type: ClusterIP
  selector:
    app: server
  ports:
    - protocol: "TCP"
      port: 80
      targetPort: 80

As the service is a ClusterIP inside my local cluster I have might be able to curl straight to it, I have not tried though. When I curl I use commands like

curl -X GET "http://my-host.info/server/subpath/" -H "accept: application/json"
curl -X POST "http://my-host.info/server/subpath/update/" -H "accept: application/json"

from outside the local cluster.

These are all the services that are running:

NAMESPACE              NAME                        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE
default                kubernetes                  ClusterIP   10.96.0.1       <none>        443/TCP                  11d
default                my-app-service              ClusterIP   10.96.68.29     <none>        80/TCP                   18h
kube-system            kube-dns                    ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP,9153/TCP   28d
kubernetes-dashboard   dashboard-metrics-scraper   ClusterIP   10.96.114.1     <none>        8000/TCP                 28d
kubernetes-dashboard   kubernetes-dashboard        ClusterIP   10.96.249.255   <none>        80/TCP                   28d

and inside my /etc/hosts file I have connected 10.0.0.1 (cluster "external" IP) to my-host.info.

Any ideas of why this is happening?

Kajsa
  • 409
  • 6
  • 16
  • 1
    Could you please share your `service` yaml and exact command you are using to curl it? You ale trying to connect from outside the cluster? Also could you provide `kubectl get svc --all-namespaces`? It's On-Prem or local cluster? – PjoterS Feb 25 '20 at 15:54
  • First issue I found here is that your deployment and service have different selectors/labels. In deployment its `app: nginx` but in svc is `app: server`. Should be the same. I will try to reproduce your issue. You can check this answer `Issue 2` - https://stackoverflow.com/a/59372425/11148139 – PjoterS Feb 26 '20 at 09:03
  • Its On-Prem or local cluster? – PjoterS Feb 26 '20 at 09:06
  • @PjoterS I have the deployment in a separate file. The app for the server is `server` and the nginx app is a different one, at least that's the way all the examples I've seen work. If this was the problem it wouldn't have worked at all. And as I've stated it's on a local cluster. The app is functioning fully it is just the connection to the FastAPI default docs page that doesn't work. – Kajsa Feb 26 '20 at 09:41
  • @PjoterS Turns out I misunderstood the tutorials and the nginx pod I deploy is not being used at all. Instead a different pod that is deployed by my minikube is running the nginx-ingress-controller which is then used by the ingress I've defined. – Kajsa Feb 26 '20 at 16:02
  • to map service with deployment you should use labe/selector (https://kubernetes.io/docs/concepts/services-networking/connect-applications-service/). As you are using Minikube and Ingress you can check this thread (https://stackoverflow.com/a/60061508/1114813)). What will happen if you will use the same labels/selectors in delployment and svc? – PjoterS Feb 27 '20 at 09:15

2 Answers2

8

I think you definitely should look into official documentation of FastApi: https://fastapi.tiangolo.com/advanced/behind-a-proxy/

As you mentioned, when accessing your app internally the Swagger auto docs works fine but when accessing from outside your cluster you've got an error regarding /openapi.json.

In your service.yaml you have:

      - path: /server(/|$)(.*)
        backend:
          serviceName: my-app-service # This is the service that runs my fastAPI server pod
          servicePort: 80

and when starting your application with uvicorn you should pass the root_path

uvicorn main:app --root-path /server

ATTENTION: Here you will be able to access the routers endpoints but no the Swagger docs. In order to get Swagger docs you have to edit your main main.py file:


from fastapi import FastAPI, Request

app = FastAPI(openapi_prefix="/server")

@app.get("/")
def read_root(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

I searched why we need explicitly pass the OpenApi prefix but I found only workarounds like: https://github.com/iwpnd/fastapi-aws-lambda-example/issues/2

So I suggest to store root_path in environment variable $ROOT_PATH=/server on your system and pass it to uvicorn main:app --root-path $ROOT_PATH as well as in main.py:

import os
from fastapi import FastAPI, Request

app = FastAPI(openapi_prefix=os.getenv('ROOT_PATH', ''))

@app.get("/")
def read_root(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

UPDATE 07.07.2020

Currently tiangolo ready to use docker images https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker are "outdated" if goes on fastapi version which currently is: 0.55.1 - reported here: link

"root_path" is supported since 0.56.0

rozacek
  • 316
  • 3
  • 6
4

Using Kubernetes rewrite feature for ingress I could solve my problem like this:

ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - host: my-app
    http:
      paths:
      - path: /server(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: my-fastapi
            port:
              number: 80

then I just needed to add root_path to my FastAPI app:

app = FastAPI(root_path="/server")
Ghasem
  • 14,455
  • 21
  • 138
  • 171