たぶん動く...

多分GIS系の人。あくまで個人的見解であり、所属団体を代表するものではありません。

データベースサービスを停止し忘れていたら止めるAzure Functions

以前、AWSのRDSを起動していて停止し忘れていたらスケジュールで止めるLambdaを作成しました。

RDS停止を忘れていたら止めるLambda - たぶん動く...

Azureを最近使用しており、Azure Database for MySQLを使用後、停止するのを忘れて不用意に課金されてしまう事態が度々発生しました。
AWSだとLambdaをスケジュールでRDSを起動、停止するPythonスクリプトがネットで探すとよく出て来ますが、Azure Functionsで同じような試みをしている記事が出てこないので作ってみました。
今回は、StopDBというAzure Functionsを作成し、その中でStopMySQLという関数を作成します。Azure Functionsを毎日22時に起動する設定とします。

サクッと作りたい人へ

  1. StopDBというpythonの関数アプリを作成します。システム割り当てマネージドIDの割り当てを許可します。
  2. cloud shell上へGitHubからリポジトリをcloneしてきます。サブスクリプションIDとリソースグループ名をfunction_app.pyに入力、起動時刻を修正します。
  3. 目的の関数プロジェクトのディレクトリへ移動し、デプロイします。
$ git clone https://github.com/TTY6335/StopAzureDB.git
$ cd StopMySQL/

(function_app.pyを編集)
$ az functionapp create --resource-group  <RESOURCE_GROUP> ---consumption-plan-location <REGION> --runtime python --runtime-version 3.9 --functions-version 4 --name StopDB --os-type linux --storage-account <STORAGE_NAME>
$ func azure functionapp publish StopDB

4.RBACディレクトリにあるjsonファイルを参考にしてロールを作成し、データベースへ付与します。

諸注意

作成、運用上での注意事項を先に述べておきます。
2023年11月現在、Azure Functionsで用意されているエディターで関数を作成する場合、必要なpythonモジュールを追加することができません。ここで紹介するコードをAzure コピペして動かそうとした場合、そのAzure Functionsが壊れます。 必ずローカル環境のコマンドラインVSCodeで作成してください。
実行ステータスを出力するように作っていません。メトリックから「実行した」ことは確認できますが、「データベースサーバが停止したか」どうかは出力されません。

ローカル環境を整備する

前提条件としてazure-cliまたはAzure Power Shelが開発PCにインストールされているものとします。 pythonの仮想環境はお好みで設定してください。 ローカルで開発、テストするにあたり、以下のコマンドで必要なAzure SDK for Pythonモジュールをインストールします。

pip install azure-identity
pip install azure-mgmt-rdbms

ローカルで関数プロジェクト作成

ローカルでAzure Functionで動かす関数プロジェクトファイルを作成します。今回はMySQLのDBサーバを止める関数なのでStopMySQLとしました。

$ func init StopMySQL --python -m V2
Found Python version 3.10.12 (python3).
The new Python programming model is generally available. Learn more at https://aka.ms/pythonprogrammingmodel
Writing requirements.txt
Writing function_app.py
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /media/Data/StopMySQL/.vscode/extensions.json

$ cd StopMySQL/ ; ls
function_app.py  host.json  local.settings.json  requirements.txt

Found Python version 〜 とありますが、現在実行しているPythonのバージョンになります。このバージョンはAzure上でAzure Functionを作成する際に重要になってきます。

ローカル関数コーディング

いよいよMySQLサーバを止めるスクリプトを書いていきます。

function_app.py

import azure.functions as func
import datetime
import json
import logging

from azure.identity import DefaultAzureCredential
# Module for Mysql - Single Server
from azure.mgmt.rdbms import mysql
# Module for MySQL - Flexible Server
from azure.mgmt.rdbms import mysql_flexibleservers 

# Azure Subscription ID
subscription_id = '<YOUR_SUBSCRIPTION_ID>'

# Azure Resource Group Name
resource_group_name = '<YOUR_RESOURCE_GROUP_NAME>'


app = func.FunctionApp()
# Set Schedule in UTC {second} {minute} {hour} {day} {month} {day-of-week}
@app.timer_trigger(schedule="0 0 13 * * *", arg_name="myTimer", run_on_startup=False,use_monitor=False) 
def stopdb(myTimer: func.TimerRequest) -> None:
    # Get Azure Credentials
    credentials = DefaultAzureCredential()

    # Make single server for MySQL management client
    mysql_client = mysql.MySQLManagementClient(credentials, subscription_id)

    # Get list of single servers
    servers = mysql_client.servers.list_by_resource_group(resource_group_name)
   logging.info(servers)
    for server in servers:
        #Stop Mysql - Single Server
        logging.info('STOP %s',server.name)
        mysql_client.servers.begin_stop(resource_group_name, server.name)


    # Make flexible servers for MySQL management client
    mysql_flex_client = mysql_flexibleservers.MySQLManagementClient(credentials, subscription_id)
    # Get list of flexible servers
    servers = mysql_flex_client.servers.list_by_resource_group(resource_group_name)
    logging.info(servers)
    for server in servers:
        #Stop MySQL - Flexible Server
        logging.info('STOP %s',server.name)
        mysql_flex_client.servers.begin_stop(resource_group_name, server.name)

現在、Azureのデータベースサービスは単一サーバとフレキシブルサーバの2つが存在します。その両方に対応するようにコーディングしているので長くなっています。使用しているデータベースサービスが1つであればコードはもっと短くなるはずです。
22行目timer_triggerでスクリプトの起動時刻を指定します。一見cronのようですが、項目が6つあり、一番左は秒を指定します。 今回は毎日22:00:00(UTC 13:00:00)に起動するように時刻を指定しました。

requirements.txtには起動するために必要なモジュールを記載します。

azure-functions
azure-identity
azure-mgmt-rdbms

azure-cliのfuncコマンドでローカルで実行してスクリプトを実行して、誤りが無いか確認します。 開発PC、ユーザにDBの操作権限がある場合、スクリプトが起動しDBが停止するので十分注意してください。

$ func start
Found Python version 3.10.12 (python3).

Azure Functions Core Tools
Core Tools Version:       4.0.5441 Commit hash: N/A  (64-bit)
Function Runtime Version: 4.25.3.21264

[2023-11-04T13:54:59.949Z] Worker process started and initialized.

Functions:

    StopMySQL: timerTrigger

For detailed output, run func with --verbose flag.
[2023-11-04T13:55:00.061Z] Executing 'Functions.StopMySQL' (Reason='Timer fired at 2023-11-04T22:55:00.0370491+09:00', Id=135cbc94-fb90-4b9e-8ee1-fee1b4fb5c46)
[2023-11-04T13:55:00.062Z] Trigger Details: UnscheduledInvocationReason: RunOnStartup

関数アプリ作成

このあたりはAzure Portal上で操作して作成したほうが早いかもです。
Azure上にAzure Functionのアプリを作成し、ローカルで開発したコードをアップロードします。
まずは、アプリを格納するためのストレージアカウントを作成します。

$ az storage account create --name <STORAGE_NAME> --location <REGION> --resource-group <RESOURCE_GROUP> --sku Standard_LRS

関数アプリを作成します。 pythonのバージョンはローカルで開発した際のバージョンに合わせてください。
作成が完了するまでしばらく待ちます。

$ az functionapp create --resource-group  <RESOURCE_GROUP> ---consumption-plan-location <REGION> --runtime python --runtime-version 3.9 --functions-version 4 --name StopDB --os-type linux --storage-account <STORAGE_NAME>

関数アプリのデプロイ

ローカルのpythonスクリプトを書いたディレクトリで以下のコマンドを実行し、Azureにアップロードします。 最後にRemote build succeeded!と表示されればデプロイ成功です。

$ func azure functionapp publish StopDB
Getting site publishing info...
[2023-11-04T14:18:06.997Z] Starting the function app deployment...
Removing WEBSITE_CONTENTAZUREFILECONNECTIONSTRING app setting.
Removing WEBSITE_CONTENTSHARE app setting.
Creating archive for current directory...
Performing remote build for functions project.
Uploading 2.28 KB [###############################################################################]

....
Remote build succeeded!
[2023-11-04T14:18:57.100Z] Syncing triggers...
Functions in StopDB:
    StopMySQL - [timerTrigger]

カスタムロール作成

作成したAzure FunctionにMySQL データベースを操作する権限を与えるロールを作成します。このAzure Functionsを動作させるためには、MySQLデータベースをlist化する権限と停止する権限が必要です。
Stop-MySQL

{
    "id": "<ID>",
    "properties": {
        "roleName": "Stop-MySQL",
        "description": "データベースサーバを止めるロール",
        "assignableScopes": [
            "<RESOURCE_GROUP_ID>"
        ],
        "permissions": [
            {
                "actions": [
                    "Microsoft.DBforMySQL/flexibleServers/read",
                    "Microsoft.DBforMySQL/flexibleServers/stop/action",
                    "Microsoft.DBforMySQL/servers/stop/action",
                    "Microsoft.DBforMySQL/servers/read"
                ],
                "notActions": [],
                "dataActions": [],
                "notDataActions": []
            }
        ]
    }
}

ロール付与

MySQLデータベースで先程作成したロールをStopDBに付与します。

PostgreSQLはどうする?

GitHubでStopPostgreSQLとしてコードをおいてあります。

データベースを起動したい

function_app.pyのbegin_stopをbegin_startに変更してください。また、ロールにはstart/action権限を付与してください。

今回作成したリソース

GitHubに今回作成したコードを置いています。
GitHub - TTY6335/StopAzureDB: Azure Functions for stopping Azure Database Services on scheduled time.

参考文献

Azure Functionsをローカルで開発・デバッグする
コマンド ラインから Python 関数を作成する - Azure Functions | Microsoft Learn
Azure Functions のタイマー トリガー | Microsoft Learn
azure.mgmt.rdbms package | Microsoft Learn