← Back to Home

Cloud Assignment 3

Purdue University CS351 Spring 2026

Due Friday April 3rd 11:00PM

Introduction

During this assignment you will implement and deploy an application architecture designed for scaling web services. You'll become familiar with local development tools for managing multiple software services and open-source software commonly used in industry. You'll work with Infrastructure-as-Code approaches for controlling cloud infrastructure and performing automated deployments. You'll continue to improve and update the bird.ai application, gaining practical experience with MVC application frameworks and YOLO machine learning models. Finally, you'll gain insight into data privacy and ethical issues when working on Software-as-a-Service applications in the cloud.

📌 Notice

Some hyperlinks in this document may not work unless you right-click to open them in a new tab.

This assignment is distributed as an HTML file on Brightspace, which displays it you in an iframe. An iframe's capabilities are limited for security reasons, and they also break the same-origin policy model of web security used by many websites. Certain websites restrict cross-origin resource sharing, causing our hyperlinks to them to fail when they load in an iframe.

Contents

  1. Initial Setup
    1. Set AWS Budget
    2. Set AWS IAM Permissions
  2. Update from bird.ai
  3. Part 1: N-Tier Architecture
    1. Adding a reverse proxy
    2. Introducing Docker Compose
    3. Pushing to a container repository
    4. Automating our deploys
    5. From ClickOps to GitOps
    6. Importing existing infrastructure
    7. What about bird.ai?
    8. Choosing a domain name
  4. Part 2: Squirrel-due University
    1. Turning cleartext into ciphertext
    2. Squirrel detection
    3. Mapping detections
    4. Gossip Squirrel
  5. Estimating Cloud Cost
    1. Resource Rates (us-east-1, on-demand, no free tier)
    2. How Billing Works
    3. Estimated Cost
  6. Teardown

Initial Setup

🚨 Important

The autograder user's AWS Account ID has changed for Cloud Assignment 3, pay careful attention to the section Set AWS IAM Permissions.

Use an account eligible for the AWS Free Tier. Make a post on Ed if you don't have an eligible account. This assignment will use AWS region "us-east-1", do not use AWS services in any other region. You can control your current region from the menu bar of the AWS Console.

https://us-east-1.console.aws.amazon.com/console/home?region=us-east-1#
AWS Console Regions menu with cursor pointing at us-east-1

Set AWS Budget

We're going to set up an AWS Budget that will alert you if you get any monetary charges.

  1. Navigate to the Billing feature in the AWS Billing and Cost Management Service.
  2. Click "Create Budget"
  3. Select "Use a template" then "Zero spend budget"
  4. Add your email to the "Email recipients" text area.
  5. Click "Create budget"

Set AWS IAM Permissions

We're going to change the permission structure that grants instructors read-only access to your account so our autograding scripts can check your progress on a Cloud Assignment.

  1. Navigate to IAM (Identity and Access Management) in your AWS account.
  2. Click on "Roles" in the left-hand navigation menu.
    {
        "Version": "2012-10-17",
        "Statement": [
    	{
    	    "Effect": "Allow",
    	    "Action": "sts:AssumeRole",
    	    "Principal": {
    		"AWS": "arn:aws:iam::949430458732:user/autograder"
    	    },
    	    "Condition": {
    		"StringEquals": {
    		    "sts:ExternalId": "<TODO_REPLACE_WITH_YOUR_OWN_EXTERNAL_ID>"
    		}
    	    }
    	}
        ]
    }
    

Finish by creating a new credentials file in an INI file format.

credentials
[default]
aws_account_id = YOUR_AWS_ACCOUNT_ID
external_id = YOUR_EXTERNAL_ID_FROM_THE_TRUST_POLICY
🚨 Important

Safekeep the credentials file, you will use it for assignment submissions during Cloud Assignment 3.

Update from bird.ai

The detection history feature went viral. Funny bird photos captured with your application are all over BipBop and CrackerGraham. It proved to investors you’ve gained traction and found product-market fit (PMF). You just raised a Series A round of 10 million dollars from the investment firm Lion Local Management. Time to scale.

After signing the term sheet you find yourself in a lot more zoom meetings with phrases like due diligence, runway, and annual recurring revenue (ARR) thrown around. You've made a few more friends, and a lot more bosses. In the last board of directors meeting the lead series A investor demanded you find new sources of revenue. The investor is also on the board of a large financial company interested in purchasing your user data to feed into credit models they use to set interest rates for clients who love squirrels. A few months ago you would never have considered monetizing user data, but you're slowly realizing this 10 million dollar bag is pretty heavy.

While you're negotiating the stock purchase agreement and pretending it's the first time you heard the junior VC's story of how he won the spelling bee in middle school, your engineering team has been working around-the-clock on-call shifts putting out fires that pop up whenever another bird picture goes viral. Your lead engineer, Bark Iceberg, has teamed up with your head of product, Cherry Sandbar, to convince you to upgrade the website's infrastructure. Shocked at the sight of enemies uniting, you realize the scale of the problem.

📋 Instructions

Complete the following "Part 1" and "Part 2" sections to accumulate assignment points. Each section contains tasks for you to perform. Some will tell you exactly what to do, while others will give you hints or ask you to refer to a previous Cloud Assignment. Take your time and learn from the process. When you see

🤖 Autograder
in a task section, submit your credentials to the assignment on Gradescope so the autograder can give you feedback on your progress.

Part 1: N-Tier Architecture

You're going to be implementing a N-Tier Web Architecture to accommodate the increasing load on the bird.ai application. This is a versatile system architecture that lets you scale individual components according to how your application's usage patterns evolve. Our initial minimum-viable product (MVP) had a strong benefit, simplicity, which allowed us to move quickly and develop a proof-of-concept on a single virtual machine. However, this virtual machine becomes a single point of failure.

Mark Zuckerberg gave a talk to a Harvard CS class in 2005, where he says "The first decision we had to make, was how to expand the architecture to go from a single school type setup that we had just at Harvard, to something that supported multiple schools", and that "one really important decision that's helped us scale really well, is how we decided to distribute the data." They were able to scale to 300,000 users and 400 million page views a day in 2005 using a N-tier web architecture!

This architecture continues to be relevant today, and we will enhance it by implementing automation that lets us manage it effectively with a small team.

Adding a reverse proxy

A reverse proxy is software that sits between a client initiating network requests and a server responding to them. It's a versatile middleman, and depending on the use case will perform load balancing, authorization, rate limiting, data transformation, and more. We're going to be using nginx today, a popular reverse proxy.

First, we'll create a Dockerfile.

nginx/Dockerfile
FROM nginx:alpine

Now build it and run it.

Terminal
$ docker build -t proxy:latest nginx/
$ docker run -p 8080:80 proxy

When you visit http://localhost:8080 in a web browser, you'll see the nginx welcome page. Check it out! It's famous, a kind of Hollywood sign for web developers.

You can also see that the web server is running from the command line. To do that, we'll use a tool called curl (or cURL).

Terminal
$ curl -v http://localhost:8080
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.29.6
< Date: Thu, 19 Mar 2026 18:33:30 GMT
< Content-Type: text/html
< Content-Length: 896
< Last-Modified: Tue, 10 Mar 2026 16:28:44 GMT
< Connection: keep-alive
< ETag: "69b046bc-380"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

curl is a useful tool you should become familiar with. It's an ubiquitous command line HTTP client (and C HTTP library) that is feature rich and flexible. We just used the verbose flag "-v" to display the HTTP response and request content. Whenever you are debugging an API you are building, use curl -v to make requests to the endpoint.

Why did we use curl in the first place? We wanted to make sure that the nginx server was running and responding to requests. This type of action is called a healthcheck, and is used to monitor software services. This type of check is so common that it's a feature in Dockerfile through the HEALTHCHECK instruction.

Your task is to add a HEALTHCHECK instruction to the nginx Dockerfile definition you created, using curl to ping port 80 on all interfaces inside the container.

💡 Hint

The image nginx:alpine uses Alpine Linux as its base, use the package manager apk to install curl.

Once you've finished, move on to the next section.

Introducing Docker Compose

The premise of an N-Tier Architecture is that multiple software services work together to deliver a final user experience. You've already learned in class how that might look in a data center. You would have multiple servers in a rack, connected through a top of rack switch, with one or more services running on each server, responding to inbound north-south traffic from the internet. But our local development environment (your laptop) does not share that type of infrastructure set up. How then can we develop our application architecture with the confidence that if it works on our computer, it will work in production? A step in that direction builds on top of virtualization and standardization features that Dockerfile definitions for software gives us.

Docker Compose is a powerful tool that lets you define your container architecture in code. It's great for emulating a production environment on your local computer and playing around with different networking settings, container configurations, and more. We're going to create a basic Docker Compose file for our application, and add to it over the course of the assignment.

compose.yaml
services:
  proxy:
    platform: linux/amd64 # to match our target EC2 instance architecture
    build: # TODO
    ports: # TODO

Your task is to fill out "build" and "ports" with the same values we used in the previous docker build and docker run commands. Once you've done that, run the following command.

Terminal
$ docker compose up

The docker compose command by default looks for a file called "compose.yaml" in the current directory, reads the service definitions, and then starts the containers. Now you can re-visit http://localhost:8080 in the browser. Notice how we didn't have to pass any command line flags specifying port forwarding rules, or build the container image ahead of time. Compose reads the service definition, builds the container on-demand, and then configures runtime settings and starts the container for you automatically.

💡 Hint

Refer to Compose documentation showing the syntax for build and ports, and make sure Docker Compose is installed.

🌈 The More You Know

A Docker Compose file has three important top-level elements: services, networks, and volumes. Services define the containers that make up your application, networks control how they communicate, and volumes manage persistent storage.

The build key specifies the build context, a directory that contains a Dockerfile. Compose will build the image from the Dockerfile automatically when you run docker compose up.

Compose also has useful features for mimicking production constraints. For example, if your EC2 instance has 0.5 vCPU and 2 GB RAM, you can set matching resource limits on your service:

compose.yaml
services:
  proxy:
    build: ./nginx
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: 2G

This helps you catch performance issues locally before they surprise you in production.

When you have Docker Compose installed and the compose stack running, move on to the next section.

Pushing to a container repository

We have a working web service, an Nginx container, that we can run. We're not satisfied with just running it in our local development environment however, our goal is to deploy it so it's publicly available on the internet for anyone to access. In Cloud Assignment 2, we would build a container image locally, save it to a tar archive, copy the archive over to the remote server, and load it into Docker engine before starting it. That was a lot of repetitive work that had a risk of human error. In this assignment, we're going to set up a container registry that will store the container image for us, so we can push to it and pull from it on-demand. To "push" is to upload an image, to "pull" is to download an image. Don't worry, we'll step through how to do that in this section.

First things first, we'll need an AWS IAM user that has permission to push to an ECR repository in your account. Since we're iterating on new application features, we'll create a user that has "PowerUserAccess". We can downgrade the user's permissions later.

  1. Visit IAM in the AWS Console after logging in.
  2. In the left-hand menu under "Access Management" click "Users"
  3. Click "Create User"
  4. Write "ca3" as the user name.
  5. Under "Permissions Options", select "Attach policies directly"
  6. Under "Permissions Policies", filter by "AWS managed - job function" and select "PowerUserAccess". Then filter by "AWS managed" and select "IAMFullAccess".
  7. Click "Next".
  8. Click "Create user"
  9. Select the "ca3" user.
  10. Click tab "Security credentials" and click "Create Access key"
  11. Under "Use case" select "Other" and click "Next"
  12. Click "Create access key"
  13. Store the access key and secret access key someplace temporary and click "Done"

And in the terminal, run:

Terminal
$ aws configure --profile ca3
AWS Access Key ID [None]: <enter access key id>
AWS Secret Access Key [None]: <enter secret access key>
Default region name [None]: us-east-1
Default output format [None]
$ export AWS_PROFILE=ca3 # important! AWS CLI commands will now use ca3 by default

Now we can create a container registry and push to it. We're going to use the AWS Elastic Container Registry (ECR) and create a new repository.

  1. Visit ECR in the AWS Console after logging in.
  2. In the left-hand menu under "Public registry" click "Repositories"
  3. In the "Public repositories" page click "Create repository"
  4. Under "Repository name" write "ca3/proxy". "ca3" is the namespace and "proxy" is the repo-name.
  5. Click "Create"

Now take the URI of the container repository, and update the docker compose file we created for local development. Your task is to add another field "image" to the proxy service definition, which includes a "latest" tag.

💡 Hint

A container image has a conventional naming scheme:

[REGISTRY_HOST/][USERNAME/]NAME[:TAG|@DIGEST]

The repository URI you copied includes the host and name, you'll have to add the tag.

🌈 The More You Know

Every Docker image has two forms of identity: a name (with an optional tag) and a content-addressed ID (a SHA-256 digest).

Tags are mutable labels, you can move latest to point to a new image at any time. The image ID on the other hand, is a hash of the image's contents and never changes. This content-addressed design is what makes Docker layer caching work: when you push an image, Docker compares layer hashes with what the registry already has and only uploads the layers that changed.

This is why ordering matters in your Dockerfile. If you COPY requirements.txt and RUN pip install before COPY . ., the dependency layer is cached and only your application code is re-uploaded on each push.

Once you've updated the docker compose file, it's time to build the image and push it to our remote repository.

First, authenticate with the remote repository. Select the "ca3/proxy" repository and click "View push commands". Follow the first instruction to authenticate your Docker client to the registry.

https://us-east-1.console.aws.amazon.com/ecr/public-registry/repositories?region=us-east-1
ECR Public Repository for Proxy

Now once you've authenticated, you'll use compose to build and push your image.

Terminal
$ docker compose build # builds from ./nginx and tags with "image" value
$ docker compose push # pushes the image to ECR
$ docker compose up # runs the stack with the image stored in ECR
🤖 Autograder +10 (10/100)

Automating our deploys

It's time to introduce you to a new tool called Ansible. It automates server setup, instead of SSHing in to an EC2 instance and running commands manually, you describe the desired final state of the server in a YAML file and Ansible takes it from there. Ansible is an example of Infrastructure as Code (IaC). You write what you want (e.g. "Docker should be installed and running"), Ansible figures out the how (through a pre-written module or a script you write), and if Ansible verifies the server is already in the desired state, it doesn't change anything. That last property is called idempotency, it's an important characteristic of Ansible. It means you can run an Ansible playbook multiple times against a server and it will always end up in the correct state (as long as your playbook is written correctly).

Inside your starter code you'll find a few files:

Using this information, Ansible SSHs into the instances defined in the inventory and runs all the commands in the playbook against them, bringing your server to your final desired state. Because Ansible runs over SSH, you don't need anything installed on your server for Ansible to work (you just need to install it on your personal computer, take a look at installing it generally or on a specific operating system).

Now, you have several tasks to perform to make your playbook ready to run:

  1. Replace <YOUR_KEY_NAME> in ansible.cfg with the key pair you attached to your EC2 instance.
  2. Replace <YOUR_PROXY_PUBLIC_IP> in inventory.ini with ca3-proxy's public IP address.
  3. Replace <YOUR_ECR_URI> in playbook.yml with the URI of the ECR repository you created (omit any tag).

First, you'll have to create an EC2 instance, you should be pretty good at that by now. Create a c7i-flex.large running Amazon Linux with 8GB of disk space named "ca3-proxy". Add an inbound security group rule allowing all traffic to port 80.

When you've spun up the EC2 instance and replaced all the values in the Ansible files, run the following command.

Terminal
$ cd ansible
$ ansible-playbook playbook.yml

Voila! Visit your EC2 instance's public IP and you'll see "Welcome to nginx!"

🤖 Autograder +10 (20/100)
🍭 Food for thought

What steps does the Ansible playbook take to prepare the instance? Do you recognize any of them? What similarities and differences do you see in the approach?

🌈 The More You Know

Why a c7i-flex.large? The c7i family is compute-optimized, meaning it has a higher ratio of CPU to memory compared to general-purpose instances. That's a good fit for a reverse proxy. Nginx spends most of its time doing CPU-bound work like handling TLS termination, compressing responses, and managing thousands of concurrent connections, rather than holding large amounts of data in memory. The "flex" variant gives you shared baseline CPU at a lower cost with the ability to burst when traffic spikes.

From ClickOps to GitOps

We've done well by automating part of our deploy process, but in order to do that, we've spent a lot of time navigating the AWS Console. This is valuable experience, some cloud computing features are only available through a user interface, but it can be tedious and non-reproducible. This method of operation is jokingly called ClickOps. So how can we improve? How can we introduce reproducibility and visibility into our cloud infrastructure?

The most common solution today is Terraform. Terraform revolutionized DevOps by moving the implicit infrastructure you provision by clicking around in a web console into explicit infrastructure described in a text file. The Terraform language consists primarily of Providers, which are modules that interact with cloud services like AWS, and Resources, which are infrastructure objects you create and manage using a provider. There are a few other types of "blocks" in Terraform code, and you'll become familiar with most of them as we transition our ClickOps-managed infrastructure into GitOps-style infrastructure in this section.

Terraform is structured so that you write a new resource block to a file, plan the change letting you review how the infrastructure will be mutated, then apply that change automatically from the command line. But how does Terraform keep track of what has changed? Terraform stores the state of your infrastructure in a file called terraform.tfstate. When you start a new Terraform project, that state will live in a file on your computer. But if you are going to collaborate with other engineers, you need some way to share that "source of truth" with them. This is called a backend. Let's step through setting up S3 as a Terraform backend.

First, visit S3 in the AWS Console and create an S3 bucket called "ca3-tfstate-<YOUR_ACCOUNT_ID>". You've done this before in Cloud Assignment 2, refer back to that assignment if you need help.

Next, locate the terraform/ directory in the Cloud Assignment 3 starter code. Fill in the values required by the terraform.tfvars file.

terraform/terraform.tfvars
# --- Your Configuration ---
#
# Fill in these values before running terraform init.

account_id  = "<YOUR_ACCOUNT_ID>" # e.g. "123456789012"
region      = "us-east-1"
ami_id      = "<YOUR_AMI_ID>"      # e.g. "ami-0abcdef1234567890" (find in EC2 console)
key_name    = "<YOUR_KEY_NAME>"    # e.g. "my-keypair"
public_key  = "<YOUR_PUBLIC_KEY>"  # run: ssh-keygen -y -f ~/.ssh/<key>.pem

Now, initialize Terraform.

Terminal
$ cd terraform # enter the terraform directory
$ terraform init # download and initialize providers
💡 Hint

You'll have the install the Terraform CLI to run these commands. Follow the official installation instructions.

You now have a local Terraform state file! Take a look inside.

Terminal
$ cat terraform.tfstate
{
"version": 4,
"terraform_version": "1.13.5",
"serial": 1,
"lineage": "29340e2f-6b8e-43f5-cb83-dc1712b9753f",
"outputs": {},
"resources": [
...

The next step is to have Terraform store the state file inside S3, securing the state file and making collaboration on infrastructure possible. Never share or store the state file in a public location, it contains sensitive credentials and details about your infrastructure an attacker can exploit.

Open backend.tf, delete the "local" backend, and uncomment the "s3" backend. Make sure to replace "<ACCOUNT_ID>" with your actual AWS Account ID (the backend block cannot use variables, it's the starting point for the rest of your terraform code).

Now run:

Terminal
$ terraform init -migrate-state

If you visit the S3 bucket again in the AWS Console, you'll see it now has an item "ca3/terraform.tfstate" inside. Now you can clean up the local state files:

Terminal
$ rm -f terraform.tfstate terraform.tfstate.backup
🤖 Autograder +10 (30/100)

Importing existing infrastructure

Terraform isn't only used for new projects. Take this Cloud Assignment for example, we've already created quite a few AWS resources manually:

We're going to use Terraform's import feature to bring our existing infrastructure under Terraform's control. The steps we work through will help you become more familiar with Terraform's CLI. Let's get started.

Look up the EC2 instance ID, security group ID, and key pair name for "ca3-proxy" in the AWS Console. Add the following import blocks to imports.tf:

terraform/imports.tf
import {
   to = aws_ecrpublic_repository.proxy
   id = "ca3/proxy"
 }

 import {
   to = aws_instance.ca3_proxy
   id = "<INSTANCE_ID>"
 }

 import {
   to = aws_security_group.ca3_proxy
   id = "<SECURITY_GROUP_ID>"
 }

 import {
   to = aws_key_pair.ca3
   id = "<KEY_PAIR_NAME>"
 }

Then run:

Terminal
$ terraform plan
$ terraform apply

When the terraform apply succeeds, delete the imports.tf file.

🌈 The More You Know

When you run terraform apply, Terraform performs three steps:

  1. Refresh - queries your cloud provider (AWS) for the current state of every resource it manages, updating the state file with any changes.
  2. Diff - compares the desired state (your .tf files) against the actual state (from the refresh), producing an execution plan of creates, updates, and deletes.
  3. Apply - executes the plan, calling the AWS API to make the changes, and records the new state.

This makes terraform destroy and terraform apply a reliable round-trip. The state file is the bridge between what you wrote and what exists in AWS. As long as the state file is safe in S3, Terraform can always reconcile reality with your code.

Now when you run terraform plan again, you should see "No Changes":

Terminal
$ terraform plan
aws_ecrpublic_repository.proxy: Refreshing state... [id=ca3/proxy]
data.aws_vpc.default: Reading...
aws_key_pair.ca3: Refreshing state... [id=ca3-keypair]
data.aws_vpc.default: Read complete after 0s [id=vpc-0ae3ae9dc53c75193]
data.aws_subnets.default: Reading...
aws_security_group.ca3_proxy: Refreshing state... [id=sg-04fe4f42d727e45a4]
data.aws_subnets.default: Read complete after 0s [id=us-east-1]
aws_instance.ca3_proxy: Refreshing state... [id=i-00beba6801cfed024]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

That wasn't too difficult, so you might not be impressed. But we've gained extraordinary capabilities. We're going to demonstrate what we've achieved using the autograder. You're going to run a new command terraform destroy, which will remove all the infrastructure we've spent time setting up in this assignment. Are you worried?

Terminal
$ terraform destroy
🤖 Autograder -30 (0/100)

Let's bring everything back with two commands:

Terminal
$ cd terraform
$ terraform apply
$ cd ../ansible
$ ansible-playbook playbook.yml
🤖 Autograder +30 (30/100)
🍭 Food for thought

How does this change how you think about building and structuring your personal software projects? Do these new tools, or even this new perspective, increase your ambition? What hard problems seem easier now?

What about bird.ai?

There's an easter egg hiding in the Ansible and Terraform code, have you spotted it already? Our architectural change moving bird.ai to an N-Tier architecture has already been mapped out for us.

:80

:8000

:5432

upload

OAC

HTTPS

User

ca3-proxy
nginx
c7i-flex.large

ca3-app
bird.ai
t3.small / t4g.small

PostgreSQL
db.t3.micro

S3 media

CloudFront

Your penultimate task for Part 1 is to find the terraform.tfvars file and uncomment the "deploy_app" and "app_instance_type" section. Make sure to choose the instance architecture that matches your personal computer. When you're ready, run

Terminal
$ cd terraform
$ terraform apply
$ cd ../ansible
$ ansible-playbook playbook.yml

It will take some time for the cloudfront distribution and RDS database to be provisioned. When it's finished, visit the public IP of the nginx instance. What do you see?

🤖 Autograder +10 (40/100)
🔮 Vibe Check

Was this deploy of bird.ai harder or easier than in Cloud Assignment 2? What stood out to you most about the differences between the deployment methods between assignments? Let us know on Ed!

Choosing a domain name

It's not convenient or memorable to type in an IP address every single time you want to visit a website. That's why the Domain Name System (DNS) exists. It's a way to give a human-readable name to an address on the internet. Instead of "101 N Grant St, West Lafayette, IN 47906", you can say "Purdue Memorial Union". If you'd like to share bird.ai with your friends and family, let's make that easier for you.

Your final task for Part 1 is to make an HTTP request to an API endpoint to create a DNS A record for the EC2 instance running the reverse proxy. The instructional team for CS 351 has created an HTTP server that exposes a REST API endpoint that lets you submit a domain name. Here are the details of the API endpoint:

🔌 PUT https://api.cs351.cloud/dns

Register or update a DNS subdomain pointing to your proxy server's IP address. Each AWS account may claim one subdomain at a time.

Authentication: HTTP Basic Auth

Authorization Header: Basic base64(account_id:external_id), where account_id is your 12-digit AWS account ID and external_id is the ExternalId from your credentials file.

Request Content-Type: JSON

ParameterTypeRequiredDescription
subdomainstringyesSubdomain name (e.g. mysite). Lower case letters, digits, and hyphens only. 1-63 chars. Must not start or end with a hyphen.
valuestringyesIPv4 address to point the subdomain to (e.g. 192.168.1.100)

Success response (200):

{
	"record": "mysite.ca3.cs351.cloud",
	"type": "A",
	"value": "3.14.15.92",
	"message": "DNS record updated. Changes propagate within 60 seconds"
}

Error responses:

StatusMeaning
400Invalid subdomain format or invalid IP address
401Missing/malformed auth header, or credentials failed verification
409Subdomain already claimed by another account
429Rate limited (one update per 30 seconds) - check Retry-After header

You have a few different options to accomplish this using tools that have been introduced to you in the Cloud Assignment.

There are strengths and weaknesses with each approach, pick the one that fits your workflow.

🤖 Autograder +10 (50/100)
🌈 The More You Know

After calling the API, you can use dig to confirm your DNS record is live. DNS changes in Route 53 propagate within 60 seconds, so if you just made the request, wait a moment before checking.

  1. Run dig <your-subdomain>.ca3.cs351.cloud
  2. Look for an ANSWER SECTION containing your A record with the IP you registered
  3. If you see NXDOMAIN or no answer section, wait 60 seconds and try again

When it succeeds, you'll be able to visit http://<your-subdomain>.ca3.cs351.cloud in the browser and see your deployed application! Take note, your connection to the website will not be encrypted, we'll implement that in Part 2 of the assignment.

🌈 The More You Know

The full stack N-Tier Architecture for the bird.ai application can be run locally, update your compose file according to the reference below then run docker compose up and open http://localhost:8080.

Docker Compose definition for N-Tier Architecture
compose.yaml
services:
 db:
   image: postgres:17-alpine
   environment:
     POSTGRES_DB: ca3
     POSTGRES_USER: ca3
     POSTGRES_PASSWORD: localdev
   volumes:
     - pgdata:/var/lib/postgresql/data
   healthcheck:
     test: ["CMD-SHELL", "pg_isready -U ca3"]
     interval: 3s
     timeout: 3s
     retries: 10

 app:
   build:
     context: .
     dockerfile: bird.ai/Dockerfile
   environment:
     DATABASE_URL: postgres://ca3:localdev@db:5432/ca3
     DEBUG: "true"
   volumes:
     - media:/app/data/media
   depends_on:
     db:
       condition: service_healthy

 proxy:
   image: nginx:alpine
   ports:
     - "8080:80"
   volumes:
     - ./nginx/default.conf.local:/etc/nginx/conf.d/default.conf:ro
     - media:/media:ro
   depends_on:
     - app

volumes:
 pgdata:
 media:

(click to open)

📌 Notice

You are now done with Part 1. Because of our use of Infrastructure-as-Code, if you want to take a break, you can destroy your infrastructure. When you're ready to tackle Part 2, run terraform apply and ansible-playbook playbook.yml, and if you submit again to the autograder you'll still have 50/100 points.

Part 2: Squirrel-due University

bird.ai has taken off and the $40 million in Series A funding has given you runway to grow, but venture capital comes with strings attached. Your investors expect returns, and they have ideas about how to get them.

You are being pushed by one of your investors to sell usage data that financial technology companies can use to build better credit models that set interest rates for customers that love squirrels. This is a familiar business model in tech, first you offer a free consumer product, then you monetize the data it generates. Meta makes money through targeted advertising powered by user behavior. Google gives away Search, Maps, and Gmail to power its ad platform. Even VPN providers that market themselves on privacy have been caught selling anonymized browsing data. The product is free because you are the product.

In order to win new fintech customers, bird.ai needs a couple upgrades. First, squirrel detection, birds are not enough in this competitive market. Second, geolocation, your customers need to know where your users are. Part 2 implements both of these features, along with the infrastructure to share detection data across the class through a gossip protocol.

But before any of that, we have a security problem to fix.

📌 Notice

If you completed Part 1 using the ca3part1.bird.ai.zip release files, make sure to download the updated starter code ca3.bird.ai.zip and follow this migration plan to transfer over your changes.

Step 1: Back up your customized files

Terminal
$ cp terraform/terraform.tfvars terraform/terraform.tfvars.bkp
$ cp terraform/backend.tf terraform/backend.tf.bkp
$ cp ansible/ansible.cfg ansible/ansible.cfg.bkp

Step 2: Unpack the Part 2 starter code

Terminal
$ unzip -o ca3.bird.ai.zip

Step 3: Restore your values

Step 4: Verify

Terminal
$ cd terraform
$ terraform init
$ terraform plan # should show no unexpected changes

Turning cleartext into ciphertext

When you visit your application over the internet, you are using the HTTP protocol, sending HTTP requests back and forth. The original HTTP specification was very simple, and omitted one important feature: encryption. At the moment, every request between you and the bird.ai application travels in plaintext over HTTP, unencrypted. Anyone on the network path (your coffee shop Wi-Fi router, Internet Service Providers, or VPN) can read the whole thing. That means when you log in to bird.ai, you are sending your username and password unencrypted over the internet. It's like sending a letter through the mail to login, but instead of writing your password inside the envelope, you put it on the outside, next to the address.

We're going to implement HTTPS to solve this problem. HTTPS wraps HTTP requests in something called Transport Layer Security (TLS). TLS provides three guarantees:

  1. Confidentiality: traffic is encrypted and eavesdroppers see ciphertext
  2. Integrity: any tampering with the request itself can be detected
  3. Authentication: the server you communicate with verifies its identity using a TLS certificate signed by a Certificate Authority (CA).

To enable HTTPS you need a TLS certificate issued for your domain. We'll use AWS Certificate Manager (ACM) to accomplish that. Instead of installing the certificate on Nginx, we'll place CloudFront in front of your proxy to terminate TLS at the edge.

Before

HTTP :80

User

ca3-proxy

ca3-app

After

HTTPS :443

HTTP :80

HTTP :80

User

Cloudfront

ca3-proxy

ca3-app

This pattern is called TLS offloading, where compute-intensive encryption and decryption work happens on AWS's global edge network rather than on your EC2 instance.

Your first task is to uncomment the domain variable in your terraform.tfvars and replace it with the domain name you registered in Choosing a domain name

terraform/terraform.tfvars
domain = "<your-subdomain>.ca3.cs351.cloud"

Now run

Terminal
$ terraform apply

Terraform will run, but block at ACM certificate validation. That's expected, in order to validate the certificate, we need to update our DNS settings again. We'll do that with a similar API call as was used in the domain name section. First, interrupt the terraform apply when it becomes blocked. Then run

Terminal
$ terraform apply 
random_password.rds[0]: Refreshing state... [id=none]
aws_ecrpublic_repository.proxy: Refreshing state... [id=ca3/proxy]
aws_cloudfront_origin_access_control.media[0]: Refreshing state... [id=E3DY32JLXZYQDA]
...
create Terraform will perform the following actions:
  # aws_acm_certificate.proxy_tls[0] will be created
  + resource "aws_acm_certificate" "proxy_tls" {
  + arn = (known after apply)
  + domain_name = "mysite.ca3.cs351.cloud"
...
Do you want to perform these actions?
  Terraform will perform the actions described above. Only 'yes' will be accepted to approve.
  Enter a value: yes

aws_acm_certificate.proxy_tls[0]: Creating...
aws_acm_certificate.proxy_tls[0]: Creation complete after 7s [id=arn:aws:acm:us-east-1:897729129333:certificate/bd093522-7646-481f-ae4f-cfdc6ddf6232]
aws_acm_certificate_validation.proxy_tls[0]: Creating...
aws_acm_certificate_validation.proxy_tls[0]: Still creating... [00m10s elapsed]
aws_acm_certificate_validation.proxy_tls[0]: Still creating... [00m20s elapsed]
aws_acm_certificate_validation.proxy_tls[0]: Still creating... [00m30s elapsed]
aws_acm_certificate_validation.proxy_tls[0]: Still creating... [00m40s elapsed]
aws_acm_certificate_validation.proxy_tls[0]: Still creating... [00m50s elapsed]
aws_acm_certificate_validation.proxy_tls[0]: Still creating... [01m00s elapsed]
^C
Interrupt received. 
Please wait for Terraform to exit or data loss may occur. 
Gracefully shutting down...

Stopping operation...
$ terraform output acm_validation_name
"_a0f0a86e85fac3db6512c9350cd21003.mysite.ca3.cs351.cloud."
$ terraform output acm_validation_value
"_55dfb05e47cd7afbff025c91a15c96f2.jkddzztszm.acm-validations.aws."

Now take those outputs and use them in another HTTP request to the instructional API. This time, you'll be submitting validation records that prove to AWS you control this domain name.

🔌 PUT https://api.cs351.cloud/dns/acm-validation

Register an ACM DNS validation CNAME record to prove domain ownership for your TLS certificate. You must have already registered a subdomain via PUT /dns before creating a validation record for it.

Authentication: HTTP Basic Auth

Authorization Header: Basic base64(account_id:external_id), where account_id is your 12-digit AWS account ID and external_id is the ExternalId from your credentials file.

Request Content-Type: JSON

ParameterTypeRequiredDescription
namestringyesACM validation record name from terraform output acm_validation_name (e.g. _abc123.mysite.ca3.cs351.cloud.)
valuestringyesACM validation record value from terraform output acm_validation_value (e.g. _xyz.acm-validations.aws.)

Success response (200):

{
	"record": "_abc123.mysite.ca3.cs351.cloud",
	"type": "CNAME",
	"value": "_xyz.acm-validations.aws",
	"status": "PENDING",
	"message": "ACM validation CNAME created. Certificate validation typically completes within a few minutes."
}

Error responses:

StatusMeaning
400Record name not under ca3.cs351.cloud, value not an ACM validation target, or name missing hash prefix
401Missing/malformed auth header, or credentials failed verification
403You do not own the base subdomain, register it via PUT /dns first

Once you've successfully updated the DNS records, within a few minutes ACM will verify the CNAME record and issue your certificate. You can check the progress by visiting ACM in the AWS Console and looking at the "Status" column for the certificate you submitted. Once "Status" becomes "Issued", return to the terminal and run

Terminal
$ terraform apply # wait for it to complete, may take 5-15 minutes
$ terraform output cloudfront_proxy_domain

Now take the cloudfront domain name and submit it to the PUT /dns endpoint as the new "value" for your "subdomain". Once that request is successful, return to the terminal and run

Terminal
$ cd ../ansible
$ ansible-playbook playbook.yml # configure nginx to tell Django HTTPS is enabled

Once DNS changes propagate, if you visit your subdomain in a web browser again, you'll see that HTTPS is enabled! No more warning that you're visiting an HTTP-only site, and you can be confident your application credentials are encrypted.

🤖 Autograder +10 (60/100)
🌈 The More You Know

When you first registered your subdomain in Part 1, the API created an A record, a DNS record that maps a domain name directly to an IPv4 address. Your browser looked up "mysite.ca3.cs351.cloud", got back an IP like "34.230.46.118", and connected to your EC2 instance.

Now you've pointed your subdomain at a Cloudfront domain name instead of an IP address. Behind the scenes, the API created a CNAME record, a DNS record that maps one domain name to another. When a browser looks up "mysite.ca3.cs351.cloud", DNS first returns "d1234.cloudfront.net", then resolves that to an IP on AWS's edge network.

The ACM validation record you created earlier is also a CNAME. AWS Certificate Manager published a challenge ("_abc123.mysite.ca3.cs351.cloud"), and you proved you control the domain by creating a CNAME pointing it to ACM's validation server. Once ACM verified the record existed, it issued your TLS certificate. This is called DNS-01 validation, the same method used by Let's Encrypt and other certificate authorities.

Squirrel detection

It's time to implement squirrel detection using our YOLO model. Remember, we can only detect birds at the moment. The model is capable of detecting many objects, we just have to tell it what to look for. In this section, you'll modify the application code that controls this feature. This is your first time modifying bird.ai's code in this assignment, and afterwards you'll re-deploy to see your changes live.

The current model (yoloe-26n-seg.pt, 11MB) is the "nano" variant of YOLO-E. It's fast and small, but its text encoder doesn't produce strong enough embeddings to reliably detect squirrels. You'll upgrade to the "large" variant (yoloe-26l-seg.pt, 75 MB), which is more capable.

🌈 The More You Know

Bigger models aren't always better for open-vocabulary detection. Here's what we found when testing squirrel detection across model sizes:

ModelSizesquirrel.jpegbird_and_squirrel.jpeg
nano (26n)11 MBnothingbird: 0.70
small (26s)29 MBnothingbird: 0.65, squirrel: 0.50
medium (26m)67 MBnothingsquirrel: 0.72, bird: 0.37
large (26l)75 MBsquirrel: 0.93squirrel: 0.77, bird: 0.42
xlarge (26x)163 MBnothingsquirrel: 0.41, bird: 0.30

The large model is the only one that reliably detects squirrels in both images. The xlarge model actually performs worse, its larger visual encoder produces features that are less well-aligned with CLIP's text embedding space. This represents a tradeoff in open-vocabulary tasks versus closed-vocabulary tasks, model size may improve your performance in one, but decrease it in the other.

The first step is to download the larger model, it should be placed alongside the existing yoloe-26n-seg.pt file in the bird.ai/ directory.

Terminal
$ cd bird.ai
$ curl -Lo yoloe-26l-seg.pt https://github.com/ultralytics/assets/releases/download/v8.4.0/yoloe-26l-seg.pt

The second step is to update the YOLO model configuration you find in bird.ai's controller code.

bird.ai/app/views.py
- model = YOLO('./yoloe-26n-seg.pt')
- model.set_classes(['bird'], model.get_text_pe(['bird']))
+ model = YOLO('./yoloe-26l-seg.pt')
+ model.set_classes(['bird', 'squirrel'], model.get_text_pe(['bird', 'squirrel']))

Finally, re-deploy the bird.ai application using Ansible (it will take longer because the model file size has increased). bird.ai works on mobile, I encourage you to go out on campus, log in, and start taking pictures of squirrels!

🤖 Autograder +10 (70/100)
🌈 The More You Know

So what is actually going on when we add the class “squirrel” to the YOLOE26 model? Is it doing anything different than when we were detecting birds? Yes, very much so. While the dataset used to train earlier versions of this model included the class “bird,” it was not trained on a “squirrel” class. That means the model is no longer relying on a fixed, closed set of labels learned during training, and instead uses YOLOE’s open-vocabulary mechanism to introduce a new class at inference time.

YOLOE26 is an object detection model that combines the YOLO26 architecture with open-vocabulary capabilities, allowing it to detect classes like “squirrel” based on text prompts without retraining (source, source). At its foundation, the model relies on artificial neurons, or perceptrons, which process inputs using learned weights, biases, and activation functions. During training, the network minimizes prediction errors through back propagation, which calculates gradients and updates these weights to improve accuracy. Because it processes image data, the YOLO model structures these neurons into a Convolutional Neural Network (CNN) backbone to combine features across multiple scales (source). The CNN uses sliding filters (kernels) to hierarchically extract visual features, building from simple edges up to complex patterns.

After extracting visual features, the core YOLO framework divides the image into a grid. Instead of generating multi-stage region proposals, each grid cell simultaneously regresses bounding box coordinates (center, width, and height) alongside a confidence score (source, source). To achieve open-vocabulary detection, YOLOE replaces the traditional fixed-class prediction head with a semantic object embedding head (source). When prompted with the word “squirrel,” a frozen vision-language encoder (CLIP) generates a high-dimensional vector representing the text. The RepRTA (Re-parameterizable Region-Text Alignment) module then aligns this text embedding with the visual features and mathematically folds it directly into the model’s weights. This allows the model to compute similarity between the CNN’s visual embeddings and the text prompt, enabling it to detect the squirrel with zero runtime overhead.

If this sounds interesting, we’ve written a much more in-depth explanation of how all of this works that focuses on:

Mapping detections

Photos taken on smartphones contain metadata called EXIF (Exchangeable Image File Format). Among other data (camera model, exposure settings, timestamp), EXIF can include the GPS coordinates of where the photo was taken (if the user has location services enabled). In this section, you'll implement a function that extracts GPS coordinates from uploaded images and store them alongside the detection record. Once implemented, detections from geotagged photos will automatically appear as markers on the map page.

https://mysite.ca3.cs351.cloud/map/
Detection Map bird.ai App

Part of becoming a professional in cloud computing is understanding the implications of working with cloud services. One of the most important is that once data leaves your device, you don't own it anymore, and the service you're interacting with can and will pull a lot of information from even a small amount of data.

🌈 The More You Know

The map uses Leaflet.js with map tiles from OpenStreetMap. Leaflet runs in the browser and fetches map tiles directly from OpenStreetMap's public tile servers, no new infrastructure needed (AWS Free Tier is too restrictive to run our own tile server).

Open bird.ai/app/views.py and find the extract_location_from_exif() function. Currently, it returns (None, None).

bird.ai/app/views.py
def extract_location_from_exif(image_path):
  # TODO: implement EXIF GPS extraction
  # Hint: use Image.open(image_path).getexif().get_ifd(IFD.GPSInfo)
  # to access the GPS tags, then convert DMS to decimal degrees
  return None, None

Replace the body with your implementation. You'll need to:

  1. Open the image with PIL.Image.open(image_path)
  2. Get the EXIF data with .getexif()
  3. Access the GPS sub-IFD with .get_ifd(IFD.GPSInfo) (import IFD from PIL.ExifTags)
  4. Read GPS tags 1-4 (latitude ref, latitude DMS, longitude ref, longitude DMS)
  5. Convert DMS to decimal degrees
  6. Apply the hemisphere sign
  7. Return (latitude, longitude) or (None, None) if no GPS data is present

Wrap everything in a try/except. Images without EXIF or without GPS tags should return (None, None), not crash.

💡 Hint

GPS coordinates in EXIF are stored as degrees, minutes, seconds (DMS) and not as decimal degrees.

For example, Lafayette, IN is:

Latitude: 40° 25' 25.32" N
Longitude: 86° 55' 16.32" W

This is stored in four EXIF GPS tags:

To convert DMS to decimal degrees:

decimal = degrees + minutes/60 + seconds/3600

Then apply the hemisphere:

So Lafayette becomes: (40.4237, -86.9212)

When you want to verify your implementation, run the unit tests locally (If you haven't set up the full docker compose stack from the end of Part 1, do that first).

Terminal
$ docker compose run --rm --entrypoint python app manage.py test app.tests.ExifExtractionTest

Once they're passing, deploy your updated application using Ansible, then upload an image with your phone and check the map page to see if a location marker appears.

🤖 Autograder +20 (90/100)
💡 Hint

If mobile uploads aren't appearing on the map, you'll need to enable Location Services:

iPhone

  1. Settings → Privacy & Security → Location Services → make sure it's ON
  2. Settings → Privacy & Security → Location Services → Camera → set to "While Using the App"

Android

  1. Settings → Location → turn ON
  2. Camera app → Settings → "Save location" (or "GPS tag") → turn ON

This should work for most phones, although recent OS versions have gotten more privacy conscious and may strip location from EXIF data by default.

Gossip Squirrel

Your bird.ai application is isolated to your AWS account and constrained to the N-Tier architecture we developed in Part 1. Any data your application stores was only generated by users registered with you. In this section, you'll connect your application to a gossip network that shares detection data across the entire class. When any student uploads a geotagged photo, every other student's map shows the detection.

Other students'bird.aiInstructional API(api.cs351.cloud)Your bird.aiOther students'bird.aiInstructional API(api.cs351.cloud)Your bird.aiUpload geotagged photoUpload geotagged photoloop[Every 10 seconds(sync_gossip)]loop[Every 10 seconds(sync_gossip)]User opens Map pageRender local markers (red)+ remote markers (blue)Images load from each peer's CloudFrontUser opens Map pageRender local + remote markersSave Detection(latitude, longitude, image_url)Save Detection(latitude, longitude, image_url)POST /gossip{domain, detections[]}Authenticate (Basic Auth)Validate domain matches subdomainDeduplicate by image_url{new: 1, skipped: 0}POST /gossip{domain, detections[]}{new: 1, skipped: 0}GET /gossip{detections: [...all peers...]}GET /gossip{detections: [...all peers...]}

The gossip API only stores metadata (location, image URL, domain name), while images stay on the CloudFront CDN they originated from. When another student's map shows your detection, their browser loads the image directly from your CloudFront distribution.

Your goal in this section is to implement the function share_detections_with_network(), which performs the POST /gossip request that publishes data to the API server.

The gossip sync runs inside the bird.ai container as a background loop (every 10 seconds). It needs three pieces of information: your domain name, AWS account ID, and external ID. These flow through a pipeline that starts in Terraform and ends inside the running container.

  1. You define external_id and domain in terraform.tfvars
  2. Terraform exposes them as outputs (along with account_id)
  3. The Ansible playbook reads those outputs and passes them as variables to Play 3
  4. Play 3 templates them into the systemd service file ca3-app.service.j2
  5. The service file passes them as environment variables to the Docker container
  6. Inside the container, sync_gossip.py reads them from os.environ

Steps 2-6 are already wired up in the starter code. Your task is step 1, uncomment and set external_id in terraform.tfvars

terraform/terraform.tfvars
# Gossip protocol: set your external_id from your credentials file
external_id = "your-external-id"

Then run terraform apply to register the new output value.

🌈 The More You Know

To see Ansible's templating in action, open ansible/templates/ca3-app.service.j2. The template defines a systemd service that runs the bird.ai Docker container:

ansible/templates/ca3-app.service.j2
...
ExecStart=/usr/bin/docker run --rm --name ca3-app \
  -p 8000:8000 \
  -e DATABASE_URL={{ database_url }} \
  -e AWS_STORAGE_BUCKET_NAME={{ s3_bucket }} \
  -e CLOUDFRONT_DOMAIN={{ cloudfront_domain }} \
  -e DOMAIN={{ domain | default('') }} \
  -e AWS_ACCOUNT_ID={{ account_id | default('') }} \
  -e AWS_EXTERNAL_ID={{ external_id | default('') }} \
  {{ app_image }}
  ...

When Ansible templates this file, it substitutes each {{ variable }} with values from the playbook's vars: section. The | default('') filter provides a fallback if the variable isn't set. The result is a concrete systemd unit file on the server with your actual database URL, bucket name, and credentials baked in as Docker environment variables.

This pattern, generating config files from templates, is how Ansible avoids hardcoding environment-specific values. The same playbook and templates work for every student; only the Terraform outputs differ.

The final step is to implement the share function. Open bird.ai/app/views.py and find share_detections_with_network. It currently is a stub.

bird.ai/app/views.py
def share_detections_with_network(detections_data, domain, account_id, external_id):
  # TODO: implement - make one HTTP POST request to
  # https://api.cs351.cloud/gossip
  pass

Replace the body with a single requests.post() call. Here's the format of the API call:

🔌 POST https://api.cs351.cloud/gossip

Push a batch of geotagged detections to the gossip network. Other students' map pages will display your detections alongside their own. You must have already registered a DNS subdomain via PUT /dns, and the domain field must match your registered subdomain.

Authentication: HTTP Basic Auth

Authorization Header: Basic base64(account_id:external_id), where account_id is your 12-digit AWS account ID and external_id is the ExternalId from your credentials file.

Request Content-Type: JSON

ParameterTypeRequiredDescription
domainstringyesYour registered subdomain (e.g. mysite.ca3.cs351.cloud). Must match the subdomain you registered via PUT /dns
detectionsarrayyesList of detection objects to share (see below)

Detection object:

FieldTypeRequiredDescription
image_urlstringyesFull HTTPS URL to the detection image on your CloudFront CDN (e.g. https://d1234.cloudfront.net/detections/abc.jpg)
latitudenumberyesDecimal degrees, -90 to 90
longitudenumberyesDecimal degrees, -180 to 180
detectionsarrayyesDetection results (e.g. [{"name":"bird", "confidence":0.85}])

Success Response (200):

{
    "new": 1,
    "skipped": 0,
    "message": "Shared 1 new detection(s)."
}

Error responses:

StatusMeaning
400No registered subdomain, or domain does not match your registered subdomain
401Missing/malformed auth header, or credentials file failed verification
422Invalid request body (missing fields, wrong types)

To verify your implementation is correct, run the unit tests:

Terminal
$ docker compose run --rm --entrypoint python app manage.py test app.tests.GossipPushTest

Once the unit tests are passing, deploy the application using Ansible.

🤖 Autograder +10 (100/100)
🎉 Nice work!

You have just implemented a coordination protocol and created a distributed system! This is one of the toughest problems in cloud computing and computer science!

🔮 Vibe Check

How did this assignment compare to Cloud Assignment 2? Was it easier, or harder? Did the starter code help you understand how Terraform and Ansible worked?

Let us know on Ed!

Estimating Cloud Cost

Your N-Tier Architecture uses several AWS services billed by the hour or by the gigabyte. Because you built with Terraform, you can terraform destroy when you stop working and terraform apply when you start again — you only pay for hours your infrastructure is running.

Resource Rates (us-east-1, on-demand, no free tier)

ResourceTypeRate
ca3-proxyc7i-flex.large$0.0848/hr
ca3-appt4g.small$0.0168/hr
RDS databasedb.t3.micro PostgreSQL$0.018/hr
EBS volumes18 GB gp2 total$1.80/mo
RDS storage20 GB gp2$2.30/mo
S3, CloudFront, ACM, ECRFree at this scale

Data transfer in is free. CloudFront egress is covered by the 1 TB/month free tier.

How Billing Works

Estimated Cost

ScenarioHoursCost
Full assignment (10 days, never tear down)240~$30
3-4 days of work, no tear down40~$6
3-4 days of work, tear down between sessions20~$4

The largest cost driver is the proxy instance ($0.0848/hr). Storage and bandwidth are negligible.

Teardown

🚨 Important

Make sure that you remove all resources you created after obtaining a full score on Gradescope. In particular, review you no longer have any running EC2 or RDS instances.

Step 1: Destroy Terraform-managed infrastructure

Terminal
$ export AWS_PROFILE=ca3
$ cd terraform
$ terraform destroy

Step 2: Delete the state bucket

Terminal
$ aws s3 rb s3://ca3-tfstate-<YOUR_ACCOUNT_ID> --force

Step 3: Visit IAM in the AWS console and delete the user "ca3"