AWS Infrastructure Tour
This activity puts into practice the concepts from the Cloud Networking, Storage, and Identity lecture. You will inspect the default VPC, build the routing and security scaffold for a two-tier design, attach an IAM role to a public EC2 probe host, validate access to a private-tier instance, upload files to S3 without touching a credential file, and push a container image to a private ECR registry. By the end, you will have the network and security pieces of a two-tier VPC in place, two instances playing the public and private roles, and a container image running on your public probe host with no credentials stored anywhere on disk.
What You Will Need
Section titled “What You Will Need”- Your AWS Academy Learner Lab session started and the AWS Console open in your browser
- The
.pemkey file from your previous lab work, accessible on your local machine - A terminal with
sshavailable
The Default VPC Under the Hood
Section titled “The Default VPC Under the Hood”Before building something better, spend a few minutes looking at what AWS gives every account by default. This makes the case for a custom VPC concrete.
-
In the AWS Console, navigate to VPC > Your VPCs. You should see one VPC labeled “default.” Record its IPv4 CIDR block.
-
In the left sidebar, click Subnets. Use the filter bar to filter by the default VPC’s ID.
Record how many subnets exist, which availability zones they are in, and the CIDR block of each one.
-
Click any one of the default subnets and open its Route table tab. Find the row where the destination is
0.0.0.0/0and note what the target is.A target that starts with
igw-is an internet gateway. This entry means: route all traffic with no more-specific match out to the internet. -
Navigate to Internet Gateways in the left sidebar. Confirm that the default VPC has an internet gateway already attached.
-
Navigate to Security Groups and find the security group named “default.” Click it and open the Inbound rules tab.
The default inbound rule allows all traffic from any source that shares the same security group. This means every instance attached to that default security group can reach every other instance attached to that same default security group on every port, regardless of what they are running.
Building a Custom VPC
Section titled “Building a Custom VPC”You will create a VPC with one public subnet and one private subnet in the same availability zone. The difference between them is entirely in their route table: the public subnet routes to an internet gateway, the private one does not. For class, you will later use a public EC2 instance as a probe host because it is easy to SSH into. The lecture’s production pattern is still the one to remember: public entry point, private application tier, private data tier.
-
Navigate to VPC > Your VPCs and click Create VPC.
- Resources to create: VPC only
- Name tag:
cs312-vpc - IPv4 CIDR block:
10.0.0.0/16
Leave all other settings at defaults and click Create VPC.
-
Navigate to Subnets and click Create subnet.
- VPC ID:
cs312-vpc - Subnet name:
public-subnet - Availability zone:
us-east-1a - IPv4 CIDR block:
10.0.1.0/24
Click Create subnet.
- VPC ID:
-
Create a second subnet in the same VPC:
- Subnet name:
private-subnet - Availability zone:
us-east-1a - IPv4 CIDR block:
10.0.2.0/24
- Subnet name:
-
Navigate to Internet Gateways and click Create internet gateway.
- Name tag:
cs312-igw
Click Create internet gateway. On the confirmation page, click Attach to a VPC and select
cs312-vpc. - Name tag:
-
Navigate to Route Tables and click Create route table.
- Name:
public-rt - VPC:
cs312-vpc
Click Create route table. With
public-rtselected, open the Routes tab and click Edit routes. Click Add route: destination0.0.0.0/0, target: yourcs312-igw. Save the changes. - Name:
-
Create a second route table for the private subnet:
- Click Create route table, name it
private-rt, VPC:cs312-vpc. - Do not add a
0.0.0.0/0route. This table intentionally has no path to the internet.
- Click Create route table, name it
-
Associate each route table with its subnet.
Select
public-rt, open the Subnet associations tab, click Edit subnet associations, selectpublic-subnet, and save. Repeat forprivate-rtandprivate-subnet. -
In Subnets, select
public-subnetand choose Actions > Edit subnet settings. Enable Auto-assign public IPv4 address and save.Instances launched in
public-subnetwill now receive a public IP automatically. Instances inprivate-subnetwill not.
Security Groups with Tier Composition
Section titled “Security Groups with Tier Composition”Security groups can reference each other as sources rather than relying on IP addresses. This is the pattern used to enforce trust between tiers without hardcoding IPs that change when instances are replaced.
-
Navigate to EC2 > Security Groups and click Create security group.
- Name:
web-sg - Description: Web tier: HTTP, HTTPS, SSH
- VPC:
cs312-vpc
Add the following inbound rules:
- Type: HTTP (TCP 80), source:
0.0.0.0/0 - Type: HTTPS (TCP 443), source:
0.0.0.0/0 - Type: SSH (TCP 22), source: My IP (the console fills in your current IP address)
Leave outbound rules at the default (allow all) and click Create security group.
- Name:
-
Create a second security group:
- Name:
db-sg - Description: Database tier: PostgreSQL from web tier only
- VPC:
cs312-vpc
Add one inbound rule:
- Type: Custom TCP
- Port range: 5432
- Source type: Custom
- Source: begin typing
web-sgand select it from the dropdown (not an IP address range)
Click Create security group.
- Name:
-
Open
db-sgand look at the inbound rule you just created. The Source column shows the security group ID ofweb-sg, not an IP address or CIDR.In plain language, this rule means: any EC2 instance that has
web-sgattached is permitted to connect to port 5432 on instances that havedb-sgattached. Everything else is denied.
IAM Roles, Instance Profiles, and the Metadata Service
Section titled “IAM Roles, Instance Profiles, and the Metadata Service”AWS Academy’s LabRole is already configured as an IAM role that EC2 instances can assume. You will attach it to a public instance that acts as your probe host, launch a second private instance behind db-sg, and then use the public host to validate both the credential flow and the tier rule you created earlier.
-
First, inspect the role. Navigate to IAM > Roles and search for
LabRole. Click it.Open the Permissions tab and note the attached policies. Then open the Trust relationships tab.
In the JSON trust policy, find the
"Service"key. Its value isec2.amazonaws.com. This is the declaration that allows EC2 instances to assume this role. Notice how much access the role appears to have overall. In AWS Academy that breadth is intentional for a sandbox. In production, the least-privilege pattern from the lecture would normally split this into smaller roles with much narrower permissions. -
Navigate to EC2 > Instances and click Launch instances.
- Name:
cs312-web - AMI: Ubuntu Server 26.04 LTS (64-bit x86)
- Instance type:
t3.micro - Key pair: select your existing key pair
- Network settings: click Edit
- VPC:
cs312-vpc - Subnet:
public-subnet - Auto-assign public IP: Enable
- Security group: select existing
web-sg
- VPC:
- Expand Advanced details and under IAM instance profile, select
LabInstanceProfile
Click Launch instance.
- Name:
-
Launch a second instance that plays the private-tier role.
- Name:
cs312-db-probe - AMI: Ubuntu Server 26.04 LTS (64-bit x86)
- Instance type:
t3.micro - Key pair: select your existing key pair
- Network settings: click Edit
- VPC:
cs312-vpc - Subnet:
private-subnet - Auto-assign public IP: Disable
- Security group: select existing
db-sg
- VPC:
- Expand Advanced details and paste this into User data:
#!/bin/bashcat >/usr/local/bin/listen-5432.py <<'PY'import socketserver = socket.socket()server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server.bind(("0.0.0.0", 5432))server.listen()while True:conn, addr = server.accept()conn.sendall(b"cs312-db-probe\n")conn.close()PYnohup python3 /usr/local/bin/listen-5432.py >/var/log/listen-5432.log 2>&1 &This boot-time script starts a tiny TCP listener on port 5432 so you can test the
db-sgrule later without installing PostgreSQL. - Name:
-
While the instances are starting, navigate to EC2 > Instances, select
cs312-web, and open the Storage tab. Under Block devices, find the root volume entry (/dev/sda1).Note the Volume ID, Volume size (8 GiB), and Delete on termination (Yes). Click the Volume ID link to open it in EC2 > Volumes and note the Type (gp3) and State (in-use). This root volume was created automatically from the AMI’s snapshot when you launched the instance and will be removed when you terminate the instance because Delete on termination is enabled.
-
Wait for both instances to show Running and for both status checks to pass. This takes 2-3 minutes. Record the Public IPv4 address of
cs312-weband the Private IPv4 address ofcs312-db-probe. -
On your local machine, load your key into the SSH agent so agent forwarding works later, then connect to
cs312-web:Terminal window ssh-add /path/to/your-key.pemssh -A -i /path/to/your-key.pem ubuntu@<WEB_PUBLIC_IP>The
-Aflag forwards your local SSH agent to the session. This lets you use the same key for onward connections without copying the.pemfile tocs312-web. -
From inside
cs312-web, request an IMDSv2 token:Terminal window TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")echo "$TOKEN"The token is long and not human-friendly. Its presence is what matters. You will reuse it in the next metadata queries.
-
Query the metadata service root to see what categories are available:
Terminal window curl -H "X-aws-ec2-metadata-token: $TOKEN" \http://169.254.169.254/latest/meta-data/You will see a list:
ami-id,hostname,iam/,instance-type,local-ipv4, and others. The address169.254.169.254is a link-local address reserved for this purpose; it is not routable beyond the instance. If this command returns an error because$TOKENis empty, revisit step 7 and check the command carefully; IMDSv2 is required on current EC2 launches. -
Query the IAM path to find the name of the attached role:
Terminal window ROLE_NAME=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \http://169.254.169.254/latest/meta-data/iam/security-credentials/)echo "$ROLE_NAME"This returns
LabRole, the name of the role attached via the instance profile. -
Retrieve the actual credentials:
Terminal window curl -H "X-aws-ec2-metadata-token: $TOKEN" \"http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME"The JSON response contains
AccessKeyId,SecretAccessKey,Token, andExpiration. Note theExpirationtimestamp: these credentials rotate automatically a few hours before they expire. -
Install the AWS CLI v2 and a TCP probe tool, then confirm the CLI uses the role without any configuration:
Terminal window sudo apt update -q && sudo apt install -y unzip netcat-openbsdcurl -s "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zipunzip -q awscliv2.zipsudo ./aws/installaws sts get-caller-identityUbuntu 26.04 does not ship
awscliin its apt repositories, so you install it from the official AWS bundle instead. The response shows your account ID and the role ARN. An ARN (Amazon Resource Name) is a globally unique identifier that AWS uses to refer to any resource across any account and region; you will see them throughout the AWS CLI and console. The CLI found and used the credentials from the metadata service without a singleaws configureprompt. -
Validate the security-group relationship you built earlier by probing the private instance on port 5432:
Terminal window nc -zv <DB_PRIVATE_IP> 5432Replace
<DB_PRIVATE_IP>with the private IP you recorded forcs312-db-probe. You should see a message ending insucceeded. This proves two things at once: the VPC has a local route between the two subnets, and thedb-sgrule that referencesweb-sgis allowing the connection even though the target instance has no public IP. -
To SSH into the private instance,
db-sgneeds to permit it. In the AWS Console, open EC2 > Security Groups, selectdb-sg, and click Edit inbound rules. Add:- Type: SSH (TCP 22), Source:
web-sg
Save the rule.
- Type: SSH (TCP 22), Source:
-
From inside
cs312-web, SSH to the private instance using only its private IP:Terminal window ssh ubuntu@<DB_PRIVATE_IP>Your agent-forwarded key handles authentication. You should land at a shell prompt on
cs312-db-probe. Runhostnameto confirm which instance you are on. Notice that this instance has no public IP: the only way in is throughcs312-webwithin the VPC. Typeexitto return tocs312-web.
S3: Object Storage via the Instance Role
Section titled “S3: Object Storage via the Instance Role”Your instance already has credentials from the metadata service. This section uses them to create an S3 bucket and upload a file, demonstrating that the instance role handles authentication transparently for any AWS service.
-
From your EC2 SSH session, create an S3 bucket. Bucket names must be globally unique, so include your ONID:
Terminal window ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)BUCKET="cs312-YOUR_ONID-$ACCOUNT_ID"aws s3 mb "s3://$BUCKET" --region us-east-1Replace
YOUR_ONIDwith your actual ONID, lowercase. Adding the account ID avoids bucket-name collisions. -
Write a small file and upload it:
Terminal window echo "uploaded from cs312-web via instance role" > testfile.txtaws s3 cp testfile.txt "s3://$BUCKET/testfile.txt" -
List the bucket contents to confirm the upload:
Terminal window aws s3 ls "s3://$BUCKET/"You should see
testfile.txtwith its size and timestamp. -
Download the file back under a different name to confirm retrieval:
Terminal window aws s3 cp "s3://$BUCKET/testfile.txt" retrieved.txtcat retrieved.txt -
In the AWS Console, navigate to S3 and find your bucket. Open its Permissions tab.
Look at the Block public access section. All four settings are enabled by default. No object in this bucket can be made public unless you explicitly disable these controls. Then look at the Bucket policy section. It should be empty unless you add one. That means the access you just used came from the instance role’s identity-based permissions, not from a resource-based policy on the bucket itself.
-
Open the Management tab. This is where lifecycle rules are configured. Click Create lifecycle rule.
- Rule name:
archive-old-files - Scope: Apply to all objects in the bucket
- Add a transition action: move to S3 Glacier Flexible Retrieval after 30 days
Do not save the rule: click Cancel after noting the available options.
- Rule name:
-
In the AWS Console, open your bucket’s Permissions tab and click Edit under Bucket policy. Paste the policy below, replacing
YOUR_BUCKET_NAMEwith the value of$BUCKET:{"Version": "2012-10-17","Statement": [{"Sid": "DenyUnencryptedUploads","Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*","Condition": {"StringNotEquals": {"s3:x-amz-server-side-encryption": "AES256"}}}]}Save the policy.
-
Back in your SSH session, retry the upload without any encryption flag:
Terminal window aws s3 cp testfile.txt "s3://$BUCKET/denied-upload.txt"You should see:
An error occurred (AccessDenied) when calling the PutObject operation. The explicitDenyin the bucket policy overrode theAllowfrom the IAM role. An explicit deny always wins in IAM policy evaluation, regardless of what any allow says. -
Retry the upload with server-side encryption specified:
Terminal window aws s3 cp testfile.txt "s3://$BUCKET/encrypted-upload.txt" \--sse AES256This upload succeeds. The condition in the
Denystatement is no longer met, so the deny does not apply and the role’s allow takes effect. -
In the AWS Console, open your bucket’s Objects tab. You should see
encrypted-upload.txtlisted but nodenied-upload.txt. The denied upload never completed, so it left no object behind. The presence of one and the absence of the other is the confirmation that the policy worked.
Your ECR Repository
Section titled “Your ECR Repository”You will create a private container registry, install Docker on the EC2 instance, build an image that contains your ONID, and push it to ECR. The entire authentication chain uses the instance role you already attached.
-
In the AWS Console, navigate to ECR > Repositories and click Create repository.
- Repository name:
cs312-YOUR_ONID(your actual ONID, lowercase)
Leave all other settings at defaults. ECR repositories are private by default. Click Create repository. Copy the full URI shown for the repository (it looks like
<account-id>.dkr.ecr.us-east-1.amazonaws.com/cs312-YOUR_ONID). - Repository name:
-
Back in your EC2 SSH session, install Docker:
Terminal window sudo apt install -y docker.iosudo usermod -aG docker ubuntunewgrp docker -
Verify Docker is running:
Terminal window docker versionIf this returns a permission error, disconnect from SSH and reconnect. The group change from step 2 will take effect on the new session.
-
Retrieve your AWS account ID, which you need to construct the ECR login command:
Terminal window aws sts get-caller-identity --query Account --output textRecord the number returned.
-
Authenticate Docker to ECR. The AWS CLI uses the instance role to obtain a temporary registry token:
Terminal window aws ecr get-login-password --region us-east-1 | \docker login --username AWS --password-stdin \<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.comReplace
<ACCOUNT_ID>with the value from step 4. You should seeLogin Succeeded. -
Create a small image that carries your ONID:
Terminal window mkdir myapp && cd myappecho '<h1>CS 312 - YOUR_ONID</h1>' > index.htmlcat > Dockerfile << 'EOF'FROM nginx:alpineCOPY index.html /usr/share/nginx/html/index.htmlEOFBuild it:
Terminal window docker build -t cs312-YOUR_ONID:v1 . -
Tag the image with both a version tag and
latest, then push both:Terminal window docker tag cs312-YOUR_ONID:v1 \<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/cs312-YOUR_ONID:v1docker tag cs312-YOUR_ONID:v1 \<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/cs312-YOUR_ONID:latestdocker push <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/cs312-YOUR_ONID:v1docker push <ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/cs312-YOUR_ONID:latestBoth tags point to the same image digest.
latestis a convention for “the current version”;v1is a permanent, immutable reference you can roll back to. -
In the AWS Console, refresh your ECR repository page. You should see both
v1andlatestlisted, sharing the same digest. Click either tag and open the Scan results tab.If the registry has basic scanning enabled, findings will appear here within a minute or two of the push. Note any findings and their severity levels. If the tab shows no results, retrieve them from the CLI instead:
Terminal window aws ecr describe-image-scan-findings \--repository-name cs312-YOUR_ONID \--image-id imageTag=v1 \--region us-east-1Look for the
findingSeverityCountsfield in the output. Anginx:alpineimage typically shows informational or low findings from package metadata; note whether any appear as CRITICAL or HIGH. -
Pull the image back from ECR to confirm the full round-trip:
Terminal window docker pull \<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/cs312-YOUR_ONID:v1 -
Run the image and verify it serves your content:
Terminal window docker run -d -p 9090:80 --name myapp \<ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/cs312-YOUR_ONID:v1curl http://localhost:9090The response should contain your ONID.
Going Further
Section titled “Going Further”You now have the core scaffold in place. The next useful step is to push the activity a little closer to the lecture’s production patterns and operational controls.
- Add a NAT gateway to
cs312-vpcso instances inprivate-subnetcan make outbound requests (software updates, package downloads) without being directly reachable from the internet. Follow the NAT gateway documentation for the exact steps. Notice the tradeoff from the lecture: NAT gateways are the standard managed path, but they also incur hourly and data-processing charges. - Save the lifecycle rule you configured in the S3 section and observe how it appears in the Management tab. Add a second action that permanently deletes objects after 365 days so the bucket reflects the storage-lifecycle pattern from the lecture.