Skip to main content
Microsoft Azure is a fully supported deployment platform for Tensor9 appliances. Deploying to Azure customer environments provides access to Microsoft’s global cloud infrastructure with enterprise-grade security, compliance, and integration with customers’ existing Azure resources.

Overview

When you deploy an application to Azure customer environments using Tensor9:
  • Customer appliances run entirely within the customer’s Azure subscription
  • Your control plane orchestrates deployments from your dedicated Tensor9 AWS account
  • Managed identities and RBAC enable your control plane to manage customer appliances with customer-approved permissions
  • Service equivalents compile your origin stack into Azure-native resources
Azure appliances leverage Azure services for compute, storage, networking, and observability, providing enterprise-grade infrastructure that integrates seamlessly with your customers’ existing Azure environments.

Prerequisites

Before deploying appliances to Azure customer environments, ensure:

Your control plane

  • Dedicated AWS account for your Tensor9 control plane
  • Control plane installed - See Installing Tensor9
  • Origin stack published - Your application infrastructure defined and uploaded

Customer Azure subscription

Your customers must provide:
  • Azure subscription where the appliance will be deployed
  • Managed identities configured for the four-phase permissions model (Install, Steady-state, Deploy, Operate)
  • Virtual network and networking configured according to their requirements
  • Sufficient subscription quotas for your application’s resource needs
  • Azure region where they want the appliance deployed

Your development environment

  • Azure CLI installed and configured
  • kubectl for Kubernetes operations
  • Terraform or OpenTofu (if using Terraform origin stacks)
  • Docker (if deploying container-based applications)

How Azure appliances work

Azure appliances are deployed using Azure-native services orchestrated by your Tensor9 control plane.
1

Customer provisions managed identities

Your customer creates four managed identities in their Azure subscription, each corresponding to a permission phase: Install, Steady-state, Deploy, and Operate. These identities define what the Tensor9 controller can do within their environment.The customer configures RBAC role assignments that allow your control plane to impersonate these managed identities with appropriate conditions (time windows, approval tags, etc.).
2

You create a release for the customer appliance

You create a release targeting the customer’s appliance:
tensor9 stack release create \
  -appName my-app \
  -customerName acme-corp \
  -vendorVersion "1.0.0" \
  -description "Initial production deployment"
Your control plane compiles your origin stack into a deployment stack tailored for Azure, compiling any non-Azure resources to their Azure service equivalents. The deployment stack downloads to your local environment.
3

Customer grants deploy access

The customer approves the deployment by granting temporary deploy access. This can be manual (updating RBAC role assignments) or automated (scheduled maintenance windows).Once approved, the Tensor9 controller in the appliance can use the Deploy managed identity in the customer’s subscription.
4

You deploy the release

You run the deployment locally against the downloaded deployment stack:
cd acme-corp-production
tofu init
tofu apply
The deployment stack is configured to route resource creation through the Tensor9 controller inside the customer’s appliance. The controller uses the Deploy managed identity and creates all infrastructure resources in the customer’s Azure subscription:
  • Virtual networks, subnets, network security groups
  • AKS clusters, Container Instances, Azure Functions
  • Azure Database for PostgreSQL/MySQL, Azure Blob Storage, Azure Cache for Redis
  • Azure Monitor workspaces, Log Analytics, managed identities, Azure DNS zones
  • Any other Azure resources defined in your origin stack
5

Steady-state observability begins

After deployment, your control plane uses the Steady-state managed identity to continuously collect observability data (logs, metrics, traces) from the customer’s appliance without requiring additional approvals.This data flows to your observability sink, giving you visibility into appliance health and performance.

Service equivalents

When you deploy an origin stack to Azure customer environments, Tensor9 automatically compiles resources from other cloud providers to their Azure equivalents.

How service equivalents work

When compiling a deployment stack for Azure:
  1. AWS resources are compiled - AWS resources are converted to their Azure equivalents
  2. Generic resources are adapted - Cloud-agnostic resources (like Kubernetes manifests) are adapted for Azure
  3. Configuration is adjusted - Resource configurations are modified to match Azure conventions and best practices

Common service equivalents

Service CategoryAWSAzure Equivalent
ComputeECS FargateAzure Container Instances (ACI)
LambdaAzure Functions
EKSAKS (Azure Kubernetes Service)
StorageS3Azure Blob Storage
EBSAzure Managed Disks
DatabaseRDS PostgreSQLAzure Database for PostgreSQL
RDS Aurora MySQL, RDS MySQLAzure Database for MySQL
ElastiCache RedisAzure Cache for Redis
NetworkingVPCVirtual Network (VNet)
ALB/NLB/CLBAzure Load Balancer, Application Gateway
NAT GatewayAzure NAT Gateway
Route 53Azure DNS
SecurityKMSAzure Key Vault
IAM RolesManaged Identities
ObservabilityCloudWatch LogsAzure Monitor Logs
CloudWatch MetricsAzure Monitor Metrics
X-RayApplication Insights
Some popular AWS services (EC2, DynamoDB, EFS) are not currently supported. See Unsupported AWS services for the full list and recommended alternatives.

Example: Compiling an AWS origin stack

If your origin stack defines a Lambda function:
# Origin stack (AWS)
resource "aws_lambda_function" "api" {
  function_name = "myapp-api-${var.instance_id}"
  handler       = "index.handler"
  runtime       = "nodejs18.x"
  role          = aws_iam_role.api_role.arn

  environment {
    variables = {
      INSTANCE_ID = var.instance_id
    }
  }
}
Tensor9 compiles it to an Azure Function:
# Deployment stack (Azure)
resource "azurerm_linux_function_app" "api" {
  name                = "myapp-api-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  service_plan_id     = azurerm_service_plan.main.id

  site_config {
    application_stack {
      node_version = "18"
    }
  }

  app_settings = {
    INSTANCE_ID = var.instance_id
  }

  identity {
    type = "SystemAssigned"
  }
}

Permissions model

Azure appliances use a four-phase managed identity permissions model that balances operational capability with customer control.

The four permission phases

PhaseManaged IdentityPurposeAccess Pattern
Installtensor9-install-${instance_id}Initial setup, major infrastructure changesCustomer-approved, rare
Steady-statetensor9-steadystate-${instance_id}Continuous observability collection (read-only)Active by default
Deploytensor9-deploy-${instance_id}Deployments, updates, configuration changesCustomer-approved, time-bounded
Operatetensor9-operate-${instance_id}Remote operations, troubleshooting, debuggingCustomer-approved, time-bounded

Managed identity structure

Each managed identity is created in the customer’s Azure subscription with RBAC role assignments that allow your control plane to use it. Example: Deploy managed identity with conditional access
# Deploy managed identity
resource "azurerm_user_assigned_identity" "deploy" {
  name                = "tensor9-deploy-${var.instance_id}"
  resource_group_name = var.resource_group_name
  location            = var.location

  tags = {
    instance-id = var.instance_id
    phase       = "deploy"
  }
}

# Grant Deploy identity permissions in customer subscription
resource "azurerm_role_assignment" "deploy_contributor" {
  scope                = data.azurerm_subscription.current.id
  role_definition_name = "Contributor"
  principal_id         = azurerm_user_assigned_identity.deploy.principal_id

  condition_version = "2.0"
  condition         = <<-EOT
    (
      @Resource[Microsoft.Resources/tags:instance-id] StringEquals '${var.instance_id}'
    )
  EOT
}

# Allow vendor control plane to use this identity
resource "azurerm_role_assignment" "deploy_identity_operator" {
  scope                = azurerm_user_assigned_identity.deploy.id
  role_definition_name = "Managed Identity Operator"
  principal_id         = var.vendor_control_plane_identity_id
}
Your control plane can only use the Deploy managed identity when:
  • The customer has granted the Managed Identity Operator role
  • Resources being created are tagged with the correct instance-id
  • The time window hasn’t expired (enforced via conditional access policies)
Customers control when and how long deploy access is granted. Example: Steady-state managed identity (read-only observability)
# Steady-state managed identity
resource "azurerm_user_assigned_identity" "steadystate" {
  name                = "tensor9-steadystate-${var.instance_id}"
  resource_group_name = var.resource_group_name
  location            = var.location

  tags = {
    instance-id = var.instance_id
    phase       = "steadystate"
  }
}

# Grant read-only permissions scoped to appliance resources
resource "azurerm_role_assignment" "steadystate_monitoring_reader" {
  scope                = data.azurerm_subscription.current.id
  role_definition_name = "Monitoring Reader"
  principal_id         = azurerm_user_assigned_identity.steadystate.principal_id

  condition_version = "2.0"
  condition         = <<-EOT
    (
      @Resource[Microsoft.Resources/tags:instance-id] StringEquals '${var.instance_id}'
    )
  EOT
}

resource "azurerm_role_assignment" "steadystate_log_reader" {
  scope                = data.azurerm_subscription.current.id
  role_definition_name = "Log Analytics Reader"
  principal_id         = azurerm_user_assigned_identity.steadystate.principal_id

  condition_version = "2.0"
  condition         = <<-EOT
    (
      @Resource[Microsoft.Resources/tags:instance-id] StringEquals '${var.instance_id}'
    )
  EOT
}

# Allow vendor control plane to use this identity (no time restriction)
resource "azurerm_role_assignment" "steadystate_identity_operator" {
  scope                = azurerm_user_assigned_identity.steadystate.id
  role_definition_name = "Managed Identity Operator"
  principal_id         = var.vendor_control_plane_identity_id
}
The Steady-state managed identity:
  • Can read observability data from resources tagged with the appliance’s instance-id
  • Cannot modify, delete, or terminate any resources
  • Cannot change RBAC role assignments

Deployment workflow with managed identities

1

Customer grants deploy access

Customer approves a deployment by granting the Managed Identity Operator role and setting up conditional access policies. This can be done manually or through automated approval workflows.
2

You execute deployment locally

You run the deployment locally against the downloaded deployment stack:
cd acme-corp-production
tofu init
tofu apply
The deployment stack is configured to route resource creation through the Tensor9 controller in the appliance.
3

Controller uses Deploy identity and creates resources

For each resource Terraform attempts to create, the Tensor9 controller inside the appliance uses the Deploy managed identity and creates the resource in the customer’s subscription.All infrastructure changes occur within the customer’s subscription using their Deploy managed identity permissions.
4

Deploy access expires

After the time window expires or the role assignment is removed, the Deploy identity can no longer be used. Your control plane automatically reverts to using only the Steady-state identity for observability.
See Permissions Model for detailed information on all four phases.

Networking

Azure appliances use an isolated networking architecture with a Tensor9 controller that manages communication with your control plane.

Tensor9 controller VNet

When an appliance is deployed, Tensor9 creates an isolated VNet containing the Tensor9 controller. This VNet is configured with:
  • Azure NAT Gateway: Provides outbound internet connectivity
  • Route to control plane: Establishes a secure channel to your Tensor9 control plane
  • No inbound NSG rules: The controller VNet does not accept inbound connections - all communication is outbound-only
The Tensor9 controller uses this secure channel to:
  • Receive deployments: Deployment stacks are pushed from your control plane to the appliance
  • Configure observability pipeline: Set up log, metric, and trace forwarding to your observability sink
  • Receive operational commands: Execute remote operations initiated from your control plane

Outbound-only security model

The Tensor9 controller in your customer’s appliance is designed to only make outbound connections and not require ingress ports to be opened in your customer’s network perimeter:
# Example: Controller VNet configuration (managed by Tensor9)
resource "azurerm_virtual_network" "tensor9_controller" {
  name                = "tensor9-controller-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  address_space       = ["10.0.0.0/24"]

  tags = {
    instance-id = var.instance_id
    managed-by  = "tensor9"
  }
}

resource "azurerm_subnet" "controller" {
  name                 = "controller-subnet"
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.tensor9_controller.name
  address_prefixes     = ["10.0.0.0/26"]
}

# NAT Gateway for outbound connectivity
resource "azurerm_public_ip" "nat" {
  name                = "tensor9-nat-ip-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  allocation_method   = "Static"
  sku                 = "Standard"

  tags = {
    instance-id = var.instance_id
    managed-by  = "tensor9"
  }
}

resource "azurerm_nat_gateway" "controller" {
  name                = "tensor9-nat-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  sku_name            = "Standard"

  tags = {
    instance-id = var.instance_id
    managed-by  = "tensor9"
  }
}

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

resource "azurerm_subnet_nat_gateway_association" "controller" {
  subnet_id      = azurerm_subnet.controller.id
  nat_gateway_id = azurerm_nat_gateway.controller.id
}

# NSG: egress only, no ingress
resource "azurerm_network_security_group" "controller" {
  name                = "tensor9-controller-nsg-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name

  # Allow outbound HTTPS
  security_rule {
    name                       = "AllowHTTPSOutbound"
    priority                   = 100
    direction                  = "Outbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "443"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  # No inbound rules - controller never accepts inbound connections

  tags = {
    instance-id = var.instance_id
    managed-by  = "tensor9"
  }
}

resource "azurerm_subnet_network_security_group_association" "controller" {
  subnet_id                 = azurerm_subnet.controller.id
  network_security_group_id = azurerm_network_security_group.controller.id
}
This architecture ensures that the customer’s appliance cannot be compromised via inbound network attacks on the controller.

Application VNet topology

Your application resources run in their own VNet(s), completely separate from the Tensor9 controller VNet. The application VNet topology is defined entirely by your origin stack - whatever VPC resources you define in your origin stack will be compiled to Azure VNet resources in the appliance. Example: Application VNet with internet-facing load balancer If your origin stack defines an AWS VPC with public subnets and a load balancer, that topology will compile to Azure VNet resources in the customer’s appliance:
# AWS origin stack - Application VPC
resource "aws_vpc" "application" {
  cidr_block           = "10.1.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name          = "myapp-vpc-${var.instance_id}"
    "instance-id" = var.instance_id
  }
}

# Public subnet for load balancer
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.application.id
  cidr_block              = "10.1.0.0/24"
  availability_zone       = data.aws_availability_zones.available.names[0]
  map_public_ip_on_launch = true

  tags = {
    Name          = "myapp-public-${var.instance_id}"
    "instance-id" = var.instance_id
  }
}

# Private subnet for application servers
resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.application.id
  cidr_block        = "10.1.1.0/24"
  availability_zone = data.aws_availability_zones.available.names[0]

  tags = {
    Name          = "myapp-private-${var.instance_id}"
    "instance-id" = var.instance_id
  }
}

# Application Load Balancer
resource "aws_lb" "application" {
  name               = "myapp-lb-${var.instance_id}"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.lb.id]
  subnets            = [aws_subnet.public.id]

  tags = {
    "instance-id" = var.instance_id
  }
}
This application VPC topology is deployed alongside the Tensor9 controller VNet, but they remain completely separate. The controller VNet manages the control plane connection, while the application VNet handles your application’s traffic and resources.

Resource naming and tagging

All Azure resources should use the instance_id variable to ensure uniqueness across multiple customer appliances.

Parameterization pattern

variable "instance_id" {
  type        = string
  description = "Uniquely identifies the instance to deploy into"
}

# Storage accounts
resource "azurerm_storage_account" "data" {
  name                     = "myappdata${var.instance_id}"
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

# Azure Database for PostgreSQL
resource "azurerm_postgresql_flexible_server" "main" {
  name                = "myapp-db-${var.instance_id}"
  resource_group_name = var.resource_group_name
  location            = var.location
  version             = "15"
  sku_name            = "B_Standard_B1ms"
}

# Azure Functions
resource "azurerm_linux_function_app" "api" {
  name                = "myapp-api-${var.instance_id}"
  resource_group_name = var.resource_group_name
  location            = var.location
  service_plan_id     = azurerm_service_plan.main.id
}

# Managed identities
resource "azurerm_user_assigned_identity" "app" {
  name                = "myapp-${var.instance_id}"
  resource_group_name = var.resource_group_name
  location            = var.location
}

Required tags

Tag all resources with instance-id to enable permissions scoping and observability:
resource "azurerm_storage_account" "data" {
  name                     = "myappdata${var.instance_id}"
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

  tags = {
    instance-id = var.instance_id
    application = "my-app"
    managed-by  = "tensor9"
  }
}

resource "azurerm_kubernetes_cluster" "main" {
  name                = "myapp-aks-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  dns_prefix          = "myapp-${var.instance_id}"

  default_node_pool {
    name       = "default"
    node_count = 2
    vm_size    = "Standard_D2_v2"
  }

  tags = {
    instance-id = var.instance_id
    application = "my-app"
    managed-by  = "tensor9"
  }
}
The instance-id tag:
  • Enables RBAC condition expressions to scope permissions to specific appliances
  • Allows Azure Monitor filters to isolate telemetry by appliance
  • Helps customers track costs per appliance
  • Facilitates resource discovery by Tensor9 controllers

Observability

Azure appliances provide comprehensive observability through Azure Monitor, Log Analytics, and Application Insights.

Azure Monitor Logs

Application and infrastructure logs flow to Log Analytics workspaces:
# Log Analytics workspace
resource "azurerm_log_analytics_workspace" "main" {
  name                = "myapp-logs-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  sku                 = "PerGB2018"
  retention_in_days   = 30

  tags = {
    instance-id = var.instance_id
  }
}

# Enable diagnostics for AKS
resource "azurerm_monitor_diagnostic_setting" "aks" {
  name                       = "aks-diagnostics"
  target_resource_id         = azurerm_kubernetes_cluster.main.id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id

  enabled_log {
    category = "kube-apiserver"
  }

  enabled_log {
    category = "kube-controller-manager"
  }

  enabled_log {
    category = "kube-scheduler"
  }

  metric {
    category = "AllMetrics"
    enabled  = true
  }
}
Your control plane uses the Steady-state managed identity to continuously fetch logs:
az monitor log-analytics query \
  --workspace customer-workspace-id \
  --analytics-query "AzureDiagnostics | where tags_s contains 'instance-id=${var.instance_id}' | take 100" \
  --identity tensor9-steadystate-000000007e
Logs are forwarded to your observability sink for centralized monitoring.

Azure Monitor Metrics

Infrastructure metrics are automatically collected:
  • Virtual Machines: CPU percentage, network in/out, disk operations
  • Azure Database for PostgreSQL: Database connections, CPU percent, storage used
  • Azure Functions: Execution count, execution units, errors
  • AKS: Node CPU/memory, pod counts, API server metrics
  • Azure Load Balancer: Data path availability, health probe status, packet count

Application Insights

Enable distributed tracing for Azure Functions and containerized applications:
resource "azurerm_application_insights" "main" {
  name                = "myapp-insights-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  application_type    = "web"
  workspace_id        = azurerm_log_analytics_workspace.main.id

  tags = {
    instance-id = var.instance_id
  }
}

resource "azurerm_linux_function_app" "api" {
  name                = "myapp-api-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  service_plan_id     = azurerm_service_plan.main.id

  app_settings = {
    APPINSIGHTS_INSTRUMENTATIONKEY             = azurerm_application_insights.main.instrumentation_key
    APPLICATIONINSIGHTS_CONNECTION_STRING      = azurerm_application_insights.main.connection_string
    ApplicationInsightsAgent_EXTENSION_VERSION = "~3"
    INSTANCE_ID                                = var.instance_id
  }
}
Application Insights traces are accessible through the Steady-state identity and forwarded to your observability sink.

Azure Activity Log

All API calls within the customer’s Azure subscription are logged to Activity Log, providing a complete audit trail of what your control plane does:
  • Managed identity usage
  • Resource creation, modification, deletion
  • Permission denials
  • Role assignment changes
Customers have full visibility into your control plane’s actions through their Activity Log.

Artifacts

Azure appliances automatically provision private artifact repositories to store container images and application files deployed by your deployment stacks.

Container images (Azure Container Registry)

When you deploy an appliance, Tensor9 automatically provisions a private Azure Container Registry in the customer’s Azure subscription to store your container images. Example: Origin stack with container service Your AWS origin stack references container images from your vendor’s Amazon ECR:
# ECS Fargate service in your origin stack
resource "aws_ecs_task_definition" "api" {
  family                   = "myapp-api-${var.instance_id}"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "256"
  memory                   = "512"

  container_definitions = jsonencode([
    {
      name  = "api"
      # Reference to your vendor ECR registry
      image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp/api:1.0.0"
      portMappings = [
        {
          containerPort = 8080
          protocol      = "tcp"
        }
      ]
      environment = [
        {
          name  = "INSTANCE_ID"
          value = var.instance_id
        }
      ]
    }
  ])

  tags = {
    "instance-id" = var.instance_id
  }
}
Container copy during deployment When you deploy the deployment stack, Tensor9 automatically:
  1. Detects the container image reference in your ECS task definition
  2. Provisions a private Azure Container Registry in the appliance (e.g., myappacr000000007e.azurecr.io)
  3. Copies the container image from your vendor ECR registry to the appliance’s private ACR
  4. Rewrites the deployment stack to reference the appliance-local registry
The compiled deployment stack will contain an Azure Container Instances or AKS deployment with the rewritten image reference:
# Azure Container Instances
image = "myappacr000000007e.azurecr.io/api:1.0.0"
This ensures the container image is stored locally in the customer’s subscription and the application doesn’t depend on cross-subscription access to your vendor registry. Artifact lifecycle Container artifacts are tied to the deployment stack lifecycle:
  • Deploy (tofu apply): Tensor9 copies the container image from your vendor registry to the appliance’s private registry
  • Destroy (tofu destroy): Deleting the deployment stack also deletes the copied container artifact from the appliance’s private registry
This ensures that artifacts are cleaned up when deployments are removed, preventing orphaned resources.

Function source code

For Lambda functions in your AWS origin stack, Tensor9 automatically handles copying function source code to the customer’s Azure environment:
# Lambda function in your AWS origin stack
resource "aws_lambda_function" "processor" {
  function_name = "myapp-processor-${var.instance_id}"
  handler       = "processor.process_event"
  runtime       = "python3.11"
  role          = aws_iam_role.processor.arn

  # Reference to function code in your vendor S3 bucket
  s3_bucket = "vendor-lambda-sources"
  s3_key    = "processor-v1.0.0.zip"

  environment {
    variables = {
      INSTANCE_ID = var.instance_id
    }
  }

  tags = {
    "instance-id" = var.instance_id
  }
}
During deployment, Tensor9:
  1. Provisions a private Azure Storage Account in the appliance for function sources
  2. Copies the Lambda source archive from your vendor S3 bucket to the appliance’s Storage Account
  3. Compiles the Lambda function to an Azure Function with the appliance-local source reference
Like container images, destroying the deployment stack (tofu destroy) removes the copied function source archives. See Artifacts for comprehensive documentation on artifact management, including immutability requirements and supported artifact types.

Secrets management

Store secrets in AWS Secrets Manager or AWS Systems Manager Parameter Store in your AWS origin stack, then pass them to your application as environment variables.

Secret naming and injection

Always use parameterized secret names and inject them as environment variables:
# AWS Secrets Manager secret
resource "aws_secretsmanager_secret" "db_password" {
  name = "${var.instance_id}/prod/db/password"

  tags = {
    "instance-id" = var.instance_id
  }
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id     = aws_secretsmanager_secret.db_password.id
  secret_string = var.db_password
}

# ECS Fargate task - inject secret as environment variable
resource "aws_ecs_task_definition" "app" {
  family = "myapp-${var.instance_id}"

  container_definitions = jsonencode([
    {
      name  = "app"
      image = "myapp:latest"

      # Inject secret as environment variable
      secrets = [
        {
          name      = "DB_PASSWORD"
          valueFrom = aws_secretsmanager_secret.db_password.arn
        }
      ]
    }
  ])

  tags = {
    "instance-id" = var.instance_id
  }
}
Your application reads secrets from environment variables:
import os

# Read secret from environment variable
db_password = os.environ['DB_PASSWORD']
If your application dynamically fetches secrets using AWS SDK calls (e.g., boto3.client('secretsmanager').get_secret_value()), those calls will NOT be automatically mapped by Tensor9. Always pass secrets as environment variables.
See Secrets for detailed secret management patterns.

Operations

Perform remote operations on Azure appliances using the Operate managed identity.

kubectl on AKS

Execute kubectl commands against AKS clusters:
tensor9 ops kubectl \
  -appName my-app \
  -customerName acme-corp \
  -originResourceId "azurerm_kubernetes_cluster.main" \
  -command "kubectl get pods -n my-app-namespace"
Output:
NAME                     READY   STATUS    RESTARTS   AGE
api-7d9f8b5c6d-9k2lm    1/1     Running   0          2h
worker-5c8d7b4f3-8h4km  1/1     Running   0          2h

Azure CLI operations

Execute Azure CLI commands:
# List storage container contents
tensor9 ops az \
  -appName my-app \
  -customerName acme-corp \
  -originResourceId "azurerm_storage_account.data" \
  -command "az storage blob list --account-name myappdata000000007e --container-name files"

# Invoke Azure Function
tensor9 ops az \
  -appName my-app \
  -customerName acme-corp \
  -originResourceId "azurerm_linux_function_app.api" \
  -command "az functionapp function invoke --name myapp-api-000000007e --function-name processor"

# View PostgreSQL status
tensor9 ops az \
  -appName my-app \
  -customerName acme-corp \
  -originResourceId "azurerm_postgresql_flexible_server.main" \
  -command "az postgres flexible-server show --name myapp-db-000000007e --resource-group myapp-rg"

Database queries

Execute SQL queries against Azure Database for PostgreSQL:
tensor9 ops db \
  -appName my-app \
  -customerName acme-corp \
  -originResourceId "azurerm_postgresql_flexible_server.main" \
  -command "SELECT count(*) FROM users WHERE created_at > NOW() - INTERVAL '24 hours'"

Operations endpoints

Create temporary operations endpoints for interactive access:
# Create kubectl endpoint
tensor9 ops endpoint create \
  -appName my-app \
  -customerName acme-corp \
  -originResourceId "azurerm_kubernetes_cluster.main" \
  -endpointType kubectl \
  -ttl 3600

# Output:
# Endpoint created: https://ops.tensor9.io/kubectl/abc123
# Expires in: 1 hour
# Use: kubectl --server=https://ops.tensor9.io/kubectl/abc123 get pods
See Operations for comprehensive operations documentation.

Example: Complete Azure appliance

Here’s a complete example of a deployment stack for an Azure appliance, compiled from an AWS origin stack:

main.tf

# Virtual Network
resource "azurerm_virtual_network" "main" {
  name                = "myapp-vnet-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  address_space       = ["10.0.0.0/16"]

  tags = {
    instance-id = var.instance_id
  }
}

# Subnets
resource "azurerm_subnet" "private" {
  name                 = "private-subnet"
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = ["10.0.1.0/24"]
}

# AKS Cluster
resource "azurerm_kubernetes_cluster" "main" {
  name                = "myapp-aks-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  dns_prefix          = "myapp-${var.instance_id}"
  kubernetes_version  = "1.28.3"

  default_node_pool {
    name       = "default"
    node_count = 2
    vm_size    = "Standard_D2_v2"
    vnet_subnet_id = azurerm_subnet.private.id
  }

  identity {
    type = "SystemAssigned"
  }

  oms_agent {
    log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id
  }

  tags = {
    instance-id = var.instance_id
  }
}

# Azure Database for PostgreSQL
resource "azurerm_postgresql_flexible_server" "main" {
  name                = "myapp-db-${var.instance_id}"
  resource_group_name = var.resource_group_name
  location            = var.location
  version             = "15"
  sku_name            = "B_Standard_B1ms"
  storage_mb          = 32768

  administrator_login    = "adminuser"
  administrator_password = var.db_password

  zone = "1"

  tags = {
    instance-id = var.instance_id
  }
}

resource "azurerm_postgresql_flexible_server_database" "main" {
  name      = "myapp"
  server_id = azurerm_postgresql_flexible_server.main.id
  charset   = "UTF8"
  collation = "en_US.utf8"
}

# Storage Account
resource "azurerm_storage_account" "data" {
  name                     = "myappdata${var.instance_id}"
  resource_group_name      = var.resource_group_name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"

  blob_properties {
    versioning_enabled = true
  }

  tags = {
    instance-id = var.instance_id
  }
}

resource "azurerm_storage_container" "data" {
  name                  = "application-data"
  storage_account_name  = azurerm_storage_account.data.name
  container_access_type = "private"
}

# Azure Cache for Redis
resource "azurerm_redis_cache" "main" {
  name                = "myapp-redis-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  capacity            = 0
  family              = "C"
  sku_name            = "Basic"
  redis_version       = "6"

  tags = {
    instance-id = var.instance_id
  }
}

# Log Analytics Workspace
resource "azurerm_log_analytics_workspace" "main" {
  name                = "myapp-logs-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  sku                 = "PerGB2018"
  retention_in_days   = 30

  tags = {
    instance-id = var.instance_id
  }
}

# Application Insights
resource "azurerm_application_insights" "main" {
  name                = "myapp-insights-${var.instance_id}"
  location            = var.location
  resource_group_name = var.resource_group_name
  workspace_id        = azurerm_log_analytics_workspace.main.id
  application_type    = "web"

  tags = {
    instance-id = var.instance_id
  }
}

variables.tf

variable "instance_id" {
  type        = string
  description = "Uniquely identifies the instance to deploy into"
}

variable "resource_group_name" {
  type        = string
  description = "Azure resource group name"
}

variable "location" {
  type        = string
  description = "Azure region"
  default     = "eastus"
}

variable "db_password" {
  type        = string
  description = "Database administrator password"
  sensitive   = true
}

outputs.tf

output "aks_cluster_endpoint" {
  description = "AKS cluster endpoint"
  value       = azurerm_kubernetes_cluster.main.kube_config[0].host
  sensitive   = true
}

output "database_fqdn" {
  description = "PostgreSQL database FQDN"
  value       = azurerm_postgresql_flexible_server.main.fqdn
  sensitive   = true
}

output "redis_hostname" {
  description = "Redis cache hostname"
  value       = azurerm_redis_cache.main.hostname
}

output "storage_account_name" {
  description = "Storage account name"
  value       = azurerm_storage_account.data.name
}

output "application_insights_key" {
  description = "Application Insights instrumentation key"
  value       = azurerm_application_insights.main.instrumentation_key
  sensitive   = true
}

Best practices

Every Azure resource with a name should include ${var.instance_id} to prevent conflicts across customer appliances:
# ✓ CORRECT
resource "azurerm_storage_account" "data" {
  name = "myappdata${var.instance_id}"
}

resource "azurerm_kubernetes_cluster" "main" {
  name = "myapp-aks-${var.instance_id}"
}

# ✗ INCORRECT - Will cause collisions
resource "azurerm_storage_account" "data" {
  name = "myappdata"
}
Note: Storage account names must be globally unique and can only contain lowercase letters and numbers (no hyphens).
Apply the instance-id tag to every resource:
tags = {
  instance-id = var.instance_id
  application = "my-app"
  managed-by  = "tensor9"
}
This enables:
  • RBAC condition expressions for permission scoping
  • Azure Monitor filtering
  • Cost tracking
  • Resource discovery
Configure diagnostics for AKS, Azure Functions, databases, and other services:
resource "azurerm_monitor_diagnostic_setting" "aks" {
  name                       = "aks-diagnostics"
  target_resource_id         = azurerm_kubernetes_cluster.main.id
  log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id

  enabled_log {
    category = "kube-apiserver"
  }

  metric {
    category = "AllMetrics"
    enabled  = true
  }
}
This ensures observability data flows to your control plane.
Never hardcode secrets. Use AWS Secrets Manager or SSM Parameter Store with parameterized names in your AWS origin stack:
# AWS Secrets Manager (recommended)
resource "aws_secretsmanager_secret" "api_key" {
  name = "${var.instance_id}/prod/api/key"

  tags = {
    "instance-id" = var.instance_id
  }
}

# Or AWS Systems Manager Parameter Store
resource "aws_ssm_parameter" "db_password" {
  name  = "/${var.instance_id}/prod/db/password"
  type  = "SecureString"
  value = var.db_password

  tags = {
    "instance-id" = var.instance_id
  }
}
Pass secrets to your application as environment variables. Runtime SDK calls to fetch secrets are not automatically mapped by Tensor9.

Troubleshooting

Symptom: Terraform apply fails with “AuthorizationFailed” or “Forbidden” errors.Solutions:
  • Verify the Tensor9 controller has successfully authenticated with the Deploy managed identity
  • Check the Deploy identity’s role assignments include necessary permissions for the resources being created
  • Ensure the RBAC conditional access policies allow the operation
  • Verify resources are tagged with the correct instance-id
  • Review Azure Activity Log in the customer subscription to see which specific API call was denied
Symptom: “ResourceExists” or “NameNotAvailable” errors during deployment.Solutions:
  • Ensure all resource names include ${var.instance_id}
  • Verify the instance_id variable is being passed correctly
  • Check that no hardcoded resource names exist in your origin stack
  • For storage accounts, remember names must be globally unique and only contain lowercase letters and numbers
  • For storage accounts, ensure the name is between 3-24 characters
Symptom: Azure Monitor logs and metrics aren’t appearing in your observability sink.Solutions:
  • Verify the Steady-state identity has Monitoring Reader and Log Analytics Reader permissions
  • Check that all resources are tagged with instance-id
  • Ensure diagnostic settings are configured for all resources
  • Verify Log Analytics workspace retention is set appropriately
  • Check that the control plane is successfully using the Steady-state identity
Symptom: “QuotaExceeded” or “OperationNotAllowed” errors when creating resources.Solutions:
  • Ask the customer to request quota increases from Azure Portal
  • Consider deploying appliances in separate Azure regions
  • Review and clean up unused resources in the customer’s subscription
  • For virtual machine quotas, consider using different VM sizes
Symptom: Kubernetes cluster creation times out or fails.Solutions:
  • Verify the region supports AKS
  • Check that the Kubernetes version is supported in the region
  • Ensure the VM SKU is available in the region
  • Verify VNet and subnet configuration is correct
  • Check that service principal or managed identity has necessary permissions
  • Review Azure Service Health for service incidents
Symptom: “StorageAccountNameInvalid” errors.Solutions:
  • Ensure storage account names only contain lowercase letters and numbers (no hyphens)
  • Verify the name is between 3-24 characters
  • Check that ${var.instance_id} doesn’t contain invalid characters
  • Consider shortening the app name prefix if the full name is too long
If you’re experiencing issues not covered here or need additional assistance with Azure deployments, we’re here to help:
  • Slack: Join our community Slack workspace for real-time support
  • Email: Contact us at [email protected]
Our team can help with deployment troubleshooting, managed identity configuration, service equivalents, and best practices for Azure environments.

Next steps

Now that you understand deploying to Azure customer environments, explore these related topics: