Infrastructure standard
Standard scope
Applies to: all HCS-governed repos, base rules; scope overlays noted inline where applicable Domain: infrastructure Status: Active
All HCS infrastructure deployments follow a declarative, config-driven model. Human operators do not run ad-hoc Azure CLI commands to create production resources. Every resource is defined in code, versioned, and deployed through a pipeline.
IaC tool selection
| Use case | Tool |
|---|---|
| Azure resource provisioning (VMs, VNets, RGs, Key Vaults, AKS) | Bicep |
| Multi-cloud deployments or complex state management | Terraform |
| OS configuration, role installation, domain join | PowerShell (DSC or Invoke- scripts) |
| Large-scale configuration management across many nodes | Ansible |
| Hybrid: Azure resources + OS configuration | Bicep (Azure) + PowerShell (OS), orchestrated by ADO pipeline |
Do not mix tools for the same resource. If Bicep provisions a VM, Bicep owns that VM's Azure-level configuration. PowerShell configures the OS inside the VM. Never split ownership of a single resource across two tools.
Bicep-first is the base (hcs) default for HCS-owned Azure provisioning. Two scopes deviate — tierpoint-prodtech is Terraform-first and azurelocal requires multi-tool parity. See Scope overlays at the end of this document.
Deployment phases
All infrastructure deployments follow four phases in order. No phase is skipped, even for small changes.
| Phase | What happens | Tool |
|---|---|---|
| Plan | Dry run — show what will change without making changes | az deployment group what-if / terraform plan |
| Provision | Create Azure resources | Bicep / Terraform |
| Configure | Install roles, configure OS, join domain, set registry keys | PowerShell |
| Validate | Assert that the deployed state matches the expected state | Pester contract tests |
Every ADO pipeline that deploys to prd must implement all four phases and require a manual approval gate between Provision and Configure.
stages:
- stage: Plan
jobs:
- job: WhatIf
steps:
- task: AzureCLI@2
inputs:
scriptType: pscore
inlineScript: |
az deployment group what-if `
--resource-group rg-hcs-platform-prd-eus-01 `
--template-file infra/main.bicep `
--parameters @infra/parameters.prd.json
- stage: Provision
dependsOn: Plan
jobs:
- job: Deploy
steps:
- task: AzureCLI@2
inputs:
scriptType: pscore
inlineScript: |
az deployment group create `
--resource-group rg-hcs-platform-prd-eus-01 `
--template-file infra/main.bicep `
--parameters @infra/parameters.prd.json
- stage: Configure
dependsOn: Provision
jobs:
- job: OSConfig
steps:
- task: PowerShell@2
inputs:
filePath: scripts/Invoke-NodeConfiguration.ps1
arguments: -ConfigPath config/variables.yml
- stage: Validate
dependsOn: Configure
jobs:
- job: ContractTest
steps:
- task: PowerShell@2
inputs:
targetType: inline
script: |
Install-Module Pester -Force -SkipPublisherCheck
$r = Invoke-Pester -Path tests/contract/ -PassThru -Output Detailed
if ($r.FailedCount -gt 0) { exit 1 }Relationship to the 3-phase framing used in some scopes
The azurelocal and tierpoint-prodtech scopes describe their deployments in three phases — Azure Foundation / Compute & Workload / Configuration. This is not a competing lifecycle; it is a scoping of the same four-phase model. Their three phases map onto Provision and Configure: "Azure Foundation" and "Compute & Workload" are sub-stages of Provision, and "Configuration" is Configure. The base model's explicit Plan (what-if) and Validate (contract-test) phases still apply on top — those scopes' pipelines already run an equivalent validate/plan step up front and a test step at the end. Use the 4-phase model as the governing structure; the 3-phase names are a convenient grouping of the provisioning work within it.
Bicep conventions
File structure
infra/
├── main.bicep # Entry point — orchestrates modules
├── modules/
│ ├── network.bicep
│ ├── compute.bicep
│ └── identity.bicep
└── parameters/
├── parameters.dev.json
├── parameters.stg.json
└── parameters.prd.jsonParameter files
Never embed environment-specific values in .bicep files. All environment differences live in parameter files.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environment": { "value": "prd" },
"location": { "value": "eastus" },
"instance": { "value": "01" }
}
}Parameter files never contain secrets. Secret values are passed as securestring parameters resolved at pipeline runtime from ADO Variable Groups linked to Key Vault.
Required tags
Apply these tags to every resource group and resource via Bicep:
tags: {
Owner: 'kris@hybridsolutions.cloud'
Project: projectName
Environment: environment
CostCenter: 'hcs-internal'
ManagedBy: 'bicep'
}Terraform conventions (when used)
State management
All Terraform state is stored in Azure Storage, never locally. State files are never committed to repos.
terraform {
backend "azurerm" {
resource_group_name = "rg-hcs-platform-prd-eus-01"
storage_account_name = "sthcsartifactsprdeus01"
container_name = "tfstate"
key = "<project>/<env>/terraform.tfstate"
}
}Variable files
terraform.tfvarsis gitignored — never committedterraform.tfvars.exampleis committed with placeholder values- Secrets are passed as environment variables (
TF_VAR_<name>) from ADO Variable Groups, never in.tfvarsfiles
Config file contract
Every infrastructure repo ships a config/variables.example.yml that defines the complete configuration schema. Operators copy it to config/variables.yml and fill in environment-specific values before running scripts.
See variables standard for the keyvault:// URI scheme, snake_case key naming, and bootstrap policy.
The example file is the documentation. It must include a comment on every key explaining what the value controls and what format it expects:
compute:
azure_local:
# Name of the Azure Local cluster. Used as the resource name in Azure.
# Format: lowercase-kebab-case, max 15 characters.
cluster_name: REPLACE-WITH-YOUR-CLUSTER-NAME
# Primary DNS server for cluster nodes.
dns_servers:
- REPLACE-WITH-DNS-SERVER-1
- REPLACE-WITH-DNS-SERVER-2Resource lifecycle
| Action | Requires |
|---|---|
| Create a new resource | PR with Bicep/Terraform changes + plan output in PR comments |
| Modify an existing resource | PR + what-if / plan output showing the diff |
| Delete a resource | PR + explicit confirmation that deletion is intentional + approval gate in pipeline |
| Emergency manual change | Allowed for P1 incidents only — must be followed within 24h by a PR that codifies the manual change |
No resource is created manually without a follow-up PR. The pipeline is the record of truth.
Security baseline
Every deployed resource must conform to this minimum security baseline. Bicep modules in this (platform) repo enforce these as defaults.
| Control | Requirement |
|---|---|
| Diagnostic settings | All resources send logs to la-hcs-prd-eus-01 (Log Analytics workspace) |
| Managed identity | All compute resources use user-assigned managed identity — no client secrets for Azure auth |
| Key Vault access | Access via RBAC, not legacy access policies |
| Storage | Require HTTPS, disable public access, enable soft delete |
| Network | NSGs on all subnets, no wildcard inbound rules to * |
| Tags | All required tags present (Owner, Project, Environment, CostCenter, ManagedBy) |
Scope overlays
The IaC tool default above (Bicep-first) is the base (hcs) rule. Two scopes carry overlays that change the default toolchain. The 4-phase lifecycle, config-file contract, resource-lifecycle rules, and security baseline are universal — they apply in every scope regardless of which IaC tool is primary. Overlay content is authored in each scope's own repo and resolved at read time (see docs/standards/_scopes/README.md).
| Scope | IaC overlay | Where it lives |
|---|---|---|
tierpoint-prodtech | Terraform-first. Terraform is the reference toolchain, with state in Azure Blob Storage, matching the tp-tf-module-* module repos. Bicep is still preferred over raw ARM for new work, but Terraform is the primary. | prodtech docs repo, staged into _scopes/tierpoint-prodtech/infrastructure.md |
azurelocal | Multi-tool parity. A given solution must be deliverable through Terraform, Bicep, ARM, PowerShell, and Ansible, all producing identical infrastructure. Parity is a hard requirement of the scope, not a preference — see the solutions.md standard for the parity rules and per-solution structure. | AzureLocal platform repo, staged into _scopes/azurelocal/infrastructure.md |
Do not reproduce overlay content here, and do not reproduce the solutions.md parity rules — reference them. Use the tool default documented for the scope you are working in; do not migrate an existing repo from its scope's toolchain to another scope's default.