DEV Community

Cover image for Quickly Build Secure Microservices in Python
Ryan Gard
Ryan Gard

Posted on

Quickly Build Secure Microservices in Python

With a constant influx of the new Blazingly Fast™ web and microservice frameworks for every (new and old) programming language two things are always overlooked: development speed and security. This article is part a missive about my journey in trying to learn and build microservices and part a quick tutorial on how to build a secure microservice using Python.

Motivation!

Let's briefly talk about how an engineering manager (me) went about learning how to make a web app without every having written a single line of HTML. First, hats off to full-stack web programmers; I will never speak ill of what you do ever again. I came from a background of working predominantly on infrastructure and back-end software projects and naively thought making web apps was for the weak software engineers that couldn't hack it. I gravely misunderstood what it takes to make a web app work, let alone make one work well.

Obviously as a manager I was going to pick an architecture that I heard some of my direct reports and co-workers talk about without much insight (bad move #1), so I decided to write it using a microservice architecture based on Dokku (basically self-hosted Heroku) with a React SPA front-end. However, I wanted to host, test and deploy my web app on cloud infrastructure that I had complete control over. (bad move #2). Here are the conclusions I came to after developing a web app:

  1. Don't do any of the things I did when building your first web app.
  2. Start with NextJS.
  3. Writing a secure microservice can be really simple if you pick the right open source projects to build upon.

It is the third conclusion that the rest of this article is about because I felt like there wasn't very good guidance on how to do it even though all the parts already exist and work really well. The struggle I went through trying to build a first time web app made me realize that even a seasoned full-stack engineer probably had to try a ton of different things to get a good developer experience.

Performance??

I discovered quickly in my learning journey that there is a quite an obsession with Blazingly Fast™ in the web app developer community. Having been around a while in software engineering it's not new to me that engineers love measuring performance, but it felt even more intense when it came to full-stack engineers talking about the newest web frameworks. Since I come from a Python programming background I'm more interested in creating features fast to test my ideas rather than trying to squeeze every last drop of performance our of every CPU cycle or HTTP request. Hence why I have a great appreciation for NextJS.

I want to emphasize my approach to performance as to help readers understand, that I understand, there are PLENTY of web frameworks that will significantly out perform the approach I suggest in this article. I'm a firm believer in aiming for optimal functionality first then worry about performance optimization later. And the best way to discover optimal functionality is to quickly refactor and try new ideas.

Develop Fast

Even though I had saddled myself with undue burden with my architecture choices for my first web app, I did happen to stumble upon some great open source projects for creating microservices. After realizing the gems I had discovered I also realized that the projects I was utilizing were known, but not readily used in the specific recipe I had cooked up. Hence I did what any good developer does in this situation and made an open source project to tie them together: flask-ligand.

Why Use this Library?

Using Flask to create a REST based microservice is a daunting process which will definitely require the use of many different Flask extensions which will really slow down the process of actually writing a functional REST microservice that can be used safely in a production environment. This library seeks to use the best Flask extensions loosely combined together to deliver a delightful developer experience by providing the following functionality out-of-the-box:

  • Create database models using the industry standard SQLAlchemy ORM
  • Leverage those same database models to create schemas for marshalling data in and out of your Flask endpoints defined via Blueprints
  • Provide automatic SwaggerUI docs for quickly developing and testing your Flask application without the need of external tools like curl, Postman or Hoppscotch
  • Generate OpenAPI clients for a variety of languages
    • Endpoints for generating Python and Typescript clients already included!
  • Protect endpoints using JWT security with a OpenID Connect IAM like Auth0 or Keycloak
    • Optionally control access to endpoints using RBAC
  • Quickly enable pagination and ETag support for your endpoints
  • Easily manage database migrations using Alembic through the fantastic Flask-Migrate library and command-line tools

Simple Example

Below is a basic “Petstore example” which is a condensed version of what can be found in the flask-ligand Quickstart guide.

All example code in this section is copied straight from the example project source code.

Database Model

The DB.Model below will store our PetModel in the configured database and also act as the basis for a schema for defining the acceptable inputs and outputs of endpoints (a.k.a. Flask View) initiated later. The PetModel below demonstrates how to utilize sqlalchemy-utils to implement much stricter data typing than what is available out-of-the-box for SQLAlchemy.

class PetModel(DB.Model):  # type: ignore
    """Pet model class."""

    __tablename__ = "pet"

    id = DB.Column(UUIDType(binary=False), primary_key=True, default=uuid.uuid4)
    name = DB.Column(DB.String(length=NAME_MAX_LENGTH), nullable=False)
    description = DB.Column(DB.Text(), nullable=False)
    created_at = DB.Column(DB.DateTime, default=DB.func.current_timestamp(), nullable=False)
    updated_at = DB.Column(
        DB.DateTime, default=DB.func.current_timestamp(), onupdate=DB.func.current_timestamp(), nullable=False
    )
Enter fullscreen mode Exit fullscreen mode

Schemas

Define an AutoSchema to expose the model.

class PetSchema(AutoSchema):
    """Automatically generate schema from the 'Pet' model."""

    class Meta(AutoSchema.Meta):
        model = PetModel

    id = auto_field(dump_only=True)
    name = auto_field(required=True, validate=NAME_VALIDATOR)
    description = auto_field(required=False, validate=DESCRIPTION_VALIDATOR, load_default="")
    created_at = auto_field(dump_only=True)
    updated_at = auto_field(dump_only=True)
Enter fullscreen mode Exit fullscreen mode

Define a Schema to validate the query arguments for a subset of fields defined in the above AutoSchema for a Flask View that will be created later.

class PetQueryArgsSchema(Schema):
    """A schema for filtering Pets."""

    name = field_for(PetModel, "name", required=False, validate=NAME_VALIDATOR)
    description = field_for(PetModel, "description", required=False, validate=DESCRIPTION_VALIDATOR)
Enter fullscreen mode Exit fullscreen mode

Endpoints

Instantiate a Blueprint.

BLP = Blueprint(
    "Pets",
    __name__,
    url_prefix="/pets",
    description="Information about all the pets you love!",
)
Enter fullscreen mode Exit fullscreen mode

Use MethodView classes to organize resources, and decorate view methods with Blueprint.arguments and Blueprint.response to specify request/response (de)serialization and data validation.

Selectively secure endpoint REST verbs to require a valid JWT access token containing certain roles by using the jwt_role_required decorator. Provide a convenient “Authorize” button in the SwaggerUI documentation by providing the to the Blueprint.arguments.

@BLP.route("/")
class Pets(MethodView):
    @BLP.etag
    @BLP.arguments(PetQueryArgsSchema, location="query")
    @BLP.response(200, PetSchema(many=True))
    @BLP.paginate(SQLCursorPage)  # noqa
    def get(self, args: dict[str, Any]) -> list[PetModel]:
        """Get all pets or filter for a subset of pets."""

        items: list[PetModel] = PetModel.query.filter_by(**args)

        return items

    @BLP.etag
    @BLP.arguments(PetSchema)
    @BLP.response(201, PetSchema)
    @BLP.doc(security=BEARER_AUTH)
    @jwt_role_required(role="user")
    def post(self, new_item: dict[str, Any]) -> PetModel:
        """Add a new pet."""

        _we_love_pets(new_item["description"])

        item = PetModel(**new_item)
        DB.session.add(item)
        DB.session.commit()

        return item
Enter fullscreen mode Exit fullscreen mode

Use abort to return an error response.

def _we_love_pets(description: str) -> None:
    """
    Verify that the description doesn't include pet hate.

    Args:
        description: The pet description to validate.

    Raises:
        werkzeug.exceptions.HTTPException
    """

    if "hate" in description:
        abort(HTTPStatus(400), "No pet hatred allowed!")
Enter fullscreen mode Exit fullscreen mode

Create the App

Connect the models, schemas and views together by calling create_app followed by registering the Blueprints for the views.

def create_app(flask_env: str, api_title: str, api_version: str, openapi_client_name: str, **kwargs: Any) -> Flask:
    """
    Create Flask application.

    Args:
        flask_env: Specify the environment to use when launching the flask app. Available environments:

            ``prod``: Configured for use in a production environment.

            ``stage``: Configured for use in a development/staging environment.

            ``local``: Configured for use with a local Flask server.

            ``testing``: Configured for use in unit testing.
        api_title: The title (name) of the API to display in the OpenAPI documentation.
        api_version: The semantic version for the OpenAPI client.
        openapi_client_name: The package name to use for generated OpenAPI clients.
        kwargs: Additional settings to add to the configuration object or overrides for unprotected settings.

    Returns:
        Fully configured Flask application.

    Raises:
        RuntimeError: Attempted to override a protected setting, specified an additional setting that was not all
            uppercase or the specified environment is invalid.
    """

    app, api = flask_ligand.create_app(__name__, flask_env, api_title, api_version, openapi_client_name, **kwargs)

    views.register_blueprints(api)

    return app
Enter fullscreen mode Exit fullscreen mode

Run the App

To run the app in a Flask server simply create an app.py (and corresponding .flaskenv file) that calls the example projects create_app and specifies the Flask environment settings it should launch with.

try:
    app = flask_ligand_example.create_app(
        getenv("FLASK_ENV", "prod"),
        "Flask Ligand Example",
        flask_ligand_example.__version__,
        "flask-ligand-example-client",
    )
except RuntimeError as e:
    print(f"Service initialization failure!\nReason: {e}")
    exit(1)
Enter fullscreen mode Exit fullscreen mode

Conclusion

Even though I started off my journey going in the wrong direction I still can say that it was time well spent. Making mistakes will make doing things right (eventually) feel even better.

I couldn't have accomplished my goals without the incredible projects I used to build flask-ligand and I want to give them all a shout-out:

Top comments (0)