Part 4: Battling Hidden AWS Costs, IP Automation & Lessons Learned

Here is where the next problem starts. I mentioned in a previous post that there was a t4g.small free trial, meaning the EC2 instance itself was free, but that didn't mean the surrounding infrastructure was. The EBS volume was still paid, but even worse, the IPv4 address was now costing me just to keep it alive. Because public IPv4 addresses are in high demand, acquiring them has become expensive for cloud providers, leading AWS to pass that cost onto users with a flat hourly charge. IPv6, on the other hand, is free to use, which made me question why I was paying for an IPv4 address at all.

Ipv4 Saga

The core philosophy was to keep monthly costs as low as possible, but it’s always a balancing act. Some services are worth paying for because the value far outweighs the price tag; an EBS volume is dirt cheap compared to the persistence and convenience it provides.

With that in mind, the moment I saw that IPv4 carried a monthly charge while IPv6 was completely free, I was on a mission to run the entire setup purely on IPv6. That drive lasted about five minutes. I hit an immediate roadblock when the instance couldn't even reach external package mirrors or install Docker, simply because so many repositories and endpoints don't serve traffic over IPv6.

An alternative method I tried with IPv6 was to utilise Cloudflare tunnels, install it with IPv4, then swap to IPv6. Surprisingly, this worked perfectly; however, one of the core philosophies of this project was to stick to AWS tooling, and so I skipped Cloudflare tunnels.

Stumbling back to IPv4

After the headache of wrestling with IPv6, I finally threw in the towel and circled back to IPv4; IPv6 introduced too many friction points, making it less viable for my use case. With CloudFront delivering content, handling both static assets from S3 and proxying traffic to the backend, the last thing was to grab a phonetically intuitive domain.

Searching for a decent .com for a normal price is an exhausting chore in itself thanks to domain squatters sitting on everything usable, but I managed to snag resonistic.com for around £12.

Once the domain was in check, I went into AWS Certificate Manager (ACM) to get an SSL certificate provisioned for HTTPS. Since CloudFront is a global edge service, any custom certificate attached to a distribution must be requested in us-east-1 (N. Virginia), regardless of where your actual backend instances or hosted zones are located.

Now, just because I gave up with the switch over to IPv6 doesn't mean I'm not still going to try and optimise my running costs. The EIP attached to my EC2 instance bills me the same $0.005 every hour regardless of whether it's on a running instance or not. Since this project is in early stages and mainly focused on availability for recruiters, I only cared if this app ran during the weekdays' work hours, so I decided to automate shutting down and booting the EC2, releasing the IP to save money during this time.

EC2 Automation

The first step to reducing the monthly cost built up from the Elastic IP was to remove the EIP completely and swap back to the default auto-assigned IP addresses. An EIP has a fixed cost regardless of whether it's attached or not, but in my case, it wasn't possible to both keep an EIP and not pay for it during downtime. An auto-assigned IPV4 address would only charge me while the EC2 instance was running, but the IP was lost when the EC2 instance was stopped.

I circumvented the issue by rotating IPv4 addresses by setting up an AWS Lambda function with boto3 which, when triggered, will invoke the EC2 instance to boot up, then wait for an IPv4 address to be assigned, then update this IP into a dummy record in AWS Route 53, specifically origin.resonistic.com and this domain is then used with CloudFront to route traffic correctly.

CloudFront custom origins strictly require a fully qualified domain name rather than an IP address; this intermediate record acts as the dynamic pointer without needing distribution updates. Another issue is DNS propagation for CloudFront: when the intermediate record is updated, CloudFront will take some time to notice that it's changed and to combat this, a lower-than-normal TTL was implemented at 60s.

The EC2 instance has a security group only allowing traffic from CloudFront endpoints; origin.resonistic.com wouldn't allow direct requests to the server. I had set up an equivalent shutdown script for the EC2 instance too, but without overriding the Route 53 record.

🤔
I'm generally sceptical of serverless for public-facing production workloads due to unpredictable scaling costs. However, for deterministic, strictly bounded tasks (like firing twice a day), Lambda's free tier is unbeatable.

To then automate these Lambda functions, I utilised AWS EventBridge to invoke the functions regularly with cron-like syntax.

Conclusion and Learnings

We're finally at the end of the build. Taking what I learned while prepping for the AWS DVA-C02, RAVN is now up and running live on AWS. Naturally, building it solo meant running into a few dead ends and making design choices I’d do differently today. Here’s a look at those trade-offs before we get into what’s next for the project.

Project shortcomings

Cloudfront security

A subtle gap in my security setup involves the EC2-to-CloudFront security group. Currently, the security group drops all inbound traffic except for CloudFront’s managed prefix list, which sounds completely secure on paper. However, that prefix list covers all of AWS's edge IPs globally; anyone could configure their own CloudFront distribution to point directly to my EC2 origin. A clean way to mitigate this is attaching a custom secret header (X-Origin-Verify: <secret>) in CloudFront origin request settings and having the server reject any inbound request lacking that exact token. This only allows my CloudFront distribution to communicate with my server.

Over-Engineering Early: The Protobuf Repository Overhead

I tried to be too clever early on. Instead of keeping things simple, I set up a completely separate repository just to hold shared .proto files. Total overkill. A single field change meant editing the proto repo, pushing a commit, pulling updates into the backend services, and rerunning protoc everywhere just to test one thing. It was pure overhead. Protobuf binary serialisation is fast, but managing an isolated schema repo completely killed my momentum.

IaC and Moving Away from the Console

Early on, I leaned heavily on the AWS Console, which was genuinely useful for building mental mind maps of where everything was. Clicking through the UI and seeing how VPCs, target groups, and CloudFront distributions connect made the initial setup easy to grasp. However, it wasn't sustainable. Setting things up by hand meant no version control, zero traceability for configuration drift, and zero automated reproducibility. The obvious next step is codifying the whole stack in Terraform—giving me a Git-backed source of truth that I can tear down and spin back up on command without leaving orphaned services burning cash.

What's next?

First up is tackling some tech debt. While the background ingestion pipeline is already running asynchronously on SQS, the initial Hevy sync route is still currently synchronous. Swapping that initial call to asynchronous would make initial setup snappier for the user.

Beyond backend cleanup, the real priority is mobile. I tried dabbling with the idea of Progressive Web Apps (PWA), which allows websites to appear as native apps on mobile phones. However, the friction of asking users to install the app as a PWA means most people would avoid it, even I'd avoid it. Throw in iOS's aggressive storage eviction policies, and it becomes completely nonviable for a fitness analytics app.

The plan would be to bite the bullet and go native with mobile and use Capacitor. This framework allows me to keep the web ui intact with extra mobile-specific features. That also means picking up a cheap Mac mini to sit on the local network as a dedicated build runner for Xcode and iOS app signing, but having an actual binary on someone's phone with native background sync is worth the hardware tax.

If you've made it through the entire series, thanks for reading. Building and documenting RAVN in public has been a massive learning experience, and hopefully, walking through these architectural trade-offs saves someone else from making the same mistakes.