Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Batch Pool Terraform Module

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), targeting hashicorp/azurerm ~> 4.0.


Terraform azurerm Module Version Type Resources Secret-aware


🧩 Overview

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 required storage_image_reference (platform image or custom image id).
  • βš–οΈ Scale mode β€” either a fixed_scale target count or an auto_scale formula (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_configuration that 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 at plan time, and it wraps every secret leaf sensitive() so credentials never render in plan output.


❀️ Support this project

If this module saves you time, please consider supporting the work:


πŸ—ΊοΈ Where this fits in the family

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;
Loading

🧬 What this module builds

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;
Loading

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.

βœ… Provider / Versions

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, and security_profile are immutable β€” changing any of them replaces the entire pool.
  • βš–οΈ fixed_scale XOR auto_scale: the provider rejects both together. This module enforces the rule at parse time with a validation {} block.
  • πŸ’½ os_disk_placement accepts only the value "CacheDisk".
  • πŸͺŸ windows blocks are valid only on Windows images.
  • 🚫 disk_encryption is not supported on Linux pools created from a VM image or a Shared Image Gallery image.

πŸ”‘ Required Azure RBAC Roles / Permissions

Least-privilege, scoped to the Batch account:

  • Microsoft.Batch/batchAccounts/pools/write
  • Microsoft.Batch/batchAccounts/pools/read
  • Microsoft.Batch/batchAccounts/pools/delete
  • Microsoft.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.


🧰 Azure Prerequisites

  • An existing Batch account.
  • Any referenced user-assigned identity, subnet, or container registry created beforehand.
  • The Microsoft.Batch resource provider registered on the subscription.

πŸ“ Module Structure

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

βš™οΈ Quick Start

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 a provider {} block.


πŸ”Œ Cross-Module Contract

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

πŸ“š Example Library

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_scale and fixed_scale are 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
    }]
  }
}

πŸ”’ password is wrapped sensitive() in the rendered resource β€” pass a reference (a sensitive variable 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_identity is required inside start_task. Use auto_user for a Batch-managed identity or user_name to reference a named user account defined in user_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_name here must match a user_accounts entry (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 UserAssigned is 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_key is wrapped sensitive() β€” provision it out of band (Key Vault data source or a sensitive variable) 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 password and ssh_private_key are wrapped sensitive() β€” 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"
    }]
  }]
}

πŸ”’ password is wrapped sensitive(). An NFS mount (nfs_mount) needs no credential and can be added to the same mount list.

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_settings is wrapped sensitive() β€” build it from a sensitive variable 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_enabled and vtpm_enabled when you turn on the security profile. ⚠️ security_profile is 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_configuration is 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_encryption is 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), not tags.

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 id straight into the Batch job β€” a clean ownership boundary at every hop.


πŸ“₯ Inputs

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)
})

🧾 Outputs

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.


🧠 Architecture Notes

  • 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, and security_profile are immutable. Changing any of them destroys and recreates the pool β€” plan carefully in production.
  • fixed_scale and auto_scale are mutually exclusive. The provider rejects both; this module catches the conflict at parse time with a validation {} block, so a bad call fails before any API round-trip.
  • Every secret leaf is wrapped sensitive() in main.tf: extension protected_settings, container/registry password, container_configuration.container_registries[].password, mount account_key / sas_key / CIFS password, user-account password, and linux_user_configuration. ssh_private_key. These values never render in plan output β€” provision them out of band and pass references.
  • metadata, not tags. azurerm_batch_pool exposes a metadata map instead of the usual tags argument, so this module's universal tail is timeouts only, with metadata offered as a dedicated input.
  • The pool depends on the caller's features {} block. As with every azurerm resource, the module carries no provider {} block; if it appears not to initialize in isolation, the cause is a missing caller-side provider "azurerm" { features {} }.
  • Optional blocks render totally. Every optional nested block is a dynamic guarded by presence, with try(..., null) on every optional field, so omitting a block simply renders nothing rather than a null error.

🧱 Design Principles

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.

πŸš€ Runbook

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 / fmt prove structure offline; a human runs terraform apply from CI against a configured provider.

πŸ§ͺ Testing

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.


πŸ’¬ Example Output

$ 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.


πŸ” Troubleshooting

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.

πŸ”— Related Docs

  • Provider resource: azurerm_batch_pool
  • azurerm provider 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."