Build and Deploy a REST API Microservice with Python Flask and Docker

swarnimwalavalkar profile imageSwarnim Walavalkar ・8 min read

Objectives:

  • Build a simple but real-world useable REST API
  • Follow REST and Microservice Arch Best Practices
  • Deploy to a Docker Container

Technologies used:

  • Python
  • Flask
  • Flask Restful
  • Docker

Prerequisites:

  • A Foundational Understanding of Python
  • Acquaintance with the Flask Microframework
  • A Primary Understanding of Docker and Docker Container Management

Difficulty: ⚡⚡Intermediate

Setup:

Make sure you have pipenv installed.

pip install pipenv

Then, start by making a directory for the project and cd into it

mkdir FlaskBookApi/
cd FlaskBookApi/

Then install the flask and flask-restful packages using pipenv. This will do a couple of things:

pipenv install flask flask-restful

It creates a python virtual environment with your directory's name in a central location and installs the specified packages (in this case, flask and flask-restful). Along with that, In your working directory, it creates the Pipfile, which contains all of the projects dependencies with their appropriate version numbers along with the Python version used by the project. It also creates the Pipfile.lock to enable deterministic builds in production.

This isn't a pipenv tutorial, So I won't go into too much detail about that.

Write The API:

Alright, Now the fun part! Lets Build this thing!

Start by creating a Python file in the working directory.

touch api.py

Great! Now import and initialize flask and flask restful

from flask import Flask
from flask_restful import Resource, Api, reqparse, abort, marshal, fields

# Initialize Flask
app = Flask(__name__)
api = Api(app)

Be sure to import the required modules from flask-restful as shown above.

We'll be building a simple API that goes over the performing some basic CRUD operations on a datastore of books (or as it happens, in this case, a List of Dictionaries).

We're going to use a simple Python Dictionary for storage, But it can easily be swapped in with a real database solution, should you need to. But for the sake of simplicity, I've decided to go with a simple List of Dictionaries

Lets get started. First declare a List containing several Dictionaries, representing individual 'Book' elements

# A List of Dicts to store all of the books
books = [{
    "id": 1,
    "title": "Zero to One",
    "author": "Peter Thiel",
    "length": 195,
    "rating": 4.17
},
    {
    "id": 2,
    "title": "Atomic Habits ",
    "author": "James Clear",
    "length": 319,
    "rating": 4.35
}
]

Next, Set up a Dictionary to determine the schema for the book object, as expected by the API. This will help with validating requests later.

# Schema For the Book Request JSON
bookFields = {
    "id": fields.Integer,
    "title": fields.String,
    "author": fields.String,
    "length": fields.Integer,
    "rating": fields.Float
}

Flask Restful is an extension to the Flask Microframework that makes it a whole lot easier to build RESTful APIs

A fundamental building block provided by flask-restful is Resources. Resources give you easy access to multiple HTTP methods just by defining the methods on your Resource Class.

Here is the Book Resource class: This contains the HTTP routes for accessing, modifying and deleting each individual book entity. Now, Lets Break this down...

# Resource: Individual Book Routes
class Book(Resource):
    def __init__(self):
        # Initialize The Flsak Request Parser and add arguments as in an expected request
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument("title", type=str, location="json")
        self.reqparse.add_argument("author", type=str, location="json")
        self.reqparse.add_argument("length", type=int, location="json")
        self.reqparse.add_argument("rating", type=float, location="json")

        super(Book, self).__init__()

    # GET - Returns a single book object given a matching id
    def get(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        return{"book": marshal(book[0], bookFields)}

    # PUT - Given an id
    def put(self, id):
        book = [book for book in books if book['id'] == id]

        if len(book) == 0:
            abort(404)

        book = book[0]

        # Loop Through all the passed agruments
        args = self.reqparse.parse_args()
        for k, v in args.items():
            # Check if the passed value is not null
            if v is not None:
                # if not, set the element in the books dict with the 'k' object to the value provided in the request.
                book[k] = v

        return{"book": marshal(book, bookFields)}

        # Delete - Given an id
    def delete(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        books.remove(book[0])

        return 201

First, In the init method of the class, you initialize the request parser. It'll allow you easy access to any variable on the flask.request and also validates the response based on the arguments provided

class Book(Resource):
    def __init__(self):
        # Initialize The Flsak Request Parser and add arguments as in an expected request
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument("title", type=str, location="json")
        self.reqparse.add_argument("author", type=str, location="json")
        self.reqparse.add_argument("length", type=int, location="json")
        self.reqparse.add_argument("rating", type=float, location="json")

        super(Book, self).__init__()

Next, The GET Method, This one's really simple, it takes in an id and loops through the books list and checks each element's id with the specified ID, If a match is found, it returns that dict. The marshal method just makes sure the object that is being returned is being filtered through the fields defined in the bookFields dict.

# GET - Returns a single book object given a matching id
    def get(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        return{"book": marshal(book[0], bookFields)}

The Put method is used to update the element with the specified id, It takes a response object with the fields to be updated First, It loops through the books list and checks each element's id with the specified ID, If a match is found, It parses all of the provided arguments using the reqparser.

Then, Loops through the parsed arguments and updates the fields as in the request object.

# PUT - Given an id
    def put(self, id):
        book = [book for book in books if book['id'] == id]

        if len(book) == 0:
            abort(404)

        book = book[0]

        # Loop Through all the passed agruments
        args = self.reqparse.parse_args()
        for k, v in args.items():
            # Check if the passed value is not null
            if v is not None:
                # if not, set the element in the books dict with the 'k' object to the value provided in the request.
                book[k] = v

        return{"book": marshal(book, bookFields)}

The Delete method just simply takes in an id and deletes the element in the books list with the id.

# Delete - Given an id
    def delete(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        books.remove(book[0])

        return 201

Next is the BookList class, This contains the routes dealing with operations on the entire database.

class BookList(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument(
            "title", type=str, required=True, help="The title of the book must be provided", location="json")
        self.reqparse.add_argument(
            "author", type=str, required=True, help="The author of the book must be provided", location="json")
        self.reqparse.add_argument("length", type=int, required=True,
                                   help="The length of the book (in pages)", location="json")
        self.reqparse.add_argument(
            "rating", type=float, required=True, help="The rating must be provided", location="json")

    def get(self):
        return{"books": [marshal(book, bookFields) for book in books]}

    def post(self):
        args = self.reqparse.parse_args()
        book = {
            "id": books[-1]['id'] + 1 if len(books) > 0 else 1,
            "title": args["title"],
            "author": args["author"],
            "length": args["length"],
            "rating": args["rating"]
        }

        books.append(book)
        return{"book": marshal(book, bookFields)}, 201

The Init method initializes the request parser. It parses the request JSON Object and also validates it based on the arguments provided.

def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument(
            "title", type=str, required=True, help="The title of the book must be provided", location="json")
        self.reqparse.add_argument(
            "author", type=str, required=True, help="The author of the book must be provided", location="json")
        self.reqparse.add_argument("length", type=int, required=True,
                                   help="The length of the book (in pages)", location="json")
        self.reqparse.add_argument(
            "rating", type=float, required=True, help="The rating must be provided", location="json")

The Get method simply returns all the elements in the books list

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

def get(self):
        return{"books": [marshal(book, bookFields) for book in books]}

The Post method takes a JSON Object. Parses it , creates a new dict and appends it to the Books list.

def post(self):
        args = self.reqparse.parse_args()
        book = {
            "id": books[-1]['id'] + 1 if len(books) > 0 else 1,
            "title": args["title"],
            "author": args["author"],
            "length": args["length"],
            "rating": args["rating"]
        }

        books.append(book)
        return{"book": marshal(book, bookFields)}, 201

And Thats it! Now just attach those Resource classes to some endpoints and test it out!

api.add_resource(BookList, "/books")
api.add_resource(Book, "/books/<int:id>")

if __name__ == "__main__":
    app.run(debug=True)

Deploy on Docker:

Alright, Now lets deploy this microservice API to a docker container.

First, create a Dockerfile in the project directory.

A Docker file is in essence a set of instructions for building an Image which is a blueprint which your container will run off of.

Lets go over this line by line.

FROM python:3.8

RUN pip3 install pipenv

ENV PROJECT_DIR /usr/src/flaskbookapi

WORKDIR ${PROJECT_DIR}

COPY Pipfile .
COPY Pipfile.lock .
COPY . .

RUN pipenv install --deploy --ignore-pipfile

EXPOSE 5000

CMD ["pipenv", "run", "python", "api.py"]

This use the python 3.8 image from Docker Hub as our base image, This makes sure we have Python and all of its dependencies on the container

FROM python:3.8

This installs pipenv on the container.

RUN pip3 install pipenv

The first line sets a environment variable as a path to a directory to store the projects code.

Then the next command sets that directory as the working directory.

ENV PROJECT_DIR /usr/src/flaskbookapi

WORKDIR ${PROJECT_DIR}

Next, We copy over all of our files to the container

COPY Pipfile .
COPY Pipfile.lock .
COPY . .

This command runs pipenv install with the —deploy flag and also sets it to ignore the pipfile and just use the pipfile.lock to install dependencies

RUN pipenv install --deploy --ignore-pipfile

Next, We expose the 5000 port to be able to use it.

EXPOSE 5000

Finally, we run our program using the CMD command.

CMD ["pipenv", "run", "python", "api.py"]

Now lets use that Dockerfile to build an image which will later be used to make a container

docker build -t flaskbookapi:1.0 .

Now, finally! Fire up a container with the image we just built!

docker run -p 5000:5000 --name FlaskBookAPI flaskbookapi:1.0

And Boom! Out API is up and running! On a Docker container

$ docker run -p 5000:5000 --name FlaskBookAPI flaskbookapi:1.0
 * Serving Flask app "api" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Download the Project from GitHub:

https://github.com/SwarnimWalavalkar/rest-api-microservice-docker

Further Reading:

Flask Documentation: https://flask.palletsprojects.com/en/1.1.x/

Flast-Restful Documentation: https://flask-restful.readthedocs.io/en/latest/

Docker Documentation: https://docs.docker.com/

Pipenv: https://pipenv-fork.readthedocs.io/en/latest/

[출처] https://dev.to/swarnimwalavalkar/build-and-deploy-a-rest-api-microservice-with-python-flask-and-docker-5c2d

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
194 [MSA] 오픈소스 모니터링 툴 - Prometheus #3 그라파나를 이용한 시각화 file 졸리운_곰 2021.03.21 603
193 [MSA] 오픈소스 모니터링 툴 - Prometheus #2 Hello Prometheus file 졸리운_곰 2021.03.21 464
192 [MSA] 오픈소스 모니터링툴 - Prometheus #1 기본 개념과 구조 file 졸리운_곰 2021.03.21 434
191 [MSA] API 게이트 웨이 & Google Cloud Endpoints file 졸리운_곰 2021.03.21 398
190 [MSA] Kong API gateway #3 - Kong on Kubernetes file 졸리운_곰 2021.03.21 441
189 [MSA] Kong API gateway #2 - 간단한 아키텍쳐와 API 테스트 file 졸리운_곰 2021.03.21 515
188 [MSA] Kong API gateway #1 - 설치와 둘러보기 file 졸리운_곰 2021.03.21 509
187 [MSA] Istio Traffic management file 졸리운_곰 2021.03.21 475
186 [MSA] Istio #4 - Istio 설치와 BookInfo 예제 file 졸리운_곰 2021.03.21 535
185 [MSA] Istio #3- Istio에 대한 소개 file 졸리운_곰 2021.03.21 586
184 [MSA] Istio #2 - Envoy proxy file 졸리운_곰 2021.03.21 445
183 [MSA] Istio #1 - 마이크로 서비스와 서비스 매쉬 file 졸리운_곰 2021.03.21 441
» [MSA][Python] Build and Deploy a REST API Microservice with Python Flask and Docker file 졸리운_곰 2021.03.20 402
181 [MSA][KONG gateway] Kong, manage your APIs ! file 졸리운_곰 2021.03.20 1120
180 [MSA] Monitoring Your Synchronous Python Web Applications Using Prometheus file 졸리운_곰 2021.03.20 466
179 [Linux][Crontab] Run Your Flask Regularly Scheduled Jobs with Cron 졸리운_곰 2021.03.20 353
178 [MSA, Docker, Kubernetes] Running Spark on Kubernetes file 졸리운_곰 2021.03.04 349
177 [MSA, Docker, Kubernetes] Docker 개념, 관리, 이미지생성까지 한번에!! file 졸리운_곰 2021.03.04 699
176 도커 시작하기 7 : Dockerfile을 이용한 이미지 생성 졸리운_곰 2021.03.04 346
175 [MAS, docker, kubernetes] [Container 시리즈] 03. Docker File, Docker Image - 도커파일 및 이미지에 대하여 file 졸리운_곰 2021.03.04 272
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED