Help us understand the problem. What is going on with this article?

Flask + pytest + Dockerで今風な開発環境を雰囲気で触る

e5541a024a275b3260831a2b881e191b.png

背景

ハンズオンっぽい形で説明する機会があったのでそれ用にまとめた資料を記事に起こしてみたやつ。

TL;DR

① アプリケーション作成 (Flask + uWSGI)
② コンテナ化 (Docker)
③ CI (CircleCI + pytest + pycodestyle)
④ CD (Heroku)

ディレクトリ構成
.
├── Dockerfile
├── LICENSE
├── README.md
├── __init__.py
├── app.py
├── flask_app
│   ├── bin
│   ├── include
│   ├── lib
│   └── pyvenv.cfg
├── requirements.txt
├── settings
│   └── app.ini
├── templates
│   └── index.html
└── tests
    └── __init__.py

概要

  • venvとは
    • 一つのマシン上で分離されたPython環境を構築するソフトウェア
  • Flaskとは
    • Python上で動くマイクロウェブアプリケーションフレームワーク
  • uWSGIとは
    • PythonでWebサービスを動かすAPサーバ
  • pytestとは
    • Pythonのテストフレームワークの一つ

環境

実行イメージ
$ uname -a
Darwin w022581902949m.local 18.7.0 Darwin Kernel Version 18.7.0: Tue Aug 20 16:57:14 PDT 2019; root:xnu-4903.271.2~2/RELEASE_X86_64 x86_64

$ python3 -V
Python 3.7.4

$ pip3 -V
pip 19.2.3 from /usr/local/lib/python3.7/site-packages/pip (python 3.7)

venv基礎

Python3系の本番での環境構築に利用できます。
virtualenvと同じような機能ですがこちらの方が多く使われいるようです。

実行イメージ
# 環境の作成
$ mkdir flask_app && cd &_
$ python3 -m venv flask_app

# Active化
$ source flask_app/bin/active

# 確認
(flask_app)$ python -V
Python 3.7.4

(flask_app)$ pip -V
pip 19.0.3 from /Users/JPZ2046/tmp/flask_app/fla/lib/python3.7/site-packages/pip (python 3.7)

# (無効化するとき)
(flask_app)$ deactivate

Flask + pytest + pep8基礎

上記で構築した仮想環境の中で2点をインストールしていく

実行イメージ
(flask_app)$ pip install --upgrade pip
(flask_app)$ pip install Flask uWSGI

ここでlintとテスト用のモジュールもインストール

実行イメージ
(flask_app)$ pip install pytest pycodestyle

FlaskでHello World

Hello Worldと実行しているPIDとアクセス元のIPアドレスを返すだけの簡単なFlaskアプリケーションを作成します。

app.py
from flask import Flask, request, jsonify, render_template
import psutil
import os
import re

app = Flask(__name__)


def gen_counter():
    """
    アクセスカウンタ用の関数
    """
    cnt = 0

    def _count():
        nonlocal cnt
        cnt += 1
        return cnt
    return _count


def get_resource():
    """
    ホストのメトリクスを収集する関数
    """
    return psutil.cpu_percent(interval=1), \
        psutil.virtual_memory().percent, \
        os.getloadavg()[0],


# カウンタ用のクロージャ
gc = gen_counter()


@app.route('/')
def hello():
    return f"Hello, World!\nIP:{request.remote_addr}\nPID:{os.getpid()}\n"


@app.route('/__health')
def test():
    agent = request.headers.get('User-Agent')

    # ホストのリソース使用料を取得する
    cpu_per, mem_per, ldg_per = get_resource()

    if "Mozilla" in agent:
        return render_template('index.html',
                               cpu=cpu_per, mem=mem_per, ldg=ldg_per)
    else:
        return jsonify(
            {
                'CPU': cpu_per,
                'MEM': mem_per,
                'LDG': ldg_per
            }
        )


@app.route('/__count')
def counter():
    return "ACCESS : " + str(gc()) + "\n" + "PID : " + str(os.getpid()) + "\n"


@app.errorhandler(404)
def err_404(error):
    response = jsonify({"message": "Not Found", "result": error.code})
    return response, error.code


if __name__ == '__main__':
    app.run(host='0.0.0.0', debug=True)

上記を実行し別のターミナルからcurlを実行して動作を確認します。
FlaskではPortの指定などをしていない場合はデフォルトの5000で上がります。

実行イメージ
# アプリケーション起動
(flask_app)$ python app.py

# 別ターミナルから
$ curl -s http://localhost:5000
Hello, World!
IP:127.0.0.1
PID:41789

pytest

tests/test_01.py
from .. import app
import pytest

class TestCommon:

    def test_get_resource(self):
        assert app.get_resource()

    def test_count(self):
        gc = app.gen_counter()
        for _ in range(10):
            gc()
        assert gc() == 11

    def test_count_init(self):
        gc = app.gen_counter()
        assert gc() == 1
実行イメージ
(flask_app)$ pytest -v
===================================================== test session starts ======================================================
platform darwin -- Python 3.7.4, pytest-5.1.2, py-1.8.0, pluggy-0.12.0 -- /usr/local/opt/python/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/JPZ2046/repo/fuppd
collected 3 items

tests/test_app.py::TestCommon::test_get_resource PASSED                                                                  [ 33%]
tests/test_app.py::TestCommon::test_count PASSED                                                                         [ 66%]
tests/test_app.py::TestCommon::test_count_init PASSED                                                                    [100%]

pycodestyle

pycodestyleはPEP8のスタイルを元にしたPythonコードをチェックするツールです。
(pep8と呼ばれるパッケージで呼ばれてましたが現在はこちらとなってます)

(flask_app)$ pycodestyle --show-source --show-pep8 app.py
番号 メッセージ 概要
E111 indentation is not a multiple of four インデントは4の倍数ではありません
E112 expected an indented block インデントブロックが予想される
E115 expected an indented block (comment) インデントされたブロック(コメント)
E116 unexpected indentation (comment) 予期しないインデント(コメント)
E133 closing bracket is missing indentation 閉じ括弧にはインデントがありません
E201 whitespace after ‘(‘ 空白の後に '('
E202 whitespace before ‘)’ 空白の前に ')'
E203 whitespace before ‘:’ 空白の前に ':'
E211 whitespace before ‘(‘ 空白の前に '('
E221 multiple spaces before operator オペレータの前に複数のスペース
E222 multiple spaces after operator オペレータの後の複数のスペース
E223 tab before operator 操作前のタブ
E224 tab after operator 操作後のタブ
E225 missing whitespace around operator オペレータの周囲に空白がない
E226 missing whitespace around arithmetic operator 算術演算子の周りに空白がない
E228 missing whitespace around modulo operator モジュロ演算子の周囲に空白がない
E231 missing whitespace after ‘,’, ‘;’, or ‘:’ '、'、 ';'、または ':'の後に空白がない
E262 inline comment should start with ‘# ‘ インラインコメントは '#'で始まります
E265 block comment should start with ‘# ‘ ブロックコメントは '#'で始まります
E266 too many leading ‘#’ for block comment ブロックコメントの先頭に「#」が多すぎます
E271 multiple spaces after keyword キーワードの後の複数のスペース
E272 multiple spaces before keyword キーワードの前に複数のスペース
E273 tab after keyword キーワードの後のタブ
E274 tab before keyword キーワードの前のタブ
E275 missing whitespace after keyword キーワードの後に​​空白がない
E303 too many blank lines (3) あまりにも多くの空白行(3)
E401 multiple imports on one line 1行に複数のインポート
E501 line too long (82 > 79 characters) 行が長すぎる(82> 79文字)
E701 multiple statements on one line (colon) 1行の複数のステートメント(コロン)
E702 multiple statements on one line (semicolon) 1行に複数のステートメント(セミコロン)
E703 statement ends with a semicolon ステートメントはセミコロンで終わります
E704 multiple statements on one line (def) 1行に複数のステートメント(def)
E901 SyntaxError or IndentationError SyntaxErrorまたはIndentationError
E902 IOError IOError

uWSGIでマルチプロセス化

uWSGIは、WSGIアプリケーションの実行環境を提供します。
そのため、マルチプロセス処理をしてくれるので、(比較的に)手数が少なくアプリケーションのパフォーマンスを上げることができます。

settings/app.ini
[uwsgi]
wsgi-file = app.py
http=0.0.0.0:5000
callable = app
processes = 4
vacuum = true
die-on-term = true
設定項目 概要
wsgi-file ロードするuswgiファイル
http アクセス許可ホスト:ポート
callable
processes 実行するプロセス数
socket ソケットの設定
pidfile 出力するpidファイルパスの設定
実行イメージ
(flask_app)$ uwsgi --ini app.ini
*** uWSGI is running in multiple interpreter mode ***

Docker

コンテナ化

venvで構築した環境をDockerコンテナへと構築します。

実行イメージ
# pipで管理しているPythonパッケージのファイルを生成
(flask_app)$ pip freeze > requirements.txt

(flask_app)$ cat requirements.txt
astroid==2.2.5
Click==7.0
Flask==1.1.1
isort==4.3.21
itsdangerous==1.1.0
Jinja2==2.10.1
lazy-object-proxy==1.4.2
MarkupSafe==1.1.1
mccabe==0.6.1
psutil==5.6.3
pycodestyle==2.5.0
pylint==2.3.1
six==1.12.0
typed-ast==1.4.0
uWSGI==2.0.18
Werkzeug==0.15.6
wrapt==1.11.2

用意するものは上記のみです。これを元にコンテナを作成します。
下記のようなDockerfileを作成します。

Dockerfile
FROM python:3.7.4-alpine3.10
LABEL maintainer="ryuichi1208 (ryucrosskey@gmail.com)"

WORKDIR /app
EXPOSE 5000
COPY . /app

RUN apk add --no-cache \
    git openssl ffmpeg \
    opus libffi-dev gcc \
    curl musl-dev
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

ENTRYPOINT ["uwsgi", "--ini", "app.ini"]

DockerHubへ上げる必要があるのですが、まずはローカルで動作確認をします。

実行イメージ
(flask_app)$ docker image build -t app_01 .

(flask_app)$ docker container run -d -p 5000:5000 app_01

# 起動したらcurlをポート5000へ向けて実行してください。
(flask_app)$ curl -LI http://localhost:5000 -o /dev/null -w '%{http_code}\n' -s
200 # ここでステータスコードが200以外が返ってきた場合は設定ミスです。

CicleCI

.circleci/config.yml
version: 2.1
jobs:
  lint:
    docker:
      - image: python:3.7.4-alpine3.10
        environment:
          PIPENV_VENV_IN_PROJECT: true
    steps:
      - checkout
      - run:
          name: install package
          command: |
            pip -V
            apk add --no-cache \
              git openssl ffmpeg \
              opus libffi-dev gcc \
              curl musl-dev
            pip install --upgrade pip
            pip install -r requirements.txt
      - run:
          name: do lint
          command: pycodestyle app.py
      - run:
          name: do test
          command: pytest -v

  build:
    machine: true
    steps:
      - checkout
      - run:
          name: start container
          command: docker version && docker image build -t app01 .
      - run:
          name: start access
          command: script/do_curl.sh

workflows:
  version: 2
  build_and_test:
    jobs:
      - lint
      - build

imageをDockerHubへPUSH

docker tagつけてdocker pushするだけなので省略

まとめ

執筆中。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away