Flask – Dealing with Routing Paths When Deployed Behind URL Prefix

aksflaskgunicornnginx-ingressurl-routing

I have single page application build using the python Flask framework. I'm using gunicorn as the web server and I have containerised it using docker. It is deployed on Azure Kubernetes Services (aks) with Nginx Ingress Controller.

The setup

My Flask app looks like this:

src/main.py

from flask import Flask
from src.routes import main_bp


app = Flask(__name__)
app.register_blueprint(main_bp)


@app.route('/health/live')
def healthLiveMsg():
    return 'Healthy'


@app.route('/health/ready')
def healthReadyMsg():
    return 'Healthy'


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

src/main_bp.py

from flask import Blueprint, render_template


main_bp = Blueprint('main', __name__)

# home page
@main_bp.route('/')
def home():
    return render_template('index.html')

# some other page
@main_bp.route('/import')
def import_page():
    # some code...
    return renter_template('import.html')


# some backend job trigger
@main_bp.route('/run_job', methods=['POST'])
def run_job():
    # some code...    


def register_blueprints(app):
    app.register_blueprint(main_bp)

The base.html has a navigation bar where I use Flask's url_for function to get the link to the home page and the import page, respectively href="{{ url_for('main.home') }} and href="{{ url_for('main.import_page') }}

The aks ingress is defined in the following yaml template:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: __AksIngress__-ingress
  namespace: __AksNamespace__
  annotations:
    nginx.ingress.kubernetes.io/proxy-buffer-size: 16k
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/server-alias: __AksNamespace__.__AksDnsZone__.__AksDomainName__
    nginx.ingress.kubernetes.io/rewrite-target: /$1
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/server-snippet: keepalive_timeout 3600s;client_body_timeout 3600s;client_header_timeout 3600s;
spec:
  tls:
  - hosts:
    - __AksNamespace__.__AksDnsZone__.__AksDomainName__
    secretName: __AksIngress__-tls
  ingressClassName: nginx
  rules:
  - host: __AksNamespace__.__AksDnsZone__.__AksDomainName__
    http:
      paths:
      - path: /myapp/?(.*)
        pathType: Prefix
        backend:
          service:
            name: myapp-service
            port:
              number: 80

The problem

When deployed on aks the app can be reached at example.com/myapp. The served page shows the navigation bar's html having hrefs as "/" and "/import". When clicking on either of them the browser navigates to example.com and example.com/import dropping the myapp prefix, hitting a 404, of course. The expectation is that when navigating around the pages, the URL is built correctly, with the prefix, e.g. example.com/myapp/import.
The liveness and readiness checks (available at example.com/myapp/health/live and example.com/myapp/health/ready) are found by Kubernetes.

My attempts

I have tried a number of solutions, but none of them worked.

SCRIPT_NAME

After a few searches I found this blog post that alluded to the right solution. I set the environment variable in my dockerfile and run the container on my local machine and yes it was working:

  • the homepage was at localhost/myapp
  • clicking the navbar sent me to localhost/myapp/import
  • clicking on the buttons in the import page posted to localhost/myapp/run_job triggering the backend job.

However, after deploying to aks, everything simply had an extra prefix:

  • the homepage was now at example.com/myapp/myapp
  • navigating to the other pages sent me to example.com/myapp/import when the page was now at example.com/myapp/myapp/import
  • similar matter with the run_job
  • in addition, the liveness and readiness checks where failing for kubernetes as they also where under the double prefix path.

ProxyFix

I tried using the ProxyFix as suggested in this SO answer and added the below line after initialising the app:

app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_host=1)

This, however, seemed to have had no effect whatsoever. I did try passing also the x_prefix=1 parameter, with no success.

The question

I have read so many things that I have gotten quite confused now. I begun searching for answers using "flask routing with aks" as key words, then moved to "wsgi server", then "nginx reverse proxy" "nginx prefix" or "nginx ingress", and now I'm not sure what is actually happening. I'm not sure if the solution should come from the ingress.yaml, gunicorn or whether it's the flask app that needs to adapt.

What is the behaviour I'm seeing and how do I solve it?

Because this project structure (together with the aks infrastructure) is built from a template, I'd like a solution that can be added to such template or would be a single addition to the code.

Best Answer

Your proxy configuration is missing the necessary X-Forwarded-Prefix header configuration.

From the documentation:

X-Forwarded-Prefix Header

To add the non-standard X-Forwarded-Prefix header to the upstream request with a string value, the following annotation can be used:

nginx.ingress.kubernetes.io/x-forwarded-prefix: "/path"

This needs to be used along with the Proxyfix flask configuration, including the x_prefix=1 argument of course.

Related Topic