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

# 概要
* [去年はRuby on Railsを使って開発をしていた](https://qiita.com/croquette0212/items/dff2fead8663f7323376)が、最近Pythonを使う機会が増えてきたので、今年はPythonの軽量WebフレームワークFlaskを使ってWebサービスを開発する流れをまとめた。
* 作ったサービスをHerokuで運用するところまでやった。

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

# 作るもの
![Screenshot](https://qiita-image-store.s3.amazonaws.com/0/14097/0c43dc6d-99c0-ccc5-f6a2-8f8b7abbacac.png)

シンプルなメモ。本文は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のテンプレートを利用してレスポンスを生成できる。

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

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

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

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

````

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

```
pip install Hamlish-Jinja
```

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

```Python
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用のファイル追加
`Flask`や`Hamlish-Jinja`など、pipでインストールしたパッケージの一覧を`pip freeze`で`requirements.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`テーブルを作成し、そこに手動でレコードを追加する。

```SQL
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](http://flask.pocoo.org/docs/0.12/tutorial/dbcon/)の内容を元に、このテーブルの内容を表示してみる。

```Python
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.db`の`entries`テーブルの中身を取得している一文。

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

取得した`entries`を`render_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経由でレコードを取得するように書き換える

```Python
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)
```

`sqlite`の`import`の代わりに、`flask_sqlalchemy`を`import`する。

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

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

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

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

HTML側にフォームを追加。

```HAML
%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']`を指定する必要がある。

```Python
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インタープリターを起動して、その中で`SQLAlchemy`の`create_all()`を使ってみよう。

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

`from app import db`で`app.py`内に定義したSQLAlchemyインスタンス`db`を`import`する。この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のインストール
`pip`で`Flask-Markdown`をインストールする。

```
pip install Flask-Markdown
```

## Markdownインスタンスの作成
`Markdown`を`import`し、インスタンスを作成する処理を入れる。

```Python
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表示したい箇所でフィルターを使う
`Entry`の`body`部分を`Markdown`として表示する。使い方はいたって簡単で`markdown`フィルターを使うだけ。

```
{{ entry.body | markdown }}
```

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

```HAML
%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など、至れり尽くせりの`Rails`や`Django`に比べると、手動でやらなければいけないことが多くて面倒さも感じるが、コードが短く見通しも良い。難なくHerokuで運用できるのも素敵。統計処理や機械学習など、何かとPythonに触れる機会が多くなってきたので、Pythonに慣れる意味でも軽量なWebサービスを作る際には積極的にFlaskを試していきたいと思う。

# 参考リンク
* [Tutorial - Flask Documentation](http://flask.pocoo.org/docs/0.12/tutorial/)
* [Flask-SQLAlchemy - Flask-SQLAlchemy Documentation](http://flask-sqlalchemy.pocoo.org/2.3/)
* [Flask-Markdown](https://pythonhosted.org/Flask-Markdown/)