Index Ai Academy
Phase 11 · Professional practice · Shipping

Cloud for AI engineers

Access policies and least privilege, storage, compute choices, managed model endpoints, secrets and budgets.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Most AI products run on one of the big clouds, and the first week of an AI engineering job often starts with a cloud account: credentials to request, a bucket of documents to index, a model endpoint to call, a budget to stay inside. None of it is hard once you know the handful of ideas every provider shares. This lesson builds those ideas with small Python models of them, so they make sense whichever cloud your team uses.

What you'll be able to do

  • Read a cloud access policy and grant a service only what it needs
  • Choose between virtual machines, containers and serverless for an AI workload
  • Call models through a cloud provider's managed endpoint, and say what that changes
  • Keep secrets out of code and images, and set budgets that warn before a bill surprises you

What it covers

  1. What the cloud rents you
  2. Identity and access
  3. Decide a request
  4. Storage: buckets and objects
  5. Compute: machines, containers, functions
  6. Managed model endpoints
  7. Secrets without long-lived keys
  8. Costs and budgets
  9. When do the alerts fire?
  10. A checklist for your first cloud project
  11. Do it for real: a small cloud deployment with a budget
Quick check
Before any explaining, a guess. Your app only needs to read documents from one storage bucket. What access should it get?
  1. Your own login, copied into the app
  2. Write access to every bucket, in case it needs it
  3. Administrator access to the whole account
  4. Read access to that one bucket
Show the answer

Read access to that one bucket

Give a service only what its job needs. If the app is ever tricked or its credentials leak, the damage stays inside that one bucket.

What the cloud rents you

A cloud provider rents you computers, storage and ready-made services by the hour, the gigabyte or the request, instead of you buying hardware. The three biggest are Amazon Web Services, Google Cloud and Microsoft Azure. Their names differ, but the building blocks match:

  • identity and access: who may do what (IAM);
  • storage: files kept as objects in buckets;
  • compute: virtual machines, containers or functions that run your code;
  • managed services: databases, queues and model endpoints someone else runs for you;
  • billing: a meter on all of the above, with budgets and alerts.

Everything lives in a region, a group of data centers in one part of the world. Pick one near your users and allowed by your data rules, because moving data between regions costs time and money.

Identity and access

Every request to a cloud is made by a principal: a person, or a service account that a program uses. A policy lists statements that allow or deny actions (like storage:read) on resources (like one bucket). Three rules decide almost every case:

  1. Nothing is allowed unless a statement allows it.
  2. An explicit deny beats any allow.
  3. Patterns with * match many actions or resources, so read them carefully.
from fnmatch import fnmatch

policy = [
    {"effect": "Allow", "action": "storage:read", "resource": "bucket/docs/*"},
    {"effect": "Deny", "action": "storage:*", "resource": "bucket/docs/private/*"},
]

def matches(stmt, action, resource):
    return fnmatch(action, stmt["action"]) and fnmatch(resource, stmt["resource"])

hits = [s["effect"] for s in policy if matches(s, "storage:read", "bucket/docs/faq.md")]
print(hits)
It runs right here in your browser, with nothing to install.

The principle behind it all is least privilege: give each principal the smallest set of actions on the smallest set of resources that lets it do its job.

Quick check
Using the same policy, what does this print?
from fnmatch import fnmatch

policy = [
    {"effect": "Allow", "action": "storage:read", "resource": "bucket/docs/*"},
    {"effect": "Deny", "action": "storage:*", "resource": "bucket/docs/private/*"},
]
hits = [s["effect"] for s in policy
        if fnmatch("storage:read", s["action"]) and fnmatch("bucket/docs/private/salaries.csv", s["resource"])]
print(hits)
  1. ['Deny']
  2. []
  3. ['Allow']
  4. ['Allow', 'Deny']
Show the answer

['Allow', 'Deny']

Both statements match: the allow covers everything under docs, and the deny covers the private folder. Since a deny beats an allow, this read is refused.

Drill

Decide a request

Write allowed(policy, action, resource): return True only if some statement with effect "Allow" matches the action and resource (with fnmatch) and no statement with effect "Deny" does.

from fnmatch import fnmatch

POLICY = [
    {"effect": "Allow", "action": "storage:read", "resource": "bucket/docs/*"},
    {"effect": "Allow", "action": "model:invoke", "resource": "model/*"},
    {"effect": "Deny", "action": "storage:*", "resource": "bucket/docs/private/*"},
]


def allowed(policy, action, resource):
    return any(s["effect"] == "Allow" for s in policy)


print(allowed(POLICY, "storage:read", "bucket/docs/faq.md"), allowed(POLICY, "storage:read", "bucket/docs/private/pay.csv"))
In the lesson, your code is checked as you write it.
In your own words
How does a cloud decide a request, and what's least privilege?

Write a sentence or two, then compare it with a good answer in the lesson.

Storage: buckets and objects

Object storage keeps files as objects in buckets, each object found by a key like docs/returns/policy.md. The slashes look like folders but are just part of the name, which is why one pattern can cover a whole "folder". It's cheap, durable and where most AI data lives: documents to index, training sets, logs.

Two habits matter most. Keep buckets private, and when someone outside needs one file, hand them a presigned URL: a link signed by your credentials that works for one object and expires, say, in fifteen minutes. And add lifecycle rules that move old data to colder, cheaper storage or delete it after a set time, so last year's logs don't quietly cost you every month.

Quick check
A customer needs to download one invoice PDF from your private bucket. What's the right way to give it to them?
  1. Make the bucket public for a moment
  2. Email them the service account's key
  3. Copy the file into a public bucket forever
  4. A presigned URL for that file that expires soon
Show the answer

A presigned URL for that file that expires soon

A presigned URL grants access to one object for a short time, without changing the bucket or sharing any credentials.

Compute: machines, containers, functions

You can run code three main ways. A virtual machine is a whole computer you manage, billed while it's on. A container service runs your Docker image and handles machines for you; many restart and scale it automatically. Serverless functions run a small piece of code per request, cost nothing while idle, and suit short, bursty work, but they start slowly after a quiet spell and usually have a time limit.

For AI work there's one more question: does it need a GPU? Calling a hosted model doesn't, since the provider's GPUs do the work. Serving your own open model, or fine-tuning one, does, and GPU machines are the most expensive thing in most cloud bills.

Try it now

Match the shape of the work to the way it runs: always on, packaged and scaled, or only when called.

Managed model endpoints

Each big cloud also sells access to frontier models through its own account: Amazon Bedrock, Google Cloud's Vertex AI and Microsoft's Azure AI Foundry, for example, offer Claude among others. The models are the same, but the plumbing changes. You authenticate with the cloud's IAM instead of a separate API key, usage lands on the same bill as everything else, and you can keep traffic and data inside a chosen region, which some companies' data rules require.

Two things catch people out. Each region has quotas, a cap on tokens or requests per minute, which you may need to ask to raise before launch. And model names and available versions differ slightly per provider, so keep the model id in configuration rather than scattered through the code.

Quick check
A reminder from the containers lesson. Your service runs in a container on a cloud. Where should its model credentials come from?
  1. A line in the Dockerfile, so every copy has them
  2. The first request, pasted in by an admin
  3. The environment at run time, from the platform's secret store
  4. A config file committed next to the code
Show the answer

The environment at run time, from the platform's secret store

Images and repositories get copied and shared; secrets belong in the platform's secret manager, injected as environment variables when the container starts.

In your own words
How do you share one private file, and where do a container's secrets come from?

Write a sentence or two, then compare it with a good answer in the lesson.

Secrets without long-lived keys

A secret manager stores credentials encrypted, controls who may read each one, logs every read and can rotate them on a schedule. Your service reads a secret at start-up, so changing it never means rebuilding the image.

Better still, avoid long-lived keys altogether. Clouds can give a running service its own identity, often called workload identity, and hand it short-lived credentials automatically. There's then no key to leak at all, and access is still checked by the same IAM policies.

Costs and budgets

Cloud bills grow quietly: a forgotten GPU machine, logs nobody deletes, or data flowing out to the internet, which providers charge for as egress. Model calls add a fourth meter, priced per token. Estimate before you build:

hours = 24 * 30
container = 2 * hours * 0.05        # two small containers at $0.05 an hour
storage = 200 * 0.02                # 200 GB at $0.02 per GB-month
egress = 50 * 0.09                  # 50 GB out at $0.09 per GB
tokens = 30_000_000 / 1_000_000 * 4 # 30 million input tokens at $4 per million
print(round(container + storage + egress + tokens, 2))

Then set a budget with alerts, say at 50%, 90% and 100% of what you expect, sent to people who will act on them. Budgets warn rather than stop spending, so the provider's own spending caps on model usage are worth setting too.

Quick check
What does this print?
spend = [30, 45, 60, 80, 95, 120]   # spend so far, by day
budget = 100
crossed = [day for day, total in enumerate(spend, start=1) if total >= 0.9 * budget]
print(crossed[0] if crossed else None)
  1. 6
  2. 5
  3. None
  4. 4
Show the answer

5

The 90% alert fires the first day spend reaches $90, which is day 5 at $95. Alerts on spend so far give you a day to act before the budget is gone.

Drill

When do the alerts fire?

Write alerts(spend_by_day, budget, thresholds=(0.5, 0.9, 1.0)): spend_by_day holds each day's spend (not the total). Return a list of (threshold, day) pairs: for each threshold, the first day (counting from 1) on which the running total reaches threshold × budget. Leave out thresholds never reached.

def alerts(spend_by_day, budget, thresholds=(0.5, 0.9, 1.0)):
    return []


print(alerts([10, 12, 15, 14, 20, 18, 25], 100))
In the lesson, your code is checked as you write it.
In your own words
What does a budget alert do, and what doesn't it?

Write a sentence or two, then compare it with a good answer in the lesson.

A checklist for your first cloud project

  • One project or account per environment (development, production), so a test can't touch real data.
  • Service accounts with least privilege, and no personal logins inside apps.
  • Secrets in the secret manager, ideally replaced by workload identity.
  • Private buckets, presigned URLs for sharing, lifecycle rules for old data.
  • A region chosen for your users and your data rules, with model quotas checked before launch.
  • A budget with alerts at 50%, 90% and 100%, and spending caps on model usage.
  • Logs and metrics turned on from day one, so the first incident isn't also the first time you look.
Quick check
Your team's staging service accidentally deleted production files. Which habit from the checklist would have prevented it?
  1. Separate accounts for staging and production
  2. A budget alert at 90% of the monthly spend
  3. Lifecycle rules that clean up the bucket
  4. A bigger container for the staging service
Show the answer

Separate accounts for staging and production

If staging can only reach staging resources, a bug there can't touch production. Separate accounts or projects make that the default.

Portfolio brief

Do it for real: a small cloud deployment with a budget

Deploy one of your services (the capstone, or any FastAPI app) on a real cloud account, set up the way a careful team would: its own service account with least privilege, secrets in the secret manager, and a budget with alerts. Many providers have free tiers or starting credits, so this can cost nothing.

Paste your service account's policy (or a written summary of its permissions) and your budget settings into the starter, which checks them with this lesson's rules. Then ask the tutor to review the deployment against the rubric.

It must

  • Create a separate project or account for this deployment, not your personal default
  • Run the service with its own service account whose permissions list only the actions it uses
  • Store any model or database credentials in the secret manager (or use workload identity)
  • Create a budget with alerts at 50%, 90% and 100%, sent to an address you read
  • Write down how you'd shut everything down, and do it once the review is done

Go further

  • Use a managed model endpoint through the cloud account instead of a separate API key
  • Add a lifecycle rule that deletes logs after 30 days
POLICY = [
    {"effect": "Allow", "action": "storage:read", "resource": "bucket/docs/*"},
    {"effect": "Allow", "action": "secrets:read", "resource": "secret/model-key"},
    {"effect": "Allow", "action": "model:invoke", "resource": "model/claude-opus-5-5"},
]
BUDGET = {"amount": 50, "alerts": [0.5, 0.9, 1.0], "notify": "ops@yourteam.example"}


def review(policy, budget):
    notes = []
    for s in policy:
        if s["effect"] == "Allow" and ("*" in s["action"] or s["resource"] in ("*", "bucket/*")):
            notes.append(f"Broad permission: {s['action']} on {s['resource']}. Can it be narrower?")
    if not budget.get("amount"):
        notes.append("Set a budget amount")
    if sorted(budget.get("alerts", [])) != [0.5, 0.9, 1.0]:
        notes.append("Use alerts at 50%, 90% and 100%")
    if "@" not in budget.get("notify", ""):
        notes.append("Send the alerts to an address someone reads")
    return notes


for note in review(POLICY, BUDGET) or ["Nothing to flag. Ask the tutor to review the deployment."]:
    print("-", note)
In the lesson, you build it here and the tutor can review it.
In your own words
Why separate staging and production accounts?

Write a sentence or two, then compare it with a good answer in the lesson.

Practice what you just read

  • 2 code examples you can edit and run
  • a practice exercise that checks your code
  • a 6-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

A policy allows storage:read on bucket/docs/* and denies storage:* on bucket/docs/private/*. Can the service read bucket/docs/private/a.txt?

  1. No: an explicit deny beats the allow
  2. Yes: the allow is listed first
  3. Yes: read isn't the same as storage:*
  4. Only on weekdays

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson