One Door to the Models, Μέρος 2: Terraform, Bicep ή ARM
Το Bicep δεν έχει state file και δεν αγγίζει το Entra ID. Το Μέρος 2 διαλέγει το επίπεδο IaC για το gateway και βρίσκει το default που μετακινεί το production.

Η πιο καθοριστική γραμμή στο Terraform αυτής της πλατφόρμας είναι μια που δεν γράφει κανείς: το version_upgrade_option σε ένα Azure model deployment έχει προεπιλογή OnceNewDefaultVersionAvailable, οπότε το deployment μετακινείται μόνο του σε νεότερη έκδοση μοντέλου όποτε το Azure αποφασίσει ότι υπάρχει νέο default. Ένα gateway του οποίου ολόκληρη η δικαιολόγηση είναι η κατοχή του κύκλου ζωής των μοντέλων δεν μπορεί να το αφήσει σε μια προεπιλογή. Αυτό το μέρος χτίζει το επίπεδο υποδομής κάτω από την αρχιτεκτονική του Μέρους 1, επιλέγει ανάμεσα σε Terraform, Bicep και ARM με κριτήρια που πραγματικά σηκώνουν βάρος, και απενεργοποιεί τα δύο defaults που παίρνουν αθόρυβα πίσω τον κύκλο ζωής.
Τρεις γλώσσες, και μόνο μία πραγματική ερώτηση
Η σύγκριση συνήθως γίνεται με όρους σύνταξης, που είναι ο λιγότερο ενδιαφέρων άξονας. Το ARM JSON είναι το υπόστρωμα: όλα καταλήγουν τελικά σε ένα ARM template, και κανείς δεν το γράφει πια με το χέρι παρά μόνο όταν κάνει debugging σε αυτό που παρήγαγε το Bicep. Μένουν δύο υποψήφιοι και μία ερώτηση που κρίνει ανάμεσά τους.
Το Bicep είναι μια domain-specific γλώσσα που μεταγλωττίζεται σε ARM, έρχεται μαζί με το Azure CLI, δεν χρειάζεται state file, και έχει μια πραγματική απάντηση στη διαχείριση κύκλου ζωής με τα deployment stacks. Ένα stack παρακολουθεί τους πόρους που διαχειρίζεται, και το actionOnUnmanage αποφασίζει τι συμβαίνει σε έναν πόρο που φεύγει από το template: το detachAll τον αφήνει να τρέχει χωρίς παρακολούθηση, το deleteResources διαγράφει τους πόρους, το deleteAll διαγράφει πόρους και resource groups. Τα stacks μεταφέρουν και deny settings, οπότε ένα stack μπορεί να δημιουργηθεί με το denySettingsMode ορισμένο σε denyDelete ή denyWriteAndDelete, με συγκεκριμένες ενέργειες και principals εξαιρεμένα. Αυτή η τελευταία δυνατότητα δεν έχει άμεσο ισοδύναμο στο Terraform: είναι το ίδιο το Azure που αρνείται τη διαγραφή, όχι ένα plan που αρνείται να την παράγει.
Το Terraform φέρνει ένα state file, ένα plan που μπορείς να επιθεωρήσεις σε ένα pull request, και, αποφασιστικά για αυτή την πλατφόρμα, περισσότερους από έναν providers σε ένα ενιαίο dependency graph. Αυτή είναι η ερώτηση που πραγματικά κρίνει την επιλογή: διαχειρίζεται η πλατφόρμα οτιδήποτε εκτός του control plane του Azure Resource Manager;
Διαχειρίζεται. Το tenant onboarding του Μέρους 1 δίνει σε μια ομάδα ένα Entra ID application, ένα service principal και ένα client credential, και συνδέει το object ID που προκύπτει με ένα API Management subscription και ένα quota. Τα αντικείμενα του Entra ID δεν είναι πόροι ARM. Το Bicep δεν μπορεί να τα δημιουργήσει. Μια πλατφόρμα που ξεκινά από το Bicep καταλήγει με ένα δεύτερο εργαλείο για την ταυτότητα και ένα χειροκίνητο βήμα που ενώνει τα δύο, που είναι ακριβώς η ραφή όπου μια ομάδα που έχει αποσυρθεί κρατά λειτουργικά credentials για έναν ακόμη χρόνο. Το Terraform κρατά τα azurerm, azuread και azapi σε ένα γράφημα, οπότε το application registration και το API Management subscription που εξαρτάται από αυτό δημιουργούνται, αλλάζουν και καταστρέφονται μαζί.
Άρα: Terraform για αυτή την πλατφόρμα. Το τίμιο αντίβαρο είναι ότι, αν η πλατφόρμα σου είναι καθαρά υποδομή στο ARM plane και ο οργανισμός σου ήδη τρέχει καλά Azure Policy και deployment stacks, το Bicep με denyWriteAndDelete είναι ισχυρότερη εγγύηση από ένα Terraform plan που ο καθένας μπορεί να παρακάμψει με ένα targeted apply.
Δύο επίπεδα, δύο state files
Ο διαχωρισμός του Μέρους 1 (το API Management ως data plane, μια υπηρεσία Python σε AKS ως control plane) έχει μια συνέπεια στην υποδομή που αξίζει να γίνει ρητή πριν γραφτεί οποιοδήποτε HCL. Αυτά τα δύο στρώματα αλλάζουν με ρυθμούς που διαφέρουν κατά δύο τάξεις μεγέθους. Το API Management instance, το AKS cluster και το δίκτυο αλλάζουν λίγες φορές τον χρόνο. Τα model deployments, τα backends και οι πολιτικές αλλάζουν κάθε εβδομάδα, μερικές φορές καθημερινά.
Το να μπουν και τα δύο σε ένα state file σημαίνει ότι μια ρουτινιάρικη αλλαγή σε ένα model deployment τρέχει plan απέναντι στο AKS cluster, και ένα apply που αποτυγχάνει στα μισά αφήνει και τα δύο στρώματα μερικώς εφαρμοσμένα. Χώρισέ τα:
infra/
10-platform/ # resource group, network, APIM instance, AKS, Log Analytics
20-models/ # Foundry account, model deployments, APIM backends
30-tenants/ # Entra ID apps, APIM subscriptions, quotas
Κάθε directory είναι δικό του root module με δικό του state, και τα μεταγενέστερα διαβάζουν τα προηγούμενα μέσω terraform_remote_state ή, καλύτερα, μέσω data sources που αναζητούν τους πόρους με το όνομά τους. Τα data sources είναι πιο αργά και πιο φλύαρα, και αξίζουν τον κόπο: σημαίνουν ότι το 20-models δεν σπάει όταν το state του 10-platform αναδιαρθρώνεται.
Η απόφαση για το tier, που δεν είναι απόφαση κόστους
Το Μέρος 1 είπε ότι το gateway διαμεσολαβεί το Anthropic Claude εγγενώς. Αυτή η μία απαίτηση επιλέγει το tier του API Management, επειδή το σχήμα του Anthropic Messages API υποστηρίζεται στα v2 tiers, όχι στα κλασικά. Τα v2 tiers επίσης γίνονται deploy σε λεπτά αντί για τη μακρά αναμονή provisioning για την οποία είναι γνωστά τα κλασικά, κλιμακώνονται σε 10 units στα Basic v2 και Standard v2 και σε 30 στο Premium v2, και από το Standard v2 και πάνω υποστηρίζονται virtual network integration και εισερχόμενα private endpoints.
Αυτό που θυσιάζεις δεν είναι μικρό, και είναι καλύτερα να το ξέρεις τώρα παρά στη διάρκεια ενός περιστατικού. Τα v2 tiers προς το παρόν δεν υποστηρίζουν multi-region deployment, backup και restore του instance, αποστολή συμβάντων στο Event Grid, διαμόρφωση της υπηρεσίας μέσω Git, απευθείας πρόσβαση στο Management API, self-hosted gateways, ή Azure DDoS Protection. Επίσης δεν υπάρχει διαδρομή αναβάθμισης από κλασικό tier σε v2 tier, ούτε μετακίνηση πόρου. Ένα gateway που ξεκινά σε Developer ή Standard και αργότερα χρειάζεται υποστήριξη Anthropic είναι migration, όχι λειτουργία κλιμάκωσης.
Το platform module επομένως ξεκινά εδώ:
resource "azurerm_api_management" "gw" {
name = "apim-genai-${var.env}"
location = azurerm_resource_group.platform.location
resource_group_name = azurerm_resource_group.platform.name
publisher_name = "Platform Engineering"
publisher_email = "platform@example.net"
# StandardV2 is required for the Anthropic Messages API schema.
# Capacity is units, not tokens: 1 unit, scale to 10 without redeploying.
sku_name = "StandardV2_1"
identity {
type = "SystemAssigned"
}
}
Δύο περιορισμοί του provider ανήκουν σε ένα σχόλιο δίπλα σε αυτό το block και όχι σε ένα postmortem. Τα delegation blocks απορρίπτονται κατηγορηματικά σε κάθε V2 SKU και στο Consumption. Και το public_ip_address_id γίνεται δεκτό μόνο για Developer και Premium instances deployed σε virtual network, οπότε ένα V2 instance δεν μπορεί να καρφώσει την εξερχόμενη IP του με αυτόν τον τρόπο, κάτι που μετράει αν ένα model backend κάθεται πίσω από ένα IP allowlist.
Model deployments, και το default που μετακινεί το production
Τα μοντέλα είναι ένα azurerm_cognitive_account συν ένα azurerm_cognitive_deployment ανά λογικό μοντέλο. Εδώ ζει η πρώτη πρόταση αυτού του post:
resource "azurerm_cognitive_deployment" "chat_default" {
name = "chat-default"
cognitive_account_id = azurerm_cognitive_account.foundry.id
model {
format = "OpenAI"
name = "gpt-4.1"
version = "2025-04-14" # pinned, never omitted
}
sku {
name = "GlobalStandard"
capacity = 300 # thousands of TPM: 300 = 300,000 TPM
}
# Default is OnceNewDefaultVersionAvailable, which upgrades this
# deployment when Azure changes the default version. The whole point
# of the catalog is that we decide when a model version changes.
version_upgrade_option = "NoAutoUpgrade"
}
Τρία πράγματα σε αυτό το block είναι εύκολο να πάνε στραβά. Το capacity δεν είναι αριθμός units ούτε ακατέργαστο νούμερο TPM: είναι tokens-per-minute σε χιλιάδες, και έχει προεπιλογή 1, δηλαδή 1.000 TPM. Ένα deployment που δημιουργείται χωρίς ρητό capacity θα κάνει throttle ακόμα και ένα μεμονωμένο test script. Το πεδίο version είναι προαιρετικό, και η παράλειψή του αναθέτει όποια έκδοση είναι η προεπιλεγμένη τη στιγμή της δημιουργίας, κάτι που κάνει τη συμπεριφορά του πόρου συνάρτηση της ημέρας που έγινε το apply. Και το version_upgrade_option έχει τρεις τιμές, OnceNewDefaultVersionAvailable, OnceCurrentVersionExpired και NoAutoUpgrade, από τις οποίες μόνο η τελευταία αφήνει την απόφαση σε σένα. Το OnceCurrentVersionExpired είναι η υπερασπίσιμη μέση λύση: κρατά την καρφωμένη έκδοση μέχρι το Azure να την αποσύρει, και τότε μετακινείται αντί να σπάσει. Διάλεξε μία σκόπιμα ανά μοντέλο, και κατέγραψε ποια στον κατάλογο που χτίζει το Μέρος 9.
Μία ακόμη ιδιότητα αυτού του πόρου εμφανίζεται ως μυστηριωδώς αργό apply αντί για σφάλμα. Ο provider παίρνει ένα lock σε επίπεδο account όσο δημιουργεί ένα deployment, οπότε έντεκα deployments σε ένα Foundry account δεν δημιουργούνται παράλληλα ό,τι κι αν λέει το -parallelism. Το να μοιράσεις τα model deployments σε περισσότερα accounts ανά workload είναι απόφαση throughput όσο και απόφαση quota.
Πού σταματά το azurerm και πού αρχίζει το azapi
Η επιφάνεια AI του Azure κινείται γρηγορότερα από οποιονδήποτε κύκλο εκδόσεων ενός Terraform provider, κάτι που είναι δομικό γεγονός και όχι παράπονο. Η ίδια η καθοδήγηση της Microsoft ονομάζει και τους δύο providers: το azurerm για σταθερούς πόρους, και το azapi για απευθείας χρήση των APIs του Azure Resource Manager, που ακολουθεί την πιο πρόσφατη λειτουργικότητα χωρίς να περιμένει ενημέρωση του provider.
Ο πρακτικός κανόνας που το κρατά μακριά από το χάος: το azapi είναι για πόρους, ποτέ για ολόκληρα υποσυστήματα. Ένα μεμονωμένο azapi_resource για μια δυνατότητα σε preview, δίπλα σε είκοσι πόρους azurerm και με αναφορές σε αυτούς μέσω ID, είναι εντάξει και αναστρέψιμο. Μια πλατφόρμα όπου οι μισοί πόροι είναι ακατέργαστα API bodies έχει παραιτηθεί από την επικύρωση σχήματος και το ευανάγνωστο plan που ήταν εξαρχής ο λόγος να χρησιμοποιηθεί το Terraform. Κάθε azapi block παίρνει ένα σχόλιο που ονομάζει τον πόρο azurerm τον οποίο περιμένει, ώστε η επιστροφή να είναι μια εργασία και όχι ένα αρχαιολογικό project.
State, backend, και το βαρετό κομμάτι που σε σώζει
Remote state σε ένα Azure Storage account, ένα container ανά environment, με κλείδωμα μέσω blob lease, το οποίο το Terraform χειρίζεται εγγενώς για το azurerm backend. Το ίδιο το storage account δεν είναι μέσα στο Terraform: δημιουργείται μία φορά από ένα τεκμηριωμένο script, επειδή ένα state backend που ζει μέσα στο state που αποθηκεύει είναι μια κυκλική εξάρτηση που περιμένει μια κακή μέρα.
terraform {
required_version = "~> 1.9"
backend "azurerm" {
resource_group_name = "rg-tfstate"
storage_account_name = "sttfstategenai"
container_name = "prod"
key = "10-platform.tfstate"
use_azuread_auth = true # no storage keys in the pipeline
}
required_providers {
azurerm = { source = "hashicorp/azurerm", version = "~> 4.36" }
azuread = { source = "hashicorp/azuread", version = "~> 3.0" }
azapi = { source = "Azure/azapi", version = "~> 2.0" }
}
}
Το use_azuread_auth είναι η γραμμή για την οποία αξίζει να επιχειρηματολογήσεις. Χωρίς αυτήν το pipeline αυθεντικοποιείται στο state backend με ένα storage account key, δηλαδή ένα μακρόβιο secret με πλήρη πρόσβαση στο state κάθε environment, που κάθεται σε ένα variable group. Με αυτήν, το workload identity του pipeline εξουσιοδοτείται με role assignment, και το state file κληρονομεί το ίδιο μοντέλο ταυτότητας με όλα τα υπόλοιπα.
Το policy XML ανήκει σε αρχεία
Το Μέρος 1 ονόμασε την εξάπλωση πολιτικών ως τρόπο αποτυχίας από την πρώτη μέρα. Η μηχανική άμυνα είναι ότι το policy XML δεν γράφεται ποτέ στο portal. Ζει στο repository, γίνεται template, και εφαρμόζεται από το ίδιο pipeline με όλα τα υπόλοιπα:
resource "azurerm_api_management_api_policy" "chat" {
api_name = azurerm_api_management_api.chat.name
api_management_name = azurerm_api_management.gw.name
resource_group_name = azurerm_resource_group.platform.name
xml_content = templatefile("${path.module}/policies/chat.xml", {
tpm_default = var.tpm_default
metric_ns = "llm-metrics"
})
}
Το αρχείο template είναι συνηθισμένο XML με τις πολιτικές ορίου tokens και μετρήσεων από το Μέρος 1 μέσα του, και επιθεωρείται ως diff όπως κάθε άλλος κώδικας:
<inbound>
<base />
<llm-token-limit counter-key="@(context.Subscription.Id)"
tokens-per-minute="${tpm_default}"
estimate-prompt-tokens="true"
remaining-tokens-variable-name="remainingTokens" />
<llm-emit-token-metric namespace="${metric_ns}">
<dimension name="Tenant" value="@(context.Subscription.Name)" />
</llm-emit-token-metric>
</inbound>
Πρόσεξε το estimate-prompt-tokens="true", που είναι η ρύθμιση που κάνει ένα αίτημα πάνω από το όριο να αποτυγχάνει στο gateway αντί να καταναλώνει quota του backend για να το μάθει. Κοστίζει λίγη ακρίβεια στην εκτίμηση και σώζει ακριβώς τα tokens που προσπαθούσες να προστατέψεις.
Τέσσερις παγίδες που αξίζει να γραφτούν στον τοίχο
- Η διαγραφή ενός πόρου Foundry δεν ελευθερώνει το όνομά του για 48 ώρες. Το soft delete τον κρατά ανακτήσιμο, και ένα
terraform destroyακολουθούμενο από apply με το ίδιο όνομα αποτυγχάνει μέχρι ο πόρος να γίνει purge ρητά. Χειρότερα, οι χρεώσεις για provisioned deployments συνεχίζονται μέχρι το purge, οπότε ένα environment που καταστράφηκε μπορεί να συνεχίσει να χρεώνει. Διάγραψε τα deployments πριν διαγράψεις το account, και κάνε purge σκόπιμα. - Το PTU δεν αγοράζεται από το Terraform. Τα provisioned SKUs (
ProvisionedManaged,GlobalProvisionedManaged,DataZoneProvisionedManaged) αγοράζονται σε ωριαία βάση με βάση τον αριθμό των deployed PTU, με εκπτώσεις δέσμευσης μέσω Azure Reservations, και αυτό το βήμα αγοράς δεν είναι κάτι που μπορεί να ολοκληρώσει ο provider. Το deployment είναι κώδικας· η δέσμευση είναι μια ενέργεια προμηθειών που συμβαίνει δίπλα του. - Το quota δεν είναι infrastructure as code. Το capacity ενός model deployment δεν μπορεί να ξεπεράσει το quota του subscription για αυτό το μοντέλο και το region, και το quota έρχεται μέσω ενός support request. Το output του plan δεν είναι ο περιορισμός· η σελίδα του quota είναι.
- Καμία αναβάθμιση από κλασικό tier σε v2. Αξίζει να επαναληφθεί επειδή είναι το ένα λάθος σε αυτό το post που κοστίζει migration αντί για apply.
Τι κληρονομεί το Μέρος 3
Στο τέλος αυτού του μέρους υπάρχει ένα API Management instance σε Standard v2 με system-assigned identity, ένα Foundry account με καρφωμένα model deployments που δεν θα μετακινηθούν κάτω από τα πόδια κανενός, ένα AKS cluster χωρίς τίποτα πάνω του ακόμη, Log Analytics και Application Insights που δεν λαμβάνουν τίποτα ακόμη, και τρία state files που μπορούν να γίνουν apply ανεξάρτητα. Καμία εφαρμογή δεν μπορεί να καλέσει τίποτα από αυτά, επειδή κανένα API δεν υπάρχει ακόμη και κανένας tenant δεν έχει credential. Αυτά είναι τα επόμενα δύο μέρη.
Διάβασε επίσης
- Μέρος 3, Το Provider Abstraction και το Streaming, όπου αυτή η υποδομή αποκτά ένα API: model aliases, δρομολόγηση με προτεραιότητες, το circuit breaker, και τι κοστίζει το streaming στον μετρητή tokens.
- Μέρος 1, Τα επιχειρήματα για ένα κεντρικό LLM Gateway, το σενάριο και η απόφαση build ή buy που υλοποιεί αυτή η υποδομή.
- How to Call Multiple Terraform Modules in a Single Terragrunt File στο ercan.cloud, για το στρώμα πάνω από τον διαχωρισμό επιπέδων που περιγράφεται εδώ, όταν μεγαλώσει ο αριθμός των environments.
Για την πλευρά υποδομής και πλατφόρμας της λειτουργίας κάτι τέτοιου σε κλίμακα, οι σημειώσεις πεδίου είναι στο ercan.cloud, και ο κόμβος είναι στο ercanermis.com.
Αναφορές
Περισσότερα από τον Ercan
Δύο ακόμη ιστότοποι, ίδιος συγγραφέας, διαφορετικό έδαφος.
Cloud, AWS, EKS, Terraform, platform engineering.
Σημειώσεις πεδίου από συστήματα παραγωγής. EKS, IAM, Terraform σε κλίμακα οργανισμού, observability, βελτιστοποίηση κόστους.
Επισκεφθείτε ercan.cloud →Ο κόμβος. Σχετικά, συμβουλευτική, επικοινωνία.
Προσωπικός κόμβος και για τις δύο διαδρομές γραφής. Ποιος είμαι, πώς λειτουργεί η συμβουλευτική, πώς να επικοινωνήσετε.
Επισκεφθείτε ercanermis.com →