Skip to content
nikhil.

~/blog/azure-to-aws-migration-four-products

Azure to AWS for four AI products: translate the guarantees, not the services

Moving Liquidmind's trade-compliance products from Azure to AWS taught me that a migration is a list of guarantees your code silently depends on. Message locks, blob triggers, idempotent claims, JSON mode: here is each one and what replaced it.

4 min read–

At Liquidmind we run four AI products for trade and customs compliance: TradeGuard, Patram AI, TariffIQ and Smart Export. TradeGuard was born on Azure. The newer products were AWS-first. One small team was paying for two clouds in money, credentials, and the cost of remembering which queue lives where.

I moved everything onto one AWS architecture. The full case study is here. This post is about the one idea that made the migration predictable.

A migration is a list of guarantees

The tempting approach is a service map: Blob to S3, Cosmos to DynamoDB, Service Bus to SQS, done. That map is correct and nearly useless. Code does not depend on a service's name. It depends on a behaviour the service provides without anyone writing it down.

So before moving anything, I listed every behaviour the Azure code relied on, and for each one asked: what gives me this on AWS, and where does the code have to change because AWS gives it differently?

1. "The message stays mine while I work on it"

Service Bus holds a lock on a message while a worker processes it. Our extract worker runs OCR and an LLM over multi-page scans, and that can take minutes.

SQS has no lock. It has a visibility timeout: the message is hidden for N seconds, then reappears for someone else. If extraction runs longer than N, a second worker starts the same document, and you pay for Textract and Claude twice.

The fix is a heartbeat: a background thread that keeps calling ChangeMessageVisibility while the work is in progress. On a transient failure, the worker sets a backoff visibility instead of crashing and letting the message reappear at once.

python
def heartbeat(queue_url, receipt, stop, every=60, extend_to=600):
    while not stop.wait(every):
        sqs.change_message_visibility(
            QueueUrl=queue_url, ReceiptHandle=receipt, VisibilityTimeout=extend_to
        )

2. "When a file lands, something runs"

On Azure, blob-triggered functions fired when a document arrived. The AWS shape is S3 ObjectCreated → SQS → a small listener service on Fargate. Two things changed:

  • S3 event delivery is at-least-once, so the listener now deduplicates in DynamoDB before forwarding work to the extract queue.
  • Using a queue instead of a direct invoke gave us buffering and retries for free, and a place to look when something is stuck.

3. "Exactly one worker starts the comparison"

A TradeGuard check compares two to four documents. Comparison can only start when all of them are extracted, and must start exactly once. On Azure that was a "create if absent" row in Table storage.

On AWS it is a DynamoDB conditional put:

python
table.put_item(
    Item={"pk": f"job#{job_id}", "claimed_at": now},
    ConditionExpression="attribute_not_exists(pk)",
)
# ConditionalCheckFailedException → someone else already claimed it; drop the message.

The blob checker that does this also runs a 300-second watchdog that re-drives jobs that went quiet, because "exactly once" is meaningless if "once" never happens.

4. "The model returns JSON"

Azure OpenAI's GPT-4o had a JSON mode. Bedrock's Converse API does not. Extraction prompts now run at temperature 0 with an explicit schema in the prompt, and the parser strips markdown fences before json.loads. Every field comes back as {value, confidence, field_id}, so downstream code never needs to know which model produced it.

Moving to Bedrock also removed something we had built by hand: a pool of three Azure OpenAI keys and endpoints that rotated to dodge rate limits. Bedrock's cross-region inference profiles do that routing for us, first the APAC profile and later global for the newer Claude models.

5. "Similar enough" needs embeddings

TradeGuard checks whether "M/S ABC Exports Pvt Ltd" on the invoice matches "ABC EXPORTS PRIVATE LIMITED" on the Shipping Bill. The Azure version embedded both sides with text-embedding-3-large and compared vectors, one field pair at a time.

During the move I replaced that with one batched LLM-judge call per document pair: every semantic rule goes in, a consistent/inconsistent verdict per rule comes out. That meant fewer calls, explainable verdicts, and one less model to host. The embedding client is now a stub kept only for the interface.

6. "Deploy is a button"

App Service gave us webapps-deploy and Kudu logs. On AWS every backend and worker is an ECS Fargate service. GitHub Actions assumes a role via OIDC (no stored keys), builds, pushes to ECR, and rolls the service by swapping the image on the live task definition. Rollback is the same operation pointed at the previous image.

Then make it boring

Once the guarantees held, the work was consolidation:

  • One platform for four products: Fargate for compute (24 services across dev and prod), RDS PostgreSQL with pgvector, S3, Bedrock, Secrets Manager, and Amplify static hosting for the React frontends.
  • Two accounts, dev and prod, in ap-south-1, with SSO for people. Dev scales to zero outside working hours.
  • One repo for TradeGuard instead of six, with a parity script that proved the monorepo built what the old repos built.
  • A full-local stack: an AWS emulator behind AWS_ENDPOINT_URL_<SVC> variables, so the same boto3 code runs on a laptop and in prod.

What I would tell myself on day one

  1. Write down the guarantees before the service map. Every bug I hit was a guarantee nobody had written down.
  2. Move the AI layer last, but design for it first. Model calls are the most expensive thing to duplicate, so idempotency and heartbeats protect your bill as well as your correctness.
  3. Terraform from the first resource. Clicking four products into existence is fast, and importing them into code later is not.

Next: what the AI layer that survived the move actually does.

Share

Linked from

Keep reading

Monthly notes

Get the next one in your inbox.

One email a month on infrastructure, backend design and applied AI. No noise, unsubscribe any time.

Comments (0)

name + comment · no login
0/2000