概要

VMにアクセスする Azure Functions の開発手順を記載する。

目次

作成する関数の仕様

以降の手順で作成する関数の仕様 及び リソースイメージは以下の通り。

  • Blobストレージへのファイルアップロードをトリガーに起動する関数とする。
  • 関数はVMで稼働する InfluxDB にサンプルデータを登録する。(ファイル内容に関係なく...)

sample-azure-image.png

作成するファイル

以降の手順で作成する全ファイルは以下の通り

.
├── 0_env.sh
├── 1_create_resources.sh
├── 2_setup_influxdb.sh
├── 3_deploy_funcapp.sh
├── 9_remove_resources.sh
├── functions
│   ├── BlobTrigger
│   │   └── function.json
│   ├── go_server_sample.exe
│   ├── go_server_sample.go
│   ├── host.json
│   └── local.settings.json
├── pem
│   ├── id_rsa
│   └── id_rsa.pub
 
... リソース名の定義など
... リソース作成用のシェル
... VMのInfluxDBのセットアップ用シェル
... 関数アプリのデプロイ用シェル
... リソース一括削除用のシェル
 
... 内部関数名(トリガー名)
... 関数のトリガー定義
... カスタムハンドラー(実行可能ファイル)
... カスタムハンドラー(ソース)
... 関数アプリの定義ファイル
... ローカル実行用の設定ファイル
... 作成するVMのSSH鍵の退避先(VM作成時に自動生成)
 
 

リソース作成/デプロイ用のシェル作成

リソース作成用のシェルを以下の通り作成する。
当記事では全てCLIをシェル化したが、恐らく ARM(Azure Resource Manager)を使うのが正解。
https://azure.microsoft.com/ja-jp/features/resource-manager/

このシェルでは作成する各リソースの名前の定義を行っている。
※ -p 付きで実行すればコンソールへの表示も行うようにしている。

0_env.sh

#!/bin/bash

# 全てのリソース名に付与する接頭文字 (Storageアカウント名などは世界でユニークな必要があるので他ユーザと被らないような名前を付ける)
PREFIX=XXXXXX

# サブスクリプションID (Application Insight を使用しない場合は空)
subscriptionId=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

# リージョン
region=japanwest
insightsRegion=japaneast # 2020/7時点では Appplication Insights で西日本(japanwest)は使用できない

# リソースグループ名
# ( az group delete --name $resourceGroup で一括削除可能 )
resourceGroup=${PREFIX}ResourceGroup

# ストレージアカウント名
storageAccountName=${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 "storageAccountName  : $storageAccountName"
  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

このシェルでは各リソースを作成する。
※関数アプリは中身が空のアプリとして作成される。(create のみ行っている)

1_create_resources.sh

#!/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 $storageAccountName \
  --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 $storageAccountName
echo "az storage container create(out)"
az storage container create \
  --name $storageContainerOut \
  --resource-group $resourceGroup \
  --account-name $storageAccountName

# 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 \
  --custom-data 2_setup_influxdb.sh \
  --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
if [ "$subscriptionId" != "" ]; then
  az monitor app-insights component create \
      --app $insightsName \
      --location $insightsRegion \
      --resource-group $resourceGroup \
      --query-access Enabled \
      --retention-time $insightsDays \
      --subscription $subscriptionId
fi

# プレミアムプランの作成(プレミアムプランでないと関数アプリからのVNetアクセス機能は提供されない)
echo az functionapp plan create
az functionapp plan create \
  --name $funcPlanName \
  --resource-group $resourceGroup \
  --location $region \
  --sku $funcPlanSku

# 関数アプリの作成
echo az functionapp create
if [ "$subscriptionId" != "" ]; then
  az functionapp create \
    --name $funcAppName \
    --storage-account $storageAccountName \
    --plan $funcPlanName \
    --resource-group $resourceGroup \
    --functions-version $funcVersion \
    --app-insights $insightsName
else
  az functionapp create \
    --name $funcAppName \
    --storage-account $storageAccountName \
    --plan $funcPlanName \
    --resource-group $resourceGroup \
    --functions-version $funcVersion
fi

# 関数アプリの環境変数の設定
echo "az functionapp config appsettings"
az functionapp config appsettings set \
    --name $funcAppName \
    --resource-group $resourceGroup \
    --settings "DB_HOST=$vmIpAddress" "DB_PORT=8086" "DB_NAME=sampledb" "DB_USER=sample" "DB_PW=sample"

# 関数アプリの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

このシェルはVM作成時に実行されるVMの初期化スクリプト。
az vm create 時の --custom-data として指定している為、手動で実行する必要はない。
処理内容は InfluxDBのセットアップ。(docker を使用)

2_setup_influxdb.sh

#!/bin/bash

apt-get update
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 -

add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io

curl -L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose

mkdir /tmp/influx_docker && cd /tmp/influx_docker

cat <<_EOCONF_ >influxdb.conf
[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
_EOCONF_

cat <<_EOYML_>docker-compose.yml
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
_EOYML_

# start influxdb
docker-compose up -d

# wait until influxdb starts
while [ true ]; do
  sleep 10
  x=`sudo docker exec -i influxdb_sample influx --execute "show databases" 2>&1`
  if [ "$?" == "0" ]; then
    break
  fi  
done

# create sample database and sample user.
docker exec -i influxdb_sample influx --execute "create database sampledb"
docker exec -i influxdb_sample influx --execute "create user sample with password 'sample' WITH ALL PRIVILEGES"

このシェルでは関数アプリのビルド/デプロイを行う。
関数アプリのOSはデフォルトのWindowsを使用している為、Windowsm用にビルドしている。

3_deploy_funcapp.sh

#!/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

このシェルはリソースの一括削除用のシェル。
やっている事はリソースグループを指定して削除しているだけ。

9_remove_resources.sh

#!/bin/bash

source ./0_env.sh
az group delete --name $resourceGroup

リソースの作成

ログイン

az login

リソース作成

./1_create_resources.sh

リソースグループ配下に作成されたリソースをAzure Portal からみると下図の通り。

azure_resources.png

VM 上に InfluxDB が正しく作成されているか確認

作成したVMのPublic IPを確認

az vm list-ip-addresses -o table
VirtualMachine    PublicIPAddresses    PrivateIPAddresses
----------------  -------------------  --------------------
XXXXXXXXX          XX.XX.XXX.XX         10.1.1.5

VMにSSH接続

source ./0_env.sh
ssh -i ./pem/id_rsa $vmUser@確認したPublic IP

InfluxDBにサンプルDBが作成されているか確認 (作成に少し時間がかかるので数分待ってから確認する)

# dockerコンテナが起動しているか確認
sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
daffbdd72d06        influxdb:1.8        "/entrypoint.sh infl…"   10 minutes ago      Up 10 minutes       0.0.0.0:8086->8086/tcp   influxdb_sample

# サンプルDBが作成されているか確認
sudo docker exec -i influxdb_sample influx --execute "show databases"
name: databases
name
----
sampledb
_internal

関数アプリの作成

functions/host.json

{
    "version": "2.0",
    "httpWorker": {
        "description": {
            "defaultExecutablePath": "go_server_sample.exe"
        }
    },
    "extensionBundle": {
        "id": "Microsoft.Azure.Functions.ExtensionBundle",
        "version": "[1.*, 2.0.0)"
    }
}

functions/local.settings.json

{
    "IsEncrypted": false,
    "Values": {
      "AzureWebJobsStorage": "UseDevelopmentStorage=true",
      "DB_HOST": "localhost",
      "DB_PORT": "8086",
      "DB_NAME": "sampledb",
      "DB_USER": "sample",
      "DB_PW": "sample"
    }   
}

functions/go_server_sample.go

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["blobData"]

    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)へのアクセス確認
    dbHost := getEnv("DB_HOST", "localhost")
    dbPort := getEnv("DB_PORT", "8086")
    dbName := getEnv("DB_NAME", "unknown")
    dbUser := getEnv("DB_USER", "unknown")
    dbPw   := getEnv("DB_PW", "unknown")
    client := influxdb2.NewClient(fmt.Sprintf("http://%s:%s", dbHost, dbPort), fmt.Sprintf("%s:%s", dbUser, dbPw))
    create_sample(client, dbName)
    client.Close()

    w.Header().Set("Content-Type", "application/json")
    w.Write(js)
}

/**
 * 環境変数の取得.
 */
func getEnv(envName string, defaultValue string) string {

    value, exists := os.LookupEnv(envName)
    if exists {
        return value
    } else {
        return defaultValue
    }   
}

/**
 * サンプルデータ登録(InfluxDB).
 */
func create_sample(client influxdb2.Client, dbName string) {

    fmt.Printf("Write START\n")

    writeAPI := client.WriteAPIBlocking("", fmt.Sprintf("%s/autogen", dbName))

    // サンプルデータ登録
    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")
}

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

{
  "bindings" : [ {
    "type" : "blobTrigger",
    "direction" : "in",
    "name" : "blobData",
    "path" : "作成したストレージコンテナ名1/{name}",
    "dataType" : "binary",
    "connection" : "AzureWebJobsStorage"
  },
  {
    "type" : "blob",
    "direction" : "out",
    "name" : "$return",
    "path" : "作成したストレージコンテナ名2/{name}",
    "dataType" : "binary",
    "connection" : "AzureWebJobsStorage"
  } ]
}

関数アプリのデプロイ

デプロイ

./3_deploy_funcapp.sh

動作確認

接続文字列の確認

source 0_env.sh
az storage account show-connection-string --name $storageAccountName
{
  "connectionString": "DefaultEndpointsProtocol=https;EndpointSuffix=core.windows.net;AccountName=xxxxxxxxxxx;AccountKey=xxxxxxxxxxxxxx"
}

ストレージエクスプローラにStorageアカウントをアタッチ

strexp1.png

strexp2.png

strexp3.png

ファイルをアップロード

strexp4.png

VMに接続して InfluxDB にデータが登録されているか確認

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

添付ファイル: fileazure_resources.png 7件 [詳細] filestrexp4.png 5件 [詳細] filestrexp3.png 6件 [詳細] filestrexp2.png 5件 [詳細] filestrexp1.png 8件 [詳細] filesample-azure-image.png 5件 [詳細]

トップ   差分 バックアップ リロード   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2020-07-28 (火) 08:00:13 (92d)