Cloud 02 — account hygiene and the zero-spend alarm

The first thing this track does on AWS creates no compute, no network and no storage worth mentioning — it creates the guard rails every later cloud item assumes. MFA goes on the root login and the root login goes in a drawer; an IAM Identity Center user becomes the thing you sign in as; a zero-spend budget wires the first cent of real spend to an SNS topic and an e-mail; and a CloudTrail management trail starts writing down who did what. You will be looking at four console pages (IAM, IAM Identity Center, Budgets, CloudTrail) and one terminal. Everything on this page reads ~$0.00/mo — and the scene below is about why each box is free, because the next cloud items are not.

~45 min~$0.00/mo (approx.) console + AWS CLI v2nothing to tear down cloud-02

What this creates

One box per resource — the AWS name on the first line, the Terraform type on the second (none of it is Terraform yet; that arrives in cloud-03, and this budget is made by hand on purpose, so it exists before the first terraform apply). Solid boxes exist; dashed ones do not yet. Pick a state of the account and the readout recomputes an approximate monthly line — prices and free-tier rules are illustrative (verify on the pricing page today); what to remember is which box would cost money if it were bigger, and which box protects you.

Preconditions

Every later cloud guide opens with "cloud-02's zero-spend budget alarm exists and has e-mailed you once". This is cloud-02, so that line is what you are about to make true, not something to check. What you need instead:

Do it

0. Pick a default region and write it down. This track uses ap-south-1 (Mumbai) as the example. Every resource in cloud-03 onwards goes there; the console's region selector (top right) and the CLI profile in step 3 both say so. Two things ignore that choice, and knowing which is the first interview-grade fact on this page: Billing and Budgets are global and their API lives in us-east-1, and IAM (users, roles, policies) is global too. A budget made "in Mumbai" is the same budget, and it counts spend from every region.

1. MFA on root, and no root access keys. Sign in to the console as the root user. Top-right menu → Security credentials. Under Multi-factor authentication (MFA) press Assign MFA device, pick Passkey or security key (or Authenticator app), name it root-mfa and finish the enrolment. On the same page, under Access keys: if any key is listed, Deactivate it, then Delete it — the root user should own zero access keys, ever. You will confirm both facts from the CLI in Verify.

2. IAM Identity Center: one user, one permission set, one assignment. Still as root, open IAM Identity Center, set the region selector to ap-south-1 (Identity Center is enabled in exactly one region — its "home" — and this is it), press Enable; if it asks, let it create an AWS Organization (free; it is what Identity Center hangs off). Then, in this order:

3. AWS CLI v2 and aws configure sso. On the Mac:

# install and check -- it must say aws-cli/2.x; v1 does not know about SSO sessions
brew install awscli
aws --version

# one profile, backed by an SSO session -- no long-lived key ever lands in ~/.aws/credentials
aws configure sso
#   SSO session name (Recommended): lbv
#   SSO start URL [None]: https://d-xxxxxxxxxx.awsapps.com/start      <- the access portal URL from step 2
#   SSO region [None]: ap-south-1                                        <- Identity Center's home region
#   SSO registration scopes [sso:account:access]:                        <- Enter (if it shows [None], type sso:account:access)
#   (a browser tab opens: sign in as the Identity Center user, approve the request)
#   (one account, one permission set -- both picked for you)
#   Default client Region [None]: ap-south-1                             <- the region you wrote down in step 0
#   CLI default output format [None]: json
#   Profile name [AdministratorAccess-123456789012]: lbv-admin           <- the default's form varies by CLI version; type lbv-admin

export AWS_PROFILE=lbv-admin        # put this in ~/.zshrc; every later cloud item assumes it
aws sts get-caller-identity

The proof line is the Arn — it must name an assumed role, not :root:

{
    "UserId": "AROAEXAMPLEEXAMPLE:abhishek",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_9f2c1e7d4b6a8c30/abhishek"
}

When the 4-hour session expires, aws sso login opens the browser again. That is the whole point: the credentials on disk are short-lived tokens, and there is nothing in ~/.aws/credentials to leak.

4. The SNS topic and the e-mail subscription — made first, because the budget in step 5 points at the topic's ARN. It lives in us-east-1, the same place the Budgets API lives, so nothing crosses regions:

aws sns create-topic --name billing-alarms --region us-east-1
# -> { "TopicArn": "arn:aws:sns:us-east-1:123456789012:billing-alarms" }   -- your account id, not this one

# let the Budgets service publish to it; the policy REPLACES the default one, so the owner statement stays in
aws sns set-topic-attributes --region us-east-1 \
  --topic-arn arn:aws:sns:us-east-1:123456789012:billing-alarms \
  --attribute-name Policy --attribute-value file://sns-policy.json

# the e-mail subscription -- replace the placeholder with the inbox you read
aws sns subscribe --region us-east-1 \
  --topic-arn arn:aws:sns:us-east-1:123456789012:billing-alarms \
  --protocol email --notification-endpoint <your-e-mail>
# -> { "SubscriptionArn": "pending confirmation" }
# open the "AWS Notification - Subscription Confirmation" e-mail and press Confirm subscription.
# THAT e-mail is the "has e-mailed you once" every later cloud page leads with.
# sns-policy.json  -- ":root" in an ARN principal means "the account", not the root login you just locked away;
# the two conditions are the ones the Budgets docs give, so only THIS account's budgets can publish here
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "OwnerFullAccess",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
      "Action": "SNS:*",
      "Resource": "arn:aws:sns:us-east-1:123456789012:billing-alarms"
    },
    {
      "Sid": "AllowBudgetsToPublish",
      "Effect": "Allow",
      "Principal": { "Service": "budgets.amazonaws.com" },
      "Action": "SNS:Publish",
      "Resource": "arn:aws:sns:us-east-1:123456789012:billing-alarms",
      "Condition": {
        "StringEquals": { "aws:SourceAccount": "123456789012" },
        "ArnLike": { "aws:SourceArn": "arn:aws:budgets::123456789012:*" }
      }
    }
  ]
}

5. The zero-spend budget — in the console. Open Billing and Cost Management → Budgets → Create budget. It is one page. At the top, Budget setup offers two radio buttons: leave Use a template (simplified) selected. Under Templates pick the first card, Zero spend budget — its caption says it notifies you after your spending exceeds the AWS Free Tier limits. The Budget name field is prefilled; change it to zero-spend. The Email recipients box takes up to ten addresses; type the one you confirmed in step 4. The card explains what the template fixes for you — at the time of writing, a monthly cost budget of $1.00 with an alert at $0.01 of actual spend; read the card, it is the definition. Press Create budget; the list page now shows zero-spend with "Current: $0.00" and "Budgeted: $1.00". That is the whole console act.

Why a $1 limit with a $0.01 alarm is the zero-spend pattern: a budget must have a non-zero limit, and an absolute notification threshold of one cent on that limit fires the moment any real spend is recorded — so the limit is a formality and the threshold is the alarm.

The same budget from the CLI, for the record — and so that it also publishes to the SNS topic from step 4 (the console template only sends e-mail directly). Run it, or read it as the definition of what the console page made:

aws budgets create-budget --region us-east-1 --account-id 123456789012 \
  --budget file://budget.json \
  --notifications-with-subscribers file://subs.json
# if the console budget already exists under the same name this returns DuplicateRecordException --
# either delete the console one first, or name this one zero-spend-sns; budgets that only notify are free.
# budget.json
{
  "BudgetName": "zero-spend",
  "BudgetType": "COST",
  "TimeUnit": "MONTHLY",
  "BudgetLimit": { "Amount": "1", "Unit": "USD" },
  "CostTypes": { "IncludeCredit": false, "IncludeRefund": false }
}
# subs.json  -- ACTUAL spend, absolute one-cent threshold, delivered to the SNS topic (and its e-mail subscriber)
[
  {
    "Notification": {
      "NotificationType": "ACTUAL",
      "ComparisonOperator": "GREATER_THAN",
      "Threshold": 0.01,
      "ThresholdType": "ABSOLUTE_VALUE"
    },
    "Subscribers": [
      { "SubscriptionType": "SNS", "Address": "arn:aws:sns:us-east-1:123456789012:billing-alarms" }
    ]
  }
]

IncludeCredit: false matters if the account holds promotional credits: with credits counted, spend can sit at $0.00 for months while the credits drain — and the alarm never fires. Counting gross spend is what "zero-spend" should mean. This is not hypothetical: an account opened on AWS's current free plan starts with promotional credits (up to $200, for six months, after which the account closes on its own unless upgraded to the paid plan — approx., verify on the Free Tier page today), so with IncludeCredit: true the console template's alarm would stay silent for the whole track.

# For later (cloud-03 onwards) -- the same resource as Terraform would write it. Comments only, on purpose:
# this budget is created by hand so that it exists BEFORE the first terraform apply, not as part of one.
#
# resource "aws_budgets_budget" "zero_spend" {
#   name         = "zero-spend"
#   budget_type  = "COST"
#   limit_amount = "1"
#   limit_unit   = "USD"
#   time_unit    = "MONTHLY"
#   notification {
#     notification_type         = "ACTUAL"
#     comparison_operator       = "GREATER_THAN"
#     threshold                 = 0.01
#     threshold_type            = "ABSOLUTE_VALUE"
#     subscriber_sns_topic_arns = ["arn:aws:sns:us-east-1:123456789012:billing-alarms"]
#   }
# }

6. CloudTrail: one multi-region management-events trail, into a bucket only CloudTrail can write. The bucket name must be globally unique — change yourname-x7q2 to something of yours, in the shell variable and in the policy file:

B=lbv-cloudtrail-yourname-x7q2

# the bucket, in the default region, closed to the public four ways
aws s3api create-bucket --bucket "$B" --region ap-south-1 \
  --create-bucket-configuration LocationConstraint=ap-south-1
aws s3api put-public-access-block --bucket "$B" \
  --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-policy --bucket "$B" --policy file://trail-bucket-policy.json

# the trail: management events from every region, first copy free; then switch it on
aws cloudtrail create-trail --name lbv-management --s3-bucket-name "$B" \
  --is-multi-region-trail --region ap-south-1
aws cloudtrail start-logging --name lbv-management --region ap-south-1
# trail-bucket-policy.json  -- exactly the two statements CloudTrail needs; the SourceArn condition pins it to THIS trail
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AWSCloudTrailAclCheck",
      "Effect": "Allow",
      "Principal": { "Service": "cloudtrail.amazonaws.com" },
      "Action": "s3:GetBucketAcl",
      "Resource": "arn:aws:s3:::lbv-cloudtrail-yourname-x7q2",
      "Condition": { "StringEquals": {
        "aws:SourceArn": "arn:aws:cloudtrail:ap-south-1:123456789012:trail/lbv-management" } }
    },
    {
      "Sid": "AWSCloudTrailWrite",
      "Effect": "Allow",
      "Principal": { "Service": "cloudtrail.amazonaws.com" },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::lbv-cloudtrail-yourname-x7q2/AWSLogs/123456789012/*",
      "Condition": { "StringEquals": {
        "s3:x-amz-acl": "bucket-owner-full-control",
        "aws:SourceArn": "arn:aws:cloudtrail:ap-south-1:123456789012:trail/lbv-management" } }
    }
  ]
}

What it costs, approx., verify on the pricing page today: the first copy of management events in a trail is free; the objects it writes are a few MB a month of S3 storage (fractions of a cent after any free tier). Data events (every S3 GetObject, every Lambda invoke) are the paid kind — this trail records none, and the ECR push in cloud-05 will still be visible as a management event.

Verify

Optional, one look: Billing → Cost Anomaly Detection — confirm a monitor exists for the account (recent accounts get one by default; approx., verify there today). It is a second, slower backstop; the zero-spend budget is the one that fires first.

Teardown

None — this stays. Everything on this page is the guard rail every later cloud item assumes: the budget alarm is the precondition line of cloud-03 onwards, the Identity Center login is what every later terraform apply runs as, and the trail is what tells you afterwards what an apply did. Nothing here has a red ring, and nothing here is torn down after the session.

What remains and what it costs, approx., verify on the pricing page today: the budget (a budget that only notifies is free; only budgets with actions are charged after the first two), the SNS topic and one e-mail subscription (the first thousand e-mail notifications a month are free), IAM Identity Center (free), and one management-events trail (first copy free) writing a few MB a month into an S3 bucket. Rounded: ~$0.00/mo. If you ever do close the account, do it from the root login — the one thing it is still for.

There is no teardown box on this page, because there is no teardown. Press the button once the five Verify lines above are true.

This is self-attestation — the site cannot see your AWS account, so pressing the button is you telling The Path the alarm exists and has e-mailed you once.

Takeaways: the root user is the one identity no IAM policy and no organisation-level guard rail can constrain — so it gets MFA, zero access keys, and no daily use; you sign in through Identity Center and receive short-lived credentials instead. A budget is a report; a budget with a notification is an alarm — and the zero-spend pattern is a $1 monthly limit with an actual, absolute $0.01 threshold, living in us-east-1 whatever your default region is, because Billing is global. The one cost trap it hides: credits. A budget that counts credits stays at $0.00 while they drain, and tells you nothing until they are gone.