A production-ready Helm chart for deploying Symfony applications to Azure Kubernetes Service (AKS) with integrated Azure Key Vault support via Workload Identity.
- PHP-FPM Deployment: Symfony application container with configurable resources
- Nginx Deployment: Web server with optimized configuration for Symfony
- Redis Deployment: In-cluster Redis for caching and message queuing
- Azure Key Vault Integration: Secure secret management using Azure Workload Identity
- MySQL SSL Support: Pre-configured with Azure MySQL CA certificate
- Ingress Configuration: Ready-to-use ingress with TLS support
- Resource Management: Configurable CPU and memory requests/limits
- Health Checks: Built-in liveness and readiness probes
-
Azure Kubernetes Service (AKS) Cluster
- AKS cluster with Workload Identity enabled
- Kubernetes version 1.20 or higher
-
Azure Key Vault
- Key Vault with your application secrets stored
- Access policies configured for the Managed Identity
-
Azure Managed Identity
- User-assigned managed identity created
- Permissions granted to access Key Vault secrets
-
Workload Identity Configuration
- OIDC issuer enabled on your AKS cluster
- Federated identity credential linking ServiceAccount to Managed Identity
- Secrets Store CSI Driver: Installed and configured in your cluster
- Azure Key Vault Provider: Secrets Store CSI driver provider for Azure
- Ingress Controller: Nginx ingress controller (or compatible)
- Cert-Manager: For automatic TLS certificate management (optional but recommended)
az aks update \
--resource-group <your-resource-group> \
--name <your-aks-cluster> \
--enable-oidc-issuer \
--enable-workload-identityaz identity create \
--resource-group <your-resource-group> \
--name <your-managed-identity-name>Note the clientId from the output - you'll need this for the Helm values.
# Get the managed identity principal ID
PRINCIPAL_ID=$(az identity show \
--resource-group <your-resource-group> \
--name <your-managed-identity-name> \
--query principalId -o tsv)
# Grant Key Vault access
az keyvault set-policy \
--name <your-key-vault-name> \
--object-id $PRINCIPAL_ID \
--secret-permissions get list# Get the OIDC issuer URL
AKS_OIDC_ISSUER=$(az aks show \
--resource-group <your-resource-group> \
--name <your-aks-cluster> \
--query "oidcIssuerProfile.issuerUrl" -o tsv)
# Get the managed identity client ID
MANAGED_IDENTITY_CLIENT_ID=$(az identity show \
--resource-group <your-resource-group> \
--name <your-managed-identity-name> \
--query clientId -o tsv)
# Create federated identity credential
az identity federated-credential create \
--name <federated-credential-name> \
--identity-name <your-managed-identity-name> \
--resource-group <your-resource-group> \
--issuer $AKS_OIDC_ISSUER \
--subject system:serviceaccount:<namespace>:<release-name>-symfony-app-workload-identity \
--audience api://AzureADTokenExchangeImportant: Replace <namespace> and <release-name> with your actual namespace and Helm release name. The ServiceAccount name follows the pattern: <release-name>-symfony-app-workload-identity.
# Add the Helm repository
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
# Install the driver
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system \
--set syncSecret.enabled=true# Install the provider
kubectl apply -f https://raw.githubusercontent.com/Azure/secrets-store-csi-driver-provider-azure/master/deployment/provider-azure-installer.yamlCopy and customize the values.yaml file:
cp values.yaml my-values.yaml
# Edit my-values.yaml with your configurationAt minimum, update these values in my-values.yaml:
azure:
tenantId: "<YOUR-AZURE-TENANT-ID>"
managedIdentityClientId: "<YOUR-MANAGED-IDENTITY-CLIENT-ID>"
azureKeyVault:
keyVaultName: "<YOUR-KEY-VAULT-NAME>"
secrets:
# Map your Key Vault secrets to environment variables
- keyVaultName: "APP-SECRET"
kubernetesKey: "APP_SECRET"
# ... add your other secrets
deployments:
php:
image: "your-registry/your-app/php:latest"
env:
DB_HOST: "your-database-host.mysql.database.azure.com"
# ... other environment variables
nginx:
image: "your-registry/your-app/nginx:latest"
ingress:
host: "your-domain.com"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-production"helm install <release-name> . \
--namespace <namespace> \
--create-namespace \
-f my-values.yaml# Check pods
kubectl get pods -n <namespace>
# Check secrets (should be synced from Key Vault)
kubectl get secrets -n <namespace>
# Check ingress
kubectl get ingress -n <namespace>The chart automatically creates a SecretProviderClass that syncs secrets from Azure Key Vault into Kubernetes secrets. Secrets are mounted as environment variables in the PHP pods.
To use a secret from Key Vault, set the environment variable value to "_FROM_AZURE_KEYVAULT":
env:
APP_SECRET: "_FROM_AZURE_KEYVAULT"
DB_PASSWORD: "_FROM_AZURE_KEYVAULT"Then map the Key Vault secret name to the Kubernetes key:
azureKeyVault:
secrets:
- keyVaultName: "APP-SECRET" # Name in Azure Key Vault
kubernetesKey: "APP_SECRET" # Environment variable nameWhen secrets change in Azure Key Vault, update the refreshToken value to force a refresh:
azureKeyVault:
refreshToken: "20241210190000" # Use current timestampThe chart includes Azure MySQL CA certificate support. The certificate is automatically mounted to /etc/ssl/certs/mysql-ca-cert.pem in PHP pods.
Important: Update the mysql.caCert value in values.yaml with your Azure MySQL CA certificate. You can download it from the Azure portal or use the default DigiCert Global Root CA certificate.
Redis is automatically configured for Symfony Messenger. The MESSENGER_TRANSPORT_DSN is set to use Redis:
MESSENGER_TRANSPORT_DSN: "doctrine://default?auto_setup=0"The Redis host is automatically set to the service name created by the chart.
If using a private container registry:
imagePullSecrets:
- name: ghcr-secretCreate the secret first:
kubectl create secret docker-registry ghcr-secret \
--docker-server=ghcr.io \
--docker-username=<username> \
--docker-password=<token> \
--namespace <namespace>Each deployment (PHP, Nginx, and Redis) has configurable CPU and memory resource requests and limits. This allows you to control resource allocation and ensure proper scheduling and performance.
- Requests: The minimum resources guaranteed to the pod. Kubernetes uses this for scheduling decisions.
- Limits: The maximum resources a pod can consume. If exceeded, the pod may be throttled (CPU) or terminated (memory).
The chart provides sensible defaults for each component:
deployments:
php:
resources:
requests:
cpu: "100m" # 0.1 CPU cores
memory: "256Mi" # 256 MiB
limits:
cpu: "500m" # 0.5 CPU cores
memory: "512Mi" # 512 MiBRecommended for: Most Symfony applications with moderate traffic.
deployments:
nginx:
resources:
requests:
cpu: "100m" # 0.1 CPU cores
memory: "128Mi" # 128 MiB
limits:
cpu: "250m" # 0.25 CPU cores
memory: "256Mi" # 256 MiBRecommended for: Standard web server workloads. Nginx is typically CPU-bound rather than memory-bound.
deployments:
redis:
resources:
requests:
cpu: "50m" # 0.05 CPU cores
memory: "128Mi" # 128 MiB
limits:
cpu: "200m" # 0.2 CPU cores
memory: "256Mi" # 256 MiBRecommended for: Small to medium caching and message queue workloads.
Adjust resources based on your application's needs:
deployments:
php:
resources:
requests:
cpu: "200m" # Increase for CPU-intensive operations
memory: "512Mi" # Increase for memory-intensive applications
limits:
cpu: "1000m" # Allow bursts up to 1 CPU core
memory: "1Gi" # Allow up to 1 GiB memory
nginx:
resources:
requests:
cpu: "200m" # Increase for high-traffic sites
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
redis:
resources:
requests:
cpu: "100m"
memory: "256Mi" # Increase if caching large datasets
limits:
cpu: "500m"
memory: "512Mi"- PHP: 100m CPU / 256Mi memory
- Nginx: 100m CPU / 128Mi memory
- Redis: 50m CPU / 128Mi memory
- PHP: 200m-500m CPU / 512Mi-1Gi memory
- Nginx: 200m-500m CPU / 256Mi-512Mi memory
- Redis: 100m-200m CPU / 256Mi-512Mi memory
- PHP: 500m-2000m CPU / 1Gi-4Gi memory
- Nginx: 500m-1000m CPU / 512Mi-1Gi memory
- Redis: 200m-500m CPU / 512Mi-2Gi memory
Monitor actual resource consumption to optimize your settings:
# View current resource usage
kubectl top pods -n <namespace>
# View detailed resource metrics
kubectl describe pod <pod-name> -n <namespace>- Start Conservative: Begin with default values and scale up based on monitoring data
- Set Limits: Always set limits to prevent resource exhaustion
- Match Requests to Limits: For predictable workloads, set requests close to limits
- Monitor OOMKills: Check for Out-of-Memory kills indicating insufficient memory limits
- CPU Throttling: Monitor for CPU throttling which may indicate CPU limits are too low
- Vertical Pod Autoscaler: Consider using VPA for automatic resource adjustment (requires additional setup)
To disable resource limits (not recommended for production):
deployments:
php:
resources: {} # Empty object removes resource constraintsWarning: Removing resource limits can lead to resource exhaustion and affect other workloads in your cluster.
APP_ENV: Environment (prod, dev, test)APP_SECRET: Application secret (recommended: use Key Vault)APP_DEBUG: Debug mode (0 or 1)
DB_HOST: MySQL hostnameDB_PORT: MySQL port (default: 3306)DB_NAME: Database nameDB_USER: Database usernameDB_PASSWORD: Database passwordMYSQL_CA_CERT: Path to CA certificate (default:/etc/ssl/certs/mysql-ca-cert.pem)MYSQL_SSL_VERIFY_SERVER_CERT: Enable SSL verification (default:true)
MAILER_DSN: Mailer DSN (supports Azure Communication Services, SMTP, etc.)MAILER_FROM: Default sender email addressMAILER_FROM_NAME: Default sender name
REDIS_HOST: Redis hostname (auto-configured if set to"REPLACED_BY_TEMPLATE")REDIS_PORT: Redis port (default: 6379)
AWS_ACCESS_KEY_ID: AWS access keyAWS_SECRET_ACCESS_KEY: AWS secret keyAWS_S3_REGION: S3 regionAWS_S3_BUCKET: S3 bucket nameAWS_S3_ENDPOINT: Custom endpoint (leave empty for AWS)AWS_S3_URL: S3 bucket URL
helm upgrade <release-name> . \
--namespace <namespace> \
-f my-values.yamlhelm uninstall <release-name> --namespace <namespace>-
Verify the ServiceAccount has the correct annotation:
kubectl get serviceaccount <release-name>-symfony-app-workload-identity -n <namespace> -o yaml
Should show:
azure.workload.identity/client-id: <your-client-id> -
Check the SecretProviderClass:
kubectl get secretproviderclass -n <namespace> kubectl describe secretproviderclass <release-name>-symfony-app-keyvault-secrets -n <namespace>
-
Check pod events:
kubectl describe pod <php-pod-name> -n <namespace>
-
Verify federated identity credential matches the ServiceAccount:
# The subject should match: # system:serviceaccount:<namespace>:<release-name>-symfony-app-workload-identity
-
Check pod logs:
kubectl logs <pod-name> -n <namespace>
-
Verify image pull secrets if using private registry:
kubectl get secrets -n <namespace>
-
Check resource limits:
kubectl describe pod <pod-name> -n <namespace>
- Verify MySQL CA certificate is correctly configured in
values.yaml - Check database credentials are correctly set or synced from Key Vault
- Verify network connectivity from pods to database:
kubectl exec -it <php-pod-name> -n <namespace> -- ping <db-host>
-
Verify ingress controller is installed:
kubectl get pods -n ingress-nginx
-
Check ingress configuration:
kubectl get ingress -n <namespace> kubectl describe ingress <release-name>-symfony-app-ingress -n <namespace>
-
Verify DNS is pointing to your ingress controller's external IP
Copyright (C) 2024 James Gibbons jgibbons@121digital.co.uk
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.