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 #
// Go: padanan handler serverless
// ✗ Anti-pattern: inisialisasi mahal di dalam handler
func handler(event map[string]any, context map[string]any) {
// Ini dieksekusi SETIAP kali handler dipanggil
// Bahkan untuk warm invocation yang sudah punya connection
dbClient := connectDynamoDB() // overhead setiap call
s3Client := connectS3()
config := loadConfigFromS3() // network call setiap kali!
// ... proses event ...
}
// ✓ Benar: inisialisasi di luar handler (global scope)
// Dieksekusi SEKALI saat cold start, di-reuse di warm invocation
// Global scope — dieksekusi saat container diinisialisasi
var (
dbClient = connectDynamoDB()
s3Client = connectS3()
config map[string]any // lazy load
)
func getConfig() map[string]any {
if config == nil {
config = loadConfigFromS3()
}
return config
}
func handler(event map[string]any, context map[string]any) {
// Handler dipanggil setiap invocation
// Tapi dbClient dan s3Client sudah ada dari global scope
cfg := getConfig() // hanya load pertama kali per container
// ... proses event menggunakan client yang sudah ada ...
return processEvent(event, cfg, dbClient)
}
// Go: teknik mitigasi cold start lainnya
// 1. Kurangi ukuran deployment package
// Semakin kecil package → download lebih cepat → cold start lebih pendek
// go.mod: 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.
// Go: padanan serverless stateless
// ✗ Anti-pattern: menyimpan state di memory container
// State ini tidak konsisten antar invocation!
var requestCounter int // container A: 5, container B: 3, container C: 0
func handler(event map[string]any, context map[string]any) {
requestCounter++
// requestCounter berbeda di setiap container
// Tidak bisa diandalkan untuk apapun
}
// ✓ Benar: semua state di external storage
// Untuk counter yang akurat
var dynamodb = connectDynamoDB()
func incrementRequestCounter(functionName string) int {
// Atomic ADD pada counter DynamoDB
resp := dynamodb.UpdateItem(map[string]any{
"TableName": "counters",
"Key": map[string]any{"name": map[string]any{"S": functionName}},
"UpdateExpression": "ADD #count :one",
"ExpressionAttributeNames": map[string]any{"#count": "count"},
"ExpressionAttributeValues": map[string]any{":one": map[string]any{"N": "1"}},
"ReturnValues": "UPDATED_NEW",
})
return int(resp["Attributes"].(map[string]any)["count"].(map[string]any)["N"].(float64))
}
// Untuk session/cache
var redisClient = redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_HOST") + ":6379",
})
func handler(event map[string]any, context map[string]any) {
// State selalu dari external storage
counter := incrementRequestCounter("my-function")
session, _ := redisClient.Get("session:" + event["session_id"].(string)).Result()
// ... 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.
// Go: contoh Lambda function yang menangani berbagai trigger
// 1. HTTP request via API Gateway
func apiHandler(event map[string]any, context map[string]any) map[string]any {
// Handler untuk HTTP request
path := event["path"]
method := event["httpMethod"]
body := json.Unmarshal([]byte(event["body"].(string)))
if method == "POST" && path == "/orders" {
return createOrder(body)
}
return map[string]any{
"statusCode": 404,
"body": `{"error": "Not found"}`,
}
}
// 2. S3 event — ketika file di-upload
func s3EventHandler(event map[string]any, context map[string]any) {
// Proses file yang baru di-upload
for _, record := range event["Records"].([]any) {
rec := record.(map[string]any)
bucket := rec["s3"].(map[string]any)["bucket"].(map[string]any)["name"]
key := rec["s3"].(map[string]any)["object"].(map[string]any)["key"]
// Process file: resize gambar, parse CSV, scan virus, dll
processUploadedFile(bucket, key)
}
}
// 3. SQS queue — proses pesan dari antrian
func sqsHandler(event map[string]any, context map[string]any) map[string]any {
// Proses pesan dari SQS queue
var failedMessages []map[string]any
for _, record := range event["Records"].([]any) {
rec := record.(map[string]any)
messageID := rec["messageId"]
body := json.Unmarshal([]byte(rec["body"].(string)))
if err := processMessage(body); err != nil {
// Tandai sebagai gagal — akan kembali ke queue atau ke DLQ
failedMessages = append(failedMessages, map[string]any{"itemIdentifier": messageID})
log.Printf("Failed to process message %v: %v", messageID, err)
}
}
// Return batch item failures — hanya yang gagal yang di-retry
return map[string]any{"batchItemFailures": failedMessages}
}
// 4. CloudWatch Events (scheduled)
func scheduledHandler(event map[string]any, context map[string]any) map[string]any {
// Dijalankan setiap hari jam 08:00
sendDailyReport()
cleanupExpiredSessions()
return map[string]any{"status": "completed"}
}
// 5. DynamoDB Stream — react terhadap perubahan database
func dynamodbStreamHandler(event map[string]any, context map[string]any) {
// Trigger ketika ada record baru atau berubah di DynamoDB
for _, record := range event["Records"].([]any) {
rec := record.(map[string]any)
if rec["eventName"] == "INSERT" {
newItem := rec["dynamodb"].(map[string]any)["NewImage"].(map[string]any)
userID := newItem["user_id"].(map[string]any)["S"]
sendWelcomeEmail(userID)
}
}
}
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.
// Go: best practices untuk observability di Lambda
// Structured logging — lebih mudah di-parse di CloudWatch Logs Insights
// logEvent mencatat event terstruktur untuk observability
func logEvent(eventType string, fields map[string]any) {
entry := map[string]any{
"event_type": eventType,
"timestamp": time.Now().Unix(),
}
for k, v := range fields {
entry[k] = v
}
logger.Info(entry)
}
// tracedHandler membungkus handler dengan tracing dan error handling
// (padanan decorator Python di Go)
func tracedHandler(handler func(map[string]any, map[string]any) map[string]any) func(map[string]any, map[string]any) map[string]any {
return func(event map[string]any, context map[string]any) map[string]any {
start := time.Now()
// Log invocation metadata
logEvent("invocation_start", map[string]any{
"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"],
})
result := handler(event, context)
durationMs := float64(time.Since(start).Microseconds()) / 1000
logEvent("invocation_success", map[string]any{
"duration_ms": math.Round(durationMs*100) / 100,
"request_id": context["aws_request_id"],
})
return result
}
}
// handler adalah fungsi handler utama
var handler = tracedHandler(func(event map[string]any, context map[string]any) map[string]any {
// Fungsi handler utama
return nil
})
// Go: AWS X-Ray untuk distributed tracing
// Patch semua AWS SDK call untuk auto-tracing
// (xray-go meng-instrumentasi HTTP client dan AWS SDK secara otomatis)
func init() {
xray.Configure(xray.Config{DaemonAddr: "xray:2000"})
}
// processOrder muncul sebagai subsegment di X-Ray trace
func processOrder(orderData map[string]any) map[string]any {
ctx, seg := xray.BeginSubsegment(context.Background(), "process_order")
defer seg.Close()
// validate_order
ctx, sub := xray.BeginSubsegment(ctx, "validate_order")
sub.AddAnnotation("order_id", orderData["id"])
validateOrder(orderData)
sub.Close()
// save_to_database
ctx, sub2 := xray.BeginSubsegment(ctx, "save_to_database")
order := saveOrder(orderData)
sub2.AddMetadata("saved_order", order)
sub2.Close()
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 #
// Go: anti-pattern
// ✗ Anti-pattern 1: inisialisasi mahal di dalam handler
func handler(event map[string]any, context map[string]any) {
db := connectDatabase() // connection baru setiap invocation!
config := loadFromS3() // network call setiap invocation!
}
// ✓ Solusi: global scope untuk inisialisasi yang bisa di-reuse
var (
db = connectDatabase() // sekali per container
config = loadFromS3() // sekali per container
)
func handler(event map[string]any, context map[string]any) {
return process(event, db, config) // reuse dari global
}
// ✗ Anti-pattern 2: menyimpan state di /tmp dan mengandalkannya persist
func handler(event map[string]any, context map[string]any) {
os.WriteFile("/tmp/data.json", data, 0644)
// 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 →