Build a Cloud Storage App Locally
Use AWS CLI and Floci to run S3, DynamoDB, and SQS on your laptop.
Introduction
30 Second Summary
Cloud services like AWS power most of the internet. Learning them usually requires a credit card and the fear of accidentally racking up a bill.
In this project, you will build a local cloud storage and messaging system using three core AWS services, all running on your laptop with zero cloud costs.
What You'll Build
You will create an S3 bucket to store files, a DynamoDB table to manage product data, and an SQS queue to send and receive messages.
By the end of this project, you'll have:
- A running Floci container emulating 45 AWS services locally on your machine.
- An S3 bucket with uploaded files you can list and download.
- A DynamoDB table with product items you can query and filter.
- An SQS queue where you send and receive messages between services.
- A shell script that ties all three services together in a mini workflow.
- Secret Mission: Build an event-driven pipeline where uploading a file to S3 triggers a message to SQS and logs metadata to DynamoDB.
Are there any prerequisites?
None. This guide walks you through installing every tool you need, including Docker and the AWS CLI. You do not need an AWS account or a credit card.
Install Docker and AWS CLI
Before you can run AWS services locally, you need two tools installed on your machine.
Docker runs Floci (the local AWS emulator) inside a container. The AWS CLI is the command-line tool you will use to interact with all AWS services. In this step, you will install both and confirm they are working.
In this step, get ready to:
- Install Docker Desktop (or Docker Engine on Linux) and verify it is running.
- Install AWS CLI v2 using the official installer for your operating system.
- Verify both installations from your terminal.
Install Docker Desktop
Docker lets you run applications inside isolated containers. Floci (the AWS emulator you will start in the next step) ships as a Docker container, so Docker must be running first.
- Open your browser and go to the Docker Desktop download page.
- Download the installer for your operating system (macOS, Windows, or Linux).
- Run the installer and follow the on-screen prompts to complete the installation.
What will the installer ask?
On macOS, you will drag the Docker icon into your Applications folder. On Windows, the installer asks you to choose an installation mode. Pick the default (per-user) option and follow the prompts.
On Linux, you can install Docker Engine directly from your distribution's package manager. Docker Desktop is also available for Ubuntu, Debian, RHEL, and Fedora.
- After installation, open Docker Desktop. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Docker and press Enter to launch Docker Desktop.
- Wait for Docker Desktop to fully start. You will see the Docker whale icon appear in your system tray (macOS menu bar or Windows taskbar).
Now verify Docker is running. Open your terminal and check the Docker version by running this command:
docker --version
✔️ I see a version number
Docker is installed and running. You are ready to move on to installing the AWS CLI.
ⓧ Command not found
Docker is not installed or your terminal cannot find it. Try the following:
- Make sure you completed the Docker Desktop installer.
- Close and reopen your terminal. New installations sometimes need a fresh terminal session to register.
- Confirm Docker Desktop is running (look for the whale icon in your system tray).
If Docker Desktop is open but the command still fails, restart your computer and try again.
Install AWS CLI v2
The AWS CLI is the official command-line tool for managing AWS services. You will use it to create buckets, tables, and queues throughout this project. Version 2 supports a global endpoint override, which is what lets you point all commands at Floci instead of real AWS.
macOS
- Download the official macOS installer from this link.
- Double-click the downloaded AWSCLIV2.pkg file.
- Follow the installer prompts and accept the defaults.
Windows
- Download the official Windows installer from this link.
- Double-click the downloaded AWSCLIV2.msi file.
- Follow the installer prompts and accept the defaults.
Linux
- Install the AWS CLI by running these commands in your terminal:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && unzip awscliv2.zip && sudo ./aws/install
After the installer finishes, close and reopen your terminal. Then check the AWS CLI version by running this command:
aws --version
✔️ I see aws-cli/2.x.x
AWS CLI v2 is installed and ready. You are all set for the next step.
ⓧ I see aws-cli/1.x.x
You have an older version of the AWS CLI installed. Version 2 is required because it supports the AWS_ENDPOINT_URL environment variable that lets you redirect all commands to Floci.
- Uninstall the old version first (check the AWS docs for your platform's uninstall steps).
- Then install version 2 using the platform-specific instructions in the tabs above.
- Close and reopen your terminal, then run aws --version again to confirm it shows version 2.
ⓧ Command not found
The AWS CLI is not installed or your terminal cannot find it. Try the following:
- Make sure the installer completed without errors.
- Close and reopen your terminal. The installer adds the aws command to your PATH, but an open terminal session may not pick it up automatically.
- If it still fails after reopening, re-download and re-run the installer from the platform tabs above.
Why AWS CLI v2 specifically?
AWS CLI v2 introduced the AWS_ENDPOINT_URL environment variable, which redirects all AWS requests to a custom endpoint. This is what lets you point the CLI at Floci running on localhost:4566 instead of real AWS servers.
Both tools are ready. Next up, you will start Floci inside Docker and configure the AWS CLI to talk to it.
Start Floci and Configure AWS CLI
Docker and the AWS CLI are installed and ready to go. Now it's time to put them to work by launching your local AWS environment.
Floci is a free, open-source AWS emulator that runs inside a Docker container. It gives you 45 AWS services on your machine, all accessible through the same AWS CLI commands used in production.
In this step, get ready to:
- Start a Floci container emulating 45 AWS services locally.
- Configure the AWS CLI to send requests to Floci instead of real AWS.
- Verify the connection works with a test command.
Create a docker-compose file and start Floci
Floci runs as a Docker container defined in a docker-compose.yml file. You need a project folder to hold this file and the scripts you will write throughout this project.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir cloud-storage-app
cd cloud-storage-app
- Confirm the folder exists by running:
ls
You should see the cloud-storage-app folder listed in the output.
- Create a file called docker-compose.yml inside your cloud-storage-app folder and paste this content:
services:
floci:
image: floci/floci:latest
ports:
- "4566:4566"
volumes:
- ./data:/app/data
What does this file do?
- image: floci/floci:latest tells Docker to pull the official Floci image.
- ports: "4566:4566" maps Floci's port to your machine so the AWS CLI can reach it at localhost:4566.
- volumes: ./data:/app/data persists Floci's data to a local folder so your resources survive container restarts.
- Save docker-compose.yml.
Not sure how to create the file?
You can use any text editor. Open your cloud-storage-app folder in VS Code, Cursor, or any editor you prefer, then create a new file named docker-compose.yml.
help me create a docker-compose.yml file in my project folder
- Start Floci in the background by running this command:
docker compose up -d
The first time you run this, Docker downloads the Floci image (about 90 MB). This takes a minute or two depending on your internet speed.
- Confirm Floci is running by checking the container status:
docker compose ps
You should see a container with the status Up or Running.
Container not starting?
- Make sure Docker Desktop is running. You can check by looking for the Docker icon in your system tray or menu bar.
- Check that you are inside the cloud-storage-app folder where docker-compose.yml lives. Run ls and confirm you see the file listed.
- If you see a port conflict error, another service is using port 4566. Stop that service or change the port in your compose file.
help me troubleshoot why my Floci container is not starting
✔️ Awesome, I've got everything!
Great. Make sure you have saved docker-compose.yml and that docker compose ps shows the container running.
ⓧ I'd like to double check the full code
services:
floci:
image: floci/floci:latest
ports:
- "4566:4566"
volumes:
- ./data:/app/data
Configure AWS CLI to use Floci
The AWS CLI normally sends requests to real AWS servers. To redirect it to your local Floci instance, you set four environment variables that tell the CLI where to connect and what credentials to use.
Why environment variables instead of aws configure?
The AWS_ENDPOINT_URL variable is a global override supported by AWS CLI v2. It redirects every AWS command to Floci without needing --endpoint-url on each command.
Floci accepts any dummy credentials. You do not need a real AWS account.
- Create a file called setup-env.sh in your cloud-storage-app folder and paste this content:
#!/bin/bash
# Set environment variables for AWS CLI to point at Floci
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_DEFAULT_REGION=us-east-1
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
echo "AWS CLI configured to use Floci at $AWS_ENDPOINT_URL"
echo "Region: $AWS_DEFAULT_REGION"
What does this script do?
- AWS_ENDPOINT_URL points the CLI at Floci running on port 4566 instead of real AWS.
- AWS_DEFAULT_REGION sets a region. Floci does not enforce regions, but the CLI requires one.
- AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are dummy credentials. Floci accepts any value here.
- The echo lines confirm the variables were set when you run the script.
- Save setup-env.sh.
- Source the script to load the environment variables into your current terminal session:
source ./setup-env.sh
You should see this output:
AWS CLI configured to use Floci at http://localhost:4566
Region: us-east-1
Source vs run
Using source runs the script in your current shell session, so the exported variables stay available. Running the script directly (./setup-env.sh) would set variables in a child process that disappears when the script finishes.
Seeing 'No such file or directory'?
- Make sure you are in the cloud-storage-app folder. Run pwd to check your current directory.
- Confirm setup-env.sh exists by running ls. You should see both docker-compose.yml and setup-env.sh listed.
help me fix the 'No such file or directory' error when sourcing setup-env.sh
✔️ Awesome, I've got everything!
Great. Make sure you have saved setup-env.sh and that sourcing it printed the confirmation messages.
ⓧ I'd like to double check the full code
#!/bin/bash
# Set environment variables for AWS CLI to point at Floci
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_DEFAULT_REGION=us-east-1
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
echo "AWS CLI configured to use Floci at $AWS_ENDPOINT_URL"
echo "Region: $AWS_DEFAULT_REGION"
Verify the connection
Time to confirm the AWS CLI can talk to Floci. The aws s3 ls command lists all S3 buckets. Since you have not created any yet, an empty response with no errors means everything is connected.
- Test the connection by running this command:
aws s3 ls
You should see no output at all. The command returns to your prompt with no error messages.
What does a successful response look like?
The command returns nothing and exits cleanly. No error, no text. This means the CLI successfully connected to Floci and confirmed there are zero buckets.
If you see an error like Could not connect to the endpoint URL, your CLI is not reaching Floci.
Seeing a connection error?
- Confirm Floci is still running with docker compose ps. If it stopped, restart it with docker compose up -d.
- Make sure you sourced setup-env.sh in this same terminal session. If you opened a new terminal, you need to run source ./setup-env.sh again.
- Verify the endpoint variable is set by running echo $AWS_ENDPOINT_URL. It should print http://localhost:4566.
help me debug why aws s3 ls is not connecting to Floci
Your local AWS environment is live. Next up, you will create your first S3 bucket and upload files to it.
Create an S3 Bucket and Upload Files
Your AWS CLI is configured and talking to Floci. Every command you run from here uses the real AWS CLI syntax that works identically on production AWS.
S3 (Simple Storage Service) is Amazon's object storage. It stores files of any size in containers called buckets. In this step, you'll create a bucket, upload a file into it, and download it back to prove the full cycle works.
In this step, get ready to:
- Create an S3 bucket in your local Floci environment.
- Upload a sample text file to the bucket.
- List and download files from the bucket.
Create your S3 bucket
Before you can store any files, you need a bucket to put them in. Think of a bucket as a top-level folder in the cloud.
- Create a new S3 bucket called my-app-files by running this command:
aws s3 mb s3://my-app-files
What does this command do?
The aws s3 mb command stands for "make bucket." It creates a new storage container in S3.
The s3://my-app-files part is the bucket's address. Every S3 path starts with s3:// followed by the bucket name.
- You should see make_bucket: s3://my-app-files confirming the bucket was created.
- Verify your bucket exists by running:
aws s3 ls
- You should see my-app-files listed in the output.
Don't see your bucket?
- Make sure your environment variables are still set. If you opened a new terminal since the last step, re-run source ./setup-env.sh to point the AWS CLI at Floci again.
- Confirm Floci is still running with docker ps. You should see a floci/floci container listed.
- help me debug why my S3 bucket wasn't created
Upload a file to your bucket
Now that your bucket exists, you can store files in it. You'll create a sample text file and push it up to S3.
- Create a sample text file by running this command:
echo "Hello from my local cloud storage!" > sample.txt
- Confirm the file exists by running:
ls
- You should see sample.txt in the output.
- Upload sample.txt to your bucket by running:
aws s3 cp sample.txt s3://my-app-files/
What does this command do?
The aws s3 cp command copies files between your local machine and S3. The first argument is the source. The second argument is the destination.
When the destination starts with s3://, the CLI uploads. When the source starts with s3://, it downloads. Same command, both directions.
- You should see upload: ./sample.txt to s3://my-app-files/sample.txt confirming the upload succeeded.
Upload failed?
- Check that sample.txt exists in your current directory. Run ls to confirm.
- Double-check the bucket name matches exactly. Run aws s3 ls to see your available buckets.
- help me fix my S3 upload
List and download files from your bucket
Storing files is only useful if you can get them back. You'll now list your bucket contents and download the file to a new location.
- List the contents of your bucket by running:
aws s3 ls s3://my-app-files/
- You should see sample.txt listed along with its size and upload timestamp.
- Download the file to a new local path by running:
aws s3 cp s3://my-app-files/sample.txt downloaded-sample.txt
What's happening here?
This time the s3:// path is the source and the local filename is the destination. The CLI downloads the file from S3 to your machine.
Downloading to a different filename (downloaded-sample.txt instead of sample.txt) proves S3 actually stored and returned your data. It's not just reading the local copy.
- Verify the downloaded file contents by running:
cat downloaded-sample.txt
- You should see Hello from my local cloud storage! printed to your terminal.
File not downloading?
- Check the bucket path spelling. The bucket name and filename are case-sensitive.
- Run aws s3 ls s3://my-app-files/ again to confirm the file is actually in the bucket.
- help me download my file from S3
Your S3 bucket is live with files stored and retrieved. Next, you'll build a DynamoDB table to manage structured data like a product catalog.
Build a DynamoDB Product Table
In the previous step, you stored and retrieved files using S3. File storage is one piece of the puzzle, but most applications also need structured data.
DynamoDB is AWS's NoSQL database service. It stores data as items (rows) in tables, and each item is identified by a unique key. In this step, you will create a product catalog table, add items to it, and retrieve them using the same AWS CLI commands you would use on real AWS.
In this step, get ready to:
- Create a DynamoDB table with a partition key.
- Insert product items into the table.
- Query and scan the table to retrieve your data.
Create the DynamoDB table
Every DynamoDB table needs a primary key that uniquely identifies each item. You will create a table called products with a partition key named product_id.
- Create the table by running this command:
aws dynamodb create-table \
--table-name products \
--attribute-definitions AttributeName=product_id,AttributeType=S \
--key-schema AttributeName=product_id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
What does this command do?
This command creates a new DynamoDB table with these settings:
- --table-name products names your table products.
- --attribute-definitions declares the key attribute product_id as a String (S).
- --key-schema sets product_id as the HASH (partition) key. This is what DynamoDB uses to look up items.
- --billing-mode PAY_PER_REQUEST means you only pay for actual reads and writes. Since Floci is free, this has no cost impact here.
- You should see JSON output containing "TableStatus": "ACTIVE" confirming your table was created.
Don't see the table output?
- Make sure your environment variables are still set. Run source ./setup-env.sh to reload them.
- Check that Floci is still running with docker ps. If the container stopped, start it again with docker compose up -d.
Help me debug my DynamoDB create-table command
Insert product items
With the table created, you can now add items to it. Each item is a JSON object where every value includes a type descriptor (S for String, N for Number).
- Add your first product by running this command:
aws dynamodb put-item \
--table-name products \
--item '{"product_id": {"S": "1"}, "name": {"S": "Laptop"}, "price": {"N": "999"}, "category": {"S": "Electronics"}}'
- Add a second product by running:
aws dynamodb put-item \
--table-name products \
--item '{"product_id": {"S": "2"}, "name": {"S": "Coffee Mug"}, "price": {"N": "12"}, "category": {"S": "Kitchen"}}'
- Add a third product by running:
aws dynamodb put-item \
--table-name products \
--item '{"product_id": {"S": "3"}, "name": {"S": "Headphones"}, "price": {"N": "79"}, "category": {"S": "Electronics"}}'
What does put-item do?
Each aws dynamodb put-item command inserts a new item (row) into your table. The --item flag takes a JSON object where each attribute uses DynamoDB's type notation.
For example, {"S": "Laptop"} means the value is a String, and {"N": "999"} means the value is a Number. Numbers are passed as strings in the JSON but stored as numbers internally.
- Each command completes silently with no output. No output means the item was inserted successfully.
Seeing a JSON parsing error?
- Check that your JSON uses single quotes on the outside and double quotes on the inside. Mixing them up causes parse errors.
- Make sure you have no trailing commas inside the JSON object.
Help me fix my put-item JSON format
Query and scan the table
DynamoDB gives you two ways to read data. A get-item fetches one specific item by its key. A scan retrieves all items in the table.
- Retrieve a single product by its key by running this command:
aws dynamodb get-item \
--table-name products \
--key '{"product_id": {"S": "1"}}'
What does get-item do?
The --key flag tells DynamoDB exactly which item to return. You pass the partition key value (product_id of "1") and DynamoDB returns that single item with all its attributes.
- You should see JSON output showing the Laptop item with its product_id, name, price, and category attributes.
- Now retrieve all products in the table by running:
aws dynamodb scan --table-name products
- You should see all three products listed in the output, along with a "Count": 3 at the bottom confirming three items exist.
When to use get-item vs scan
Use get-item when you know the exact key of the item you want. It returns instantly because DynamoDB looks up the item directly by its partition key.
Use scan when you need all items or don't know the key. Scans read every item in the table, so they are slower on large tables. For a small product catalog like this, both are fast.
Get-item returning empty?
- Double-check that you passed the key value as {"S": "1"} not {"N": "1"}. The key type must match what you defined when creating the table (String, not Number).
- Run aws dynamodb scan --table-name products to confirm items exist. If the scan also returns empty, re-run the put-item commands from the previous substep.
Help me debug my DynamoDB get-item command
Your product catalog is live. Next up, you will send messages between services using SQS queues.
Send Messages with SQS
You've stored files in S3 and queried structured data in DynamoDB. But in real applications, these services need a way to communicate with each other.
SQS (Simple Queue Service) is AWS's messaging service. It lets one part of your system drop off a message that another part picks up later. In this step, you'll create a queue, send messages into it, and retrieve them.
In this step, get ready to:
- Create an SQS queue in your local Floci environment.
- Send messages to the queue using the AWS CLI.
- Receive and delete messages from the queue.
Create an SQS queue
A message queue is like a mailbox between services. One service drops a message in, and another service picks it up when it's ready. This decouples your services so they don't need to be running at the same time.
- Create a new SQS queue called file-notifications by running this command:
aws sqs create-queue --queue-name file-notifications
What does this command do?
aws sqs create-queue tells AWS to provision a new message queue. The --queue-name flag sets the queue's identifier. You'll use this name to look up the queue's URL later.
- You should see JSON output containing a QueueUrl field. This URL is the address you'll use to interact with the queue.
Not seeing a QueueUrl in the output?
Make sure your Floci container is still running. Run docker ps to confirm.
Check that your environment variables are still set by running echo $AWS_ENDPOINT_URL. If it's empty, re-run source ./setup-env.sh.
Still stuck? Why is my SQS create-queue command failing against Floci?
Send messages to the queue
To send a message, you need the queue's URL. Instead of copying it from the previous output, you can fetch it programmatically using the queue name.
- Store the queue URL in a shell variable by running this command:
QUEUE_URL=$(aws sqs get-queue-url --queue-name file-notifications --query 'QueueUrl' --output text)
What does this command do?
aws sqs get-queue-url looks up a queue by name and returns its full URL. The --query 'QueueUrl' --output text flags extract just the URL string (no JSON wrapping). Storing it in QUEUE_URL makes the next commands cleaner.
- Send your first message to the queue by running:
aws sqs send-message --queue-url $QUEUE_URL --message-body "Hello from SQS! This is my first message."
What does this command do?
aws sqs send-message places a message onto the queue. The --message-body flag contains the actual content. In production, this is often JSON with event data. Here you're using plain text to keep things simple.
- You should see JSON output with a MessageId and MD5OfMessageBody field. This confirms your message was accepted by the queue.
- Send a second message so you have multiple messages to work with:
aws sqs send-message --queue-url $QUEUE_URL --message-body "File uploaded: report.txt"
Getting a 'queue does not exist' error?
Run echo $QUEUE_URL to verify the variable is set. If it's empty, re-run the QUEUE_URL=$(aws sqs get-queue-url ...) command from above.
Help me troubleshoot my SQS send-message command.
Receive and delete messages
SQS doesn't push messages to you. Your application polls the queue and pulls messages when it's ready to process them. Once you finish processing, you delete the message so it doesn't get picked up again.
- Receive a message from your queue by running:
aws sqs receive-message --queue-url $QUEUE_URL
What does this output mean?
The output contains a Messages array. Each message has a Body (your message text), a MessageId, and a ReceiptHandle. The receipt handle is a temporary token that proves you received this specific message. You need it to delete the message.
- You should see your message body in the output along with a long ReceiptHandle string.
- Copy the entire ReceiptHandle value from your output. You'll need it for the next command.
Now that you've read the message, delete it from the queue to mark it as processed.
- Delete the message by running this command, replacing the receipt handle placeholder with the value you copied:
aws sqs delete-message --queue-url $QUEUE_URL --receipt-handle [[RECEIPT_HANDLE="your ReceiptHandle value from the receive output"]]
Why do I need to delete messages manually?
SQS keeps messages in the queue until you explicitly delete them. This guarantees that if your application crashes mid-processing, the message reappears for another attempt. Deleting confirms you handled it successfully.
- Verify the message is gone by receiving again:
aws sqs receive-message --queue-url $QUEUE_URL
- You should see your second message now (the one about report.txt). The first message is gone because you deleted it.
Seeing an empty response with no messages?
If you already deleted both messages, the queue is empty. Send another test message and try receiving again.
If you never deleted a message but still see nothing, wait a few seconds and retry. SQS has a short visibility timeout that temporarily hides messages after they've been received once.
Why am I not seeing messages in my SQS queue?
You now have three core AWS services running locally. S3 stores your files, DynamoDB manages your data, and SQS handles communication between services. Next up, you'll connect all three in a single workflow script.
Connect Everything with a Workflow Script
You have used S3, DynamoDB, and SQS individually. In real-world applications, these services rarely work in isolation.
In this step, you will write a shell script that ties all three services together into a single workflow. The script uploads a file, logs its metadata, and sends a notification. Then you will clean everything up with a dedicated cleanup script.
In this step, get ready to:
- Write a workflow script connecting S3, DynamoDB, and SQS.
- Run the script and verify data flows to each service.
- Clean up all resources with a cleanup script.
Write the workflow script
This script will automate a common cloud pattern. It creates a file, uploads it to S3, records metadata in DynamoDB, and sends a notification to SQS.
- Create a new file called workflow.sh in your project folder by running:
touch workflow.sh
- Open workflow.sh in your text editor and paste the following code for the script header, configuration, and S3 upload:
#!/bin/bash
# Cloud Storage Workflow: Upload file, log metadata, send notification
# Source environment variables
source ./setup-env.sh
# Configuration
BUCKET_NAME="my-app-files"
TABLE_NAME="file-metadata"
QUEUE_NAME="file-notifications"
FILE_NAME="report.txt"
# Step 1: Create the file to upload
echo "Monthly sales report - Generated on $(date)" > "$FILE_NAME"
echo "Created file: $FILE_NAME"
# Step 2: Upload to S3
aws s3 cp "$FILE_NAME" "s3://$BUCKET_NAME/$FILE_NAME"
echo "Uploaded $FILE_NAME to S3 bucket $BUCKET_NAME"
What does this code do?
source ./setup-env.sh loads the environment variables you created earlier so the AWS CLI points at Floci.
The configuration block defines all resource names in one place. If you ever rename a bucket or table, you only change one line. Steps 1 and 2 create a sample report file and upload it to your S3 bucket.
- Below the S3 upload section, paste the DynamoDB metadata logging code:
# Step 3: Get file size and log metadata to DynamoDB
FILE_SIZE=$(wc -c < "$FILE_NAME" | tr -d ' ')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
aws dynamodb put-item \
--table-name "$TABLE_NAME" \
--item "{
\"file_id\": {\"S\": \"$FILE_NAME\"},
\"file_size\": {\"N\": \"$FILE_SIZE\"},
\"uploaded_at\": {\"S\": \"$TIMESTAMP\"},
\"bucket\": {\"S\": \"$BUCKET_NAME\"}
}"
echo "Logged metadata to DynamoDB"
What does this code do?
wc -c counts the file size in bytes. tr -d ' ' trims any whitespace from the output.
The put-item command writes a record to DynamoDB with the filename as the key, plus the file size, upload timestamp, and bucket name as attributes. This is the same metadata pattern used in production file-processing pipelines.
- Below the DynamoDB section, paste the SQS notification and verification code:
# Step 4: Send notification to SQS
QUEUE_URL=$(aws sqs get-queue-url --queue-name "$QUEUE_NAME" --query 'QueueUrl' --output text)
aws sqs send-message \
--queue-url "$QUEUE_URL" \
--message-body "{\"event\": \"file_uploaded\", \"file\": \"$FILE_NAME\", \"bucket\": \"$BUCKET_NAME\", \"timestamp\": \"$TIMESTAMP\"}"
echo "Sent notification to SQS queue"
echo ""
echo "Workflow complete! Verifying..."
echo ""
# Verify S3
echo "=== S3 Bucket Contents ==="
aws s3 ls "s3://$BUCKET_NAME/"
# Verify DynamoDB
echo ""
echo "=== DynamoDB Record ==="
aws dynamodb get-item \
--table-name "$TABLE_NAME" \
--key "{\"file_id\": {\"S\": \"$FILE_NAME\"}}" \
--query 'Item' \
--output json
# Verify SQS
echo ""
echo "=== SQS Message ==="
aws sqs receive-message \
--queue-url "$QUEUE_URL" \
--query 'Messages[0].Body' \
--output text
What does this code do?
The script fetches the queue URL dynamically using get-queue-url. This is a best practice because queue URLs can change between environments.
The notification message is a JSON object with the event type, filename, bucket, and timestamp. After sending it, the verification section checks all three services to confirm data arrived correctly.
- Save workflow.sh.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
#!/bin/bash
# Cloud Storage Workflow: Upload file, log metadata, send notification
# Source environment variables
source ./setup-env.sh
# Configuration
BUCKET_NAME="my-app-files"
TABLE_NAME="file-metadata"
QUEUE_NAME="file-notifications"
FILE_NAME="report.txt"
# Step 1: Create the file to upload
echo "Monthly sales report - Generated on $(date)" > "$FILE_NAME"
echo "Created file: $FILE_NAME"
# Step 2: Upload to S3
aws s3 cp "$FILE_NAME" "s3://$BUCKET_NAME/$FILE_NAME"
echo "Uploaded $FILE_NAME to S3 bucket $BUCKET_NAME"
# Step 3: Get file size and log metadata to DynamoDB
FILE_SIZE=$(wc -c < "$FILE_NAME" | tr -d ' ')
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
aws dynamodb put-item \
--table-name "$TABLE_NAME" \
--item "{
\"file_id\": {\"S\": \"$FILE_NAME\"},
\"file_size\": {\"N\": \"$FILE_SIZE\"},
\"uploaded_at\": {\"S\": \"$TIMESTAMP\"},
\"bucket\": {\"S\": \"$BUCKET_NAME\"}
}"
echo "Logged metadata to DynamoDB"
# Step 4: Send notification to SQS
QUEUE_URL=$(aws sqs get-queue-url --queue-name "$QUEUE_NAME" --query 'QueueUrl' --output text)
aws sqs send-message \
--queue-url "$QUEUE_URL" \
--message-body "{\"event\": \"file_uploaded\", \"file\": \"$FILE_NAME\", \"bucket\": \"$BUCKET_NAME\", \"timestamp\": \"$TIMESTAMP\"}"
echo "Sent notification to SQS queue"
echo ""
echo "Workflow complete! Verifying..."
echo ""
# Verify S3
echo "=== S3 Bucket Contents ==="
aws s3 ls "s3://$BUCKET_NAME/"
# Verify DynamoDB
echo ""
echo "=== DynamoDB Record ==="
aws dynamodb get-item \
--table-name "$TABLE_NAME" \
--key "{\"file_id\": {\"S\": \"$FILE_NAME\"}}" \
--query 'Item' \
--output json
# Verify SQS
echo ""
echo "=== SQS Message ==="
aws sqs receive-message \
--queue-url "$QUEUE_URL" \
--query 'Messages[0].Body' \
--output text
Run the workflow and verify results
The workflow script writes metadata to a table called file-metadata. This is a new table dedicated to tracking uploaded files, separate from the products table you created earlier.
- Create the file-metadata DynamoDB table by running:
aws dynamodb create-table \
--table-name file-metadata \
--attribute-definitions AttributeName=file_id,AttributeType=S \
--key-schema AttributeName=file_id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
Why a separate table?
In production AWS applications, you typically create purpose-built tables rather than reusing a general one. The file-metadata table uses file_id as its partition key, which maps directly to the filename. This makes lookups fast and predictable.
- Make the workflow script executable by running:
chmod +x workflow.sh
- Run the workflow script:
./workflow.sh
You should see output for each step of the workflow, followed by verification output showing data in all three services.
What should I see?
The output shows each step completing in sequence. The S3 section lists report.txt in your bucket. The DynamoDB section shows a JSON object with file_id, file_size, uploaded_at, and bucket fields. The SQS section shows the notification message with the event details.
Seeing an error about the table?
Make sure you ran the aws dynamodb create-table command above before running the script. The file-metadata table must exist for put-item to succeed.
Also verify your environment variables are still set. Run source ./setup-env.sh again if you opened a new terminal window.
Help me debug my workflow script error
Clean up all resources
A cleanup script is essential for any cloud project. It ensures you can tear down everything in one command, whether locally or in production.
- Create a new file called cleanup.sh and paste the following code:
#!/bin/bash
# Clean up all AWS resources
source ./setup-env.sh
echo "Cleaning up resources..."
# Delete S3 bucket and contents
aws s3 rb s3://my-app-files --force 2>/dev/null
echo "Deleted S3 bucket"
# Delete DynamoDB table
aws dynamodb delete-table --table-name file-metadata 2>/dev/null
echo "Deleted DynamoDB table"
# Delete SQS queue
QUEUE_URL=$(aws sqs get-queue-url --queue-name file-notifications --query 'QueueUrl' --output text 2>/dev/null)
if [ -n "$QUEUE_URL" ]; then
aws sqs delete-queue --queue-url "$QUEUE_URL"
echo "Deleted SQS queue"
fi
# Stop Floci
docker compose down
echo "Stopped Floci"
echo "All resources cleaned up!"
What does this code do?
aws s3 rb --force removes the bucket and all objects inside it. aws dynamodb delete-table removes the DynamoDB table and its data.
For SQS, the script first fetches the queue URL, then deletes the queue only if it exists. The 2>/dev/null redirects error messages so cleanup continues even if a resource was already deleted. Finally, docker compose down stops the Floci container.
- Save cleanup.sh.
- Make the cleanup script executable and run it:
chmod +x cleanup.sh
./cleanup.sh
You should see confirmation messages for each deleted resource, ending with the Floci container stopping.
Seeing permission denied?
Make sure you ran chmod +x cleanup.sh before executing the script. Without the execute permission, your terminal cannot run the file directly.
Help me fix a permission error running my cleanup script
✔️ Awesome, I've got everything!
Great. Double check you have saved cleanup.sh before moving on.
ⓧ I'd like to double check the full code
#!/bin/bash
# Clean up all AWS resources
source ./setup-env.sh
echo "Cleaning up resources..."
# Delete S3 bucket and contents
aws s3 rb s3://my-app-files --force 2>/dev/null
echo "Deleted S3 bucket"
# Delete DynamoDB table
aws dynamodb delete-table --table-name file-metadata 2>/dev/null
echo "Deleted DynamoDB table"
# Delete SQS queue
QUEUE_URL=$(aws sqs get-queue-url --queue-name file-notifications --query 'QueueUrl' --output text 2>/dev/null)
if [ -n "$QUEUE_URL" ]; then
aws sqs delete-queue --queue-url "$QUEUE_URL"
echo "Deleted SQS queue"
fi
# Stop Floci
docker compose down
echo "Stopped Floci"
echo "All resources cleaned up!"
Your local AWS environment is now fully cleaned up. You have practiced the complete lifecycle of cloud resources, from creation through usage to teardown. Next up, take on the Secret Mission to build an event-driven pipeline that automates this entire workflow.
Secret mission
Build an Event-Driven Pipeline
Take your workflow to the next level by automating the entire pipeline. Instead of a single script doing everything sequentially, create a system where each step triggers the next automatically.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Floci Docker container (running on port 4566).
- Local project files (docker-compose.yml, setup-env.sh, workflow.sh, cleanup.sh, data/ directory).
- AWS CLI v2 (installed on your machine).
- Docker Desktop (installed on your machine).
Keep everything running
No action needed. Choose this if you are still actively building or plan to continue experimenting with AWS CLI commands against Floci.
- Your Floci container stays running in the background and uses minimal resources (about 13 MiB of memory).
- Your project files remain on your Desktop.
- AWS CLI and Docker Desktop stay installed and available for other projects.
Pause - I'll come back to this later
Shut down the Floci container to free up memory and port 4566. Your project files stay on disk so you can restart quickly later.
- Stop the Floci container by running this command from your project folder:
docker compose down
- To restart later, navigate back to your project folder and run:
docker compose up -d
Your local AWS resources (S3 buckets, DynamoDB tables, SQS queues) are stored in the data/ volume. They persist between restarts.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes the Floci container, all local AWS data, and your project files.
- Stop and remove the Floci container by running:
docker compose down
- Delete your project folder by running:
macOS
rm -rf ~/Desktop/cloud-storage-app
Linux users can run the same command above.
Windows
Remove-Item -Recurse -Force "$env:USERPROFILE\Desktop\cloud-storage-app"
- Remove the Floci Docker image by running:
docker rmi floci/floci:latest
If you also want to uninstall AWS CLI v2:
macOS
- Delete the CLI symlinks and install directory by running:
sudo rm /usr/local/bin/aws
sudo rm /usr/local/bin/aws_completer
sudo rm -rf /usr/local/aws-cli
Linux users can run the same commands above.
Windows
- Press the Windows key and search for Add or remove programs.
- Find AWS Command Line Interface in the list.
- Click Uninstall and confirm.
If you also want to uninstall Docker Desktop:
macOS
- Open Docker Desktop.
- Click the Troubleshoot icon in the top-right corner of the Dashboard.
- Select Uninstall.
- Confirm by clicking Uninstall again.
- Move the Docker app from your Applications folder to the Trash.
Windows
- Open Settings from the Start menu.
- Select Apps, then Apps & features.
- Find Docker Desktop in the list and click Uninstall.
- Confirm the uninstall.
That's a Wrap!
That's a Wrap!
Nice work! You just learned AWS without spending a penny.
Every single AWS CLI command you used in this project works identically on real AWS. When you are ready to move to the real cloud, the only thing that changes is removing the endpoint URL.
You've learned how to:
- Set up a local AWS emulator (Floci) running 45 services inside Docker, giving you a full cloud sandbox on your laptop.
- Created and managed S3 buckets for file storage, DynamoDB tables for structured data, and SQS queues for messaging.
- Connected multiple AWS services together in a workflow script that uploads files, logs metadata, and sends notifications.
- Secret Mission: Built an event-driven pipeline with dead-letter queue error handling.
Ready to quiz yourself?