Python
Heroku
Flask
sqlalchemy

FlaskとPostgreSQLでウェブアプリを作ってHerokuで無料で運用する

概要

  • 去年はRuby on Railsを使って開発をしていたが、最近Pythonを使う機会が増えてきたので、今年はPythonの軽量WebフレームワークFlaskを使ってWebサービスを開発する流れをまとめた。
  • 作ったサービスをHerokuで運用するところまでやった。

前提

  • Pythonの基本的な使い方を理解している人
  • Webサービスを作ったことがある人

作るもの

Screenshot

シンプルなメモ。本文はMarkdown記法に対応させる。

使うもの

  • Pyenv
    • プロジェクト毎にPythonのバージョンを切り替えるために使用。インストール方法、設定方法は省略。
  • Python 3.6.3
    • 今回は、2系を使う必要性がないため、最新版の3.6.3を使用。Pyenvでインストール。
  • Virtualenv
    • プロジェクト単位でパッケージ群などを含むPython環境を切り替えるために使用。
  • Flask
    • Python用の軽量Webフレームワーク。
  • Jinja2 / Hamlish Jinja
    • Flask用のHTMLテンプレートエンジンJinja2でもHAML的な構文を使えるようにするために使用。
  • SQLite / PostgreSQL
    • 最初の動作確認にはSQLiteを使用。その後、Herokuで運用するにあたってPostgreSQLに切り替える。
  • Heroku
    • 運用はHerokuで。
    • アカウント登録とCLIのインストールが必要。

プロジェクトの作成

mkdir flasknote
cd flasknote
git init

Flaskには特にプロジェクト作成用のコマンドなどは用意されていないので、新しく空のディレクトリ一つ用意して、その中で作業をして行く。なお、以降gitのファイル管理のコマンドについては省略。

開発用Virtualenv環境を作る

Pyenvで作成した環境に直接パッケージをインストールしていくと、複数のプロジェクトを走らせる際に問題が起きたりするので、Virtualenvを使って環境を切り分けて開発を進めていく。新しくVirtualenv環境を作成する。

virtualenv venv

Virtualenv環境は通常git上で管理する必要はないので、.gitignoreに登録しておく。

以降、開発の際は作成したVirtualenv環境をアクティベートした上で、サーバーの起動やパッケージのインストールなどを行っていく。

source venv/bin/activate

Flaskアプリの作成

まず、DBを使わないシンプルなHello Worldアプリを作ってHerokuにデプロイする。

Flaskのインストール

pip install flask

Flaskのインストールはpipコマンド一発。

Hello World

/にアクセスされたらHello, World!を表示するシンプルなプログラムを作る。

app.py

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

デコレーター@app.route()を使って、パス(/)がリクエストされた時に呼ばれる関数を紐づける。今回の設定では、(/)が呼ばれた時にhello_world()関数の返り値が画面に表示されるようになる。

Flaskサーバーの起動

flask runコマンドで、サーバーを起動する。FLASK_APPで、Flaskアプリの実装を行ったファイルを指定する。

FLASK_APP=app.py FLASK_DEBUG=1 flask run

デバッグ用にFLASK_DEBUG=1を指定する。これを指定しないと、コードを変更しても実行コードに反映されないので不便。このオプションは本番環境では使用してはいけない。

サーバーが起動したらhttp://localhost:5000/を開く。

HTMLテンプレートを使う

Flaskでは、HTMLテンプレートエンジンJinja2が利用できる。関数の返り値でテキストを直接返す代わりに、render_template関数を使うと、Jinja2のテンプレートを利用してレスポンスを生成できる。

from flask import Flask, render_template # 変更
app = Flask(__name__)

@app.route('/')
def hello_world():
    return render_template('index.html') # 変更

templatesディレクトリを作成して、そこにindex.htmlを作る

<html>
<body>
  <h1>Hello world</h1>
</body>
</html>

Hamlish Jinjaを使う

Hamlish Jinjaを使うと、JinjaでHAML風の記法を使うことができる。

pip install Hamlish-Jinja

HamlishExtensionを有効にしたjinja_optionsを持ったFlaskインスタンスを作るために、サブクラスを作る。

from flask import Flask, render_template
from hamlish_jinja import HamlishExtension # 追加
from werkzeug import ImmutableDict # 追加

class FlaskWithHamlish(Flask): # 追加
    jinja_options = ImmutableDict( # 追加
        extensions=[HamlishExtension] # 追加
    ) # 追加
app = FlaskWithHamlish(__name__) # 変更

@app.route('/')
def hello_world():
    return render_template('index.haml') # 変更

index.htmlを削除して、代わりにindex.hamlを作成する

%html
  %body
    %h1
      Hello world

変数を使う

render_templateの引数で変数を渡すと、テンプレート内で{{変数名}}の形で展開できる。

return render_template('index.haml', username="John") # 変更

haml側

%html
  %body
    %h1
      Hello, {{username}}

HerokuにDeployする

なんの意味もないHello Worldだが、一旦ここまでをHerokuにDeployしてみる。

gunicornで動作確認する

FlaskアプリをHeroku上で動かすときは、gunicornをサーバーとして使用する。動作確認用に、ローカルにもgunicornをインストールする。

pip install gunicorn
pyenv rehash

ローカルでgunicornを使ってFlaskアプリを立ち上げてみる。Flaskアプリ(app.py内のapp)を引数で指定する。

gunicorn app:app

gunicornを使う場合は8000番ポートにサーバーが起動するので、http://localhost:8000/を開いて動作確認する。flask runした時と同じように表示されればOK。

Heroku用のファイル追加

FlaskHamlish-Jinjaなど、pipでインストールしたパッケージの一覧をpip freezerequirements.txtに書き出す。(このリストの内容が、Herokuサーバー上でもinstallされることになる)

pip freeze > requirements.txt

Procfileを作成し、フロントインスタンス(web)用のgunicornを使ったサーバーの起動コマンドを指定する。

web: gunicorn app:app

最後にruntime.txtを作成し、そこに使用するPythonのバージョンを記述する。

python-3.6.3

Herokuアプリの作成

Heroku CLIから、新しいHerokuアプリを作成する。

heroku create myflaskapp

HerokuにDeployする

Railsアプリなど同様、git push heroku masterでHerokuにPushする。

git push heroku master

Pushに成功したらheroku openコマンドで、デプロイされたアプリを開いて動作確認する。

データベースを使う(SQLite)

シンプルなHello Worldアプリをデプロイできたところで、今度はデータベースを使ってみる。

HerokuではSQLiteを使用できないので、PostgreSQLを使うことになるのだが、より良い理解のために素のSQLを書いてSQLiteデータベースをいじるところから順を追って動かしてみる。

SQLiteデータベースのデータを表示する

手始めに、SQLを書いてデータベースのテーブルの内容を表示してみる。

SQLiteデータベースの作成とテーブルの用意

新しくSQLiteデータベースを作成し、そこにentriesテーブルを作る。

sqlite3 flasknote.db

entriesテーブルを作成し、そこに手動でレコードを追加する。

create table entries (
  id integer primary key autoincrement,
  title text not null,
  body text not null
);
insert into entries (title, body) values ("First message", "Hello world.");
insert into entries (title, body) values ("Second message", "Hello Japan.");

レコードを表示してみる

Tutorialの内容を元に、このテーブルの内容を表示してみる。

from flask import Flask, render_template, g # 変更
from hamlish_jinja import HamlishExtension
from werkzeug import ImmutableDict
import os # 追加
import sqlite3 # 追加

class FlaskWithHamlish(Flask):
    jinja_options = ImmutableDict(
        extensions=[HamlishExtension]
    )
app = FlaskWithHamlish(__name__)

@app.route('/')
def hello_world():
    entries = get_db().execute('select title, body from entries').fetchall() # 追加
    return render_template('index.haml', entries=entries) # 変更

# Database
def connect_db():
    db_path = os.path.join(app.root_path, 'flasknote.db')
    rv = sqlite3.connect(db_path)
    rv.row_factory = sqlite3.Row
    return rv

def get_db():
    if not hasattr(g, 'sqlite_db'):
        g.sqlite_db = connect_db()
    return g.sqlite_db

@app.teardown_appcontext
def close_db(error):
    if hasattr(g, 'sqlite_db'):
        g.sqlite_db.close()

多少コードが長くなったが、重要なところはget_db().execute('select title, body from entries').fetchall()flasknote.dbentriesテーブルの中身を取得している一文。

効率化のために、General Purpose Variablegにデータベースとのコネクションを持たせ、リクエスト毎に新しいコネクションを作らないようにしていたり、アプリと同じディレクトリにあるflasknote.dbのパスを取得するための処理が入っていたりするが、これらの部分はあまり重要ではない(それに、あとで使わなくなる)ので、一旦スルーしても問題ない。

取得したentriesrender_templateの引数にわたして、テンプレート(index.haml)内でそれを表示する。

%html
  %body
    %ul.entries
      -for entry in entries:
        %li
          {{ entry.title | safe }}
          {{ entry.body | safe }}

|は所謂フィルタ。safeフィルターは、HTMLエスケープ処理をしてくれるフィルター。

SQLAlchemyを使う

生のSQLを書くのは、セキュリティ的にも効率的にも避けたいので、O/RマッパーであるSQLAlchemyを使う形に実装を置き換える。

flask_sqlalchemyのインストール

Flask用のSQLAlchemyをpipでインストール。

pip install flask_sqlalchemy

SQLAlchemyを使ってデータを表示する

SQLAlchemy経由でレコードを取得するように書き換える

from flask import Flask, render_template, g
from hamlish_jinja import HamlishExtension
from werkzeug import ImmutableDict
import os
from flask_sqlalchemy import SQLAlchemy # 変更

class FlaskWithHamlish(Flask):
    jinja_options = ImmutableDict(
        extensions=[HamlishExtension]
    )
app = FlaskWithHamlish(__name__)

db_uri = "sqlite:///" + os.path.join(app.root_path, 'flasknote.db') # 追加
app.config['SQLALCHEMY_DATABASE_URI'] = db_uri # 追加
db = SQLAlchemy(app) # 追加

class Entry(db.Model): # 追加
    __tablename__ = "entries" # 追加
    id = db.Column(db.Integer, primary_key=True) # 追加
    title = db.Column(db.String(), nullable=False) # 追加
    body = db.Column(db.String(), nullable=False) # 追加

@app.route('/')
def hello_world():
    entries = Entry.query.all() #変更
    return render_template('index.haml', entries=entries)

sqliteimportの代わりに、flask_sqlalchemyimportする。

SQLiteデータベースの場所は、app.config['SQLALCHEMY_DATABASE_URI']に指定する。SQLiteデータベースであることを示すスキーマsqlite:///でデータベースのパスを指定する。

DBのテーブルと、Python上のモデルのO/Rマッピング用のクラスEntryを定義する。Python上でのクラスは単数形(Entry)、テーブル名は複数形(entries)を使いたかったので、__tablename__でテーブル名を明示的に指定している。その下のidtitlebodyはフィールドの型の定義。primary_keynullableの指定は、SQLAlchemyの機能を使ってテーブルを作成する時用の設定(後述)

ブラウザで開いて、生のSQLを書いていた時同様、データを取得・表示できていればOK。

投稿フォームを作ってデータを追加する

表示がうまくいったので、投稿フォームを作ってデータの追加もできるようにする。

HTML側にフォームを追加。

%html
  %body
    %ul.entries
      -for entry in entries:
        %li
          {{ entry.title | safe }}
          {{ entry.body | safe }}
    %form(action="/post" method="POST")
      %input(type="text" name="title")
      %input(type="text" name="body")
      %input(type="submit")

受け取り用のURL(/post)を追加する。POSTメソッドに対応させる場合は、パスのパターンだけでなく、methods=['POST']を指定する必要がある。

from flask import Flask, render_template, g, request, redirect, url_for # 変更

...

@app.route('/post', methods=['POST'])
def add_entry():
    entry = Entry()
    entry.title = request.form['title']
    entry.body = request.form['body']
    db.session.add(entry)
    db.session.commit()
    return redirect(url_for('hello_world'))

新しくEntryインスタンスを作り、フォームから送信された内容(request.form['title'], request.form['body'])をフィールドにセットする。db.sessionに追加してcommitすると、SQLが発行され、データベースに追加が行われる。

処理が完了したら、redirect関数を使って一覧画面にリダイレクトする。一覧画面のURLはurl_for('リダイレクトしたいURL用の関数名')で取得。

ブラウザ上で、フォームから値を入力・送信してみて、データが追加されればOK。

PostgreSQLを使う

HerokuではSQLiteを使うことができないので、PostgreSQLを使う形に変更する。幸い、SQLAlchemyはPostgreSQLにも対応しているため、データベースの接続先をSQLiteデータベースからPostgreSQLデータベースに変更するだけ。

Herokuで動かす前に、一応ローカルのPostgreSQLデータベースを使って動作確認する。

PostgreSQLデータベースの作成

PostgreSQLのcreatedbコマンドで、新しいデータベースを作成する。

createdb flasknote

psycopg2のインストール

SQLAlchemyでPostgreSQLを利用するために、psycopg2パッケージが別途必要になるので、pipでインストールする。

pip install psycopg2

SQLAlchemyを使ってテーブルを作る

SQLiteを使う際にやったように、手動でSQLを叩いてテーブルを作っても良いのだが、SQLAlchemyにはマッピングクラスの定義からテーブルを作ってくれる機能があるので、今回はそれを使ってテーブルを作ってみよう。

pythonコマンドでPythonインタープリターを起動して、その中でSQLAlchemycreate_all()を使ってみよう。

python
>>> from app import db
>>> db.create_all()

from app import dbapp.py内に定義したSQLAlchemyインスタンスdbimportする。このSQLAlchemyインスタンスのcreate_all()メソッドを呼ぶと、モデル内の定義(今回の場合はEntryクラス内に記述した__tablename__db.Columnで指定した各フィールドの情報)を元に、SQLを発行してテーブルを作成してくれる。(対象となるデータベースは、app.pyでSQLAlchemyのインスタンスの生成前に指定したapp.config['SQLALCHEMY_DATABASE_URI']の向き先となる)

動作確認

サーバーを再起動して、動作確認。問題がなさそうだったら、いよいよHerokuにデプロイする。

HerokuにDeployする

requirements.txtの更新

SQLAlchemyなど、使用するパッケージが増えたので、最新のパッケージ一覧をrequirements.txtに書き出す。

pip freeze > requirements.txt

Heroku Postgresアドオンを追加

Heroku管理画面からHeroku Postgresアドオンを追加する。(CLIから追加してもOK)プランはどれでも良いが、無料で使いたい場合はHobby Dev

データベースURLの変更

app.py内で、データベースの向き先を変更する。

db_uri = os.environ.get('DATABASE_URL') or "postgresql://localhost/flasknote"

Herokuで、Heroku Postgresアドオンを追加した場合、環境変数DATABASE_URLにPostgreSQLデータベースの接続先URLがセットされるので、この値がセットされているとき(Heroku上で動作するとき)はそれを使い、セットされていないとき(ローカルでのデバッグなど)はローカルのPostgreSQLデータベースを使うようにする。

デプロイ

いよいよソースコードをデプロイ。あと一歩。

git push heroku master

テーブルの作成

HerokuのPostgreSQLデータベース上に、テーブルを作成する。ローカルで作成したとき同様、SQLAlchemyの機能を使ってテーブルを作成する。

heroku run pythonでHeroku上のPythonインタープリターを起動。ローカルの時と同様のコマンドで、テーブルを作成する。

heroku run python
>>> from app import db
>>> db.create_all()

動作確認

最後にheroku openでHerokuサーバーをブラウザで開き、動作確認をする。

Markdown対応

データベースへの接続も含め、Heroku上で動作するようになった。このままではあまりに機能が少なすぎるので、最後におまけでMarkdown対応をしてみる。

Flask-Markdownのインストール

pipFlask-Markdownをインストールする。

pip install Flask-Markdown

Markdownインスタンスの作成

Markdownimportし、インスタンスを作成する処理を入れる。

from flask import Flask, render_template, g, request, redirect, url_for
from hamlish_jinja import HamlishExtension
from werkzeug import ImmutableDict
import os
from flask_sqlalchemy import SQLAlchemy
from flaskext.markdown import Markdown # 追加

...

Markdown(app) # 追加

...

Markdown表示したい箇所でフィルターを使う

Entrybody部分をMarkdownとして表示する。使い方はいたって簡単でmarkdownフィルターを使うだけ。

{{ entry.body | markdown }}

UIの調整

最後に、Bootstrapなどを使って見た目を整えたり、一旦完成。

%html
  %head
    %meta charset="utf-8"
    %meta http-equiv="X-UA-Compatible" content="IE=edge"
    %meta name="viewport" content="width=device-width, initial-scale=1"
    %link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"
  %body
    %div.container
      %h1
        Notes
      -for entry in entries:
        %div.panel.panel-default
          %div.panel-heading
            %h3.panel-title
              {{ entry.title | safe }}
          %div.panel-body
            {{ entry.body | markdown }}
      %div.panel.panel-default
        %div.panel-body
          %form(action="/post" method="POST")
            %div.form-group
              %label for="title-text-field" << Title
              %input#title-text-field.form-control(type="text" name="title")
              %label for="body-text-area" << Body
              %textarea#body-text-area.form-control(name="body" rows="3")
              %input.btn.btn-primary.btn-block(type="submit")

まとめ

データベースを使ったFlaskアプリをHerokuにデプロイするところまでできた。テーブルのマイグレーションやScaffoldなど、至れり尽くせりのRailsDjangoに比べると、手動でやらなければいけないことが多くて面倒さも感じるが、コードが短く見通しも良い。難なくHerokuで運用できるのも素敵。統計処理や機械学習など、何かとPythonに触れる機会が多くなってきたので、Pythonに慣れる意味でも軽量なWebサービスを作る際には積極的にFlaskを試していきたいと思う。

参考リンク