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 href
s 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.