Azure Functions ゼンゼンワカラナイ

# はじめに

今回は[こちらの記事](https://blogs.technet.microsoft.com/jpaztech/2017/10/04/instance-metadata-service/)にあるように、AzureVMでFreezeが発生したときは少しの間一時停止するだけだよ問題ないよって言うけど、Freezeイベント前後でサーバー内で通信がちゃんと行われているよねっていうのを確認するためにネットワークのキャプチャをします。

「マイクロソフトのサーバーレスは全て筋肉で解決します」
「サーバーは要らんけど筋肉は必要」
とMSの人が言うのでAzureFunctionsを使ってできないかと探してみたところ

[この記事](https://docs.microsoft.com/ja-jp/azure/network-watcher/network-watcher-alert-triggered-packet-capture)を発見しました。
VMのサーバー内で検知してAzureFunctionsでNetworkWatcherを起動させるという仕組みの肝となる部分はそのままで部分的に環境やバージョンに合わせてアレンジして構築してみようと思います。

# 環境情報

* Azure Virtual Machine Scale Set
    * Ubuntu
    * Load BalancerによりPublicIPは１つでポートによる振り分け

サーバー内ではbashのシェルスクリプト
AzureFunctionsはv1でHttp TrrigerをPowershell
で実装していきます。

# 事前作業

* 最新バージョンの Azure PowerShellをインストール
* VMにNetworkWatcherAgentの拡張機能を導入
* 下記コマンドでNetwork Watcher のインスタンスを作成（VMの配置先リージョンに作成する）
    *  ``` New-AzureRmNetworkWatcher -Name “適当な名前" -ResourceGroupName “ForPacketCapture" -Location "VMと同じとこ" ```
* Capture配置先Storageの作成（Functions用Storageと兼用でも可）

#リソース作成

## AzureFunctionsのサービスを作成していきます

Azure Portal で、[リソースの作成] > [コンピューティング] > [Function App] の順に選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/de97c1d2-4e95-a154-494f-39d15ec90790.png)

[Function App] ブレードで、任意の値を入力して [OK] を選び、アプリを作成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/7fb9e161-5950-1d61-7e89-34aa08ad89f8.png)

上図のように入力し作成します。

# 関数の作成

AzureFunctionsをV1対応に変更します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/404efeff-394e-b838-09d3-7378f2210fc6.png)

関数の追加でcreate your own custom function. のリンクをクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/9d2b16e1-97b2-5fc7-6909-32f0852c3577.png)

Enableに変更する

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/20bbd851-f19b-b1f3-b88c-2298d374139b.png)

HTTP triggerのPowershellのリンクをクリック

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/7f2a23e3-419d-83ae-6230-f8be41261eb8.png)

必要に応じ関数名を変更し作成

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/fcdae66e-d1f5-99ad-b5d7-875cd5a72256.png)

これで空っぽのFunctionsが一つできました。

# モジュール追加

## Network Watcher PowerShell コマンドレットを使うには、最新の PowerShell モジュールを Function App にアップロードします。

ローカルPCのモジュールの配置場所の確認をしておく

* Azure PowerShell モジュールの以下のモジュールをアップロードします
    * AzureRM.Network
    * AzureRM.Profile
    * AzureRM.Resources
* ``` (Get-Module AzureRM.Network).Path ```を実行し、モジュールのパスを取得しておきます。

ポータルに戻ります。

Platform features ＞ App Service Editorを選択

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/7b64fca2-9086-2c9d-249b-c8646ae9819f.png)

作成したカスタム関数の名前を右クリックし、azuremodules という名前のフォルダーを作成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/4bbff7b7-9594-9bfd-ff12-3a464a883c5e.png)

必要なモジュールごとにサブフォルダーを作成します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/87b4869e-b098-16b7-9d65-d6fe927b755a.png)

Azure モジュールに移動します。 ローカルの AzureRM.Network フォルダーで、フォルダー内のすべてのファイルを選びます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/797fbd5e-0253-3924-dfda-de29b66effb8.png)

AzureRM.Profile と AzureRM.Resources についても、同様の手順を繰り返します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/c76c8151-1371-fbeb-da56-23a8f9930e20.png)

# 認証情報の作成

* PowerShellのAzureコマンドレットを使用するには、認証する必要があります。
* 認証を構成するには、環境変数を構成して、暗号化されたキー ファイルを Function App にアップロードする必要があります。

暗号化された資格情報の作成

* PowerShellでキー ファイルを作成します。
* 指定されているパスワードの暗号化バージョンも提供します。このパスワードは、認証に使われるAzure Active Directory アプリケーション用に定義されたパスワードと同じです。

``` crePassword.ps1
# Variables
$keypath = "C:\temp\PassEncryptKey.key"
$AESKey = New-Object Byte[] 32
$Password = "<insert a password here>"

# Keys
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($AESKey) 
Set-Content $keypath $AESKey

# Get encrypted password
$secPw = ConvertTo-SecureString -AsPlainText $Password -Force
$AESKey = Get-content $KeyPath
$Encryptedpassword = $secPw | ConvertFrom-SecureString -Key $AESKey
$Encryptedpassword
```

実行結果

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/0402ac46-61ca-bddf-40f6-8f42378d2af4.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/4fafe0fd-1bc3-7e67-926b-2c24ebb39ef8.png)

Function App の App Service エディターで、keysフォルダーを作成し、作成した PassEncryptKey.key ファイルをアップロードします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/1d387581-2c99-9690-9ed0-8f718363208a.png)

# 環境変数の値に設定する認証情報の取得

* 認証用の値にアクセスするために必要な環境変数を設定
* 下記を取得します
    * AzureClientID
    * AzureTenant
    * AzureCredPassword

## AzureClientID

* Azure Active Directory 内のアプリケーションのアプリケーション ID

下記スクリプトを実行します

``` creAzureClientID.ps1
$secPw = ConvertTo-SecureString -AsPlainText “先ほどのPW” -Force
$app = New-AzureRmADApplication -DisplayName “アプリ名” -HomePage “https://exampleapp.com” -IdentifierUris “https://exampleapp1.com/ExampleFunctionsAccount” -Password $secPw
New-AzureRmADServicePrincipal -ApplicationId $app.ApplicationId

Start-Sleep 15

New-AzureRmRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $app.ApplicationId
```

Azure Portal で、[サブスクリプション] を選びます。 使うサブスクリプションを選び、[アクセス制御 (IAM)] を選択

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/86f14751-ea36-a799-3bf5-408ee1d3593d.png)

使うアカウントを選び、[プロパティ] を選びます。 アプリケーション ID をコピーします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/4dc2fac2-b9f5-835f-ecd5-051e4bad86f4.png)

## AzureTenant

* 次の PowerShell サンプルを実行して、テナント ID を取得します。

``` (Get-AzureRmSubscription -SubscriptionName "<subscriptionName>").TenantId ```

## AzureCredPassword

* AzureCredPassword 環境変数の値は、前の[暗号化された資格情報](#暗号化された資格情報の作成)セクションで示したものと同じです。
* 必要な値は、$Encryptedpassword 変数の出力です。
* これは、PowerShell スクリプトを使って暗号化したサービス プリンシパル パスワードです。

# 環境変数を格納する

Function App に移動します。 [Function App の設定] > [アプリケーション設定の構成] の順に選びます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/55b2634a-8dda-24f7-31af-679a4cffac93.png)

環境変数とその値をアプリの設定に追加し、[保存] を選びます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/a7c18406-7f4a-5fbf-e196-d23cc0bd6ad0.png)

# 関数へのPSスクリプトの追加

スクリプト上部にある変数は各環境に合わせたリソースの情報に置き換えます。

``` functions.ps1
# Process request body
$requestBody = Get-Content $req -Raw | ConvertFrom-Json

#Azure vars
$VMresourceGroupName = "XXXXXX"
$StorageresourceGroupName = "XXXXXX"
$storageAccountName = "XXXXXXXX"
$region = "XXXXXXX"
$AzureFunctionName = "XXXXXXXXXX"
#vars
$PCBaseName = "pc_" # "" + InstanceName Is PacketCaputureName
$EventType = "Freeze"#Freeze
$packetCaptureLimit = 30 #number limit in one networkwatcher
$packetCaptureDuration = 1800 #Second 30min=1800sec

# Import Azure PowerShell modules required to make calls to Network Watcher
Import-Module "D:\home\site\wwwroot\$AzureFunctionName\azuremodules\AzureRM.Profile\AzureRM.Profile.psd1" -Global
Import-Module "D:\home\site\wwwroot\$AzureFunctionName\azuremodules\AzureRM.Network\AzureRM.Network.psd1" -Global
Import-Module "D:\home\site\wwwroot\$AzureFunctionName\azuremodules\AzureRM.Resources\AzureRM.Resources.psd1" -Global

if($requestBody){
    if($requestBody.Events){

        #Credentials
        $tenant = $env:AzureTenant
        $pw = $env:AzureCredPassword
        $clientid = $env:AzureClientId
        $keypath = "D:\home\site\wwwroot\$AzureFunctionName\keys\PassEncryptKey.key"
        #Authentication
        $secpassword = ConvertTo-SecureString -String $pw -Key (Get-Content $keypath)
        $credential = New-Object System.Management.Automation.PSCredential ($clientid, $secpassword)
        Connect-AzureRmAccount -ServicePrincipal -Tenant $tenant -Credential $credential #-WarningAction SilentlyContinue | out-null
        #Storage account ID to save captures in
        $storageaccountid = (Get-AzureRmResource -ResourceGroupName $StorageresourceGroupName -ResourceType Microsoft.Storage/storageAccounts -ResourceName $storageAccountName -ApiVersion 2017-10-01).ResourceId
        #Get the Network Watcher in the VM's region
        $nw = Get-AzurermResource | Where {$_.ResourceType -eq "Microsoft.Network/networkWatchers" -and $_.Location -eq $region }
        $networkWatcher = Get-AzureRmNetworkWatcher -Name $nw.Name -ResourceGroupName $nw.ResourceGroupName

        #Check RequestBody
        foreach ($event in $requestBody.Events) {
        
            if($event.EventType -eq $EventType){
                Write-Output "Freeze Event Will Occur"
                #リソースID取得（Jsonからリソース名取得して_を/に置換して検索）
                $TargetVmResourceName = ($event.Resources).Replace("_", "/") 
                $VMResourceInfo = Get-AzureRmResource -ResourceGroupName $VMresourceGroupName -ResourceType Microsoft.Compute/virtualMachineScaleSets/virtualMachines -ResourceName $TargetVmResourceName -ApiVersion 2018-06-01
                $VMResourceID = $VMResourceInfo.ResourceId
                        
                #Packet capture vars
                $packetcapturename = $PCBaseName + $VMResourceInfo.Name

                #Get existing packetCaptures
                $packetCaptures = Get-AzureRmNetworkWatcherPacketCapture -NetworkWatcher $networkWatcher -PacketCaptureName $packetCaptureName -ErrorAction SilentlyContinue

                #Remove existing packet capture created by the function (if it exists)
                if($packetCaptures)
                { 
                    Write-Output $packetCaptures.PacketCaptureStatus 

                    Stop-AzureRmNetworkWatcherPacketCapture -NetworkWatcher $networkWatcher -PacketCaptureName $packetCaptureName
                    Start-Sleep -Seconds 3
                    Remove-AzureRmNetworkWatcherPacketCapture -NetworkWatcher $networkWatcher -PacketCaptureName $packetCaptureName
                    Start-Sleep -Seconds 3
                }

                #Initiate packet capture on the VM that fired the alert
                if ((Get-AzureRmNetworkWatcherPacketCapture -NetworkWatcher $networkWatcher -ErrorAction SilentlyContinue).Count -lt $packetCaptureLimit)
                {
                    Write-Output "Initiating Packet Capture"
                    New-AzureRmNetworkWatcherPacketCapture -NetworkWatcher $networkWatcher -TargetVirtualMachineId $VMResourceID -PacketCaptureName $packetCaptureName -StorageAccountId $storageaccountid -TimeLimitInSeconds $packetCaptureDuration
                    Start-Sleep -Seconds 3
                }
            }else
            {
                Write-Output "$event.EventType Event Will Occur"
            }
        }  
        Out-File -Encoding Ascii -FilePath $res -inputObject "Packet Capture created"
    }else{
        Write-Output "Nothing Events!!"
    }
}else{
    Write-Output "No RequestBody"
}
```

# 関数の URL を取得する

関数を作成した後は、その関数に関連付けられた URL を呼び出すようにシェルを構成します。 この値を取得するには、Function App から関数の URL をコピーします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/9899d718-d86b-8cf6-7981-2f632c1e6287.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/16672333-2a1b-5e96-2b74-54e0f214ca18.png)

シェルスクリプトの書き換え

``` chkMetaDataService.sh
#!/bin/sh

while true
do
    metadatajson=$(curl -H Metadata:true http://169.254.169.254/metadata/scheduledevents?api-version=2017-08-01)
    echo $metadatajson

    if [[ "$metadatajson" =~ "EventType" ]]; then
        #if [[ "$metadatajson" =~ "Freeze" ]]; then
            echo "Some Event Will Occur"

            #AzureFunctionsにWebHook
            HOOK="https://X.azurewebsites.net/api/X?code=X"
            curl -X POST -H "Content-Type: application/json" -d "$metadatajson" "$HOOK" 
        
            sleep 30m
        #fi
    fi
    
    sleep 10s
done

exit 0

```

これで実行する準備が完了です。

# 実際に動かしてみる

Freezeはいつ発生するかわからないのでRebootに書き換えてVMを再起動させての稼働確認です。

# Linux側

まずはLinuxの方でbashを実行させておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/d7cfbd3d-ccc7-7481-4f17-1cf079dd10e3.png)

それではポータルからsshしているVMの再起動を行ってみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/319a30e8-e4e3-1d63-cea6-22ffa1b5bf2c.png)

すると、、、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/a64b7e2d-93f7-e5a0-bee8-6e93f5802414.png)

再起動イベントが15分後に行われますというレスポンスが返ってきます。

# Functions側

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/d0763af4-5040-e5bf-a1e7-47e7dab3fe2f.png)

無事にFunctions側が起動してPacketCaptureを作成してくれたようです。

# NetworkWatcher側

![image.png](https://qiita-image-store.s3.amazonaws.com/0/80457/91bbbbc1-041f-728e-4093-f7e141c80aa0.png)

実際にキャプチャされていました。

# 終わり

V2がGAされた中いまさらV1かよって感じもあります、V1もいつまで存続するかわからないので、実は代替案としてAzureFunctionsの代わりにAzureAutomationで動かすよう構成を考え直していたりしています。AzureAutomationでもやることはほぼ同じ設定値系と認証系を少し変更するだけで同じように動きました。