Week 8: Cloud Computing Fundamentals (AWS)

Everything you've built so far — Dockerized apps, GitHub Actions pipelines, deployment strategies — has been aimed at a target you haven't actually stood up yet: a real server somewhere on the internet. This week that target becomes AWS. You'll create IAM identities instead of relying on the root account, launch and lock down an EC2 instance, store objects in S3, and understand the VPC that everything else lives inside — the same access-key authentication model you used for SSH in Week 1 reappears here as an AWS credential, and the exact resources you create by hand this week are what you'll codify in Terraform starting next week.

Module 6 of 22 Week 8 of 26 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Create least-privilege IAM users, groups and policies instead of using the AWS root account
  • Launch an EC2 instance behind a correctly scoped security group and store objects in a locked-down S3 bucket
  • Explain how a VPC's subnets and route tables control what traffic can reach a resource, and apply basic cost-control habits

1. IAM: Users, Roles & Least Privilege

The root account you signed up with can do literally anything in your AWS account, including deleting the account itself — which is exactly why you never use it day to day. Instead, AWS Identity and Access Management (IAM) lets you create scoped-down identities: users for people, roles for services and temporary access, and policies — JSON documents — that define exactly which actions on which resources are allowed or denied.

terminal — create a non-root user
aws configure                       # enter your root/admin access key once, interactively
aws sts get-caller-identity         # confirm which identity the CLI is using

aws iam create-group --group-name developers
aws iam create-user --user-name ada
aws iam add-user-to-group --user-name ada --group-name developers

A policy is built from Effect (Allow/Deny), Action (the API calls it covers, like s3:GetObject), and Resource (the ARN it applies to). Least privilege means granting only the actions and resources a role actually needs — not "Action": "*" against "Resource": "*":

app-logs-readonly.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadAppLogsBucket",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::codeverse-app-logs-ada",
        "arn:aws:s3:::codeverse-app-logs-ada/*"
      ]
    }
  ]
}
terminal — attach a custom policy
aws iam create-policy \
  --policy-name AppLogsReadOnly \
  --policy-document file://app-logs-readonly.json

aws iam attach-group-policy \
  --group-name developers \
  --policy-arn arn:aws:iam::123456789012:policy/AppLogsReadOnly

Roles work like a policy without a permanent identity attached — an EC2 instance or a Lambda function assumes a role and receives short-lived credentials for exactly as long as it runs, which is safer than baking a long-lived access key into an instance.

Lock the root account down immediately

Enable MFA on the root account, store its credentials somewhere offline, and never generate an access key for it. Every day-to-day action — including everything else in this lesson — should run as an IAM user or role with only the permissions that task needs.

2. EC2 Instances & Security Groups

EC2 (Elastic Compute Cloud) is a virtual machine you rent by the second. Launching one requires an AMI (the OS image), an instance type (the hardware size), a key pair (for SSH, same public/private key model as Week 1), and a security group — a stateful virtual firewall attached to the instance's network interface.

terminal — create a scoped security group
aws ec2 create-security-group \
  --group-name web-sg \
  --description "SSH from my IP only, HTTPS from anywhere" \
  --vpc-id vpc-0123456789abcdef0

# Allow SSH only from your own IP -- never 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp --port 22 --cidr 203.0.113.10/32

# Allow HTTPS from anywhere, since this is a public web server
aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp --port 443 --cidr 0.0.0.0/0
terminal — launch and tag an instance
aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --key-name my-key \
  --security-group-ids sg-0123456789abcdef0 \
  --subnet-id subnet-0123456789abcdef0 \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=web-01},{Key=Environment,Value=dev},{Key=Owner,Value=ada}]'

"Stateful" means a security group automatically allows the response traffic for a connection you initiated — you don't need a separate outbound rule just to receive a reply to a request the instance made. Rules are additive and default-deny: nothing is reachable until you explicitly open it.

The single most common AWS misconfiguration

Opening port 22 (SSH) or a database port to 0.0.0.0/0 is how compromised EC2 instances become cryptomining hosts within minutes of launch — automated scanners find open ports constantly. Scope SSH to your own IP (or better, a bastion host or Session Manager) and never open a database port to the public internet at all.

3. S3 Buckets & Bucket Policies

S3 (Simple Storage Service) stores objects — files of any size, up to 5TB each — inside buckets, which need a globally unique name across all of AWS, not just your account. It's where you'll put build artifacts, log archives, static site assets, and later, Terraform state.

terminal
aws s3 mb s3://codeverse-app-logs-ada --region us-east-1
aws s3 cp app.log s3://codeverse-app-logs-ada/2026-08-03/app.log
aws s3 sync ./dist s3://codeverse-static-site-ada    # upload a whole directory
aws s3 ls s3://codeverse-app-logs-ada --recursive

aws s3api put-bucket-versioning \
  --bucket codeverse-app-logs-ada \
  --versioning-configuration Status=Enabled

Every new bucket blocks public access by default, and you should leave it that way unless you're intentionally hosting something public, like static site assets behind CloudFront. A bucket policy is a resource-based policy — attached to the bucket rather than a user — that can further restrict access even beyond what IAM allows:

bucket-policy.json — deny any non-HTTPS request
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::codeverse-app-logs-ada",
        "arn:aws:s3:::codeverse-app-logs-ada/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    }
  ]
}
terminal
aws s3api put-bucket-policy \
  --bucket codeverse-app-logs-ada \
  --policy file://bucket-policy.json

aws s3api put-public-access-block \
  --bucket codeverse-app-logs-ada \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
Bucket names are permanent and public

A deleted bucket's name can be claimed by anyone else, and buckets are frequently found by scanning common name patterns. Never put a secret in a bucket name, and always run put-public-access-block explicitly rather than trusting the console default not to change.

4. VPC Fundamentals

A VPC (Virtual Private Cloud) is your own isolated slice of the AWS network, defined by a CIDR block like 10.0.0.0/16. Inside it, you carve out subnets — smaller CIDR ranges tied to a single Availability Zone — and each subnet is public or private purely based on whether its route table sends internet-bound traffic to an internet gateway.

terminal — VPC with a public and a private subnet
aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
  --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=codeverse-vpc}]'

aws ec2 create-subnet --vpc-id vpc-0123456789abcdef0 \
  --cidr-block 10.0.1.0/24 --availability-zone us-east-1a \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=public-1a}]'

aws ec2 create-subnet --vpc-id vpc-0123456789abcdef0 \
  --cidr-block 10.0.101.0/24 --availability-zone us-east-1a \
  --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=private-1a}]'
terminal — route the public subnet to the internet
aws ec2 create-internet-gateway \
  --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=codeverse-igw}]'
aws ec2 attach-internet-gateway \
  --vpc-id vpc-0123456789abcdef0 --internet-gateway-id igw-0123456789abcdef0

aws ec2 create-route-table --vpc-id vpc-0123456789abcdef0
aws ec2 create-route \
  --route-table-id rtb-0123456789abcdef0 \
  --destination-cidr-block 0.0.0.0/0 \
  --gateway-id igw-0123456789abcdef0
aws ec2 associate-route-table \
  --subnet-id subnet-0123456789abcdef0 --route-table-id rtb-0123456789abcdef0

The private subnet has no route to an internet gateway, so nothing inside it is directly reachable from outside the VPC — that's exactly where a database or internal service belongs. If a private-subnet resource needs outbound internet access (to pull an OS update, say) without being inbound-reachable, that route instead points at a NAT gateway sitting in the public subnet.

A default VPC is fine to learn on, not to build on

Every AWS region starts with a default VPC where every subnet is public — convenient for a first EC2 instance, dangerous for anything holding real data. Once you get to Terraform in Week 9, you'll define this subnet layout as code instead of clicking through the console.

5. AWS CLI & Cost Control

You've been using the AWS CLI all week — it's the same tool your CI pipelines and, soon, Terraform will use under the hood. aws configure writes credentials to ~/.aws/credentials, and named profiles let you keep multiple accounts or roles separate without overwriting each other:

terminal
aws configure --profile personal
export AWS_PROFILE=personal          # every subsequent command uses this profile
aws sts get-caller-identity --profile personal

Tagging every resource you create (Environment, Owner, Project) isn't optional busywork — it's how you'll filter a cost report or figure out whose EC2 instance is still running six weeks after an experiment. Set a billing alarm on day one so a misconfigured resource doesn't surprise you at the end of the month:

budget.json
{
  "BudgetName": "monthly-guardrail",
  "BudgetLimit": { "Amount": "20", "Unit": "USD" },
  "TimeUnit": "MONTHLY",
  "BudgetType": "COST"
}
terminal — a monthly budget alert, and cleanup habits
aws budgets create-budget \
  --account-id 123456789012 \
  --budget file://budget.json

# Find anything still running before you close your laptop
aws ec2 describe-instances \
  --filters "Name=instance-state-name,Values=running" \
  --query "Reservations[].Instances[].[InstanceId,Tags]"

# Unattached Elastic IPs are billed hourly even when idle
aws ec2 describe-addresses --query "Addresses[?AssociationId==null]"
aws ec2 release-address --allocation-id eipalloc-0123456789abcdef0
The free tier has expiration dates

An EC2 t2.micro or t3.micro is free for the first 12 months on a new account only — after that, or on an older account, it bills per second. Make cleaning up test resources at the end of every session (this week's exercise included) a habit before the bill teaches you the hard way.

6. Hands-on Exercise

Hands-on

Stand up a locked-down AWS environment by hand

Build the exact environment you'll reproduce in Terraform next week, entirely with the AWS CLI, so you understand what the code will actually create.

Requirements:

  1. Create an IAM user for yourself in a developers group, attach a custom least-privilege policy (not AdministratorAccess), and confirm the identity with aws sts get-caller-identity.
  2. Create a VPC with one public subnet and one private subnet, an internet gateway, and a route table that gives only the public subnet a route to 0.0.0.0/0.
  3. Create a security group in that VPC allowing SSH only from your current public IP and HTTPS from anywhere, then launch a t3.micro EC2 instance into the public subnet using it.
  4. Create an S3 bucket, enable versioning, block all public access explicitly, and attach a bucket policy that denies any non-HTTPS request.
  5. Tag every resource you created with Environment=dev and Owner=<your-name>, and create a $20 monthly budget alert.
  6. Terminate the instance, delete the bucket, and remove the VPC's resources when you're done, then confirm nothing billable is left running.
Hint

Run curl -s ifconfig.me to get your current public IP for the security group's --cidr value — it's the same trick a curl -v trace from Week 2 relies on, just pointed at a service that echoes your address back.

7. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why shouldn't you use the AWS root account, or an IAM user with AdministratorAccess, for everyday work?

Both can perform any action on any resource, so a leaked credential, a typo'd CLI command, or a compromised laptop puts the entire account at risk instead of one narrow blast radius. Least-privilege IAM users and roles limit the damage a mistake or a stolen key can do to exactly the actions that identity actually needs.

Q2

Why is a security group rule opening port 22 to 0.0.0.0/0 dangerous, and what's the fix?

It lets any host on the internet attempt to connect to SSH on that instance, and automated scanners find newly opened ports within minutes, immediately trying default credentials and known exploits. Scoping the rule's CIDR to your own IP address (or routing through a bastion host or AWS Session Manager) means only you can even attempt a connection.

Q3

Why run put-public-access-block on an S3 bucket explicitly, even though new buckets already block public access by default?

Defaults can be changed later by a teammate, a script, or a misapplied bucket policy, and a bucket holding sensitive data with public access silently re-enabled is one of the most common real-world data leaks. Setting the block explicitly, and treating any change to it as something that should require review, makes the intent durable rather than relying on a setting nobody is watching.

Q4

Why put a database in a private subnet instead of a public one, if you could just add a restrictive security group to it either way?

A security group is one layer of defense that depends on every rule being correct forever; a private subnet's route table has no path to an internet gateway at all, so the resource isn't reachable from the internet even if a security group rule is misconfigured or accidentally widened later. Defense in depth means the network topology itself, not just the firewall rules on top of it, should reflect what's supposed to be reachable from outside.