さて、今まで作成したPython + Flask + BlueprintのAPIサーバをAWSのBeanstalkで動作させようと思います。
この記事では、AWSのBeanstalkで動作させるために必要なファイルはなにか、どのようにファイルを作成すればよいかを説明していきます。

以下の記事のプログラムがベースとなっているため、合わせて参考にしていただければ幸いです。

Python+Flask+Blueprintを使ったAPIサーバ構築 ① 〜ユニットテスト実施方法編〜

Python+Flask+Blueprintを使ったAPIサーバ構築 ② 〜エラーハンドリング編〜

Python+Flask+Blueprintを使ったAPIサーバ構築 ③ 〜ユニットテスト実施方法編〜

環境

本記事では、以下の環境で動作確認を行いました。

  • Mac OSX Mojave(10.14.3)
  • Python 3.7.2
  • Click==7.0
  • coverage==4.5.3
  • Flask==1.0.2
  • Flask-Cors==3.0.7
  • itsdangerous==1.1.0
  • Jinja2==2.10
  • MarkupSafe==1.1.1
  • six==1.12.0
  • Werkzeug==0.15.1

前提

Elastic BeanstalkにAPIサーバをアップロードする前に、以下のことが必要になります。

  • AWS Beanstalkにアップロードするため、事前にAWSアカウントを取得する
  • Elastic Beanstalkの使用を開始する
  • Elastic Beanstalk コマンドラインインターフェイス (EB CLI)をインストールする

AWS Beanstalkにアップロードするため、事前にAWSアカウントを取得する

これは解説するほど難しくないので、スキップします。

Elastic Beanstalkの使用を開始する

これも特に難しくありません。AWSの画面上からポチポチするだけです。スクリーンショットだけ共有します。

image

image

Elastic Beanstalk コマンドラインインターフェイス (EB CLI)をインストールする

これも特に難しくありません。pipでインストールするだけです。

$ pip install awsebcli

インストールできているか、確認します。

$ eb --version
EB CLI 3.15.0 (Python 3.7.2)

問題なくインストールできていますね。
ちなみに、EB CLIを使用するユーザーはElastic Beanstalkサービスにアクセスする権限を持っている必要があります。

アップロード準備

AWSはチュートリアルが充実してます。
チュートリアルを参考にしながら、新たなファイル作成、ファイル修正をしていきます。

AWS Elastic Beanstalk への Flask アプリケーションのデプロイ

pip freezeを実行してrequirements.txtという名前のファイルを作成する。

$ pip freeze > requirements.txt

virtualenv環境のために必要なフォルダを、.ebignoreというファイルを作り記載します。
そうすると、Elastic Beanstalkにはアップロードされなくなります。

$ touch .ebignore
bin
include
lib

さて、ファイルを変更していきます。

application.py

import os
import sys
import click
from app import create_app

COV = None
if os.getenv('FLASK_COVERAGE'):
    import coverage
    COV = coverage.coverage(branch=True, include='application/*')
    COV.start()

# print(os.getenv('FLASK_CONFIG'))
application = create_app(os.getenv('FLASK_CONFIG') or 'default')

if __name__ == '__main__':
    print("##### in __main__ function #####")
    application.run()

@application.cli.command()
@click.option('--coverage/--no-coverage', default=False, help='Run tests under code coverage.')
def test(coverage):
    """Run the unit tests."""
    if coverage and not os.getenv('FLASK_COVERAGE'):
        import subprocess
        os.environ['FLASK_COVERAGE'] = '1'
        sys.exit(subprocess.call(sys.argv))

    import unittest
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)
    if COV:
        COV.stop()
        COV.save()
        print('Coverage Summary:')
        COV.report()
        basedir = os.path.abspath(os.path.dirname(__file__))
        covdir = os.path.join(basedir, 'tmp/coverage')
        COV.html_report(directory=covdir)
        print('HTML version: file://%s/index.html' % covdir)
        COV.erase()

@application.cli.command()
@click.option('--length', default=25, help='Number of functions to include in the profiler report.')
@click.option('--profile-dir', default=None, help='Directory where profiler data files are saved.')
def profile(length, profile_dir):
    """Start the application under the code profiler."""
    from werkzeug.contrib.profiler import ProfilerMiddleware
    application.wsgi_application = ProfilerMiddleware(application.wsgi_application, restrictions=[length],
                                      profile_dir=profile_dir)
    application.run()

変更点は2つです。

1つは目は、Flaskのアプリケーションの名前をappからapplicationに変更した点です。これは、 FlaskはWSGIのApacheプロキシサーバーの背後で実行されますが、そのプロキシサーバーはapplicationという名前を探す、という制約からきています。

2つ目は、if __name__ == '__main__':部分を追加して、application.run()を追加した点です。if __name__ == '__main__':の部分は、この関数がpython application.pyのように直接実行された場合のみ、呼ばれるようにする記述の仕方です。先程説明したとおり、Elastic beanstalkはWSGIのApacheプロキシサーバーの背後で実行されるため、application.run()はWSGIが代替して実施します。
そのため、application.run()の部分は、自分がローカル環境等で実行する場合のみ、実行するように記述を変更しています。

.ebextensions/environment.config

下記のように記述すれば、このプログラムが実行されるサーバーに環境変数を設定することができます。

option_settings:
 aws:elasticbeanstalk:application:environment:
   FLASK_APP: application.py
   FLASK_CONFIG: production

Elastic Beanstalkは.ebextensions/[xxxx].configという形のファイルを読み込むため、ファイル形式には注意が必要です。

ファイルをアップロードする

さて、それではファイルをアップロードしていきます。

eb initコマンドでEB CLIリポジトリを初期化する。

$ eb init -p python-3.6 qiita-beanstalk --region ap-northeast-1

ここまで作ってみて気づいたのですが、Python3.7はまだサポートされていません。
そのため、今回は3.6を使用することにしました。商用のシステムではバージョンを合わせることをおすすめします。

eb createを使用してアプリケーションをデプロイします。

$ eb create qiita-beanstalk

ユーザー権限が足りないようで失敗しました。
そのため、IAMに以下のポリシーを付与して再度トライしたところ、上手く行きました。

ElasticLoadBalancingFullAccess
AWSElasticBeanstalkFullAccess
AmazonS3FullAccess

なお、今回は簡単にするために上記のポリシーを付与しましたが、商用のシステムではちゃんと必要な権限を絞ることをおすすめします。
ユーザー権限に関する詳細は下記。

https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/concepts-roles-instance.html

設定したエンドポイントを開いてみましょう。

image

URL等にはマスクをかけていますが、うまくいきました。

ハマりどころ

ここまではかなりすんなりいった感じで書いていますが、大いにハマったポイントがあります。
今回ハマった箇所を共有します。

SERVER_NAME

もともとdevelopment環境としてSERVER_NAMEを127.0.0.1:5000で実装していましたが、SERVER_NAMEを設定しているとHTTPリクエストを送っても200が返ってきませんでした。SERVER_NAMEを設定する場合、WSGI側で設定を上書きしないといけないのか原因は不明ですが、SERVER_NAMEを設定しない(FLASK_ENVにproductionを設定)ことで回避しました。

SERVER_NAMEに原因があるとは思わず、application.pyとかapp/__init__.pyとかapp/apiv1/__init__.pyとかapp/apiv1/testapp.pyとかをくまなくprintしながらチェックして、なぜかapp/apiv1/testapp.py/testが呼ばれていないことが分かり、blueprintの記述の仕方がおかしいのかとごにょごにょ変更して、ググってうまくいくはずのコードで試したけど上手く行かず、最終的にもしかしてと思ってSERVER_NAMEを変更したらうまくいきました。

終わり

最終的には問題なくElastic Beanstalkにアップロードできたので良かったのですが、プログラムというのは面白くて、数時間(もしくは数日とか)悩んだ後、たった一行の変更でうまくいくということあるんですよね。

ここまでくればあとは機能を追加していくだけだと思うので、存分にPythonを楽しんでいただければと思います。