Azure
PowerShell
REST-API
AzureWebApps
Kudu

PowerShellでAzure WebJobsを実行しようとしたら思いのほか大変だった話

やりたいこと

Azure Web Appの無料プランでWebアプリを公開しているのですが、このWebアプリに対して定期的にバックアップのためのスクリプトを動かしたかった。

こういう時に使える便利な機能としてWebJobsがあるのですが、残念ながら無料プランでは手動実行のみで、スケジュール実行ができません。
ところが、WebJobsはWebHookを使って外部からタスク実行させることができます。
これを利用して外部からPowerShellスクリプトで実行させることで、無料プランでも定期的に実行ができるのではないかと考えました。

準備

前提として、実行したいWebJobsタスクは作成済みとします。
Azureポータルで作成済みのタスクのプロパティを表示させると、WebHookの実行に必要な情報(URL、ユーザ名、パスワード)が書いてあるので控えておきます。
azure-portal.png

最初にやってみたこと

WebHookのURLに対してBASIC認証付きでPOSTすればいいだけなので、
楽勝だぜ!と思ってこう書きました。

$Cred = New-Object 'System.Management.Automation.PsCredential' $UserName, (ConvertTo-SecureString -AsPlainText -Force -String $Password)
Invoke-RestMethod -Uri $WebHookURL -Method Post -Credential $Cred

ところがうまくいきません。
Error 403 - This web app is stopped.となってしまいます
image.png

PowerShellじゃない方法ならできた

パラメータを確認したり、スクリプトをいじったりしてもさっぱりうまくいかないので、一度PowerShell以外の方法で試すことにしました。
RESTクライアント(ChromeアプリのYARC!)を使ってPOSTを投げてみます。

rest.png

するとこれは202 - Acceptedが返ってきて成功しました。タスクも確かに実行されていました。

rest2.png

となるとやはり問題はPowerShellにあるようです。う~む・・・

有力な情報を発見

すっかり行き詰まって諦めかけていたところ、有力な情報を見つけました。

任意のタイミングで WebJob を実行するための API が Kudu に用意されています。(中略)
ただし、最初から Authentication ヘッダーを送信しておく必要があるので、注意が必要です。

As noted in the comments, this method will not send the Authorization header on the initial request. It waits for a challenge response then re-sends the request with the Authorization header. This will not work for services that require credentials on the initial request.

ふむふむ。最初からAuthorization ヘッダーを送信しないといけないけれど、PowerShellのInvoke-RestMethodはそういう仕様にはなっていないと。どうやらこれが原因のようです。

これでどうだ

上記のStack Overflowに回避策のサンプルコードが記載されていたので、ありがたくコピペさせて頂きこのようなコードになりました。

$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName,$Password)))
Invoke-RestMethod -Uri $WebHookURL -Method Post -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -ContentType "application/json"

これを実行すると、無事に成功しました。
Azureポータルから確認するとバッチリWebJobsが実行されています。 :tada:

最終的にこうなった

一応これでPowerShellからWebJobsを実行させることには成功したのですが、Invoke-RestMethodは実行後に何も値を返さないので成功したのかどうか分からず不安です。
なのでコマンドをInvoke-WebRequestに置き換えたりして、最終的にはこのようなスクリプトに仕上げました。

# Authorizationヘッダー用の資格情報作成
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $UserName, $Password)))

try {
    # POST実行
    $Response = Invoke-WebRequest -Uri $WebHookURL -Method Post -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo)} -ContentType "application/json" -UseBasicParsing -ErrorAction Stop
}
catch {
    $Response = $_.Exception.Response
}

# 成否確認
if ([int]$Response.StatusCode -ne 202) {
    Write-Error ('Job execution failed - Expected status is "202(Accepted)", but status "{0}({1})" has returned.' -f [int]$Response.StatusCode, $Response.StatusDescription)
}
else {
    Write-Host ('Job started successfully.')
}

このスクリプトをタスクスケジューラを使って毎日実行してあげることで、無料プランのままでスケジュール実行ができます。

感想

30秒でサクッと出来るかと思いきや、ガッツリ落とし穴にハマってしまって辛かったです。 :disappointed_relieved: