Quickly Build Secure Microservices in Python
Tags: python, microservices, security, tutorial
Published: 2022-10-13 20:52:00

With a constant influx of the new [_Blazingly Fast™_](https://www.youtube.com/ThePrimeagen) 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](https://dokku.com/) (basically self-hosted [Heroku](https://www.heroku.com/)) with a React [SPA](https://en.wikipedia.org/wiki/Single-page_application) 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](https://nextjs.org/).
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™_](https://www.youtube.com/ThePrimeagen) 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](https://nextjs.org/). 

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](https://flask-ligand.readthedocs.io/en/latest/).

## Why Use this Library?

Using [Flask](https://flask.palletsprojects.com/en/2.2.x/) to create a REST based microservice is a daunting process which will definitely require the use of many different [Flask extensions](https://flask.palletsprojects.com/en/2.2.x/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](https://flask.palletsprojects.com/en/2.2.x/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](https://www.sqlalchemy.org/)
- Leverage those same database models to create schemas for marshalling data in and out of your [Flask](https://flask.palletsprojects.com/en/2.2.x/) endpoints defined via Blueprints
- Provide automatic [SwaggerUI](https://swagger.io/tools/swagger-ui/) docs for quickly developing and testing your [Flask](https://flask.palletsprojects.com/en/2.2.x/) application without the need of external tools like curl, Postman or Hoppscotch
- [Generate OpenAPI clients](https://openapi-generator.tech/) for a variety of languages
    - Endpoints for generating Python and Typescript clients already included!
- Protect endpoints using JWT security with a [OpenID Connect](https://openid.net/connect/) IAM like [Auth0](https://auth0.com/) or [Keycloak](https://www.keycloak.org/)
    - Optionally control access to endpoints using [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control)
- Quickly enable [pagination](https://flask-smorest.readthedocs.io/en/latest/pagination.html) and [ETag](https://flask-smorest.readthedocs.io/en/latest/etag.html) support for your endpoints
- Easily manage database migrations using Alembic through the fantastic [Flask-Migrate](https://flask-migrate.readthedocs.io/en/latest/) 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](https://flask-ligand.readthedocs.io/en/latest/quickstart.html). 

All example code in this section is copied straight from the [example project](https://github.com/cowofevil/flask-ligand-example) source code.

### Database Model

The [DB.Model](https://flask-ligand.readthedocs.io/en/latest/api_reference.html#flask_ligand.extensions.database.DB) 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](https://flask.palletsprojects.com/en/latest/api/#flask.views.MethodView)) initiated later. The `PetModel` below demonstrates how to utilize [sqlalchemy-utils](https://sqlalchemy-utils.readthedocs.io/en/latest/data_types.html) to implement much stricter data typing than what is available out-of-the-box for [SQLAlchemy](https://docs.sqlalchemy.org/en/14/index.html).

```python
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
    )
```

### Schemas

Define an [AutoSchema](https://flask-ligand.readthedocs.io/en/latest/api_reference.html#flask_ligand.extensions.api.AutoSchema) to expose the model.

```python
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)
```

Define a [Schema](https://flask-ligand.readthedocs.io/en/latest/api_reference.html#flask_ligand.extensions.api.Schema) to validate the query arguments for a subset of fields defined in the above [AutoSchema](https://flask-ligand.readthedocs.io/en/latest/api_reference.html#flask_ligand.extensions.api.AutoSchema) for a [Flask View](https://flask.palletsprojects.com/en/latest/api/#flask.views.MethodView) that will be created later.

```python
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)
```

### Endpoints

Instantiate a [Blueprint](https://flask-ligand.readthedocs.io/en/latest/api_reference.html#flask_ligand.extensions.api.Blueprint).

```python
BLP = Blueprint(
    "Pets",
    __name__,
    url_prefix="/pets",
    description="Information about all the pets you love!",
)
```

Use [MethodView](https://flask.palletsprojects.com/en/latest/api/#flask.views.MethodView) classes to organize resources, and decorate view methods with [Blueprint.arguments](https://flask-smorest.readthedocs.io/en/latest/api_reference.html#flask_smorest.Blueprint.arguments) and [Blueprint.response](https://flask-smorest.readthedocs.io/en/latest/api_reference.html#flask_smorest.Blueprint.response) to specify request/response (de)serialization and data validation.

Selectively secure endpoint REST verbs to require a valid [JWT access token](https://auth0.com/blog/id-token-access-token-what-is-the-difference/) containing certain roles by using the [jwt_role_required decorator](https://flask-ligand.readthedocs.io/en/latest/api_reference.html#flask_ligand.extensions.jwt.jwt_role_required). Provide a convenient “Authorize” button in the [SwaggerUI documentation](https://swagger.io/tools/swagger-ui/) by providing the to the [Blueprint.arguments](https://flask-smorest.readthedocs.io/en/latest/api_reference.html#flask_smorest.Blueprint.arguments).

```python
@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
```

Use [abort](https://flask-ligand.readthedocs.io/en/latest/api_reference.html#flask_ligand.extensions.api.abort) to return an error response.

```python
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!")
```

### Create the App

Connect the models, schemas and views together by calling [create_app](https://flask-ligand.readthedocs.io/en/latest/api_reference.html#flask_ligand.create_app) followed by registering the Blueprints for the views.

```python
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
```

### Run the App

To run the app in a [Flask server](https://flask.palletsprojects.com/en/latest/quickstart/) simply create an app.py (and corresponding .flaskenv file) that calls the example projects `create_app` and specifies the [Flask environment settings](https://flask-ligand.readthedocs.io/en/latest/configuration.html#built-in-flask-environments) it should launch with.

```python
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)
```

## 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](https://flask-ligand.readthedocs.io/en/latest/) and I want to give them all a shout-out:

- [flask-smorest](https://flask-smorest.readthedocs.io/en/latest/index.html)
    - [apispec](https://apispec.readthedocs.io/en/latest/index.html)
    - [marshmallow](https://marshmallow.readthedocs.io/en/stable/index.html)
    - [webargs](https://webargs.readthedocs.io/en/latest/index.html)
- [flask-sqlalchemy](https://flask-sqlalchemy.palletsprojects.com/en/2.x/)
- [flask-jwt-extended](https://flask-jwt-extended.readthedocs.io/en/stable/)
- [flask-cors](https://flask-cors.readthedocs.io/en/latest/index.html)
- [flask-migrate](https://flask-migrate.readthedocs.io/en/latest/index.html)
- [marshmallow-sqlalchemy](https://marshmallow-sqlalchemy.readthedocs.io/en/latest/index.html)
    - [SQLAlchemy](https://docs.sqlalchemy.org/en/14/index.html)
- [sqlalchemy-utils](https://sqlalchemy-utils.readthedocs.io/en/latest/index.html)