⏺ Perfect! I've created a comprehensive set of deployment documentation for ERPNext using Google

Cloud managed services (Cloud SQL for MySQL and Memorystore for Redis). Here's what I've
  delivered:

  📚 Complete Documentation Suite

  Core Documentation

  1. 00-prerequisites-managed.md - Prerequisites with VPC, managed services setup
  2. 01-gke-managed-deployment.md - GKE deployment using Cloud SQL & Memorystore
  3. 02-cloud-run-deployment.md - Serverless Cloud Run deployment guide
  4. 03-production-managed-setup.md - Enterprise production hardening
  5. README.md - Comprehensive overview with decision matrix

  Production-Ready Kubernetes Manifests

  - namespace.yaml - Optimized resource quotas for managed services
  - storage.yaml - Reduced storage needs (no database volumes)
  - configmap.yaml - Configuration for managed DB connections
  - secrets.yaml - External Secrets Operator integration
  - erpnext-backend.yaml - Backend with Cloud SQL Proxy sidecars
  - erpnext-frontend.yaml - Nginx frontend optimized for managed services
  - erpnext-workers.yaml - Queue workers with managed DB connections
  - ingress.yaml - Enhanced ingress with security and performance optimizations
  - jobs.yaml - Site creation and backup jobs for managed environment

  Automation Scripts

  - deploy-managed.sh - Complete GKE deployment automation
  - cloud-run-deploy.sh - Serverless Cloud Run deployment automation

  🎯 Key Features & Benefits

  Managed Services Architecture

  - Cloud SQL: Fully managed MySQL with 99.95% SLA
  - Memorystore: Managed Redis with automatic scaling
  - Cloud Storage: File storage with lifecycle management
  - VPC Integration: Secure private connectivity
  - Cloud SQL Proxy: Secure database connections

  Two Deployment Options

  Option 1: GKE + Managed Services

  - Full Kubernetes control with managed databases
  - Auto-scaling with HPA and VPA
  - Production-grade security and monitoring
  - Estimated cost: -/month

  Option 2: Cloud Run + Managed Services

  - Serverless with zero operational overhead
  - Automatic scaling from 0 to 1000+ instances
  - Pay-per-use pricing model
  - Estimated cost: -/month

  Enterprise Features

  - Workload Identity for secure GCP API access
  - External Secrets Operator for credential management
  - Network Policies for micro-segmentation
  - Binary Authorization for container security
  - Multi-region backup with automated DR testing
  - SLO/SLI monitoring with custom dashboards

  Cost Optimization

  - Reduced infrastructure footprint (no self-hosted DB)
  - Automatic storage scaling without over-provisioning
  - Right-sizing recommendations based on usage
  - Committed use discounts for predictable workloads

  Operational Benefits

  - Zero database administration - Google manages it all
  - Automatic security patches and updates
  - Point-in-time recovery for disaster scenarios
  - Performance insights with query optimization
  - Automated backups with cross-region replication

  🚀 Quick Start Commands

  For GKE Deployment:

  cd documentation/deployment-guides/gcp-managed/scripts/
  export PROJECT_ID=your-gcp-project
  export DOMAIN=erpnext.yourdomain.com
  ./deploy-managed.sh deploy

  For Cloud Run Deployment:

  cd documentation/deployment-guides/gcp-managed/scripts/
  export PROJECT_ID=your-gcp-project
  export DOMAIN=erpnext.yourdomain.com
  ./cloud-run-deploy.sh deploy

  📊 Architecture Comparison

  The managed services approach provides:
  - Better reliability (99.95% SLA vs self-managed)
  - Lower operational overhead (zero DB admin)
  - Enhanced security (Google's security team manages DB)
  - Cost predictability (no over-provisioning)
  - Faster scaling (automatic storage scaling)
  - Better disaster recovery (built-in backup and replication)

  This documentation provides organizations with enterprise-ready deployment options that
  leverage Google Cloud's managed services for maximum reliability and minimum operational
  overhead while maintaining full ERPNext functionality.
This commit is contained in:
Brian Tan Seng 2025-08-22 18:36:41 +08:00
parent 294cbdca9d
commit 696ce0670c
16 changed files with 8667 additions and 0 deletions

View File

@ -0,0 +1,438 @@
# Google Cloud Prerequisites for ERPNext with Managed Services
## Overview
This guide covers the prerequisites and initial setup required for deploying ERPNext on Google Cloud Platform (GCP) using managed database services: Cloud SQL for MySQL and Memorystore for Redis.
## 🔧 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) - For GKE Option
```bash
# Install kubectl
gcloud components install kubectl
# Verify installation
kubectl version --client
```
### 3. Docker (for local testing and Cloud Run)
```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 GKE 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 for managed services deployment
gcloud services enable \
container.googleapis.com \
compute.googleapis.com \
sqladmin.googleapis.com \
redis.googleapis.com \
secretmanager.googleapis.com \
cloudbuild.googleapis.com \
cloudresourcemanager.googleapis.com \
monitoring.googleapis.com \
logging.googleapis.com \
run.googleapis.com \
vpcaccess.googleapis.com \
servicenetworking.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-managed \
--display-name="ERPNext Managed Services Account" \
--description="Service account for ERPNext with managed database services"
# Grant necessary roles for managed services
gcloud projects add-iam-policy-binding erpnext-production \
--member="serviceAccount:erpnext-managed@erpnext-production.iam.gserviceaccount.com" \
--role="roles/cloudsql.client"
gcloud projects add-iam-policy-binding erpnext-production \
--member="serviceAccount:erpnext-managed@erpnext-production.iam.gserviceaccount.com" \
--role="roles/redis.editor"
gcloud projects add-iam-policy-binding erpnext-production \
--member="serviceAccount:erpnext-managed@erpnext-production.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
# For GKE deployment
gcloud projects add-iam-policy-binding erpnext-production \
--member="serviceAccount:erpnext-managed@erpnext-production.iam.gserviceaccount.com" \
--role="roles/container.developer"
# For Cloud Run deployment
gcloud projects add-iam-policy-binding erpnext-production \
--member="serviceAccount:erpnext-managed@erpnext-production.iam.gserviceaccount.com" \
--role="roles/run.developer"
```
### 2. Create Service Account Key (for local development)
```bash
# Generate service account key
gcloud iam service-accounts keys create ~/erpnext-managed-key.json \
--iam-account=erpnext-managed@erpnext-production.iam.gserviceaccount.com
# Set environment variable
export GOOGLE_APPLICATION_CREDENTIALS=~/erpnext-managed-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")
# Additional secret for database connection
gcloud secrets create erpnext-db-connection-name \
--data-file=<(echo -n "erpnext-production:us-central1:erpnext-db")
```
## 🌐 Networking Setup
### 1. VPC Network for Private Services
```bash
# Create custom VPC network for managed services
gcloud compute networks create erpnext-vpc \
--subnet-mode=custom
# Create subnet for compute resources
gcloud compute networks subnets create erpnext-subnet \
--network=erpnext-vpc \
--range=10.0.0.0/24 \
--region=us-central1
# Create subnet for private services (Cloud SQL, Memorystore)
gcloud compute networks subnets create erpnext-private-subnet \
--network=erpnext-vpc \
--range=10.1.0.0/24 \
--region=us-central1
# Allocate IP range for private services
gcloud compute addresses create erpnext-private-ip-range \
--global \
--purpose=VPC_PEERING \
--prefix-length=16 \
--network=erpnext-vpc
# Create private connection for managed services
gcloud services vpc-peerings connect \
--service=servicenetworking.googleapis.com \
--ranges=erpnext-private-ip-range \
--network=erpnext-vpc
```
### 2. VPC Access Connector (for Cloud Run)
```bash
# Create VPC access connector for Cloud Run to access private services
gcloud compute networks vpc-access connectors create erpnext-connector \
--network=erpnext-vpc \
--region=us-central1 \
--range=10.2.0.0/28 \
--min-instances=2 \
--max-instances=10
```
### 3. Firewall Rules
```bash
# Create firewall rules
gcloud compute firewall-rules create erpnext-allow-internal \
--network=erpnext-vpc \
--allow=tcp,udp,icmp \
--source-ranges=10.0.0.0/16
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
# Allow Cloud SQL connections
gcloud compute firewall-rules create erpnext-allow-mysql \
--network=erpnext-vpc \
--allow=tcp:3306 \
--source-ranges=10.0.0.0/16,10.1.0.0/16,10.2.0.0/28
# Allow Redis connections
gcloud compute firewall-rules create erpnext-allow-redis \
--network=erpnext-vpc \
--allow=tcp:6379 \
--source-ranges=10.0.0.0/16,10.1.0.0/16,10.2.0.0/28
```
## 💾 Managed Database Services Setup
### 1. Cloud SQL (MySQL) Instance
```bash
# Create Cloud SQL instance
gcloud sql instances create erpnext-db \
--database-version=MYSQL_8_0 \
--tier=db-n1-standard-2 \
--region=us-central1 \
--network=erpnext-vpc \
--no-assign-ip \
--storage-size=100GB \
--storage-type=SSD \
--storage-auto-increase \
--backup \
--backup-start-time=02:00 \
--maintenance-window-day=SUN \
--maintenance-window-hour=3 \
--maintenance-release-channel=production \
--deletion-protection
# Create database
gcloud sql databases create erpnext --instance=erpnext-db
# Create database user
gcloud sql users create erpnext \
--instance=erpnext-db \
--password=YourDBPassword123!
# Get connection name for applications
gcloud sql instances describe erpnext-db --format="value(connectionName)"
```
### 2. Memorystore (Redis) Instance
```bash
# Create Memorystore Redis instance
gcloud redis instances create erpnext-redis \
--size=1 \
--region=us-central1 \
--network=erpnext-vpc \
--redis-version=redis_6_x \
--maintenance-window-day=sunday \
--maintenance-window-hour=3 \
--redis-config maxmemory-policy=allkeys-lru
# Get Redis host IP for applications
gcloud redis instances describe erpnext-redis --region=us-central1 --format="value(host)"
```
### 3. Database Initialization
```bash
# Create initialization script
cat > /tmp/init_erpnext_db.sql <<EOF
-- Create ERPNext database with proper charset
CREATE DATABASE IF NOT EXISTS erpnext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Grant privileges to erpnext user
GRANT ALL PRIVILEGES ON erpnext.* TO 'erpnext'@'%';
FLUSH PRIVILEGES;
-- Set MySQL configurations for ERPNext
SET GLOBAL innodb_file_format = Barracuda;
SET GLOBAL innodb_large_prefix = 1;
SET GLOBAL innodb_file_per_table = 1;
SET GLOBAL character_set_server = utf8mb4;
SET GLOBAL collation_server = utf8mb4_unicode_ci;
EOF
# Apply initialization script
gcloud sql connect erpnext-db --user=root < /tmp/init_erpnext_db.sql
```
## 📊 Monitoring and Logging
### 1. Enable Enhanced Monitoring
```bash
# Enable enhanced monitoring for Cloud SQL
gcloud sql instances patch erpnext-db \
--insights-config-query-insights-enabled \
--insights-config-record-application-tags \
--insights-config-record-client-address
# Monitoring is enabled by default for Memorystore
# Verify monitoring is working
gcloud logging logs list --limit=5
```
### 2. Create Log-based Metrics
```bash
# Create custom log metric for ERPNext errors
gcloud logging metrics create erpnext_errors \
--description="ERPNext application errors" \
--log-filter='resource.type="cloud_run_revision" OR resource.type="k8s_container" AND severity="ERROR"'
# Create metric for Cloud SQL slow queries
gcloud logging metrics create erpnext_slow_queries \
--description="ERPNext slow database queries" \
--log-filter='resource.type="cloudsql_database" AND protoPayload.methodName="cloudsql.instances.query" AND protoPayload.request.query_time > 1'
```
## 🔍 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|redis|run)"
# Check service account exists
gcloud iam service-accounts list | grep erpnext-managed
# Verify secrets are created
gcloud secrets list | grep erpnext
# Check VPC network
gcloud compute networks list | grep erpnext-vpc
# Verify Cloud SQL instance
gcloud sql instances list | grep erpnext-db
# Check Memorystore instance
gcloud redis instances list --region=us-central1 | grep erpnext-redis
# Test VPC connector (for Cloud Run)
gcloud compute networks vpc-access connectors list --region=us-central1 | grep erpnext-connector
# Check private service connection
gcloud services vpc-peerings list --network=erpnext-vpc
```
## 💡 Cost Optimization for Managed Services
### 1. Cloud SQL Optimization
```bash
# Use appropriate machine types
# Development: db-f1-micro or db-g1-small
# Production: db-n1-standard-2 or higher
# Enable automatic storage increases
gcloud sql instances patch erpnext-db \
--storage-auto-increase
# Use regional persistent disks for HA
gcloud sql instances patch erpnext-db \
--availability-type=REGIONAL # Only for production
```
### 2. Memorystore Optimization
```bash
# Right-size Redis instance
# Start with 1GB and scale based on usage
# Monitor memory utilization and adjust
# Use basic tier for non-HA workloads
# Use standard tier only for production HA requirements
```
### 3. Network Cost Optimization
```bash
# Use same region for all services to minimize egress costs
# Monitor VPC connector usage for Cloud Run
# Consider shared VPC for multiple projects
```
## 🚨 Security Best Practices for Managed Services
### 1. Network Security
- **Private IP only**: All managed services use private IPs
- **VPC peering**: Secure communication within VPC
- **No public access**: Database and Redis not accessible from internet
### 2. Access Control
```bash
# Use IAM database authentication (Cloud SQL)
gcloud sql instances patch erpnext-db \
--database-flags=cloudsql_iam_authentication=on
# Create IAM database user
gcloud sql users create erpnext-iam-user \
--instance=erpnext-db \
--type=cloud_iam_service_account \
--iam-account=erpnext-managed@erpnext-production.iam.gserviceaccount.com
```
### 3. Encryption
- **Encryption at rest**: Enabled by default for both services
- **Encryption in transit**: SSL/TLS enforced
- **Customer-managed keys**: Optional for additional security
## 📚 Additional Resources
- [Cloud SQL Documentation](https://cloud.google.com/sql/docs)
- [Memorystore Documentation](https://cloud.google.com/memorystore/docs)
- [VPC Documentation](https://cloud.google.com/vpc/docs)
- [Cloud Run Documentation](https://cloud.google.com/run/docs)
- [GKE Documentation](https://cloud.google.com/kubernetes-engine/docs)
## ➡️ Next Steps
After completing prerequisites:
1. **GKE with Managed Services**: Follow `01-gke-managed-deployment.md`
2. **Cloud Run Deployment**: Follow `02-cloud-run-deployment.md`
3. **Production Hardening**: See `03-production-managed-setup.md`
---
**⚠️ Important Notes**:
- Managed services incur continuous costs even when not in use
- Plan your backup and disaster recovery strategy
- Monitor costs regularly using Cloud Billing
- Keep track of all resources created for billing purposes

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,966 @@
# ERPNext Cloud Run Deployment with Managed Services
## Overview
This guide provides step-by-step instructions for deploying ERPNext on Google Cloud Run using Cloud SQL for MySQL and Memorystore for Redis. This serverless approach offers automatic scaling, pay-per-use pricing, and minimal operational overhead.
## 🏗️ Architecture Overview
### Cloud Run ERPNext Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Load Balancer │────│ Cloud Run │────│ Cloud SQL │
│ (Global) │ │ (Frontend) │ │ (MySQL) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
┌──────────────────┐ ┌─────────────────┐
│ Cloud Run │────│ Memorystore │
│ (Backend API) │ │ (Redis) │
└──────────────────┘ └─────────────────┘
┌──────────────────┐ ┌─────────────────┐
│ Cloud Tasks │ │ Cloud Storage │
│ (Background) │ │ (Files) │
└──────────────────┘ └─────────────────┘
```
### Key Components
- **Frontend Service**: Nginx serving static assets
- **Backend API Service**: ERPNext application server
- **Background Tasks Service**: Queue processing via Cloud Tasks
- **Scheduled Tasks**: Cloud Scheduler for cron jobs
- **Database**: Cloud SQL (MySQL)
- **Cache**: Memorystore (Redis)
- **File Storage**: Cloud Storage
## 📋 Prerequisites
Ensure you have completed the setup in `00-prerequisites-managed.md`, including:
- VPC network with VPC Access Connector
- Cloud SQL instance
- Memorystore Redis instance
- Service accounts and IAM roles
## 🔧 Service Configuration
### 1. Prepare Environment Variables
```bash
# Set project variables
export PROJECT_ID="erpnext-production"
export REGION="us-central1"
export VPC_CONNECTOR="erpnext-connector"
# Get database connection details
export DB_CONNECTION_NAME=$(gcloud sql instances describe erpnext-db --format="value(connectionName)")
export REDIS_HOST=$(gcloud redis instances describe erpnext-redis --region=$REGION --format="value(host)")
# Domain configuration
export DOMAIN="erpnext.yourdomain.com"
export API_DOMAIN="api.yourdomain.com"
export FILES_DOMAIN="files.yourdomain.com"
```
### 2. Create Cloud Storage Bucket for Files
```bash
# Create bucket for ERPNext files
gsutil mb gs://erpnext-files-$PROJECT_ID
# Set lifecycle policy for file management
gsutil lifecycle set - gs://erpnext-files-$PROJECT_ID <<EOF
{
"lifecycle": {
"rule": [
{
"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"},
"condition": {"age": 30}
},
{
"action": {"type": "SetStorageClass", "storageClass": "COLDLINE"},
"condition": {"age": 90}
}
]
}
}
EOF
# Set CORS for web access
gsutil cors set - gs://erpnext-files-$PROJECT_ID <<EOF
[
{
"origin": ["https://$DOMAIN", "https://$API_DOMAIN"],
"method": ["GET", "PUT", "POST"],
"responseHeader": ["Content-Type"],
"maxAgeSeconds": 3600
}
]
EOF
```
## 🐳 Custom ERPNext Images for Cloud Run
### 1. Create Dockerfile for Backend Service
```bash
# Create directory for Cloud Run builds
mkdir -p cloud-run-builds/backend
cd cloud-run-builds/backend
# Create Dockerfile
cat > Dockerfile <<EOF
FROM frappe/erpnext-worker:v14
# Install Cloud SQL Proxy
RUN apt-get update && apt-get install -y wget && \
wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy && \
chmod +x cloud_sql_proxy && \
mv cloud_sql_proxy /usr/local/bin/
# Install Python packages for Cloud Storage
RUN pip install google-cloud-storage google-cloud-secret-manager
# Create startup script
COPY startup.sh /startup.sh
RUN chmod +x /startup.sh
# Copy custom configurations
COPY custom_configs/ /home/frappe/frappe-bench/
# Set Cloud Run specific configurations
ENV PORT=8080
ENV WORKERS=4
ENV TIMEOUT=120
EXPOSE 8080
CMD ["/startup.sh"]
EOF
# Create startup script
cat > startup.sh <<'EOF'
#!/bin/bash
set -e
# Start Cloud SQL Proxy in background
if [ -n "$DB_CONNECTION_NAME" ]; then
echo "Starting Cloud SQL Proxy..."
/usr/local/bin/cloud_sql_proxy -instances=$DB_CONNECTION_NAME=tcp:3306 &
# Wait for proxy to be ready
until nc -z localhost 3306; do
echo "Waiting for database connection..."
sleep 2
done
echo "Database connection established"
fi
# Set database connection
export DB_HOST="127.0.0.1"
export DB_PORT="3306"
# Initialize site if needed
if [ ! -d "/home/frappe/frappe-bench/sites/frontend" ]; then
echo "Initializing ERPNext site..."
cd /home/frappe/frappe-bench
bench new-site frontend \
--admin-password "$ADMIN_PASSWORD" \
--mariadb-root-password "$DB_PASSWORD" \
--install-app erpnext \
--set-default
fi
# Start ERPNext
echo "Starting ERPNext backend..."
cd /home/frappe/frappe-bench
exec gunicorn -b 0.0.0.0:$PORT -w $WORKERS -t $TIMEOUT frappe.app:application
EOF
# Build and push image
gcloud builds submit --tag gcr.io/$PROJECT_ID/erpnext-backend:latest
```
### 2. Create Dockerfile for Frontend Service
```bash
# Create directory for frontend build
mkdir -p ../frontend
cd ../frontend
cat > Dockerfile <<EOF
FROM frappe/erpnext-nginx:v14
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Set Cloud Run port
ENV PORT=8080
# Create startup script for Cloud Run
COPY startup.sh /startup.sh
RUN chmod +x /startup.sh
EXPOSE 8080
CMD ["/startup.sh"]
EOF
# Create Cloud Run optimized nginx config
cat > nginx.conf <<'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
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 API
upstream backend {
server api.yourdomain.com:443;
keepalive 32;
}
server {
listen 8080;
server_name _;
# Security headers
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
# Serve static assets from Cloud Storage
location /assets {
proxy_pass https://storage.googleapis.com/erpnext-files-PROJECT_ID/assets;
proxy_set_header Host storage.googleapis.com;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy API requests to backend service
location /api {
proxy_pass https://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Serve app from backend
location / {
proxy_pass https://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
EOF
# Create startup script
cat > startup.sh <<'EOF'
#!/bin/bash
# Replace PROJECT_ID placeholder in nginx config
sed -i "s/PROJECT_ID/$PROJECT_ID/g" /etc/nginx/nginx.conf
# Update backend upstream with actual API domain
sed -i "s/api.yourdomain.com/$API_DOMAIN/g" /etc/nginx/nginx.conf
# Start nginx
exec nginx -g 'daemon off;'
EOF
# Build and push frontend image
gcloud builds submit --tag gcr.io/$PROJECT_ID/erpnext-frontend:latest
```
### 3. Background Tasks Service
```bash
# Create directory for background tasks
mkdir -p ../background
cd ../background
cat > Dockerfile <<EOF
FROM frappe/erpnext-worker:v14
# Install Cloud SQL Proxy and Cloud Tasks
RUN apt-get update && apt-get install -y wget && \
wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy && \
chmod +x cloud_sql_proxy && \
mv cloud_sql_proxy /usr/local/bin/
RUN pip install google-cloud-tasks google-cloud-storage
# Copy task processor script
COPY task_processor.py /task_processor.py
COPY startup.sh /startup.sh
RUN chmod +x /startup.sh
ENV PORT=8080
EXPOSE 8080
CMD ["/startup.sh"]
EOF
# Create task processor
cat > task_processor.py <<'EOF'
import os
import json
import logging
from flask import Flask, request, jsonify
import frappe
from frappe.utils.background_jobs import execute_job
app = Flask(__name__)
@app.route('/process-task', methods=['POST'])
def process_task():
"""Process background task from Cloud Tasks"""
try:
# Get task data
task_data = request.get_json()
# Set site context
frappe.init(site='frontend')
frappe.connect()
# Execute the job
job_name = task_data.get('job_name')
kwargs = task_data.get('kwargs', {})
result = execute_job(job_name, **kwargs)
frappe.db.commit()
frappe.destroy()
return jsonify({'status': 'success', 'result': result})
except Exception as e:
logging.error(f"Task processing failed: {str(e)}")
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({'status': 'healthy'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))
EOF
# Create startup script
cat > startup.sh <<'EOF'
#!/bin/bash
set -e
# Start Cloud SQL Proxy
if [ -n "$DB_CONNECTION_NAME" ]; then
/usr/local/bin/cloud_sql_proxy -instances=$DB_CONNECTION_NAME=tcp:3306 &
until nc -z localhost 3306; do
echo "Waiting for database connection..."
sleep 2
done
fi
export DB_HOST="127.0.0.1"
export DB_PORT="3306"
# Start task processor
cd /home/frappe/frappe-bench
exec python /task_processor.py
EOF
# Build and push background service image
gcloud builds submit --tag gcr.io/$PROJECT_ID/erpnext-background:latest
```
## 🚀 Deploy Cloud Run Services
### 1. Deploy Backend API Service
```bash
# Deploy backend service
gcloud run deploy erpnext-backend \
--image gcr.io/$PROJECT_ID/erpnext-backend:latest \
--platform managed \
--region $REGION \
--allow-unauthenticated \
--vpc-connector $VPC_CONNECTOR \
--set-env-vars="DB_CONNECTION_NAME=$DB_CONNECTION_NAME" \
--set-env-vars="REDIS_HOST=$REDIS_HOST" \
--set-env-vars="REDIS_PORT=6379" \
--set-env-vars="PROJECT_ID=$PROJECT_ID" \
--set-secrets="ADMIN_PASSWORD=erpnext-admin-password:latest" \
--set-secrets="DB_PASSWORD=erpnext-db-password:latest" \
--set-secrets="API_KEY=erpnext-api-key:latest" \
--set-secrets="API_SECRET=erpnext-api-secret:latest" \
--service-account erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com \
--memory 2Gi \
--cpu 2 \
--concurrency 80 \
--timeout 300 \
--min-instances 1 \
--max-instances 100
# Get backend service URL
export BACKEND_URL=$(gcloud run services describe erpnext-backend --region=$REGION --format="value(status.url)")
echo "Backend URL: $BACKEND_URL"
```
### 2. Deploy Frontend Service
```bash
# Update API_DOMAIN environment variable
export API_DOMAIN=$(echo $BACKEND_URL | sed 's|https://||')
# Deploy frontend service
gcloud run deploy erpnext-frontend \
--image gcr.io/$PROJECT_ID/erpnext-frontend:latest \
--platform managed \
--region $REGION \
--allow-unauthenticated \
--set-env-vars="PROJECT_ID=$PROJECT_ID" \
--set-env-vars="API_DOMAIN=$API_DOMAIN" \
--memory 512Mi \
--cpu 1 \
--concurrency 1000 \
--timeout 60 \
--min-instances 0 \
--max-instances 10
# Get frontend service URL
export FRONTEND_URL=$(gcloud run services describe erpnext-frontend --region=$REGION --format="value(status.url)")
echo "Frontend URL: $FRONTEND_URL"
```
### 3. Deploy Background Tasks Service
```bash
# Deploy background service
gcloud run deploy erpnext-background \
--image gcr.io/$PROJECT_ID/erpnext-background:latest \
--platform managed \
--region $REGION \
--no-allow-unauthenticated \
--vpc-connector $VPC_CONNECTOR \
--set-env-vars="DB_CONNECTION_NAME=$DB_CONNECTION_NAME" \
--set-env-vars="REDIS_HOST=$REDIS_HOST" \
--set-env-vars="PROJECT_ID=$PROJECT_ID" \
--set-secrets="DB_PASSWORD=erpnext-db-password:latest" \
--service-account erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com \
--memory 1Gi \
--cpu 1 \
--concurrency 10 \
--timeout 900 \
--min-instances 0 \
--max-instances 10
# Get background service URL
export BACKGROUND_URL=$(gcloud run services describe erpnext-background --region=$REGION --format="value(status.url)")
echo "Background URL: $BACKGROUND_URL"
```
## 📅 Setup Cloud Scheduler for Scheduled Tasks
### 1. Create Cloud Tasks Queue
```bash
# Create task queue for background jobs
gcloud tasks queues create erpnext-tasks \
--location=$REGION \
--max-dispatches-per-second=100 \
--max-concurrent-dispatches=1000 \
--max-attempts=3
```
### 2. Create Scheduled Jobs
```bash
# Daily backup job
gcloud scheduler jobs create http erpnext-daily-backup \
--location=$REGION \
--schedule="0 2 * * *" \
--uri="$BACKGROUND_URL/process-task" \
--http-method=POST \
--headers="Content-Type=application/json" \
--message-body='{"job_name": "frappe.utils.backup.daily_backup", "kwargs": {}}' \
--oidc-service-account-email=erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com
# ERPNext scheduler (every 5 minutes)
gcloud scheduler jobs create http erpnext-scheduler \
--location=$REGION \
--schedule="*/5 * * * *" \
--uri="$BACKGROUND_URL/process-task" \
--http-method=POST \
--headers="Content-Type=application/json" \
--message-body='{"job_name": "frappe.utils.scheduler.execute_all", "kwargs": {}}' \
--oidc-service-account-email=erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com
# Email queue processor (every minute)
gcloud scheduler jobs create http erpnext-email-queue \
--location=$REGION \
--schedule="* * * * *" \
--uri="$BACKGROUND_URL/process-task" \
--http-method=POST \
--headers="Content-Type=application/json" \
--message-body='{"job_name": "frappe.email.queue.flush", "kwargs": {}}' \
--oidc-service-account-email=erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com
```
## 🌐 Custom Domain and Load Balancer Setup
### 1. Create Global Load Balancer
```bash
# Create HTTP health check
gcloud compute health-checks create http erpnext-health-check \
--request-path="/health" \
--port=8080
# Create backend service for frontend
gcloud compute backend-services create erpnext-frontend-backend \
--protocol=HTTP \
--health-checks=erpnext-health-check \
--global
# Add Cloud Run NEG for frontend
gcloud compute network-endpoint-groups create erpnext-frontend-neg \
--region=$REGION \
--network-endpoint-type=serverless \
--cloud-run-service=erpnext-frontend
gcloud compute backend-services add-backend erpnext-frontend-backend \
--global \
--network-endpoint-group=erpnext-frontend-neg \
--network-endpoint-group-region=$REGION
# Create backend service for API
gcloud compute backend-services create erpnext-api-backend \
--protocol=HTTP \
--health-checks=erpnext-health-check \
--global
# Add Cloud Run NEG for backend API
gcloud compute network-endpoint-groups create erpnext-backend-neg \
--region=$REGION \
--network-endpoint-type=serverless \
--cloud-run-service=erpnext-backend
gcloud compute backend-services add-backend erpnext-api-backend \
--global \
--network-endpoint-group=erpnext-backend-neg \
--network-endpoint-group-region=$REGION
```
### 2. Create URL Map and SSL Certificate
```bash
# Create URL map
gcloud compute url-maps create erpnext-url-map \
--default-service=erpnext-frontend-backend
# Add path matcher for API routes
gcloud compute url-maps add-path-matcher erpnext-url-map \
--path-matcher-name=api-matcher \
--default-service=erpnext-frontend-backend \
--path-rules="/api/*=erpnext-api-backend,/method/*=erpnext-api-backend"
# Create managed SSL certificate
gcloud compute ssl-certificates create erpnext-ssl-cert \
--domains=$DOMAIN
# Create HTTPS proxy
gcloud compute target-https-proxies create erpnext-https-proxy \
--ssl-certificates=erpnext-ssl-cert \
--url-map=erpnext-url-map
# Create global forwarding rule
gcloud compute forwarding-rules create erpnext-https-rule \
--global \
--target-https-proxy=erpnext-https-proxy \
--ports=443
# Get global IP address
export GLOBAL_IP=$(gcloud compute forwarding-rules describe erpnext-https-rule --global --format="value(IPAddress)")
echo "Global IP: $GLOBAL_IP"
echo "Point your domain $DOMAIN to this IP address"
```
## 🔍 Initialize ERPNext Site
### 1. Run Site Creation
```bash
# Trigger site creation via background service
curl -X POST "$BACKGROUND_URL/process-task" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-d '{
"job_name": "frappe.installer.install_app",
"kwargs": {
"site": "frontend",
"app": "erpnext"
}
}'
```
### 2. Alternative: Manual Site Creation
```bash
# Create a temporary Cloud Run job for site creation
gcloud run jobs create erpnext-init-site \
--image gcr.io/$PROJECT_ID/erpnext-backend:latest \
--region $REGION \
--vpc-connector $VPC_CONNECTOR \
--set-env-vars="DB_CONNECTION_NAME=$DB_CONNECTION_NAME" \
--set-secrets="ADMIN_PASSWORD=erpnext-admin-password:latest" \
--set-secrets="DB_PASSWORD=erpnext-db-password:latest" \
--service-account erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com \
--memory 2Gi \
--cpu 2 \
--max-retries 3 \
--parallelism 1 \
--task-count 1 \
--args="bench,new-site,frontend,--admin-password,\$ADMIN_PASSWORD,--mariadb-root-password,\$DB_PASSWORD,--install-app,erpnext,--set-default"
# Execute the job
gcloud run jobs execute erpnext-init-site --region $REGION --wait
```
## 📊 Monitoring and Observability
### 1. Cloud Run Metrics
```bash
# Enable detailed monitoring
gcloud services enable cloudmonitoring.googleapis.com
# Create custom dashboard
cat > monitoring-dashboard.json <<EOF
{
"displayName": "ERPNext Cloud Run Dashboard",
"mosaicLayout": {
"tiles": [
{
"width": 6,
"height": 4,
"widget": {
"title": "Request Count",
"xyChart": {
"dataSets": [
{
"timeSeriesQuery": {
"timeSeriesFilter": {
"filter": "resource.type=\"cloud_run_revision\" AND resource.labels.service_name=\"erpnext-backend\"",
"aggregation": {
"alignmentPeriod": "60s",
"perSeriesAligner": "ALIGN_RATE"
}
}
}
}
]
}
}
}
]
}
}
EOF
gcloud monitoring dashboards create --config-from-file=monitoring-dashboard.json
```
### 2. Alerting Policies
```bash
# Create alerting policy for high error rate
cat > alert-policy.json <<EOF
{
"displayName": "ERPNext High Error Rate",
"conditions": [
{
"displayName": "Cloud Run Error Rate",
"conditionThreshold": {
"filter": "resource.type=\"cloud_run_revision\" AND resource.labels.service_name=\"erpnext-backend\"",
"comparison": "COMPARISON_GREATER_THAN",
"thresholdValue": 0.1,
"duration": "300s",
"aggregations": [
{
"alignmentPeriod": "60s",
"perSeriesAligner": "ALIGN_RATE"
}
]
}
}
],
"enabled": true
}
EOF
gcloud alpha monitoring policies create --policy-from-file=alert-policy.json
```
## 🔒 Security Configuration
### 1. Identity-Aware Proxy (Optional)
```bash
# Enable IAP for additional security
gcloud compute backend-services update erpnext-frontend-backend \
--global \
--iap=enabled
# Configure OAuth consent screen and IAP settings through Console
echo "Configure IAP through Cloud Console:"
echo "https://console.cloud.google.com/security/iap"
```
### 2. Cloud Armor Security Policies
```bash
# Create security policy
gcloud compute security-policies create erpnext-security-policy \
--description="ERPNext Cloud Armor policy"
# Add rate limiting rule
gcloud compute security-policies rules create 1000 \
--security-policy=erpnext-security-policy \
--expression="true" \
--action=rate-based-ban \
--rate-limit-threshold-count=100 \
--rate-limit-threshold-interval-sec=60 \
--ban-duration-sec=600 \
--conform-action=allow \
--exceed-action=deny-429 \
--enforce-on-key=IP
# Apply to backend service
gcloud compute backend-services update erpnext-frontend-backend \
--global \
--security-policy=erpnext-security-policy
```
## 🗄️ Backup and Disaster Recovery
### 1. Automated Backups
```bash
# Cloud SQL backups are automatic
# Verify backup configuration
gcloud sql instances describe erpnext-db --format="value(settings.backupConfiguration)"
# Create additional backup job for site files
gcloud scheduler jobs create http erpnext-files-backup \
--location=$REGION \
--schedule="0 4 * * *" \
--uri="$BACKGROUND_URL/process-task" \
--http-method=POST \
--headers="Content-Type=application/json" \
--message-body='{"job_name": "custom.backup.backup_files_to_storage", "kwargs": {}}' \
--oidc-service-account-email=erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com
```
### 2. Disaster Recovery Procedures
```bash
# Create disaster recovery script
cat > disaster_recovery.md <<EOF
# ERPNext Cloud Run Disaster Recovery
## Database Recovery
1. Restore from Cloud SQL backup:
\`gcloud sql backups restore BACKUP_ID --restore-instance=erpnext-db-new\`
2. Update connection string in Cloud Run services
## Service Recovery
1. Redeploy services:
\`gcloud run deploy erpnext-backend --image gcr.io/$PROJECT_ID/erpnext-backend:latest\`
2. Verify health checks
## File Recovery
1. Restore from Cloud Storage:
\`gsutil -m cp -r gs://erpnext-backups/files/ gs://erpnext-files-$PROJECT_ID/\`
EOF
```
## 💰 Cost Optimization
### 1. Service Configuration for Cost Optimization
```bash
# Optimize backend service for cost
gcloud run services update erpnext-backend \
--region=$REGION \
--min-instances=0 \
--max-instances=10 \
--concurrency=100
# Optimize frontend service
gcloud run services update erpnext-frontend \
--region=$REGION \
--min-instances=0 \
--max-instances=5 \
--concurrency=1000
# Set up budget alerts
gcloud billing budgets create \
--billing-account=BILLING_ACCOUNT_ID \
--display-name="ERPNext Cloud Run Budget" \
--budget-amount=500USD \
--threshold-rules-percent=50,90 \
--threshold-rules-spend-basis=current-spend
```
### 2. Performance Optimization
```bash
# Enable request tracing
gcloud run services update erpnext-backend \
--region=$REGION \
--add-cloudsql-instances=$DB_CONNECTION_NAME
# Configure connection pooling
gcloud run services update erpnext-backend \
--region=$REGION \
--set-env-vars="DB_MAX_CONNECTIONS=20,DB_POOL_SIZE=10"
```
## 🧪 Testing and Verification
### 1. Service Health Checks
```bash
# Test backend service
curl -f "$BACKEND_URL/api/method/ping"
# Test frontend service
curl -f "$FRONTEND_URL/health"
# Test background service
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-f "$BACKGROUND_URL/health"
```
### 2. Load Testing
```bash
# Simple load test
for i in {1..100}; do
curl -s "$FRONTEND_URL" > /dev/null &
done
wait
echo "Load test completed. Check Cloud Run metrics in console."
```
## 🚨 Troubleshooting
### 1. Common Issues
```bash
# Check service logs
gcloud logging read "resource.type=\"cloud_run_revision\" AND resource.labels.service_name=\"erpnext-backend\"" --limit=50
# Check database connectivity
gcloud run services logs read erpnext-backend --region=$REGION --limit=20
# Test VPC connector
gcloud compute networks vpc-access connectors describe $VPC_CONNECTOR --region=$REGION
```
### 2. Performance Issues
```bash
# Check instance allocation
gcloud run services describe erpnext-backend --region=$REGION --format="value(status.traffic[].allocation)"
# Monitor cold starts
gcloud logging read "resource.type=\"cloud_run_revision\" AND textPayload:\"Container called exit\"" --limit=10
```
## 📈 Performance Benefits
### 1. Scalability
- **Auto-scaling**: 0 to 1000+ instances automatically
- **Global distribution**: Multi-region deployment capability
- **Pay-per-use**: No idle costs
### 2. Reliability
- **Managed infrastructure**: Google handles all infrastructure
- **Automatic deployments**: Zero-downtime deployments
- **Health checks**: Automatic instance replacement
### 3. Cost Efficiency
- **No minimum costs**: Pay only for actual usage
- **Automatic optimization**: CPU and memory auto-scaling
- **Reduced operational overhead**: No server management
## 📚 Additional Resources
- [Cloud Run Documentation](https://cloud.google.com/run/docs)
- [Cloud Tasks Documentation](https://cloud.google.com/tasks/docs)
- [Cloud Scheduler Documentation](https://cloud.google.com/scheduler/docs)
- [Cloud Run Best Practices](https://cloud.google.com/run/docs/best-practices)
## ➡️ Next Steps
1. **Production Hardening**: Follow `03-production-managed-setup.md`
2. **Monitoring Setup**: Configure detailed monitoring and alerting
3. **Performance Tuning**: Optimize based on usage patterns
4. **Custom Development**: Add custom ERPNext apps and configurations
---
**⚠️ Important Notes**:
- Cloud Run bills per 100ms of CPU time and memory allocation
- Cold starts may affect initial response times
- Database connections should be managed carefully due to Cloud SQL limits
- Consider using Cloud CDN for better global performance

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,334 @@
# ERPNext Google Cloud Deployment with Managed Services
## Overview
This directory contains comprehensive guides and resources for deploying ERPNext on Google Cloud Platform (GCP) using **managed database services**: Cloud SQL for MySQL and Memorystore for Redis. This approach provides better reliability, security, and operational efficiency compared to self-hosted databases.
## 🏗️ Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ Google Cloud Platform │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Cloud Run │ │ GKE │ │
│ │ (Serverless) │ │ (Kubernetes) │ │
│ │ │ │ │ │
│ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │
│ │ │ Frontend │ │ │ │ Pods │ │ │
│ │ │ Backend │ │ │ │ - Frontend │ │ │
│ │ │ Workers │ │ │ │ - Backend │ │ │
│ │ └─────────────┘ │ │ │ - Workers │ │ │
│ └─────────────────┘ │ └─────────────┘ │ │
│ └─────────────────┘ │
│ │ │
│ ┌─────────────────────────────┼─────────────────────────────┐ │
│ │ Managed Services │ │ │
│ │ │ │ │
│ │ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ │ │
│ │ │ Cloud SQL │ │ Memorystore │ │Cloud Storage │ │ │
│ │ │ (MySQL) │ │ (Redis) │ │ (Files) │ │ │
│ │ └──────────────┘ └─────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## 📁 Directory Structure
```
gcp-managed/
├── README.md # This file
├── 00-prerequisites-managed.md # Prerequisites for managed services
├── 01-gke-managed-deployment.md # GKE with managed databases
├── 02-cloud-run-deployment.md # Cloud Run serverless deployment
├── 03-production-managed-setup.md # Production hardening
├── kubernetes-manifests/ # K8s manifests for managed services
│ ├── namespace.yaml # Namespace with reduced quotas
│ ├── storage.yaml # Only application file storage
│ ├── configmap.yaml # Config for managed services
│ ├── secrets.yaml # External Secrets integration
│ ├── erpnext-backend.yaml # Backend with Cloud SQL proxy
│ ├── erpnext-frontend.yaml # Optimized frontend
│ ├── erpnext-workers.yaml # Workers with managed DB
│ ├── ingress.yaml # Enhanced ingress config
│ └── jobs.yaml # Site creation and backup jobs
└── scripts/ # Automation scripts
├── deploy-managed.sh # GKE deployment script
└── cloud-run-deploy.sh # Cloud Run deployment script
```
## 🚀 Quick Start
### Option 1: GKE with Managed Services (Recommended for Production)
```bash
# 1. Complete prerequisites
cd gcp-managed/
# Follow 00-prerequisites-managed.md
# 2. Deploy to GKE
cd scripts/
export PROJECT_ID="your-gcp-project"
export DOMAIN="erpnext.yourdomain.com"
export EMAIL="admin@yourdomain.com"
./deploy-managed.sh deploy
```
### Option 2: Cloud Run Serverless Deployment
```bash
# 1. Complete prerequisites
cd gcp-managed/
# Follow 00-prerequisites-managed.md
# 2. Deploy to Cloud Run
cd scripts/
export PROJECT_ID="your-gcp-project"
export DOMAIN="erpnext.yourdomain.com"
./cloud-run-deploy.sh deploy
```
## 🎯 Key Benefits of Managed Services
### 🛡️ Enhanced Reliability
- **99.95% SLA** for Cloud SQL and Memorystore
- **Automatic failover** and disaster recovery
- **Point-in-time recovery** for databases
- **Automated backups** with cross-region replication
### 🔧 Operational Efficiency
- **Zero database administration** overhead
- **Automatic security patches** and updates
- **Performance insights** and optimization recommendations
- **Built-in monitoring** and alerting
### 🔒 Enterprise Security
- **Private IP connectivity** within VPC
- **Encryption at rest and in transit** by default
- **IAM integration** for access control
- **Audit logging** for compliance
### 💰 Cost Optimization
- **Pay-as-you-scale** pricing model
- **Automatic storage scaling** without downtime
- **Right-sizing recommendations** based on usage
- **No over-provisioning** of database resources
## 📊 Deployment Options Comparison
| Feature | GKE + Managed DB | Cloud Run + Managed DB | Self-Hosted DB |
|---------|------------------|------------------------|-----------------|
| **Scalability** | Manual/Auto HPA | Automatic (0-1000+) | Manual |
| **Operational Overhead** | Medium | Very Low | High |
| **Database Reliability** | 99.95% SLA | 99.95% SLA | Depends on setup |
| **Cost (Small)** | ~$450/month | ~$200/month | ~$300/month |
| **Cost (Large)** | ~$800/month | ~$400/month | ~$600/month |
| **Cold Start** | None | 1-3 seconds | None |
| **Customization** | High | Medium | Very High |
| **Multi-tenancy** | Supported | Limited | Supported |
## 🛠️ Managed Services Configuration
### Cloud SQL (MySQL)
- **Instance Types**: db-n1-standard-2 to db-n1-standard-96
- **Storage**: 10GB to 64TB, automatic scaling
- **Backup**: Automated daily backups with 7-day retention
- **High Availability**: Regional persistent disks with automatic failover
- **Security**: Private IP, SSL/TLS encryption, IAM database authentication
### Memorystore (Redis)
- **Tiers**: Basic (1-5GB) or Standard (1-300GB) with HA
- **Features**: Persistence, AUTH, in-transit encryption
- **Performance**: Up to 12 Gbps network throughput
- **Monitoring**: Built-in metrics and alerting
### Additional Services
- **Cloud Storage**: File uploads and static assets
- **Secret Manager**: Secure credential management
- **VPC Access Connector**: Secure serverless-to-VPC communication
- **Cloud Tasks**: Background job processing (Cloud Run)
- **Cloud Scheduler**: Cron jobs and scheduled tasks
## 🔧 Advanced Features
### Auto-scaling Configuration
- **GKE**: Horizontal Pod Autoscaler based on CPU/memory
- **Cloud Run**: Automatic scaling from 0 to 1000+ instances
- **Database**: Automatic storage scaling, manual compute scaling
- **Redis**: Manual scaling with zero-downtime
### Security Hardening
- **Network isolation** with private subnets
- **Workload Identity** for secure GCP API access
- **External Secrets Operator** for credential management
- **Network policies** for pod-to-pod communication
- **Binary Authorization** for container security
### Monitoring & Observability
- **Stackdriver integration** for logs and metrics
- **Custom dashboards** for ERPNext-specific metrics
- **SLO/SLI monitoring** with alerting
- **Distributed tracing** with Cloud Trace
- **Error reporting** with automatic grouping
### Backup & Disaster Recovery
- **Cloud SQL**: Automated backups with point-in-time recovery
- **Application files**: Automated backup to Cloud Storage
- **Cross-region replication** for disaster recovery
- **Automated DR testing** with validation
## 💰 Cost Estimation & Optimization
### Typical Monthly Costs (US-Central1)
#### Small Deployment (< 50 users)
```
Cloud SQL (db-n1-standard-1): $50
Memorystore Redis (1GB): $37
Cloud Run (avg 2 instances): $60
Cloud Storage (50GB): $1
Load Balancer: $18
Total: ~$166/month
```
#### Medium Deployment (50-200 users)
```
Cloud SQL (db-n1-standard-2): $278
Memorystore Redis (5GB): $185
GKE (3 e2-standard-4 nodes): $420
Cloud Storage (200GB): $4
Load Balancer: $18
Total: ~$905/month
```
#### Large Deployment (200+ users)
```
Cloud SQL (db-n1-standard-4): $556
Memorystore Redis (10GB): $370
GKE (6 e2-standard-4 nodes): $840
Cloud Storage (500GB): $10
Load Balancer: $18
Total: ~$1,794/month
```
### Cost Optimization Strategies
1. **Use committed use discounts** (up to 57% savings)
2. **Right-size instances** based on monitoring data
3. **Use preemptible nodes** for non-critical workloads
4. **Implement storage lifecycle policies** for Cloud Storage
5. **Scale down during off-hours** with automation
## 🚨 Migration Path from Self-Hosted
### Phase 1: Assessment (Week 1)
- [ ] Audit current database size and performance
- [ ] Identify custom configurations and extensions
- [ ] Plan migration windows and rollback procedures
- [ ] Set up managed services in parallel
### Phase 2: Data Migration (Week 2)
- [ ] Export data from existing MySQL/Redis
- [ ] Import to Cloud SQL/Memorystore
- [ ] Validate data integrity and performance
- [ ] Update connection strings and test
### Phase 3: Application Migration (Week 3)
- [ ] Deploy ERPNext with managed services
- [ ] Migrate file storage to Cloud Storage
- [ ] Update backup procedures
- [ ] Conduct full testing
### Phase 4: Cutover and Optimization (Week 4)
- [ ] DNS cutover to new deployment
- [ ] Monitor performance and costs
- [ ] Optimize resource allocation
- [ ] Decommission old infrastructure
## 🔍 Troubleshooting Common Issues
### Cloud SQL Connection Issues
```bash
# Test connectivity from GKE
kubectl run mysql-test --rm -i --tty --image=mysql:8.0 -- mysql -h PRIVATE_IP -u erpnext -p
# Check Cloud SQL Proxy logs
kubectl logs deployment/erpnext-backend -c cloud-sql-proxy
```
### Redis Connection Issues
```bash
# Test Redis connectivity
kubectl run redis-test --rm -i --tty --image=redis:alpine -- redis-cli -h REDIS_IP ping
# Check AUTH configuration
gcloud redis instances describe erpnext-redis --region=us-central1
```
### Performance Issues
```bash
# Check database performance
gcloud sql operations list --instance=erpnext-db
# Monitor Redis memory usage
gcloud redis instances describe erpnext-redis --region=us-central1 --format="value(memorySizeGb,redisMemoryUsage)"
```
## 📚 Additional Resources
### Google Cloud Documentation
- [Cloud SQL Best Practices](https://cloud.google.com/sql/docs/mysql/best-practices)
- [Memorystore Best Practices](https://cloud.google.com/memorystore/docs/redis/memory-management-best-practices)
- [Cloud Run Best Practices](https://cloud.google.com/run/docs/best-practices)
- [GKE Networking Best Practices](https://cloud.google.com/kubernetes-engine/docs/best-practices/networking)
### ERPNext Specific
- [ERPNext Database Configuration](https://docs.erpnext.com/docs/user/manual/en/setting-up/database-setup)
- [Performance Optimization](https://docs.erpnext.com/docs/user/manual/en/setting-up/performance)
- [Backup Strategies](https://docs.erpnext.com/docs/user/manual/en/setting-up/backup)
### Monitoring & Operations
- [SRE Best Practices](https://sre.google/books/)
- [Prometheus Monitoring](https://prometheus.io/docs/practices/naming/)
- [Grafana Dashboards](https://grafana.com/grafana/dashboards/)
## 🎯 Decision Matrix
### Choose GKE + Managed Services if:
- ✅ Need full control over application deployment
- ✅ Require complex networking or multi-tenancy
- ✅ Have existing Kubernetes expertise
- ✅ Need consistent performance with no cold starts
- ✅ Plan to run multiple applications in same cluster
### Choose Cloud Run + Managed Services if:
- ✅ Want minimal operational overhead
- ✅ Have variable or unpredictable traffic
- ✅ Need rapid scaling capabilities
- ✅ Want to minimize costs for smaller deployments
- ✅ Prefer serverless architecture
## 📞 Support & Contributing
### Getting Help
- **Documentation Issues**: Create issues in the repository
- **Deployment Support**: Follow troubleshooting guides
- **Performance Issues**: Check monitoring dashboards
- **Cost Optimization**: Use GCP billing reports and recommendations
### Contributing
- **Documentation improvements**: Submit pull requests
- **Script enhancements**: Share automation improvements
- **Best practices**: Contribute lessons learned
- **Cost optimizations**: Share optimization strategies
---
**⚠️ Important Notes**:
- Managed services incur continuous costs even when not in use
- Always test deployments in staging before production
- Monitor costs and usage regularly
- Keep credentials secure and rotate regularly
- Follow GCP security best practices
**🎯 Recommendation**: For most production deployments, GKE with managed services provides the best balance of control, reliability, and operational efficiency.

View File

@ -0,0 +1,279 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: erpnext-managed-config
namespace: erpnext
labels:
app: erpnext
component: config
deployment-type: managed-services
data:
# ERPNext Application Configuration
APP_VERSION: "v14"
APP_URL: "erpnext.yourdomain.com"
APP_USER: "Administrator"
APP_DB_PARAM: "db"
DEVELOPER_MODE: "0"
ENABLE_SCHEDULER: "1"
SOCKETIO_PORT: "9000"
# Cloud SQL Configuration (via Cloud SQL Proxy)
DB_HOST: "127.0.0.1"
DB_PORT: "3306"
DB_NAME: "erpnext"
DB_USER: "erpnext"
DB_TIMEOUT: "60"
DB_CHARSET: "utf8mb4"
# Memorystore Redis Configuration
# Note: REDIS_HOST will be set by deployment script
REDIS_PORT: "6379"
REDIS_CACHE_URL: "redis://REDIS_HOST:6379/0"
REDIS_QUEUE_URL: "redis://REDIS_HOST:6379/1"
REDIS_SOCKETIO_URL: "redis://REDIS_HOST:6379/2"
REDIS_TIMEOUT: "5"
REDIS_MAX_CONNECTIONS: "20"
# Performance settings optimized for managed services
WORKER_PROCESSES: "4"
WORKER_TIMEOUT: "120"
WORKER_MAX_REQUESTS: "1000"
WORKER_MAX_REQUESTS_JITTER: "50"
# Logging settings for Cloud Logging
LOG_LEVEL: "INFO"
LOG_FORMAT: "json"
ENABLE_CLOUD_LOGGING: "1"
# Cloud SQL Proxy settings
CLOUDSQL_CONNECTION_NAME: "PROJECT_ID:REGION:erpnext-db"
ENABLE_CLOUDSQL_PROXY: "1"
# Cloud Storage settings for file uploads
USE_CLOUD_STORAGE: "1"
CLOUD_STORAGE_BUCKET: "erpnext-files-PROJECT_ID"
# Security settings
SECURE_HEADERS: "1"
CSRF_PROTECTION: "1"
SESSION_SECURE: "1"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-managed-config
namespace: erpnext
labels:
app: erpnext
component: nginx
deployment-type: managed-services
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;
# Enhanced logging for managed services
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
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;
# Enhanced gzip configuration
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;
# Connection pooling for backend
upstream backend {
server erpnext-backend:8000;
keepalive 32;
keepalive_requests 100;
keepalive_timeout 60s;
}
upstream socketio {
server erpnext-backend:9000;
keepalive 16;
}
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
listen 8080;
server_name _;
root /home/frappe/frappe-bench/sites;
# Enhanced security headers for managed services
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
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:; frame-ancestors 'self';" always;
# Static assets with enhanced caching
location /assets {
try_files $uri =404;
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
# Serve from Cloud Storage for managed deployments
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass https://storage.googleapis.com/erpnext-files-PROJECT_ID/assets$uri;
proxy_set_header Host storage.googleapis.com;
proxy_cache_valid 200 1y;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# Protected files
location ~ ^/protected/(.*) {
internal;
try_files /frontend/$1 =404;
}
# WebSocket connections with enhanced handling
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_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Enhanced timeouts for WebSocket
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_connect_timeout 10s;
proxy_pass http://socketio;
}
# API endpoints with rate limiting
location /api/ {
limit_req zone=api burst=20 nodelay;
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_set_header X-Real-IP $remote_addr;
# Enhanced timeouts for API calls
proxy_read_timeout 120s;
proxy_send_timeout 120s;
proxy_connect_timeout 10s;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://backend;
}
# Login endpoint with stricter rate limiting
location /api/method/login {
limit_req zone=login burst=3 nodelay;
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-Real-IP $remote_addr;
proxy_pass http://backend;
}
# Main application routes
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_set_header X-Real-IP $remote_addr;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
proxy_connect_timeout 10s;
proxy_redirect off;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_pass http://backend;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Metrics endpoint for monitoring
location /metrics {
access_log off;
proxy_pass http://backend/api/method/frappe.utils.response.get_response_length;
allow 10.0.0.0/8; # VPC internal only
deny all;
}
# Block access to sensitive files
location ~* \.(conf|htaccess|htpasswd|ini|log|sh|sql|tar|gz|bak|backup)$ {
deny all;
return 404;
}
# Block access to hidden files
location ~ /\. {
deny all;
return 404;
}
}
}

View File

@ -0,0 +1,374 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: erpnext-backend
namespace: erpnext
labels:
app: erpnext-backend
component: backend
environment: production
version: v14
deployment-type: managed-services
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
deployment-type: managed-services
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/api/method/frappe.utils.response.get_response_length"
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-cloudsql
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4-alpine
command:
- sh
- -c
- |
echo 'Starting Cloud SQL Proxy for health check...'
/cloud_sql_proxy -instances=$DB_CONNECTION_NAME=tcp:3306 &
PROXY_PID=$!
# Wait for proxy to be ready
until nc -z localhost 3306; do
echo 'Waiting for Cloud SQL connection...'
sleep 5
done
# Test database connection
echo 'Testing database connection...'
timeout 10 sh -c 'until nc -z localhost 3306; do sleep 1; done'
echo 'Cloud SQL connection verified'
kill $PROXY_PID
env:
- name: DB_CONNECTION_NAME
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-connection-name
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 2
capabilities:
drop:
- ALL
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
containers:
- name: erpnext-backend
image: frappe/erpnext-worker:v14
envFrom:
- configMapRef:
name: erpnext-managed-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-password
- name: REDIS_AUTH
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: redis-auth
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/google/key.json
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
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "1.5Gi"
cpu: "750m"
limits:
memory: "3Gi"
cpu: "1500m"
livenessProbe:
httpGet:
path: /api/method/ping
port: 8000
initialDelaySeconds: 90
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: 60
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 30
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4-alpine
command:
- /cloud_sql_proxy
- -instances=$(DB_CONNECTION_NAME)=tcp:127.0.0.1:3306
- -credential_file=/var/secrets/google/key.json
- -enable_iam_login
env:
- name: DB_CONNECTION_NAME
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-connection-name
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 2
capabilities:
drop:
- ALL
volumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: assets-data
persistentVolumeClaim:
claimName: erpnext-assets-pvc
- name: tmp
emptyDir: {}
- name: logs
emptyDir: {}
- name: google-cloud-key
secret:
secretName: gcp-service-account-key
---
apiVersion: v1
kind: Service
metadata:
name: erpnext-backend
namespace: erpnext
labels:
app: erpnext-backend
component: backend
deployment-type: managed-services
annotations:
cloud.google.com/neg: '{"ingress": true}'
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
spec:
selector:
app: erpnext-backend
ports:
- name: http
port: 8000
targetPort: 8000
protocol: TCP
- name: socketio
port: 9000
targetPort: 9000
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: erpnext-backend-hpa
namespace: erpnext
labels:
app: erpnext-backend
component: backend
deployment-type: managed-services
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: erpnext-backend
minReplicas: 3
maxReplicas: 15
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
- type: Pods
value: 2
periodSeconds: 60
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: erpnext-backend-pdb
namespace: erpnext
labels:
app: erpnext-backend
component: backend
deployment-type: managed-services
spec:
minAvailable: 2
selector:
matchLabels:
app: erpnext-backend
---
# Network Policy for backend service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: erpnext-backend-netpol
namespace: erpnext
spec:
podSelector:
matchLabels:
app: erpnext-backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: erpnext-frontend
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8000
- protocol: TCP
port: 9000
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow access to managed services (Cloud SQL, Redis)
- to: []
ports:
- protocol: TCP
port: 3306 # Cloud SQL
- protocol: TCP
port: 6379 # Redis
- protocol: TCP
port: 443 # HTTPS for GCP APIs
# Allow internal cluster communication
- to:
- namespaceSelector: {}
ports:
- protocol: TCP
port: 8080

View File

@ -0,0 +1,338 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: erpnext-frontend
namespace: erpnext
labels:
app: erpnext-frontend
component: frontend
environment: production
version: v14
deployment-type: managed-services
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
deployment-type: managed-services
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/health"
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
initContainers:
- name: setup-nginx-config
image: busybox:1.35
command:
- sh
- -c
- |
# Copy base nginx config
cp /config/nginx.conf /shared/nginx.conf
# Replace placeholders with actual values
sed -i "s/PROJECT_ID/$PROJECT_ID/g" /shared/nginx.conf
echo "Nginx configuration prepared"
env:
- name: PROJECT_ID
valueFrom:
configMapRef:
name: erpnext-managed-config
key: PROJECT_ID
optional: true
volumeMounts:
- name: nginx-config
mountPath: /config
- name: nginx-config-processed
mountPath: /shared
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
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-processed
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
- name: var-log
mountPath: /var/log/nginx
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
env:
- name: NGINX_WORKER_PROCESSES
value: "auto"
- name: NGINX_WORKER_CONNECTIONS
value: "1024"
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: assets-data
persistentVolumeClaim:
claimName: erpnext-assets-pvc
- name: nginx-config
configMap:
name: nginx-managed-config
- name: nginx-config-processed
emptyDir: {}
- name: tmp
emptyDir: {}
- name: var-cache
emptyDir: {}
- name: var-run
emptyDir: {}
- name: var-log
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: erpnext-frontend
namespace: erpnext
labels:
app: erpnext-frontend
component: frontend
deployment-type: managed-services
annotations:
cloud.google.com/neg: '{"ingress": true}'
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
spec:
selector:
app: erpnext-frontend
ports:
- port: 8080
targetPort: 8080
name: http
protocol: TCP
type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: erpnext-frontend-hpa
namespace: erpnext
labels:
app: erpnext-frontend
component: frontend
deployment-type: managed-services
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
- 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
- type: Pods
value: 1
periodSeconds: 60
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: erpnext-frontend-pdb
namespace: erpnext
labels:
app: erpnext-frontend
component: frontend
deployment-type: managed-services
spec:
minAvailable: 1
selector:
matchLabels:
app: erpnext-frontend
---
# Network Policy for frontend service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: erpnext-frontend-netpol
namespace: erpnext
spec:
podSelector:
matchLabels:
app: erpnext-frontend
policyTypes:
- Ingress
- Egress
ingress:
# Allow traffic from ingress controller
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8080
# Allow traffic from monitoring
- from:
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- protocol: TCP
port: 8080
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow communication with backend
- to:
- podSelector:
matchLabels:
app: erpnext-backend
ports:
- protocol: TCP
port: 8000
- protocol: TCP
port: 9000
# Allow access to Cloud Storage for assets
- to: []
ports:
- protocol: TCP
port: 443 # HTTPS for Cloud Storage
---
# Service Monitor for Prometheus (if using Prometheus Operator)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: erpnext-frontend-monitor
namespace: erpnext
labels:
app: erpnext-frontend
component: frontend
deployment-type: managed-services
spec:
selector:
matchLabels:
app: erpnext-frontend
endpoints:
- port: http
path: /health
interval: 30s
scrapeTimeout: 10s

View File

@ -0,0 +1,696 @@
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
deployment-type: managed-services
spec:
replicas: 2
selector:
matchLabels:
app: erpnext-queue-default
template:
metadata:
labels:
app: erpnext-queue-default
component: worker
queue: default
environment: production
version: v14
deployment-type: managed-services
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-managed-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-password
- name: REDIS_AUTH
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: redis-auth
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/google/key.json
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
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
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
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4-alpine
command:
- /cloud_sql_proxy
- -instances=$(DB_CONNECTION_NAME)=tcp:127.0.0.1:3306
- -credential_file=/var/secrets/google/key.json
env:
- name: DB_CONNECTION_NAME
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-connection-name
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 2
capabilities:
drop:
- ALL
volumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: tmp
emptyDir: {}
- name: logs
emptyDir: {}
- name: google-cloud-key
secret:
secretName: gcp-service-account-key
---
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
deployment-type: managed-services
spec:
replicas: 1
selector:
matchLabels:
app: erpnext-queue-long
template:
metadata:
labels:
app: erpnext-queue-long
component: worker
queue: long
environment: production
version: v14
deployment-type: managed-services
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-managed-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-password
- name: REDIS_AUTH
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: redis-auth
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/google/key.json
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
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
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
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4-alpine
command:
- /cloud_sql_proxy
- -instances=$(DB_CONNECTION_NAME)=tcp:127.0.0.1:3306
- -credential_file=/var/secrets/google/key.json
env:
- name: DB_CONNECTION_NAME
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-connection-name
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 2
capabilities:
drop:
- ALL
volumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: tmp
emptyDir: {}
- name: logs
emptyDir: {}
- name: google-cloud-key
secret:
secretName: gcp-service-account-key
---
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
deployment-type: managed-services
spec:
replicas: 2
selector:
matchLabels:
app: erpnext-queue-short
template:
metadata:
labels:
app: erpnext-queue-short
component: worker
queue: short
environment: production
version: v14
deployment-type: managed-services
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-managed-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-password
- name: REDIS_AUTH
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: redis-auth
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/google/key.json
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
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
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
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4-alpine
command:
- /cloud_sql_proxy
- -instances=$(DB_CONNECTION_NAME)=tcp:127.0.0.1:3306
- -credential_file=/var/secrets/google/key.json
env:
- name: DB_CONNECTION_NAME
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-connection-name
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 2
capabilities:
drop:
- ALL
volumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: tmp
emptyDir: {}
- name: logs
emptyDir: {}
- name: google-cloud-key
secret:
secretName: gcp-service-account-key
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: erpnext-scheduler
namespace: erpnext
labels:
app: erpnext-scheduler
component: scheduler
environment: production
version: v14
deployment-type: managed-services
spec:
replicas: 1
selector:
matchLabels:
app: erpnext-scheduler
template:
metadata:
labels:
app: erpnext-scheduler
component: scheduler
environment: production
version: v14
deployment-type: managed-services
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-managed-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-password
- name: REDIS_AUTH
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: redis-auth
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/google/key.json
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
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
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
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4-alpine
command:
- /cloud_sql_proxy
- -instances=$(DB_CONNECTION_NAME)=tcp:127.0.0.1:3306
- -credential_file=/var/secrets/google/key.json
env:
- name: DB_CONNECTION_NAME
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-connection-name
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 2
capabilities:
drop:
- ALL
volumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: tmp
emptyDir: {}
- name: logs
emptyDir: {}
- name: google-cloud-key
secret:
secretName: gcp-service-account-key
nodeSelector:
cloud.google.com/gke-preemptible: "false"
---
# HPA for queue workers based on queue depth
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: erpnext-queue-default-hpa
namespace: erpnext
labels:
app: erpnext-queue-default
component: worker
deployment-type: managed-services
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: erpnext-queue-default
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
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Pods
value: 2
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: erpnext-queue-short-hpa
namespace: erpnext
labels:
app: erpnext-queue-short
component: worker
deployment-type: managed-services
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: erpnext-queue-short
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 30
policies:
- type: Pods
value: 2
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 180
policies:
- type: Pods
value: 1
periodSeconds: 60
---
# Network Policy for worker services
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: erpnext-workers-netpol
namespace: erpnext
spec:
podSelector:
matchLabels:
component: worker
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow access to managed services
- to: []
ports:
- protocol: TCP
port: 3306 # Cloud SQL
- protocol: TCP
port: 6379 # Redis
- protocol: TCP
port: 443 # HTTPS for GCP APIs
---
# Network Policy for scheduler
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: erpnext-scheduler-netpol
namespace: erpnext
spec:
podSelector:
matchLabels:
app: erpnext-scheduler
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow access to managed services
- to: []
ports:
- protocol: TCP
port: 3306 # Cloud SQL
- protocol: TCP
port: 6379 # Redis
- protocol: TCP
port: 443 # HTTPS for GCP APIs

View File

@ -0,0 +1,319 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: erpnext-ingress
namespace: erpnext
labels:
app: erpnext
component: ingress
environment: production
deployment-type: managed-services
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"
# CORS configuration for managed services
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"
# SSL configuration
cert-manager.io/cluster-issuer: letsencrypt-prod
# Rate limiting for API protection
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
# Enhanced security headers for managed services
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https: https://storage.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' wss: https://storage.googleapis.com; frame-ancestors 'self';" always;
# Custom headers for managed services
add_header X-Deployment-Type "managed-services" always;
add_header X-Backend-Type "cloud-sql-redis" always;
# Performance optimizations
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
# Security for API endpoints
if ($request_uri ~* "^/api/") {
add_header X-API-Rate-Limit "100/min" always;
}
# Server-side configuration snippet for managed services
nginx.ingress.kubernetes.io/server-snippet: |
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=upload:10m rate=2r/s;
# Connection limiting
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
limit_conn conn_limit_per_ip 20;
# Security configurations
server_tokens off;
client_header_timeout 10s;
client_body_timeout 10s;
# Logging for managed services
access_log /var/log/nginx/erpnext-managed.access.log combined;
error_log /var/log/nginx/erpnext-managed.error.log warn;
spec:
tls:
- hosts:
- erpnext.yourdomain.com
- api.yourdomain.com
secretName: erpnext-tls
rules:
- host: erpnext.yourdomain.com
http:
paths:
# Static assets - serve from Cloud Storage or CDN
- path: /assets
pathType: Prefix
backend:
service:
name: erpnext-frontend
port:
number: 8080
# File uploads and downloads
- path: /files
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 for real-time features
- path: /socket.io
pathType: Prefix
backend:
service:
name: erpnext-backend
port:
number: 9000
# API endpoints with rate limiting
- path: /api
pathType: Prefix
backend:
service:
name: erpnext-frontend
port:
number: 8080
# Method endpoints (Frappe framework)
- path: /method
pathType: Prefix
backend:
service:
name: erpnext-frontend
port:
number: 8080
# Main application
- path: /
pathType: Prefix
backend:
service:
name: erpnext-frontend
port:
number: 8080
# API subdomain for cleaner separation
- host: api.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: erpnext-backend
port:
number: 8000
---
# Additional ingress for API-only access with stricter security
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: erpnext-api-ingress
namespace: erpnext
labels:
app: erpnext
component: api-ingress
environment: production
deployment-type: managed-services
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-body-size: 10m
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: letsencrypt-prod
# Stricter rate limiting for API
nginx.ingress.kubernetes.io/rate-limit: "50"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
# API-specific security
nginx.ingress.kubernetes.io/configuration-snippet: |
# Stricter security for API access
add_header X-API-Version "v14" always;
add_header X-Rate-Limit "50/min" always;
# Block non-API requests
if ($request_uri !~* "^/api/") {
return 404;
}
# Additional API security headers
add_header X-API-Security "enhanced" always;
spec:
tls:
- hosts:
- api-secure.yourdomain.com
secretName: erpnext-api-tls
rules:
- host: api-secure.yourdomain.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: erpnext-backend
port:
number: 8000
---
# ClusterIssuer for Let's Encrypt SSL certificates
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
podTemplate:
spec:
nodeSelector:
cloud.google.com/gke-preemptible: "false"
---
# Staging issuer for testing
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
---
# Certificate for main domain
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: erpnext-cert
namespace: erpnext
spec:
secretName: erpnext-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- erpnext.yourdomain.com
- api.yourdomain.com
---
# Certificate for API domain
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: erpnext-api-cert
namespace: erpnext
spec:
secretName: erpnext-api-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api-secure.yourdomain.com
---
# Nginx configuration for managed services optimization
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-ingress-config
namespace: ingress-nginx
data:
# Global nginx configurations optimized for managed services
proxy-connect-timeout: "10"
proxy-send-timeout: "120"
proxy-read-timeout: "120"
proxy-body-size: "50m"
proxy-buffer-size: "4k"
proxy-buffers-number: "8"
# Performance optimizations
worker-processes: "auto"
worker-connections: "16384"
worker-rlimit-nofile: "65536"
# SSL optimizations
ssl-protocols: "TLSv1.2 TLSv1.3"
ssl-ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"
ssl-prefer-server-ciphers: "true"
ssl-session-cache: "shared:SSL:10m"
ssl-session-timeout: "10m"
# Connection keep-alive
keep-alive: "75"
keep-alive-requests: "100"
# Rate limiting for managed services
limit-req-status-code: "429"
limit-conn-status-code: "429"
# Logging optimizations
log-format-escape-json: "true"
log-format-upstream: '{"time": "$time_iso8601", "remote_addr": "$proxy_protocol_addr", "x_forwarded_for": "$proxy_add_x_forwarded_for", "request_id": "$req_id", "remote_user": "$remote_user", "bytes_sent": $bytes_sent, "request_time": $request_time, "status": $status, "vhost": "$host", "request_proto": "$server_protocol", "path": "$uri", "request_query": "$args", "request_length": $request_length, "duration": $request_time,"method": "$request_method", "http_referrer": "$http_referer", "http_user_agent": "$http_user_agent", "upstream_addr": "$upstream_addr", "upstream_response_time": $upstream_response_time, "upstream_response_length": $upstream_response_length, "upstream_status": $upstream_status }'

View File

@ -0,0 +1,606 @@
apiVersion: batch/v1
kind: Job
metadata:
name: erpnext-create-site
namespace: erpnext
labels:
app: erpnext
component: setup
job-type: create-site
deployment-type: managed-services
spec:
backoffLimit: 3
activeDeadlineSeconds: 1800 # 30 minutes timeout
template:
metadata:
labels:
app: erpnext
component: setup
job-type: create-site
deployment-type: managed-services
spec:
serviceAccountName: erpnext-ksa
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
- name: wait-for-managed-services
image: busybox:1.35
command:
- sh
- -c
- |
echo 'Waiting for Cloud SQL and Redis managed services...'
# Wait for Cloud SQL Proxy to establish connection
until nc -z 127.0.0.1 3306; do
echo 'Waiting for Cloud SQL connection...'
sleep 10
done
# Test Redis connection
echo 'Testing Redis connection...'
timeout 30 sh -c 'until nc -z $REDIS_HOST 6379; do sleep 5; done'
echo 'All managed services are ready!'
# Additional wait for services to be fully ready
sleep 30
env:
- name: REDIS_HOST
value: "REDIS_HOST_PLACEHOLDER" # Will be replaced by deployment script
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 with managed services..."
# Set up environment for managed services
export DB_HOST="127.0.0.1"
export DB_PORT="3306"
# Check if site already exists
if [ -d "/home/frappe/frappe-bench/sites/frontend" ]; then
echo "Site 'frontend' already exists. Checking configuration..."
# Update site configuration for managed services
cd /home/frappe/frappe-bench
bench --site frontend set-config db_host "$DB_HOST"
bench --site frontend set-config db_port "$DB_PORT"
bench --site frontend set-config redis_cache "redis://$REDIS_HOST:6379/0"
bench --site frontend set-config redis_queue "redis://$REDIS_HOST:6379/1"
bench --site frontend set-config redis_socketio "redis://$REDIS_HOST:6379/2"
echo "Site configuration updated for managed services."
exit 0
fi
echo "Creating new ERPNext site with managed services..."
cd /home/frappe/frappe-bench
# Create the site with managed database
bench new-site frontend \
--admin-password "$ADMIN_PASSWORD" \
--mariadb-root-password "$DB_PASSWORD" \
--db-host "$DB_HOST" \
--db-port "$DB_PORT" \
--install-app erpnext \
--set-default
# Configure Redis for managed Memorystore
echo "Configuring Redis for Memorystore..."
bench --site frontend set-config redis_cache "redis://$REDIS_HOST:6379/0"
bench --site frontend set-config redis_queue "redis://$REDIS_HOST:6379/1"
bench --site frontend set-config redis_socketio "redis://$REDIS_HOST:6379/2"
# Set additional configurations for managed services
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
bench --site frontend set-config auto_update 0
bench --site frontend set-config enable_scheduler 1
# Configure for Cloud Storage
bench --site frontend set-config use_google_cloud_storage 1
bench --site frontend set-config google_cloud_storage_bucket "$CLOUD_STORAGE_BUCKET"
# Set up email configuration (optional)
bench --site frontend set-config mail_server "smtp.gmail.com"
bench --site frontend set-config mail_port 587
bench --site frontend set-config use_tls 1
# Install additional apps if needed
# bench --site frontend install-app custom_app
# Run post-installation setup
bench --site frontend migrate
bench --site frontend clear-cache
echo "Site creation completed successfully with managed services!"
# Verify the setup
echo "Verifying installation..."
bench --site frontend list-apps
# Create initial data if needed
# bench --site frontend execute frappe.utils.install.make_demo_data
echo "ERPNext installation verification completed."
envFrom:
- configMapRef:
name: erpnext-managed-config
env:
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: admin-password
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-password
- name: REDIS_HOST
value: "REDIS_HOST_PLACEHOLDER" # Will be replaced by deployment script
- name: CLOUD_STORAGE_BUCKET
value: "erpnext-files-PROJECT_ID"
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/google/key.json
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: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4-alpine
command:
- /cloud_sql_proxy
- -instances=$(DB_CONNECTION_NAME)=tcp:127.0.0.1:3306
- -credential_file=/var/secrets/google/key.json
- -enable_iam_login
env:
- name: DB_CONNECTION_NAME
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-connection-name
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 2
capabilities:
drop:
- ALL
volumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: tmp
emptyDir: {}
- name: google-cloud-key
secret:
secretName: gcp-service-account-key
---
# CronJob for backing up site files (database is handled by Cloud SQL)
apiVersion: batch/v1
kind: CronJob
metadata:
name: erpnext-files-backup
namespace: erpnext
labels:
app: erpnext
component: backup
backup-type: files
deployment-type: managed-services
spec:
schedule: "0 3 * * *" # Daily at 3 AM
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 7
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 3600 # 1 hour timeout
template:
metadata:
labels:
app: erpnext
component: backup
backup-type: files
deployment-type: managed-services
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="erpnext_files_backup_${BACKUP_DATE}.tar.gz"
echo "Starting ERPNext files backup: $BACKUP_FILE"
# Create compressed backup of sites directory
echo "Creating compressed archive..."
tar -czf /tmp/$BACKUP_FILE -C /sites \
--exclude='*.log' \
--exclude='*.tmp' \
--exclude='__pycache__' \
.
# Get file size
BACKUP_SIZE=$(stat -f%z /tmp/$BACKUP_FILE 2>/dev/null || stat -c%s /tmp/$BACKUP_FILE 2>/dev/null || echo "unknown")
echo "Backup file size: $BACKUP_SIZE bytes"
# Upload to Cloud Storage with metadata
echo "Uploading to Cloud Storage..."
gsutil -h "x-goog-meta-backup-date:$BACKUP_DATE" \
-h "x-goog-meta-backup-type:files" \
-h "x-goog-meta-deployment-type:managed-services" \
-h "x-goog-meta-source:erpnext-gke" \
cp /tmp/$BACKUP_FILE gs://erpnext-backups-$PROJECT_ID/files/
echo "Files backup uploaded to GCS: gs://erpnext-backups-$PROJECT_ID/files/$BACKUP_FILE"
# Verify upload
if gsutil ls gs://erpnext-backups-$PROJECT_ID/files/$BACKUP_FILE > /dev/null 2>&1; then
echo "Backup verification successful"
else
echo "Backup verification failed!"
exit 1
fi
# Clean up local file
rm /tmp/$BACKUP_FILE
# Clean up old backups (keep last 30 days)
echo "Cleaning up old backups..."
gsutil -m rm -r gs://erpnext-backups-$PROJECT_ID/files/erpnext_files_backup_$(date -d '30 days ago' +%Y%m%d)_* 2>/dev/null || true
echo "Files backup completed successfully!"
env:
- name: PROJECT_ID
value: "PROJECT_ID_PLACEHOLDER" # Will be replaced by deployment script
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/google/key.json
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
volumeMounts:
- name: sites-data
mountPath: /sites
readOnly: true
- name: tmp
mountPath: /tmp
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "250m"
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: tmp
emptyDir: {}
- name: google-cloud-key
secret:
secretName: gcp-service-account-key
---
# Job for database migration (for upgrades)
apiVersion: batch/v1
kind: Job
metadata:
name: erpnext-migrate
namespace: erpnext
labels:
app: erpnext
component: maintenance
job-type: migrate
deployment-type: managed-services
spec:
backoffLimit: 3
activeDeadlineSeconds: 3600 # 1 hour timeout
template:
metadata:
labels:
app: erpnext
component: maintenance
job-type: migrate
deployment-type: managed-services
spec:
serviceAccountName: erpnext-ksa
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
- name: wait-for-services
image: busybox:1.35
command:
- sh
- -c
- |
echo 'Waiting for managed services...'
until nc -z 127.0.0.1 3306; do
echo 'Waiting for Cloud SQL...'
sleep 5
done
echo 'Services ready for migration'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
capabilities:
drop:
- ALL
containers:
- name: migrate
image: frappe/erpnext-worker:v14
command:
- bash
- -c
- |
set -e
echo "Starting ERPNext migration with managed services..."
cd /home/frappe/frappe-bench
# Create backup before migration
echo "Creating pre-migration backup..."
bench --site all backup --with-files
# Run database migrations
echo "Running database migrations..."
bench --site all migrate
# Clear cache
echo "Clearing cache..."
bench --site all clear-cache
# Clear website cache
echo "Clearing website cache..."
bench --site all clear-website-cache
# Rebuild search index if needed
echo "Rebuilding search index..."
bench --site all build-search-index || true
# Update translations
echo "Updating translations..."
bench --site all build-translations || true
# Verify migration
echo "Verifying migration..."
bench --site all list-apps
echo "Migration completed successfully with managed services!"
envFrom:
- configMapRef:
name: erpnext-managed-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-password
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/google/key.json
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: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
- name: cloud-sql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.35.4-alpine
command:
- /cloud_sql_proxy
- -instances=$(DB_CONNECTION_NAME)=tcp:127.0.0.1:3306
- -credential_file=/var/secrets/google/key.json
env:
- name: DB_CONNECTION_NAME
valueFrom:
secretKeyRef:
name: erpnext-managed-secrets
key: db-connection-name
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 2
capabilities:
drop:
- ALL
volumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: sites-data
persistentVolumeClaim:
claimName: erpnext-sites-pvc
- name: tmp
emptyDir: {}
- name: google-cloud-key
secret:
secretName: gcp-service-account-key
---
# CronJob for health checks and maintenance
apiVersion: batch/v1
kind: CronJob
metadata:
name: erpnext-health-check
namespace: erpnext
labels:
app: erpnext
component: maintenance
job-type: health-check
deployment-type: managed-services
spec:
schedule: "*/15 * * * *" # Every 15 minutes
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 300 # 5 minutes timeout
template:
metadata:
labels:
app: erpnext
component: maintenance
job-type: health-check
deployment-type: managed-services
spec:
serviceAccountName: erpnext-ksa
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: health-check
image: curlimages/curl:latest
command:
- sh
- -c
- |
set -e
echo "Running ERPNext health checks..."
# Check frontend service
echo "Checking frontend service..."
curl -f http://erpnext-frontend:8080/health || exit 1
# Check backend service
echo "Checking backend service..."
curl -f http://erpnext-backend:8000/api/method/ping || exit 1
# Check Redis connectivity (if accessible from cluster)
echo "Checking Redis connectivity..."
# This would require a Redis client in the image
# nc -z $REDIS_HOST 6379 || exit 1
echo "All health checks passed!"
env:
- name: REDIS_HOST
value: "REDIS_HOST_PLACEHOLDER"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534 # nobody user in curl image
capabilities:
drop:
- ALL
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
cpu: "100m"

View File

@ -0,0 +1,43 @@
apiVersion: v1
kind: Namespace
metadata:
name: erpnext
labels:
name: erpnext
environment: production
deployment-type: managed-services
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: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
persistentvolumeclaims: "5" # Reduced since no DB storage needed
pods: "15" # Reduced since no DB/Redis pods
services: "8"
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

View File

@ -0,0 +1,181 @@
# Note: This file shows the structure of secrets that need to be created
# In practice, these should be created using kubectl commands or External Secrets Operator
# DO NOT commit actual secret values to version control
apiVersion: v1
kind: Secret
metadata:
name: erpnext-db-credentials
namespace: erpnext
labels:
app: erpnext
component: database
deployment-type: managed-services
type: Opaque
stringData:
# Cloud SQL database password
password: "PLACEHOLDER_DB_PASSWORD"
# Cloud SQL connection name
connection-name: "PROJECT_ID:REGION:erpnext-db"
---
apiVersion: v1
kind: Secret
metadata:
name: erpnext-admin-credentials
namespace: erpnext
labels:
app: erpnext
component: admin
type: Opaque
stringData:
# ERPNext administrator password
password: "PLACEHOLDER_ADMIN_PASSWORD"
---
apiVersion: v1
kind: Secret
metadata:
name: erpnext-api-credentials
namespace: erpnext
labels:
app: erpnext
component: api
type: Opaque
stringData:
# ERPNext API credentials
api-key: "PLACEHOLDER_API_KEY"
api-secret: "PLACEHOLDER_API_SECRET"
---
apiVersion: v1
kind: Secret
metadata:
name: redis-auth
namespace: erpnext
labels:
app: erpnext
component: redis
deployment-type: managed-services
type: Opaque
stringData:
# Memorystore Redis AUTH string
auth-string: "PLACEHOLDER_REDIS_AUTH"
---
apiVersion: v1
kind: Secret
metadata:
name: cloudsql-ssl-certs
namespace: erpnext
labels:
app: erpnext
component: database
security: ssl
type: Opaque
stringData:
# Cloud SQL SSL certificates
client-cert.pem: "PLACEHOLDER_CLIENT_CERT"
client-key.pem: "PLACEHOLDER_CLIENT_KEY"
server-ca.pem: "PLACEHOLDER_SERVER_CA"
---
apiVersion: v1
kind: Secret
metadata:
name: gcp-service-account-key
namespace: erpnext
labels:
app: erpnext
component: auth
deployment-type: managed-services
type: Opaque
stringData:
# Service account key for accessing managed services
key.json: "PLACEHOLDER_SERVICE_ACCOUNT_KEY"
---
# External Secrets Operator configuration (recommended approach)
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: gcpsm-secret-store
namespace: erpnext
spec:
provider:
gcpsm:
projectId: "PROJECT_ID"
auth:
workloadIdentity:
clusterLocation: us-central1-a
clusterName: erpnext-managed-cluster
serviceAccountRef:
name: erpnext-ksa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: erpnext-managed-secrets
namespace: erpnext
spec:
refreshInterval: 15m
secretStoreRef:
name: gcpsm-secret-store
kind: SecretStore
target:
name: erpnext-managed-secrets
creationPolicy: Owner
data:
- secretKey: admin-password
remoteRef:
key: erpnext-admin-password
- secretKey: db-password
remoteRef:
key: erpnext-db-password
- secretKey: api-key
remoteRef:
key: erpnext-api-key
- secretKey: api-secret
remoteRef:
key: erpnext-api-secret
- secretKey: redis-auth
remoteRef:
key: redis-auth-string
- secretKey: db-connection-name
remoteRef:
key: erpnext-db-connection-name
---
# Service Account for Workload Identity
apiVersion: v1
kind: ServiceAccount
metadata:
name: erpnext-ksa
namespace: erpnext
annotations:
iam.gke.io/gcp-service-account: erpnext-managed@PROJECT_ID.iam.gserviceaccount.com
labels:
app: erpnext
component: auth
deployment-type: managed-services
---
# ClusterRole for accessing secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: erpnext
name: erpnext-secrets-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: erpnext-secrets-binding
namespace: erpnext
subjects:
- kind: ServiceAccount
name: erpnext-ksa
namespace: erpnext
roleRef:
kind: Role
name: erpnext-secrets-reader
apiGroup: rbac.authorization.k8s.io

View File

@ -0,0 +1,75 @@
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
---
# ERPNext sites storage (smaller since database is external)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: erpnext-sites-pvc
namespace: erpnext
labels:
app: erpnext
component: sites
storage-type: application-files
spec:
storageClassName: ssd-retain
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 30Gi # Reduced since no database files
---
# ERPNext assets storage (for static files and uploads)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: erpnext-assets-pvc
namespace: erpnext
labels:
app: erpnext
component: assets
storage-type: static-files
spec:
storageClassName: ssd-retain
accessModes:
- ReadWriteMany
resources:
requests:
storage: 15Gi # Reduced for managed services
---
# Backup storage for application files only
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: backup-pvc
namespace: erpnext
labels:
app: erpnext
component: backup
storage-type: backup
spec:
storageClassName: standard-retain
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi # Smaller since database backups are handled by Cloud SQL

View File

@ -0,0 +1,980 @@
#!/bin/bash
# ERPNext Cloud Run Deployment Script with Managed Services
# This script automates the deployment of ERPNext on Cloud Run using Cloud SQL and Memorystore
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
PROJECT_ID=${PROJECT_ID:-""}
REGION=${REGION:-"us-central1"}
DOMAIN=${DOMAIN:-"erpnext.yourdomain.com"}
API_DOMAIN=${API_DOMAIN:-"api.yourdomain.com"}
# Managed services configuration
DB_INSTANCE_NAME=${DB_INSTANCE_NAME:-"erpnext-db"}
REDIS_INSTANCE_NAME=${REDIS_INSTANCE_NAME:-"erpnext-redis"}
VPC_NAME=${VPC_NAME:-"erpnext-vpc"}
VPC_CONNECTOR=${VPC_CONNECTOR:-"erpnext-connector"}
# Cloud Run service names
BACKEND_SERVICE=${BACKEND_SERVICE:-"erpnext-backend"}
FRONTEND_SERVICE=${FRONTEND_SERVICE:-"erpnext-frontend"}
BACKGROUND_SERVICE=${BACKGROUND_SERVICE:-"erpnext-background"}
# 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 for Cloud Run deployment..."
# Check if required tools are installed
local required_tools=("gcloud" "docker")
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
# Check if Docker is configured for gcloud
if ! gcloud auth configure-docker --quiet; then
print_error "Failed to configure Docker for gcloud"
exit 1
fi
print_success "Prerequisites check passed"
}
# Function to check managed services
check_managed_services() {
print_status "Checking managed services status..."
# Check Cloud SQL instance
if ! gcloud sql instances describe "$DB_INSTANCE_NAME" &> /dev/null; then
print_error "Cloud SQL instance '$DB_INSTANCE_NAME' not found. Please create it first."
exit 1
fi
# Check Memorystore Redis instance
if ! gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" &> /dev/null; then
print_error "Memorystore Redis instance '$REDIS_INSTANCE_NAME' not found. Please create it first."
exit 1
fi
# Check VPC connector
if ! gcloud compute networks vpc-access connectors describe "$VPC_CONNECTOR" --region="$REGION" &> /dev/null; then
print_error "VPC connector '$VPC_CONNECTOR' not found. Please create it first."
exit 1
fi
print_success "All managed services are available"
}
# Function to get managed services information
get_managed_services_info() {
print_status "Gathering managed services information..."
# Get Cloud SQL connection name
DB_CONNECTION_NAME=$(gcloud sql instances describe "$DB_INSTANCE_NAME" --format="value(connectionName)")
print_status "Cloud SQL connection name: $DB_CONNECTION_NAME"
# Get Redis host IP
REDIS_HOST=$(gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" --format="value(host)")
print_status "Redis host IP: $REDIS_HOST"
# Get Redis AUTH if enabled
REDIS_AUTH=""
if gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" --format="value(authEnabled)" | grep -q "True"; then
REDIS_AUTH=$(gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" --format="value(authString)")
print_status "Redis AUTH enabled"
fi
print_success "Managed services information gathered"
}
# Function to setup Cloud Storage for files
setup_cloud_storage() {
print_status "Setting up Cloud Storage for ERPNext files..."
local bucket_name="erpnext-files-$PROJECT_ID"
# Create bucket if it doesn't exist
if ! gsutil ls -b gs://"$bucket_name" &> /dev/null; then
gsutil mb gs://"$bucket_name"
print_success "Created Cloud Storage bucket: $bucket_name"
else
print_warning "Bucket $bucket_name already exists"
fi
# Set lifecycle policy
gsutil lifecycle set - gs://"$bucket_name" <<EOF
{
"lifecycle": {
"rule": [
{
"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"},
"condition": {"age": 30}
},
{
"action": {"type": "SetStorageClass", "storageClass": "COLDLINE"},
"condition": {"age": 90}
}
]
}
}
EOF
# Set CORS for web access
gsutil cors set - gs://"$bucket_name" <<EOF
[
{
"origin": ["https://$DOMAIN", "https://$API_DOMAIN"],
"method": ["GET", "PUT", "POST", "DELETE"],
"responseHeader": ["Content-Type", "Content-Range", "Content-Disposition"],
"maxAgeSeconds": 3600
}
]
EOF
print_success "Cloud Storage setup completed"
}
# Function to create and store secrets
setup_secrets() {
print_status "Setting up secrets in Secret Manager..."
# Create secrets if they don't exist
if ! gcloud secrets describe erpnext-admin-password &> /dev/null; then
local admin_password=${ADMIN_PASSWORD:-$(openssl rand -base64 32)}
gcloud secrets create erpnext-admin-password --data-file=<(echo -n "$admin_password")
print_warning "Admin password: $admin_password"
print_warning "Please save this password securely!"
fi
if ! gcloud secrets describe erpnext-db-password &> /dev/null; then
local db_password=${DB_PASSWORD:-$(openssl rand -base64 32)}
gcloud secrets create erpnext-db-password --data-file=<(echo -n "$db_password")
print_warning "Database password: $db_password"
print_warning "Please save this password securely!"
fi
if ! gcloud secrets describe erpnext-api-key &> /dev/null; then
local api_key=${API_KEY:-$(openssl rand -hex 32)}
gcloud secrets create erpnext-api-key --data-file=<(echo -n "$api_key")
fi
if ! gcloud secrets describe erpnext-api-secret &> /dev/null; then
local api_secret=${API_SECRET:-$(openssl rand -hex 32)}
gcloud secrets create erpnext-api-secret --data-file=<(echo -n "$api_secret")
fi
# Store connection information
gcloud secrets create erpnext-db-connection-name --data-file=<(echo -n "$DB_CONNECTION_NAME") --quiet || \
gcloud secrets versions add erpnext-db-connection-name --data-file=<(echo -n "$DB_CONNECTION_NAME")
if [[ -n "$REDIS_AUTH" ]]; then
gcloud secrets create redis-auth-string --data-file=<(echo -n "$REDIS_AUTH") --quiet || \
gcloud secrets versions add redis-auth-string --data-file=<(echo -n "$REDIS_AUTH")
fi
print_success "Secrets created in Secret Manager"
}
# Function to build and push container images
build_and_push_images() {
print_status "Building and pushing container images..."
# Create temporary directory for builds
local build_dir="/tmp/erpnext-cloudrun-builds"
mkdir -p "$build_dir"
# Build backend image
print_status "Building ERPNext backend image..."
cat > "$build_dir/Dockerfile.backend" <<'EOF'
FROM frappe/erpnext-worker:v14
# Install Cloud SQL Proxy
RUN apt-get update && apt-get install -y wget netcat-openbsd && \
wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy && \
chmod +x cloud_sql_proxy && \
mv cloud_sql_proxy /usr/local/bin/ && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Install additional Python packages
RUN pip install --no-cache-dir google-cloud-storage google-cloud-secret-manager
# Create startup script
COPY startup-backend.sh /startup.sh
RUN chmod +x /startup.sh
# Set environment for Cloud Run
ENV PORT=8080
ENV WORKERS=4
ENV TIMEOUT=300
ENV MAX_REQUESTS=1000
EXPOSE 8080
CMD ["/startup.sh"]
EOF
# Create backend startup script
cat > "$build_dir/startup-backend.sh" <<'EOF'
#!/bin/bash
set -e
echo "Starting ERPNext backend for Cloud Run..."
# Start Cloud SQL Proxy in background
if [ -n "$DB_CONNECTION_NAME" ]; then
echo "Starting Cloud SQL Proxy..."
/usr/local/bin/cloud_sql_proxy -instances=$DB_CONNECTION_NAME=tcp:3306 &
# Wait for proxy to be ready
echo "Waiting for database connection..."
until nc -z localhost 3306; do
sleep 2
done
echo "Database connection established"
fi
# Set database connection environment
export DB_HOST="127.0.0.1"
export DB_PORT="3306"
# Set Redis connection
export REDIS_CACHE_URL="redis://$REDIS_HOST:6379/0"
export REDIS_QUEUE_URL="redis://$REDIS_HOST:6379/1"
export REDIS_SOCKETIO_URL="redis://$REDIS_HOST:6379/2"
# Add Redis AUTH if available
if [ -n "$REDIS_AUTH" ]; then
export REDIS_CACHE_URL="redis://:$REDIS_AUTH@$REDIS_HOST:6379/0"
export REDIS_QUEUE_URL="redis://:$REDIS_AUTH@$REDIS_HOST:6379/1"
export REDIS_SOCKETIO_URL="redis://:$REDIS_AUTH@$REDIS_HOST:6379/2"
fi
# Initialize site if it doesn't exist (for first run)
cd /home/frappe/frappe-bench
if [ ! -d "sites/frontend" ] && [ "$INIT_SITE" = "true" ]; then
echo "Initializing ERPNext site..."
bench new-site frontend \
--admin-password "$ADMIN_PASSWORD" \
--mariadb-root-password "$DB_PASSWORD" \
--install-app erpnext \
--set-default
fi
# Start ERPNext with Gunicorn optimized for Cloud Run
echo "Starting ERPNext backend..."
exec gunicorn -b 0.0.0.0:$PORT \
-w $WORKERS \
-t $TIMEOUT \
--max-requests $MAX_REQUESTS \
--preload \
--access-logfile - \
--error-logfile - \
frappe.app:application
EOF
# Build and push backend image
cd "$build_dir"
gcloud builds submit --tag gcr.io/$PROJECT_ID/erpnext-backend-cloudrun:latest \
--dockerfile=Dockerfile.backend .
# Build frontend image
print_status "Building ERPNext frontend image..."
cat > "$build_dir/Dockerfile.frontend" <<'EOF'
FROM nginx:alpine
# Install envsubst for template processing
RUN apk add --no-cache gettext
# Copy nginx configuration template
COPY nginx.conf.template /etc/nginx/nginx.conf.template
COPY startup-frontend.sh /startup.sh
RUN chmod +x /startup.sh
# Set Cloud Run port
ENV PORT=8080
EXPOSE 8080
CMD ["/startup.sh"]
EOF
# Create nginx configuration template
cat > "$build_dir/nginx.conf.template" <<'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
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 ${BACKEND_URL};
keepalive 32;
}
server {
listen ${PORT};
server_name _;
# 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";
# Serve static assets from Cloud Storage
location /assets/ {
proxy_pass https://storage.googleapis.com/${STORAGE_BUCKET}/assets/;
proxy_set_header Host storage.googleapis.com;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy API requests to backend service
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
# WebSocket support
location /socket.io/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Main application
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
EOF
# Create frontend startup script
cat > "$build_dir/startup-frontend.sh" <<'EOF'
#!/bin/sh
set -e
echo "Starting ERPNext frontend for Cloud Run..."
# Process nginx configuration template
envsubst '${PORT} ${BACKEND_URL} ${STORAGE_BUCKET}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
echo "Nginx configuration processed"
cat /etc/nginx/nginx.conf
# Start nginx
exec nginx -g 'daemon off;'
EOF
# Build and push frontend image
gcloud builds submit --tag gcr.io/$PROJECT_ID/erpnext-frontend-cloudrun:latest \
--dockerfile=Dockerfile.frontend .
# Build background worker image
print_status "Building ERPNext background worker image..."
cat > "$build_dir/Dockerfile.background" <<'EOF'
FROM frappe/erpnext-worker:v14
# Install Cloud SQL Proxy and additional tools
RUN apt-get update && apt-get install -y wget netcat-openbsd && \
wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy && \
chmod +x cloud_sql_proxy && \
mv cloud_sql_proxy /usr/local/bin/ && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Install additional Python packages
RUN pip install --no-cache-dir google-cloud-tasks google-cloud-storage flask
# Copy task processor
COPY background_processor.py /background_processor.py
COPY startup-background.sh /startup.sh
RUN chmod +x /startup.sh
ENV PORT=8080
EXPOSE 8080
CMD ["/startup.sh"]
EOF
# Create background processor
cat > "$build_dir/background_processor.py" <<'EOF'
import os
import json
import logging
from flask import Flask, request, jsonify
import frappe
from frappe.utils.background_jobs import execute_job
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route('/process-task', methods=['POST'])
def process_task():
"""Process background task from Cloud Tasks or Cloud Scheduler"""
try:
# Get task data
task_data = request.get_json()
if not task_data:
return jsonify({'status': 'error', 'message': 'No task data provided'}), 400
# Set site context
frappe.init(site='frontend')
frappe.connect()
# Execute the job
job_name = task_data.get('job_name')
kwargs = task_data.get('kwargs', {})
if not job_name:
return jsonify({'status': 'error', 'message': 'No job_name provided'}), 400
logging.info(f"Executing job: {job_name}")
result = execute_job(job_name, **kwargs)
frappe.db.commit()
frappe.destroy()
return jsonify({'status': 'success', 'result': str(result)})
except Exception as e:
logging.error(f"Task processing failed: {str(e)}")
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/health', methods=['GET'])
def health_check():
return jsonify({'status': 'healthy'})
@app.route('/', methods=['GET'])
def root():
return jsonify({'service': 'erpnext-background', 'status': 'running'})
if __name__ == '__main__':
port = int(os.environ.get('PORT', 8080))
app.run(host='0.0.0.0', port=port)
EOF
# Create background startup script
cat > "$build_dir/startup-background.sh" <<'EOF'
#!/bin/bash
set -e
echo "Starting ERPNext background processor for Cloud Run..."
# Start Cloud SQL Proxy in background
if [ -n "$DB_CONNECTION_NAME" ]; then
echo "Starting Cloud SQL Proxy..."
/usr/local/bin/cloud_sql_proxy -instances=$DB_CONNECTION_NAME=tcp:3306 &
# Wait for proxy to be ready
until nc -z localhost 3306; do
echo "Waiting for database connection..."
sleep 2
done
echo "Database connection established"
fi
export DB_HOST="127.0.0.1"
export DB_PORT="3306"
# Start background processor
cd /home/frappe/frappe-bench
exec python /background_processor.py
EOF
# Build and push background image
gcloud builds submit --tag gcr.io/$PROJECT_ID/erpnext-background-cloudrun:latest \
--dockerfile=Dockerfile.background .
# Cleanup
rm -rf "$build_dir"
print_success "Container images built and pushed"
}
# Function to deploy Cloud Run services
deploy_cloud_run_services() {
print_status "Deploying Cloud Run services..."
# Deploy backend service
print_status "Deploying backend service..."
gcloud run deploy "$BACKEND_SERVICE" \
--image gcr.io/$PROJECT_ID/erpnext-backend-cloudrun:latest \
--platform managed \
--region "$REGION" \
--allow-unauthenticated \
--vpc-connector "$VPC_CONNECTOR" \
--set-env-vars="DB_CONNECTION_NAME=$DB_CONNECTION_NAME,REDIS_HOST=$REDIS_HOST,PROJECT_ID=$PROJECT_ID" \
--set-secrets="ADMIN_PASSWORD=erpnext-admin-password:latest,DB_PASSWORD=erpnext-db-password:latest,API_KEY=erpnext-api-key:latest,API_SECRET=erpnext-api-secret:latest" \
--service-account "erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--memory 2Gi \
--cpu 2 \
--concurrency 80 \
--timeout 300 \
--min-instances 1 \
--max-instances 100
if [[ -n "$REDIS_AUTH" ]]; then
gcloud run services update "$BACKEND_SERVICE" \
--region "$REGION" \
--update-secrets="REDIS_AUTH=redis-auth-string:latest"
fi
# Get backend service URL
BACKEND_URL=$(gcloud run services describe "$BACKEND_SERVICE" --region="$REGION" --format="value(status.url)")
BACKEND_HOSTNAME=$(echo "$BACKEND_URL" | sed 's|https://||')
print_success "Backend service deployed: $BACKEND_URL"
# Deploy frontend service
print_status "Deploying frontend service..."
gcloud run deploy "$FRONTEND_SERVICE" \
--image gcr.io/$PROJECT_ID/erpnext-frontend-cloudrun:latest \
--platform managed \
--region "$REGION" \
--allow-unauthenticated \
--set-env-vars="BACKEND_URL=$BACKEND_HOSTNAME,STORAGE_BUCKET=erpnext-files-$PROJECT_ID" \
--memory 512Mi \
--cpu 1 \
--concurrency 1000 \
--timeout 60 \
--min-instances 0 \
--max-instances 10
# Get frontend service URL
FRONTEND_URL=$(gcloud run services describe "$FRONTEND_SERVICE" --region="$REGION" --format="value(status.url)")
print_success "Frontend service deployed: $FRONTEND_URL"
# Deploy background service
print_status "Deploying background service..."
gcloud run deploy "$BACKGROUND_SERVICE" \
--image gcr.io/$PROJECT_ID/erpnext-background-cloudrun:latest \
--platform managed \
--region "$REGION" \
--no-allow-unauthenticated \
--vpc-connector "$VPC_CONNECTOR" \
--set-env-vars="DB_CONNECTION_NAME=$DB_CONNECTION_NAME,REDIS_HOST=$REDIS_HOST,PROJECT_ID=$PROJECT_ID" \
--set-secrets="DB_PASSWORD=erpnext-db-password:latest" \
--service-account "erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--memory 1Gi \
--cpu 1 \
--concurrency 10 \
--timeout 900 \
--min-instances 0 \
--max-instances 10
if [[ -n "$REDIS_AUTH" ]]; then
gcloud run services update "$BACKGROUND_SERVICE" \
--region "$REGION" \
--update-secrets="REDIS_AUTH=redis-auth-string:latest"
fi
# Get background service URL
BACKGROUND_URL=$(gcloud run services describe "$BACKGROUND_SERVICE" --region="$REGION" --format="value(status.url)")
print_success "Background service deployed: $BACKGROUND_URL"
}
# Function to initialize ERPNext site
initialize_site() {
print_status "Initializing ERPNext site..."
# Create a temporary Cloud Run job to initialize the site
gcloud run jobs create erpnext-init \
--image gcr.io/$PROJECT_ID/erpnext-backend-cloudrun:latest \
--region "$REGION" \
--vpc-connector "$VPC_CONNECTOR" \
--set-env-vars="DB_CONNECTION_NAME=$DB_CONNECTION_NAME,REDIS_HOST=$REDIS_HOST,INIT_SITE=true" \
--set-secrets="ADMIN_PASSWORD=erpnext-admin-password:latest,DB_PASSWORD=erpnext-db-password:latest" \
--service-account "erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--memory 2Gi \
--cpu 2 \
--max-retries 3 \
--parallelism 1 \
--task-count 1 \
--task-timeout 1800
if [[ -n "$REDIS_AUTH" ]]; then
gcloud run jobs update erpnext-init \
--region "$REGION" \
--update-secrets="REDIS_AUTH=redis-auth-string:latest"
fi
# Execute the job
print_status "Executing site initialization job..."
gcloud run jobs execute erpnext-init --region "$REGION" --wait
# Check if job succeeded
local job_status=$(gcloud run jobs describe erpnext-init --region="$REGION" --format="value(status.conditions[0].type)")
if [[ "$job_status" == "Succeeded" ]]; then
print_success "ERPNext site initialized successfully"
else
print_error "Site initialization failed. Check job logs:"
gcloud logging read "resource.type=\"cloud_run_job\" AND resource.labels.job_name=\"erpnext-init\"" --limit=50
exit 1
fi
# Clean up the job
gcloud run jobs delete erpnext-init --region="$REGION" --quiet
}
# Function to setup Cloud Tasks and Cloud Scheduler
setup_background_processing() {
print_status "Setting up Cloud Tasks and Cloud Scheduler..."
# Create task queue
gcloud tasks queues create erpnext-tasks \
--location="$REGION" \
--max-dispatches-per-second=100 \
--max-concurrent-dispatches=1000 \
--max-attempts=3 || true
# Create scheduled jobs
print_status "Creating scheduled jobs..."
# ERPNext scheduler (every 5 minutes)
gcloud scheduler jobs create http erpnext-scheduler \
--location="$REGION" \
--schedule="*/5 * * * *" \
--uri="$BACKGROUND_URL/process-task" \
--http-method=POST \
--headers="Content-Type=application/json" \
--message-body='{"job_name": "frappe.utils.scheduler.execute_all", "kwargs": {}}' \
--oidc-service-account-email="erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--quiet || true
# Email queue processor (every minute)
gcloud scheduler jobs create http erpnext-email-queue \
--location="$REGION" \
--schedule="* * * * *" \
--uri="$BACKGROUND_URL/process-task" \
--http-method=POST \
--headers="Content-Type=application/json" \
--message-body='{"job_name": "frappe.email.queue.flush", "kwargs": {}}' \
--oidc-service-account-email="erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--quiet || true
# Daily backup job
gcloud scheduler jobs create http erpnext-daily-backup \
--location="$REGION" \
--schedule="0 2 * * *" \
--uri="$BACKGROUND_URL/process-task" \
--http-method=POST \
--headers="Content-Type=application/json" \
--message-body='{"job_name": "frappe.utils.backup.backup_to_cloud", "kwargs": {}}' \
--oidc-service-account-email="erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--quiet || true
print_success "Background processing setup completed"
}
# Function to setup custom domain and load balancer
setup_custom_domain() {
print_status "Setting up custom domain and load balancer..."
# Create health check
gcloud compute health-checks create http erpnext-health-check \
--request-path="/health" \
--port=8080 \
--quiet || true
# Create backend service for frontend
gcloud compute backend-services create erpnext-frontend-backend \
--protocol=HTTP \
--health-checks=erpnext-health-check \
--global \
--quiet || true
# Create NEG for frontend service
gcloud compute network-endpoint-groups create erpnext-frontend-neg \
--region="$REGION" \
--network-endpoint-type=serverless \
--cloud-run-service="$FRONTEND_SERVICE" \
--quiet || true
gcloud compute backend-services add-backend erpnext-frontend-backend \
--global \
--network-endpoint-group=erpnext-frontend-neg \
--network-endpoint-group-region="$REGION" \
--quiet || true
# Create URL map
gcloud compute url-maps create erpnext-url-map \
--default-service=erpnext-frontend-backend \
--quiet || true
# Create managed SSL certificate
gcloud compute ssl-certificates create erpnext-ssl-cert \
--domains="$DOMAIN" \
--quiet || true
# Create HTTPS proxy
gcloud compute target-https-proxies create erpnext-https-proxy \
--ssl-certificates=erpnext-ssl-cert \
--url-map=erpnext-url-map \
--quiet || true
# Create global forwarding rule
gcloud compute forwarding-rules create erpnext-https-rule \
--global \
--target-https-proxy=erpnext-https-proxy \
--ports=443 \
--quiet || true
# Get global IP address
GLOBAL_IP=$(gcloud compute forwarding-rules describe erpnext-https-rule --global --format="value(IPAddress)")
print_success "Custom domain setup completed"
print_warning "Point your domain $DOMAIN to IP address: $GLOBAL_IP"
}
# Function to show deployment status
show_status() {
print_status "Cloud Run deployment status:"
echo ""
echo "=== Cloud Run Services ==="
gcloud run services list --region="$REGION" --filter="metadata.name:erpnext"
echo ""
echo "=== Managed Services ==="
echo "Cloud SQL:"
gcloud sql instances describe "$DB_INSTANCE_NAME" --format="table(name,state,region)"
echo ""
echo "Redis:"
gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" --format="table(name,state,host)"
echo ""
echo "=== Cloud Scheduler Jobs ==="
gcloud scheduler jobs list --location="$REGION" --filter="name:erpnext"
echo ""
echo "=== Load Balancer ==="
gcloud compute forwarding-rules list --global --filter="name:erpnext"
echo ""
echo "=== Service URLs ==="
echo "Frontend: $(gcloud run services describe "$FRONTEND_SERVICE" --region="$REGION" --format="value(status.url)")"
echo "Backend API: $(gcloud run services describe "$BACKEND_SERVICE" --region="$REGION" --format="value(status.url)")"
echo "Custom Domain: https://$DOMAIN (if DNS is configured)"
}
# Function to cleanup deployment
cleanup() {
print_warning "This will delete the Cloud Run deployment but preserve managed services. Are you sure? (y/N)"
read -r response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])$ ]]; then
print_status "Cleaning up Cloud Run deployment..."
# Delete Cloud Run services
gcloud run services delete "$FRONTEND_SERVICE" --region="$REGION" --quiet || true
gcloud run services delete "$BACKEND_SERVICE" --region="$REGION" --quiet || true
gcloud run services delete "$BACKGROUND_SERVICE" --region="$REGION" --quiet || true
# Delete Cloud Scheduler jobs
gcloud scheduler jobs delete erpnext-scheduler --location="$REGION" --quiet || true
gcloud scheduler jobs delete erpnext-email-queue --location="$REGION" --quiet || true
gcloud scheduler jobs delete erpnext-daily-backup --location="$REGION" --quiet || true
# Delete Cloud Tasks queue
gcloud tasks queues delete erpnext-tasks --location="$REGION" --quiet || true
# Delete load balancer components
gcloud compute forwarding-rules delete erpnext-https-rule --global --quiet || true
gcloud compute target-https-proxies delete erpnext-https-proxy --quiet || true
gcloud compute ssl-certificates delete erpnext-ssl-cert --quiet || true
gcloud compute url-maps delete erpnext-url-map --quiet || true
gcloud compute backend-services delete erpnext-frontend-backend --global --quiet || true
gcloud compute network-endpoint-groups delete erpnext-frontend-neg --region="$REGION" --quiet || true
gcloud compute health-checks delete erpnext-health-check --quiet || true
print_success "Cloud Run deployment cleaned up"
print_warning "Managed services (Cloud SQL, Redis) and container images are preserved"
else
print_status "Cleanup cancelled"
fi
}
# Function to show help
show_help() {
echo "ERPNext Cloud Run Deployment Script with Managed Services"
echo ""
echo "Usage: $0 [COMMAND]"
echo ""
echo "Commands:"
echo " deploy - Full Cloud Run deployment (default)"
echo " status - Show deployment status"
echo " cleanup - Delete deployment (preserves managed services)"
echo " help - Show this help"
echo ""
echo "Environment Variables:"
echo " PROJECT_ID - GCP Project ID"
echo " REGION - GCP region (default: us-central1)"
echo " DOMAIN - Domain name (default: erpnext.yourdomain.com)"
echo " API_DOMAIN - API domain (default: api.yourdomain.com)"
echo " DB_INSTANCE_NAME - Cloud SQL instance (default: erpnext-db)"
echo " REDIS_INSTANCE_NAME - Memorystore instance (default: erpnext-redis)"
echo " VPC_NAME - VPC network (default: erpnext-vpc)"
echo " VPC_CONNECTOR - VPC connector (default: erpnext-connector)"
echo ""
echo "Prerequisites:"
echo " - Complete setup in 00-prerequisites-managed.md"
echo " - Cloud SQL and Memorystore instances must exist"
echo " - VPC network with VPC Access Connector"
echo ""
echo "Example:"
echo " PROJECT_ID=my-project DOMAIN=erp.mycompany.com $0 deploy"
}
# Main deployment function
main_deploy() {
print_status "Starting ERPNext Cloud Run deployment with managed services..."
check_prerequisites
check_managed_services
get_managed_services_info
setup_cloud_storage
setup_secrets
build_and_push_images
deploy_cloud_run_services
initialize_site
setup_background_processing
setup_custom_domain
print_success "Cloud Run deployment completed successfully!"
echo ""
print_status "Service URLs:"
print_status " Frontend: $FRONTEND_URL"
print_status " Backend API: $BACKEND_URL"
print_status " Custom Domain: https://$DOMAIN (after DNS configuration)"
echo ""
print_status "Managed Services:"
print_status " Cloud SQL: $DB_CONNECTION_NAME"
print_status " Redis: $REDIS_HOST:6379"
print_status " Storage: gs://erpnext-files-$PROJECT_ID"
echo ""
print_warning "Configure DNS to point $DOMAIN to $GLOBAL_IP"
print_warning "Retrieve admin password: gcloud secrets versions access latest --secret=erpnext-admin-password"
}
# Main script logic
case "${1:-deploy}" in
"deploy")
main_deploy
;;
"status")
show_status
;;
"cleanup")
cleanup
;;
"help"|"-h"|"--help")
show_help
;;
*)
print_error "Unknown command: $1"
show_help
exit 1
;;
esac

View File

@ -0,0 +1,683 @@
#!/bin/bash
# ERPNext GKE Deployment Script with Managed Services
# This script automates the deployment of ERPNext on GKE using Cloud SQL and Memorystore
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-managed-cluster"}
ZONE=${ZONE:-"us-central1-a"}
REGION=${REGION:-"us-central1"}
PROJECT_ID=${PROJECT_ID:-""}
DOMAIN=${DOMAIN:-"erpnext.yourdomain.com"}
EMAIL=${EMAIL:-"admin@yourdomain.com"}
NAMESPACE=${NAMESPACE:-"erpnext"}
# Managed services configuration
DB_INSTANCE_NAME=${DB_INSTANCE_NAME:-"erpnext-db"}
REDIS_INSTANCE_NAME=${REDIS_INSTANCE_NAME:-"erpnext-redis"}
VPC_NAME=${VPC_NAME:-"erpnext-vpc"}
VPC_CONNECTOR=${VPC_CONNECTOR:-"erpnext-connector"}
# 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 for managed services deployment..."
# 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 check managed services
check_managed_services() {
print_status "Checking managed services status..."
# Check Cloud SQL instance
if ! gcloud sql instances describe "$DB_INSTANCE_NAME" &> /dev/null; then
print_error "Cloud SQL instance '$DB_INSTANCE_NAME' not found. Please create it first."
exit 1
fi
# Check Memorystore Redis instance
if ! gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" &> /dev/null; then
print_error "Memorystore Redis instance '$REDIS_INSTANCE_NAME' not found. Please create it first."
exit 1
fi
# Check VPC network
if ! gcloud compute networks describe "$VPC_NAME" &> /dev/null; then
print_error "VPC network '$VPC_NAME' not found. Please create it first."
exit 1
fi
print_success "All managed services are available"
}
# Function to get managed services information
get_managed_services_info() {
print_status "Gathering managed services information..."
# Get Cloud SQL connection name
DB_CONNECTION_NAME=$(gcloud sql instances describe "$DB_INSTANCE_NAME" --format="value(connectionName)")
print_status "Cloud SQL connection name: $DB_CONNECTION_NAME"
# Get Redis host IP
REDIS_HOST=$(gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" --format="value(host)")
print_status "Redis host IP: $REDIS_HOST"
# Get VPC connector
if ! gcloud compute networks vpc-access connectors describe "$VPC_CONNECTOR" --region="$REGION" &> /dev/null; then
print_warning "VPC connector '$VPC_CONNECTOR' not found. This is needed for Cloud Run deployment."
fi
print_success "Managed services information gathered"
}
# Function to create GKE cluster for managed services
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=15 \
--enable-autorepair \
--enable-autoupgrade \
--enable-network-policy \
--enable-ip-alias \
--network="$VPC_NAME" \
--subnetwork=erpnext-subnet \
--enable-private-nodes \
--master-ipv4-cidr-block=172.16.0.0/28 \
--enable-cloud-logging \
--enable-cloud-monitoring \
--workload-pool="$PROJECT_ID.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
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 required operators and controllers
install_operators() {
print_status "Installing required operators and controllers..."
# Install External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
-n external-secrets-system \
--create-namespace \
--wait
# Install 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
# Install 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 "Operators and controllers installed"
}
# Function to create namespace and basic resources
create_namespace() {
print_status "Creating namespace and basic resources..."
# Update namespace.yaml with correct labels
cp ../kubernetes-manifests/namespace.yaml /tmp/namespace-updated.yaml
kubectl apply -f /tmp/namespace-updated.yaml
rm /tmp/namespace-updated.yaml
print_success "Namespace created"
}
# Function to create service account and workload identity
setup_workload_identity() {
print_status "Setting up Workload Identity..."
# Create Kubernetes service account
kubectl create serviceaccount erpnext-ksa --namespace="$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
# Create Google Cloud service account if it doesn't exist
if ! gcloud iam service-accounts describe "erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" &> /dev/null; then
gcloud iam service-accounts create erpnext-managed \
--display-name="ERPNext Managed Services Account"
fi
# Grant necessary permissions
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/cloudsql.client"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/redis.editor"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/storage.admin"
# Bind service accounts
gcloud iam service-accounts add-iam-policy-binding \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:$PROJECT_ID.svc.id.goog[$NAMESPACE/erpnext-ksa]" \
"erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com"
# Annotate Kubernetes service account
kubectl annotate serviceaccount erpnext-ksa \
--namespace="$NAMESPACE" \
"iam.gke.io/gcp-service-account=erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com" \
--overwrite
print_success "Workload Identity configured"
}
# Function to create secrets using External Secrets Operator
create_secrets() {
print_status "Creating secrets with External Secrets Operator..."
# Generate random passwords if secrets don't exist in Secret Manager
if ! gcloud secrets describe erpnext-admin-password &> /dev/null; then
local admin_password=${ADMIN_PASSWORD:-$(openssl rand -base64 32)}
gcloud secrets create erpnext-admin-password --data-file=<(echo -n "$admin_password")
print_warning "Admin password: $admin_password"
fi
if ! gcloud secrets describe erpnext-db-password &> /dev/null; then
local db_password=${DB_PASSWORD:-$(openssl rand -base64 32)}
gcloud secrets create erpnext-db-password --data-file=<(echo -n "$db_password")
print_warning "Database password: $db_password"
fi
if ! gcloud secrets describe erpnext-api-key &> /dev/null; then
local api_key=${API_KEY:-$(openssl rand -hex 32)}
gcloud secrets create erpnext-api-key --data-file=<(echo -n "$api_key")
fi
if ! gcloud secrets describe erpnext-api-secret &> /dev/null; then
local api_secret=${API_SECRET:-$(openssl rand -hex 32)}
gcloud secrets create erpnext-api-secret --data-file=<(echo -n "$api_secret")
fi
# Create connection name secret
gcloud secrets create erpnext-db-connection-name --data-file=<(echo -n "$DB_CONNECTION_NAME") --quiet || \
gcloud secrets versions add erpnext-db-connection-name --data-file=<(echo -n "$DB_CONNECTION_NAME")
# Get Redis AUTH string if enabled
local redis_auth=""
if gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" --format="value(authEnabled)" | grep -q "True"; then
redis_auth=$(gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" --format="value(authString)")
gcloud secrets create redis-auth-string --data-file=<(echo -n "$redis_auth") --quiet || \
gcloud secrets versions add redis-auth-string --data-file=<(echo -n "$redis_auth")
fi
# Apply External Secrets configuration
cp ../kubernetes-manifests/secrets.yaml /tmp/secrets-updated.yaml
sed -i "s/PROJECT_ID/$PROJECT_ID/g" /tmp/secrets-updated.yaml
sed -i "s/REGION/$REGION/g" /tmp/secrets-updated.yaml
sed -i "s/erpnext-managed-cluster/$CLUSTER_NAME/g" /tmp/secrets-updated.yaml
kubectl apply -f /tmp/secrets-updated.yaml
rm /tmp/secrets-updated.yaml
# Wait for External Secrets to sync
print_status "Waiting for secrets to be synced..."
sleep 30
# Create service account key for Cloud SQL Proxy
if ! kubectl get secret gcp-service-account-key -n "$NAMESPACE" &> /dev/null; then
local key_file="/tmp/erpnext-managed-key.json"
gcloud iam service-accounts keys create "$key_file" \
--iam-account="erpnext-managed@$PROJECT_ID.iam.gserviceaccount.com"
kubectl create secret generic gcp-service-account-key \
--namespace="$NAMESPACE" \
--from-file=key.json="$key_file"
rm "$key_file"
fi
print_success "Secrets created"
print_warning "Please save the generated credentials securely!"
}
# Function to update ConfigMap with managed services configuration
update_configmap() {
print_status "Updating ConfigMap with managed services configuration..."
# Copy and update configmap
cp ../kubernetes-manifests/configmap.yaml /tmp/configmap-updated.yaml
sed -i "s/erpnext.yourdomain.com/$DOMAIN/g" /tmp/configmap-updated.yaml
sed -i "s/PROJECT_ID/$PROJECT_ID/g" /tmp/configmap-updated.yaml
sed -i "s/REGION/$REGION/g" /tmp/configmap-updated.yaml
sed -i "s/REDIS_HOST/$REDIS_HOST/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 ERPNext application with managed services
deploy_application() {
print_status "Deploying ERPNext application with managed services..."
# Update manifests with managed services configuration
local manifests=("erpnext-backend.yaml" "erpnext-frontend.yaml" "erpnext-workers.yaml")
for manifest in "${manifests[@]}"; do
cp "../kubernetes-manifests/$manifest" "/tmp/${manifest%.yaml}-updated.yaml"
sed -i "s/PROJECT_ID/$PROJECT_ID/g" "/tmp/${manifest%.yaml}-updated.yaml"
sed -i "s/REDIS_HOST/$REDIS_HOST/g" "/tmp/${manifest%.yaml}-updated.yaml"
kubectl apply -f "/tmp/${manifest%.yaml}-updated.yaml"
rm "/tmp/${manifest%.yaml}-updated.yaml"
done
# 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 with managed services..."
# Update jobs manifest
cp ../kubernetes-manifests/jobs.yaml /tmp/jobs-updated.yaml
sed -i "s/PROJECT_ID_PLACEHOLDER/$PROJECT_ID/g" /tmp/jobs-updated.yaml
sed -i "s/REDIS_HOST_PLACEHOLDER/$REDIS_HOST/g" /tmp/jobs-updated.yaml
# Apply site creation job
kubectl apply -f /tmp/jobs-updated.yaml
rm /tmp/jobs-updated.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=1200s
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 with managed services"
else
print_error "Site creation failed. Check job logs:"
kubectl logs job/erpnext-create-site -n "$NAMESPACE"
exit 1
fi
}
# Function to deploy ingress with managed services optimization
deploy_ingress() {
print_status "Deploying ingress with managed services optimization..."
# Update ingress with correct domains and managed services configuration
cp ../kubernetes-manifests/ingress.yaml /tmp/ingress-updated.yaml
sed -i "s/erpnext.yourdomain.com/$DOMAIN/g" /tmp/ingress-updated.yaml
sed -i "s/api.yourdomain.com/api.$DOMAIN/g" /tmp/ingress-updated.yaml
sed -i "s/admin@yourdomain.com/$EMAIL/g" /tmp/ingress-updated.yaml
sed -i "s/PROJECT_ID/$PROJECT_ID/g" /tmp/ingress-updated.yaml
kubectl apply -f /tmp/ingress-updated.yaml
rm /tmp/ingress-updated.yaml
print_success "Ingress deployed with managed services optimization"
}
# Function to setup monitoring for managed services
setup_monitoring() {
print_status "Setting up monitoring for managed services..."
# Install Prometheus stack with managed services configuration
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
# Create values file for managed services monitoring
cat > /tmp/prometheus-values.yaml <<EOF
prometheus:
prometheusSpec:
storageSpec:
volumeClaimTemplate:
spec:
resources:
requests:
storage: 50Gi
retention: 30d
additionalScrapeConfigs:
- job_name: 'cloud-sql-exporter'
static_configs:
- targets: ['cloud-sql-exporter:9308']
- job_name: 'redis-exporter'
static_configs:
- targets: ['redis-exporter:9121']
grafana:
adminPassword: SecurePassword123!
dashboardProviders:
dashboardproviders.yaml:
apiVersion: 1
providers:
- name: 'erpnext-managed'
orgId: 1
folder: 'ERPNext Managed Services'
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards/erpnext-managed
dashboards:
erpnext-managed:
cloud-sql-dashboard:
gnetId: 14114
revision: 1
datasource: Prometheus
redis-dashboard:
gnetId: 763
revision: 4
datasource: Prometheus
alertmanager:
alertmanagerSpec:
storage:
volumeClaimTemplate:
spec:
resources:
requests:
storage: 10Gi
EOF
helm install prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--values /tmp/prometheus-values.yaml \
--wait
rm /tmp/prometheus-values.yaml
print_success "Monitoring setup completed for managed services"
}
# Function to create backup bucket and setup backup jobs
setup_backup() {
print_status "Setting up backup for managed services..."
# Create backup bucket
gsutil mb gs://erpnext-backups-$PROJECT_ID || true
# Set lifecycle policy
gsutil lifecycle set - gs://erpnext-backups-$PROJECT_ID <<EOF
{
"lifecycle": {
"rule": [
{
"action": {"type": "Delete"},
"condition": {"age": 90}
},
{
"action": {"type": "SetStorageClass", "storageClass": "NEARLINE"},
"condition": {"age": 30}
},
{
"action": {"type": "SetStorageClass", "storageClass": "COLDLINE"},
"condition": {"age": 60}
}
]
}
}
EOF
# Backup jobs are already included in jobs.yaml and will be deployed
print_success "Backup setup completed (Cloud SQL automated backups + file backups)"
}
# Function to get deployment status
get_status() {
print_status "Getting deployment status..."
echo ""
echo "=== Cluster Information ==="
kubectl cluster-info
echo ""
echo "=== Managed Services Status ==="
echo "Cloud SQL Instance:"
gcloud sql instances describe "$DB_INSTANCE_NAME" --format="table(name,state,region,databaseVersion)"
echo ""
echo "Redis Instance:"
gcloud redis instances describe "$REDIS_INSTANCE_NAME" --region="$REGION" --format="table(name,state,host,port)"
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
echo ""
echo "=== Secrets Status ==="
kubectl get externalsecret -n "$NAMESPACE" 2>/dev/null || echo "External Secrets not found"
}
# Function to cleanup deployment
cleanup() {
print_warning "This will delete the entire ERPNext deployment but preserve managed services. 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_warning "Managed services (Cloud SQL, Redis) are preserved."
print_warning "To delete them manually:"
print_warning " gcloud sql instances delete $DB_INSTANCE_NAME"
print_warning " gcloud redis instances delete $REDIS_INSTANCE_NAME --region=$REGION"
print_success "Cleanup completed"
else
print_status "Cleanup cancelled"
fi
}
# Function to show help
show_help() {
echo "ERPNext GKE Deployment Script with Managed Services"
echo ""
echo "Usage: $0 [COMMAND]"
echo ""
echo "Commands:"
echo " deploy - Full deployment with managed services (default)"
echo " status - Show deployment status"
echo " cleanup - Delete deployment (preserves managed services)"
echo " help - Show this help"
echo ""
echo "Environment Variables:"
echo " PROJECT_ID - GCP Project ID"
echo " CLUSTER_NAME - GKE cluster name (default: erpnext-managed-cluster)"
echo " ZONE - GCP zone (default: us-central1-a)"
echo " REGION - GCP region (default: us-central1)"
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 " DB_INSTANCE_NAME - Cloud SQL instance name (default: erpnext-db)"
echo " REDIS_INSTANCE_NAME - Memorystore instance name (default: erpnext-redis)"
echo " VPC_NAME - VPC network name (default: erpnext-vpc)"
echo ""
echo "Prerequisites:"
echo " - Complete setup in 00-prerequisites-managed.md"
echo " - Cloud SQL and Memorystore instances must exist"
echo " - VPC network with proper configuration"
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 with managed services..."
check_prerequisites
check_managed_services
get_managed_services_info
create_cluster
configure_kubectl
install_operators
create_namespace
setup_workload_identity
create_secrets
update_configmap
deploy_storage
deploy_application
create_site
deploy_ingress
setup_monitoring
setup_backup
print_success "Deployment completed successfully with managed services!"
echo ""
print_status "Access your ERPNext instance at: https://$DOMAIN"
print_status "API endpoint: https://api.$DOMAIN"
echo ""
print_status "Managed Services:"
print_status " Cloud SQL: $DB_CONNECTION_NAME"
print_status " Redis: $REDIS_HOST:6379"
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"
echo ""
print_status "Default credentials are stored in Secret Manager."
print_status "Retrieve admin password with: gcloud secrets versions access latest --secret=erpnext-admin-password"
}
# 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