Lewati ke konten utama

WhatsApp Service โ€” VM Deploy Runbook

Status: ๐ŸŸข Active (mirror pola firebase + email) Owner: Backend Lead Latest: 2026-05-25 โ€” pattern systemd unit + binary statik, folder layout mirror firebase-service (per memory project_firebase_service_vm_deployment).

Dokumen terkait:


1. VM topologyโ€‹

ItemValue
Hostptikn3-vm.cluster-vps.dalang.io
User / Groupenalfarid / enalfarid
Folder root/home/enalfarid/kesles_merchant/merchant_whatsapp/
Binary namawhatsapp-service (Linux ELF amd64 stripped)
Env file.env.production (chmod 644, gitignored)
Systemd unit/etc/systemd/system/whatsapp-service.service
Listen port127.0.0.1:8091 (loopback internal, dipakai core_api)
Logssudo journalctl -u whatsapp-service [-f] [--since="1 hour ago"]
Reverse proxyCaddy / nginx (public webhook callback only, lihat ยง6)

Pattern folder + systemd unit identik dengan merchant_firebase/ + merchant_email/ supaya operator tidak belajar layout baru per service.


2. Initial deploy (first-time setup)โ€‹

2.1 Build binary di laptopโ€‹

cd ~/Macbook\ pro/development/kesles_merchant/services/whatsapp_service

# Cross-compile Linux amd64 stripped
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build \
-trimpath \
-ldflags="-s -w" \
-o dist/whatsapp-service \
./cmd/server

# Verifikasi ukuran (~8-12 MB)
ls -lh dist/whatsapp-service
file dist/whatsapp-service # โ†’ ELF 64-bit LSB executable, x86-64, statically linked

2.2 Create folder + env di VMโ€‹

ssh enalfarid@ptikn3-vm.cluster-vps.dalang.io
mkdir -p ~/kesles_merchant/merchant_whatsapp/
cd ~/kesles_merchant/merchant_whatsapp/

# Copy template lalu isi credential
cp ~/kesles_merchant/services/whatsapp_service/.env.example .env.production
chmod 644 .env.production
nano .env.production # โ†’ set INTERNAL_NOTIFICATION_API_KEY, WHATSAPP_API_KEY, WHATSAPP_API_SECRET, WHATSAPP_VERIFY_TOKEN, POSTGRES_DSN

Env wajib di production (kalau salah satu kosong, service refuse start via validateConfig):

EnvSumber
APP_ENV=productionstatic
INTERNAL_NOTIFICATION_API_KEYrotate sync dengan caller WHATSAPP_SERVICE_API_KEY di core_api
WHATSAPP_API_KEYMeta WhatsApp Business API token (System User permanent token)
WHATSAPP_API_SECRETMeta App Secret (untuk HMAC webhook verify)
WHATSAPP_VERIFY_TOKENrandom string, daftar di Meta webhook config + match dengan ini
WHATSAPP_PHONE_NUMBER_IDMeta WABA phone number ID
WHATSAPP_PROVIDER=meta-cloud-apistatic
WHATSAPP_BASE_URL=https://graph.facebook.com/v23.0static
WHATSAPP_ALLOWED_TEMPLATEScomma-separated whitelist (merchant_otp_code,merchant_payment_receipt,...)
WHATSAPP_TEMPLATE_NAME=merchant_otp_codeOTP default template
WHATSAPP_TEMPLATE_LANGUAGE=idstatic
POSTGRES_DSNpostgres://kesles:...@<DB_HOST>:5432/db_kesles_merchant?sslmode=disable

2.3 SCP binaryโ€‹

# Dari laptop
scp dist/whatsapp-service enalfarid@ptikn3-vm.cluster-vps.dalang.io:~/kesles_merchant/merchant_whatsapp/

# Verifikasi di VM
ssh enalfarid@ptikn3-vm.cluster-vps.dalang.io 'ls -lh ~/kesles_merchant/merchant_whatsapp/whatsapp-service'

2.4 Install systemd unitโ€‹

Buat /etc/systemd/system/whatsapp-service.service:

[Unit]
Description=Kesles Merchant - WhatsApp Service
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=enalfarid
Group=enalfarid
WorkingDirectory=/home/enalfarid/kesles_merchant/merchant_whatsapp
EnvironmentFile=/home/enalfarid/kesles_merchant/merchant_whatsapp/.env.production
ExecStart=/home/enalfarid/kesles_merchant/merchant_whatsapp/whatsapp-service
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=whatsapp-service

# Hardening (selaras firebase + email)
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=read-only
PrivateTmp=true
ReadWritePaths=/home/enalfarid/kesles_merchant/merchant_whatsapp

[Install]
WantedBy=multi-user.target

Enable + start:

sudo systemctl daemon-reload
sudo systemctl enable whatsapp-service
sudo systemctl start whatsapp-service
sudo systemctl status whatsapp-service # Expected: active (running)

2.5 Smoke test post-deployโ€‹

# 1. Liveness (selalu 200 saat process up)
curl -s http://127.0.0.1:8091/health | jq
# Expected: {"service":"whatsapp-service","status":"ok"}

# 2. Readiness (DB ping + provider config)
curl -s http://127.0.0.1:8091/ready | jq
# Expected: {"service":"whatsapp-service","status":"ready","checks":{"postgres":"ok","provider":"ok"}}

# 3. Internal auth wall (tanpa header โ†’ 401)
curl -s -X POST http://127.0.0.1:8091/internal/whatsapp/otp \
-H "Content-Type: application/json" \
-d '{"phone":"628000000000","code":"000000"}'
# Expected: {"error":"unauthorized"}

# 4. Internal endpoint dengan header (test ke nomor sandbox dulu, JANGAN nomor live)
KEY=$(grep "^INTERNAL_NOTIFICATION_API_KEY=" .env.production | cut -d= -f2)
curl -s -X POST http://127.0.0.1:8091/internal/whatsapp/otp \
-H "Content-Type: application/json" \
-H "X-Internal-API-Key: $KEY" \
-d '{"phone":"<sandbox-phone>","code":"123456"}'
# Expected: {"status":"queued","message_id":"<uuid>","provider_message_id":"wamid..."}

3. Re-deploy (update binary)โ€‹

# 1. Build di laptop
cd ~/Macbook\ pro/development/kesles_merchant/services/whatsapp_service
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o dist/whatsapp-service ./cmd/server

# 2. SCP (overwrite binary)
scp dist/whatsapp-service enalfarid@ptikn3-vm.cluster-vps.dalang.io:~/kesles_merchant/merchant_whatsapp/

# 3. Restart service (zero-downtime kurang dari 2 detik)
ssh enalfarid@ptikn3-vm.cluster-vps.dalang.io 'sudo systemctl restart whatsapp-service'

# 4. Verify (jangan skip โ€” kalau startup gagal validateConfig, systemd restart loop)
ssh enalfarid@ptikn3-vm.cluster-vps.dalang.io \
'sudo systemctl status whatsapp-service && curl -s http://127.0.0.1:8091/ready | jq'

Selama restart, ada window ~2 detik service tidak respond. Caller core_api sudah punya retry policy (lihat internal/whatsapp/service_sender.go), jadi in-flight request bakal di-retry, tidak hilang.


4. Env rotationโ€‹

4.1 Rotate INTERNAL_NOTIFICATION_API_KEY (paired dengan core_api)โ€‹

Service ini + caller core_api harus rotate sekaligus supaya tidak ada window auth mismatch.

# 1. Generate new key (32 char minimum)
NEW_KEY=$(openssl rand -base64 48 | tr -d '/+=' | head -c 48)

# 2. Di VM whatsapp: update .env.production
ssh enalfarid@ptikn3-vm.cluster-vps.dalang.io
nano ~/kesles_merchant/merchant_whatsapp/.env.production
# โ†’ INTERNAL_NOTIFICATION_API_KEY=<NEW_KEY>

# 3. Di VM core_api: update .env.production
nano ~/kesles_merchant/merchant_core_api/.env.production
# โ†’ WHATSAPP_SERVICE_API_KEY=<NEW_KEY> (same value)

# 4. Restart kedua service hampir bersamaan
sudo systemctl restart whatsapp-service && sudo systemctl restart merchant-core-api

# 5. Verify dengan trigger OTP dari mobile / dashboard

Window mismatch ~5 detik antara dua restart. Mobile yang request OTP saat window itu bakal dapat 401 โ†’ minta user retry.

4.2 Rotate WHATSAPP_API_KEY (Meta token)โ€‹

Meta System User token tidak expire kalau di-generate dengan "Never expires" checkbox. Tapi kalau di-rotate manual (mis. token leak):

# 1. Generate token baru di Meta Business Manager โ†’ System Users โ†’ Generate token
# 2. Update .env.production di VM
nano ~/kesles_merchant/merchant_whatsapp/.env.production
# โ†’ WHATSAPP_API_KEY=<NEW_TOKEN>

# 3. Restart
sudo systemctl restart whatsapp-service

# 4. Smoke test send template

4.3 Rotate WHATSAPP_API_SECRET (Meta App Secret)โ€‹

App Secret di-rotate jarang (hanya saat App Review baru atau secret leak). Sequence:

# 1. Reset di Meta Business โ†’ My Apps โ†’ <App> โ†’ Settings โ†’ Basic โ†’ Show App Secret โ†’ Reset
# 2. CATAT secret lama (sebagai fallback kalau ada in-flight webhook)
# 3. Update .env.production
# 4. Restart service
# 5. Test webhook handshake dari Meta UI (Test โ†’ Send sample message)

Selama rotation window (~10 detik), webhook callback dari Meta dengan signature lama bakal di-reject 401 oleh validateMetaSignature โ€” Meta auto-retry 3ร— dengan backoff, jadi data tidak hilang.


5. Monitoring & alertingโ€‹

5.1 Log streamโ€‹

# Real-time tail
sudo journalctl -u whatsapp-service -f

# Hari ini
sudo journalctl -u whatsapp-service --since today

# Filter structured event (slog JSON)
sudo journalctl -u whatsapp-service --since "1 hour ago" -o cat | jq 'select(.event == "whatsapp_send")'

# Filter error class tertentu
sudo journalctl -u whatsapp-service --since today -o cat | \
jq 'select(.event == "whatsapp_send" and .error_class == "meta_auth")'

5.2 Event schema referenceโ€‹

Lihat architecture.md ยง6 Observability untuk schema lengkap whatsapp_send + whatsapp_webhook event + error class taxonomy. Aggregator (Loki / ELK) filter by JSON field, bukan regex stdout.

5.3 Alerting thresholdโ€‹

EventThresholdAction
error_class=meta_auth count > 0 / 5 minInvestigate immediatelyToken rotate atau revoke event
error_class=meta_rate_limit count > 0 / 5 minQuota reviewEscalate ke Meta Business Support
error_class=meta_template_invalid count > 0Investigate template configCompare cfg WHATSAPP_ALLOWED_TEMPLATES vs Meta Approved
error_class=meta_5xx rate > 5% / 10 minMonitoringBiasanya self-recover, tidak action
event=whatsapp_webhook status=rejected count > 0Signature mismatchCek WHATSAPP_API_SECRET sync dengan Meta UI
/ready 503 > 1 minDB unreachableCek POSTGRES_DSN + DB host network

6. Public webhook exposure (reverse proxy)โ€‹

whatsapp-service listen di 127.0.0.1:8091 (loopback only). Webhook callback dari Meta perlu reachable lewat HTTPS publik di domain wa.kesles.com/webhooks/whatsapp/status (lihat webhook-meta-setup.md).

Contoh snippet Caddy:

wa.kesles.com {
# Hanya expose path webhook โ€” endpoint /internal/* DI-BLOCK PUBLIC
@webhook path /webhooks/whatsapp/status
reverse_proxy @webhook 127.0.0.1:8091

# Semua path lain โ†’ 404 (defense in depth โ€” middleware service sudah
# fail-closed, tapi proxy layer jadi guard tambahan)
respond 404
}

JANGAN expose /internal/* ke public domain. Service punya fail-closed auth middleware via INTERNAL_NOTIFICATION_API_KEY, tapi defense-in-depth: proxy layer tolak path internal sebelum sampai service.


7. Rollback procedureโ€‹

Kalau redeploy bermasalah:

# 1. SSH ke VM, lihat binary backup (kalau ada โ€” manual practice belum baku)
ls -la ~/kesles_merchant/merchant_whatsapp/

# 2. Rollback ke binary commit sebelumnya
cd ~/Macbook\ pro/development/kesles_merchant/services/whatsapp_service
git log --oneline -5 # cari commit hash terakhir yang stable
git checkout <commit-hash> -- . # checkout source ke commit
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o dist/whatsapp-service ./cmd/server
scp dist/whatsapp-service enalfarid@ptikn3-vm.cluster-vps.dalang.io:~/kesles_merchant/merchant_whatsapp/

# 3. Restart
ssh enalfarid@ptikn3-vm.cluster-vps.dalang.io 'sudo systemctl restart whatsapp-service'
git checkout main -- . # restore source di laptop

Future improvement (TODO): bikin scripts/build_whatsapp_service.sh yang otomatis preserve last binary sebagai whatsapp-service.prev sebelum overwrite, supaya rollback tinggal mv whatsapp-service.prev whatsapp-service && systemctl restart. Pola sama dengan scripts/build_email_service.sh.


8. Troubleshootingโ€‹

Service refuse start di productionโ€‹

sudo systemctl status whatsapp-service
sudo journalctl -u whatsapp-service --since "5 minutes ago"

Pesan-pesan common:

LogPenyebabFix
INTERNAL_NOTIFICATION_API_KEY is required in productionenv kosong di .env.productionedit env, restart
WHATSAPP_API_SECRET is required in productionenv kosongedit env, restart
WHATSAPP_VERIFY_TOKEN is required in productionenv kosongedit env, restart
postgres init failed: ... no such hostPOSTGRES_DSN salah host atau DB VM downcek DB topology
bind: address already in useport 8091 dipakai process lainsudo ss -lntp | grep 8091, kill atau ganti port

Endpoint /internal/* selalu 503 internal_key_not_configuredโ€‹

INTERNAL_NOTIFICATION_API_KEY belum di-load. Kalau service running tapi balas 503, kemungkinan .env.production typo atau systemd unit lupa EnvironmentFile= line.

# Cek env actual yang di-load systemd
sudo systemctl show whatsapp-service | grep -i environment

Webhook signature rejectedโ€‹

sudo journalctl -u whatsapp-service --since "10 minutes ago" -o cat | \
jq 'select(.event == "whatsapp_webhook" and .status == "rejected")'

Common causes:

  • WHATSAPP_API_SECRET di service โ‰  App Secret di Meta UI (sync mismatch)
  • Reverse proxy mengubah body (Caddy/nginx body buffering atau gzip decompress โ€” HMAC butuh raw body utuh)
  • Meta App Secret di-reset tapi service belum di-restart

Template send 422 template_not_allowedโ€‹

Template name belum di whitelist env WHATSAPP_ALLOWED_TEMPLATES. Tambah template ke list (comma-separated), restart service. Catat: template juga harus Approved di Meta WA Manager โ€” whitelist di service adalah extra guard, bukan pengganti approval Meta.


9. Disaster recoveryโ€‹

Service stateless โ€” semua state di Postgres. Rebuild scenario:

  1. Re-provision VM (atau VM baru)
  2. Setup user enalfarid + folder ~/kesles_merchant/merchant_whatsapp/
  3. SCP binary + .env.production (dari secret vault / 1Password)
  4. Install systemd unit dari ยง2.4
  5. systemctl enable && start
  6. Update reverse proxy / DNS untuk arahkan ke VM baru
  7. Test webhook Meta dengan Send sample message

RTO target: <30 menit (kalau credential + binary tersedia siap). RPO: 0 (Postgres replicated, message audit row tidak hilang).


10. Dokumen referensiโ€‹