FlaskアプリをAWS Elastic Beanstalkにデプロイする

# はじめに
この記事はFlaskで作成したアプリをAWS Elastic Beanstalk（以下EB）にデプロイするまでの手順を自分の備忘録としてまとめたものです。
AWS EBのドキュメントにはFlaskアプリをEBにデプロイする方法が記載されており、この通りにやれば簡単にできるのですが、デフォルトVPCが自動で選択されたり、ELBの種類がClassicから変更できなかったり、EBの環境内にRDS作成すると後で面倒だったりするので、いろいろ設定をいじって環境を作成しました。

### 今回作成したAWSアーキテクチャ
![architecture.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/5c47f426-8093-df34-514c-7962815d2adc.jpeg)
・・・あってるかな？


# EB CLI
EBをコマンドラインから操作するため、[ドキュメント](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/eb-cli3-install.html)にしたがってEB CLIをインストールしておきます。

# アプリケーションの作成
まずはプロジェクトディレクトリを作成します。

```
> mkdir eb-flask
> cd eb-flask
```

次にeb-flask内で`eb init`コマンドでアプリケーションを作成します。

```
eb-flask> eb init -p python-3.8 flask-app --region ap-northeast-1
```

プラットフォームは`Python 3.8`を選択。アプリケーション名は`flask-app`とします。pythonはバージョンをしていないとpython2系がインストールされるので注意。`Application flask-app has been created.`と表示されれば成功です。
EBのコンソールをチェックするとアプリケーションが作成されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/47b08168-db14-51d1-4c29-d81e52a7c991.png)

# ネットワークの設定
### VPC
次に環境を作成していくわけですが、自分で作成したカスタムVPCに環境を紐づけたいので、まずはVPCを作成します（環境作成時にVPCを設定しなければ自動でデフォルトVPCが紐づけられます）。VPCのコンソールから設定を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/99d99065-e80c-e724-9c3b-37bab3a2a588.png)
VPCの名前は`flask-app-vpc`で作成。IPv4 CIDRブロックは`10.0.0.0/16`。その他は変更なし。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/c9faeb9b-ca63-a58e-f6bf-974ccd8b445e.png)
作成されました。

### サブネット
次に作成したVPCにパブリックサブネット2つとプライベートサブネット2つを作ります。パブリックサブネットはEBの環境作成時にApplication Load Balancerを設定できるようにするため、プライベートサブネットは後々RDSを置くためです。
> Application Load Balancer を作成するには、2 つ以上のアベイラビリティーゾーンにサブネットを含む VPC にお客様の環境があることが必要です。すべての新しい AWS アカウントには、この要件を満たすデフォルトの VPC が含まれます。環境が 1 つのアベイラビリティーゾーンのみにサブネットを持つ VPC にある場合、デフォルトで Classic Load Balancer になります。サブネットがない場合、負荷分散を有効にすることはできません。

正直、ApplicationとClassicの違いも分かっていないし、webサーバー1つにして負荷分散する必要もないんじゃないかと思いますが、ここでは勉強用のために設定しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/ae208943-4e8c-398f-2f88-afddee044773.png)
こんな感じで2つのアベイラビリティゾーンにそれぞれ作成しておきます。

### インターネットゲートウェイ
インターネットゲートウェイを作成してVPCにアタッチしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/f851b66c-5199-bc47-5681-82bdcc26ed19.png)

### ルートテーブル
パブリックサブネットに関連づけるルートテーブルを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/6547a978-a96c-bc37-1eec-5d61764791cc.png)
作成したら`10.0.0.0/16`以外の通信をインターネットに流すように以下のようにルートを設定しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/95684506-bdab-6225-1ca9-985f202b5eaf.png)
さらにパブリックサブネットの関連付けを行っておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/d2e2c8ed-de67-4f48-2550-4b6e17ca1dca.png)

# 環境の作成
VPCの設定が完了したので、EBの環境を作成していきます。EBコンソールでアプリケーション`flask-app`を選択して**新しい環境の作成**を行います。
環境枠は**ウェブサーバー環境**を選択。
環境名は自動で作成されます（変更も可）。今回は`Flaskapp-env`。
プラットフォームはPythonを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/8895ec03-6303-6c27-145a-e16f2a329c63.png)
アプリケーションコードは**サンプルアプリケーション**を選択。
次に**より多くのオプションの設定**を行います。
まずはネットワークの設定。VPCは`flask-app-vpc`を選択。
インスタンスの設定は以下の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/54164767-6d26-1856-b4b9-b9990666af72.png)
次に容量の編集。環境タイプは**負荷分散**を選択。インスタンスは最小1、最大2とします。
その他は変更なし。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/eb2412bc-7db2-5291-551f-288c9f364e29.png)
この時点で自動でロードバランサーが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/c8854874-3ce2-2ce9-e177-9ccabaf0616e.png)
ここまで設定したら**環境を作成**します。

ダッシュボードがこんな感じになったら環境の作成成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/f5891e6f-9669-44f4-6555-c3d56c57a250.png)
左上のURLをクリックするとこんな画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/1abeaebc-2ce4-1e03-e5cf-6058faeffd72.png)

一番最初に作成したプロジェクトディレクトリで`eb list`を実行すると`flask-app`内の環境一覧が表示されます。いまは`Flaskapp-env`のみです。

```
eb-flask> eb list
Flaskapp-env
```

# 作成された環境を確認する
ここまででサンプルアプリケーションを起動できたので、EBで作成された環境を確認してみます。
まずは**EC2インスタンス**。`Flaskapp-env`というEC2インスタンスが起動しています。ちゃんと自分で作成したVPC内のパブリックサブネットにあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/53bac09c-9918-6ecb-ce43-c8b013696108.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/b37cf6f6-4efc-5cad-7359-5b09f1e5c60e.png)
**セキュリティグループ**も自動で作成されます。セキュリティグループの中身を確認するとHTTP通信はロードバランサーに設定されているセキュリティグループからの通信のみ受け入れるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/c0c2f5d5-c3be-8549-d213-3ce8cbbe3ab9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/1b3a86ee-9e8f-d3d2-9eed-0263d4d3085c.png)
後でSSH通信できるようにルールを追加しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/7f5db3e7-fd0b-654a-d083-adca1daf4ade.png)
**ロードバランサー**も自動で作成されます。名前がわかりづらい・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/780301e9-d612-d9cf-79fa-e01b701298bb.png)

### EC2サーバにSSH通信する
`eb ssh`コマンドで環境内のEC2サーバにSSH通信してみます。まずはキーペアを作成します。

```
eb-flask> eb ssh Flaskapp-env --setup
```

作成されたキーペアを使ってログインします(キーペアのファイルがどこにあるかわからない場合は、ユーザフォルダ直下の`.ssh`にあると思います)。

```
eb-flask> eb ssh Flaskapp-env
  _____ _           _   _      ____                       _        _ _
 | ____| | __   ___| |_(_) ___| __ )  ___  __ _ _ __  ___| |_ __ _| | | __
 |  _| | |/ _ \/ __| __| |/ __|  _ \ / _ \/ _\ | '_ \/ __| __/ _\ | | |/ /
 | |___| | (_| \__ \ |_| | (__| |_) |  __/ (_| | | | \__ \ || (_| | |   <
 |_____|_|\__,_|___/\__|_|\___|____/ \___|\__,_|_| |_|___/\__\__,_|_|_|\_\

 Amazon Linux 2 AMI

 This EC2 instance is managed by AWS Elastic Beanstalk. Changes made via SSH
 WILL BE LOST if the instance is replaced by auto-scaling. For more information
 on customizing your Elastic Beanstalk environment, see our documentation here:
 http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers-ec2.html

[ec2-user@ip-10-0-11-97 ~]$ 
```

無事ログインできました。後から設定のためにSSH通信しますが、とりあえず今はログアウトしておきます。

# データベースの設定
### RDSインスタンスはEB環境から作成しないほうがよい
RDSは環境の設定から作成することができます。しかし、RDSは環境外に作成して環境内のEC2インスタンスから接続する形にしたほうがいいです。公式ドキュメントにも書いていますが、一度追加すると環境から削除することができず、環境を終了するとRDSインスタンスも終了してしまうからです。その他にもEB環境からRDSを作成するとサブネットグループ、パラメータグループ、オプショングループを自分で設定できません。

>環境の一部であるデータベースインスタンスは、環境のライフサイクルに固定されます。一度追加すると環境から削除することはできません。環境を終了すると、データベースインスタンスも終了します。

### セキュリティグループ
そんなわけで、RDSインスタンスを作成しますが、その前にRDSインスタンス用のセキュリティグループを作成します。
セキュリティグループの作成はEC2のコンソールから行います。インバウンドルールはタイプ`MYSQL/Aurora`、ソースにはwebサーバになるEC2インスタンスのセキュリティグループを設定します。RDSはwebサーバからの通信のみ受け入れ、セキュリティを高めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/630bc263-5858-71c5-8be3-6ff10ec79c65.png)

### サブネットグループ
RDSコンソールに移動して、サブネットグループ、パラメータグループ、オプショングループを作成します。
サブネットグループはVPCの設定時に作成したプライベートサブネット2つを割り当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/74412cbf-e70a-0574-d718-c926fdd86de0.png)
### パラメータグループ、オプショングループ
パラメータグループとオプショングループは特に何も設定せず、作成だけしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/4b8fe032-193f-715f-c80c-824db34339b8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/9155c7a1-91df-b4c8-0697-be6c8918a8da.png)

### データベースの作成
ここまで来たらデータベースを作成していきます。設定は次の通り。

* データベース作成方法を選択：標準作成
* エンジンのタイプ：MySQL
* バージョン：MySQL 8.0.20
* テンプレート：開発/テスト
* DBインスタンス識別子：flask-app-db
* マスターユーザー名：admin
* マスターパスワード：（パスワード）
* DBインスタンスサイズ：バースト可能クラス(db.t3.micro)
* ストレージタイプ：汎用(SSD)
* ストレージ割り当て：20GB
* ストレージの自動スケーリング：OFF
* 可用性と耐久性：スタンバイインスタンスを作成しないでください
* VPC：flask-app-vpc
* サブネットグループ：自分で作成したサブネットグループ
* パブリックアクセス可能：なし
* 既存のVPCセキュリティグループ：自分で作成したセキュリティグループ
* アベイラビリティゾーン：任意
* データベース認証：パスワード認証

追加設定は次の通り。下記以外の項目は変更なし。

* DBパラメータグループ：自分で作成したパラメータグループ
* オプショングループ：自分で作成したオプショングループ
* 暗号化：OFF

作成できたら、webサーバにmysqlをインストールします。

```
eb-flask> eb ssh Flaskapp-env
$ sudo yum -y install mysql
```

mysqlをインストールしたら以下のコマンドでMySQLにログインします。ホスト名にはRDSのエンドポイントを指定します。

```
$ mysql -h {endpoint} -u admin -p
パスワードを入力
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 1689
Server version: 8.0.20 Source distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [(none)]>
```

この後デプロイするFlaskアプリから接続するデータベースをMySQLに作成しておきます。

```
MySQL [(none)]> create database flask_app_db;
Query OK, 1 row affected (0.01 sec)
```

# Flaskアプリをデプロイする
いよいよFlaskアプリをデプロイしていきます。プロジェクトディレクトリの構造は以下のようになっています。

```
eb-flask/
├─__pycache__/
├─.elasticbeanstalk/
├─flaskr/
├─migrations/
├─virt/
├─.ebignore
├─.gitignore
├─requirements.txt
└─application.py
```

```
eb-flask/flaskr/
├─config/
│ ├─development/
│ │ └─settings.cfg
│ └─production/
│   └─settings.cfg
├─templates
├─__init__.py
├─forms.py
├─models.py
└─views.py
```

`.elasticbeanstalk`と`.gitignore`は`eb init`コマンド実行時に自動で作成されます。`virt`は仮想環境です。`migrations`はFlaskの拡張機能であるFlask-Migradeの`flask db init`コマンドを実行すると自動で作成されます。`.ebignore`には仮想環境`virt`を追加します。デプロイ前に`pip freeze > requirements.txt`を実行して現在の環境の設定ファイルを書き出しておきます。`models.py`内で`Flask-SQLAlchemy`を使ってPythonのClassでデータベース構造を定義しています。

> ただし、virt フォルダは、Elastic Beanstalk でアプリケーションを実行するために必要ありません。デプロイすると、Elastic Beanstalk によりサーバーインスタンスに新しい仮想環境が作成され、requirements.txt にリストされているライブラリがインストールされます。デプロイ中にアップロードするソースバンドルのサイズを最小化するには、virt フォルダを離れるように EB CLI に指示する .ebignore ファイルを追加します。

`__init__.py`でアプリケーション本体となるFlaskインスタンスを作成し、プロジェクトディレクトリ直下の`application.py`から起動しています。

```python:__init__.py
config = {
    'development': 'config/development/settings.cfg',
    'production': 'config/production/settings.cfg'
}

def create_app():
    app = Flask(__name__)
    config_file = config[os.getenv('FLASK_ENV', 'development')]
    app.config.from_pyfile(config_file)
    # ここでBlueprintの追加やアプリケーションと拡張機能の関連付けも行っておきます。
    return app
```

```python:application.py
from flaskr import create_app

application = create_app()

if __name__ == '__main__':
    application.run()
```

注意点はプロジェクトディレクトリ直下におくアプリを起動するファイルは必ず名前を`application.py`とし、関数`create_app()`の返り値(Flaskインスタンス)を格納する変数の名前も`application`にすることです。こうすることによって、EBがアプリケーションを認識することができます。

> ファイル名として application.py を使用し、呼び出し可能な application オブジェクト（この場合は Flask オブジェクト）を提供することで、Elastic Beanstalk がアプリケーションコードを見つけやすくなります。

`development`と`production`内の`settings.cfg`では以下の項目だけ設定しておきます。

```settings.cfg
SECRET_KEY={secret key}
SQLALCHEMY_DATABASE_URI='mysql+pymysql://{username}:{password}@{endpoint}/{database}'
SQLALCHEMY_TRACK_MODIFICATIONS=False
# productionとdevelopmentで設定値は変更する
```

{database}は今回の例では`flask_app_db`です。ここらへんを設定して、ローカルでアプリが正常に起動することを確認したらEBにデプロイしていきます。次のコマンドを実行するだけです。

```
eb-flask> eb deploy Flaskapp-env
```

デプロイ後、EBコンソールからヘルスステータスを確認し問題なかったらOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/7b34e746-ecb2-2e29-2a0a-ba8cab6acb3a.png)

# EC2インスタンスからデータベースのマイグレーションを実行する
### EC2インスタンス上の仮想環境を探す
ここまででアプリケーションをデプロイしてwebサーバからMySQLに接続することができました。しかし、今のままではFlask-SQLAlchemyを使って`models.py`に定義したデータベース構造を`flask_app_db`にマイグレーションすることが出来ていません。この操作はFlask-Migrateの`flask db`コマンドを使って実行することが出来ます。ローカルであればシンプルに仮想環境内にFlask-Migrateをインストールして、`flask db`コマンドを実行するだけです。これをEC2インスタンス上で実行するために、まずはアプリケーションが置かれている仮想環境を探します。前述したように、EBはデプロイするときにEC2インスタンスに自動的に新しい仮想環境を作成しますが、これは`/var/app/venv/staging-LQM1lest/bin/`にあります。どうやら`staging-LQM1lest`という仮想環境が作成されているようです。次のコマンドで仮想環境に入ります。

```
$ source /var/app/venv/staging-LQM1lest/bin/activate
(staging)$
```

### 環境変数を設定する
やっと仮想環境に入れましたが、このまま`flask db`を実行すると「`FLASK_APP`を設定しろ」と怒られるので先に設定しておきます。私の場合は`FLASK_ENV`が`production`か`development`かによって設定も異なるので、これも設定しておきます。

```
(staging)$ export FLASK_APP=application.py
(staging)$ export FLASK_ENV=production
```

### マイグレーションを実行する
`/var/app/current/`に作成したFlaskアプリが入っているので、移動してから、マイグレーションを実行します。`current`ディレクトリ直下に`migrations`ディレクトリがあることを確認しておきます。

```
(staging)$ cd /var/app/current/
(staging)[current]$ flask db upgrade
```

以上でマイグレーション完了です。MySQLに接続してテーブルが作成されているか確認しておきます。

これで出来るんですが、マイグレーションを実行する正しいやり方なのか正直疑問です。誰か正しい方法知っていたら教えてください・・・。

# EBの環境変数を設定する
最後にEBの環境`Flaskapp-env`の環境変数を設定します。紛らわしいですが、マイグレーションのときの仮想環境`staging-LQM1lest`とは異なります。本番環境の設定ファイルを参照できるように`FLASK_APP`に`production`を設定します。

```
eb-flask> eb setenv FLASK_ENV=production
eb-flask> eb printenv Flaskapp-env
Environment Variables:
     FLASK_ENV = production
     PYTHONPATH = /var/app/venv/staging-LQM1lest/bin
```

以上ですべての作業完了です。

# 環境を終了するときの注意点
環境を終了するときは、事前にRDSインスタンスに設定されているセキュリティグループのインバウンドルールにあるwebサーバからの通信を削除しておきます。そうしないと、環境終了時に依存関係のエラーが出て正常に終了することができません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681683/502d8b44-bb07-b678-1d7e-e10e4771fe52.png)
↑このルールを削除しておく

# 謝辞
Flaskアプリの作成とAWSの使い方については、下記のudemy講座で勉強しました。本当にありがとうございます。

Python+FlaskでのWebアプリケーション開発講座！！～0からFlaskをマスターしてSNSを作成する～
https://www.udemy.com/course/flaskpythonweb/

AWS：ゼロから実践するAmazon Web Services。手を動かしながらインフラの基礎を習得
https://www.udemy.com/course/aws-and-infra/