Summary
- Azure Batch の閉域コンテナ実行環境(Private Endpoint・Managed Identity・NFSv3 マウント)を Bicep で IaC 化し、4時間・約75円の検証コストですぐ立てすぐ消すことができた
- NFSv3 マウントによる透過的な Blob 読み書き・完全閉域ネットワーク下でのコンテナ実行・Batch による stdout/stderr の自動収集と Blob 保管、という3つの検証目的をすべて実機で確認した
- IBM Bob に Plan モードで要件を渡すと実装計画を自動生成し(+人がレビューし、要件に合うよう手動での更新も実施)、その後は Azure Documentation MCP を活用しながら
az bicep build/az bicep lintのエラーが消えるまで自律的に試行錯誤・修正してデプロイまで完走した
やらないこと
- Azure Batch の本番運用・コスト最適化
- Terraform や ARM テンプレートとの比較評価
- BatchノードのAuto scaling
本記事における課題
検証の動機
業務バッチ処理をクラウドへ移行する際の選択肢として Azure Batch が候補に上がった。コンテナで処理を実行でき、タスクごとにスケールする点が魅力だが、対象のアプリには「ファイルシステム上のパスを読み書きする」という仕様があり、コードを変えられない制約がある。
今回の検証で確認したかったことは以下の3点である。
- NFSv3 透過マウント:Azure Blob Storage を NFSv3 でマウントすることで、コンテナアプリが Azure SDK を一切使わず、ローカルファイルを読み書きする感覚でそのまま Blob を読み書きできるか
- 完全閉域ネットワーク:Compute Node をパブリック IP なしで VNet 内に閉じ、ストレージ・ACR・ノード管理通信をすべて Private Endpoint 経由にした構成で、コンテナタスクが問題なく実行できるか
- stdout/stderr の自動収集:Batch の OutputFiles 機能により、アプリ側を一切改変せずにタスクの標準出力・標準エラー出力を Blob に自動保管できるか
加えて、社内ネットワーク要件として以下が求められる想定である。
- Compute Node はパブリック IP を持たせない
- ストレージ・コンテナレジストリへの経路はすべて Private Endpoint 経由
- ノード通信(管理トラフィック)も VNet 内に閉じる
- 認証はすべて Managed Identity(共有キー・管理者パスワード禁止)
これらを満たしつつ、環境を再現性高く構築・破棄できるよう Bicep による IaC 化も必須条件とした。
やったこと
IBM Bob との進め方
今回の検証では、IBM Bob を単なるコード補完ツールとしてではなく、設計から実装・デバッグまでを一貫して委ねる相手として使った。
Plan モードで要件を渡し、実装計画を立てる
まず Bob の Plan モードに対して、上記の要件(閉域ネットワーク構成・Bicep IaC・NFSv3 マウント検証・コンテナダミー実装など)を渡した。Bob はこれを受けて以下のような詳細な実装計画を自動生成した。
- リポジトリ構成(完成形のディレクトリ構造)
- アーキテクチャ(Batch Account のパブリック EP + ノード閉域の設計方針)
- リソース SKU 一覧(必要最小限のプランを明示)
- ネットワーク設計(サブネット CIDR・Private DNS Zone の対象)
- Blob コンテナ設計(
dataコンテナを NFS マウント先・logsを OutputFiles 先) - ダミーコンテナのログ設計(stdout/stderr のフォーマット・各ステップの出力内容)
- サブタスク一覧(10 個のサブタスクに分割・依存順を整理)
この時点でアーキテクチャ上の技術課題も Bob が列挙してくれた。「Managed Identity の循環参照が起きるので先行デプロイが必要」「NFSv3 には HNS と StorageV2 が必須」といった内容は、計画フェーズで Bob が先に気づいて提示してくれたものである。
こうして先に全体の見通しを立てつつ、必要に応じて計画の修正を依頼したり、こちらから計画書を更新して今回の要件を満たすように整えたうえで、実装フェーズに入った。
Bob が選んだ最小コストの SKU 構成
コスト最小化を意識して、ゾーン冗長はすべて無効にしている。
| リソース | SKU / 設定 | 選定理由 |
|---|---|---|
| Batch Compute Node |
Standard_B2s_v2(vCPU 2) |
検証に十分なスペック・最安値帯・ゾーン冗長なし |
| Blob Storage |
Standard_LRS・NFSv3 有効・HNS 有効 |
NFSv3 は LRS / ZRS が対応(GRS / GZRS は非対応)。検証用途なのでゾーン冗長なし LRS を選択 |
| ACR | Premium SKU・ゾーン冗長無効 | Private Endpoint は Premium のみ対応。Basic / Standard では Private Link が使えない。ゾーン冗長は無効化 |
| Log Analytics |
PerGB2018・保持 30 日・Daily Quota 0.1 GB |
課金爆発防止のため上限を明示。Free Tier は新規作成不可 |
Bob は要件を満たしながら検証コストを最小化するための SKU・設定を自動で選択した。ACR は最初から Premium SKU が必要だと判断しており(Private Link は Premium 専用機能)、Daily Quota の上限設定など見落としがちなコスト対策も計画の段階で盛り込まれていた。
Agent モードで自律的に実装・デバッグ
Plan フェーズで生成した計画を渡した後は、Agent モードで Bob が自律的に Bicep コードを書き進めた。
az bicep build / az bicep lint をコード修正のフィードバックループに組み込む
Bob は Bicep を書いた後に自ら az bicep build と az bicep lint を実行し、エラーが出ると原因を特定して修正→再実行を繰り返した。人間が「エラーが出た」と伝える必要がなく、エラーが消えるまで自律的に試行錯誤してくれた。Bicep の文法エラーや型の不一致といったビルドエラーを自分で潰していく様子は、まさに自律的なコーディングループだった。
デプロイ時のエラーも自分で修正
az deployment sub what-if や az deployment sub create を実行し、Azure から返ってきたエラーメッセージを読み取って Bicep を修正した。Private Endpoint の設定ミスや NSG ルールの抜け漏れなど、デプロイしてみないとわからない問題も Bob が反復的に修正した。
Azure Documentation MCP で公式ドキュメントを自律的に参照
Bob には Azure Documentation MCP サーバーを接続した。「Simplified Node Communication に必要な NSG ルール」「NFSv3 の mountOptions の正しい書き方」「ACR の Private Endpoint 対応 SKU」など、Bicep のプロパティ値や制約が不明なケースでは、Bob が MCP 経由で Microsoft Docs を参照してから実装した。人間が URL を渡す必要はなく、Bob 自身が検索・確認して正確な値を使っていた。前述の ACR SKU 選定も、Bob がドキュメントを参照して「Premium のみ対応」と正確に判断した結果である。
アーキテクチャ概要
完成した構成は以下の通りである。ジョブ投入は HTTPS(Entra ID 認証)でインターネット経由を許可し、ノードの実行トラフィックはすべて VNet に閉じるという「ジョブ投入だけ外・ノードは完全閉域」という設計になった。
開発者 PC
│ az batch job/task create(Entra ID / HTTPS)
▼
Batch Account(パブリック エンドポイント)
│
▼
VNet: 10.0.0.0/16
├── batch-subnet (10.0.1.0/24)
│ └── Compute Node (Standard_B2s_v2 / ubuntu-hpc 22.04)
│ └── コンテナ (Node.js)
│ └── /mnt/batch/data/log.txt を読み書き(NFS マウント経由)
└── pe-subnet (10.0.2.0/24)
├── PE: Blob Storage (NFSv3 マウント)
├── PE: ACR (コンテナイメージ pull)
└── PE: Batch nodeManagement (ノード管理通信)
Bicep モジュール構成は以下の 7 本立てにした。
infra/
├── main.bicep # サブスクリプションスコープ エントリポイント
└── modules/
├── batchIdentity.bicep # User Assigned Managed Identity(先行デプロイ)
├── network.bicep # VNet / サブネット / NSG
├── storage.bicep # Blob Storage(NFSv3 + HNS + PEP)
├── acr.bicep # ACR(Basic + PEP)
├── logAnalytics.bicep # Log Analytics Workspace
├── batch.bicep # Batch Account + Pool + nodeManagement PE
└── privateDns.bicep # Private DNS Zones
NFSv3 マウントで Blob をローカルファイルのように読み書きする
Batch Pool の mountConfiguration に NFSv3 の設定を書くと、Compute Node 起動時に Blob Storage の data コンテナが /mnt/batch/data にマウントされる。
mountConfiguration: [
{
nfsMountConfiguration: {
source: '${storageAccountName}.blob.${environment().suffixes.storage}:/${storageAccountName}/data'
relativeMountPath: 'batch/data' // → /mnt/batch/data にマウントされる
mountOptions: 'vers=3,proto=tcp,sec=sys'
}
}
]
コンテナ側のアプリ(Node.js)は Azure SDK を一切使っていない。Node.js 標準の fs モジュールだけで /mnt/batch/data/log.txt を読み書きするだけである。
// Azure SDK なし・標準ライブラリの fs だけ
const existingContent = fs.readFileSync('/mnt/batch/data/log.txt', 'utf8');
fs.appendFileSync('/mnt/batch/data/log.txt', entry, 'utf8');
タスクを複数回投入するたびに log.txt に行が累積されていき、異なるタスク間でもファイルが共有された。NFSv3 マウントが正常に機能すれば、既存のファイル I/O コードをほぼそのまま持ち込めることが確認できた。
開発中に気がついたポイント
Managed Identity は先に作らなければならない
Batch Account・Pool に付与する User Assigned Managed Identity は、Storage および ACR に対してロールを割り当てる際に principalId を参照する。Batch モジュールと同一 dependsOn ツリーに入れると循環参照になるため、batchIdentity.bicep という専用モジュールで先行デプロイし、その output を後続モジュールへ渡すパターンを採用した。
// batchIdentity.bicep を先に実行し principalId を取得
module batchIdentityModule 'modules/batchIdentity.bicep' = {
name: 'batchIdentity'
scope: rg
params: { ... }
}
// Storage モジュールに principalId を渡してロール割り当て
module storage 'modules/storage.bicep' = {
params: {
batchIdentityPrincipalId: batchIdentityModule.outputs.principalId
...
}
}
これは Bob が実装計画の段階で事前に指摘してくれた課題で、設計時点で正しい構造を採用できた。
NFSv3 には HNS + StorageV2 が必須、GRS / GZRS は使えない
NFSv3 を有効化するには isHnsEnabled: true(階層型名前空間)と kind: StorageV2 の両方が必要で、さらにレプリケーションは LRS または ZRS のみ対応している(GRS・GZRS・RA-GRS は非対応)。Standard_GRS に変更しようとするとデプロイエラーになる。検証用途なのでゾーン冗長なし Standard_LRS を選択した。加えて NFSv3 はプロトコルレベルで HTTP も使用するため、supportsHttpsTrafficOnly: false に設定しなければならない(Private Endpoint 閉域内であれば許容できるトレードオフである)。
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
sku: { name: 'Standard_LRS' } // GRS 不可
kind: 'StorageV2'
properties: {
isHnsEnabled: true // NFSv3 に必須
isNfsV3Enabled: true
supportsHttpsTrafficOnly: false // NFSv3 のプロトコル要件
allowSharedKeyAccess: false // 共有キー無効(Managed Identity のみ)
publicNetworkAccess: 'Disabled'
...
}
}
Bob は az bicep lint の警告でこの制約に気づき、supportsHttpsTrafficOnly: false の設定を自律的に追加した。
ACR は Premium SKU でないと Private Endpoint が使えない
ACR の Private Endpoint(Private Link)は Premium SKU 専用の機能である。Basic・Standard では Private Endpoint を作成できない。最初は検証コストを下げるために Basic SKU を検討したが、Bob が Azure Documentation MCP で公式ドキュメントを確認し、「Private Endpoint は Premium SKU のみ対応」と指摘したため Premium に変更した。
コンテナイメージの push については、Premium を使っても閉域構成では外からプッシュできない問題は残る。ローカル PC から az acr login + docker push を行うにはパブリックアクセスが必要なため、プッシュ時だけ一時開放する運用を取らざるを得なかった。
# プッシュ前にパブリックアクセスを一時開放
az acr update --name <ACR名> --public-network-enabled true
docker push ${ACR_LOGIN_SERVER}/batch-dummy:latest
# プッシュ後に閉じる
az acr update --name <ACR名> --public-network-enabled false
本番環境では専用線を介して push する想定だが、今回は「検証用途なので許容」という判断をしている。
ノードが起動しても Unusable になり続ける問題
今回の検証で最も時間を取られたのがこの問題である。az batch node list でノードの状態を確認すると、起動はするが unusable のまま止まり続けてタスクを受け付けない状態が続いた。
Batch エージェントがノードに自動インストールされる際、ノードは Batch の nodeManagement エンドポイントへ通信しようとする。この通信が失敗するとエージェントの起動自体ができず、ノードが unusable になる。
この問題は最初の実装計画フェーズでも Bob の初版 Bicep にもすり抜けてしまった。原因は2つ重なっていた。
原因 1:Private DNS Zone の VNet リンクが Pool より先に適用されていなかった
Bicep のデプロイは内部的に並列実行されるモジュールがある。privateDns.bicep(DNS ゾーン + VNet リンク作成)と batch.bicep(Pool 作成)のデプロイ順が保証されておらず、DNS が VNet に反映される前にノードが起動し、nodeManagement エンドポイントの名前解決に失敗していた。
公式ドキュメントのトラブルシュートにも明記されている通り、privatelink.batch.azure.com ゾーンが VNet にリンクされていないと、ノードは nodeManagement エンドポイントに到達できず unusable になる。
原因 2:nodeManagement PE の A レコードが正しく登録されていなかった
privateDns.bicep では PE の NIC から IP を取得して A レコードを明示的に書くアプローチを採用していたが、Batch の nodeManagement エンドポイントは <accountName>.<region>.service.batch.azure.com という FQDN を持ち、DNS ゾーン内の A レコードのサブドメイン名を <accountName>.<region> とする必要がある。最初の実装では <accountName> のみで登録しており、名前解決が通らなかった。
// 正しい A レコード名の例(<accountName>.<region> 形式)
resource batchNodeDnsRecord 'Microsoft.Network/privateDnsZones/A@2020-06-01' = {
parent: batchDnsZone
name: '${batchAccountName}.${location}' // 例: batchXXXXXX.japaneast
properties: {
ttl: 10
aRecords: [{ ipv4Address: batchNodePeNic.properties.ipConfigurations[0].properties.privateIPAddress }]
}
}
この2点を修正した後、ノードが正常に idle 状態に遷移してタスクを受け付けるようになった。診断方法としては、ノードと同じサブネット内の VM(またはノード上)で以下のコマンドが有効である。
# nodeManagement エンドポイントの DNS 解決確認(Private IP が返れば OK)
nslookup <accountName>.<region>.service.batch.azure.com
# TCP 接続確認
nc -v <accountName>.<region>.service.batch.azure.com 443
Batch Simplified Node Communication と NSG 設定
旧来の Classic Node Communication は 2026 年 3 月で廃止予定されている。Simplified では nodeManagement グループの Private Endpoint を作成し、プールに targetNodeCommunicationMode: 'Simplified' を指定する。
NSG のアウトバウンド設定も Simplified 準拠にする必要がある。Bob が Azure Documentation MCP で Microsoft Docs を参照し、BatchNodeManagement サービスタグへの TCP 443 ルールを漏れなく生成してくれた。
{
name: 'Allow-Outbound-BatchNodeManagement'
properties: {
priority: 100
direction: 'Outbound'
protocol: 'Tcp'
destinationAddressPrefix: 'BatchNodeManagement.${location}'
destinationPortRange: '443'
}
}
Private DNS ゾーンを Bicep 内で組む落とし穴
Azure Portal を使った Private Endpoint 作成時は「Private DNS Zone を自動統合する」オプションが使えるが、Bicep で Microsoft.Network/privateEndpoints/privateDnsZoneGroups を書いても、デプロイのタイミング問題で A レコードが空になるケースがあった。
最終的に採用したのは、Private Endopoint の NIC リソース(自動生成される)から privateIPAddress を取得して A レコードを明示的に書くアプローチである。
resource storagePeNic 'Microsoft.Network/networkInterfaces@2023-09-01' existing = {
name: last(split(storagePeNicId, '/'))
}
resource blobDnsRecord 'Microsoft.Network/privateDnsZones/A@2020-06-01' = {
parent: blobDnsZone
name: storageAccountName
properties: {
ttl: 10
aRecords: [
{
ipv4Address: storagePeNic.properties.ipConfigurations[0].properties.privateIPAddress
}
]
}
}
Private Endpoint の NIC ID は各モジュールの output として main.bicep に持ち上げ、privateDns.bicep に渡す設計にした。Private Endpoint 作成後の NIC ID は privateEndpoint.properties.networkInterfaces[0].id で取得できる。Bob は az deployment sub create のエラー出力からこの問題を特定し、アプローチを切り替えた。
OutputFiles の identityReference を忘れると動かない
タスクの stdout/stderr を Blob Storage へ自動アップロードする outputFiles 機能では、共有キーアクセスを無効にしている場合は identityReference を明示しなければならない。省略すると Batch は SAS トークン生成を試みるが、共有キーが使えないためエラーになる。
"outputFiles": [
{
"filePattern": "../stdout.txt",
"destination": {
"container": {
"containerUrl": "https://<storage>.blob.core.windows.net/logs",
"identityReference": {
"resourceId": "<ManagedIdentityのリソースID>"
},
"path": "<TASK_ID>/stdout.txt"
}
},
"uploadOptions": { "uploadCondition": "taskCompletion" }
}
]
エラーメッセージが「SharedKeyCredential is not allowed」という形で現れるが、OutputFiles の設定ミスとは一見結びつきにくい。
動作確認の流れ
デプロイ後の動作確認は以下の流れで実施した。
-
az batch node listでノードがidleになっていることを確認 -
az batch job createでジョブを作成 -
az batch task showでタスクがcompleted / exitCode=0になるまで待機 -
Log Analytics の KQL クエリで
TaskCompleteEventを確認
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.BATCH"
| where OperationName == "TaskCompleteEvent"
| project TimeGenerated, jobId_s, id_s, executionInfo_exitCode_d, executionInfo_result_s
| order by TimeGenerated desc
複数回タスクを再投入すると data/log.txt に行が累積されていく動作も確認できた。
2025-07-01T12:00:00.005Z | job=batch-job-20250701120000 | task=task-20250701120000 | done
2025-07-01T13:00:00.005Z | job=batch-job-20250701130000 | task=task-20250701130000 | done
リソースの削除
検証完了後は az group delete でリソースグループごと一括削除した。IaC 化していると「消す」操作も一発である点が、スポット検証との相性の良さだと改めて感じた。
az group delete --name "$RG_NAME" --yes --no-wait
実際にここまででかかったコストを確認すると、わずか 74.05 円だった。
ちょっと宣伝
この記事もBobに下書きしてもらいました!
生成AIによる開発プロセスの効率化に関心がある場合は、IBMのAIエージェント製品コミュニティであるBobサロンのような場もおすすめです!実際にBobを活用されているユーザー様の声にも触れてみてください!
私がBobを試した記事もご参考になれば幸いです。
- IBM Bob で作る Personal Knowlege Management デスクトップアプリ on Tauri
- WebブラウザからIBM Bobを使ってみた ― ACPで開発環境を変えずにAIエージェントを使う
- IBM Bobに自作MCPサーバーを組み合わせ、他社AIにレビューさせてみた
- IBM Bob + Foam で構築する Personal Knowledge Management 環境
- IBM Bobを使って就活管理アプリをExcel VBAからWebアプリへ移行した話
- IBM BobでAWS&AzureのDocumentation MCPサーバーを使ったクラウドサービス比較レポートを作成させてみた
- IBM Bobと一緒にOpen Notebookのモバイル版UIを開発してみた
まとめ・所感
当初の3つの検証目的はすべて実機で確認できた。NFSv3 マウントで Blob をローカルファイルのように読み書きでき、Azure SDK の組み込みは不要だった。完全閉域ネットワーク下でもコンテナタスクは問題なく動作し、Batch の OutputFiles 機能によってアプリを一切変えずに stdout/stderr が Blob に自動保管された。
Azure Batch の閉域コンテナ実行は、設定の組み合わせが多く「何かに阻まれて動かない」という場面が連続した。特に Managed Identity の先行デプロイ、NFSv3 の StorageV2 + LRS 制約、OutputFiles の identityReference 要件は、いずれも公式ドキュメントに記載はあるが散在しており、一度試行錯誤しないと気づきにくい。
IBM Bob との使い方として今回効果的だったのは、Plan モードで要件を渡して全体設計を先行させることである。技術課題の洗い出し・SKU 選定・サブタスク分割まで計画フェーズで整理されていたため、実装フェーズでの手戻りが少なかった。Agent モードでは az bicep build / az bicep lint のフィードバックループを Bob 自身が回し、デプロイエラーも自律的に修正した。Microsoft Docs を参照しながら正確なプロパティ値を使う挙動も、人間のレビューコストを大きく下げてくれた。
4 時間・約 75 円という検証コストで、本番相当の閉域構成を実機確認できたのは Bicep IaC と Bob の組み合わせによるところが大きい。「すぐ立ててすぐ消す」検証スタイルとの相性が非常に良く、今後の技術検証にも同じアプローチを使いたい。






