1. Pengenalan AWS Lambda & Serverless
AWS Lambda adalah layanan compute serverless dari Amazon Web Services yang memungkinkan Anda menjalankan kode tanpa perlu mengelola server. Anda cukup mengupload kode, mengkonfigurasi trigger, dan Lambda akan menangani sisanya — provisioning, scaling, patching, dan operasional server.
Konsep serverless bukan berarti tidak ada server — server tetap ada, tetapi Anda tidak perlu mengelolanya. AWS menangani seluruh infrastruktur, dan Anda hanya membayar untuk waktu eksekusi yang benar-benar digunakan (per milidetik).
Serverless vs Traditional Architecture
| Aspek | Traditional (EC2) | Serverless (Lambda) |
|---|---|---|
| Server Management | 🔴 Anda kelola OS, patch, scaling | 🟢 AWS kelola semuanya |
| Scaling | 🟡 Manual atau Auto Scaling Group | 🟢 Otomatis hingga ribuan concurrent |
| Billing | 🔴 Per jam (meski idle) | 🟢 Per milidetik eksekusi |
| Min Cost | 🔴 ~$5/bulan (t3.micro) | 🟢 $0 (free tier: 1M requests/bulan) |
| Cold Start | 🟢 Selalu running | 🟡 Ada cold start delay |
| Execution Limit | 🟢 Tidak terbatas | 🔴 Max 15 menit per invocation |
| State | 🟢 Stateful | 🔴 Stateless (perlu external storage) |
| Use Case | Long-running, stateful apps | Event-driven, API, microservices |
Bahasa Pemrograman yang Didukung
| Bahasa | Runtime | Cocok Untuk |
|---|---|---|
| Node.js | 18, 20, 22 | API, webhooks, data processing |
| Python | 3.10, 3.11, 3.12 | ML inference, data analysis, automation |
| Java | 17, 21 | Enterprise apps, heavy processing |
| Go | Provided.al2023 | High-performance, CLI tools |
| .NET (C#) | 6, 8 | Enterprise .NET apps |
| Ruby | 3.3 | Web apps, scripting |
| Custom Runtime | provided.al2023 | Bahasa apapun (Rust, PHP, dll) |
2. Function Pertama
Hello World dengan Node.js
REST API Handler
// Lambda: Full REST API handler dengan routing
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || 'users';
exports.handler = async (event) => {
console.log('Event:', JSON.stringify(event, null, 2));
const { httpMethod, path, pathParameters, queryStringParameters, body } = event;
const userId = pathParameters?.userId;
try {
// Simple router
switch (httpMethod) {
case 'GET':
if (userId) {
return await getUser(userId);
}
return await listUsers(queryStringParameters);
case 'POST':
return await createUser(JSON.parse(body));
case 'PUT':
return await updateUser(userId, JSON.parse(body));
case 'DELETE':
return await deleteUser(userId);
default:
return response(405, { error: 'Method not allowed' });
}
} catch (error) {
console.error('Error:', error);
return response(500, { error: 'Internal server error' });
}
};
// Helper: Format response
function response(statusCode, body) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS'
},
body: JSON.stringify(body)
};
}
// CRUD Operations
async function getUser(userId) {
const result = await dynamodb.get({
TableName: TABLE_NAME,
Key: { userId }
}).promise();
if (!result.Item) {
return response(404, { error: 'User not found' });
}
return response(200, result.Item);
}
async function listUsers(queryParams) {
const limit = parseInt(queryParams?.limit) || 20;
const params = {
TableName: TABLE_NAME,
Limit: limit
};
// Pagination
if (queryParams?.lastKey) {
params.ExclusiveStartKey = JSON.parse(
Buffer.from(queryParams.lastKey, 'base64').toString()
);
}
const result = await dynamodb.scan(params).promise();
return response(200, {
items: result.Items,
count: result.Count,
lastKey: result.LastEvaluatedKey
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64')
: null
});
}
async function createUser(data) {
const user = {
userId: data.userId || AWS.util.uuid.v4(),
name: data.name,
email: data.email,
createdAt: new Date().toISOString()
};
await dynamodb.put({
TableName: TABLE_NAME,
Item: user,
ConditionExpression: 'attribute_not_exists(userId)'
}).promise();
return response(201, user);
}
async function updateUser(userId, data) {
const result = await dynamodb.update({
TableName: TABLE_NAME,
Key: { userId },
UpdateExpression: 'SET #name = :name, email = :email, updatedAt = :now',
ExpressionAttributeNames: { '#name': 'name' },
ExpressionAttributeValues: {
':name': data.name,
':email': data.email,
':now': new Date().toISOString()
},
ReturnValues: 'ALL_NEW'
}).promise();
return response(200, result.Attributes);
}
async function deleteUser(userId) {
await dynamodb.delete({
TableName: TABLE_NAME,
Key: { userId }
}).promise();
return response(204, null);
}
5. Lambda Layers
Lambda Layers memungkinkan Anda memisahkan kode yang bisa di-reuse (libraries, dependencies, custom runtime) dari function code. Layers bisa di-share antar functions dan bahkan antar accounts.
Mengapa Menggunakan Layers?
| Keuntungan | Penjelasan |
|---|---|
| Reduce size | Function code lebih kecil, dependencies di layer |
| Share code | Layer bisa dipakai banyak function |
| Version management | Layer punya versi sendiri |
| Faster deploy | Hanya upload perubahan function code |
Membuat & Menggunakan Layers
# ===========================
# CREATE LAMBDA LAYER
# ===========================
# Struktur folder layer:
# my-layer/
# nodejs/
# node_modules/
# sharp/
# uuid/
# package.json
# 1. Install dependencies ke folder layer
mkdir -p my-layer/nodejs
cd my-layer/nodejs
npm init -y
npm install sharp uuid axios
cd ../..
# 2. Package layer
cd my-layer
zip -r ../my-layer.zip .
cd ..
# 3. Publish layer
aws lambda publish-layer-version \
--layer-name my-node-utils \
--description "Common Node.js utilities (sharp, uuid, axios)" \
--zip-file fileb://my-layer.zip \
--compatible-runtimes nodejs20.x nodejs22.x \
--compatible-architectures x86_64 arm64
# Output:
# LayerVersionArn: arn:aws:lambda:REGION:ACCOUNT:layer:my-node-utils:1
# 4. Attach layer ke function
aws lambda update-function-configuration \
--function-name my-function \
--layers arn:aws:lambda:REGION:ACCOUNT:layer:my-node-utils:1
# 5. Function bisa akses layer di /opt/
# Contoh: const sharp = require('sharp'); // dari /opt/nodejs/node_modules/
# ===========================
# PYTHON LAYER EXAMPLE
# ===========================
# Struktur:
# python-layer/
# python/
# requests/
# boto3/
mkdir -p python-layer/python
cd python-layer/python
pip install requests boto3 -t .
cd ../..
cd python-layer
zip -r ../python-layer.zip .
cd ..
aws lambda publish-layer-version \
--layer-name python-utils \
--zip-file fileb://python-layer.zip \
--compatible-runtimes python3.12
Gunakan Lambda Layers untuk dependencies yang jarang berubah (SDK, utility libraries) dan function code yang sering di-update. Ini mempercepat proses deploy karena function package menjadi lebih kecil.
6. Cold Starts & Optimization
Cold start adalah delay saat Lambda pertama kali diinisialisasi untuk menjalankan function. Ini termasuk waktu download code, inisialisasi runtime, dan menjalankan init code. Cold start bisa berlangsung dari puluhan milidetik hingga beberapa detik.
Warm Start vs Cold Start
Cold Start vs Warm Start
❄ COLD START
First invocation / after ~15 min idle
Download
Code Package
50-200ms
Initialize
Runtime
50-200ms
Init Code
Imports & Setup
100-500ms
Handler
Your Function
varies
TOTAL
400ms-5s+
⚡ WARM START
Container reuse (warm Lambda)
Download
SKIPPED
Initialize
SKIPPED
Init Code
SKIPPED
Handler
Only!
TOTAL
~1ms
Performance Comparison
400ms — 5000ms+
~1ms
Cold Start Optimization Strategies
# ===========================
# COLD START OPTIMIZATION
# ===========================
# 1. PILIH RUNTIME YANG CEPAT
# Fast: Node.js, Python
# Slow: Java, .NET (butuh JVM/CLR init)
# Java fix: Gunakan SnapStart (AWS Lambda SnapStart)
# 2. MINIMALISASI DEPENDENCIES
# ❌ Import seluruh AWS SDK
# const AWS = require('aws-sdk'); // ~50MB
# ✅ Import hanya yang dibutuhkan
# const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); // ~2MB
# 3. KURANGI PACKAGE SIZE
# - Tree-shaking (webpack/esbuild)
# - Exclude devDependencies
# - Use minified packages
# 4. GUNAKAN LAYERS
# - Taruh dependencies besar di layers
# - Function code jadi lebih kecil
# 5. PILIH ARCHITECTURE ARM64 (Graviton2)
# - ~34% better price-performance
# - Set di Lambda config: Architectures: ['arm64']
# 6. PROVISIONED CONCURRENCY
# - Pre-warm sejumlah container
# - Eliminasi cold start untuk traffic dasar
# - Biaya: tetap bayar seperti reserved capacity
# 7. INIT CODE DI LUAR HANDLER
# - Database connections, SDK clients → module scope
# - JANGAN buat koneksi baru di setiap invocation
# 8. GUNAKAN SNAPSTART (Java)
# - Snapshot JVM state saat init
# - Restore dari snapshot (milidetik)
Init Code Pattern (Best Practice)
// ===========================
// COLD START OPTIMIZATION PATTERN
// ===========================
// Module scope: HANYA diinisialisasi sekali (cold start)
// Ini akan di-reuse di warm starts!
// ✅ Import yang spesifik
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, GetCommand, PutCommand } = require('@aws-sdk/lib-dynamodb');
// ✅ Client di module scope (reuse warm container)
const dynamoClient = new DynamoDBClient({
region: process.env.AWS_REGION || 'ap-southeast-1',
maxAttempts: 3
});
const docClient = DynamoDBDocumentClient.from(dynamoClient);
// ✅ Config di module scope
const TABLE_NAME = process.env.TABLE_NAME;
const CACHE = new Map(); // In-memory cache (warm starts only)
// ❌ JANGAN buat koneksi baru di handler!
// exports.handler = async (event) => {
// const client = new DynamoDBClient({...}); // BAD!
// }
exports.handler = async (event) => {
// Handler code: hanya business logic
const userId = event.pathParameters?.userId;
// Cek cache dulu (warm start benefit)
if (CACHE.has(userId)) {
console.log('Cache hit!');
return response(200, CACHE.get(userId));
}
const result = await docClient.send(new GetCommand({
TableName: TABLE_NAME,
Key: { userId }
}));
if (result.Item) {
CACHE.set(userId, result.Item); // Cache for warm starts
}
return response(200, result.Item);
};
function response(statusCode, body) {
return {
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body || { error: 'Not found' })
};
}
7. Environment & Configuration
Environment Variables
# Set environment variables
aws lambda update-function-configuration \
--function-name my-function \
--environment Variables="{
TABLE_NAME=users,
ENVIRONMENT=production,
LOG_LEVEL=info,
REGION=ap-southeast-1
}"
# Untuk secrets, gunakan AWS Secrets Manager
# Lambda akan mengambil secrets dari Secrets Manager saat runtime
# Encrypt environment variables
# Enable helpers encryption in Lambda config
# KMS key: alias/lambda-key
Secrets Manager Integration
// Mengambil secrets dari AWS Secrets Manager
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
const secretsClient = new SecretsManagerClient({ region: 'ap-southeast-1' });
// Cache secrets (module scope - warm starts)
let cachedSecret = null;
async function getSecret(secretName) {
// Return cached if available
if (cachedSecret) return cachedSecret;
const result = await secretsClient.send(new GetSecretValueCommand({
SecretId: secretName
}));
cachedSecret = JSON.parse(result.SecretString);
return cachedSecret;
}
exports.handler = async (event) => {
// Get database credentials
const dbSecret = await getSecret('prod/database/credentials');
// Use credentials
const connection = await createConnection({
host: dbSecret.host,
port: dbSecret.port,
user: dbSecret.username,
password: dbSecret.password,
database: dbSecret.dbname
});
// ... business logic
};
8. IAM Roles & Permissions
Setiap Lambda function memiliki execution role — IAM role yang menentukan apa yang boleh dilakukan function. Selalu ikuti prinsip least privilege.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudWatchLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Sid": "DynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:ap-southeast-1:*:table/users"
},
{
"Sid": "S3ReadAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
},
{
"Sid": "SecretsManagerRead",
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:ap-southeast-1:*:secret:prod/*"
}
]
}
JANGAN menggunakan "Action": "*" atau "Resource": "*" pada Lambda execution role. Ini memberikan akses penuh ke semua layanan AWS — sangat berbahaya jika Lambda function terkompromi. Selalu tentukan resource spesifik yang dibutuhkan.
9. Step Functions & Orchestration
AWS Step Functions memungkinkan Anda mengorkestrasi beberapa Lambda function dalam workflow yang kompleks — termasuk conditional logic, parallel execution, error handling, dan retry.
Step Functions State Machine
{
"Comment": "Order Processing Workflow",
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:REGION:ACCOUNT:function:validate-order",
"Next": "CheckInventory",
"Retry": [
{
"ErrorEquals": ["States.TaskFailed"],
"IntervalSeconds": 3,
"MaxAttempts": 2,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "HandleError",
"ResultPath": "$.error"
}
]
},
"CheckInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:REGION:ACCOUNT:function:check-inventory",
"Next": "IsInStock"
},
"IsInStock": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.inStock",
"BooleanEquals": true,
"Next": "ProcessPayment"
}
],
"Default": "NotifyOutOfStock"
},
"ProcessPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:REGION:ACCOUNT:function:process-payment",
"Next": "ParallelNotifications"
},
"ParallelNotifications": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "SendEmail",
"States": {
"SendEmail": {
"Type": "Task",
"Resource": "arn:aws:lambda:REGION:ACCOUNT:function:send-email",
"End": true
}
}
},
{
"StartAt": "SendSMS",
"States": {
"SendSMS": {
"Type": "Task",
"Resource": "arn:aws:lambda:REGION:ACCOUNT:function:send-sms",
"End": true
}
}
}
],
"Next": "OrderComplete"
},
"OrderComplete": {
"Type": "Succeed"
},
"NotifyOutOfStock": {
"Type": "Task",
"Resource": "arn:aws:lambda:REGION:ACCOUNT:function:notify-out-of-stock",
"End": true
},
"HandleError": {
"Type": "Task",
"Resource": "arn:aws:lambda:REGION:ACCOUNT:function:handle-error",
"End": true
}
}
}
10. Monitoring & Debugging
CloudWatch Logs & Metrics
# ===========================
# MONITORING LAMBDA
# ===========================
# Tail logs secara real-time
aws logs tail /aws/lambda/my-function --follow
# Search logs
aws logs filter-log-events \
--log-group-name /aws/lambda/my-function \
--filter-pattern "ERROR" \
--start-time $(date -d '1 hour ago' +%s)000
# Get function metrics
aws lambda get-function \
--function-name my-function \
--query '{Status: Configuration.State, LastModified: Configuration.LastModified, MemorySize: Configuration.MemorySize, Timeout: Configuration.Timeout}'
# Get function URL (jika dikonfigurasi)
aws lambda get-function-url-config \
--function-name my-function
Structured Logging Best Practice
// Structured logging untuk CloudWatch Insights
const logger = {
info: (message, data = {}) => {
console.log(JSON.stringify({
level: 'INFO',
message,
timestamp: new Date().toISOString(),
...data
}));
},
error: (message, error, data = {}) => {
console.error(JSON.stringify({
level: 'ERROR',
message,
error: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
...data
}));
},
warn: (message, data = {}) => {
console.warn(JSON.stringify({
level: 'WARN',
message,
timestamp: new Date().toISOString(),
...data
}));
}
};
exports.handler = async (event, context) => {
logger.info('Function started', {
requestId: context.awsRequestId,
functionName: context.functionName,
remainingMs: context.get_remaining_time_in_millis()
});
try {
// ... business logic
logger.info('Processing complete', {
itemCount: items.length,
durationMs: Date.now() - startTime
});
} catch (error) {
logger.error('Processing failed', error, {
input: event
});
throw error;
}
};
// CloudWatch Insights Query contoh:
// fields @timestamp, level, message, requestId
// | filter level = "ERROR"
// | sort @timestamp desc
// | limit 20
11. Production Best Practices
Lambda Best Practices Summary
| Area | Best Practice | Dampak |
|---|---|---|
| Performance | Minimalkan dependencies, gunakan layers | 🟢 Faster cold start |
| Architecture | Gunakan ARM64 (Graviton2) | 🟢 34% lebih murah |
| Memory | Tune memory dengan AWS Lambda Power Tuning | 🟢 Cost & speed optimal |
| Security | Least privilege IAM, encrypt env vars | 🟢 Reduced blast radius |
| Concurrency | Set reserved concurrency untuk critical functions | 🟢 Predictable scaling |
| Code | Init code di module scope, handler bersih | 🟢 Warm start benefit |
| Logging | Structured JSON logging | 🟢 Easy debugging |
| Dependencies | SDK v3 modular imports | 🟢 Smaller package size |
Lambda Limit Reference
| Limit | Default | Max |
|---|---|---|
| Memory | 128 MB | 10,240 MB |
| Timeout | 3 detik | 900 detik (15 menit) |
| Package size (zip) | - | 50 MB |
| Package size (unzip) | - | 250 MB |
| Layers per function | - | 5 layers |
| Env variables | - | 4 KB total |
| Concurrency | 1,000 | Custom limit |
| /tmp storage | 512 MB | 10,240 MB |
| Payload (sync) | - | 6 MB |
| Payload (async) | - | 256 KB |
12. Quiz Pemahaman
Uji pemahaman Anda tentang AWS Lambda:
1. Apa itu cold start di AWS Lambda?
2. Apa fungsi Lambda Layers?
3. Mengapa harus mengimpor SDK client secara spesifik (modular)?
4. Apa batasan waktu eksekusi maksimum Lambda?
5. Mengapa init code (koneksi DB, SDK clients) harus di module scope?