Serverless #
Nama “serverless” adalah salah satu nama yang paling menyesatkan di dunia teknologi. Server masih ada — kamu hanya tidak mengelolanya. Yang benar-benar hilang adalah tanggung jawab untuk menyediakan, mengkonfigurasi, men-scale, dan memelihara infrastruktur server. Kamu cukup menulis fungsi, deploy, dan platform yang mengurus sisanya: scaling, availability, patching sistem operasi, dan resource allocation.
Model ini mengubah cara berpikir tentang backend: bukan lagi tentang “berapa server yang dibutuhkan” tapi tentang “fungsi apa yang perlu dieksekusi dan kapan”. Sebuah fungsi yang hanya dibutuhkan saat ada event tertentu — upload file, webhook masuk, jadwal harian — tidak perlu server yang selalu berjalan. Di serverless, kamu membayar hanya untuk eksekusi yang sebenarnya terjadi, bukan untuk kapasitas yang menunggu.
Tapi serverless bukan silver bullet. Ia memperkenalkan constraint baru — cold start, execution timeout, statelessness yang ketat, debugging yang lebih sulit — yang perlu dipahami sebelum memilihnya sebagai arsitektur.
Cara Kerja Function as a Service (FaaS) #
FaaS adalah inti dari serverless. Setiap “fungsi” adalah unit deployment yang independen — bisa ditulis dalam berbagai bahasa, di-trigger oleh berbagai event, dan di-scale secara independen.
sequenceDiagram
participant E as Event Source
participant P as Platform\n(AWS Lambda, GCP Functions)
participant C as Container
participant F as Function Code
E->>P: Event masuk\n(HTTP request, S3 upload, SQS message)
alt Cold Start (tidak ada container aktif)
P->>C: Provision container baru
C->>C: Download dan init runtime
C->>F: Load function code
F->>F: Inisialisasi (global scope)
Note over C,F: Cold start: 100ms - 3 detik
else Warm Start (container sudah ada)
P->>C: Route ke container yang ada
Note over C: Warm start: < 10ms
end
F->>F: Eksekusi handler function
F->>E: Return responsePerbedaan model traditional server vs serverless:
Traditional Server:
Server selalu berjalan 24/7
→ Bayar untuk waktu server aktif, bukan hanya saat digunakan
→ Scale manual atau auto-scaling yang lambat
→ Tanggung jawab: OS update, security patch, capacity planning
→ Cocok untuk: workload yang konstan dan prediktabel
Serverless (FaaS):
Fungsi hanya berjalan saat ada event
→ Bayar per eksekusi (dan per GB-seconds memory yang digunakan)
→ Scale otomatis dari 0 ke ribuan concurrent dalam detik
→ Tidak ada tanggung jawab infrastruktur
→ Cocok untuk: workload yang spiky, intermittent, atau event-driven
Cold Start: Tantangan Utama Serverless #
Cold start terjadi ketika tidak ada container yang sudah “warm” untuk menangani request. Platform harus menyiapkan environment baru dari awal — ini butuh waktu yang bisa terasa oleh user jika tidak ditangani dengan baik.
Anatomi cold start:
1. Platform provision container baru (~50-200ms)
2. Download dan extract deployment package (~50-500ms tergantung ukuran)
3. Inisialisasi runtime (Node.js, Python, Java, dll) (~50-500ms)
4. Eksekusi kode di luar handler (global scope) (~10-1000ms)
5. Eksekusi handler function (varies)
Total cold start:
→ Node.js/Python: biasanya 100-500ms
→ Java/C#: bisa 1-5 detik (JVM startup yang berat)
→ Go/Rust: biasanya < 100ms (binary yang compiled)
Warm start (container yang sudah ada):
→ Langsung ke langkah 5
→ Biasanya < 10ms overhead
Strategi Mitigasi Cold Start #
# ✗ Anti-pattern: inisialisasi mahal di dalam handler
import boto3
def handler(event, context):
# Ini dieksekusi SETIAP kali handler dipanggil
# Bahkan untuk warm invocation yang sudah punya connection
db_client = boto3.client('dynamodb') # overhead setiap call
s3_client = boto3.client('s3')
config = load_config_from_s3() # network call setiap kali!
# ... proses event ...
# ✓ Benar: inisialisasi di luar handler (global scope)
# Dieksekusi SEKALI saat cold start, di-reuse di warm invocation
import boto3
import json
# Global scope — dieksekusi saat container diinisialisasi
db_client = boto3.client('dynamodb')
s3_client = boto3.client('s3')
config = None # lazy load
def get_config():
global config
if config is None:
response = s3_client.get_object(Bucket='config', Key='app.json')
config = json.loads(response['Body'].read())
return config
def handler(event, context):
# Handler dipanggil setiap invocation
# Tapi db_client, s3_client sudah ada dari global scope
cfg = get_config() # hanya load pertama kali per container
# ... proses event menggunakan client yang sudah ada ...
return process_event(event, cfg, db_client)
# Teknik mitigasi cold start lainnya:
# 1. Kurangi ukuran deployment package
# Semakin kecil package → download lebih cepat → cold start lebih pendek
# requirements.txt: hanya include yang benar-benar dibutuhkan
# Gunakan Lambda Layers untuk dependency yang jarang berubah
# 2. Provisioned Concurrency (AWS Lambda)
# Pre-warm N container sebelum ada request
# Bayar extra tapi cold start dihilangkan untuk traffic yang bisa diprediksi
# aws lambda put-provisioned-concurrency-config \
# --function-name my-function \
# --qualifier LIVE \
# --provisioned-concurrent-executions 5
# 3. Scheduled ping untuk keep-alive (workaround murah)
# CloudWatch Event setiap 5 menit → invoke Lambda
# → Container tetap warm
# Tapi ini tidak scale untuk fungsi yang butuh banyak concurrent instance
# 4. Pilih runtime dengan cold start lebih cepat
# Cold start ranking (dari tercepat):
# Go/Rust > Node.js ≈ Python > C# > Java
# Untuk fungsi yang latency-sensitive, hindari Java tanpa GraalVM native image
Stateless by Design #
Serverless memaksa statelessness yang ketat — tidak ada jaminan bahwa invocation berikutnya akan menggunakan container yang sama. Setiap invocation harus memperlakukan state lokal sebagai sementara.
# ✗ Anti-pattern: menyimpan state di memory container
# State ini tidak konsisten antar invocation!
request_counter = 0 # container A: 5, container B: 3, container C: 0
def handler(event, context):
global request_counter
request_counter += 1
# request_counter berbeda di setiap container
# Tidak bisa diandalkan untuk apapun
# ✓ Benar: semua state di external storage
import boto3
import redis
# Untuk counter yang akurat
dynamodb = boto3.client('dynamodb')
def increment_request_counter(function_name: str) -> int:
response = dynamodb.update_item(
TableName='counters',
Key={'name': {'S': function_name}},
UpdateExpression='ADD #count :one',
ExpressionAttributeNames={'#count': 'count'},
ExpressionAttributeValues={':one': {'N': '1'}},
ReturnValues='UPDATED_NEW'
)
return int(response['Attributes']['count']['N'])
# Untuk session/cache
redis_client = redis.Redis(
host=os.environ['REDIS_HOST'],
port=6379,
decode_responses=True
)
def handler(event, context):
# State selalu dari external storage
counter = increment_request_counter('my-function')
session = redis_client.get(f"session:{event['session_id']}")
# ... proses ...
Di mana menyimpan state di serverless:
Temporary (dalam satu invocation saja):
→ Local variable di function
→ /tmp filesystem (max 512MB di AWS Lambda, tapi hilang saat container recycle)
Persistent state:
→ Database (DynamoDB, RDS via connection pooling)
→ Cache (ElastiCache/Redis via koneksi yang di-reuse di global scope)
→ Object storage (S3 untuk file)
→ Message queue (SQS, SNS untuk komunikasi async)
State yang harus dihindari:
→ File lokal yang diharapkan persist antar invocation
→ In-memory cache yang diharapkan konsisten antar container
→ Connection yang tidak di-cleanup (connection leak di database)
Event-Driven Architecture di Serverless #
Serverless bersinar paling terang dalam arsitektur event-driven — fungsi yang dipicu oleh event dari berbagai sumber.
# Contoh Lambda function yang menangani berbagai trigger
# 1. HTTP request via API Gateway
def api_handler(event, context):
"""Handler untuk HTTP request."""
path = event['path']
method = event['httpMethod']
body = json.loads(event.get('body', '{}'))
if method == 'POST' and path == '/orders':
return create_order(body)
return {
'statusCode': 404,
'body': json.dumps({'error': 'Not found'})
}
# 2. S3 event — ketika file di-upload
def s3_event_handler(event, context):
"""Proses file yang baru di-upload."""
for record in event['Records']:
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
# Process file: resize gambar, parse CSV, scan virus, dll
process_uploaded_file(bucket, key)
# 3. SQS queue — proses pesan dari antrian
def sqs_handler(event, context):
"""Proses pesan dari SQS queue."""
failed_messages = []
for record in event['Records']:
message_id = record['messageId']
body = json.loads(record['body'])
try:
process_message(body)
except Exception as e:
# Tandai sebagai gagal — akan kembali ke queue atau ke DLQ
failed_messages.append({'itemIdentifier': message_id})
logger.error(f"Failed to process message {message_id}: {e}")
# Return batch item failures — hanya yang gagal yang di-retry
return {'batchItemFailures': failed_messages}
# 4. CloudWatch Events (scheduled)
def scheduled_handler(event, context):
"""Dijalankan setiap hari jam 08:00."""
send_daily_report()
cleanup_expired_sessions()
return {'status': 'completed'}
# 5. DynamoDB Stream — react terhadap perubahan database
def dynamodb_stream_handler(event, context):
"""Trigger ketika ada record baru atau berubah di DynamoDB."""
for record in event['Records']:
if record['eventName'] == 'INSERT':
new_item = record['dynamodb']['NewImage']
user_id = new_item['user_id']['S']
send_welcome_email(user_id)
Batasan Serverless yang Perlu Dipahami #
Serverless bukan tanpa keterbatasan. Memahami ini penting sebelum memilih serverless untuk use case tertentu.
Batasan utama AWS Lambda (angka bisa berbeda per provider):
Execution timeout:
→ Maksimum 15 menit per invocation
→ Tidak cocok untuk long-running process (video encoding, ML training)
Memory:
→ 128MB sampai 10GB (lebih banyak memory = lebih banyak CPU)
→ Tidak ada GPU (untuk workload ML/AI)
Storage:
→ /tmp: 512MB (bisa dikonfigurasi sampai 10GB)
→ Bukan persistent storage
Concurrent executions:
→ Default limit: 1000 concurrent per region (bisa dinaikkan)
→ Penting untuk dipahami untuk workload burst
Deployment package:
→ 50MB compressed, 250MB uncompressed
→ Layer membantu tapi ada limit total juga
Networking:
→ Cold start lebih lama jika di dalam VPC (ENI provisioning)
→ Perlu NAT Gateway untuk akses internet dari VPC
Payload:
→ API Gateway: 10MB request/response
→ Synchronous invocation: 6MB payload
→ SQS: 256KB per message
Kapan serverless TIDAK cocok:
✗ Long-running workload (> 15 menit)
→ ETL yang memproses dataset besar
→ Video transcoding
→ ML model training
→ Solusi: ECS Fargate, EC2, AWS Batch
✗ Latency-sensitive yang tidak toleran cold start
→ Gaming backend real-time
→ HFT (High Frequency Trading)
→ Solusi: Provisioned Concurrency atau traditional server
✗ Heavy stateful workload
→ WebSocket connection yang persist lama
→ Stateful streaming (biasanya butuh connection persist)
→ Solusi: EC2/ECS dengan WebSocket server
✗ Workload yang butuh GPU
→ Deep learning inference
→ Solusi: EC2 GPU instances, SageMaker
✗ Very high constant traffic
→ Jika selalu 1000+ concurrent executions 24/7
→ Cost bisa lebih mahal dari EC2
→ Hitung dahulu sebelum memutuskan
Cost Model Serverless #
Cost model serverless berbeda fundamental dari server tradisional — ini bisa sangat hemat untuk beberapa use case, tapi juga bisa lebih mahal dari ekspektasi.
AWS Lambda pricing (2025):
Compute:
→ $0.0000166667 per GB-second
→ Contoh: fungsi 256MB yang berjalan 1 detik
= 0.25 GB × 1 detik × $0.0000166667 = $0.0000041667
Request:
→ $0.20 per 1 juta request
Free tier (per bulan):
→ 1 juta request gratis
→ 400.000 GB-seconds gratis
Kalkulasi biaya:
Skenario A — Low traffic:
100 request/hari × 30 hari = 3.000 request/bulan
Masih dalam free tier → GRATIS
vs EC2 t3.micro: ~$8.5/bulan
Skenario B — Medium traffic:
100.000 request/hari × 30 hari = 3.000.000 request/bulan
Fungsi 256MB, rata-rata 100ms
Compute: 3M × 0.25GB × 0.1s = 75.000 GB-s × $0.0000166667 = $1.25
Request: (3M - 1M) × $0.2/1M = $0.40
Total: ~$1.65/bulan vs EC2 t3.micro $8.5/bulan → HEMAT
Skenario C — High traffic:
10 juta request/hari = 300 juta request/bulan
Fungsi 512MB, rata-rata 200ms
Compute: 300M × 0.5GB × 0.2s = 30M GB-s × $0.0000166667 = $500
Request: 300M × $0.2/1M = $60
Total: ~$560/bulan vs fleet EC2 yang bisa jadi lebih murah
→ Perlu kalkulasi lebih detail sebelum memutuskan
Observability di Serverless #
Debugging dan monitoring di serverless lebih menantang dari traditional server — tidak ada SSH ke server, log tersebar per fungsi, dan distributed tracing jadi lebih penting.
# Best practices untuk observability di Lambda
import json
import logging
import time
from functools import wraps
import boto3
# Structured logging — lebih mudah di-parse di CloudWatch Logs Insights
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
def log_event(event_type: str, **kwargs):
"""Log structured event untuk observability."""
logger.info(json.dumps({
'event_type': event_type,
'timestamp': time.time(),
**kwargs
}))
# Decorator untuk tracing dan error handling
def traced_handler(func):
@wraps(func)
def wrapper(event, context):
start_time = time.time()
# Log invocation metadata
log_event('invocation_start', {
'function_name': context.function_name,
'request_id': context.aws_request_id,
'remaining_time_ms': context.get_remaining_time_in_millis(),
'memory_limit_mb': context.memory_limit_in_mb,
})
try:
result = func(event, context)
duration_ms = (time.time() - start_time) * 1000
log_event('invocation_success', {
'duration_ms': round(duration_ms, 2),
'request_id': context.aws_request_id,
})
return result
except Exception as e:
duration_ms = (time.time() - start_time) * 1000
log_event('invocation_error', {
'error_type': type(e).__name__,
'error_message': str(e),
'duration_ms': round(duration_ms, 2),
'request_id': context.aws_request_id,
})
# Re-raise agar Lambda tahu ini adalah error
raise
return wrapper
@traced_handler
def handler(event, context):
# Fungsi handler utama
pass
# AWS X-Ray untuk distributed tracing
from aws_xray_sdk.core import xray_recorder, patch_all
from aws_xray_sdk.core import xray_recorder
# Patch semua AWS SDK call untuk auto-tracing
patch_all()
@xray_recorder.capture('process_order')
def process_order(order_data: dict):
"""Function ini akan muncul sebagai subsegment di X-Ray trace."""
with xray_recorder.in_subsegment('validate_order') as subsegment:
subsegment.put_annotation('order_id', order_data['id'])
validate_order(order_data)
with xray_recorder.in_subsegment('save_to_database') as subsegment:
order = save_order(order_data)
subsegment.put_metadata('saved_order', order)
return order
Infrastructure as Code untuk Serverless #
# serverless.yml — menggunakan Serverless Framework
service: my-app
provider:
name: aws
runtime: python3.12
region: ap-southeast-1
memorySize: 256 # MB, default untuk semua fungsi
timeout: 30 # detik, default
environment:
APP_ENV: ${opt:stage, 'development'}
DATABASE_URL: ${env:DATABASE_URL}
# IAM role — least privilege
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
Resource: !GetAtt OrdersTable.Arn
functions:
# HTTP API
api:
handler: src/api.handler
events:
- httpApi:
path: /orders
method: POST
- httpApi:
path: /orders/{id}
method: GET
memorySize: 512 # override default untuk fungsi yang butuh lebih
# Scheduled job
dailyReport:
handler: src/reports.daily_handler
timeout: 300 # 5 menit untuk proses report
events:
- schedule:
rate: cron(0 8 * * ? *) # Setiap hari jam 08:00 UTC
enabled: true
# SQS consumer
orderProcessor:
handler: src/processor.sqs_handler
reservedConcurrency: 10 # Limit concurrency untuk melindungi downstream
events:
- sqs:
arn: !GetAtt OrderQueue.Arn
batchSize: 10
functionResponseType: ReportBatchItemFailures
resources:
Resources:
OrdersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${opt:stage}-orders
BillingMode: PAY_PER_REQUEST # On-demand — no capacity planning
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
OrderQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-${opt:stage}-orders
VisibilityTimeout: 60
RedrivePolicy:
deadLetterTargetArn: !GetAtt OrderDLQ.Arn
maxReceiveCount: 3 # Retry 3 kali sebelum ke DLQ
OrderDLQ:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-${opt:stage}-orders-dlq
MessageRetentionPeriod: 1209600 # 14 hari
Anti-Pattern yang Harus Dihindari #
# ✗ Anti-pattern 1: inisialisasi mahal di dalam handler
def handler(event, context):
db = connect_database() # connection baru setiap invocation!
config = load_from_s3() # network call setiap invocation!
# ✓ Solusi: global scope untuk inisialisasi yang bisa di-reuse
db = connect_database() # sekali per container
config = load_from_s3() # sekali per container
def handler(event, context):
return process(event, db, config) # reuse dari global
# ✗ Anti-pattern 2: menyimpan state di /tmp dan mengandalkannya persist
def handler(event, context):
with open('/tmp/data.json', 'w') as f:
json.dump(data, f)
# Invocation berikutnya mungkin container berbeda — file tidak ada!
# ✓ Solusi: gunakan S3 atau database untuk persistent state
# ✗ Anti-pattern 3: timeout yang terlalu pendek tanpa retry logic
# Default Lambda timeout: 3 detik
# Jika downstream service lambat → timeout sebelum selesai
# ✓ Solusi: set timeout yang realistic + implementasi retry di SQS
# ✗ Anti-pattern 4: tidak ada Dead Letter Queue
# Message gagal diproses → hilang begitu saja
# ✓ Solusi: selalu konfigurasi DLQ untuk SQS consumer dan async invocation
# ✗ Anti-pattern 5: concurrency tidak di-limit untuk downstream protection
# Lambda scale ke ribuan concurrent → flood database atau third-party API
# ✓ Solusi: set reservedConcurrency atau gunakan SQS untuk throttling alami
# ✗ Anti-pattern 6: fungsi yang melakukan terlalu banyak (monolithic Lambda)
# Satu fungsi handle semua: auth + business logic + data access + notification
# ✓ Solusi: single responsibility per fungsi, composisi via event/message
Checklist Serverless #
DESAIN FUNGSI:
□ Setiap fungsi punya satu tanggung jawab yang jelas
□ Handler function tipis — logika bisnis di module terpisah
□ Inisialisasi mahal di global scope, bukan di dalam handler
□ Tidak ada state yang disimpan di memory atau /tmp untuk persist antar invocation
COLD START:
□ Deployment package sekecil mungkin
□ Dependency yang tidak perlu tidak di-include
□ Runtime dipilih berdasarkan kebutuhan latency (Go/Node.js untuk latency rendah)
□ Provisioned Concurrency dikonfigurasi untuk fungsi yang latency-critical
ERROR HANDLING:
□ Dead Letter Queue dikonfigurasi untuk async invocation dan SQS consumer
□ Retry logic yang tepat (exponential backoff untuk transient error)
□ Idempotent handler — eksekusi dua kali menghasilkan hasil yang sama
□ Partial batch failure di-handle dengan benar (reportBatchItemFailures)
KEAMANAN:
□ IAM role dengan least privilege — hanya permission yang diperlukan
□ Secret dari AWS Secrets Manager atau Parameter Store, bukan env var hardcode
□ Tidak ada sensitive data di log
□ VPC jika fungsi perlu akses resource private
OBSERVABILITY:
□ Structured logging (JSON) untuk kemudahan query di CloudWatch Logs Insights
□ Custom metrics untuk business event penting
□ X-Ray tracing diaktifkan untuk distributed tracing
□ Alert dipasang untuk error rate, duration, throttling
COST:
□ Memory size dikonfigurasi sesuai kebutuhan (lebih banyak memory = lebih mahal)
□ Timeout dikonfigurasi sesuai kebutuhan (bukan set ke maksimum)
□ Reserved concurrency di-set untuk mencegah runaway cost
□ Cost estimate dibuat sebelum go-live
OPERASIONAL:
□ Infrastructure as Code (Serverless Framework, AWS CDK, SAM)
□ CI/CD pipeline untuk deployment
□ Environment terpisah (dev, staging, production)
□ Rollback bisa dilakukan dengan cepat
Ringkasan #
- Serverless bukan tanpa server — ia tanpa manajemen server — platform menangani provisioning, scaling, dan availability. Kamu fokus pada kode dan logika bisnis.
- Cold start adalah trade-off utama — kontainer baru butuh waktu untuk diinisialisasi. Minimalkan dengan inisialisasi di global scope, package yang kecil, dan runtime yang tepat.
- Stateless adalah syarat, bukan pilihan — tidak ada jaminan invocation berikutnya pakai container yang sama. Semua state harus di external storage (DynamoDB, Redis, S3).
- Event-driven adalah konteks terbaik serverless — file upload, webhook, pesan dari queue, jadwal periodik — semua cocok. Workload yang constant dan latency-critical lebih cocok dengan traditional server.
- Inisialisasi di global scope untuk warm invocation — connection database, konfigurasi, dan client SDK yang di-inisialisasi di luar handler function di-reuse antar invocation dalam container yang sama.
- Dead Letter Queue adalah safety net yang wajib — pesan yang gagal diproses tidak boleh hilang begitu saja. DLQ memungkinkan investigasi dan reprocessing.
- IAM least privilege adalah wajib — setiap fungsi hanya boleh punya akses ke resource yang benar-benar dibutuhkan. Satu fungsi dikompromikan tidak boleh bisa mengakses semua resource.
- Idempoten adalah keharusan — Lambda bisa mengeksekusi fungsi lebih dari sekali (exactly-once delivery tidak dijamin). Handler yang idempoten aman untuk di-retry.
- Cost model berbeda dari tradisional — sangat hemat untuk low-to-medium traffic, bisa lebih mahal dari EC2 untuk traffic sangat tinggi yang konstan. Kalkulasi selalu sebelum memutuskan.
- Observability lebih menantang — structured logging, X-Ray tracing, dan alert yang tepat adalah wajib karena tidak ada SSH dan log tersebar di banyak fungsi.
← Sebelumnya: Configuration Manager Berikutnya: Microservices →