Skip to content

Migrate ask.defang.io deploy from AWS to Azure - #130

Draft
defangdevs wants to merge 2 commits into
mainfrom
feat/azure-migration
Draft

Migrate ask.defang.io deploy from AWS to Azure#130
defangdevs wants to merge 2 commits into
mainfrom
feat/azure-migration

Conversation

@defangdevs

@defangdevs defangdevs commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the AWS→Azure migration plan from #126, per Lio's answers
in that issue:

  • Drop x-defang-dns-role (AWS-only) and switch deploy.yaml to
    provider: azure with defang-github-action's OIDC flow — no
    cloud-credential step needed, and the AWS-Bedrock "Ensure model
    access" step goes with it.
  • llm's x-defang-llm now provisions a managed Azure AI Foundry
    deployment instead of running the Bedrock gateway container, so
    MODEL becomes the deployment alias ("llm") instead of a Bedrock
    model ID — Azure picks the actual model dynamically from its own
    preference list at deploy time (no way to pin a specific
    "haiku-quality" model there; see the note on the issue).
  • Add .defang/production, reusing the shared UAMI website already
    uses (client ID 6e15e174-...), same call as the website migration.

Depends on

This is a draft because it needs DefangLabs/defang-mvp#3225 merged
and applied first — that PR adds the federated credential
(repo:DefangLabs/docs-chatbot:environment:production) this identity
needs to trust this repo's production environment. Without it, the
Deploy step will fail to get Azure credentials.

Not included

  • Project rename / dropping the stack suffix: nothing here sets an
    explicit legacy suffix — this uses project: docs-chatbot / stack: production (no -beta/-staging), so there's nothing left to
    rename on the Azure side — it's a clean project name from day one.
    If you meant something more (e.g. renaming the GitHub repo itself),
    let me know on Deploy to Defang Azure subscription #126.
  • DNS cutover: Azure's custom-domain writer only touches Azure DNS
    zones, so ask.defang.io needs a manual Route53 A + asuid TXT
    record after the first successful deploy (root AWS account, same
    shape as defang-mvp#3190). I'll post the exact runbook once this
    merges and the first deploy exists.

Test plan

  • DefangLabs/defang-mvp#3225 merged + pulumi up --stack defang applied
  • Mark this ready for review and merge
  • First deploy to Azure succeeds (Deploy step green)
  • Manual Route53 cutover for ask.defang.io
  • Site answers questions correctly (verify Ask Defang still works
    with whichever GPT model Azure picked)

🤖 Generated with Claude Code

https://claude.ai/code/session_hook-DefangLabs-docs-chatbot-c7fc

- drop x-defang-dns-role (AWS-only; Azure's DNS writer only touches
  Azure DNS zones, same as defang-mvp PR 3187 for the website)
- llm's x-defang-llm now provisions a managed Azure AI Foundry
  deployment instead of running the Bedrock gateway container, so
  MODEL becomes the deployment alias ("llm") instead of a Bedrock
  model ID — accepted per-Lio's call on #126
  to let Azure pick the model
- switch deploy.yaml from AWS role-chaining + Bedrock model-agreement
  steps to Azure OIDC (defang-github-action exchanges the workflow's
  own token, no credential step needed) and provider: azure
- add .defang/production, reusing the shared UAMI already used by
  defang-mvp's website (needs a new federated credential for this
  repo's subject — see defang-mvp PR for that)

No staging/dev deploy exists for this repo, so this takes effect the
moment it merges to main.
- compose.yaml: Azure Container Apps caps a probe's InitialDelaySeconds at
  60, and Defang maps start_period onto it, so the inherited 240s failed
  the deploy outright with ContainerAppProbeInitialDelaySecondsOutOfRange.
  Keep the startup budget inside that limit (60 + 10*30 = 360s); ECS
  treats start_period as a grace window, so this stays equivalent there.
- app/rag_system.py: two real bugs against GPT-5.1 that Bedrock never
  exposed, found via a live Azure preview deploy: max_tokens is rejected
  in favor of max_completion_tokens, and Azure opens a stream with a
  chunk whose choices list is empty (content filtering), which raised
  IndexError and killed the answer mid-stream.

Verified end to end against a throwaway Azure preview stack: Deploy
succeeds, the llm service provisions via Azure AI Foundry with MODEL
correctly wired to the "llm" deployment alias, and /v1/ask answers
correctly on gpt-5.1. Stack has been torn down.

Same fixes as #134, ported here so this PR is
correct standalone regardless of merge order between the two.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPfV4G82XiSnHVUhyqXzXv
@defangdevs

Copy link
Copy Markdown
Contributor Author

Route53 cutover runbook for ask.defang.io (manual — needs the AWS root account)

Same shape as defang-mvp#3190 (the website's Azure cutover). Azure's custom-domain
writer only touches Azure DNS zones, so the CI deploy will silently skip
ask.defang.io — this has to be done by hand, once, after the first production
deploy exists.

CAA check — action required (differs from the website cutover)

dig CAA ask.defang.io currently returns only:

ask.defang.io. 3600 IN CAA 0 issue "amazon.com"

This is more restrictive than the defang.io apex (which already allows both
amazon.com and digicert.com — that's why PR 3190's cutover needed no CAA
change). ask.defang.io has its own CAA record, which wins over the parent
zone's for cert issuance on that exact name. Azure Container Apps managed
certificates are issued by DigiCert, so this record must add digicert.com
before the cutover
, or Azure's managed-cert issuance will fail/timeout the
same way the website's did before that gap was found.

Add this to Step 3's change batch (or apply first, separately — CAA can go in
any time before the A/TXT cutover):

{
  "Comment": "Allow DigiCert to issue for ask.defang.io ahead of Azure cutover",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "ask.defang.io.",
        "Type": "CAA",
        "TTL": 3600,
        "ResourceRecords": [
          { "Value": "0 issue \"amazon.com\"" },
          { "Value": "0 issue \"digicert.com\"" }
        ]
      }
    }
  ]
}

(Keeping the amazon.com entry is harmless and avoids touching anything
AWS-side before rollback is no longer needed.)

Step 1 — get the target values from Azure (after this PR merges and the first production deploy runs)

az account set --subscription f311c4db-e998-4c94-906c-7e2637303a05

RG=Defang-docs-chatbot-production   # confirm the exact name: az group list -o table | grep -i docs-chatbot

# Environment default domain + static IP
az containerapp env list -g "$RG" -o table
ENV_NAME=<name-from-above>
az containerapp env show -g "$RG" -n "$ENV_NAME" \
  --query "{defaultDomain:properties.defaultDomain, staticIp:properties.staticIp}"

# The "app" service's verification id (this is the Container App with domainname: ask.defang.io)
az containerapp show -g "$RG" -n app \
  --query "{fqdn:properties.configuration.ingress.fqdn, verificationId:properties.customDomainVerificationId}"

Step 2 — back up the current record before changing anything

Zone: defang.io (hosted zone id Z06745443AJLK8MFSDW2D) — ask.defang.io is a
record in this zone, not its own hosted zone.

aws route53 list-resource-record-sets --hosted-zone-id Z06745443AJLK8MFSDW2D \
  --query "ResourceRecordSets[?starts_with(Name, 'ask.defang.io')]" > ask-defang-io-backup.json

Current record (for the rollback step below):

[
  {
    "Name": "ask.defang.io.",
    "Type": "A",
    "AliasTarget": {
      "HostedZoneId": "Z35SXDOTRQ7X7K",
      "DNSName": "defang-docs-chatbot-beta-alb-1439011063.us-east-1.elb.amazonaws.com.",
      "EvaluateTargetHealth": true
    }
  }
]

(The AWS-side ALB above must stay up until the cutover is confirmed healthy —
see Rollback.)

Step 3 — apply the new records

Replace <PLACEHOLDER> with values read in Step 1. ask.defang.io is a single
apex-style hostname for one Container App (app) — no www pair like the
website needed.

{
  "Comment": "Azure cutover for docs-chatbot (ask.defang.io) - #126 / PR 130",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "ask.defang.io.",
        "Type": "A",
        "TTL": 60,
        "ResourceRecords": [{ "Value": "<PROD_ENV_STATIC_IP>" }]
      }
    },
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "asuid.ask.defang.io.",
        "Type": "TXT",
        "TTL": 60,
        "ResourceRecords": [{ "Value": "\"<APP_VERIFICATION_ID>\"" }]
      }
    }
  ]
}
aws route53 change-resource-record-sets --hosted-zone-id Z06745443AJLK8MFSDW2D \
  --change-batch file://ask-defang-io-cutover.json

Note: the existing record is an ALIAS (Route53's ELB-target A record), so
dig A ask.defang.io will keep returning an A-looking answer throughout —
that's expected, it's now pointing at the Container Apps environment's static
IP instead of the ALB.

Step 4 — verify

  1. Wait for propagation (dig A ask.defang.io, dig TXT asuid.ask.defang.io — TTL 60s).
  2. Confirm the managed cert issues: az containerapp show -g "$RG" -n app --query "properties.configuration.ingress.customDomains" should show a Succeeded/bound state within a few minutes, not 30 min (see the CAA note above if it hangs).
  3. Confirm https://ask.defang.io serves over HTTPS with a DigiCert-issued cert and the chatbot answers questions correctly.

Rollback

If the cutover needs to be reverted (e.g. Azure production isn't healthy yet):

  1. Re-apply the backed-up record from Step 2 with change-resource-record-sets.
  2. Do not tear down the AWS ECS/ALB stack until the Azure cutover is confirmed stable — rollback has nothing to point back to otherwise.
  3. The CAA digicert.com addition is harmless to leave in place even after a rollback (it's additive, doesn't remove amazon.com).

References

  • DefangLabs/defang-mvp#3190 (same shape, for the website's defang.io cutover)
  • Deploy to Defang Azure subscription #126 (this migration)
  • pulumi-defang provider/defangazure/azure/customdomain.go (record shapes Azure expects)

@defangdevs

Copy link
Copy Markdown
Contributor Author

Status update

Rebased onto current main (this needed real conflict resolution, not just markers — main had meanwhile moved the LLM wiring to the generic models:/litellm construct in #131/#132/#133). Verified via defang compose config --provider azure that the resolved config still produces MODEL: llm on the Azure path.

Verified end to end against a throwaway Azure preview stack

Deployed this branch's compose/deploy config to an isolated stack (not production, torn down after). Found and fixed two real bugs along the way (pushed in 835814a):

  • start_period: 240s exceeds Azure Container Apps' 60s cap on a probe's InitialDelaySeconds and fails the deploy outright (ContainerAppProbeInitialDelaySecondsOutOfRange). Now 60s with retries: 10.
  • GPT-5.1 rejects max_tokens (wants max_completion_tokens), and Azure opens a stream with a chunk carrying an empty choices list (content filtering), which raised IndexError and killed the answer mid-stream.

With both fixed, /v1/ask answered a real question correctly against Azure AI Foundry's gpt-5.1, with MODEL correctly wired to the llm deployment alias.

Note: #134 (open, independent) found and fixed the same two bugs via its own preview-overlay approach, plus flagged two pulumi-defang defects (#536, #538, both merged). Worth deciding merge order between #130 and #134 — right now they'll conflict on compose.yaml/rag_system.py since I ported the same fixes here so #130 is correct standalone.

Also found while verifying: even with pulumi-defang#538 (raise LLM deployment capacity to 10K TPM) merged, the actual deployed defang-cd image still requested SKU capacity 1 (1K TPM) — hit rate_limit_exceeded on the very first real question until I manually bumped it. Worth checking whether the defang-cd image needs a fresh release/publish to pick up #538, or production will hit the same 429 on its first real traffic.

Still blocking a real (non-preview) Deploy

DefangLabs/defang-mvp#3225 is merged, but pulumi up --stack defang has not been applied yet — confirmed via az identity federated-credential list that the shared UAMI still only has 3 of the expected 4 federated credentials (missing environment-docs-chatbot-production). I don't have the defang-admin AWS profile this stack's backend needs, so this needs to be run by someone who does before the real CI Deploy step (via GitHub OIDC) can get Azure credentials.

Route53 cutover runbook posted separately once the above lands and a real production deploy exists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant