Skip to content

microsoftexpert/tf-mod-azure-batch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ Azure Batch Terraform Module

A composite module that provisions an Azure Batch account and its tightly-coupled child resources β€” applications, certificates, pools, and jobs β€” behind one deeply-typed, secure-by-default boundary. Built for AzureRM v4.x.

Terraform AzureRM Module Type Resources


🧩 Overview

This module creates and wires together everything an Azure Batch deployment needs:

  • 🧱 A Batch account (azurerm_batch_account) β€” the control-plane resource, named this.
  • πŸ“¦ Applications (azurerm_batch_application) β€” package definitions for the code your tasks run.
  • πŸ” Certificates (azurerm_batch_certificate) β€” # DEPRECATED by Azure (see Troubleshooting), exposed for completeness.
  • πŸ–₯️ Pools (azurerm_batch_pool) β€” the compute-node fleets, with the full provider surface: fixed/auto scale, container configuration, mounts, network configuration, security profile, start tasks, user accounts, extensions, and identities.
  • πŸ—‚οΈ Jobs (azurerm_batch_job) β€” task containers that target a pool created here (by pool_key) or an external pool (by batch_pool_id).
  • πŸ”’ Secure defaults β€” public_network_access_enabled = false, network_profile default actions of Deny, and a sensitive-split for certificate secret material so private keys never appear in plan output.

πŸ’‘ Why it matters: one module call stands up the account, its compute pools, and the jobs that run on them β€” with no manual ID plumbing between them β€” while the type system rejects a malformed pool definition long before a single API call.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


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

flowchart LR
 RG["tf-mod-azure-resource-group<br/>(resource group Β· location)"]
 SA["tf-mod-azure-storage-account<br/>(auto-storage Β· packages)"]
 KV["tf-mod-azure-key-vault<br/>(CMK key Β· vault ref)"]
 UAI["tf-mod-azure-user-assigned-identity<br/>(node / account identity)"]
 NET["tf-mod-azure-subnet<br/>(pool VNet integration)"]
 BATCH["tf-mod-azure-batch<br/>(this module)"]
 RBAC["tf-mod-azure-role-assignment"]
 PE["tf-mod-azure-private-endpoint"]

 RG -->|"resource_group_name Β· location"| BATCH
 SA -->|"storage_account_id"| BATCH
 KV -->|"key_vault_reference Β· encryption.key_vault_key_id"| BATCH
 UAI -->|"identity_ids Β· *_assigned_identity_id"| BATCH
 NET -->|"pools[*].network_configuration.subnet_id"| BATCH
 BATCH -->|"identity_principal_id"| RBAC
 BATCH -->|"id"| PE

 style BATCH fill:#8957E5,color:#fff
 style RG fill:#0078D4,color:#fff
Loading

This module consumes a resource group, optional storage account, Key Vault references, managed identities, and pool subnets from its siblings, and emits the account id / name / account_endpoint, the identity principal/tenant IDs, and per-collection ID maps (application_ids, pool_ids, job_ids, …) for downstream role assignments, private endpoints, and monitoring β€” see the Cross-Module Contract.


🧬 What this module builds

flowchart TD
 ACCT["azurerm_batch_account.this<br/>(primary β€” 1)"]
 APP["azurerm_batch_application.application<br/>(for_each β€” 0..N)"]
 CERT["azurerm_batch_certificate.certificate<br/>(for_each β€” 0..N Β· DEPRECATED)"]
 POOL["azurerm_batch_pool.pool<br/>(for_each β€” 0..N)"]
 JOB["azurerm_batch_job.job<br/>(for_each β€” 0..N)"]

 ACCT -->|"account_name"| APP
 ACCT -->|"account_name"| CERT
 ACCT -->|"account_name"| POOL
 POOL -->|"pool_key β‡’ batch_pool_id"| JOB
 ACCT -.->|"or external batch_pool_id"| JOB

 style ACCT fill:#8957E5,color:#fff
 style CERT stroke:#C9510C,stroke-width:2px,stroke-dasharray:4 3
Loading

Resource inventory β€” what Terraform actually creates when you call this module:

  • 🧱 azurerm_batch_account.this β€” primary control-plane resource; everything else is addressed through its name.
  • πŸ“¦ azurerm_batch_application.application β€” for_each over var.applications; one application package definition per key.
  • πŸ” azurerm_batch_certificate.certificate β€” for_each over var.certificates; secret material joined in from the sensitive var.certificate_secrets map. # DEPRECATED β€” emits a provider deprecation warning by design.
  • πŸ–₯️ azurerm_batch_pool.pool β€” for_each over var.pools; the compute fleet, rendered through ~20 dynamic blocks.
  • πŸ—‚οΈ azurerm_batch_job.job β€” for_each over var.jobs; batch_pool_id resolved from pool_key (intra-module) or supplied directly.

βœ… Provider / Versions

Requirement Version
Terraform >= 1.12.0
AzureRM provider >= 4.0, < 5.0
Azure API Microsoft.Batch 2024-07-01

Schema notes that bite on the 4.x line:

  • ⚠️ azurerm_batch_certificate is deprecated β€” the Azure Batch Certificates feature retired 2024-02-29 and the resource is removed in provider v5.0. terraform validate emits a deprecation warning whenever any certificate is declared. Plan new work around Key Vault VM extensions / pool certificate references.
  • ℹ️ No sku argument. Unlike most Azure services, azurerm_batch_account exposes no SKU. The tier behaviour is governed entirely by pool_allocation_mode.
  • ℹ️ Cross-variable validations fire at plan, not terraform validate. The UserSubscription β‡’ key_vault_reference and certificates ⇔ certificate_secrets checks reference a second variable, so they evaluate at plan time.
  • ℹ️ Pool identity is UserAssigned-only in the provider; the module fixes type and exposes just identity_ids. (Account identity accepts SystemAssigned or UserAssigned.)

Pin Terraform >= 1.12.0 β€” the module relies on optional object defaults and cross-variable validation blocks.


πŸ“ Module Structure

tf-mod-azure-batch/
β”œβ”€β”€ providers.tf # Terraform + AzureRM version pins (no provider config)
β”œβ”€β”€ variables.tf # Deeply-typed inputs, heredoc schemas, enum + cross-field validations
β”œβ”€β”€ main.tf # Primary `this` + 4 for_each collections, total renderer (dynamic blocks)
β”œβ”€β”€ outputs.tf # Composition contract: account IDs/keys/identity + per-collection ID maps
β”œβ”€β”€ SCOPE.md # Composite scope contract (allow-list, consumes/emits, gotchas)
└── README.md # You are here

βš™οΈ Quick Start

The smallest call that produces a real, usable account with one pool:

module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  pools = {
    linux = {
      vm_size           = "STANDARD_A1_V2"
      node_agent_sku_id = "batch.node.ubuntu 20.04"

      storage_image_reference = {
        publisher = "canonical"
        offer     = "0001-com-ubuntu-server-focal"
        sku       = "20_04-lts"
        version   = "latest"
      }

      fixed_scale = {
        target_dedicated_nodes = 2
      }

      target_node_communication_mode = "Simplified" # Classic retired 2026-03-31
    }
  }
}

πŸ”’ public_network_access_enabled defaults to false β€” the account is private out of the box. Set it explicitly to true only if you have a justified reason.


πŸ”Œ Cross-Module Contract

Sourced directly from SCOPE.md.

Consumes

Input Type Source
🏷️ resource_group_name string tf-mod-azure-resource-group.name
πŸ“ location string tf-mod-azure-resource-group.location
πŸ’Ύ storage_account_id string tf-mod-azure-storage-account.id
πŸͺͺ storage_account_node_identity string tf-mod-azure-user-assigned-identity.id
πŸͺͺ identity.identity_ids list(string) tf-mod-azure-user-assigned-identity.id
πŸ”‘ key_vault_reference object({ id, url }) tf-mod-azure-key-vault (id + vault URI) β€” UserSubscription mode
πŸ” encryption.key_vault_key_id string tf-mod-azure-key-vault-key.id β€” CMK
🌐 pools[*].network_configuration.subnet_id string tf-mod-azure-subnet.id

Emits

Output Description Consumed by
id Full Batch account resource ID tf-mod-azure-role-assignment (scope), tf-mod-azure-private-endpoint, diagnostic settings
name Batch account name Dependent *_account_name references
resource_group_name Resource group name (passthrough) Sibling modules in the same RG
location Azure region Region-aware siblings
account_endpoint Batch service endpoint Application clients / job schedulers
primary_access_key πŸ” Primary shared key (conditional) Key Vault secret modules, app config
secondary_access_key πŸ” Secondary shared key (conditional) Key Vault secret modules, app config
identity_principal_id Account identity principal ID (null if no identity) tf-mod-azure-role-assignment (principal_id)
identity_tenant_id Account identity tenant ID (null if no identity) Cross-tenant wiring
application_ids Map: app handle β‡’ resource ID Package upload pipelines
certificate_ids Map: cert handle β‡’ resource ID (# DEPRECATED) Pool certificate references
pool_ids Map: pool handle β‡’ resource ID Jobs (intra-module via pool_key), scheduling
job_ids Map: job handle β‡’ resource ID Job-monitoring automation

πŸ“š Example Library

1️⃣ Minimal β€” account only
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
}

πŸ’‘ The empty call creates just the account β€” private, with Microsoft-managed storage and no pools. Every child collection defaults to {}.

2️⃣ Linked auto-storage with managed-identity auth
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  storage_account_id                  = module.batch_storage.id
  storage_account_authentication_mode = "BatchAccountManagedIdentity" # preferred over StorageKeys
  storage_account_node_identity       = module.batch_identity.id

  identity = {
    type         = "UserAssigned"
    identity_ids = [module.batch_identity.id]
  }
}

πŸ”’ BatchAccountManagedIdentity keeps storage keys out of the picture. The identity needs a role (e.g. Storage Blob Data Contributor) on the auto-storage account or application-package uploads fail with HTTP 409.

3️⃣ System-assigned identity + customer-managed-key encryption
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  identity = {
    type = "SystemAssigned"
  }

  encryption = {
    key_vault_key_id = module.batch_cmk.versionless_id # versioned or versionless both supported
  }
}

πŸ” CMK requires the account identity to hold Key Get / Wrap / Unwrap on a Key Vault that has soft-delete + purge protection enabled. The module wires the key URL; the RBAC/Key Vault config is the caller's.

4️⃣ UserSubscription pool allocation mode
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                 = "caseybatch001"
  resource_group_name  = module.resource_group.name
  location             = module.resource_group.location
  pool_allocation_mode = "UserSubscription"

  key_vault_reference = {
    id  = module.batch_kv.id
    url = module.batch_kv.vault_uri
  }
}

⚠️ UserSubscription requires key_vault_reference (enforced at plan time by this module). The subscription must also be registered with Azure Batch, and the "Microsoft Azure Batch" service principal needs Contributor on the subscription. In this mode your subscription vCPU quotas apply β€” not the Batch-account core quotas.

5️⃣ Network-locked account (private + IP allow-list)
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                          = "caseybatch001"
  resource_group_name           = module.resource_group.name
  location                      = module.resource_group.location
  public_network_access_enabled = false

  network_profile = {
    account_access = {
      default_action = "Deny"
      ip_rule = [
        { ip_range = "10.20.0.0/16" },
      ]
    }
    node_management_access = {
      default_action = "Deny"
      ip_rule = [
        { ip_range = "10.20.0.0/16" },
      ]
    }
  }
}

πŸ”’ default_action defaults to Deny for both access blocks; the ip_rule.action value can only be Allow. Pair with a tf-mod-azure-private-endpoint on the emitted id for full lockdown.

6️⃣ Auto-scaling pool
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  pools = {
    autoscale = {
      vm_size           = "STANDARD_D2_V3"
      node_agent_sku_id = "batch.node.ubuntu 20.04"

      storage_image_reference = {
        publisher = "canonical"
        offer     = "0001-com-ubuntu-server-focal"
        sku       = "20_04-lts"
        version   = "latest"
      }

      auto_scale = {
        evaluation_interval = "PT15M"
        formula             = <<-EOF
 startingNumberOfVMs = 1;
 maxNumberofVMs = 25;
 pendingTaskSamplePercent = $PendingTasks.GetSamplePercent(180 * TimeInterval_Second);
 pendingTaskSamples = pendingTaskSamplePercent < 70 ? startingNumberOfVMs: avg($PendingTasks.GetSample(180 * TimeInterval_Second));
 $TargetDedicatedNodes = min(maxNumberofVMs, pendingTaskSamples);
 EOF
      }
    }
  }
}

⚠️ A pool may set at most one of fixed_scale / auto_scale (validated). Provide neither and the pool defaults to one dedicated node.

7️⃣ Container pool with a start task
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  pools = {
    containers = {
      vm_size           = "STANDARD_D2_V3"
      node_agent_sku_id = "batch.node.ubuntu 20.04"

      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/worker:1.0"]
        container_registries = [{
          registry_server           = "myregistry.azurecr.io"
          user_assigned_identity_id = module.acr_pull_identity.id # prefer identity over username/password
        }]
      }

      start_task = {
        command_line     = "/bin/bash -c 'echo provisioning'"
        wait_for_success = true
        user_identity = {
          auto_user = {
            elevation_level = "Admin"
            scope           = "Pool"
          }
        }
      }
    }
  }
}

πŸ”’ Use user_assigned_identity_id on registries instead of user_name / password so credentials never enter Terraform state.

8️⃣ VNet-integrated pool with endpoint configuration
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  pools = {
    vnet = {
      vm_size           = "STANDARD_D2_V3"
      node_agent_sku_id = "batch.node.ubuntu 20.04"

      storage_image_reference = {
        publisher = "canonical"
        offer     = "0001-com-ubuntu-server-focal"
        sku       = "20_04-lts"
        version   = "latest"
      }

      fixed_scale = { target_dedicated_nodes = 1 }

      network_configuration = {
        subnet_id                        = module.batch_subnet.id
        public_address_provisioning_type = "NoPublicIPAddresses"
        endpoint_configuration = [{
          name                = "ssh"
          backend_port        = 22
          protocol            = "TCP"
          frontend_port_range = "1000-1100"
          network_security_group_rules = [{
            access                = "Allow"
            priority              = 150
            source_address_prefix = "10.0.0.0/16"
          }]
        }]
      }
    }
  }
}

⚠️ network_configuration is # IMMUTABLE β€” changing the subnet forces pool recreation. frontend_port_range must span at least 100 ports and avoid the 50000–55000 reserved range.

9️⃣ Hardened pool β€” trusted launch + host encryption
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  pools = {
    secure = {
      vm_size           = "STANDARD_D2S_V3"
      node_agent_sku_id = "batch.node.ubuntu 22.04"

      storage_image_reference = {
        publisher = "canonical"
        offer     = "0001-com-ubuntu-server-jammy"
        sku       = "22_04-lts"
        version   = "latest"
      }

      fixed_scale = { target_dedicated_nodes = 1 }

      security_profile = {
        security_type           = "trustedLaunch"
        secure_boot_enabled     = true
        vtpm_enabled            = true
        host_encryption_enabled = true
      }

      target_node_communication_mode = "Simplified"
    }
  }
}

πŸ”’ security_profile is # IMMUTABLE (creation-only). security_type must be set to enable secure_boot_enabled / vtpm_enabled.

πŸ”Ÿ Applications
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  storage_account_id                  = module.batch_storage.id
  storage_account_authentication_mode = "StorageKeys"

  applications = {
    ffmpeg = {
      display_name    = "FFmpeg Transcoder"
      allow_updates   = false # lock packages once published
      default_version = "6.0"
    }
    solver = {}
  }
}

ℹ️ A linked storage_account_id is required for application packages (Batch stores package binaries in auto-storage). Max 200 applications per account.

1️⃣1️⃣ Certificates (DEPRECATED) β€” sensitive-split inputs
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  # Non-sensitive, iterable structural fields
  certificates = {
    signing = {
      format     = "Pfx"
      thumbprint = "42C107874FD0E4A9583292A2F1098E8FE4B2EDDA"
      # thumbprint_algorithm defaults to "SHA1"
    }
  }

  # Sensitive secret material, keyed identically
  certificate_secrets = {
    signing = {
      certificate = filebase64("${path.module}/signing.pfx")
      password    = var.signing_cert_password
    }
  }
}

⚠️ Deprecated by Azure β€” prefer Key Vault for new work. The certificates map holds structural fields so Terraform can for_each over it; the encoded cert + password live in the sensitive certificate_secrets map. Every certificates key must have a matching certificate_secrets key (validated).

1️⃣2️⃣ Jobs targeting in-module and external pools
module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  pools = {
    primary = {
      vm_size           = "STANDARD_A1_V2"
      node_agent_sku_id = "batch.node.ubuntu 20.04"
      storage_image_reference = {
        publisher = "canonical"
        offer     = "0001-com-ubuntu-server-focal"
        sku       = "20_04-lts"
        version   = "latest"
      }
      fixed_scale = { target_dedicated_nodes = 1 }
    }
  }

  jobs = {
    nightly = {
      pool_key                      = "primary" # resolved to azurerm_batch_pool.pool["primary"].id
      priority                      = 100
      task_retry_maximum            = 3
      common_environment_properties = { STAGE = "prod" }
    }
    external = {
      batch_pool_id = module.other_batch.pool_ids["gpu"] # a pool from another module
    }
  }
}

⚠️ Each job must set exactly one of pool_key / batch_pool_id (validated). priority ranges βˆ’1000…1000. Jobs are # IMMUTABLE β€” any change recreates them.

1️⃣3️⃣ for_each at scale β€” many pools from one map
locals {
  # Stable keys are identity β€” never renumber them (renaming a key destroys & recreates the pool).
  pool_defs = {
    cpu-small  = { vm_size = "STANDARD_A1_V2", nodes = 2 }
    cpu-large  = { vm_size = "STANDARD_D4_V3", nodes = 4 }
    burst-pool = { vm_size = "STANDARD_D2_V3", nodes = 0 }
  }
}

module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatch001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  pools = {
    for k, v in local.pool_defs : k => {
      vm_size           = v.vm_size
      node_agent_sku_id = "batch.node.ubuntu 20.04"
      storage_image_reference = {
        publisher = "canonical"
        offer     = "0001-com-ubuntu-server-focal"
        sku       = "20_04-lts"
        version   = "latest"
      }
      fixed_scale                    = { target_dedicated_nodes = v.nodes }
      target_node_communication_mode = "Simplified"
    }
  }
}

πŸ’‘ The map key becomes the pool name unless you set name. Keep keys descriptive and permanent β€” they are the resource's identity in state.

1️⃣4️⃣ πŸ—οΈ End-to-end composition (mandatory)
module "resource_group" {
  source   = "git::https://github.com/microsoftexpert/tf-mod-azure-resource-group?ref=v1.0.0"
  name     = "rg-batch-prod"
  location = "eastus2"
}

module "batch_identity" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-user-assigned-identity?ref=v1.0.0"
  name                = "id-batch-prod"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
}

module "batch_storage" {
  source              = "git::https://github.com/microsoftexpert/tf-mod-azure-storage-account?ref=v1.0.0"
  name                = "stbatchprod001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
}

module "batch" {
  source = "git::https://github.com/microsoftexpert/tf-mod-azure-batch?ref=v1.0.0"

  name                = "caseybatchprod001"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location

  storage_account_id                  = module.batch_storage.id
  storage_account_authentication_mode = "BatchAccountManagedIdentity"
  storage_account_node_identity       = module.batch_identity.id

  identity = {
    type         = "UserAssigned"
    identity_ids = [module.batch_identity.id]
  }

  public_network_access_enabled = false

  pools = {
    workers = {
      vm_size           = "STANDARD_D2_V3"
      node_agent_sku_id = "batch.node.ubuntu 22.04"
      storage_image_reference = {
        publisher = "canonical"
        offer     = "0001-com-ubuntu-server-jammy"
        sku       = "22_04-lts"
        version   = "latest"
      }
      fixed_scale                    = { target_dedicated_nodes = 3 }
      target_node_communication_mode = "Simplified"
      identity                       = { identity_ids = [module.batch_identity.id] }
    }
  }

  jobs = {
    etl = {
      pool_key = "workers"
      priority = 100
    }
  }

  tags = { workload = "batch", env = "prod" }
}

# Grant the account identity access to its CMK / storage downstream
module "batch_storage_role" {
  source       = "git::https://github.com/microsoftexpert/tf-mod-azure-role-assignment?ref=v1.0.0"
  scope        = module.batch_storage.id
  role         = "Storage Blob Data Contributor"
  principal_id = module.batch.identity_principal_id
}

πŸ—οΈ The full chain: resource group β†’ identity + storage β†’ batch (account + pool + job) β†’ role assignment β€” wired entirely through module outputs, no hand-copied IDs.


πŸ“₯ Inputs

Core (account): name, resource_group_name, location, pool_allocation_mode, public_network_access_enabled.

Storage & auth: storage_account_id, storage_account_authentication_mode, storage_account_node_identity, allowed_authentication_modes.

Identity & security: identity, key_vault_reference, encryption, network_profile.

Collections: applications, certificates + certificate_secrets πŸ”, pools, jobs.

Lifecycle: tags, timeouts.

Full object schemas for the map inputs
# applications β€” map keyed by a stable handle (key = name unless overridden)
map(object({
 name = optional(string) # defaults to map key
 allow_updates = optional(bool, true)
 default_version = optional(string)
 display_name = optional(string)
}))

# certificates β€” NON-sensitive structural fields (# DEPRECATED feature)
map(object({
 format = string # "Cer" | "Pfx"
 thumbprint = string # # IMMUTABLE
 thumbprint_algorithm = optional(string, "SHA1") # only "SHA1". # IMMUTABLE
}))

# certificate_secrets β€” SENSITIVE, keyed identically to certificates
map(object({
 certificate = string # base64 contents
 password = optional(string) # only valid when format = "Pfx"
}))

# pools β€” abbreviated; see variables.tf for the exhaustive nested schema
map(object({
 name = optional(string) # defaults to key. # IMMUTABLE
 vm_size = string # # IMMUTABLE
 node_agent_sku_id = string # # IMMUTABLE
 display_name = optional(string) # # IMMUTABLE
 max_tasks_per_node = optional(number, 1) # # IMMUTABLE
 inter_node_communication = optional(string, "Enabled") # "Enabled" | "Disabled"
 license_type = optional(string) # "Windows_Server" | "Windows_Client"
 os_disk_placement = optional(string) # "CacheDisk"
 target_node_communication_mode = optional(string) # "Classic" | "Default" | "Simplified"
 stop_pending_resize_operation = optional(bool)
 metadata = optional(map(string), {})
 storage_image_reference = object({ publisher, offer, sku, version, id (all optional) }) # required. # IMMUTABLE
 fixed_scale | auto_scale # at most one
 data_disks, disk_encryption, extensions, identity, start_task, container_configuration,
 mount, network_configuration, node_placement, security_profile, task_scheduling_policy,
 user_accounts, windows
}))

# jobs β€” map keyed by a stable handle; exactly one of pool_key / batch_pool_id
map(object({
 name = optional(string) # defaults to key
 pool_key = optional(string) # key into this module's pools
 batch_pool_id = optional(string) # explicit external pool ID
 display_name = optional(string)
 priority = optional(number, 0) # -1000.. 1000
 task_retry_maximum = optional(number) # 0 = none, -1 = unlimited
 common_environment_properties = optional(map(string))
}))

🧾 Outputs

Output Description Notes
id Full Batch account resource ID β€”
name Batch account name β€”
resource_group_name Resource group (passthrough) β€”
location Azure region β€”
account_endpoint Batch service endpoint β€”
primary_access_key Primary shared key πŸ” sensitive; null unless SharedKey + BatchService
secondary_access_key Secondary shared key πŸ” sensitive; same conditions as primary
identity_principal_id Account identity principal/object ID null when no identity configured
identity_tenant_id Account identity tenant ID null when no identity configured
application_ids Map: handle β‡’ application ID {} when none
certificate_ids Map: handle β‡’ certificate ID {} when none; # DEPRECATED feature
pool_ids Map: handle β‡’ pool ID {} when none
job_ids Map: handle β‡’ job ID {} when none

🧠 Architecture Notes

  • One primary, four collections. azurerm_batch_account.this is the only singleton; applications, certificates, pools, and jobs are for_each collections over map(object(...)). No count anywhere β€” order changes never cascade into spurious re-creates.
  • for_each keys are identity. Each collection is keyed by a caller-supplied string; that key is the resource address in state. Renaming a key destroys and recreates the resource (and, for pools, any jobs that referenced it by pool_key). Treat keys as permanent. The resource name defaults to the key but can be overridden with name when you want a logical handle distinct from the Azure name.
  • Sensitive-split for certificate secrets. Terraform forbids for_each over a sensitive value, so the iterable structural fields live in certificates (non-sensitive) and the private-key material lives in certificate_secrets (sensitive = true), keyed identically. main.tf joins them per entry (var.certificate_secrets[each.key].certificate). Because Terraform rejects mutual cross-variable validations as a dependency cycle, the "every cert has secret material" check lives one-directionally on certificates. This is the same pattern the APIM tf-mod-azure-apim-shared module uses for named_values / named_value_secrets β€” and it means no nonsensitive hack is needed here.
  • The "SKU" is pool_allocation_mode. Batch has no sku argument. BatchService (default) allocates pools in Microsoft-managed subscriptions and enforces Batch-account core quotas; UserSubscription allocates VMs directly in your subscription (enabling Reserved Instances, Azure Policy on the scale sets, and shared subscription core quota) and requires key_vault_reference. Switching modes is # IMMUTABLE β€” it forces a new account, so choose at creation.
  • Immutable vs in-place. Account name/location/pool_allocation_mode and most pool fields (vm_size, node_agent_sku_id, storage_image_reference, network_configuration, security_profile, max_tasks_per_node) are ForceNew β€” changing them recreates the resource. Scale settings, metadata, and start tasks update in place. Every ForceNew field is labelled # IMMUTABLE in variables.tf.
  • Intra-module jobβ†’pool wiring. A job's pool_key is resolved in main.tf to azurerm_batch_pool.pool[key].id, so a single call can stand up pools and the jobs that run on them with no manual ID copying. Use batch_pool_id for pools owned by another module.
  • Implicit ordering. Every child references azurerm_batch_account.this.name (jobs additionally reference pool IDs), so Terraform derives create/destroy order with no explicit depends_on.
  • Conditional outputs. Access keys and identity IDs are wrapped in try(..., null) because they only exist under specific configurations β€” the outputs are always valid, returning null when the feature is off.

🧱 Design Principles

  • πŸ”’ Secure by default β€” private network access, Deny network rules, identity-based storage auth encouraged over shared keys.
  • πŸ” Secrets stay out of plan output β€” certificate_secrets and the access-key outputs are sensitive; identity/registry references are preferred over inline passwords.
  • 🧬 The type is the contract β€” deeply-typed object schemas with optional defaults; a malformed pool is a plan-time type error, not a runtime failure.
  • πŸ›‘οΈ Validation at the boundary β€” enums, closed value sets, and the provider's hard cross-field rules (UserSubscription β‡’ KeyVault, single scale block, exactly-one job pool reference) are enforced before any API call.
  • 🎯 Small blast radius β€” for_each collections mean one bad pool doesn't re-plan the account or the other pools.
  • πŸͺž main.tf is a total renderer β€” pure projection of typed input onto provider blocks via dynamic blocks and try(x, null); no business logic.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan
terraform apply
terraform output

⚠️ Always pin the module with ?ref=v1.0.0 β€” never a branch. A tag is immutable once consumers reference it; a branch can change underneath every caller.


πŸ§ͺ Testing

The offline proof gate (no cloud credentials required):

terraform fmt -check # zero formatting differences
terraform validate # valid configuration (emits the expected azurerm_batch_certificate deprecation warning)
tflint # lint for provider-specific issues

ℹ️ terraform validate passes with a single deprecation warning for azurerm_batch_certificate whenever certificates are declared β€” this is expected and documented, not an error.


πŸ’¬ Example Output

account_endpoint = "caseybatchprod001.eastus2.batch.azure.com"
id = "/subscriptions/…/resourceGroups/rg-batch-prod/providers/Microsoft.Batch/batchAccounts/caseybatchprod001"
identity_principal_id = "00000000-0000-0000-0000-000000000000"
job_ids = {
 "etl" = "/subscriptions/…/batchAccounts/caseybatchprod001/jobs/etl"
}
name = "caseybatchprod001"
pool_ids = {
 "workers" = "/subscriptions/…/batchAccounts/caseybatchprod001/pools/workers"
}
primary_access_key = (sensitive value)

πŸ” Troubleshooting

  • πŸŸ₯ Pool applies cleanly but never reaches its node count (AccountCoreQuotaReached / AccountLowPriorityCoreQuotaReached / AccountVMSeriesCoreQuotaReached). New Batch accounts in many regions and subscription types ship with core quotas reduced to 0. Terraform creates the account and pool resource successfully, but the allocation state stays resizing forever. Fix: request a per-VM-series core quota increase for the Batch account (Azure portal β†’ Batch account β†’ Quotas, or the Azure Quota REST API) β€” it's free and usually granted in minutes to two business days. This is the single most common "it deployed but nothing runs" surprise.
  • πŸŸ₯ Classic node communication mode rejected / deprecated. The classic node communication model retired 2026-03-31. Set target_node_communication_mode = "Simplified" on new pools (it also reduces the required inbound/outbound connectivity and removes the Azure Storage outbound dependency).
  • 🟧 azurerm_batch_certificate deprecation warning on every validate/plan. Expected β€” the Azure Batch Certificates feature retired 2024-02-29 and the resource is removed in provider v5.0. If you're on the >= 4.0, < 5.0 pin it still works; plan migration to Key Vault VM extensions before adopting v5.0.
  • 🟧 HTTP 409 when uploading an application package. With storage_account_authentication_mode = "BatchAccountManagedIdentity", the account's managed identity needs a data-plane role (e.g. Storage Blob Data Contributor) on the linked auto-storage account. Grant it via tf-mod-azure-role-assignment on identity_principal_id.
  • 🟧 UserSubscription account fails to create. The subscription must be registered with the Azure Batch resource provider, the "Microsoft Azure Batch" service principal needs Contributor on the subscription, and key_vault_reference is mandatory (the module enforces the last one at plan time). In this mode the Batch core quotas don't apply β€” your subscription regional/per-series vCPU quotas do.
  • 🟧 CMK (encryption) fails to activate. The referenced Key Vault must have soft-delete and purge protection enabled, and the account identity (system- or user-assigned) needs Key Get / Wrap / Unwrap. Both versioned and versionless key URLs are accepted.
  • 🟨 primary_access_key / secondary_access_key come back null. Shared keys are only populated when allowed_authentication_modes includes "SharedKey" and pool_allocation_mode = "BatchService". AAD-only accounts (the more secure posture) return null by design.
  • 🟨 Renaming a map key recreates a resource. Changing a key in pools / jobs / applications / certificates is a destroy-and-recreate β€” and renaming a pool key orphans any job referencing it by pool_key. Add new keys; don't renumber existing ones.
  • 🟨 Pool/job limits. Max 200 applications and 100–300 active jobs per account (completed jobs are unlimited), 100 pools (default; 500 max), 10 mounts per node, 12 certificates per pool, and a 180-day maximum task lifetime. Hitting these is a quota/limit issue, not a Terraform error.

πŸ”— Related Docs

  • Terraform Registry β€” azurerm_batch_account, azurerm_batch_pool, azurerm_batch_application, azurerm_batch_certificate, azurerm_batch_job
  • Microsoft Learn β€” What is Azure Batch? and Batch service workflow and resources
  • Microsoft Learn β€” Batch service quotas and limits
  • Microsoft Learn β€” Azure Batch best practices (pool allocation & node communication modes)
  • Microsoft Learn β€” Use managed identities in an Azure Batch account or pool
  • Microsoft Learn β€” Configure customer-managed keys for your Azure Batch account
  • β€” tf-mod-azure-batch/SCOPE.md (composite scope contract)

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."