5 min read

Building AWS Systems, Chapter 1: A Local AWS Lab

The first chapter in our Building AWS Systems series: create a reproducible local AWS lab with Proxmox, Ubuntu Server, Docker, LocalStack, Terraform, AWS CLI, awslocal, Python, and uv.

Building AWS Systems, Chapter 1: A Local AWS Lab

Chapter 1: Building a Local AWS Lab with Proxmox, Ubuntu, Docker, LocalStack, Terraform and Python

Learn cloud architecture by building real systems locally.

This series is not about memorizing AWS services. It is about rebuilding the engineering intuition required to design, deploy, debug and explain cloud-native systems.


Table of contents

  1. Why this series exists
  2. What we are building
  3. Why use a local AWS lab?
  4. The architecture of the lab
  5. Creating the virtual machine in Proxmox
  6. Installing Ubuntu Server 24.04
  7. First boot: update the system
  8. Installing the QEMU Guest Agent
  9. Installing Docker from the official Docker repository
  10. Running the first Docker container
  11. Installing the AWS tooling
  12. Installing Terraform
  13. Installing Python tooling with uv
  14. Installing helper tools
  15. Creating the project repository
  16. Running LocalStack with Docker Compose
  17. Understanding the LocalStack configuration
  18. Verifying the environment
  19. Troubleshooting notes from the real setup
  20. What we built
  21. Exercises
  22. Next chapter

1. Why this series exists

Many engineers use AWS professionally for years without creating the infrastructure from scratch very often.

That may sound strange at first, but it is completely normal.

In a real company, when you join a team, the infrastructure usually already exists. The Lambda functions are deployed. The SQS queues are connected. IAM roles and policies are already defined. Terraform modules have been maintained for years. The CI/CD pipelines are already running.

Your daily work may involve adding features, fixing bugs, improving observability, debugging production incidents, writing application code, or modifying existing infrastructure. All of that is valuable experience.

But after enough time, a different problem appears.

You know how to work inside a cloud system, but you may not feel sharp when asked to build the system again from zero.

This series exists to solve that problem.

The goal is not to pass a certification exam. The goal is to rebuild practical AWS muscle memory by creating real systems locally, one component at a time.

We will use:

  • Proxmox for virtualization
  • Ubuntu Server 24.04 as the Linux environment
  • Docker as the container runtime
  • Docker Compose to orchestrate local services
  • LocalStack to emulate AWS APIs locally
  • Terraform for infrastructure as code
  • Python for application code
  • Rust later, after the fundamentals are in place

By the end of the series, we want to be comfortable not only using AWS services, but also explaining why each service exists, how it behaves under failure, and how it fits into production architectures.


2. What we are building

In this chapter, we are building the foundation for the rest of the course: a local AWS laboratory.

This lab should be:

  • repeatable
  • isolated
  • cheap to run
  • easy to reset
  • close enough to real AWS to be useful for learning
  • simple enough to understand completely

The final result will look like this:

Proxmox host

    -> Ubuntu Server 24.04 VM

        -> Docker Engine

            -> LocalStack container

                -> AWS-compatible APIs on port 4566

        -> Terraform
        -> AWS CLI
        -> awslocal
        -> Python 3.12
        -> uv

Later chapters will build on this foundation.

For example:

Python producer

    -> SQS queue running in LocalStack

        -> Python consumer

Then:

API request

    -> Lambda

        -> SQS

            -> Lambda worker

                -> DynamoDB

And later:

EventBridge

    -> SQS

        -> Lambda

            -> PostgreSQL
            -> Redis
            -> CloudWatch-style logs

This first chapter is less glamorous than those future systems, but it matters. A weak environment makes every future lesson harder. A strong environment lets us focus on learning AWS instead of debugging random machine setup issues.


3. Why use a local AWS lab?

There are two common ways to learn AWS.

The first is using a real AWS account.

That is valuable, and eventually we should do it. Real AWS teaches us real IAM, real billing, real regional behavior, real service limits, and real deployment constraints.

The second is using a local AWS emulator such as LocalStack.

That is what we will use first.

Why?

Because early learning should be fast and safe.

When you are practicing, you should be able to destroy and recreate infrastructure many times. You should be able to make mistakes without worrying about accidentally leaving a resource running. You should be able to run tests repeatedly. You should be able to work offline or from a home lab.

LocalStack gives us that freedom.

It does not replace AWS completely. It is not supposed to. Some services behave differently. Some advanced features require a paid LocalStack plan. Some production issues only appear in the real cloud.

But for learning the fundamentals of event-driven systems, infrastructure as code, queues, functions, tables, retries and service integration, it is an excellent starting point.

The mental model is:

Learn locally first.
Understand the concepts.
Build muscle memory.
Then deploy selected projects to real AWS.

4. The architecture of the lab

Before installing tools, let’s understand the stack.

Proxmox

Proxmox is the virtualization platform running on the physical machine. It allows us to create a dedicated VM for this lab.

The VM gives us isolation. If we break something, we can restore a snapshot or rebuild the environment without affecting our laptop or other services.

Ubuntu Server 24.04

Ubuntu Server is the operating system inside the VM.

We use a server installation because this machine does not need a desktop environment. It will be accessed through SSH and used as a development server.

Docker

Docker runs containers.

LocalStack itself will run as a Docker container. Later, we may also run PostgreSQL, Redis, Grafana, Prometheus, application containers, or Kubernetes tooling.

Docker Compose

Docker Compose lets us define multi-container environments using a YAML file.

Instead of running long docker run commands manually, we describe our services in docker-compose.yml.

LocalStack

LocalStack exposes AWS-compatible endpoints locally.

For most services, instead of sending requests to AWS, our tools will send requests to:

http://localhost:4566

or, from another machine:

http://<vm-ip>:4566

Terraform

Terraform lets us define infrastructure as code.

We will not manually create queues and tables whenever possible. We will write Terraform files and apply them.

This matters because production infrastructure is rarely created by clicking around in the AWS Console. It is usually declared, reviewed, versioned and applied.

Python and uv

Python will be our primary application language.

uv will help us create virtual environments, install dependencies, run tools and keep projects reproducible.


5. Creating the virtual machine in Proxmox

The VM used for this lab was created with the following approximate resources:

Operating system: Ubuntu Server 24.04
CPU:              8 vCPUs
Memory:           16 GB RAM
Disk:             100 GB
Network:          VirtIO
Disk controller:  VirtIO SCSI
CPU type:         host

These values are not mandatory, but they are comfortable for this type of lab.

Why 8 vCPUs?

LocalStack itself does not require 8 CPUs for simple exercises.

But the lab will grow. Over time, we may run:

  • LocalStack
  • Python applications
  • PostgreSQL
  • Redis
  • test suites
  • Terraform
  • Docker builds
  • possibly Kubernetes tools such as k3d or k3s

Having extra CPU makes the environment feel responsive.

Why 16 GB RAM?

Docker-based development environments can consume memory quickly.

A minimal lab can run with less, but 16 GB gives us room to experiment without constantly thinking about memory pressure.

Why 100 GB disk?

Docker images, build caches, volumes and package downloads accumulate over time.

A 30 GB disk may work at first, but it becomes annoying quickly. A 100 GB disk is a practical starting point.

Why CPU type host?

In Proxmox, the CPU type controls which CPU features are exposed to the VM.

Using host tells Proxmox to expose the host CPU capabilities directly to the guest VM.

This is useful for performance and compatibility, especially when running modern development tooling, Docker workloads, and possibly Kubernetes-related tools later.

In the Proxmox VM configuration, the CPU line should look similar to this:

cpu: host,flags=+aes

If it says something like this instead:

cpu: x86-64-v2-AES,flags=+aes

then the VM is using a generic CPU model. That can still work, but for this lab host is preferred.

Why VirtIO networking?

VirtIO is a paravirtualized driver designed for virtual machines.

It is usually faster and more efficient than emulated network adapters such as Intel E1000.

For a Docker host, VirtIO is the right choice.

Why disable the Proxmox VM firewall initially?

For a learning lab, it is often easier to start with fewer moving parts.

Docker manages networking and iptables rules internally. Proxmox firewall rules can coexist with Docker, but while learning, they add one more layer to debug.

The recommendation for this first milestone is:

Disable the Proxmox VM firewall initially.
Re-enable it later when you intentionally want to practice network security.

Security matters, but so does controlling the learning curve.


6. Installing Ubuntu Server 24.04

During the Ubuntu installation, choose a minimal server setup.

Recommended options:

Install OpenSSH Server: yes
Install Docker from Ubuntu installer: no
Install Kubernetes/LXD/snaps: no, unless you specifically need them

Why not install Docker from the Ubuntu installer?

Because we want Docker from Docker’s official repository, not the distribution package.

Linux distributions often prioritize stability over the latest upstream version. That is reasonable for many servers, but for a development lab we usually want the official Docker packages.

After installation, SSH into the VM.

Example:

ssh <your-user>@192.168.88.200

Use your own username and IP address.

In our setup, the VM initially received an IP through DHCP, then we discussed assigning a static IP so the lab can always be reached at the same address.

For example:

Subnet:  192.168.88.0/24
Address: 192.168.88.200
Gateway: 192.168.88.1
DNS:     192.168.88.1

If your router uses a different network, adapt these values.


7. First boot: update the system

The first commands executed on the VM were the standard update commands:

sudo apt update
sudo apt upgrade -y

What do they do?

apt update refreshes the local package index. It does not upgrade software by itself. It only asks Ubuntu’s package repositories what versions are available.

apt upgrade -y upgrades installed packages without asking for confirmation for each package.

After a fresh server installation, this is a good first step.

If the kernel or important system packages were upgraded, reboot:

sudo reboot

Then reconnect through SSH.


8. Installing the QEMU Guest Agent

The QEMU Guest Agent is not required for Docker, LocalStack or Terraform.

But it is useful when running Ubuntu inside Proxmox.

It allows Proxmox to communicate with the guest OS. With the guest agent installed and enabled, Proxmox can:

  • show the VM IP address
  • shut down the VM cleanly
  • coordinate better backups and snapshots
  • query guest information

Install it inside Ubuntu:

sudo apt install qemu-guest-agent -y
sudo systemctl enable --now qemu-guest-agent

Then, in Proxmox, make sure the VM option is enabled:

VM -> Options -> QEMU Guest Agent -> Enabled

You can check the service status inside Ubuntu:

systemctl status qemu-guest-agent

You can also check whether the VirtIO port exists:

ls /dev/virtio-ports/

A working setup should show something like:

org.qemu.guest_agent.0

Troubleshooting note

During the real setup, we saw an error like this:

Timed out waiting for device dev-virtio\x2dports-org.qemu.guest_agent.0.device
Timed out waiting for device /dev/virtio-ports/org.qemu.guest_agent.0

That usually means the guest agent package is installed inside Ubuntu, but Proxmox is not exposing the guest agent device to the VM.

The fix is usually:

  1. Shut down the VM.
  2. Enable QEMU Guest Agent in Proxmox VM options.
  3. Start the VM again.
  4. Restart the service if needed.

Again, this is not required for the AWS lab itself. It is a Proxmox quality-of-life feature.


9. Installing Docker from the official Docker repository

Now we install Docker.

First, install base packages required to configure external repositories:

sudo apt install -y \
    curl \
    git \
    unzip \
    jq \
    make \
    ca-certificates \
    gnupg \
    lsb-release

Some of these tools are needed immediately; others are useful throughout the lab.

  • curl downloads files and scripts.
  • git manages source code.
  • unzip extracts downloaded archives.
  • jq processes JSON, which is extremely useful with AWS CLI output.
  • make will help us create repeatable project commands.
  • ca-certificates and gnupg help apt verify repository signatures.
  • lsb-release helps scripts detect the Ubuntu codename.

Before installing Docker CE, remove possible conflicting packages:

for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
    sudo apt remove -y $pkg
done

This step is defensive. On a fresh Ubuntu server, many of these packages may not be installed. That is fine.

Now create the directory for apt keyrings:

sudo install -m 0755 -d /etc/apt/keyrings

Download Docker’s GPG key:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
    | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

Make it readable by apt:

sudo chmod a+r /etc/apt/keyrings/docker.gpg

Add Docker’s official apt repository:

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Update the package index again:

sudo apt update

Install Docker Engine and Compose:

sudo apt install -y \
    docker-ce \
    docker-ce-cli \
    containerd.io \
    docker-buildx-plugin \
    docker-compose-plugin

Verify Docker:

docker version

If that fails with a permission error, try:

sudo docker version

At this point, Docker works, but your user may not yet have permission to access the Docker daemon directly.

Add your user to the Docker group:

sudo usermod -aG docker $USER

Then either log out and log back in, reboot, or run:

newgrp docker

Now verify Docker Compose:

docker compose version

Important: the modern command is:

docker compose

not:

docker-compose

The older docker-compose command was a separate Python-based tool. The modern Compose plugin is integrated into the Docker CLI.


10. Running the first Docker container

Run Docker’s hello-world test:

docker run hello-world

This command verifies the full Docker path:

  1. The Docker CLI can talk to the Docker daemon.
  2. Docker can pull an image from a registry.
  3. Docker can create a container.
  4. Docker can start the container.
  5. The container can write output.

This is a small command, but it validates a lot.

If it works, Docker is ready.


11. Installing the AWS tooling

Even though we will use LocalStack, we still install the official AWS CLI.

Why?

Because LocalStack imitates AWS APIs. We want to learn the same commands and mental models that apply to the real cloud.

Download AWS CLI v2:

cd /tmp
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip

Unzip it:

unzip awscliv2.zip

Install it:

sudo ./aws/install

Verify:

aws --version

You should see output similar to:

aws-cli/2.x.x Python/3.x Linux/x86_64

Do not worry if your exact version differs.

Why install awslocal?

The standard AWS CLI sends requests to AWS endpoints by default.

LocalStack runs locally, usually on:

http://localhost:4566

You can point AWS CLI manually at LocalStack using --endpoint-url:

aws --endpoint-url=http://localhost:4566 sqs list-queues

That works, but it becomes repetitive.

awslocal is a convenience wrapper that automatically targets LocalStack.

We will install it after installing uv.


12. Installing Terraform

Terraform will be used throughout this series.

Instead of manually creating AWS resources, we will describe them using code.

Install HashiCorp’s GPG key:

wget -O- https://apt.releases.hashicorp.com/gpg \
    | sudo gpg --dearmor \
    -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

Add the HashiCorp apt repository:

echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com \
$(. /etc/os-release && echo $VERSION_CODENAME) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list

Update apt:

sudo apt update

Install Terraform:

sudo apt install terraform

Verify:

terraform version

Why install Terraform so early?

Because we want infrastructure as code to be part of the learning process from the beginning.

A common beginner mistake is to learn cloud services through manual clicks first and only later learn Terraform. That approach creates two separate learning paths.

In this course, we will do the opposite.

When we learn SQS, we will create queues with Terraform.

When we learn DynamoDB, we will create tables with Terraform.

When we learn IAM, we will write policies with Terraform.

This makes our work reproducible from day one.


13. Installing Python tooling with uv

Ubuntu 24.04 ships with Python 3.12.

Verify:

python3 --version

Install Python support packages:

sudo apt install -y python3-venv python3-pip

Now install uv:

curl -LsSf https://astral.sh/uv/install.sh | sh

Reload your shell:

source ~/.bashrc

Verify:

uv --version

uv is a modern Python package and project manager. We will use it for virtual environments, dependency management and command execution.

Now install awslocal as a uv-managed tool:

uv tool install awscli-local

Verify:

awslocal --version

Later, once LocalStack is running, we will use commands like:

awslocal sqs list-queues

Instead of:

aws --endpoint-url=http://localhost:4566 sqs list-queues

Both are valid. awslocal is simply more convenient for local work.


14. Installing helper tools

A good lab environment needs more than Docker and Terraform.

Install useful command-line tools:

sudo apt install -y \
    git \
    make \
    curl \
    wget \
    unzip \
    zip \
    jq \
    tree \
    ripgrep \
    htop \
    btop \
    vim \
    nano \
    ca-certificates \
    gnupg \
    software-properties-common \
    build-essential \
    pkg-config \
    libssl-dev

Why these tools?

  • tree helps visualize project structure.
  • ripgrep is a fast code search tool.
  • htop and btop help inspect system resources.
  • build-essential, pkg-config, and libssl-dev are common dependencies for compiling Python packages and Rust crates.
  • vim and nano give us terminal editors.

Install yq for YAML processing:

sudo snap install yq

Alternatively, if you prefer avoiding snaps, install Mike Farah’s yq binary manually:

sudo wget \
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
-O /usr/local/bin/yq

sudo chmod +x /usr/local/bin/yq

Install direnv:

sudo apt install direnv

Enable it for Bash:

echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
source ~/.bashrc

direnv automatically loads environment variables when entering a project directory. This will be useful later when we define AWS variables for LocalStack.

Install just, a command runner similar to Make:

sudo snap install --edge just

make is universal and we will still use it, but just is convenient for developer workflows.

Finally, configure Git defaults:

git config --global init.defaultBranch main
git config --global pull.rebase false
git config --global core.editor vim

These settings are personal preferences, but they avoid small annoyances later.


15. Creating the project repository

Now create the project directory.

mkdir ~/aws-gym
cd ~/aws-gym

Initialize Git:

git init
git branch -M main

Create the first directory structure:

mkdir -p infrastructure/terraform
mkdir -p app
mkdir -p scripts
mkdir -p volume/localstack

Create the first files:

touch .env
touch Makefile
touch docker-compose.yml

The project structure now looks like this:

aws-gym/
|-- app/
|-- docker-compose.yml
|-- infrastructure/
|   `-- terraform/
|-- Makefile
|-- scripts/
`-- volume/
    `-- localstack/

Why this structure?

  • app/ will contain application code.
  • infrastructure/terraform/ will contain Terraform code.
  • scripts/ will contain helper scripts.
  • volume/localstack/ will persist LocalStack state when persistence is enabled.
  • docker-compose.yml defines local services.
  • .env stores local environment variables.
  • Makefile will provide repeatable commands.

A note about naming

For the course, we will standardize on:

infrastructure/terraform

Small naming consistency matters because paths become documentation. Using one clear spelling across examples, commands and future chapters keeps the project easier to follow.


16. Running LocalStack with Docker Compose

Now create the .env file:

cat > .env <<'ENV'
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_DEFAULT_REGION=us-east-1

PERSISTENCE=1
DEBUG=1
ENV

These credentials are fake.

LocalStack does not require real AWS credentials for local development. Many AWS SDKs and tools expect credentials to exist, so we provide dummy values.

Now create docker-compose.yml:

cat > docker-compose.yml <<'COMPOSE'
services:

  localstack:
    image: localstack/localstack:4.5
    container_name: localstack

    ports:
      - "4566:4566"

    environment:
      - DEBUG=${DEBUG}
      - PERSISTENCE=${PERSISTENCE}
      - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}

    volumes:
      - ./volume/localstack:/var/lib/localstack
      - /var/run/docker.sock:/var/run/docker.sock

    healthcheck:
      test: ["CMD", "bash", "-c", "awslocal sts get-caller-identity"]
      interval: 10s
      timeout: 5s
      retries: 20
COMPOSE

Start LocalStack:

docker compose up

If you want it to run in the background:

docker compose up -d

Check running containers:

docker ps

You should see a container named:

localstack

17. Understanding the LocalStack configuration

Let’s look at the Compose file piece by piece.

The image

image: localstack/localstack:4.5

This tells Docker which image to run.

Pinning a version is better than using latest for learning material. If a future LocalStack release changes behavior, readers following this chapter still get the same baseline.

The container name

container_name: localstack

This gives the container a predictable name.

That makes commands easier:

docker logs localstack

instead of needing to discover a generated container name.

The port mapping

ports:
  - "4566:4566"

LocalStack exposes most AWS service endpoints through port 4566.

The left side is the host port. The right side is the container port.

This means:

VM port 4566 -> LocalStack container port 4566

From inside the VM, tools can reach LocalStack at:

http://localhost:4566

From another machine on the network, tools may reach it at:

http://<vm-ip>:4566

assuming firewall rules allow it.

Environment variables

environment:
  - DEBUG=${DEBUG}
  - PERSISTENCE=${PERSISTENCE}
  - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}

These values come from .env.

DEBUG=1 makes LocalStack more verbose. This is useful while learning.

PERSISTENCE=1 tells LocalStack to persist state under /var/lib/localstack.

AWS_DEFAULT_REGION=us-east-1 gives us a default AWS region.

Volumes

volumes:
  - ./volume/localstack:/var/lib/localstack
  - /var/run/docker.sock:/var/run/docker.sock

The first mount persists LocalStack data in the project directory.

The second mount gives LocalStack access to the Docker socket. Some LocalStack features need Docker to start additional containers, especially when working with Lambda-like execution.

This is powerful, but it is also something to understand: mounting the Docker socket gives the container significant control over the Docker host. In this isolated lab VM, that is acceptable. On a shared production server, it would require more careful consideration.

Healthcheck

healthcheck:
  test: ["CMD", "bash", "-c", "awslocal sts get-caller-identity"]
  interval: 10s
  timeout: 5s
  retries: 20

The healthcheck periodically runs a simple AWS identity command through LocalStack.

If it succeeds, Docker considers the service healthy.

The command:

awslocal sts get-caller-identity

asks the local STS service: who am I?

In real AWS, STS returns information about the current caller identity. In LocalStack, it returns a local/fake identity, which is still useful for verifying connectivity.


18. Verifying the environment

After installing the tooling, run a full verification block:

python3 --version
uv --version
aws --version
awslocal --version
terraform version
docker --version
docker compose version
jq --version
yq --version
git --version

A healthy environment should show versions for all of them.

Now verify LocalStack:

awslocal sts get-caller-identity

Expected output will be JSON, similar to:

{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "000000000000",
    "Arn": "arn:aws:iam::000000000000:root"
}

The exact values do not matter.

What matters is that:

  1. awslocal runs.
  2. It reaches LocalStack.
  3. LocalStack responds with valid JSON.

You can also test through the standard AWS CLI:

aws --endpoint-url=http://localhost:4566 sts get-caller-identity

This should return similar output.

If both commands work, your local AWS lab is alive.


19. Troubleshooting notes from the real setup

A good technical guide should include mistakes and debugging steps, not only the clean path.

Here are a few notes from the actual setup.

Docker permission denied

Symptom:

permission denied while trying to connect to the Docker daemon socket

Cause:

Your user is not in the docker group, or the current shell session has not picked up the new group membership yet.

Fix:

sudo usermod -aG docker $USER
newgrp docker

Or log out and log back in.

Then retry:

docker run hello-world

docker compose versus docker-compose

Use:

docker compose up

Not:

docker-compose up

The first one is the modern Docker Compose plugin.

QEMU Guest Agent timeout

Symptom:

Timed out waiting for device /dev/virtio-ports/org.qemu.guest_agent.0

Cause:

The guest agent service is installed in Ubuntu, but the Proxmox VM does not expose the guest agent device.

Fix:

Enable QEMU Guest Agent in Proxmox VM options, then reboot the VM.

LocalStack asks for a token

Some LocalStack features may require a token or a paid plan.

For the early parts of this series, we will stay within features that are useful for local AWS learning without depending on paid-only behavior where possible.

If you see token-related output, do not panic. First verify whether the service you are using actually requires it.

20. What we built

In this chapter, we built the foundation for a local AWS learning environment.

We created:

A dedicated Ubuntu Server VM
A Docker-based runtime
A LocalStack AWS-compatible endpoint
A Terraform installation
A Python 3.12 development environment
A uv-based Python toolchain
AWS CLI and awslocal
A project repository for future lessons

This may look like basic setup work, but it is more important than it appears.

We now have a safe environment where we can create AWS-style infrastructure, test applications, break things, inspect behavior, and destroy everything without worrying about cloud bills.

Most importantly, we have established the mindset of the course:

Do not memorize cloud services.
Build systems.
Understand the reason behind every component.
Make the environment reproducible.
Verify everything.

21. Exercises

Before moving to the next chapter, try these exercises.

Exercise 1: Verify all tools

Run:

python3 --version
uv --version
aws --version
awslocal --version
terraform version
docker --version
docker compose version
jq --version
yq --version
git --version

Write down the versions in a NOTES.md file.

Why?

Because future debugging is easier when you know the baseline you started from.

Exercise 2: Restart LocalStack

Start LocalStack:

docker compose up -d

Check it:

docker ps

View logs:

docker logs localstack

Stop it:

docker compose down

Start it again:

docker compose up -d

The goal is to become comfortable managing the local cloud process.

Exercise 3: Compare AWS CLI and awslocal

Run:

awslocal sts get-caller-identity

Then run:

aws --endpoint-url=http://localhost:4566 sts get-caller-identity

Confirm they return equivalent results.

This proves that awslocal is convenience, not magic.

Exercise 4: Inspect the Docker volume

Run:

tree volume

Then create resources in later chapters and inspect the directory again.

This will help you understand what LocalStack persists locally.

Exercise 5: Create a Proxmox snapshot

After the environment is working, create a Proxmox snapshot named:

localstack-ready

This gives you a clean restore point before starting the AWS exercises.


22. Next chapter

In the next chapter, we will create our first real AWS-style resource: an SQS queue.

We will learn:

  • what a message queue is
  • why distributed systems use queues
  • how SQS differs from RabbitMQ and Kafka
  • how to create an SQS queue with Terraform
  • how to send messages with Python
  • how to receive messages with Python
  • what visibility timeout means
  • why dead-letter queues exist
  • how to inspect queue state through LocalStack

The goal of the next chapter is not just to run send-message and receive-message.

The goal is to understand why queues are one of the most important building blocks in cloud architecture.

Once queues make sense, Lambda, EventBridge, retries, idempotency and distributed workflows become much easier to understand.


Appendix: Command summary

This appendix is intentionally repetitive. It exists so the environment can be recreated quickly later.

System update

sudo apt update
sudo apt upgrade -y
sudo reboot

Base packages

sudo apt install -y \
    curl \
    git \
    unzip \
    jq \
    make \
    ca-certificates \
    gnupg \
    lsb-release

Docker installation

for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
    sudo apt remove -y $pkg
done

sudo install -m 0755 -d /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
    | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update

sudo apt install -y \
    docker-ce \
    docker-ce-cli \
    containerd.io \
    docker-buildx-plugin \
    docker-compose-plugin

sudo usermod -aG docker $USER
newgrp docker

Docker verification

docker version
docker compose version
docker run hello-world

QEMU Guest Agent

sudo apt install qemu-guest-agent -y
sudo systemctl enable --now qemu-guest-agent
systemctl status qemu-guest-agent

AWS CLI

cd /tmp
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
unzip awscliv2.zip
sudo ./aws/install
aws --version

Terraform

wget -O- https://apt.releases.hashicorp.com/gpg \
    | sudo gpg --dearmor \
    -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com \
$(. /etc/os-release && echo $VERSION_CODENAME) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list

sudo apt update
sudo apt install terraform
terraform version

Python and uv

sudo apt install -y python3-venv python3-pip
python3 --version
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.bashrc
uv --version
uv tool install awscli-local
awslocal --version

Extra tooling

sudo apt install -y \
    git \
    make \
    curl \
    wget \
    unzip \
    zip \
    jq \
    tree \
    ripgrep \
    htop \
    btop \
    vim \
    nano \
    ca-certificates \
    gnupg \
    software-properties-common \
    build-essential \
    pkg-config \
    libssl-dev

sudo snap install yq
sudo apt install direnv
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc
sudo snap install --edge just

Git defaults

git config --global init.defaultBranch main
git config --global pull.rebase false
git config --global core.editor vim

Project repository

mkdir ~/aws-gym
cd ~/aws-gym

git init
git branch -M main

mkdir -p infrastructure/terraform
mkdir -p app
mkdir -p scripts
mkdir -p volume/localstack

touch .env
touch Makefile
touch docker-compose.yml

LocalStack .env

cat > .env <<'ENV'
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_DEFAULT_REGION=us-east-1

PERSISTENCE=1
DEBUG=1
ENV

LocalStack docker-compose.yml

cat > docker-compose.yml <<'COMPOSE'
services:

  localstack:
    image: localstack/localstack:4.5
    container_name: localstack

    ports:
      - "4566:4566"

    environment:
      - DEBUG=${DEBUG}
      - PERSISTENCE=${PERSISTENCE}
      - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}

    volumes:
      - ./volume/localstack:/var/lib/localstack
      - /var/run/docker.sock:/var/run/docker.sock

    healthcheck:
      test: ["CMD", "bash", "-c", "awslocal sts get-caller-identity"]
      interval: 10s
      timeout: 5s
      retries: 20
COMPOSE

Start LocalStack

docker compose up -d
docker ps
awslocal sts get-caller-identity

Want to build something similar?

If you are validating a product, modernizing operations, or shipping a technical MVP, we can help.

Start a Conversation