fix(devops): resolve 4 P2 DevOps improvements (Wave 3 — TEC-263)

- DEVOPS-W-01: Add oliver006/redis_exporter to docker-compose.yml so
  the existing prometheus.yml scrape job (redis-exporter:9121) resolves
- DEVOPS-W-04: Add redis-sentinel.yaml with Redis Sentinel HA setup
  (1 master StatefulSet + 2 replica StatefulSet + 3 sentinel pods)
  replacing the single-instance SPOF redis.yaml in staging K8s
- DEVOPS-W-05: Add network-policy.yaml with default-deny-all NetworkPolicy
  + explicit allow rules for inter-service, Traefik ingress, Redis access,
  Prometheus scrape, and external egress (Neon PostgreSQL, AMQP)
- DEVOPS-M-01: Add aquasecurity/trivy-action to docker-build.yml to scan
  every built image for CRITICAL/HIGH CVEs; results uploaded to GitHub
  Security tab via SARIF

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-03-23 09:54:32 +07:00
parent dd57cff6b1
commit 7b92332710
4 changed files with 832 additions and 0 deletions

View File

@@ -112,3 +112,22 @@ jobs:
tags: ${{ steps.tags.outputs.tags }}
cache-from: type=registry,ref=${{ matrix.image }}:buildcache
cache-to: type=registry,ref=${{ matrix.image }}:buildcache,mode=max
# EN: Scan image for vulnerabilities with Trivy (DEVOPS-M-01)
# VI: Quet lo hong bao mat image bang Trivy (DEVOPS-M-01)
- name: Scan ${{ matrix.service }} image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ matrix.image }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results-${{ matrix.service }}.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
ignore-unfixed: true
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results-${{ matrix.service }}.sarif'
category: 'trivy-${{ matrix.service }}'

View File

@@ -68,6 +68,22 @@ services:
timeout: 5s
retries: 5
# Redis Exporter - Prometheus metrics for Redis (DEVOPS-W-01)
redis-exporter:
image: oliver006/redis_exporter:latest
container_name: redis-exporter-local
environment:
REDIS_ADDR: "redis://redis:6379"
REDIS_PASSWORD: "goodgo-redis-local"
ports:
- "9121:9121"
networks:
- microservices-network
depends_on:
redis:
condition: service_healthy
restart: unless-stopped
# MinIO - Object Storage (S3-compatible)
minio:
image: minio/minio:latest

View File

@@ -0,0 +1,370 @@
# EN: Kubernetes NetworkPolicy — default-deny + explicit allow rules (DEVOPS-W-05)
# VI: Kubernetes NetworkPolicy — tu choi mac dinh + cho phep tuong minh (DEVOPS-W-05)
#
# Strategy:
# 1. Default-deny all ingress and egress in staging namespace
# 2. Allow DNS resolution (kube-dns)
# 3. Allow inter-service communication via explicit rules
# 4. Allow ingress from Traefik (API gateway)
# 5. Allow Prometheus scraping metrics from all services
# 6. Allow Redis access only from application services
# =============================================================================
# Default Deny — block all ingress + egress by default
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# =============================================================================
# Allow DNS (kube-dns) — required for all pods to resolve service names
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
---
# =============================================================================
# Allow Traefik ingress → microservices (port 8080)
# EN: Traefik runs in kube-system namespace (or traefik namespace).
# VI: Traefik chay trong namespace kube-system (hoac traefik namespace).
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-traefik-ingress
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- iam-service
- merchant-service
- order-service
- fnb-engine
- inventory-service
- wallet-service
- catalog-service
- storage-service
- booking-service
- pos-web
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
ports:
- port: 8080
protocol: TCP
---
# =============================================================================
# Allow microservices to egress to each other (port 8080)
# EN: Services communicate internally via REST (HTTP/8080).
# VI: Cac service giao tiep noi bo qua REST (HTTP/8080).
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-inter-service-egress
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- iam-service
- merchant-service
- order-service
- fnb-engine
- inventory-service
- wallet-service
- catalog-service
- storage-service
- booking-service
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchExpressions:
- key: app
operator: In
values:
- iam-service
- merchant-service
- order-service
- fnb-engine
- inventory-service
- wallet-service
- catalog-service
- storage-service
- booking-service
ports:
- port: 8080
protocol: TCP
---
# =============================================================================
# Allow Redis access — only from app services (port 6379)
# EN: Redis is accessed only by application pods (not pos-web frontend).
# VI: Redis chi duoc truy cap boi cac pod ung dung (khong phai pos-web frontend).
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-redis-ingress
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- redis-master
- redis-replica
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchExpressions:
- key: app
operator: In
values:
- iam-service
- merchant-service
- order-service
- fnb-engine
- inventory-service
- wallet-service
- catalog-service
- storage-service
- booking-service
ports:
- port: 6379
protocol: TCP
---
# Allow Redis master → replica replication egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-redis-replication
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- redis-master
- redis-replica
- redis-sentinel
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchExpressions:
- key: app
operator: In
values:
- redis-master
- redis-replica
- redis-sentinel
ports:
- port: 6379
protocol: TCP
- port: 26379
protocol: TCP
egress:
- to:
- podSelector:
matchExpressions:
- key: app
operator: In
values:
- redis-master
- redis-replica
- redis-sentinel
ports:
- port: 6379
protocol: TCP
- port: 26379
protocol: TCP
---
# =============================================================================
# Allow microservices to reach Redis (egress)
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-redis-egress
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- iam-service
- merchant-service
- order-service
- fnb-engine
- inventory-service
- wallet-service
- catalog-service
- storage-service
- booking-service
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchExpressions:
- key: app
operator: In
values:
- redis-master
- redis-replica
- redis-sentinel
ports:
- port: 6379
protocol: TCP
- port: 26379
protocol: TCP
---
# =============================================================================
# Allow Prometheus scrape — port 8080 metrics from all app pods
# EN: Prometheus runs in monitoring namespace and scrapes /metrics.
# VI: Prometheus chay trong namespace monitoring va thu thap /metrics.
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- iam-service
- merchant-service
- order-service
- fnb-engine
- inventory-service
- wallet-service
- catalog-service
- storage-service
- booking-service
- pos-web
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- port: 8080
protocol: TCP
---
# =============================================================================
# Allow microservices to reach external services (Neon PostgreSQL, etc.)
# EN: Egress to external HTTPS/PostgreSQL (Neon cloud DB, SMTP, etc.)
# VI: Egress ra ngoai HTTPS/PostgreSQL (Neon cloud DB, SMTP, v.v.)
# =============================================================================
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-egress
namespace: staging
labels:
environment: staging
platform: goodgo
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- iam-service
- merchant-service
- order-service
- fnb-engine
- inventory-service
- wallet-service
- catalog-service
- storage-service
- booking-service
policyTypes:
- Egress
egress:
- ports:
- port: 443
protocol: TCP
- port: 5432
protocol: TCP
- port: 5671
protocol: TCP
- port: 5672
protocol: TCP

View File

@@ -0,0 +1,427 @@
# EN: Redis HA with Sentinel — replaces single-instance redis.yaml (DEVOPS-W-04)
# VI: Redis HA voi Sentinel — thay the redis.yaml don le (DEVOPS-W-04)
#
# Architecture:
# - redis-master: 1 pod StatefulSet (read/write)
# - redis-replica: 2 pod StatefulSet (read-only replicas)
# - redis-sentinel: 3 pod StatefulSet (monitors master, auto-failover)
#
# EN: Apply this file AND remove (or stop applying) redis.yaml to avoid conflicts.
# VI: Apply file nay VA xoa (hoac ngung apply) redis.yaml de tranh conflict.
# =============================================================================
# ConfigMap — Sentinel configuration template
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-sentinel-config
namespace: staging
labels:
app: redis-sentinel
environment: staging
platform: goodgo
data:
sentinel.conf: |
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel monitor goodgo-master redis-master-0.redis-master.staging.svc.cluster.local 6379 2
sentinel auth-pass goodgo-master $(REDIS_PASSWORD)
sentinel down-after-milliseconds goodgo-master 5000
sentinel failover-timeout goodgo-master 60000
sentinel parallel-syncs goodgo-master 1
replica.conf: |
replicaof redis-master-0.redis-master.staging.svc.cluster.local 6379
masterauth $(REDIS_PASSWORD)
requirepass $(REDIS_PASSWORD)
maxmemory 256mb
maxmemory-policy allkeys-lru
appendonly yes
---
# =============================================================================
# StatefulSet — Redis Master (1 replica)
# =============================================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-master
namespace: staging
labels:
app: redis-master
role: master
environment: staging
platform: goodgo
tier: infrastructure
spec:
serviceName: redis-master
replicas: 1
selector:
matchLabels:
app: redis-master
role: master
template:
metadata:
labels:
app: redis-master
role: master
environment: staging
spec:
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- "--requirepass"
- "$(REDIS_PASSWORD)"
- "--maxmemory"
- "256mb"
- "--maxmemory-policy"
- "allkeys-lru"
- "--appendonly"
- "yes"
ports:
- containerPort: 6379
protocol: TCP
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: Redis__Password
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
livenessProbe:
exec:
command:
- sh
- -c
- redis-cli -a "$REDIS_PASSWORD" ping
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- sh
- -c
- redis-cli -a "$REDIS_PASSWORD" ping
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: redis-master-data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: redis-master-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
---
# Service — Redis Master (headless for StatefulSet DNS)
apiVersion: v1
kind: Service
metadata:
name: redis-master
namespace: staging
labels:
app: redis-master
role: master
environment: staging
spec:
clusterIP: None
selector:
app: redis-master
role: master
ports:
- name: redis
port: 6379
targetPort: 6379
---
# Service — Redis write endpoint (used by apps)
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: staging
labels:
app: redis-master
role: master
environment: staging
spec:
selector:
app: redis-master
role: master
ports:
- name: redis
port: 6379
targetPort: 6379
type: ClusterIP
---
# =============================================================================
# StatefulSet — Redis Replicas (2 replicas for HA)
# =============================================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-replica
namespace: staging
labels:
app: redis-replica
role: replica
environment: staging
platform: goodgo
tier: infrastructure
spec:
serviceName: redis-replica
replicas: 2
selector:
matchLabels:
app: redis-replica
role: replica
template:
metadata:
labels:
app: redis-replica
role: replica
environment: staging
spec:
initContainers:
- name: init-replica-conf
image: busybox:1.36
command:
- sh
- -c
- |
sed "s/\$(REDIS_PASSWORD)/$REDIS_PASSWORD/g" /config/replica.conf > /data/redis.conf
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: Redis__Password
volumeMounts:
- name: redis-config
mountPath: /config
- name: redis-replica-data
mountPath: /data
containers:
- name: redis
image: redis:7-alpine
command:
- redis-server
- /data/redis.conf
ports:
- containerPort: 6379
protocol: TCP
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: Redis__Password
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
livenessProbe:
exec:
command:
- sh
- -c
- redis-cli -a "$REDIS_PASSWORD" ping
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- sh
- -c
- redis-cli -a "$REDIS_PASSWORD" ping
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: redis-replica-data
mountPath: /data
volumes:
- name: redis-config
configMap:
name: redis-sentinel-config
volumeClaimTemplates:
- metadata:
name: redis-replica-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
---
# Service — Redis Replicas (headless)
apiVersion: v1
kind: Service
metadata:
name: redis-replica
namespace: staging
labels:
app: redis-replica
role: replica
environment: staging
spec:
clusterIP: None
selector:
app: redis-replica
role: replica
ports:
- name: redis
port: 6379
targetPort: 6379
---
# =============================================================================
# StatefulSet — Redis Sentinel (3 nodes — quorum = 2)
# =============================================================================
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-sentinel
namespace: staging
labels:
app: redis-sentinel
role: sentinel
environment: staging
platform: goodgo
tier: infrastructure
spec:
serviceName: redis-sentinel
replicas: 3
selector:
matchLabels:
app: redis-sentinel
role: sentinel
template:
metadata:
labels:
app: redis-sentinel
role: sentinel
environment: staging
spec:
initContainers:
- name: init-sentinel-conf
image: busybox:1.36
command:
- sh
- -c
- |
sed "s/\$(REDIS_PASSWORD)/$REDIS_PASSWORD/g" /config/sentinel.conf > /data/sentinel.conf
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: Redis__Password
volumeMounts:
- name: sentinel-config
mountPath: /config
- name: sentinel-data
mountPath: /data
containers:
- name: sentinel
image: redis:7-alpine
command:
- redis-sentinel
- /data/sentinel.conf
ports:
- containerPort: 26379
protocol: TCP
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: goodgo-secrets
key: Redis__Password
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
livenessProbe:
exec:
command:
- redis-cli
- -p
- "26379"
- ping
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command:
- redis-cli
- -p
- "26379"
- ping
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: sentinel-data
mountPath: /data
volumes:
- name: sentinel-config
configMap:
name: redis-sentinel-config
volumeClaimTemplates:
- metadata:
name: sentinel-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Mi
---
# Service — Redis Sentinel (headless + ClusterIP)
apiVersion: v1
kind: Service
metadata:
name: redis-sentinel
namespace: staging
labels:
app: redis-sentinel
role: sentinel
environment: staging
spec:
selector:
app: redis-sentinel
role: sentinel
ports:
- name: sentinel
port: 26379
targetPort: 26379
type: ClusterIP