After wrapping up the fundamentals of AWS in my previous post, I’ve now completed the meatier parts of my cloud computing course at Quantic. This section took me from migrating traditional applications to the cloud all the way to building fully serverless architectures.
The best part? I didn’t just read about these concepts – I got hands-on access to a real AWS console through the course’s lab environment. Actually clicking through the AWS Management Console, provisioning resources, and watching everything come together was completely different from just studying the theory. Here’s what I learned.
Module 2: Migrating an Existing Web Application (Part I)
The first big topic was migration – moving applications that are currently hosted on-premises to the cloud. Turns out there are three main strategies for this, and they represent different levels of commitment and effort:
The Three Migration Strategies
1. Rehosting (“Lift and Shift”) This is the quickest approach. You basically recreate your on-premises environment in the cloud using EC2 instances and install the same software you’re already running. It’s the path of least resistance but doesn’t take full advantage of cloud-native features.
2. Replatforming A middle-ground approach where you migrate to AWS-managed platforms that are code-compatible with your app. For example, instead of managing your own database servers, you’d use RDS (Relational Database Service) where AWS handles backups, patching, and scaling for you.
3. Refactoring The most ambitious option – redesigning and rewriting your code to maximize cloud capabilities. Think API Gateway + Lambda functions instead of traditional web servers. This gives you the most cloud-specific benefits but requires the most upfront work.
The relationship is interesting: as you move from rehosting → replatforming → refactoring, your upfront costs and effort increase, but so do your long-term cloud-specific benefits.
Networking Fundamentals
I also learned about Virtual Private Clouds (VPCs) – AWS’s networking service that lets you create isolated network environments. This was where the lab work really started to click for me.
In the lab environment, I actually created my own VPC from scratch. Setting up the CIDR blocks, creating subnets across multiple availability zones, and configuring routing tables made everything make sense in a way the reading material couldn’t. Seeing the VPC visualizer in the AWS console helped me understand how all the pieces fit together.
Some key takeaways:
-
CIDR notation tells you how IP addresses are divided between network/subnet identifiers and host identifiers. For example,
104.58.129.42/8means 8 bits for the network ID (104) and 24 bits for the host ID (58.129.42). -
You need to design your VPC carefully: ensure your CIDR blocks don’t overlap with other private networks, create subnets for different routing requirements (public vs. private), and always reserve address space for future growth.
-
Routing tables direct packet flow, with internet gateways for hosts with public IPs and NAT gateways for private hosts that need external access.
Actually configuring the routing tables in the console was eye-opening. I had to add routes manually, attach internet gateways, and set up NAT gateways in public subnets. When I first tried to access the internet from a private subnet without a NAT gateway, it obviously failed – which was actually a great learning moment about why these components exist.
Load Balancing
I learned the difference between two types of load balancers:
- Application Load Balancers – These inspect the actual content of network packets and route based on that content. Perfect for HTTP/HTTPS traffic.
- Network Load Balancers – These distribute traffic based solely on server health, used for general TCP and UDP traffic.
Security with IAM
AWS Identity and Access Management (IAM) is how you control who can do what in your AWS environment. You can assign permissions to:
- Users – individual people or applications
- Groups – multiple users with the same permissions
- Roles – temporary permissions that can be assumed when needed
AWS provides pre-built policies like AmazonVPCReadOnlyAccess that bundle common permissions together, which is super convenient.
Module 3: Migrating an Existing Web Application (Part II)
This module went deeper into the infrastructure components you’d actually provision.
EC2 Instance Types
When you create an EC2 instance, you select an instance type that defines its resources. The naming convention is actually pretty logical:
- Family – the general capabilities (CPU, memory, network, storage)
- Generation – version number (higher = newer)
- Size – amount of vCPUs, memory, bandwidth, etc.
For example: t3.medium = t family, generation 3, medium size.
One important note: an AWS virtual CPU is only equivalent to a single core of a physical CPU, not a full CPU!
In my lab work, I provisioned several different instance types to see how they performed. The AWS console makes this pretty straightforward – you just select your instance type from a dropdown, choose your AMI, and configure your network settings. Watching the instance go from “pending” to “running” status was oddly satisfying.
Amazon Machine Images (AMI)
AMIs contain the operating system and system-level software for your instances. AWS offers many pre-configured AMIs, but you can also create your own, which is great for standardizing configurations across multiple instances.
Bastion Hosts
I learned about bastion hosts – a security pattern where you set up a single EC2 instance in a public subnet that provides secure SSH access to instances in private subnets. This way, your application servers never need to be directly exposed to the internet.
Setting this up in the lab was probably my favorite exercise. I created a bastion host in a public subnet, configured its security group to only accept SSH from my IP address, then used it as a jump box to access instances in private subnets. The first time I successfully SSH’d through the bastion to reach a private instance felt like I’d unlocked a secret passage.
Security groups were interesting to work with – they’re essentially virtual firewalls that control inbound and outbound traffic. I had to be really careful with the rules. Too restrictive and nothing works; too permissive and you’ve created a security hole. Finding that balance through trial and error in the lab helped me understand why security group design is so important.
Database Options
For databases, AWS offers several features:
- Standby instances – real-time replication that provides automatic failover if the primary database fails
- Read replicas – separate read-only copies that offload read requests from the primary database
- Storage autoscaling – automatically increases storage size as your database grows (but only upwards, never down!)
I created an RDS instance in the lab, and it was eye-opening how much AWS handles for you. I selected MySQL as my database engine, configured the instance size, set up automated backups, and enabled Multi-AZ deployment for high availability. The console walked me through every step, but understanding why I was making each choice required the theoretical knowledge from the course.
Connecting to the RDS instance from an EC2 instance in the same VPC was a great exercise in understanding security groups and network connectivity. My first attempt failed because I hadn’t added the EC2 security group as an allowed source in the RDS security group – another one of those face-palm moments that actually taught me more than if it had worked the first time.
Storage comes in two flavors:
- General Purpose (GP) – performance scales with storage size
- Provisioned IOPS (PIOPS) – you provision performance separately from size; more expensive but necessary for sustained high demand
Module 4: Serverless Application Development (Part I)
This is where things got really interesting. Serverless doesn’t mean “no servers” – it means you don’t manage the servers yourself. AWS handles all the infrastructure while you just focus on your code.
Designing Cloud-Native Apps
The approach taught was to list your app’s functions, then match AWS services to each:
- S3 – serve static client code and store files
- API Gateway – host APIs
- Lambda – execute business logic
- DynamoDB – store and serve application data
Setting Up S3 for Static Hosting
To serve static websites from S3, you:
- Create a bucket
- Configure it for HTTP access
- Enable public access from the internet
- Upload your client code
(For production, you’d add CloudFront in front to keep the S3 bucket private and get CDN benefits.)
I actually built this in the lab! I created an S3 bucket, uploaded a simple HTML file with some CSS, and configured it for static website hosting. The trickiest part was getting the bucket policy right to allow public read access. AWS has safety measures to prevent accidental public exposure, so I had to explicitly acknowledge that I wanted to make the bucket public.
Once I got the permissions right and accessed the website endpoint URL, seeing my simple webpage load from S3 was pretty cool. It’s such a simple, elegant solution for hosting static sites – no servers to manage, automatic scaling, and dirt cheap.
Lambda Functions
Lambda functions are the heart of serverless computing. Each function includes:
- An entry point where AWS starts execution
- Variables passed during invocation
- The function body (your code)
- Return values sent back to the calling service
Important: Data passed to and from Lambda is in JSON format.
Lambda functions need IAM roles to interact with other AWS services – you can’t just assume they have permissions by default.
Creating my first Lambda function in the console was a bit intimidating at first, but the inline code editor made it pretty straightforward. I wrote a simple Python function, configured the runtime, and set up a test event to invoke it. Watching the execution logs in CloudWatch as my function ran was fascinating – you can see every print statement, execution time, and memory used.
I made plenty of mistakes. Forgot to return data in the right format? Lambda throws an error. Didn’t give the function permission to access S3? Another error. But each error message was actually pretty helpful, and fixing them taught me how the pieces fit together.
The coolest part was seeing how fast Lambda scales. The same function code can handle one request or thousands without me changing anything. AWS just spins up more instances in the background.
API Gateway
An HTTP API in API Gateway has two parts:
- A route (HTTP method + URL)
- An integration (the service requests get routed to)
API Gateway also handles CORS (Cross-Origin Resource Sharing) to tell browsers it’s okay to load resources from the gateway even though they came from a different domain.
Setting up API Gateway in the lab to connect to my Lambda function was where everything started to feel like a real application. I created an HTTP API, defined a GET route, and integrated it with my Lambda function. The first time I hit the API endpoint and got back the JSON response from my Lambda function, I literally said “whoa” out loud.
The CORS configuration tripped me up initially. My frontend in S3 couldn’t access the API until I enabled CORS properly in API Gateway. Once I added the right headers, everything worked smoothly. These are the kinds of issues you only really understand by experiencing them firsthand.
DynamoDB
AWS’s NoSQL database organizes data differently from traditional relational databases:
- Attributes – name-value pairs
- Items – unique sets of attributes (like rows)
- Tables – collections of related items
The key insight: DynamoDB stores data in partitions based on the partition key. Queries using the partition key are fast and cheap; queries on other attributes are slow and expensive. This completely changes how you design your data model.
Unlike relational databases, DynamoDB works best when you store related data in the same table and use as few tables as possible. This was a mind shift for me coming from SQL databases!
Creating my first DynamoDB table in the console was relatively straightforward – just define the table name and partition key. But actually designing what data to store and how to structure it required thinking completely differently than I’m used to with relational databases.
I built a simple meme application table in the lab (following the course example), storing meme metadata with attributes like memeId (partition key), creator, timestamp, and likes. Then I wrote Lambda functions to read from and write to the table.
The real learning happened when I tried querying. Queries on the partition key were lightning fast. But when I tried to query by a different attribute (like creator name), I had to create a secondary index, and the performance difference was noticeable. This really drove home why partition key design is so critical in DynamoDB.
SDK Integration
AWS provides Software Development Kits (SDKs) for different programming languages. In Python, Boto3 is the SDK, and it provides:
- Resources – object-oriented interfaces to AWS APIs
- Collections – ways to iterate over groups of resources
The SDK translates AWS’s data types into language-native types. For example, AWS’s “M” (mapping) type becomes a Python dictionary.
Using Boto3 in my Lambda functions was where I spent a lot of time in the lab. The documentation is extensive but sometimes overwhelming. I learned to keep the AWS console open in one tab and the Boto3 documentation in another.
Writing code to interact with DynamoDB using Boto3 was especially educational. Here’s a pattern I used repeatedly:
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('my-meme-table')
# Put an item
table.put_item(Item={'memeId': '123', 'creator': 'me', 'likes': 0})
# Get an item
response = table.get_item(Key={'memeId': '123'})
item = response['Item']
The code itself is pretty clean, but understanding what’s happening under the hood – the API calls, the IAM permissions required, the data transformations – that took hands-on experimentation.
Module 5: Serverless Application Development (Part II)
The final module covered more advanced serverless patterns and real-world implementation details.
Including External Libraries
Lambda functions can use external libraries two ways:
- Include them in a ZIP file with your code and upload the whole thing
- Create a Layer with the libraries that can be attached to multiple functions
Layers are recommended because they’re more flexible and reusable. One critical note: Lambda runs on Linux, so binary libraries must be Linux-compatible!
I experimented with both approaches in the lab. For a simple function needing the requests library, I created a layer by packaging the library in the correct directory structure and uploading it. Then I attached the layer to my Lambda function and imported it like normal. It worked seamlessly and felt much cleaner than bundling everything in one deployment package.
Lambda Performance
Lambda functions have maximum duration and memory settings. Since billing is based on time and memory consumed, it’s best to keep functions quick and small. This encourages good design patterns anyway.
Working with Documentation
A practical tip I appreciated: if SDK documentation isn’t clear, check the corresponding API documentation. AWS API return values include explicit data type notations, and SDKs translate these into language-native types.
You should also print sample outputs to CloudWatch logs to confirm what the docs tell you. Debugging serverless functions is different from debugging traditional apps!
This became my go-to debugging strategy in the lab. I’d add print statements throughout my Lambda code, invoke the function, then check CloudWatch Logs to see exactly what was happening. The logs show you everything – the values of variables, the structure of API responses, error stack traces. CloudWatch became my best friend for understanding what my code was actually doing versus what I thought it was doing.
Error Handling Inconsistencies
One frustrating discovery: SDKs aren’t guaranteed to handle errors consistently. For example:
- Boto3 raises an exception when accessing a missing S3 object
- But it returns an error code without an exception when accessing a missing DynamoDB item
You have to know these quirks for each service.
DynamoDB Update Expressions
Updating DynamoDB items uses update expressions with keywords indicating the operation type (SET, REMOVE, etc.) and actions referencing specific attributes. This is more complex than SQL UPDATE statements but also more flexible.
In the lab, I implemented a “like” feature for memes that incremented a counter. The update expression looked like this:
table.update_item(
Key={'memeId': '123'},
UpdateExpression='SET likes = likes + :inc',
ExpressionAttributeValues={':inc': 1}
)
The syntax felt weird at first – why use :inc instead of just 1? But it’s actually a safety feature to prevent injection attacks and handle special characters. Once I got used to it, update expressions felt quite powerful.
Adding New Attributes
One of DynamoDB’s strengths: it’s easy to add new attributes to items without changing the schema. This flexibility is great for evolving applications.
I tested this in the lab by adding a thumbnail attribute to some of my meme items without having to modify the table structure. Coming from SQL where you’d need an ALTER TABLE statement and potentially migration scripts, this felt liberating.
Deployment Benefits
Deploying changes to serverless apps usually doesn’t require downtime because each invocation uses the most recent version of the code. This is a huge advantage over traditional deployments.
Advanced DynamoDB Features
Time to Live (TTL) – DynamoDB can automatically delete items when a specified timestamp field is older than the current Unix epoch time. Perfect for temporary data like sessions.
I enabled TTL on a test table and set expiration timestamps on some items. Coming back later and seeing them automatically deleted was pretty slick. No cron jobs, no cleanup scripts – just set a timestamp and DynamoDB handles it.
DynamoDB Streams – Capture a time-ordered sequence of item modifications in near real-time. You can trigger Lambda functions based on these events to build reactive applications.
Setting up DynamoDB Streams with Lambda triggers was one of the coolest parts of the lab. I configured a stream on my table, created a Lambda function to process stream records, and watched it automatically trigger whenever I added, updated, or deleted items. This pattern of event-driven architecture really shows the power of serverless.
Event Filtering – To avoid unnecessary Lambda invocations, you can filter which events actually trigger your functions. This saves costs and reduces complexity.
I added event filters to only trigger my Lambda when specific types of changes occurred (like only on INSERT events, not UPDATE or DELETE). This cut down on unnecessary function invocations and helped me understand how to build cost-efficient serverless applications.
Reflections
Going through these modules has completely changed how I think about building applications. The serverless paradigm is so different from traditional web development:
- Instead of long-running servers, you have short-lived functions
- Instead of carefully normalized databases, you denormalize everything in DynamoDB
- Instead of managing infrastructure, you wire together managed services
The learning curve is real, though. Each AWS service has its own quirks, pricing model, and best practices. The hardest part isn’t understanding individual services – it’s knowing how to combine them effectively.
What struck me most is how migration strategies reflect different levels of cloud commitment. Rehosting gets you into the cloud quickly but you’re basically just renting VMs. Refactoring requires rethinking your entire architecture but unlocks massive benefits: auto-scaling, pay-per-use pricing, no server management, and built-in high availability.
The Lab Experience Made All the Difference
I can’t emphasize enough how valuable the hands-on lab work was. Reading about Lambda functions is one thing. Actually writing the code, debugging JSON formatting errors, troubleshooting IAM permissions, and watching your function execute in CloudWatch is completely different.
Some of my key takeaways from working in the AWS console:
Error messages are your teachers. Every time something didn’t work – and it often didn’t on the first try – the error message pointed me toward the solution. Lambda couldn’t access DynamoDB? Check IAM permissions. API Gateway returning CORS errors? Add the right headers. VPC subnet can’t reach the internet? Configure your routing table and NAT gateway.
The console is more intuitive than I expected. AWS has a reputation for complexity, and it’s deserved, but the console UI actually guides you through most setup processes pretty well. It won’t make architectural decisions for you, but it will help you execute them.
Breaking things is the best way to learn. The lab environment let me experiment without fear. I deliberately misconfigured security groups to see what would happen. I created Lambda functions with intentional bugs to understand error handling. I set up DynamoDB queries that I knew would be inefficient just to see the performance impact. This kind of experimental learning is impossible with just reading.
Watching services work together is magical. The first time I built a complete pipeline – S3 static site → API Gateway → Lambda → DynamoDB – and saw it all work together, something clicked. Each service was doing its one job well, and the result was a functioning application without me managing a single server.
CloudWatch is indispensable. I thought logging was just for debugging, but CloudWatch logs became my window into understanding what AWS services were actually doing. Every API call, every function execution, every error – it’s all there if you know where to look.
The course provided excellent structured learning, but the lab environment is where that knowledge became muscle memory. I’m genuinely excited to build something real with these skills now.
This post covers modules 2-5 of my AWS Cloud Applications and Architectures course at Quantic School of Business and Technology, including extensive hands-on work in the AWS Management Console.






Leave a Reply