#author("2020-07-29T10:59:59+00:00","","") #mynavi(Azureメモ) #setlinebreak(on); * 概要 [#q629b743] #html(<div class="pl10">) VMにアクセスする Azure Functions の開発手順を記載する。 #html(</div>) * 目次 [#uf1ea711] #contents - 関連 -- [[Azureメモ]] - 参考 -- [[Premium プランの関数アプリを作成する:https://docs.microsoft.com/ja-jp/azure/azure-functions/scripts/functions-cli-create-premium-plan]] -- [[Azure Functions の Premium プラン:https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-premium-plan]] -- [[サンプルスクリプト:https://docs.microsoft.com/ja-jp/azure/azure-functions/scripts/functions-cli-create-premium-plan#sample-script]] -- [[Azure Functions のデプロイ テクノロジ:https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-deployment-technologies]] -- [[Azure Functions の zip デプロイ:https://docs.microsoft.com/ja-jp/azure/azure-functions/deployment-zip-push]] -- [[チュートリアル: Functions を Azure 仮想ネットワークに統合する:https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-create-vnet]] -- [[アプリを Azure 仮想ネットワークと統合する:https://docs.microsoft.com/ja-jp/azure/app-service/web-sites-integrate-with-vnet]] -- [[Azure Functions のネットワーク オプション:https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-networking-options]] * 作成する関数の仕様 [#i6e368a7] #html(<div class="pl10">) 以降の手順で作成する関数の仕様 及び リソースイメージは以下の通り。 - Blobストレージへのファイルアップロードをトリガーに起動する関数とする。 - 関数はVMで稼働する InfluxDB にサンプルデータを登録する。(ファイル内容に関係なく...) #TODO(絵) #html(</div>) * 作成するファイル [#h684fc8d] #html(<div class="pl10">) 以降の手順で作成する全ファイルは以下の通り #html(){{ <div style="padding: 0px 10px 10px 10px; border: 1px solid #333;"> <pre style="display: inline-block; margin: 0; padding-right: 10px; font-size: 1rem; background: transparent; border: 0px; vertical-align: top;"> . ├── 0_env.sh ├── 1_create.sh ├── 2_deploy.sh ├── 9_remove.sh ├── functions │   ├── BlobTrigger │   │   └── function.json │   ├── go_server_sample.exe │   ├── go_server_sample.go │   ├── host.json │   └── local.settings.json ├── pem │   ├── id_rsa │   └── id_rsa.pub </pre> <pre style="display: inline-block; margin: 0; font-size: 1rem; background: transparent; border: 0px; vertical-align: top;"> ... リソース名の定義など ... リソース作成用のシェル ... 関数アプリのデプロイ用シェル ... リソース一括削除用のシェル ... 内部関数名(トリガー名) ... 関数のトリガー定義 ... カスタムハンドラー(実行可能ファイル) ... カスタムハンドラー(ソース) ... 関数アプリの定義ファイル ... ローカル実行用の設定ファイル ... 作成するVMのSSH鍵の退避先 </pre> </div> }} #html(</div>) * リソース作成/デプロイ用のシェル作成 [#z256443a] #html(<div class="pl10">) リソース作成用のシェルを以下の通り作成する。 0_env.sh #mycode2(){{ #!/bin/bash # 全てのリソース名に付与する接頭文字 (Storageアカウント名などは世界でユニークな必要があるので他ユーザと被らないような名前を付ける) PREFIX=XXXXXX # サブスクリプションID subscriptionId=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # リージョン region=japanwest insightsRegion=japaneast # 2020/7時点では Appplication Insights で西日本(japanwest)は使用できない # リソースグループ名 # ( az group delete --name $resourceGroup で一括削除可能 ) resourceGroup=${PREFIX}ResourceGroup # ストレージアカウント名 storageName=${PREFIX}straccount storageSku=Standard_LRS # Storageコンテナ名 storageContainerIn=${PREFIX}-strcontainer-i storageContainerOut=${PREFIX}-strcontainer-o # 仮想ネットワーク名 vnetName=${PREFIX}VNet vnetPrefix=10.1.0.0/16 # ネットワークセキュリティグループ nsgName=${vnetName}SecGrp nsgPubRuleName=${nsgName}PubRule nsgPubInboundPort="22" #nsgPubInboundPort="443" nsgPriRuleName=${nsgName}PriRule nsgPriInboundPort="8086" # VM用のサブネット vmSubnetName=${PREFIX}VmSubnet vmSubnetPrefix=10.1.1.0/24 # 関数アプリ用のサブネット funcSubnetName=${PREFIX}FuncSubnet funcSubnetPrefix=10.1.2.0/24 # 仮想マシン vmName=${PREFIX}Vm vmImage=UbuntuLTS # "az vm image list -o table" で利用可能なイメージの一覧を確認可能 vmIpAddress=10.1.1.5 vmUser=sample # 関数アプリ名 funcAppName=${PREFIX}FuncApp # 使用するFunctionsのバージョン funcVersion=2 # 関数アプリのプラン名 funcPlanName=${PREFIX}premiumplan funcPlanSku=EP1 # Application insights insightsName=${PREFIX}Insights insightsDays=30 if [ "$1" == "-p" ]; then echo "region : $region" echo "resourceGroup : $resourceGroup" echo "storageName : $storageName" echo "storageSku : $storageSku" echo "storageContainerIn : $storageContainerIn" echo "storageContainerOut : $storageContainerOut" echo "vnetName : $vnetName" echo "vnetPrefix : $vnetPrefix" echo "nsgName : $nsgName" echo "nsgPubRuleName : $nsgPubRuleName" echo "nsgPubInboundPort : $nsgPubInboundPort" echo "nsgPriRuleName : $nsgPriRuleName" echo "nsgPriInboundPort : $nsgPriInboundPort" echo "vmSubnetName : $vmSubnetName" echo "vmSubnetPrefix : $vmSubnetPrefix" echo "vmName : $vmName" echo "vmImage : $vmImage" echo "vmIpAddress : $vmIpAddress" echo "vmUser : $vmUser" echo "funcAppName : $funcAppName" echo "funcVersion : $funcVersion" echo "funcPlanName : $funcPlanName" echo "funcPlanSku : $funcPlanSku" echo "funcSubnetName : $funcSubnetName" echo "funcSubnetPrefix : $funcSubnetPrefix" echo "insightsName : $insightsName" echo "insightsDays : $insightsDays" echo "insightsRegion : $insightsRegion" fi }} 1_create.sh #mycode2(){{ #!/bin/bash source ./0_env.sh # リソースグループの作成 echo az group create az group create \ --name $resourceGroup \ --location $region # ストレージアカウントの作成 echo az storage account create az storage account create \ --name $storageName \ --location $region \ --resource-group $resourceGroup \ --sku $storageSku # Storageコンテナの作成 echo "az storage container create(in)" az storage container create \ --name $storageContainerIn \ --resource-group $resourceGroup \ --account-name $storageName echo "az storage container create(out)" az storage container create \ --name $storageContainerOut \ --resource-group $resourceGroup \ --account-name $storageName # NSG(ネットワークセキュリティグループの)作成 echo az network nsg create az network nsg create \ --resource-group $resourceGroup \ --name $nsgName # NSGルール(Public) echo az network nsg rule create az network nsg rule create \ --resource-group $resourceGroup \ --nsg-name $nsgName \ --name $nsgPubRuleName \ --access Allow \ --protocol Tcp \ --direction Inbound \ --priority 100 \ --source-address-prefix Internet \ --source-port-range "*" \ --destination-port-range $nsgPubInboundPort # NSGルール(Private) # TODO: 同一ネットワーク内であれば不要 #echo az network nsg rule create #az network nsg rule create \ # --resource-group $resourceGroup \ # --nsg-name $nsgName \ # --name $nsgPriRuleName \ # --access Allow \ # --protocol Tcp \ # --direction Inbound \ # --priority 110 \ # --source-address-prefix VirtualNetwork \ # --source-port-range "*" \ # --destination-port-range $nsgPriInboundPort # 仮想ネットワーク 及び サブネット作成 echo az network vnet create az network vnet create \ --name $vnetName \ --resource-group $resourceGroup \ --address-prefixes $vnetPrefix \ --subnet-name $vmSubnetName \ --subnet-prefixes $vmSubnetPrefix \ --network-security-group $nsgName # 仮想マシンの作成 # TODO: 管理用の踏み台サーバは別で用意する rm -rf ~/.ssh/id_rsa rm -rf ~/.ssh/id_rsa.pub echo az vm create az vm create \ --resource-group $resourceGroup \ --name $vmName \ --image $vmImage \ --generate-ssh-keys \ --subnet $vmSubnetName \ --vnet-name $vnetName \ --private-ip-address $vmIpAddress \ --admin-username $vmUser \ --output json \ --verbose # --public-ip-address "" # TODO: 踏み台以外はPublicアクセスなしにする # 生成されたSSH鍵を移動( --ssh-dest-key-path が効かない為 ) # ※ https://docs.microsoft.com/en-us/cli/azure/vm?view=azure-cli-latest#az-vm-create rm -rf pem mkdir -p pem if [ -e ~/.ssh/id_rsa ]; then mv ~/.ssh/id_rsa ./pem/ mv ~/.ssh/id_rsa.pub ./pem/ fi # InfluxDB用のポート開放 echo az vm open-port az vm open-port \ --resource-group $resourceGroup \ --name $vmName \ --port 8086 # Application Insights 拡張が利用できない場合は追加インストール x=`az monitor app-insights --help 2>&1` if [ "$?" != "0" ]; then az extension add -n application-insights fi # Application Insights コンポーネント作成 echo az monitor app-insights component create az monitor app-insights component create \ --app $insightsName \ --location $insightsRegion \ --resource-group $resourceGroup \ --query-access Enabled \ --retention-time $insightsDays \ --subscription $subscriptionId # プレミアムプランの作成 echo az functionapp plan create az functionapp plan create \ --name $funcPlanName \ --resource-group $resourceGroup \ --location $region \ --sku $funcPlanSku # 関数アプリの作成 echo az functionapp create az functionapp create \ --name $funcAppName \ --storage-account $storageName \ --plan $funcPlanName \ --resource-group $resourceGroup \ --functions-version $funcVersion \ --app-insights $insightsName # 関数アプリのVNet統合用のサブネット echo az network vnet subnet create az network vnet subnet create \ --name $funcSubnetName \ --resource-group $resourceGroup \ --vnet-name $vnetName \ --address-prefixes $funcSubnetPrefix # 関数アプリのVNet統合(当コマンドはプレビュー版の為、将来で変更/削除される可能性あり) echo az functionapp vnet-integration add az functionapp vnet-integration add \ --name $funcAppName \ --resource-group $resourceGroup \ --vnet $vnetName \ --subnet $funcSubnetName }} 2_deploy_func.sh #mycode2(){{ #!/bin/bash source 0_env.sh rm -rf functions.zip exefile=`cat functions/host.json | grep defaultExecutablePath | awk '{print $2}' | sed 's/"//g'` cd functions rm -rf $exefile #GOOS=linux GOARCH=amd64 go build -o $exefile #GOOS=windows GOARCH=386 go build -o $exefile GOOS=windows GOARCH=amd64 go build -o $exefile zip -r ../functions.zip * cd ../ az functionapp deployment source config-zip -g $resourceGroup -n $funcAppName --src functions.zip }} #html(</div>) * リソースの作成 [#kef97978] #html(<div class="pl10">) #myterm2(){{ ./1_creaate.sh }} #html(</div>) * InfluxDBインストール [#x5d23908] #html(<div class="pl10">) 作成したVMのPublic IPを確認 #myterm2(){{ az vm list-ip-addresses -o table VirtualMachine PublicIPAddresses PrivateIPAddresses ---------------- ------------------- -------------------- XXXXXXXXX XX.XX.XXX.XX 10.1.1.5 }} VMにSSH接続 #myterm2(){{ source ./0_env.sh ssh -i ./pem/id_rsa $vmUser@確認したPublic IP }} 以降で VM に InfluxDB をインストールする。 ここでは docker を使用する為、先に docker からインストールを行う。 ** docker、docker-compose インストール [#n5400cc9] #html(<div class="pl10">) #TODO(az vm create の --custom-data オプションで指定する) https://docs.docker.com/engine/install/ubuntu/ https://docs.docker.com/compose/install/ dockerインストール #myterm2(){{ sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository \ "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) \ stable" sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io }} docker-compose インストール #myterm2(){{ sudo curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose }} #html(</div>) ** InfluxDBインストール [#u9a56dd9] #html(<div class="pl10">) VM上の任意のディレクトリに以下のファイルを作成する influxdb.conf #mycode2(){{ [meta] dir = "/var/lib/influxdb/meta" [data] dir = "/var/lib/influxdb/data" engine = "tsm1" wal-dir = "/var/lib/influxdb/wal" [http] enabled = true flux-enabled = true }} docker-compose.yml #mycode2(){{ version: "3" services: influxdb: image: influxdb:1.8 hostname: influxdb_sample container_name: influxdb_sample volumes: - ./influxdb:/var/lib/influxdb - ./influxdb.conf:/etc/influxdb/influxdb.conf ports: - 8086:8086 }} 作成したファイルを使用して InfluxDBをインストール/起動 #myterm2(){{ sudo docker-compose up -d }} InfluxDBにサンプルDB 及び ユーザを作成 #myterm2(){{ sudo docker exec -i influxdb_sample influx --execute "create database sampledb" sudo docker exec -i influxdb_sample influx --execute "create user sample with password 'sample' WITH ALL PRIVILEGES" }} #html(</div>) #html(</div>) * 関数アプリの作成 [#x09d7bdd] #html(<div class="pl10">) functions/host.json #mycode2(){{ { "version": "2.0", "httpWorker": { "description": { "defaultExecutablePath": "go_server_sample.exe" } }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[1.*, 2.0.0)" } } }} functions/go_server_sample.go #mycode2(){{ package main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "math/rand" "time" "github.com/influxdata/influxdb-client-go" ) type ReturnValue struct { Data string } type InvokeResponse struct { Outputs map[string]interface{} Logs []string ReturnValue interface{} } type InvokeRequest struct { Data map[string]interface{} Metadata map[string]interface{} } func blobTriggerHandler(w http.ResponseWriter, r *http.Request) { var invokeReq InvokeRequest d := json.NewDecoder(r.Body) decodeErr := d.Decode(&invokeReq) if decodeErr != nil { http.Error(w, decodeErr.Error(), http.StatusBadRequest) return } fmt.Println("The JSON data is:invokeReq metadata......") fmt.Println(invokeReq.Metadata) returnValue := invokeReq.Data["triggerBlob"] invokeResponse := InvokeResponse{Logs: []string{"test log1", "test log2"}, ReturnValue: returnValue} js, err := json.Marshal(invokeResponse) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // VM(InfluxDB)へのアクセス確認 hostName := "10.1.1.5" // TODO: 環境変数から取得 hostPort := 8086 userName := "sample" password := "sample" client := influxdb2.NewClient(fmt.Sprintf("http://%s:%d", hostName, hostPort), fmt.Sprintf("%s:%s",userName, password)) create(client) search(client) client.Close() w.Header().Set("Content-Type", "application/json") w.Write(js) } /** * サンプルデータ登録(InfluxDB). */ func create(client influxdb2.Client) { fmt.Printf("Write START\n") writeAPI := client.WriteAPIBlocking("", "sampledb/autogen") for i := 0; i < 5; i++ { p := influxdb2.NewPointWithMeasurement("sample"). AddTag("tag1", "sample1"). AddTag("tag2", fmt.Sprintf("row%d", i+1)). AddField("field1", i + 1). AddField("field2", rand.Float64()). SetTime(time.Now()) err := writeAPI.WritePoint(context.Background(), p) if err != nil { fmt.Printf("Write2 error: %s\n", err.Error()) } else { fmt.Printf("Write2 success.\n") } } fmt.Printf("Write END\n") } /** * サンプルデータ検索(InfluxDB). */ func search(client influxdb2.Client) { queryAPI := client.QueryAPI("") result, err := queryAPI.Query(context.Background(), `from(bucket:"sampledb")|> range(start: -1h) |> filter(fn: (r) => r._measurement == "sample" and r.tag1 == "sample1")`) if err == nil { rowCount := 0 for result.Next() { record := result.Record() values := record.Values() val := values["_value"] if result.TableChanged() { //fmt.Printf("%d列目\n", result.TablePosition() + 1) if (values["_field"] == "field1") { rowCount = rowCount + 1 fmt.Printf("%d行目\n", rowCount) fmt.Printf(" time : %s\n", values["_time"]) fmt.Printf(" tag1 : %s\n", values["tag1"]) fmt.Printf(" tag2 : %s\n", values["tag2"]) } } column := values["_field"] switch val := val.(type){ case int64: fmt.Printf(" %s : %d\n", column, val) case float64: fmt.Printf(" %s : %f\n", column, val) } //fmt.Printf("record: %s\n", record) } if result.Err() != nil { fmt.Printf("Query error: %s\n", result.Err().Error()) } } else { fmt.Printf("Query error: %s\n", err.Error()) } } func main() { httpInvokerPort, exists := os.LookupEnv("FUNCTIONS_HTTPWORKER_PORT") if exists { fmt.Println("FUNCTIONS_HTTPWORKER_PORT: " + httpInvokerPort) } mux := http.NewServeMux() mux.HandleFunc("/BlobTrigger", blobTriggerHandler) log.Println("Go server Listening...on httpInvokerPort:", httpInvokerPort) log.Fatal(http.ListenAndServe(":"+httpInvokerPort, mux)) }} functions/BlobTrigger/function.json #mycode2(){{ { "bindings" : [ { "type" : "blobTrigger", "direction" : "in", "name" : "triggerBlob", "path" : "作成したストレージコンテナ名1/{name}", "dataType" : "binary", "connection" : "AzureWebJobsStorage" }, { "type" : "blob", "direction" : "out", "name" : "$return", "path" : "作成したストレージコンテナ名2/{name}", "dataType" : "binary", "connection" : "AzureWebJobsStorage" } ] } }} #html(</div>) * 関数アプリのデプロイ [#v8360ab2] #html(<div class="pl10">) デプロイ #myterm2(){{ ./2_deploy_func.sh }} #html(</div>) * 動作確認 [#rcf31f7d] #html(<div class="pl10">) 接続文字列の確認 #myterm2(){{ source 0_env.sh az storage account show-connection-string --name $storageName { "connectionString": "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=xxxxxxxxxxx;AccountKey=xxxxxxxxxxxxxx" } }} ストレージエクスプローラにStorageアカウントをアタッチ #TODO(絵) ファイルをアップロード #TODO(絵) VMに接続して InfluxDB にデータが登録されているか確認 #myterm2(){{ ssh -i ./pem/id_rsa $vmUser@確認したPublic IP sudo docker exec -i influxdb_sample influx -database sampledb -username sample --execute "select * from sample" name: sample time field1 field2 tag1 tag2 ---- ------ ------ ---- ---- 1596015616248498800 1 0.6046602879796196 sample1 row1 1596015616436070000 2 0.9405090880450124 sample1 row2 1596015616451632700 4 0.4377141871869802 sample1 row4 1596015616451632700 3 0.6645600532184904 sample1 row3 1596015616467330200 5 0.4246374970712657 sample1 row5 }} #html(</div>)