Thirty days in, and today both tasks required deliberately switching off something that exists to protect you. Git refuses to let you overwrite shared history. EC2 refuses to forward packets that aren't addressed to it. Both refusals are correct. Both had to go.
One Git task, one AWS task. Hard reset a branch and force-push it, then build a NAT instance so a private EC2 instance can reach S3. The tasks come from the KodeKloud Engineer platform.
Hard reset: the undo with no record
Three days ago on Day 27, I wrote, in bold, that you should never rewrite history you have already pushed. Today's task is doing precisely that. So let me deal with that head-on rather than pretend the two posts don't touch.
cd /usr/src/kodekloudrepos/official
git branch --show-current
git log --oneline
git reset --hard <commit-id>
git log --oneline
git status
reset --hard moves the branch pointer backwards and takes the working tree and index with it. Everything after that commit is gone from the branch. Compare that to revert, which writes a new commit undoing an old one and leaves both on the record. Revert adds. Reset removes.
There are three modes, and only one is dangerous. --soft moves the pointer and leaves your changes staged. --mixed, the default, moves it and unstages them. --hard moves it and throws away uncommitted work entirely. That last part has no undo — the reflog can recover commits, but nothing recovers changes you never committed.
Then the push is rejected because the branch is no longer a fast-forward. Git is not being difficult; it is telling you that accepting this push would destroy commits sitting on the remote.
# What the task asked for
git push origin master --force
# What you should almost always type instead
git push origin master --force-with-lease
--force-with-lease is the one worth building into your fingers. It checks that the remote is still where you last saw it and refuses if anyone has pushed since. Plain --force overwrites their work silently. When you're working alone, the two are identical. When you're not, one of them costs a colleague their afternoon.
So does Day 27's rule still hold? Yes. Force-pushing a branch only you use, or a lab repo, is fine. Force-pushing a shared branch rewrites history other people already pulled — their next pull produces divergent branches and a merge that reintroduces exactly what you removed. The rule was never "never force-push." It was "know whose history you're rewriting."
The NAT instance: an invisible default that drops everything
A private EC2 instance had a cron job uploading a file to S3 every minute, and every upload was failing. No internet access. The fix is a NAT instance in a public subnet — cheaper than a NAT Gateway, and considerably more educational.
The first lesson arrived before I built anything. The task said the VPC, private subnet and private EC2 already existed, implying I only needed the public subnet and the NAT instance. So I ran discovery anyway:
aws ec2 describe-internet-gateways --region us-east-1 \
--filters "Name=attachment.vpc-id,Values=$VPC" \
--query 'InternetGateways[].InternetGatewayId' --output text
Empty. There was no internet gateway attached to the VPC at all. A NAT instance in a public subnet with no route to an IGW is useless — the NAT instance needs internet access before it can hand any out. The task description was wrong about its own environment, and discovery is what caught it.
With the IGW, public subnet and route table in place, the NAT configuration itself is three things in user-data:
dnf install -y iptables-services
sysctl -w net.ipv4.ip_forward=1
IFACE=$(ip -o -4 route show to default | awk '{print $5}' | head -1)
iptables -t nat -A POSTROUTING -o "$IFACE" -j MASQUERADE
service iptables save
ip_forward is off by default and lets the kernel route packets between interfaces. MASQUERADE rewrites the source IP of outbound packets to the NAT instance's own address and reverses it on the way back — that is literally the network address translation. And IFACE is detected rather than hardcoded, because AL2023 names interfaces ens5, not eth0.
Then the line that decides whether any of it works:
aws ec2 modify-instance-attribute --instance-id $NAT_ID --no-source-dest-check
By default, EC2 drops any packet whose source or destination IP is not the instance's own address. That is a sensible anti-spoofing guardrail, and it is exactly what a NAT instance does all day long. Leave it on, and the instance discards every forwarded packet. No error. No log line. The cron job just keeps failing and everything you built looks correct.
That is what makes it the classic NAT instance necessity: it is not a mistake you can see. It is a default you have to know about in advance.
One more detail worth stealing. When routing the private subnet through the instance, target the ENI, not the instance ID:
ENI=$(aws ec2 describe-instances --instance-ids $NAT_ID \
--query 'Reservations[0].Instances[0].NetworkInterfaces[0].NetworkInterfaceId' --output text)
aws ec2 create-route --route-table-id $PRIV_RTB \
--destination-cidr-block 0.0.0.0/0 --network-interface-id $ENI
And if that route shows blackhole rather than active, the instance wasn't running when you created it. Run aws ec2 wait instance-running first.
For the record: NAT instances are a cost play and an exam topic. In production, you use a NAT Gateway, which is managed, highly available per AZ, and scales to 100 Gbps. You pay per gigabyte for the privilege of never thinking about ip_forward again.
Guardrails you turn off knowingly
Git's push rejection and EC2's source/destination check are the same kind of thing: a default that says no to something that is usually a mistake. Both tasks needed that not to be overridden. The difference between a professional and an incident is whether you knew what the guardrail was for before you switched it off.
So here is the Day 30 question, a third of the way in. When you disable a safety check, is it because you understand why it exists, or because it was in the way?
Day 30 down. Seventy to go.
Top comments (0)