Add comprehensive documentation for CI/CD pipeline, NATS, security practices, Tailscale networking, and troubleshooting guide
- Created CI/CD pipeline documentation detailing workflows, triggers, and actions for `asepharyana-hub`. - Added NATS + JetStream guide covering architecture, configuration, CLI tools, and event topics. - Introduced a security guide outlining best practices for secrets management, TLS, container security, and access control. - Documented Tailscale networking setup and troubleshooting for connectivity between VPS and bare-metal nodes. - Compiled a troubleshooting guide addressing common issues across deployment, Dapr, NATS, Traefik, Tailscale, Docker, database, and submodules.
This commit is contained in:
@@ -0,0 +1,186 @@
|
|||||||
|
# Arsitektur asepharyana-hub
|
||||||
|
|
||||||
|
## Topologi Fisik
|
||||||
|
|
||||||
|
Dua node terhubung via **Tailscale** overlay network:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────┐ ┌──────────────────────────────┐
|
||||||
|
│ orangevps (VPS) │ │ imrnes (Bare-metal) │
|
||||||
|
│ IP: 45.127.35.244 │ │ Tailscale: 100.121.180.82 │
|
||||||
|
│ Tailscale: 100.x.x.x │◄──────┤ │
|
||||||
|
│ │ │ Layanan: │
|
||||||
|
│ Layanan: │ │ ├─ PostgreSQL (port 6432) │
|
||||||
|
│ ├─ Traefik (port 80/443) │ │ └─ Redis (port 6379) │
|
||||||
|
│ ├─ NATS + JetStream │ │ │
|
||||||
|
│ ├─ Dapr Placement │ └──────────────────────────────┘
|
||||||
|
│ ├─ Redis (cache, Dapr) │
|
||||||
|
│ ├─ Scraper API + Dapr │
|
||||||
|
│ └─ (future services) │
|
||||||
|
└──────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Konektivitas Container ke Tailscale
|
||||||
|
|
||||||
|
Container di `orangevps` tidak bisa langsung mencapai IP Tailscale (`100.x.x.x`). Route Tailscale harus ditambahkan ke tabel routing utama (`main`) via `tailscale-routes.service` agar traffic dari container bisa melewati host ke Tailscale.
|
||||||
|
|
||||||
|
## Alur Request HTTP (External)
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
│
|
||||||
|
▼ Port 443
|
||||||
|
Traefik (v3.6)
|
||||||
|
├─ TLS termination (sertifikat dari volume mount)
|
||||||
|
├─ Middleware chain: secure-headers → compress → retry → rate-limit → buffer
|
||||||
|
├─ Plugin: real-ip (Cloudflare), block-sensitive-paths
|
||||||
|
│
|
||||||
|
▼ Router matching
|
||||||
|
Host(`scraper.asepharyana.my.id`) || Host(`api.asepharyana.my.id`)
|
||||||
|
│
|
||||||
|
▼ Service load balancer
|
||||||
|
http://scraper-api:4091
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Scraper API (Rust / Axum)
|
||||||
|
├─ Health check: GET /, respon 200
|
||||||
|
├─ REST endpoints
|
||||||
|
├─ Database via `DATABASE_URL` (Tailscale → PostgreSQL di imrnes)
|
||||||
|
├─ Cache via `REDIS_URL` (Redis lokal di container)
|
||||||
|
└─ Pub/sub via Dapr sidecar (localhost:3500)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Infrastruktur Internal
|
||||||
|
|
||||||
|
### Docker Compose Project
|
||||||
|
|
||||||
|
Semua service berjalan dalam satu Docker Compose project bernama `compose` dan bergabung di network `app-shared-net`:
|
||||||
|
|
||||||
|
| File | Service | Peran |
|
||||||
|
|------|---------|-------|
|
||||||
|
| `traefik.yml` | `traefik` | Reverse proxy, TLS termination, middleware |
|
||||||
|
| `shared.yml` | `redis` | Cache, session store, backend Dapr pub/sub & state |
|
||||||
|
| `nats.yml` | `nats` | Message broker + JetStream persistent streaming |
|
||||||
|
| `dapr.yml` | `dapr-placement` | Koordinasi actor placement untuk sidecar Dapr |
|
||||||
|
| `scraper.yml` | `scraper-api` + `scraper-api-dapr` | Aplikasi Rust + sidecar Dapr |
|
||||||
|
|
||||||
|
### Dapr Sidecar Pattern
|
||||||
|
|
||||||
|
Setiap aplikasi yang menggunakan Dapr mendapat sidecar container `daprd`:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ scraper-api │
|
||||||
|
│ (app port 4091) │
|
||||||
|
└────────┬────────────┘
|
||||||
|
│ localhost:3500 (HTTP)
|
||||||
|
│ localhost:50001 (gRPC)
|
||||||
|
┌────────▼────────────┐
|
||||||
|
│ scraper-api-dapr │
|
||||||
|
│ (daprd sidecar) │
|
||||||
|
│ │
|
||||||
|
│ Dapr components: │
|
||||||
|
│ ├─ pubsub.redis │
|
||||||
|
│ └─ state.redis │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Komponen Dapr:
|
||||||
|
|
||||||
|
| Komponen | Tipe | Backend |
|
||||||
|
|----------|------|---------|
|
||||||
|
| `pubsub` | `pubsub.redis` | `redis:6379` |
|
||||||
|
| `statestore` | `state.redis` | `redis:6379` (prefix `dapr`) |
|
||||||
|
|
||||||
|
### NATS + JetStream
|
||||||
|
|
||||||
|
NATS berjalan dengan flag `-js` untuk mengaktifkan JetStream. Persistent stream disimpan di volume `nats_data`. Dapr pub/sub routing:
|
||||||
|
|
||||||
|
```
|
||||||
|
Service → Dapr sidecar (pubsub.redis) → Redis streams
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Catatan:** Saat ini Dapr pub/sub menggunakan Redis, bukan NATS. Jika ingin migrasi ke NATS untuk pub/sub, komponen Dapr perlu diganti dengan `pubsub.nats`.
|
||||||
|
|
||||||
|
## Arsitektur CI/CD
|
||||||
|
|
||||||
|
```
|
||||||
|
Push ke main (apps/**, infra/**)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
docker-build-push.yml
|
||||||
|
├─ Phase 1: Detect changed services
|
||||||
|
├─ Phase 2: Build & Push image ke GHCR
|
||||||
|
└─ Phase 3: Update compose manifest + submodule pointer
|
||||||
|
│
|
||||||
|
▼ (workflow_run trigger)
|
||||||
|
deploy-docker.yml
|
||||||
|
├─ SSH ke orangevps
|
||||||
|
├─ Git sync, pull images
|
||||||
|
├─ Remove stale containers
|
||||||
|
└─ Selective restart service
|
||||||
|
```
|
||||||
|
|
||||||
|
Submodule update dari remote repo via `repository_dispatch`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Push ke asepharyana-hub-scraper
|
||||||
|
│
|
||||||
|
▼ (repository_dispatch)
|
||||||
|
update-submodule.yml
|
||||||
|
├─ Update submodule pointer
|
||||||
|
└─ Commit & push ke hub repo
|
||||||
|
│
|
||||||
|
▼ (repository_dispatch trigger)
|
||||||
|
docker-build-push.yml
|
||||||
|
└─ Build, push, deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Tagging Strategy
|
||||||
|
|
||||||
|
| Tag | Contoh | Penggunaan |
|
||||||
|
|-----|--------|------------|
|
||||||
|
| `sha-<short>` | `sha-a3c5d74` | Immutable, deterministic rollback |
|
||||||
|
| `latest` | `latest` | Mutable, convenience |
|
||||||
|
| `buildcache` | `sha-a3c5d74-buildcache` | Registry-based build cache (internal) |
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
### Port Map
|
||||||
|
|
||||||
|
| Port | Service | Deskripsi |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| 443 | Traefik | HTTPS eksternal |
|
||||||
|
| 80 | Traefik | Redirect ke HTTPS |
|
||||||
|
| 4222 | NATS | Client connections |
|
||||||
|
| 8222 | NATS | HTTP monitor / health |
|
||||||
|
| 6379 | Redis | Internal container network |
|
||||||
|
| 3500 | Dapr sidecar | Dapr HTTP API (per service) |
|
||||||
|
| 50001 | Dapr sidecar | Dapr gRPC API (per service) |
|
||||||
|
| 50005 | Dapr placement | Actor placement |
|
||||||
|
| 4091 | Scraper API | Aplikasi HTTP |
|
||||||
|
|
||||||
|
## Event Topics Convention
|
||||||
|
|
||||||
|
Semua event menggunakan prefix `hub.`:
|
||||||
|
|
||||||
|
| Topic | Payload | Deskripsi |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| `hub.image.cached` | `{original_url, cdn_url, source}` | Image selesai di-cache |
|
||||||
|
| `hub.image.repaired` | `{old_url, new_url}` | CNAME image diperbaiki |
|
||||||
|
| `hub.scrape.anime.done` | `{source, slug, duration}` | Scrape anime selesai |
|
||||||
|
| `hub.system.alert` | `{service, level, message}` | Error/alert dari service |
|
||||||
|
|
||||||
|
## Service Registry (Traefik)
|
||||||
|
|
||||||
|
Domain routing:
|
||||||
|
|
||||||
|
| Subdomain | Service | URL Backend |
|
||||||
|
|-----------|---------|-------------|
|
||||||
|
| `scraper.*` | Scraper API | `http://scraper-api:4091` |
|
||||||
|
| `api.*` | Scraper API (alias) | `http://scraper-api:4091` |
|
||||||
|
| `traefik.*` | Traefik Dashboard | `api@internal` |
|
||||||
|
|
||||||
|
Semua domain tersedia di:
|
||||||
|
- `<service>.asepharyana.my.id`
|
||||||
|
- `<service>.asepharyana.web.id`
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Development Guide
|
||||||
|
|
||||||
|
Panduan setup lingkungan development lokal untuk kontributor `asepharyana-hub`.
|
||||||
|
|
||||||
|
## Prasyarat
|
||||||
|
|
||||||
|
| Tool | Versi Minimal | Catatan |
|
||||||
|
|------|---------------|---------|
|
||||||
|
| Git | 2.40+ | Submodule support |
|
||||||
|
| Docker | 24+ | Dengan Docker Compose v2 plugin |
|
||||||
|
| Rust | 1.85+ | Hanya untuk `apps/scraper` |
|
||||||
|
| Bun | 1.x | Root tooling (Biome) |
|
||||||
|
| Dapr CLI | 1.14+ | Opsional, untuk development dengan Dapr |
|
||||||
|
|
||||||
|
## Setup Awal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone repo
|
||||||
|
git clone https://github.com/asepharyana/asepharyana-hub.git
|
||||||
|
cd asepharyana-hub
|
||||||
|
|
||||||
|
# 2. Init submodules
|
||||||
|
make init-submodules
|
||||||
|
|
||||||
|
# 3. Setup environment
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env sesuai kebutuhan lokal
|
||||||
|
|
||||||
|
# 4. Install root dependencies
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Menjalankan Infrastruktur Lokal
|
||||||
|
|
||||||
|
Beberapa service membutuhkan Redis. Jalankan dengan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev
|
||||||
|
# atau equivalen:
|
||||||
|
docker compose -f infra/compose/shared.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Ini akan menjalankan Redis Alpine di `localhost:6379`.
|
||||||
|
|
||||||
|
### (Opsional) NATS Lokal
|
||||||
|
|
||||||
|
Jika service membutuhkan pub/sub:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f infra/compose/nats.yml up -d
|
||||||
|
# NATS client: localhost:4222
|
||||||
|
# NATS monitor: localhost:8222
|
||||||
|
```
|
||||||
|
|
||||||
|
### (Opsional) Dapr Placement Lokal
|
||||||
|
|
||||||
|
Jika service membutuhkan sidecar Dapr:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f infra/compose/dapr.yml up -d
|
||||||
|
# Dapr placement: localhost:50005
|
||||||
|
```
|
||||||
|
|
||||||
|
## Menjalankan Service Lokal
|
||||||
|
|
||||||
|
### Scraper API (Rust)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pastikan Redis sudah running (make dev)
|
||||||
|
cd apps/scraper
|
||||||
|
|
||||||
|
# Cargo run
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
# Dengan Dapr sidecar (jika placement running)
|
||||||
|
dapr run \
|
||||||
|
--app-id scraper-api \
|
||||||
|
--app-port 4091 \
|
||||||
|
--dapr-http-port 3500 \
|
||||||
|
--resources-path ../../infra/dapr/components \
|
||||||
|
-- cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dengan Docker Compose (Full Stack)
|
||||||
|
|
||||||
|
Untuk menjalankan semua service sekaligus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
-f infra/compose/shared.yml \
|
||||||
|
-f infra/compose/nats.yml \
|
||||||
|
-f infra/compose/dapr.yml \
|
||||||
|
-f infra/compose/scraper.yml \
|
||||||
|
--env-file .env \
|
||||||
|
up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Untuk service baru, tambahkan compose file-nya ke daftar.
|
||||||
|
|
||||||
|
## Update Submodules
|
||||||
|
|
||||||
|
### Pull latest dari semua submodule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make update-submodules
|
||||||
|
# atau:
|
||||||
|
git submodule update --remote --merge --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check status submodule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make status
|
||||||
|
# atau:
|
||||||
|
git submodule status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sync .env ke submodule
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/2updateenv.sh
|
||||||
|
# Copy .env root ke apps/*/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linting & Formatting
|
||||||
|
|
||||||
|
Root repo menggunakan **Biome** untuk linting dan formatting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run check # Lint + format + write
|
||||||
|
bun run ci # CI mode (no write, exit code on issues)
|
||||||
|
bun run lint # Lint only
|
||||||
|
bun run format # Format only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Docker Image Lokal
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Scraper API
|
||||||
|
docker build -f infra/docker/scraper.Dockerfile -t scraper-api:local .
|
||||||
|
|
||||||
|
# Service baru: tambahkan Dockerfile di infra/docker/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Saat ini belum ada test runner di root level. Masing-masing submodule mengelola testing sendiri:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Scraper API (Rust)
|
||||||
|
cd apps/scraper && cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validasi YAML
|
||||||
|
|
||||||
|
Sebelum commit perubahan infra, validasi semua file YAML:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "
|
||||||
|
import pathlib, yaml
|
||||||
|
for p in pathlib.Path('infra').rglob('*.yml'):
|
||||||
|
with open(p) as f: yaml.safe_load(f)
|
||||||
|
print(f'OK {p}')
|
||||||
|
for p in pathlib.Path('infra').rglob('*.yaml'):
|
||||||
|
with open(p) as f: yaml.safe_load(f)
|
||||||
|
print(f'OK {p}')
|
||||||
|
"
|
||||||
|
|
||||||
|
for f in infra/compose/*.yml; do
|
||||||
|
docker compose -f "$f" config >/dev/null && echo "OK $f"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
## Git Workflow
|
||||||
|
|
||||||
|
### Commit Convention
|
||||||
|
|
||||||
|
```
|
||||||
|
<type>(<scope>): <description>
|
||||||
|
```
|
||||||
|
|
||||||
|
Type: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `perf`, `style`
|
||||||
|
Scope: `scraper`, `infra`, `ci`, `dapr`, `nats`, `docs`, `deps`, `scripts`, `root`
|
||||||
|
|
||||||
|
Contoh:
|
||||||
|
```
|
||||||
|
feat(scraper): add image cache endpoint
|
||||||
|
fix(infra): correct Traefik rate-limit config
|
||||||
|
chore(deps): bump biome to 2.5.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Branch Strategy
|
||||||
|
|
||||||
|
- `main` — production branch, push triggers CI/CD
|
||||||
|
- Fitur baru: branch dari `main`, PR ke `main`
|
||||||
|
- Submodule development: dilakukan di repo masing-masing, hub hanya update pointer
|
||||||
|
|
||||||
|
## Deployment ke VPS
|
||||||
|
|
||||||
|
Push ke `main` otomatis trigger CI/CD. Untuk trigger manual:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run deploy-docker.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Lihat `docs/DEPLOYMENT.md` untuk detail.
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# Backup & Disaster Recovery
|
||||||
|
|
||||||
|
## Aset yang Perlu di-Backup
|
||||||
|
|
||||||
|
| Aset | Lokasi | Frekuensi | Metode |
|
||||||
|
|------|--------|-----------|--------|
|
||||||
|
| Database PostgreSQL | `imrnes` (100.121.180.82:6432) | Harian | `pg_dump` |
|
||||||
|
| Volume Redis | `orangevps` (Docker volume) | Opsional | Redis RDB / AOF |
|
||||||
|
| Volume NATS JetStream | `orangevps` (Docker volume) | Opsional | File copy |
|
||||||
|
| Docker Compose manifests | GitHub (hub repo) | Real-time | Git |
|
||||||
|
| Environment variables | GitHub secret `ENV_FILE_PRODUCTION` | Manual | `gh secret set` |
|
||||||
|
| TLS certificates | `orangevps` (`/root/*.pem`, `*.key`) | Saat renew | SCP |
|
||||||
|
| Tailscale auth | Tailscale admin console | - | Cloud-managed |
|
||||||
|
| GitHub Actions secrets | GitHub UI | Manual | Backup list |
|
||||||
|
|
||||||
|
## Database PostgreSQL (Prioritas Tertinggi)
|
||||||
|
|
||||||
|
### Backup Manual
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dari orangevps (via Tailscale)
|
||||||
|
pg_dump -h 100.121.180.82 -p 6432 -U asephs -d hub \
|
||||||
|
--no-owner --no-acl \
|
||||||
|
-F c -f /root/db-backups/hub-$(date +%Y%m%d-%H%M%S).dump
|
||||||
|
|
||||||
|
# Atau dari imrnes langsung
|
||||||
|
pg_dump -U asephs -d hub \
|
||||||
|
-F c -f /backup/hub/hub-$(date +%Y%m%d-%H%M%S).dump
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Drop dan recreate database
|
||||||
|
dropdb -h 100.121.180.82 -p 6432 -U asephs hub
|
||||||
|
createdb -h 100.121.180.82 -p 6432 -U asephs hub
|
||||||
|
|
||||||
|
# Restore dari dump
|
||||||
|
pg_restore -h 100.121.180.82 -p 6432 -U asephs -d hub \
|
||||||
|
--no-owner --no-acl \
|
||||||
|
/path/to/backup/hub-20260101-120000.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Otomatis (via Cron di imrnes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# /etc/cron.d/hub-db-backup
|
||||||
|
0 2 * * * root pg_dump -U asephs -d hub -F c -f /backup/hub/hub-$(date +\%Y\%m\%d).dump && find /backup/hub -name "hub-*.dump" -mtime +30 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
## Volume Docker
|
||||||
|
|
||||||
|
### Redis
|
||||||
|
|
||||||
|
Redis data bisa di-recover dari NATS events (event sourcing). Jika tidak ada persistence requirement, cukup restart:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker volume rm redis_data
|
||||||
|
docker compose -f infra/compose/shared.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Jika perlu backup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Save RDB snapshot
|
||||||
|
docker exec redis redis-cli SAVE
|
||||||
|
|
||||||
|
# Copy dari volume
|
||||||
|
docker run --rm -v redis_data:/data -v /backup:/backup alpine cp /data/dump.rdb /backup/redis-$(date +%Y%m%d).rdb
|
||||||
|
```
|
||||||
|
|
||||||
|
### NATS JetStream
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup volume
|
||||||
|
docker run --rm -v nats_data:/data -v /backup:/backup alpine \
|
||||||
|
tar czf /backup/nats-$(date +%Y%m%d).tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
### Backup `.env` dari VPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simpan current .env dari VPS
|
||||||
|
ssh root@45.127.35.244 "cat /root/asepharyana-hub/.env" > .env.backup.$(date +%Y%m%d)
|
||||||
|
|
||||||
|
# Update GitHub secret
|
||||||
|
cat .env.backup.$(date +%Y%m%d) | gh secret set ENV_FILE_PRODUCTION --repo asepharyana/asepharyana-hub
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore `.env` jika hilang
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Buat .env baru dari template
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit secrets (manual dari password manager atau GitHub secret)
|
||||||
|
# Atau download dari GitHub secret
|
||||||
|
gh secret list --repo asepharyana/asepharyana-hub
|
||||||
|
```
|
||||||
|
|
||||||
|
## TLS Certificates
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Di orangevps
|
||||||
|
tar czf /root/cert-backup-$(date +%Y%m%d).tar.gz \
|
||||||
|
/root/asepharyana.my.id.pem \
|
||||||
|
/root/asepharyana.my.id.key \
|
||||||
|
/root/asepharyana.web.id.pem \
|
||||||
|
/root/asepharyana.web.id.key \
|
||||||
|
/root/asepharyana-hub/infra/traefik/dynamic/ssl.yaml
|
||||||
|
|
||||||
|
# SCP ke local
|
||||||
|
scp root@45.127.35.244:/root/cert-backup-*.tar.gz .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SCP ke VPS
|
||||||
|
scp cert-backup-20260101.tar.gz root@45.127.35.244:/root/
|
||||||
|
|
||||||
|
# Extract
|
||||||
|
ssh root@45.127.35.244 "tar xzf /root/cert-backup-20260101.tar.gz -C / && docker restart traefik"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Disaster Recovery Scenarios
|
||||||
|
|
||||||
|
### Skenario 1: VPS (orangevps) mati total
|
||||||
|
|
||||||
|
**Dampak:** Semua service down.
|
||||||
|
|
||||||
|
**Recovery:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Provision VPS baru (atau restore dari snapshot)
|
||||||
|
# 2. Install Docker + Tailscale
|
||||||
|
# 3. Clone repo
|
||||||
|
git clone https://github.com/asepharyana/asepharyana-hub.git /root/asepharyana-hub
|
||||||
|
|
||||||
|
# 4. Setup Tailscale, route service
|
||||||
|
# 5. Restore .env
|
||||||
|
echo "<ENV_FILE_PRODUCTION>" > /root/asepharyana-hub/.env
|
||||||
|
|
||||||
|
# 6. Restore TLS certs
|
||||||
|
# 7. Create network
|
||||||
|
docker network create app-shared-net
|
||||||
|
|
||||||
|
# 8. Start services sesuai urutan
|
||||||
|
cd /root/asepharyana-hub
|
||||||
|
for f in shared.yml nats.yml dapr.yml traefik.yml scraper.yml; do
|
||||||
|
docker compose -f infra/compose/$f --env-file .env up -d
|
||||||
|
done
|
||||||
|
|
||||||
|
# 9. Update DNS jika IP baru
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skenario 2: Database (imrnes) mati total
|
||||||
|
|
||||||
|
**Dampak:** Semua service yang butuh database error.
|
||||||
|
|
||||||
|
**Recovery:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Fix imrnes atau provision server baru
|
||||||
|
# 2. Setup PostgreSQL
|
||||||
|
# 3. Restore dari backup terakhir
|
||||||
|
# 4. Update Tailscale IP jika perlu
|
||||||
|
# 5. Update .env dan GitHub secret
|
||||||
|
# 6. Redeploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skenario 3: GitHub repository hilang
|
||||||
|
|
||||||
|
**Dampak:** Kehilangan CI/CD, tapi Docker images masih ada di GHCR.
|
||||||
|
|
||||||
|
**Recovery:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create repo baru di GitHub
|
||||||
|
# 2. Push dari local clone
|
||||||
|
git remote add origin-new https://github.com/asepharyana/asepharyana-hub-new.git
|
||||||
|
git push origin-new main
|
||||||
|
|
||||||
|
# 3. Re-create GitHub secrets
|
||||||
|
# 4. Re-create workflows
|
||||||
|
# 5. Update VPS remote
|
||||||
|
ssh root@45.127.35.244 "cd /root/asepharyana-hub && git remote set-url origin https://github.com/asepharyana/asepharyana-hub-new.git"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skenario 4: GHCR registry tidak bisa diakses
|
||||||
|
|
||||||
|
**Dampak:** Tidak bisa pull image.
|
||||||
|
|
||||||
|
**Recovery:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Build image langsung di VPS
|
||||||
|
docker build -f infra/docker/scraper.Dockerfile -t ghcr.io/asepharyana/asepharyana-hub/scraper-api:local .
|
||||||
|
|
||||||
|
# 2. Update compose file untuk sementara
|
||||||
|
sed -i 's|image: ghcr.io/.*|image: ghcr.io/asepharyana/asepharyana-hub/scraper-api:local|' infra/compose/scraper.yml
|
||||||
|
|
||||||
|
# 3. Start
|
||||||
|
docker compose -f infra/compose/scraper.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skenario 5: Semua server mati (total loss)
|
||||||
|
|
||||||
|
**Recovery:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Provision VPS baru
|
||||||
|
# 2. Provision server database baru
|
||||||
|
# 3. Setup Tailscale
|
||||||
|
# 4. Clone repo, restore .env, certs
|
||||||
|
# 5. Restore database dari backup (jika ada)
|
||||||
|
# 6. Jika tidak ada backup database:
|
||||||
|
# - Build image dari GHCR
|
||||||
|
# - Start service dengan database kosong
|
||||||
|
# - Data akan terisi ulang dari scraping
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checklist Pencegahan
|
||||||
|
|
||||||
|
- [ ] Cron job backup database berjalan
|
||||||
|
- [ ] Backup `.env` disimpan di luar VPS (password manager)
|
||||||
|
- [ ] TLS certificates backup disimpan di luar VPS
|
||||||
|
- [ ] GitHub secrets terdaftar (tidak hanya diingat)
|
||||||
|
- [ ] Docker images bisa di-rebuild dari CI (GHCR sebagai source of truth)
|
||||||
|
- [ ] Tailscale admin access via multiple accounts
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
# CI/CD Pipeline
|
||||||
|
|
||||||
|
Dokumentasi pipeline CI/CD untuk `asepharyana-hub`. Terdiri dari 5 GitHub Actions workflow yang saling terhubung.
|
||||||
|
|
||||||
|
## Workflow Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ Lint │ (PR/push → Biome)
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
Push ke main ─────┼────── repository_dispatch
|
||||||
|
│
|
||||||
|
┌──────▼──────────────────┐
|
||||||
|
│ docker-build-push.yml │
|
||||||
|
│ │
|
||||||
|
│ Phase 1: Detect │
|
||||||
|
│ Phase 2: Build & Push │
|
||||||
|
│ Phase 3: Update │
|
||||||
|
│ manifests │
|
||||||
|
└──────┬──────────────────┘
|
||||||
|
│ workflow_run
|
||||||
|
┌──────▼──────────────┐
|
||||||
|
│ deploy-docker.yml │
|
||||||
|
│ SSH → VPS │
|
||||||
|
│ Pull → Restart │
|
||||||
|
└─────────────────────┘
|
||||||
|
|
||||||
|
repository_dispatch ──► update-submodule.yml
|
||||||
|
(dari submodule) (update pointer → commit)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
docker-build-push.yml
|
||||||
|
(triggered by push)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Detail
|
||||||
|
|
||||||
|
### 1. Lint (`lint.yml`)
|
||||||
|
|
||||||
|
**Trigger:** PR/push ke `main` yang mengubah `*.json`, `*.js`, `biome.json`
|
||||||
|
|
||||||
|
**Aksi:**
|
||||||
|
- Checkout repo dengan submodules
|
||||||
|
- Setup Bun
|
||||||
|
- `bun install --frozen-lockfile`
|
||||||
|
- `bun run ci` (Biome CI mode)
|
||||||
|
|
||||||
|
**Permissions:** read-only
|
||||||
|
|
||||||
|
### 2. Build and Push Docker Images (`docker-build-push.yml`)
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
- Push ke `main` yang mengubah `apps/**`, `infra/**`, atau file workflow
|
||||||
|
- `repository_dispatch` tipe `submodule-updated`
|
||||||
|
- `workflow_dispatch` (manual)
|
||||||
|
|
||||||
|
**Concurrency:** Satu workflow per branch (cancel-in-progress=false)
|
||||||
|
|
||||||
|
#### Phase 1: Detect Changes
|
||||||
|
|
||||||
|
Job `changes` mendeteksi service mana yang perlu di-build:
|
||||||
|
|
||||||
|
- **Push event:** `git diff --name-only` antara `before` dan `after` SHA
|
||||||
|
- **repository_dispatch:** Parse payload `{service, sha}` dan validasi
|
||||||
|
- **workflow_dispatch:** Build semua service
|
||||||
|
|
||||||
|
Output format matrix:
|
||||||
|
```json
|
||||||
|
[{"id":"scraper-api","target":"docker-scraper","path":"apps/scraper"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Phase 2: Build & Push (Matrix)
|
||||||
|
|
||||||
|
Job `build` berjalan paralel per service (matrix strategy):
|
||||||
|
|
||||||
|
1. Checkout repo + sync submodule
|
||||||
|
2. Jika `repository_dispatch`, checkout submodule ke SHA tertentu
|
||||||
|
3. Login ke GHCR
|
||||||
|
4. Setup Docker Buildx
|
||||||
|
5. Build & push dengan tag:
|
||||||
|
- `ghcr.io/asepharyana/asepharyana-hub/<service>:latest`
|
||||||
|
- `ghcr.io/asepharyana/asepharyana-hub/<service>:sha-<shortsha>`
|
||||||
|
6. Build cache: registry-based (`:<service>:buildcache`)
|
||||||
|
|
||||||
|
#### Phase 3: Update Manifests
|
||||||
|
|
||||||
|
Job `update-manifest`:
|
||||||
|
|
||||||
|
1. Update image tag di compose file (`infra/compose/<service>.yml`)
|
||||||
|
2. Jika `repository_dispatch`, update submodule pointer
|
||||||
|
3. Commit dengan message `chore: update manifests and submodules [skip ci]`
|
||||||
|
4. Push dengan retry (3 attempts, rebase jika conflict)
|
||||||
|
|
||||||
|
### 3. Deploy Docker to VPS (`deploy-docker.yml`)
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
- `workflow_run` setelah `docker-build-push.yml` selesai
|
||||||
|
- Push ke `main` yang mengubah `infra/**`
|
||||||
|
- `workflow_dispatch` (manual)
|
||||||
|
|
||||||
|
**Concurrency:** Satu deployment dalam satu waktu (`group: deploy-vps`)
|
||||||
|
|
||||||
|
**Aksi di VPS (via SSH):**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Setup SSH multiplexing
|
||||||
|
2. SCP .env dari GitHub secret ke VPS
|
||||||
|
3. Docker login ke GHCR
|
||||||
|
4. Git sync (fetch + reset --hard)
|
||||||
|
5. Detect changed files:
|
||||||
|
├─ Compose stack changes → selective container update
|
||||||
|
├─ Traefik dynamic config → SIGHUP
|
||||||
|
└─ Other infra → full deploy
|
||||||
|
6. Pull images (retry 3x)
|
||||||
|
7. Remove stale containers
|
||||||
|
8. Up services
|
||||||
|
9. SIGHUP Traefik jika perlu
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Security Scan (`security.yml`)
|
||||||
|
|
||||||
|
**Trigger:**
|
||||||
|
- PR ke `main`
|
||||||
|
- Jadwal: Setiap Senin (`0 6 * * 1`)
|
||||||
|
|
||||||
|
**Aksi:**
|
||||||
|
- Checkout dengan fetch-depth 2
|
||||||
|
- CodeQL init untuk Rust
|
||||||
|
- `cargo build` di `apps/scraper`
|
||||||
|
- CodeQL analyze
|
||||||
|
|
||||||
|
### 5. Update Submodule Pointer (`update-submodule.yml`)
|
||||||
|
|
||||||
|
**Trigger:** `repository_dispatch` tipe `submodule-updated`
|
||||||
|
|
||||||
|
**Aksi:**
|
||||||
|
1. Validasi payload (`service`, `sha`)
|
||||||
|
2. Map service ke submodule path (e.g., `scraper-api` → `apps/scraper`)
|
||||||
|
3. Update submodule ke SHA yang diberikan
|
||||||
|
4. Commit sebagai `monrepo-bot` dengan message:
|
||||||
|
`chore: update <service> to <shortsha>`
|
||||||
|
5. Push dengan retry (3 attempts)
|
||||||
|
|
||||||
|
## Flow Submodule Update
|
||||||
|
|
||||||
|
Flow lengkap ketika code berubah di submodule repo:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Developer push ke asepharyana-hub-scraper
|
||||||
|
2. GitHub Action di scraper repo kirim repository_dispatch
|
||||||
|
ke asepharyana-hub
|
||||||
|
3. update-submodule.yml terima dispatch, update pointer
|
||||||
|
4. Commit masuk ke hub repo main
|
||||||
|
5. Commit ini trigger docker-build-push.yml
|
||||||
|
(push ke main dengan path apps/scraper/**)
|
||||||
|
6. Build image baru, update compose file
|
||||||
|
7. Deploy ke VPS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secrets yang Diperlukan
|
||||||
|
|
||||||
|
| Secret | Workflow | Deskripsi |
|
||||||
|
|--------|----------|-----------|
|
||||||
|
| `SSH_PRIVATE_KEY` | deploy-docker | SSH key untuk akses VPS |
|
||||||
|
| `VPS_HOST` | deploy-docker | IP VPS (`45.127.35.244`) |
|
||||||
|
| `VPS_USER` | deploy-docker | User SSH (`root`) |
|
||||||
|
| `VPS_TARGET_DIR` | deploy-docker | Dir di VPS (`/root/asepharyana-hub`) |
|
||||||
|
| `ENV_FILE_PRODUCTION` | deploy-docker | Full `.env` production |
|
||||||
|
|
||||||
|
## Menambahkan Service Baru ke Pipeline
|
||||||
|
|
||||||
|
Untuk menambahkan service baru, update:
|
||||||
|
|
||||||
|
### `docker-build-push.yml`
|
||||||
|
|
||||||
|
1. **Phase 1 — `changes` job:** Tambah detection logic untuk service baru:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
echo "new-service=$(changed '^(apps/new-service(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/new-service\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Phase 1 — `repository_dispatch`:** Tambah case:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
case "$SERVICE" in
|
||||||
|
scraper-api|new-service) ;;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Phase 1 — `set-matrix`:** Tambah service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [ "${{ ...['new-service'] == 'true' ... }}" == "true" ]; then add_service "new-service" "docker-new-service" "apps/new-service"; fi
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Phase 2 — `meta` step:** Tambah mapping Dockerfile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"new-service") echo "dockerfile=infra/docker/new-service.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Phase 3 — `update-manifest`:** Tambah mapping:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SERVICES["new-service"]="new-service.yml"
|
||||||
|
PATHS["new-service"]="apps/new-service"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `deploy-docker.yml`
|
||||||
|
|
||||||
|
Tambah compose file ke `ALL_COMPOSE_FILES`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/nats.yml infra/compose/dapr.yml infra/compose/new-service.yml"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
### Rollback Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cari SHA tag sebelumnya di GHCR packages
|
||||||
|
# Update compose file ke tag tersebut
|
||||||
|
sed -i 's|sha-badcommit|sha-goodcommit|g' infra/compose/scraper.yml
|
||||||
|
git commit -am "fix: rollback scraper-api to sha-goodcommit"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback via Git Revert
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git revert HEAD
|
||||||
|
git push origin main
|
||||||
|
# Pipeline otomatis build dan deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring Pipeline
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek status workflow terbaru
|
||||||
|
gh run list --limit 5
|
||||||
|
|
||||||
|
# Lihat log workflow tertentu
|
||||||
|
gh run view <run-id> --log
|
||||||
|
|
||||||
|
# Trigger workflow manual
|
||||||
|
gh workflow run deploy-docker.yml
|
||||||
|
```
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
# NATS + JetStream Guide
|
||||||
|
|
||||||
|
Dokumentasi konfigurasi, penggunaan, dan troubleshooting NATS di infrastruktur `asepharyana-hub`.
|
||||||
|
|
||||||
|
## Arsitektur
|
||||||
|
|
||||||
|
NATS berjalan di container `nats` dengan JetStream diaktifkan (`-js`). Data persistent disimpan di volume Docker `nats_data`.
|
||||||
|
|
||||||
|
```
|
||||||
|
Service ──► NATS (port 4222) ──► JetStream (disk)
|
||||||
|
│
|
||||||
|
├─ Monitoring HTTP: port 8222
|
||||||
|
└─ Client connections: port 4222
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hubungan dengan Dapr
|
||||||
|
|
||||||
|
Saat ini Dapr pub/sub menggunakan **Redis** (`pubsub.redis`), bukan NATS. NATS berfungsi sebagai message broker independen untuk:
|
||||||
|
|
||||||
|
- Event streaming antar service
|
||||||
|
- Persistent job queues
|
||||||
|
- Pub/sub untuk service yang tidak menggunakan Dapr
|
||||||
|
|
||||||
|
Jika ingin Dapr menggunakan NATS sebagai backend pub/sub, ganti komponen `pubsub.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: dapr.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: pubsub
|
||||||
|
spec:
|
||||||
|
type: pubsub.nats
|
||||||
|
version: v1
|
||||||
|
metadata:
|
||||||
|
- name: natsURL
|
||||||
|
value: nats://nats:4222
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfigurasi Compose
|
||||||
|
|
||||||
|
File: `infra/compose/nats.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
nats:
|
||||||
|
container_name: nats
|
||||||
|
image: nats:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
app-shared-net:
|
||||||
|
aliases:
|
||||||
|
- nats
|
||||||
|
ports:
|
||||||
|
- '4222:4222' # client connections
|
||||||
|
- '8222:8222' # HTTP monitor
|
||||||
|
command:
|
||||||
|
- '-js' # enable JetStream
|
||||||
|
- '-sd'
|
||||||
|
- '/data' # storage directory
|
||||||
|
volumes:
|
||||||
|
- nats_data:/data
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Tools
|
||||||
|
|
||||||
|
### Install NATS CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
curl -sf https://bin.nats.dev/nats | sh
|
||||||
|
sudo mv nats /usr/local/bin/
|
||||||
|
|
||||||
|
# Atau via package manager
|
||||||
|
# brew install nats-io/nats-tools/nats (macOS)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Koneksi ke NATS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dari host (port 4222 ter-expose)
|
||||||
|
nats context save hub --server nats://localhost:4222 --description "Hub Production"
|
||||||
|
nats context select hub
|
||||||
|
|
||||||
|
# Test koneksi
|
||||||
|
nats server check
|
||||||
|
nats server info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage Streams (JetStream)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List semua stream
|
||||||
|
nats stream list
|
||||||
|
|
||||||
|
# Lihat detail stream
|
||||||
|
nats stream info <stream-name>
|
||||||
|
|
||||||
|
# Buat stream
|
||||||
|
nats stream add <stream-name> \
|
||||||
|
--subjects "hub.>" \
|
||||||
|
--storage file \
|
||||||
|
--max-msgs 1000000 \
|
||||||
|
--max-bytes 1G \
|
||||||
|
--retention limits
|
||||||
|
|
||||||
|
# Hapus stream
|
||||||
|
nats stream rm <stream-name>
|
||||||
|
|
||||||
|
# Purge (hapus semua message, retain stream)
|
||||||
|
nats stream purge <stream-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pub/Sub
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Subscribe ke subject
|
||||||
|
nats sub "hub.>"
|
||||||
|
nats sub "hub.image.cached"
|
||||||
|
|
||||||
|
# Publish message
|
||||||
|
nats pub "hub.test" '{"message": "hello"}'
|
||||||
|
nats pub "hub.image.cached" '{"original_url": "https://example.com/img.jpg", "cdn_url": "https://cdn.example.com/img.jpg"}'
|
||||||
|
|
||||||
|
# Request-reply
|
||||||
|
nats request "hub.service.do" '{"task": "process"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring via HTTP API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server info
|
||||||
|
curl http://localhost:8222/
|
||||||
|
|
||||||
|
# JetStream info
|
||||||
|
curl http://localhost:8222/jszetstream
|
||||||
|
|
||||||
|
# Stream detail
|
||||||
|
curl http://localhost:8222/jszetstream?stream=<stream-name>
|
||||||
|
|
||||||
|
# Consumer info
|
||||||
|
curl http://localhost:8222/jszetstream?stream=<stream-name>&consumer=<consumer-name>
|
||||||
|
|
||||||
|
# Server stats
|
||||||
|
curl http://localhost:8222/varz
|
||||||
|
|
||||||
|
# Connections
|
||||||
|
curl http://localhost:8222/connz
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Topics Convention
|
||||||
|
|
||||||
|
Semua topik menggunakan prefix `hub.`:
|
||||||
|
|
||||||
|
| Subject | Payload | Deskripsi |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| `hub.image.cached` | `{original_url, cdn_url, source}` | Image selesai di-cache |
|
||||||
|
| `hub.image.repaired` | `{old_url, new_url}` | CNAME image diperbaiki |
|
||||||
|
| `hub.scrape.anime.done` | `{source, slug, duration}` | Scrape anime selesai |
|
||||||
|
| `hub.system.alert` | `{service, level, message}` | Error/alert dari service |
|
||||||
|
| `hub.test` | Any | Testing |
|
||||||
|
|
||||||
|
### Wildcard Subjects
|
||||||
|
|
||||||
|
NATS mendukung wildcard:
|
||||||
|
|
||||||
|
- `hub.>` — semua event hub (multi-level)
|
||||||
|
- `hub.image.*` — semua event image (single-level)
|
||||||
|
- `hub.*.done` — semua event yang selesai (single-level)
|
||||||
|
|
||||||
|
## JetStream Configuration
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
Data JetStream disimpan di volume Docker `nats_data`.
|
||||||
|
|
||||||
|
Lokasi di VPS:
|
||||||
|
```bash
|
||||||
|
docker volume inspect nats_data
|
||||||
|
# atau
|
||||||
|
ls -la /var/lib/docker/volumes/nats_data/_data/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory & Limits
|
||||||
|
|
||||||
|
NATS tidak memiliki konfigurasi limit memori default. Untuk production, pertimbangkan:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
command:
|
||||||
|
- '-js'
|
||||||
|
- '-sd'
|
||||||
|
- '/data'
|
||||||
|
- '--max_pending_size=64MB'
|
||||||
|
- '--max_payload=1MB'
|
||||||
|
```
|
||||||
|
|
||||||
|
Atau gunakan NATS configuration file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# nats-server.conf
|
||||||
|
jetstream:
|
||||||
|
max_memory_store: 256MB
|
||||||
|
max_file_store: 10GB
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Stream data tidak muncul
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Cek koneksi NATS
|
||||||
|
nats server check
|
||||||
|
|
||||||
|
# 2. Cek apakah JetStream aktif
|
||||||
|
curl http://localhost:8222/jszetstream
|
||||||
|
|
||||||
|
# 3. Cek stream dan message count
|
||||||
|
nats stream list
|
||||||
|
|
||||||
|
# 4. Subscribe langsung untuk test
|
||||||
|
nats sub ">"
|
||||||
|
```
|
||||||
|
|
||||||
|
### NATS tidak bisa start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek log
|
||||||
|
docker logs nats
|
||||||
|
|
||||||
|
# Cek apakah port 4222 sudah dipakai
|
||||||
|
ss -tlnp | grep 4222
|
||||||
|
|
||||||
|
# Cek volume data korup
|
||||||
|
docker run --rm -v nats_data:/data alpine ls -la /data
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker compose -f infra/compose/nats.yml up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disk JetStream penuh
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek ukuran volume
|
||||||
|
docker system df -v | grep nats_data
|
||||||
|
|
||||||
|
# Purge stream jika perlu
|
||||||
|
nats stream purge <stream-name>
|
||||||
|
|
||||||
|
# Atau hapus volume (data hilang!)
|
||||||
|
docker compose -f infra/compose/nats.yml down
|
||||||
|
docker volume rm nats_data
|
||||||
|
docker compose -f infra/compose/nats.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slow consumer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek consumer lag
|
||||||
|
nats stream info <stream-name>
|
||||||
|
# Lihat fields: "Pending" dan "Acknowledgment"
|
||||||
|
|
||||||
|
# Lihat stats server
|
||||||
|
curl http://localhost:8222/varz | jq '.slow_consumers'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration: Redis Pub/Sub ke NATS
|
||||||
|
|
||||||
|
Jika ingin migrasi dari Dapr pub/sub Redis ke NATS:
|
||||||
|
|
||||||
|
1. Buat stream NATS untuk topik `hub.>`
|
||||||
|
2. Update `infra/dapr/components/pubsub.yaml` dari `pubsub.redis` ke `pubsub.nats`
|
||||||
|
3. Deploy ulang semua service (Dapr sidecar akan reconnect)
|
||||||
|
4. Verifikasi event flow
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# infra/dapr/components/pubsub.yaml (setelah migrasi)
|
||||||
|
apiVersion: dapr.io/v1alpha1
|
||||||
|
kind: Component
|
||||||
|
metadata:
|
||||||
|
name: pubsub
|
||||||
|
spec:
|
||||||
|
type: pubsub.nats
|
||||||
|
version: v1
|
||||||
|
metadata:
|
||||||
|
- name: natsURL
|
||||||
|
value: nats://nats:4222
|
||||||
|
```
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
# Security Guide
|
||||||
|
|
||||||
|
Praktik keamanan untuk infrastruktur `asepharyana-hub`.
|
||||||
|
|
||||||
|
## Ringkasan
|
||||||
|
|
||||||
|
| Area | Status | Prioritas |
|
||||||
|
|------|--------|-----------|
|
||||||
|
| Secrets management | GitHub encrypted secrets | Tinggi |
|
||||||
|
| TLS termination | Traefik + cert volume mounts | Tinggi |
|
||||||
|
| Container security | Non-root user (scraper-api) | Sedang |
|
||||||
|
| Network security | Tailscale overlay, app-shared-net | Sedang |
|
||||||
|
| Access control | SSH key, GitHub permissions | Sedang |
|
||||||
|
| Monitoring | Belum ada alert system | Rendah |
|
||||||
|
| Firewall | UFW/iptables (manual) | Sedang |
|
||||||
|
| Backup | lihat `docs/backup-recovery.md` | Sedang |
|
||||||
|
|
||||||
|
## Secrets Management
|
||||||
|
|
||||||
|
### Yang Tidak Boleh di-Commit
|
||||||
|
|
||||||
|
- [ ] `.env` production (disimpan sebagai GitHub secret `ENV_FILE_PRODUCTION`)
|
||||||
|
- [ ] SSH private keys
|
||||||
|
- [ ] API tokens, JWT secret
|
||||||
|
- [ ] Docker registry tokens
|
||||||
|
- [ ] Database passwords
|
||||||
|
- [ ] TLS certificate private keys
|
||||||
|
|
||||||
|
### GitHub Secrets
|
||||||
|
|
||||||
|
Setting di Settings > Secrets and variables > Actions:
|
||||||
|
|
||||||
|
| Secret | Tujuan | Rotasi |
|
||||||
|
|--------|--------|--------|
|
||||||
|
| `SSH_PRIVATE_KEY` | Akses SSH ke VPS | 6 bulan |
|
||||||
|
| `VPS_HOST` | IP VPS | Tidak berubah |
|
||||||
|
| `VPS_USER` | User SSH | Tidak berubah |
|
||||||
|
| `VPS_TARGET_DIR` | Directory di VPS | Tidak berubah |
|
||||||
|
| `ENV_FILE_PRODUCTION` | Full `.env` production | Saat ada perubahan |
|
||||||
|
|
||||||
|
### Update Secrets dengan aman
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Baca current .env dari VPS via SSH
|
||||||
|
ssh root@45.127.35.244 "cat /root/asepharyana-hub/.env" | gh secret set ENV_FILE_PRODUCTION --repo asepharyana/asepharyana-hub --repos
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production `.env` tidak boleh di-commit
|
||||||
|
|
||||||
|
`.env` di root repo adalah untuk development lokal. Production `.env` hanya ada di:
|
||||||
|
1. GitHub secret `ENV_FILE_PRODUCTION`
|
||||||
|
2. File `/root/asepharyana-hub/.env` di VPS (hasil SCP dari CI/CD)
|
||||||
|
|
||||||
|
## TLS / SSL
|
||||||
|
|
||||||
|
### Konfigurasi
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Traefik TLS certs dari file mount (bukan auto-ACME)
|
||||||
|
volumes:
|
||||||
|
- ${TRAEFIK_CERT_MY_ID_PEM:-/root/asepharyana.my.id.pem}:/etc/traefik/certs/asepharyana.my.id.pem:ro
|
||||||
|
- ${TRAEFIK_CERT_MY_ID_KEY:-/root/asepharyana.my.id.key}:/etc/traefik/certs/asepharyana.my.id.key:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
- Certificates disimpan di host (`/root/`), bukan di repo
|
||||||
|
- Volume mount read-only (`:ro`)
|
||||||
|
- Private key hanya bisa dibaca oleh root (chmod 600)
|
||||||
|
- Renew certificates sebelum expired (monitor expiry)
|
||||||
|
- Dua domain: `asepharyana.my.id` + `asepharyana.web.id`
|
||||||
|
|
||||||
|
## Container Security
|
||||||
|
|
||||||
|
### Non-Root User
|
||||||
|
|
||||||
|
Scraper API berjalan sebagai `appuser` (UID 1001):
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN groupadd -g 1001 appgroup && \
|
||||||
|
useradd -u 1001 -g appgroup -s /bin/sh appuser
|
||||||
|
USER appuser
|
||||||
|
```
|
||||||
|
|
||||||
|
Service baru harus mengikuti pattern yang sama.
|
||||||
|
|
||||||
|
### Read-Only Filesystem
|
||||||
|
|
||||||
|
Untuk container yang tidak perlu write ke filesystem:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: app:latest
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Socket
|
||||||
|
|
||||||
|
Hanya Traefik yang perlu akses ke Docker socket (read-only):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
Service lain tidak boleh mount Docker socket.
|
||||||
|
|
||||||
|
### Image Security
|
||||||
|
|
||||||
|
- Build dari base image resmi dan minimal (`debian:bookworm-slim`, `redis:alpine`, `nats:latest`)
|
||||||
|
- Multi-stage build untuk production image (tidak include build tools)
|
||||||
|
- Update base image secara berkala
|
||||||
|
|
||||||
|
## Network Security
|
||||||
|
|
||||||
|
### Firewall (UFW/iptables)
|
||||||
|
|
||||||
|
Di VPS (`orangevps`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hanya buka port yang diperlukan
|
||||||
|
sudo ufw default deny incoming
|
||||||
|
sudo ufw default allow outgoing
|
||||||
|
sudo ufw allow 22/tcp # SSH
|
||||||
|
sudo ufw allow 80/tcp # HTTP redirect
|
||||||
|
sudo ufw allow 443/tcp # HTTPS
|
||||||
|
sudo ufw allow 4222/tcp # NATS (jika perlu external akses)
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
Di `imrnes`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Hanya dari Tailscale interface
|
||||||
|
sudo ufw allow in on tailscale0 to any port 6432 proto tcp # PostgreSQL
|
||||||
|
sudo ufw allow in on tailscale0 to any port 6379 proto tcp # Redis
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network Segmentation
|
||||||
|
|
||||||
|
- Semua container di network `app-shared-net` (internal bridge)
|
||||||
|
- Tidak ada port yang di-expose ke host kecuali Traefik (80,443)
|
||||||
|
- Redis hanya accessible via Docker DNS (`redis:6379`) — tidak di-expose
|
||||||
|
- Database hanya via Tailscale — tidak accessible dari public internet
|
||||||
|
|
||||||
|
### SSH Hardening
|
||||||
|
|
||||||
|
Konfigurasi di `/etc/ssh/sshd_config`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Port 22
|
||||||
|
PermitRootLogin prohibit-password
|
||||||
|
PasswordAuthentication no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
AllowUsers root
|
||||||
|
MaxAuthTries 3
|
||||||
|
ClientAliveInterval 300
|
||||||
|
ClientAliveCountMax 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## Access Control
|
||||||
|
|
||||||
|
### GitHub Repository
|
||||||
|
|
||||||
|
- `contents: write` hanya untuk workflow `update-manifest` dan `update-submodule`
|
||||||
|
- `packages: write` hanya untuk workflow `build`
|
||||||
|
- `security-events: write` hanya untuk workflow `security`
|
||||||
|
- Branch protection di `main`: require PR review, status checks
|
||||||
|
|
||||||
|
### VPS
|
||||||
|
|
||||||
|
- SSH hanya dengan key-based authentication
|
||||||
|
- Key disimpan di GitHub secret, bukan di repo
|
||||||
|
- Rotate SSH key secara berkala (minimal 6 bulan)
|
||||||
|
- Jangan gunakan password login
|
||||||
|
|
||||||
|
## Monitoring Keamanan
|
||||||
|
|
||||||
|
### Saat Ini
|
||||||
|
|
||||||
|
- Traefik access logs (format JSON, buffer size 100)
|
||||||
|
- Docker logs via `docker logs`
|
||||||
|
- CodeQL analysis untuk Rust code (setiap PR + weekly)
|
||||||
|
|
||||||
|
### Rekomendasi
|
||||||
|
|
||||||
|
- [ ] Alert untuk SSH failed login (fail2ban)
|
||||||
|
- [ ] Log monitoring (Loki / Promtail)
|
||||||
|
- [ ] Container vulnerability scanning (Trivy / Snyk)
|
||||||
|
- [ ] Certificate expiry monitoring
|
||||||
|
- [ ] Disk usage alert
|
||||||
|
- [ ] Unauthorized access detection
|
||||||
|
|
||||||
|
## Checklist Security
|
||||||
|
|
||||||
|
- [ ] SSH password authentication disabled
|
||||||
|
- [ ] Root login via SSH key only
|
||||||
|
- [ ] UFW/iptables configured
|
||||||
|
- [ ] Docker socket only mounted where necessary (read-only)
|
||||||
|
- [ ] Container berjalan sebagai non-root user
|
||||||
|
- [ ] `.env` tidak di-commit
|
||||||
|
- [ ] GitHub secrets ter-encrypt
|
||||||
|
- [ ] TLS certificates valid dan belum expired
|
||||||
|
- [ ] CodeQL analysis berjalan
|
||||||
|
- [ ] Backup database berjalan
|
||||||
|
- [ ] SSH key di-rotate
|
||||||
|
- [ ] Docker image di-scan untuk vulnerability
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# Tailscale Networking
|
||||||
|
|
||||||
|
Dokumentasi setup dan troubleshooting konektivitas Tailscale antara node `orangevps` (VPS) dan `imrnes` (bare-metal).
|
||||||
|
|
||||||
|
## Topologi
|
||||||
|
|
||||||
|
```
|
||||||
|
orangevps (VPS)
|
||||||
|
├─ Tailscale IP: 100.x.x.x (dynamic)
|
||||||
|
├─ Public IP: 45.127.35.244
|
||||||
|
├─ Docker containers (app-shared-net)
|
||||||
|
│ └─ perlu akses ke imrnes via Tailscale
|
||||||
|
└─ tailscale-routes.service
|
||||||
|
└─ menambahkan route 100.x.x.x ke tabel routing main
|
||||||
|
|
||||||
|
imrnes (Bare-metal)
|
||||||
|
├─ Tailscale IP: 100.121.180.82
|
||||||
|
├─ Layanan:
|
||||||
|
│ ├─ PostgreSQL (port 6432)
|
||||||
|
│ └─ Redis (port 6379)
|
||||||
|
└─ Layanan hanya listen di Tailscale interface
|
||||||
|
```
|
||||||
|
|
||||||
|
## Masalah: Container Tidak Bisa Mencapai Tailscale IP
|
||||||
|
|
||||||
|
Docker container secara default hanya bisa mencapai IP di Docker bridge network dan network host. Tailscale menggunakan interface virtual `tailscale0` yang tidak secara otomatis di-route ke container.
|
||||||
|
|
||||||
|
### Solusi: `tailscale-routes.service`
|
||||||
|
|
||||||
|
Service systemd yang menambahkan route Tailscale ke tabel routing `main` agar traffic dari container bisa melewati host ke Tailscale.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/systemd/system/tailscale-routes.service
|
||||||
|
[Unit]
|
||||||
|
Description=Add Tailscale routes to main routing table
|
||||||
|
After=tailscaled.service
|
||||||
|
Requires=tailscaled.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
ExecStart=/bin/sh -c 'ip rule add from all lookup main priority 10000 2>/dev/null; ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || true'
|
||||||
|
ExecStop=/bin/sh -c 'ip rule del from all lookup main priority 10000 2>/dev/null; ip route del 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || true'
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verifikasi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah route sudah ada
|
||||||
|
ip route show table main | grep tailscale
|
||||||
|
|
||||||
|
# Test dari dalam container
|
||||||
|
docker run --rm alpine ping -c 3 100.121.180.82
|
||||||
|
|
||||||
|
# Test koneksi PostgreSQL dari container
|
||||||
|
docker run --rm alpine sh -c "apk add postgresql-client && psql -h 100.121.180.82 -p 6432 -U asephs -d hub -c 'SELECT 1'"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup Tailscale di Node Baru
|
||||||
|
|
||||||
|
### 1. Install Tailscale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://tailscale.com/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Authenticate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tailscale up --advertise-routes=<LAN_SUBNET_CIDR>
|
||||||
|
```
|
||||||
|
|
||||||
|
Untuk node yang hanya sebagai client (tidak advertise routes):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tailscale up
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Enable dan Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable --now tailscaled
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Setup Route Service (khusus node dengan Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Buat service file
|
||||||
|
sudo nano /etc/systemd/system/tailscale-routes.service
|
||||||
|
# Paste content di atas
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now tailscale-routes.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Konfigurasi ACL di Tailscale Admin
|
||||||
|
|
||||||
|
Pastikan ACL di [Tailscale Admin Console](https://login.tailscale.com/admin/acls) mengizinkan traffic antar node:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Atau jika ingin lebih ketat:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"acls": [
|
||||||
|
{"action": "accept", "src": ["tag:server"], "dst": ["tag:server:*"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfigurasi iptables/ufw
|
||||||
|
|
||||||
|
Pastikan port yang diperlukan terbuka di `imrnes`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PostgreSQL
|
||||||
|
sudo ufw allow in on tailscale0 to any port 6432 proto tcp
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
sudo ufw allow in on tailscale0 to any port 6379 proto tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
Atau menggunakan iptables langsung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo iptables -A INPUT -i tailscale0 -p tcp --dport 6432 -j ACCEPT
|
||||||
|
sudo iptables -A INPUT -i tailscale0 -p tcp --dport 6379 -j ACCEPT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container timeout connect ke Tailscale IP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Cek apakah route service berjalan
|
||||||
|
systemctl status tailscale-routes.service
|
||||||
|
|
||||||
|
# 2. Cek route di host
|
||||||
|
ip route show table main | grep 100.64
|
||||||
|
|
||||||
|
# 3. Cek apakah host bisa ping ke target
|
||||||
|
ping 100.121.180.82
|
||||||
|
|
||||||
|
# 4. Test dari container dengan --network host
|
||||||
|
docker run --rm --network host alpine ping -c 3 100.121.180.82
|
||||||
|
|
||||||
|
# 5. Pastikan tidak ada firewall blocking
|
||||||
|
iptables -L FORWARD -n -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tailscale disconnect
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek status
|
||||||
|
tailscale status
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
sudo systemctl restart tailscaled
|
||||||
|
```
|
||||||
|
|
||||||
|
### IP Tailscale berubah
|
||||||
|
|
||||||
|
Tailscale IP bisa berubah jika node dire-auth. Update:
|
||||||
|
|
||||||
|
1. `.env` production di VPS (via GitHub secret `ENV_FILE_PRODUCTION`)
|
||||||
|
2. Database connection strings
|
||||||
|
3. Redis connection strings
|
||||||
|
4. Trigger redeploy
|
||||||
|
|
||||||
|
### MagicDNS tidak resolve
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek DNS
|
||||||
|
tailscale dns status
|
||||||
|
|
||||||
|
# Flush DNS cache
|
||||||
|
sudo resolvectl flush-caches
|
||||||
|
```
|
||||||
|
|
||||||
|
## Catatan Keamanan
|
||||||
|
|
||||||
|
- Interface Tailscale (`tailscale0`) hanya boleh diakses oleh node yang terautentikasi dalam network yang sama
|
||||||
|
- Jangan expose port database ke public interface (`eth0`), hanya ke Tailscale
|
||||||
|
- Gunakan ACL untuk membatasi akses antar node jika diperlukan
|
||||||
|
- Rotate auth key secara berkala di Tailscale admin console
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
Kumpulan solusi untuk masalah umum yang spesifik di infrastruktur `asepharyana-hub`.
|
||||||
|
|
||||||
|
## Daftar Isi
|
||||||
|
|
||||||
|
- [Deployment](#deployment)
|
||||||
|
- [Dapr](#dapr)
|
||||||
|
- [NATS](#nats)
|
||||||
|
- [Traefik](#traefik)
|
||||||
|
- [Tailscale / Networking](#tailscale--networking)
|
||||||
|
- [Docker / Container](#docker--container)
|
||||||
|
- [Database](#database)
|
||||||
|
- [Submodule](#submodule)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Workflow deploy gagal: "Secrets not fully configured"
|
||||||
|
|
||||||
|
**Penyebab:** Salah satu GitHub secrets tidak diset.
|
||||||
|
|
||||||
|
**Solusi:** Cek secrets di Settings > Secrets and variables > Actions:
|
||||||
|
|
||||||
|
| Secret | Status |
|
||||||
|
|--------|--------|
|
||||||
|
| `SSH_PRIVATE_KEY` | Wajib |
|
||||||
|
| `VPS_HOST` | Wajib (`45.127.35.244`) |
|
||||||
|
| `VPS_USER` | Wajib (`root`) |
|
||||||
|
| `VPS_TARGET_DIR` | Wajib (`/root/asepharyana-hub`) |
|
||||||
|
| `ENV_FILE_PRODUCTION` | Wajib |
|
||||||
|
|
||||||
|
### Workflow build gagal: "Submodule commit not fetchable"
|
||||||
|
|
||||||
|
**Penyebab:** Commit SHA dari `repository_dispatch` belum tersedia di remote submodule repo (eventual consistency).
|
||||||
|
|
||||||
|
**Solusi:** Workflow akan retry hingga 5 menit. Jika masih gagal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah commit ada di remote
|
||||||
|
git ls-remote https://github.com/asepharyana/asepharyana-hub-scraper.git <SHA>
|
||||||
|
|
||||||
|
# Trigger ulang dispatch dari submodule repo, atau push langsung ke hub
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push manifest gagal: conflict di main
|
||||||
|
|
||||||
|
**Penyebab:** Ada commit lain yang masuk sebelum workflow selesai.
|
||||||
|
|
||||||
|
**Solusi:** Workflow otomatis retry rebase 3 kali. Jika semua gagal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Manual fix di lokal
|
||||||
|
git pull --rebase origin main
|
||||||
|
# resolve conflict
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dapr
|
||||||
|
|
||||||
|
### Dapr sidecar tidak connect ke placement
|
||||||
|
|
||||||
|
**Gejala:** Container `scraper-api-dapr` restart loop. Log: `failed to connect to placement`
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek log sidecar
|
||||||
|
docker logs scraper-api-dapr --tail 50
|
||||||
|
|
||||||
|
# Cek apakah placement service running
|
||||||
|
docker ps -a | grep dapr-placement
|
||||||
|
docker logs dapr-placement --tail 20
|
||||||
|
|
||||||
|
# Cek konektivitas
|
||||||
|
docker exec scraper-api-dapr curl -s http://dapr-placement:50005
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart placement dulu, lalu sidecar
|
||||||
|
docker compose -f infra/compose/dapr.yml up -d --force-recreate
|
||||||
|
sleep 5
|
||||||
|
docker compose -f infra/compose/scraper.yml up -d --force-recreate scraper-api-dapr
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dapr pub/sub tidak bekerja
|
||||||
|
|
||||||
|
**Gejala:** Event di-publish tapi tidak sampai ke subscriber.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek komponen Dapr
|
||||||
|
curl http://localhost:3500/v1.0/components
|
||||||
|
|
||||||
|
# Cek health sidecar
|
||||||
|
curl http://localhost:3500/v1.0/healthz
|
||||||
|
|
||||||
|
# Cek Redis (backend pub/sub)
|
||||||
|
docker exec redis redis-cli ping
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart sidecar
|
||||||
|
docker restart scraper-api-dapr
|
||||||
|
|
||||||
|
# Jika Redis bermasalah, restart juga
|
||||||
|
docker restart redis
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dapr state store error: "key not found"
|
||||||
|
|
||||||
|
**Penyebab:** Key belum ada di state store, atau prefix berbeda.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek state langsung di Redis
|
||||||
|
docker exec redis redis-cli KEYS 'dapr*'
|
||||||
|
|
||||||
|
# State store menggunakan prefix "dapr"
|
||||||
|
# Format key: dapr || <app-id> || <key>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NATS
|
||||||
|
|
||||||
|
### NATS tidak bisa start
|
||||||
|
|
||||||
|
**Gejala:** Container NATS restart loop.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs nats --tail 50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:** Kemungkinan korupsi data JetStream:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup dulu volume data
|
||||||
|
docker run --rm -v nats_data:/data -v /tmp:/backup alpine cp -r /data /backup/nats_data_backup
|
||||||
|
|
||||||
|
# Hapus volume dan recreate
|
||||||
|
docker compose -f infra/compose/nats.yml down
|
||||||
|
docker volume rm asepharyana-hub_nats_data
|
||||||
|
docker compose -f infra/compose/nats.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### JetStream stream overflow
|
||||||
|
|
||||||
|
**Gejala:** Disk penuh, NATS lambat.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek ukuran volume
|
||||||
|
docker system df | grep nats_data
|
||||||
|
du -sh /var/lib/docker/volumes/nats_data/_data/
|
||||||
|
|
||||||
|
# Cek stream info
|
||||||
|
nats stream list
|
||||||
|
nats stream info <stream-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Purge stream tertentu (data hilang)
|
||||||
|
nats stream purge <stream-name>
|
||||||
|
|
||||||
|
# Atau tambah limit stream via NATS config
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Slow Consumer" warning
|
||||||
|
|
||||||
|
**Gejala:** Log NATS menampilkan "slow consumer".
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8222/varz | jq '.slow_consumers'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
- Scale consumer (tambah worker)
|
||||||
|
- Percepat processing message
|
||||||
|
- Kurangi ukuran payload
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Traefik
|
||||||
|
|
||||||
|
### Traefik tidak routing ke service
|
||||||
|
|
||||||
|
**Gejala:** 404 atau 503 dari Traefik.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah service container running
|
||||||
|
docker ps -a | grep scraper-api
|
||||||
|
|
||||||
|
# Cek log Traefik
|
||||||
|
docker logs traefik --tail 50
|
||||||
|
|
||||||
|
# Cek apakah container ada di network yang benar
|
||||||
|
docker network inspect app-shared-net | grep scraper-api
|
||||||
|
|
||||||
|
# Test routing langsung
|
||||||
|
curl -H "Host: scraper.asepharyana.my.id" http://localhost/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pastikan service terdaftar di apps.yaml
|
||||||
|
# Pastikan container join app-shared-net
|
||||||
|
# Restart Traefik
|
||||||
|
docker compose -f infra/compose/traefik.yml up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLS certificate error
|
||||||
|
|
||||||
|
**Gejala:** Browser menampilkan warning certificate.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek sertifikat di host
|
||||||
|
ls -la /root/asepharyana.my.id.pem
|
||||||
|
openssl x509 -in /root/asepharyana.my.id.pem -text -noout | head -20
|
||||||
|
|
||||||
|
# Cek apakah Traefik bisa mount
|
||||||
|
docker exec traefik ls -la /etc/traefik/certs/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
- Update sertifikat di host
|
||||||
|
- Restart Traefik
|
||||||
|
- Jika path berbeda, set environment variable `TRAEFIK_CERT_*`
|
||||||
|
|
||||||
|
### Rate limit terlalu ketat
|
||||||
|
|
||||||
|
**Gejala:** Request legitimate di-block.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek rate-limit config di middlewares.yaml
|
||||||
|
# Current: average 100, burst 50
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:** Ubah nilai `average` dan `burst` di `infra/traefik/dynamic/middlewares.yaml`, lalu reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker kill --signal HUP traefik
|
||||||
|
# atau
|
||||||
|
docker exec traefik kill -HUP 1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tailscale / Networking
|
||||||
|
|
||||||
|
### Container tidak bisa connect ke Tailscale IP
|
||||||
|
|
||||||
|
**Gejala:** Timeout saat container connect ke `100.121.180.82:6432`.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek route service dari host
|
||||||
|
systemctl status tailscale-routes.service
|
||||||
|
|
||||||
|
# Cek route di host
|
||||||
|
ip route show table main | grep 100.64
|
||||||
|
|
||||||
|
# Cek koneksi dari host
|
||||||
|
ping 100.121.180.82
|
||||||
|
|
||||||
|
# Test dari container (dengan --network host)
|
||||||
|
docker run --rm --network host alpine ping -c 3 100.121.180.82
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart route service
|
||||||
|
sudo systemctl restart tailscale-routes.service
|
||||||
|
|
||||||
|
# Atau tambah route manual
|
||||||
|
sudo ip rule add from all lookup main priority 10000
|
||||||
|
sudo ip route add 100.64.0.0/10 dev tailscale0 table main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database connection refused
|
||||||
|
|
||||||
|
**Gejala:** Service tidak bisa konek ke PostgreSQL.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah DB listen di Tailscale (dari imrnes)
|
||||||
|
ss -tlnp | grep 6432
|
||||||
|
|
||||||
|
# Cek dari orangevps
|
||||||
|
nc -zv 100.121.180.82 6432
|
||||||
|
|
||||||
|
# Cek firewall di imrnes
|
||||||
|
sudo ufw status
|
||||||
|
sudo iptables -L -n | grep 6432
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Di imrnes: pastikan PostgreSQL bind ke Tailscale interface
|
||||||
|
# Di postgresql.conf:
|
||||||
|
listen_addresses = 'localhost,100.121.180.82'
|
||||||
|
|
||||||
|
# Di pg_hba.conf:
|
||||||
|
host hub asephs 100.0.0.0/8 md5
|
||||||
|
|
||||||
|
# Restart PostgreSQL
|
||||||
|
sudo systemctl restart postgresql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Redis connection refused dari container
|
||||||
|
|
||||||
|
**Gejala:** Service tidak bisa connect ke `redis://redis:6379`.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek apakah container Redis running
|
||||||
|
docker ps -a | grep redis
|
||||||
|
|
||||||
|
# Cek apakah container target join network yang sama
|
||||||
|
docker inspect <container> | grep -A5 Networks
|
||||||
|
|
||||||
|
# Cek DNS resolve dari container
|
||||||
|
docker exec <container> getent hosts redis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pastikan Redis ada di network app-shared-net
|
||||||
|
docker network inspect app-shared-net | grep redis
|
||||||
|
|
||||||
|
# Jika tidak, attach
|
||||||
|
docker network connect app-shared-net redis
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker / Container
|
||||||
|
|
||||||
|
### Container restart loop
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs <container> --tail 50
|
||||||
|
docker inspect <container> | jq '.[].State'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Penyebab umum:**
|
||||||
|
- Health check gagal
|
||||||
|
- Dependency service belum siap
|
||||||
|
- Environment variable tidak diset
|
||||||
|
|
||||||
|
### Image pull gagal dari GHCR
|
||||||
|
|
||||||
|
**Gejala:** `docker pull` gagal di VPS.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek login
|
||||||
|
cat ~/.docker/config.json | grep ghcr
|
||||||
|
|
||||||
|
# Cek visibility package
|
||||||
|
# Buka https://github.com/orgs/asepharyana/packages
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Re-login
|
||||||
|
echo $GITHUB_TOKEN | docker login ghcr.io -u asepharyana --password-stdin
|
||||||
|
|
||||||
|
# Pastikan package visibility public atau di-share ke org
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disk penuh
|
||||||
|
|
||||||
|
**Gejala:** Container crash, write error.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
df -h
|
||||||
|
docker system df
|
||||||
|
du -sh /var/lib/docker/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Bersihkan container/image/volume yang tidak dipakai
|
||||||
|
docker system prune -a -f
|
||||||
|
|
||||||
|
# Hapus image lama
|
||||||
|
docker image prune -a -f
|
||||||
|
|
||||||
|
# Lihat volume terbesar
|
||||||
|
docker system df -v | grep -E "(nats_data|redis_data)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
### Koneksi PostgreSQL lambat
|
||||||
|
|
||||||
|
**Gejala:** Query time high, connection timeout.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dari container, test latency
|
||||||
|
docker exec scraper-api ping -c 5 100.121.180.82
|
||||||
|
|
||||||
|
# Cek koneksi aktif
|
||||||
|
docker exec scraper-api psql $DATABASE_URL -c "SELECT count(*) FROM pg_stat_activity;"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
- Cek Tailscale latency
|
||||||
|
- Adjust connection pool size
|
||||||
|
- Cek resource PostgreSQL di `imrnes`
|
||||||
|
|
||||||
|
### Migration gagal
|
||||||
|
|
||||||
|
**Gejala:** Service error setelah image update.
|
||||||
|
|
||||||
|
**Diagnosis:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cek log service
|
||||||
|
docker logs scraper-api --tail 100 | grep -i migration
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
- Migration ada di submodule `apps/scraper`, bukan di hub
|
||||||
|
- Pastikan schema sesuai dengan versi code
|
||||||
|
- Rollback image jika migration tidak backward-compatible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Submodule
|
||||||
|
|
||||||
|
### HEAD detached di submodule
|
||||||
|
|
||||||
|
**Gejala:** `git status` di `apps/scraper` menunjukkan "HEAD detached".
|
||||||
|
|
||||||
|
**Penyebab:** Normal. Submodule selalu dalam keadaan detached HEAD karena mengacu pada commit spesifik.
|
||||||
|
|
||||||
|
**Solusi:** Jangan commit perubahan dari dalam submodule. Selalu bekerja di repo asli.
|
||||||
|
|
||||||
|
### Submodule tidak ter-update setelah pull
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
### Konflik submodule saat rebase/merge
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Resolve dengan memilih versi yang benar
|
||||||
|
git add apps/scraper
|
||||||
|
git rebase --continue
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user