hashicorp/nomad · error

modifying node pool %q is not allowed

Error message

modifying node pool %q is not allowed

What it means

upsertNodePoolTxn blocks any update to a built-in node pool (e.g. default, all). When an existing pool is found and the submitted pool IsBuiltIn(), the upsert returns this error instead of applying changes, protecting Nomad's internal pools from operator modification.

Source

Thrown at nomad/state/state_store_node_pools.go:136

	}

	return txn.Commit()
}

func (s *StateStore) upsertNodePoolTxn(txn *txn, index uint64, pool *structs.NodePool) error {
	if pool == nil {
		return nil
	}

	existing, err := txn.First(TableNodePools, "id", pool.Name)
	if err != nil {
		return fmt.Errorf("node pool lookup failed: %w", err)
	}

	if existing != nil {
		// Prevent changes to built-in node pools.
		if pool.IsBuiltIn() {
			return fmt.Errorf("modifying node pool %q is not allowed", pool.Name)
		}

		exist := existing.(*structs.NodePool)
		pool.CreateIndex = exist.CreateIndex
		pool.ModifyIndex = index
	} else {
		pool.CreateIndex = index
		pool.ModifyIndex = index
	}

	if err := txn.Insert(TableNodePools, pool); err != nil {
		return fmt.Errorf("node pool insert failed: %w", err)
	}

	return nil
}

// fetchOrCreateNodePoolTxn returns an existing node pool with the given name

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Only include custom (non-built-in) node pools in the upsert payload; skip pools where pool.IsBuiltIn() is true.
  2. If tuning behavior of built-in pools is the goal, create a custom node pool and assign nodes/workloads to it instead.
  3. Filter the response of node pool list APIs before replaying them as updates in automation.
  4. On newer Nomad versions, check whether the desired setting is supported for built-in pools; otherwise keep them untouched.

Example fix

// before
for _, pool := range pools {
    stateStore.UpsertNodePools(idx, []*structs.NodePool{pool}) // includes "default"
}

// after
for _, pool := range pools {
    if !pool.IsBuiltIn() {
        stateStore.UpsertNodePools(idx, []*structs.NodePool{pool})
    }
}
Defensive patterns

Strategy: validation

Validate before calling

for _, pool := range pools {
    if pool.IsBuiltIn() {
        continue // never upsert built-in pools
    }
    err := s.UpsertNodePools(idx, []*structs.NodePool{pool})
    if err != nil {
        return err
    }
}

Type guard

func isUpdatablePool(p *structs.NodePool) bool {
    return p != nil && !p.IsBuiltIn()
}

Try / catch

err := s.UpsertNodePools(idx, pools)
if err != nil && strings.Contains(err.Error(), "is not allowed") {
    // strip built-in pools from the batch and retry with custom pools only
}

Prevention

When it happens

Trigger: Calling UpsertNodePools (or fetchOrCreateNodePoolTxn resolving a request to update) with a NodePool whose Name matches a built-in pool — e.g. trying to change the description, scheduler config, or node identity ttl of the "default" or "all" pool via the node pool update API/CLI.

Common situations: `nomad node pool update default ...` attempts; Terraform/automation applying node pool configs that include built-in pools; jobs or API clients sending a full pool object back that was fetched from /node/pools and happens to include built-ins.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/974c45aca5e34a1d. Report an issue: GitHub.