Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 99 additions & 49 deletions terraform/bindings/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,19 @@ data "terraform_remote_state" "connectivity" {
# ── Locals ────────────────────────────────────────────────────────────────────

locals {
rg_name = data.terraform_remote_state.connectivity.outputs.rg_taskflow_name
snet_compute_id = data.terraform_remote_state.connectivity.outputs.snet_compute_id
oidc_issuer_url = data.terraform_remote_state.compute.outputs.oidc_issuer_url
kubelet_identity = data.terraform_remote_state.compute.outputs.kubelet_identity_object_id
rg_name = data.terraform_remote_state.connectivity.outputs.rg_taskflow_name
snet_compute_id = data.terraform_remote_state.connectivity.outputs.snet_compute_id
oidc_issuer_url = data.terraform_remote_state.compute.outputs.oidc_issuer_url
kubelet_identity = data.terraform_remote_state.compute.outputs.kubelet_identity_object_id
acr_id = data.terraform_remote_state.foundation.outputs.acr_id
key_vault_id = data.terraform_remote_state.foundation.outputs.key_vault_id
api_service_principal_id = data.terraform_remote_state.foundation.outputs.mi_api_service_principal_id
processor_service_principal_id = data.terraform_remote_state.foundation.outputs.mi_processor_service_principal_id
notification_service_principal_id = data.terraform_remote_state.foundation.outputs.mi_notification_service_principal_id
mi_api_service_id = data.terraform_remote_state.foundation.outputs.mi_api_service_id
mi_processor_service_id = data.terraform_remote_state.foundation.outputs.mi_processor_service_id
mi_notification_service_id = data.terraform_remote_state.foundation.outputs.mi_notification_service_id
service_bus_namespace_id = data.terraform_remote_state.foundation.outputs.service_bus_namespace_id

common_tags = {
environment = var.environment
Expand All @@ -61,10 +70,11 @@ locals {
# The kubelet identity is created by AKS at cluster provisioning time —
# this is why the assignment cannot be in the foundation tier.
#
# TODO: implement azurerm_role_assignment.acr_pull
# scope = data.terraform_remote_state.foundation.outputs.acr_id
# role_definition_name = "AcrPull"
# principal_id = local.kubelet_identity
resource "azurerm_role_assignment" "acr_pull" {
scope = local.acr_id
role_definition_name = "AcrPull"
principal_id = local.kubelet_identity
}


# ── Federated Identity Credentials ───────────────────────────────────────────
Expand All @@ -80,37 +90,50 @@ locals {
# 4. Azure AD returns an access token scoped to the managed identity.
# 5. Pod uses that token to authenticate to Key Vault / Service Bus.

# TODO: implement azurerm_federated_identity_credential.api_service
# name = "fed-taskflow-api-service"
# resource_group_name = local.rg_name
# audience = ["api://AzureADTokenExchange"]
# issuer = local.oidc_issuer_url
# parent_id = data.terraform_remote_state.foundation.outputs.mi_api_service_id
# subject = "system:serviceaccount:${var.kubernetes_namespace}:api-service"
resource "azurerm_federated_identity_credential" "api_service" {
name = "fed-taskflow-api-service"
audience = ["api://AzureADTokenExchange"]
issuer = local.oidc_issuer_url
subject = "system:serviceaccount:taskflow:api-service"
}

# TODO: implement azurerm_federated_identity_credential.processor_service
# subject = "system:serviceaccount:${var.kubernetes_namespace}:processor-service"
resource "azurerm_federated_identity_credential" "processor_service" {
name = "fed-taskflow-processor-service"
audience = ["api://AzureADTokenExchange"]
issuer = local.oidc_issuer_url
subject = "system:serviceaccount:taskflow:processor-service"
}

# TODO: implement azurerm_federated_identity_credential.notification_service
# subject = "system:serviceaccount:${var.kubernetes_namespace}:notification-service"
resource "azurerm_federated_identity_credential" "notification_service" {
name = "fed-taskflow-notification-service"
audience = ["api://AzureADTokenExchange"]
issuer = local.oidc_issuer_url
subject = "system:serviceaccount:taskflow:notification-service"
}


# ── Key Vault Role Assignments ────────────────────────────────────────────────
# Grants each service identity read access to Key Vault secrets.
# Key Vault uses RBAC (enable_rbac_authorization = true) — no access policies.
#
# TODO: implement azurerm_role_assignment.kv_api_service
# scope = data.terraform_remote_state.foundation.outputs.key_vault_id
# role_definition_name = "Key Vault Secrets User"
# principal_id = data.terraform_remote_state.foundation.outputs.mi_api_service_principal_id
resource "azurerm_role_assignment" "kv_secrets_api_service" {
scope = local.key_vault_id
role_definition_name = "Key Vault Secrets User"
principal_id = local.api_service_principal_id
}

# TODO: implement azurerm_role_assignment.kv_processor_service
# role_definition_name = "Key Vault Secrets User"
# principal_id = data.terraform_remote_state.foundation.outputs.mi_processor_service_principal_id
resource "azurerm_role_assignment" "kv_secrets_processor_service" {
scope = local.key_vault_id
role_definition_name = "Key Vault Secrets User"
principal_id = local.processor_service_principal_id
}

# TODO: implement azurerm_role_assignment.kv_notification_service
# role_definition_name = "Key Vault Secrets User"
# principal_id = data.terraform_remote_state.foundation.outputs.mi_notification_service_principal_id
resource "azurerm_role_assignment" "kv_secrets_notification_service" {
scope = local.key_vault_id
role_definition_name = "Key Vault Secrets User"
principal_id = local.notification_service_principal_id
}


# ── Service Bus Role Assignments ──────────────────────────────────────────────
Expand All @@ -119,20 +142,26 @@ locals {
#
# Scope is the namespace — not the topic or subscription.
# Namespace-scoped roles cover all topics/subscriptions within it.
#
# TODO: implement azurerm_role_assignment.sb_sender_api_service
# scope = data.terraform_remote_state.foundation.outputs.service_bus_namespace_id
# role_definition_name = "Azure Service Bus Data Sender"
# principal_id = data.terraform_remote_state.foundation.outputs.mi_api_service_principal_id

resource "azurerm_role_assignment" "sb_sender_api_service" {
scope = local.service_bus_namespace_id
role_definition_name = "Azure Service Bus Data Sender"
principal_id = local.api_service_principal_id
}

# TODO: implement azurerm_role_assignment.sb_sender_processor_service
# role_definition_name = "Azure Service Bus Data Sender"
# principal_id = data.terraform_remote_state.foundation.outputs.mi_processor_service_principal_id
resource "azurerm_role_assignment" "sb_sender_processor_service" {
scope = local.service_bus_namespace_id
role_definition_name = "Azure Service Bus Data Sender"
principal_id = local.processor_service_principal_id
}

# TODO: implement azurerm_role_assignment.sb_receiver_notification_service
# scope = data.terraform_remote_state.foundation.outputs.service_bus_namespace_id
# role_definition_name = "Azure Service Bus Data Receiver"
# principal_id = data.terraform_remote_state.foundation.outputs.mi_notification_service_principal_id
resource "azurerm_role_assignment" "sb_receiver_notification_service" {
scope = local.service_bus_namespace_id
role_definition_name = "Azure Service Bus Data Receiver"
principal_id = local.notification_service_principal_id
}


# ── Key Vault Private Endpoint ────────────────────────────────────────────────
Expand All @@ -148,19 +177,40 @@ locals {
# Until then, this endpoint is provisioned but not resolvable.
#
# TODO: implement azurerm_private_endpoint.key_vault
# name = "pe-kv-taskflow"
# resource_group_name = local.rg_name
# location = var.location
# subnet_id = local.snet_compute_id
resource "azurerm_private_endpoint" "kv" {
name = "pe-kv-taskflow"
resource_group_name = local.rg_name
location = var.location
subnet_id = local.snet_compute_id

private_service_connection {
name = "pe-kv-taskflow-conn"
private_connection_resource_id = local.key_vault_id
subresource_names = ["vault"]
is_manual_connection = false
}

tags = local.common_tags
}

# -----------------------------------------------------------------------------
# DNS A record — ON DEMAND ONLY
# Uncomment when the platform deploys the privatelink.vaultcore.azure.net zone.
#
# private_service_connection {
# name = "psc-kv-taskflow"
# private_connection_resource_id = data.terraform_remote_state.foundation.outputs.key_vault_id
# subresource_names = ["vault"]
# is_manual_connection = false
# }
# Without that zone, this private endpoint provisions successfully and receives
# a private IP in snet-compute, but Key Vault's hostname continues to resolve
# to its public IP. Because public_network_access_enabled = false on the vault,
# services will receive a connection timeout — not a 403. The PE is functional;
# the DNS is the missing link.
#
# tags = local.common_tags
# resource "azurerm_private_dns_a_record" "kv" {
# name = local.key_vault_name
# zone_name = "privatelink.vaultcore.azure.net"
# resource_group_name = "<platform-dns-rg>"
# ttl = 300
# records = [azurerm_private_endpoint.kv.private_service_connection[0].private_ip_address]
# }
# -----------------------------------------------------------------------------

# ── Private DNS A Record (on-demand) ─────────────────────────────────────────
# Add this block when platform/connectivity provisions the DNS zone.
Expand Down
177 changes: 98 additions & 79 deletions terraform/compute/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -67,28 +67,36 @@ locals {
# - Traffic path: AKS node → snet-aks → NAT Gateway → internet
# The NVA only handles east-west (spoke-to-spoke). AKS egress bypasses it.

# TODO: implement azurerm_public_ip.nat
# name = "pip-nat-taskflow"
# resource_group_name = local.rg_name
# location = var.location
# allocation_method = "Static"
# sku = "Standard"
# tags = local.common_tags

# TODO: implement azurerm_nat_gateway.taskflow
# name = "nat-taskflow"
# resource_group_name = local.rg_name
# location = var.location
# sku_name = "Standard"
# tags = local.common_tags

# TODO: implement azurerm_nat_gateway_public_ip_association.taskflow
# nat_gateway_id = azurerm_nat_gateway.taskflow.id
# public_ip_address_id = azurerm_public_ip.nat.id

# TODO: implement azurerm_subnet_nat_gateway_association.aks
# subnet_id = local.snet_aks_id
# nat_gateway_id = azurerm_nat_gateway.taskflow.id
resource "azurerm_public_ip" "nat" {
name = "pip-nat-taskflow"
resource_group_name = local.rg_name
location = var.location
allocation_method = "Static"
sku = "Standard"
zones = ["1", "2", "3"]

tags = local.common_tags
}

resource "azurerm_nat_gateway" "nat" {
name = "nat-taskflow"
resource_group_name = local.rg_name
location = var.location
sku_name = "Standard"
idle_timeout_in_minutes = 10

tags = local.common_tags
}

resource "azurerm_nat_gateway_public_ip_association" "nat" {
nat_gateway_id = azurerm_nat_gateway.nat.id
public_ip_address_id = azurerm_public_ip.nat.id
}

resource "azurerm_subnet_nat_gateway_association" "aks" {
subnet_id = local.snet_aks_id
nat_gateway_id = azurerm_nat_gateway.nat.id
}


# ── AKS ───────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -127,62 +135,73 @@ locals {
# Cluster-level: SystemAssigned (manages cluster infrastructure — load balancers, etc.)
# Workload-level: UserAssigned identities from foundation tier, referenced per service.

# TODO: implement azurerm_kubernetes_cluster.taskflow
# name = "aks-taskflow"
# resource_group_name = local.rg_name
# location = var.location
# dns_prefix = "taskflow"
# kubernetes_version = var.kubernetes_version
# sku_tier = var.aks_sku_tier
#
# oidc_issuer_enabled = true
# workload_identity_enabled = true
#
# network_profile {
# network_plugin = "azure"
# network_plugin_mode = "overlay"
# network_policy = "cilium"
# ebpf_data_plane = "cilium"
# outbound_type = "userAssignedNATGateway"
# service_cidr = "172.16.0.0/16"
# dns_service_ip = "172.16.0.10"
# }
#
# api_server_access_profile {
# authorized_ip_ranges = var.authorized_ip_ranges
# }
#
# default_node_pool {
# name = "system"
# node_count = 1
# vm_size = var.system_node_vm_size
# vnet_subnet_id = local.snet_aks_id
# only_critical_addons_enabled = true
# node_labels = {
# "nodepool-type" = "system"
# }
# upgrade_settings {
# max_surge = "10%"
# }
# }
#
# identity {
# type = "SystemAssigned"
# }
#
# tags = local.common_tags
resource "azurerm_kubernetes_cluster" "aks" {
name = "aks-taskflow"
resource_group_name = local.rg_name
location = var.location
dns_prefix = "aks-taskflow"

oidc_issuer_enabled = true
workload_identity_enabled = true

default_node_pool {
name = "system"
node_count = 1
vm_size = "Standard_D2s_v3"
vnet_subnet_id = local.snet_aks_id
os_disk_size_gb = 30
type = "VirtualMachineScaleSets"
only_critical_addons_enabled = true
temporary_name_for_rotation = "systmp"

node_labels = {
"nodepool-type" = "system"
}
}

identity {
type = "SystemAssigned"
}

network_profile {
network_plugin = "azure"
network_plugin_mode = "overlay"
network_policy = "cilium"
network_data_plane = "cilium"
service_cidr = "172.16.0.0/16"
dns_service_ip = "172.16.0.10"
outbound_type = "userAssignedNATGateway"
}

api_server_access_profile {
authorized_ip_ranges = var.authorized_ip_ranges
}

oms_agent {
log_analytics_workspace_id = local.law_id
}

depends_on = [azurerm_subnet_nat_gateway_association.aks]

tags = local.common_tags
}

# TODO: implement azurerm_kubernetes_cluster_node_pool.workload
# name = "workload"
# kubernetes_cluster_id = azurerm_kubernetes_cluster.taskflow.id
# vm_size = var.workload_node_vm_size
# vnet_subnet_id = local.snet_aks_id
# enable_auto_scaling = true
# min_count = var.workload_node_min_count
# max_count = var.workload_node_max_count
# node_labels = {
# "nodepool-type" = "workload"
# }
# upgrade_settings {
# max_surge = "33%"
# }
resource "azurerm_kubernetes_cluster_node_pool" "workload" {
name = "workload"
kubernetes_cluster_id = azurerm_kubernetes_cluster.aks.id
vm_size = "Standard_D2s_v3"
vnet_subnet_id = local.snet_aks_id
os_disk_size_gb = 30
mode = "User"

auto_scaling_enabled = true
min_count = 1
max_count = 3

node_labels = {
"nodepool-type" = "workload"
}

tags = local.common_tags
}
Loading
Loading