From be85528c62b44ab299db78c1ca9e0ada2c4599f9 Mon Sep 17 00:00:00 2001 From: Aakarshit Agarwal Date: Tue, 22 Oct 2024 17:57:11 +0530 Subject: [PATCH] Adding Magento2 ARM Template --- Magento2/Kubernetes/azurefile/sc.yaml | 9 + Magento2/Kubernetes/elasticsearch/pv.yaml | 23 + Magento2/Kubernetes/elasticsearch/pvc.yaml | 11 + .../Kubernetes/elasticsearch/services.yaml | 12 + .../Kubernetes/elasticsearch/statefulset.yaml | 35 + Magento2/Kubernetes/ingress/ingress-tls.yaml | 33 + Magento2/Kubernetes/ingress/ingress.yaml | 28 + Magento2/Kubernetes/magento/configmap.yaml | 75 ++ Magento2/Kubernetes/magento/cron.yaml | 46 + Magento2/Kubernetes/magento/deployment.yaml | 55 + Magento2/Kubernetes/magento/job.yaml | 40 + Magento2/Kubernetes/magento/pv.yaml | 23 + Magento2/Kubernetes/magento/pvc.yaml | 11 + .../Kubernetes/magento/services-external.yaml | 13 + Magento2/Kubernetes/magento/services.yaml | 12 + Magento2/Kubernetes/namespace.yaml | 4 + Magento2/Kubernetes/redis/services.yaml | 12 + Magento2/Kubernetes/redis/statefulset.yaml | 22 + Magento2/Kubernetes/tls/cert-pod.yaml | 21 + Magento2/Kubernetes/tls/secret-provider.yaml | 27 + Magento2/Kubernetes/varnish/configmap.yaml | 279 +++++ Magento2/Kubernetes/varnish/deployment.yaml | 36 + Magento2/Kubernetes/varnish/services.yaml | 12 + Magento2/LICENSE | 17 + Magento2/LICENSE-DOCS | 395 +++++++ Magento2/README.md | 82 ++ Magento2/azuredeploy.json | 964 ++++++++++++++++++ Magento2/deploy-aks.sh | 425 ++++++++ Magento2/images/magento2-architecture.png | Bin 0 -> 56687 bytes 29 files changed, 2722 insertions(+) create mode 100644 Magento2/Kubernetes/azurefile/sc.yaml create mode 100644 Magento2/Kubernetes/elasticsearch/pv.yaml create mode 100644 Magento2/Kubernetes/elasticsearch/pvc.yaml create mode 100644 Magento2/Kubernetes/elasticsearch/services.yaml create mode 100644 Magento2/Kubernetes/elasticsearch/statefulset.yaml create mode 100644 Magento2/Kubernetes/ingress/ingress-tls.yaml create mode 100644 Magento2/Kubernetes/ingress/ingress.yaml create mode 100644 Magento2/Kubernetes/magento/configmap.yaml create mode 100644 Magento2/Kubernetes/magento/cron.yaml create mode 100644 Magento2/Kubernetes/magento/deployment.yaml create mode 100644 Magento2/Kubernetes/magento/job.yaml create mode 100644 Magento2/Kubernetes/magento/pv.yaml create mode 100644 Magento2/Kubernetes/magento/pvc.yaml create mode 100644 Magento2/Kubernetes/magento/services-external.yaml create mode 100644 Magento2/Kubernetes/magento/services.yaml create mode 100644 Magento2/Kubernetes/namespace.yaml create mode 100644 Magento2/Kubernetes/redis/services.yaml create mode 100644 Magento2/Kubernetes/redis/statefulset.yaml create mode 100644 Magento2/Kubernetes/tls/cert-pod.yaml create mode 100644 Magento2/Kubernetes/tls/secret-provider.yaml create mode 100644 Magento2/Kubernetes/varnish/configmap.yaml create mode 100644 Magento2/Kubernetes/varnish/deployment.yaml create mode 100644 Magento2/Kubernetes/varnish/services.yaml create mode 100644 Magento2/LICENSE create mode 100644 Magento2/LICENSE-DOCS create mode 100644 Magento2/README.md create mode 100644 Magento2/azuredeploy.json create mode 100644 Magento2/deploy-aks.sh create mode 100644 Magento2/images/magento2-architecture.png diff --git a/Magento2/Kubernetes/azurefile/sc.yaml b/Magento2/Kubernetes/azurefile/sc.yaml new file mode 100644 index 0000000..2bb5d8e --- /dev/null +++ b/Magento2/Kubernetes/azurefile/sc.yaml @@ -0,0 +1,9 @@ +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: azurefile-sc + annotations: + storageclass.kubernetes.io/is-default-class: "true" +provisioner: kubernetes.io/azure-file +reclaimPolicy: Retain +allowVolumeExpansion: true diff --git a/Magento2/Kubernetes/elasticsearch/pv.yaml b/Magento2/Kubernetes/elasticsearch/pv.yaml new file mode 100644 index 0000000..dd4299f --- /dev/null +++ b/Magento2/Kubernetes/elasticsearch/pv.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: elasticsearch-pv +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + azureFile: + secretName: input-secrets + secretNamespace: magento + shareName: elasticsearch + readOnly: false + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - mfsymlinks + - uid=1000 + - gid=1000 + - cache=none + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-sc \ No newline at end of file diff --git a/Magento2/Kubernetes/elasticsearch/pvc.yaml b/Magento2/Kubernetes/elasticsearch/pvc.yaml new file mode 100644 index 0000000..fc006f7 --- /dev/null +++ b/Magento2/Kubernetes/elasticsearch/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: elasticsearch-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: azurefile-sc diff --git a/Magento2/Kubernetes/elasticsearch/services.yaml b/Magento2/Kubernetes/elasticsearch/services.yaml new file mode 100644 index 0000000..8ff6898 --- /dev/null +++ b/Magento2/Kubernetes/elasticsearch/services.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: elasticsearch-service + namespace: magento +spec: + selector: + app: elasticsearch + ports: + - protocol: TCP + port: 9200 + targetPort: 9200 \ No newline at end of file diff --git a/Magento2/Kubernetes/elasticsearch/statefulset.yaml b/Magento2/Kubernetes/elasticsearch/statefulset.yaml new file mode 100644 index 0000000..3b79db0 --- /dev/null +++ b/Magento2/Kubernetes/elasticsearch/statefulset.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: elasticsearch + namespace: magento +spec: + replicas: 1 + selector: + matchLabels: + app: elasticsearch + serviceName: elasticsearch-service + template: + metadata: + labels: + app: elasticsearch + spec: + containers: + - name: elasticsearch + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.24 + ports: + - containerPort: 9200 + name: main + env: + - name: discovery.type + value: "single-node" + - name: xpack.security.enabled + value: "false" + volumeMounts: + - mountPath: /usr/share/elasticsearch/data + name: elasticsearch-data + volumes: + - name: elasticsearch-data + persistentVolumeClaim: + claimName: elasticsearch-pvc + readOnly: false diff --git a/Magento2/Kubernetes/ingress/ingress-tls.yaml b/Magento2/Kubernetes/ingress/ingress-tls.yaml new file mode 100644 index 0000000..bf7b673 --- /dev/null +++ b/Magento2/Kubernetes/ingress/ingress-tls.yaml @@ -0,0 +1,33 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: magento-ingress + namespace: magento + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/proxy-buffer-size: "256k" + nginx.org/proxy-buffers: "4 256k" + nginx.ingress.kubernetes.io/proxy-buffers-number: "4" + nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "256k" + nginx.org/proxy-connect-timeout: "60s" + nginx.org/proxy-read-timeout: "60s" + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" +spec: + ingressClassName: nginx + tls: + - hosts: + - __FQDN__ + secretName: magento-secret + rules: + - host: __FQDN__ + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: __SERVICE_NAME__ + port: + number: __SERVICE_PORT__ diff --git a/Magento2/Kubernetes/ingress/ingress.yaml b/Magento2/Kubernetes/ingress/ingress.yaml new file mode 100644 index 0000000..1e76092 --- /dev/null +++ b/Magento2/Kubernetes/ingress/ingress.yaml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: magento-ingress + namespace: magento + annotations: + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + nginx.ingress.kubernetes.io/proxy-buffer-size: "256k" + nginx.org/proxy-buffers: "4 256k" + nginx.ingress.kubernetes.io/proxy-buffers-number: "4" + nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "256k" + nginx.org/proxy-connect-timeout: "60s" + nginx.org/proxy-read-timeout: "60s" + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: __SERVICE_NAME__ + port: + number: __SERVICE_PORT__ diff --git a/Magento2/Kubernetes/magento/configmap.yaml b/Magento2/Kubernetes/magento/configmap.yaml new file mode 100644 index 0000000..66a6d0a --- /dev/null +++ b/Magento2/Kubernetes/magento/configmap.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: magento-config + namespace: magento +data: + PHP_VERSION: "8.3" + MAGENTO_PORT: "8080" + MAGENTO_DATABASE_NAME: "magento" + MAGENTO_SEARCH_ENGINE: "elasticsearch7" + ELASTICSEARCH_HOST: elasticsearch-service.magento.svc.cluster.local + ELASTICSEARCH_PORT: "9200" + MAGENTO_ADMIN_FIRSTNAME: "Admin" + MAGENTO_ADMIN_LASTNAME: "User" + MAGENTO_ADMIN_FRONTNAME: "admin" + MAGENTO_LANGUAGE: "en_US" + MAGENTO_USE_REWRITES: "1" + SESSION_BACKEND: "redis" + SESSION_REDIS_SERVER_HOST: redis-service.magento.svc.cluster.local + SESSION_REDIS_PORT: "6379" + SESSION_REDIS_DATABASE: "2" + SESSION_REDIS_LOG_LEVEL: "4" + MAGENTO_FORCE_INSTALL: "false" + MAGENTO_DEPLOY_SAMPLE_DATA: "false" + MAGENTO_REMOVE_SAMPLE_DATA: "false" + MAGENTO_DISABLE_2FA: "true" + VARNISH_LISTEN_HOST: "varnish-service.magento.svc.cluster.local" + VARNISH_LISTEN_PORT: "8081" + VARNISH_BACKEND_HOST: magento-service.magento.svc.cluster.local + VARNISH_BACKEND_PORT: "8080" + VARNISH_STORAGE: "malloc,256M" + default.conf: | + # Example configuration: + upstream fastcgi_backend { + # use tcp connection + # server 127.0.0.1:9000; + # or socket + server unix:/run/php/php8.3-fpm.sock; + } + server { + listen 8080; + server_name magento-service.magento.svc.cluster.local; + set $MAGE_ROOT /var/www/html/magento2; + set $MAGE_DEBUG_SHOW_ARGS 0; + + include /usr/local/bin/nginx.conf.sample; + } + + # Optional override of deployment mode. We recommend you use the + # command 'bin/magento deploy:mode:set' to switch modes instead. + # + # set $MAGE_MODE default; # or production or developer + # + # If you set MAGE_MODE in server config, you must pass the variable into the + # PHP entry point blocks, which are indicated below. You can pass + # it in using: + # + # fastcgi_param MAGE_MODE $MAGE_MODE; + # + # In production mode, you should uncomment the 'expires' directive in the /static/ location block + + # Modules can be loaded only at the very beginning of the Nginx config file, please move the line below to the main config file + # load_module /etc/nginx/modules/ngx_http_image_filter_module.so; + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: magento-cron-config + namespace: magento +data: + CONFIG__DEFAULT__SYSTEM__CRON__INDEX__USE_SEPARATE_PROCESS: "0" + CONFIG__DEFAULT__SYSTEM__CRON__DEFAULT__USE_SEPARATE_PROCESS: "0" + CONFIG__DEFAULT__SYSTEM__CRON__CONSUMERS__USE_SEPARATE_PROCESS: "0" + CONFIG__DEFAULT__SYSTEM__CRON__DDG_AUTOMATION__USE_SEPARATE_PROCESS: "0" diff --git a/Magento2/Kubernetes/magento/cron.yaml b/Magento2/Kubernetes/magento/cron.yaml new file mode 100644 index 0000000..65d7e1f --- /dev/null +++ b/Magento2/Kubernetes/magento/cron.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: magento-cron + namespace: magento +spec: + replicas: 1 + selector: + matchLabels: + app: magento-cron + template: + metadata: + labels: + app: magento-cron + spec: + containers: + - name: magento-cron + image: aaakarshit/magento-cron:v1.0.0 + envFrom: + - configMapRef: + name: input-config + - configMapRef: + name: magento-config + - configMapRef: + name: magento-cron-config + - secretRef: + name: input-secrets + volumeMounts: + - name: azurefile-magento + mountPath: /var/www/html/mount/static + subPath: static + - name: azurefile-magento + mountPath: /var/www/html/mount/media + subPath: media + readinessProbe: + exec: + command: ["sh", "-c", "test -f /tmp/magento-setup-complete-${HOSTNAME}"] + failureThreshold: 145 # Check till 15 minutes + initialDelaySeconds: 180 # Start checking after 3 minutes + periodSeconds: 5 # Check every 5 seconds + successThreshold: 1 # If the file is present, it's ready + volumes: + - name: azurefile-magento + persistentVolumeClaim: + claimName: magento-pvc + readOnly: false diff --git a/Magento2/Kubernetes/magento/deployment.yaml b/Magento2/Kubernetes/magento/deployment.yaml new file mode 100644 index 0000000..793abcf --- /dev/null +++ b/Magento2/Kubernetes/magento/deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: magento-server + namespace: magento +spec: + replicas: 1 + selector: + matchLabels: + app: magento-server + template: + metadata: + labels: + app: magento-server + spec: + containers: + - name: magento-server + image: aaakarshit/magento-server:v1.0.0 + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: magento-config + - configMapRef: + name: input-config + - secretRef: + name: input-secrets + volumeMounts: + - name: azurefile-magento + mountPath: /var/www/html/mount/static + subPath: static + - name: azurefile-magento + mountPath: /var/www/html/mount/media + subPath: media + - name: magento-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + readinessProbe: + exec: + command: ["sh", "-c", "test -f /tmp/magento-setup-complete-${HOSTNAME}"] + failureThreshold: 145 # Check till 15 minutes + initialDelaySeconds: 180 # Start checking after 3 minutes + periodSeconds: 5 # Check every 5 seconds + successThreshold: 1 # If the file is present, it's ready + volumes: + - name: azurefile-magento + persistentVolumeClaim: + claimName: magento-pvc + readOnly: false + - name: magento-config + configMap: + name: magento-config + items: + - key: default.conf + path: default.conf diff --git a/Magento2/Kubernetes/magento/job.yaml b/Magento2/Kubernetes/magento/job.yaml new file mode 100644 index 0000000..d04d7f3 --- /dev/null +++ b/Magento2/Kubernetes/magento/job.yaml @@ -0,0 +1,40 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: magento-setup-job + namespace: magento +spec: + template: + metadata: + labels: + app: magento-setup-job + spec: + containers: + - name: magento-setup-job + image: aaakarshit/magento-setup-job:v1.0.0 + envFrom: + - configMapRef: + name: magento-config + - configMapRef: + name: input-config + - secretRef: + name: input-secrets + volumeMounts: + - name: azurefile-magento + mountPath: /var/www/html/mount/static + subPath: static + - name: azurefile-magento + mountPath: /var/www/html/mount/media + subPath: media + restartPolicy: Never + volumes: + - name: azurefile-magento + persistentVolumeClaim: + claimName: magento-pvc + readOnly: false + - name: magento-config + configMap: + name: magento-config + items: + - key: default.conf + path: default.conf diff --git a/Magento2/Kubernetes/magento/pv.yaml b/Magento2/Kubernetes/magento/pv.yaml new file mode 100644 index 0000000..89237d6 --- /dev/null +++ b/Magento2/Kubernetes/magento/pv.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: magento-pv +spec: + capacity: + storage: 20Gi + accessModes: + - ReadWriteMany + azureFile: + secretName: input-secrets + secretNamespace: magento + shareName: magento + readOnly: false + mountOptions: + - dir_mode=0777 + - file_mode=0777 + - mfsymlinks + - uid=33 + - gid=33 + - cache=none + persistentVolumeReclaimPolicy: Retain + storageClassName: azurefile-sc \ No newline at end of file diff --git a/Magento2/Kubernetes/magento/pvc.yaml b/Magento2/Kubernetes/magento/pvc.yaml new file mode 100644 index 0000000..e83733d --- /dev/null +++ b/Magento2/Kubernetes/magento/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: magento-pvc +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 20Gi + storageClassName: azurefile-sc diff --git a/Magento2/Kubernetes/magento/services-external.yaml b/Magento2/Kubernetes/magento/services-external.yaml new file mode 100644 index 0000000..cd2f895 --- /dev/null +++ b/Magento2/Kubernetes/magento/services-external.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: magento-service + namespace: magento +spec: + selector: + app: magento-server + type: LoadBalancer + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 diff --git a/Magento2/Kubernetes/magento/services.yaml b/Magento2/Kubernetes/magento/services.yaml new file mode 100644 index 0000000..e7aa67d --- /dev/null +++ b/Magento2/Kubernetes/magento/services.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: magento-service + namespace: magento +spec: + selector: + app: magento-server + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 diff --git a/Magento2/Kubernetes/namespace.yaml b/Magento2/Kubernetes/namespace.yaml new file mode 100644 index 0000000..c5ff9f3 --- /dev/null +++ b/Magento2/Kubernetes/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: magento diff --git a/Magento2/Kubernetes/redis/services.yaml b/Magento2/Kubernetes/redis/services.yaml new file mode 100644 index 0000000..56082f3 --- /dev/null +++ b/Magento2/Kubernetes/redis/services.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis-service + namespace: magento +spec: + selector: + app: redis + ports: + - protocol: TCP + port: 6379 + targetPort: 6379 diff --git a/Magento2/Kubernetes/redis/statefulset.yaml b/Magento2/Kubernetes/redis/statefulset.yaml new file mode 100644 index 0000000..44a4aef --- /dev/null +++ b/Magento2/Kubernetes/redis/statefulset.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: redis + namespace: magento +spec: + replicas: 1 + selector: + matchLabels: + app: redis + serviceName: redis-service + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7.4.1 + ports: + - containerPort: 6379 + name: main diff --git a/Magento2/Kubernetes/tls/cert-pod.yaml b/Magento2/Kubernetes/tls/cert-pod.yaml new file mode 100644 index 0000000..afc6996 --- /dev/null +++ b/Magento2/Kubernetes/tls/cert-pod.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: cert-pod + namespace: magento +spec: + containers: + - name: busybox + image: busybox + command: [ "sh", "-c", "sleep 3600" ] + volumeMounts: + - name: secrets-store + mountPath: "/mnt/secrets-store" + readOnly: true + volumes: + - name: secrets-store + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: magento-secret-provider diff --git a/Magento2/Kubernetes/tls/secret-provider.yaml b/Magento2/Kubernetes/tls/secret-provider.yaml new file mode 100644 index 0000000..c680c48 --- /dev/null +++ b/Magento2/Kubernetes/tls/secret-provider.yaml @@ -0,0 +1,27 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: magento-secret-provider + namespace: magento +spec: + provider: azure + secretObjects: + - secretName: magento-secret + type: kubernetes.io/tls + data: + - objectName: __CERTIFICATE_NAME__ + key: tls.crt + - objectName: __CERTIFICATE_NAME__ + key: tls.key + parameters: + usePodIdentity: "false" + useVMManagedIdentity: "true" + userAssignedIdentityID: __USER_ASSIGNED_IDENTITY_ID__ + keyvaultName: __KEYVAULT_NAME__ + objects: | + array: + - | + objectName: __CERTIFICATE_NAME__ + objectType: secret + objectFormat: "pem" + tenantId: __TENANT_ID__ diff --git a/Magento2/Kubernetes/varnish/configmap.yaml b/Magento2/Kubernetes/varnish/configmap.yaml new file mode 100644 index 0000000..9de41ae --- /dev/null +++ b/Magento2/Kubernetes/varnish/configmap.yaml @@ -0,0 +1,279 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: varnish-config + namespace: magento +data: + VARNISH_LISTEN_HOST: varnish-service.magento.svc.cluster.local + VARNISH_LISTEN_PORT: "8081" + VARNISH_BACKEND_HOST: magento-service.magento.svc.cluster.local + VARNISH_BACKEND_PORT: "8080" + VARNISH_STORAGE: "malloc,512M" + default.vcl: | + # VCL version 5.0 is not supported so it should be 4.0 even though actually used Varnish version is 6 + vcl 4.0; + + import std; + # The minimal Varnish version is 6.0 + # For SSL offloading, pass the following header in your proxy server or load balancer: 'X-Forwarded-Proto: https' + + backend default { + .host = "magento-service.magento.svc.cluster.local"; + .port = "8080"; + .first_byte_timeout = 600s; + } + + acl purge { + "varnish-service.magento.svc.cluster.local"; + } + + sub vcl_recv { + if (req.restarts > 0) { + set req.hash_always_miss = true; + } + + if (req.method == "PURGE") { + if (client.ip !~ purge) { + return (synth(405, "Method not allowed")); + } + # To use the X-Pool header for purging varnish during automated deployments, make sure the X-Pool header + # has been added to the response in your backend server config. This is used, for example, by the + # capistrano-magento2 gem for purging old content from varnish during its deploy routine. + if (!req.http.X-Magento-Tags-Pattern && !req.http.X-Pool) { + return (synth(400, "X-Magento-Tags-Pattern or X-Pool header required")); + } + if (req.http.X-Magento-Tags-Pattern) { + ban("obj.http.X-Magento-Tags ~ " + req.http.X-Magento-Tags-Pattern); + } + if (req.http.X-Pool) { + ban("obj.http.X-Pool ~ " + req.http.X-Pool); + } + return (synth(200, "Purged")); + } + + if (req.method != "GET" && + req.method != "HEAD" && + req.method != "PUT" && + req.method != "POST" && + req.method != "TRACE" && + req.method != "OPTIONS" && + req.method != "DELETE") { + /* Non-RFC2616 or CONNECT which is weird. */ + return (pipe); + } + + # We only deal with GET and HEAD by default + if (req.method != "GET" && req.method != "HEAD") { + return (pass); + } + + # Bypass search requests, shopping cart, checkout + if (req.url ~ "/catalogsearch" || req.url ~ "/checkout") { + return (pass); + } + + # Bypass health check requests + if (req.url ~ "^/(pub/)?(health_check.php)$") { + return (pass); + } + + # Set initial grace period usage status + set req.http.grace = "none"; + + # normalize url in case of leading HTTP scheme and domain + set req.url = regsub(req.url, "^http[s]?://", ""); + + # collect all cookies + std.collect(req.http.Cookie); + + # Compression filter. See https://www.varnish-cache.org/trac/wiki/FAQ/Compression + if (req.http.Accept-Encoding) { + if (req.url ~ "\.(jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|swf|flv|svg)$") { + # No point in compressing these + unset req.http.Accept-Encoding; + } elsif (req.http.Accept-Encoding ~ "gzip") { + set req.http.Accept-Encoding = "gzip"; + } elsif (req.http.Accept-Encoding ~ "deflate" && req.http.user-agent !~ "MSIE") { + set req.http.Accept-Encoding = "deflate"; + } else { + # unknown algorithm + unset req.http.Accept-Encoding; + } + } + + # Remove all marketing get parameters to minimize the cache objects + if (req.url ~ "(\?|&)(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=") { + set req.url = regsuball(req.url, "(gclid|cx|ie|cof|siteurl|zanpid|origin|fbclid|mc_[a-z]+|utm_[a-z]+|_bta_[a-z]+)=[-_A-z0-9+()%.]+&?", ""); + set req.url = regsub(req.url, "[?|&]+$", ""); + } + + # Static files caching + if (req.url ~ "^/(pub/)?(media|static)/") { + # Static files should not be cached by default + return (pass); + + # But if you use a few locales and dont use CDN you can enable caching static files by commenting previous line (#return (pass);) and uncommenting next 3 lines + #unset req.http.Https; + #unset req.http.X-Forwarded-Proto; + #unset req.http.Cookie; + } + + # Bypass authenticated GraphQL requests without a X-Magento-Cache-Id + if (req.url ~ "/graphql" && !req.http.X-Magento-Cache-Id && req.http.Authorization ~ "^Bearer") { + return (pass); + } + + return (hash); + } + + sub vcl_hash { + if ((req.url !~ "/graphql" || !req.http.X-Magento-Cache-Id) && req.http.cookie ~ "X-Magento-Vary=") { + hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1")); + } + + # To make sure http users dont see ssl warning + if (req.http.X-Forwarded-Proto) { + hash_data(req.http.X-Forwarded-Proto); + } + + + if (req.url ~ "/graphql") { + call process_graphql_headers; + } + } + + sub process_graphql_headers { + if (req.http.X-Magento-Cache-Id) { + hash_data(req.http.X-Magento-Cache-Id); + + # When the frontend stops sending the auth token, make sure users stop getting results cached for logged-in users + if (req.http.Authorization ~ "^Bearer") { + hash_data("Authorized"); + } + } + + if (req.http.Store) { + hash_data(req.http.Store); + } + + if (req.http.Content-Currency) { + hash_data(req.http.Content-Currency); + } + } + + sub vcl_backend_response { + + set beresp.grace = 3d; + + if (beresp.http.content-type ~ "text") { + # set beresp.do_esi = true; + } + + if (bereq.url ~ "\.js$" || beresp.http.content-type ~ "text") { + set beresp.do_gzip = true; + } + + if (beresp.http.X-Magento-Debug) { + set beresp.http.X-Magento-Cache-Control = beresp.http.Cache-Control; + } + + # cache only successfully responses and 404s + if (beresp.status != 200 && beresp.status != 404) { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + return (deliver); + } elsif (beresp.http.Cache-Control ~ "private") { + set beresp.uncacheable = true; + set beresp.ttl = 86400s; + return (deliver); + } + + # validate if we need to cache it and prevent from setting cookie + if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) { + # Collapse beresp.http.set-cookie in order to merge multiple set-cookie headers + # Although it is not recommended to collapse set-cookie header, + # it is safe to do it here as the set-cookie header is removed below + std.collect(beresp.http.set-cookie); + # Do not cache the response under current cache key (hash), + # if the response has X-Magento-Vary but the request does not. + if ((bereq.url !~ "/graphql" || !bereq.http.X-Magento-Cache-Id) + && bereq.http.cookie !~ "X-Magento-Vary=" + && beresp.http.set-cookie ~ "X-Magento-Vary=") { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + } + unset beresp.http.set-cookie; + } + + # If page is not cacheable then bypass varnish for 2 minutes as Hit-For-Pass + if (beresp.ttl <= 0s || + beresp.http.Surrogate-control ~ "no-store" || + (!beresp.http.Surrogate-Control && + beresp.http.Cache-Control ~ "no-cache|no-store") || + beresp.http.Vary == "*") { + # Mark as Hit-For-Pass for the next 2 minutes + set beresp.ttl = 120s; + set beresp.uncacheable = true; + } + + # If the cache key in the Magento response doesnt match the one that was sent in the request, don't cache under the request's key + if (bereq.url ~ "/graphql" && bereq.http.X-Magento-Cache-Id && bereq.http.X-Magento-Cache-Id != beresp.http.X-Magento-Cache-Id) { + set beresp.ttl = 0s; + set beresp.uncacheable = true; + } + + return (deliver); + } + + sub vcl_deliver { + if (resp.http.X-Magento-Debug) { + if (resp.http.x-varnish ~ " ") { + set resp.http.X-Magento-Cache-Debug = "HIT"; + set resp.http.Grace = req.http.grace; + } else { + set resp.http.X-Magento-Cache-Debug = "MISS"; + } + } else { + unset resp.http.Age; + } + + # Not letting browser to cache non-static files. + if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(pub/)?(media|static)/") { + set resp.http.Pragma = "no-cache"; + set resp.http.Expires = "-1"; + set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0"; + } + + if (!resp.http.X-Magento-Debug) { + unset resp.http.Age; + } + + unset resp.http.X-Magento-Debug; + unset resp.http.X-Magento-Tags; + unset resp.http.X-Powered-By; + unset resp.http.Server; + unset resp.http.X-Varnish; + unset resp.http.Via; + unset resp.http.Link; + } + + sub vcl_hit { + if (obj.ttl >= 0s) { + # Hit within TTL period + return (deliver); + } + if (std.healthy(req.backend_hint)) { + if (obj.ttl + 300s > 0s) { + # Hit after TTL expiration, but within grace period + set req.http.grace = "normal (healthy server)"; + return (deliver); + } else { + # Hit after TTL and grace expiration + return (restart); + } + } else { + # server is not healthy, retrieve from cache + set req.http.grace = "unlimited (unhealthy server)"; + return (deliver); + } + } diff --git a/Magento2/Kubernetes/varnish/deployment.yaml b/Magento2/Kubernetes/varnish/deployment.yaml new file mode 100644 index 0000000..07cfa4d --- /dev/null +++ b/Magento2/Kubernetes/varnish/deployment.yaml @@ -0,0 +1,36 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: varnish + namespace: magento +spec: + replicas: 1 + selector: + matchLabels: + app: varnish + template: + metadata: + labels: + app: varnish + spec: + containers: + - name: varnish + image: varnish:6.0.13 + command: ["sh", "-c", "varnishd -F -n /var/lib/varnish -f /etc/varnish/default.vcl -s ${VARNISH_STORAGE} -a :${VARNISH_LISTEN_PORT} & \ + varnishncsa -n /var/lib/varnish -F '%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\" %{Varnish:handling}x'"] + ports: + - containerPort: 8081 + volumeMounts: + - name: varnish-config + mountPath: /etc/varnish/default.vcl + subPath: default.vcl + envFrom: + - configMapRef: + name: varnish-config + volumes: + - name: varnish-config + configMap: + name: varnish-config + items: + - key: default.vcl + path: default.vcl diff --git a/Magento2/Kubernetes/varnish/services.yaml b/Magento2/Kubernetes/varnish/services.yaml new file mode 100644 index 0000000..7f9f30b --- /dev/null +++ b/Magento2/Kubernetes/varnish/services.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: varnish-service + namespace: magento +spec: + selector: + app: varnish + ports: + - protocol: TCP + port: 80 + targetPort: 8081 diff --git a/Magento2/LICENSE b/Magento2/LICENSE new file mode 100644 index 0000000..b17b032 --- /dev/null +++ b/Magento2/LICENSE @@ -0,0 +1,17 @@ +The MIT License (MIT) +Copyright (c) Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Magento2/LICENSE-DOCS b/Magento2/LICENSE-DOCS new file mode 100644 index 0000000..a2c95fc --- /dev/null +++ b/Magento2/LICENSE-DOCS @@ -0,0 +1,395 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. \ No newline at end of file diff --git a/Magento2/README.md b/Magento2/README.md new file mode 100644 index 0000000..630d210 --- /dev/null +++ b/Magento2/README.md @@ -0,0 +1,82 @@ +# One-click ARM template to deploy Magento OSS e-commerce app on Azure + +## Introduction +[Magento Open Source](https://business.adobe.com/products/magento/open-source.html) is a free and flexible e-commerce platform that allows you to create and manage online stores. This Azure Resource Manager JSON template provides a easy one-click method to deploy Magento on Azure using following Azure services to provide an optimal experience of hosting Magento on Azure with tight integrations. + +* [Azure Resource Manager (ARM) templates](https://learn.microsoft.com/en-in/azure/azure-resource-manager/templates/overview) +* [Azure Kubernetes Service (AKS) documentation](https://learn.microsoft.com/en-us/azure/aks/) +* [Azure Container Registry documentation](https://learn.microsoft.com/en-us/azure/container-registry/) +* [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/) +* [Azure networking](https://learn.microsoft.com/en-us/azure/networking/) +* [Azure Virtual Network](https://learn.microsoft.com/en-us/azure/virtual-network/) +* [Azure DNS](https://learn.microsoft.com/en-us/azure/dns/) +* [Azure Private Link](https://learn.microsoft.com/en-us/azure/private-link/) +* [Azure NetApp Files](https://learn.microsoft.com/en-us/azure/azure-netapp-files/) +* [Azure Virtual Machine Scale Sets](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/) +* [Azure Database for MySQL - Flexible Server](https://learn.microsoft.com/en-us/azure/mysql/) +* [Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/) + +This solution includes automation to deploy in a single-click all the required AKS infrastructure, database server and deploy Magento and dependent components in AKS. + +The ARM template will create and deploy the following resources in your Azure account: +![Magento2 solution architecture](images/magento2-architecture.png) + +* A virtual network with a subnet and a network security group. +* A public IP address and a load balancer. +* A MySQL server and a database for Magento Open Source. +* A Redis cache for Magento Open-Source session and page caching. +* A storage account for Magento Open-Source media files. +* An application insights instance for Magento Open-Source monitoring and analytics. +* A deployment with a specified number of pods that run Magento Open-Source containers. +* A service that exposes the Magento Open-Source pods to the internet. +* A persistent volume claim and a storage class that provide persistent storage for Magento Open-Source data. +* A secret that stores the Magento Open-Source credentials and encryption keys. +* An Azure Database for MySQL – Flexible Server PaaS database for Magento Open Source, the best place for MySQL on Azure. +* An Elasticsearch subchart that deploys an Elasticsearch cluster for Magento Open-Source search functionality. +* A Redis subchart that deploys a Redis server for Magento Open-Source session and page caching. + +## Deployment Steps +The following pre-requisites need to be configured before deploying the ARM template. +1. Create a Resource group in your Azure Subscription to deploy the Magento solution. Please note that a second resource group is created for AKS specific infrastructure deployment with the resource group name as prefix that you created. +2. Get your authentication keys from [Commerce Marketplace](https://commercemarketplace.adobe.com/). You may need to register and generate the public and private keys. Follow Adobe document: [Get your authentication keys | Adobe Commerce](https://experienceleague.adobe.com/en/docs/commerce-operations/installation-guide/prerequisites/authentication-keys) +3. Run the following commands on Azure CLI to create RBAC Role and assign necessary permissions for running the automated script to configure AKS: + +``` +az login + +az ad sp create-for-rbac --name magento2 --role "Azure Kubernetes Service Contributor Role" --scopes /subscriptions//resourceGroups/ + +az role assignment create --assignee --role "CDN Profile Contributor" --scope /subscriptions//resourceGroups/ + +az role assignment create --assignee --role "Virtual Machine Contributor" --scope /subscriptions//resourceGroups/ +``` + +Save the output from the above command as you need to provide this information to the template when deploying in later steps filling required information in ARM deployment. + +## HTTPS using SSL +If using SSL encryption for users to reach the Magento E-Commerce site via HTTPS: +1. Create key vault in the same resource group created initially. +2. Import your certificates for TLS to the same key vault. + +## Deploy the ARM template +To deploy this ARM template, open the Azure CLI, and then run the following command: +``` +az deployment group create --resource-group --template-file azuredeploy.json +``` +See [this article](https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/common-deployment-errors) for troubleshooting the deployment errors. +Alternatively, the following button will allow you to deploy the APM template from Azure portal: [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-mysql%2Frefs%2Fheads%2Fmaster%2FMagento2%2Fazuredeploy.json) + +## Cleanup resources +To remove this deployment simply remove the following Resource groups: +1. Resource group created as part of AKS infra whose name starts with resource group created in initial pre-requisite step. +2. Resource group created in initial pre-requisite step. + +#### Code of Conduct +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact opencode@microsoft.com with any additional questions or comments. + +#### Legal Notices +Microsoft and any contributors grant you a license to the Microsoft documentation and other content in this repository under the [Creative Commons Attribution 4.0 International Public License](https://creativecommons.org/licenses/by/4.0/legalcode), see the [LICENSE](https://github.com/Azure/azure-mysql/blob/master/Magento2/LICENSE) file, and grant you a license to any code in the repository under the [MIT License](https://opensource.org/licenses/MIT) file. + +Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. Microsoft's general trademark guidelines can be found at [Microsoft Trademarks](https://github.com/Azure/azure-mysql/blob/master/Magento/LICENSE). + +Privacy information can be found at [Privacy at Microsoft](https://privacy.microsoft.com/). Microsoft and any contributors reserve all others rights, whether under their respective copyrights, patents, or trademarks, whether by implication, estoppel or otherwise. \ No newline at end of file diff --git a/Magento2/azuredeploy.json b/Magento2/azuredeploy.json new file mode 100644 index 0000000..fbc3e08 --- /dev/null +++ b/Magento2/azuredeploy.json @@ -0,0 +1,964 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "servicePrincipalAppID": { + "type": "securestring", + "metadata": { + "description": "Provide the Service Principle App ID from the output of the Azure CLI commands to create the RBAC roles in the earlier steps." + } + }, + "servicePrincipalPassword": { + "type": "securestring", + "metadata": { + "description": "Provide the Service Principle App Password from the output of the Azure CLI commands to create the RBAC roles in the earlier steps." + } + }, + "composerUsername": { + "type": "string", + "metadata": { + "description": "Provide the public key from the authentication access keys generated at Commerce Marketplace." + } + }, + "composerPassword": { + "type": "string", + "metadata": { + "description": "Provide the private key from the authentication access keys generated at Commerce Marketplace." + } + }, + "magentoAdminUsername": { + "type": "string", + "defaultValue": "magentoadmin", + "metadata": { + "description": "Provide the username you want for the Magento administator." + } + }, + "magentoAdminPassword": { + "type": "securestring", + "metadata": { + "description": "Provide the password for the Magento administator." + } + }, + "redisCache": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Set this to true for enabling Redis database caching for the Magento application." + } + }, + "varnishCache": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Set this to true for enabling Varnish HTML content cache for Magento application." + } + }, + "azureCDN": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Set this to true for enabling Azure Content Delivery Network (CDN) for static, CSS and scripts for Magento application. (This requires SSL/TLS enabled.)" + } + }, + "SSLEncryptionTLS": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable SSL/TLS encryption for users reaching Magento web application over HTTPS." + } + }, + "resourceNamePrefix": { + "type": "string", + "defaultValue": "magento", + "metadata": { + "description": "Provide a prefix for generating resource names as part of the Magento solution." + } + }, + "keyVaultName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Provide a keyvault name (created in same resource group as pre-requisite) to access certificate for TLS configuration." + } + }, + "certificateName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Provide the respective certificate name in key vault for SSL/TLS configuration." + } + }, + "externalFQDN": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Provide the external Fully Qualified DNS Name (FQDN) that you have obtained via any web hosting to map to the Magento site." + } + }, + "MYSQLAdministratorLogin": { + "type": "string", + "defaultValue": "mysqlmagento", + "metadata": { + "description": "Admin username for Azure Database for MySQL Flexible Server." + } + }, + "MYSQLAdministratorPassword": { + "type": "securestring", + "metadata": { + "description": "Admin password for Azure Database for MySQL Flexible Server." + } + }, + "MYSQLDatabaseName": { + "type": "string", + "defaultValue": "magento" + }, + "MYSQLServerEdition": { + "type": "string", + "defaultValue": "GeneralPurpose", + "allowedValues": [ + "Burstable", + "GeneralPurpose", + "MemoryOptimized" + ], + "metadata": { + "description": "Choose the Azure Database for MySQL compute tier. High availability is available only for GeneralPurpose and MemoryOptimized Tiers." + } + }, + "MYSQLVersion": { + "type": "string", + "defaultValue": "8.0.21", + "allowedValues": [ + "5.7", + "8.0.21" + ], + "metadata": { + "description": "Choose the MySQL version to be used in Azure Database for MySQL." + } + }, + "MYSQLAvailabilityZone": { + "type": "string", + "defaultValue": "", + "allowedValues": [ + "1", + "2", + "3", + "" + ], + "metadata": { + "description": "Choose the Azure availability zone for the Azure Database for MySQL server, ensure that the zone exists in respective region. (If you don't have a preference, leave this setting blank.)" + } + }, + "MYSQLHighAvailability": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "SameZone", + "ZoneRedundant" + ], + "metadata": { + "description": "Choose High availability mode for Azure Database for MySQL server: Disabled, SameZone, or ZoneRedundant." + } + }, + "MYSQLStandbyAvailabilityZone": { + "type": "string", + "defaultValue": "", + "allowedValues": [ + "1", + "2", + "3", + "" + ], + "metadata": { + "description": "Availability zone of the High availability standby server, ensure that the zone exists in the respective region. (Leave blank for No Preference). Add this value if HA is enabled." + } + }, + "MYSQLStorageSizeGB": { + "defaultValue": 20, + "minValue": 20, + "maxValue": 16384, + "type": "int", + "metadata": { + "description": "Azure database for MySQL storage size in GiB. Storage is used for the database files, temporary files, transaction logs, and the MySQL server logs. In all compute tiers, the minimum storage supported is 20 GiB and maximum is 16 TiB (16384 GiB)." + } + }, + "MYSQLComputeSKU": { + "type": "string", + "defaultValue": "Standard_D2ads_v5", + "allowedValues": [ + "Standard_B1ms", + "Standard_B2ms", + "Standard_B4ms", + "Standard_B8ms", + "Standard_B12ms", + "Standard_B16ms", + "Standard_B20ms", + "Standard_D2ads_v5", + "Standard_D2ds_v4", + "Standard_D4ads_v5", + "Standard_D4ds_v4", + "Standard_D8ads_v5", + "Standard_D8ds_v4", + "Standard_D16ads_v5", + "Standard_D16ds_v4", + "Standard_D32ads_v5", + "Standard_D32ds_v4", + "Standard_D48ads_v5", + "Standard_D48ds_v4", + "Standard_D64ads_v5", + "Standard_D64ds_v4", + "Standard_E2ds_v4", + "Standard_E2ads_v5", + "Standard_E4ds_v4", + "Standard_E4ads_v5", + "Standard_E8ds_v4", + "Standard_E8ads_v5", + "Standard_E16ds_v4", + "Standard_E16ads_v5", + "Standard_E20ds_v4", + "Standard_E20ads_v5", + "Standard_E32ds_v4", + "Standard_E32ads_v5", + "Standard_E48ds_v4", + "Standard_E48ads_v5", + "Standard_E64ds_v4", + "Standard_E64ads_v5", + "Standard_E80ds_v4", + "Standard_E2ds_v5", + "Standard_E4ds_v5", + "Standard_E8ds_v5", + "Standard_E16ds_v5", + "Standard_E20ds_v5", + "Standard_E32ds_v5", + "Standard_E48ds_v5", + "Standard_E64ds_v5", + "Standard_E96ds_v5" + ], + "metadata": { + "description": "The name of the sku for Azure Database for MySQL Flexible Servers, e.g. Standard_D32ds_v4. Complete list is available here : https://learn.microsoft.com/en-us/azure/mysql/flexible-server/concepts-service-tiers-storage. Ensure the SKU capacity is available in the region before selecting from the MySQL Flexible server create page." + } + }, + "MYSQLBackupRetentionDays": { + "type": "int", + "defaultValue": 7, + "minValue": 1, + "maxValue": 35, + "metadata": { + "description": "By default, backups are retained for 7 days. The minimum retention period is 1 day and the maximum retention period is 35 days." + } + }, + "MYSQLGeoRedundantBackup": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Enable Geo-redundant backups for Azure Database for MySQL Flexible Server." + } + }, + "kubernetesSystemNodePoolCount": { + "defaultValue": 1, + "type": "int", + "minValue": 1, + "maxValue": 1000, + "metadata": { + "description": "Specifies the number of agents (VMs) to host docker containers. Allowed values must be in the range of 1 to 1000 (inclusive). The default value is 1." + } + }, + "kubernetesSystemNodePoolVMSize": { + "defaultValue": "Standard_DS2_v2", + "type": "string", + "metadata": { + "description": "Specifies the vm size of nodes in the node pool." + } + }, + "enableSecretRotation": { + "type": "string", + "defaultValue": "true", + "metadata": { + "description": "Accepts value true/false. If true, periodically updates the pod mount and Kubernetes Secret with the latest content from external secrets store." + } + }, + "rotationPollInterval": { + "type": "string", + "defaultValue": "2m", + "metadata": { + "description": "If enableSecretRotation is true, this setting specifies the secret rotation poll interval duration. This duration can be adjusted based on how frequently the mounted contents for all pods and Kubernetes secrets need to be resynced to the latest." + } + }, + "storageAccountName": { + "type": "string", + "defaultValue": "magentostorage", + "metadata": { + "description": "he unique name for Azure Storage Account to host Magento content." + } + }, + "storageAccountSKU": { + "type": "string", + "defaultValue": "Premium_LRS", + "allowedValues": [ + "Premium_LRS", + "Premium_ZRS", + "Standard_GRS", + "Standard_GZRS", + "Standard_LRS", + "Standard_ZRS" + ], + "metadata": { + "description": "Choose the SKU of the storage account." + } + }, + "magentoFileShareQuotaGB": { + "defaultValue": "100", + "type": "string", + "metadata": { + "description": "Size of the azure file share in GB. This quota is for the Magento media and static files. The minimum share size is 100 GB." + } + }, + "elasticsearchFileShareQuotaGB": { + "defaultValue": "100", + "type": "string", + "metadata": { + "description": "Size of the azure file share in GB. This quota is for the elasticsearch. The minimum share size is 100 GB." + } + }, + "VMSize": { + "type": "string", + "defaultValue": "Standard_DS1_v2", + "metadata": { + "description": "Choose the compute size for virtual machine." + } + }, + "VMAdminUsername": { + "type": "string", + "defaultValue": "magentovmadmin", + "metadata": { + "description": "Admin username for the temporary virtual machine used for AKS setup." + } + }, + "VMAdminPassword": { + "type": "securestring", + "metadata": { + "description": "Admin password for the temporary virtual machine used for AKS setup." + } + }, + "SKUAzureCDN": { + "type": "string", + "defaultValue": "Standard_Microsoft", + "allowedValues": [ + "Standard_Akamai", + "Standard_Microsoft", + "Standard_Verizon", + "Premium_Verizon" + ], + "metadata": { + "description": "Provide the profile SKU for the Azure CDN." + } + }, + "magentoAdminEmail": { + "type": "string", + "metadata": { + "description": "The email address for the Magento admin." + } + } + }, + "variables": { + "azureSubscriptionId": "[subscription().subscriptionId]", + "azureResourceGroup": "[resourceGroup().name]", + "tenentId": "[subscription().tenantId]", + "uniqueSuffix": "[uniqueString(resourceGroup().id)]", + "virtualNetworkName": "[concat(parameters('resourceNamePrefix'), '-vnet')]", + "virtualNetworkAddressPrefix": "10.0.0.0/8", + "mySqlVnetSubnetName": "[concat(parameters('resourceNamePrefix'), '-mysql-subnet')]", + "mySqlVnetSubnetAddressPrefix": "10.1.0.0/16", + "kubernetesVnetSubnetName": "[concat(parameters('resourceNamePrefix'), '-kubernetes-subnet')]", + "kubernetesVnetSubnetAddressPrefix": "10.2.0.0/16", + "privateLinksVnetSubnetName": "[concat(parameters('resourceNamePrefix'), '-private-links-subnet')]", + "privateLinksVnetSubnetAddressPrefix": "10.3.0.0/16", + "mySqlServerName": "[concat(parameters('resourceNamePrefix'), '-mysql-', variables('uniqueSuffix'))]", + "kubernetesClusterName": "[concat(parameters('resourceNamePrefix'), '-aks')]", + "kubernetesClusterDnsPrefix": "[concat(parameters('resourceNamePrefix'), '-aks-dns')]", + "kubernetesClusterRoleAssignmentName": "[guid(subscription().subscriptionId, resourceGroup().name, variables('kubernetesClusterName'), variables('kubernetesVnetSubnetName'))]", + "kubernetesClusterSecretProviderRoleAssignmentName": "[guid(parameters('keyVaultName'), variables('kubernetesClusterName'), 'AzureKeyVaultSecretProvider', uniqueString(deployment().name))]", + "cdnProfileName": "[concat(parameters('resourceNamePrefix'), '-cdn')]", + "fileSharePrivateEndpointName": "[concat(parameters('resourceNamePrefix'), '-fs-pe')]", + "enableVirtualMachinePublicIp": true, + "virtualMachinePublicIpAddressesName": "[concat(parameters('resourceNamePrefix'), '-vm-ip')]", + "virtualMachineNetworkInterfaceName": "[concat(parameters('resourceNamePrefix'), '-vm-nic')]", + "virtualMachineName": "[concat(parameters('resourceNamePrefix'), '-vm')]", + "fileSharePrivateIpDeploymentName": "[concat(parameters('resourceNamePrefix'), '-fs-ip-deployment')]", + "filePrivateDnsZoneName": "privatelink.file.core.windows.net", + "mySqlPrivateDnsZoneName": "[concat(variables('uniqueSuffix'), '-mysql.private.mysql.database.azure.com')]", + "storageAccountFileServicesName": "default", + "magentoFileShareName": "magento", + "elasticsearchFileShareName": "elasticsearch", + "magentoCurrency": "USD", + "magentoTimezone": "UTC" + }, + "resources": [ + { + "type": "Microsoft.Network/virtualNetworks", + "name": "[variables('virtualNetworkName')]", + "location": "[resourceGroup().location]", + "apiVersion": "2023-06-01", + "dependsOn": [], + "tags": { + "displayName": "magento-vnet" + }, + "properties": { + "addressSpace": { + "addressPrefixes": [ + "[variables('virtualNetworkAddressPrefix')]" + ] + }, + "subnets": [ + { + "name": "[variables('mySqlVnetSubnetName')]", + "properties": { + "addressPrefix": "[variables('mySqlVnetSubnetAddressPrefix')]", + "delegations": [ + { + "name": "mysql-flexibleServers-subnet-delegation", + "properties": { + "serviceName": "Microsoft.DBforMySQL/flexibleServers" + } + } + ], + "privateEndpointNetworkPolicies": "Enabled", + "privateLinkServiceNetworkPolicies": "Enabled" + } + }, + { + "name": "[variables('kubernetesVnetSubnetName')]", + "properties": { + "addressPrefix": "[variables('kubernetesVnetSubnetAddressPrefix')]" + } + }, + { + "name": "[variables('privateLinksVnetSubnetName')]", + "properties": { + "addressPrefix": "[variables('privateLinksVnetSubnetAddressPrefix')]", + "serviceEndpoints": [ + { + "service": "Microsoft.Sql", + "locations": [ + "[resourceGroup().location]" + ] + }, + { + "service": "Microsoft.Storage", + "locations": [ + "[resourceGroup().location]" + ] + } + ], + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Disabled" + } + } + ] + } + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('mysqlPrivateDnsZoneName')]", + "location": "global", + "properties": {} + }, + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2020-06-01", + "name": "[concat(variables('mysqlPrivateDnsZoneName'), '/', variables('virtualNetworkName'))]", + "location": "global", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('mysqlPrivateDnsZoneName'))]" + ], + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } + } + }, + { + "type": "Microsoft.DBforMySQL/flexibleServers", + "apiVersion": "2023-06-30", + "name": "[variables('mySqlServerName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "[parameters('MYSQLComputeSKU')]", + "tier": "[parameters('MYSQLServerEdition')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('mysqlPrivateDnsZoneName'))]", + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('mysqlPrivateDnsZoneName'), variables('virtualNetworkName'))]" + ], + "properties": { + "version": "[parameters('MYSQLVersion')]", + "administratorLogin": "[parameters('MYSQLAdministratorLogin')]", + "administratorLoginPassword": "[parameters('MYSQLAdministratorPassword')]", + "availabilityZone": "[parameters('MYSQLAvailabilityZone')]", + "highAvailability": { + "mode": "[parameters('MYSQLHighAvailability')]", + "standbyAvailabilityZone": "[parameters('MYSQLStandbyAvailabilityZone')]" + }, + "storageProfile": { + "storageSizeGB": "[parameters('MYSQLStorageSizeGB')]" + }, + "backup": { + "backupRetentionDays": "[parameters('MYSQLBackupRetentionDays')]", + "geoRedundantBackup": "[parameters('MYSQLGeoRedundantBackup')]" + }, + "createMode": "Default", + "network": { + "delegatedSubnetResourceId": "[format('{0}/subnets/{1}', reference(resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('mysqlPrivateDnsZoneName'), variables('virtualNetworkName'))).virtualNetwork.id, variables('mySqlVnetSubnetName'))]", + "privateDnsZoneResourceId": "[resourceId('Microsoft.Network/privateDnsZones', variables('mysqlPrivateDnsZoneName'))]" + } + }, + "resources": [ + { + "type": "configurations", + "name": "log_bin_trust_function_creators", + "apiVersion": "2023-06-30", + "location": "[resourceGroup().location]", + "properties": { + "value": "ON", + "source": "user-override" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforMySQL/flexibleServers', variables('mySqlServerName'))]" + ] + }, + { + "type": "configurations", + "name": "require_secure_transport", + "apiVersion": "2023-06-30", + "location": "[resourceGroup().location]", + "properties": { + "value": "OFF", + "source": "user-override" + }, + "dependsOn": [ + "[resourceId('Microsoft.DBforMySQL/flexibleServers', variables('mySqlServerName'))]" + ] + }, + { + "type": "databases", + "apiVersion": "2023-06-30", + "name": "[parameters('MYSQLDatabaseName')]", + "dependsOn": [ + "[resourceId('Microsoft.DBforMySQL/flexibleServers', variables('mySqlServerName'))]" + ], + "properties": { + "charset": "utf8", + "collation": "utf8_general_ci" + } + } + ], + "tags": { + "PackagedApp": "Magento2" + } + }, + { + "type": "Microsoft.ContainerService/managedClusters", + "apiVersion": "2023-07-01", + "name": "[variables('kubernetesClusterName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + ], + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "kubernetesVersion": "1.29", + "dnsPrefix": "[variables('kubernetesClusterDnsPrefix')]", + "agentPoolProfiles": [ + { + "name": "agentpool", + "mode": "System", + "count": "[parameters('kubernetesSystemNodePoolCount')]", + "VMSize": "[parameters('kubernetesSystemNodePoolVMSize')]", + "osDiskSizeGB": 0, + "vnetSubnetID": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('kubernetesVnetSubnetName'))]", + "type": "VirtualMachineScaleSets", + "osType": "Linux", + "storageProfile": "ManagedDisks" + } + ], + "servicePrincipalProfile": { + "clientId": "msi" + }, + "enableRBAC": true, + "networkProfile": { + "networkPlugin": "azure", + "loadBalancerSku": "standard", + "serviceCidr": "10.4.0.0/16", + "dnsServiceIP": "10.4.0.10", + "dockerBridgeCidr": "172.17.0.1/16" + }, + "addonProfiles": { + "azureKeyvaultSecretsProvider": { + "enabled": true, + "config": { + "enableSecretRotation": "[parameters('enableSecretRotation')]", + "rotationPollInterval": "[parameters('rotationPollInterval')]" + } + } + } + }, + "tags": { + "PackagedApp": "Magento2" + } + }, + { + "condition": "[parameters('SSLEncryptionTLS')]", + "type": "Microsoft.KeyVault/vaults/providers/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[concat(if(parameters('SSLEncryptionTLS'), parameters('keyVaultName'), 'no-keyvault'), '/', 'Microsoft.Authorization/', variables('kubernetesClusterSecretProviderRoleAssignmentName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "[resourceId('Microsoft.ContainerService/managedClusters', variables('kubernetesClusterName'))]" + ], + "properties": { + "principalId": "[reference(resourceId('Microsoft.ContainerService/managedClusters', variables('kubernetesClusterName')), '2023-01-01').addonProfiles.azureKeyvaultSecretsProvider.identity.objectId]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]", + "scope": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + } + }, + { + "type": "Microsoft.Network/virtualNetworks/subnets/providers/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[concat(variables('virtualNetworkName'), '/', variables('kubernetesVnetSubnetName'), '/Microsoft.Authorization/', variables('kubernetesClusterRoleAssignmentName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "[resourceId('Microsoft.ContainerService/managedClusters', variables('kubernetesClusterName'))]" + ], + "properties": { + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "principalId": "[reference(resourceId('Microsoft.ContainerService/managedClusters/', variables('kubernetesClusterName')), '2023-10-01', 'Full').identity.principalId]", + "scope": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('kubernetesVnetSubnetName'))]" + } + }, + { + "condition": "[parameters('azureCDN')]", + "name": "[variables('cdnProfileName')]", + "type": "Microsoft.Cdn/profiles", + "location": "[resourceGroup().location]", + "apiVersion": "2023-05-01", + "sku": { + "name": "[parameters('SKUAzureCDN')]" + }, + "properties": {} + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-09-01", + "name": "[parameters('storageAccountName')]", + "location": "[resourceGroup().location]", + "sku": { + "name": "[parameters('storageAccountSKU')]" + }, + "kind": "FileStorage", + "properties": { + "minimumTlsVersion": "TLS1_0", + "allowBlobPublicAccess": false, + "largeFileSharesState": "Enabled", + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2023-04-01", + "name": "[concat(parameters('storageAccountName'), '/', variables('storageAccountFileServicesName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + ], + "sku": { + "name": "[parameters('storageAccountSKU')]" + }, + "properties": {} + }, + { + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2023-04-01", + "name": "[concat(parameters('storageAccountName'), '/', variables('storageAccountFileServicesName'), '/', variables('magentoFileShareName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/fileServices', parameters('storageAccountName'), variables('storageAccountFileServicesName'))]" + ], + "properties": { + "accessTier": "[if(or(equals(parameters('storageAccountSKU'), 'Premium_LRS'), equals(parameters('storageAccountSKU'), 'Premium_ZRS')), 'Premium', 'TransactionOptimized')]", + "shareQuota": "[parameters('magentoFileShareQuotaGB')]", + "enabledProtocols": "SMB" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2023-04-01", + "name": "[concat(parameters('storageAccountName'), '/', variables('storageAccountFileServicesName'), '/', variables('elasticsearchFileShareName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/fileServices', parameters('storageAccountName'), variables('storageAccountFileServicesName'))]" + ], + "properties": { + "accessTier": "[if(or(equals(parameters('storageAccountSKU'), 'Premium_LRS'), equals(parameters('storageAccountSKU'), 'Premium_ZRS')), 'Premium', 'TransactionOptimized')]", + "shareQuota": "[parameters('elasticsearchFileShareQuotaGB')]", + "enabledProtocols": "SMB" + } + }, + { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2023-06-01", + "name": "[variables('fileSharePrivateEndpointName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]" + ], + "properties": { + "privateLinkServiceConnections": [ + { + "name": "[concat(parameters('resourceNamePrefix'), '-fs-pe-plsc')]", + "properties": { + "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]", + "groupIds": [ + "file" + ] + } + } + ], + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('privateLinksVnetSubnetName'))]" + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2019-10-01", + "name": "[variables('fileSharePrivateIpDeploymentName')]", + "dependsOn": [ + "[resourceId('Microsoft.Network/privateEndpoints', variables('fileSharePrivateEndpointName'))]" + ], + "properties": { + "mode": "Incremental", + "expressionEvaluationOptions": { + "scope": "inner" + }, + "parameters": { + "networkInterfaceId": { + "value": "[reference(resourceId('Microsoft.Network/privateEndpoints', variables('fileSharePrivateEndpointName')), '2020-05-01').networkInterfaces[0].id]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-08-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "networkInterfaceId": { + "type": "string" + } + }, + "variables": {}, + "resources": [], + "outputs": { + "privateIpAddress": { + "type": "string", + "value": "[reference(parameters('networkInterfaceId'), '2020-05-01').ipConfigurations[0].properties.privateIpAddress]" + } + } + } + } + }, + { + "type": "Microsoft.Network/privateDnsZones", + "apiVersion": "2020-06-01", + "name": "[variables('filePrivateDnsZoneName')]", + "location": "global", + "properties": {} + }, + { + "type": "Microsoft.Network/privateDnsZones/A", + "apiVersion": "2020-06-01", + "name": "[concat(variables('filePrivateDnsZoneName'), '/', parameters('storageAccountName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Network/privateDnsZones', variables('filePrivateDnsZoneName'))]", + "[resourceId('Microsoft.Resources/deployments', variables('fileSharePrivateIpDeploymentName'))]" + ], + "properties": { + "ttl": 3600, + "aRecords": [ + { + "ipv4Address": "[reference(variables('fileSharePrivateIpDeploymentName')).outputs.privateIpAddress.value]" + } + ] + } + }, + { + "type": "Microsoft.Network/privateDnsZones/virtualNetworkLinks", + "apiVersion": "2018-09-01", + "name": "[concat(variables('filePrivateDnsZoneName'), '/', variables('virtualNetworkName'))]", + "location": "global", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('filePrivateDnsZoneName'))]" + ], + "properties": { + "registrationEnabled": false, + "virtualNetwork": { + "id": "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]" + } + } + }, + { + "condition": "[equals(variables('enableVirtualMachinePublicIp'), true())]", + "type": "Microsoft.Network/publicIPAddresses", + "apiVersion": "2023-06-01", + "name": "[variables('virtualMachinePublicIpAddressesName')]", + "location": "[resourceGroup().location]", + "properties": { + "publicIPAllocationMethod": "Dynamic" + } + }, + { + "condition": "[equals(variables('enableVirtualMachinePublicIp'), true())]", + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-06-01", + "name": "[variables('virtualMachineNetworkInterfaceName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "[resourceId('Microsoft.Network/publicIPAddresses', variables('virtualMachinePublicIpAddressesName'))]" + ], + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('kubernetesVnetSubnetName'))]" + }, + "privateIPAllocationMethod": "Dynamic", + "publicIPAddress": { + "id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('virtualMachinePublicIpAddressesName'))]" + } + } + } + ] + } + }, + { + "condition": "[equals(variables('enableVirtualMachinePublicIp'), false())]", + "type": "Microsoft.Network/networkInterfaces", + "apiVersion": "2023-06-01", + "name": "[variables('virtualMachineNetworkInterfaceName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Network/virtualNetworks', variables('virtualNetworkName'))]", + "[resourceId('Microsoft.Network/publicIPAddresses', variables('virtualMachinePublicIpAddressesName'))]" + ], + "properties": { + "ipConfigurations": [ + { + "name": "ipconfig1", + "properties": { + "subnet": { + "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('virtualNetworkName'), variables('kubernetesVnetSubnetName'))]" + }, + "privateIPAllocationMethod": "Dynamic" + } + } + ] + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2023-03-01", + "name": "[variables('virtualMachineName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Network/networkInterfaces', variables('virtualMachineNetworkInterfaceName'))]" + ], + "properties": { + "hardwareProfile": { + "VMSize": "[parameters('VMSize')]" + }, + "osProfile": { + "computerName": "[variables('virtualMachineName')]", + "adminUsername": "[parameters('VMAdminUsername')]", + "adminPassword": "[parameters('VMAdminPassword')]" + }, + "storageProfile": { + "imageReference": { + "publisher": "Canonical", + "offer": "UbuntuServer", + "sku": "18.04-LTS", + "version": "latest" + }, + "osDisk": { + "name": "[concat(parameters('resourceNamePrefix'), '-vm-osdisk')]", + "createOption": "FromImage", + "deleteOption": "Delete" + } + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('virtualMachineNetworkInterfaceName'))]", + "properties": { + "primary": true, + "deleteOption": "Delete" + } + } + ] + } + } + }, + { + "type": "Microsoft.Compute/virtualMachines/extensions", + "apiVersion": "2023-03-01", + "name": "[concat(variables('virtualMachineName'), '/CustomScriptExtension')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Network/publicIPAddresses', variables('virtualMachinePublicIpAddressesName'))]", + "[resourceId('Microsoft.Compute/virtualMachines', variables('virtualMachineName'))]", + "[resourceId('Microsoft.DBforMySQL/flexibleServers', variables('mySqlServerName'))]", + "[resourceId('Microsoft.Cdn/profiles', variables('cdnProfileName'))]", + "[resourceId('Microsoft.ContainerService/managedClusters', variables('kubernetesClusterName'))]", + "[resourceId('Microsoft.Network/virtualNetworks/subnets/providers/roleAssignments', variables('virtualNetworkName'), variables('kubernetesVnetSubnetName'), 'Microsoft.Authorization', variables('kubernetesClusterRoleAssignmentName'))]", + "[resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), variables('storageAccountFileServicesName'), variables('magentoFileShareName'))]", + "[resourceId('Microsoft.Network/privateDnsZones/A', variables('filePrivateDnsZoneName'), parameters('storageAccountName'))]", + "[resourceId('Microsoft.Network/privateDnsZones/virtualNetworkLinks', variables('filePrivateDnsZoneName'), variables('virtualNetworkName'))]" + ], + "properties": { + "publisher": "Microsoft.Azure.Extensions", + "type": "CustomScript", + "typeHandlerVersion": "2.1", + "autoUpgradeMinorVersion": true, + "ForceUpdateTag": "1.0", + "protectedSettings": { + "fileUris": [ + "https://raw.githubusercontent.com/aakagarwal/azure-mysql/refs/heads/master/Magento2/deploy-aks.sh" + ], + "commandToExecute": "[concat('bash deploy-aks.sh ', variables('azureSubscriptionId'), ' ', variables('azureResourceGroup'), ' ', parameters('servicePrincipalAppId'), ' ', parameters('servicePrincipalPassword'), ' ', variables('tenentId'), ' ', parameters('composerUsername'), ' ', parameters('composerPassword'), ' ', concat(variables('mySqlServerName'), '.', variables('mySqlPrivateDnsZoneName')), ' ', parameters('MYSQLAdministratorLogin'), ' ', parameters('MYSQLAdministratorPassword'), ' ', variables('kubernetesClusterName'), ' ', parameters('storageAccountName'), ' ', listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2022-09-01').keys[0].value, ' ', parameters('magentoAdminUsername'), ' ', parameters('magentoAdminPassword'), ' ', variables('virtualMachineName'), ' ', if(not(empty(parameters('externalFQDN'))), parameters('externalFQDN'), 'EXTERNAL_FQDN'), ' ', variables('cdnProfileName'), ' ', parameters('redisCache'), ' ', parameters('varnishCache'), ' ', parameters('azureCDN'), ' ', parameters('SSLEncryptionTLS'), ' ', reference(resourceId('Microsoft.ContainerService/managedClusters', variables('kubernetesClusterName')), '2023-01-01').addonProfiles.azureKeyvaultSecretsProvider.identity.clientId, ' ', if(parameters('SSLEncryptionTLS'), parameters('keyVaultName'), 'KEYVAULT_NAME'), ' ', if(parameters('SSLEncryptionTLS'), parameters('certificateName'), 'CERTIFICATE_NAME'), ' ', parameters('magentoAdminEmail'), ' ', variables('magentoCurrency'), ' ', variables('magentoTimezone'))]" + }, + "settings": {} + } + } + ], + "outputs": { + "mySqlServerHostName": { + "type": "string", + "value": "[concat(variables('mySqlServerName'), '.', variables('mySqlPrivateDnsZoneName'))]" + }, + "kubernetesClusterName": { + "type": "string", + "value": "[variables('kubernetesClusterName')]" + }, + "storageAccountName": { + "type": "string", + "value": "[parameters('storageAccountName')]" + } + } +} \ No newline at end of file diff --git a/Magento2/deploy-aks.sh b/Magento2/deploy-aks.sh new file mode 100644 index 0000000..05a0aed --- /dev/null +++ b/Magento2/deploy-aks.sh @@ -0,0 +1,425 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status +set -e + +# Log file +LOG_FILE="deploy-aks.log" + +# Redirect stdout and stderr to log file +exec > >(tee -a $LOG_FILE) 2>&1 + +waitAndGetExternalIpForService() { + local service_name=$1 + local namespace=$2 + local external_ip="" + while [ -z "$external_ip" ]; do + echo "Waiting for external IP..." >&2 + external_ip=$(sudo kubectl get service $service_name -n $namespace --output jsonpath='{.status.loadBalancer.ingress[0].ip}') + if [ -z "$external_ip" ]; then + sleep 10 + fi + done + echo $external_ip +} + +# Parameters +AZURE_SUBSCRIPTION_ID=$1 +AZURE_RESOURCE_GROUP=$2 +SERVICE_PRINCIPAL_APP_ID=$3 +SERVICE_PRINCIPAL_PASSWORD=$4 +TENANT_ID=$5 +COMPOSER_USERNAME=$6 +COMPOSER_PASSWORD=$7 +MY_SQL_SERVER_HOST_NAME=$8 +MY_SQL_SERVER_ADMIN_LOGIN=$9 +MY_SQL_SERVER_ADMIN_PASSWORD=${10} +AKS_CLUSTER_NAME=${11} +STORAGE_ACCOUNT_ACCESS_NAME=${12} +STORAGE_ACCOUNT_ACCESS_KEY=${13} +MAGENTO_ADMIN_USERNAME=${14} +MAGENTO_ADMIN_PASSWORD=${15} +VIRTUAL_MACHINE_NAME=${16} +EXTERNAL_FQDN=${17} +CDN_PROFILE_NAME=${18} +REDIS_CACHE_SWITCH=${19} +VARNISH_CACHE_SWITCH=${20} +CDN_SWITCH=${21} +TLS_SWITCH=${22} +AKS_SECRET_PROVIDER_CLIENT_ID=${23} +KEYVAULT_NAME=${24} +CERTIFICATE_NAME=${25} +MAGENTO_ADMIN_EMAIL=${26} +MAGENTO_CURRENCY=${27} +MAGENTO_TIMEZONE=${28} + +# Printing all parameters +echo "AZURE_SUBSCRIPTION_ID: $AZURE_SUBSCRIPTION_ID" +echo "AZURE_RESOURCE_GROUP: $AZURE_RESOURCE_GROUP" +echo "SERVICE_PRINCIPAL_APP_ID: $SERVICE_PRINCIPAL_APP_ID" +echo "SERVICE_PRINCIPAL_PASSWORD: $SERVICE_PRINCIPAL_PASSWORD" +echo "TENANT_ID: $TENANT_ID" +echo "COMPOSER_USERNAME: $COMPOSER_USERNAME" +echo "COMPOSER_PASSWORD: $COMPOSER_PASSWORD" +echo "MY_SQL_SERVER_HOST_NAME: $MY_SQL_SERVER_HOST_NAME" +echo "MY_SQL_SERVER_ADMIN_LOGIN: $MY_SQL_SERVER_ADMIN_LOGIN" +echo "MY_SQL_SERVER_ADMIN_PASSWORD: $MY_SQL_SERVER_ADMIN_PASSWORD" +echo "AKS_CLUSTER_NAME: $AKS_CLUSTER_NAME" +echo "STORAGE_ACCOUNT_ACCESS_NAME: $STORAGE_ACCOUNT_ACCESS_NAME" +echo "STORAGE_ACCOUNT_ACCESS_KEY: $STORAGE_ACCOUNT_ACCESS_KEY" +echo "MAGENTO_ADMIN_USERNAME: $MAGENTO_ADMIN_USERNAME" +echo "MAGENTO_ADMIN_PASSWORD: $MAGENTO_ADMIN_PASSWORD" +echo "VIRTUAL_MACHINE_NAME: $VIRTUAL_MACHINE_NAME" +echo "EXTERNAL_FQDN: $EXTERNAL_FQDN" +echo "CDN_PROFILE_NAME: $CDN_PROFILE_NAME" +echo "REDIS_CACHE_SWITCH: $REDIS_CACHE_SWITCH" +echo "VARNISH_CACHE_SWITCH: $VARNISH_CACHE_SWITCH" +echo "CDN_SWITCH: $CDN_SWITCH" +echo "TLS_SWITCH: $TLS_SWITCH" +echo "AKS_SECRET_PROVIDER_CLIENT_ID: $AKS_SECRET_PROVIDER_CLIENT_ID" +echo "KEYVAULT_NAME: $KEYVAULT_NAME" +echo "CERTIFICATE_NAME: $CERTIFICATE_NAME" +echo "MAGENTO_ADMIN_EMAIL: $MAGENTO_ADMIN_EMAIL" +echo "MAGENTO_CURRENCY: $MAGENTO_CURRENCY" +echo "MAGENTO_TIMEZONE: $MAGENTO_TIMEZONE" + +echo "External FQDN: $EXTERNAL_FQDN" +# Mocking External FQDN (if not provided) +if [ "${EXTERNAL_FQDN}" = "EXTERNAL_FQDN" ]; then + EXTERNAL_FQDN='' +fi +echo "Final External FQDN: $EXTERNAL_FQDN" + +echo "KeyVault Name: $KEYVAULT_NAME" +# Mocking KeyVault Name (if not provided) +if [ "${KEYVAULT_NAME}" = "KEYVAULT_NAME" ]; then + KEYVAULT_NAME='' +fi +echo "Final KeyVault Name: $KEYVAULT_NAME" + +echo "Certificate Name: $CERTIFICATE_NAME" +# Mocking Certificate Name (if not provided) +if [ "${CERTIFICATE_NAME}" = "CERTIFICATE_NAME" ]; then + CERTIFICATE_NAME='' +fi +echo "Final Certificate Name: $CERTIFICATE_NAME" + + +# Variables +aks_namespace="magento" + +# Install Azure CLI +echo "Installing Azure CLI..." +curl -sL https://aka.ms/InstallAzureCLIDeb | bash + +# Install kubectl +echo "Installing kubectl..." +sudo az aks install-cli + +# Install Helm +echo "Installing Helm..." +curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null +sudo apt-get install apt-transport-https --yes +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list +sudo apt-get update +sudo apt-get install helm + +# Login to Azure using Service Principal +echo "Logging into Azure..." +sudo az login --service-principal --username "$SERVICE_PRINCIPAL_APP_ID" --password "$SERVICE_PRINCIPAL_PASSWORD" --tenant "$TENANT_ID" + +# Set the Azure subscription +echo "Setting the Azure subscription..." +sudo az account set --subscription "$AZURE_SUBSCRIPTION_ID" + +# Get AKS credentials +echo "Getting AKS credentials..." +while [[ $(sudo az aks show -g "$AZURE_RESOURCE_GROUP" -n "$AKS_CLUSTER_NAME" --query "provisioningState" -o tsv) != "Succeeded" ]]; do + echo "Waiting for AKS cluster "$AKS_CLUSTER_NAME" in resource group "$AZURE_RESOURCE_GROUP" to be provisioned..." + sleep 10s +done +sudo az aks get-credentials --resource-group "$AZURE_RESOURCE_GROUP" --name "$AKS_CLUSTER_NAME" + +# Test sudo kubectl access +echo "Testing sudo kubectl access..." +sudo kubectl get nodes + +# Start deploying to ASK Cluster +echo "Deploying to AKS Cluster..." + +# Defining deployment file URLs +echo "Deploying YAML configurations..." +file_base_url="https://raw.githubusercontent.com/aakagarwal/azure-mysql/refs/heads/master/Magento2/Kubernetes" + +# Create the namespace +echo "Creating namespace..." +curl -s "$file_base_url/namespace.yaml" | sudo kubectl apply -f - + +# Applying the secret(s) according to Infra deployment +echo "Applying secrets..." +sudo kubectl create secret generic input-secrets -n $aks_namespace \ + --from-literal=MAGENTO_DATABASE_USER=$MY_SQL_SERVER_ADMIN_LOGIN \ + --from-literal=MAGENTO_DATABASE_PASSWORD=$MY_SQL_SERVER_ADMIN_PASSWORD \ + --from-literal=MAGENTO_ADMIN_USER=$MAGENTO_ADMIN_USERNAME \ + --from-literal=MAGENTO_ADMIN_PASSWORD=$MAGENTO_ADMIN_PASSWORD \ + --from-literal=COMPOSER_USERNAME=$COMPOSER_USERNAME \ + --from-literal=COMPOSER_PASSWORD=$COMPOSER_PASSWORD \ + --from-literal=azurestorageaccountname=$STORAGE_ACCOUNT_ACCESS_NAME \ + --from-literal=azurestorageaccountkey=$STORAGE_ACCOUNT_ACCESS_KEY \ + --dry-run=client -o yaml | sudo kubectl apply -f - + +# Add the ingress-nginx repository +echo "Adding ingress-nginx repository..." +sudo helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +sudo helm repo update + +# Install nginx-ingress controller +echo "Installing nginx-ingress controller..." +sudo helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ + --namespace $aks_namespace \ + --set controller.replicaCount=1 \ + --set controller.nodeSelector."kubernetes\.io/os"=linux \ + --set defaultBackend.nodeSelector."beta\.kubernetes\.io/os"=linux \ + --set controller.service.externalTrafficPolicy=Local + +# Get the external IP of the Ingress Controller +echo "Getting external IP of the Ingress Controller..." +external_ip_service_name="ingress-nginx-controller" +external_ip_address=$(waitAndGetExternalIpForService $external_ip_service_name $aks_namespace) +echo "External IP Address: $external_ip_address" + +# Applying the input configmap according to Infra deployment +echo "Applying configmaps..." +magento_base_url="" +magento_base_url_secure="" +cdn_origin="" +if [ -n "$EXTERNAL_FQDN" ]; then + magento_base_url="http://$EXTERNAL_FQDN:80/" + magento_base_url_secure="https://$EXTERNAL_FQDN:443/" + cdn_origin=$EXTERNAL_FQDN +else + magento_base_url="http://$external_ip_address:80/" + magento_base_url_secure="https://$external_ip_address:443/" + cdn_origin=$external_ip_address +fi + +echo "Magento Base URL: $magento_base_url" +echo "Magento Base URL Secure: $magento_base_url_secure" +echo "CDN Origin: $cdn_origin" + +CDN_STATIC_ENDPOINT_HOST="" +CDN_MEDIA_ENDPOINT_HOST="" +if [ "${CDN_SWITCH}" = "true" ] || [ "${CDN_SWITCH}" = "True" ]; then + # Create CDN Endpoint for static + echo "Creating CDN Endpoint for static..." + sudo az cdn endpoint create \ + --endpoint-name magento-static \ + --profile-name $CDN_PROFILE_NAME \ + --resource-group $AZURE_RESOURCE_GROUP \ + --origin-host-header $cdn_origin \ + --origin $cdn_origin + + # Create CDN Endpoint rule for static + echo "Creating CDN Endpoint rule for static..." + sudo az cdn endpoint rule add \ + --profile-name $CDN_PROFILE_NAME \ + --resource-group $AZURE_RESOURCE_GROUP \ + --name magento-static \ + --rule-name Global \ + --order 0 \ + --action-name ModifyResponseHeader \ + --header-action Overwrite \ + --header-name Access-Control-Allow-Origin \ + --header-value "*" + + # Get CDN URL for static + echo "Getting CDN URL for static..." + CDN_STATIC_ENDPOINT_HOST=$(sudo az cdn endpoint show \ + --endpoint-name magento-static \ + --profile-name $CDN_PROFILE_NAME \ + --resource-group $AZURE_RESOURCE_GROUP \ + --query "hostName" \ + --output tsv) + + # Create CDN Endpoint for media + echo "Creating CDN Endpoint for media..." + sudo az cdn endpoint create \ + --endpoint-name magento-media \ + --profile-name $CDN_PROFILE_NAME \ + --resource-group $AZURE_RESOURCE_GROUP \ + --origin-host-header $cdn_origin \ + --origin $cdn_origin + + # Create CDN Endpoint rule for media + echo "Creating CDN Endpoint rule for media..." + sudo az cdn endpoint rule add \ + --profile-name $CDN_PROFILE_NAME \ + --resource-group $AZURE_RESOURCE_GROUP \ + --name magento-media \ + --rule-name Global \ + --order 0 \ + --action-name ModifyResponseHeader \ + --header-action Overwrite \ + --header-name Access-Control-Allow-Origin \ + --header-value "*" + + # Get CDN URL for media + echo "Getting CDN URL for media..." + CDN_MEDIA_ENDPOINT_HOST=$(sudo az cdn endpoint show \ + --endpoint-name magento-media \ + --profile-name $CDN_PROFILE_NAME \ + --resource-group $AZURE_RESOURCE_GROUP \ + --query "hostName" \ + --output tsv) +else + CDN_STATIC_ENDPOINT_HOST=$cdn_origin + CDN_MEDIA_ENDPOINT_HOST=$cdn_origin +fi +echo "CDN Static Endpoint Host: $CDN_STATIC_ENDPOINT_HOST" +echo "CDN Media Endpoint Host: $CDN_MEDIA_ENDPOINT_HOST" + + +CDN_STATIC_URL="http://$CDN_STATIC_ENDPOINT_HOST/static/" +CDN_MEDIA_URL="http://$CDN_MEDIA_ENDPOINT_HOST/media/" + +# Using HTTP for CDN URLs if TLS is not enabled because of self-signed certificate error at CDN +if [ "${TLS_SWITCH}" = "true" ] || [ "${TLS_SWITCH}" = "True" ]; then + CDN_STATIC_URL_SECURE="https://$CDN_STATIC_ENDPOINT_HOST/static/" + CDN_MEDIA_URL_SECURE="https://$CDN_MEDIA_ENDPOINT_HOST/media/" +else + CDN_STATIC_URL_SECURE="http://$CDN_STATIC_ENDPOINT_HOST/static/" + CDN_MEDIA_URL_SECURE="http://$CDN_MEDIA_ENDPOINT_HOST/media/" +fi + +echo "CDN Static URL: $CDN_STATIC_URL" +echo "CDN Media URL: $CDN_MEDIA_URL" +echo "CDN Static URL Secure: $CDN_STATIC_URL_SECURE" +echo "CDN Media URL Secure: $CDN_MEDIA_URL_SECURE" + +# Apply Input Configmap +echo "Applying Input Configmap..." +sudo kubectl create configmap input-config -n $aks_namespace \ + --from-literal=MAGENTO_BASE_URL=$magento_base_url \ + --from-literal=MAGENTO_BASE_URL_SECURE=$magento_base_url_secure \ + --from-literal=MAGENTO_ADMIN_EMAIL=$MAGENTO_ADMIN_EMAIL \ + --from-literal=MAGENTO_CURRENCY=$MAGENTO_CURRENCY \ + --from-literal=MAGENTO_TIMEZONE=$MAGENTO_TIMEZONE \ + --from-literal=DATABASE_HOST=$MY_SQL_SERVER_HOST_NAME \ + --from-literal=MAGENTO_BASE_URL_STATIC=$CDN_STATIC_URL \ + --from-literal=MAGENTO_BASE_URL_MEDIA=$CDN_MEDIA_URL \ + --from-literal=MAGENTO_BASE_URL_STATIC_SECURE=$CDN_STATIC_URL_SECURE \ + --from-literal=MAGENTO_BASE_URL_MEDIA_SECURE=$CDN_MEDIA_URL_SECURE \ + --from-literal=REDIS_CACHE_SWITCH=$REDIS_CACHE_SWITCH \ + --from-literal=VARNISH_CACHE_SWITCH=$VARNISH_CACHE_SWITCH \ + --from-literal=CDN_SWITCH=$CDN_SWITCH \ + --dry-run=client -o yaml | sudo kubectl apply -f - + +# Apply Storage Class +echo "Applying Storage Class..." +curl -s "$file_base_url/azurefile/sc.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - + + +# Deploy Elasticsearch +# Apply Elasticsearch PV & PVC +echo "Applying Elasticsearch PV & PVC..." +curl -s "$file_base_url/elasticsearch/pv.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +curl -s "$file_base_url/elasticsearch/pvc.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +# Apply Elasticserach Service +echo "Applying Elasticsearch service..." +curl -s "$file_base_url/elasticsearch/services.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +# Apply Elasticsearch StatefulSet +echo "Applying Elasticsearch StatefulSet..." +curl -s "$file_base_url/elasticsearch/statefulset.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - + +# Deploy Redis (if required) +if [ "${REDIS_CACHE_SWITCH}" = "true" ] || [ "${REDIS_CACHE_SWITCH}" = "True" ]; then + # Apply Redis Service + echo "Applying Redis Service..." + curl -s "$file_base_url/redis/services.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - + # Apply Redis StatefulSet + echo "Applying Redis StatefulSet..." + curl -s "$file_base_url/redis/statefulset.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +fi + +# Deploy Magento +# Apply Magento Configmap +echo "Applying Magento Configmap..." +curl -s "$file_base_url/magento/configmap.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +# Apply Magento PV & PVC +echo "Applying Magento PV & PVC..." +curl -s "$file_base_url/magento/pv.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +curl -s "$file_base_url/magento/pvc.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +# Apply Magento Services +echo "Applying Magento Services..." +curl -s "$file_base_url/magento/services.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +# Apply Magento Job Deployment +echo "Applying Magento job Deployment..." +curl -s "$file_base_url/magento/job.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +# Wait for the job to complete (timeout 30 minutes) +echo "Waiting for the job to complete..." +sudo kubectl wait --for=condition=complete --timeout=1800s job/magento-setup-job -n $aks_namespace +# Apply Magento server Deployment +echo "Applying Magento server Deployment..." +curl -s "$file_base_url/magento/deployment.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - +# Apply Magento Cron Deployment +echo "Applying Magento cron Deployment..." +curl -s "$file_base_url/magento/cron.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - + +# Deeploy Varnish (if required) +backend_service="" +backend_service_port="" +if [ "${VARNISH_CACHE_SWITCH}" = "true" ] || [ "${VARNISH_CACHE_SWITCH}" = "True" ]; then + # Apply Varnish Configmap + echo "Applying Varnish Configmap..." + curl -s "$file_base_url/varnish/configmap.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - + # Apply Varnish Services + echo "Applying Varnish Services..." + curl -s "$file_base_url/varnish/services.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - + # Apply Varnish Deployment + echo "Applying Varnish Deployment..." + curl -s "$file_base_url/varnish/deployment.yaml?$sas_token" | sudo kubectl apply -n $aks_namespace -f - + backend_service="varnish-service" + backend_service_port="80" +else + backend_service="magento-service" + backend_service_port="8080" +fi + +# Deploy tls (if required) and ingress +if [ "${TLS_SWITCH}" = "true" ] || [ "${TLS_SWITCH}" = "True" ]; then + # Apply TLS Secret Provider + echo "Applying TLS Secret Provider..." + curl -s "$file_base_url/tls/secret-provider.yaml?$sas_token" | \ + sed -e "s/__USER_ASSIGNED_IDENTITY_ID__/${AKS_SECRET_PROVIDER_CLIENT_ID}/g" \ + -e "s/__KEYVAULT_NAME__/${KEYVAULT_NAME}/g" \ + -e "s/__CERTIFICATE_NAME__/${CERTIFICATE_NAME}/g" \ + -e "s/__TENANT_ID__/${TENANT_ID}/g" | \ + sudo kubectl apply -n "$aks_namespace" -f - + + # Certing TLS Cert Pod + echo "Certing TLS Cert Pod..." + curl -s "$file_base_url/tls/cert-pod.yaml?$sas_token" | sudo kubectl apply -n "$aks_namespace" -f - + + # Apply nginx Ingress with TLS + echo "Applying nginx Ingress with TLS..." + curl -s "$file_base_url/ingress/ingress-tls.yaml?$sas_token" | \ + sed -e "s/__FQDN__/${EXTERNAL_FQDN}/g" \ + -e "s/__SERVICE_NAME__/${backend_service}/g" \ + -e "s/__SERVICE_PORT__/${backend_service_port}/g" | \ + sudo kubectl apply -n "$aks_namespace" -f - +else + # Apply nginx Ingress without TLS + echo "Applying nginx Ingress without TLS..." + curl -s "$file_base_url/ingress/ingress.yaml?$sas_token" | \ + sed -e "s/__SERVICE_NAME__/${backend_service}/g" \ + -e "s/__SERVICE_PORT__/${backend_service_port}/g" | \ + sudo kubectl apply -n "$aks_namespace" -f - +fi + +# Ensure the log file is accessible +chmod 644 $LOG_FILE +echo "Log file can be found at: $LOG_FILE" + +# Schedule VM deletion in 10 seconds +echo "Scheduled VM deletion in 10 seconds..." +nohup bash -c "sleep 10 && sudo az vm delete --resource-group $AZURE_RESOURCE_GROUP --name $VIRTUAL_MACHINE_NAME --yes --no-wait" > deploy-aks.log 2>&1 & diff --git a/Magento2/images/magento2-architecture.png b/Magento2/images/magento2-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..35e9c0b97a1be0a6c662c89d3fbbe63592fc78a3 GIT binary patch literal 56687 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 Ux-pLTv5jnm$J8_#&G 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%0A^ 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 I>wWrv0CpSgKmY&$ literal 0 HcmV?d00001