Guidelines and rules for all developers working with goapps-infra.
- Golden Rules
- Naming Conventions
- Directory Structure Rules
- Kustomize Patterns
- Adding New Service
- Database Management
- Monitoring Configuration
- Backup Management
- Security Policies
- Git Workflow
- Emergency Procedures
- Checklists
⚠️ Rules that MUST NOT be violated!
# ❌ WRONG - Never do this!
git add secrets/production-credentials.yaml
# ✅ CORRECT - Create secrets manually in cluster
kubectl create secret generic postgres-secret -n database \
--from-literal=POSTGRES_PASSWORD='<password>'# ✅ Correct order:
1. Deploy to staging
2. Verify in staging (minimum 24 hours)
3. Deploy to production (with approval)- Update README for architecture changes
- Create/update runbooks for new procedures
- Comment on PRs for non-obvious changes
# ❌ WRONG - Don't duplicate manifests
services/new-service/staging/deployment.yaml
services/new-service/production/deployment.yaml
# ✅ CORRECT - Use base + overlays + patches
services/new-service/
├── base/
│ └── deployment.yaml
└── overlays/
├── staging/
│ └── patches/replicas.yaml
└── production/
└── patches/replicas.yamlConsistent naming is essential for:
- Debugging and troubleshooting
- ArgoCD application matching
- Monitoring and alerting
- Documentation
| Resource | Pattern | Example |
|---|---|---|
| Namespace | <purpose> or <app>-<env> |
database, monitoring, goapps-staging |
| Deployment | <service-name> |
finance-service, frontend |
| StatefulSet | <app-name> |
postgres, rabbitmq |
| Service | <deployment-name> |
finance-service, postgres |
| ConfigMap | <app>-config |
postgres-config, grafana-config |
| Secret | <app>-secret |
postgres-secret, minio-secret |
| HPA | <deployment>-hpa |
finance-service-hpa |
| VPA | <deployment>-vpa |
postgres-vpa |
| PVC | <app>-data |
postgres-data, grafana-data |
| CronJob | <purpose>-<schedule> |
postgres-backup-morning |
| Ingress | <app>-ingress |
grafana-ingress, argocd-ingress |
| ServiceMonitor | <service>-monitor |
finance-service-monitor |
| Pattern | Example |
|---|---|
<service>-<env> |
finance-service-staging, frontend-production |
infra-<component> |
infra-database, infra-monitoring, infra-backup |
All resources MUST have these labels:
labels:
app: <service-name> # Required
app.kubernetes.io/name: <service-name>
app.kubernetes.io/part-of: goapps
app.kubernetes.io/component: <type> # backend, frontend, database
app.kubernetes.io/version: <version> # Optionalannotations:
# Prometheus scraping
prometheus.io/scrape: "true"
prometheus.io/port: "8090"
prometheus.io/path: "/metrics"
# Description
description: "Brief description of this resource"| Pattern | Purpose | Example |
|---|---|---|
main |
Production-ready configs | - |
develop |
Development integration | - |
infra/<description> |
Infrastructure changes | infra/add-redis-cluster |
feat/<service> |
New service setup | feat/iam-service |
fix/<issue> |
Bug fixes | fix/backup-cronjob |
hotfix/<issue> |
Urgent production fix | hotfix/postgres-oom |
Format: <type>(<scope>): <description>
Types:
feat: New featurefix: Bug fixdocs: Documentationchore: Maintenancerefactor: Refactoring without new featuresperf: Performance improvement
Examples:
feat(finance-service): add staging deployment
fix(backup): correct minio endpoint configuration
docs(readme): update architecture diagram
chore(deps): upgrade prometheus stack to 67.0.0
base/<component>/
├── kustomization.yaml # Required - lists all resources
├── deployment.yaml # or statefulset.yaml
├── service.yaml
├── configmap.yaml # Optional
└── pvc.yaml # Optional if storage needed
overlays/<environment>/
├── kustomization.yaml # Required - references base + patches
├── patches/ # Strategic merge patches
│ └── replicas.yaml
└── <env>-specific.yaml # Environment-specific resources
services/<service-name>/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── hpa.yaml
│ └── ingress.yaml # Optional
└── overlays/
├── staging/
│ ├── kustomization.yaml
│ └── patches/
│ ├── replicas.yaml
│ ├── resources.yaml
│ └── image.yaml
└── production/
├── kustomization.yaml
└── patches/
- Don't put resources in root directory - Everything must be in appropriate subdirectories
- Every directory must have kustomization.yaml - For Kustomize build
- Base must not reference environment-specific values - Use overlays
- Patches must be minimal - Only override what's necessary
# base/database/postgres/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: database
resources:
- statefulset.yaml
- service.yaml
- configmap.yaml
commonLabels:
app.kubernetes.io/name: postgres
app.kubernetes.io/part-of: goapps# services/finance-service/overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: goapps-staging
resources:
- ../../base
patches:
- path: patches/replicas.yaml
- path: patches/resources.yaml
images:
- name: ghcr.io/mutugading/finance-service
newTag: staging-latest# patches/replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: finance-service
spec:
replicas: 2# patches/env-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: finance-service
spec:
template:
spec:
containers:
- name: finance-service
env:
- name: LOG_LEVEL
value: "debug"SERVICE_NAME="my-service"
mkdir -p services/${SERVICE_NAME}/{base,overlays/{staging,production}/patches}apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
labels:
app: my-service
spec:
replicas: 1
selector:
matchLabels:
app: my-service
template:
metadata:
labels:
app: my-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8090"
prometheus.io/path: "/metrics"
spec:
imagePullSecrets:
- name: ghcr-secret
containers:
- name: my-service
image: ghcr.io/mutugading/my-service:latest
imagePullPolicy: Always
ports:
- containerPort: 50051
name: grpc
- containerPort: 8080
name: http
- containerPort: 8090
name: metrics
env:
- name: APP_ENV
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: DATABASE_HOST
value: "postgres.database.svc.cluster.local"
- name: DATABASE_PORT
value: "5432"
- name: DATABASE_NAME
value: "goapps"
- name: DATABASE_SSLMODE
value: "disable"
- name: DATABASE_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: POSTGRES_USER
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: POSTGRES_PASSWORD
- name: REDIS_HOST
value: "redis.database.svc.cluster.local"
- name: REDIS_PORT
value: "6379"
- name: TRACING_ENABLED
value: "true"
- name: JAEGER_ENDPOINT
value: "jaeger-collector.observability.svc.cluster.local:4317"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
grpc:
port: 50051
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
periodSeconds: 5apiVersion: v1
kind: Service
metadata:
name: my-service
labels:
app: my-service
spec:
selector:
app: my-service
ports:
- name: grpc
port: 50051
targetPort: grpc
- name: http
port: 8080
targetPort: http
- name: metrics
port: 8090
targetPort: metricsapiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-service
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- hpa.yaml
commonLabels:
app.kubernetes.io/name: my-service
app.kubernetes.io/part-of: goapps
app.kubernetes.io/component: backendapiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: goapps-staging
resources:
- ../../base
patches:
- path: patches/replicas.yaml
images:
- name: ghcr.io/mutugading/my-service
newTag: stagingapiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
spec:
replicas: 1# argocd/apps/staging/my-service.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-service-staging
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: goapps
source:
repoURL: https://github.com/mutugading/goapps-infra.git
targetRevision: main
path: services/my-service/overlays/staging
destination:
server: https://kubernetes.default.svc
namespace: goapps-staging
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=trueEdit base/database/postgres/configmap.yaml:
-- Add to init-schemas.sql
CREATE SCHEMA IF NOT EXISTS my_service;
GRANT ALL PRIVILEGES ON SCHEMA my_service TO postgres;git checkout -b infra/add-my-service
git add .
git commit -m "feat(my-service): add deployment configuration"
git push origin infra/add-my-service
# Create PR to main| Setting | Value | Reason |
|---|---|---|
| max_connections | 150 | Allow for PgBouncer pooling + direct connections |
| shared_buffers | 256MB | ~25% of available RAM for caching |
| work_mem | 16MB | Per-operation memory |
| maintenance_work_mem | 128MB | For VACUUM, CREATE INDEX |
- Edit
base/database/postgres/configmap.yaml - Add CREATE SCHEMA statement to
init-schemas.sql
CREATE SCHEMA IF NOT EXISTS new_schema;
GRANT ALL PRIVILEGES ON SCHEMA new_schema TO postgres;- Re-deploy PostgreSQL pod (if needed, data is preserved)
kubectl rollout restart statefulset/postgres -n databaseAll services MUST connect through PgBouncer:
# ✅ CORRECT
env:
- name: DATABASE_HOST
value: "pgbouncer.database.svc.cluster.local"
- name: DATABASE_PORT
value: "5432"
# ❌ WRONG - Direct PostgreSQL connection
env:
- name: DATABASE_HOST
value: "postgres.database.svc.cluster.local"Migrations are run from services, not from infra repo:
# In goapps-backend/services/finance
make migrate-up- Create JSON file in
base/monitoring/dashboards/ - Create ConfigMap with label
grafana_dashboard: "1"
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-my-service
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
my-service-dashboard.json: |
{
"title": "My Service Dashboard",
...
}- Grafana sidecar will auto-load the dashboard
- Edit
base/monitoring/alert-rules/grafana-alerts.yaml - Follow existing alert format:
- name: MyServiceAlerts
folder: GoApps
interval: 1m
rules:
- uid: my-service-high-error-rate
title: My Service High Error Rate
condition: C
data:
- refId: A
relativeTimeRange:
from: 300
to: 0
datasourceUid: prometheus
model:
expr: rate(grpc_server_handled_total{grpc_code!="OK",service="my-service"}[5m]) > 0.1
for: 5m
annotations:
summary: "My Service error rate is high"
description: "Error rate: {{ $value }}"
labels:
severity: warningapiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-service-monitor
namespace: monitoring
labels:
release: prometheus
spec:
selector:
matchLabels:
app: my-service
namespaceSelector:
matchNames:
- goapps-staging
- goapps-production
endpoints:
- port: metrics
interval: 30s
path: /metrics- Check CronJob status:
kubectl get cronjobs -n database - Verify MinIO bucket:
mc ls minio/postgres-backups - Verify Backblaze B2: Check B2 console
- Verify VPS disk backups:
ls -la /mnt/goapps-backup/postgres - Test restore on staging (monthly)
Add to base/backup/cronjobs/postgres-backup.yaml:
# In backup.sh script, add:
if [ -n "${NEW_BACKUP_ENDPOINT}" ]; then
echo "[$(date)] Uploading to new backup target..."
mc alias set newbackup https://${NEW_BACKUP_ENDPOINT} ${ACCESS_KEY} ${SECRET_KEY}
mc cp "${BACKUP_FILE}" newbackup/${BUCKET}/
fiPerform at least once per month on staging:
# 1. Get latest backup
BACKUP=$(ls -t /mnt/stgapps-backup/postgres/*.sql.gz | head -1)
# 2. Create test database
kubectl exec -it postgres-0 -n database -- \
psql -U postgres -c "CREATE DATABASE goapps_restore_test"
# 3. Restore
kubectl exec -it postgres-0 -n database -- bash -c "
gunzip -c ${BACKUP} | psql -U postgres -d goapps_restore_test
"
# 4. Verify
kubectl exec -it postgres-0 -n database -- \
psql -U postgres -d goapps_restore_test -c "SELECT COUNT(*) FROM finance.some_table"
# 5. Cleanup
kubectl exec -it postgres-0 -n database -- \
psql -U postgres -c "DROP DATABASE goapps_restore_test"| DO ✅ | DON'T ❌ |
|---|---|
| Create secrets via kubectl | Commit secrets to Git |
| Use secretKeyRef in manifests | Hardcode values in manifests |
| Use different secrets per env | Share secrets between environments |
| Rotate secrets quarterly | Use weak passwords |
| Use RBAC for secret access | Give cluster-admin to services |
Services MUST define NetworkPolicy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: my-service-network-policy
namespace: goapps-staging
spec:
podSelector:
matchLabels:
app: my-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 50051
egress:
- to:
- namespaceSelector:
matchLabels:
name: database
ports:
- protocol: TCP
port: 5432
- protocol: TCP
port: 6379- All images MUST be from trusted registry (ghcr.io/mutugading)
- Use specific tags, not
latestin production - Scan images with Trivy in CI
gitGraph
commit id: "main"
branch infra/add-new-service
checkout infra/add-new-service
commit id: "Add base manifests"
commit id: "Add overlays"
commit id: "Add ArgoCD app"
checkout main
merge infra/add-new-service
commit id: "Auto-sync to staging"
# 1. Create hotfix branch
git checkout -b hotfix/postgres-oom main
# 2. Make fix
vim base/database/postgres/statefulset.yaml
# 3. Test locally with kustomize
kustomize build base/database/postgres
# 4. Commit
git commit -am "fix(postgres): increase memory limit to prevent OOM"
# 5. Push and create PR (with "hotfix" label for fast-track review)
git push origin hotfix/postgres-oom- CI checks pass (kustomize build, yamllint, trivy)
- At least 1 reviewer approval
- PR description explains what and why
- No secrets in diff
- Documentation updated if needed
# 1. Check events
kubectl describe pod <pod> -n <namespace>
# 2. Check logs (current and previous)
kubectl logs <pod> -n <namespace>
kubectl logs <pod> -n <namespace> --previous
# 3. If OOM, check resources
kubectl top pod <pod> -n <namespace>
# 4. Rollback if needed
kubectl rollout undo deployment/<name> -n <namespace># 1. Check PostgreSQL status
kubectl get pods -n database -l app=postgres
kubectl logs postgres-0 -n database --tail=100
# 2. Check PgBouncer
kubectl get pods -n database -l app=pgbouncer
kubectl logs deploy/pgbouncer -n database
# 3. Test direct connection
kubectl exec -it postgres-0 -n database -- \
psql -U postgres -d goapps -c "SELECT 1"
# 4. Check connection count
kubectl exec -it postgres-0 -n database -- \
psql -U postgres -c "SELECT count(*) FROM pg_stat_activity"# Via kubectl
kubectl rollout undo deployment/<name> -n <namespace>
# Via ArgoCD
argocd app rollback <app-name>
# Check rollout history
kubectl rollout history deployment/<name> -n <namespace>
# Rollback to specific revision
kubectl rollout undo deployment/<name> -n <namespace> --to-revision=2-
Severity 1 (Production Down)
- Immediate rollback
- Notify on-call engineer
- Post-incident review within 24h
-
Severity 2 (Degraded Performance)
- Investigate logs and metrics
- Apply fix if identified
- Monitor for 1 hour
-
Severity 3 (Non-critical)
- Create issue in GitHub
- Schedule fix in next sprint
- Kustomize build passes:
kustomize build <path> - YAML lint passes:
yamllint . - No secrets in code
- Documentation updated
- PR reviewed and approved
- Tested in staging for minimum 24 hours
- No critical alerts in staging
- Backup verified
- Rollback plan documented
- Team notified
- Check backup status
- Review Grafana alerts
- Check ArgoCD sync status
- Review resource utilization
- Check PVC usage
- On-call Rotation: TBD
- Escalation Path: Developer → Team Lead → CTO
- Slack Channel: #devops-goapps