Skip to content

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 caseTool
Azure resource provisioning (VMs, VNets, RGs, Key Vaults, AKS)Bicep
Multi-cloud deployments or complex state managementTerraform
OS configuration, role installation, domain joinPowerShell (DSC or Invoke- scripts)
Large-scale configuration management across many nodesAnsible
Hybrid: Azure resources + OS configurationBicep (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.

PhaseWhat happensTool
PlanDry run — show what will change without making changesaz deployment group what-if / terraform plan
ProvisionCreate Azure resourcesBicep / Terraform
ConfigureInstall roles, configure OS, join domain, set registry keysPowerShell
ValidateAssert that the deployed state matches the expected statePester contract tests

Every ADO pipeline that deploys to prd must implement all four phases and require a manual approval gate between Provision and Configure.

yaml
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.json

Parameter files

Never embed environment-specific values in .bicep files. All environment differences live in parameter files.

json
{
  "$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:

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.

hcl
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.tfvars is gitignored — never committed
  • terraform.tfvars.example is committed with placeholder values
  • Secrets are passed as environment variables (TF_VAR_<name>) from ADO Variable Groups, never in .tfvars files

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:

yaml
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-2

Resource lifecycle

ActionRequires
Create a new resourcePR with Bicep/Terraform changes + plan output in PR comments
Modify an existing resourcePR + what-if / plan output showing the diff
Delete a resourcePR + explicit confirmation that deletion is intentional + approval gate in pipeline
Emergency manual changeAllowed 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.

ControlRequirement
Diagnostic settingsAll resources send logs to la-hcs-prd-eus-01 (Log Analytics workspace)
Managed identityAll compute resources use user-assigned managed identity — no client secrets for Azure auth
Key Vault accessAccess via RBAC, not legacy access policies
StorageRequire HTTPS, disable public access, enable soft delete
NetworkNSGs on all subnets, no wildcard inbound rules to *
TagsAll 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).

ScopeIaC overlayWhere it lives
tierpoint-prodtechTerraform-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
azurelocalMulti-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.

Copyright © Hybrid Cloud Solutions LLC — Kristopher Turner