MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Introduction to AWS: Services Every Developer Should Know

Essential AWS services: EC2, S3, RDS, Lambda, CloudFront, and IAM.

AWSCloudInfrastructureDevOps

By MinhVo

Introduction

Amazon Web Services (AWS) is the world's most comprehensive and widely adopted cloud platform, offering over 200 fully featured services from data centers globally. Launched in 2006 with S3 and EC2, AWS has grown to power millions of customers—from startups to enterprises like Netflix, Airbnb, and NASA—and commands roughly 31% of the global cloud infrastructure market. For developers, understanding AWS is no longer a specialization; it's a fundamental skill required for building modern applications.

The sheer breadth of AWS services can be overwhelming for newcomers. With compute options ranging from virtual machines (EC2) to serverless functions (Lambda), storage choices from object stores (S3) to managed databases (RDS), and networking tools from load balancers to CDN, knowing which services to use and when is critical. Misusing services or over-architecting can lead to unnecessary complexity and inflated costs, while under-utilizing AWS's capabilities means leaving performance and reliability on the table.

This guide focuses on the six essential AWS services every developer should understand deeply: EC2 for compute, S3 for storage, RDS for databases, Lambda for serverless, CloudFront for content delivery, and IAM for security. We'll explore each service's architecture, practical usage patterns, cost optimization strategies, and real-world integration patterns that form the foundation of most AWS architectures.

Cloud computing infrastructure and data centers

Understanding AWS: Core Services Architecture

AWS organizes its services around regions and availability zones. A region is a geographical area (e.g., us-east-1, eu-west-1) containing multiple availability zones (AZs)—isolated data centers with independent power, networking, and connectivity. Deploying across multiple AZs provides high availability and fault tolerance, as failures in one AZ don't affect others.

EC2: Elastic Compute Cloud

EC2 provides resizable virtual machines (instances) in the cloud. You choose an instance type based on your workload requirements: general-purpose (t3, m5), compute-optimized (c5), memory-optimized (r5), storage-optimized (i3), or GPU instances (p3, g4). Each instance runs an AMI (Amazon Machine Image) containing your operating system and applications.

EC2 pricing models include On-Demand (pay per hour/second), Reserved Instances (1-3 year commitment for 30-72% savings), Spot Instances (bid for unused capacity at up to 90% discount), and Savings Plans (flexible commitment across instance families). For development environments, Spot Instances dramatically reduce costs; for production, Reserved Instances or Savings Plans provide predictable pricing.

S3: Simple Storage Service

S3 is an object storage service designed for 99.999999999% (11 nines) durability. Objects are stored in buckets with a flat namespace—there are no real folders, though key prefixes simulate directory structures. S3 supports multiple storage classes optimized for different access patterns: S3 Standard for frequently accessed data, S3 Intelligent-Tiering for automatic cost optimization, S3 Glacier for archival storage, and S3 Glacier Deep Backup for long-term retention.

RDS: Relational Database Service

RDS manages relational databases including PostgreSQL, MySQL, MariaDB, Oracle, SQL Server, and Amazon Aurora. RDS handles provisioning, patching, backup, recovery, and scaling. Aurora, AWS's proprietary database engine, offers MySQL and PostgreSQL compatibility with up to 5x better performance than standard MySQL and 3x better than PostgreSQL.

Lambda: Serverless Functions

Lambda runs your code in response to events without provisioning or managing servers. You upload your function code, configure memory (128MB to 10GB) and timeout (up to 15 minutes), and Lambda handles everything else—provisioning, scaling, patching, and logging. Lambda scales automatically from zero to thousands of concurrent executions in seconds.

CloudFront: Content Delivery Network

CloudFront is AWS's CDN, caching your content at 400+ edge locations worldwide. It reduces latency by serving content from the nearest edge location, supports HTTPS, and integrates natively with S3, EC2, and Lambda@Edge for dynamic content manipulation at the edge.

IAM: Identity and Access Management

IAM controls who can access which AWS resources. It manages users, groups, roles, and policies with fine-grained permissions. The principle of least privilege should guide every IAM decision—grant only the minimum permissions required for each user, service, and application.

Cloud architecture and distributed systems

Architecture and Design Patterns

The Three-Tier Architecture on AWS

The most common AWS architecture separates presentation (CloudFront + S3), application (EC2 or Lambda behind ALB), and data (RDS + ElastiCache) tiers. Each tier scales independently and can be secured with appropriate IAM policies and security groups.

The Serverless Architecture

A serverless architecture eliminates server management by combining Lambda for compute, API Gateway for HTTP routing, DynamoDB for data, and S3 for static assets. This architecture scales to zero when idle (paying nothing) and scales automatically under load, making it ideal for variable-traffic applications.

The Microservices Pattern

AWS supports microservices through ECS (Elastic Container Service) or EKS (Elastic Kubernetes Service) for container orchestration, SQS/SNS for inter-service communication, and API Gateway for service mesh routing. Each microservice owns its data store and communicates asynchronously through events.

Step-by-Step Implementation

Let's build a practical application on AWS using EC2, S3, RDS, Lambda, and CloudFront.

Setting Up IAM Users and Policies

// IAM policy for a developer role
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:StartInstances",
        "ec2:StopInstances",
        "s3:GetObject",
        "s3:PutObject",
        "rds:DescribeDBInstances",
        "lambda:InvokeFunction",
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

Deploying an Application on EC2

#!/bin/bash
# User data script for EC2 instance bootstrap
# Runs automatically when the instance starts
 
# Update system packages
yum update -y
 
# Install Node.js 20
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
yum install -y nodejs
 
# Install PM2 for process management
npm install -g pm2
 
# Clone application
cd /home/ec2-user
git clone https://github.com/myorg/my-app.git
cd my-app
 
# Install dependencies and start
npm ci --production
pm2 start ecosystem.config.js
pm2 startup
pm2 save

Creating an S3 Bucket with Static Website Hosting

import { S3Client, PutObjectCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
 
const s3 = new S3Client({ region: 'us-east-1' });
 
async function deployStaticSite(bucketName: string, files: Map<string, Buffer>) {
  // Create bucket
  await s3.send(new CreateBucketCommand({ Bucket: bucketName }));
 
  // Upload files
  for (const [key, content] of files) {
    const contentType = key.endsWith('.html') ? 'text/html'
      : key.endsWith('.css') ? 'text/css'
      : key.endsWith('.js') ? 'application/javascript'
      : key.endsWith('.png') ? 'image/png'
      : 'application/octet-stream';
 
    await s3.send(new PutObjectCommand({
      Bucket: bucketName,
      Key: key,
      Body: content,
      ContentType: contentType,
      CacheControl: key.includes('/static/') ? 'max-age=31536000' : 'no-cache',
    }));
  }
 
  console.log(`Deployed ${files.size} files to s3://${bucketName}`);
}

Setting Up an RDS PostgreSQL Database

// Using AWS CDK to provision RDS
import * as cdk from 'aws-cdk-lib';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
 
export class DatabaseStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string) {
    super(scope, id);
 
    const vpc = new ec2.Vpc(this, 'AppVpc', { maxAzs: 2 });
 
    const cluster = new rds.DatabaseCluster(this, 'AppDatabase', {
      engine: rds.DatabaseClusterEngine.auroraPostgres({
        version: rds.AuroraPostgresEngineVersion.VER_15_4,
      }),
      credentials: rds.Credentials.fromGeneratedSecret('dbadmin'),
      defaultDatabaseName: 'myapp',
      vpc,
      writer: rds.ClusterInstance.provisioned('Writer', {
        instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
      }),
      readers: [
        rds.ClusterInstance.provisioned('Reader', {
          instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
        }),
      ],
      backup: { retention: cdk.Duration.days(7) },
      deletionProtection: true,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
    });
  }
}

Building a Lambda Function

// lambda/handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDB } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
 
const dynamo = DynamoDBDocument.from(new DynamoDB({}));
 
export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
  const tableName = process.env.TABLE_NAME!;
  const body = JSON.parse(event.body || '{}');
 
  try {
    switch (event.httpMethod) {
      case 'GET': {
        const id = event.pathParameters?.id;
        if (!id) {
          const result = await dynamo.scan({ TableName: tableName });
          return response(200, result.Items);
        }
        const item = await dynamo.get({ TableName: tableName, Key: { id } });
        return item.Item ? response(200, item.Item) : response(404, { error: 'Not found' });
      }
 
      case 'POST': {
        const newItem = { id: crypto.randomUUID(), ...body, createdAt: new Date().toISOString() };
        await dynamo.put({ TableName: tableName, Item: newItem });
        return response(201, newItem);
      }
 
      case 'PUT': {
        const updateId = event.pathParameters?.id;
        await dynamo.update({
          TableName: tableName,
          Key: { id: updateId },
          UpdateExpression: 'SET #data = :data, updatedAt = :now',
          ExpressionAttributeNames: { '#data': 'data' },
          ExpressionAttributeValues: { ':data': body.data, ':now': new Date().toISOString() },
        });
        return response(200, { id: updateId, ...body });
      }
 
      case 'DELETE': {
        const deleteId = event.pathParameters?.id;
        await dynamo.delete({ TableName: tableName, Key: { id: deleteId } });
        return response(204, null);
      }
 
      default:
        return response(405, { error: 'Method not allowed' });
    }
  } catch (error) {
    console.error('Error:', error);
    return response(500, { error: 'Internal server error' });
  }
}
 
function response(statusCode: number, body: any): APIGatewayProxyResult {
  return {
    statusCode,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*',
    },
    body: body ? JSON.stringify(body) : '',
  };
}

Configuring CloudFront Distribution

// AWS CDK for CloudFront
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3 from 'aws-cdk-lib/aws-s3';
 
const bucket = new s3.Bucket(this, 'StaticAssets', {
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
 
const distribution = new cloudfront.Distribution(this, 'Distribution', {
  defaultBehavior: {
    origin: new origins.S3Origin(bucket),
    viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
    cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
    compress: true,
  },
  additionalBehaviors: {
    '/api/*': {
      origin: new origins.HttpOrigin('api.example.com'),
      cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
      viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
    },
  },
  defaultRootObject: 'index.html',
  priceClass: cloudfront.PriceClass.PRICE_CLASS_100,
});

Cloud deployment and DevOps workflow

Real-World Use Cases

Use Case 1: Static Website with Dynamic API

A marketing site uses S3 for static assets, CloudFront for global delivery, Lambda + API Gateway for a contact form handler, and DynamoDB for form submissions. Total cost for low-traffic sites: under $1/month due to Lambda's free tier (1M requests/month) and S3's low storage costs.

Use Case 2: Scalable Web Application

A SaaS application uses EC2 Auto Scaling behind an Application Load Balancer, RDS Aurora for the primary database, ElastiCache Redis for session storage and caching, and S3 for user uploads. Auto Scaling policies adjust instance count based on CPU utilization, maintaining response times during traffic spikes.

Use Case 3: Data Processing Pipeline

A data analytics platform uses S3 as a data lake, Lambda for ETL processing triggered by S3 events, Athena for ad-hoc SQL queries against S3 data, and QuickSight for visualization. This architecture processes terabytes of data without managing a single server.

Use Case 4: Real-Time Notification System

A notification service uses SNS for pub/sub messaging, SQS for reliable message queuing, Lambda for processing notifications, and SES for email delivery. This decoupled architecture handles millions of notifications per day with automatic retry and dead-letter queue support.

Best Practices for Production

  1. Use IAM roles, not access keys: Assign IAM roles to EC2 instances and Lambda functions instead of embedding access keys. Roles provide temporary credentials that rotate automatically, eliminating the risk of leaked keys.

  2. Enable MFA on all IAM accounts: Multi-factor authentication adds a second layer of security to console and programmatic access. Use hardware MFA tokens for root accounts and virtual MFA for regular users.

  3. Tag everything: Implement a consistent tagging strategy (environment, team, project, cost-center) to track costs, automate operations, and enforce policies. Use AWS Tag Editor and tag policies to enforce compliance.

  4. Use VPC endpoints for AWS services: Access S3, DynamoDB, and other AWS services through VPC endpoints instead of public internet. This reduces latency, eliminates NAT Gateway costs, and improves security.

  5. Enable CloudTrail and Config: CloudTrail logs all API calls for audit and compliance. AWS Config tracks resource configurations and detects drift from your desired state.

  6. Implement backup strategies: Use RDS automated backups with point-in-time recovery, enable S3 versioning for critical buckets, and create AMIs for EC2 instances. Test restore procedures regularly.

  7. Use Infrastructure as Code: Define all resources using AWS CDK, CloudFormation, or Terraform. This enables version control, reproducible environments, and automated deployments.

  8. Monitor with CloudWatch: Set up alarms for critical metrics (CPU, memory, disk, error rates), create dashboards for visibility, and use CloudWatch Logs Insights for log analysis.

Common Pitfalls and Solutions

PitfallImpactSolution
Using root account for daily operationsSecurity risk, no audit trailCreate IAM users with appropriate roles; use root only for account-level operations
Not configuring billing alertsUnexpected chargesSet up AWS Budgets with alerts at 50%, 80%, and 100% of expected spend
Storing secrets in environment variablesSecurity breach riskUse AWS Secrets Manager or Parameter Store for sensitive configuration
Over-provisioning instancesWasted costsUse Auto Scaling, right-size instances with AWS Compute Optimizer, use Spot for non-critical workloads
No disaster recovery planExtended downtime during outagesDeploy across multiple AZs, use cross-region replication for critical data, test failover regularly

Performance Optimization

// Lambda function optimization
// 1. Use Lambda Layers for shared dependencies
// 2. Set appropriate memory (memory also controls CPU)
// 3. Use provisioned concurrency for latency-sensitive functions
 
import { LambdaClient, PutFunctionConcurrencyCommand } from '@aws-sdk/client-lambda';
 
const lambda = new LambdaClient({ region: 'us-east-1' });
 
// Provision 10 concurrent instances to eliminate cold starts
await lambda.send(new PutFunctionConcurrencyCommand({
  FunctionName: 'my-api-handler',
  ReservedConcurrentExecutions: 10,
}));
 
// S3 performance: use multipart uploads for large files
import { Upload } from '@aws-sdk/lib-storage';
import { S3Client } from '@aws-sdk/client-s3';
 
const s3 = new S3Client({ region: 'us-east-1' });
 
async function uploadLargeFile(bucket: string, key: string, body: ReadableStream) {
  const upload = new Upload({
    client: s3,
    params: { Bucket: bucket, Key: key, Body: body },
    queueSize: 4,     // Parallel upload parts
    partSize: 10 * 1024 * 1024, // 10MB per part
    leavePartsOnError: false,
  });
 
  upload.on('httpUploadProgress', (progress) => {
    console.log(`Uploaded ${progress.loaded} of ${progress.total}`);
  });
 
  await upload.done();
}

Comparison with Alternatives

FeatureAWSGoogle CloudAzureDigitalOcean
Service breadth200+ services150+ services200+ services30+ services
Market share~31%~11%~25%~2%
Compute optionsEC2, Lambda, ECS, EKSGCE, Cloud Run, GKEVMs, Functions, AKSDroplets, App Platform
DatabaseRDS, Aurora, DynamoDBCloud SQL, Spanner, FirestoreSQL DB, Cosmos DBManaged Databases
CDNCloudFrontCloud CDNAzure CDNCDN add-on
Pricing complexityHighMediumHighLow
Enterprise featuresExtensiveGrowingExtensiveLimited
Learning curveSteepModerateSteepGentle

AWS offers the broadest service catalog and most mature ecosystem. Google Cloud excels in data analytics and machine learning. Azure integrates deeply with Microsoft tools. DigitalOcean targets developers who want simplicity. Choose based on your team's expertise, required services, and existing vendor relationships.

Advanced Patterns and Techniques

Multi-Region Active-Active Architecture

For applications requiring global low latency and high availability:

// Route 53 latency-based routing configuration
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
 
// Deploy identical stacks in us-east-1 and eu-west-1
// Configure Route 53 with latency-based routing
new route53.ARecord(this, 'ApiUsEast', {
  zone: hostedZone,
  target: route53.RecordTarget.fromAlias(new targets.ApiGateway(restApi)),
  region: 'us-east-1',
});
 
new route53.ARecord(this, 'ApiEuWest', {
  zone: hostedZone,
  target: route53.RecordTarget.fromAlias(new targets.ApiGateway(restApiEu)),
  region: 'eu-west-1',
});

Event-Driven Architecture with EventBridge

import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
 
// Create an event bus for custom events
const eventBus = new events.EventBus(this, 'AppEventBus', {
  eventBusName: 'app-events',
});
 
// Rule: route order events to the fulfillment service
new events.Rule(this, 'OrderRule', {
  eventBus,
  eventPattern: {
    source: ['myapp.orders'],
    detailType: ['OrderCreated'],
  },
  targets: [new targets.LambdaFunction(fulfillmentLambda)],
});
 
// Rule: route all events to the analytics pipeline
new events.Rule(this, 'AnalyticsRule', {
  eventBus,
  eventPattern: { source: [{ prefix: 'myapp.' }] },
  targets: [new targets.SqsQueue(analyticsQueue)],
});

Testing Strategies

// Local testing with LocalStack
import { S3Client, CreateBucketCommand, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
 
// Point SDK to LocalStack
const s3 = new S3Client({
  endpoint: 'http://localhost:4566',
  region: 'us-east-1',
  credentials: { accessKeyId: 'test', secretAccessKey: 'test' },
  forcePathStyle: true,
});
 
describe('S3 Integration Tests', () => {
  const bucketName = 'test-bucket';
 
  beforeAll(async () => {
    await s3.send(new CreateBucketCommand({ Bucket: bucketName }));
  });
 
  test('uploads and retrieves object', async () => {
    await s3.send(new PutObjectCommand({
      Bucket: bucketName,
      Key: 'test.txt',
      Body: 'Hello, World!',
      ContentType: 'text/plain',
    }));
 
    const result = await s3.send(new GetObjectCommand({
      Bucket: bucketName,
      Key: 'test.txt',
    }));
 
    const body = await result.Body!.transformToString();
    expect(body).toBe('Hello, World!');
  });
});
 
// Lambda testing with SAM CLI
// sam local invoke MyFunction --event event.json
// sam local start-api

Future Outlook

AWS continues to innovate at a rapid pace. Key trends include: the growing adoption of serverless architectures that eliminate infrastructure management; the rise of edge computing with Lambda@Edge and CloudFront Functions; the integration of generative AI services like Amazon Bedrock and SageMaker for building AI-powered applications; and the expansion of Graviton (ARM-based) instances offering better price-performance for most workloads.

The AWS Well-Architected Framework provides a structured approach to building cloud-native applications across five pillars: operational excellence, security, reliability, performance efficiency, and cost optimization. Adopting these principles from the start prevents costly re-architecture later.

Conclusion

AWS provides the building blocks for virtually any application architecture, from simple static sites to global distributed systems. The six services covered—EC2, S3, RDS, Lambda, CloudFront, and IAM—form the foundation of most AWS architectures. Understanding their capabilities, limitations, and cost models is essential for making informed architectural decisions.

Key takeaways: Always follow the principle of least privilege with IAM. Use S3 for object storage and static hosting with CloudFront for global delivery. Choose between EC2 (full control), Lambda (serverless), or containers (ECS/EKS) based on your application's compute requirements. Implement monitoring, backup, and disaster recovery from day one.

Start by creating an AWS Free Tier account and deploying a simple application using S3, Lambda, and API Gateway. The AWS documentation and tutorials at aws.amazon.com/getting-started provide hands-on labs for each service. For cost optimization, use AWS Cost Explorer and set up billing alerts before deploying anything to production.