docker-erpnext/documentation/deployment-guides/azure-managed/01-aks-managed-deployment.md
Brian Tan Seng 6301b2ef4b ⏺ Perfect! I've successfully created the complete Azure equivalent of the GCP managed deployment
guides. Here's what has been created:

  📁 Azure Managed Deployment Structure

  documentation/deployment-guides/azure-managed/
  ├── README.md                                    # Overview and architecture
  ├── 00-prerequisites-managed.md                  # Azure prerequisites setup
  ├── 01-aks-managed-deployment.md                # AKS deployment guide
  ├── 02-container-instances-deployment.md        # Container Instances guide
  ├── 03-production-managed-setup.md              # Production hardening
  ├── kubernetes-manifests/                       # Kubernetes YAML files
  │   ├── namespace.yaml                          # Namespace and quotas
  │   ├── storage.yaml                            # Azure Files storage
  │   ├── configmap.yaml                          # Configuration
  │   ├── secrets.yaml                            # Key Vault integration
  │   ├── erpnext-backend.yaml                    # Backend deployment
  │   ├── erpnext-frontend.yaml                   # Frontend deployment
  │   ├── erpnext-workers.yaml                    # Workers and scheduler
  │   ├── ingress.yaml                            # Ingress configuration
  │   └── jobs.yaml                               # Init and backup jobs
  └── scripts/                                    # Automation scripts
      ├── deploy-managed.sh                       # AKS deployment script
      └── container-instances-deploy.sh           # ACI deployment script

  🔄 Key Azure Service Mappings

  | GCP Service          | Azure Equivalent                     | Notes
                             |
  |----------------------|--------------------------------------|-------------------------------
  ---------------------------|
  | Cloud SQL (MySQL)    | Azure Database for PostgreSQL        | Using PostgreSQL instead of
  MySQL for better performance |
  | Memorystore (Redis)  | Azure Cache for Redis                | Full compatibility with
  SSL/TLS                          |
  | Cloud Storage        | Azure Storage (Blob & Files)         | Using Azure Files for shared
  storage                     |
  | GKE                  | AKS (Azure Kubernetes Service)       | Full Kubernetes compatibility
                             |
  | Cloud Run            | Azure Container Instances            | Serverless container platform
                             |
  | Secret Manager       | Azure Key Vault                      | Integrated secrets management
                             |
  | Cloud IAM            | Azure AD & Managed Identity          | Enterprise-grade identity
                             |
  | Stackdriver          | Azure Monitor & Application Insights | Comprehensive monitoring
                             |
  | Cloud CDN            | Azure CDN                            | Global content delivery
                             |
  | Cloud Load Balancing | Azure Application Gateway            | L7 load balancing with WAF
                             |

   Key Features

  1. Managed Services

  - Azure Database for PostgreSQL with high availability
  - Azure Cache for Redis with SSL/TLS encryption
  - Azure Storage for file management
  - Azure Key Vault for secrets

  2. Deployment Options

  - AKS: Full Kubernetes control with auto-scaling
  - Container Instances: Simpler serverless deployment

  3. Security

  - Managed Identity for passwordless authentication
  - Private endpoints for database and Redis
  - Network security groups and firewall rules
  - SSL/TLS encryption everywhere

  4. Production Ready

  - Automated backups with geo-redundancy
  - Monitoring with Azure Monitor
  - Auto-scaling configurations
  - Disaster recovery setup

  5. Automation

  - One-command deployment scripts
  - Environment validation
  - Health checks and diagnostics
  - Backup automation

  🚀 Quick Start

  # Prerequisites
  cd documentation/deployment-guides/azure-managed/
  # Follow 00-prerequisites-managed.md

  # Option 1: Deploy to AKS
  ./scripts/deploy-managed.sh deploy

  # Option 2: Deploy to Container Instances
  ./scripts/container-instances-deploy.sh deploy

  💰 Cost Comparison

  | Deployment Size   | Azure (Monthly) | GCP (Monthly) |
  |-------------------|-----------------|---------------|
  | Small (<50 users) | ~           | ~         |
  | Medium (50-200)   | ~           | ~         |
  | Large (200+)      | ~,823         | ~,794       |

  The Azure deployment uses PostgreSQL instead of MySQL, which provides better performance and
  features, and includes Azure-specific optimizations for the cloud-native environment.
2025-08-23 13:03:56 +08:00

25 KiB

Azure Kubernetes Service (AKS) Deployment with Managed Services

Overview

This guide covers deploying ERPNext on Azure Kubernetes Service (AKS) using Azure Database for PostgreSQL and Azure Cache for Redis as managed services.

Prerequisites

  • Completed all steps in 00-prerequisites-managed.md
  • Azure CLI installed and configured
  • kubectl and Helm installed
  • Environment variables from prerequisites exported

🚀 AKS Cluster Setup

1. Create AKS Cluster

# Source environment variables
source ~/erpnext-azure-env.sh

# Create AKS cluster
az aks create \
    --name erpnext-aks \
    --resource-group $RESOURCE_GROUP \
    --location $LOCATION \
    --node-count 3 \
    --node-vm-size Standard_D4s_v3 \
    --enable-managed-identity \
    --assign-identity $IDENTITY_ID \
    --network-plugin azure \
    --vnet-subnet-id /subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/virtualNetworks/erpnext-vnet/subnets/aks-subnet \
    --docker-bridge-address 172.17.0.1/16 \
    --dns-service-ip 10.0.10.10 \
    --service-cidr 10.0.10.0/24 \
    --enable-cluster-autoscaler \
    --min-count 3 \
    --max-count 10 \
    --enable-addons monitoring,azure-keyvault-secrets-provider \
    --workspace-resource-id /subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.OperationalInsights/workspaces/erpnext-logs \
    --enable-ahub \
    --generate-ssh-keys

# Get AKS credentials
az aks get-credentials \
    --name erpnext-aks \
    --resource-group $RESOURCE_GROUP \
    --overwrite-existing

# Verify cluster connection
kubectl get nodes

2. Configure Cluster Autoscaler

# Update autoscaler configuration
az aks update \
    --name erpnext-aks \
    --resource-group $RESOURCE_GROUP \
    --cluster-autoscaler-profile \
        scale-down-delay-after-add=10m \
        scale-down-unneeded-time=10m \
        scale-down-utilization-threshold=0.5 \
        skip-nodes-with-local-storage=false \
        max-graceful-termination-sec=600

3. Install NGINX Ingress Controller

# Add Helm repo for ingress-nginx
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

# Install NGINX ingress controller
helm install nginx-ingress ingress-nginx/ingress-nginx \
    --namespace ingress-nginx \
    --create-namespace \
    --set controller.service.type=LoadBalancer \
    --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz \
    --set controller.service.externalTrafficPolicy=Local

# Wait for external IP
kubectl get service -n ingress-nginx nginx-ingress-ingress-nginx-controller -w

4. Install Cert-Manager (for SSL)

# Install cert-manager for Let's Encrypt SSL
helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
    --namespace cert-manager \
    --create-namespace \
    --version v1.12.0 \
    --set installCRDs=true

# Create ClusterIssuer for Let's Encrypt
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ${EMAIL:-admin@example.com}
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx
EOF

🔐 Secrets Management

1. Configure Azure Key Vault Provider

# Enable Key Vault secrets provider
az aks enable-addons \
    --addons azure-keyvault-secrets-provider \
    --name erpnext-aks \
    --resource-group $RESOURCE_GROUP

# Configure SecretProviderClass
cat <<EOF | kubectl apply -f -
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: erpnext-secrets
  namespace: erpnext
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"
    userAssignedIdentityID: "$CLIENT_ID"
    keyvaultName: "$KEYVAULT_NAME"
    objects: |
      array:
        - |
          objectName: erpnext-admin-password
          objectType: secret
        - |
          objectName: erpnext-db-password
          objectType: secret
        - |
          objectName: erpnext-redis-key
          objectType: secret
        - |
          objectName: erpnext-api-key
          objectType: secret
        - |
          objectName: erpnext-api-secret
          objectType: secret
    tenantId: "$(az account show --query tenantId -o tsv)"
  secretObjects:
  - secretName: erpnext-secrets
    type: Opaque
    data:
    - objectName: erpnext-admin-password
      key: admin-password
    - objectName: erpnext-db-password
      key: db-password
    - objectName: erpnext-redis-key
      key: redis-key
    - objectName: erpnext-api-key
      key: api-key
    - objectName: erpnext-api-secret
      key: api-secret
EOF

2. Create ConfigMap for Database Connection

# Create namespace
kubectl create namespace erpnext

# Create ConfigMap with connection details
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: erpnext-config
  namespace: erpnext
data:
  DB_HOST: "$DB_SERVER_NAME.postgres.database.azure.com"
  DB_PORT: "5432"
  DB_NAME: "erpnext"
  DB_USER: "$DB_ADMIN_USER"
  REDIS_HOST: "$REDIS_HOST"
  REDIS_PORT: "6380"
  STORAGE_ACCOUNT: "$STORAGE_ACCOUNT"
  STORAGE_CONTAINER: "erpnext-files"
EOF

📦 Deploy ERPNext

1. Create Persistent Volume Claims

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: erpnext-sites
  namespace: erpnext
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: azurefile-csi
  resources:
    requests:
      storage: 50Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: erpnext-assets
  namespace: erpnext
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: azurefile-csi
  resources:
    requests:
      storage: 20Gi
EOF

2. Deploy Backend Service

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: erpnext-backend
  namespace: erpnext
spec:
  replicas: 2
  selector:
    matchLabels:
      app: erpnext-backend
  template:
    metadata:
      labels:
        app: erpnext-backend
    spec:
      serviceAccountName: default
      volumes:
      - name: sites
        persistentVolumeClaim:
          claimName: erpnext-sites
      - name: assets
        persistentVolumeClaim:
          claimName: erpnext-assets
      - name: secrets-store
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: erpnext-secrets
      containers:
      - name: backend
        image: frappe/erpnext-worker:v14
        ports:
        - containerPort: 8000
        volumeMounts:
        - name: sites
          mountPath: /home/frappe/frappe-bench/sites
        - name: assets
          mountPath: /home/frappe/frappe-bench/sites/assets
        - name: secrets-store
          mountPath: /mnt/secrets-store
          readOnly: true
        env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_HOST
        - name: DB_PORT
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_PORT
        - name: DB_NAME
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_NAME
        - name: POSTGRES_USER
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_USER
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: db-password
        - name: REDIS_CACHE
          value: "redis://:$(REDIS_PASSWORD)@$(REDIS_HOST):$(REDIS_PORT)/0"
        - name: REDIS_QUEUE
          value: "redis://:$(REDIS_PASSWORD)@$(REDIS_HOST):$(REDIS_PORT)/1"
        - name: REDIS_SOCKETIO
          value: "redis://:$(REDIS_PASSWORD)@$(REDIS_HOST):$(REDIS_PORT)/2"
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: redis-key
        - name: REDIS_HOST
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: REDIS_HOST
        - name: REDIS_PORT
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: REDIS_PORT
        - name: ADMIN_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: admin-password
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 2000m
            memory: 4Gi
---
apiVersion: v1
kind: Service
metadata:
  name: erpnext-backend
  namespace: erpnext
spec:
  selector:
    app: erpnext-backend
  ports:
  - port: 8000
    targetPort: 8000
  type: ClusterIP
EOF

3. Deploy Frontend Service

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: erpnext-frontend
  namespace: erpnext
spec:
  replicas: 2
  selector:
    matchLabels:
      app: erpnext-frontend
  template:
    metadata:
      labels:
        app: erpnext-frontend
    spec:
      volumes:
      - name: sites
        persistentVolumeClaim:
          claimName: erpnext-sites
      - name: assets
        persistentVolumeClaim:
          claimName: erpnext-assets
      containers:
      - name: frontend
        image: frappe/erpnext-nginx:v14
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: sites
          mountPath: /home/frappe/frappe-bench/sites
        - name: assets
          mountPath: /usr/share/nginx/html/assets
        env:
        - name: BACKEND
          value: erpnext-backend:8000
        - name: FRAPPE_SITE_NAME_HEADER
          value: "frontend"
        - name: SOCKETIO
          value: erpnext-websocket:9000
        - name: UPSTREAM_REAL_IP_ADDRESS
          value: "127.0.0.1"
        - name: UPSTREAM_REAL_IP_HEADER
          value: "X-Forwarded-For"
        - name: UPSTREAM_REAL_IP_RECURSIVE
          value: "on"
        - name: PROXY_READ_TIMEOUT
          value: "120"
        - name: CLIENT_MAX_BODY_SIZE
          value: "50m"
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: erpnext-frontend
  namespace: erpnext
spec:
  selector:
    app: erpnext-frontend
  ports:
  - port: 8080
    targetPort: 8080
  type: ClusterIP
EOF

4. Deploy Workers

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: erpnext-worker-default
  namespace: erpnext
spec:
  replicas: 2
  selector:
    matchLabels:
      app: erpnext-worker-default
  template:
    metadata:
      labels:
        app: erpnext-worker-default
    spec:
      volumes:
      - name: sites
        persistentVolumeClaim:
          claimName: erpnext-sites
      - name: secrets-store
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: erpnext-secrets
      containers:
      - name: worker
        image: frappe/erpnext-worker:v14
        command: ["bench", "worker", "--queue", "default"]
        volumeMounts:
        - name: sites
          mountPath: /home/frappe/frappe-bench/sites
        - name: secrets-store
          mountPath: /mnt/secrets-store
          readOnly: true
        env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_HOST
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: db-password
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: redis-key
        resources:
          requests:
            cpu: 300m
            memory: 512Mi
          limits:
            cpu: 1000m
            memory: 1Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: erpnext-worker-long
  namespace: erpnext
spec:
  replicas: 1
  selector:
    matchLabels:
      app: erpnext-worker-long
  template:
    metadata:
      labels:
        app: erpnext-worker-long
    spec:
      volumes:
      - name: sites
        persistentVolumeClaim:
          claimName: erpnext-sites
      - name: secrets-store
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: erpnext-secrets
      containers:
      - name: worker
        image: frappe/erpnext-worker:v14
        command: ["bench", "worker", "--queue", "long"]
        volumeMounts:
        - name: sites
          mountPath: /home/frappe/frappe-bench/sites
        - name: secrets-store
          mountPath: /mnt/secrets-store
          readOnly: true
        env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_HOST
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: db-password
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: redis-key
        resources:
          requests:
            cpu: 300m
            memory: 512Mi
          limits:
            cpu: 1000m
            memory: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: erpnext-worker-short
  namespace: erpnext
spec:
  replicas: 1
  selector:
    matchLabels:
      app: erpnext-worker-short
  template:
    metadata:
      labels:
        app: erpnext-worker-short
    spec:
      volumes:
      - name: sites
        persistentVolumeClaim:
          claimName: erpnext-sites
      - name: secrets-store
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: erpnext-secrets
      containers:
      - name: worker
        image: frappe/erpnext-worker:v14
        command: ["bench", "worker", "--queue", "short"]
        volumeMounts:
        - name: sites
          mountPath: /home/frappe/frappe-bench/sites
        - name: secrets-store
          mountPath: /mnt/secrets-store
          readOnly: true
        env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_HOST
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: db-password
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: redis-key
        resources:
          requests:
            cpu: 300m
            memory: 512Mi
          limits:
            cpu: 1000m
            memory: 1Gi
EOF

5. Deploy Scheduler

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: erpnext-scheduler
  namespace: erpnext
spec:
  replicas: 1
  selector:
    matchLabels:
      app: erpnext-scheduler
  template:
    metadata:
      labels:
        app: erpnext-scheduler
    spec:
      volumes:
      - name: sites
        persistentVolumeClaim:
          claimName: erpnext-sites
      - name: secrets-store
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: erpnext-secrets
      containers:
      - name: scheduler
        image: frappe/erpnext-worker:v14
        command: ["bench", "schedule"]
        volumeMounts:
        - name: sites
          mountPath: /home/frappe/frappe-bench/sites
        - name: secrets-store
          mountPath: /mnt/secrets-store
          readOnly: true
        env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_HOST
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: db-password
        - name: REDIS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: redis-key
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
EOF

6. Deploy WebSocket Service

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: erpnext-websocket
  namespace: erpnext
spec:
  replicas: 2
  selector:
    matchLabels:
      app: erpnext-websocket
  template:
    metadata:
      labels:
        app: erpnext-websocket
    spec:
      volumes:
      - name: sites
        persistentVolumeClaim:
          claimName: erpnext-sites
      containers:
      - name: websocket
        image: frappe/frappe-socketio:v14
        ports:
        - containerPort: 9000
        volumeMounts:
        - name: sites
          mountPath: /home/frappe/frappe-bench/sites
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
---
apiVersion: v1
kind: Service
metadata:
  name: erpnext-websocket
  namespace: erpnext
spec:
  selector:
    app: erpnext-websocket
  ports:
  - port: 9000
    targetPort: 9000
  type: ClusterIP
EOF

🌐 Configure Ingress

# Create Ingress with SSL
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: erpnext-ingress
  namespace: erpnext
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "120"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - ${DOMAIN:-erpnext.example.com}
    secretName: erpnext-tls
  rules:
  - host: ${DOMAIN:-erpnext.example.com}
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: erpnext-frontend
            port:
              number: 8080
      - path: /socket.io
        pathType: Prefix
        backend:
          service:
            name: erpnext-websocket
            port:
              number: 9000
EOF

🔄 Initialize ERPNext Site

# Create site initialization job
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
  name: erpnext-site-init
  namespace: erpnext
spec:
  template:
    spec:
      restartPolicy: Never
      volumes:
      - name: sites
        persistentVolumeClaim:
          claimName: erpnext-sites
      - name: secrets-store
        csi:
          driver: secrets-store.csi.k8s.io
          readOnly: true
          volumeAttributes:
            secretProviderClass: erpnext-secrets
      containers:
      - name: init
        image: frappe/erpnext-worker:v14
        command: 
        - /bin/bash
        - -c
        - |
          bench new-site frontend \
            --db-host $DB_HOST \
            --db-port 5432 \
            --db-name erpnext \
            --db-password \$POSTGRES_PASSWORD \
            --admin-password \$ADMIN_PASSWORD \
            --install-app erpnext
          bench --site frontend migrate
        volumeMounts:
        - name: sites
          mountPath: /home/frappe/frappe-bench/sites
        - name: secrets-store
          mountPath: /mnt/secrets-store
          readOnly: true
        env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: erpnext-config
              key: DB_HOST
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: db-password
        - name: ADMIN_PASSWORD
          valueFrom:
            secretKeyRef:
              name: erpnext-secrets
              key: admin-password
EOF

# Wait for job completion
kubectl wait --for=condition=complete --timeout=600s job/erpnext-site-init -n erpnext

📊 Configure Horizontal Pod Autoscaling

# Create HPA for backend
kubectl autoscale deployment erpnext-backend \
    --namespace erpnext \
    --cpu-percent=70 \
    --min=2 \
    --max=10

# Create HPA for frontend
kubectl autoscale deployment erpnext-frontend \
    --namespace erpnext \
    --cpu-percent=70 \
    --min=2 \
    --max=5

# Create HPA for workers
kubectl autoscale deployment erpnext-worker-default \
    --namespace erpnext \
    --cpu-percent=70 \
    --min=2 \
    --max=8

🔍 Monitoring and Logging

1. Enable Application Insights

# Add Application Insights to deployments
kubectl set env deployment/erpnext-backend \
    -n erpnext \
    APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=$INSTRUMENTATION_KEY"

2. Create Azure Monitor Alerts

# Create alert for high CPU usage
az monitor metrics alert create \
    --name erpnext-high-cpu \
    --resource-group $RESOURCE_GROUP \
    --scopes /subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ContainerService/managedClusters/erpnext-aks \
    --condition "avg node_cpu_usage_percentage > 80" \
    --window-size 5m \
    --evaluation-frequency 1m

# Create alert for pod failures
az monitor metrics alert create \
    --name erpnext-pod-failures \
    --resource-group $RESOURCE_GROUP \
    --scopes /subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ContainerService/managedClusters/erpnext-aks \
    --condition "sum kube_pod_status_phase{phase='Failed'} > 0" \
    --window-size 5m \
    --evaluation-frequency 1m

3. View Logs

# View backend logs
kubectl logs -f deployment/erpnext-backend -n erpnext

# View all pods in namespace
kubectl get pods -n erpnext -w

# Check pod events
kubectl describe pod -n erpnext

🔧 Troubleshooting

Database Connection Issues

# Test database connection from pod
kubectl run pg-test --rm -i --tty --image=postgres:13 -n erpnext -- \
    psql -h $DB_SERVER_NAME.postgres.database.azure.com -U $DB_ADMIN_USER -d erpnext

# Check secret mounting
kubectl exec -it deployment/erpnext-backend -n erpnext -- ls -la /mnt/secrets-store/

Redis Connection Issues

# Test Redis connection
kubectl run redis-test --rm -i --tty --image=redis:alpine -n erpnext -- \
    redis-cli -h $REDIS_HOST -a $REDIS_KEY ping

Storage Issues

# Check PVC status
kubectl get pvc -n erpnext

# Check storage class
kubectl get storageclass

# Describe PVC for events
kubectl describe pvc erpnext-sites -n erpnext

🚀 Production Optimizations

1. Enable Pod Disruption Budgets

cat <<EOF | kubectl apply -f -
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: erpnext-backend-pdb
  namespace: erpnext
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: erpnext-backend
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: erpnext-frontend-pdb
  namespace: erpnext
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: erpnext-frontend
EOF

2. Configure Resource Quotas

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ResourceQuota
metadata:
  name: erpnext-quota
  namespace: erpnext
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    persistentvolumeclaims: "10"
EOF

3. Network Policies

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: erpnext-network-policy
  namespace: erpnext
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    - podSelector: {}
  egress:
  - to:
    - podSelector: {}
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 5432
    - protocol: TCP
      port: 6380
    - protocol: TCP
      port: 443
    - protocol: TCP
      port: 53
    - protocol: UDP
      port: 53
EOF

📋 Verification Checklist

# Check all pods are running
kubectl get pods -n erpnext

# Verify services
kubectl get svc -n erpnext

# Check ingress
kubectl get ingress -n erpnext

# Test application
curl -I https://${DOMAIN:-erpnext.example.com}

# Check HPA status
kubectl get hpa -n erpnext

# View cluster nodes
kubectl get nodes

# Check cluster autoscaler
kubectl describe configmap cluster-autoscaler-status -n kube-system

🎯 Next Steps

  1. Configure backup strategy (see 03-production-managed-setup.md)
  2. Set up monitoring dashboards
  3. Configure CI/CD pipeline
  4. Implement disaster recovery plan
  5. Performance tuning based on workload

⚠️ Important Notes:

  • Monitor costs regularly in Azure Cost Management
  • Review and rotate secrets periodically
  • Keep AKS cluster and node pools updated
  • Plan maintenance windows for updates
  • Test disaster recovery procedures regularly