【Microsoft Azure】Azure SQL Databaseと接続するPython-DjangoアプリをApp Serviceにデプロイする(前編)

こんにちは、今日は、Python-Djangoで構築したアプリをMicrosoft AzureのApp Serviceにデプロイする手順を整理しておこうと思います。さらに、一般的なWebアプリケーションではデータベースへの接続も伴いますので、Azure SQL Databaseに接続するアプリケーションの前提で解説します。

また、私自身も試行錯誤しながらでたくさんのエラーにぶつかったので、備忘も兼ねてそれらへの対処法も記録しておこうと思います。今後同じことを試される方の参考になれば幸いです。

Contents

前提

今回の手順では、以下のツール・環境を利用する前提です。実施内容は大きく変わらない想定ですが、念のため・・

  • コードエディタ:VSCode
  • OS:Windows
  • PCにPythonがインストールされていること(私の環境は3.8.10)
  • Microsoft Azureのアカウントを持っていること

今回参考にした記事:

https://medium.com/@pamaron/deploy-a-django-application-connected-to-azure-sql-using-docker-and-azure-app-service-a2c107773c11

ローカルで動作するDjangoアプリケーションの準備

はじめに、Djangoアプリケーションを作成します。Djangoをインストールし、Djangoのコマンドより空のプロジェクトを作成します。

pip install django
django-admin startproject django_webapp_sqldb

この状態で、まずはデフォルトのアプリが問題なく動作することを確認しておきます。以下のコマンドを実行します。

cd django_webapp_sqldb

# venvという名前の仮想環境を有効化し、必要パッケージを準備
python -m venv venv 
venv\Scripts\activate.bat
pip install --upgrade pip #pipのバージョンを最新化しておく
pip list #venvにインストールされているパッケージの一覧を確認
pip install django # vnevにもdjangoをインストール

# ローカルでアプリ起動
python manage.py migrate
python manage.py runserver
## 実行ログ (migrate)
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK

## 実行ログ (runservere)
Performing system checks...

System check identified no issues (0 silenced).
June 20, 2021 - 12:58:46
Django version 2.1.15, using settings 'django_webapp_sqldb.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

runserver実行後、ブラウザからlocalhost:8000にアクセスし、Djangoのデフォルトアプリが表示されることが確認できました!

必要なAzureリソースの作成

続いて、このアプリをクラウド (Micrsoft Azure)上で動くように構成していきます。今回必要なクラウド上のリソースは、以下の2つです。

  • Azure App Service:Webアプリケーションをホストするためのサービス
  • Azure SQL Database:データベース

Azure App Serviceリソースの作成

Webアプリの作成から、以下の設定値(ランタイムスタック= Python 3.8、OS = Linux)で新規アプリの作成を行います。(他の設定値は、何でも良いので規定値にしておいても大丈夫です)でこのデプロイは数分で完了します。

デプロイ後、アプリのURLへアクセスして、App Serviceのデフォルト画面が出ていることを確認しておきます。

Azure SQL Databaseリソースの作成

SQL Databaseの作成にあたっては、サーバーとデータベースの2つのリソースの作成が必要です。サーバーについては、データベースの作成時に未作成であれば、その場で新規作成可能です。

こちらも、設定値は基本的に既定値で問題ないのですが、価格プランだけ、コストを抑えるため”サーバレス”を選択しておきます。

(参考)SQLデータベースのサーバレスモード:

https://docs.microsoft.com/ja-jp/azure/azure-sql/database/serverless-tier-overview

運用環境であれば、ここからさらに各リソースのネットワーク構成なども考慮が必要ですが、今回はお試しなので、いずれもパブリックにエンドポイントが公開された状況である点にご注意ください。

Azure SQL Databseとの接続を構成する

さて、それではAzure SQL DatabaseをDjangoのデータベースとして利用するようにDjangoプロジェクトの構成を変更していきます。

settings.pyの構成

Djangoでは、データベースの接続設定は、settings.pyファイルの中で記述します。規定の設定値を以下の通り書き換えます。

# settings.py
DATABASES = {
     'default': {
         'ENGINE': 'sql_server.pyodbc',
         'NAME': '<DatabaseName>',
         'USER': '<UserName>',
         'PASSWORD': '{your_password_here}',
         'HOST': '<ServerName>',
         'PORT': '<ServerPort>',
         'OPTIONS': {
             'driver': 'ODBC Driver 17 for SQL Server',
             'MARS_Connection': 'True',
         }
     }
 }

なお、上記情報は、先ほど作成したAzure SQL Databaseの接続文字列ブレード>ODBCタブより確認できます。

ODBCドライバ・ライブラリのインストール

さて、必要な構成設定はこれで終わりなのですが、Azure SQL Database(中身はSQL Server)と接続するにあたって、いくつかのライブラリやツールをインストールしておく必要があります。

詳しくは、こちらもご参考に。

https://docs.microsoft.com/ja-jp/sql/connect/python/pyodbc/step-1-configure-development-environment-for-pyodbc-python-development?view=sql-server-ver15

1つ目がODBCドライバです。こちらは以下からインストールできます。

https://docs.microsoft.com/ja-jp/sql/connect/odbc/download-odbc-driver-for-sql-server?redirectedfrom=MSDN&view=sql-server-ver15

続いて、pyodbcと、django-pyodbc-azureいうPythonパッケージのインストールが必要です。pyodbcパッケージは、pythonでodbcを経由してデータベースアクセスを可能にするために必要になります。また、django-pyodbc-azureは、Azure SQL Databaseへの接続を可能にするパッケージです。

pip install pyodbc django-pyodbc-azure

1点、django-pyodbc-azureは現在django 2.1との互換性しかサポートされていないようです。例えば、djangoの3.xをインストールした環境であっても、django-pyodbc-azureのインストールの段階でdjango 2.1がインストールし直されるため、バージョンの要件がある場合には注意が必要です。

(参考)pyodbc

https://github.com/mkleehammer/pyodbc

pyodbc is an open source Python module that makes accessing ODBC databases simple. It implements the DB API 2.0 specification but is packed with even more Pythonic convenience.

(参考)django-pyodbc-azure

https://pypi.org/project/django-pyodbc-azure/

django-pyodbc-azure is a modern fork of django-pyodbc, a Django Microsoft SQL Server external DB backend that uses ODBC by employing the pyodbc library. It supports Microsoft SQL Server and Azure SQL Database.

Features
Supports Django 2.1
Supports Microsoft SQL Server 2008/2008R2, 2012, 2014, 2016, 2017 and Azure SQL Database
Passes most of the tests of the Django test suite
Compatible with Micosoft ODBC Driver for SQL Server, SQL Server Native Client, and FreeTDS ODBC drivers

Dependencies
Django 2.1
pyodbc 3.0 or newer

# django-pyodbc-azureインストール時のdjangoの再インストール

Installing collected packages: Django, django-pyodbc-azure
  Attempting uninstall: Django
    Found existing installation: Django 3.2.4
    Uninstalling Django-3.2.4:
      Successfully uninstalled Django-3.2.4
Successfully installed Django-2.1.15 django-pyodbc-azure-2.1.0.0

上記インストールの上、再びプロジェクトのmigrateとrunserverを実施して動作確認を行います。

# ローカルでアプリ起動
python manage.py migrate
python manage.py runserver

ちなみに、上記パッケージが適切にがインストールされていない場合、migrateの際に以下のエラーが出ます。

ModuleNotFoundError: No module named 'sql_server'
The above exception was the direct cause of the following exception:

Azure App Serviceへのデプロイ・稼働確認

さて、続いてAzureへのデプロイです。ここでも、いくつか必要なステップがあります。

requirements.txtファイルの作成

プロジェクト直下で以下コマンドを実行し、requirements.txtファイルを作成しておきます。

pip freeze > requirements.txt

このファイルが必要な理由は、以下「トラブル:デプロイでCritical ErrorによるExceptionが発生する」を参照ください。

STATIC_ROOTの構成追加

こちらは、ローカル上でアプリを動かす場合には不要ですが、Azure(Azureに限らず、Herokuなどでも)に展開する上で必要になる設定です。settings.pyファイルのSTATIC_URL設定の下に以下設定を追加します。

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

この設定が必要な理由は、以下記事をご参照ください。

ALLOWED_HOSTの構成追加

こちらも、Webサーバ上でアプリを稼働させるために必要な設定のようです。

Settings.pyに以下を追加します。

ALLOWED_HOSTS = ['*']

この設定が必要な理由については、以下をご参照ください。

Azure Tools拡張機能のインストール

VSCodeからAzure App Serviceへのデプロイに際しては、VSCodeのAzure拡張機能をインストールしておくと便利です。

この拡張機能を使って、先ほど作成したAzure App Serviceへモジュールをデプロイします。リソースを右クリックして、Deploy to WebAppを選択すると、現在のプロジェクトのデプロイが開始します。

デプロイが正常に終了したことを確認して、App Serviceのリソースに接続すると・・・

よし!無事にデプロイに成功しました!

(参考)デプロイに関するトラブル集

トラブル:デプロイでCritical ErrorによるExceptionが発生する

問題:VSCodeからのデプロイの結果、Critical ErrorによるExceptionが発生する

14:48:34 WebAppLinuxPythonDjango013: Starting deployment...
14:48:36 WebAppLinuxPythonDjango013: Creating zip package...
14:48:37 WebAppLinuxPythonDjango013: Ignoring files from "appService.zipIgnorePattern"
"__pycache__{,/**}"
"*.py[cod]"
"*$py.class"
".Python{,/**}"
"build{,/**}"
"develop-eggs{,/**}"
"dist{,/**}"
"downloads{,/**}"
"eggs{,/**}"
".eggs{,/**}"
"lib{,/**}"
"lib64{,/**}"
"parts{,/**}"
"sdist{,/**}"
"var{,/**}"
"wheels{,/**}"
"share/python-wheels{,/**}"
"*.egg-info{,/**}"
".installed.cfg"
"*.egg"
"MANIFEST"
".env{,/**}"
".venv{,/**}"
"env{,/**}"
"venv{,/**}"
"ENV{,/**}"
"env.bak{,/**}"
"venv.bak{,/**}"
".vscode{,/**}"
14:48:37 WebAppLinuxPythonDjango013: Zip package size: 11.4 kB
14:48:39 WebAppLinuxPythonDjango013: Fetching changes.
14:48:41 WebAppLinuxPythonDjango013: Cleaning up temp folders from previous zip deployments and extracting pushed zip file /tmp/zipdeploy/62710589-79a5-47b5-bfce-935c32a46d8d.zip (0.01 MB) to /tmp/zipdeploy/extracted
14:48:49 WebAppLinuxPythonDjango013: Updating submodules.
14:48:51 WebAppLinuxPythonDjango013: Preparing deployment for commit id '4b3e0e38-b'.
14:48:51 WebAppLinuxPythonDjango013: Repository path is /tmp/zipdeploy/extracted
14:48:52 WebAppLinuxPythonDjango013: Running oryx build...
14:48:52 WebAppLinuxPythonDjango013: Command: oryx build /tmp/zipdeploy/extracted -o /home/site/wwwroot --platform python --platform-version 3.8 -i /tmp/8d933af1504b2f4 --compress-destination-dir -p virtualenv_name=antenv --log-file /tmp/build-debug.log 
14:48:59 WebAppLinuxPythonDjango013: Operation performed by Microsoft Oryx, https://github.com/Microsoft/Oryx
14:48:59 WebAppLinuxPythonDjango013: You can report issues at https://github.com/Microsoft/Oryx/issues
14:48:59 WebAppLinuxPythonDjango013: Oryx Version: 0.2.20210225.2, Commit: f7b557f29a83aa078cc94c0056e7337c07f14271, ReleaseTagName: 20210225.2
14:48:59 WebAppLinuxPythonDjango013: Build Operation ID: |jwRjJinTnOs=.5162869_
14:48:59 WebAppLinuxPythonDjango013: Repository Commit : 4b3e0e38-b4b1-4420-beaa-be1476ef7fad
14:48:59 WebAppLinuxPythonDjango013: Detecting platforms...
14:49:04 WebAppLinuxPythonDjango013: Detected following platforms:
14:49:04 WebAppLinuxPythonDjango013:   python: 3.8.6
14:49:05 WebAppLinuxPythonDjango013: Using intermediate directory '/tmp/8d933af1504b2f4'.
14:49:05 WebAppLinuxPythonDjango013: Copying files to the intermediate directory...
14:49:05 WebAppLinuxPythonDjango013: Done in 0 sec(s).
14:49:05 WebAppLinuxPythonDjango013: Source directory     : /tmp/8d933af1504b2f4
14:49:05 WebAppLinuxPythonDjango013: Destination directory: /home/site/wwwroot
14:49:05 WebAppLinuxPythonDjango013: Python Version: /opt/python/3.8.6/bin/python3.8
14:49:06 WebAppLinuxPythonDjango013: Python Virtual Environment: antenv
14:49:06 WebAppLinuxPythonDjango013: Creating virtual environment...
14:49:21 WebAppLinuxPythonDjango013: Activating virtual environment...
14:49:21 WebAppLinuxPythonDjango013: Could not find setup.py or requirements.txt; Not running pip install
14:49:21 WebAppLinuxPythonDjango013: Preparing output...
14:49:21 WebAppLinuxPythonDjango013: grep: /tmp/8d933af1504b2f4/requirements.txt: No such file or directory
14:49:22 WebAppLinuxPythonDjango013: Copying files to destination directory '/tmp/_preCompressedDestinationDir'...
14:49:22 WebAppLinuxPythonDjango013: Done in 1 sec(s).
14:49:22 WebAppLinuxPythonDjango013: Compressing content of directory '/tmp/_preCompressedDestinationDir'...
14:49:25 WebAppLinuxPythonDjango013: Copied the compressed output to '/home/site/wwwroot'
14:49:25 WebAppLinuxPythonDjango013: Removing existing manifest file
14:49:25 WebAppLinuxPythonDjango013: Creating a manifest file...
14:49:26 WebAppLinuxPythonDjango013: Manifest file created.
14:49:26 WebAppLinuxPythonDjango013: Done in 21 sec(s).
14:49:28 WebAppLinuxPythonDjango013: Running post deployment command(s)...
14:49:28 WebAppLinuxPythonDjango013: Triggering recycle (preview mode disabled).
14:49:29 WebAppLinuxPythonDjango013: Deployment successful.
14:49:34: Deployment to "WebAppLinuxPythonDjango013" completed.
14:49:51: "WebAppLinuxPythonDjango013" reported a critical error: Exception

VSCodeのリンクから、App Service上のアプリログを確認しに行くと、Module Not Foundエラーが出ていた。Djangoのライブラリがないと。このために、デプロイは完了したもののそのあとのアプリ起動に失敗していた状況でした。

2021-06-20T05:47:44.129875995Z Generating `gunicorn` command for 'django_webapp_sqldb.wsgi'
2021-06-20T05:47:45.385798997Z Writing output script to '/opt/startup/startup.sh'
2021-06-20T05:47:46.833800398Z Using packages from virtual environment antenv located at /tmp/8d933ae792394c4/antenv.
2021-06-20T05:47:46.834514939Z Updated PYTHONPATH to ':/tmp/8d933ae792394c4/antenv/lib/python3.8/site-packages'
2021-06-20T05:47:48.806907166Z [2021-06-20 05:47:48 +0000] [36] [INFO] Starting gunicorn 20.0.4
2021-06-20T05:47:48.808381450Z [2021-06-20 05:47:48 +0000] [36] [INFO] Listening at: http://0.0.0.0:8000 (36)
2021-06-20T05:47:48.808973584Z [2021-06-20 05:47:48 +0000] [36] [INFO] Using worker: sync
2021-06-20T05:47:48.840547585Z [2021-06-20 05:47:48 +0000] [38] [INFO] Booting worker with pid: 38
2021-06-20T05:47:48.855957864Z [2021-06-20 05:47:48 +0000] [38] [ERROR] Exception in worker process
2021-06-20T05:47:48.855973965Z Traceback (most recent call last):
2021-06-20T05:47:48.855979566Z   File "/opt/python/3.8.6/lib/python3.8/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
2021-06-20T05:47:48.855996166Z     worker.init_process()
2021-06-20T05:47:48.856000067Z   File "/opt/python/3.8.6/lib/python3.8/site-packages/gunicorn/workers/base.py", line 119, in init_process
2021-06-20T05:47:48.856003967Z     self.load_wsgi()
2021-06-20T05:47:48.856007367Z   File "/opt/python/3.8.6/lib/python3.8/site-packages/gunicorn/workers/base.py", line 144, in load_wsgi
2021-06-20T05:47:48.856011167Z     self.wsgi = self.app.wsgi()
2021-06-20T05:47:48.856014668Z   File "/opt/python/3.8.6/lib/python3.8/site-packages/gunicorn/app/base.py", line 67, in wsgi
2021-06-20T05:47:48.856018568Z     self.callable = self.load()
2021-06-20T05:47:48.856021968Z   File "/opt/python/3.8.6/lib/python3.8/site-packages/gunicorn/app/wsgiapp.py", line 49, in load
2021-06-20T05:47:48.856025768Z     return self.load_wsgiapp()
2021-06-20T05:47:48.856029268Z   File "/opt/python/3.8.6/lib/python3.8/site-packages/gunicorn/app/wsgiapp.py", line 39, in load_wsgiapp
2021-06-20T05:47:48.856033069Z     return util.import_app(self.app_uri)
2021-06-20T05:47:48.856036669Z   File "/opt/python/3.8.6/lib/python3.8/site-packages/gunicorn/util.py", line 358, in import_app
2021-06-20T05:47:48.856040369Z     mod = importlib.import_module(module)
2021-06-20T05:47:48.856043969Z   File "/opt/python/3.8.6/lib/python3.8/importlib/__init__.py", line 127, in import_module
2021-06-20T05:47:48.856047769Z     return _bootstrap._gcd_import(name[level:], package, level)
2021-06-20T05:47:48.856051370Z   File "", line 1014, in _gcd_import
2021-06-20T05:47:48.856055470Z   File "", line 991, in _find_and_load
2021-06-20T05:47:48.856059170Z   File "", line 975, in _find_and_load_unlocked
2021-06-20T05:47:48.856062970Z   File "", line 671, in _load_unlocked
2021-06-20T05:47:48.856066670Z   File "", line 783, in exec_module
2021-06-20T05:47:48.856070471Z   File "", line 219, in _call_with_frames_removed
2021-06-20T05:47:48.856074271Z   File "/tmp/8d933ae792394c4/django_webapp_sqldb/wsgi.py", line 12, in 
2021-06-20T05:47:48.856078071Z     from django.core.wsgi import get_wsgi_application
2021-06-20T05:47:48.856081671Z ModuleNotFoundError: No module named 'django'
2021-06-20T05:47:48.856085172Z [2021-06-20 05:47:48 +0000] [38] [INFO] Worker exiting (pid: 38)
2021-06-20T05:47:48.952727585Z [2021-06-20 05:47:48 +0000] [36] [INFO] Shutting down: Master
2021-06-20T05:47:48.953582734Z [2021-06-20 05:47:48 +0000] [36] [INFO] Reason: Worker failed to boot.

これは、アプリ展開時にインストールすべきパッケージを指定したrequirements.txtファイルを用意しておかなかったからですね。

(参考)requirements.txtについて

https://qiita.com/sakusaku12/items/21083c73c8afa4f6c78d

プロジェクト上で以下コマンドを実行してrequirements.txtファイルを作成した上で、再デプロイすればこのエラーは回避できます!

pip freeze > requirements.txt

今度は、デプロイ中に、requirements.txtで指定されているモジュールがインストールされていることが確認できました!

# デプロイログ
・・・
(略)
・・・
15:05:57 WebAppLinuxPythonDjango013: Activating virtual environment...
15:05:57 WebAppLinuxPythonDjango013: Running pip install...
15:06:04 WebAppLinuxPythonDjango013: [06:06:04+0000] Collecting asgiref==3.3.4
15:06:05 WebAppLinuxPythonDjango013: [06:06:05+0000]   Downloading asgiref-3.3.4-py3-none-any.whl (22 kB)
15:06:06 WebAppLinuxPythonDjango013: [06:06:06+0000] Collecting Django==2.1.15
15:06:06 WebAppLinuxPythonDjango013: [06:06:06+0000]   Downloading Django-2.1.15-py3-none-any.whl (7.3 MB)
15:06:08 WebAppLinuxPythonDjango013: [06:06:08+0000] Collecting django-pyodbc-azure==2.1.0.0
15:06:08 WebAppLinuxPythonDjango013: [06:06:08+0000]   Downloading django_pyodbc_azure-2.1.0.0-py3-none-any.whl (39 kB)
15:06:09 WebAppLinuxPythonDjango013: [06:06:09+0000] Collecting pyodbc==4.0.30
15:06:09 WebAppLinuxPythonDjango013: [06:06:09+0000]   Downloading pyodbc-4.0.30.tar.gz (266 kB)
15:06:14 WebAppLinuxPythonDjango013: [06:06:14+0000] Collecting pytz==2021.1
15:06:14 WebAppLinuxPythonDjango013: [06:06:14+0000]   Downloading pytz-2021.1-py2.py3-none-any.whl (510 kB)
15:06:14 WebAppLinuxPythonDjango013: [06:06:14+0000] Collecting sqlparse==0.4.1
15:06:14 WebAppLinuxPythonDjango013: [06:06:14+0000]   Downloading sqlparse-0.4.1-py3-none-any.whl (42 kB)
15:06:15 WebAppLinuxPythonDjango013: [06:06:15+0000] Building wheels for collected packages: pyodbc
15:06:15 WebAppLinuxPythonDjango013: [06:06:15+0000]   Building wheel for pyodbc (setup.py): started
15:06:45 WebAppLinuxPythonDjango013: [06:06:45+0000]   Building wheel for pyodbc (setup.py): finished with status 'done'
15:06:45 WebAppLinuxPythonDjango013: [06:06:45+0000]   Created wheel for pyodbc: filename=pyodbc-4.0.30-cp38-cp38-linux_x86_64.whl size=277024 sha256=6abb9d021d8130360a71851bb45bf4dd251f3c49d919db28ce8533ce14408f7d
15:06:45 WebAppLinuxPythonDjango013: [06:06:45+0000]   Stored in directory: /usr/local/share/pip-cache/wheels/0e/6c/b9/4349f7ee206f997dcde5675372aed185a788729278ad749953
15:06:45 WebAppLinuxPythonDjango013: [06:06:45+0000] Successfully built pyodbc
15:06:46 WebAppLinuxPythonDjango013: [06:06:46+0000] Installing collected packages: asgiref, pytz, Django, pyodbc, django-pyodbc-azure, sqlparse
・・・
(略)

前編の終わりに

さて、ここまでのステップで、Azure SQL Databaseとの接続を構成したDjangoアプリのデプロイが成功しました・・・!(いろいろエラーにぶちあたって一苦労・・)

後編ではいよいよ、このアプリをベースに、実際にSQL DBと連携したアプリのサンプルを作ってみようと思います。

前編に長い間お付き合いいただきありがとうございました!後編もお楽しみに!

最後に、この記事が少しでも役に立った!という方は、ぜひ下のイイネボタンを押していただけると励みになります!また、記事へのフィードバックがありましたら、コメントもお待ちしております!

この記事を気に入っていただけたらシェアをお願いします!
ABOUT US
Yuu113
初めまして。ゆうたろうと申します。 兵庫県出身、東京でシステムエンジニアをしております。現在は主にデータ分析、機械学習を活用してビジネスモデリングに取り組んでいます。 日々学んだことや経験したことを整理していきたいと思い、ブログを始めました。旅行、カメラ、IT技術、江戸文化が大好きですので、これらについても記事にしていきたいと思っています。