Creating Microservices with Python
- Introduction to Microservices
- Introduction to FastAPI
- Microservice Data Management Patterns
- Creating a Python Microservice in Docker
- Conclusion and Next Step
As a Python developer you might have heard about the term microservices, and want to build a Python microservice by yourself. Microservices is a great architecture for building highly scalable applications. Before starting to build your application using the microservice, you must be familiar with the benefits and drawbacks of using microservices. In this article, you will learn the benefits and drawbacks of using microservices. You will also learn how you can build your own microservice and deploy it using the Docker Compose.
In this tutorial you'll learn:
- What the benefits and drawbacks of microservices are
- Why you should build microservice with Python
- How to build REST API using FastAPI and PostgreSQL
- How to build microservice using FastAPI
- How to run microservices using
docker-compose
- How to manage microservices using Nginx
You will first build a simple REST API using FastAPI and then use PostgreSQL as our database. You will then extend the same application to a microservice.
Introduction to Microservices
Microservice is the approach of breaking down large monolith application into individual applications specializing in a specific service/functionality. This approach is often known as Service-Oriented Architecture or SOA.
In monolithic architecture, every business logic resides in the same application. Application services such as user management, authentication, and other features use the same database.
In a microservice architecture, the application is broken down into several separate services that run in separate processes. There is a different database for different functionality of the application and the services communicate with each other using the HTTP, AMQP, or a binary protocol like TCP, depending on the nature of each service. Inter-service communication can also be performed using the message queues like RabbitMQ, Kafka or Redis.
Benefits of Microservice
The microservice architecture comes with lots of benefits. Some of these benefits are:
Loosely coupled application means the different services can be build using the technologies that suit them best. So, the development team is not bounded to the choices made while starting the project.
Since the services are responsible for specific functionality which makes it easier to understand and keep the application under control.
Application scaling also becomes easier because if one of the services requires high GPU usage then only the server consisting that service needs to have high GPU and others can run on a normal server.
Drawbacks of Microservice
The microservice architecture is not a silver bullet that solves all your problems, it comes with its drawbacks too. Some of these drawbacks are:
Since different services use the different database the transactions involving more than one service needs to use eventual consistency.
Perfect splitting of the services is very difficult to achieve at the first try and this needs to be iterated before coming with the best possible separation of the services.
Since the services communicate with each other through the use of network interaction, this makes the application slower due to the network latency and slow service.
Why Microservice in Python
Python is a perfect tool for building micro-services because it comes with a great community, easy learning curve and tons of libraries. Due to the introduction of asynchronous programming in Python, web frameworks with performance on-par with GO and Node.js, has emerged.
Introduction to FastAPI
FastAPI is a modern, high-performance, web framework, which comes with tons of cool features like auto-documentation based on OpenAPI and built-in serialization and validation library. See here for the list of all cool features in FastAPI.
Why FastAPI
Some of the reason why I think FastAPI is a great choice for building microservices in Python is:
- Auto documentation
- Async/Await support
- Built-in validation and serialization
- 100% type annotated so autocompletion works great
Installing FastAPI
Before installing FastAPI create a new directory movie_service
and create a new virtual environment inside the newly created directory using virtualenv.
If you haven't already installed virtualenv
:
pip install virtualenv
Now, create a new virtual environment.
virtualenv env
If you are on Mac/Linux you can activate the virtual environment using the command:
source ./env/bin/activate
Windows users can run this command instead:
.\env\Scripts\activate
Finally, Your are ready to install FastAPI, run the following command:
pip install fastapi
Since FastAPI doesn't come with inbuilt service, you need to install uvicorn
for it to run. uvicorn
is an ASGI server which allows us to use async/await features.
Install uvicorn
using the command
pip install uvicorn
Creating Simple REST API using FastAPI
Before You start building a microservice using FastAPI, let's learn the basics of FastAPI. Create a new directory app
and a new file main.py
inside the newly-created directory.
Add the following code in main.py
.
#~/movie_service/app/main.py
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def index():
return {"Real": "Python"}
Here you first import and instantiate the FastAPI and then register the root endpoint /
which then returns a JSON
.
You can run the application server using uvicorn app.main:app --reload
. Here app.main
indicates you use main.py
file inside the app
directory and :app
indicates our FastAPI
instance name.
You can access the app from http://127.0.0.1:8000. To access the cool automatic documentation, head over to http://127.0.0.1:8000/docs. You can play around and interact with your API from the browser itself.
Let's add some CRUD functionality to our application.
Update your main.py
to look like the following:
#~/movie_service/app/main.py
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
fake_movie_db = [
{
'name': 'Star Wars: Episode IX - The Rise of Skywalker',
'plot': 'The surviving members of the resistance face the First Order once again.',
'genres': ['Action', 'Adventure', 'Fantasy'],
'casts': ['Daisy Ridley', 'Adam Driver']
}
]
class Movie(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
@app.get('/', response_model=List[Movie])
async def index():
return fake_movie_db
As you can see you have created a new class Movie
which extends BaseModel
from pydantic.
The Movie
model contains the name, phot, genres, and casts. Pydantic comes built-in with FastAPI which makes making models and request validation a breeze.
If you head over to the docs site you can see there are fields of our Movies model mentioned already in the example response section. This is possible because you have defined the response_model
in our route definition.
Now, let's add the endpoint to add a movie to our movies list.
Add a new endpoint definition to handle the POST
request.
@app.post('/', status_code=201)
async def add_movie(payload: Movie):
movie = payload.dict()
fake_movie_db.append(movie)
return {'id': len(fake_movie_db) - 1}
Now, head over to the browser and test the new API. Try adding a movie with an invalid field or without the required fields and see that the validation is automatically handled by FastAPI.
Let's add a new endpoint to update the movie.
@app.put('/{id}')
async def update_movie(id: int, payload: Movie):
movie = payload.dict()
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
fake_movie_db[id] = movie
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
Here id
is the index of our fake_movie_db
list.
Note: Remember to import HTTPException
from fastapi
Now you can also add the endpoint to delete the movie.
@app.delete('/{id}')
async def delete_movie(id: int):
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
del fake_movie_db[id]
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
Before you move forward, let's structure our app in a better way. Create a new folder api
inside app
and create a new file movies.py
inside the recently created folder. Move all the routes related codes from main.py
to movies.py
. So, the movies.py
should look like the following:
#~/movie-service/app/api/movies.py
from typing import List
from fastapi import Header, APIRouter
from app.api.models import Movie
fake_movie_db = [
{
'name': 'Star Wars: Episode IX - The Rise of Skywalker',
'plot': 'The surviving members of the resistance face the First Order once again.',
'genres': ['Action', 'Adventure', 'Fantasy'],
'casts': ['Daisy Ridley', 'Adam Driver']
}
]
movies = APIRouter()
@movies.get('/', response_model=List[Movie])
async def index():
return fake_movie_db
@movies.post('/', status_code=201)
async def add_movie(payload: Movie):
movie = payload.dict()
fake_movie_db.append(movie)
return {'id': len(fake_movie_db) - 1}
@movies.put('/{id}')
async def update_movie(id: int, payload: Movie):
movie = payload.dict()
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
fake_movie_db[id] = movie
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
@movies.delete('/{id}')
async def delete_movie(id: int):
movies_length = len(fake_movie_db)
if 0 <= id <= movies_length:
del fake_movie_db[id]
return None
raise HTTPException(status_code=404, detail="Movie with given id not found")
Here you registered a new API route using the APIRouter from FastAPI.
Also, create a new file models.py
inside api
where you will be keeping our Pydantic models.
#~/movie-service/api/models.py
from typing import List
from pydantic import BaseModel
class Movie(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
Now register this new routes file in main.py
#~/movie-service/app/main.py
from fastapi import FastAPI
from app.api.movies import movies
app = FastAPI()
app.include_router(movies)
Finally, our application directory structure looks like this:
movie-service
βββ app
β βββ api
β β βββ models.py
β β βββ movies.py
β |ββ main.py
βββ env
Make sure your application is working properly before you move forward.
Using PostgreSQL Database with FastAPI
Previously, you used fake Python list to add movies but now you are finally ready to use an actual database for this purpose. You are going to use PostgreSQL for this purpose. Install PostgreSQL if you haven't already. After installing the PostgreSQl create a new database, I am going to call mine movie_db
.
You are going to use encode/databases to connect to the database using async
and await
support. Learn more about async/await
in Python here
Install the required library using:
pip install 'databases[postgresql]'
this will install sqlalchemy
and asyncpg
as well, which are required for working with PostgreSQL.
Create a new file inside api
and call it db.py
. This file will contain the actual database model for our REST API.
#~/movie-service/app/api/db.py
from sqlalchemy import (Column, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URL = 'postgresql://movie_user:movie_password@localhost/movie_db'
engine = create_engine(DATABASE_URL)
metadata = MetaData()
movies = Table(
'movies',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('plot', String(250)),
Column('genres', ARRAY(String)),
Column('casts', ARRAY(String))
)
database = Database(DATABASE_URL)
Here, DATABASE_URI
is the URL used to connect to the PostgreSQL database. Here movie_user
is the name of the database user, movie_password
is the password of the database user and movie_db
is the name of the database.
Just like you would to in SQLAlchemy you have created the table for the movies database.
Update main.py
to connect to the database. main.py
should look like the following:
#~/movie-service/app/main.py
from fastapi import FastAPI
from app.api.movies import movies
from app.api.db import metadata, database, engine
metadata.create_all(engine)
app = FastAPI()
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
app.include_router(movies)
FastAPI provides some event handlers which you can use to connect to our database when the application starts and disconnect when it shuts down.
Update movies.py
so that it uses a database instead of a fake Python list.
#~/movie-service/app/api/movies.py
from typing import List
from fastapi import Header, APIRouter
from app.api.models import MovieIn, MovieOut
from app.api import db_manager
movies = APIRouter()
@movies.get('/', response_model=List[MovieOut])
async def index():
return await db_manager.get_all_movies()
@movies.post('/', status_code=201)
async def add_movie(payload: MovieIn):
movie_id = await db_manager.add_movie(payload)
response = {
'id': movie_id,
**payload.dict()
}
return response
@movies.put('/{id}')
async def update_movie(id: int, payload: MovieIn):
movie = payload.dict()
fake_movie_db[id] = movie
return None
@movies.put('/{id}')
async def update_movie(id: int, payload: MovieIn):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
update_data = payload.dict(exclude_unset=True)
movie_in_db = MovieIn(**movie)
updated_movie = movie_in_db.copy(update=update_data)
return await db_manager.update_movie(id, updated_movie)
@movies.delete('/{id}')
async def delete_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return await db_manager.delete_movie(id)
Let's add db_manager.py
to manipulate our database.
#~/movie-service/app/api/db_manager.py
from app.api.models import MovieIn, MovieOut, MovieUpdate
from app.api.db import movies, database
async def add_movie(payload: MovieIn):
query = movies.insert().values(**payload.dict())
return await database.execute(query=query)
async def get_all_movies():
query = movies.select()
return await database.fetch_all(query=query)
async def get_movie(id):
query = movies.select(movies.c.id==id)
return await database.fetch_one(query=query)
async def delete_movie(id: int):
query = movies.delete().where(movies.c.id==id)
return await database.execute(query=query)
async def update_movie(id: int, payload: MovieIn):
query = (
movies
.update()
.where(movies.c.id == id)
.values(**payload.dict())
)
return await database.execute(query=query)
Let's update our models.py
so that you can use the Pydantic model with the sqlalchemy table.
#~/movie-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class MovieIn(BaseModel):
name: str
plot: str
genres: List[str]
casts: List[str]
class MovieOut(MovieIn):
id: int
class MovieUpdate(MovieIn):
name: Optional[str] = None
plot: Optional[str] = None
genres: Optional[List[str]] = None
casts: Optional[List[str]] = None
Here MovieIn
is the base model that you use to add the movie to the database. You have to add the id
to this model while getting it from the database, hence the MovieOut
model. MovieUpdate
model allows us to set the values in the model to be optional so that while updating the movie only the field that needs to be updated can be sent.
Now, head over to the browser documentation site and start playing with the API.
Microservice Data Management Patterns
Managing data in microservice is one of the most challenging aspects of building a microservice. Since different functions of the application are handled by different services, usage of a database can be tricky.
Here are some patterns that you can use to manage data flow in the application.
Database Per Service
Using a database per service is great if you want your microservices to be as loosely coupled as possible. Having a different database per service allows us to scale different services independently. A transaction involving multiple databases is done through well-defined APIs. This comes with its drawback as implementing business transactions involving multiple services is not straightforward. Also, the addition of network overhead makes this less efficient to use.
Shared Database
If there are lots of transactions involving multiple services it is better to use a shared database. This comes with the benefits of highly consistent application but takes away most of the benefits that come with the microservices architecture. Developers working on one service needs to coordinate with the schema changes in other services.
API Composition
In transactions involving multiple databases, API composer acts as an API gateway and executes API calls to other microservices in the required order. Finally, the results from each microservices are returned to the client service after performing an in-memory join. The downside of this approach is inefficient in-memory joins of a large dataset.
Creating a Python Microservice in Docker
The pain of deploying the microservice can be greatly reduced by using the Docker. Docker helps to encapsulate each service and scale them independently.
Installing Docker and Docker Compose
If you haven't already install docker in your system. Verify if the docker is installed by running the command docker
. After you have done installing Docker, install Docker Compose. Docker Compose is used for defining and running multiple Docker containers. It also helps in easy interaction between them.
Creating Movies Service
Since a lot of the work for building a movie service is already done while getting started with the FastAPI, you are going to reuse the code you have already written. Create a brand new folder, I am going to call mine python-microservices
. Move the code you wrote earlier which I had named movie-service
.
So, the folder structure would look like this:
python-microservices/
βββ movie-service/
βββ app/
βββ env/
First of all, let's create a requirements.txt
file where you are going to keep all the dependencies you are going to use in our movie-service
.
Create a new file requirements.txt
inside movie-service
and add the following to it:
asyncpg==0.20.1
databases[postgresql]==0.2.6
fastapi==0.48.0
SQLAlchemy==1.3.13
uvicorn==0.11.2
httpx==0.11.1
You have used all the libraries mentioned there except httpx which you are going to use while making service to service API call.
Create a Dockerfile
inside movie-service
with the following contents:
FROM python:3.8-slim
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN apt-get update \
&& apt-get install gcc -y \
&& apt-get clean
RUN pip install -r /app/requirements.txt \
&& rm -rf /root/.cache/pip
COPY . /app/
Here first, you define which Python version you want to use. Then set the WORKDIR
to be inside app
folder inside the Docker container. After that gcc
is installed which is required by the libraries that you are using in the application.
Finally, install all dependencies in requirements.txt
and copy all the files inside movie-service/app
.
Update db.py
and replace
DATABASE_URI = 'postgresql://movie_user:movie_password@localhost/movie_db'
with
DATABASE_URI = os.getenv('DATABASE_URI')
Note: Don't forget to import os
on the top of the file.
You need to do this so that you can latter provide DATABASE_URI
as an environment variable.
Also, update main.py
and replace
app.include_router(movies)
with
app.include_router(movies, prefix='/api/v1/movies', tags=['movies'])
Here, you have added prefix
/api/v1/movies
so, that managing different version of API becomes easier. Also, tags make finding API related to movies
easier in FastAPI docs.
Also, you need to update our models so that the casts
stores the cast's id instead of the actual name. So, update the models.py
to look like this:
#~/python-microservices/movie-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class MovieIn(BaseModel):
name: str
plot: str
genres: List[str]
casts_id: List[int]
class MovieOut(MovieIn):
id: int
class MovieUpdate(MovieIn):
name: Optional[str] = None
plot: Optional[str] = None
genres: Optional[List[str]] = None
casts_id: Optional[List[int]] = None
Likewise, you need to update the database tables, let's update db.py
:
#~/python-microservices/movie-service/app/api/db.py
import os
from sqlalchemy import (Column, DateTime, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URL = os.getenv('DATABASE_URL')
engine = create_engine(DATABASE_URL)
metadata = MetaData()
movies = Table(
'movies',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('plot', String(250)),
Column('genres', ARRAY(String)),
Column('casts_id', ARRAY(Integer))
)
database = Database(DATABASE_URL)
Now, update movies.py
to check if the cast with the given id presents in cast service before adding a new movie or updating a movie.
#~/python-microservices/movie-service/app/api/movies.py
from typing import List
from fastapi import APIRouter, HTTPException
from app.api.models import MovieOut, MovieIn, MovieUpdate
from app.api import db_manager
from app.api.service import is_cast_present
movies = APIRouter()
@movies.post('/', response_model=MovieOut, status_code=201)
async def create_movie(payload: MovieIn):
for cast_id in payload.casts_id:
if not is_cast_present(cast_id):
raise HTTPException(status_code=404, detail=f"Cast with id:{cast_id} not found")
movie_id = await db_manager.add_movie(payload)
response = {
'id': movie_id,
**payload.dict()
}
return response
@movies.get('/', response_model=List[MovieOut])
async def get_movies():
return await db_manager.get_all_movies()
@movies.get('/{id}/', response_model=MovieOut)
async def get_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return movie
@movies.put('/{id}/', response_model=MovieOut)
async def update_movie(id: int, payload: MovieUpdate):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
update_data = payload.dict(exclude_unset=True)
if 'casts_id' in update_data:
for cast_id in payload.casts_id:
if not is_cast_present(cast_id):
raise HTTPException(status_code=404, detail=f"Cast with given id:{cast_id} not found")
movie_in_db = MovieIn(**movie)
updated_movie = movie_in_db.copy(update=update_data)
return await db_manager.update_movie(id, updated_movie)
@movies.delete('/{id}', response_model=None)
async def delete_movie(id: int):
movie = await db_manager.get_movie(id)
if not movie:
raise HTTPException(status_code=404, detail="Movie not found")
return await db_manager.delete_movie(id)
Let's add a service to make an API call to cast service:
#~/python-microservices/movie-service/app/api/service.py
import os
import httpx
CAST_SERVICE_HOST_URL = 'http://localhost:8002/api/v1/casts/'
url = os.environ.get('CAST_SERVICE_HOST_URL') or CAST_SERVICE_HOST_URL
def is_cast_present(cast_id: int):
r = httpx.get(f'{url}{cast_id}')
return True if r.status_code == 200 else False
You make an api call to get the cast with the given id and return true if the cast exists and false otherwise.
Creating Casts Service
Similar to a movie-service
, for creating a casts-service
you are going to use FastAPI and PostgreSQL database.
Create a folder structure like the following:
python-microservices/
.
βββ cast_service/
β βββ app/
β β βββ api/
β β β βββ casts.py
β β β βββ db_manager.py
β β β βββ db.py
β β β βββ models.py
β β βββ main.py
β βββ Dockerfile
β βββ requirements.txt
βββ movie_service/
...
Add the following to requirements.txt
:
asyncpg==0.20.1
databases[postgresql]==0.2.6
fastapi==0.48.0
SQLAlchemy==1.3.13
uvicorn==0.11.2
Dockerfile
:
FROM python:3.8-slim
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN apt-get update \
&& apt-get install gcc -y \
&& apt-get clean
RUN pip install -r /app/requirements.txt \
&& rm -rf /root/.cache/pip
COPY . /app/
main.py
#~/python-microservices/cast-service/app/main.py
from fastapi import FastAPI
from app.api.casts import casts
from app.api.db import metadata, database, engine
metadata.create_all(engine)
app = FastAPI()
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()
app.include_router(casts, prefix='/api/v1/casts', tags=['casts'])
You have added the prefix of /api/v1/casts
so that managing the API becomes easier. Also, adding tags
makes finding docs related to casts
in the FastAPI docs easier.
casts.py
#~/python-microservices/cast-service/app/api/casts.py
from fastapi import APIRouter, HTTPException
from typing import List
from app.api.models import CastOut, CastIn, CastUpdate
from app.api import db_manager
casts = APIRouter()
@casts.post('/', response_model=CastOut, status_code=201)
async def create_cast(payload: CastIn):
cast_id = await db_manager.add_cast(payload)
response = {
'id': cast_id,
**payload.dict()
}
return response
@casts.get('/{id}/', response_model=CastOut)
async def get_cast(id: int):
cast = await db_manager.get_cast(id)
if not cast:
raise HTTPException(status_code=404, detail="Cast not found")
return cast
db_manager.py
#~/python-microservices/cast-service/app/api/db_manager.py
from app.api.models import CastIn, CastOut, CastUpdate
from app.api.db import casts, database
async def add_cast(payload: CastIn):
query = casts.insert().values(**payload.dict())
return await database.execute(query=query)
async def get_cast(id):
query = casts.select(casts.c.id==id)
return await database.fetch_one(query=query)
db.py
#~/python-microservices/cast-service/app/api/db.py
import os
from sqlalchemy import (Column, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URI = os.getenv('DATABASE_URI')
engine = create_engine(DATABASE_URI)
metadata = MetaData()
casts = Table(
'casts',
metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50)),
Column('nationality', String(20)),
)
database = Database(DATABASE_URI)
models.py
#~/python-microservices/cast-service/app/api/models.py
from pydantic import BaseModel
from typing import List, Optional
class CastIn(BaseModel):
name: str
nationality: Optional[str] = None
class CastOut(CastIn):
id: int
class CastUpdate(CastIn):
name: Optional[str] = None
Running the microservice using Docker Compose
To run the microservices, create a docker-compose.yml
file and add the following to it:
version: '3.7'
services:
movie_service:
build: ./movie-service
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
volumes:
- ./movie-service/:/app/
ports:
- 8001:8000
environment:
- DATABASE_URI=postgresql://movie_db_username:movie_db_password@movie_db/movie_db_dev
- CAST_SERVICE_HOST_URL=http://cast_service:8000/api/v1/casts/
movie_db:
image: postgres:12.1-alpine
volumes:
- postgres_data_movie:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=movie_db_username
- POSTGRES_PASSWORD=movie_db_password
- POSTGRES_DB=movie_db_dev
cast_service:
build: ./cast-service
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
volumes:
- ./cast-service/:/app/
ports:
- 8002:8000
environment:
- DATABASE_URI=postgresql://cast_db_username:cast_db_password@cast_db/cast_db_dev
cast_db:
image: postgres:12.1-alpine
volumes:
- postgres_data_cast:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=cast_db_username
- POSTGRES_PASSWORD=cast_db_password
- POSTGRES_DB=cast_db_dev
volumes:
postgres_data_movie:
postgres_data_cast:
Here you have 4 different services, movie_service, a database for movie_service, cast_service and a database for cast service. You have exposed movie_service
to port 8001
similarly cast_service
to port 8002
.
For the database, you have used volumes so that the data doesn't get destroyed when the docker container is shutdown.
Run the docker-compose using the command:
docker-compose up -d
This creates the docker image if it doesn't already exist and runs them.
Head over to http://localhost:8002/docs to add a cast in casts service. Similarly, http://localhost:8001/docs to add the movie in the movie service.
Using Nginx to Access Both Services using a Single Host Address
You have deployed the microservices using Docker compose but there is one minor problem. Each of the microservices needs to be accessed using a different Port. You can solve this issue using Nginx reverse proxy, using Nginx you can direct the request add a middleware which routes our requests to different services based on the API URL.
Add a new file nginx_config.conf
inside python-microservices
with the following contents.
server {
listen 8080;
location /api/v1/movies {
proxy_pass http://movie_service:8000/api/v1/movies;
}
location /api/v1/casts {
proxy_pass http://cast_service:8000/api/v1/casts;
}
}
Here you are running the Nginx at port 8080
and routing the requests to movie service if the endpoint starts with /api/v1/movies
and similarly to cast service if the endpoint starts with /api/v1/casts
Now, you need to add the nginx service in our docker-compose-yml
. Add the following service after cast_db
service:
nginx:
image: nginx:latest
ports:
- "8080:8080"
volumes:
- ./nginx_config.conf:/etc/nginx/conf.d/default.conf
depends_on:
- cast_service
- movie_service
Now, shut down the containers with the command:
docker-compose down
And run it back again with:
docker-compose up -d
Now, you can access both movie service and cast service at port 8080
.
Head over to http://localhost:8080/api/v1/movies/ to get the list of movies.
Now, you might be wondering how you can access the docs of the services. For that update main.py
of movie service and replace
app = FastAPI()
with
app = FastAPI(openapi_url="/api/v1/movies/openapi.json", docs_url="/api/v1/movies/docs")
Similarly, for cast service replace it with
app = FastAPI(openapi_url="/api/v1/casts/openapi.json", docs_url="/api/v1/casts/docs")
Here, you changed which endpoint the docs are served and from where the openapi.json
is served.
Now, you can access the docs from http://localhost:8080/api/v1/movies/docs for movie service and from http://localhost:8080/api/v1/casts/docs for casts service.
If you are stuck at some point or just want to have a look at the complete code, head over to the Github Repo
Conclusion and Next Step
The microservice architecture is great for breaking down a large monolith application into separate business logics but this comes with the complication too. Python is great for building microservice because of the developer experience and tons of packages and frameworks to make developers more productive.
Deploying microservices has become easier thanks to Docker. Learn more on How to develop microservices using Docker, and Docker Compose
Want me to cover any topic? Let me know at twitter or write a comment down below.
Top comments (45)
Hello @paurakhsharma , I tried using these steps to create another project but when I try inserting in the database, I get
asyncpg.exceptions.DataError: invalid input for query argument $1: 213127865166 (value out of int32 range)
. What could be wrong? Kindly assist!You can see this stack overflow for more
https://stackoverflow.com/questions/63404139/asyncpg-exceptions-dataerror-invalid-input-for-query-argument-1-217027642536
I see you have already accepted the answer in StackOverflow, good luck :)
Thanks a lot for your introduction with FastAPI.
Consider using
wemake-python-styleguide
for your next FastAPI project.It is the strictest Python linter out there. It will help you to find possible errors in your code early, show you possible refactoring opportunities, and enforce consistency across the project's codebase.
Check it out:
wemake-services / wemake-python-styleguide
The strictest and most opinionated python linter ever!
wemake-python-styleguide
Welcome to the strictest and most opinionated Python linter ever.
wemake-python-styleguide
is actually a flake8 plugin with some other plugins as dependencies.Quickstart
You will also need to create a
setup.cfg
file with the configuration.Try it online!
We highly recommend to also use:
Running
This app is still just good old
flake8
! And it won't change your existing workflow.See "Usage" section in the docs for examples and integrations.
We also support GitHub Actions as first class-citizens. Try it out!
Strict is the new cool
Strict linting offers the following benefits to developers and companies:
Sure, I will definitely have a look at it.
Thanks for sharing π
Thanks a lot for your article, really appreciate that !
But I have a little problem, when I clone the code and run "docker-compose up -d", it return
"502 Bad Gateway"
Do you know how to fix this ? I quite new to docker and nginx :v
Thanks in advance !
I was facing the issue, fixed by solving this BUG, because in docker-compose.yml we used DATABASE_URI under movie-service, change it in movie-service\app\api\db.py as well, replace everywhere you have DATABASE_URL to DATABASE_URI
same here
[SOLVED]
Check your docker-compose logs to see what problem came from.
docker-compose logs
Major bugs:
(1) you need to initialize first de dabases in the "docker-compose.yml". Otherwise the services try to conect (in my case) to them without being initialize.
(2) change DATABASE_URL for DATABASE_URI here:
"""
~/python-microservices/movie-service/app/api/db.py
import os
from sqlalchemy import (Column, DateTime, Integer, MetaData, String, Table,
create_engine, ARRAY)
from databases import Database
DATABASE_URL = os.getenv('DATABASE_URL')
"""
Helped me, a lot, thanks
And thanks indeed for the post.
Excellent article. Looking forward to read more of your posts.
If you can do a variation with mongodb would be much appreciated.
Not much content out there on that stack.
Thank you for following the article. I am glad it helped you.
Thank you for the suggestion, I will try to come up with the article for using FastAPI with Momgodb
I earned another fan. Yes, please make an article FastAPI + Mongo... pls, pls plssssss
This is a great, great post that touches many things. FastAPI has been on my back burner for quite some time to try out so thanks for sharing!
A question about async database queries though: Last time I looked, depending on where your database is (same network boundary as your app), you can actually incur a performance penalty by using the event loop. Do you know if that's still the case? I'm imagining a lot of work has gone into that since I last looked so maybe it's not as noticeable. That said, you'd still be freeing up the worker with the async call, so depending on your application needs, taking that performance hit may still be best.
I am glad that you liked it.
I am not so sure about it either.
This was a much needed introduction to FastAPI and possibly one of my favorites in terms of how comprehensive it was in terms of bringing in Docker, the use of Databases, and nginx configuration.
Can I request that you do something about security as well? Like authentication schemes within fast-api?
Hey Paurakh, first of all, thanks for the post very helpful. I am trying to understand a few things here any guidance would be appreciated. I am a novice so, pls excuse if my questions are lame,
Q1 - I tried the example that you have given were there were only 4 fields name, plot, genres, casts
everything is fine. Now if I need to make this work for let's say mongo, I need to add 2 more fields id and year, how would you go about doing that?
fake_movie_db = [
{
'name': 'Star Wars: Episode IX - The Rise of Skywalker',
'plot': 'The surviving members of the resistance face the First Order once again.',
'genres': ['Action', 'Adventure', 'Fantasy'],
'casts': ['Daisy Ridley', 'Adam Driver']
}
]
Q2 - when I tried the same with our existing code, POST request was successful but when do a get it was not showing any of the new fields. there should be an error in this scenario?
Thanks
Aravinth
No, this is not a lame question. Thank you for asking the question.
To achieve this you have to add id and year to your
Movie
model.e.g
I hope that answers your question. Please let me know how that goes.
Beautiful article, following what you made I created a personal "library" project. I would like to ask you if you can add the way to do the tests on the various endpoints, because following the fastapi documentation I find myself having problems with the db
Dosen't work for me
this is my docker-compose.yml:
version: '3.7'
services:
movie_service:
build: ./movie-service
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
volumes:
- ./movie-service/:/app/
ports:
- 8001:8000
environment:
- DATABASE_URI=postgresql://postgres:"password"@movie_db/movie_db_dev
- CAST_SERVICE_HOST_URL=cast_service:8000/api/v1/casts/
movie_db:
image: postgres:10.14
volumes:
- postgres_data_movie:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD="password"
- POSTGRES_DB=movie_db_dev
cast_service:
build: ./cast-service
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
volumes:
- ./cast-service/:/app/
ports:
- 8002:8000
environment:
- DATABASE_URI=postgresql://postgres:"password"@cast_db/cast_db_dev
cast_db:
image: postgres:10.14
volumes:
- postgres_data_cast:/var/lib/postgresql/data/
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD="password"
- POSTGRES_DB=cast_db_dev
nginx:
image: nginx:latest
ports:
- "8080:8080"
volumes:
- ./nginx_config.conf:/etc/nginx/conf.d/default.conf
depends_on:
- cast_service
- movie_service
volumes:
postgres_data_movie:
postgres_data_cast:
hint: "password" is my postgresql password
My nginx runing:
prnt.sc/v0qi2v
My docker:
prnt.sc/v0qind