diff --git a/CI_CD_PIPELINE_SETUP_GUIDE.md b/CI_CD_PIPELINE_SETUP_GUIDE.md index b54c04a..2352d1e 100644 --- a/CI_CD_PIPELINE_SETUP_GUIDE.md +++ b/CI_CD_PIPELINE_SETUP_GUIDE.md @@ -1359,7 +1359,7 @@ The CI/CD pipeline uses ephemeral Podman-in-Podman containers with a secure four #### 7.5 Production Deployment Architecture -The production deployment uses a separate pod configuration (`prod-pod.yaml`) that pulls built images from the Forgejo Container Registry and deploys the complete application stack. +The production deployment uses a separate pod configuration (`prod-pod.yml`) that pulls built images from the Forgejo Container Registry and deploys the complete application stack. **Production Stack Components:** - **PostgreSQL**: Production database with persistent storage @@ -1370,7 +1370,7 @@ The production deployment uses a separate pod configuration (`prod-pod.yaml`) th **Deployment Flow:** 1. **Production Runner**: Runs on Production Linode with `production` label 2. **Image Pull**: Pulls latest images from Forgejo Container Registry -3. **Stack Deployment**: Uses `prod-pod.yaml` to deploy complete stack +3. **Stack Deployment**: Uses `prod-pod.yml` to deploy complete stack 4. **Health Verification**: Ensures all services are healthy before completion **Key Benefits:** @@ -1992,7 +1992,7 @@ sudo journalctl -u forgejo-runner.service -f --no-pager When the workflow runs, it will: 1. Pull the latest Docker images from Forgejo Container Registry -2. Use the `prod-pod.yaml` file to deploy the application stack +2. Use the `prod-pod.yml` file to deploy the application stack 3. Create the necessary environment variables for production deployment 4. Verify that all services are healthy after deployment @@ -2000,7 +2000,7 @@ The production runner will automatically handle the deployment process when you #### 12.6 Understanding the Production Pod Setup -The `prod-pod.yaml` file is specifically designed for production deployment and uses Kubernetes pod specifications: +The `prod-pod.yml` file is specifically designed for production deployment and uses Kubernetes pod specifications: **Key Features**: - **Image-based deployment**: Uses pre-built images from Forgejo Container Registry instead of building from source @@ -2019,7 +2019,7 @@ The `prod-pod.yaml` file is specifically designed for production deployment and **Deployment Process**: 1. The production runner pulls the latest images from Forgejo Container Registry 2. Creates environment variables for the deployment -3. Runs `podman play kube prod-pod.yaml` +3. Runs `podman play kube prod-pod.yml` 4. Waits for all services to be healthy 5. Verifies the deployment was successful @@ -2424,12 +2424,12 @@ After successful setup, you can clean up the installation files to remove sensit |---------|--------------------------|--------------------------| | Test | All integration/unit tests| `ci-pod.yaml` | | Build | Build & push images | Podman build & push | -| Deploy | Deploy to production | `prod-pod.yaml` | +| Deploy | Deploy to production | `prod-pod.yml` | **How it works:** - **Test:** The workflow spins up a full test environment using `ci-pod.yaml` (Postgres, backend, frontend, etc.) and runs all tests inside containers. - **Build:** If tests pass, the workflow uses Podman to build backend and frontend images and push them to Forgejo Container Registry. -- **Deploy:** The production runner pulls images from Forgejo Container Registry and deploys the stack using `prod-pod.yaml`. +- **Deploy:** The production runner pulls images from Forgejo Container Registry and deploys the stack using `prod-pod.yml`. **Expected Output:** - Each stage runs in its own isolated environment. diff --git a/deploy/prod-pod.yml b/deploy/prod-pod.yml new file mode 100644 index 0000000..8383872 --- /dev/null +++ b/deploy/prod-pod.yml @@ -0,0 +1,185 @@ +apiVersion: v1 +kind: Pod +metadata: + name: prod-pod + labels: + app: sharenet-production + annotations: + io.containers.no-new-privileges: "true" +spec: + hostname: prod-pod + # Security: run as non-root user with specific UID/GID + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + + containers: + - name: postgres + image: localhost/postgres:deployed + # Security: drop all capabilities, read-only root filesystem except data volume + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + envFrom: + - secretRef: + name: postgres-secrets + ports: + - containerPort: 5432 + protocol: TCP + volumeMounts: + - name: pgdata + mountPath: /var/lib/postgresql/data + readOnly: false + # Health checks + livenessProbe: + exec: + command: ["pg_isready", "-U", "sharenet"] + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + readinessProbe: + exec: + command: ["pg_isready", "-U", "sharenet"] + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 5 + # Resource limits + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" + + - name: backend + image: localhost/backend:deployed + # Security: drop all capabilities, read-only root filesystem + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + envFrom: + - secretRef: + name: backend-secrets + ports: + - containerPort: 3001 + protocol: TCP + # Health checks + livenessProbe: + httpGet: + path: /health + port: 3001 + scheme: HTTP + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 3001 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + # Resource limits + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "250m" + + - name: frontend + image: localhost/frontend:deployed + # Security: drop all capabilities, read-only root filesystem + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + envFrom: + - secretRef: + name: frontend-secrets + ports: + - containerPort: 3000 + protocol: TCP + # Resource limits + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "250m" + + - name: nginx + image: localhost/nginx:deployed + # Security: drop all capabilities, read-only root filesystem + securityContext: + readOnlyRootFilesystem: true + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + ports: + - containerPort: 80 + protocol: TCP + hostPort: 80 + - containerPort: 443 + protocol: TCP + hostPort: 443 + volumeMounts: + - name: nginx-conf + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + - name: nginx-cache + mountPath: /var/cache/nginx + readOnly: false + - name: letsencrypt + mountPath: /etc/letsencrypt + readOnly: true + # Health check + livenessProbe: + httpGet: + path: /healthz + port: 80 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + # Resource limits + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + + volumes: + - name: pgdata + hostPath: + path: /opt/sharenet/volumes/pgdata + type: Directory + - name: nginx-conf + hostPath: + path: /opt/sharenet/nginx + type: Directory + - name: nginx-cache + hostPath: + path: /opt/sharenet/volumes/nginx-cache + type: Directory + - name: letsencrypt + hostPath: + path: /etc/letsencrypt + type: Directory \ No newline at end of file diff --git a/prod-pod.yaml b/prod-pod.yaml deleted file mode 100644 index cc25191..0000000 --- a/prod-pod.yaml +++ /dev/null @@ -1,121 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: sharenet-production-pod - labels: - app: sharenet-production -spec: - containers: - - name: postgres - image: postgres:15-alpine - env: - - name: POSTGRES_DB - value: "sharenet" - - name: POSTGRES_USER - value: "sharenet" - - name: POSTGRES_PASSWORD - value: "${POSTGRES_PASSWORD}" - ports: - - containerPort: 5432 - protocol: TCP - volumeMounts: - - name: postgres-data - mountPath: /var/lib/postgresql/data - livenessProbe: - exec: - command: - - pg_isready - - -U - - sharenet - initialDelaySeconds: 30 - periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 3 - readinessProbe: - exec: - command: - - pg_isready - - -U - - sharenet - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 5 - - - name: backend - image: ${REGISTRY_HOST}/${OWNER_REPO}/backend:${IMAGE_TAG} - env: - - name: DATABASE_URL - value: "postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}" - - name: RUST_LOG - value: "info" - - name: RUST_BACKTRACE - value: "1" - ports: - - containerPort: 3001 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: 3001 - initialDelaySeconds: 30 - periodSeconds: 30 - timeoutSeconds: 10 - failureThreshold: 3 - readinessProbe: - httpGet: - path: /health - port: 3001 - initialDelaySeconds: 10 - periodSeconds: 10 - timeoutSeconds: 5 - - - name: frontend - image: ${REGISTRY_HOST}/${OWNER_REPO}/frontend:${IMAGE_TAG} - env: - - name: NEXT_PUBLIC_API_HOST - value: "localhost" - - name: NEXT_PUBLIC_API_PORT - value: "3001" - - name: NODE_ENV - value: "production" - ports: - - containerPort: 3000 - protocol: TCP - dependsOn: - - name: backend - condition: Ready - - - name: nginx - image: nginx:alpine - ports: - - containerPort: 80 - protocol: TCP - - containerPort: 443 - protocol: TCP - volumeMounts: - - name: nginx-config - mountPath: /etc/nginx/nginx.conf - subPath: nginx.conf - readOnly: true - - name: nginx-ssl - mountPath: /etc/nginx/ssl - readOnly: true - dependsOn: - - name: frontend - condition: Ready - - name: backend - condition: Ready - - volumes: - - name: postgres-data - hostPath: - path: /var/lib/postgresql/data - type: Directory - - name: nginx-config - hostPath: - path: /opt/sharenet/nginx/nginx.conf - type: File - - name: nginx-ssl - hostPath: - path: /opt/sharenet/nginx/ssl - type: Directory