AzureにPython3.6/Django/PostgresのWebアプリをデプロイする

Microsoft AzureのApp ServiceにPostgreSQLを使ったDjangoアプリをデプロイしようしたのですが、けっこう苦労したのでわかったことを書いておきます。

# 基礎知識

- Microsoft Azure
    - Microsoftが提供するクラウドプラットフォーム(PaaS/IaaS)
- App Service
    - AzureでWebアプリを提供するためのプラットフォーム (PaaS) 

# わかったこと

ドキュメントと色々試行錯誤してわかったことです。

- App ServiceのOSはデフォルトではWindows
- OSはLinuxも選択できるけど現在はプレビュー版という扱いで制限がある
- LinuxではDockerでのデプロイが可能
- デフォルトのデプロイメントスクリプトではDjangoの静的ファイル配置に対応している
- 選択可能なPythonバージョンは2.7または3.4のみ
- WindowsのPython3.4ではpipでインストールできないモジュールがある(psycopg2など)
- 拡張機能を使えば他のPythonバージョンをインストールできる
- この場合デプロイコマンド(pipを実行したりするコマンド)は自分で作る必要がある

OSがWindowsなのはMicrosoftなので当たり前なのですが、運用環境はLinuxというのが常識の世界で育った人だと見落としがちです。

# 公式ドキュメント

[Azure Web Apps のドキュメント](https://docs.microsoft.com/ja-jp/azure/app-service-web/)

メニューからPythonに関するページを探すと以下４つがありました。

まずは1をやってみてから4を見るのがいいと思います。
2はチュートリアルとなってますが、プレビュー版であるLinuxにDockerイメージでデプロイするという特殊な使い方なので注意が必要です。
3はVisualStudioを使わないなら見なくてもいいです。

### 1. クイック スタート > Python アプリの作成
**[Azure に Python Web アプリを作成する](https://docs.microsoft.com/ja-jp/azure/app-service-web/app-service-web-get-started-python)**

- Flaskを使ったシンプルなアプリをデプロイする手順が書かれています。
- 必要なファイルは全て[Gitのサンプルコード](https://github.com/Azure-Samples/python-docs-hello-world)に含まれています。
- Azure上の操作はAzure CLI を使って行います。
- デプロイはローカルGitという方法で、git push するとデプロイが実行されます。

### 2. Tutorials (チュートリアル) > 1 - Python と PostgreSQL
**[Azure で Docker Python と PostgreSQL アプリを構築する](https://docs.microsoft.com/ja-jp/azure/app-service-web/app-service-web-tutorial-docker-python-postgresql-app)**

- FlaskでPostgreSQLを使ったアプリの例です。
- Dockerイメージでデプロイする方法が書かれています。
- [サンプルコード](https://github.com/Azure-Samples/docker-flask-postgres.git)

### 3. ハウツー ガイド > アプリの開発 > Python > MySQL を使用した Django アプリ
**[Python Tools 2.2 for Visual Studio を使用した Azure 上の Django と MySQL](https://docs.microsoft.com/ja-jp/azure/app-service-web/web-sites-python-ptvs-django-mysql)**

- Python Tools 2.2 for Visual Studio(PTVS)を使った開発方法が書かれています。

### 4. ハウツー ガイド > アプリの開発 > ランタイムの構成 > Python
**[Azure App Service Web Apps による Python の構成](https://docs.microsoft.com/ja-jp/azure/app-service-web/web-sites-python-configure)**

- デプロイに必要な以下のファイルの説明が書かれています。
    - requirements.txt
    - runtime.txt
    - web.config
    - ptvs_virtualenv_proxy.py
    - .deployment
    - deploy.cmd

# Python3.6/Django/PostgreSQLのWebアプリをデプロイする

公式ドキュメント＋αの情報で次のような手順でデプロイすることができました。

1. ローカルでDjangoアプリを作成する
- AzureにPostgreSQLデータベースを作成する
- Azureに App Service を作成する
- App Service に拡張機能でPython3.6をインストールする
- デプロイ用ファイルを作成する
- Djangoアプリをデプロイする

※Azure では PostgreSQL の使用は有償になります。

## 1. ローカルでDjangoアプリを作成する

ふつうにDjangoアプリを作ります。
デプロイする際には以下の設定が必要です。

```:settings.py
DEBUG = False
ALLOW_HOST = ['*']
STATIC_ROOT = 'static'
```

## 2. AzureにPostgreSQLデータベースを作成する

[Azure で Docker Python と PostgreSQL アプリを構築する](https://docs.microsoft.com/ja-jp/azure/app-service-web/app-service-web-tutorial-docker-python-postgresql-app) の「運用 PostgreSQL データベースを作成する」を参考に AzureにPostgreSQLデータベースを作成します。

```
AZ_GROUP="<resource_group>"
AZ_LOCATION="japanwest"
AZ_PG="<postgresql_name>"
AZ_PG_USER="<admin_username>"
AZ_PG_PASS="<admin_password>"

az postgres server create -g $AZ_GROUP -n $AZ_PG -l $AZ_LOCATION -u $AZ_PG_USER -p $AZ_PG_PASS
az postgres server firewall-rule create -g $AZ_GROUP --server-name $AZ_PG --start-ip-address=0.0.0.0 --end-ip-address=255.255.255.255 --name AllowAllIPs
```

データベースとユーザーを作成します。

```
$ psql -h ${AZ_PG}.postgres.database.azure.com -U ${AZ_PG_USER}@${AZ_PG} postgres
```
```postgres:
CREATE DATABASE <database_name>;
CREATE USER <user> WITH PASSWORD '<password>';
GRANT ALL PRIVILEGES ON DATABASE <database_name> TO <user>;
```

## 3. Azureに App Service を作成する

[Azure に Python Web アプリを作成する](https://docs.microsoft.com/ja-jp/azure/app-service-web/app-service-web-get-started-python) を参考に以下のコマンドを実行します。

```
AZ_GROUP="<resource_group>"
AZ_LOCATION="japanwest"
AZ_APPSERVICE_PLAN="<plan_name>"
AZ_APPSERVICE_PLAN_TYPE="Free"
AZ_APP_NAME="<app_name>"

az group create -n $AZ_GROUP -l $AZ_LOCATION
az appservice plan create -n $AZ_APPSERVICE_PLAN -g $AZ_GROUP --sku $AZ_APPSERVICE_PLAN_TYPE
az webapp create -n $AZ_APP_NAME -g $AZ_GROUP --plan $AZ_APPSERVICE_PLAN --deployment-local-git
```

# 4. App Service に拡張機能でPython3.6をインストールする

[Upgrading Python on Azure App Service – Python Engineering at Microsoft](https://blogs.msdn.microsoft.com/pythonengineering/2016/08/04/upgrading-python-on-azure-app-service/)

Azure Portal の画面から作成した App Service を選択 > 拡張機能を選択 > インストールしたいPythonバージョンを選択してインストールします。

## 5. デプロイ用ファイルを作成する

以下のファイルを作成します。

- requirements.txt
- .deployment
- deploy.cmd
- web.config

```:requirements.txt
Django
psycopg2
```
```:.deployment
[config]
command = deploy.cmd
```

`deploy.cmd`はデフォルトのものをコピーしてpython.exeやpipのパスをバージョンに合わせて変更します。

```:deploy.cmd
@if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off

:: ----------------------
:: KUDU Deployment Script
:: Version: 1.0.15
:: ----------------------

SET PYTHON_HOME=D:\home\python361x86
SET PYTHON=%PYTHON_HOME%\python.exe
SET PIP=%PYTHON% -m pip

:: Prerequisites
:: -------------

:: Verify node.js installed
where node 2>nul >nul
IF %ERRORLEVEL% NEQ 0 (
  echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment.
  goto error
)

:: Setup
:: -----

setlocal enabledelayedexpansion

SET ARTIFACTS=%~dp0%..\artifacts

IF NOT DEFINED DEPLOYMENT_SOURCE (
  SET DEPLOYMENT_SOURCE=%~dp0%.
)

IF NOT DEFINED DEPLOYMENT_TARGET (
  SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot
)

IF NOT DEFINED NEXT_MANIFEST_PATH (
  SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest

  IF NOT DEFINED PREVIOUS_MANIFEST_PATH (
    SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest
  )
)

IF NOT DEFINED KUDU_SYNC_CMD (
  :: Install kudu sync
  echo Installing Kudu Sync
  call npm install kudusync -g --silent
  IF !ERRORLEVEL! NEQ 0 goto error

  :: Locally just running "kuduSync" would also work
  SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd
)
goto Deployment

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: Deployment
:: ----------

:Deployment
echo Handling python deployment.

:: 1. KuduSync
IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" (
  call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd"
  IF !ERRORLEVEL! NEQ 0 goto error
)

IF NOT EXIST "%DEPLOYMENT_TARGET%\requirements.txt" goto postPython

pushd "%DEPLOYMENT_TARGET%"

:: 4. Install packages
echo Pip install requirements.

%PIP% install -r requirements.txt
IF !ERRORLEVEL! NEQ 0 goto error

REM Add additional package installation here
REM -- Example --
REM env\scripts\easy_install pytz
REM IF !ERRORLEVEL! NEQ 0 goto error

:: 5. Copy web.config
IF EXIST "%DEPLOYMENT_SOURCE%\web.%PYTHON_VER%.config" (
  echo Overwriting web.config with web.%PYTHON_VER%.config
  copy /y "%DEPLOYMENT_SOURCE%\web.%PYTHON_VER%.config" "%DEPLOYMENT_TARGET%\web.config"
)

:: 6. Django collectstatic
IF EXIST "%DEPLOYMENT_TARGET%\manage.py" (
  echo Collecting Django static files. You can skip Django specific steps with a .skipDjango file.
  IF NOT EXIST "%DEPLOYMENT_TARGET%\static" (
    MKDIR "%DEPLOYMENT_TARGET%\static"
  )
  %PYTHON% manage.py collectstatic --noinput --clear
)

popd

:postPython

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto end

:: Execute command routine that will echo out when error
:ExecuteCmd
setlocal
set _CMD_=%*
call %_CMD_%
if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_%
exit /b %ERRORLEVEL%

:error
endlocal
echo An error has occurred during web site deployment.
call :exitSetErrorLevel
call :exitFromFunction 2>nul

:exitSetErrorLevel
exit /b 1

:exitFromFunction
()

:end
endlocal
echo Finished successfully.
```

```:web.config
<?xml version="1.0"?>
<configuration>
  <appSettings>
    <add key="PYTHONPATH" value="D:\home\site\wwwroot" />
    <add key="WSGI_HANDLER" value="django.core.wsgi.get_wsgi_application()" />
    <add key="WSGI_LOG" value="D:\home\LogFiles\wfastcgi.log" />
    <add key="DJANGO_SETTINGS_MODULE" value="project.settings" />
  </appSettings>
  <system.webServer>
    <handlers>
      <remove name="Python27_via_FastCGI" />
      <remove name="Python34_via_FastCGI" />
      <add name="Python FastCGI"
           path="handler.fcgi"
           verb="*"
           modules="FastCgiModule"
           scriptProcessor="D:\home\python361x86\python.exe|D:\home\python361x86\wfastcgi.py"
           resourceType="Unspecified"
           requireAccess="Script" />
    </handlers>
    <rewrite>
      <rules>
        <rule name="Static Files" stopProcessing="true">
          <conditions>
            <add input="true" pattern="false" />
          </conditions>
        </rule>
        <rule name="Configure Python" stopProcessing="true">
          <match url="(.*)" ignoreCase="false" />
          <conditions>
            <add input="{REQUEST_URI}" pattern="^/static/.*" ignoreCase="true" negate="true" />
          </conditions>
          <action type="Rewrite" url="handler.fcgi/{R:1}" appendQueryString="true" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>
```

`DJANGO_SETTINGS_MODULE`の部分はDjangoプロジェクトに合わせて変更してください。

## 6. Djangoアプリをデプロイする

```
AZ_USER="<user>"
AZ_PASS="<password>"
az webapp deployment user set --user-name $AZ_USER --password $AZ_PASS
git remote add azure <git_url>
git push azure master
```
gitのURLは`az webapp create`したときに表示されます。
ポータルの App Service の概要でも確認できます。

ブラウザで`<app_name>.azurewebsites.net/admin`にアクセスしてadminのログイン画面が表示されれば完了です。

# 感想

公式ドキュメントの構成のせいでかなり振り回されました。
チュートリアルの部分が通常と全く違うやり方を説明しているので混乱しました。。。