- 追加された行はこの色です。
- 削除された行はこの色です。
#author("2020-08-28T05:31:24+00:00","","")
#mynavi(Azureメモ)
#setlinebreak(on);
#html(){{
<style>
.images img { border: 1px solid #333; margin-right: 20px;}
.images div { vertical-align: top; }
</style>
}}
* 概要 [#l83f8e0c]
#html(<div class="pl10">)
#TODO
Azure Monitor の統合アラート エクスペリエンスには、以前は Log Analytics と Application Insights によって管理されていたアラートも含まれるようになった。
ここでは Azure モニター を使用して関数アプリの異常を検知する方法について記載する。
#html(</div>)
* 目次 [#q34c50fa]
#contents
- 関連
-- [[Azureメモ]]
-- [[Azure Functions のログを参照する]]
- 参考
-- [[Azure Monitor の概要>https://docs.microsoft.com/ja-jp/azure/azure-monitor/overview]]
-- [[Azure App Service のアプリの監視>https://docs.microsoft.com/ja-jp/azure/app-service/web-sites-monitor]]
-- [[Azure での Web アプリケーションの監視>https://docs.microsoft.com/ja-jp/azure/architecture/reference-architectures/app-service-web-app/app-monitoring]]
-- [[Microsoft Azure のアラートの概要>https://docs.microsoft.com/ja-jp/azure/azure-monitor/platform/alerts-overview]]
-- [[Azure Functions を監視する>https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-monitoring?tabs=cmd]]
-- [[Application Insights の使用量とコストを管理する>https://docs.microsoft.com/ja-jp/azure/azure-monitor/app/pricing]]
-- [[Azure Monitor でログクエリの使用を開始する>https://docs.microsoft.com/ja-jp/azure/azure-monitor/log-query/get-started-queries]]
-- [[Kusto の概要>https://docs.microsoft.com/ja-jp/azure/data-explorer/kusto/concepts/]]
-- [[Kusto クエリ言語>https://docs.microsoft.com/ja-jp/azure/data-explorer/kusto/query/]]
* どのようなログを検知するかを決める。 [#m56ceb07]
#html(<div class="pl10">)
Azure Monitor では、設定したクエリに合致するレコードの件数や、数値の平均値などが一定数以上の時に通知を行う事ができる。
今回は以下の前提で以降の手順を記載する。
- 関数アプリは Go で作成されているものとする。(カスタムハンドラーを使用)
- 関数アプリは処理の最後に処理結果を以下の形式で出力する。
| 正常/異常 | 出力されるログ |h
| 正常時 | [INFO] Result: Success ... |
| 異常時時 | [ERROR] Result: Failure ... |
- 異常チェックは1時間に一回行うものとする。
上記の前提で、今回は異常チェック用として以下のクエリを使用する。
#myterm2(){{
traces
| where (message has_cs "[ERROR] Result:" or message has_cs "http: panic serving")
and cloud_RoleName == "関数アプリ名(全て小文字)"
and timestamp > ago (1h)
}}
※ Kustoクエリについては [[Kusto クエリ言語>https://docs.microsoft.com/ja-jp/azure/data-explorer/kusto/query/]] を参照。
※ 重大度(severityLevel)等も使用できるが今回は使用していない。
#html(</div>)
* サンプルアプリ [#lf2a9e7c]
#html(<div class="pl10">)
ストレージコンテナ1にアップロードされたCSVファイルをストレージコンテナ2にそのまま出力するシンプルな関数アプリ。
#html(){{
<div id="tabs1">
<ul>
<li><a href="#tabs1-1">functions/host.json</a></li>
<li><a href="#tabs1-2">functions/MyBlobTrigger/function.json</a></li>
<li><a href="#tabs1-3">functions/server.go</a></li>
<li><a href="#tabs1-4">create_resources.sh</a></li>
<li><a href="#tabs1-5">deploy_funcapp.sh</a></li>
</ul>
}}
// START tabs1-1
#html(<div id="tabs1-1">)
#mycode2(){{
{
"version": "2.0",
"httpWorker": {
"description": {
"defaultExecutablePath": "server.exe"
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[1.*, 2.0.0)"
}
}
}}
#html(</div>)
// END tabs1-1
// START tabs1-2
#html(<div id="tabs1-2">)
#mycode2(){{
{
"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"
} ]
}
}}
#html(</div>)
// END tabs1-2
// START tabs1-3
#html(<div id="tabs1-3">)
#mycode2(){{
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"strings"
)
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 printDebug(format string, params ...interface{}){
log.SetOutput(os.Stdout)
msg := fmt.Sprintf(format, params...)
log.Printf("[DEBUG] %s\n", msg)
}
func printInfo(format string, params ...interface{}){
log.SetOutput(os.Stdout)
msg := fmt.Sprintf(format, params...)
log.Printf("[INFO] %s\n", msg)
}
func printError(format string, params ...interface{}){
log.SetOutput(os.Stderr)
msg := fmt.Sprintf(format, params...)
log.Printf("[ERROR] %s\n", msg)
log.SetOutput(os.Stdout)
}
func init(){
log.SetOutput(os.Stdout)
log.SetFlags(0)
}
func blobTriggerHandler(w http.ResponseWriter, r *http.Request) {
printInfo("START blobTriggerHandler")
fileUri := ""
defer func(){
err := recover()
if fileUri == "" {
fileUri = "unknown"
}
if err != nil {
printError("Result: Failure %s", fileUri)
panic("blobTriggerHandler Error!\n");
} else {
printInfo("Result: Success %s", fileUri)
}
}()
logs := make([]string, 0)
//------------------------------------------------
// データ取得
//------------------------------------------------
var invokeReq InvokeRequest
d := json.NewDecoder(r.Body)
decodeErr := d.Decode(&invokeReq)
if decodeErr != nil {
http.Error(w, decodeErr.Error(), http.StatusBadRequest)
return
}
fileUri = invokeReq.Metadata["Uri"].(string)
filename := invokeReq.Metadata["name"].(string)
sysInfo := fmt.Sprintf("%v", invokeReq.Metadata["sys"])
fileData, _ := base64.StdEncoding.DecodeString(invokeReq.Data["blobData"].(string))
printDebug("filename : %s", filename)
printDebug("fileUri : %s", fileUri)
printDebug("sysinfo : %s", sysInfo)
//------------------------------------------------
// CSV文字列をパースする
//------------------------------------------------
rows := parseCsv(string(fileData), fileUri)
printDebug("csv data: %v", rows)
//------------------------------------------------
// レスポンス生成
//------------------------------------------------
returnValue := fileData
invokeResponse := InvokeResponse{Logs: logs, ReturnValue: string(returnValue)}
js, err := json.Marshal(invokeResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
printInfo("END blobTriggerHandler")
}
/**
* CSV文字列のパース.
*/
func parseCsv(csvText string, fileUri string) ([]map[string]string) {
procIndex := -1
defer func(){
err := recover()
if err != nil {
printError("error: file: %s, line: %d, %v", fileUri, procIndex, err)
panic("parseCsv Error!\n");
}
}()
lines := strings.Split(csvText, "\n")
var columns []string
rows := make([]map[string]string, 0)
for i, line := range lines {
procIndex = i
if i == 0 {
columns = strings.Split(line, ",")
} else {
values := strings.Split(line, ",")
row := make(map[string]string, len(values))
for j, val := range values {
// ヘッダの列数より多い時はわざとコケるようにしておく
colname := columns[j]
row[colname] = val
}
rows = append(rows, row)
}
}
return rows
}
/**
* メイン.
*/
func main() {
httpInvokerPort, exists := os.LookupEnv("FUNCTIONS_HTTPWORKER_PORT")
if exists {
log.Println("FUNCTIONS_HTTPWORKER_PORT: " + httpInvokerPort)
}
mux := http.NewServeMux()
mux.HandleFunc("/MyBlobTrigger", blobTriggerHandler)
log.Println("Go server Listening...on httpInvokerPort:", httpInvokerPort)
log.Fatal(http.ListenAndServe(":"+httpInvokerPort, mux))
}
}}
#html(</div>)
// END tabs1-3
// START tabs1-4
#html(<div id="tabs1-4">)
#mycode2(){{
#!/bin/bash
# 全てのリソース名に付与する接頭文字 (Storageアカウント名などは世界でユニークな必要があるので他ユーザと被らないような名前を付ける)
PREFIX=XXXXXXX
subscriptionId=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
region=japanwest
insightsRegion=japaneast # 2020/7時点では Appplication Insights で西日本(japanwest)は使用できない
resourceGroup=${PREFIX}ResourceGroup
storageAccountName=${PREFIX}straccount
storageSku=Standard_LRS
storageContainerIn=${PREFIX}-strcontainer-i
storageContainerOut=${PREFIX}-strcontainer-o
funcAppName=${PREFIX}FuncApp
funcVersion=2
funcPlanName=${PREFIX}premiumplan
funcPlanSku=EP1
insightsName=${PREFIX}Insights
insightsDays=30
# リソースグループの作成
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
# Application Insights 拡張が利用できない場合は追加インストール
x=`az monitor app-insights --help 2>&1`
if [ "$?" != "0" ]; then
az extension add -n application-insights
fi
# Application Insights コンポーネント作成
if [ "$subscriptionId" != "" ]; then
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
fi
# 関数アプリのプラン作成(プレミアムプラン)
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 $storageAccountName \
--resource-group $resourceGroup \
--functions-version $funcVersion \
--plan $funcPlanName \
--app-insights $insightsName
}}
#html(</div>)
// END tabs1-4
// START tabs1-5
#html(<div id="tabs1-5">)
#mycode2(){{
#!/bin/bash
PREFIX=XXXXXXX
resourceGroup=${PREFIX}ResourceGroup
funcAppName=${PREFIX}FuncApp
rm -rf functions.zip
exefile=`cat functions/host.json | grep defaultExecutablePath | awk '{print $2}' | sed 's/"//g'`
cd functions
rm -rf $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
rm -rf functions.zip
}}
#html(</div>)
// END tabs1-5
#html(</div>)
// END tabs1
#html(<script>$(function() { $("#tabs1").tabs(); });</script>)
#html(<div class="pl10">)
リソース作成
#myterm2(){{
./create_resources.sh
}}
関数アプリのデプロイ
#myterm2(){{
./deploy_funcapp.sh
}}
#html(</div>)
#html(</div>)
* アラートの作成 [#ua7becfb]
#html(<div class="pl10">)
#html(<div class="images"><div class="ib">)
Azureポータルで "モニター" を検索し、選択。
&ref(azure_monitor01.png,nolink);
#html(</div><div class="ib">)
アラートから [新しいアラートルール] を選択。
&ref(azure_monitor02.png,nolink);
#html(</div><div class="ib">)
関数アプリに関連付けている Application Insight を選択し、条件名のリンクをクリック。
&ref(azure_monitor03.png,nolink);
#html(</div><div class="ib">)
custom log search で下図のように入力/選択。
&ref(azure_monitor04.png,nolink);
#html(</div><div class="ib">)
[アクショングループの選択] を押下。
&ref(azure_monitor05.png,nolink);
#html(</div><div class="ib">)
リソースグループ、アクショングループ名などを入力。(まだ [確認及び作成] は押さない)
&ref(azure_monitor06.png,nolink);
#html(</div><div class="ib">)
[通知]タブに切り替えて、通知の種類、名前を入力。(電子メール/プッシュ通知... を選択する)
&ref(azure_monitor07.png,nolink);
#html(</div><div class="ib">)
通知先のメールアドレスを入力し [OK]
&ref(azure_monitor08.png,nolink);
#html(</div><div class="ib">)
アクションタブでは他の関数の起動等を設定できるが、今回は何も指定せずに [確認及び作成]。
&ref(azure_monitor09.png,nolink);
#html(</div><div class="ib">)
メールの件名などを入力し [アラートルールの作成] を押下。
&ref(azure_monitor10.png,nolink);
#html(</div></div>)
#html(</div>)
* エラー通知の例 [#dce7fbf2]
#html(<div class="pl10">)
上記で設定したアラートに合致するログが見つかった時には、下図のようなメールが送信されてくる。
※ クエリに引っかかったログの最初の10件の内容もメール本文に記載されている。
#html(<div class="images">)
&ref(azure_alert_mail_sample1.png,nolink);
#html(</div>)
メール本文の [View 10 Result(s)] ボタンを押下すると Azure ポータルの Insight のログ検索画面が開き、結果が表示される。
#html(<div class="images">)
&ref(azure_alert_mail_clicked1.png,nolink);
#html(</div>)
#html(</div>)
* 料金 [#f18b5083]
#html(<div class="pl10">)
https://azure.microsoft.com/ja-jp/pricing/details/monitor/ には下表の通り記載されている。(2020/8月現在)
** アラート [#n514d09c]
| アラート シグナル | 含まれている無料ユニット | 料金 |h
| メトリック | 監視対象メトリック時系列 10 個 (1 か月あたり) | 監視対象メトリック時系列 1 つにつき ¥11.200 (1 か月あたり) |
| ログ | なし |15 分以上の間隔: 監視対象ログ 1 つにつき ¥56 (1 か月あたり)&br;10 分間隔: 監視対象ログ 1 つにつき ¥112 (1 か月あたり)&br;5 分間隔: 監視対象ログ 1 つにつき ¥168 (1 か月あたり) |
| アクティビティ ログ | サブスクリプションあたり 100 ルールが上限 | 無料 |
| 動的しきい値 | なし | 動的しきい値あたりの ¥11.200/月 |
** 通知 [#wb723a2c]
| 機能 | 含まれている無料ユニット | 料金 |h
| ITSM コネクタの作成イベントまたは更新イベント | 1 か月あたりイベント 1,000 件 | ¥560/1,000 イベント |
| メール | 1 か月あたりメール 1,000 通 | メール 100,000 通につき ¥224 |
| (Azure Mobile Apps への) プッシュ通知 | 1 か月あたり通知 1,000 件 | 通知 100,000 回につき ¥224 |
| Web hook をセキュリティで保護する | セキュリティ保護された 1 つの Web hook | セキュリティ保護された ¥672/1,000,000 の Web hook |
| webhook | 1 か月あたりの webhook 100,000 件 | webhook 1,000,000 件につき ¥67.20 |
※SMS と音声通話は省略
#html(</div>)
//* クエリサンプル [#l05d37f4]
//traces | where operation_Name=="BlobTrigger2" and message has_cs "test"
//traces | operation_Name=="MyBlobTrigger2" and message has_cs "test" and timestamp > ago(1h)