Install Flask,Gunicorn & Nginx

A common way to run python codes from a web service it to use some existing Python web framework and web host.

The layers are as follows:

         web server (where nginx sits)

             |

 WSGI server1------WSGI server2------WSGI server... (where Gunicorn sits)

    |                   |

WSGI framework1    WSGI framework2  (where Flask sits)

    |                   |

  python codes       python codes

Firstly, install a few environment packages. The following was tested on Ubuntu. (use pip3 so it sticks to python 3.x otherwise pip will work)

sudo apt-get install python3-dev python3-pip

sudo pip3 install flask      --proxy http://1.1.1.1:8080 #if proxy required 

sudo pip3 install gunicorn   --proxy http://1.1.1.1:8080

to support odbc connection it also needs:

sudo apt-get install unixodbc-dev

sudo pip3 install pyodbc

------------------------------------

test Flask individually

crate a sample script

   #sample.py#

   from flask import Flask, Response

   app_name = Flask(__name__)

   @app_name.route("/test")

   def test():

       return Response("helloworld"), 200

   if __name__ == '__main__'

       app_name.run(host='0.0.0.0')

run: 

    Python sample.py 

and the flask app will listen on http://0.0.0.0:5000/test by default. The if __name__ == __main__ checks if

the script is run directly by python as an entry point.

go web browser, type in http://0.0.0.0:5000 (replace 0.0.0.0 as the actual ip) and it will display "helloworld"

------------------------------------

2 ways to deploy the flask app on Gunicorn, as flask is only a framework and not supposed to process web request

1. deploy Flask app as a module 

   save the same sample code, but dont need the app.run():

   #sample.py#

   from flask import Flask, Response

   app_name = Flask(__name__)

   @app_name.route("/test")

   def test():

       return Response("helloworld"), 200

go to the directory and run:

      gunicorn -b 0.0.0.0:8000 sample:app_name  

here 'sample' is the module(file) name and 'app_name' is the Flask instance

In this way gunicorn manages the web requests and use the flask app as a backend logic.   

   

2. deploy as a package

create a folder flask_folder

under the folder create sample.py same as above

under the folder create __init__.py as an empty file, so the folder is recoginized as a package

go to the parent of the folder and run:

   gunicorn -b 0.0.0.0:8000 flask_folder.sample:app_name

pretty much same as above but the flask app is put in a package

3. Register the gunicorn service as a system service so it automatically starts

note: this is based on red hat, ubuntu would have a slightly different setting

Firstly create a service definition at:

/etc/systemd/system/helloworld.service

The minimum of the definition is as follows. 

[unit]

Description=helloworld

[Service]

WorkingDirectory=/home/user

ExecStart=/bin/gunicorn -b 0.0.0.0:8080 sample:app_name 

[Install]

WantedBy=multi-user.target

A more complete version is:

[Unit]

Description=helloworld

After=network.target

[Service]

User=xxx

Group=yyy

RemainAfterExit=yes

RuntimeDirectory=helloworld

WorkingDirectory=/home/user

ExecStart=/usr/bin/gunicorn -b 0.0.0.0:8080 sample:app_name 

ExecReload=/bin/kill -s HUP $MAINPID

ExecStop=/bin/kill -s TERM $MAINPID

PrivateTmp=True

[Install]

WantedBy=multi-user.target

To enable the service to run as root

sudo systemctl enable myfirst

Restart the server and the gunicorn service should be running.

Add Nginx as a proxy

usually gunicorn service is surfaced as an internal wsgi service. It needs to stay behind a reverse proxy (strongly recommend nginx) to provide service to external users

sudo apt-get install nginx

the configuration file is usually at:

sudo vi /etc/nginx/nginx.conf

to turn on the reverse proxy on http, add the following section to the http{} block in the conf file. The example below listen at port 8081, so the request url is http://url:8081/... which maps to http://127.0.0.1:8080/...

server {

      listen 8081;

      server_name _;

      location /score {

        proxy_pass http://127.0.0.1:8080;

      }

    }

To turn on the proxy on https

Firstly create self signed key and certificate to support ssl. Save the key and certificate a folder, eg. /etc/cert/nginx

sudo openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/cert/nginx/nginx-selfsigned.key -out /etc/cert/nginx/nginx -selfsigned.crt                                                                                                     

Add a server section to the conf file for https service. this is a minimum. The request url is https://url/... which maps to http://127.0.0.1:8080/...

server {

        listen       443 ssl;

        server_name  _;

        ssl_certificate "/etc/cert/nginx/nginx-selfsigned.crt";

        ssl_certificate_key "/etc/cert/nginx/nginx-selfsigned.key";

        location / {

            proxy_pass http://127.0.0.1:8080;

        }

    }

The following is a more complete version

server {

        listen       443 ssl; 

        server_name  _;

        root         /var/www/html;

        ssl_certificate "/etc/cert/nginx/nginx-selfsigned.crt";

        ssl_certificate_key "/etc/cert/nginx/nginx-selfsigned.key";

        ssl_session_cache shared:SSL:1m;

        ssl_session_timeout  10m;

        ssl_ciphers HIGH:!aNULL:!MD5;

        ssl_prefer_server_ciphers on;

        location / {

            proxy_pass http://127.0.0.1:8080;

        }

        error_page 404 /404.html;

            location = /40x.html {

        }

        error_page 500 502 503 504 /50x.html;

            location = /50x.html {

        }

    }

Modify the firewall setting to allow traffic coming in and out

sudo firewall-cmd --permanent --zone=public --add-service=http 

sudo firewall-cmd --permanent --zone=public --add-service=https

sudo firewall-cmd --reload

If other ports, e.g. 8081 need to be opened:

sudo firewall-cmd --zone=public --add-port=8081/tcp --permanent

Note, because it's a self-signed certificate, when open the service from the browser, it would say it's a unsafe website and display all sort of alerts, simply skip it and proceed to the site

However, if calling the service from a program, the httprequest needs to accepts the certificate.

The following C# script (the bold line) is an example to accept the self signed certificate:

HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create("https://url/showmethemoney");

httpWebRequest.ContentType = "text/xml; encoding='utf-8'";

httpWebRequest.Method = "POST";

httpWebRequest.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => true; //accepts the certificate anyway

//send out the request

String xml = ".... you request message ...";

StreamWriter streamWriter = new StreamWriter(httpWebRequest.GetRequestStream());

streamWriter.Write(xml);

streamWriter.Flush();

streamWriter.Close();

//receive the response

HttpWebResponse httpResponse = (HttpWebResponse)httpWebRequest.GetResponse();

 StreamReader streamReader = new StreamReader(httpResponse.GetResponseStream());

String result = streamReader.ReadToEnd();

Console.WriteLine(result);

#############url rewrite in nginx#########################

Sometimes people need to rewrite the request url.

For example, people want to direct http://1.1.1.1/superservice/showmethemoney to http://1.1.1.2:8080/showmethemoney

and direct http://1.1.1.1/ironservice/showmethemoney to http://1.1.1.3:8080/showmethemoney

So the base url superservice / ironservice determines which gunicorn instance the request is forwarded to, and it needs to strip the base url before passing it to gunicorn.

A very simply way is to define the location as follows:

location /superservice {

  proxy_pass http://1.1.1.1:8080/;

}

The location /superservice will match the incoming url

When it come to proxy_pass, the additional / at the end of the url strips the matched prefix /superserice and pass the remainder on.

A more complex way is to use REWRITE

location /superservice{

             rewrite /superservice/(.*) /$1 break;  

             proxy_pass http://1.1.1.1:8080;

        }

The rewrite statement has a syntax of "rewrite regex replacement flag"

The reges "/superservice/(.*)" capture url starts with /superservice, and match the rest of the url as a group (.*)

The $1 refers to the first matching group which is the suburl, so it simply replaces the whole url with the suburl (i..e striping the superservice)

The flag "break" means stop processing the original url.