Azure Functionsを使用してVMインスタンスを指定した時間に停止・起動させる方法

2023年3月31日掲載

キービジュアル

ご覧いただきありがとうございます。ソフトバンクの小柳です。

今回はMicrosoft Azure Functionsを使用して、VMインスタンスを指定した時間に停止・起動させる方法を紹介します。

他のパブリッククラウドでも指定した時間にインスタンスを停止・起動したいというお問い合わせいただくことがあるため、Microsoft Azure環境でAzure Functionsを使用して実現する方法を画像を交えて紹介します。

目次

Azure Functionsとは

初めにAzure Functionsとは何かについて、簡単に説明します。

Azure Functionsは、プログラムだけ用意すればサーバを意識しなくてもプログラムを実行する環境だけ提供してくれるサービスです。サーバを意識しなくてもよいというところからサーバレスなサービスの一種です。

他クラウドの類似製品としては、Alibaba Cloud Function Compute、AWS Lambda、Google Cloud Functionsなどが挙げられます。

今回は手順説明のため詳細は省略しますが、興味がある方は、参考リンクの公式ドキュメントやブログ記事をご覧ください。

利用ケースとデモ環境について

実際にお問い合わせいただくケースでは、小規模なシステムや開発環境などを、夜間や休日などの使われていない時間帯には自動で停止し、平日日中帯に利用する時間には自動で起動しておきたいという要望があります。

これからAzure Functionsを使用して指定した時間にVMインスタンスを停止・起動させる具体的な手順を紹介していきますが、本記事のデモでは、下図のような構成でAzure Functionsの環境を構築します。

必要なローカル環境としては、WindowsにDockerをインストールした環境、スクリプトの言語はPythonを使用しています。
Dockerでイメージを作成し、そのイメージをプッシュし、イメージからAzure Functionにデプロイしてコードを稼働させます。

dataintegration

まずは、Azure上にデモ用のVMを2台作成します。
すでに環境がある場合は、VMの作成はスキップして問題ありません。

作成したVMの詳細を開いて、それぞれにTagを付与します。
このTagによって停止・起動させるマシンを絞り込むようにするため、すでに環境がある場合でも、このTag付けは忘れないようにしてください。
画像のTag付与例では、Keyはfunctions、Valueはstart&stopとしています。

dataintegration
dataintegration

次にローカルのDocker環境を整えます。

まずは、Docker HubでDocker IDを用意します。
Docker IDが用意できたら、Docker Desktop for Windowsのインストールガイドを参照の上、DockerDesktopをインストールします。

dataintegration

次にAzure CLIのインストールガイド を参照して、Azure CLI をインストールします。
デフォルトの設定でAzure CLIのインストールが完了したら、コマンドプロンプトを開き、az --versionを実行しバージョンが表示されることを確認します。

dataintegration

さらにAzure Functions Core Toolsのインストールガイドを参照して、Azure Functions Core Toolsをインストールします。
デフォルトの設定でAzure Functions Core Toolsのインストールが完了したら、コマンドプロンプトを開いて、funcを実行しバージョンが表示されることを確認します。

dataintegration

起動スクリプトのデプロイ

ここまでインストールが終わったら、起動用のスクリプトの作成準備を始めます。

まず、ローカルの任意の場所にazure-docker-vm-startフォルダを作成します。
コマンドプロンプトを開いて、作成したazure-docker-vm-startフォルダに移動します。
az loginを実行してAzureにログインし、起動されたブラウザでアカウント情報を入力して、ログインします。

dataintegration

作成したazure-docker-vm-startフォルダ配下にvenvを作成して、venvをアクティブにします。

py -m venv .venv
.venv\scripts\activate

下記のコマンドを実行して、ローカルプロジェクトを初期化します。

func init --worker-runtime python --docker

スクリプト格納用に、フォルダを作成します。
ここでは、「functions-vm-start」フォルダとします。

ここまでで、フォルダ構造が以下のようになっていることを確認します。
この後、①から③のファイルを編集します。

azure-docker-vm-start
├── host.json 
├── local.settings.json
├── Dockerfile
├── requirements.txt ③
└──functions-vm-start
    ├── __init__.py ①
    └── function.json ②


まず、__init__.pyファイルを、下記のように編集します。

import datetime
import logging


import azure.functions as func
from azure.mgmt.compute import ComputeManagementClient
from azure.common.credentials import ServicePrincipalCredentials
   

def main(mytimer: func.TimerRequest) -> None:
    utc_timestamp = datetime.datetime.utcnow().replace(
        tzinfo=datetime.timezone.utc).isoformat()
   
    if mytimer.past_due:
        logging.info('The timer is past due!')


    logging.info('Python timer trigger function ran at %s', utc_timestamp)
   
    Subscription_Id = "XXXXXXXXXXXXXX"
    Tenant_Id = "XXXXXXXXXXXXXX"
    Client_Id = "XXXXXXXXXXXXXX"
    Secret = "XXXXXXXXXXXXXX"
       
    credential = ServicePrincipalCredentials(
        client_id=Client_Id,
        secret=Secret,
        tenant=Tenant_Id
    )
           
    compute_client = ComputeManagementClient(credential, Subscription_Id)
       
    vm_list = compute_client.virtual_machines.list_all()
    target_vm_list_filtered = [vm.name for vm in vm_list if vm.tags and "functions" in vm.tags and vm.tags["functions"] == "start&stop"]
    for target_vm_name in target_vm_list_filtered :
        group_name = "demo-resourcegroup"
        print("Start vm server with name:{0}".format(target_vm_name))
        compute_client.virtual_machines.start(group_name, target_vm_name)
    print(target_vm_list_filtered)
   
    if __name__ =='__main__':
        main()

スクリプトの途中に記載されている以下の箇所は、自分の環境にあわせて変更してください。
Subscription IdとTenant_Idを取得する方法は、公式ドキュメントのAzure portal でサブスクリプションとテナントの ID を取得するで確認することができます。
Client IdとSecretを取得する方法は、公式ドキュメントのリソースにアクセスできる Azure Active Directory アプリケーションとサービス プリンシパルを作成するで確認することができます。

Subscription_Id = 
Tenant_Id = 
Client_Id = 
Secret = 
       
###

    target_vm_list_filtered = [vm.name for vm in vm_list if vm.tags and "" in vm.tags and vm.tags[""] == ""]
    for target_vm_name in target_vm_list_filtered :
        group_name = ""

function.jsonファイルを、下記のように編集します。
このファイルで、実行スケジュールの設定をしていますが、サンプルでは、月曜日から金曜日の9時に実行するように記載しています。
スケジュールの設定については、最後にあらためて説明します。

{
    "scriptFile": "__init__.py",
    "bindings": [
    {
      "name": "mytimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 9 * * 1-5"
    }
    ]
}

requirements.txtファイルを、下記のように編集します。

# Do not include azure-functions-worker as it may conflict with the Azure Functions platform
azure-functions
azureml-core
azure-mgmt-compute==10.0.0

スクリプトの準備ができたので、カスタムコンテナーイメージを作成して、Azureにプッシュします。
まず、Dockerfileが下記のようになっていることを確認します。

dataintegration

下記コマンドを実行して、カスタムコンテナーイメージをビルドして、DockerdesktopのLOCALにイメージができたことを確認します。
Docker IDの部分は、自分の物と置き換えてください。

docker build --tag [Docker ID]/azure-functions-vm-start:1.0 .
dataintegration

次に下記コマンドを実行して、カスタムコンテナーのイメージをプッシュしたら、DockerdesktopのREMOTE REPOSITORIESにイメージがあることを確認します。
こちらもDocker IDの部分は、自分の物と置き換えてください。

docker push [Docker ID]/azure-functions-vm-start:1.0
dataintegration

プッシュしたカスタムコンテナーイメージで、Azure Functionsの関数を作成します。
下記の3つのコマンドで、リソースグループ、ストレージアカウント、プランを作成します。

az group create --name demo-resourcegroup --location japaneast

az storage account create --name demo-sac --location japaneast --resource-group demo-resourcegroup --sku Standard_LRS

az functionapp plan create --resource-group  demo-resourcegroup --name demo-plan --location japaneast --number-of-workers 1 --sku EP1 --is-linux

ここでようやく、起動用の関数を作成します。

az functionapp create --name vm-start --storage-account demo-sac --resource-group demo-resourcegroup --plan demo-plan  --functions-version 4 --deployment-container-image-name [Docker ID]/azure-functions-vm-start:1.0

上記のコマンド実行後に、Azure Portal内のFunction Appで関数が作成されたかことを確認します。

dataintegration
dataintegration
dataintegration
dataintegration

関数が作成できたので、関数を手動で実行してVMインスタンスを起動できるかテストします。
まずは、VMインスタンスが停止していることを確認します。

dataintegration

コードを手動実行して、インスタンスが起動状態になることを確認します。

dataintegration
dataintegration
dataintegration

停止スクリプトのデプロイ

指定した時間にVMを起動するスクリプトがデプロイできましたので、次はVMを停止するスクリプトをデプロイします。

デプロイ手順までの手順は、起動スクリプトのデプロイ手順とほとんど同じです。
ローカルの任意の場所にazure-docker-vm-stopフォルダを作成します。
コマンドプロンプトを開いて、作成したazure-docker-vm-stopフォルダに移動します。
az loginを実行してAzureにログインし、起動されたブラウザでアカウント情報を入力して、ログインします。

作成したazure-docker-vm-stopフォルダ配下にvenvを作成して、venvをアクティブにします。

py -m venv .venv
.venv\scripts\activate

下記のコマンドを実行して、ローカルプロジェクトを初期化します。

func init --worker-runtime python --docker

スクリプト格納用に、フォルダを作成します。
ここでは、「functions-vm-stop」フォルダとします。

ここまでで、フォルダ構造が以下のようになっていることを確認します。
この後、④から⑥のファイルを編集します。

azure-docker-vm-stop
├── host.json
├── local.settings.json
├── Dockerfile
├── requirements.txt ⑥
└──functions-vm-stop
    ├── __init__.py ④
    └── function.json ⑤


__init__.pyファイルを、下記のように編集します。

import datetime
import logging

import azure.functions as func
from azure.mgmt.compute import ComputeManagementClient
from azure.common.credentials import ServicePrincipalCredentials
   

def main(mytimer: func.TimerRequest) -> None:
    utc_timestamp = datetime.datetime.utcnow().replace(
        tzinfo=datetime.timezone.utc).isoformat()
   
    if mytimer.past_due:
        logging.info('The timer is past due!')

    logging.info('Python timer trigger function ran at %s', utc_timestamp)
   
    Subscription_Id = "XXXXXXXXXXXXXXXXXXXX"
    Tenant_Id = "XXXXXXXXXXXXXXXXXXXX"
    Client_Id = "XXXXXXXXXXXXXXXXXXXX"
    Secret = "XXXXXXXXXXXXXXXXXXXX"
       
    credential = ServicePrincipalCredentials(
        client_id=Client_Id,
        secret=Secret,
        tenant=Tenant_Id
    )
           
    compute_client = ComputeManagementClient(credential, Subscription_Id)
       
    vm_list = compute_client.virtual_machines.list_all()
    target_vm_list_filtered = [vm.name for vm in vm_list if vm.tags and "functions" in vm.tags and vm.tags["functions"] == "start&stop"]
    for target_vm_name in target_vm_list_filtered :
        group_name = "demo-resourcegroup"
        print("Start vm server with name:{0}".format(target_vm_name))
        compute_client.virtual_machines.deallocate(group_name, target_vm_name)
    print(target_vm_list_filtered)
   
    if __name__ =='__main__':
        main()

スクリプトの途中に記載されている以下の箇所は、自分の環境にあわせて変更してください。

Subscription_Id = 
Tenant_Id = 
Client_Id = 
Secret = 
       
###

    target_vm_list_filtered = [vm.name for vm in vm_list if vm.tags and "" in vm.tags and vm.tags[""] == ""]
    for target_vm_name in target_vm_list_filtered :
        group_name = ""

function.jsonファイルを、下記のように編集します。
このファイルで、実行スケジュールの設定をしていますが、サンプルでは、月曜日から金曜日の21時に実行するように記載しています。

{
    "scriptFile": "__init__.py",
    "bindings": [
    {
      "name": "mytimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "0 0 21 * * 1-5"
    }
    ]
}

requirements.txtファイルも、下記のように編集します。

# Do not include azure-functions-worker as it may conflict with the Azure Functions platform
azure-functions
azureml-core
azure-mgmt-compute==10.0.0

下記コマンドを実行して、カスタムコンテナーイメージをビルドして、DockerdesktopのLOCALにイメージができたことを確認します。
Docker IDの部分は、自分の物と置き換えてください。

docker build --tag [Docker ID]/azure-functions-vm-stop:1.0 .
dataintegration

次に下記コマンドを実行して、カスタムコンテナーのイメージをプッシュしたら、DockerdesktopのREMOTE REPOSITORIESにイメージがあることを確認します。
こちらもDocker IDの部分は、自分の物と置き換えてください。

docker push [Docker ID]/azure-functions-vm-start:1.0
dataintegration

プッシュしたカスタムコンテナーイメージで、Azure FunctionsにVM停止用の関数を作成します。
リソースグループ、ストレージアカウント、プランは、起動用に作成したものを利用します。

# 再掲
az group create --name demo-resourcegroup --location japaneast

az storage account create --name demo-sac --location japaneast --resource-group demo-resourcegroup --sku Standard_LRS

az functionapp plan create --resource-group  demo-resourcegroup --name demo-plan --location japaneast --number-of-workers 1 --sku EP1 --is-linux

以下のコマンドを実行して、停止用の関数を作成します。

az functionapp create --name vm-stop --storage-account demo-sac --resource-group demo-resourcegroup --plan demo-plan  --functions-version 4 --deployment-container-image-name [Docker ID]/azure-functions-vm-stop:1.0

上記のコマンド実行後に、Azure Portal内のFunction Appで関数が作成されたことを確認します。

dataintegration
dataintegration
dataintegration

関数が作成できたので、関数を手動で実行してVMインスタンスを停止できるかテストします。
VM停止用のコードを実行すると、コード内で指定したタグが付与されているVMインスタンスが停止しますので、実行には十分に注意してください。

まずは、VMインスタンスが起動していることを確認します。

dataintegration

コードを手動実行して、インスタンスが停止状態になることを確認します。

dataintegration
dataintegration

実行スケジュールについて

このデモでは、function.jsonに実行スケジュールを記載しています。
スケジュールを変更したいときは、function.json内にあるscheduleの箇所をncrontabの記述式で記載することで変更することが出来ます。

#月曜から金曜の毎日午前 9 時 に実行
0 0 9 * * 1-5

##月曜から金曜の毎日午後 9 時 に実行
0 0 21 * * 1-5

他のncrontabの記述方法については公式ドキュメントのNCRONTABの記載を参考にしてください。

また、1つ重要な注意事項として、作成した関数のタイムゾーンはデフォルトでは、UTCになっています。
日本時間はUTCよりも9時間進んでいますので、日本時間で実行したい場合はタイムゾーンを考慮してスケジュールを書くか、タイムゾーンを変更する必要があります。

タイムゾーンを変更するには、Azure Portalから以下のように設定することで変更できます。

1.Function Appのコンソールを開き、作成した関数をクリックして、設定にある構成をクリックします。

2.新しいアプリケーションの設定をクリックし、以下を入力してOKを押します。
名前:WEBSITE_TIME_ZONE
値:Asia/Tokyo

3.アプリケーション設定の一覧に名前:WEBSITE_TIME_ZONEが作成されますので、右にある「非表示の値です。値を表示するにはクリックしてください」をクリックして、Asia/Tokyoになっていることを確認します。

4.アプリケーション設定の上にある保存をクリックします。
変更後に、function.jsonに記載した時間(日本時間)でコードが動作することを確認します。

まとめ

長くなりましたが、Azure Functionsを使用して、VMインスタンスを指定した時間に停止・起動する方法を紹介しました。

VMの自動停止や起動は、今回はAzure Functionsを利用しましたが、他にもAzure Automationを使用してコードを書かずにVMインスタンスを自動で停止したり起動することもできます。Azure Automationについては、機会があれば別記事で紹介したいと思います。

VMのランニングコストを少しでも削減したい方に、今回の記事が少しでもお役に立てば幸いです。

関連サービス

Microsoft Azure

Microsoft Azureは、Microsoftが提供するパブリッククラウドプラットフォームです。コンピューティングからデータ保存、アプリケーションなどのリソースを、必要な時に必要な量だけ従量課金で利用することができます。

おすすめの記事

条件に該当するページがございません