1923

都内と業界の隅っこで生活しているエンジニアのノート

Microsoft Graph チュートリアル を Go で試してみる

Microsoft Graph チュートリアル の、Microsoft Graph で .NET Core アプリを構築する と似たような感じの簡単なサンプルを Go で試してみます。

まだパブリックプレビューにはなっていませんが、Microsoft Authentication Library (MSAL) for Go がありますので、認証(デバイスコードフロー)はこちらを使用。予定表は REST API を使用します。

github.com

docs.microsoft.com

今回作成したサンプル(認証のキャッシュや mailbox settings 部分は省略)
Graph チュートリアル go sample

アプリ作成

Go コンソール アプリを作成。MSAL Go を使用するのでインストール。

go get -u github.com/AzureAD/microsoft-authentication-library-for-go/

ポータルでアプリを登録する

ポータルでアプリを登録するを参考にアプリを登録します。ページに記載されているようにアプリケーション (クライアント) ID の値を使用しますので、メモしておきます。

Azure AD 認証を追加する

環境変数に以下の値を設定します。 mailbox settingsは今回使いませんでしたので、SCOPESから削除しています。

APP_ID:アプリケーション (クライアント) ID
SCOPES:user.read,calendars.readwrite

main.go

appId := os.Getenv("APP_ID")
scopes := strings.Split(os.Getenv("SCOPES"), ",")

MSAL Go のサンプルを参考にデバイスコードフローを実装します。

graph.go

app, err := public.New(appId, public.WithAuthority("https://login.microsoftonline.com/common"))
if err != nil {
    panic(err)
}

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Second)
defer cancel()
devCode, err := app.AcquireTokenByDeviceCode(ctx, scopes)
if err != nil {
    panic(err)
}
fmt.Printf("Device Code is: %s\n", devCode.Result.Message)
result, err := devCode.AuthenticationResult(ctx)
if err != nil {
    panic(fmt.Sprintf("got error while waiting for user to input the device code: %s", err))
}

予定表ビューを取得する

Microsoft Graph をアプリケーションに組み込みます。このアプリケーションでは http.Client を使用して Microsoft Graph を呼び出します。

graph.go

t := time.Now().UTC()
t1 := t.AddDate(0, 0, int(t.Weekday())*-1)
t2 := t1.AddDate(0, 0, 7)
url := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/calendarview?startDateTime=%s&endDateTime=%s", t1.Format("2006-01-02T00:00:00Z"), t2.Format("2006-01-02T00:00:00Z"))
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", "Bearer "+g.token)

resp, err := g.cli.Do(req)
if err != nil {
    return err
}

新しいイベントを作成する

予定表ビューの取得と同様に http.Client を使用して Microsoft Graph を呼び出します。

graph.go

event := models.Event{}
event.Subject = subject
event.Start.DateTime = start.Format("2006-01-02T15:04:05Z")
event.Start.TimeZone = "UTC"
event.End.DateTime = end.Format("2006-01-02T15:04:05Z")
event.End.TimeZone = "UTC"
event.Body.Content = content
event.Body.ContentType = "Text"
event.Attendees = []models.Attendee{}
j, _ := json.Marshal(event)
req, _ := http.NewRequest("POST", "https://graph.microsoft.com/v1.0/me/events", bytes.NewBuffer(j))
req.Header.Set("Authorization", "Bearer "+g.token)
req.Header.Set("Content-Type", "application/json")
resp, err := g.cli.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()

終わり

これで、デバイスコードフローで認証し、予定表ビューの取得、イベントを作成する Go アプリ(Graph チュートリアル go sample)は完成です。

Microsoft Graph チュートリアル 的なものを Go で試すことができました。

f:id:taka1923:20210609190333p:plain

Azure Functions カスタム ハンドラー + Golang で Event Grid トリガーを試してみる

Azure Functions custom handler in Go に EventGridTrigger がなかったので試してみました。

カスタム ハンドラーの説明、その他のサンプルは以下にありますので、設定とコードのみざっくりと。

Event Grid トリガーでQueue Storage 出力の function.json です。

function.json

{
  "bindings":[
    {
      "type":"eventGridTrigger",
      "name":"eventGridEvent",
      "direction":"in"
    },
    {
      "name": "$return",
      "type": "queue",
      "direction": "out",
      "queueName": "test-output-node",
      "connection": "AzureWebJobsStorage"
    }
  ]
}
Name Trigger Input Output
eventGridTrigger Event Grid Event Grid Queue Storage

イベントを受信する部分は、GoCustomHandlers.goとちょっと変えて、ginを使ってみます。

main.go

package main

import (
    "encoding/json"
    "fmt"
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "os"
)

func main() {
    customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
    if !exists {
        customHandlerPort = "7071"
    }
    fmt.Println("FUNCTIONS_CUSTOMHANDLER_PORT: " + customHandlerPort)

    r := gin.Default()
    r.GET("/", home)
    r.POST("/EventGridTrigger", eventGridTriggerHandler)
    r.Run(":" + customHandlerPort)
}

func home(ctx *gin.Context) {
    ctx.JSON(http.StatusOK, nil)
}

func  eventGridTriggerHandler(ctx *gin.Context) {
    var req EventMessage
    if err := ctx.ShouldBindJSON(&req); err != nil {
        log.Fatalf(err.Error())
        return
    }

    var me EventGridData
    if val1, ok := req.Data["eventGridEvent"]; ok {
        jsonb, err := json.Marshal(val1)
        if err != nil {
            log.Fatalf(err.Error())
            return
        }
        if err := json.Unmarshal(jsonb, &me); err != nil {
            log.Fatalf(err.Error())
            return
        }
    }

    returnValue := HTTPResponse {}
    returnValue.Outputs.Res.Body = "OK"
    returnValue.Outputs.Res.StatusCode = "200"
    returnValue.ReturnValue = me.Data

    ctx.JSON(http.StatusOK, returnValue)
}
type HTTPResponse struct {
    Outputs struct {
        Res struct {
            Body       string `json:"body"`
            StatusCode string `json:"statusCode"`
        }                   `json:"res"`
    }
    Logs        []string
    ReturnValue interface{}
}

type EventMessage struct {
    Data map[string]interface{}
    Metadata map[string]interface{}
}

type EventGridData struct {
    Id string                 `json:"id"`
    Topic string             `json:"topic"`
    Subject string          `json:"subject"`
    Data struct {
        UserId int64            `json:"userId"`
        Name string                 `json:"name"`
    }                        `json:"data"`
    EventType string          `json:"eventType"`
    EventTime string          `json:"eventTime"`
    MetadataVersion string  `json:"metadataVersion"`
    DataVersion string      `json:"dataVersion"`
}

あとは、Azure上に必要なリソース作成してデプロイ。適当なカスタムトピック(Azure Event Grid イベント スキーマ)を送信して動作しましたので、お試しは完了です。

func SendTopic(url string) error {
    key1 := "****"
    msg := [1]Topic{}

    uid, err := uuid.NewRandom()
    if err != nil {
        log.Fatalf(err.Error())
    }
    msg[0].Id = uid.String()
    msg[0].Subject = "app/sample/test1"
    msg[0].EventType = "inserted"
    n := time.Now()
    msg[0].EventTime = n.Format(time.RFC3339)
    msg[0].Data.UserId = 1
    msg[0].Data.Name = "Example"
    msg[0].DataVersion = "1.0"
    json, _ := json.Marshal(msg)

    req, _ := http.NewRequest("POST", url, bytes.NewBuffer(json))
    req.Header.Set("aeg-sas-key", key1)
    client := new(http.Client)
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer func() {
        if resp.Body != nil {
            resp.Body.Close()
        }
    }()

    contents, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(contents))
    fmt.Println(err)
    if err != nil {
        return err
    }
    return err
}

Golang + SQLBoiler で Azure SQL Database を使ってみる

GoのORM、SQLBoilerを紹介して頂いたのでSQL Databaseで試してみます。 試した環境がAzure+SQL Databaseですが、特にローカルなどでSQL Serverを使った場合と同じかと思います。

Quickstart: Create an Azure SQL Database single databaseを参考にSQL Databaseとサンプルデータベースを作成して、Firewallの設定は完了している状態です。

今回作成したサンプルコードはこちらとなります。

準備

インストール

プロジェクトを作成して、modファイルの作成とsqlboilerに書かれている通りsqlboilerをインストール。

go mod init golang-azure-sqldb-sqlboiler
go get github.com/volatiletech/sqlboiler/v4
go get github.com/volatiletech/null/v8

設定ファイル作成

SQL Databaseの接続情報を記載した設定ファイルを作成します。今回は出力場所を設定ファイルに記載しています。

[sqlboiler.toml]

output   = "./common/models"

[mssql]
dbname  = "sample1"
host    = "*****.database.windows.net"
port    = 1433
user    = "*****"
pass    = "*****"
sslmode = "true"
schema  = "SalesLT"

モデル作成

sqlboiler mssql

ファイルが作成されましたので、これで準備完了です。

f:id:taka1923:20210220122948p:plain

プログラム作成

試しに Product テーブルの CRUD をする API を作成してみます。

DBへの接続はgo-mssqldb/The connection string can be specified in one of three formatsに記載されているフォーマットとなります。今回はドライバーのMSSQLBuildQueryStringを使用して接続文字列を作成しています。

[config.yaml]

database:
    dbname: "****"
    host: "****"
    port: 1433
    user: "****"
    password: "****"
    sslMode: "****"
    schema: "****"

[cmd/main.go]

   conn  := driver.MSSQLBuildQueryString(
        c.Database.User,
        c.Database.Password,
        c.Database.Dbname,
        c.Database.Host,
        c.Database.Port,
        c.Database.SslMode)

[common/database/db.go]

func NewDB(conn string) (*sql.DB, func(), error) {
    db, err := sql.Open("mssql", conn)
    if err != nil {
        return nil, nil, err
    }

    cleanup := func() {
        if err := db.Close(); err != nil {
            log.Print(err)
        }
    }
    return db, cleanup, nil
}

接続すれば、sqlboilerに記載のように、 Findinsertupdatedeleteなどが可能です。今回のサンプルでも、以下のように簡単に実装しています。

[app/product_service.go]

Insert

  if err := prod.Insert(context.Background(), svc.db, boil.Blacklist(productBlacklist...));err != nil {
        return nil, err
    }

Update

  if _, err := prod.Update(context.Background(), svc.db, boil.Blacklist(productBlacklist...)); err != nil {
        return nil, err
    }

Delete

  if _, err := prod.Delete(context.Background(), svc.db); err != nil {
        return err
    }

Find

  prod, err := models.FindProduct(context.Background(), svc.db, id)
    if err != nil {
        return nil, err
    }

JOINもQuery Mod を指定して簡単にできます。例えば以下のようなSQLを発行する場合

SELECT Product.* 
FROM [SalesLT].[Product] INNER JOIN SalesLT.ProductCategory AS c 
    on Product.ProductCategoryID = c.ProductCategoryID 
WHERE (c.Name = @p1);

以下のようなコードになります。

  prods, err := models.Products(
        qm.Select("Product.*"),
        qm.InnerJoin("SalesLT.ProductCategory AS c on Product.ProductCategoryID = c.ProductCategoryID"),
        qm.Where("c.Name = ?", "Road Bikes"),
    ).All(context.Background(), svc.db)

また、生クエリを書くこともできますので同じ処理は以下のようにも書けます。

  var prods []models.Product
    if err := queries.Raw(`
        SELECT Product.* 
        FROM [SalesLT].[Product] INNER JOIN SalesLT.ProductCategory AS c 
            on Product.ProductCategoryID = c.ProductCategoryID 
        WHERE (c.Name = ?);`,
        categoryName,
    ).Bind(context.Background(), svc.db, &prods); err != nil {
        return nil, err
    }

そのほか、Eager Loading は Load を使用します。例えば先ほどの例にLoadを追加して以下とした場合、

  prods, err := models.Products(
        qm.Load(models.ProductRels.ProductIDSalesOrderDetails),
        qm.Select("Product.*"),
        qm.InnerJoin("SalesLT.ProductCategory AS c on Product.ProductCategoryID = c.ProductCategoryID"),
        qm.Where("c.Name = ?", categoryName),
    ).All(context.Background(), svc.db)

以下のようなクエリで関連するSalesOrderDetailを一括で取得します。

SELECT * 
FROM [SalesLT].[SalesOrderDetail] 
WHERE ([SalesLT].[SalesOrderDetail].[ProductID] IN (@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,@p11,@p12,@p13,@p14,@p15,@p16,@p17,@p18,@p19,@p20,@p21,@p22,@p23,@p24,@p25,@p26,@p27,@p28,@p29,@p30,@p31,@p32,@p33,@p34,@p35,@p36,@p37,@p38,@p39,@p40,@p41,@p42,@p43));

Goでデータベースファーストな開発をする場合、SQLBoilerは便利で使いやすそうです。Go + SQL Database でもうちょっと試していきたいと思います。 まだまだGoもAzureもわからない事が多くお勉強中の日々です。

Azure Container Instances のGoアプリから Fluentd 経由で Azure Monitor にログ出力

Azure Container Instances(ACI)で動いているGoアプリのログをAzure Monitorに送信してみます。今回はGoアプリから直接送らずに同じコンテナグループ内のFluentd経由でAzure Monitorにログを転送しています

アプリの準備

fluent-logger-golangを使って、GoアプリからFluentdへのログを転送します。README.mdに書かれている通りに使ってみます。

package main

import (
    "fmt"
    "github.com/fluent/fluent-logger-golang/fluent"
    "github.com/gin-gonic/gin"
)

func main() {
    logger, err := fluent.New(fluent.Config{})
    if err != nil {
        fmt.Println(err)
    }
    defer logger.Close()
    tag := "azure-loganalytics.access"

    router := gin.Default()
    router.LoadHTMLGlob("templates/*.html")
    router.GET("/", func(ctx *gin.Context){
        message := "Hello World"
        var data = map[string]string{
            "message": message,
        }
        if err := logger.Post(tag, data); err != nil {
            panic(err)
        }
        ctx.HTML(200, "index.html", gin.H{
            "message": message,
        })
    })
    router.Run()
}

fluentdの準備

fluent-plugin-azure-loganalyticsをインストールしたコンテナを準備します。こちらもREADME.mdに書かれている通りです。

fluent.conf

<source>
  @type  forward
  @id    input1
  port  24224
  tag azure-loganalytics.access
</source>

<match azure-loganalytics.**>
    @type azure-loganalytics
    customer_id XXX
    shared_key XXX
    log_type ApplicationLog
</match>

Dockerfile

FROM fluent/fluentd:v1.12-1

USER root

RUN apk add --no-cache --update --virtual .build-deps \
 sudo build-base ruby-dev \
 # cutomize following instruction as you wish
 && sudo gem install fluent-plugin-azure-loganalytics \
 && sudo gem sources --clear-all \
 && apk del .build-deps \
 && rm -rf /home/fluent/.gem/ruby/2.5.0/cache/*.gem

COPY fluent.conf /fluentd/etc/
COPY entrypoint.sh /bin/

USER fluent

実行

準備完了。適当にローカルで動かして問題なさそうでしたので、Azure Container Registry(ACR)にイメージをプッシュして、ACI でアプリケーションを起動。

docker compose up -f docker-compose.aci.yml

docker-compose.aci.yml

version: '3'
services:
  web:
    image: <ACRNAME>.azurecr.io/fluent-go
    ports:
      - 8080:8080
  fluentd:
    image: <ACRNAME>.azurecr.io/fluent-loganalytics
    ports:
      - "24224:24224"

出力出来ました。

f:id:taka1923:20210117133325p:plain

ACI以外でGoアプリからAzure Monitorへのログ送信する場合は、Web Apps for ContainerはAzure Monitor にログを送信する (プレビュー)。Virtual MachinesはLogging Driverの方がシンプルですね。

Azure Functions カスタム ハンドラー + Golang でマネージド ID を使用して Azure Database for MySQL に接続する

Azure Functions のカスタム ハンドラー の Timer trigger で Golang のプログラムを実行。 マネージド IDを使用して、Azure Database for MySQL のデータベースへの接続を試してみます。

Function App と MySQL のデータベースは作成済み。MySQLファイアウォールの設定が終わっている程度の状態です。

準備

マネージドID作成して、Function Appに割り当て
az identity create --resource-group TESTRG  --name TESTIDENTITY
$resourceID=$(az identity show --resource-group TESTRG --name TESTIDENTITY --query id --output tsv)
az functionapp identity assign --resource-group TESTRG --name TESTFUNC --identities $resourceID

MySQLユーザー作成時にクライアントIDを使うのでメモ

az identity show --resource-group Testing --name testscgo1220a1Identity --query clientId --output tsv
MySQL サーバー に Active Directory 管理者を設定

ポータルから Azure Database for MySQL サーバーの [Active Directory 管理者] > [管理者の設定] で設定できます。

Azure CLI で以下のコマンドで実行したときは結構時間がかかり、ポータルの方が早かった感じです。

$objectID=$(az ad user list --display-name "DISPLAYNAME" --query [].objectId --output tsv)
az mysql server ad-admin create --server-name TESTMYSQLSRV --resource-group TESTRG --display-name "DISPLAYNAME" --object-id $objectID

Command group 'mysql server ad-admin' is in preview. It may be changed/removed in a future release.

マネージド ID の MySQL ユーザーを作成してデータベースへの権限を設定

上記で設定した Azure AD 管理者ユーザーで MySQL データベースに接続し、先ほど作成したマネージド ID の MySQL ユーザー(user1)を作成します。ついでにデータベースの権限も適当に設定しておきます。

SET aad_auth_validate_oids_in_tenant = OFF;
CREATE AADUSER 'user1' IDENTIFIED BY 'クライアントID';
GRANT SELECT,INSERT,UPDATE,DELETE ON TESTMYSQLDB.* TO user1@'%'

MySQL Workbench で接続するときは、Advanced の Enable Cleartext Authentication Plugin にチェックします。パスワードに使用するトークンは以下で取得。

az account get-access-token --resource-type oss-rdbms

実装

helloフォルダを作成して、Timer Trigger の設定を記述した function.jsonを入れておけば、設定した時間(今回は毎時30分)に、"/hello" が呼ばれますので、その中に処理を記載します。

MySQLの接続は、Go を使用してトークンを取得するを参考にローカル トークン サービスからMySQLのアクセストークンを取得して、MySQLのパスワードに設定しています。アクセストークンの有効期限については無配慮です。

https://docs.microsoft.com/en-us/azure/mysql/howto-configure-ssl

hello/function.json

{
  "bindings": [
    {
      "schedule": "0 30 * * * *",
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in"
    }
  ]
}

main.go

package main

import (
    "crypto/tls"
    "crypto/x509"
    "encoding/json"
    "errors"
    "fmt"
    "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
    "os"
    "time"
)
type InvokeResponse struct {
    Outputs     map[string]interface{}
    Logs        []string
    ReturnValue interface{}
}
type ReturnValue struct {
    Data string
}
type Hello struct {
    gorm.Model
    Value  string
    DateTime  time.Time
}
type responseJson struct {
    AccessToken string `json:"access_token"`
    RefreshToken string `json:"refresh_token"`
    ExpiresIn string `json:"expires_in"`
    ExpiresOn string `json:"expires_on"`
    NotBefore string `json:"not_before"`
    Resource string `json:"resource"`
    TokenType string `json:"token_type"`
}

func main() {
    customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
    if exists {
        fmt.Println("FUNCTIONS_CUSTOMHANDLER_PORT: " + customHandlerPort)
    }

    mux := http.NewServeMux()
    mux.HandleFunc("/hello", helloHandler)
    fmt.Println("Go server Listening...on FUNCTIONS_CUSTOMHANDLER_PORT:", customHandlerPort)
    log.Fatal(http.ListenAndServe(":"+customHandlerPort, mux))
}

// Timer Trigger Event
func helloHandler(w http.ResponseWriter, r *http.Request) {
    db, err := sqlConnect()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer db.Close()

    user := Hello{ Value: "Hello", DateTime : time.Now() }
    db.NewRecord(user)
    db.Create(&user)

    returnValue := ReturnValue{Data: "return val"}
    outputs := make(map[string]interface{})
    outputs["output1"] = "output1"
    invokeResponse := InvokeResponse{outputs,  []string{ "log1" }, returnValue}
    json, err := json.Marshal(invokeResponse)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    w.Write(json)
}

func sqlConnect() (database *gorm.DB, err error) {
    json, err := getToken()
    if err != nil {
        return nil, err
    }

    fmt.Println("sqlConnect...")
    DBMS := "mysql"
    USER := "user1@TESTMYSQLSRV"
    PASS := json.AccessToken
    DB := "tcp(TESTMYSQLSRV.mysql.database.azure.com:3306)"
    DBNAME := "TESTMYSQLDB"
    CONNECT := fmt.Sprintf("%s:%s@%s/%s?tls=custom&parseTime=true&allowCleartextPasswords=1",USER , PASS, DB, DBNAME)

    rootCertPool := x509.NewCertPool()
    pem, err := ioutil.ReadFile("./BaltimoreCyberTrustRoot.crt.pem")
    if err != nil {
        return nil, err
    }
    if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
        return nil, errors.New(fmt.Sprint("Failed to append PEM."))
    }
    mysql.RegisterTLSConfig("custom", &tls.Config{RootCAs: rootCertPool})

    db, err := gorm.Open(DBMS, CONNECT)
    if err != nil {
        return nil, err
    }
    fmt.Println("db connected: ", &db)
    return db, nil
}

// トークン取得
func getToken() (responseJson, error) {
    var r responseJson

    // Create HTTP request for a managed services for Azure resources token to access Azure Resource Manager
    vaultURL := fmt.Sprintf("%s?resource=%s&api-version=2019-08-01&client_id=%s",
        os.Getenv("IDENTITY_ENDPOINT"),
        "https://ossrdbms-aad.database.windows.net",
        "クライアントID")
    msiEndpoint, err := url.Parse(vaultURL)
    if err != nil {
        return r, err
    }

    req, err := http.NewRequest("GET", msiEndpoint.String(), nil)
    if err != nil {
        return r, err
    }
    req.Header.Add("X-IDENTITY-HEADER", os.Getenv("IDENTITY_HEADER"))

    // Call managed services for Azure resources token endpoint
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil{
        return r, err
    }

    // Pull out response body
    responseBytes, err := ioutil.ReadAll(resp.Body)
    defer resp.Body.Close()
    if err != nil {
        return r, err
    }
    if resp.StatusCode >= 400 {
        return r, fmt.Errorf("bad response status code %d", resp.StatusCode)
    }

    // Unmarshall response body into struct
    err = json.Unmarshal(responseBytes, &r)
    if err != nil {
        return r, err
    }
    return r, nil
}

参考

Azure Functions カスタム コンテナーで Golang を使う

Azure Functions のカスタム ハンドラーが GA です。カスタム ハンドラーを使うと Golang でもで関数アプリを実装することができます。

ですが・・・今回は、Azure Functions カスタム コンテナーです。Azure Functions Core Tools 使って function app と Dockerfile を作成し、Golang で作ったアプリを簡単に動かすことができますので試してみます。

Azure Functions Core Tools (試したのは 3.0.3160 )でプロジェクトを作成します。

func init go-func-app1 --worker-runtime custom --docker

関数と同じ名前のサブフォルダー(Hello)を作成して、構成ファイル function.json を追加します。シンプルなHTTPトリガーです。

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

関数を実装します。 /Hello にHTTPリクエストが来たら JSONを返すだけのコードです。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    customHandlerPort, exists := os.LookupEnv("FUNCTIONS_CUSTOMHANDLER_PORT")
    if exists {
        fmt.Println("FUNCTIONS_CUSTOMHANDLER_PORT: " + customHandlerPort)
    }
    mux := http.NewServeMux()
    mux.HandleFunc("/Hello", helloHandler)
    fmt.Println("Go server Listening...on FUNCTIONS_CUSTOMHANDLER_PORT:", customHandlerPort)
    log.Fatal(http.ListenAndServe(":"+customHandlerPort, mux))
}
type InvokeResponse struct {
    Outputs     map[string]interface{}
    Logs        []string
    ReturnValue interface{}
}
type user struct {
    Name string
    Address  string
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
    outputs := make(map[string]interface{})
    resData := make(map[string]interface{})
    resData["body"] = user{
        Name: "Taka",
        Address:  "Tokyo",
    }
    outputs["res"] = resData
    invokeResponse := InvokeResponse{outputs, nil, nil}
    responseJson, _ := json.Marshal(invokeResponse)
    w.Header().Set("Content-Type", "application/json")
    w.Write(responseJson)
}

ファイル構成はこんな感じです。

f:id:taka1923:20201210223411p:plain

host.jsonのdefaultExecutablePathにビルドで作成される main を指定します。

 "defaultExecutablePath": "main",

Dockerfile に Go のビルドとコピーなどを追加して準備は終わりです。

FROM golang:latest
COPY . /go/app
WORKDIR /go/app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

FROM mcr.microsoft.com/azure-functions/dotnet:3.0-appservice
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

COPY --from=0 /go/app/ /home/site/wwwroot

ビルドしてローカルで確認してみます。

docker build -t go-func-app1:latest .
docker run -p 8080:80 -it go-func-app1:latest .

f:id:taka1923:20201210223433p:plain

あとは、 Azure Container Registrなどに Push して functions に設定すれば完了です。

f:id:taka1923:20201210223252p:plain

Golang で Azure Blob Storage

Go 始めてみようと Tour of Go を終えた後、Azure Blob Storage を使った簡単なサンプルWebサイトを作った備忘録です。 Webフレームワークは最初に見かけた Gin を使って、Blobの操作は クイック スタート: Go を使用して BLOB をアップロード、ダウンロード、および一覧表示する を参考にしています。

作ったサンプルから、エラー処理などを省いてBLOB操作部分を抜粋すると以下な感じです。

main.go

一覧

   for marker := (azblob.Marker{}); marker.NotDone(); {
        listBlob, err := containerURL.ListBlobsFlatSegment(ctx, marker, azblob.ListBlobsSegmentOptions{})
        if err != nil {
            ctx.Error(err).SetType(gin.ErrorTypePublic)
            return
        }
        marker = listBlob.NextMarker

        for _, blobInfo := range listBlob.Segment.BlobItems {
            items = append(items, blobItem{
                Name: blobInfo.Name,
                Size: *blobInfo.Properties.ContentLength,
            })
        }
    }

アップロード

    formFiles := form.File["file1"]
    formFile := formFiles[0]
    fileName := formFile.Filename
    blobURL := containerURL.NewBlockBlobURL(fileName)

    file, err := formFile.Open()
    defer file.Close()
    if err != nil {
        ctx.Error(err).SetType(gin.ErrorTypePublic)
        return
    }
    buf := bytes.NewBuffer(nil)
    if _, err := io.Copy(buf, file); err != nil {
        ctx.Error(err).SetType(gin.ErrorTypePublic)
        return
    }

    _, err = azblob.UploadBufferToBlockBlob(ctx, buf.Bytes(), blobURL, azblob.UploadToBlockBlobOptions{
        BlockSize:   4 * 1024 * 1024,
        Parallelism: 16})

削除

   _, err := blobURL.Delete(ctx, azblob.DeleteSnapshotsOptionNone, azblob.BlobAccessConditions{})

Web App for Containers にデプロイして動作確認。

f:id:taka1923:20201130223112p:plain

DockerFile

FROM golang:latest
RUN go get -u github.com/Azure/azure-storage-blob-go/azblob && \
  go get -u github.com/gin-gonic/gin
COPY . /go/app
WORKDIR /go/app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/app/ .
CMD ["./main"]