1
Help us understand the problem. What are the problem?

posted at

updated at

コマンドラインでAzure Functions Custom Handlerをデプロイする(Go / HTTP trigger編)

はじめに

Microsoft AzureのFaaSであるAzure Functionsにおいて、GoやRustはネイティブにサポートされておらず、Custom Handlerという仕組みを使って実現されています。

また、Visual Studio CodeAzure Functions エクステンションを使うことで、Functions プロジェクトの作成を含めて、開発しやすくなっています。

本記事では、HTTP トリガーで起動するGoの関数アプリを例に、コマンドラインでAzure Functions Custom Handlerにデプロイする手順を紹介します。

環境情報

開発環境:ローカル

関数の開発およびテストはLinux、macOS、Windowsで行えます。私はmacOS Monterey 12.3.1です。

動作環境:Azure

関数のデプロイ先にはWindowsとLinuxが選択できます。本記事ではLinuxを選択しています。

Windowsユーザーの皆様へ

上記の理由により、本記事におきましては、実行バイナリーのファイル名にはファイル拡張子「.exe」をつける、コマンドプロンプトでファイルの一覧を表示する際は dir コマンドを使うなど、適宜の読み替えをお願い致します。

事前に必要なもの

ツール類は、記事を書いた時点で最新のバージョンを使用しています。インストールやセットアップにつきましては、インターネット上の各種記事をご参照ください。

% az version  
{
  "azure-cli": "2.36.0",
  "azure-cli-core": "2.36.0",
  "azure-cli-telemetry": "1.0.6",
  "extensions": {}
}
% func version
4.0.4483

本記事では、関数の作成にGoを使用しています。

% go version
go version go1.18.1 darwin/amd64

作業手順

プロジェクト ディレクトリーの準備

カレント ディレクトリーの確認

本記事にて、最初のカレント ディレクトリーは次の通りです。適宜ご自分の環境に読み替えてください。

% pwd     
/Users/luigi/go/src/github.com/qt-luigi/serverless-go

プロジェクト ディレクトリーの作成

本記事では、プロジェクト ディレクトリーを「httpTrigger」としています。

mkdir コマンドでプロジェクト ディレクトリーを作成した後、cd コマンドでカレント ディレクトリーを作成したプロジェクト ディレクトリー配下に移動します。

% mkdir httpTrigger
% cd httpTrigger

Functions プロジェクトの準備

Functions プロジェクトの作成

func init コマンドにて、ランタイムを「カスタム ハンドラー」とするFunctions プロジェクトを生成します。

  • --worker-runtime:ランタイムの種類。今回は「カスタム ハンドラー」です。
% func init --worker-runtime custom
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /Users/luigi/go/src/github.com/qt-luigi/serverless-go/httpTrigger/.vscode/extensions.json

オプションを指定しなかった場合、対話形式で設定できます。

  • ランタイムの選択にて、今回は「カスタム ハンドラー」なので「6」を選択します。
% func init
Select a number for worker runtime:
1. dotnet
2. dotnet (isolated process)
3. node
4. python
5. powershell
6. custom
Choose option: 6
custom
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /Users/luigi/go/src/github.com/qt-luigi/serverless-go/httpTrigger/.vscode/extensions.json

ls コマンドにて、各ファイルが作成されていることを確認します。

% ls -la
drwxr-xr-x  6 luigi  staff  192  5  7 17:34 .
drwxr-xr-x  5 luigi  staff  160  5  7 17:34 ..
-rw-r--r--  1 luigi  staff  470  5  7 17:34 .gitignore
drwxr-xr-x  3 luigi  staff   96  5  7 17:34 .vscode
-rw-r--r--  1 luigi  staff  428  5  7 17:34 host.json
-rw-r--r--  1 luigi  staff  117  5  7 17:34 local.settings.json
% ls -la .vscode 
drwxr-xr-x  3 luigi  staff   96  5  7 17:34 .
drwxr-xr-x  6 luigi  staff  192  5  7 17:34 ..
-rw-r--r--  1 luigi  staff   80  5  7 17:34 extensions.json

拡張機能バンドルの指定を削除

Azure Functionsでは、HTTP トリガー以外のトリガーを使用する時など、必要な機能を追加して拡張できます。

今回、カスタム ハンドラー/macOS/Goの組み合わせが原因かどうかわかりませんが、私の場合、明示的に手動でインストールする方法でないと上手く動作しない、および、今回の関数はHTTP トリガーであり拡張機能を使用するコーディングは行なっていないため、次の手順にて、自動での拡張機能バンドルの追加を回避します。(残しておくと、この後の func newfunc start でエラーになります)

お好みのテキスト エディターで host.json ファイルを開いて "extensionBundle" ブロックの記述を削除します。

変更前

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[2.*, 3.0.0)"
  },
  "customHandler": {
    "description": {
      "defaultExecutablePath": "",
      "workingDirectory": "",
      "arguments": []
    }
  }
}

変更後

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "customHandler": {
    "description": {
      "defaultExecutablePath": "",
      "workingDirectory": "",
      "arguments": []
    }
  }
}

関数用の設定ファイルを作成

func new コマンドにて、関数用の設定ファイルを作成します。

  • --name:関数名、今回は「hello」とします。
  • --template:関数を起動させる種類。今回は「HTTP トリガー」です。
  • --authlevel:認証レベル。今回は「anonymous」(不特定多数がアクセス可能)。
% func new --name hello --template "HTTP trigger" --authlevel anonymous
Select a number for template:HTTP trigger
Function name: [HttpTrigger] Writing /Users/luigi/go/src/github.com/qt-luigi/serverless-go/httpTrigger/hello/function.json
The function "hello" was created successfully from the "HTTP trigger" template.

オプションを指定しなかった場合、対話形式で設定できますが、作成された function.json ファイルを手直しする必要があります。

  • テンプレートの選択にて、今回は「HTTP トリガー」なので「5」を選択します。
  • 関数名にて、今回は「hello」なので「hello」を入力します。
% func new     
Select a number for template:
1. Azure Blob Storage trigger
2. Azure Cosmos DB trigger
3. Azure Event Grid trigger
4. Azure Event Hub trigger
5. HTTP trigger
6. IoT Hub (Event Hub)
7. Azure Queue Storage trigger
8. RabbitMQ trigger
9. SendGrid
10. Azure Service Bus Queue trigger
11. Azure Service Bus Topic trigger
12. SignalR negotiate HTTP trigger
13. Timer trigger
Choose option: 5
HTTP trigger
Function name: [HttpTrigger] hello
Writing /Users/luigi/go/src/github.com/qt-luigi/serverless-go/httpTrigger/hello/function.json
The function "hello" was created successfully from the "HTTP trigger" template.

お好みのテキストエディターで hello/function.json ファイルを開いて "bindings" - "authLevel" の値を "Anonymous" に修正します。

変更前

{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

変更後

{
  "bindings": [
    {
      "authLevel": "Anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

ls コマンドにて、各ファイルが作成されていることを確認します。

% ls -l
drwxr-xr-x  3 luigi  staff   96  5  7 15:11 hello
-rw-r--r--  1 luigi  staff  316  5  7 15:10 host.json
-rw-r--r--  1 luigi  staff  117  5  7 15:05 local.settings.json
% ls -l hello 
-rw-r--r--  1 luigi  staff  280  5  7 15:11 function.json

関数の作成

本記事では、関数の作成にGoを使用しています。Custom Handlerは、RustやDenoで作成した関数にも対応しています。

Goプロジェクトの初期化

go mod init コマンドにて、Goのプロジェクトを初期化します。

% go mod init
go: creating new go.mod: module github.com/qt-luigi/serverless-go/httpTrigger
go: to add module requirements and sums:
	go mod tidy

ls コマンドにて、各ファイルが作成されていることを確認します。

% ls -l
-rw-r--r--  1 luigi  staff   67  5  7 15:25 go.mod
drwxr-xr-x  3 luigi  staff   96  5  7 15:11 hello
-rw-r--r--  1 luigi  staff  366  5  7 15:18 host.json
-rw-r--r--  1 luigi  staff  117  5  7 15:05 local.settings.json

Goコードの作成

お好みのテキストエディターで、Goの関数をコーディングします。

今回は、次のサイトのソースコードを使用しています。(ソースコードの著作や修正の反映を考慮して、リンクで紹介しています。)

アプリケーションは、GETメソッドでリクエストが届いたらレスポンスとして ”hello world" の文字を返す関数です。

ソースファイル名において、上記のリンク記事では「handler.go」となっていますが、こちらでは「server.go」として話を進めます。

go build コマンドにて、ソースファイル「server.go」をコンパイルして、実行バイナリー「server」を生成します。

% go build server.go

ls コマンドにて、各ファイルが作成されていることを確認します。

% ls -l
-rw-r--r--  1 luigi  staff       67  5  7 15:25 go.mod
drwxr-xr-x  3 luigi  staff       96  5  7 15:11 hello
-rw-r--r--  1 luigi  staff      366  5  7 15:18 host.json
-rw-r--r--  1 luigi  staff      117  5  7 15:05 local.settings.json
-rwxr-xr-x  1 luigi  staff  6161472  5  7 15:26 server
-rw-r--r--  1 luigi  staff      636  5  7 15:14 server.go

関数を設定ファイルに登録

お好みのテキスト エディターで、host.json ファイルの以下の箇所を修正します。

  • "customHandler" - "description" - "defaultExecutablePath" の値を "./server" に変更します。
  • "customHandler" ブロック内にて、"description" と同じ階層に「"enableForwardingHttpRequest" : true」の一行を追加します。(お互いの項目を区切るためのカンマ「,」の記述をお忘れなく!)

変更前

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "customHandler": {
    "description": {
      "defaultExecutablePath": "",
      "workingDirectory": "",
      "arguments": []
    }
  }
}

変更後

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "excludedTypes": "Request"
      }
    }
  },
  "customHandler": {
    "description": {
      "defaultExecutablePath": "./server",
      "workingDirectory": "",
      "arguments": []
    },
    "enableForwardingHttpRequest" : true
  }
}

ローカルでの動作確認

関数アプリケーションの起動

func start コマンドにて、関数アプリケーションを起動します。

% func start

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


Functions:

	hello: [GET,POST] http://localhost:7071/api/hello

For detailed output, run func with --verbose flag.
[2022-05-07T06:19:32.753Z] Go server Listening on:  56196
[2022-05-07T06:19:32.766Z] Worker process started and initialized.
[2022-05-07T06:19:37.081Z] Host lock lease acquired by instance ID '000000000000000000000000000B9DD5'.

動作の確認

提示されたURL "http://localhost:7071/api/hello" に、Web ブラウザーや curl コマンドでアクセスします。「hello world」の文字が表示されたら成功です。

curl コマンドの場合、別途、端末画面を立ち上げてアクセスします。

% curl http://localhost:7071/api/hello
hello world

アクセスが成功した場合、ログが出力されます。

[2022-05-07T06:20:10.476Z] Executing 'Functions.hello' (Reason='This function was programmatically called via the host APIs.', Id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
[2022-05-07T06:20:10.570Z] Executed 'Functions.hello' (Succeeded, Id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, Duration=151ms)

関数を停止する場合は、[Ctrl] + [C] キーを押下します。

^C[2022-05-07T06:20:38.090Z] Go server Listening on:  56196
[2022-05-07T06:20:38.114Z] Worker process started and initialized.

Azureへのデプロイ

この後の、リソース グループ、ストレージ アカウント、関数 アプリケーションの作成などは、事前にWebブラウザーにて「Azure ポータル サイト」にログインして「関数 アプリ」の機能を使って対応して頂いても大丈夫です。

Azureにログイン

az login コマンドにて、コマンドラインベースでAzureにログインします。

% az login
A web browser has been opened at https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize. Please continue the login in the web browser. If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.
[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    ...
      "name": "メールアドレス",
      "type": "user"
    }
  }
]

Azure 関数 アプリケーションの作成

ここでは、次の記事のスクリプトを参考に、コマンドラインベースで準備および実行しました。

また、こちらの記事も参考にしました。

環境変数の値を設定

参考記事の固定値を「serverless-go」で書き換えて行った感じです。

  • $RANDOM:0から32767までの乱数を発生させる環境変数。
  • location:関数をデプロイするサーバーインスタンスの場所。今回は「eastus」(西アメリカ)です。
  • resourceGroup:リソース グループ名。Azureではリソース グループ単位でプロジェクトや課金を扱います。
  • tag:リソースやインスタンスに、わかりやすい名前をつけます。
  • storage:ストレージ アカウント名。Azureにデプロイした関数のファイル群は、ストレージ サービスに保管されるため必要となります。
  • functionApp:Azureでの関数のアプリケーション名。
  • skuStorage:ストレージの種類。今回は「Standard_LRS」です。
  • functionsVersion:Azure Functionsのバージョン。現在は「4」が推奨です。

次の2つは、参考記事にはなく、新しく追加したものです。

  • osType:OSの種類。今回は「Linux」です。「Windows」も選択できます。
  • runtime:ランタイムの種類。今回は「custom」です。
% let "randomIdentifier=$RANDOM*$RANDOM"
% location="eastus"
% resourceGroup="serverless-go-rg-$randomIdentifier"
% tag="create-serverless-go-consumption"
% storage="serverlessgo$randomIdentifier"
% functionApp="serverless-go-$randomIdentifier"
% skuStorage="Standard_LRS"
% functionsVersion="4"

% osType="Linux"
% runtime="custom"

以降、生成された $randomIdentifier の乱数値は「999999999」で表現しています。

リソース グループを作成

az group create コマンドにて、リソース グループを作成します。

% az group create
 --name $resourceGroup
 --location "$location"
 --tags $tag

実行結果。

% echo "Creating $resourceGroup in "$location"..."
Creating serverless-go-rg-999999999 in eastus...

% az group create --name $resourceGroup --location "$location" --tags $tag
{
  "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/serverless-go-rg-999999999",
  "location": "eastus",
  ...
  },
  "type": "Microsoft.Resources/resourceGroups"
}

ストレージ アカウントを作成

az storage account create コマンドにて、ストレージ アカウントを作成します。

% az storage account create
 --name $storage
 --location "$location"
 --resource-group $resourceGroup
 --sku $skuStorage

実行結果。

% echo "Creating $storage"
Creating serverlessgo999999999

% az storage account create --name $storage --location "$location" --resource-group $resourceGroup --sku $skuStorage
{
  "accessTier": "Hot",
  "allowBlobPublicAccess": true,
   ...
  "tags": {},
  "type": "Microsoft.Storage/storageAccounts"
}

Azure 関数 アプリケーションを作成

az functionapp create コマンドにて、Azure 関数 アプリケーションを作成します。

% az functionapp create
 --name $functionApp
 --storage-account $storage
 --consumption-plan-location "$location"
 --resource-group $resourceGroup
 --functions-version $functionsVersion
 --os-type $osType
 --runtime $runtime

実行結果。

% echo "Creating $functionApp"
Creating serverless-go-999999999

% az functionapp create --name $functionApp --storage-account $storage --consumption-plan-location "$location" --resource-group $resourceGroup --functions-version $functionsVersion --os-type $osType --runtime $runtime
Resource provider 'Microsoft.Web' used by this operation is not registered. We are registering for you.
Registration succeeded.
Your Linux function app 'serverless-go-999999999', that uses a consumption plan has been successfully created but is not active until content is published using Azure Portal or the Functions Core Tools.
Resource provider 'microsoft.insights' used by this operation is not registered. We are registering for you.
Registration succeeded.
Application Insights "serverless-go-999999999" was created for this Function App. You can visit https://portal.azure.com/#resource/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/serverless-go-rg-999999999/providers/microsoft.insights/components/serverless-go-999999999/overview to view your Application Insights component
{
  "availabilityState": "Normal",
  "clientAffinityEnabled": false,
  ...
  "usageState": "Normal",
  "virtualNetworkSubnetId": null
}

GoでLinux用の実行バイナリーを再生成

GOOS および GOARCH にて linux/amd64 を指定して、macOS上でLinux用の実行バイナリーを再生成します。標準機能で簡単にクロスコンパイルができるところが、Goの利点のひとつですよね。

% GOOS=linux GOARCH=amd64 go build server.go 

生成された実行バイナリーはLinux用なので、macOS上では実行できません。

% ./server 
zsh: exec format error: ./server

ls コマンドでも、ファイル「server」の更新日時とサイズが変わったことぐらいしか確認できませんね。

% ls -l
-rw-r--r--  1 luigi  staff       67  5  7 15:25 go.mod
drwxr-xr-x  3 luigi  staff       96  5  7 15:11 hello
-rw-r--r--  1 luigi  staff      366  5  7 15:18 host.json
-rw-r--r--  1 luigi  staff      117  5  7 15:05 local.settings.json
-rwxr-xr-x  1 luigi  staff  6238169  5  7 16:26 server
-rw-r--r--  1 luigi  staff      636  5  7 15:14 server.go

Azureにデプロイ

func azure functionapp publish コマンドにて、Functions プロジェクトをAzureにデプロイします。

関数を修正して再デプロイする際も、本手順を行います。

% func azure functionapp publish $functionApp           
Getting site publishing info...
Uploading package...
Uploading 3.4 MB [################################################################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Functions in serverless-go-999999999:
    hello - [httpTrigger]
        Invoke url: https://serverless-go-999999999.azurewebsites.net/api/hello

関数の確認

提示されたURL "https://serverless-go-999999999.azurewebsites.net/api/hello" に、Web ブラウザーや curl コマンドでアクセスします。「hello world」の文字が表示されたら成功です。

curl コマンドの場合、別途、端末画面を立ち上げてアクセスします。

% curl https://serverless-go-999999999.azurewebsites.net/api/hello
hello world

Azure 関数 アプリケーションを削除

az group delete コマンドにて、リソース グループの単位で削除します。削除の実行確認で「y」を入力します。

% az group delete --name $resourceGroup
Are you sure you want to perform this operation? (y/n): y

さいごに

Azure Functions および Custom Handler の情報に関しては、一次ソースである公式ドキュメントサイト「Microsoft Docs」に多く掲載されています。私も参考記事をベースに、コマンド引数などを含めて不明なものが出てきた際には、ひとつずつ調べて理解も進めて行きました。(作業中、ドキュメントの不具合を見つけて報告したりもしました。)

Custom Handler の場合、実際に試した内容や詳細なデプロイ手順となると、インターネット上でも見つけづらかったので、本記事が少しでもお役に立てれば幸いです。

私は関数をGoで作成していますが、RustやDenoなど、Azure Functionsでネイティブ サポートされていないプログラミング言語ユーザーの皆様にも、本記事を参考にぜひ「Custom Handler」にチャレンジして頂きたいです。

今後は、拡張機能を使ったHTTP トリガー以外の起動方法にチャレンジする予定です。上手くいけば、またこちらに記事を投稿させて頂けれたらと思っています。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?