Self-hosted Terraform deployment (Azure)
This guide explains how to deploy Espresso AI's Proxy Service on your Azure infrastructure with Terraform.
You can deploy either:
In a dedicated VNet that Terraform creates.
In an existing VNet that you provide.
Prerequisites
Access to an Azure subscription with permissions to create resource groups, VNets, AKS clusters, public IPs, role assignments, user-assigned managed identities, and (optionally) Key Vault, Azure DNS records, and Azure Front Door.
In the Espresso AI dashboard, go to
Proxy Onboardingand:Enter your Azure Subscription ID so we can grant ACR access for the Proxy image. We will generate a username and password for you to be able to pull the image from our ACR.
Copy your customer name.
If running on Azure, copy Espresso AI's Azure Account ID. This is needed for the ACR url.
Generate an API key for Espresso API authentication.
What this module creates
Resource group (optional) or uses your existing resource group.
VNet with a node subnet (optional) or uses your existing VNet/subnet.
AKS cluster with a system-assigned identity, Workload Identity + OIDC issuer enabled, and an autoscaling node pool.
Static public IP for ingress (placed in the AKS-managed node resource group).
ingress-nginxcontroller wired to the static public IP, with Azure LB annotations tuned for AKS Standard LB.Proxy deployment, service, and HPA in Kubernetes.
TLS for the Ingress, via either:
cert-manager + Let's Encrypt (HTTP-01 by default, DNS-01 via Azure DNS for wildcard hosts), or
a bring-your-own
kubernetes.io/tlssecret you pre-create in the proxy namespace.
Optional Azure DNS A record pointing at the ingress public IP.
Optional managed API key flow via Azure Key Vault + External Secrets Operator (federated workload identity).
Optional Azure Front Door fronting the AKS LB (recommended when clients enforce strict OCSP behavior, e.g. Snowflake's connector).
Example usage
Dedicated VNet + cert-manager (Let's Encrypt) + Azure DNS record
Existing VNet + bring-your-own TLS secret + managed API key in Key Vault
Wildcard ingress + Azure Front Door + DNS-01 wildcard cert
Use this shape when fronting Snowflake's connector (or any client with strict OCSP behavior). AFD terminates TLS for clients with a DigiCert-issued managed cert, and the LE cert on the AKS LB only secures the AFD-to-origin hop.
Argument reference
Top-level arguments
location: Required. Azure region for deployment.customer: Required. Customer identifier used in naming andAPI_URLsuffixing.resource_group_config: Optional. Setcreate = falseand supplynameto deploy into an existing resource group. Default: createsespresso-ai-proxy-rg.create_dedicated_vnet: Optional. Creates a dedicated VNet (true) or uses an existing VNet (false). Default:true.vnet_config: Optional/conditional. Used whencreate_dedicated_vnet = true.existing_vnet_config: Optional/conditional. Required whencreate_dedicated_vnet = false.aks_config: Optional. AKS cluster and node pool settings.proxy_config: Required. Proxy runtime configuration.proxy_api_key_value: Optional/conditional, sensitive. Required whenproxy_config.api_key_secret_mode = MANAGED_AZURE_KEY_VAULT.ingress_config: Optional. NGINX ingress configuration, TLS provisioning, and optional Azure Front Door fronting.dns_config: Optional. Azure DNS A-record configuration.letsencrypt_dns01_azure_dns: Optional. Switches cert-manager to the DNS-01 solver via Azure DNS. Required wheningress_config.ingress_hostis a wildcard.autoscaling_config: Optional. Proxy HPA configuration.tags: Optional. Additional Azure tags. Default:{}.
resource_group_config
create: Optional. Default:true.name: Optional. Default:espresso-ai-proxy-rg. Must be non-empty. Whencreate = false, an existing resource group with this name must exist.
vnet_config
vnet_name: Optional. Default:espresso-ai-proxy-vnet.address_space: Optional. Default:["10.240.0.0/16"]. Must contain at least one valid CIDR whencreate_dedicated_vnet = true.node_subnet_cidr: Optional. Default:10.240.0.0/22. Must be a valid CIDR withinaddress_space.
existing_vnet_config
vnet_id: Required in existing-VNet mode.node_subnet_id: Required in existing-VNet mode.
aks_config
cluster_name: Optional. Default:espresso-ai-proxy. Used as the resource name prefix throughout the module.kubernetes_version: Optional. Default:1.35.api_server_authorized_ranges: Optional. Required whenenable_private_cluster = false. CIDRs allowed to reach the AKS API server.enable_private_cluster: Optional. Default:false. Whentrue, the API server has only a private endpoint.pod_cidr: Optional. Default:10.244.0.0/16. CNI Overlay pod range; not part of the VNet.service_cidr: Optional. Default:10.245.0.0/16. ClusterIP service range.dns_service_ip: Optional. Default:10.245.0.10. Must lie withinservice_cidr.vm_size: Optional. Default:Standard_D8s_v5.node_pool_min_count: Optional. Default:2.node_pool_max_count: Optional. Default:10. Must be ≥node_pool_min_count.enable_log_analytics: Optional. Default:false. Whentrue, attaches a Log Analytics workspace and enables Container Insights.log_analytics_retention_days: Optional. Default:90.
proxy_config
image: Required. Proxy container image URI in Espresso AI's ACR.replicas: Optional. Default:2.proxy_host: Required. Your base domain (e.g.example.com), injected asPROXY_HOST. Use the registrable base domain only — not a full hostname (proxy.example.com), scheme, or port.otel_collector: Optional. OTEL Collector sidecar configuration. Seeotel_collectorbelow.api_key_secret_name: Optional. Kubernetes secret name in the proxy namespace from whichESPRESSO_AI_API_KEYis mounted. Default:espresso-ai.API key secret key name is fixed to
ESPRESSO_AI_API_KEYand is not configurable.api_key_secret_mode: Optional.BYO_K8S_SECRETorMANAGED_AZURE_KEY_VAULT. Default:BYO_K8S_SECRET.api_key_azure_key_vault_secret: Optional. Key Vault secret name used in managed mode. Default:espresso-ai-proxy-api-key. Required (non-empty) whenapi_key_secret_mode = MANAGED_AZURE_KEY_VAULT.api_url: Optional. Base URL. Default:https://api.espressocomputing.com:25831.env_vars: Optional. Map of environment variable key/value pairs. Currently supported keys:keytypedefinitionEXCLUDE_QUERY_TEXTboolDefault:
false. Whether to exclude query text on requests to Espresso AI's API. Note: Enabling this will limit supported functionality.
otel_collector
Nested object under proxy_config. When enabled = true (the default, starting in v0.4.0), the module deploys an OpenTelemetry Collector sidecar in the proxy pod and renders a ConfigMap with its pipeline. The proxy is automatically pointed at http://localhost:4318; the sidecar forwards to your-owned OTLP backend.
enabled: Optional. Default:true. Set tofalseto disable the sidecar; the proxy will then emit OTLP directly tootel_exporter_otlp_endpoint.image: Optional. Full image reference for the collector container. Default:otel/opentelemetry-collector-contrib:0.152.0.customer_endpoint: Optional. OTLP endpoint for the customer's own observability backend. Leave empty (default) to disable the customer exporter; the Espresso pipeline still runs.customer_protocol: Optional.grpc(renders theotlp/customerexporter) orhttp(rendersotlphttp/customer). Default:grpc.customer_signals: Optional. Signals to mirror to the customer exporter. Any subset ofmetrics,logs. Default: all three.customer_auth_secret_name: Optional. Existing Kubernetes Secret in the proxy namespace whose value is mounted asCUSTOMER_OTLP_AUTHand sent as the customer exporter'sAuthorizationheader. Leave empty for unauthenticated endpoints.customer_auth_secret_key: Optional. Key withincustomer_auth_secret_name. Default:authorization.customer_tls_insecure: Optional. Disable TLS verification on the customer exporter. Default:false.
Example — also mirror metrics (not logs) to the customer's own OTLP backend with bearer-token auth:
The customer-otlp-auth Secret must exist in the proxy namespace and contain an authorization key whose value is the full header (e.g. Bearer eyJ...).
For the full list of metrics, spans, and resource attributes the proxy emits — useful for building dashboards and alerts against the customer exporter — see Proxy telemetry reference.
ingress_config
enable_ingress: Optional. Enables the nginx Ingress. Default:true.ingress_host: Required whenenable_ingress = true. Hostname (or wildcard, e.g.*.example.com) the Ingress serves.letsencrypt_email: Optional. When set, installs cert-manager and a Let's EncryptClusterIssuer, and cert-manager auto-issues/renews akubernetes.io/tlssecret for the Ingress. Mutually exclusive withtls_secret_name. Exactly one must be provided whenenable_ingress = true.use_letsencrypt_staging: Optional. Default:false. Switches the issuer to Let's Encrypt staging (useful while iterating to avoid hitting prod rate limits).tls_secret_name: Optional. Bring-your-ownkubernetes.io/tlssecret name in the proxy namespace (e.g. synced from a Key Vault cert via the Secrets Store CSI driver). Mutually exclusive withletsencrypt_email.front_door: Optional. Azure Front Door fronting configuration:enabled: Optional. Default:false.sku_name: Optional.Standard_AzureFrontDoororPremium_AzureFrontDoor. Default:Standard_AzureFrontDoor. Premium adds WAF and Private Link to origin.
dns_config
create_record: Optional. Creates an Azure DNS A record pointing at the ingress public IP. Default:false.zone_name: Required whencreate_record = true. Apex of the existing Azure DNS zone (e.g.customer.example.com).zone_resource_group_name: Required whencreate_record = true. Resource group of the DNS zone.record_name: Optional. Falls back toingress_config.ingress_hostwhen omitted.ttl: Optional. Default:300.
letsencrypt_dns01_azure_dns
When set, cert-manager uses DNS-01 (which supports wildcard certs) instead of HTTP-01. Required when ingress_config.ingress_host is a wildcard, since Let's Encrypt only issues wildcards via DNS-01. The module provisions a user-assigned managed identity, federates it to cert-manager's controller service account, and grants it DNS Zone Contributor on the named zone — no static credentials are needed.
zone_name: Required.zone_resource_group_name: Required.
autoscaling_config
min_replicas: Optional. Default:2.max_replicas: Optional. Default:10. Must be ≥min_replicas.target_cpu_utilization: Optional. Default:70. Must be between 1 and 100.
Secret modes
BYO_K8S_SECRET(default): Proxy reads from an existing Kubernetes secret (api_key_secret_name) in the proxy namespace using fixed keyESPRESSO_AI_API_KEY.MANAGED_AZURE_KEY_VAULT: Module provisions an Azure Key Vault, writes the API key as a secret, federates a user-assigned managed identity to the External Secrets Operator service account, installs ESO, and creates anExternalSecretthat syncs the Key Vault secret into akubernetes.io/tls-style Kubernetes secret in the proxy namespace.
TLS modes
Let's Encrypt (default path). Set
ingress_config.letsencrypt_email. cert-manager runs an HTTP-01 challenge through the nginx Ingress and writes aproxy-tlssecret in the proxy namespace. Renewals are automatic.Let's Encrypt with DNS-01. Add
letsencrypt_dns01_azure_dnsto switch the solver to Azure DNS. Required for wildcard hosts. The module wires up a federated managed identity scoped toDNS Zone Contributoron the target zone — no service-principal credentials needed.Bring-your-own TLS secret. Set
ingress_config.tls_secret_name(and pre-create that secret in the proxy namespace) — useful when an existing process syncs a Key Vault cert via the Secrets Store CSI driver, or when cert-manager is managed outside this module.Front Door fronting. When
ingress_config.front_door.enabled = true, AFD terminates TLS for end clients with a DigiCert-issued managed cert (which has working OCSP). The LE/BYO cert on the AKS LB then only secures the AFD-to-origin hop. Use this when the client enforces strict OCSP (e.g. Snowflake's connector).
Outputs
The module exports:
resource_group_namevnet_idnode_subnet_idaks_cluster_nameaks_node_resource_groupaks_oidc_issuer_urlaks_kubelet_identity_object_id— exposed for advanced cases (e.g. grantingAcrPullon a private registry of your own). Not needed for the standard flow, which uses Espresso AI's ACR.proxy_namespaceproxy_service_nameproxy_ingress_public_ipproxy_hpa_nameproxy_dns_fqdnproxy_api_key_key_vault_name— only set whenMANAGED_AZURE_KEY_VAULTis enabled.front_door_endpoint_hostname— point a CNAME from your custom domain at this. Only set when AFD fronting is enabled.front_door_custom_domain_validation_token— publish at_dnsauth.<ingress_host>so AFD will issue the managed cert. Only set when AFD fronting is enabled.front_door_route_id,front_door_custom_domain_id,front_door_custom_domain_association_id— AFD resource IDs, only set when AFD fronting is enabled.
Customer-side DNS work after AFD provisioning
When ingress_config.front_door.enabled = true, after terraform apply finishes, publish:
CNAME <ingress_host> → <front_door_endpoint_hostname>TXT _dnsauth.<ingress_host> → <front_door_custom_domain_validation_token>
Both values are exposed as outputs above.
How to deploy
Deployment typically takes around 20-30 minutes (longer when Azure Front Door is enabled and waiting on managed-cert validation).
ACR pull is handled on the Espresso AI side — once your tenant ID is registered in the dashboard, our onboarding workflow grants your AKS cluster pull access on the proxy repository in Espresso AI's ACR. No az role assignment is needed in your subscription.
Best practices
Manage sensitive variables (e.g.
proxy_api_key_value) via environment variables or.tfvarsfiles excluded from source control.Keep
aks_config.api_server_authorized_rangestight when running a public API server, or setenable_private_cluster = trueand reach the cluster via a peered network/jumpbox.For wildcard ingress hosts, always pair
letsencrypt_emailwithletsencrypt_dns01_azure_dns— HTTP-01 cannot validate a wildcard.When fronting clients with strict OCSP behavior (Snowflake's connector, in particular), enable
ingress_config.front_doorso end clients see AFD's DigiCert cert instead of the LE cert on the AKS LB.
Last updated