本ブログですが、今まではレンタルサーバー上で稼働させていたのですが、結構な頻度で 500 エラーが発生していたと思います。
ということで、ブログを Web Apps 上にお引越ししてみたところ、500 エラーの発生状況は改善したようでした。
(低いサービスレベルで動作させているからか、安定性がいまいちで、まだまだ対応が必要なことが多いですが…。)
左がレンタルサーバー上で実行していた時のステータスコードの比率なのですが、1 日のアクセス数の中で 18% 近くが 500 エラーとなっていたんですね…。
個人サイトですので、SLA は必要がないのでひとまず、F1 で動作させているのですが、Free や Standard というようなサービスプランでは、1 日の中で CPU が使用できる時間が決まっています。
次の画像は料金表の内容となります。
F1 で 60 分 / D1 で 240 分の CPU 時間を使用することができるのですが、私のサイトの場合、F1 では確実に CPU 時間が不足し、D1 では平日にいろいろと作業をした場合に CPU 時間が不足する可能性があるのですよね。
ということで Azure Automation を使用して、クォータのリミットに近づいているかを取得しながら、スケールアップをするスクリプトが組めるかどうかを試してみました。
今回の処理は実行時間が長いわけではないので、Azure Automation ではなく、Azure Functions で実行した方が手っ取り早いのですが、今の Automation がどのようになっているのかを知りたくて、Automation を使っています。
■CPU 時間の使用状況を取得
F1 / D1 のサービスレベルでは、App Service のブレードの「クォータ」から、CPU 時間の消費状況を取得することができます。
Azure の各種メトリックについては、Azure Monitor のサポートされるメトリック に情報が記載されています。
Get-AzMetric のようなメトリックを取得するコマンドレットでとることを考えたのですが、リミットに対しての CPU 時間が取れるようなデータが見当たらず、ずばりなコマンドレットを探すのも面倒だったので REST API を直接呼び出して取得してしまいました。
Connect-AzAccount を実行した後の Azure Context を使用した例となりますが、次のようなスクリプトで F1 / D1 のサービスレベルの CPU クォータの状態を取得することができるはずです。
$tenantId = "xxxxxxxx" $subscriptionId = "xxxxxxxx" $resoruceGroup = "xxxxxxxx" $planName = "xxxxxxxx" # REST API URL $usagesUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Web/serverfarms/{2}/usages?api-version=2019-08-01" -f $subscriptionId, $resoruceGroup, $planName $planUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Web/serverfarms/{2}?api-version=2019-08-01" -f $subscriptionId, $resoruceGroup, $planName $context = Get-AzContext $accessToken = $context.TokenCache.ReadItems() | ? TenantId -eq $tenantId $token = "Bearer {0}" -f $accessToken.AccessToken # REST API Call $header = @{ "Authorization" = $token "Content-Type" = "application/json" } # SKU の取得 $response = Invoke-WebRequest -Method "GET" -Uri $planUri -Headers $header $jsonValue = $response.Content | ConvertFrom-Json $sku = $jsonValue.sku.name Write-Host ("Current SKU : {0}" -f $sku) switch ($sku) { "F1" { $cpuLimit = 60 * 60 * 1000 } "D1" { $cpuLimit = 240 * 60 * 1000 } Default { } } $response = Invoke-WebRequest -Method "GET" -Uri $usagesUri -Headers $header $jsonValue = $response.Content | ConvertFrom-Json$cpuTime = $jsonValue.value | % { if ($_.name.value -eq "CpuTime") { return $_ } } $cpuTime $remainingCpuTime = ($cpuLimit - $cpuTime.currentValue) / 1000 / 60 Write-Host ("Remaining CPU Time is {0:#,##0.##} Min." -f $remainingCpuTime
情報は App Service Plan に対して取得する必要がありますので、planName には App Service の名称ではなく、App Service Plan の名称を指定してください。
正常に実行できると次のような情報を取得することができます。
CurrentValue が残りの CPU 時間 (ms) となります。
nextResetTime がクォータのリセットが発生するタイミングとなりますが、こちらについては UTC の時間の 00:00 にリセットされます。
F1 の場合は limit に 3,600,000 (60 分) が設定されているのですが、D1 の場合は、-1 となっているので、上限値についてはどちらのパターンでも計算して算出するようにしています。
これで、残り使用可能な CPU 時間の算出ができましたので、Azure Automation でスクリプトを組んでいきます。
■Azure Automation を設定
まずは、Automation アカウントを「Azure 実行アカウントの作成」を有効にした状態で作成します。
モジュールのアップデート
Azure Automation ですが、標準でいくつかの Azure の PowerShell モジュールが入っているのですが、バージョンが 1.0.3 となっており、かなり古いです。
そのため最初に、モジュールの最新化を行っておきます。
(初期状態のバージョンだと REST を呼び出すときの Token の取得が面倒なので)
モジュールの更新ですが、ポータルのブレードにある「Azure モジュールの更新」は現在使用することができなくなっています。
Updating Azure PowerShell modules in Azure Automation accounts というスクリプトが公開されており、これを Automation の Runbook として実行することで、モジュールが更新されるようになっています。
実行が正常に完了すると、投稿を書いている時点では、次の画像のように各モジュールのバージョンがアップデートされますので、この状態にしておきます。
Azure Automation での Az モジュールのサポート の内容で AzureRM モジュールではなく、Az モジュールを使用するという方法もありますが、まずはスクリプトを稼働させたかったので、既存モジュールのアップデートで対応しています。
共有リソースの設定
次に共有リソースの設定を行います。
今回、接続と証明書は、Automation アカウントを作成した際に「Azure 実行アカウントの作成」を有効にした際に、自動で作成される情報を使用します。
(デフォルトでは証明書の有効期間が 1 年なので本来は期間の長い証明書や証明書の更新についても検討する必要がありますが)
今回明示的に設定を行う必要があるものは「remainingCpuTimeLimit」という整数の変数となります。
Runbook の実行時に、残り何分の CPU 時間になっていたらサイズの変更を行うという閾値を設定します。
次の画像の内容でしたら残り時間が 20 分になっていた場合にスケールアップを行います。
Runbook の作成
Runbook には次のようなスクリプトを設定します。
param ( [Parameter(Mandatory=$true)] [String]$resourceGroup, [Parameter(Mandatory=$true)] [String]$planName, [String]$location="japaneast" ) $servicePrincipalConnection = Get-AutomationConnection -Name "AzureRunAsConnection" # Azure Setting $tenantId = $servicePrincipalConnection.TenantId $subscriptionId = $servicePrincipalConnection.SubscriptionId # REST API URL $usagesUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Web/serverfarms/{2}/usages?api-version=2019-08-01" -f $subscriptionId, $resourceGroup, $planName $planUri = "https://management.azure.com/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Web/serverfarms/{2}?api-version=2019-08-01" -f $subscriptionId, $resourceGroup, $planName Write-Host $usagesUri # Limit $remainingCpuTimeLimit = Get-AutomationVariable -Name 'remainingCpuTimeLimit' # Minute $context = Add-AzureRmAccount -ServicePrincipal ` -TenantId $servicePrincipalConnection.TenantId ` -ApplicationId $servicePrincipalConnection.ApplicationId ` -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint $accessToken = $context.Context.TokenCache.ReadItems() $token = "Bearer {0}" -f $accessToken.AccessToken # REST API Call $header = @{ "Authorization" = $token "Content-Type" = "application/json" } # SKU の取得 $response = Invoke-WebRequest -Method "GET" -Uri $planUri -Headers $header -UseBasicParsing $jsonValue = $response.Content | ConvertFrom-Json $sku = $jsonValue.sku.name Write-Host ("Current SKU : {0}" -f $sku) switch ($sku) { "F1" { $newSKU = "D1"; $cpuLimit = 60 * 60 * 1000 } "D1" { $newSKU = "B1"; $cpuLimit = 240 * 60 * 1000 } Default { } } # UTC の 00:00 にクォーターがリセットされるため、SKU が F1 でない場合は、F1 にスケールダウンする $now = (Get-Date).ToUniversalTime() if ($now.Hour -eq 0) { if ($SKU -ne "F1") { Write-Host ("Change SKU {0} to ""F1""" -f $sku) $body = @{ "location" = $location "sku" = @{ "name" = "F1" } } Invoke-WebRequest -Method "PUT" -Uri $planUri -Headers $header -Body ($body | ConvertTo-Json -Compress) -UseBasicParsing $sku = "F1" } } else { # SKU が F1 or D1 の場合はスケールアップの対象とする if ($sku -in @("F1", "D1")) { $response = Invoke-WebRequest -Method "GET" -Uri $usagesUri -Headers $header -UseBasicParsing $jsonValue = $response.Content | ConvertFrom-Json $cpuTime = $jsonValue.value | % { if ($_.name.value -eq "CpuTime") { return $_ } } # D1 の場合、CPU Time の Limit が -1 になっているため、Limit はハードコード # 残りの CPU 使用時間をチェック $remainingCpuTime = ($cpuLimit - $cpuTime.currentValue) / 1000 / 60 Write-Host ("Remaining CPU Time is {0:#,##0.##} Min." -f $remainingCpuTime) # 残りの CPU 時間がリミットの分数を切った場合、上位の価格レベルに変更する if ($remainingCpuTime -le $remainingCpuTimeLimit) { Write-Host ("Change SKU {0} to {1}" -f $sku , $newSKU) $body = @{ "location" = "japaneast" "sku" = @{ "name" = $newSKU } } Invoke-WebRequest -Method "PUT" -Uri $planUri -Headers $header -Body ($body | ConvertTo-Json -Compress) -UseBasicParsing } } }
Azure の処理を実行するための資格情報は、Automation アカウントと同時に作成された「AzureRunAsConnection」の設定をそのまま使用しています。
この資格情報に設定されている情報から、テナントやサブスクリプションの ID を取得して、「Add-AzureRmAccount」で Azure Context を生成します。
そのコンテキスト内のトークンを使用して REST API を実行します。
コンテキストからトークンキャッシュを使用するためには、標準で導入されている 1.0.3 の AzureRM.Profile ではバージョンが不足しているため、モジュールのアップデートをしています。
あとは、Invoke-WebRequest を実行して REST を叩いていますが、「-UseBasicParsing」を設定していないと IE を使用してパースを実行しようとしてエラーとなりますので気を付けてください。
(Azure Automation は Azure Function とお隣、PowerShell のバージョンは 5.1 なのですよね)
このスクリプトで、指定した CPU 時間を下回っている場合はスケールアップが行われます。
スケールアップについては「F1 → D1」「D1 → B1」の 2 パターンで実行するようになっており、B1 になったら移行のスケールアップは行われません。
また、UTC で 00:00 にクォータのリセットが行われますので、00 時台に実行された場合は F1 にスケールダウンするようにしています。
Runbook の設定が終わったらスケジュールを設定します。
Azure Function であれば、秒単位で繰り返し実行ができますが、Azure Automation の場合は、最小の繰り返し間隔が 1 時間となります。
(複数のスケジュールをリンクすることで毎時 00 分 / 30 分に実行というような調整は可能です)
実行タイミングのスケジュールが設定できたら、実行時のパラメーターも設定します。
必須なのはリソースグループと App Service Plan の名称の 2 つです。
SKU を変更する場合は、location を設定する必要があるのですが、東日本にデプロイしているようでしたら、設定は省略可能です。東日本以外のリージョンを使用している場合に設定して下さい。
これで指定した時間になれば、Runbook が実行され必要に応じてスケールアップ / スケールダウンが実行されるかと。
Automation では定期的な実行は、1 つのスケジューラーでは1 時間間隔が最小ですが、Automation の Runbook については、アラートルールからも実行することができますので、スケジュールだけでなく、App Service のアラートルールからも起動するようにしておくのもよさそうですね。