Azure Automation 証明書自動更新までの道のり

# そもそもの話
Automation実行アカウントを使ってAzure Virtual MachinのStart/Stopを行っていたのですが、Automatio実行アカウントには証明書があり、期限が切れると権限の都合で処理がうまく行かない場合があるみたいです。
証明書の更新自体は簡単なのですが、数が増えると大変なので極力自動化したいなぁと思いながら色々と調べていたところ、Runbook機能を使えばなんとかなることがわかりました。

# 手順
1.　AutomationアカウントにAzureADモジュールをインストールする
「Automationアカウント」 > 「共有リソース：モジュールギャラリー」を
選択して、「AzureAD」モジュールをインポートします。下の画像の一番上ですね。
<img width="1301" alt="スクリーンショット 2020-05-22 13.55.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594267/9c56d140-cab5-c0ee-d65f-416a5f996894.png">


インポートできたかどうかは、「Automationアカウント」 > 「共有リソース：モジュール」を見ればわかります。
<img width="564" alt="スクリーンショット 2020-05-22 13.55.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594267/59802562-18c3-f7b5-17ae-7dfa8bca4843.png">


2.　AutomationアカウントにAzureADへのAPIアクセス許可を与える
「Azure Active Directory」 > 「管理：アプリの登録」から対象としたいAutomationアカウントを選択します。
次に、「管理：APIのアクセスを許可」を選択します。
それから「APIアクセス許可の要求」を選択して、様々なAPIが表示される中から、「Microsoft API」の「Azure Active Directory Graph」を選択、「ReadWrite.ALL」にチェックをつけます。
その後、「アプリケーションの許可」を実行します。

<img width="782" alt="スクリーンショット 2020-05-15 18.01.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594267/77ec8f30-552d-abaf-b624-efd488b80041.png">

3.　Runbookを作成する
Powershell Runbookを追加します。
Powershellの処理内容は、後ほどご紹介します。
<img width="1411" alt="スクリーンショット 2020-05-22 13.57.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/594267/f3be3c1f-9013-0307-a90b-f9b6f62c34f0.png">

4.　スケジュールに紐付ける
作成したRunbookを公開し、スケジュールに紐付けて定期的に動作するようにします。

5.　完成
あとは無事に動くことを祈ります。

# Runbook
マイクロソフトの「[Azure Automation の実行アカウントを管理する](https://docs.microsoft.com/ja-jp/azure/automation/manage-runas-account)」と「[Update-AutomationRunAsCredential](https://github.com/azureautomation/runbooks/blob/master/Utility/ARM/Update-AutomationRunAsCredential.ps1)」を参考にしながら作成しました。
基本は「[Update-AutomationRunAsCredential](https://github.com/azureautomation/runbooks/blob/master/Utility/ARM/Update-AutomationRunAsCredential.ps1)」のままですが、実行時にエラーを吐き出す点をいくつか修正し、Webhookによる通知機能を付け足した形になります。

```powershell:Runbook
# ========================================================================================================================================================================================================================= #
# 事前準備
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
# 1. RunbookにAzureADモジュールをインポートする
# 2. 当該AutomationアカウントにAzureADへのアクセス権を付与する（APIを許可する)
# 3. Runbookに登録してスケジューリングする（デフォルトは1週間のキックを想定）
# ========================================================================================================================================================================================================================= #

# ========================================================================================================================================================================================================================= #
# Error発生時には動作を停止するように設定
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
$ErrorActionPreference = 'stop'
# ========================================================================================================================================================================================================================= #

# ========================================================================================================================================================================================================================= #
# 変数初期設定
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
# 名称関連
# 基本は変更しない
$CertifcateAssetName = "AzureRunAsCertificate"
$ConnectionAssetName = "AzureRunAsConnection"
$ConnectionTypeName = "AzureServicePrincipal"

# 残り日数がどれくらいになったら証明書を更新するか
# 1週間に1度の起動であれば、8日に設定すれば良い
$notificationDay = 8

# 通知したいAutomationアカウント名
$Webhook_AutomationAccountName = "hoge"

# Webhookで使用するURI
$Webhook_URI = 'WebhookURI'

# ========================================================================================================================================================================================================================= #

# ========================================================================================================================================================================================================================= #
# Function を設定
# AutomationModuleをインポートする関数
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
Function ImportAutomationModule {
    param(
        [Parameter(Mandatory=$true)]
        [String] $ResourceGroupName,

        [Parameter(Mandatory=$true)]
        [String] $AutomationAccountName,

        [Parameter(Mandatory=$true)]
        [String] $ModuleName,

        [Parameter(Mandatory=$false)]
        [String] $ModuleVersion

    )

    # ModuleNameには"AzureAD"が入る
    $Url = "https://www.powershellgallery.com/api/v2/Search()?`$filter=IsLatestVersion&searchTerm=%27$ModuleName%27&targetFramework=%27%27&includePrerelease=false&`$skip=0&`$top=40"
    $SearchResult = Invoke-RestMethod -Method Get -Uri $Url -UseBasicParsing

    # REST APIを飛ばして、
    if($SearchResult.Length -and $SearchResult.Length -gt 1) {
        $SearchResult = $SearchResult | Where-Object -FilterScript {

            return $_.properties.title -eq $ModuleName

        }
    }

    $PackageDetails = Invoke-RestMethod -Method Get -UseBasicParsing -Uri $SearchResult.id 

    if(!$ModuleVersion) {

        $ModuleVersion = $PackageDetails.entry.properties.version
    }

    $ModuleContentUrl = "https://www.powershellgallery.com/api/v2/package/$ModuleName/$ModuleVersion"

    do {

        $ActualUrl = $ModuleContentUrl
        $ModuleContentUrl = (Invoke-WebRequest -Uri $ModuleContentUrl -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location 

    } while(!$ModuleContentUrl.Contains(".nupkg"))

    $ActualUrl = $ModuleContentUrl

    $AutomationModule = New-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $ModuleName -ContentLink $ActualUrl -AzureRmContext $Context

    while(
        (!([string]::IsNullOrEmpty($AutomationModule))) -and
        $AutomationModule.ProvisioningState -ne "Created" -and
        $AutomationModule.ProvisioningState -ne "Succeeded" -and
        $AutomationModule.ProvisioningState -ne "Failed"

    ){
        Write-Verbose -Message "Polling for module import completion"
        Start-Sleep -Seconds 10
        $AutomationModule = $AutomationModule | Get-AzureRmAutomationModule -AzureRmContext $Context
    }


    if($AutomationModule.ProvisioningState -eq "Failed") {
        Write-Error "Importing $ModuleName module to Automation failed."
    } else {
        $ActualUrl
    }
}
# ========================================================================================================================================================================================================================= #

# ========================================================================================================================================================================================================================= #
# 更新処理
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
# 証明書の期限を獲得し、残りの日数が(Get-Date).AddDays($notificationDay)より大きい場合は更新処理に入る
Write-Output ("CheckPoint_1")
$RunAsCert = Get-AutomationCertificate -Name $CertifcateAssetName
if ($RunAsCert.NotAfter -gt (Get-Date).AddDays($notificationDay)) {
    Write-Output ("Certificate will expire at " + $RunAsCert.NotAfter)
    $expriedDay = $RunAsCert.NotAfter
    $Webhook_MSG = "$Webhook_AutomationAccountName's certificate will expire in $expriedDay "
    $body = ConvertTo-JSON @{text = $Webhook_MSG}
    Invoke-RestMethod -uri $Webhook_URI -Method Post -body $body -ContentType 'application/json'
    Exit(1)
}
Write-Output ("CheckPoint_2")

# RunAsアカウント認証
Write-Output ("CheckPoint_3")
$RunAsConnection = Get-AutomationConnection -Name "AzureRunAsConnection"
Add-AzureRmAccount -ServicePrincipal -TenantId $RunAsConnection.TenantId -ApplicationId $RunAsConnection.ApplicationId -CertificateThumbprint $RunAsConnection.CertificateThumbprint | Write-Verbose

Write-Output ("CheckPoint_4")
$Context = Set-AzureRmContext -SubscriptionId $RunAsConnection.SubscriptionID 
$AutomationResource = Find-AzureRmResource -ResourceType Microsoft.Automation/AutomationAccounts

Write-Output ("CheckPoint_5")
foreach ($Automation in $AutomationResource) {
    $Job = Get-AzureRmAutomationJob -ResourceGroupName $Automation.ResourceGroupName -AutomationAccountName $Automation.Name -Id $PSPrivateMetadata.JobId.Guid -ErrorAction SilentlyContinue
    if (!([string]::IsNullOrEmpty($Job))) {
        $AutomationResourceGroupName = $Job.ResourceGroupName
        $AutomationAccountName = $Job.AutomationAccountName
        break;
    }
}

Write-Output ("CheckPoint_6")
$ADModule = Get-AzureRMAutomationModule -ResourceGroupName $AutomationResourceGroupName -AutomationAccountName $AutomationAccountName -Name "AzureAD" -ErrorAction SilentlyContinue

Write-Output ("CheckPoint_7")
if ([string]::IsNullOrEmpty($ADModule)) {
    # Automationmoduleをインポートする
    $AzureADGalleryURL = ImportAutomationModule -ResourceGroupName $AutomationResourceGroupName -AutomationAccountName $AutomationAccountName -ModuleName "AzureAD"

    # AzureADモジュールをローカルにダウンロードして、インポート
    $LocalFolder = 'C:\AzureAD'
    New-Item -ItemType directory $LocalFolder -Force -ErrorAction SilentlyContinue | Write-Verbose
    (New-Object System.Net.WebClient).DownloadFile($AzureADGalleryURL, "$LocalFolder\AzureAD.zip")
    Unblock-File $LocalFolder\AzureAD.zip
    Expand-Archive -Path $LocalFolder\AzureAD.zip -DestinationPath $LocalFolder\AzureAD -force
    Import-Module $LocalFolder\AzureAD\AzureAD.psd1
}


# RunAs証明書の発行
Write-Output ("CheckPoint_8")
$SelfSignedCertNoOfMonthsUntilExpired = 12
$SelfSignedCertPlainPassword = (New-Guid).Guid
$CertificateName = $AutomationAccountName + $CertifcateAssetName
$PfxCertPathForRunAsAccount = Join-Path $env:TEMP ($CertificateName + ".pfx")
$CerCertPathForRunAsAccount = Join-Path $env:TEMP ($CertificateName + ".cer")

$Cert = New-SelfSignedCertificate -DnsName $CertificateName -CertStoreLocation Cert:\LocalMachine\My -KeyExportPolicy Exportable -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" -NotBefore (Get-Date).AddDays(-1) -NotAfter (Get-Date).AddMonths($SelfSignedCertNoOfMonthsUntilExpired) -HashAlgorithm SHA256

$CertPassword = ConvertTo-SecureString $SelfSignedCertPlainPassword -AsPlainText -Force
Export-PfxCertificate -Cert ("Cert:\LocalMachine\My\" + $Cert.Thumbprint) -FilePath $PfxCertPathForRunAsAccount -Password $CertPassword -Force | Write-Verbose
Export-Certificate -Cert ("Cert:\LocalMachine\My\" + $Cert.Thumbprint) -FilePath $CerCertPathForRunAsAccount -Type CERT | Write-Verbose


# AzureADに接続してアプリケーションを管理する
Write-Output ("CheckPoint_9")
Connect-AzureAD -CertificateThumbprint $RunAsConnection.CertificateThumbprint -TenantId $RunAsConnection.TenantId -ApplicationId $RunAsConnection.ApplicationId | Write-Verbose

# アプリケーションを探す
Write-Output ("CheckPoint_10")
$Filter = "AppId eq '" + $RunasConnection.ApplicationId + "'"
$Application = Get-AzureADApplication -Filter $Filter 


# 新しい証明書をアプリケーションにインストールする
Write-Output ("CheckPoint_11")
New-AzureADApplicationKeyCredential -ObjectId $Application.ObjectId -CustomKeyIdentifier ([System.Convert]::ToBase64String($cert.GetCertHash())) -Type AsymmetricX509Cert -Usage Verify -Value ([System.Convert]::ToBase64String($cert.GetRawCertData())) -StartDate $cert.NotBefore -EndDate $cert.NotAfter | Write-Verbose


# Automationアカウントの新しい証明書で証明書を更新する
Write-Output ("CheckPoint_12")
$CertPassword = ConvertTo-SecureString $SelfSignedCertPlainPassword -AsPlainText -Force
Set-AzureRmAutomationCertificate -ResourceGroupName $AutomationResourceGroupName -AutomationAccountName $AutomationAccountName -Path $PfxCertPathForRunAsAccount -Name $CertifcateAssetName -Password $CertPassword -Exportable:$true | Write-Verbose

# RunAs接続を新しい証明書情報で更新する
Write-Output ("CheckPoint_13")
$ConnectionFieldValues = @{"ApplicationId" = $RunasConnection.ApplicationId ; "TenantId" = $RunAsConnection.TenantId; "CertificateThumbprint" = $Cert.Thumbprint; "SubscriptionId" = $RunAsConnection.SubscriptionId }

# ========================================================================================================================================================================================================================= #
# バグ回避処理(詳細；https://github.com/Azure/azure-powershell/issues/5862)
# ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- #
# Connectionの削除
Remove-AzureRmAutomationConnection -ResourceGroupName $AutomationResourceGroupName -AutomationAccountName $AutomationAccountName -Name $ConnectionAssetName -Force
# Connectionの作成
New-AzureRMAutomationConnection -ResourceGroupName $AutomationResourceGroupName -AutomationAccountName $AutomationAccountName -Name $ConnectionAssetName -ConnectionFieldValues $ConnectionFieldValues -ConnectionTypeName $ConnectionTypeName | Write-Verbose
# ========================================================================================================================================================================================================================= #

Write-Output ("RunAs certificate credentials have been updated")
$Webhook_MSG = "$Webhook_AutomationAccountName's RunAs certificate credentials have been updated"
$body = ConvertTo-JSON @{text = $Webhook_MSG}
Invoke-RestMethod -uri $Webhook_URI -Method Post -body $body -ContentType 'application/json'

```
# 最後に
公式ページの情報がいっぱいあるのは嬉しいですね。

# 注意および免責事項
本記事の内容はあくまで私個人の見解であり、所属する組織の公式見解ではありません。
本記事の内容を実施し、利用者および第三者にトラブルが発生しても、筆者および所属組織は一切の責任を負いかねます。