Custom domain for the OpenAPI public HTTPS endpoint¶
Motivation¶
The public HTTPS path in front of the in-cluster elb-openapi service was hard-pinned
to the Azure-assigned *.cloudapp.azure.com FQDN. An operator who owns a domain
(e.g. an Azure DNS zone
elasticblast.com) wants the API served on a stable, branded host such as
api.elasticblast.com, with the Let's Encrypt
certificate issued for that name.
User-facing change¶
Settings → Public HTTPS now has an optional Custom domain field:
- Leave it empty → unchanged behaviour (cloudapp FQDN + cert for that FQDN).
- Set
api.elasticblast.com→ the setup pipeline: - keeps the cloudapp FQDN as the stable CNAME target,
- best-effort upserts a DNS record in the owning Azure DNS zone (CNAME → cloudapp FQDN for a sub-domain; A → LB IP for an apex),
- applies the Ingress + Certificate with the custom domain as the host,
- cert-manager runs the HTTP-01 challenge against the custom domain,
- stores
https://api.elasticblast.comas the public base URL.
If the managed identity lacks DNS permission, or no hosted zone owns the domain, the DNS step degrades to a manual instruction (returned in the task result) instead of aborting — the certificate still issues once the operator adds the record.
Client- and server-side validation gate the field to a bare public-TLD FQDN (no scheme/path/port; private-use TLDs rejected, same as the operator email).
API / IaC diff summary¶
- Dependency:
azure-mgmt-dns==8.2.0added. - Service:
api/services/azure_clients.py::dns_client; newapi/services/azure_dns.py(split_custom_domain,find_zone_for_fqdn,ensure_public_dns_record) — best-effort, never raises. - Pipeline:
setup_openapi_public_httpsgains acustom_domainarg; the effective Ingress/cert host becomes the custom domain while the cloudapp FQDN stays the CNAME target. Newensure_custom_domain_dnsprogress phase. Metadata + result carrycustom_domain,cloudapp_fqdn,dns_record. - Route:
OpenApiPublicHttpsRequest.custom_domain+_validate_custom_domain. - SPA: custom domain input in
PublicHttpsSection,isValidCustomDomainhelper,enableOpenApiPublicHttps(..., customDomain). - IaC: new
infra/modules/dnsZoneRoles.bicep(DNS Zone Contributor, zone-scoped) wired intoinfra/main.bicepas a conditional module gated onopenApiCustomDnsZoneName(default empty = no new role — charter §12a Rule 4).
RBAC (apply separately — not auto-deployed by this change)¶
The DNS auto-creation needs the shared user-assigned managed identity to hold DNS Zone Contributor on the hosted zone. This change ships the Bicep but does not redeploy. To enable automation, the maintainer sets the params and provisions:
azd provision \
-e <env> \
--set openApiCustomDnsZoneName=elasticblast.com \
--set openApiCustomDnsZoneResourceGroup=rg-elb-dashboard
Or grant it directly (least privilege, zone-scoped):
ZONE_ID=$(az network dns zone show -g rg-elb-dashboard -n elasticblast.com --query id -o tsv)
az role assignment create \
--assignee <shared-uami-principal-id> \
--role "DNS Zone Contributor" \
--scope "$ZONE_ID"
Until then the pipeline still works — it just returns the manual "create this record" instruction instead of writing the CNAME itself.
Validation evidence¶
uv run pytest -q api/tests→ 3810 passed, 3 skipped (newapi/tests/test_azure_dns.py: split / longest-suffix zone match / apex / CNAME upsert / no-zone / forbidden degrade;test_route_validate_custom_domain).uv run ruff checkclean on all touched Python files.az bicep build --file infra/main.bicepcompiles clean.cd web && npm run buildclean;npm test -- --run→ 898 passed; ESLint clean.
Live validation (cannot be done locally)¶
Actual certificate issuance requires a real DNS resolution + Let's Encrypt order, so verify against a live cluster after deploying:
- Enable Public HTTPS with
custom_domain=api.elasticblast.com. - Confirm the CNAME exists:
az network dns record-set cname show -g rg-elb-dashboard -z elasticblast.com -n api. - Wait for the cert: the task's
wait_certificate_readyphase turns green (1-3 min first issuance). curl https://api.elasticblast.com/openapi.json -H "X-ELB-API-Token: <token>".