Foundation Tier Blueprint¶
The Foundation tier is the minimum viable private AI deployment. It provides secure networking, AI services, data storage, and observability without governance or compute modules.
What Is Included¶
| Layer | Modules |
|---|---|
| Foundation | naming, tags, resource_group |
| Networking | vnet, private_dns |
| Security | key_vault + private endpoint |
| AI | openai + PE, ai_search + PE |
| Data | cosmos OR postgres + PE |
| Observability | log_analytics, diagnostics |
What Is Not Included¶
governance/policy_baseline— disabled at this tiercompute/aks_private— disabled at this tier- Customer-managed keys —
enable_cmk = false
Recommended Variables¶
Reference Composition¶
# =============================================================================
# TenantZero AI -- Foundation Tier Blueprint
# =============================================================================
#
# WHAT THIS IS
# ------------
# A reference composition showing the minimum viable private AI deployment.
# Copy this file into envs/<your-env>/ as a starting point, then replace
# placeholder comments with real values from your .tfvars file.
#
# THIS FILE IS NOT EXECUTABLE. It is documentation that mirrors the real
# module composition pattern used by every TenantZero environment stack.
#
# WHAT IS INCLUDED
# ----------------
# - Foundation : naming, tags, resource_group
# - Networking : vnet, private_dns
# - Security : key_vault + private endpoint
# - AI : openai + PE, ai_search + PE
# - Data : cosmos OR postgres + PE
# - Observability: log_analytics, diagnostics
#
# WHAT IS NOT INCLUDED (see enterprise.tf / regulated.tf)
# -------------------------------------------------------
# - governance/policy_baseline (disabled at this tier)
# - compute/aks_private (disabled at this tier)
# - Customer-managed keys (enable_cmk = false)
#
# MODULE PATH CONVENTION
# ----------------------
# All source paths are relative to envs/<env>/ -- i.e. ../../modules/<ns>/<mod>
#
# COMPOSITION PATTERN
# -------------------
# 1. Service module creates the Azure resource (e.g. module "openai")
# 2. Separate PE module wires a private endpoint to it (e.g. module "pe_openai")
# 3. The PE module references the service module's `resource_id` output
# 4. DNS zone IDs come from the shared private_dns module
# This keeps each module dependency-free; the root stack is the only place
# that knows how modules connect.
# =============================================================================
# -- Data source: current Azure context (tenant ID, subscription, etc.) -------
data "azurerm_client_config" "current" {}
# =============================================================================
# Local helpers
# =============================================================================
locals {
# Map logical resource-group keys to generated names from the naming module.
group_names = {
core = module.naming.resource_groups.core
net = module.naming.resource_groups.net
sec = module.naming.resource_groups.sec
obs = module.naming.resource_groups.obs
}
# Private DNS zones required by the Foundation tier services.
# The data zone varies based on whether you chose Cosmos DB or PostgreSQL.
private_dns_zones = concat(
[
"privatelink.openai.azure.com",
"privatelink.vaultcore.azure.net",
"privatelink.search.windows.net",
],
var.data_profile == "cosmos"
? ["privatelink.documents.azure.com"]
: ["privatelink.postgres.database.azure.com"]
)
# Resolve data-layer networking references (used by Cosmos PE / Postgres).
cosmos_zone_id = try(module.private_dns.zone_ids["privatelink.documents.azure.com"], null)
postgres_zone_id = try(module.private_dns.zone_ids["privatelink.postgres.database.azure.com"], null)
postgres_subnet = try(module.networking.subnet_ids["snet-data"], module.networking.subnet_ids["snet-compute"])
# Foundation tier: diagnostics cover the core AI + security resources.
diagnostics_targets = compact(concat(
[
module.openai.resource_id,
module.key_vault.resource_id,
module.ai_search.resource_id,
],
var.data_profile == "cosmos"
? [module.cosmos[0].resource_id]
: [module.postgres[0].resource_id]
))
}
# =============================================================================
# 1. FOUNDATION -- naming, tags, resource groups
# =============================================================================
# These three modules establish the naming convention, tagging policy, and
# resource group layout that every other module depends on.
module "naming" {
source = "../../modules/foundation/naming"
client_name = var.client_name # e.g. "contoso"
env = var.env # e.g. "dev"
unique_suffix = var.unique_suffix # e.g. "x7q" -- keeps global names unique
}
module "tags" {
source = "../../modules/foundation/tags"
client_name = var.client_name
env = var.env
# cost_center = "" # Foundation tier: cost center tag omitted by default
extra_tags = var.tags
}
module "resource_groups" {
source = "../../modules/foundation/resource_group"
groups = {
core = { name = local.group_names.core, location = var.location, tags = module.tags.common_tags }
net = { name = local.group_names.net, location = var.location, tags = module.tags.common_tags }
sec = { name = local.group_names.sec, location = var.location, tags = module.tags.common_tags }
obs = { name = local.group_names.obs, location = var.location, tags = module.tags.common_tags }
}
}
# =============================================================================
# 2. NETWORKING -- VNet, subnets, NSGs, private DNS zones
# =============================================================================
# The VNet provides address space; subnets isolate workloads and private
# endpoints. Private DNS zones enable name resolution for Private-Link-enabled
# services so traffic never leaves the Microsoft backbone.
module "networking" {
source = "../../modules/networking/vnet"
name = module.naming.vnet_name
resource_group_name = module.resource_groups.names["net"]
location = var.location
vnet_cidr = var.vnet_cidr # e.g. "10.0.0.0/16"
subnets = var.subnets # map of subnet objects -- see variables.tf
nsg_rules = var.nsg_rules # optional custom NSG rules per subnet
tags = module.tags.common_tags
}
module "private_dns" {
source = "../../modules/networking/private_dns"
resource_group_name = module.resource_groups.names["net"]
vnet_id = module.networking.vnet_id
zones = local.private_dns_zones
tags = module.tags.common_tags
}
# =============================================================================
# 3. SECURITY -- Key Vault + private endpoint
# =============================================================================
# Key Vault stores secrets, keys, and certificates. It is deployed in
# private-only mode with RBAC authorization and purge protection enabled.
#
# COMPOSITION PATTERN (PE wiring):
# 1. module "key_vault" --> creates the Key Vault, outputs resource_id
# 2. module "pe_key_vault" --> creates PE, references key_vault.resource_id
# 3. DNS zone link comes from module.private_dns zone_ids map
module "key_vault" {
source = "../../modules/security/key_vault"
name = module.naming.key_vault_name
resource_group_name = module.resource_groups.names["sec"]
location = var.location
tenant_id = data.azurerm_client_config.current.tenant_id
tags = module.tags.common_tags
# All security defaults are already private-first:
# public_network_access_enabled = false
# enable_rbac_authorization = true
# purge_protection_enabled = true
}
module "pe_key_vault" {
source = "../../modules/networking/private_endpoint"
name = "pe-${module.naming.key_vault_name}"
location = var.location
resource_group_name = module.resource_groups.names["sec"]
subnet_id = module.networking.subnet_ids["snet-pe"]
private_connection_resource_id = module.key_vault.resource_id
subresource_names = ["vault"]
private_dns_zone_ids = [module.private_dns.zone_ids["privatelink.vaultcore.azure.net"]]
tags = module.tags.common_tags
}
# -- CMK key (disabled in Foundation; enabled in Regulated tier) ---------------
# The CMK resource lives at the ROOT level, not inside a module, because it
# depends on the PE being established first (Key Vault is private-only).
resource "azurerm_key_vault_key" "cmk" {
count = var.enable_cmk ? 1 : 0 # Foundation: false | Regulated: true
name = "cmk-default"
key_vault_id = module.key_vault.resource_id
key_type = "RSA"
key_size = 2048
key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"]
depends_on = [module.pe_key_vault]
}
# =============================================================================
# 4. AI SERVICES -- Azure OpenAI + AI Search, each with private endpoints
# =============================================================================
# Both services are deployed with public access disabled. They are reachable
# only through their respective private endpoints on snet-pe.
module "openai" {
source = "../../modules/ai/openai"
name = module.naming.openai_name
resource_group_name = module.resource_groups.names["core"]
location = var.location
sku = var.openai_sku # default: "S0"
public_network_access_enabled = false
deployments = var.models # list of model deployment objects
tags = module.tags.common_tags
}
module "pe_openai" {
source = "../../modules/networking/private_endpoint"
name = "pe-${module.naming.openai_name}"
location = var.location
resource_group_name = module.resource_groups.names["core"]
subnet_id = module.networking.subnet_ids["snet-pe"]
private_connection_resource_id = module.openai.resource_id
subresource_names = ["account"]
private_dns_zone_ids = [module.private_dns.zone_ids["privatelink.openai.azure.com"]]
tags = module.tags.common_tags
}
module "ai_search" {
source = "../../modules/ai/ai_search"
name = module.naming.ai_search_name
resource_group_name = module.resource_groups.names["core"]
location = var.location
sku = var.search_sku # default: "standard"
replicas = var.search_replicas # default: 1
partitions = var.search_partitions # default: 1
public_network_access_enabled = false
tags = module.tags.common_tags
}
module "pe_ai_search" {
source = "../../modules/networking/private_endpoint"
name = "pe-${module.naming.ai_search_name}"
location = var.location
resource_group_name = module.resource_groups.names["core"]
subnet_id = module.networking.subnet_ids["snet-pe"]
private_connection_resource_id = module.ai_search.resource_id
subresource_names = ["searchService"]
private_dns_zone_ids = [module.private_dns.zone_ids["privatelink.search.windows.net"]]
tags = module.tags.common_tags
}
# =============================================================================
# 5. DATA -- Cosmos DB -or- PostgreSQL (mutually exclusive via data_profile)
# =============================================================================
# The data profile is selected by the `data_profile` variable.
# - "cosmos" --> Cosmos DB (NoSQL) + PE on snet-pe
# - "postgres" --> PostgreSQL Flexible Server via delegated subnet + private DNS
#
# NOTE: PostgreSQL uses VNet-integrated (delegated subnet) networking rather
# than a separate private endpoint resource. The private_dns_zone_id wires
# DNS resolution for the delegated interface.
module "cosmos" {
count = var.data_profile == "cosmos" ? 1 : 0
source = "../../modules/data/cosmos"
name = module.naming.cosmos_name
resource_group_name = module.resource_groups.names["core"]
location = var.location
tags = module.tags.common_tags
}
module "pe_cosmos" {
count = var.data_profile == "cosmos" ? 1 : 0
source = "../../modules/networking/private_endpoint"
name = "pe-${module.naming.cosmos_name}"
location = var.location
resource_group_name = module.resource_groups.names["core"]
subnet_id = module.networking.subnet_ids["snet-pe"]
private_connection_resource_id = module.cosmos[0].resource_id
subresource_names = ["Sql"]
private_dns_zone_ids = [local.cosmos_zone_id]
tags = module.tags.common_tags
}
module "postgres" {
count = var.data_profile == "postgres" ? 1 : 0
source = "../../modules/data/postgres"
name = module.naming.postgres_name
resource_group_name = module.resource_groups.names["core"]
location = var.location
delegated_subnet_id = local.postgres_subnet
private_dns_zone_id = local.postgres_zone_id
tenant_id = data.azurerm_client_config.current.tenant_id
administrator_password = var.postgres_administrator_password
tags = module.tags.common_tags
}
# =============================================================================
# 6. OBSERVABILITY -- Log Analytics workspace + diagnostic settings
# =============================================================================
# Log Analytics collects platform logs and metrics. The diagnostics module
# fans out diagnostic settings to every resource in diagnostics_targets.
module "log_analytics" {
source = "../../modules/observability/log_analytics"
name = module.naming.log_analytics_name
resource_group_name = module.resource_groups.names["obs"]
location = var.location
log_retention_days = var.log_retention_days # Foundation default: 30
tags = module.tags.common_tags
}
module "diagnostics" {
source = "../../modules/observability/diagnostics"
target_resource_ids = local.diagnostics_targets
workspace_id = module.log_analytics.workspace_id
name_prefix = "diag-${module.naming.prefix}"
}
# =============================================================================
# MODULES NOT USED IN FOUNDATION TIER
# =============================================================================
#
# The following modules exist but are disabled at the Foundation tier:
#
# governance/policy_baseline --> enable_policy_baseline = false
# compute/aks_private --> enable_aks = false
#
# See enterprise.tf for governance additions.
# See regulated.tf for compute and CMK additions.