ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

Azure FunctionsでGolangを動かす

Azure FunctionsのカスタムハンドラーでGolangを動かしてみた。最終的なリポジトリは下記。

github.com

  • HTTPトリガー
  • Timerトリガー

を試してみた。

公式リファレンス

docs.microsoft.com

docs.microsoft.com

前提

ローカルでの動作確認のため、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.jsonhost.jsonlocal.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を再起動して終了

Azure Portal > 関数アプリ > デプロイセンターでイメージを指定して
再起動して最新のイメージを反映

しばらくすると関数アプリ > 関数 のところにSampleHttpTriggerが表示されるので待つ。

Timerトリガー

次はcronのようにタイマーで発火するTimerトリガーを実装する。
追加するのはSampleTimer/function.jsonhandler/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としてアクセスする形式ではないが、このような設定が必要。また、/SampleTimerSampleTimer/function.jsonnameと同じにしておく必要がある。

動作確認

確認方法は先程と同じ

go build main.go && func start

10秒ぐらいするとトリガーが発火される。

SampleTimerTrigger is executed!!

タイマートリガーのデプロイも同じでDockerイメージをpushして関数アプリを再起動してしばらくすると(2,3分で)Azure Portal上で確認できるようになる。

終わりに

サンプルが少なくて試行錯誤しながらだったので結構詰まった。特にHTTPトリガーのauthLevelのところは公式の手順ママにすすめると必ず引っかかる罠になっている。

Golangのスクリプトをただ実行したいだけなのにDockerイメージを作ってpushして、、、などと毎回やるのは結構面倒くさく、開発以外の箇所で時間が食われるのであんまりおすすめしないかも。たぶんGCPとかにはもっと楽に実行できるようなサービスがあるはず。たぶん。