背景
ハンズオンっぽい形で説明する機会があったのでそれ用にまとめた資料を記事に起こしてみたやつ。
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アプリケーションを作成します。
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
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アプリケーションの実行環境を提供します。
そのため、マルチプロセス処理をしてくれるので、(比較的に)手数が少なくアプリケーションのパフォーマンスを上げることができます。
[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を作成します。
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
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するだけなので省略
まとめ
執筆中。