Exposing a model
Publish a model at an HTTPS address under your own domain, and decide who may call it. A model with no external access is reachable only inside its cluster.
Before you start
- On the cluster, from its Add-ons tab: Certificates with a ready certificate issuer, Model Serving and the Gateway. DNS Automation is optional: it publishes the endpoint's DNS record, or you create the record yourself. How these add-ons connect: AI inference blueprint. The same list per goal is in Serve a model.
- A domain your organization has verified under Integrations > Domains. See Custom Domains.
- An access policy, created by an organization owner. An admin can create keys on an existing policy. See Access policies.
In the console
Create the access policy and its first key
-
Open Integrations > Access Policies and select API keys. Enter a Policy name and select Create. AstroPulse holds the keys for this policy; it starts with none.
Open zoom view -
On the policy's row, select Keys. Enter a Label, optionally an Expires date, and select Create key. The key is shown once: store it before you close the dialog.
Open zoom view
Expose the model
-
Open Workloads > Models > Deploy model and go to the Access step. For a running model, open it and select Update.
-
Under External access, pick Serve on your domain. Type a hostname, or leave it blank to generate one.
-
Under Who may call it, select the access policy. Anyone (no authentication) serves the model with no credential check; it needs the organization owner role and is recorded in the audit log.
Open zoom view -
Continue to Review, where the dry run checks the cluster can expose the model, then Deploy.
Call it from the browser
- Open the model from Workloads > Models. On Overview, the Endpoint section shows the public address, the in-cluster address, and Who may call it.
- Select Try it to chat with the model from your browser. Paste a key the policy accepts. Try it then shows the key masked to its last four characters, with Change key to use another; a new key starts a new conversation.
Try it is off until the endpoint is ready, with the reason under it when AstroPulse gives one, and off for a model that makes embeddings. Closing the drawer or switching tabs keeps the key and the conversation. Reloading the page or leaving the Models page forgets both. The key is never saved in the browser.
Close or reopen the endpoint
-
In the Endpoint section, select Close endpoint, then Close endpoint again to confirm. Callers can no longer reach the model at its address. The model keeps running inside the cluster, its access policy stays, and the hostname stays reserved.
Open zoom view -
To serve callers again, select Reopen endpoint. AstroPulse checks the policy, its keys and the certificate again, and the endpoint returns to ready within seconds, with no redeploy. A redeploy never reopens a closed endpoint.
You should see
The Endpoint section shows the pill Ready for callers and the policy's rule, for example "Callers must send an API key from access policy qwen-keys.", the public address with Copy and Try it, and Close endpoint.
Before the policy holds a key, the pill reads Not working with the notice The access policy has no API keys: "This endpoint can't serve until its access policy holds an API key. It opens by itself once one exists, with no redeploy." The endpoint becomes ready within about a minute of the first key.
A call without a key gets 401; a call with a key the policy accepts gets 200.
Keys and revoked keys
A key created while the endpoint is ready works within about 10 seconds; until then it gets 401. A revoked key gets 401 within about 10 seconds. Other keys are not affected, and neither change needs a redeploy. Revoke a key under Integrations > Access Policies, Keys, Revoke.
Ready, but the name does not resolve yet
Ready for callers means the certificate, gateway, route and policy check out and the name answered from inside your cluster. It does not mean every caller's DNS resolver can see the name yet.
If someone looks up the hostname before its DNS record exists, their resolver caches "no such host" for the zone's negative TTL: the smaller of the SOA record's TTL and its minimum field, up to 15 minutes on a default Route 53 zone. During that time callers get Could not resolve host, with no HTTP status, while the console shows Ready for callers. A name two or more labels below your domain, such as chat.models.example.com before anything exists under models., is the most likely to be cached as missing.
To check the endpoint meanwhile, ask a public resolver for the name and pin the call to the address it returns (see With astroctl). If no public resolver answers after a few minutes, check that DNS Automation runs with gatewayRoutes: true, or publish the record at your DNS provider.
Shorten the negative TTL
Set the SOA record's TTL to 60 seconds on the zone that holds your endpoint hostnames. That limits a cached miss to about a minute and changes nothing for names that already exist.
- In the Route 53 console, open Hosted zones, then your public zone.
- Edit the
SOArecord, set its TTL to60, and leave its value as it is.
The same with the AWS CLI is under With astroctl.
A private endpoint
Set exposure to Private to serve the model through an internal load balancer, reachable only from the cluster's network. Moving a running model from public to private warns on the dry run and on the apply:
Warning: Load balancer is replaced
This application's endpoint moves from public exposure to private exposure:
its address changes, the old load balancer goes once nothing else uses it,
any hostname follows through external-dns, and connections drop briefly.
Rolling back to a version without private exposure warns that the rollback "can make <host> reachable from the internet. To keep it off the internet, close the application's endpoint before you roll back".
The address and the model id
The endpoint's path follows the API format and the runtime:
| API format | Runtime | Path |
|---|---|---|
V2 | any | /v2/models/<app>/infer, the open inference protocol |
OpenAI (also when left out) | vLLM (generative) | /v1: /v1/chat/completions, /v1/completions, /v1/embeddings |
| default | a predictive format AstroPulse installed (SKLearn, XGBoost, …) | /v1/models/<app>:predict. Set V2 for /v2/models/<app>/infer |
| default | a runtime pinned by name | none reported: AstroPulse does not know where a custom runtime serves |
Inside the cluster, the endpoint is http://<app>-predictor.<namespace>.svc.cluster.local with the same path. The model id a request sends in its model field is neither the application name nor the repository id. The model's Overview shows it next to the endpoint.
Who checks the credential
The Gateway add-on terminates TLS and enforces the access policy. An authenticated endpoint is only possible through the Gateway. A cluster missing a piece refuses to expose a model rather than publish an https:// endpoint that nothing terminates or nothing guards.
A cert-manager or ExternalDNS already on the cluster can be used instead of AstroPulse's: name its ClusterIssuer with astroctl infra k8s exposure set, and an ExternalDNS that runs the Gateway route source publishes route hostnames. DNS is reported on the endpoint, never required.
Other ways in
- Nova: ask it in plain language, for example "expose qwen-chat on acme.ai behind the partner-keys policy".
- SDK and API: the Python SDK and the API reference use the same manifest and endpoint calls.
With astroctl
APP=qwen-chat; PROFILE=prod-inference; DOMAIN=acme.ai; POLICY=partner-keys
ENDPOINT_HOST="${APP}.${DOMAIN}"
cat > policy.yaml <<YAML
apiVersion: platform.astropulse.io/v1
kind: AccessPolicy
metadata:
name: $POLICY
spec:
apiKeys:
header: Authorization
managed: true
YAML
astroctl access-policy apply -f policy.yaml # revision: 1, no keys
cat > model.yaml <<YAML
apiVersion: platform.astropulse.io/v1
kind: Application
spec:
name: $APP
profileName: $PROFILE
externalAccess:
domain: $DOMAIN
accessPolicyRef:
name: $POLICY
source:
type: Model
model:
uri: hf://Qwen/Qwen2.5-0.5B-Instruct:7ae557604adf67be50417f59c2c2f167def9a775
serving:
maxContextTokens: 2048
resources:
cpu: "2"
memory: 6Gi
YAML
astroctl app apply -f model.yaml
astroctl app endpoint get "$APP" # phase: Error, no API keys yet
(umask 077; astroctl access-policy key create "$POLICY" --label first-caller -o json > key.json)
KEY=$(jq -r .key key.json) # shown once; delete key.json once stored
until astroctl app endpoint get "$APP" -o json | jq -e '.phase == "Ready"' >/dev/null; do sleep 10; done
dig +short "${ENDPOINT_HOST}" @1.1.1.1 # IP addresses once the record is published
curl -sS -o /dev/null -w '%{http_code}\n' "https://${ENDPOINT_HOST}/v1/models" # 401
curl -sS -o /dev/null -w '%{http_code}\n' "https://${ENDPOINT_HOST}/v1/models" -H "Authorization: Bearer $KEY" # 200
The resources and maxContextTokens size this model for a CPU node; on a GPU node add resources.accelerator (see Sizing a replica).
Manage the endpoint and check the cluster:
astroctl app endpoint get <app> # desiredState, phase, message, probe, dns, url
astroctl app endpoint close <app> # asks you to type the app name; --yes without a terminal
astroctl app endpoint reopen <app>
astroctl infra k8s exposure get <cluster> # issuer, DNS and whether a model takes the gateway path
astroctl infra k8s exposure set <cluster> --cluster-issuer my-issuer # --clear removes it
probe is AstroPulse calling the endpoint itself: vantage: Cluster from inside, Public from outside. probe.result: Refused means an unauthenticated call was refused, so the policy is enforced. The DNS line in astroctl app get and the dns field are read from the hostname's own zone, never through a shared resolver:
| DNS line | Meaning |
|---|---|
resolves to <addresses>; matches the exposure address | The zone publishes the record and it points at the cluster. A caller's resolver can still hold a cached miss |
resolves to <addresses>; the hostname resolves to an address the exposure does not publish | The record points somewhere else |
does not resolve yet (the hostname does not resolve yet) | The zone has no record for the hostname |
the exposure publishes no public address (NodePort); the hostname cannot be compared | Expected on clusters without a cloud load balancer |
no DNS line and no dns field | No name server answered in time, or the name answered only from inside the cluster (a private or split-horizon name). Not a miss. Also absent with -o json or -o yaml, and without a public hostname |
Pin a call to the address a public resolver returned, while a caller's resolver still caches a miss:
ADDRESS=$(dig +short "${ENDPOINT_HOST}" @1.1.1.1 | tail -n 1)
curl -sS -o /dev/null -w '%{http_code}\n' "https://${ENDPOINT_HOST}/v1/models" \
-H "Authorization: Bearer $KEY" --resolve "${ENDPOINT_HOST}:443:${ADDRESS}" # 200
Shorten the negative TTL on Route 53 from the CLI. Confirm echo prints exactly one zone ID; two mean a public and a private zone share the name, so set ZONE_ID to the one aws route53 get-hosted-zone shows with "PrivateZone": false:
ZONE_NAME="${DOMAIN}"
NS1=$(dig +short NS "${ZONE_NAME}" | head -n 1)
dig +noall +answer SOA "${ZONE_NAME}" "@${NS1}" # 2nd column: TTL; last number: minimum
ZONE_ID=$(aws route53 list-hosted-zones-by-name --dns-name "${ZONE_NAME}" \
--query "HostedZones[?Name=='${ZONE_NAME}.'].Id" --output text)
echo "${ZONE_ID}"
SOA_VALUE=$(aws route53 list-resource-record-sets --hosted-zone-id "${ZONE_ID}" \
--query "ResourceRecordSets[?Type=='SOA'].ResourceRecords[0].Value" --output text)
aws route53 change-resource-record-sets --hosted-zone-id "${ZONE_ID}" \
--change-batch "{\"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{\"Name\":\"${ZONE_NAME}.\",\"Type\":\"SOA\",\"TTL\":60,\"ResourceRecords\":[{\"Value\":\"${SOA_VALUE}\"}]}}]}"
dig +noall +answer SOA "${ZONE_NAME}" "@${NS1}" # 60 in the 2nd column
See the CLI reference for app endpoint, access-policy and infra k8s exposure.
Settings
All fields are under the application's externalAccess.
domainThe verified domain the model is served under, at <app>.<domain>. In the console, Serve on under External access. Set this or hostname, not both. Requires accessPolicyRef.
hostnameThe exact hostname to serve at: a verified domain's apex, or a name under it. Set this or domain, not both.
accessPolicyRef.nameRequiredThe access policy whose API keys or tokens callers must send. In the console, Who may call it. Required with domain or hostname: it never defaults to public.
accessPolicyRef.revisionThe policy revision the endpoint enforces. It is kept with the deployed version: a policy update reaches the model on its next deploy, and a rollback restores the revision it was deployed with.
accessPolicyRef.noneThe explicit opt-out: TLS and no caller authentication. In the console, Anyone (no authentication). Needs the organization owner role.
UnauthenticatedPublicInference, compared exactlyexposureWho can reach the endpoint: Public, the internet, or Private, the cluster's network only.
Public, PrivateaccessTypeHow traffic reaches the model. LoadBalancer is refused for a model: it hands out a raw address with no domain and no certificate.
IngressIngress, which for a model with a policy is the Gateway pathThe API format, which sets the path, is serving.apiFormat on the model; see Settings on Serve a model.
If it's refused
Endpoint states are read on the model's Overview, in the Endpoint section: the pill reads Not ready yet (Pending), Ready for callers, Closed to callers or Not working (Error), with the message under Try it.
UnauthenticatedPublicInference exactly, or choose Anyone (no authentication) in the console.accessType is LoadBalancer.accessType.Could not resolve host401 with the key401, check the key and that it belongs to the policy the model names.astroctl app endpoint get)For a certificate that stays not ready, follow cert-manager's own order: Certificate, CertificateRequest, the ClusterIssuer, then Order and Challenge (cert-manager troubleshooting).
Next
- Access policies: API keys and JWT, and what a policy checks.
- Operate a model: metrics, updates and rollback.
- AI inference blueprint: how the add-ons behind an endpoint connect.