dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Herokuのpythonチュートリアル(Getting Started with Python on Heroku)をやってみた

ここ。 Getting Started on Heroku with Python | Heroku Dev Center

Heroku は RubyRails を使ったことがなかったので今まで敬遠してたんだけど
pythonチュートリアルがあったのでやってみた。
つまずいたポイントとか覚えておきたいことなど、軽くメモ。

f:id:dackdive:20150725132647p:plain

heroku アカウントでログインしながら進めるとチャプターに線が引かれていくのいいすね。

(2015/11/01 追記)
こちらのチュートリアルは Heroku での開発手順や基本的なコマンドを学ぶにはいいですが、
Heroku での Django アプリケーション開発という点ではこちらの方が詳しい。

Getting Started on Heroku with Python | Heroku Dev Center
(追記ここまで)

(2015/12/22 追記)
Heroku で Django アプリケーション開発をするためのテンプレートがありました。

(追記ここまで)

Introduction

  • Heroku アカウントを作成する
  • python をインストールする
  • Setuptools, pip, virtualenv をインストールする
  • Postgres をインストールする

virtualenv のあたりは昔 このあたりの記事 を読んで pyenv と pyenv-virtualenv というプラグインを入れていたので、そのまま。

Postgres については、公式ドキュメント だと Postgres.app でインストールするようにとの記載があるけど、これまた昔に Homebrew 経由で入れてたっぽい。
ただ、知らず知らずのうちに brew upgrade とかしてたせいか

$ postgres -D /usr/local/var/postgres
LOG:  skipping missing configuration file "/usr/local/var/postgres/postgresql.auto.conf"
FATAL:  database files are incompatible with server
DETAIL:  The data directory was initialized by PostgreSQL version 9.3, which is not compatible with this version 9.4.4.

というエラーが表示されてしまったので、以下の記事を参考に修正。
homebrewで PostgreSQL 9.3へバージョンアップ方法 - Qiita


Setup

特になし。 Heroku toolbelt をインストールした後、

$ heroku login

でログイン。


Prepare the app

このリポジトリソースコードがあるので取ってくる。

$ git clone https://github.com/heroku/python-getting-started.git


Deploy the app

$ heroku create [アプリ名]

で heroku 上にアプリを作成する。[アプリ名] は省略可能で、省略すると heroku が勝手に lit-bastion-5032 みたいなアプリ名をつける。

$ git push heroku master

でデプロイ 。

$ heroku open

でブラウザでアプリを確認。

f:id:dackdive:20150725132745p:plain


View logs

$ heroku logs --tail

でログが見られる。


Define a Procfile

Procfile という名前のテキストファイルが重要で、アプリの起動時に実行するコマンドを記述する。
詳しくはここ。

The Procfile | Heroku Dev Center

Procfile は

web: gunicorn gettingstarted.wsgi --log-file -

のように、<process type>: <command> というフォーマットで記述する。
<process type>web, worker など。

Scale the app

Free の dyno の特徴としては

  • 30 分アクセスがなければスリープする
  • 1 日の稼働時間の max は 18 h
  • 18 h を越えなければスリープ状態の dyno にアクセスがあってもリクエストはさばいてくれる。ただし、起動のため通常のアクセスよりも時間がかかる(2 回目以降のアクセスでは普通)

Declare app dependencies

python アプリの場合、依存関係のあるライブラリは requirements.txt というファイルをルートディレクトリ直下に置くことで管理する。
チュートリアルrequirements.txt はこんな感じ。

dj-database-url==0.3.0
Django==1.8.1
django-postgrespool==0.3.0
gunicorn==19.3.0
psycopg2==2.6
SQLAlchemy==1.0.4
whitenoise==1.0.6

ローカルで pip freeze を実行するとインストールされているライブラリの一覧が確認できる。
自分の場合はこんな感じ。

$ pip freeze
Django==1.5.10
GoogleAppEngineCloudStorageClient==1.9.5.0
Pillow==2.5.3
PyYAML==3.05
astroid==1.3.2
beautifulsoup4==4.3.2
django-appengine-toolkit==0.2.1
django-blog-zinnia==0.13
django-mptt==0.6.1
django-tagging==0.3.2
django-xmlrpc==0.1.5
logilab-common==0.62.1
mercurial==3.3.2
pep8==1.5.7
pylint==1.4.0
pyparsing==2.0.2
pytz==2014.7
six==1.9.0
virtualenv==1.11.6
wsgiref==0.1.2

heroku にデプロイする時、自動的に以下のコマンドが実行され、依存関係のあるライブラリがインストールされるらしい。

$ pip install -r requirements.txt --allow-all-external

ローカルの pyenv 環境を構築する

公式ドキュメントだと

$ virtualenv venv

というコマンドで venv という virtualenv 環境を新しく作ってるみたいだけど、私の場合は pyenv-virtualenv を使っているので

$ pyenv install 2.7.8
$ pyenv virtualenv 2.7.8 venv
$ pyenv local venv
# .python-version ファイルを .gitignore に追記しておく
$ echo .python-version >> .gitignore

# 新しく作成した venv という環境下で依存ライブラリをインストール
$ pip install -r requirements.txt --allow-all-external
$ pyenv rehash

とした。runtime.txt 見ると python-2.7.9 と書いてるんだけど記事執筆時点で pip で 2.7.9 がインストールできないようなので
とりあえず 2.7.8 にした。

Run the app locally

$ python manage.py collectstatic

としたところ、以下のようなエラーがでた。

  File "/Users/yama/.pyenv/versions/2.7.8/lib/python2.7/hmac.py", line 8, in <module>
    from operator import _compare_digest as compare_digest
ImportError: cannot import name _compare_digest

結局根本的な原因はわからなかったんだけど、pyenv で 2.7.8 を再インストールしたり色々試してたら直った。
元々 python の 2.7.5 を入れていて、2.7.5 の時はエラーの原因になっている hmac.py に from operator import ... という行がないのが関係している?

$ pyenv global 2.7.5
$ python
Python 2.7.5 (default, Aug 16 2014, 22:15:56)
[GCC 4.2.1 Compatible Apple LLVM 5.1 (clang-503.0.40)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import operator
>>> operator._copare_digest
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute '_copare_digest'
>>>
$ pyenv global 2.7.8
$ pyenv rehash
$ python
Python 2.7.8 (default, Jul 25 2015, 00:35:51)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import operator
>>> operator._compare_digest
<built-in function _compare_digest>

(というか、別件で 2.7.5 使った後、単に pyenv rehash し忘れただけ?)

参考:
Python/Flask error: "ImportError: cannot import name _compare_digest" - Stack Overflow


python manage.py collectstatic というコマンドについて

Django の機能で、プロジェクト内に散らばっている静的リソース(チュートリアルでは local assets と呼んでる)を 1 つのディレクトリにまとめることができる。
ためしにチュートリアルgettingstarted/settings.py を見てみると、

INSTALLED_APPS = (
    ...
    'django.contrib.staticfiles',
    'hello'
)

...
STATIC_ROOT = 'staticfiles'
STATIC_URL = '/static/'

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

のように書かれている。ポイントは 、

  • STATIC_ROOT は 1 箇所にまとめた静的リソースをどこに置くかを指定する (この場合ルートディレクトリ直下に staticfiles というディレクトリが作成され、そこにまとめられる)
  • 基本的には STATIC_DIRS に指定したディレクトリから静的リソースが収集される
  • ↑に加え、各アプリケーションディレクトリの static というディレクトリに配置した静的リソースは自動的に収集される
    (しないように設定することもできる)

参考:
django.contrib.staticfilesを使う - 偏った言語信者の垂れ流し
静的ファイルの公開方法 — Django 1.4 documentation


静的リソースを staticfiles にまとめた後、heroku local web というコマンドでローカルでアプリを起動してみる。
が、ここでもエラーが。

$ heroku local web
08:57:05 web.1  | started with pid 4825
08:57:05 web.1  | [2015-07-25 08:57:05 +0900] [4825] [INFO] Starting gunicorn 19.3.0
08:57:05 web.1  | [2015-07-25 08:57:05 +0900] [4825] [INFO] Listening at: http://0.0.0.0:5000 (4825)
08:57:05 web.1  | [2015-07-25 08:57:05 +0900] [4825] [INFO] Using worker: sync
08:57:05 web.1  | [2015-07-25 08:57:05 +0900] [4857] [INFO] Booting worker with pid: 4857
08:57:07 web.1  | [2015-07-24 23:57:07 +0000] [4857] [ERROR] Error handling request
08:57:07 web.1  | Traceback (most recent call last):
08:57:07 web.1  |   File "/Users/yama/.pyenv/versions/heroku/lib/python2.7/site-packages/gunicorn/workers/sync.py", line 130, in handle
08:57:07 web.1  |     self.handle_request(listener, req, client, addr)
08:57:07 web.1  |   File "/Users/yama/.pyenv/versions/heroku/lib/python2.7/site-packages/gunicorn/workers/sync.py", line 171, in handle_request
08:57:07 web.1  |     respiter = self.wsgi(environ, resp.start_response)
08:57:07 web.1  |   File "/Users/yama/.pyenv/versions/heroku/lib/python2.7/site-packages/whitenoise/base.py", line 62, in __call__
08:57:07 web.1  |     return self.application(environ, start_response)
08:57:07 web.1  |   File "/usr/local/google_appengine/lib/django-1.5/django/core/handlers/wsgi.py", line 236, in __call__
08:57:07 web.1  |     self.load_middleware()
08:57:07 web.1  |   File "/usr/local/google_appengine/lib/django-1.5/django/core/handlers/base.py", line 55, in load_middleware
08:57:07 web.1  |     raise exceptions.ImproperlyConfigured('Middleware module "%s" does not define a "%s" class' % (mw_module, mw_classname))
08:57:07 web.1  | ImproperlyConfigured: Middleware module "django.contrib.auth.middleware" does not define a "SessionAuthenticationMiddleware" class

なぜか google app enginedjango を参照しているっぽい。。。

で、確認してみたところどうやら昔 GAE python の勉強をしていた時に
ここ を参考に .zshrc にパスを追加していたのが原因のよう。

PYTHONPATH="/usr/local/google_appengine:/usr/local/google_appengine/lib/django-1.5:$PYTHONPATH"
export PYTHONPATH

これを削除してターミナルを再起動したらちゃんと動いた。


Push local changes

特になし。ローカルでの変更をコミットして git push heroku master すると heroku にデプロイできますよという話。


Provision add-ons

デフォルトでは、heroku は 1500 行分のログしか保存してくれない。
そのため papertrail というアドオンをインストールする。
「heroku addon logging」とかでググって出て来るのは papertrail の他、Logentries とかも同じような機能のアドオンなのかな?(未確認)

また、アドオンのインストールは事前にアカウントを verify する必要がある。
verify なしだとこんなメッセージが表示される。

$ heroku addons:create papertrail
 !    Please verify your account to install this add-on plan (please enter a credit card) For more information, see https://devcenter.heroku.com/categories/billing Verify now at https://heroku.com/verify

メッセージに記載されている通り、https://heroku.com/verify にアクセスしてクレジットカード情報や住所を入力する。


Start a console

heroku run というコマンドで Heroku 上でコマンドを実行できる。
そのときに使われるのが one-off dyno という一時的な dyno らしい。


Define config vars

heroku で環境変数を利用するための config vars について。
ローカル環境の場合、.env というファイルに記述すると heroku local 時に自動的に export して使うことができる。

.env

TIMES=2

と書くと、python 側で

import os

times = int(os.environ.get('TIMES', 3))

といった書き方で取得できる。

また、デプロイされている heroku アプリケーションに対しては heroku config コマンドを使う。

# config vars をセットする
$ heroku config:set TIMES=2

# config vars を確認する
$ heroku config
=== glacial-tor-4711 Config Vars
DATABASE_URL:         postgres://***************
PAPERTRAIL_API_TOKEN: **********
TIMES:                2


Provision a database

heroku では様々なデータベースをアドオンとして用意してある。
たぶん一番一般的なのが Postgres で、チュートリアルのアプリには最初から heroku-postgres アドオンが追加されている。

$ heroku addons
=== Resources for glacial-tor-4711
Plan                         Name                  Price
---------------------------  --------------------  -----
heroku-postgresql:hobby-dev  cooking-surely-2089   free
papertrail:choklad           dreaming-purely-9244  free

heroku pg コマンドを使うと DB についてのもう少し詳しい情報が見られる。

$ heroku pg
=== DATABASE_URL
Plan:        Hobby-dev
Status:      Available
Connections: 0/20
PG Version:  9.4.4
Created:     2015-07-24 13:11 UTC
Data Size:   7.0 MB
Tables:      11
Rows:        40/10000 (In compliance)
Fork/Follow: Unsupported
Rollback:    Unsupported
Add-on:      cooking-surely-2089

参考: Heroku Postgres | Heroku Dev Center

また、チュートリアルのコードでは /db にアクセスするとデータベースへの保存が行われる。

views.py (抜粋)
def db(request):

    greeting = Greeting()
    greeting.save()

    greetings = Greeting.objects.all()

    return render(request, 'db.html', {'greetings': greetings})

ただし、このままアクセスするとエラーになる。

f:id:dackdive:20151027065726p:plain

ProgrammingError at /db

autocommit cannot be used inside a transaction

テーブルがまだ作成されていないかららしい。

これを解決するためには manage.py migrate コマンドを Heroku 上で実行する。

$ heroku run python manage.py migrate
Running `python manage.py migrate` attached to terminal... up, run.1059
Synchronizing apps without migrations:
  Creating tables...
    Creating table hello_greeting
    Running deferred SQL...
  Installing custom SQL...
Running migrations:
  Rendering model states... DONE
  Applying contenttypes.0001_initial... OK
...

このコマンドを実行した後に再度 /db にアクセスすると、今度は正常に表示される。

f:id:dackdive:20151027070416p:plain

TODO:
ローカルだとまだ動かないみたい。heroku local web で起動した後、http://localhost:5000/db にアクセスすると

f:id:dackdive:20151027092418p:plain

OperationalError at /db
could not connect to server: No such file or directory
Is the server running locally and accepting
connections on Unix domain socket "/tmp/.s.PGSQL.5432"?

というエラーが出る。

Postgres を Homebrew からインストールしたのが原因...?

もうちょい調べる。


Next steps

この次に読むべきものとして 2 つの URL が紹介されている。

感想

とりあえず基本的な使い方はわかった!
前はデプロイしてとりあえず動かす時点で断念したけど、このコードを元にすればデプロイまでは大丈夫そう。

データベースの設定周りがよく理解できてないのと、Procfile で使っている gunicorn というライブラリがわからないのでそれは今後の課題として。