Pythonの場合、pyodbcライブラリ等を利用すれば、SQLデータベースに接続することができる。

今回は、Dockerコンテナを利用した、FlaskによるAzure App Service上で動作するアプリケーションに、SQLデータベースに接続する処理を追加してみたので、その手順を共有する。

前提条件

下記記事の実装が完了していること。

Azure App Serviceで動作するDockerコンテナを利用したPython Flaskアプリケーションを作成してみたPythonのライブラリ「Flask」を利用したWebアプリケーションは、Azure App Service上で動作させることができる。...

やってみたこと

  1. ソースコードの修正
  2. ローカル環境でのAzure App Service動作検証
  3. Azure上でのAzure App Service動作検証

ソースコードの修正

作成したソースコードの構成は、以下の通り。
ソースコードの構成
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

Dockerfile、requirements.txtについては、以下のように、GitHub Copilot CLIを用いて修正している。

1-1) 修正対象となるファイルを配置したフォルダ上で、GitHub Copilot CLIを起動する。
ソースコードの修正_1_1

1-2) pyodbcを利用できるような設定を追加するよう指示を出し、「Enter」キーを押下する。
ソースコードの修正_1_2

1-3) Azure SQL Databaseに接続可能なドライバの設定を追加するよう指示を出し、「Enter」キーを押下する。
ソースコードの修正_1_3

GitHub Copilotで修正後のDockerfile、requirements.txtの内容は、以下の通り。

FROM python:3.14.3-slim-bookworm

# Pythonライブラリを読み込む
WORKDIR /code
COPY requirements.txt .
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        ca-certificates \
        build-essential \
        curl \
        gnupg \
        unixodbc \
        unixodbc-dev \
    && curl -fsSL https://packages.microsoft.com/keys/microsoft.asc \
        | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg \
    && echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" \
        > /etc/apt/sources.list.d/microsoft-prod.list \
    && apt-get update \
    && ACCEPT_EULA=Y apt-get install -y --no-install-recommends \
        msodbcsql18 \
    && rm -rf /var/lib/apt/lists/* \
    && pip3 install --no-cache-dir -r requirements.txt
COPY . .

# ログをバッファリングせず即時出力する設定を追加
ENV PYTHONUNBUFFERED=1

# Flaskアプリケーションを起動
EXPOSE 50505
ENTRYPOINT ["gunicorn", "app:app"]
Flask==3.1.0
gunicorn
pyodbc==5.3.0

なお、GitHub Copilot CLIの導入や使い方については、以下のサイトを参照のこと。

GitHub Copilot CLIを使ってSQL実行時リスクを評価してみたGitHub Copilotは、OpenAIの技術を活用したAIコード補完・支援ツールで、コマンドラインベース(CLI、Command ...

また、app.pyについては、pyodbcライブラリ等を利用すしてSQLデータベースに接続する処理を追加した上で、以下のように、GitHub Copilot CLIを用いて修正した。

2-1) GitHub Copilot CLIで、修正対象となるファイルを配置したフォルダ上で、app.pyに例外処理を追加するよう指示を出し、「Enter」キーを押下する。
ソースコードの修正_2_1

2-2) app.pyにpydoc(Docstring)コメントを追加するよう指示を出し、「Enter」キーを押下する。
ソースコードの修正_2_2

2-3) app.pyにpydoc(Docstring)コメントが英語だったため、日本語に修正するよう指示を出し、「Enter」キーを押下する。
ソースコードの修正_2_3

2-4) 修正できたため、お礼を伝えて終了する。
ソースコードの修正_2_4

GitHub Copilotで修正後のapp.pyの内容は、以下の通り。

"""データベースからユーザー名を取得する Flask アプリケーション。"""

import os

import pyodbc
from flask import (Flask, redirect, render_template, request,
                   send_from_directory, url_for)
from jinja2 import TemplateNotFound
from werkzeug.exceptions import InternalServerError, NotFound

app = Flask(__name__)


def get_db_connection_string():
    """必須の環境変数からデータベース接続文字列を組み立てる。"""
    settings = {
        'ODBC_DRIVER': os.getenv('ODBC_DRIVER'),
        'DB_SERVER': os.getenv('DB_SERVER'),
        'DB_PORT': os.getenv('DB_PORT'),
        'DB_NAME': os.getenv('DB_NAME'),
        'DB_USER': os.getenv('DB_USER'),
        'DB_PASS': os.getenv('DB_PASS'),
    }
    missing_settings = [name for name, value in settings.items() if not value]

    if missing_settings:
        raise ValueError(
            'Missing required database configuration: '
            + ', '.join(missing_settings)
        )

    return (
        f"DRIVER={settings['ODBC_DRIVER']};"
        f"SERVER={settings['DB_SERVER']};"
        f"PORT={settings['DB_PORT']};"
        f"DATABASE={settings['DB_NAME']};"
        f"UID={settings['DB_USER']};"
        f"PWD={settings['DB_PASS']};"
    )


def get_user_name(user_id):
    """指定された ID のユーザー名を返し、未登録なら空文字を返す。"""
    with pyodbc.connect(get_db_connection_string()) as conn:
        with conn.cursor() as cur:
            row = cur.execute(
                'SELECT name FROM user_data WHERE id = ?',
                (user_id,),
            ).fetchone()

    return row.name if row else ''


@app.route('/')
def index():
    """トップページを表示する。"""
    print('Request for index page received')

    try:
        return render_template('index.html')
    except TemplateNotFound as exc:
        app.logger.exception('Index template is missing')
        raise InternalServerError(
            description='The index page is not available right now.'
        ) from exc

@app.route('/favicon.ico')
def favicon():
    """static ディレクトリから favicon を返す。"""
    try:
        return send_from_directory(
            os.path.join(app.root_path, 'static'),
            'favicon.ico',
            mimetype='image/vnd.microsoft.icon',
        )
    except NotFound as exc:
        app.logger.warning('Favicon file is missing')
        raise NotFound(description='favicon.ico was not found.') from exc

@app.route('/hello', methods=['POST'])
def hello():
    """送信されたユーザー ID を検索し、挨拶ページを表示する。"""
    user_id = request.form.get('user_id')

    if not user_id:
        print('Request for hello page received with no id or blank id -- redirecting')
        return redirect(url_for('index'))

    try:
        user_name = get_user_name(user_id)
    except ValueError as exc:
        app.logger.error(str(exc))
        raise InternalServerError(
            description='Database configuration is incomplete.'
        ) from exc
    except pyodbc.Error as exc:
        app.logger.exception('Database query failed for user_id=%s', user_id)
        raise InternalServerError(
            description='Unable to retrieve user information right now.'
        ) from exc

    if not user_name:
        print('Request for hello page received with blank name -- redirecting')
        return redirect(url_for('index'))

    print('Request for hello page received with name=%s' % user_name)

    try:
        return render_template('hello.html', name=user_name)
    except TemplateNotFound as exc:
        app.logger.exception('Hello template is missing')
        raise InternalServerError(
            description='The hello page is not available right now.'
        ) from exc


@app.errorhandler(404)
def not_found(error):
    """404 エラーに対してプレーンテキストの応答を返す。"""
    return getattr(error, 'description', 'Not found.'), 404


@app.errorhandler(500)
def internal_server_error(error):
    """500 エラーに対してプレーンテキストの応答を返す。"""
    return getattr(error, 'description', 'Internal server error.'), 500


if __name__ == '__main__':
    app.run()

また、上記app.pyで定義している環境変数は、以下の.envで定義している。

ODBC_DRIVER={ODBC Driver 18 for SQL Server}
DB_SERVER=azure-db-purinit.database.windows.net
DB_PORT=1433
DB_NAME=azureSqlDatabase
DB_USER=purinit@azure-db-purinit
DB_PASS=(DBのパスワード)

その他、index.htmlは、名前でなくuser_idを入力するよう修正している。

<!doctype html>
<head>
    <title>Hello Azure - Python Quickstart</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}">
    <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>
<html>
   <body>
     <main>
        <div class="px-4 py-3 my-2 text-center">
            <img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='images/azure-icon.svg') }}" alt="Azure Logo" width="192" height="192"/>
            <h1 class="display-6 fw-bold text-primary">Welcome to Azure</h1>            
          </div>
        <form method="post" action="{{url_for('hello')}}">
            <div class="col-md-6 mx-auto text-center">
                <label for="name" class="form-label fw-bold fs-5">Could you please tell me your user_id?</label>

                <div class="d-grid gap-2 d-sm-flex justify-content-sm-center align-items-center my-1">
                    <input type="text" class="form-control" id="user_id" name="user_id" style="max-width: 256px;">
                  </div>            
                <div class="d-grid gap-2 d-sm-flex justify-content-sm-center my-2">
                  <button type="submit" class="btn btn-primary btn-lg px-4 gap-3">Say Hello</button>
                </div>            
            </div>
        </form>
     </main>      
   </body>
</html>
エンジニアファーストバナー

ローカル環境でのAzure App Service動作検証

ローカル環境でのAzure App Service動作検証は、Docker Desktopを起動して行える。その手順は、以下の通り。

1) Docker Desktopを起動し、Dockerfileが存在するディレクトリ上で、「docker build –tag (Dockerイメージ名) .」コマンドを実行する。

<ファイル構成>
ローカル環境でのAzure App Service動作検証_1_1

「docker build –tag (Dockerイメージ名) .」コマンドを実行する。
ローカル環境でのAzure App Service動作検証_1_2

2) 先ほど作成したDockerfileが存在するディレクトリ上で、「docker run –detach –publish 5000:50505 –env-file (envファイル名) (Dockerイメージ名)」コマンドを実行する。
ローカル環境でのAzure App Service動作検証_2

なお、「–env-file」オプションで、環境変数「.env」の内容を、Dockerコンテナに読み込ませている。

3) ブラウザを起動し、「http://localhost:5000」にアクセスすると、以下のように、Flaskアプリケーションが動作することが確認できる。
ローカル環境でのAzure App Service動作検証_3_1

「1」を入力し、「Say Hello」ボタンを押下する。
ローカル環境でのAzure App Service動作検証_3_2

次画面に遷移し、「Hello (入力したidに対応するuser_data.name)」が表示されることが確認できる。
ローカル環境でのAzure App Service動作検証_3_3

なお、SQLデータベースの参照元テーブルの内容は以下の通りで、id=1に対応するnameの値が、上記画面に表示されていることが確認できる。
ローカル環境でのAzure App Service動作検証_3_4

Azure上でのAzure App Service動作検証

Azure上でのAzure App Service動作検証は、Azure App Serviceの環境変数を設定後、Azure Container RegistryにDockerイメージを配置して行える。その手順は、以下の通り。

1) Azure Portal上で、Azure App Serviceを開く。
Azure上でのAzure App Service動作検証_1_1

確認するApp Service名のリンクを押下する。
Azure上でのAzure App Service動作検証_1_2

2) Azure App Serviceの「環境変数」メニューを開き、アプリ設定の「追加」ボタンを押下する。
Azure上でのAzure App Service動作検証_2

3)「.env」に設定していた環境変数「ODBC_DRIVER」を以下のように設定し、「適用」ボタンを押下する。
Azure上でのAzure App Service動作検証_3

4) 同様に他の環境変数も追加し、「適用」ボタンを押下する。
Azure上でのAzure App Service動作検証_4

5) 確認ダイアログが表示されるため、「確認」ボタンを押下する。
Azure上でのAzure App Service動作検証_5

6) 更新が完了すると、以下のように、完了メッセージが表示される。
Azure上でのAzure App Service動作検証_6

7) Azure Container Registry上にDockerイメージを配置するため、「az login」コマンドを実行し、Azure環境にログインする。
Azure上でのAzure App Service動作検証_7

8)「az acr build –resource-group (リソースグループ名) –registry (コンテナレジストリ名) –image (Dockerイメージ名):(バージョン) .」コマンドを実行して、Azure Container Registry上にDockerイメージを配置する。
Azure上でのAzure App Service動作検証_8_1

Azure上でのAzure App Service動作検証_8_2

9) Azureからログアウトするため、「az logout」コマンドを実行する。
Azure上でのAzure App Service動作検証_9

10) 変更内容を反映させるため、App Serviceを再起動する。
Azure上でのAzure App Service動作検証_10

11) 確認ダイアログが表示されるため、「はい」ボタンを押下する。
Azure上でのAzure App Service動作検証_11

12) 再起動が完了すると、以下のように、完了メッセージが表示される。
Azure上でのAzure App Service動作検証_12

13) ブラウザを起動し、「https://(AppService名).azurewebsites.net」にアクセスすると、以下のように、Flaskアプリケーションが動作することが確認できる。
Azure上でのAzure App Service動作検証_13_1

「1」を入力し、「Say Hello」ボタンを押下する。
Azure上でのAzure App Service動作検証_13_2

次画面に遷移し、「Hello (入力したidに対応するuser_data.name)」が表示されることが確認できる。
Azure上でのAzure App Service動作検証_13_3

なお、SQLデータベースの参照元テーブルの内容は以下の通りで、id=1に対応するnameの値が、上記画面に表示されていることが確認できる。
Azure上でのAzure App Service動作検証_13_4

その他、マネージドIDを利用することで、DB接続時のパスワードを設定しないで済む。その方法については、以下のサイトを参照のこと。

Azure Functions上で動作するPythonアプリケーションでマネージドIDを利用してSQL Databaseに接続してみたAzure Functions上で動作するPythonアプリケーションにおいて、マネージドIDを利用すると、データベースに接続するための...

要点まとめ

  • Dockerコンテナを利用した、FlaskによるAzure App Service上で動作するアプリケーションの場合、Dockerfileに設定を追加することで、pyodbcライブラリを利用してSQLデータベースに接続することができる。