A standalone Terraform module that provisions a single Azure Batch compute-node pool β
azurerm_batch_pool.thisβ with its full nested configuration surface (scale mode, start task, mounts, identities, extensions, user accounts, and network settings), targetinghashicorp/azurerm ~> 4.0.
This module manages a single Azure Batch pool and the deeply-typed nested blocks that shape its compute fleet:
- π₯οΈ The node fleet β
vm_size,node_agent_sku_id, and a requiredstorage_image_reference(platform image or custom image id). - βοΈ Scale mode β either a
fixed_scaletarget count or anauto_scaleformula (mutually exclusive). - π Start task β a command run on every node as it joins, with
user_identity, container settings, and resource files. - πΎ Mounts β Azure Blob (blobfuse), Azure File Share, CIFS, and NFS file systems.
- π Identity & security β a user-assigned managed identity, plus a trusted-launch
security_profile(secure boot / vTPM). - π Networking β an optional
network_configurationthat joins nodes to a subnet with endpoint and NSG rules. - π§© Extras β extensions, certificates, data disks, disk encryption, user accounts, Windows settings, node placement, and task scheduling policy.
π‘ Why it matters: the pool is where Batch actually runs work. A malformed nested field (a wrong scale mode, a bad mount, a plaintext registry password) usually fails late, at the Azure API, after a slow apply. This module models the entire block structure as typed
object()schemas so those mistakes surface atplantime, and it wraps every secret leafsensitive()so credentials never render in plan output.
If this module saves you time, please consider supporting the work:
- β Star the repository β it helps others find it.
- πΌ Connect on LinkedIn β linkedin.com/in/microsoftexpert
- β Buy me a coffee β buymeacoffee.com/microsoftexpert
The pool sits downstream of the Batch account, the identity it runs as, and the network it joins; jobs and tasks are scheduled onto it.
flowchart LR
acct["terraform-azurerm-batch-account"]
uai["terraform-azurerm-user-assigned-identity"]
subnet["terraform-azurerm-virtual-network"]
me["terraform-azurerm-batch-pool"]
v["azurerm_batch_pool"]
job["terraform-azurerm-batch-job"]
acct -->|"account_name"| me
uai -->|"identity_ids"| me
subnet -->|"subnet_id"| me
me -->|"creates"| v
v -->|"batch_pool_id"| job
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
class me me;
class v target;
class acct,uai,subnet,job ext;
A single keystone resource fed by required identity/image inputs, one of the two scale blocks, and a set of optional nested blocks whose secret leaves are wrapped before they reach the resource.
flowchart LR
in_id["name / account_name / vm_size / node_agent_sku_id"]
in_img["storage_image_reference"]
in_scale["fixed_scale XOR auto_scale"]
in_blk["start_task Β· mount Β· extensions Β· user_accounts Β· identity Β· network_configuration"]
res["azurerm_batch_pool.this"]
out_id["id"]
out_name["name"]
in_id -->|"input"| res
in_img -->|"input"| res
in_scale -->|"input"| res
in_blk -->|"input (secrets wrapped)"| res
res -->|"output"| out_id
res -->|"output"| out_name
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
class res me;
Resource inventory β one keystone resource, azurerm_batch_pool.this, rendering these nested blocks:
| Block | Cardinality | Notes |
|---|---|---|
storage_image_reference |
single (required) | Platform image (publisher/offer/sku/version) or custom id. |
fixed_scale / auto_scale |
one of | Mutually exclusive scale modes. |
identity |
single | UserAssigned only. |
start_task |
single | user_identity required; nested container β registry, and resource_file. |
certificate |
list | store_location validated. |
container_configuration |
single | Nested container_registries (secret password). |
data_disks |
list | Per-node data disks. |
disk_encryption |
list | Encryption targets. |
extensions |
list | Secret protected_settings. |
mount |
list | azure_blob_file_system / azure_file_share / cifs_mount / nfs_mount. |
network_configuration |
single (force-new) | Nested endpoint_configuration β network_security_group_rules. |
node_placement |
list | Placement policy. |
security_profile |
single (force-new) | Secure boot / vTPM / host encryption. |
task_scheduling_policy |
list | node_fill_type. |
user_accounts |
list | Secret password / ssh_private_key; linux_user_configuration / windows_user_configuration. |
windows |
list | Windows images only. |
timeouts |
single | Per-operation Go durations. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, auth, subscription, and region. |
Schema notes that bite:
- π Force-new fields:
name,resource_group_name,account_name,node_agent_sku_id,vm_size,storage_image_reference,max_tasks_per_node,display_name,network_configuration, andsecurity_profileare immutable β changing any of them replaces the entire pool. - βοΈ
fixed_scaleXORauto_scale: the provider rejects both together. This module enforces the rule at parse time with avalidation {}block. - π½
os_disk_placementaccepts only the value"CacheDisk". - πͺ
windowsblocks are valid only on Windows images. - π«
disk_encryptionis not supported on Linux pools created from a VM image or a Shared Image Gallery image.
Least-privilege, scoped to the Batch account:
Microsoft.Batch/batchAccounts/pools/writeMicrosoft.Batch/batchAccounts/pools/readMicrosoft.Batch/batchAccounts/pools/deleteMicrosoft.ManagedIdentity/userAssignedIdentities/assign/actionβ on any user-assigned identity attached to the pool.Microsoft.Network/virtualNetworks/subnets/join/actionβ on any subnet the pool joins.
The built-in Contributor role scoped to the Batch account (or its resource group) covers the pool operations; identity assignment and subnet joins need the corresponding scoped permissions above.
- An existing Batch account.
- Any referenced user-assigned identity, subnet, or container registry created beforehand.
- The
Microsoft.Batchresource provider registered on the subscription.
terraform-azurerm-batch-pool/
βββ providers.tf # terraform{} + required_providers (azurerm ~> 4.0); no provider block
βββ variables.tf # deeply-typed object() schemas; metadata + timeouts tail
βββ main.tf # azurerm_batch_pool.this β thin renderer; dynamic blocks; secrets wrapped sensitive()
βββ outputs.tf # id (first), name
βββ README.md # this document
βββ SCOPE.md # cross-module contract
βββ LICENSE # MIT
βββ .gitignore # canonical library ignore set
The smallest real call: a fixed-scale Ubuntu pool.
provider "azurerm" {
features {}
}
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "cpu-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = {
target_dedicated_nodes = 2
}
}βΉοΈ The caller owns provider configuration β authentication, subscription, region, and the mandatory
features {}block. This module never declares aprovider {}block.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group |
account_name |
string |
terraform-azurerm-batch-account |
identity.identity_ids |
list(string) |
terraform-azurerm-user-assigned-identity |
network_configuration.subnet_id |
string |
terraform-azurerm-virtual-network |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Resource ID of the Batch pool | terraform-azurerm-batch-job (batch_pool_id) |
name |
Pool name | references |
account_name |
The Batch account containing the pool. Force-new. | composition |
resource_group_name |
The resource group holding the account. Force-new. | composition |
vm_size |
The node VM size. Force-new - unlike the node count, which resizes in place | cost review |
node_agent_sku_id |
The node agent SKU. Must match the image, and nothing checks it | verification |
scale_mode |
fixed / auto / unset |
plan-time |
target_node_count |
Dedicated + low-priority nodes on a fixed-scale pool. The cost signal. Null on autoscale | cost review |
uses_low_priority_nodes |
Spot nodes present - reclaimable at any time | reliability review |
autoscale_formula_is_unvalidated |
true on an autoscaling pool - nothing validates the formula |
operations |
uses_containers |
Container configuration supplied | composition |
uses_deprecated_certificate_block |
certificate is removed at provider 5.0 |
migration audit |
nodes_are_in_a_customer_virtual_network |
Nodes in a caller-supplied subnet | network review |
nodes_have_no_public_ip_addresses |
Locked-down, and needs subnet egress or the pool never fills | network review |
exposes_inbound_endpoints |
Inbound ports opened to the nodes | security review |
has_elevated_start_task |
The start task runs as administrator on every node | security review |
local_user_account_count |
Standing credentials on every node | security review |
stopping_a_pending_resize_cancels_a_running_operation |
Always true - the flag ABORTS an in-flight resize |
change review |
the_pool_bills_for_allocated_nodes_not_for_work |
Always true |
cost review |
a_node_and_everything_on_it_is_disposable |
Always true |
design |
many_failures_here_do_not_fail_the_apply |
Always true - a clean apply does not mean the pool works |
operations |
the_certificate_argument_disappears_at_5_0 |
Always true |
migration |
metadata_is_not_tags |
Always true - invisible to tag policies and cost reports |
governance |
node_deallocation_method_never_round_trips |
Always true - write-only; no drift detection |
operations |
1 Β· Fixed-scale pool with dedicated and low-priority nodes
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "mixed-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D4s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = {
target_dedicated_nodes = 3
target_low_priority_nodes = 5
resize_timeout = "PT15M"
}
}π‘ Low-priority nodes are cheaper but pre-emptible β mix them with dedicated nodes for cost-tolerant work.
2 Β· Auto-scale pool with a formula
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "elastic-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
auto_scale = {
evaluation_interval = "PT5M"
formula = <<-FORMULA
$samples = $PendingTasks.GetSamplePercent(TimeInterval_Minute * 5);
$tasks = $samples < 70 ? max(0, $PendingTasks.GetSample(1)) : max($PendingTasks.GetSample(1), avg($PendingTasks.GetSample(TimeInterval_Minute * 5)));
$TargetDedicatedNodes = min(10, $tasks);
FORMULA
}
}
β οΈ auto_scaleandfixed_scaleare mutually exclusive β set at most one. The module rejects both at parse time.
3 Β· Container pool with a private registry (secret password)
variable "acr_password" {
type = string
sensitive = true
}
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "container-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "microsoft-azure-batch"
offer = "ubuntu-server-container"
sku = "20-04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
container_configuration = {
type = "DockerCompatible"
container_image_names = ["myregistry.azurecr.io/etl:1.4.0"]
container_registries = [{
registry_server = "myregistry.azurecr.io"
user_name = "myregistry"
password = var.acr_password
}]
}
}π
passwordis wrappedsensitive()in the rendered resource β pass a reference (asensitivevariable or a Key Vault data source), never a committed literal.
4 Β· Start task with an auto-user identity
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "prep-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
start_task = {
command_line = "/bin/bash -c 'apt-get update && apt-get install -y jq'"
wait_for_success = true
user_identity = {
auto_user = {
elevation_level = "Admin"
scope = "Pool"
}
}
}
}π‘
user_identityis required insidestart_task. Useauto_userfor a Batch-managed identity oruser_nameto reference a named user account defined inuser_accounts.
5 Β· Start task with a named user and resource files
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "seeded-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 1 }
start_task = {
command_line = "/bin/bash setup.sh"
wait_for_success = true
user_identity = {
user_name = "batchadmin"
}
resource_file = [{
http_url = "https://raw.githubusercontent.com/example/repo/main/setup.sh"
file_path = "setup.sh"
file_mode = "0755"
}]
}
}βΉοΈ
user_namehere must match auser_accountsentry (see example 8).
6 Β· User-assigned managed identity
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "identity-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
identity = {
type = "UserAssigned"
identity_ids = [module.batch_identity.id]
}
}π Only
UserAssignedis supported on this resource β prefer a managed identity over embedded credentials for registry pulls and storage access.
7 Β· Azure File Share mount (secret account key)
variable "storage_account_key" {
type = string
sensitive = true
}
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "fileshare-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
mount = [{
azure_file_share = [{
account_name = "datasa01"
azure_file_url = "https://datasa01.file.core.windows.net/shared"
account_key = var.storage_account_key
relative_mount_path = "data"
mount_options = "-o vers=3.0,dir_mode=0777,file_mode=0777"
}]
}]
}π
account_keyis wrappedsensitive()β provision it out of band (Key Vault data source or asensitivevariable) and pass a reference.
8 Β· User account with a Linux SSH private key (secret)
variable "node_ssh_private_key" {
type = string
sensitive = true
}
variable "node_admin_password" {
type = string
sensitive = true
}
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "useracct-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 1 }
user_accounts = [{
name = "batchadmin"
password = var.node_admin_password
elevation_level = "Admin"
linux_user_configuration = [{
uid = 1000
gid = 1000
ssh_private_key = var.node_ssh_private_key
}]
}]
}π Both
passwordandssh_private_keyare wrappedsensitive()β never commit key material; reference secrets held out of band.
9 Β· CIFS mount (secret password)
variable "cifs_password" {
type = string
sensitive = true
}
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "cifs-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
mount = [{
cifs_mount = [{
source = "//fileserver.internal/share"
user_name = "svc-batch"
password = var.cifs_password
relative_mount_path = "cifs"
mount_options = "vers=3.0"
}]
}]
}π
passwordis wrappedsensitive(). An NFS mount (nfs_mount) needs no credential and can be added to the samemountlist.
10 Β· Extension with protected settings (secret)
variable "monitor_workspace_key" {
type = string
sensitive = true
}
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "monitored-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
extensions = [{
name = "azure-monitor"
publisher = "Microsoft.Azure.Monitor"
type = "AzureMonitorLinuxAgent"
type_handler_version = "1.0"
auto_upgrade_minor_version = true
settings_json = jsonencode({ workspaceId = "00000000-0000-0000-0000-000000000000" })
protected_settings = jsonencode({ workspaceKey = var.monitor_workspace_key })
}]
}π
protected_settingsis wrappedsensitive()β build it from asensitivevariable or Key Vault data source; it never renders in plan output.
11 Β· Trusted-launch security profile (secure boot + vTPM)
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "secure-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
security_profile = {
security_type = "trustedLaunch"
secure_boot_enabled = true
vtpm_enabled = true
host_encryption_enabled = true
}
}π Enable both
secure_boot_enabledandvtpm_enabledwhen you turn on the security profile.β οΈ security_profileis force-new β changing it replaces the pool.
12 Β· Network configuration joining a subnet
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "vnet-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
network_configuration = {
subnet_id = module.network.subnet_ids["batch"]
public_address_provisioning_type = "NoPublicIPAddresses"
endpoint_configuration = [{
name = "ssh"
backend_port = 22
protocol = "TCP"
frontend_port_range = "1-100"
network_security_group_rules = [{
access = "Deny"
priority = 150
source_address_prefix = "*"
}]
}]
}
}
β οΈ network_configurationis force-new. The default-deny NSG rule keeps inbound access closed unless you add an explicit allow rule from a known source.
13 Β· Data disks and disk encryption
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "scratch-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D4s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
data_disks = [{
lun = 0
disk_size_gb = 128
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}]
disk_encryption = [{
disk_encryption_target = "TemporaryDisk"
}]
}
β οΈ disk_encryptionis not supported on Linux pools built from a VM image or a Shared Image Gallery image β use it only where the platform image supports it.
14 Β· Metadata and per-operation timeouts
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "tagged-pool"
resource_group_name = "rg-batch-prod"
account_name = "batchprod01"
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D2s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
fixed_scale = { target_dedicated_nodes = 2 }
metadata = {
environment = "production"
team = "data-platform"
cost_center = "cc-4471"
}
timeouts = {
create = "30m"
delete = "30m"
}
}βΉοΈ This resource exposes
metadata(key/value strings on the pool), nottags.
15 Β· ποΈ End-to-end composition
Wire a resource group, a Batch account, a user-assigned identity, and a virtual network into the pool, then feed the pool id to a Batch job.
provider "azurerm" {
features {}
}
module "resource_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-batch-prod"
location = "eastus2"
}
module "network" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
name = "vnet-batch"
resource_group_name = module.resource_group.name
location = module.resource_group.location
address_space = ["10.40.0.0/16"]
subnets = {
batch = { address_prefixes = ["10.40.1.0/24"] }
}
}
module "batch_identity" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"
name = "id-batch-prod"
resource_group_name = module.resource_group.name
location = module.resource_group.location
}
module "batch_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-account.git?ref=v1.0.0"
name = "batchprod01"
resource_group_name = module.resource_group.name
location = module.resource_group.location
}
module "batch_pool" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-pool.git?ref=v1.0.0"
name = "cpu-pool"
resource_group_name = module.resource_group.name
account_name = module.batch_account.name
node_agent_sku_id = "batch.node.ubuntu 22.04"
vm_size = "Standard_D4s_v3"
storage_image_reference = {
publisher = "canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts"
version = "latest"
}
auto_scale = {
evaluation_interval = "PT5M"
formula = "$TargetDedicatedNodes = min(10, $PendingTasks.GetSample(1));"
}
identity = {
type = "UserAssigned"
identity_ids = [module.batch_identity.id]
}
network_configuration = {
subnet_id = module.network.subnet_ids["batch"]
public_address_provisioning_type = "NoPublicIPAddresses"
}
security_profile = {
security_type = "trustedLaunch"
secure_boot_enabled = true
vtpm_enabled = true
}
metadata = {
environment = "production"
}
}
module "batch_job" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-batch-job.git?ref=v1.0.0"
name = "nightly-etl"
batch_pool_id = module.batch_pool.id
}π‘ The pool consumes account name, identity id, and subnet id from its siblings, and emits
idstraight into the Batch job β a clean ownership boundary at every hop.
Required
| Name | Type | Description |
|---|---|---|
name |
string |
Pool name (force-new). |
resource_group_name |
string |
Resource group of the Batch account (force-new). |
account_name |
string |
Batch account name (force-new). |
node_agent_sku_id |
string |
Node agent SKU, e.g. batch.node.ubuntu 22.04 (force-new). |
vm_size |
string |
VM size, e.g. Standard_D2s_v3 (force-new). |
storage_image_reference |
object |
Platform image or custom image id (force-new). |
Optional scalars β display_name, inter_node_communication, license_type, max_tasks_per_node,
metadata, os_disk_placement, stop_pending_resize_operation, target_node_communication_mode.
Scale β fixed_scale XOR auto_scale (set at most one).
Optional blocks β identity, start_task, certificate, container_configuration, data_disks,
disk_encryption, extensions, mount, network_configuration, node_placement, security_profile,
task_scheduling_policy, user_accounts, windows, timeouts.
Full nested object() schemas
storage_image_reference = object({ # required
publisher = optional(string)
offer = optional(string)
sku = optional(string)
version = optional(string)
id = optional(string)
})
fixed_scale = object({ # XOR auto_scale
target_dedicated_nodes = optional(number)
target_low_priority_nodes = optional(number)
resize_timeout = optional(string)
node_deallocation_method = optional(string)
})
auto_scale = object({ # XOR fixed_scale
evaluation_interval = optional(string)
formula = string
})
identity = object({
type = string # UserAssigned only
identity_ids = list(string)
})
start_task = object({
command_line = string
common_environment_properties = optional(map(string))
task_retry_maximum = optional(number)
wait_for_success = optional(bool)
user_identity = object({
user_name = optional(string)
auto_user = optional(object({
elevation_level = optional(string)
scope = optional(string)
}))
})
container = optional(list(object({
image_name = string
run_options = optional(string)
working_directory = optional(string)
registry = optional(list(object({
registry_server = string
user_name = optional(string)
password = optional(string) # sensitive() in main.tf
user_assigned_identity_id = optional(string)
})), [])
})), [])
resource_file = optional(list(object({
auto_storage_container_name = optional(string)
blob_prefix = optional(string)
file_mode = optional(string)
file_path = optional(string)
http_url = optional(string)
storage_container_url = optional(string)
user_assigned_identity_id = optional(string)
})), [])
})
certificate = list(object({
id = string
store_location = string # CurrentUser | LocalMachine
store_name = optional(string)
visibility = optional(list(string))
}))
container_configuration = object({
type = optional(string)
container_image_names = optional(list(string))
container_registries = optional(list(object({
registry_server = string
user_name = optional(string)
password = optional(string) # sensitive() in main.tf
user_assigned_identity_id = optional(string)
})), [])
})
data_disks = list(object({
lun = number
disk_size_gb = number
caching = optional(string)
storage_account_type = optional(string)
}))
disk_encryption = list(object({
disk_encryption_target = string
}))
extensions = list(object({
name = string
publisher = string
type = string
type_handler_version = optional(string)
auto_upgrade_minor_version = optional(bool)
automatic_upgrade_enabled = optional(bool)
settings_json = optional(string)
protected_settings = optional(string) # sensitive() in main.tf
provision_after_extensions = optional(list(string))
}))
mount = list(object({
azure_blob_file_system = optional(object({
account_name = string
container_name = string
relative_mount_path = string
account_key = optional(string) # sensitive() in main.tf
sas_key = optional(string) # sensitive() in main.tf
blobfuse_options = optional(string)
identity_id = optional(string)
}))
azure_file_share = optional(list(object({
account_name = string
azure_file_url = string
account_key = string # sensitive() in main.tf
relative_mount_path = string
mount_options = optional(string)
})), [])
cifs_mount = optional(list(object({
source = string
user_name = string
password = string # sensitive() in main.tf
relative_mount_path = string
mount_options = optional(string)
})), [])
nfs_mount = optional(list(object({
source = string
relative_mount_path = string
mount_options = optional(string)
})), [])
}))
network_configuration = object({ # force-new
subnet_id = optional(string)
accelerated_networking_enabled = optional(bool)
dynamic_vnet_assignment_scope = optional(string)
public_address_provisioning_type = optional(string)
public_ips = optional(list(string))
endpoint_configuration = optional(list(object({
name = string
backend_port = number
protocol = string
frontend_port_range = string
network_security_group_rules = optional(list(object({
access = string
priority = number
source_address_prefix = string
source_port_ranges = optional(list(string))
})), [])
})), [])
})
node_placement = list(object({
policy = optional(string)
}))
security_profile = object({ # force-new
host_encryption_enabled = optional(bool)
secure_boot_enabled = optional(bool)
security_type = optional(string)
vtpm_enabled = optional(bool)
})
task_scheduling_policy = list(object({
node_fill_type = optional(string) # Pack | Spread
}))
user_accounts = list(object({
name = string
password = string # sensitive() in main.tf
elevation_level = string
linux_user_configuration = optional(list(object({
uid = optional(number)
gid = optional(number)
ssh_private_key = optional(string) # sensitive() in main.tf
})), [])
windows_user_configuration = optional(list(object({
login_mode = string
})), [])
}))
windows = list(object({
enable_automatic_updates = optional(bool)
}))
timeouts = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})| Output | Description | Sensitive |
|---|---|---|
id |
Resource ID of the Batch pool (emitted first). | No |
name |
Name of the Batch pool. | No |
account_name |
The Batch account containing the pool. Force-new. | composition |
resource_group_name |
The resource group holding the account. Force-new. | composition |
vm_size |
The node VM size. Force-new - unlike the node count, which resizes in place | cost review |
node_agent_sku_id |
The node agent SKU. Must match the image, and nothing checks it | verification |
scale_mode |
fixed / auto / unset |
plan-time |
target_node_count |
Dedicated + low-priority nodes on a fixed-scale pool. The cost signal. Null on autoscale | cost review |
uses_low_priority_nodes |
Spot nodes present - reclaimable at any time | reliability review |
autoscale_formula_is_unvalidated |
true on an autoscaling pool - nothing validates the formula |
operations |
uses_containers |
Container configuration supplied | composition |
uses_deprecated_certificate_block |
certificate is removed at provider 5.0 |
migration audit |
nodes_are_in_a_customer_virtual_network |
Nodes in a caller-supplied subnet | network review |
nodes_have_no_public_ip_addresses |
Locked-down, and needs subnet egress or the pool never fills | network review |
exposes_inbound_endpoints |
Inbound ports opened to the nodes | security review |
has_elevated_start_task |
The start task runs as administrator on every node | security review |
local_user_account_count |
Standing credentials on every node | security review |
stopping_a_pending_resize_cancels_a_running_operation |
Always true - the flag ABORTS an in-flight resize |
change review |
the_pool_bills_for_allocated_nodes_not_for_work |
Always true |
cost review |
a_node_and_everything_on_it_is_disposable |
Always true |
design |
many_failures_here_do_not_fail_the_apply |
Always true - a clean apply does not mean the pool works |
operations |
the_certificate_argument_disappears_at_5_0 |
Always true |
migration |
metadata_is_not_tags |
Always true - invisible to tag policies and cost reports |
governance |
node_deallocation_method_never_round_trips |
Always true - write-only; no drift detection |
operations |
No secret is ever emitted β secret-bearing inputs stay inside the resource and never surface as outputs.
- Force-new fields bite hard.
name,resource_group_name,account_name,node_agent_sku_id,vm_size,storage_image_reference,max_tasks_per_node,display_name,network_configuration, andsecurity_profileare immutable. Changing any of them destroys and recreates the pool β plan carefully in production. fixed_scaleandauto_scaleare mutually exclusive. The provider rejects both; this module catches the conflict at parse time with avalidation {}block, so a bad call fails before any API round-trip.- Every secret leaf is wrapped
sensitive()inmain.tf: extensionprotected_settings, container/registrypassword,container_configuration.container_registries[].password, mountaccount_key/sas_key/ CIFSpassword, user-accountpassword, andlinux_user_configuration. ssh_private_key. These values never render in plan output β provision them out of band and pass references. metadata, nottags.azurerm_batch_poolexposes ametadatamap instead of the usualtagsargument, so this module's universal tail istimeoutsonly, withmetadataoffered as a dedicated input.- The pool depends on the caller's
features {}block. As with everyazurermresource, the module carries noprovider {}block; if it appears not to initialize in isolation, the cause is a missing caller-sideprovider "azurerm" { features {} }. - Optional blocks render totally. Every optional nested block is a
dynamicguarded by presence, withtry(..., null)on every optional field, so omitting a block simply renders nothing rather than a null error.
| Principle | How this module applies it |
|---|---|
| Deeply-typed schemas | Every nested block is a typed object() β a typo in a nested key is a parse-time type error, not a late API failure. |
| Enum validations | inter_node_communication, license_type, os_disk_placement, target_node_communication_mode, and certificate store_location are enforced with validation {} blocks naming the legal values. |
| Secrets never leak | Every secret leaf (protected_settings, registry/CIFS/user passwords, storage keys, SSH key) is wrapped sensitive(); no secret is ever emitted as an output. |
| Scale exclusivity | The fixed_scale XOR auto_scale rule is enforced at parse time. |
metadata instead of tags |
The resource has no tags argument; a metadata map is offered and the tail is timeouts only. |
| Provider stays with the caller | No provider {} block; authentication, subscription, region, and features {} are the caller's. |
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0β never track a branch. - This module is plan-only from an authoring standpoint:
init/validate/fmtprove structure offline; a human runsterraform applyfrom CI against a configured provider.
The offline proof gate:
| Check | Proves |
|---|---|
terraform init -backend=false |
Providers resolve; the azurerm ~> 4.0 pin is satisfiable. |
terraform validate |
The typed schemas parse; enum validation {} blocks and the scale-XOR rule hold; dynamic blocks render. |
terraform fmt -check |
Canonical formatting. |
Only terraform plan against a real provider exercises Azure-side behavior β force-new detection, image
availability, subnet joins, and identity assignment. Those run in CI with the caller's features {} block
configured, never during authoring.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-batch-prod/providers/Microsoft.Batch/batchAccounts/batchprod01/pools/cpu-pool"
name = "cpu-pool"
Any secret-derived value (a registry password, an account key, a protected extension setting) is redacted by
Terraform as <sensitive> and never printed.
| Symptom | Cause | Fix |
|---|---|---|
Set at most one of fixed_scale or auto_scale |
Both scale blocks supplied. | Pick one β remove either fixed_scale or auto_scale. |
| API error on a Windows setting for a Linux pool | windows block set on a non-Windows image. |
Use windows only with a Windows storage_image_reference. |
disk encryption not supported |
disk_encryption set on a Linux pool from a VM / Shared Image Gallery image. |
Remove disk_encryption, or use a platform image that supports it. |
os_disk_placement rejected |
A value other than CacheDisk supplied. |
Set os_disk_placement = "CacheDisk" or leave it null. |
| Plan wants to replace the whole pool | A force-new field changed (vm_size, storage_image_reference, network_configuration, security_profile, β¦). |
Confirm the change is intended; force-new fields recreate the pool. |
| Pool fails to initialize with a provider error | Caller's provider "azurerm" { features {} } block missing. |
Add features {} to the root provider configuration. |
- Provider resource:
azurerm_batch_pool azurermprovider registry- Sibling modules:
terraform-azurerm-batch-account,terraform-azurerm-batch-job,terraform-azurerm-user-assigned-identity,terraform-azurerm-virtual-network. - This module's
SCOPE.mdβ the cross-module contract.
π "Infrastructure as Code should be standardized, consistent, and secure."