diff --git a/documentation/deployment-guides/00-prerequisites.md b/documentation/deployment-guides/00-prerequisites.md new file mode 100644 index 0000000..afffc12 --- /dev/null +++ b/documentation/deployment-guides/00-prerequisites.md @@ -0,0 +1,283 @@ +# Google Cloud Prerequisites for ERPNext Deployment + +## Overview + +This guide covers the prerequisites and initial setup required for deploying ERPNext on Google Cloud Platform (GCP). + +## ๐Ÿ”ง Required Tools + +### 1. Google Cloud SDK +```bash +# Install gcloud CLI +curl https://sdk.cloud.google.com | bash +exec -l $SHELL + +# Initialize gcloud +gcloud init +gcloud auth login +gcloud config set project YOUR_PROJECT_ID +``` + +### 2. kubectl (Kubernetes CLI) +```bash +# Install kubectl +gcloud components install kubectl + +# Verify installation +kubectl version --client +``` + +### 3. Docker (for local testing) +```bash +# Install Docker +curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh + +# Enable Docker BuildKit +export DOCKER_BUILDKIT=1 +``` + +### 4. Helm (for Kubernetes package management) +```bash +# Install Helm +curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + +# Verify installation +helm version +``` + +## ๐Ÿ—๏ธ Google Cloud Project Setup + +### 1. Create or Select Project +```bash +# Create new project +gcloud projects create erpnext-production --name="ERPNext Production" + +# Set as current project +gcloud config set project erpnext-production + +# Enable billing (required for most services) +# This must be done via the Console: https://console.cloud.google.com/billing +``` + +### 2. Enable Required APIs +```bash +# Enable essential APIs +gcloud services enable \ + container.googleapis.com \ + compute.googleapis.com \ + sqladmin.googleapis.com \ + secretmanager.googleapis.com \ + cloudbuild.googleapis.com \ + monitoring.googleapis.com \ + logging.googleapis.com +``` + +### 3. Set Default Region/Zone +```bash +# Set default compute region and zone +gcloud config set compute/region us-central1 +gcloud config set compute/zone us-central1-a + +# Verify configuration +gcloud config list +``` + +## ๐Ÿ” Security Setup + +### 1. Service Account Creation +```bash +# Create service account for ERPNext +gcloud iam service-accounts create erpnext-gke \ + --display-name="ERPNext GKE Service Account" \ + --description="Service account for ERPNext GKE deployment" + +# Grant necessary roles +gcloud projects add-iam-policy-binding erpnext-production \ + --member="serviceAccount:erpnext-gke@erpnext-production.iam.gserviceaccount.com" \ + --role="roles/container.developer" + +gcloud projects add-iam-policy-binding erpnext-production \ + --member="serviceAccount:erpnext-gke@erpnext-production.iam.gserviceaccount.com" \ + --role="roles/secretmanager.secretAccessor" +``` + +### 2. Create Service Account Key (Optional) +```bash +# Generate service account key (for local development) +gcloud iam service-accounts keys create ~/erpnext-gke-key.json \ + --iam-account=erpnext-gke@erpnext-production.iam.gserviceaccount.com + +# Set environment variable +export GOOGLE_APPLICATION_CREDENTIALS=~/erpnext-gke-key.json +``` + +### 3. Secret Manager Setup +```bash +# Create secrets for ERPNext +gcloud secrets create erpnext-admin-password \ + --data-file=<(echo -n "YourSecurePassword123!") + +gcloud secrets create erpnext-db-password \ + --data-file=<(echo -n "YourDBPassword123!") + +gcloud secrets create erpnext-api-key \ + --data-file=<(echo -n "your-api-key-here") + +gcloud secrets create erpnext-api-secret \ + --data-file=<(echo -n "your-api-secret-here") +``` + +## ๐Ÿ’พ Storage Configuration + +### 1. Cloud SQL (Managed Database Option) +```bash +# Create Cloud SQL instance for production +gcloud sql instances create erpnext-db \ + --database-version=MYSQL_8_0 \ + --cpu=2 \ + --memory=7680MB \ + --storage-size=100GB \ + --storage-type=SSD \ + --region=us-central1 \ + --backup \ + --maintenance-window-day=SUN \ + --maintenance-window-hour=3 + +# Create database +gcloud sql databases create erpnext --instance=erpnext-db + +# Create database user +gcloud sql users create erpnext \ + --instance=erpnext-db \ + --password=YourDBPassword123! +``` + +### 2. Persistent Disks (for GKE Storage) +```bash +# Create persistent disks for ERPNext data +gcloud compute disks create erpnext-sites-disk \ + --size=50GB \ + --type=pd-ssd \ + --zone=us-central1-a + +gcloud compute disks create erpnext-assets-disk \ + --size=20GB \ + --type=pd-ssd \ + --zone=us-central1-a +``` + +## ๐ŸŒ Networking Setup + +### 1. VPC Network (Optional - for advanced setups) +```bash +# Create custom VPC network +gcloud compute networks create erpnext-vpc \ + --subnet-mode=custom + +# Create subnet +gcloud compute networks subnets create erpnext-subnet \ + --network=erpnext-vpc \ + --range=10.0.0.0/24 \ + --region=us-central1 + +# Create firewall rules +gcloud compute firewall-rules create erpnext-allow-internal \ + --network=erpnext-vpc \ + --allow=tcp,udp,icmp \ + --source-ranges=10.0.0.0/24 + +gcloud compute firewall-rules create erpnext-allow-http \ + --network=erpnext-vpc \ + --allow=tcp:80,tcp:443,tcp:8080 \ + --source-ranges=0.0.0.0/0 +``` + +## ๐Ÿ“Š Monitoring and Logging + +### 1. Enable Monitoring +```bash +# Monitoring is enabled by default with the APIs +# Verify monitoring is working +gcloud logging logs list --limit=5 +``` + +### 2. Create Log-based Metrics (Optional) +```bash +# Create custom log metric for ERPNext errors +gcloud logging metrics create erpnext_errors \ + --description="ERPNext application errors" \ + --log-filter='resource.type="k8s_container" AND resource.labels.container_name="backend" AND severity="ERROR"' +``` + +## ๐Ÿ” Verification Checklist + +Before proceeding to deployment, verify: + +```bash +# Check project and authentication +gcloud auth list +gcloud config get-value project + +# Verify APIs are enabled +gcloud services list --enabled | grep -E "(container|compute|sql)" + +# Check service account exists +gcloud iam service-accounts list | grep erpnext-gke + +# Verify secrets are created +gcloud secrets list | grep erpnext + +# Check kubectl configuration +kubectl cluster-info --show-labels 2>/dev/null || echo "GKE cluster not yet created" +``` + +## ๐Ÿ’ก Cost Optimization Tips + +### 1. Use Preemptible Instances +- For non-production workloads +- 60-91% cost savings +- Automatic restarts handled by Kubernetes + +### 2. Right-size Resources +- Start with smaller instances +- Monitor usage and scale as needed +- Use Horizontal Pod Autoscaler + +### 3. Storage Optimization +- Use Standard persistent disks for non-critical data +- Enable automatic storage increases +- Regular cleanup of logs and temporary files + +## ๐Ÿšจ Security Best Practices + +1. **Never commit secrets to code** + - Always use Secret Manager + - Use Workload Identity when possible + +2. **Network Security** + - Use private GKE clusters + - Implement proper firewall rules + - Enable network policies + +3. **Access Control** + - Use IAM roles with least privilege + - Enable audit logging + - Regular security reviews + +## ๐Ÿ“š Additional Resources + +- [Google Kubernetes Engine Documentation](https://cloud.google.com/kubernetes-engine/docs) +- [Cloud SQL Documentation](https://cloud.google.com/sql/docs) +- [Secret Manager Documentation](https://cloud.google.com/secret-manager/docs) +- [GCP Pricing Calculator](https://cloud.google.com/products/calculator) + +## โžก๏ธ Next Steps + +After completing prerequisites: +1. **GKE Deployment**: Follow `01-gke-deployment.md` +2. **Cloud Run Assessment**: Review `02-cloud-run-analysis.md` +3. **Production Hardening**: See `03-production-setup.md` + +--- + +**โš ๏ธ Important**: Keep track of all resources created for billing purposes. Use resource labels and proper naming conventions for easier management. \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/01-gke-deployment.md b/documentation/deployment-guides/gcp/01-gke-deployment.md new file mode 100644 index 0000000..29ba222 --- /dev/null +++ b/documentation/deployment-guides/gcp/01-gke-deployment.md @@ -0,0 +1,1128 @@ +# ERPNext GKE Deployment Guide + +## Overview + +This guide provides step-by-step instructions for deploying ERPNext on Google Kubernetes Engine (GKE). It assumes you have completed the prerequisites in `00-prerequisites.md`. + +## ๐Ÿ—๏ธ GKE Cluster Setup + +### 1. Create GKE Cluster + +```bash +# Create GKE cluster with recommended configuration +gcloud container clusters create erpnext-cluster \ + --zone=us-central1-a \ + --num-nodes=3 \ + --node-locations=us-central1-a,us-central1-b,us-central1-c \ + --machine-type=e2-standard-4 \ + --disk-type=pd-ssd \ + --disk-size=50GB \ + --enable-autoscaling \ + --min-nodes=2 \ + --max-nodes=10 \ + --enable-autorepair \ + --enable-autoupgrade \ + --maintenance-window-start=2023-01-01T09:00:00Z \ + --maintenance-window-end=2023-01-01T17:00:00Z \ + --maintenance-window-recurrence="FREQ=WEEKLY;BYDAY=SA,SU" \ + --enable-network-policy \ + --enable-ip-alias \ + --enable-cloud-logging \ + --enable-cloud-monitoring \ + --addons=HorizontalPodAutoscaling,HttpLoadBalancing,NodeLocalDNS \ + --workload-pool=erpnext-production.svc.id.goog + +# Get cluster credentials +gcloud container clusters get-credentials erpnext-cluster --zone=us-central1-a +``` + +### 2. Verify Cluster Setup + +```bash +# Check cluster status +kubectl cluster-info +kubectl get nodes -o wide + +# Verify node readiness +kubectl get nodes +kubectl top nodes +``` + +## ๐Ÿ” Workload Identity Setup + +### 1. Enable Workload Identity (if not done during cluster creation) + +```bash +# Enable Workload Identity on existing cluster +gcloud container clusters update erpnext-cluster \ + --zone=us-central1-a \ + --workload-pool=erpnext-production.svc.id.goog +``` + +### 2. Create Kubernetes Service Account + +```bash +# Create namespace +kubectl create namespace erpnext + +# Create Kubernetes service account +kubectl create serviceaccount erpnext-ksa --namespace=erpnext + +# Bind Google service account to Kubernetes service account +gcloud iam service-accounts add-iam-policy-binding \ + --role roles/iam.workloadIdentityUser \ + --member "serviceAccount:erpnext-production.svc.id.goog[erpnext/erpnext-ksa]" \ + erpnext-gke@erpnext-production.iam.gserviceaccount.com + +# Annotate Kubernetes service account +kubectl annotate serviceaccount erpnext-ksa \ + --namespace=erpnext \ + iam.gke.io/gcp-service-account=erpnext-gke@erpnext-production.iam.gserviceaccount.com +``` + +## ๐Ÿ’พ Storage Setup + +### 1. Create Storage Classes + +```bash +# Create storage class for fast SSD storage +kubectl apply -f - < B[Cloud Run Frontend] + A --> C[Cloud Run Backend API] + C --> D[Cloud SQL] + C --> E[Memorystore Redis] + F[GKE Workers] --> D + F --> E + G[Cloud Scheduler] --> F +``` + +**Components:** +- **Cloud Run**: Frontend + API endpoints +- **GKE**: Queue workers + scheduled tasks +- **Managed Services**: Cloud SQL + Memorystore + +### Option 2: Event-Driven Architecture + +```mermaid +graph TD + A[Cloud Run API] --> B[Cloud Tasks] + B --> C[Cloud Functions] + C --> D[Cloud SQL] + A --> D + A --> E[Cloud Storage] + F[Cloud Scheduler] --> C +``` + +**Components:** +- **Cloud Run**: Main application +- **Cloud Functions**: Background job processing +- **Cloud Tasks**: Job queue management +- **Cloud Scheduler**: Scheduled tasks + +## ๐Ÿš€ Implementation Roadmap + +### Phase 1: Assessment (2 weeks) +1. Audit current ERPNext customizations +2. Identify background job dependencies +3. Test basic Cloud Run deployment +4. Measure performance baselines + +### Phase 2: Proof of Concept (4 weeks) +1. Deploy read-only ERPNext on Cloud Run +2. Implement Cloud Storage file adapter +3. Test basic CRUD operations +4. Benchmark performance and costs + +### Phase 3: Background Processing (6 weeks) +1. Implement Cloud Tasks integration +2. Migrate scheduled tasks to Cloud Scheduler +3. Test job processing workflows +4. Implement monitoring and alerting + +### Phase 4: Production Migration (4 weeks) +1. Full data migration +2. DNS cutover +3. Performance optimization +4. Documentation and training + +## ๐Ÿ” Decision Framework + +### Technical Readiness Checklist + +- [ ] ERPNext version compatibility assessment +- [ ] Custom app background job inventory +- [ ] File storage usage analysis +- [ ] WebSocket feature usage evaluation +- [ ] Performance requirements definition + +### Business Readiness Checklist + +- [ ] Cost-benefit analysis completed +- [ ] Stakeholder buy-in obtained +- [ ] Migration timeline approved +- [ ] Rollback plan prepared +- [ ] Team training planned + +## ๐Ÿ“Š Success Metrics + +### Performance Metrics +- **Response Time**: < 2 seconds for 95% of requests +- **Availability**: 99.9% uptime +- **Scalability**: Handle 10x traffic spikes + +### Cost Metrics +- **Monthly Savings**: Target 20%+ reduction +- **Operational Overhead**: Reduce maintenance time by 50% + +### Feature Metrics +- **Functionality Parity**: 95%+ feature compatibility +- **User Satisfaction**: No degradation in user experience + +## ๐ŸŽฏ Final Recommendation + +### For Most Organizations: **Stick with GKE** + +ERPNext's architecture is fundamentally designed for traditional server environments. The significant development effort required to make it Cloud Run compatible, combined with feature limitations, makes GKE the recommended approach for most production deployments. + +### Cloud Run Makes Sense For: + +1. **Development/Testing**: Temporary environments +2. **API-Only Deployments**: Headless ERPNext integrations +3. **Proof of Concepts**: Quick demos and trials +4. **Cost-Constrained Projects**: Where savings justify limitations + +### Hybrid Approach Recommendation: + +Consider a **hybrid model** where: +- **Frontend/API** runs on Cloud Run for cost optimization +- **Background processing** remains on GKE/Cloud Functions +- **Database/Cache** uses managed services + +This provides cost benefits while maintaining full functionality. + +--- + +**Next Steps**: If proceeding with Cloud Run, start with Phase 1 assessment and proof of concept before committing to full migration. \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/03-production-setup.md b/documentation/deployment-guides/gcp/03-production-setup.md new file mode 100644 index 0000000..7de103d --- /dev/null +++ b/documentation/deployment-guides/gcp/03-production-setup.md @@ -0,0 +1,1171 @@ +# ERPNext GKE Production Hardening Guide + +## Overview + +This guide covers production-ready configurations, security hardening, monitoring, backup strategies, and operational best practices for ERPNext on GKE. + +## ๐Ÿ” Security Hardening + +### 1. Private GKE Cluster Setup + +```bash +# Create private GKE cluster with enhanced security +gcloud container clusters create erpnext-prod \ + --zone=us-central1-a \ + --node-locations=us-central1-a,us-central1-b,us-central1-c \ + --enable-private-nodes \ + --master-ipv4-cidr-block=172.16.0.0/28 \ + --enable-ip-alias \ + --cluster-ipv4-cidr=10.1.0.0/16 \ + --services-ipv4-cidr=10.2.0.0/16 \ + --enable-network-policy \ + --enable-autoscaling \ + --min-nodes=3 \ + --max-nodes=20 \ + --machine-type=e2-standard-4 \ + --disk-type=pd-ssd \ + --disk-size=100GB \ + --enable-autorepair \ + --enable-autoupgrade \ + --maintenance-window-start=2024-01-01T03:00:00Z \ + --maintenance-window-end=2024-01-01T07:00:00Z \ + --maintenance-window-recurrence="FREQ=WEEKLY;BYDAY=SU" \ + --workload-pool=erpnext-production.svc.id.goog \ + --enable-shielded-nodes \ + --enable-image-streaming \ + --logging=SYSTEM,WORKLOAD,API_SERVER \ + --monitoring=SYSTEM,WORKLOAD,STORAGE,POD,DEPLOYMENT,STATEFULSET,DAEMONSET,HPA,CADVISOR,KUBELET +``` + +### 2. Network Security Policies + +```bash +# Deny all traffic by default +kubectl apply -f - < 2 + for: 2m + labels: + severity: warning + annotations: + summary: "ERPNext response time is high" + description: "95th percentile response time is {{ $value }}s" + + - alert: ERPNextPodCrashLooping + expr: rate(kube_pod_container_status_restarts_total{namespace="erpnext"}[5m]) > 0 + for: 2m + labels: + severity: critical + annotations: + summary: "ERPNext pod is crash looping" + description: "Pod {{ $labels.pod }} is restarting frequently" + + - alert: ERPNextHighCPUUsage + expr: sum(rate(container_cpu_usage_seconds_total{namespace="erpnext"}[5m])) by (pod) > 0.8 + for: 5m + labels: + severity: warning + annotations: + summary: "ERPNext pod high CPU usage" + description: "Pod {{ $labels.pod }} CPU usage is {{ $value }}" + + - alert: ERPNextHighMemoryUsage + expr: sum(container_memory_working_set_bytes{namespace="erpnext"}) by (pod) / sum(container_spec_memory_limit_bytes{namespace="erpnext"}) by (pod) > 0.9 + for: 5m + labels: + severity: warning + annotations: + summary: "ERPNext pod high memory usage" + description: "Pod {{ $labels.pod }} memory usage is {{ $value }}" + + - alert: ERPNextDatabaseDown + expr: up{job="mariadb"} == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "ERPNext database is down" + description: "MariaDB database is not responding" +EOF +``` + +## ๐Ÿ”„ Backup and Disaster Recovery + +### 1. Database Backup Strategy + +```bash +# Create backup job using CronJob +kubectl apply -f - < /backup/\$BACKUP_FILE + gzip /backup/\$BACKUP_FILE + gsutil cp /backup/\$BACKUP_FILE.gz gs://erpnext-backups/database/ + # Keep only last 30 days of backups + find /backup -name "*.gz" -mtime +30 -delete + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + volumeMounts: + - name: backup-storage + mountPath: /backup + volumes: + - name: backup-storage + persistentVolumeClaim: + claimName: backup-pvc +EOF +``` + +### 2. Site Files Backup + +```bash +# Create site files backup job +kubectl apply -f - < r.status === 200, + 'response time < 2s': (r) => r.timings.duration < 2000, + }); + sleep(1); + } +EOF +``` + +## ๐Ÿšจ Incident Response + +### 1. Runbook for Common Issues + +```bash +# Create incident response ConfigMap +kubectl apply -f - < -n erpnext + 2. Check events: kubectl describe pod -n erpnext + 3. Check resource usage: kubectl top pod -n erpnext + + ## Common Causes + - Database connection issues + - Insufficient resources + - Configuration errors + - Image pull failures + + ## Resolution Steps + 1. Scale down problematic deployment + 2. Fix underlying issue + 3. Scale back up + 4. Monitor for stability + + high-response-time.md: | + # High Response Time Incident Response + + ## Investigation Steps + 1. Check current load: kubectl top pods -n erpnext + 2. Check HPA status: kubectl get hpa -n erpnext + 3. Check database performance + 4. Review nginx access logs + + ## Resolution Steps + 1. Scale up if needed: kubectl scale deployment erpnext-backend --replicas=5 + 2. Check database queries + 3. Clear Redis cache if needed + 4. Review and optimize slow queries +EOF +``` + +## ๐Ÿ“‹ Production Checklist + +### Pre-Deployment Checklist + +- [ ] Security hardening applied +- [ ] Network policies configured +- [ ] RBAC properly set up +- [ ] Secrets management implemented +- [ ] Monitoring stack deployed +- [ ] Backup procedures tested +- [ ] Load testing completed +- [ ] Disaster recovery plan documented +- [ ] Incident response procedures ready +- [ ] Documentation updated + +### Post-Deployment Checklist + +- [ ] All pods running and healthy +- [ ] Ingress working correctly +- [ ] SSL certificates issued +- [ ] Monitoring alerts configured +- [ ] Backup jobs scheduled +- [ ] Log aggregation working +- [ ] Performance metrics baseline established +- [ ] Team trained on operational procedures + +## ๐Ÿ“š Additional Resources + +- [GKE Security Best Practices](https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster) +- [Kubernetes Production Best Practices](https://kubernetes.io/docs/setup/best-practices/) +- [ERPNext Administration Guide](https://docs.erpnext.com/docs/user/manual/en/setting-up) +- [Prometheus Monitoring Best Practices](https://prometheus.io/docs/practices/) + +--- + +**โš ๏ธ Important**: Regular security audits and updates are essential for maintaining a secure production environment. Schedule quarterly reviews of all security configurations and keep up with the latest security patches. \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/README.md b/documentation/deployment-guides/gcp/README.md new file mode 100644 index 0000000..a016824 --- /dev/null +++ b/documentation/deployment-guides/gcp/README.md @@ -0,0 +1,258 @@ +# ERPNext Google Cloud Deployment Guide + +## Overview + +This directory contains comprehensive guides and resources for deploying ERPNext on Google Cloud Platform (GCP) using Google Kubernetes Engine (GKE). + +## ๐Ÿ“ Directory Structure + +``` +gcp/ +โ”œโ”€โ”€ README.md # This file +โ”œโ”€โ”€ 01-gke-deployment.md # Complete GKE deployment guide +โ”œโ”€โ”€ 02-cloud-run-analysis.md # Cloud Run feasibility analysis +โ”œโ”€โ”€ 03-production-setup.md # Production hardening guide +โ”œโ”€โ”€ kubernetes-manifests/ # Kubernetes YAML manifests +โ”‚ โ”œโ”€โ”€ namespace.yaml # Namespace and resource quotas +โ”‚ โ”œโ”€โ”€ storage.yaml # Storage classes and PVCs +โ”‚ โ”œโ”€โ”€ configmap.yaml # Configuration maps +โ”‚ โ”œโ”€โ”€ redis.yaml # Redis deployment +โ”‚ โ”œโ”€โ”€ mariadb.yaml # MariaDB deployment +โ”‚ โ”œโ”€โ”€ erpnext-backend.yaml # ERPNext backend services +โ”‚ โ”œโ”€โ”€ erpnext-frontend.yaml # ERPNext frontend (Nginx) +โ”‚ โ”œโ”€โ”€ erpnext-workers.yaml # Queue workers and scheduler +โ”‚ โ”œโ”€โ”€ ingress.yaml # Ingress and SSL configuration +โ”‚ โ””โ”€โ”€ jobs.yaml # Site creation and backup jobs +โ””โ”€โ”€ scripts/ # Automation scripts + โ”œโ”€โ”€ deploy.sh # Automated deployment script + โ””โ”€โ”€ backup-restore.sh # Backup and restore utilities +``` + +## ๐Ÿš€ Quick Start + +### Prerequisites + +Before starting, ensure you have completed the setup in `../00-prerequisites.md`. + +### 1. Automated Deployment + +The easiest way to deploy ERPNext on GKE: + +```bash +cd scripts/ +export PROJECT_ID="your-gcp-project" +export DOMAIN="erpnext.yourdomain.com" +export EMAIL="admin@yourdomain.com" +./deploy.sh deploy +``` + +### 2. Manual Deployment + +For more control, follow the step-by-step guide in `01-gke-deployment.md`. + +### 3. Production Setup + +After basic deployment, harden your installation using `03-production-setup.md`. + +## ๐Ÿ“– Documentation Guide + +### For First-Time Deployments + +1. **Start with Prerequisites**: Read `../00-prerequisites.md` +2. **Choose Your Path**: + - **Quick Setup**: Use the automated deployment script + - **Detailed Setup**: Follow `01-gke-deployment.md` step by step +3. **Production Ready**: Apply configurations from `03-production-setup.md` + +### For Production Deployments + +1. **Security First**: Implement all security measures from `03-production-setup.md` +2. **Monitoring**: Set up comprehensive monitoring and alerting +3. **Backup Strategy**: Configure automated backups using the provided scripts +4. **Performance Tuning**: Optimize based on your workload + +### For Cloud Run Consideration + +- **Analysis**: Review `02-cloud-run-analysis.md` for Cloud Run vs GKE comparison +- **Recommendation**: Most production workloads should use GKE + +## ๐Ÿ› ๏ธ Key Features + +### Security Hardening +- Private GKE clusters +- Network policies +- Pod security standards +- RBAC configuration +- Secrets management with External Secrets Operator + +### High Availability +- Multi-zone node pools +- Pod anti-affinity rules +- Horizontal Pod Autoscaling +- Pod Disruption Budgets +- Health checks and probes + +### Monitoring & Observability +- Prometheus and Grafana integration +- Custom ERPNext dashboards +- Alerting rules +- Log aggregation + +### Backup & Recovery +- Automated database backups +- Site files backup +- Point-in-time recovery +- Cross-region backup storage + +### Performance Optimization +- Resource requests and limits +- Vertical Pod Autoscaling +- Persistent SSD storage +- Nginx optimization + +## ๐Ÿ“Š Cost Estimation + +### Typical Production Setup +- **GKE Cluster**: ~$562/month + - 3 ร— e2-standard-4 nodes: ~$420/month + - Cluster management: $72.50/month + - Storage and networking: ~$70/month + +### Cost Optimization Tips +1. **Use Preemptible Nodes**: 60-80% cost savings for non-critical workloads +2. **Right-size Resources**: Start small and scale based on usage +3. **Use Regional Persistent Disks**: Better availability with minimal cost increase +4. **Enable Cluster Autoscaling**: Scale down during low-usage periods + +## ๐Ÿ”ง Customization + +### Environment Variables +All scripts support environment variable customization: + +```bash +# Deployment configuration +export PROJECT_ID="your-project" +export CLUSTER_NAME="erpnext-prod" +export ZONE="us-central1-a" +export DOMAIN="erp.company.com" +export EMAIL="admin@company.com" + +# Resource configuration +export NAMESPACE="erpnext" +export BACKUP_BUCKET="company-erpnext-backups" +``` + +### Kubernetes Manifests +Modify the YAML files in `kubernetes-manifests/` to: +- Adjust resource allocations +- Change storage sizes +- Modify security policies +- Add custom configurations + +## ๐Ÿšจ Troubleshooting + +### Common Issues + +1. **Pod Startup Failures** + ```bash + kubectl logs -f deployment/erpnext-backend -n erpnext + kubectl describe pod -n erpnext + ``` + +2. **Database Connection Issues** + ```bash + kubectl exec -it deployment/erpnext-backend -n erpnext -- mysql -h mariadb -u erpnext -p + ``` + +3. **SSL Certificate Problems** + ```bash + kubectl get certificate -n erpnext + kubectl describe certificate erpnext-tls -n erpnext + ``` + +4. **Storage Issues** + ```bash + kubectl get pvc -n erpnext + kubectl get pv + ``` + +### Getting Help + +- Check deployment status: `./scripts/deploy.sh status` +- View backup status: `./scripts/backup-restore.sh status` +- Monitor logs: `kubectl logs -f deployment/erpnext-backend -n erpnext` + +## ๐Ÿ”„ Upgrade Process + +### ERPNext Version Upgrades + +1. **Backup Current Installation** + ```bash + ./scripts/backup-restore.sh backup full + ``` + +2. **Update Image Tags** + Edit `kubernetes-manifests/erpnext-*.yaml` files to use new version + +3. **Apply Migrations** + ```bash + kubectl apply -f kubernetes-manifests/jobs.yaml + ``` + +4. **Rolling Update** + ```bash + kubectl set image deployment/erpnext-backend erpnext-backend=frappe/erpnext-worker:v15 -n erpnext + ``` + +### Kubernetes Upgrades + +Follow GKE's automatic upgrade schedule or manually upgrade: +```bash +gcloud container clusters upgrade erpnext-cluster --zone=us-central1-a +``` + +## ๐Ÿ›ก๏ธ Security Considerations + +### Network Security +- Private clusters with authorized networks +- Network policies restricting pod-to-pod communication +- Web Application Firewall (Cloud Armor) + +### Access Control +- RBAC with minimal permissions +- Workload Identity for GCP service access +- Regular access reviews + +### Data Protection +- Encryption at rest and in transit +- Regular security scans +- Backup encryption +- Secrets rotation + +## ๐Ÿ“ˆ Performance Monitoring + +### Key Metrics to Monitor +- Response time (target: <2s for 95% of requests) +- CPU and memory usage +- Database performance +- Queue processing time +- Storage utilization + +### Scaling Triggers +- CPU > 70% for 5 minutes โ†’ scale up +- Memory > 80% for 5 minutes โ†’ scale up +- Queue depth > 100 jobs โ†’ scale workers + +## ๐Ÿ”— Additional Resources + +- [ERPNext Documentation](https://docs.erpnext.com/) +- [Frappe Framework Docs](https://frappeframework.com/docs) +- [GKE Best Practices](https://cloud.google.com/kubernetes-engine/docs/best-practices) +- [Kubernetes Security](https://kubernetes.io/docs/concepts/security/) + +--- + +**Need Help?** +- Check the troubleshooting sections in each guide +- Review common issues in `03-production-setup.md` +- Use the provided scripts for automated operations \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/configmap.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/configmap.yaml new file mode 100644 index 0000000..52fc7c8 --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/configmap.yaml @@ -0,0 +1,155 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: erpnext-config + namespace: erpnext + labels: + app: erpnext + component: config +data: + APP_VERSION: "v14" + APP_URL: "erpnext.yourdomain.com" + APP_USER: "Administrator" + APP_DB_PARAM: "db" + DEVELOPER_MODE: "0" + ENABLE_SCHEDULER: "1" + SOCKETIO_PORT: "9000" + REDIS_CACHE_URL: "redis://redis:6379/0" + REDIS_QUEUE_URL: "redis://redis:6379/1" + REDIS_SOCKETIO_URL: "redis://redis:6379/2" + DB_HOST: "mariadb" + DB_PORT: "3306" + DB_NAME: "erpnext" + DB_USER: "erpnext" + # Database connection pool settings + DB_POOL_SIZE: "20" + DB_MAX_OVERFLOW: "30" + DB_POOL_TIMEOUT: "30" + # Performance settings + WORKER_PROCESSES: "4" + WORKER_TIMEOUT: "120" + WORKER_MAX_REQUESTS: "1000" + WORKER_MAX_REQUESTS_JITTER: "50" + # Logging settings + LOG_LEVEL: "INFO" + LOG_FORMAT: "json" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-config + namespace: erpnext + labels: + app: erpnext + component: nginx +data: + nginx.conf: | + user nginx; + worker_processes auto; + error_log /var/log/nginx/error.log notice; + pid /var/run/nginx.pid; + + events { + worker_connections 1024; + use epoll; + multi_accept on; + } + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 50m; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + upstream backend { + server erpnext-backend:8000; + keepalive 32; + } + + upstream socketio { + server erpnext-backend:9000; + keepalive 32; + } + + server { + listen 8080; + server_name _; + root /home/frappe/frappe-bench/sites; + + # Security headers + add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + + location /assets { + try_files $uri =404; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + location ~ ^/protected/(.*) { + internal; + try_files /frontend/$1 =404; + } + + location /socket.io/ { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header X-Frappe-Site-Name frontend; + proxy_set_header Origin $scheme://$http_host; + proxy_set_header Host $host; + proxy_pass http://socketio; + } + + location / { + try_files /frontend/public/$uri @webserver; + } + + location @webserver { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Frappe-Site-Name frontend; + proxy_set_header Host $host; + proxy_set_header X-Use-X-Accel-Redirect True; + proxy_read_timeout 120; + proxy_redirect off; + + proxy_pass http://backend; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } + } \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-backend.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-backend.yaml new file mode 100644 index 0000000..8a172f6 --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-backend.yaml @@ -0,0 +1,229 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: erpnext-backend + namespace: erpnext + labels: + app: erpnext-backend + component: backend + environment: production + version: v14 +spec: + replicas: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + selector: + matchLabels: + app: erpnext-backend + template: + metadata: + labels: + app: erpnext-backend + component: backend + environment: production + version: v14 + spec: + serviceAccountName: erpnext-ksa + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - erpnext-backend + topologyKey: kubernetes.io/hostname + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: cloud.google.com/gke-preemptible + operator: DoesNotExist + initContainers: + - name: wait-for-services + image: busybox:1.35 + command: + - sh + - -c + - | + echo 'Waiting for database and redis...' + until nc -z mariadb 3306 && nc -z redis 6379; do + echo 'Waiting for services...' + sleep 5 + done + echo 'Services are ready!' + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + containers: + - name: erpnext-backend + image: frappe/erpnext-worker:v14 + envFrom: + - configMapRef: + name: erpnext-config + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + ports: + - containerPort: 8000 + name: http + - containerPort: 9000 + name: socketio + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + volumeMounts: + - name: sites-data + mountPath: /home/frappe/frappe-bench/sites + - name: assets-data + mountPath: /home/frappe/frappe-bench/sites/assets + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /home/frappe/frappe-bench/logs + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /api/method/ping + port: 8000 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /api/method/ping + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + startupProbe: + httpGet: + path: /api/method/ping + port: 8000 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: assets-data + persistentVolumeClaim: + claimName: erpnext-assets-pvc + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: erpnext-backend + namespace: erpnext + labels: + app: erpnext-backend + component: backend +spec: + selector: + app: erpnext-backend + ports: + - name: http + port: 8000 + targetPort: 8000 + - name: socketio + port: 9000 + targetPort: 9000 + type: ClusterIP +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: erpnext-backend-hpa + namespace: erpnext + labels: + app: erpnext-backend + component: backend +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: erpnext-backend + minReplicas: 3 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percent + value: 50 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Percent + value: 100 + periodSeconds: 15 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: erpnext-backend-pdb + namespace: erpnext + labels: + app: erpnext-backend + component: backend +spec: + minAvailable: 2 + selector: + matchLabels: + app: erpnext-backend \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-frontend.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-frontend.yaml new file mode 100644 index 0000000..32ff57f --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-frontend.yaml @@ -0,0 +1,195 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: erpnext-frontend + namespace: erpnext + labels: + app: erpnext-frontend + component: frontend + environment: production + version: v14 +spec: + replicas: 2 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: erpnext-frontend + template: + metadata: + labels: + app: erpnext-frontend + component: frontend + environment: production + version: v14 + spec: + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - erpnext-frontend + topologyKey: kubernetes.io/hostname + containers: + - name: erpnext-frontend + image: frappe/erpnext-nginx:v14 + ports: + - containerPort: 8080 + name: http + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + volumeMounts: + - name: sites-data + mountPath: /home/frappe/frappe-bench/sites + readOnly: true + - name: assets-data + mountPath: /home/frappe/frappe-bench/sites/assets + readOnly: true + - name: nginx-config + mountPath: /etc/nginx/nginx.conf + subPath: nginx.conf + readOnly: true + - name: tmp + mountPath: /tmp + - name: var-cache + mountPath: /var/cache/nginx + - name: var-run + mountPath: /var/run + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + startupProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 12 + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: assets-data + persistentVolumeClaim: + claimName: erpnext-assets-pvc + - name: nginx-config + configMap: + name: nginx-config + - name: tmp + emptyDir: {} + - name: var-cache + emptyDir: {} + - name: var-run + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: erpnext-frontend + namespace: erpnext + labels: + app: erpnext-frontend + component: frontend +spec: + selector: + app: erpnext-frontend + ports: + - port: 8080 + targetPort: 8080 + name: http + type: ClusterIP +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: erpnext-frontend-hpa + namespace: erpnext + labels: + app: erpnext-frontend + component: frontend +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: erpnext-frontend + minReplicas: 2 + maxReplicas: 8 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percent + value: 50 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 60 + policies: + - type: Percent + value: 100 + periodSeconds: 15 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: erpnext-frontend-pdb + namespace: erpnext + labels: + app: erpnext-frontend + component: frontend +spec: + minAvailable: 1 + selector: + matchLabels: + app: erpnext-frontend \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-workers.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-workers.yaml new file mode 100644 index 0000000..a6bec08 --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/erpnext-workers.yaml @@ -0,0 +1,423 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: erpnext-queue-default + namespace: erpnext + labels: + app: erpnext-queue-default + component: worker + queue: default + environment: production + version: v14 +spec: + replicas: 2 + selector: + matchLabels: + app: erpnext-queue-default + template: + metadata: + labels: + app: erpnext-queue-default + component: worker + queue: default + environment: production + version: v14 + spec: + serviceAccountName: erpnext-ksa + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: queue-worker + image: frappe/erpnext-worker:v14 + command: + - bench + - worker + - --queue + - default + envFrom: + - configMapRef: + name: erpnext-config + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + volumeMounts: + - name: sites-data + mountPath: /home/frappe/frappe-bench/sites + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /home/frappe/frappe-bench/logs + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" + livenessProbe: + exec: + command: + - pgrep + - -f + - "bench worker" + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: erpnext-queue-long + namespace: erpnext + labels: + app: erpnext-queue-long + component: worker + queue: long + environment: production + version: v14 +spec: + replicas: 1 + selector: + matchLabels: + app: erpnext-queue-long + template: + metadata: + labels: + app: erpnext-queue-long + component: worker + queue: long + environment: production + version: v14 + spec: + serviceAccountName: erpnext-ksa + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: queue-worker + image: frappe/erpnext-worker:v14 + command: + - bench + - worker + - --queue + - long + envFrom: + - configMapRef: + name: erpnext-config + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + volumeMounts: + - name: sites-data + mountPath: /home/frappe/frappe-bench/sites + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /home/frappe/frappe-bench/logs + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" + livenessProbe: + exec: + command: + - pgrep + - -f + - "bench worker" + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: erpnext-queue-short + namespace: erpnext + labels: + app: erpnext-queue-short + component: worker + queue: short + environment: production + version: v14 +spec: + replicas: 2 + selector: + matchLabels: + app: erpnext-queue-short + template: + metadata: + labels: + app: erpnext-queue-short + component: worker + queue: short + environment: production + version: v14 + spec: + serviceAccountName: erpnext-ksa + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: queue-worker + image: frappe/erpnext-worker:v14 + command: + - bench + - worker + - --queue + - short + envFrom: + - configMapRef: + name: erpnext-config + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + volumeMounts: + - name: sites-data + mountPath: /home/frappe/frappe-bench/sites + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /home/frappe/frappe-bench/logs + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "500m" + livenessProbe: + exec: + command: + - pgrep + - -f + - "bench worker" + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: erpnext-scheduler + namespace: erpnext + labels: + app: erpnext-scheduler + component: scheduler + environment: production + version: v14 +spec: + replicas: 1 + selector: + matchLabels: + app: erpnext-scheduler + template: + metadata: + labels: + app: erpnext-scheduler + component: scheduler + environment: production + version: v14 + spec: + serviceAccountName: erpnext-ksa + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: scheduler + image: frappe/erpnext-worker:v14 + command: + - bench + - schedule + envFrom: + - configMapRef: + name: erpnext-config + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + volumeMounts: + - name: sites-data + mountPath: /home/frappe/frappe-bench/sites + - name: tmp + mountPath: /tmp + - name: logs + mountPath: /home/frappe/frappe-bench/logs + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "250m" + livenessProbe: + exec: + command: + - pgrep + - -f + - "bench schedule" + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: tmp + emptyDir: {} + - name: logs + emptyDir: {} + nodeSelector: + cloud.google.com/gke-preemptible: "false" +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: erpnext-queue-default-hpa + namespace: erpnext + labels: + app: erpnext-queue-default + component: worker +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: erpnext-queue-default + minReplicas: 2 + maxReplicas: 6 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: erpnext-queue-short-hpa + namespace: erpnext + labels: + app: erpnext-queue-short + component: worker +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: erpnext-queue-short + minReplicas: 2 + maxReplicas: 8 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/ingress.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/ingress.yaml new file mode 100644 index 0000000..93fc35e --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/ingress.yaml @@ -0,0 +1,110 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: erpnext-ingress + namespace: erpnext + labels: + app: erpnext + component: ingress + environment: production + annotations: + kubernetes.io/ingress.class: nginx + nginx.ingress.kubernetes.io/proxy-body-size: 50m + nginx.ingress.kubernetes.io/proxy-read-timeout: "600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "600" + nginx.ingress.kubernetes.io/proxy-connect-timeout: "60" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/cors-allow-origin: "*" + nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, DELETE, OPTIONS" + nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" + nginx.ingress.kubernetes.io/cors-expose-headers: "Content-Length,Content-Range" + nginx.ingress.kubernetes.io/enable-cors: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + # Security headers + nginx.ingress.kubernetes.io/configuration-snippet: | + add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss:"; +spec: + tls: + - hosts: + - erpnext.yourdomain.com + secretName: erpnext-tls + rules: + - host: erpnext.yourdomain.com + http: + paths: + # Static assets with caching + - path: /assets + pathType: Prefix + backend: + service: + name: erpnext-frontend + port: + number: 8080 + # Protected files + - path: /protected + pathType: Prefix + backend: + service: + name: erpnext-frontend + port: + number: 8080 + # WebSocket connections + - path: /socket.io + pathType: Prefix + backend: + service: + name: erpnext-backend + port: + number: 9000 + # API endpoints + - path: /api + pathType: Prefix + backend: + service: + name: erpnext-frontend + port: + number: 8080 + # Main application + - path: / + pathType: Prefix + backend: + service: + name: erpnext-frontend + port: + number: 8080 +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: admin@yourdomain.com + privateKeySecretRef: + name: letsencrypt-prod + solvers: + - http01: + ingress: + class: nginx +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-staging +spec: + acme: + server: https://acme-staging-v02.api.letsencrypt.org/directory + email: admin@yourdomain.com + privateKeySecretRef: + name: letsencrypt-staging + solvers: + - http01: + ingress: + class: nginx \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/jobs.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/jobs.yaml new file mode 100644 index 0000000..d3e32e2 --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/jobs.yaml @@ -0,0 +1,403 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: erpnext-create-site + namespace: erpnext + labels: + app: erpnext + component: setup + job-type: create-site +spec: + backoffLimit: 3 + template: + metadata: + labels: + app: erpnext + component: setup + job-type: create-site + spec: + serviceAccountName: erpnext-ksa + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + initContainers: + - name: wait-for-services + image: busybox:1.35 + command: + - sh + - -c + - | + echo 'Waiting for database and redis...' + until nc -z mariadb 3306 && nc -z redis 6379; do + echo 'Waiting for services...' + sleep 5 + done + echo 'Services are ready!' + + # Additional wait for database to be fully ready + sleep 30 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + containers: + - name: create-site + image: frappe/erpnext-worker:v14 + command: + - bash + - -c + - | + set -e + echo "Starting ERPNext site creation..." + + # Check if site already exists + if [ -d "/home/frappe/frappe-bench/sites/frontend" ]; then + echo "Site 'frontend' already exists. Skipping creation." + exit 0 + fi + + # Create the site + bench new-site frontend \ + --admin-password "$ADMIN_PASSWORD" \ + --mariadb-root-password "$DB_PASSWORD" \ + --install-app erpnext \ + --set-default + + # Set site configuration + bench --site frontend set-config developer_mode 0 + bench --site frontend set-config server_script_enabled 1 + bench --site frontend set-config allow_tests 0 + + # Install additional apps if needed + # bench --site frontend install-app custom_app + + echo "Site creation completed successfully!" + envFrom: + - configMapRef: + name: erpnext-config + env: + - name: ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: admin-password + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + volumeMounts: + - name: sites-data + mountPath: /home/frappe/frappe-bench/sites + - name: tmp + mountPath: /tmp + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "1000m" + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: tmp + emptyDir: {} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: erpnext-db-backup + namespace: erpnext + labels: + app: erpnext + component: backup + backup-type: database +spec: + schedule: "0 2 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + backoffLimit: 2 + template: + metadata: + labels: + app: erpnext + component: backup + backup-type: database + spec: + serviceAccountName: erpnext-ksa + restartPolicy: OnFailure + securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + containers: + - name: backup + image: mysql:8.0 + command: + - /bin/bash + - -c + - | + set -e + + BACKUP_DATE=$(date +%Y%m%d_%H%M%S) + BACKUP_FILE="erpnext_backup_${BACKUP_DATE}.sql" + + echo "Starting database backup: $BACKUP_FILE" + + # Create backup + mysqldump -h mariadb -u erpnext -p$DB_PASSWORD \ + --single-transaction \ + --routines \ + --triggers \ + --events \ + --default-character-set=utf8mb4 \ + erpnext > /backup/$BACKUP_FILE + + # Compress backup + gzip /backup/$BACKUP_FILE + + # Upload to Google Cloud Storage + if command -v gsutil &> /dev/null; then + gsutil cp /backup/$BACKUP_FILE.gz gs://erpnext-backups/database/ + echo "Backup uploaded to GCS: gs://erpnext-backups/database/$BACKUP_FILE.gz" + else + echo "gsutil not available, backup saved locally only" + fi + + # Clean up local backups older than 7 days + find /backup -name "*.gz" -mtime +7 -delete + + echo "Backup completed successfully!" + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + capabilities: + drop: + - ALL + volumeMounts: + - name: backup-storage + mountPath: /backup + - name: tmp + mountPath: /tmp + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: backup-storage + persistentVolumeClaim: + claimName: backup-pvc + - name: tmp + emptyDir: {} +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: erpnext-files-backup + namespace: erpnext + labels: + app: erpnext + component: backup + backup-type: files +spec: + schedule: "0 3 * * *" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + backoffLimit: 2 + template: + metadata: + labels: + app: erpnext + component: backup + backup-type: files + spec: + serviceAccountName: erpnext-ksa + restartPolicy: OnFailure + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + containers: + - name: files-backup + image: google/cloud-sdk:alpine + command: + - /bin/bash + - -c + - | + set -e + + BACKUP_DATE=$(date +%Y%m%d_%H%M%S) + BACKUP_FILE="sites_backup_${BACKUP_DATE}.tar.gz" + + echo "Starting files backup: $BACKUP_FILE" + + # Create compressed backup of sites + tar -czf /tmp/$BACKUP_FILE -C /sites . + + # Upload to Google Cloud Storage + if command -v gsutil &> /dev/null; then + gsutil cp /tmp/$BACKUP_FILE gs://erpnext-backups/sites/ + echo "Files backup uploaded to GCS: gs://erpnext-backups/sites/$BACKUP_FILE" + else + echo "gsutil not available, copying to backup volume" + cp /tmp/$BACKUP_FILE /backup/ + fi + + # Clean up + rm /tmp/$BACKUP_FILE + + echo "Files backup completed successfully!" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + volumeMounts: + - name: sites-data + mountPath: /sites + readOnly: true + - name: backup-storage + mountPath: /backup + - name: tmp + mountPath: /tmp + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: backup-storage + persistentVolumeClaim: + claimName: backup-pvc + - name: tmp + emptyDir: {} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: erpnext-migrate + namespace: erpnext + labels: + app: erpnext + component: maintenance + job-type: migrate +spec: + backoffLimit: 3 + template: + metadata: + labels: + app: erpnext + component: maintenance + job-type: migrate + spec: + serviceAccountName: erpnext-ksa + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + containers: + - name: migrate + image: frappe/erpnext-worker:v14 + command: + - bash + - -c + - | + set -e + echo "Starting ERPNext migration..." + + # Run database migrations + bench --site all migrate + + # Clear cache + bench --site all clear-cache + + # Rebuild search index if needed + # bench --site all rebuild-index + + echo "Migration completed successfully!" + envFrom: + - configMapRef: + name: erpnext-config + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + volumeMounts: + - name: sites-data + mountPath: /home/frappe/frappe-bench/sites + - name: tmp + mountPath: /tmp + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "1000m" + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: tmp + emptyDir: {} \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/mariadb.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/mariadb.yaml new file mode 100644 index 0000000..ea5e8b6 --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/mariadb.yaml @@ -0,0 +1,130 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mariadb + namespace: erpnext + labels: + app: mariadb + component: database + environment: production +spec: + replicas: 1 + selector: + matchLabels: + app: mariadb + template: + metadata: + labels: + app: mariadb + component: database + environment: production + spec: + securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + containers: + - name: mariadb + image: mariadb:10.6 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + - name: MYSQL_DATABASE + value: "erpnext" + - name: MYSQL_USER + value: "erpnext" + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + ports: + - containerPort: 3306 + name: mysql + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + capabilities: + drop: + - ALL + volumeMounts: + - name: mariadb-data + mountPath: /var/lib/mysql + - name: tmp + mountPath: /tmp + - name: run + mountPath: /var/run/mysqld + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "1000m" + livenessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + startupProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 30 + volumes: + - name: mariadb-data + persistentVolumeClaim: + claimName: mariadb-data-pvc + - name: tmp + emptyDir: {} + - name: run + emptyDir: {} + nodeSelector: + cloud.google.com/gke-preemptible: "false" +--- +apiVersion: v1 +kind: Service +metadata: + name: mariadb + namespace: erpnext + labels: + app: mariadb + component: database +spec: + selector: + app: mariadb + ports: + - port: 3306 + targetPort: 3306 + name: mysql + type: ClusterIP \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/namespace.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/namespace.yaml new file mode 100644 index 0000000..da9892b --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/namespace.yaml @@ -0,0 +1,42 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: erpnext + labels: + name: erpnext + environment: production + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/warn: restricted +--- +apiVersion: v1 +kind: ResourceQuota +metadata: + name: erpnext-quota + namespace: erpnext +spec: + hard: + requests.cpu: "10" + requests.memory: 20Gi + limits.cpu: "20" + limits.memory: 40Gi + persistentvolumeclaims: "10" + pods: "20" + services: "10" + secrets: "10" + configmaps: "10" +--- +apiVersion: v1 +kind: LimitRange +metadata: + name: erpnext-limits + namespace: erpnext +spec: + limits: + - default: + cpu: "500m" + memory: "1Gi" + defaultRequest: + cpu: "100m" + memory: "256Mi" + type: Container \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/redis.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/redis.yaml new file mode 100644 index 0000000..3342fe5 --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/redis.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: erpnext + labels: + app: redis + component: cache + environment: production +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + component: cache + environment: production + spec: + securityContext: + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + fsGroup: 999 + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + name: redis + command: + - redis-server + - --appendonly + - "yes" + - --maxmemory + - "256mb" + - --maxmemory-policy + - "allkeys-lru" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 999 + runAsGroup: 999 + capabilities: + drop: + - ALL + volumeMounts: + - name: redis-data + mountPath: /data + - name: tmp + mountPath: /tmp + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + exec: + command: + - redis-cli + - ping + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + volumes: + - name: redis-data + emptyDir: {} + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: erpnext + labels: + app: redis + component: cache +spec: + selector: + app: redis + ports: + - port: 6379 + targetPort: 6379 + name: redis + type: ClusterIP \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/kubernetes-manifests/storage.yaml b/documentation/deployment-guides/gcp/kubernetes-manifests/storage.yaml new file mode 100644 index 0000000..5e7ad05 --- /dev/null +++ b/documentation/deployment-guides/gcp/kubernetes-manifests/storage.yaml @@ -0,0 +1,85 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: ssd-retain +provisioner: kubernetes.io/gce-pd +parameters: + type: pd-ssd +reclaimPolicy: Retain +allowVolumeExpansion: true +volumeBindingMode: WaitForFirstConsumer +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: standard-retain +provisioner: kubernetes.io/gce-pd +parameters: + type: pd-standard +reclaimPolicy: Retain +allowVolumeExpansion: true +volumeBindingMode: WaitForFirstConsumer +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: erpnext-sites-pvc + namespace: erpnext + labels: + app: erpnext + component: sites +spec: + storageClassName: ssd-retain + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 50Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: erpnext-assets-pvc + namespace: erpnext + labels: + app: erpnext + component: assets +spec: + storageClassName: ssd-retain + accessModes: + - ReadWriteMany + resources: + requests: + storage: 20Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mariadb-data-pvc + namespace: erpnext + labels: + app: mariadb + component: data +spec: + storageClassName: ssd-retain + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: backup-pvc + namespace: erpnext + labels: + app: erpnext + component: backup +spec: + storageClassName: standard-retain + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Gi \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/scripts/backup-restore.sh b/documentation/deployment-guides/gcp/scripts/backup-restore.sh new file mode 100755 index 0000000..caa9bf0 --- /dev/null +++ b/documentation/deployment-guides/gcp/scripts/backup-restore.sh @@ -0,0 +1,581 @@ +#!/bin/bash + +# ERPNext Backup and Restore Script for GKE +# This script provides backup and restore functionality for ERPNext on GKE + +set -e + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +NAMESPACE=${NAMESPACE:-"erpnext"} +BACKUP_BUCKET=${BACKUP_BUCKET:-"erpnext-backups"} +PROJECT_ID=${PROJECT_ID:-$(gcloud config get-value project)} + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to check prerequisites +check_prerequisites() { + print_status "Checking prerequisites..." + + # Check if required tools are installed + local required_tools=("kubectl" "gcloud" "gsutil") + for tool in "${required_tools[@]}"; do + if ! command -v "$tool" &> /dev/null; then + print_error "$tool is not installed. Please install it first." + exit 1 + fi + done + + # Check if namespace exists + if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then + print_error "Namespace $NAMESPACE does not exist" + exit 1 + fi + + print_success "Prerequisites check passed" +} + +# Function to create manual backup +create_backup() { + local backup_type=${1:-"full"} + local timestamp=$(date +%Y%m%d_%H%M%S) + + print_status "Creating $backup_type backup at $timestamp" + + case $backup_type in + "database"|"db") + backup_database "$timestamp" + ;; + "files") + backup_files "$timestamp" + ;; + "full") + backup_database "$timestamp" + backup_files "$timestamp" + ;; + *) + print_error "Unknown backup type: $backup_type" + print_status "Available types: database, files, full" + exit 1 + ;; + esac + + print_success "Backup completed successfully" +} + +# Function to backup database +backup_database() { + local timestamp=$1 + local backup_name="manual_db_backup_$timestamp" + + print_status "Creating database backup: $backup_name" + + # Create temporary job for backup + kubectl apply -f - < /backup/\$BACKUP_FILE + + gzip /backup/\$BACKUP_FILE + + if command -v gsutil &> /dev/null; then + gsutil cp /backup/\$BACKUP_FILE.gz gs://$BACKUP_BUCKET/manual/database/ + echo "Backup uploaded to GCS" + fi + + echo "Database backup completed: \$BACKUP_FILE.gz" + env: + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: erpnext-secrets + key: db-password + volumeMounts: + - name: backup-storage + mountPath: /backup + volumes: + - name: backup-storage + persistentVolumeClaim: + claimName: backup-pvc +EOF + + # Wait for job to complete + kubectl wait --for=condition=complete job/$backup_name -n "$NAMESPACE" --timeout=600s + + # Check if job succeeded + if kubectl get job $backup_name -n "$NAMESPACE" -o jsonpath='{.status.conditions[?(@.type=="Complete")].status}' | grep -q "True"; then + print_success "Database backup completed: $backup_name" + else + print_error "Database backup failed. Check logs:" + kubectl logs job/$backup_name -n "$NAMESPACE" + exit 1 + fi + + # Cleanup job + kubectl delete job $backup_name -n "$NAMESPACE" +} + +# Function to backup files +backup_files() { + local timestamp=$1 + local backup_name="manual_files_backup_$timestamp" + + print_status "Creating files backup: $backup_name" + + # Create temporary job for backup + kubectl apply -f - < /dev/null; then + gsutil cp /tmp/\$BACKUP_FILE gs://$BACKUP_BUCKET/manual/sites/ + echo "Files backup uploaded to GCS" + else + cp /tmp/\$BACKUP_FILE /backup/ + fi + + rm /tmp/\$BACKUP_FILE + echo "Files backup completed: \$BACKUP_FILE" + volumeMounts: + - name: sites-data + mountPath: /sites + readOnly: true + - name: backup-storage + mountPath: /backup + volumes: + - name: sites-data + persistentVolumeClaim: + claimName: erpnext-sites-pvc + - name: backup-storage + persistentVolumeClaim: + claimName: backup-pvc +EOF + + # Wait for job to complete + kubectl wait --for=condition=complete job/$backup_name -n "$NAMESPACE" --timeout=600s + + # Check if job succeeded + if kubectl get job $backup_name -n "$NAMESPACE" -o jsonpath='{.status.conditions[?(@.type=="Complete")].status}' | grep -q "True"; then + print_success "Files backup completed: $backup_name" + else + print_error "Files backup failed. Check logs:" + kubectl logs job/$backup_name -n "$NAMESPACE" + exit 1 + fi + + # Cleanup job + kubectl delete job $backup_name -n "$NAMESPACE" +} + +# Function to list backups +list_backups() { + print_status "Listing available backups..." + + echo "" + echo "=== Database Backups ===" + gsutil ls gs://$BACKUP_BUCKET/database/ 2>/dev/null | tail -20 || echo "No database backups found" + + echo "" + echo "=== Files Backups ===" + gsutil ls gs://$BACKUP_BUCKET/sites/ 2>/dev/null | tail -20 || echo "No files backups found" + + echo "" + echo "=== Manual Backups ===" + gsutil ls gs://$BACKUP_BUCKET/manual/ 2>/dev/null | tail -20 || echo "No manual backups found" +} + +# Function to restore from backup +restore_backup() { + local backup_type=$1 + local backup_file=$2 + + if [[ -z "$backup_file" ]]; then + print_error "Please specify backup file to restore" + print_status "Usage: $0 restore [database|files] [backup_file]" + print_status "Use '$0 list' to see available backups" + exit 1 + fi + + print_warning "This will restore $backup_type from $backup_file" + print_warning "This operation will OVERWRITE existing data!" + print_warning "Are you sure you want to continue? (y/N)" + read -r response + if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then + print_status "Restore cancelled" + exit 0 + fi + + case $backup_type in + "database"|"db") + restore_database "$backup_file" + ;; + "files") + restore_files "$backup_file" + ;; + *) + print_error "Unknown restore type: $backup_type" + print_status "Available types: database, files" + exit 1 + ;; + esac +} + +# Function to restore database +restore_database() { + local backup_file=$1 + local timestamp=$(date +%Y%m%d_%H%M%S) + local restore_job="restore-db-$timestamp" + + print_status "Restoring database from: $backup_file" + + # Scale down ERPNext pods to prevent conflicts + print_status "Scaling down ERPNext pods..." + kubectl scale deployment erpnext-backend --replicas=0 -n "$NAMESPACE" + kubectl scale deployment erpnext-queue-default --replicas=0 -n "$NAMESPACE" + kubectl scale deployment erpnext-queue-long --replicas=0 -n "$NAMESPACE" + kubectl scale deployment erpnext-queue-short --replicas=0 -n "$NAMESPACE" + kubectl scale deployment erpnext-scheduler --replicas=0 -n "$NAMESPACE" + + # Wait for pods to be terminated + sleep 30 + + # Create restore job + kubectl apply -f - < /dev/null; then + gsutil mb gs://$BACKUP_BUCKET + print_success "Backup bucket created" + else + print_warning "Backup bucket already exists" + fi + + # Set lifecycle policy + gsutil lifecycle set - gs://$BACKUP_BUCKET </dev/null || echo "No backups found" +} + +# Function to show help +show_help() { + echo "ERPNext Backup and Restore Script" + echo "" + echo "Usage: $0 [COMMAND] [OPTIONS]" + echo "" + echo "Commands:" + echo " backup [type] - Create manual backup (type: database, files, full)" + echo " restore [type] [file] - Restore from backup" + echo " list - List available backups" + echo " status - Show backup system status" + echo " setup - Setup backup bucket and policies" + echo " help - Show this help" + echo "" + echo "Environment Variables:" + echo " NAMESPACE - Kubernetes namespace (default: erpnext)" + echo " BACKUP_BUCKET - GCS bucket for backups (default: erpnext-backups)" + echo " PROJECT_ID - GCP Project ID" + echo "" + echo "Examples:" + echo " $0 backup database # Backup database only" + echo " $0 backup files # Backup files only" + echo " $0 backup full # Full backup" + echo " $0 restore database gs://bucket/backup.sql.gz # Restore database" + echo " $0 restore files gs://bucket/backup.tar.gz # Restore files" +} + +# Main script logic +case "${1:-help}" in + "backup") + check_prerequisites + create_backup "${2:-full}" + ;; + "restore") + check_prerequisites + restore_backup "$2" "$3" + ;; + "list") + list_backups + ;; + "status") + show_status + ;; + "setup") + setup_backup_bucket + ;; + "help"|"-h"|"--help") + show_help + ;; + *) + print_error "Unknown command: $1" + show_help + exit 1 + ;; +esac \ No newline at end of file diff --git a/documentation/deployment-guides/gcp/scripts/deploy.sh b/documentation/deployment-guides/gcp/scripts/deploy.sh new file mode 100755 index 0000000..16fecd3 --- /dev/null +++ b/documentation/deployment-guides/gcp/scripts/deploy.sh @@ -0,0 +1,380 @@ +#!/bin/bash + +# ERPNext GKE Deployment Script +# This script automates the deployment of ERPNext on Google Kubernetes Engine + +set -e + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration +CLUSTER_NAME=${CLUSTER_NAME:-"erpnext-cluster"} +ZONE=${ZONE:-"us-central1-a"} +PROJECT_ID=${PROJECT_ID:-""} +DOMAIN=${DOMAIN:-"erpnext.yourdomain.com"} +EMAIL=${EMAIL:-"admin@yourdomain.com"} +NAMESPACE=${NAMESPACE:-"erpnext"} + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to check prerequisites +check_prerequisites() { + print_status "Checking prerequisites..." + + # Check if required tools are installed + local required_tools=("gcloud" "kubectl" "helm") + for tool in "${required_tools[@]}"; do + if ! command -v "$tool" &> /dev/null; then + print_error "$tool is not installed. Please install it first." + exit 1 + fi + done + + # Check if user is authenticated + if ! gcloud auth list --filter=status:ACTIVE --format="value(account)" | head -n 1 &> /dev/null; then + print_error "Not authenticated with gcloud. Please run 'gcloud auth login'" + exit 1 + fi + + # Check if project ID is set + if [[ -z "$PROJECT_ID" ]]; then + PROJECT_ID=$(gcloud config get-value project) + if [[ -z "$PROJECT_ID" ]]; then + print_error "PROJECT_ID not set. Please set it or configure gcloud project." + exit 1 + fi + fi + + print_success "Prerequisites check passed" +} + +# Function to create GKE cluster +create_cluster() { + print_status "Creating GKE cluster: $CLUSTER_NAME" + + # Check if cluster already exists + if gcloud container clusters describe "$CLUSTER_NAME" --zone="$ZONE" &> /dev/null; then + print_warning "Cluster $CLUSTER_NAME already exists" + return 0 + fi + + gcloud container clusters create "$CLUSTER_NAME" \ + --zone="$ZONE" \ + --num-nodes=3 \ + --node-locations="$ZONE" \ + --machine-type=e2-standard-4 \ + --disk-type=pd-ssd \ + --disk-size=50GB \ + --enable-autoscaling \ + --min-nodes=2 \ + --max-nodes=10 \ + --enable-autorepair \ + --enable-autoupgrade \ + --enable-network-policy \ + --enable-ip-alias \ + --enable-cloud-logging \ + --enable-cloud-monitoring \ + --workload-pool="$PROJECT_ID.svc.id.goog" \ + --enable-shielded-nodes + + print_success "Cluster created successfully" +} + +# Function to configure kubectl +configure_kubectl() { + print_status "Configuring kubectl..." + + gcloud container clusters get-credentials "$CLUSTER_NAME" --zone="$ZONE" + + # Verify connection + if kubectl cluster-info &> /dev/null; then + print_success "kubectl configured successfully" + else + print_error "Failed to configure kubectl" + exit 1 + fi +} + +# Function to install nginx ingress controller +install_nginx_ingress() { + print_status "Installing NGINX Ingress Controller..." + + kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml + + # Wait for ingress controller to be ready + kubectl wait --namespace ingress-nginx \ + --for=condition=ready pod \ + --selector=app.kubernetes.io/component=controller \ + --timeout=300s + + print_success "NGINX Ingress Controller installed" +} + +# Function to install cert-manager +install_cert_manager() { + print_status "Installing cert-manager..." + + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.yaml + + # Wait for cert-manager to be ready + kubectl wait --for=condition=available --timeout=300s deployment/cert-manager -n cert-manager + kubectl wait --for=condition=available --timeout=300s deployment/cert-manager-webhook -n cert-manager + + print_success "cert-manager installed" +} + +# Function to create namespace and basic resources +create_namespace() { + print_status "Creating namespace and basic resources..." + + kubectl apply -f ../kubernetes-manifests/namespace.yaml + + print_success "Namespace created" +} + +# Function to create secrets +create_secrets() { + print_status "Creating secrets..." + + # Generate random passwords if not provided + local admin_password=${ADMIN_PASSWORD:-$(openssl rand -base64 32)} + local db_password=${DB_PASSWORD:-$(openssl rand -base64 32)} + local api_key=${API_KEY:-$(openssl rand -hex 32)} + local api_secret=${API_SECRET:-$(openssl rand -hex 32)} + + # Create secrets + kubectl create secret generic erpnext-secrets \ + --namespace="$NAMESPACE" \ + --from-literal=admin-password="$admin_password" \ + --from-literal=db-password="$db_password" \ + --from-literal=api-key="$api_key" \ + --from-literal=api-secret="$api_secret" \ + --dry-run=client -o yaml | kubectl apply -f - + + print_success "Secrets created" + print_warning "Admin password: $admin_password" + print_warning "Please save these credentials securely!" +} + +# Function to update configmap with domain +update_configmap() { + print_status "Updating ConfigMap with domain configuration..." + + # Copy configmap template and update domain + cp ../kubernetes-manifests/configmap.yaml /tmp/configmap-updated.yaml + sed -i "s/erpnext.yourdomain.com/$DOMAIN/g" /tmp/configmap-updated.yaml + + kubectl apply -f /tmp/configmap-updated.yaml + rm /tmp/configmap-updated.yaml + + print_success "ConfigMap updated" +} + +# Function to deploy storage +deploy_storage() { + print_status "Deploying storage resources..." + + kubectl apply -f ../kubernetes-manifests/storage.yaml + + print_success "Storage resources deployed" +} + +# Function to deploy database and redis +deploy_infrastructure() { + print_status "Deploying infrastructure components..." + + kubectl apply -f ../kubernetes-manifests/redis.yaml + kubectl apply -f ../kubernetes-manifests/mariadb.yaml + + # Wait for database to be ready + print_status "Waiting for database to be ready..." + kubectl wait --for=condition=available deployment/mariadb -n "$NAMESPACE" --timeout=300s + kubectl wait --for=condition=available deployment/redis -n "$NAMESPACE" --timeout=300s + + print_success "Infrastructure components deployed" +} + +# Function to deploy ERPNext application +deploy_application() { + print_status "Deploying ERPNext application..." + + kubectl apply -f ../kubernetes-manifests/erpnext-backend.yaml + kubectl apply -f ../kubernetes-manifests/erpnext-frontend.yaml + kubectl apply -f ../kubernetes-manifests/erpnext-workers.yaml + + # Wait for backend to be ready + print_status "Waiting for ERPNext backend to be ready..." + kubectl wait --for=condition=available deployment/erpnext-backend -n "$NAMESPACE" --timeout=600s + + print_success "ERPNext application deployed" +} + +# Function to create ERPNext site +create_site() { + print_status "Creating ERPNext site..." + + # Apply site creation job + kubectl apply -f ../kubernetes-manifests/jobs.yaml + + # Wait for job to complete + print_status "Waiting for site creation to complete..." + kubectl wait --for=condition=complete job/erpnext-create-site -n "$NAMESPACE" --timeout=600s + + if kubectl get job erpnext-create-site -n "$NAMESPACE" -o jsonpath='{.status.conditions[?(@.type=="Complete")].status}' | grep -q "True"; then + print_success "ERPNext site created successfully" + else + print_error "Site creation failed. Check job logs:" + kubectl logs job/erpnext-create-site -n "$NAMESPACE" + exit 1 + fi +} + +# Function to deploy ingress +deploy_ingress() { + print_status "Deploying ingress..." + + # Update ingress with correct domain and email + cp ../kubernetes-manifests/ingress.yaml /tmp/ingress-updated.yaml + sed -i "s/erpnext.yourdomain.com/$DOMAIN/g" /tmp/ingress-updated.yaml + sed -i "s/admin@yourdomain.com/$EMAIL/g" /tmp/ingress-updated.yaml + + kubectl apply -f /tmp/ingress-updated.yaml + rm /tmp/ingress-updated.yaml + + print_success "Ingress deployed" +} + +# Function to get deployment status +get_status() { + print_status "Getting deployment status..." + + echo "" + echo "=== Cluster Information ===" + kubectl cluster-info + + echo "" + echo "=== Namespace Resources ===" + kubectl get all -n "$NAMESPACE" + + echo "" + echo "=== Ingress Information ===" + kubectl get ingress -n "$NAMESPACE" + + echo "" + echo "=== Certificate Status ===" + kubectl get certificate -n "$NAMESPACE" 2>/dev/null || echo "No certificates found" + + echo "" + echo "=== External IP ===" + kubectl get service -n ingress-nginx ingress-nginx-controller +} + +# Function to cleanup deployment +cleanup() { + print_warning "This will delete the entire ERPNext deployment. Are you sure? (y/N)" + read -r response + if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then + print_status "Cleaning up deployment..." + + kubectl delete namespace "$NAMESPACE" --ignore-not-found=true + + print_status "Deleting cluster..." + gcloud container clusters delete "$CLUSTER_NAME" --zone="$ZONE" --quiet + + print_success "Cleanup completed" + else + print_status "Cleanup cancelled" + fi +} + +# Function to show help +show_help() { + echo "ERPNext GKE Deployment Script" + echo "" + echo "Usage: $0 [COMMAND]" + echo "" + echo "Commands:" + echo " deploy - Full deployment (default)" + echo " status - Show deployment status" + echo " cleanup - Delete deployment" + echo " help - Show this help" + echo "" + echo "Environment Variables:" + echo " PROJECT_ID - GCP Project ID" + echo " CLUSTER_NAME - GKE cluster name (default: erpnext-cluster)" + echo " ZONE - GCP zone (default: us-central1-a)" + echo " DOMAIN - Domain name (default: erpnext.yourdomain.com)" + echo " EMAIL - Email for Let's Encrypt (default: admin@yourdomain.com)" + echo " NAMESPACE - Kubernetes namespace (default: erpnext)" + echo "" + echo "Example:" + echo " PROJECT_ID=my-project DOMAIN=erp.mycompany.com $0 deploy" +} + +# Main deployment function +main_deploy() { + print_status "Starting ERPNext GKE deployment..." + + check_prerequisites + create_cluster + configure_kubectl + install_nginx_ingress + install_cert_manager + create_namespace + create_secrets + update_configmap + deploy_storage + deploy_infrastructure + deploy_application + create_site + deploy_ingress + + print_success "Deployment completed successfully!" + echo "" + print_status "Access your ERPNext instance at: https://$DOMAIN" + print_status "Default credentials: Administrator / [check secrets]" + echo "" + print_warning "It may take a few minutes for the SSL certificate to be issued." + print_warning "Monitor certificate status with: kubectl get certificate -n $NAMESPACE" +} + +# Main script logic +case "${1:-deploy}" in + "deploy") + main_deploy + ;; + "status") + get_status + ;; + "cleanup") + cleanup + ;; + "help"|"-h"|"--help") + show_help + ;; + *) + print_error "Unknown command: $1" + show_help + exit 1 + ;; +esac \ No newline at end of file