1. はじめに
近年、Webアプリケーションの脆弱性を狙った攻撃はますます巧妙化しており、セキュリティ対策は「導入して終わり」ではなく、継続的な診断と改善が求められています。
OWASP ZAP は、オープンソースの脆弱性診断ツールとして広く利用されており、GUI 操作による手軽な診断から、CI/CD やクラウド環境への組み込みまで、柔軟な運用が可能です。
前回の記事では、ローカル環境に OWASP ZAP をインストールし、GUI を使って手動で脆弱性診断を行う方法を紹介しましたが、今回は、Azure Automation Runbook × Azure Container Instance × Azure Storage を組み合わせて、OWASP ZAP による脆弱性診断を自動実行する構成を紹介します。
GUI 操作に頼らず、完全にクラウド上で診断を完結させることで、運用負荷を軽減しつつ、継続的なセキュリティチェックを実現します。
診断の実行頻度はシステムの重要度や変更頻度に応じて柔軟に設定可能です。
月に1回や四半期に1回の診断でも、十分なセキュリティ維持に貢献するケースは多く、過剰な頻度によるリソース消費を避けることも重要です。
また、本記事は「セキュリティ診断の自動化」に関心がある方だけでなく、Azure の Runbook や ACI を使ってみたいという練習目的の方にもおすすめです。
クラウドサービスの操作経験を積みながら、セキュリティの実践的な知識も得られる構成となっています。
2. 脆弱性診断の自動実行のメリット
脆弱性診断は、セキュリティ対策の中でも「見えづらいが重要な工程」です。
手動での診断は、ツールの起動・対象の設定・レポートの取得など、
毎回同じ作業を繰り返す必要があり、担当者の負担が大きくなりがちです。
さらに、診断のタイミングが人に依存するため、実施漏れやタイミングの遅れが発生するリスクもあります。
自動化することで、以下のようなメリットが得られます
✅ 定期的な診断の実施が担保される
– スケジュールに従って診断が実行されるため、忘れたり後回しにされることがありません。
✅ 最新版の診断エンジンを常に使用できる
– OWASP ZAP の公式 Docker イメージを毎回 pull することで、手動運用でありがちな「バージョンアップの見落とし」や「古い定義での診断」を防げます。
✅ 運用負荷の軽減
– GUI 操作や手動レポート取得が不要になり、担当者は結果の確認と対応に集中できます。
✅ セキュリティ意識の定着
– 定期的な診断が組織の習慣となることで、セキュリティへの意識が自然と高まります。
3. 全体構成
本構成では、Azure Automation Runbook(以降,Runbookと表記) をトリガーとして、Azure Container Instance(以降,ACIと表記)上で OWASP ZAP を起動し、診断結果を Azure Blob Storage に保存します。
以下の図は、全体の構成と処理の流れを示しています。

| サービス名 | 概要 |
|---|---|
| Azure Automation Runbook | 処理制御(SAS トークン生成、ACI 起動) |
| Azure Container Instance(ACI) | OWASP ZAP のコンテナ実行環境 |
| Azure Blob Storage | 診断結果の保存先 |
| Docker Hub | ZAP の公式 Docker イメージ ( zaproxy/zap-weekly:latest) |
| SAS トークン認証 | Storage へのセキュアなアクセス手段 |
| RBAC(ロールベースアクセス制御) | Automation に必要な権限を付与 |
4. ハンズオン手順
ステップ1: ストレージアカウントの準備
まずは診断結果を保存するためのストレージアカウントを作成します。
今回紹介する構成ではセキュリティを意識して下記の2点を設定します。
ストレージアカウントはプライベート設定
→ 外部からのアクセスはすべて拒否し、SAS トークン経由のみ許可。
Blob コンテナは非公開(Private)
→ 匿名アクセス不可。 ACI からのアクセスは SAS トークンを使用。
SAS(Shared Access Signature)トークンは、ストレージアカウントに対して一時的かつ限定的なアクセス権を付与するための認証情報です。
ストレージアカウントやBlobコンテナを「完全公開」にすることなく、必要な操作だけを安全に許可できます。
大きな特徴を下記にまとめました。
- 有効期限を設定可能。例えば有効期限を生成から1時間と設定することも可能。
- 操作の制限が可能。読み取り・書き込み・削除などを細かく制御することができる。
- URLに埋め込んで使用。HTTPリクエスト時に簡単に利用することができる。Runbook や ACI からのファイルアップロード時に利用
では、さっそくリソースを作成しましょう。
Azure portalから [ストレージアカウント]へ移動し、[作成]ボタンをクリックします。

最初は[基本情報]タブです
サブスクリプションやリソースグループ、ストレージアカウント名に任意の値を入れ[次へ]をクリックします。

次は[詳細]タブです。
ここもデフォルトでOKですが、[個々のコンテナーでの匿名アクセスの有効化を許可する]のチェックは必ず外してください。

次に[ネットワーク]タブですが、こちらもデフォルト設定でOKです。

タグについては任意です。
[レビューと作成]をクリックして診断ファイルを格納するコンテナを作成します。
最後に作成したストレージアカウントの左メニュー[データストレージ] → [コンテナー] → [コンテナーの追加]をクリック。
匿名アクセスレベルが プライベートになっていることを確認できればOKです。

ステップ2: Azure Automation アカウントの作成
次にAzure Automation アカウントを作成します。
Azure Automation は、クラウド上で定期的なタスクや運用処理を自動化できるサービスです。
その中核となるのが Runbook(ランブック)という機能です。
Runbook は、PowerShell や Python などのスクリプトを記述・実行するための単位であり、Azure Automation 上で定期的に実行したり、他のサービスから呼び出したりすることができます。
今回はこのRunbookでOWASP ZAPをコンテナに展開し、脆弱性診断、診断結果のアップロードを実行します。
Automationアカウント の作成手順
Azure portalから [Automation アカウント]へ移動し、[作成]ボタンをクリックします。
リソースグループやリソース名は任意のものを設定し[次へ]をクリック。

[マネージドID]-[システム割り当て]にチェックを入れ、[次へ]をクリック。

ネットワーク接続は[パブリックアクセス]を選択して[次へ]をクリック。

タグについては任意で設定してください。
最後に[確認および作成]画面が表示されますので設定が正しいことを確認してください。
[作成]をクリックするとAutomationアカウントが作成されます。

ステップ2.1 マネージドIDの設定
Automationアカウントの作成が完了したら、次はこのAutomationアカウントのマネージドIDの設定を行います。
AzureマネージドIDはAzureリソースが安全に他のAzureサービスにアクセスするための仕組みです。
ロール(権限)を付与することで、下記のような処理が可能となります。
・RunbookがACIを作成/起動する。
・RunbookがBlobストレージにアクセスするためのSASトークンを生成する。
作成したAutomationアカウントの左メニュー[アカウント設定] → [ID] → [Azure ロールの割り当て]をクリック。

ページ上部の[ロールの割り当ての追加(プレビュー)]をクリック。

ページの右側に設定用の画面が表示されるので下記の値を設定します。
スコープ: [リソースグループ]
サブスクリプション: 自身のサブスクリプション
リソースグループ: 既に作成したストレージアカウントと同じリソースグループを指定する。
役割: Azure Container Instances Contributor Role
付与したロール(役割)は Contributor = 共同作成者です。
つまり、ACIリソースに対して作成や設定変更の権限を持つことになります。

続けて,同じ手順でBlobstorageのSASトークンを生成するために操作用のロールを付与します。
スコープ: [リソースグループ]
サブスクリプション: 自身のサブスクリプション
リソースグループ: 既に作成したストレージアカウントと同じリソースグループを指定する。
役割: ストレージ BLOB データ共同作成者
こちらのロール(役割)は日本語に訳されていますね。
先ほどのACIと同様にBLOBストレージに対して作成や設定変更の権限を持つことになります。

ステップ2.2 Automation Runbookの作成

作成したAutomationアカウントの左メニュー[プロセスオートメーション] → [Runbook] → [Runbookの作成]をクリックして下記の値を設定します。
名前: 任意
Runbookの種類: PowerShell
ランタイムバージョン: 7.2
説明: 任意

確認および作成をクリックしてRunbookの作成が完了すると、自動的にRunbookの編集画面に遷移します。
ステップ3: 脆弱性診断自動実行スクリプトの作成
スクリプトエディタ部分をクリックするとコードを記述することができるようになります。

このRunbookで実行される大まかな処理の流れは下記になります。
1. OWASP ZAP の Docker イメージを使って ACI を作成
2. 指定した診断対象に脆弱性スキャンを実行
3. スキャン結果(HTML形式)を Blob Storage にアップロード
この処理を実現する実際のコードは下記です。
# 認証(マネージド ID)
Connect-AzAccount -Identity
$context = Get-AzContext
# ストレージ情報
$resourceGroup = "<RESOURCE_GROUP_NAME>" # リソースグループ名
$storageAccountName = "<STORAGE_ACCOUNT_NAME>" # ストレージアカウント名
$containerName = "<BLOB_CONTAINER_NAME>" # Blob コンテナ名
$location = "<LOCATION>" # リージョン名(例: japaneast)
$targetUrl = "<TARGET_URL>" # 診断対象の URL(例: https://example.com)
$containerGroupName = "<CONTAINER_GROUP_NAME>" # ACI のコンテナグループ名
$imageName = "zaproxy/zap-weekly:latest" # Docker イメージ名(今回は zaproxy/zap-weekly:latest)
Write-Output "START!!!"
# ストレージアカウントのコンテキスト取得
$context = New-AzStorageContext -StorageAccountName $storageAccountName -UseConnectedAccount
Write-Output "コンテキスト取得完了"
# Blobに格納するファイル名の生成
$timestamp = Get-Date -Format "yyyy-MM-dd-HHmm"
$blobName = "report-$timestamp.html"
# SAS トークン生成
$expiryTime = (Get-Date).AddHours(1)
$sasToken = New-AzStorageBlobSASToken `
-Container $containerName `
-Blob $blobName `
-Context $context `
-Permission rwd `
-ExpiryTime $expiryTime `
-FullUri
# 環境変数定義
$envVars = @(
New-AzContainerInstanceEnvironmentVariableObject -Name "TARGET_URL" -Value "$targetUrl"
New-AzContainerInstanceEnvironmentVariableObject -Name "SAS_URL" -Value "$sasToken"
)
# コンテナポート定義
$containerPort = New-AzContainerInstancePortObject -Port 8080 -Protocol TCP
# コンテナ定義
$container = New-AzContainerInstanceObject `
-Name "zap" `
-Image $imageName `
-Command @(
"sh", "-c",
'echo "Running ZAP..." && zap.sh -cmd -quickurl "$TARGET_URL" -quickout /home/zap/report.html && echo "ZAP scan completed" && ls -l /home/zap/report.html && curl -v -X PUT -T /home/zap/report.html "$SAS_URL"'
) `
-EnvironmentVariable $envVars `
-RequestCpu 1 `
-RequestMemoryInGb 2 `
-Port @($containerPort)
# コンテナグループポート定義
$groupPort = New-AzContainerGroupPortObject -Port 8080 -Protocol TCP
# コンテナグループ作成
New-AzContainerGroup `
-ResourceGroupName $resourceGroup `
-Name $containerGroupName `
-Location $location `
-OsType "Linux" `
-RestartPolicy "Never" `
-Container $container `
-IpAddressType "Public" `
-IpAddressDnsNameLabel "zapscan-aci" `
-IpAddressPort @($groupPort)
Write-Output "コンテナ作成完了: $containerGroupName"
# コンテナの完了を待機(タイムアウト付きポーリング)
$maxWait = 600 # 最大待機時間(秒)
$elapsed = 0
$state = ""
do {
Start-Sleep -Seconds 10
$elapsed += 10
$state = (Get-AzContainerGroup -ResourceGroupName $resourceGroup -Name $containerGroupName).InstanceView.State
Write-Output "現在の状態: $state ($elapsed 秒経過)"
} while ($state -ne "Terminated" -and $state -ne "Succeeded" -and $state -ne "Failed" -and $elapsed -lt $maxWait)
if ($elapsed -ge $maxWait) {
Write-Output "タイムアウトしました。コンテナが終了しませんでした。"
} else {
Write-Output "コンテナが終了しました。状態: $state"
}
# コンテナ完了後にログ取得
$log = Get-AzContainerInstanceLog `
-ResourceGroupName $resourceGroup `
-ContainerGroupName $containerGroupName `
-ContainerName "zap"
Write-Output "=== コンテナログ ==="
Write-Output $log
少々長いのでスクリプトの主要部分を分割して解説します。
設定値一覧
$resourceGroup = "<RESOURCE_GROUP_NAME>" # リソースグループ名
$storageAccountName = "<STORAGE_ACCOUNT_NAME>" # ストレージアカウント名
$containerName = "<BLOB_CONTAINER_NAME>" # Blob コンテナ名
$location = "<LOCATION>" # リージョン名(例: japaneast)
$targetUrl = "<TARGET_URL>" # 診断対象の URL(例: https://example.com)
$containerGroupName = "<CONTAINER_GROUP_NAME>" # ACI のコンテナグループ名
$imageName = "zaproxy/zap-weekly:latest" # Docker イメージ名(今回は zaproxy/zap-weekly:latest)
各リソースの名前や診断対象のURLを記述します。
今回はハンズオン形式なのでスクリプトに直接記入していますが
必要に応じてAzureAutomationの[変数]機能を利用したり,外部設定ファイルを参照するようにしてください。
SASトークンの生成, レポートファイル名の生成, 環境変数設定
# === レポートファイル名の生成 ===
# Blobに格納するファイル名の生成
$timestamp = Get-Date -Format "yyyy-MM-dd-HHmm"
$blobName = "report-$timestamp.html"
アップロードされるレポートファイルが上書きされないようタイムスタンプを付けてユニークなファイル名を作成します。
# === SAS トークン生成 ===
# SAS トークン生成
$expiryTime = (Get-Date).AddHours(1)
$sasToken = New-AzStorageBlobSASToken `
-Container $containerName `
-Blob $blobName `
-Context $context `
-Permission rwd `
-ExpiryTime $expiryTime `
-FullUri
レポートファイルをアップロードする際のSASトークンを作成します。
この時、Runbookに[ストレージ BLOB データ共同作成者]ロールが付与されていないと作成に失敗しますので注意してください。
# === 環境変数の定義 ===
# 環境変数定義
$envVars = @(
New-AzContainerInstanceEnvironmentVariableObject -Name "TARGET_URL" -Value "$targetUrl"
New-AzContainerInstanceEnvironmentVariableObject -Name "SAS_URL" -Value "$sasToken"
)
Runbookで定義したPowerShell 変数は、コンテナ内の Linux シェルから直接参照できません。
そのため、Runbook内で定義した診断対象URLやSASトークンなどの値をコンテナの環境変数として再定義することで、シェルスクリプト内からも参照が可能となります。
脆弱性診断の実行,診断結果のアップロード
# === コンテナの定義 ===
$container = New-AzContainerInstanceObject `
-Name "zap" `
-Image $imageName `
-Command @(
"sh", "-c",
'echo "Running ZAP..." && zap.sh -cmd -quickurl "$TARGET_URL" -quickout /home/zap/report.html && echo "ZAP scan completed" && ls -l /home/zap/report.html && curl -v -X PUT -T /home/zap/report.html "$SAS_URL"'
) `
-EnvironmentVariable $envVars `
-RequestCpu 1 `
-RequestMemoryInGb 2 `
-Port @($containerPort)
-commandには起動したOWASP ZAPコンテナに実行してもらいたいコマンドを記述しています。
少し長くなっていますが2つのコマンドが実行されますので分割してみましょう。
1つ目のコマンド
zap.sh -cmd -quickurl "$TARGET_URL" -quickout /home/zap/report.html
zap.shは脆弱性診断を行うためのスクリプトです。
$TARGET_URL(診断対象)を引数として渡しており、そのURLに対して脆弱性診断を実施しています。レポートファイルは一旦、コンテナ内の/home/zap/report.htmlに出力されます。
2つ目のコマンド
curl -v -X PUT -H "x-ms-blob-type: BlockBlob" -T /home/zap/report.html "$SAS_URL"
レポートファイルをBlobストレージにアップロードします。
$SAS_URL(SASトーxクン)を利用しているのでreport-$timestamp.htmlという名前でコンテナに格納されます。
繰り返しになりますが、
Runbookで定義したPowerShell 変数は、コンテナ内の Linux シェルから直接参照できません。
このコマンドでは コンテナの環境変数として定義した変数(サンプルコードだと、$TARGET_URL,$SAS_URL)を必ず参照するようにしてください。
# === コンテナの完了を待機(ポーリング) ===
# コンテナの完了を待機(タイムアウト付きポーリング)
$maxWait = 600 # 最大待機時間(秒)
$elapsed = 0
$state = ""
do {
Start-Sleep -Seconds 10
$elapsed += 10
$state = (Get-AzContainerGroup -ResourceGroupName $resourceGroup -Name $containerGroupName).InstanceView.State
Write-Output "現在の状態: $state ($elapsed 秒経過)"
} while ($state -ne "Terminated" -and $state -ne "Succeeded" -and $state -ne "Failed" -and $elapsed -lt $maxWait)
if ($elapsed -ge $maxWait) {
Write-Output "タイムアウトしました。コンテナが終了しませんでした。"
} else {
Write-Output "コンテナが終了しました。状態: $state"
}
診断に時間がかかることを考慮して明示的に10分ほど待機するようにしています。
また、経過秒数などをWrite-Outputで標準出力しています。
今回、記述するコードは以上となります。
コードを書き終えたら編集画面の左上、[保存] -> [公開]の順番でクリックします。

その後、対象のRunbookで[開始]をクリックすると、作成したコードが実行されます。


[出力]タブにはコード内の[Write-Output]などの標準出力が表示されます。
処理に失敗するときは[エラー]や[例外]タブの出力を確認しましょう。
自動では更新されないので、画面上部の[最新の状態に更新]をクリックして出力の更新を行ってください。
Runbookの状態が[完了]になれば処理は完了となります。
診断結果ファイルがBlobストレージにアップロードされているはずなので結果を確認します。
ステップ4: 診断結果の確認
Azure portalから [ストレージアカウント]へ移動し、左メニューから[コンテナ]をクリック。
アップロードパスに指定したコンテナに診断結果ファイルがアップロードされていることを確認します。

診断レポートの内容に関しては前回の記事で紹介しております。
ステップ5: スケジュール設定
最後に、定期実行のためのスケジュール設定を行います。
対象Runbookの左メニュー[リソース]→[スケジュール] →[スケジュールの追加]をクリックする。

スケジュール設定画面が表示されます。[スケジュール]をクリックします。

[スケジュールの追加]をクリックすると設定画面が右側に表示されます。
実行頻度(例:毎週月曜 9:00)やタイムゾーンなどが設定できます。
今回は下記の値でスケジュールを設定します。
タイムゾーン: Japan- Japan Time
繰り返し: 定期的
間隔: 3, 月
月の指定した日に実行: 1日
月の最終日に実行: いいえ
有効期限の設定: いいえ

[作成]をクリックするとスケジュール設定画面に戻りますので、今度は[パラメータと実行設定]をクリックします。
今回は[実行対象]にAzureを選択して[OK]をクリックします。

またスケジュール設定画面に戻りますので、ページ下部の[OK]をクリックします。

[スケジュール]に先ほど追加したスケジュールが追加されればスケジュール設定は完了となります。
以上で、OWASP ZAPの自動実行環境の構築が完了しました。
本記事では、OWASP ZAP を Azure Automation と Azure Container Instance を活用して、3ヶ月に1度のペースで自動実行するシステムを構築しました。
Logic Apps や Azure Functions を使って、柔軟なスケジューリング(例:第1月曜日、営業日ベースなど)や脆弱性診断実行完了時にSlackで通知することも可能です。
OWASP ZAPは他にも、CI/CDパイプラインと組み合わせることで、開発プロセスの中にもセキュリティを組み込む「DevSecOps」的な運用も多くみられます。
今後は、CI/CD パイプラインに OWASP ZAP を組み込む方法についてもご紹介する予定ですので、ぜひご期待ください!

