Purdue University CS351 Spring 2026
Due February 20th 11:00PM
In this assignment, you will implement a new feature in the Software-as-a-Service (Saas) introduced in Cloud Assignment 1 and deploy it to production. You will learn how application requirements drive system architecture decisions. You will have practical experience integrating a cloud service API in an application. You will continue to build skills managing and deploying to virtual machines in the cloud. Docker is introduced and you will build your first container. You'll gain knowledge of virtual networking and virtual storage and how those work together to create secure and elastic environments for software. Finally, you will become aware of how cost and performance are entangled in cloud software systems.
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.
The autograder user's permissions have changed, 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.
We're going to set up an AWS Budget that will alert you if you get any monetary charges.
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.
Follow these steps, and then we'll discuss why this approach is more secure than the approach in previous assignments.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Principal": {
"AWS": "arn:aws:iam::897729129333: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.
[default]
aws_account_id = YOUR_AWS_ACCOUNT_ID
external_id = YOUR_EXTERNAL_ID_FROM_THE_TRUST_POLICYSafekeep the credentials file, you will use it for assignment submissions during the semester.
Why did we take this approach? In Cloud Assignments 0 and 1, you created an access key associated with a specific user. That access key id and secret is like a username and password for the user itself. Anything or anyone that gets a hold of it, can pretend to be the user from your AWS account! It's a "long-lived" credential, so if it gets exposed because you accidentally posted it online, or someone read it unencrypted in transit on a network, or if your computer gets hacked, you'll have to act quickly to delete that access key before the attacker can use it against you.
What we've done here is transitioned the permission structure between you and the course's autograder from exchanging a long-lived credential that can be used by anyone, to you creating a role that can only be accessed by the instructional staff's AWS account, where all you submit is your account ID (not privileged) and a secret (that doesn't grant permissions or access by itself), enabling the autograder to generate short-lived credentials for a single grading session. Even if those short-lived credentials are picked up in transit by an attacker, that attacker would need to use them before they expire, and their capabilities would be limited by the permissions of the role. Your credentials went from expires never to expires soon.
Let's quickly discuss the external ID you created (the "secret" you submit along with your account ID). It addresses the confused deputy problem. Imagine you're collaborating with a fellow student on a Cloud Assignment or another project, and they learn your AWS Account ID. This happens frequently, you often need it to set permission policies, or it's included in URLs for Amazon services you use. It's not a huge deal, even though you should treat it as sensitive information and make an effort to keep it confidential. If the fellow student pretends to be you by submitting your account ID to an assignment on Gradescope, the "external id" you specified in the trust policy will cause their attempt to impersonate you to fail. Don't worry, even if another student knows your account ID and external ID, we keep track of student AWS account IDs for each submission so we can resolve any grading issues that come up during the course (or even identify students using the same AWS account).
Access and identity when using cloud services is nuanced. You have an incredible power to rent huge amounts of data center capacity from AWS, but with great power comes great responsibility. Generally, engineers are responsible enough to have privileged access to a company AWS account (usually through "PowerUserAccess"), but the moment a piece of software you create or a third-party you integrate with (like Purdue's Cloud Computing course!) needs that access to enable a feature, the principle of least privilege comes into play, and you have to consider carefully the minimal permissions that feature will need.
When using AWS, "Users" are for humans, "Roles" are for machines, and when you can afford to take the time, only grant each what they need to get their jobs done.
Good news. Investors liked bird.ai's Minimum Viable Product (MVP). You were able to raise a seed round and now are on the hunt for Product-market fit (PMF).
You've set up a few customer calls where you interview and observe people when they use your application. During these sessions you've noticed that your heaviest users will upload the same bird photo to the website multiple times. "Sometimes I forget if a bird was detected or not" one said when asked. Another mentioned they keep the website looping on an old phone for entertainment, checking if a bird has been outside their office window in the last 5 minutes.
When reflecting on your customer calls, you realize that both customers shared a negative user experience. They didn't describe it in negative terms, but Customer 1 has to snap a new picture to remember the detection result, and Customer 2 can only look at his most recent picture to entertain him at work. Neither of them can see their past bird detections. Both these usage patterns increase load on your application servers and frustrate customers. As a product-first cloud-native SaaS startup, that means this common use case is both raising your cloud costs and reducing your customer satisfaction.
Complete the following "Feature" 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
Last assignment we had to perform a long series of steps to get our application up and running on the EC2 instance. A lot of manual steps increases the risk of human error (that means you, the human, making a mistake). How can we define the steps needed to package and run an application in a clear way to make our life easier? That's where a Dockerfile comes in. A Dockerfile is a text file in a standard format that holds all the command line instructions needed for building and running an application.
Set up an EC2 instance called "vm-ca2". Remember how? If you don't, refer back to Cloud Assignment 1. The instance should be a t4g.small or t3.small. It will be quicker to build and deploy a container if you target the same architecture as your personal computer. Your EC2 instance must be running Amazon Linux with 30GB of disk storage. You'll have to add a key pair to connect to the instance using SSH.
Add a 2GB to 4GB swapfile to vm-ca2. Where you add the swapfile matters, make sure you create the swapfile on the 30GB storage volume you
attached to the EC2 instance. Run df -h to show your file system mounts, you
should see an NVMe SSD mounted at root (/). If you need help setting up a
swapfile, refer back to Cloud Assignment 1. If you get an "out of memory" error
when trying to load the docker image, try reducing the swapfile size to make
more room on instance, this may happen to images built for the platform
linux/amd64 and deployed on a t3.small.
Add the following public key to the authorized_keys file.
$ cat id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAORDoNEnPakX83lU2uUDnMWQm4L4ytchU3Mi80Uw+H7Install Docker and start the Docker daemon.
$ sudo dnf update -y
$ sudo dnf install -y docker
$ sudo systemctl start docker
$ sudo usermod -a -G docker ec2-user # then start a new SSH sessionYou're going to create a Dockerfile for the bird.ai application. Let's review the steps we took to get bird.ai up and running.
python manage.py collectstatic)python manage.py migrate)python manage.py runserver 0.0.0.0:8000)For your Dockerfile, use an official Python base image that has Python 3.14.2. I would use the "slim" distribution of Python for the base image. Slim is based on Debian and ships glibc, a highly used C standard library. The machine learning (ML) libraries in our application distribute binaries pre-built for glibc. Another common base image for Python, alpine, uses musl. Musl is 10x smaller than glibc, but less commonly used, so it may force you to re-compile certain system dependencies during the docker build, causing it to take longer. There are more differences, feel free to explore on your own!
You won't need to use a virtual environment in the Docker container, you can
pip install directly into the system. The container is already an isolated environment.
You'll want to install libgl1, libglib2.0-0, and git using apt-get install. Our ML dependencies will need them.
Tag the image with "bird.ai:latest". You'll have to build the image so it targets the ARM platform, that's the architecture of our EC2 instance. You can test your image is working by running:
$ docker build -t bird.ai:latest --platform linux/arm64 .
$ docker run --rm -p 8000:8000 bird.ai:latest # "--rm" cleans up the containerAnd visiting http://localhost:8000 in a web browser.
When you run docker build ., you set the build context for the docker
client to the current directory, ".". This is an important concept. The build
context is bundled into a tar archive by the client and sent to the docker daemon. The docker
daemon (called dockerd) then builds your container image according to the
instructions in the Dockerfile from only the files in the build context, it
doesn't have access to anything else on your filesystem.
If you are struggling to build your container image, make sure the required files are in the build context (including the Dockerfile) and any COPY paths are relative to the build context.
Docker has a lot of tools for virtual networking. By default,
all containers are placed into a Docker bridge network. This network connects
all containers running on the same host so they can communicate with each
other. It also blocks the containers from communicating with anything outside
the virtual bridge network, unless you publish a port using -p! The Docker
daemon then sets up port forwarding between the host and the container so you
can connect to your containerized web application from a web browser outside
the virtual network on your host machine. If you don't publish the port, your
web browser can't reach Docker's virtual network.
Once you get your container built and running, run the following commands to inspect its network settings.
$ cid="$(docker ps -q --filter ancestor=bird.ai:latest)"
$ net="$(docker inspect --format '{{json .NetworkSettings}}' $cid)"
$ echo "$net" | python3 -m json.toolRemember to run docker help if you have questions about a command!
Why do we run containers in these isolated, virtual networks? Well, we're wizards, Harry 🪄. We can't let muggles wander into Diagon Alley and see the magic.
Make sure to install Docker Desktop on your personal computer if you haven't already. If you need help writing a Dockerfile, review Chapter 6 Section 11 in the course textbook.
Once your Docker container is working, we're going to deploy it to EC2 server vm-ca2.
First, create a tar archive of the final image, then copy it over to your EC2 instance.
$ docker save -o bird.ai.tar bird.ai:latest
$ scp -i keypair.pem bird.ai.tar ec2-user@${INSTANCE_PUBLIC_IP}:~SSH into vm-ca2 then use the following commands to run your container image. When you submit, the autograder will check that a container is running and your application is accessible over the internet, so make sure to update the security group to allow inbound TCP traffic to port 8000 (if you don't remember how, refer to Cloud Assignment 1).
$ ssh -i keypair.pem ec2-user@${INSTANCE_PUBLIC_IP}
$ docker load -i bird.ai.tar
$ docker run --rm -d -p 8000:8000 bird.ai:latest # "-d" runs it in backgroundYou now have a containerized application running on your web server!
Was that easier than in Cloud Assignment 1? Share what you think on Ed 😄
Our business model depends on our users being able to access our website. If something happens and our servers go down, we're going to need to reboot and come back online fast. Our SaaS business loses money for every second of downtime.
But is our EC2 instance ready to reboot? Have we configured the virtual machine so that it can bring up our application without manual intervention?
Let's perform an experiment. We're going to let the autograder reboot our EC2 instance and rerun the previous tests so we can see which test fails. This will tell us what part of our application architecture will fail when a server reboots.
This is called chaos engineering! Netflix uses it to test their automatic recovery mechanisms on AWS. Their implementation is called Chaos Monkey, read about it on their blog.
First, SSH onto vm-ca2 and create a file in the home directory that will indicate to the autograder we want it to run the reboot test.
$ ssh -i keypair.pem ec2-user@${INSTANCE_PUBLIC_IP}
$ touch .reboot-ready # creates an empty file in the home directorySubmit your credentials to the autograder on Gradescope. Wait to see what happens!
Uh oh, now some tests are failing. Let's make our application deployment robust.
First, we'll make sure our swap file is enabled at boot time. SSH into vm-ca2
and open up the file /etc/fstab with your preferred text editor. Add your swapfile
at the end:
#
UUID=c1486bb3-de8e-4c78-a3ae-f5d5cbfb8912 / xfs defaults,noatime 1 1
UUID=6B1D-8531 /boot/efi vfat defaults,noatime,uid=0,gid=0,umask=0077,shortname=winnt,x-systemd.automount 0 2
+ /swapfile swap swap defaults 0 0Test your change by running the following command and verifying there are no errors
$ sudo swapon -a/etc/fstab is the "file system table". Redhat has a nice introduction to it, and you can also learn more on Wikipedia.
Now that swap is taken care of, it's time to start the Docker daemon so it runs at boot.
$ sudo systemctl enable dockerIt's that easy. systemctl is a command that controls systemd. Systemd is
a centralized initialization system for Linux. It performs a lot of
responsibilites, and we just informed it to enable the "docker" service at
boot. So what's a service? A service is just a file that systemd reads to
understand how to run a particular program. Doesn't this sound familiar? We're
tip-toeing around Dockerfile territory. They're both declarative
specifications. Where a Dockerfile describes how to build a piece of software, a
systemd service defines how that software should be run by an operating
system.
There are a bunch of software services systemd manages (for example, it's
responsible for starting the SSH daemon on your EC2 instance). You can think of
it as a platform for managing and orchestrating software on a single Linux
server. Take at look at all the active services with the command systemctl list-units and run systemctl cat on one of them to see the service
definition!
Let's take a look at the service definition file for docker.
$ systemctl cat docker
# /usr/lib/systemd/system/docker.service
[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target docker.socket firewalld.service containerd.service time-set.target
Wants=network-online.target containerd.service
Requires=docker.socket
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
EnvironmentFile=-/etc/sysconfig/docker
EnvironmentFile=-/etc/sysconfig/docker-storage
EnvironmentFile=-/run/docker/runtimes.env
ExecStartPre=/bin/mkdir -p /run/docker
ExecStartPre=/usr/libexec/docker/docker-setup-runtimes.sh
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock $OPTIONS $DOCKER_STORAG>
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=infinity
TimeoutStartSec=0
RestartSec=2
Restart=always
...The service definition contains sequencing rules during boot and includes
instructions for how to start and monitor the process. Here, for example, it
tells systemd that it should be started after the network is online and the
containerd service has started, and that it requires a Unix domain socket
file docker.socket to be available before starting.
Everyone time you run a Docker command, you are asking the Docker daemon to do
something on your behalf. When you docker run or docker build, those CLI
commands trigger HTTP requests whose destination is docker.socket. The Docker
daemon is listening to that socket, just like a web server listens to a TCP
socket to accept internet requests. But how do you know what those HTTP requests
need to look like? The Docker Engine has a REST API. In fact, if you'd like,
you never need to run a Docker CLI command, you can do everything and more
through the REST API (or the Python SDK!).
You might have a question, "If I'm running commands on a single server, why does the Docker daemon run on HTTP requests? Isn't that for websites?", and you would be right! The advantage is now you can control the Docker daemon from another server over the internet. By treating the Docker daemon like a web server, it can now be part of a larger distributed system. And because HTTP is the de-facto standard for internet applications, it speaks that language so it integrates easily into the larger internet ecosystem.
We're going to make a systemd service definition for our bird.ai application container, and instruct systemd to start it at boot.
On the EC2 instance, create a file /etc/systemd/system/bird-ai.service.
[Unit]
Description=bird.ai container
After=docker.service
Requires=docker.service
[Service]
Restart=always
# Clean up existing container
ExecStartPre=-/usr/bin/docker stop %n
ExecStartPre=-/usr/bin/docker rm %n
# The main command to start the container
# --rm: Clean up the container when it exits
# --name %n: Uses the service name (bird-ai) as the container name
ExecStart=/usr/bin/docker run --rm --name %n -p 8000:8000 bird.ai:latest
ExecStop=/usr/bin/docker stop %n
[Install]
WantedBy=multi-user.targetNow run:
$ sudo systemctl daemon-reload # force systemd to find new service definition
$ sudo systemctl enable --now bird-ai.service # enable the service at boot
$ sudo systemctl status bird-ai.service # check the status
$ sudo journalctl -u bird-ai.service -f # view logsWe've taken big steps towards creating a robust application deployment. Why don't you submit to autograder now and see what tests are passing?
A single test will still fail! When running tests, the autograder creates a user in your bird.ai application using the new "create account" page. That user has been forgotten after the reboot because fresh containers are stateless. Whenever you run a new container from the same image it gets recreated from scratch. That means the database Django uses is in the same state it was when you built the image (empty!).
The next piece to fix now is the database. We're going to create a docker volume that persists the database between reboots so our users can still log in after we recover from a server crash or re-deploy an updated application.
$ docker volume create bird-ai-dataWe've just created a named volume. Now, you're going to update the systemd service definition for the bird.ai application so when it runs the container, it attaches the volume into the container filesystem at the path the application expects the database to be.
[Unit]
Description=bird.ai container
After=docker.service
Requires=docker.service
[Service]
Restart=always
# Clean up existing container (ignore errors with '-')
ExecStartPre=-/usr/bin/docker stop %n
ExecStartPre=-/usr/bin/docker rm %n
# The main command to start the container
# --rm: Clean up the container when it exits
# --name %n: Uses the service name (bird-ai) as the container name
- ExecStart=/usr/bin/docker run --rm --name %n -p 8000:8000 bird.ai:latest
+ ExecStart=/usr/bin/docker run --rm --name %n -p 8000:8000 -v bird-ai-data:/app/data bird.ai:latest
ExecStop=/usr/bin/docker stop %n
[Install]
WantedBy=multi-user.targetDjango applications rely on a settings.py file that,
among other things, configures the database for the
application. Right now, we have it configured to use SQLite and to store the
database file in the directory /app/data/.
Even though the container is recreated every time, the volume won't be. New containers will re-use it, which means our SQLite database has gone from stateless to stateful across container lifetimes. Restart the application with our new volume attached.
$ sudo systemctl daemon-reload
$ sudo systemctl restart bird-ai.service
$ sudo systemctl status bird-ai.serviceAnd submit to the autograder.
The database is no longer in the container, so where is it? It's not someplace magical, docker just stashed it away in the filesystem. Let's inspect where it is.
$ docker volume inspect bird-ai-data
[
{
"CreatedAt": "2026-02-10T07:17:55Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/bird-ai-data/_data",
"Name": "bird-ai-data",
"Options": null,
"Scope": "local"
}
]
$ sudo ls /var/lib/docker/volumes/bird-ai-data/_data
db.sqlite3We found it! Docker stores all its volumes in /var/lib/docker/volumes/.
You are now done with Part 1. Because of our work to handle instance reboots, if you want to take a break, you can stop your instance. Select your instance in AWS Console and click "Instance state > Stop instance". This will save you money as you'll only be paying for a storage volume and not CPU time, as you wait to work on the assignment again. When you're ready to tackle Part 2, start your instance, and if you submit again to autograder you'll still have 50/100 points.
Now that we have a robust application deployment, we can start iterating on our application feature. This is the DevOps methodology: tightening the loop between design, development, testing, then release. Why are we developing software in the first place? Well, it's to make our customers' lives easier and give them capabilities they didn't have before, whether those customers are other engineers or non-technical consumers in the marketplace. Welcome to the wonderful world of tech entrepreneurship! We don't know if our application adequately addresses customer needs if we don't put it in front of them and receive feedback as they use it. Does a falling tree in an empty forest make a sound? Is Justin Timberlake still famous if no one listened to his last album? Do we even exist if no one has used the software we've written? Yes of course we do, but you get the idea.
For our development process, we work on our application with Python and Docker. We can test our application releases by submitting to the autograder. And we can deploy relatively quickly by creating a container image, copying it to the EC2 instance, loading it into the Docker daemon, then restarting the service using systemd. Customers will be able to use the newly released application by visiting our EC2 instance's public IP at port 8000. Now we can focus on the design for our latest feature, and push it out to customers using the DevOps flow we just discussed.
Returning to what we learned from our customer calls, let's design a history page for our web application.
When a user submits a photo, that photo and the detection result will now be saved to disk, and associated with the user. You will be implementing the Django model logic that makes that association.
Remember discussing the Model-View-Controller (MVC) design pattern for web applications in Cloud Assignment 1? A Model is the definitive source of information about your application's data. It's a representation of the data you store and the relationships between that data. Django relies on a relational database model. Django transforms your model definition into a database migration that modifies a database schema. This is called Object-relational mapping (ORM). The following dropdown shows what happens when you use a model to query the database and return a set of results.
sequenceDiagram autonumber participant App as Python Runtime<br/>(Views/Logic) participant ORM as Django ORM<br/>(Manager & Compiler) participant DB as SQL Database<br/>(PostgreSQL/MySQL) Note over App, DB: Phase 1: The Request (Python to SQL) App->>ORM: User.objects.filter(active=True) Note right of App: Python Method Call ORM->>ORM: Construct QuerySet ORM->>ORM: SQLCompiler.as_sql() Note right of ORM: Translates Python syntax<br/>to SQL string dialect ORM->>DB: SELECT * FROM app_user WHERE active = 1; Note right of ORM: Transmits raw SQL over socket Note over App, DB: Phase 2: The Response (SQL to Python) DB->>DB: Execute Query Plan DB-->>ORM: Returns Raw Rows (Tuples) Note left of DB: e.g., (1, "Connor", 1) ORM->>ORM: Model.from_db() Note right of ORM: "Hydration"<br/>Maps columns to attributes ORM-->>App: Returns QuerySet [User(id=1), ...] Note left of ORM: Iterable Python Objects
(click to open)
You will be implementing a Python class "Detection". Refer to Django's tutorial section "Creating models" for what to look for.
Your model will have have seven fields:
Write your model definition into bird.ai/app/models.py, there is a stub
class waiting to be implemented. You can see how it's used in
bird.ai/app/views.py, which may help you implement it.
Run your application locally to test if your model works.
$ source .venv/bin/activate
(.venv) $ python manage.py runserverWhen the history page is working, deploy it to your production server vm-ca2.
If you are rebuilding your container image and deploying to production many times, old images might be piling up on your local machine and EC2 instance. If you find yourself running out of memory, make sure to run:
$ docker system prune
$ ssh -i keypair.pem ec2-user@$INSTANCE_PUBLIC_IP "docker system prune -f"Remember to makemigrations when you create or update your Django models!
$ python manage.py makemigrations
$ python manage.py migrateIf something goes horribly wrong, you can delete the database file and the
migrations folder and re-run the commands (if the application is running in the
container, you'll have to stop the container and delete the volume
bird-ai-data). Definitely don't do this at an internship, but it works well for this assignment.
Now that we are storing images on our instance's disk storage instead of deleting them, what does that mean for our disk usage? Have you already encountered disk usage issues in the Cloud Assignments? Will this feature implementation improve or worsen those issues? Can you predict how the design decisions for this feature will affect how we operate this application in production on our EC2 instance?
Exploring this trade-off, and having you implement production-ready solutions, is the subject of the rest of the assignment.
Now that we're storing files to disk storage, we risk running out of disk space. We started with 30GB. Let's say after our instance initialization and container deployment we have 20GB left. Mobile phones can take pictures that are 4MB in size. Doing the math: 20GB / 4MB = only 5000 photos for all our customers! That's not enough for all the bird selfies.
To solve this problem, you are going to implement data de-duplication based on a content hash. When the application recieves an image upload, it will hash the file and compare that hash against our database of past detections. If it finds a match, instead of saving the photo and running the YOLO model on it, it will delete the file, skip the inference run, and serve the old file and old inference result instead, saving time, space, and money.
Data de-duplication ("chunk sharing") is how Amazon Lambda scales their container service. They have over a million customers uploading container images as Lambda functions. On average, only 1% of a container is unique. Most of its data comes from one of a small number of base images (alpine, ubuntu) and contains popular dependencies, which AWS de-duplicates on the block-level.
Open up bird.ai/app/models.py again. You're going to add another field to your "Detection" model.
You'll then implement a class method called "find_duplicate". Here's the type hint.
@classmethod
def find_duplicate(cls, image_bytes: bytes) -> tuple[str, "Detection | None"]:
Your method will calculate a hash based on image_bytes, then .filter the
Detection model to find a matching detection. If it doesn't find one, it will
return None, and the controller will treat it like a new image. Take a look
at bird.ai/app/views.py to see how "find_duplicate" is used.
Python's standard library contains a bunch of useful functions you should use in your software. One of them, hashlib, could be very useful!
Remember, try to develop this locally, and when you're happy with the result, send it off to production and submit your credentials to the autograder.
We've made some progress reducing disk usage on our servers, but can we take it further? Instead of trying to pack as many images as we can into a small space, can we try to pack as few as possible into an infinite space? Introducing S3, Amazon's Simple Storage Service.
AWS S3 is what's called object storage. Think of it like a secure safe, where you can be confident that whatever you put into it you can get out without data corruption. It's storage capabilities are essentially infinite, your wallet will run out before its storage space does. When you have a huge reliable place to store your data, suddenly more things are possible.
In S3, your data is organized into "buckets". Buckets map a key (a filepath) to an object (a file). Those buckets can be assigned different permissions, and by default are private (and most of the time you should keep them that way). Check out the Pragmatic Engineer on How AWS S3 is built.
You are going to be creating an S3 bucket to upload images to.
Navigate to Cloudfront in the AWS Console.
Now navigate to IAM in the AWS Console.
We've just created a minimal set of permissions that will enable a service to put files into our bucket, and get files out of the bucket, nothing else (Remember the principle of least privilege?). We're going to assign the permission set to a specific role, and then you'll see how our application will use that role in our instance, securely.
Navigate to IAM in the AWS Console.
Now navigate to EC2 in the AWS Console.
You just assigned an IAM role to an EC2 instance. Now, when software running on your instance creates an AWS SDK session, by default it will assume the IAM role and gain its permissions. What that means is (1) you don't have to package AWS credentials with your software, it's already set on the running instance, and (2) AWS manages the security of your credentials, which is one less thing for you to worry about.
We just gave permission for the EC2 instance to GET and PUT to the S3 bucket, but what about our local application? If you want to develop locally, you will have to make an IAM User for yourself. Let's do that quickly and securely.
You're going to create another IAM policy. Click "Create Policy". Select the
"S3" service and add the same permissions as for the policy
"get-and-put-bkt-ca2". Now click the "Request conditions" dropdown. Check
"requested from IP" and enter your IP address (find your IP by typing curl -s http://checkip.amazonaws.com into the terminal or visiting the URL in a
browser). Click "Add Ip" then "Next". Fill in "developers-bird.ai" for the
policy name.
Navigate to IAM in the AWS Console and create a user for yourself. Attach the policy "developers-bird.ai" to them. Click "Next" then "Create user". You can now create an access key for that user. Store the access key id and secret access key in a dotenv file.
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...You will find dotenv files used in a lot of different software applications. It's a common approach to implement the "Config" of a Twelve-Factor App, which is a set of design principles for building Software-as-a-Service applications.
To get started developing locally you have a few options , but the easiest is just to put the values into your local shell environment and then run the Django application.
$ set -a; source .env; set +a # "-a" is "allexport"
$ python manage.py runserverNow that all our infrastructure is in place, you'll be integrating with the Content Delivery Network (CDN) called AWS Cloudfront. Companies use CDNs to reduce latency and costs. S3 has to make a lot of trade-offs in order to provide you with durable storage for any type of object. Cloudfront just serves files, and fast. To put objects into, or get objects out of S3, you have 100GB of free bandwidth. Cloudfront will give you 1TB for free, and not charge you for pulling data from S3 origin servers. Lastly, S3 buckets are tied to a specific AWS region. Cloudfront's servers are a combination of regional edge caches, points of presence, and collaborations with Internet Service Providers (ISP), connected through a backbone network. That means they're closer to you, and closer means faster in computer networks.
You will implement a class method called "upload_to_s3". Here's the type hint.
@classmethod
def upload_to_s3(cls, image_bytes: bytes, object_key: str) -> None:
Take a look at the documentation for boto3, it's the Python SDK for the AWS
API. You will import it in that function and create an "s3" client with it. You
will then put those bytes in our bucket we created at the key
specified by object_key. If you have any questions, look at
bird.ai/app/views.py to see how "upload_to_s3" is used. Remember to set the
ContentType to "image/jpeg".
You must open up bird.ai/core/settings.py and uncomment the following lines.
Replace <your-distribution-name> with the domain name from your CloudFront
distribution (visible on the CloudFront distributions page in the AWS Console).
- # S3_BUCKET_NAME = '<your-bucket-name>'
- # MEDIA_URL = 'https://<your-distribution-name>.cloudfront.net/'
+ S3_BUCKET_NAME = '<your-bucket-name>'
+ MEDIA_URL = 'https://<your-distribution-name>.cloudfront.net/'Re-deploy your solution and you've completed the assignment!
You've just implemented a feature that professional software engineers are often asked to implement at companies! Now you have something to talk about in interviews!
Did you learn something new? Have an idea for an application you can develop and deploy on cloud infrastructure? Let us know on Ed!
Coming soon!
Make sure that you remove all resources you created after obtaining a full score on Gradescope. Pay particular attention to the fact that the free tier includes running instances. After you stop the instance, you are no longer charged usage or data transfer fees for it. However, you will still be billed for associated Elastic IP addresses and EBS volumes. Make sure to terminate (delete) any instances instead of stopping.
The resources we created this assignment are