Skip to content

Architectural Decision Records (ADR)

ADR-001: Private-by-Default Network Posture

Status: Accepted Date: 2026-02-18

Context: AI workloads deployed in customer tenants must not expose services to the public internet. Enterprise and regulated customers require private connectivity as a baseline, not an option.

Decision: All PaaS services (OpenAI, Key Vault, AI Search, Cosmos DB) deploy with public_network_access_enabled = false and use private endpoints. PostgreSQL uses private access mode via delegated subnet.

Consequences: Services are only accessible from within the VNet or peered networks. External access requires additional networking configuration (VPN, ExpressRoute, or bastion).


ADR-002: Managed Identity Over Shared Keys

Status: Accepted Date: 2026-02-18

Context: Shared keys create security risk through rotation complexity, exposure in logs, and lateral movement potential.

Decision: All services use system-assigned managed identities. OpenAI has local_auth_enabled = false. Key Vault uses RBAC authorization. No shared keys in the application layer.

Consequences: PostgreSQL currently uses password authentication as a transitional measure. Migration to Entra ID authentication is planned.


ADR-003: Environment Isolation via Separate State

Status: Accepted Date: 2026-02-18

Context: Cross-environment state references create blast radius risk and prevent independent lifecycle management.

Decision: Each environment (dev, staging, prod) is a separate root stack with independent state files, backend configuration, and resource names. No cross-environment data plane references.

Consequences: Module changes must be promoted through environments sequentially. There is no shared state between environments.


ADR-004: Contract-Driven Module Design

Status: Accepted Date: 2026-02-18

Context: Modules that assume upstream context (naming patterns, resource group existence, network topology) become tightly coupled and resist composition.

Decision: Every resource module must accept resource_group_name, location, and tags as explicit inputs. Modules never reference siblings or assume naming patterns. The root stack is the sole composition layer.

Consequences: Root stacks are more verbose but fully explicit. Module reuse across different compositions is straightforward.


ADR-005: No Hidden Defaults

Status: Accepted Date: 2026-02-18

Context: Hardcoded infrastructure values (SKU tiers, capacity settings, access toggles) create security and cost risk that is invisible to consumers.

Decision: Every configurable value must be exposed as a variable with an explicit default. Defaults are documented and secure. Values previously hardcoded in modules (Cosmos DB consistency level, Log Analytics SKU, Key Vault network ACLs, etc.) have been extracted to variables.

Consequences: Module variable surfaces are larger but fully transparent. Consumers can override any default without forking.


ADR-006: Three-Tier Maturity Model

Status: Accepted Date: 2026-02-18

Context: Different customers have different compliance and governance requirements. A single deployment profile cannot serve Foundation, Enterprise, and Regulated use cases.

Decision: Define three maturity tiers (Foundation, Enterprise, Regulated). Each tier extends the previous one by adding modules to the root stack composition. Existing modules are never modified when adding new tiers.

Consequences: Open/Closed principle is maintained. New capabilities extend the stack without modifying core modules. Blueprint composition patterns can be formalized in a blueprints/ directory.


ADR-007: Composition Over Conditionals

Status: Accepted Date: 2026-02-18

Context: Using conditional flags inside modules to enable/disable features creates complex, hard-to-test modules and violates single responsibility.

Decision: Root stacks compose modules explicitly using count or for_each at the composition layer. Modules themselves do not contain conditional resource creation for sibling capabilities.

Consequences: Root stacks are the decision point for what gets deployed. Modules remain focused and testable in isolation.


ADR-008: Private Endpoints Managed at Composition Layer

Status: Accepted Date: 2026-02-18

Context: Service modules (openai, ai_search, cosmos, key_vault) previously embedded ../private_endpoint module calls, creating tight coupling between service modules and the PE module. This violated ADR-004 (dependency inversion) and made PE configuration opaque at the composition level.

Decision: Private endpoint creation is moved to the root stack. Service modules only create the service resource and expose resource_id as output. The root stack composes pe_* modules explicitly, passing in resource IDs, subnet IDs, and DNS zone IDs.

Consequences: Root stacks are more verbose (4 additional PE module blocks). Service modules are fully decoupled and can be composed without private endpoints (e.g., for testing). CMK key creation is also moved to the root stack with explicit depends_on on the Key Vault PE.


ADR-009: PostgreSQL Entra ID Authentication

Status: Accepted Date: 2026-02-18

Context: The postgres module previously used password-based authentication with auto-generated passwords, contradicting the "no shared keys" identity standard. Password rotation creates operational burden and security risk.

Decision: PostgreSQL Flexible Server defaults to Microsoft Entra ID authentication with password auth disabled. The authentication block is configured with active_directory_auth_enabled = true and password_auth_enabled = false. Password auth can be re-enabled for transitional scenarios.

Consequences: Callers must configure Entra ID admin users after provisioning. The random password generation is now conditional on password_auth_enabled = true.


ADR-010: Remote State on Azure Blob Storage

Status: Accepted Date: 2026-02-18

Context: Local Terraform state files are not suitable for team collaboration, CI/CD pipelines, or production deployments. State must be centralized, locked, and access-controlled.

Decision: Each environment uses an Azure Blob Storage backend with environment-specific state keys. Backend configuration is externalized (not committed to git) via backend.tf files copied from backend.tf.example.

Consequences: A storage account must be bootstrapped before first terraform init. Backend configuration files are gitignored to prevent credential leakage.


ADR-011: CI/CD via GitHub Actions with Promotion Gates

Status: Accepted Date: 2026-02-18

Context: Manual Terraform operations do not scale for team-based workflows and create risk of unreviewed changes reaching production.

Decision: A GitHub Actions workflow validates on every PR (format, init, validate, plan for all environments) and applies sequentially on merge to main (dev -> staging -> prod). Each environment apply requires GitHub Environment approval.

Consequences: Requires GitHub Environments configured with protection rules and Azure OIDC credentials per environment.


ADR-012: Namespaced Module Directory Structure

Status: Accepted Date: 2026-02-18

Context: A flat modules/ directory with 15+ modules obscures the logical groupings (foundation, networking, security, AI, data, observability, governance, compute). As the module count grows, discoverability suffers and new contributors cannot quickly identify which modules serve which function.

Decision: Reorganize modules/ into namespaced subdirectories: foundation/, networking/, security/, ai/, data/, observability/, governance/, compute/. Module source paths in environment stacks use ../../modules/<namespace>/<module>.

Consequences: All source = paths in env stacks changed. The original networking module was renamed to networking/vnet to avoid namespace collision. Blueprints and documentation reference the new paths.


ADR-013: Blueprint Tier Compositions

Status: Accepted Date: 2026-02-18

Context: The three-tier maturity model (Foundation, Enterprise, Regulated) is documented in prose but clients lack a concrete starting point showing which modules belong to each tier.

Decision: Create blueprints/ directory with foundation.tf, enterprise.tf, and regulated.tf reference compositions. Each file is a complete root stack pattern showing which modules to include, how to compose private endpoints, and what variables to set for that tier.

Consequences: Blueprints are documentation artifacts, not executable stacks. They must be kept in sync with module interface changes. Each tier is additive — Enterprise extends Foundation, Regulated extends Enterprise.