Enterprise Software with the Flask Microframework
Typically used within the context of web application frameworks, a microframework provides only the most essential features, thereby allowing the developer to make judicious decisions...
Introduction
The term "enterprise software" conjures up many feelings within the heart of a software developer, and most are not positive. Overly complicated, monolithic, inflexible, very expensive, bloated, and painful to support, "enterprise software" has a bad reputation. The good news is that it's never been easier to write software with qualities opposite to those listed above, even at enterprise scale and to enterprise standards. In stark contrast to the idea of "enterprise software," let us consider the humble microframework.
Typically used within the context of web application frameworks, a microframework provides only the most essential features, thereby allowing the developer to make judicious decisions about which additional dependencies to include. One of my favorites is Flask, a microframework for the python programming language. Whether you are implementing a series of microservices or simply beginning a new project and looking to keep things small and simple, growing organically, Flask is a great starting point.
Speaking of starting points, let’s look at a simple Flask application.
import flask
app = flask.Flask(__name__)
@app.route("/")
def hello_universe():
return "<p>Hello, universe!</p>"
As you can see, this code creates a Flask application and decorates a simple function, designating it as a route. When a web request is made to the root path, a familiar message is returned as a response. As we are building enterprise-grade software here, we skip the world and go straight for the universe. But now let’s think about enterprise software and see how we can improve the above. Here we will focus on three widely agreed-upon characteristics of enterprise software: manageability, security, and scalability.
Enterprise software must be easy to manage. When problems arise, they must be easy to diagnose. As such, logging is an essential aspect of mission-critical software. There are many python libraries that can assist here, and one such library is structlog. Let’s log every request and response we process, and since good logs are JSON logs, let’s render our logs in that format.
import structlog
from structlog.processors import JSONRenderer
An example of an essential feature is facilitating a request/response cycle. An HTTP request is received and routed to the appropriate part of your code, where the developer is responsible for implementing the desired functionality and returning an HTTP response.
import flask
structlog.configure(
processors=[
JSONRenderer(indent=2),
]
)
log = structlog.get_logger()
app = flask.Flask(__name__)
@app.before_request
def log_before_request():
log.info(
"request incoming",
url=flask.request.url,
args=flask.request.args,
json=flask.request.json,
method=flask.request.method,
)
@app.after_request
def log_after_request(response):
log.info(
"request complete",
url=flask.request.url,
response={"status": response.status, "payload": response.json},
)
return response
@app.route("/")
def hello_universe():
return "<p>Hello, universe!</p>"
Here we have defined functions that get executed before and after each request. Now we have helpful logs that will get written upon every request/response cycle, no matter how many endpoints we add to our system. Our logs are easy to read and understand. In an enterprise scenario, our logs will likely be exported to an enterprise monitoring solution, where they will be easy to aggregate and search.
{
"url": "http://127.0.0.1:5000/",
"args": {},
"json": null,
"method": "GET",
"event": "request incoming"
}
{
"url": "http://127.0.0.1:5000/",
"response": {
"status": "200 OK",
"payload": null
},
"event": "request complete"
}
Enterprise software must be secure. Therefore, authentication and authorization are essential. Let’s add an endpoint for logging in the user. In most cases we will defer the actual authentication to an enterprise authentication/authorization platform, via OpenID Connect, SAML, or similar.
@app.route("/login/", methods=["POST"])
def login():
username = flask.request.json["username"]
password = flask.request.json["password"]
if authenticate(username, password):
"""Authenticate u/p with your enterprise auth platform."""
return f"<p>Hello, {username}</p>"
flask.abort(401)
Here we have defined a new route for a login endpoint that accepts only POST requests. We simply read the username and password provided by the user, and then we pass that data along to an authenticate function, which is assumed to be implemented elsewhere -- that function is where you would interface with your enterprise auth platform. If everything checks out, we greet the user. Otherwise, an exception is raised that is handled sensibly by Flask, resulting in an HTTP 400 Bad Request for missing username or password, or an HTTP 401 Unauthorized if the credentials are incorrect.
That was easy, but now the user’s password is being logged. Part of maintaining a secure system is ensuring that Personally Identifiable Information is not exposed. Let’s systematically redact some log messages. This entails defining a new function that takes three parameters and enrolling that function into our list of log processors. Note that the renderer must always come last in the list of processors.
SENSITIVE_KEYS = ("password", "passwd", "secret")
def redact_secrets(_, __, event_dict):
"""For GET requests, json exists but is null, so
ensure we're always dealing with a dictionary."""
json = event_dict.get("json", {}) or {}
for k, v in json.items():
for word in SENSITIVE_KEYS:
if word in k.lower():
json[k] = "*******"
return event_dict
structlog.configure(
processors=[
redact_secrets,
JSONRenderer(indent=2),
]
)
Now anytime a password or secret is passed to any of our endpoints, we will wisely redact the value from our logs.
{
"url": "http://localhost:5000/login/",
"args": {},
"json": {
"username": "stuart",
"password": "*******",
"secret_key": "*******"
},
"method": "POST",
"event": "request incoming"
}
Conclusion
Finally, enterprise software must be scalable. Flask conforms to a Web Server Gateway Interface convention. As such it can be deployed in a variety of ways. For ultimate scalability, a serverless deployment is perhaps the best option. There are technologies that can greatly simplify such deployments, such as Zappa. By installing Zappa and issuing a few commands, your Flask application can be up and running in the AWS Cloud within minutes. As you improve your system and develop new features, new releases can be deployed by simply issuing a Zappa update command. Using a serverless approach, it doesn’t matter if you are handling a handful of requests per day or hundreds per second. The system scales to meet demand, and you pay only for the services you use.
Starting small is no guarantee that your completed software project will be successful and avoid embodying the negative qualities so often associated with enterprise software. However, by focusing on simplicity and extensibility, a microframework like Flask can provide the foundation you need to create robust, flexible, and relatively pain-free software. With experience, discipline, and wisdom, your enterprise-grade software can be a joy, to both your users and developers. If you haven’t yet experimented with microframeworks, now is the time.
The JBS Quick Launch Lab
Free Qualified Assessment
Quantify what it will take to implement your next big idea!
Our assessment session will deliver tangible timelines, costs, high-level requirements, and recommend architectures that will work best. Let JBS prove to you and your team why over 24 years of experience matters.