Alternative Architecture DOJO

オルターブースのクラウドネイティブ特化型ブログです。

Flask+MySQLのアプリを作ってAzureにデプロイする!

オルターブース エンジニアのみっつーです!
この記事はオルターブース Advent Calendar 2021の6日目の記事です。 adventar.org

昨日は松村さんによるAzure FunctionsのAzure SQLバインディングの話でした! aadojo.alterbooth.com

今日の投稿では、FlaskとMySQLを使用したアプリケーションの開発と、開発したアプリのAzureデプロイ方法を紹介します!
私は日常でPythonを使うことが一切無かったのですが、今年学生向けハッカソンのメンターとして参加させて頂く中でPython、特にFlaskを見かける機会が増えたので勉強がてらまとめてみました。
力技な部分もありますが、少しでもクラウド上でPythonを動かす際の参考になれば幸いです。
では、早速やっていきたいと思います!

今回実装する構成図

目次

  1. 必要なツール
  2. 初期アプリの作成とローカルデバッグ
  3. Azureへデプロイ
  4. Jinja2テンプレートの作成とビュー出力
  5. MySQLの作成、CRUD処理の実装
  6. 再デプロイ
  7. 本投稿の作成にあたって躓いた総て
  8. 参考

1. 必要なツール

  • Python実行環境
    • 今回はバージョン3.8で実施しました
  • ブラウザ
    • Webアプリの動作確認、Azureポータル操作用途
    • Google Chromeで実施しました
  • VSCode
    • アプリ実装用途
    • Azure Tools拡張機能を入れておくとデプロイがGUIでも行えます
  • Docker
    • 本投稿の6章でコンテナーとしてデプロイする場合に使用します

2. 初期アプリの作成とローカルデバッグ

Flaskのクイックスタートに沿ってアプリ作成し、ローカルデバッグできるところまで進めます。
下記コマンドを実行し、Flaskパッケージを取得します。

# プロジェクトディレクトリの作成
mkdir myproject
cd myproject

# Flaskパッケージの取得
python3 -m venv venv
. venv/bin/activate
pip install Flask

app.py を作成します。

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

flask run を実行し、ブラウザで http://127.0.0.1:5000/ にアクセスします。
<p>Hello, World!</p> の通り出力されればOKです。

3. Azureへデプロイ

ここまでの内容を一度Azureにデプロイします。
下記コマンドを実行し、 requirements.txt を作成後、Web Appsへデプロイします。

# requirements.txt出力
pip freeze > requirements.txt

# azコマンドを使用してAzure Web Appsへデプロイ
az login
az webapp up --sku F1 --resource-group <rg-name> --name <app-name> --location japaneast

※skuオプションが料金プランにあたるもので、F1とすることで無料プランで作成できます。
その他az webapp upのコマンドオプションは下記参照↓
https://docs.microsoft.com/ja-jp/cli/azure/webapp?view=azure-cli-latest#az_webapp_up

デプロイ完了後、 https://<app-name>.azurewebsites.net/ にアクセスして反映されていることを確認します。

4. Jinja2テンプレートの作成とビュー出力

テンプレートを作成し、まずは変数をレンダリングする部分を簡易的に実装します。
templates ディレクトリを作成し、配下に hello.html を作成します。

<!doctype html>
<title>Hello from Flask</title>
{% if name %}
  <!-- nameが渡されたら値を表示する -->
  <h1>Hello {{ name }}!</h1>
{% else %}
  <h1>Hello, World!</h1>
{% endif %}

app.pyに、テンプレートを使用する処理を追加します。

from flask import render_template
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)

ここまででディレクトリ構成は下記のようになっています。

myproject
├ templates
│ └ hello.html
├ requirements.py
└ app.py

flask run を実行し、ブラウザで http://127.0.0.1:5000/hello/hoge にアクセスします。
<h1>Hello hoge!</h1> の通り出力されればOKです。
URL中『hoge』の部分を別の文字に変えたり、何も入力しない場合に画面に変化があるかを確認します。

この状態で再度デプロイし、変更がAzure上にも反映されることを確認します。
再デプロイ時のコマンドは下記の通りです。

az webapp up -g <rg-name> -n <app-name> 

5. MySQLの作成、CRUD処理の実装

いよいよMySQLリソースを作成し、CRUD機能を実装していきます。

もし今回のような構成でガッツリ開発される際は、MySQLはローカル環境にも作成して、ローカルデバッグ時はそちらを使用するよう使い分けると良いです。
尺の都合上、最初からAzure Database for MySQLを使って行きます。

Azureポータルから、Azure Database for MySQLリソースを作成します。
https://azuremarketplace.microsoft.com/ja-jp/marketplace/apps/Microsoft.MySQLServer?tab=Overview

『サーバーの構成』からスペックや金額を調整することができます。

MySQLの作成完了後、MySQLリソースの画面から『接続のセキュリティ』を開き、Azureサービスへのアクセスを許可を『はい』にし、『現在のクライアント IP アドレスを追加する ( x.x.x.x )』を選択してIPを追加した状態で『保存』を選択します。
Azureサービスへのアクセス許可は、Web Appsからのアクセスを許可します。
クライアントIPの追加は、ローカル環境からのアクセスを許可します。

今後、DB接続時に「SSL接続をする必要がある」旨のエラーが出る場合は、下記ドキュメントを参考に BaltimoreCyberTrustRoot.crt.pem を取得し、ツールであればcert_caを使用する設定を、アプリ側はプロジェクトディレクトリに配置したうえで.envの SQLALCHEMY_DATABASE_URI 値の末尾に ?ssl_ca=BaltimoreCyberTrustRoot.crt.pem を追記してください。
https://docs.microsoft.com/ja-jp/azure/mysql/howto-configure-ssl#step-1-obtain-ssl-certificate

MySQLに接続できるツール等で CREATE DATABASE <db_name>; を実行します。
(db_nameは任意の値でOKで、次に示す接続文字列内で使用します。)

.env を作成し、下記の通り接続文字列を定義します。
user_nameは <ユーザー名>@<リソース名> となるのでご注意ください。

SQLALCHEMY_DATABASE_URI=mysql://<user_name>:<password>@<host>/<db_name>

下記記載のpip installで、必要なパッケージを導入します。
python-dotenvはPythonから.envを読み込めるようにします。
SQLAlchemyはPython用のORMです。
Flask-MigrateはDBマイグレーションを行うためのパッケージです。
mysqlclientはMySQLを使うので入れます(問答無用)

pip install python-dotenv
pip install SQLAlchemy flask-sqlalchemy
pip install Flask-Migrate
pip install mysqlclient

models/user.py を以下の通り作成します。
ID,ユーザー名を持つモデルクラスとして使用します。

from data.database import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)

    def __repr__(self):
        return '<User %r>' % self.username

data/database.py を以下の通り作成します。
SQLAlchemyオブジェクトを持ちます。

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()

def init_db(app):
    db.init_app(app)
    Migrate(app, db)

app.pyに、DBアクセスを行う処理を追加します。

from data.database import init_db
import os
app = Flask(__name__) # この下に追記
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('SQLALCHEMY_DATABASE_URI', 'sqlite:////tmp/test.db')
init_db(app)

下記コマンドで、DBマイグレーションを実行します。
これがFlask-Migrateの機能で、テーブル定義の変更履歴管理ができます。

flask db init
flask db migrate # migrations/versionsディレクトリにファイルができたことを確認します
flask db upgrade # DBにusersテーブルができたことを確認します

CRUDを行う画面のテンプレートファイルを作成します。
templatesディレクトリ配下に users ディレクトリを作成し、下記htmlファイルの作成を行います。

templates/users/index.html を下記の通り作成します。
Userデータ一覧表示画面です。

<!doctype html>
<title>Hello from Flask</title>
<h1>Index</h1>
<a href="{{ url_for('users_create') }}">新規作成</a>

<table>
    <thead>
        <tr>
            <th>ユーザー名</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <!-- ループで一覧表示する。各データの編集・詳細画面のリンクも出力 -->
        {% for user in users %}
        <tr>
            <td>
                {{ user.username }}
            </td>
            <td>
                <a href="{{ url_for('users_edit', id=user.id) }}">編集</a>
                <a href="{{ url_for('users_details', id=user.id) }}">詳細</a>
            </td>
        </tr>
        {% endfor %}
    </tbody>
</table>

templates/users/details.html を下記の通り作成します。
(1件の)Userデータ詳細表示画面です。

<!doctype html>
<title>Hello from Flask</title>
<h1>Details</h1>

<!-- reprで定義したヤツが表示できたりもします -->
<h2>{{ "{}".format(user) }}</h2>

<form action="{{ url_for('users_delete_post') }}" method="post">
    <input type="hidden" name="id" value="{{ user.id }}" />
    <input type="submit" value="削除" class="btn btn-danger" /> |
    <a href="{{ url_for('users_index') }}">一覧へ戻る</a>
</form>

templates/users/create.html を下記の通り作成します。
Userデータ作成画面です。

<!doctype html>
<title>Hello from Flask</title>
<h1>Create</h1>

<form action="{{ url_for('users_create_post') }}" method="post">
    <div class="form-group">
        <label for="InputTitle">ユーザー名</label>
        <input type="text" class="form-control" name="username" />
    </div>
    <input type="submit" value="作成" class="btn btn-primary" />
</form>

<a href="{{ url_for('users_index') }}">一覧へ戻る</a>

templates/users/edit.html を下記の通り作成します。
Userデータ編集画面です。

<!doctype html>
<title>Hello from Flask</title>
<h1>Edit</h1>

<form action="{{ url_for('users_edit_post') }}" method="post">
    <input type="hidden" name="id" value="{{ user.id }}" />
    <div class="form-group">
        <label for="InputTitle">ユーザー名</label>
        <input type="text" class="form-control" name="username" value="{{ user.username }}" />
    </div>
    <input type="submit" value="更新" class="btn btn-primary" />
</form>

<a href="{{ url_for('users_index') }}">一覧へ戻る</a>

app.pyに、各画面の表示・submit時処理を追加します。

from flask import redirect
from flask import request
from flask import url_for
from data.database import db
from models.user import User
@app.route('/users', methods=['GET'])
def users_index():
    users = User.query.order_by(User.id.desc()).all()
    return render_template('users/index.html', users=users)

@app.route('/users/details/<int:id>', methods=['GET'])
def users_details(id=0):
    user = User.query.get(id)
    return render_template('users/details.html', user=user)

@app.route('/users/create', methods=['GET'])
def users_create():
    return render_template('users/create.html')

@app.route('/users/create', methods=['POST'])
def users_create_post():
    user = User(username = request.form['username'])
    db.session.add(user)
    db.session.commit()
    return redirect(url_for('users_index'))

@app.route('/users/edit/<int:id>', methods=['GET'])
def users_edit(id=0):
    user = User.query.get(id)
    return render_template('users/edit.html', user=user)

@app.route('/users/edit', methods=['POST'])
def users_edit_post():
    id = request.form['id']
    user = User.query.get(id)
    user.username = request.form['username']
    db.session.commit()
    return redirect(url_for('users_index'))

@app.route('/users/delete', methods=['POST'])
def users_delete_post():
    id = request.form['id']
    user = User.query.get(id)
    db.session.delete(user)
    db.session.commit()
    return redirect(url_for('users_index'))

ここまででディレクトリ構成は下記のようになっています。

myproject
├ data
│ └ database.py
├ migrations
│ └ (コマンドにより生成されたファイル群)
├ models
│ └ user.py
├ templates
│ ├ users
│ │ │ create.html
│ │ │ details.html
│ │ │ edit.html
│ │ └ index.html
│ └ hello.html
├ .env
├ requirements.py
└ app.py

flask run を実行し、ブラウザで http://127.0.0.1:5000/users にアクセスします。
『新規登録』ボタンを押下して /users/create 画面へ遷移し、ユーザー名を入力してデータを作成します。
一覧画面に自動遷移後、データが表示されればOKです。
『編集』『詳細』画面や『削除』処理も確認してみてください。

6. 再デプロイ

ここまでの内容を改めてAzureにデプロイします。

flask_sqlalchemyのエラーが解消できなかったので、ACR(Azure Container Registry)経由でのデプロイ方法を記載します。
今年ハッカソンでも近しいエラー(requirements.txtに含まれるパッケージの一部が上手く読み込まれない問題)を何度か見かけたので、時間が無い中でそのような状況に陥ったときの参考にして頂ければと思います(プラス思考)。

pip freeze > requirements.txt で最新のパッケージ一覧をrequirements.txtに反映します。
src ディレクトリを作成します。
ここまでで作成したファイル全てをsrcディレクトリに移します。
(venvディレクトリは移さなくてOKです。移行先で flask run によるデバッグを行う場合は、移すか再度venv作成し直しを行ってください。)

(ここまででまだ取得していなければ)下記ドキュメントを参考に BaltimoreCyberTrustRoot.crt.pem を取得し、src直下に配置します。
https://docs.microsoft.com/ja-jp/azure/mysql/howto-configure-ssl#step-1-obtain-ssl-certificate
.envの SQLALCHEMY_DATABASE_URI 値の末尾に ?ssl_ca=BaltimoreCyberTrustRoot.crt.pem を追記します。

Dockerfile を下記の通り作成します。

FROM python:3.8

RUN mkdir /code
WORKDIR /code

# add files
ADD entrypoint.sh /code/entrypoint.sh
ADD src/ /code

# pip install
RUN pip install --upgrade pip --no-cache-dir
RUN pip install -r requirements.txt --no-cache-dir

EXPOSE 80
ENV PYTHONPATH "${PYTHONPATH}:/code/"
CMD ["/code/entrypoint.sh"]

entrypoint.sh を下記の通り作成します。

#!/bin/sh
flask run --host=0.0.0.0 --port=80

.dockerignore を下記の通り作成します。
イメージ作成上不要になるディレクトリです。

__pycache__/
.vscode/
venv/

ここまででディレクトリ構成は下記のようになっています。

src # リネームという横着
├ data
│ └ database.py
├ migrations
│ └ (コマンドにより生成されたファイル群)
├ models
│ └ user.py
├ templates
│ ├ users
│ │ │ create.html
│ │ │ details.html
│ │ │ edit.html
│ │ └ index.html
│ └ hello.html
├ .env
├ BaltimoreCyberTrustRoot.crt.pem
├ requirements.py
└ app.py
.dockerignore
Dockerfile
entrypoint.sh

docker build コマンドを実行し、イメージを作成します。
ローカルで動作確認する場合は、 docker run を実行します。
コマンド例は下記の通りです。 <> の箇所は任意の値で置き換えてください。

docker build -t <image_name>:<tag> .
docker run -itd -p 8080:80 --name <name> <image_name>:<tag>

ACRリソースを作成します。
リソース名(を全て小文字にしたもの)を次のコマンドで使用するので、控えておきます。
https://azuremarketplace.microsoft.com/en-us/marketplace/apps/microsoft.containerregistry?tab=overview

下記コマンドで、イメージをACRへプッシュします。
acr_name に先程控えたリソース名を適用します。

az login
az acr login <acr_name>

docker tag <image_name>:<tag> <acr_name>.azurecr.io/<acr_image_name>:<acr_tag>
docker push <acr_name>.azurecr.io/<acr_image_name>:<acr_tag>

作成したACRをポータルから開き、『リポジトリ』メニューを選択します。
先程プッシュしたイメージからWeb Apps(新しいリソース)へデプロイを行います。
『Web アプリにデプロイ』を選択して進めていけばOKです。

作成されたWeb Apps画面からURLを確認・アクセスし、動作確認を行います。
以上で、FlaskでMySQLアクセスを行うアプリケーションのデプロイが完了しました!

上手く起動できない・エラー画面が出る場合は下記よりログを確認することも可能です。


 

7. 本投稿の作成にあたって躓いた総て

  • デプロイする際はrequirements.txtを忘れない(忘れない)
  • Azure Databaseの接続にcert_caの設定が必要(999回目)
  • FlaskアプリをWeb Appsへデプロイした場合、デフォルトではapp.pyまたはapplication.pyが起点になる
  • VSCodeのazure account系コマンドがnot foundになる時は、VSCodeのバージョンか拡張機能のバージョンを更新してみると良さげ
  • VSCode拡張機能でデプロイする際、workspaceの選択ステップがある
    • プロジェクトのディレクトリを作業ディレクトリにしていればそのままでOK
    • プロジェクト以外のディレクトリでVSCode開いてる場合は設定内容や保持されている設定(.vscodeのファイル)に少し注意
  • 自前Linux上で実行する場合、ライブラリ不足に注意(Ubuntuで実行しようとした際に引っかかった)
    • libffi-dev、libmysqlclient-dev など
    • pyenv uninstall→installが必要になる場面もあり(ctypes不足時)
  • Web Appsでflask_sqlalchemy読み込めない問題
    • 何らかのバージョンが噛み合っていないためかもしれないがpythonのバージョン変更(3.6~3.9)では解決せず
      • requirementsを必要なものだけ(pip installで明示的に追加したもの)に絞っても未解決
    • ggったら「requirements.txt作ってないだけだったからpip freeze出力したら直ったわ~」的な投稿が多かったがこちら直らず。次Flaskアプリ組むようなことがあれば再チャレンジします

[ERROR] from flask_sqlalchemy import SQLAlchemy
[ERROR] ModuleNotFoundError: No module named 'flask_sqlalchemy'

8. 参考


以上です!めっちゃ躓きました!笑
ここまで読んで頂きありがとうございます。

本投稿をきっかけに、是非Webアプリケーション開発に踏み込んでみてください~🤗✨
と、引き続き明日以降の記事もお楽しみに🌟


追記: 最終形をGitHubにアップしましたので、比較参考などにご活用ください! github.com

www.alterbooth.com

www.alterbooth.com

cloudpointer.tech