Azure FunctionsのカスタムハンドラーでGolangを動かしてみた。最終的なリポジトリは下記。
- HTTPトリガー
- Timerトリガー
を試してみた。
公式リファレンス
前提
ローカルでの動作確認のため、func
コマンドが必要
npm i -g azure-functions-core-tools@4 --unsafe-perm true
func --version 4.0.4544
HTTPトリガー
まずはhttp://localhost:7071/api/SampleHttpTrigger
のようなエンドポイントを叩くと、スクリプトが実行されるHTTPトリガーをやってみる。
ディレクトリ構成はこんな感じ
$ tree . ├── SampleHttpTrigger │ └── function.json ├── go.mod ├── go.sum ├── handler │ └── sample_http_trigger.go ├── host.json ├── local.settings.json └── main.go
main.go
はシンプルにサーバーを立ててハンドラーを割り当てるのみ。
package main import ( "log" "main/handler" "net/http" "os" ) func main() { listenAddr := ":8080" if val, ok := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT"); ok { listenAddr = ":" + val } mux := http.NewServeMux() mux.HandleFunc("/api/SampleHttpTrigger", handler.SampleHttpTrigger) log.Printf("About to listen on %s. Go to https://127.0.0.1%s/", listenAddr, listenAddr) log.Fatal(http.ListenAndServe(listenAddr, mux)) }
handler/sample_http_trigger.go
にメインの処理を書く。といってもjsonレスポンスを返すだけ。
package handler import ( "encoding/json" "net/http" ) type response struct { Status int Rssult string } func SampleHttpTrigger(w http.ResponseWriter, r *http.Request) { res := response{http.StatusOK, "ok"} js, err := json.Marshal(res) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(js) }
次にAzure Functionsで動作させるための設定をいくつか作る。
作るのはSampleHttpTrigger/function.json
、host.json
、local.settings.json
の3つ。
SampleHttpTrigger/function.json
GET,POSTなどの設定をするファイル。
{ "bindings": [ { "authLevel": "anonymous", "type": "httpTrigger", "direction": "in", "name": "req", "methods": ["get", "post"] }, { "type": "http", "direction": "out", "name": "res" } ] }
ポイントは"authLevel": "anonymous"
を設定すること。これをしていないとローカルでもエラーが出てデバッグができない。
本番にデプロイする際は不要なのでこの1行を削除する。
あとSampleHttpTrigger/
のように1段ディレクトリを噛ませた下に置く必要がある。
host.json
これはAzureの公式からそのままコピった。"defaultExecutablePath"
はmain
に変更。
{ "version": "2.0", "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[2.*, 3.0.0)" }, "customHandler": { "description": { "defaultExecutablePath": "main" }, "enableForwardingHttpRequest": true } }
local.settings.json
これも公式のまま。
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "YOUR_STORAGE_CONNECTION_STRING", "FUNCTIONS_WORKER_RUNTIME": "custom" }, "ConnectionStrings": {} }
"AzureWebJobsStorage": "YOUR_STORAGE_CONNECTION_STRING"
も文字通りYOUR_STORAGE_CONNECTION_STRING
のまま設定しているけど問題なく動くので一旦無視でOK。
動作確認
go build main.go && func start
実際にURLにアクセス、またはcurlで叩いてみるとレスポンスが返ってくるはず。
curl http://localhost:7071/api/SampleHttpTrigger {"Status":200,"Rssult":"ok"}%
デプロイ
GolangをAzure Functionsで動かすためにはDockerコンテナでデプロイする必要がある。
ただ別に難しいことではなく、Azure App Serviceと同じようにコンテナレジストリにDockerイメージをpushし、Azure Functionsのデプロイセンターでイメージを選択するだけだ。
Dockerfile
FROM golang:1.18-alpine COPY . /go/app WORKDIR /go/app RUN go mod tidy RUN CGO_ENABLED=0 GOOS=linux go build main.go FROM mcr.microsoft.com/azure-functions/dotnet:3.0-appservice ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ AzureFunctionsJobHost__Logging__Console__IsEnabled=true ENV TZ=Asia/Tokyo COPY --from=0 /go/app/ /home/site/wwwroot
まずはコンテナレジストリにDockerイメージをpush
DOCKER_IMAGE_TAG='your_azure_acr.azurecr.io/tools/demo' az acr login --name your_azure_acr docker build -t tools-docker -f Dockerfile . && docker tag tools-docker:latest ${DOCKER_IMAGE_TAG} && docker push ${DOCKER_IMAGE_TAG}
Azure PortalでAzure Functionsを再起動して終了
しばらくすると関数アプリ > 関数 のところにSampleHttpTriggerが表示されるので待つ。
Timerトリガー
次はcronのようにタイマーで発火するTimerトリガーを実装する。
追加するのはSampleTimer/function.json
とhandler/sample_timer.go
の2つだけ。
. ├── SampleHttpTrigger │ └── function.json +├── SampleTimer +│ └── function.json ├── go.mod ├── go.sum ├── handler │ ├── sample_http_trigger.go +│ └── sample_timer.go ├── host.json ├── local.settings.json ├── main └── main.go
SampleTimer/function.json
{ "bindings": [ { "name": "SampleTimer", "type": "timerTrigger", "direction": "in", "schedule": "*/10 * * * * *" } ] }
ここでタイマーのスケジュールを設定する。今回はデバッグ用に10秒に1回発火するようにしている。
handler/sample_timer.go
package handler import ( "encoding/json" "fmt" "net/http" ) func SampleTimerTrigger(w http.ResponseWriter, r *http.Request) { fmt.Println("^^^^^^^^^^ SampleTimerTrigger is executed!! ^^^^^^^^^^^^") res := response{http.StatusOK, "ok"} js, err := json.Marshal(res) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(js) }
これはsample_http_trigger.go
とほぼ変わらない。デバッグ用にわかりやすくfmt.Println()
を追加しているのみ。
最後のmain.go
に1行追加
package main
import (
"log"
"main/handler"
"net/http"
"os"
)
func main() {
listenAddr := ":8080"
if val, ok := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT"); ok {
listenAddr = ":" + val
}
mux := http.NewServeMux()
mux.HandleFunc("/api/SampleHttpTrigger", handler.SampleHttpTrigger)
+ mux.HandleFunc("/SampleTimer", handler.SampleTimerTrigger)
log.Printf("About to listen on %s. Go to https://127.0.0.1%s/", listenAddr, listenAddr)
log.Fatal(http.ListenAndServe(listenAddr, mux))
}
URLとしてアクセスする形式ではないが、このような設定が必要。また、/SampleTimer
はSampleTimer/function.json
のname
と同じにしておく必要がある。
動作確認
確認方法は先程と同じ
go build main.go && func start
10秒ぐらいするとトリガーが発火される。
タイマートリガーのデプロイも同じでDockerイメージをpushして関数アプリを再起動してしばらくすると(2,3分で)Azure Portal上で確認できるようになる。
終わりに
サンプルが少なくて試行錯誤しながらだったので結構詰まった。特にHTTPトリガーのauthLevelのところは公式の手順ママにすすめると必ず引っかかる罠になっている。
Golangのスクリプトをただ実行したいだけなのにDockerイメージを作ってpushして、、、などと毎回やるのは結構面倒くさく、開発以外の箇所で時間が食われるのであんまりおすすめしないかも。たぶんGCPとかにはもっと楽に実行できるようなサービスがあるはず。たぶん。