In 2010 we had a dedicated server in a colocation datacenter. A 1U rack-mount machine, dual Xeon processors, 16GB RAM, RAID-1 storage array. It cost $400/month for the rack space plus the upfront hardware cost of around $3,500. Provisioning a new server meant ordering hardware, waiting for delivery, physically installing it (or paying the datacenter to do it), and installing the OS over KVM.
New project? Same server. It ran Apache, MySQL, PHP, and a growing collection of client sites that had no business sharing the same machine. Deploying meant SSH-ing in and copying files. "Staging environment" meant a subdirectory.
Amazon Web Services launched EC2 in August 2006. We ignored it for years - the pricing was confusing, the tooling was primitive, and "the cloud" sounded like a marketing term for someone else's computer. By 2010 we started paying attention. By 2013 we were committed.
The EC2 Learning Curve
AWS in 2010 looked nothing like AWS today. The management console was rudimentary. CloudFormation didn't exist. Auto Scaling existed but was complex to configure. The primary tool was the AWS CLI and hand-crafted shell scripts.
Our first EC2 instance (September 2010):
# AWS CLI v1 (Ruby-based, before Python rewrite)
# Launch a t1.micro instance with Ubuntu AMI
ec2-run-instances ami-6936fb00 \
--instance-type t1.micro \
--key-name aunimeda-key \
--group web-sg \
--region us-east-1
# Check it launched
ec2-describe-instances
# Allocate and associate an Elastic IP (static IP)
ec2-allocate-address
ec2-associate-address -i i-12345678 54.23.45.67
# SSH in
ssh -i ~/.ssh/aunimeda-key.pem ubuntu@54.23.45.67
Then the familiar Linux setup: apt-get install apache2 mysql-server php5... exactly the same as a physical machine, but without the hardware delivery wait. The first time we launched a server in 90 seconds rather than 3 weeks, the business model changed.
The pricing revelation. A t1.micro was $0.02/hour in 2010. $14.40/month. For a development environment that we only needed during business hours, we could stop it evenings and weekends: 10 hours/day × 5 days/week × 4.3 weeks = ~215 hours/month × $0.02 = $4.30/month. Our $400/month dedicated server was running 24/7 serving sites that got 500 visitors/day. The economics were wrong.
The S3 Revelation
Amazon S3 (Simple Storage Service) launched in March 2006. We started using it seriously in 2011.
Before S3: user-uploaded files went to the web server's filesystem. Backups were rsync to another server. If the server filled up, you bought a bigger disk (not possible in EC2 without effort) or cleaned up old files. If the server died, files were potentially lost.
// Old approach: files on the web server filesystem
$uploadDir = '/var/www/html/uploads/';
$filename = time() . '_' . $_FILES['image']['name'];
$filePath = $uploadDir . $filename;
if (move_uploaded_file($_FILES['image']['tmp_name'], $filePath)) {
$imageUrl = 'https://yoursite.com/uploads/' . $filename;
saveToDatabase($imageUrl);
}
// New approach: files on S3
require 'vendor/autoload.php';
use Aws\S3\S3Client;
$s3 = S3Client::factory([
'key' => 'YOUR_AWS_KEY',
'secret' => 'YOUR_AWS_SECRET',
'region' => 'us-east-1',
]);
$filename = time() . '_' . basename($_FILES['image']['name']);
$bucket = 'aunimeda-uploads';
try {
$result = $s3->putObject([
'Bucket' => $bucket,
'Key' => 'images/' . $filename,
'SourceFile' => $_FILES['image']['tmp_name'],
'ContentType' => $_FILES['image']['type'],
'ACL' => 'public-read', // Publicly accessible
]);
$imageUrl = $result['ObjectURL']; // https://s3.amazonaws.com/bucket/key
saveToDatabase($imageUrl);
} catch (Aws\S3\Exception\S3Exception $e) {
error_log('S3 upload failed: ' . $e->getMessage());
// Fallback to local upload
}
The consequences: uploads survived server replacements. Storage scaled without capacity planning. S3's durability guarantee (99.999999999% - eleven 9s) was vastly better than a single-disk RAID array. And the cost was $0.023/GB/month - essentially free for most use cases.
Heroku: Zero-Infrastructure Deployment
Heroku launched public beta in 2009, full launch in 2011. It abstracted away all infrastructure: no SSH, no server configuration, no Apache setup. Just push code.
# The Heroku workflow (2011)
heroku create aunimeda-app
# Configure environment variables (no .env files in production)
heroku config:set DATABASE_URL=postgres://...
heroku config:set SECRET_KEY=randomstring
heroku config:set S3_BUCKET=aunimeda-production
# Deploy: git push = deploy
git push heroku main
# Scale
heroku ps:scale web=2 # Two web dynos
heroku ps:scale worker=1 # One background worker
# Logs
heroku logs --tail
# Database (Heroku Postgres)
heroku addons:create heroku-postgresql:dev
heroku pg:info
heroku pg:psql # Direct database console
The Procfile defined what ran:
# Procfile
web: gunicorn app:app
worker: python worker.py
For a startup with no ops engineers, Heroku was transformative. A solo developer could deploy a production Rails or Python app with a Postgres database, a Redis cache, and background workers in 30 minutes. No SysAdmin degree required.
The cost: Heroku's dynos were expensive at scale compared to raw EC2. A 1X dyno ($25/month) had 512MB RAM. A small AWS t2.small ($17/month) had 2GB RAM. At scale, the infrastructure cost difference was significant.
The tradeoff we found: Heroku for early-stage projects (under $2000/month infrastructure cost), AWS for mature products (where ops investment was justified by savings).
The Auto-Scaling Moment
The most powerful AWS feature wasn't EC2 or S3. It was Auto Scaling Groups - servers that scaled up under load and back down when traffic dropped.
Our first real scaling event happened during a client's TV advertisement. A regional retailer ran a 30-second ad at 9pm prime time. Their product page got 2,400 concurrent visitors in the 5 minutes after the ad aired, versus a normal load of 40 concurrent users.
Without auto-scaling: server would have gone down.
With Auto Scaling:
# Auto Scaling Group configuration (2012 CLI)
as-create-auto-scaling-group aunimeda-web-asg \
--launch-configuration web-server-lc \
--min-size 1 \
--max-size 10 \
--desired-capacity 2 \
--availability-zones us-east-1a us-east-1b \
--load-balancers aunimeda-elb
# Scale-up policy: add 2 servers when CPU > 70% for 3 minutes
as-put-scaling-policy scale-up \
--auto-scaling-group aunimeda-web-asg \
--adjustment-type ChangeInCapacity \
--scaling-adjustment 2 \
--cooldown 300
# CloudWatch alarm triggers scale-up
mon-put-metric-alarm high-cpu \
--alarm-actions arn:aws:autoscaling:...:scalingPolicy:...scale-up \
--metric-name CPUUtilization \
--namespace AWS/EC2 \
--statistic Average \
--period 180 \
--threshold 70 \
--comparison-operator GreaterThanOrEqualToThreshold \
--dimensions AutoScalingGroupName=aunimeda-web-asg
During the TV ad event, 6 servers spun up within 4 minutes. Traffic handled. Ad over, servers terminated 30 minutes later. Total extra cost: ~$0.40. If we'd provisioned for peak traffic on dedicated hardware, we'd have had 6 servers running idle 23.5 hours per day.
The Shift in How We Thought
The most fundamental change from physical to cloud wasn't technical. It was psychological.
Physical servers were pets: named, carefully maintained, upgraded individually, mourned when they died. Cloud servers were cattle: numbered, replaceable, terminated without ceremony when they misbehaved.
This shift enabled automated deployments without fear: if a deployment broke a server, terminate it and launch a new one from a known-good AMI. It enabled blue-green deployments: launch new servers with new code, route traffic to them, terminate old servers. No maintenance windows. No SSH-in-and-patch-carefully.
The cloud migration wasn't just moving infrastructure to someone else's hardware. It was changing the relationship between software developers and operations - the beginning of what would be called DevOps.
Aunimeda provides DevOps engineering and infrastructure services - CI/CD pipelines, containerization, cloud deployments, and monitoring setups.
Contact us to discuss your infrastructure needs. See also: DevOps Services, Custom Software Development